Skip to content

v2.3.0

Choose a tag to compare

@osauer osauer released this 24 Jul 06:26

The data flows out. Orders don't go in.

ibkr is a read-only interface to your Interactive Brokers account, reachable from a Go library, a shell CLI, a stdio MCP server (Claude Desktop, Cursor, Continue, Zed), and a Claude Code plugin. Hand it to an assistant, a cron job, a notebook, or your own service.

What's new in v2.3.0

  • Alerts became one inbox, and eight more sources can now reach your phone. Until now only Canary, governance, and order-integrity alerts had a send path, each with its own owner, while Regime, Rulebook, risk policy, Protection, reconciliation, and Data Health were recorded as shadow evidence that could never page (delivery_active=false, delivery_preference=unapproved) — this supersedes the shadow-commissioning wording in the two entries further down. The fixed nine-producer universe now enters one source-neutral candidate snapshot, inbox, unread cursor, and dispatcher. The daemon decides whether a condition exists, how serious it is, and whether it opened, escalated, or recovered; the app records that decision, decides whether the chosen notification level permits a push, and owns every delivery attempt and receipt. One global notification mode covers the app host and all paired devices — Off (inbox and unread history only), Action required (act and urgent), Watch + action — and eligibility is sampled when an occurrence is first recorded, so turning notifications up later never arms a condition that was suppressed when it was created; observe severity is always inbox-only. The cutover deliberately does not page a backlog: the first complete snapshot in each account-and-mode scope sets a delivery baseline, and conditions already active at that moment stay visible in the inbox as existing context but are never pushed. The delivery gate is per source, so an unrelated source being unavailable cannot suppress an independently current candidate — but an empty candidate list means "clear" only when the whole expected source set is complete and current, otherwise the snapshot is unknown. Delivery identity is the private tuple of scope, occurrence, and target, reserved before transport, so replayed snapshots, repeated polls, and restarts cannot produce a duplicate push; retryable transport failures wait 1, 5, and 15 minutes and a fourth is terminal, a definite rejection is never retried, and a dead subscription is retired so it cannot keep degrading healthy targets. Payloads carry fixed app copy plus allowlisted severity, kind, destination, display id, and app URL — never producer prose, symbols, account fields, or source errors. A push-service acceptance is reported as exactly that and never as proof that a device displayed or you read the notification. The cutover grants no trading authority: an alert cannot place, modify, or cancel an order, change a freeze or limit, approve a risk policy, or turn a degraded observation into a decision.
  • Sensors are now explained on their own public page. docs/sensors.md covers the five families — gamma (SPX/SPXW dealer structure with SPY as context), regime (eight broad-market rows into six independent clusters and a lifecycle), canary (that market state against the held portfolio), rulebook (14 advisory discipline checks), and market events (held-name borrow, threshold-list, and halt context) — and states the boundary the surfaces depend on: the daemon is the measurement authority, and CLI, MCP, app, and web read the same typed result rather than re-fetching sources or rebuilding a verdict. Two generated diagrams carry what prose handles badly: how observed sources become sensors, dependent decisions, and downstream alerts, and how freshness actually behaves — proactive refresh while the old value stays authoritative, hard expiry into fail-closed, and the difference between a producer-authored not_due window and a genuine overdue gap. A sensor result is never evidence that delivery is active or that a notification arrived.
  • Broker-adjacent capabilities now fail closed where the daemon lacks complete authority. This entry supersedes older current-release wording that exempted close/reduce orders from size caps or implied that a platform toggle enabled purge/restore submission. Every equity/ETF and single-leg option order now carries the ordinary notional/quantity cap, and a sell-side apparent exit also needs short/sell-to-open permission under worst-case exposure because the daemon cannot see future manual TWS activity; a genuine blocked exit says to use TWS and reconcile. Purge/restore preview and submission are typed-disabled until exact per-leg portfolio plus account-global working-order authority exists, and option-exercise review mints no token and exposes no Canary submit action until exact option-to-underlying risk policy plus durable one-shot authority are approved. The purge/restore setting controls only its workflow/read surface. Account-base cap evidence is bound to the exact broker session from an explicit currency tag or one consistent allowlisted aggregate-value suffix; ExchangeRate=1 and ledger rows are ineligible. Cross-currency drafts now resolve a fresh exact-session CASH/IDEALPRO quote, bind the quote- and base-currency notionals plus FX provenance into a v4 preview token, refresh the conversion at redemption, and carry that base amount to the first-byte guard; unavailable FX fails closed instead of assuming parity or using the stale display cache. Runtime trading-control updates are human-only in every mode and commit their state plus normalized-origin audit event atomically; semantic no-ops advance neither revision nor audit stream.
  • Daemon state and decision history now live in one SQLite database. $XDG_STATE_HOME/ibkr/daemon.db is the sole live store for runtime settings, risk and alert state, order safety, retained observations, and local decision history. The existing ibkr regime history, ibkr rules history, ibkr canary history, ibkr recon equity, and order-history commands read through the daemon instead of opening side files. Original Flex XML statements remain separate broker evidence and are projected transactionally into the database. Do not delete daemon.db: it is authoritative state, not a rebuildable cache.
  • The SQLite cutover and upgrade path fails closed. Startup validates the database shape, integrity, write watermark, and private file permissions before serving requests or connecting to the broker. Upgrades operate on a verified candidate and publish it atomically only after validation; backups, the external head watermark, and sealed legacy artifacts remain recovery material rather than live read fallbacks. Future, damaged, rolled-back, or ambiguous state stops startup instead of silently switching to older files.
  • The daily brief is now a trading-process artifact on its own tab. Re-conceived from five data-domain sections (Market, Calendar, Portfolio, Risk & limits, Process) into two process movements the daemon composes and every surface renders verbatim: Review — post-trade of the last completed session (session P&L, daily attribution by underlying, process coherence [the rulebook-adherence delta plus a new proposals-offered-vs-acted count from retained proposal outcomes, and overrides used], capital events, the reconcile clock with its one-tap sign-off, and the working-orders end state) — and Ready — pre-trade for today (overnight & market, calendar, risk capacity, and desk readiness). It is a regrouping of facts the daemon already had — row severities and the worst-child rollup are unchanged — with the one new derivation carrying only counts and the covered day to the wire (no proposal keys, symbols, order refs, or tokens). The brief moves off the Monitor tab to its own bottom-tab slot, "Brief" (Monitor · Brief · Alerts · Orders · Settings, Monitor still the default landing and universal fallback), with sunrise (Ready) and history-clock (Review) phase glyphs and sentence-case titles. Process/governance push nudges that used to land on Monitor now land on the Brief tab; the one-tap reconcile sign-off keeps its exact endpoint, evidence, and semantics. VaR and any risk-unit measure stay out of scope (an open operator decision).
  • The account header stops shouting the account number. The account id is demoted from an 18px near-black headline to a quiet 12px muted subtitle (tabular figures), and it is now masked by the same eye toggle that hides money values (U•••••NN, last two characters visible) — previously the id was the one sensitive string the eye left in the clear. Money values become the visual headline by the id's demotion; the folded "Detail" toggle regains a readable label; and when the freshness stamp goes stale it takes the id's former weight in red, because staleness outranks identity. The trading-environment pill is operator-decided: live renders no pill at all (the safe default is silent), paper renders a loud red PAPER ("portfolio data is fake"), and an unresolved mode renders a muted "mode?" that fails visible instead of silently resembling live.
  • The Monitor tab now separates "needs eyes" from "data is cold." The follow-up trader-panel review (discretionary trader and prop-desk risk manager, same bar as the Alerts pass) caught the Monitor tab running both dishonesty modes at once: the capital tile rendered "OK" while its own values showed a breached block tier and an engaged drawdown latch (the "OK" was plumbing health dressed as a risk verdict, and the "Risk & limits" section header green-lit the lot as "section complete"), while a normal closed weekend inflated four "Count 0" event rows and an idle dealer-gamma cache into DEGRADED noise. The daily brief now carries a fourth row status, attention (orange), derived strictly from values: a breached or latched capital tier, an active policy override, or held-name earnings whose governing rulebook rule cannot be evaluated all render attention no matter how healthy the plumbing looks, and section headers roll up their worst child with counts ("risk and limits: 2 of 4 row(s) need attention") instead of reporting completeness as health. Expected closed-session coldness went the other way: on an official non-trading date a cold gamma cache and stale-or-idle event sources render OK, with copy that claims only what the code verified ("no fresh update expected while the market is closed (source health stale; last checked 2026-07-17 23:58)"), each event row now judged by its own sources (an unreachable borrow-fee feed can no longer degrade the halt row), and degraded reserved for abnormal-for-session states at any hour. The adversarial re-review then pushed provenance onto the risk rows themselves: the capital row prints when its adjusted peak was observed ("peak set …", the tell that exposes a poisoned observation), the latch row prints the consumed share recorded at engagement ("Engaged at 30.4%") so a later data glitch cannot rewrite why it fired, and a rulebook transition into act lifts the Rules-delta row to attention instead of hiding under data-quality vocabulary. CLI renders share the same daemon-authored statuses and copy.
  • The Alerts page now talks like a trading desk, not a debug console. A trader-panel review (discretionary trader, prop-desk risk manager, UX writer) found the page inverted: all-clear rows dressed as alerts, one real warning buried in telemetry ("0 red clusters (none), 5/6 ranked"), a counter in storage vocabulary ("3 current / 1 stored"), and weekend-idle governance plumbing that read as an outage. The page is now severity-first: the header counts conditions ("2 watch · 2 data", "All clear"), a plain-English status sentence leads with first-seen and data-as-of stamps, only rows that need attention render as alerts, each with a text severity chip (ACT / WATCH / DATA / INFO) instead of unexplained bullet colors, and passed checks collapse into one "✓ N checks passed" line. Evidence moved behind per-row Details disclosures and dropped its zero-count noise. History and previous-context alerts fold into one collapsed, dated section whose clear button states its consequence, and "Governance evidence" became a one-line "Process checks" status ("paused — data sources are not ready (normal outside market hours)") with the delivery ledger tucked behind the evidence disclosure, nonzero counters only.
  • A push-delivery failure during market hours now interrupts instead of hiding in evidence. The accepted follow-up batch from that Alerts trader review lands, led by its top item: when web-push delivery health is degraded or unavailable while the US-equity session is open, the Alerts page leads with a plain-English banner ("Alerts may not be reaching your phone — push delivery is degraded while the market is open. Rely on this page until delivery recovers."), instead of burying the only warning in the governance evidence disclosure. Known-closed sessions keep the quiet disclosure-only presentation, an unknown session state fails visible, and operator-chosen "Off" (suppressed delivery) never banners. A JS fixture pins the degraded-delivery + open-session rendering and every quiet case.
  • Broken market inputs no longer masquerade as an early warning. Regime now reserves early_warning for current, usable market evidence. Any missing, invalid, or overdue required source publishes the explicit state "Market state undefined — data incomplete", with decision readiness blocked and low confidence; provisional measurements remain visible as unconfirmed context but cannot define the market posture. Source cadence is exchange-aware rather than wall-clock-only: VIX and VIX3M have separate official dissemination windows, gamma distinguishes not-due from a missed session or no last-good result, and the latest completed-session or weekly observation remains usable for its declared cadence instead of disappearing at midnight. Canary carries the same typed source health and surfaces unrelated data gaps separately rather than rewriting a valid Regime state.
  • Regime, Protection, and Data Health now participate in the durable shadow alert lifecycle. The desk-approved Regime producer opens one market-stress episode only from current early_warning, follows the existing governed severity through confirmed_stress and panic, rechecks source age/classification, and refuses to treat data_quality as a warning or recovery. Protection covers only structurally complete, current orphaned-order and reconciliation-required evidence; ordinary partial or unprotected holdings stay context until a separate protection-obligation policy exists. Data Health records allowlisted root incidents without farm-name fan-out, distinguishes a live startup handshake from a failed connection, and treats source-authored not_due and normal computing as healthy cadence. Daemon-owned 30-second heartbeats keep all three observed when no app or CLI is open. Status remains responsive through an ordered semantic-transition, shutdown-drained worker if shadow persistence fails, and contract identity remains intact in unattended Protection reads so an ambiguous symbol fallback cannot fabricate a clear. Existing throttled reconnect and account-stream renewal remain read-side repairs, not broker writes. SQLite's own shadow incident is explicitly best-effort while its typed health row remains authoritative. All three were commissioned as shadow evidence rather than pages or enforcement; that status is superseded within this same release by the alert cutover above, which routes them through the single dispatcher and its per-source delivery gate.
  • The remaining shadow alert sources now stay observed without an open app or CLI. Daemon-owned 30-second heartbeats reuse the canonical Nudge boundary for Risk Policy, Reconciliation, and Governance, and the canonical unfiltered read models for Rulebook and Order Integrity. Every read is deadline-bounded and broker-scope-bound; stale, incomplete, future-dated, wrong-account, timed-out, or reconnect-crossing evidence is explicitly uncovered and cannot recover an active episode. Rulebook now applies the same completed portfolio-stream receipt health as Order Integrity, so a cached empty book after stream silence cannot manufacture a clear. The loops drain before SQLite closes, suppress duplicate Protection observations caused by their shared position projections, and introduce no broker write, policy change, or submit-eligibility change. Their shadow-only delivery status is likewise superseded by the alert cutover above; the heartbeats are what make those sources observable enough to deliver.
  • Earnings rules can now distinguish a verified terminal issuer from missing data. An optional private [rulebook].terminal_evidence_file imports reviewed, exact-contract terminal/non-reporting evidence into a typed daemon.db authority at startup. The closed schema requires ConID + symbol + stock type, a classification, strictly non-future review/verification timestamps, a bounded revalidation deadline, and at least two independent allowlisted primary-source documents; an optional CIK must be a nonzero ten-digit identity and SEC evidence must match it. Rule snapshots serve only the committed SQLite revision and expose its revision, fingerprint, catalog review watermark, expiry, and redacted primary-source provenance. Identity reuse, provider/override conflict, expired evidence, unsafe input, and rollback all fail closed. A valid exact-contract match renders rules 6–8 as explicit not_evaluated with an Exempt row—never as pass, including stock-only terminal holdings—while mixed ordinary names remain assessed. Revocation persists a per-ConID tombstone, reactivation requires record verification strictly after that watermark, and immutable typed authority-change observations commit atomically with every initialize/import/update/revoke revision. Every rule-transition payload now links accepted terminal evidence back to its exact authority revision and record fingerprint using a sorted typed list; the audit link excludes issuer, symbol, CIK, URLs, and prose, and is explicitly empty when no terminal authority was accepted.
  • Borrow-fee outages now have a narrow, fail-closed diagnosis path. The global IBKR FTP file remains primary and is still the only source eligible to drive the annualized 50% extreme-fee flag; stale FTP evidence cannot emit or clear it, and not_due never triggers a substitute read. When a regular-session FTP refresh actually fails, backs off, or is unusable, the daemon can request historical FEE_RATE only for exact positive-ConID stocks currently held short in a complete, fresh, same-account portfolio stream. The RPC reports one typed borrow_fee_coverage row per exact contract (or an explicit symbol/route gap), including global versus portfolio-only scope, entitlement, nullable value, data type, scale status, policy eligibility, and redacted failure. Because IBKR documents the OHLC meaning but no controlled numeric-scale fixture is commissioned, every TWS row is scale_status=unverified, policy_eligible=false, and unable to create or clear the flag. Separate atomic SQLite state keeps opaque account-scope/exact-contract fingerprints, validated completed-session last-good records, and dated attempts without raw account ids or broker prose. Runtime entitlement, failure, and backoff state is discarded on restart; only the exact identical-wire 15-second retry boundary survives, preventing an immediate duplicate request without treating old runtime evidence as current. The portfolio cache now preserves same-symbol stock ConIDs and account identities independently and returns rows plus stream receipt under one projection lock, so collisions, foreign rows, stale receipts, incomplete routes, or reconnect scope drift fail closed before any historical result is published.

