Skip to content

v1.15.0

Choose a tag to compare

@osauer osauer released this 08 Jul 06:20

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 v1.15.0

  • New advisory trading rulebook: ibkr rules (and the ibkr_rules MCP tool) evaluates a 12-rule daily discipline checklist against the live book — per-name exposure cap, single option-line premium cap, negative-cash sell-only mode, portfolio extrinsic (theta-rent) budget, expiry runway, earnings catalyst coverage, overwrites spanning earnings, pre-earnings size freeze, red-on-green-tape weakness, winner-trim into strength, a green-day execution nudge, and hedge-band integrity — ranked hardest-first. Earnings dates come from a free per-symbol fetch with manual overrides (ibkr settings set features.rulebook.earnings_overrides), cached last-known-good across restarts. Statuses are pass/info/watch/act/unknown/not_evaluated, and missing inputs always degrade to unknown — a cold daemon or unknown earnings date can never render a false pass. The Canary app shows a compact Rules card beside the portfolio hero with a full-checklist drill-in, and order previews carry advisory rule_* warnings when a draft would worsen a breached rule; nothing in the rulebook affects submit eligibility or any broker-write gate, and features.rulebook.enabled=false turns the whole surface off.

  • Corrected a long-standing mislabel in account P&L: the daily_pnl_unrealized / daily_pnl_realized fields are renamed to pnl_unrealized_total / pnl_realized_total. They carry the account's inception-to-now TOTAL unrealized/realized P&L from IBKR's reqPnL stream — not a breakdown of the daily figure, so they never summed to daily_pnl. The ibkr account CLI now shows them under a "Total P&L (reqPnL stream)" block instead of the misleading "Daily P&L breakdown / of which unrealized/realized".

  • ibkr status now reports the quote / scanner / history / chain subsystems as ready when the market-data farms are actually connected. They were stuck showing degraded ("no market-data farm connection notice observed") for the whole session even while quotes, chains, and gamma worked — a false alarm, now fixed.

  • Canary app money metrics no longer flap between currencies and formats across refreshes ("No-stop exposure" flipping €12K↔"3 rows", winner/loser daily P/L flipping $↔€, "Risk over target" flipping $24K↔"--"). The protection panel's theta/day and risk-over-target now render in the account base currency next to no-stop exposure, with an explicit "Mixed"/"Review" disclosure when a one-currency total is genuinely unavailable — never a silent USD label on a sum that isn't USD.

  • Canary app mobile usability pass. Open orders now read like orders instead of journal rows: the status line shows "Pre-submitted · At broker" instead of raw pre_submitted / broker_acknowledged / ibkr-… internals (full detail moves to the tooltip), the quantity/trail/stop/offset edit boxes carry visible labels, and the order type / TIF line no longer clips. Tapping an alert on the Alerts tab expands its full text in place — it previously activated a detail panel on the hidden Monitor tab, so the tap looked dead and truncated alert text was unreachable.

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-v1.15.0-* tarballs — is read-only by construction: order transmission is not compiled in.

The ibkr-trading-v1.15.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/cmd/ibkr@v1.15.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.


Added

  • Trading rulebook engine (docs/design/trading-rulebook.md): daemon RPC rules.snapshot returning RulesResult (12 RuleRows with observed/threshold/evidence/offenders, hardest-first ranked, breach_counts, result-level input_health, per-name earnings[] provenance, policy fingerprint); pure evaluator in internal/risk with a never-false-pass property test; Nasdaq earnings fetcher with last-known-good cache (~/.cache/ibkr/earnings-dates.json, async refresh off the snapshot path, per-symbol failure backoff); runtime settings features.rulebook.enabled and features.rulebook.earnings_overrides; advisory rule_* DataWarnings on order previews (reduce/close intents never warn); rules-decisions transition journal; snapshot.rules sibling section + rules SSE event in the app layer; SPA Rules card with drill-in; hook-behavior-check make gate running a 19-case table-driven test of the plugin safety hook, whose read-only allowlist now covers bare ibkr orders.

  • Trade-proposal snapshots now carry base-currency twins of the money aggregates: per-proposal theta_per_day_base / risk_excess_notional_base and counts-level theta_per_day_base, risk_reduction_excess_notional_base, base_currency, plus theta_per_day_currency labeling the existing raw theta sum (omitted when the contributing proposals span currencies). All additive; nil/omitted means unavailable, never zero.

