Portfolio Truth — the Reconcile pill + broker_state daemon.
Until v7.8, "what does the dashboard think you hold" and "what does your broker think you hold" could drift apart for hours before anyone noticed. v7.8 closes that gap. A new background daemon polls your broker every 30 seconds, writes broker_snapshot.json as the single source of broker truth, and surfaces drift through a Reconcile pill in the dashboard hero strip — color-coded by severity, click-through to a per-row diff with accept/reject. Companion pieces handle transaction routing and paranoid-mode order verification. This lesson covers the architecture, the user-facing surface, and the failure modes the design specifically prevents.
Why a separate daemon
The framework's audit cycle runs every ~2 minutes and reads broker data when it has a reason to. That cadence is right for scoring but wrong for reconciliation. If you place a manual trade in E*TRADE's web UI at 10:03 and the next audit cycle runs at 10:05, the dashboard's view of your portfolio is wrong for ~2 minutes. Most of the time that doesn't matter; some of the time it matters a lot (you almost place a duplicate order, or the position sizer recommends size that ignores the fresh fill).
The fix is a poll loop at a faster cadence than the audit. A dedicated broker_state_fetcher daemon hits E*TRADE every 30 seconds and writes the result atomically to broker_snapshot.json. Everything downstream reads the snapshot, not the broker directly — so the dashboard, the audit cycle, the Reconcile pill, and any future consumer all see the same broker truth without each having to maintain its own API throttle.
The account-picker heuristic
E*TRADE accounts often include both an INDIVIDUAL brokerage and an IRA. The daemon has to pick one to poll. Wrong-account selection was the bug v7.8.35 fixed: the daemon was happily polling a Roth IRA the user wasn't trading from, surfacing an unfamiliar $44k NAV in the Reconcile pill while the actual trading account drifted silently.
The fix is a symbol-overlap heuristic. The daemon enumerates accounts, computes the overlap between each account's positions and the user's positions.json, and picks the account with the highest overlap. Ties break toward INDIVIDUAL over ROTHIRA. The picker logs which account it chose and why, so a wrong pick is debuggable from the log rather than mysterious from the UI.
The Reconcile pill
The pill sits in the hero strip, always visible. It has four states:
| State | Color | Meaning |
|---|---|---|
| ✓ synced | green | Positions, cash, open orders all match within thresholds |
| ⚠ drift | amber/red | One or more axes disagree beyond threshold |
| ⚠ stale snapshot | yellow | broker_snapshot.json older than 15 min — daemon not running or E*TRADE unreachable |
| offline | grey | Reconcile endpoint unreachable |
The drift state is severity-bucketed. Three positions out of sync is amber. Cash off by $500 is amber. Cash off by $5,000 is red. The thresholds live in config.py as RECONCILE_CASH_DRIFT_USD and similar, so an operator can tune them without code changes.
Click the pill to open the Reconcile modal: per-row diff between broker truth and dashboard state, with accept/reject buttons per row. Accept-cash writes the broker number to wt_cash3; accept-position writes the broker quantity to positions.json. Reject keeps the dashboard's local value (useful when the daemon is wrong, e.g., right after a manual edit that the daemon hasn't yet re-polled).
Transaction auto-routing
When the broker_snapshot daemon detects a new transaction (BUY/SELL/DIV/INT/FEE) on the broker's transactions endpoint, it classifies the transaction by type and routes it to the right ledger automatically — equity P&L for BUY/SELL, dividend ledger for DIV, interest ledger for INT, expense ledger for FEE. The user doesn't see this step; it just happens.
DEPOSIT/WITHDRAW/SPLIT are different. Those affect the drawdown baseline — a $10K deposit shouldn't trigger the daemon to think you just earned $10K (it would, naively, since portfolio value just jumped $10K). So DEPOSIT/WITHDRAW queue for confirmation in a Pending Confirmations widget. Confirm the deposit and portfolio_rebalancer.adjust_drawdown_baseline() shifts the week_high reference by the capital delta. The daemon's drawdown math stays honest.
Paranoid-mode order verify
The last piece of Portfolio Truth handles a specific failure mode: ghosted orders. You click Place, the broker returns place_ok=true, the order should appear in the open-orders book — but sometimes it doesn't. Network blip between place and the broker's backend persistence. The user has no idea their order vaporized until they next look.
v7.8's order_verify_tracker schedules T+45s / T+90s / T+120s verify checks after every place_ok event. Each check polls the broker for the order_id. If it appears, great — the tracker marks verified and stops. If by T+120s the order still hasn't appeared, an alert fires: "Order ghosted at broker — placed N seconds ago, not visible in open orders. Investigate before re-placing."
The failure modes this design prevents
- Hours-long broker-vs-dashboard drift — the 30-second daemon caps it at ~30 seconds plus reconcile-poll cadence.
- Wrong-account silent NAV — symbol-overlap heuristic catches IRA-vs-INDIVIDUAL picks.
- Phantom drawdown peaks during deposits — DEPOSIT/WITHDRAW confirmation step + baseline-adjust API.
- Ghost orders unnoticed — T+45/90/120 verify schedule.
- Stale snapshot masquerading as fresh — yellow ⚠ stale state when broker_snapshot.json is >15 min old.
The real lesson
Broker truth is the riskiest data class in the dashboard. The framework can be wrong about scoring and you'll just take fewer trades; broker truth being wrong leads to duplicate fills, oversized positions, ghosted protective stops. v7.8's Portfolio Truth treats broker reconciliation with the same architectural seriousness the rest of the framework treats methodology — separate daemon, atomic snapshot file, per-row diff UX, paranoid-mode verify on every place. The discipline is structural; the user doesn't have to remember to reconcile because the dashboard is always reconciling and showing the result.
Related: L32 — 8-check pre-flight chain · L33 — performance accounting · v7.8 release post