Claude Desktop MCPB

Download ibkr.mcpb from the Assets section below or from:

https://github.com/osauer/ibkr/releases/latest/download/ibkr.mcpb

Open the .mcpb file with Claude Desktop, drag it into Claude Desktop, or use Settings -> Extensions -> Advanced settings -> Install Extension. The bundle carries the local ibkr binary for macOS and Linux. Windows is not supported outside WSL because the daemon uses Unix-only primitives.

Shell and generic MCP install

curl -fsSL https://raw.githubusercontent.com/osauer/ibkr/main/install.sh | sh
ibkr setup claude-desktop

The first command picks the right binary for your platform, verifies its SHA-256, installs it to ~/.local/bin/ibkr, and adds that directory to your PATH if needed. On macOS it also clears the Gatekeeper quarantine flag. The second command writes the legacy MCP server entry into Claude Desktop's config; fully quit Claude Desktop and reopen.

If you only want the shell tool, stop after the first command and try:

ibkr account
ibkr quote AAPL
ibkr positions --by underlying

Prerequisite: a running IB Gateway 10.37+ or TWS (paper or live) on the same machine. The daemon auto-discovers it across the four standard ports.

See the README for the full feature menu and the troubleshooting matrix. Read-only by construction; the Safety section walks through the four guards.