Changed

  • Renamed account-financials fields daily_pnl_unrealizedpnl_unrealized_total and daily_pnl_realizedpnl_realized_total across ibkr account --json and the ibkr_account MCP tool, and relabeled the corresponding CLI block. These are the reqPnL-stream account totals (inception to now) and stay distinct from the account-updates unrealized_pnl / realized_pnl — the two total-unrealized measurements come off different gateway feeds and can legitimately differ.

  • Decluttered the Canary app's repeated chrome: panel freshness reads "updated now" when fresh (the clock time moved to the tooltip; aged and stale renderings are unchanged), the Market & Portfolio head collapses its two identical regime/canary timestamps into one, the exposure-composition bar shows percentages only in the legend instead of twice, redundant per-row "current signal" chips are gone from the Alerts tab's Current-signal section, the Opportunities hero metric is labeled "Exercises" (the panel is exercise-only, and the count includes not-yet-eligible rows), "Purge all!" lost its exclamation mark, and the brand header is more compact so more of a phone screen shows data.

Fixed

  • Tightened rule 1's hedge exemption in the trading rulebook to match what rule 12 actually sizes. A policy-hedge index name with net-short delta is exempted from the per-name concentration cap only up to the short delta-dollars of its rule-12-classified hedge legs (long puts with delta present); short stock or short calls in a hedge symbol, and any residual short beyond the sized legs, count toward concentration as usual, with the sized portion still disclosed in the row's exempt list.
  • A portfolio risk-off sweep retried with the same request_ref inside the 90-second dedupe window now re-checks the broker-write gate before replaying the cached result, so a trading freeze engaged between the two calls surfaces as a blocker instead of replaying a pre-freeze "ready" preview (the stale preview's token was already unredeemable under freeze — this fixes the misleading status, not an order path). The freeze blocker's action text now spells out its scope: new orders, modifies, and reduce sweeps are all blocked while frozen; only cancels remain allowed.
  • Corrected two stale safety-policy claims in docs/design/trading-paper-smoke.md that predated the 2026-06-20 agent-origin policy flip: it still described a daemon-side agent-origin live block and an agent restriction on paper-smoke, neither of which exists — origin is audit/policy metadata, and every broker write is gated by pins, mode, preview tokens, WhatIf eligibility, and freeze for all origins. The agent-policy doc drift test now guards this file and both stale phrasings.
  • Fixed the Canary app's selected-alert "Clear" button rendering white-on-white: it used the dark-panel button variant on a light panel and stretched to the panel's full height; it now renders as a normal light-context button.
  • Fixed a false-degraded market-data-farm status. The gateway sends its farm-status burst (2104/2106/2158 "connection is OK") immediately after the API handshake, and the connection read loop consumed that burst before the connector wired its system-notice handler — so the notices were logged but dropped, and because the gateway only re-sends farm status on change, the connector's farm view stayed empty for the rest of the session. ibkr status then reported quote / scanner / history / chain as degraded even though the farms were connected and data flowed normally. System notices arriving before the handler is wired are now buffered and replayed when it is set (mirroring the existing order-message pending buffer), so a farm that has announced "OK" is reported ready and still flips back to degraded on a 2103/2105 disconnect.
  • Stopped the daemon log from ballooning (observed at 1.5 GB). IBKR re-sends orderStatus frames for unchanged working orders many times per second, and each was logged at INFO; a single resting PreSubmitted order could write thousands of identical lines per minute. Only genuine status/filled/remaining transitions log at INFO now — verbatim repeats drop to Debug. Order state tracking is unchanged. The daemon log additionally rotates at boot when it exceeds 64 MiB, keeping one previous generation (ibkr-daemon.log.1), so the file no longer grows without bound across restarts.
  • Fixed the root cause of the Canary app's flapping money metrics: the daemon re-derived the base-currency FX ledger on every positions/account request, and one transient FX snapshot-quote timeout (budget 1.2s per pair direction) zeroed the rate and stripped every fx_rate / *_base field — including protection_coverage.unprotected_notional_base — from that single response, also flipping the positions fingerprint and staling proposal revisions mid-confirm. The daemon now keeps a last-known-good FX-rate cache (rates only, never account balances): a rate fresher than 15 minutes is served without a gateway quote (the app host polls every 5s and used to pay up to 2×1.2s per suspicious currency per poll), a failed live resolution falls back to the cached rate for up to 72 hours (spanning weekends), and serve-mode transitions log once per episode instead of once per poll. The cache also persists across daemon restarts (rates only, ~/.cache/ibkr/fx-rates.json, best-effort): a restart during IBKR's nightly server-reset window or over a weekend re-serves last-known-good rates immediately instead of starting cold with stripped base fields until the first live resolution.
  • Removed the Canary app's remaining hardcoded-USD fallbacks. Every money formatter (money, compactMoney, compactWholeMoney, riskMoney, and the protection-coverage base-currency resolver) now renders an amount whose currency is genuinely unknown as a bare number — with a tooltip disclosure on the no-stop-exposure and opportunities-gain hero metrics — instead of silently labeling it USD. The audit also fixed two real mislabels on non-USD-base accounts: the Settings trading-limits line rendered [trading].max_notional (defined in the account currency) with a fixed $, and the portfolio risk-off sweep labeled each leg's "cuts ~X of risk" (a base-currency delta-adjusted amount) with the leg's contract currency. A static contract test now forbids any "USD" literal in the app source.
  • Stopped WARN-logging routine "No security definition" (code 200) gateway notices for derivative/probe requests. Option-chain fan-outs (gamma, proposals/theta scans on held names) deliberately subscribe the secDefOptParams strike superset, where some (expiry, strike, right) triples are unlisted — the gamma engine already counts these as contract_missing_legs — and FX resolution quotes both directions of a currency pair when only one is listed, with the FX cache absorbing the miss. Each such designed-to-miss probe still echoed a WARN per request: ~12 per 15-minute gamma cycle all session, and 2,102 in a single hour during an FX-degraded episode on 2026-07-07 when every 5-second app-host poll retried live FX resolution. Definition-miss notices for OPT/CASH-aliased requests now log at Debug; stock and alias-less requests keep the WARN trail (that's the signal feeding dead-symbol inactive marking). System-notice request tags now include the security type (reqID=42 (USD CASH) instead of (USD)), naming the failing contract class directly.
  • Removed a per-request WARN + full goroutine stack dump (reqContractData anomaly) from the daemon log. The leftover debugging tripwire fired on every by-fields stock contract-details request — the normal resolution path behind order previews (min-tick lookup) and quote historical fallbacks, which by design sends an empty primary exchange with conID=0 — writing a multi-KB stack trace per occurrence (179 in one log file; three routine order previews produced three dumps). The wire-shape trace survives at Debug without the stack for wire-level forensics; contract resolution itself is unchanged.
  • Quieted the off-hours gateway-reconnect log flood. While TWS / IB Gateway is closed the daemon retries the connection on its normal cadence, and each attempt logged its full multi-line connect sequence (Starting connection process, Client N: Connecting to …, Failed to connect … connection refused, Daemon up but gateway not connected, plus the connector start/stop narration) at INFO/WARN — roughly 50k lines over a 13.5h overnight window, a larger contributor to log growth than the order-status flood above. The daemon now logs the transition into "gateway unreachable" once at WARN, demotes the identical per-cycle repeats (and the low-level connect narration) to Debug, and logs a one-line "Gateway reachable again" when the handshake recovers. Reconnect timing, backoff, and retry counts are untouched — this is a logging-volume change only.
  • Hardened the opportunities subsystem's refresh loop to the proposals engine's standard. Three fixes: (1) failure streaks were double-counted — the scheduled loop recorded every outcome twice — so ibkr status reported "refresh blocked 4 consecutive times" after two boot-race attempts and flagged the subsystem degraded at half the intended threshold; (2) a transient account/positions fetch failure (every daemon boot, any gateway outage) replaced the served snapshot — including the last-good one just re-adopted from opportunities-current.json — with an empty blocker shell; transient failures now preserve the same-scope, same-policy snapshot with the blocker disclosed, and the portfolio-stream positions_pending guard from proposals now applies to opportunities too, so an unprimed empty position list can no longer generate a wrong empty snapshot; (3) blocked refreshes never logged — the subsystem now emits the same throttled warn trail and recovery line as proposals, and the degraded status row reports the actually-served snapshot's as_of and the streak's true start time instead of internal bookkeeping that rendered as 0001-01-01T00:00:00Z. Operator-owned states (opportunities_disabled, policy drift) no longer count toward the refresh-failure streak.