⚠️ Broker-write capable build (ibkr-trading-* tarballs)

Everything above — the installer, the MCPB bundle, and the plain ibkr-v2.3.0-* tarballs — is read-only by construction: order transmission is not compiled in.

The ibkr-trading-v2.3.0-* tarballs are different: that binary can place, modify, and cancel orders with your broker once you configure the trading gates ([trading] mode plus a pinned gateway endpoint and account, cross-checked against the connected session; every write still needs a submit-eligible preview token). Only download it if you intend to trade through ibkr. Before enabling anything, read SECURITY.md and the trading preview guide, start against a paper account, and verify with ibkr trading status. Each release's order pipeline is exercised by an automated paper-trading round-trip before tagging.


Paranoid? Inspect the installer before running it

curl -fsSL https://raw.githubusercontent.com/osauer/ibkr/main/install.sh -o install.sh
less install.sh
sh install.sh

Doing something custom?

  • go install: go install github.com/osauer/ibkr/v2/cmd/ibkr@v2.3.0 (or @latest).
  • Different install dir: IBKR_INSTALL_DIR=/usr/local/bin sh install.sh. The installer won't touch your shell rc when you override; manage PATH yourself.
  • Manual download: pick a tarball or .mcpb from the Assets section below. Verify against SHA256SUMS.
  • Cursor / Continue / Zed / other local MCP clients: see Pick your path in the README for the JSON snippet (config file path differs per client).
  • Claude Code: /plugin marketplace add osauer/ibkr then /plugin install ibkr@ibkr inside any session.

Windows isn't supported. The daemon uses Unix-only primitives (setsid, flock, AF_UNIX sockets). WSL works.


Changed

  • Order reads now use the same daemon.db authority as order safety. orders.open, orders.history, preview-token redemption, and the reserved-order-ID floor read the committed order tables while preserving the existing Go predicates, critical sections, accept/reject decisions, and error text. There is no file-scan fallback or settings escape hatch; unavailable or inconsistent database authority fails closed before a broker write.
  • The public architecture page was rewritten for readers rather than converters. docs/architecture.md is restructured into ten numbered sections in plain English with a new observability section, and two claims were corrected against the code: HyperServe serves only the app process (the daemon speaks newline-delimited JSON over its Unix socket and MCP is stdio-only, with no HTTP transport), and the embedded exchange calendars cover 2026 through 2028 and report an explicit unknown state outside that range rather than falling back to weekday logic. The published twin at osauer.dev/ibkr/architecture.html is now a designed page with numbered chip navigation, process and ownership cards, and scroll-contained tables, and both diagrams are regenerated from the deterministic generator around the single-database storage and recovery model.
  • Canary copy is authored in plain English at the source: "A provisional market warning is visible" → "An early market warning is flashing (credit red, not confirmed yet)"; market evidence renders only informative facts ("2 yellow (credit and vol); red but unconfirmed: credit; 5 of 6 clusters reporting") and never zero-count buckets; low-exposure watch guidance reads "your exposure is low; keep watching — no reductions needed"; internal pipeline terms (confirmation-eligible, ranked regime clusters, cached compute, "rebalance this title") were rewritten as observable facts. CLI and MCP surfaces render the same daemon-authored copy.
  • A trader-panel re-review then drove a second pass: with the market closed, the status line stamps "market closed — prices are last-session prints" instead of presenting frozen Friday quotes as fresh; data-quality caveats sit in their own quieter "Data caveats" band with a muted DATA chip and never inflate the needs-attention count (which now counts decisions only); the market-stress check explains itself when an early warning is flashing but below the de-risking trigger, so the status line and the passed-checks line can no longer contradict each other; SPY/VIX tape evidence prints its trigger levels next to the observed moves; history timestamps older than two days carry real dates; and the history section is titled "Signal history".
  • Dismissing live alerts is now safe by construction: the control is labeled "Hide until signal changes", hides only watch/data rows, never touches act-severity rows or the header counter, and a regression test pins the act-day rendering (act rows sort first, dismiss keeps them visible, counter stays truthful).
  • Stored alert records are titled "Canary: watch" with a plain body ("Watch severity — market stress partly confirmed."); the "Open ibkr for portfolio details." call to action now appears only in the web-push payload, never inside the app. Push payloads remain redacted (no symbols, quantities, or account data).
  • The alerts severity filter bar (All / Warnings / Info) was removed: with only exceptions rendered, the filter had no job left, and its labels were the page's only (accidental) color legend.
  • Canary tape severity is now session-aware, closing the top follow-up from the trader-panel review: on official non-trading dates (weekends and holidays, per the embedded NYSE calendar) a frozen last-session SPY/VIX shock demotes to observe with "confirm at next open" guidance naming the reopening time, instead of holding "Freeze new risk" WATCH wallpaper all weekend, and the same frozen prints can no longer confirm stress, panic, carry unwind, or constructive tape anywhere in the canary. Trading-date pre/post-market moves keep full severity (live extended-hours prints are the row's documented purpose; the journal shows VIX printing all night on weekdays), and dates outside embedded calendar coverage keep legacy behavior, so historical replay is unchanged (all four sourced backtest corpora report identical metrics before and after). Tape evidence now carries the provenance stamp at the source ("market closed (weekend), frozen last-session prints"), which the Alerts page previously could only approximate in presentation. Six years of daily history motivated the demotion: 39 weekend-lit WATCH rows (roughly 434 stale-lit hours per year), 77% fully faded by the next close, and the real shocks re-light from live prints at the next open. Date-only replay observations are now clocked 15:59 ET inside their observation date rather than midnight UTC, which lands on the prior New York evening.
  • The regime lifecycle stage machine now applies the same closed-date tape rule, completing the pass the canary change deferred: on official non-trading dates frozen SPY/VIX day-change prints can no longer enter or hold any tape-driven stage (panic, confirmed stress, early warning, opportunity, stabilization), co-sign a heuristic confirmation, or claim the pure-tape panic severity exemption, which is what held early_warning/watch on half-reset Saturday prints across the 2026-07-18/19 weekend journal. Cluster-driven evidence is untouched, so real cluster reds still warn and confirm on any date, and the lifecycle's tape evidence rows keep the frozen print's magnitude while reading forward-warning/observe/unconfirmed. Every regime snapshot and decisions-journal line now discloses the calendar classification (tape_session_state / tape_session), and the trading rulebook's regime-stage latch holds through closed dates so the last trading-date stage governs weekend rule thresholds via the existing carried worse-of path, instead of a weekend stage re-latching fresh in either direction. Weekday pre/post/overnight prints keep full effect, dates outside embedded calendar coverage fail open, and both sourced regime backtest corpora report identical metrics before and after.
  • Brief movers now aggregate daily P&L by underlying (stock plus option legs per name, the same basis the Underlyings panel uses) and disclose the truncation residual as "N others" so the row reconciles to the account's daily attribution instead of silently dropping the tail; the Underlyings hero states the shared basis on screen ("Daily P/L by underlying · all held names · last session"). Closed sessions stamp themselves everywhere the number could read as live: the brief account row appends "market closed — daily P&L is from the last completed session" and the account header's "+x% today" becomes "+x% last session".
  • Protection tiles stopped rendering unavailable as zero: "Actionable theta/day" shows "--" with the Greeks-coverage reason when option legs lack Greeks (a €0.00 there meant "could not evaluate", not "nothing pending"), and "No-stop exposure" shows the uncovered row count instead of "€0" when those rows cannot be valued (stale or delisted marks). "Premium at risk" keeps its amount but downgrades with "the protective share of this premium is unknown" whenever a hedge-candidate leg cannot be classified. The "Trim Δ-adjusted risk" picker and Preview grey out with the reason on the control while portfolio delta is unavailable, instead of offering a preview that cannot size.
  • The rules card cross-links live events to their governing rules: upcoming held-name earnings while the earnings rules report unknown get an amber note naming both ("Earnings ahead (…) while rule 8 (At size before earnings) is unknown — the freeze cannot be confirmed"), and the brief's earnings event row escalates to attention with the same fact. Disclosure only; verdicts, ranking, and gating stay with the daemon.
  • Brief copy nits from the same review: the canary row collapses "watch · watch" to one word (app and CLI), capital tokens carry labels ("Tier block · Enforcement shadow"), the sign-off button speaks plainly and scopes its claim ("Sign off this reconcile report — statement clean", exact report id in the tooltip and row value, plus an on-control caveat whenever the rulebook changed since the last stamped brief), weekly artefacts read "not yet completed this week", the exposure-composition legend drops sub-half-percent "0%" chips, and the screen-reader-only theta and FX spans now carry their labels.
  • Every remaining passed-check number prints its policy trigger beside the observed value, extending the tape row's disclosure style to the whole checklist: margin cushion carries "(watch below 35%)", daily P&L "(watch at ±5%)", the exposure caps "(watch 150%)" per component, and the single-name concentration caps "(watch 35%)" — all read live from the risk policy, never hand-typed into copy. The reader now sees how far each number sits from the line that would flip the row, on the Alerts page, CLI, and MCP alike.
  • The protection-coverage row names the largest unprotected position and its uncovered amount in the row copy itself ("Review largest unprotected stock/ETF exposures before adding risk; largest unprotected MSFT $ 12,000.00."), mirroring the Monitor protection panel's "largest unprotected SYM amount" wording; a largest-unprotected row whose notional cannot be valued still contributes its name.
  • The daemon now stamps portfolio_alert_relevant on every canary snapshot, and both downstream relevance gates — the app's push/history gate and the Alerts page's preview gate — read the stamped verdict instead of each re-deriving the low-fit/exposure edge cases (the JS copy was one fit-semantics refactor away from silent drift after the recent portfolio-fit changes). The policy's single copy lives in internal/canary; an unstamped snapshot from an older daemon fails open to relevant, so version skew can add market-weather noise but never suppress alert delivery, and the compact alert view carries the stamp for scheduled monitors.
  • The act-day regression test now pins the negative literally: the alerts header can never render "All clear" while any act or watch condition exists, before or after dismissing rows.
  • Previous-context alert history now expires itself instead of waiting for the manual clear button (operator decision: 14 days). On every canary observation the app stamps records that still match the live context (fingerprint for canary-source records, account and mode for all sources); read records expire 14 days after they stop matching. Unread records and still-matching history never expire, an unknown live context expires nothing, and governance occurrences keep their separate 90-day retention.
  • Earnings dates now resolve through typed, per-provider evidence instead of a single opaque lookup. Each symbol records its aggregate resolution plus every provider's latest attempt, next retry, last-good value, and typed redacted failure, and applicability is decided from the exact broker contract identity rather than the symbol string — a definitive COMMON classification, an unknown one, and an identity mismatch are three different answers, and only the first can carry an ordinary issuer's date. An observed Nasdaq envelope that legitimately carries no date is parsed as such rather than as a provider failure, a persisted resolution is upgraded when better evidence arrives, and a genuinely unsupported instrument is recorded as unsupported rather than retried forever. Coverage gaps are typed and distinguished from negative answers throughout. What the desk sees is the same rule as before, now harder to evade: rules 6, 7, and 8 report unknown — never pass — whenever an ordinary issuer's date is unknown or stale, that check runs before any stock-only, option-side, or size normalization, and on those rules not_evaluated is a trusted negative only when every input is covered.
  • ibkr rules and ibkr rules history are now discoverable where every other command is listed: the CLI command table and the agent skill's command reference both carry them, with the history read labelled for what it is — persisted evaluator state transitions, not trade causality or broker evidence. The rulebook's own vocabulary was aligned at the same time: rules.snapshot emits rulebook-fp-v3 as a policy-identity fingerprint of the compiled discipline model, so the docs now say "model" where they said "policy" and state plainly that a compiled model is not proof that every threshold carries operator approval.

Fixed

  • Sensors now refresh on a schedule that keeps their authority current instead of letting it lapse and then reacting. A daemon-owned Regime scheduler starts a normal refresh one full timeout ahead of the hard freshness boundary, so the ordinary case replaces the published value before it can expire, and a failed early attempt no longer suppresses recovery for another whole window. Dealer gamma schedules its next run from when the last one completed rather than when it started — the old start-based clock could ask the gateway for a fresh fan-out while a successful result was two minutes old — and a persisted result without an in-memory completion stamp is dated conservatively rather than treated as brand new. Dependent sensors recover as soon as their inputs are current again instead of waiting out the next cycle, and S&P 500 breadth publication survives a restart mid-computation rather than losing the in-flight window.
  • Daily P&L health is now its own retained observation rather than a per-read guess. A detected regular-session failure is held until a newer valid frame proves recovery, so a failure raised at 16:20 no longer disappears merely because the session closed; the observation carries an explicit timestamp instead of inheriting the read's, after-hours reads are typed not_due rather than counted as missing data, and provider errors are normalized so the same underlying fault reads the same way every time. Only an opaque scope fingerprint and the value-free health state are persisted. The account projection behind it also stopped tripping over aggregate rows: correctly scoped aggregate ledger rows are admitted, and aggregate account rows the model does not describe are ignored rather than misread as position or ledger facts.
  • Shadow Protection and Order Integrity can no longer clear from a mixed broker/journal moment. Their final decision is now bound to one exact socket session, order-lifecycle generation, structural portfolio generation, and local SQLite order-event head. Protection compares the complete all-client API snapshot in both directions: an API order outside the daemon journal or an open journaled order absent from the broker snapshot becomes explicit unknown / reconciliation-required evidence. Lifecycle persistence failure also latches the source unavailable until a complete, same-session, compare-and-append reconciliation succeeds.
  • Reconnects can no longer move an already-authorized order instruction onto a replacement socket. Place, broker WhatIf, cancel, exercise, and all-client open-order snapshot requests carry the exact socket epoch at which their authority or request ID was claimed, use zero automatic retries, and write zero bytes if that epoch is no longer current. Rulebook shadow recovery is likewise fail-closed: it requires the exact fourteen governed rule rows and all five typed input-health rows, and accepts not_evaluated as a safe negative only for the explicitly defined terminal-issuer, off-session, and no-long-book cases.
  • Dealer gamma can recover a strictly validated, quarantined legacy-import observation into a separate context-only last-good copy when SQLite has no current result. The immutable imported observation remains decision-ineligible, recovery is idempotent and provenance-stamped, and any fresh compute replaces the recovered context. A restart now serves the retained rankable result while the new fan-out runs instead of presenting a false cold start.
  • Daily-brief Canary observations are timestamped after their account, positions, Regime, Rulebook, and market-event reads. The previous pre-read timestamp made healthy source rows appear to come from the future, so the shadow producer accumulated source_time_invalid coverage failures even though the Gateway and inputs were healthy. Genuine partial inputs still fail closed under their real reason.
  • The Alerts status line no longer calls live pre-market prints "last-session". Observed live on the Monday pre-open: the header ticker counted down "pre-open · opening in 7:10" over moving indicative quotes while the status line claimed "market closed — prices are last-session prints" — wrong for the prints, right only for the daily-change anchors. The stamp now reads the same official-calendar session bounds as the header ticker and tells the price story per phase: pre-open on a trading date stamps "pre-market — live indicative prints; daily changes anchor to the last close", after the close it stamps "after close", weekends and holidays name themselves, and a session without calendar coverage claims nothing instead of defaulting to "market closed". The delivery-down banner made the deliberate companion call: pre-open on a trading date now counts as attention-on — pre-market alerts are exactly the ones a phone must deliver before the bell — so failing push delivery banners with "ahead of the open" honesty instead of staying quiet until 15:30 CEST, while after-close, weekend, and holiday phases keep the quiet disclosure-only presentation and an unknown session state still fails visible.
  • The app browser smoke's attention-read guard guards again. The SPA's service worker claims its clients immediately, and WebKit never surfaces service-worker-controlled page fetches to Playwright's network routes — so the smoke's route-based interception silently let its QA acknowledge POST reach the real app host, exactly the "mark the operator's real unread as read" hazard the guard exists to prevent (benign in the observed runs only because nothing was unread). The guard now diverts the POST inside the page's wrapped fetch, before any network layer can see it, with the route kept as a second net; the governance not-observed fixture also caught up with the redesign and asserts the "waiting" chip instead of the raw enum.
  • Weekend and holiday regime tape headers no longer drift: on official non-trading dates the SPY/VIX day-change fields pin to the official daily closes of the last two completed sessions, with new spy_change_basis / vix_change_basis provenance fields naming the sessions spanned. The gateway's last print and its tick-9 previous-close anchor can each reset independently while the market is closed, and live Sunday evidence caught the result: the header read SPY +0.00% beside VIX +12.19% (then SPY −1.09% half an hour later) while Friday truly closed SPY −0.99% / VIX +12.19% — a pair no market ever printed, rendered into tape evidence and the dashboard header. Bars are matched by exact official session date, so a stale series withholds the change (fields_missing: spy_day_change / vix_day_change) instead of quietly reporting a wrong-session move, and a failed pin never falls back to the drifted snapshot values. Trading dates, including pre- and post-market, keep the live tick-9 behavior; the closed-date key is the same embedded-calendar authority the canary's session-aware severity and the regime lifecycle's closed-date gating use.
  • The canary could tell the operator "your exposure is low; keep watching — no reductions needed" when every exposure-measuring signal was blocked or data-quality (the "low" portfolio fit was a fall-through default, and the greeks-degraded escalation branch was unreachable dead code), converting missing Greeks into reassurance with a prescriptive instruction attached. Portfolio fit is now "unknown" when the measuring signals themselves are blind, the watch summary says "your portfolio exposure could not be measured from this snapshot; verify exposure before relying on this reading", live market pressure keeps its defensive watch frame instead of demoting to a data-quality footnote, and a regression test pins that data-blocked exposure signals can never yield a "low" fit.
  • The risk-capital peak ratchet accepted equity observations from the wrong broker session: a paper-pinned daemon sharing the production state directory could silently ratchet the live adjusted peak with the paper account's equity (the proposals engine already scope-checks its snapshots; the capital store did not), overstating drawdown and consumed risk against a phantom peak — the trader-panel review caught exactly this state rendering as "OK". Capital state now binds to one broker identity: observations from a non-live mode, an unresolved scope, or a different account are refused and journaled ("equity_observation_rejected" with both identities), the state file records its owning account, and every accepted peak ratchet is journaled ("adjusted_peak_advanced" with before/after peak, the observed equity, and its timestamp). A new human-only, journaled ibkr policy correct-peak verb repairs a poisoned peak — corrections may only lower it, anchored to the retained-statement replay (--from-statements) or an explicit value with a mandatory reason — and deliberately never touches the drawdown latch, which records a real engagement and stays reset-drawdown's job.
  • urgent, the top canary severity, rendered with the informational blue tone in the alerts list; severity tones now map the daemon ladder exactly (observe < watch < act < urgent, order-mismatch critical included) and the copy-text sniffing fallback is gone.
  • Protection evidence no longer renders a bare "unprotected € —" when the unprotected notional is zero or unavailable.
  • Protective-stop mismatch alerts no longer sink into "previous context" the moment the canary fingerprint moves: fingerprint staleness now applies only to canary-source alerts, so order-mismatch alerts stay current until their account or mode context actually changes.
  • Opening the Alerts tab before the first canary snapshot no longer floods "Needs attention" with the entire stored history (each stored record counted as a live condition); history now stays history until a staleness verdict is possible, and repeated records of one condition count once.
  • A data race in connection teardown (caught intermittently by make test): Disconnect nulled the transport fields while the reader goroutine could still dereference them. Teardown now closes only the socket to unblock a parked reader and drops the buffered transport state after the goroutine wait succeeds; a reader that outlives the bounded wait keeps valid fields until the next connect attempt replaces them.