Skip to content

Repository files navigation

ibkr_core_mcp

Python library for Interactive Brokers clients. Wraps the IBKR Client Portal API and ships batteries-included tooling for algorithmic trading, backtesting, real-time streaming, and Claude AI integration.

Who is this for? IBKR account holders who want to automate market data retrieval, portfolio monitoring, and order staging from Python — or who want to connect an AI assistant to their brokerage.


📚 Full documentation catalog: docs/README.md

Feature overview

Module What it does
GatewayManager Builds and runs the official IBKR Client Portal Gateway as a Docker container, guides browser login + 2FA
IBKRClient Full REST client for the Client Portal API — market data, positions, orders, scanners
ClaudeToolkit 44 ready-made Claude AI tools (tools= parameter) for Anthropic SDK integration
SQLiteStore Local SQLite store — trade history, price alerts, session log
GDriveCache Google Drive Parquet cache for OHLCV data
streaming IBKR WebSocket live quotes + price alert engine
backtest Safe sandboxed strategy backtester
indicators Technical indicators (RSI, MACD, Bollinger, ATR, VWAP, …)
analytics Portfolio analytics — drawdown, Sharpe, Sortino, Calmar, CAGR, win rate, profit factor
pinescript PineScript v5 generator
web_scraper / local_browser Whole-web search (Firecrawl) + the local Crawl4AI browser for anything with a URL, and the web_docs/ Drive archive
mcp_server MCP server (stdio + SSE) exposing all 46 tools to any MCP client

Requirements

  • Python 3.11 – 3.13 (3.14 not yet supported — enforced by requires-python)
  • Docker Desktop (for GatewayManager)
  • An Interactive Brokers account (live or paper)
  • Anthropic API key (for Claude AI tools / MCP server)

macOS — required for order execution

Order write methods (place_order, place_order_and_confirm, modify_order, modify_order_and_confirm, cancel_order, reply_order) are gated by Touch ID. This gate is enforced inside the library and cannot be bypassed. It requires:

Requirement Minimum
Operating system macOS 10.12.1 (Sierra)
Hardware Any Mac with a built-in Touch ID sensor or a Touch ID keyboard
Python package pyobjc-framework-LocalAuthentication (installed automatically with ibkr_core_mcp)
Policy LAPolicyDeviceOwnerAuthentication — Touch ID/Face ID first, falls back to the device's system password if the biometric scan fails or is cancelled

Touch ID is available on: MacBook Pro (late 2016+), MacBook Air (2018+), Mac mini (2020+), iMac (2021+), Mac Studio, Mac Pro (2023+).

Linux / Windows: All read-only tools (market data, portfolio queries, backtesting, analytics, MCP server) work on any platform. Order execution is macOS-only by design.


API Documentation

This library is built on official documented APIs. Any contribution touching API behavior, error codes, endpoint paths, or field names must reference the official source — never assume from memory or training data.

API Official reference
IBKR Client Portal API https://www.interactivebrokers.com/docs/web-api/v1/introduction
IBKR Flex Web Service https://www.ibkrguides.com/clientportal/performanceandstatements/flex3.htm
Flex error codes https://www.ibkrguides.com/clientportal/performanceandstatements/flex3error.htm
IBKR WebSocket streaming https://www.interactivebrokers.com/docs/web-api/v1/ws/introduction
Google Drive API v3 https://developers.google.com/drive/api/reference/rest/v3
macOS LocalAuthentication https://developer.apple.com/documentation/localauthentication
Firecrawl API https://docs.firecrawl.dev/api-reference/endpoint/scrape
Crawl4AI (local browser) https://docs.crawl4ai.com/

Full details and per-file API ownership are in CLAUDE.md.


Installation

pip install git+https://github.com/stephus182/ibkr_core_mcp.git

Or pin to a specific version:

pip install git+https://github.com/stephus182/ibkr_core_mcp.git@v1.0.0

Or for local development:

git clone https://github.com/stephus182/ibkr_core_mcp.git
cd ibkr_core_mcp
pip install -e ".[dev,server]"

Quick start

1. Start the IBKR gateway

GatewayManager handles the entire Docker lifecycle — building the image, starting the container, and guiding you through browser login and 2FA.

from ibkr_core_mcp.gateway import GatewayManager

gm = GatewayManager()
gm.startup()   # interactive: starts container → opens browser → waits for auth

Or use the programmatic API (for non-interactive environments, e.g. a web UI or a batch job):

gm = GatewayManager()
gm.start()                   # build image (first run) + docker run
gm.wait_for_gateway()        # wait up to 120 s for Java process
gm.open_login_page()         # open https://localhost:5055 in browser
# … user logs in …
gm.wait_for_auth(timeout=300)  # poll until authenticated

startup() steps on first run:

  1. Launch Docker Desktop (macOS) if not running
  2. Build the gateway image (~60 MB IBKR zip, cached afterwards)
  3. Start the container on port 5055
  4. Open https://localhost:5055 in your browser
  5. You log in with your IBKR credentials + 2FA
  6. Verify the session is active

2. Query IBKR

from ibkr_core_mcp import IBKRClient, SQLiteStore, Config

config = Config.from_env()             # reads env vars / .env
store  = SQLiteStore(config)
client = IBKRClient(config)            # BrowserCookieAuth used by default

# Most endpoints need an account ID first
accounts   = client.get_accounts()
account_id = accounts[0]["accountId"]

summary   = client.get_account_summary(account_id)
positions = client.get_positions(account_id)

# Market data requires a contract ID (conid), not a symbol string
contracts = client.search_contract("AAPL")
conid     = contracts[0]["conid"]
bars      = client.get_market_history(conid, period="1Y", bar="1d")

3. Use Claude AI tools

import anthropic
from ibkr_core_mcp import ClaudeToolkit, IBKRClient, SQLiteStore, GDriveCache, Config

config  = Config.from_env()
store   = SQLiteStore(config)
cache   = GDriveCache(config)
client  = IBKRClient(config)
toolkit = ClaudeToolkit(client=client, cache=cache, store=store, config=config)

ai = anthropic.Anthropic()

response = ai.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=4096,
    tools=toolkit.tools,               # drop-in for Anthropic SDK
    messages=[{"role": "user", "content": "What are my current positions?"}],
)

# Route tool calls back through the toolkit
for block in response.content:
    if block.type == "tool_use":
        text, _fig = toolkit.execute(block.name, block.input)  # _fig is always None in v1.0

Available tools (Claude AI / MCP)

See docs/tools-reference.md for full parameter docs and output shapes.

Tool Description
fetch_market_data OHLCV history with Google Drive cache
check_cache Check whether data is cached
list_cache List all cached datasets
delete_cache Delete a cached dataset
get_account_summary Net liquidation, cash, P&L
get_positions All open positions
get_pnl Real-time P&L partitioned by position
get_ledger Cash balances by currency
get_allocation Portfolio breakdown by asset class
get_trades Trade history (live: last 6 days; store: unlimited)
sync_flex_trades Sync full history via IBKR Flex Web Service
sync_flex_archive Re-sync full Flex archive from GDrive parquet
check_flex_coverage Activity distribution report — trade-date coverage across stored history (not an integrity check)
import_flex_file Import a locally downloaded Flex XML file into SQLite
verify_flex_import Import integrity check — cross-checks XML tradeIDs on Drive against SQLite; uses manifest to skip re-verifying unchanged files
get_live_orders Working orders (Submitted, PreSubmitted, Inactive, …)
get_order_status Status of a specific order by ID
diagnose_orders Diagnose order issues — checks session, permissions, account
preview_order Whatif order preview — no order placed
get_pa_performance Portfolio Analyst NAV performance
get_pa_transactions Portfolio Analyst transactions
search_contract Resolve symbol → conid, exchange, currency
get_contract_info Full contract details (exchange, trading hours, etc.)
get_option_chain Option chain — expiry months + call/put strikes
get_futures Futures contracts — expiry months, conids
get_market_snapshot Live bid/ask/last/volume for one or more symbols
get_trading_schedule Trading hours and next session for a symbol
run_scanner Market scanner (top gainers, losers, most active, …)
get_notifications IBKR FYI account notifications
get_alerts List IBKR native price alerts
create_price_alert Create a server-side IBKR price alert
modify_price_alert Update threshold or direction on an existing IBKR alert
delete_alert Delete an IBKR price alert
activate_alert Enable or disable an IBKR price alert
get_watchlists List IBKR watchlists and their contents
add_indicators Compute RSI, MACD, Bollinger, ATR, VWAP, …
run_backtest Sandboxed RestrictedPython strategy backtester
generate_pinescript Generate PineScript v5 strategy/indicator
get_analytics Sharpe, Sortino, Calmar, CAGR, max drawdown
firecrawl_search Search the whole web — a query with no site in mind. Returns ranked URLs + snippets
search_site Search one site — domain + query, BM25-ranked. Free (local browser + public sitemaps)
crawl_site Archive a site to Google Drive under web_docs/; reuses a cached crawl <48h old. Free
fetch_page Read one page as markdown, incl. paywalled sites with a saved login. Free

MCP server

Expose all 44 tools (+ 2 MCP-only alert tools = 46 total) to any MCP-compatible client (Claude Desktop, Cursor, etc.):

# stdio transport (Claude Desktop / Cursor)
python -m ibkr_core_mcp.mcp_server

# SSE transport with live streaming
python -m ibkr_core_mcp.mcp_server --transport sse --port 5174 --stream

Streaming (live quotes)

import asyncio
from ibkr_core_mcp.streaming import IBKRWebSocket

# IBKRWebSocket takes the HTTPS gateway URL — it converts to wss:// internally
ws = IBKRWebSocket(gateway_url="https://localhost:5055", session_cookie="")

async def main():
    await ws.connect()
    conid = 265598  # AAPL — use search_contract() to find conids
    await ws.subscribe(conid)
    async for quote in ws.listen():
        print(quote.symbol, quote.last, quote.bid, quote.ask)

asyncio.run(main())

For price alerts, use the native IBKR alert system via ClaudeToolkit.execute("create_price_alert", ...) — alerts fire server-side and deliver to the IBKR mobile app even when the app is closed.


Backtesting

from ibkr_core_mcp.backtest import run_backtest

# Strategy code receives a DataFrame `df` and must set df['signal']
# 1 = long, 0 = flat, -1 = short
code = """
fast = df['close'].ewm(span=12).mean()
slow = df['close'].ewm(span=26).mean()
df['signal'] = (fast > slow).astype(int)
"""

result = run_backtest(code=code, df=bars_dataframe, strategy_name="EMA crossover", symbol="AAPL")
print(result.sharpe, result.max_drawdown, result.total_return)

Strategy code runs in a RestrictedPython sandbox — no file system or network access. 4096-character limit; 10-second timeout. Available: df, pd, np.


Web Scraping

Four tools, one job each, and no fallback between them. Anything that takes a URL goes to a free local browser (Crawl4AI, Playwright-based, no API key). Firecrawl is kept for the one thing the browser cannot do: search the web when you have no URL yet.

Tool Engine Cost Job
firecrawl_search Firecrawl ~1 credit Find pages anywhere
search_site Crawl4AI free Find pages on one site, BM25-ranked
crawl_site Crawl4AI free Archive a site to Drive web_docs/{url-slug}/
fetch_page Crawl4AI free Read one page as markdown

The first two find and return URLs; the last two read. Neither finder returns page text — follow one with fetch_page.

Why the local browser is the default and not the fallback. Until 2026-07-30 this was a two-rung ladder: Firecrawl first, Crawl4AI only as a rescue. Measured on the same URLs minutes apart, that was backwards — local returned 17,364 B in 1.2 s where Firecrawl returned 14,341 B in 16.8 s, and 8,786 B in 1.3 s against 5,515 B in 13.2 s. Bigger, ~10× faster, free. The ladder and ~900 lines of arbitration went with it. Counter-case worth knowing: hosts with real anti-bot protection refuse the local browser outright (wsj.com → HTTP 401 and 1 byte, and no saved login changes that).

crawl_site checks Drive for an existing manifest before opening a browser — if one under 48h old exists for that URL it is returned directly, fetching nothing (force_refresh: true bypasses). The 48h window is this package's own choice for reference-doc content, informed by Firecrawl's v2 scrapeOptions.maxAge default (172,800,000 ms) as an externally-validated reference point; the v1 API this package calls defaults that same parameter to 0.

# The three browser tools need this extra (base install works without it)
pip install "ibkr_core_mcp[scraper]"
crawl4ai-setup   # installs Playwright/Chromium, one-time

For paywalled sites you already subscribe to, Crawl4AI can reuse a saved browser login — no credentials are ever stored by ibkr_core_mcp, only the resulting browser session:

# One-time interactive login; opens a real browser window. Needs a TTY.
python -m ibkr_core_mcp.local_browser create-profile https://www.ft.com

The session lives under CRAWL4AI_PROFILES_DIR (default ~/.ibkr_core/crawl4ai_profiles/<domain>/) and is reused automatically on later scrapes of that domain. Every URL reaching the local browser is SSRF-validated first, and a second Playwright-level guard re-checks every navigation, redirect and subresource — see SECURITY.md.

Live tests are mandatory for this subsystem, because every defect in it was found by running a tool rather than by a test failing:

set -a; source ./.env; set +a
pytest tests/test_web_tools_live.py -v -m integration     # 11 tests, ~30s

Full detail, including the credit model and per-host notes: docs/web-scraper-reference.md.


Environment variables

Copy .env.example to .env and fill in:

Variable Required Description
ANTHROPIC_API_KEY ✅ for Config.from_env() Anthropic API key (required by ClaudeToolkit; Config.from_env() raises if absent)
IBKR_GATEWAY_URL Client Portal URL (default: https://localhost:5055/v1/api) — the /v1/api suffix is required; paths are appended verbatim
IBKR_SQLITE_PATH optional SQLite store path (default: ~/.ibkr_core/store.db)
GOOGLE_DRIVE_FOLDER_ID for GDrive Root Drive folder — parent of db/ and market_data/ subfolders
GDRIVE_DB_FOLDER_ID optional Explicit folder for claudia.db. If unset, auto-created as db/ inside GOOGLE_DRIVE_FOLDER_ID
GDRIVE_CACHE_FOLDER_ID optional Explicit Drive folder for Parquet cache. If unset, auto-created as market_data/ inside GOOGLE_DRIVE_FOLDER_ID
GDRIVE_TOKEN_FILE for GDrive OAuth2 token path
GDRIVE_CREDENTIALS_FILE for GDrive OAuth2 credentials path
IBKR_FLEX_TOKEN for Flex sync Flex Web Service token
IBKR_FLEX_QUERY_ID for Flex sync Flex query ID
FIRECRAWL_API_KEY for whole-web search only Firecrawl API key. If unset, only firecrawl_search returns a "not available" message rather than raising
GDRIVE_WEB_DOCS_FOLDER_ID optional Explicit Drive folder for scraped docs/search snapshots. If unset, auto-created as web_docs/ inside GOOGLE_DRIVE_FOLDER_ID
CRAWL4AI_PROFILES_DIR optional Saved Crawl4AI login profiles for paywalled sites (default: ~/.ibkr_core/crawl4ai_profiles)

Security

ibkr_core_mcp does not place orders autonomously. Order write methods (place_order, place_order_and_confirm, modify_order, modify_order_and_confirm, cancel_order, reply_order) on IBKRClient are gated by two sequential controls enforced at the innermost call site inside the library. A single IBKR order can require several chained confirmation replies before reaching a terminal state — place_order_and_confirm/modify_order_and_confirm are the recommended entry points, since they re-run both gates automatically for every reply in the chain (see CLAUDE.md — Security & Fingerprint Authentication):

Gate 1 — Touch ID (macOS LocalAuthentication)

Implemented in human_auth.py using the macOS LocalAuthentication framework via pyobjc-framework-LocalAuthentication.

  • Policy: LAPolicyDeviceOwnerAuthentication — tries Touch ID/Face ID first, then falls back to the device's system password if the biometric scan fails or is cancelled. The fallback exists because a fingerprint scan can genuinely fail to read (wet/dry skin, worn ridge detail, sensor angle) even for the real account owner — the stricter biometrics-only policy has no recovery path on a failed scan.
  • No bypass inside the library: ibkr_core_mcp itself never intercepts, caches, or skips this call — every order-write attempt calls require_touch_id() fresh. If both the biometric scan and the system password fail, HumanAuthError is raised immediately and the order is never submitted.
  • Timeout: 60 seconds. An unanswered prompt raises HumanAuthError and the order is not submitted.
  • Prompt text: The caller-supplied reason string appears in the macOS Touch ID dialog (e.g. "Confirm order: BUY 100 AAPL").
  • Thread-safe: Uses a threading.Event to wait for the async LAContext reply callback without blocking the main run loop.

If pyobjc-framework-LocalAuthentication is not installed, or if the Mac hardware does not support biometrics (e.g. a Mac mini without a Touch ID keyboard attached), the gate raises HumanAuthError and the order is never submitted.

Gate 2 — Visual confirmation dialog (tkinter)

Implemented in order_confirm.py.

  • Full order details displayed in a modal window
  • 60-second countdown timer — dialog auto-cancels on timeout
  • Enter key disabled — confirmation requires a deliberate mouse click on the "Confirm" button
  • Runs on the main thread; the tkinter event loop is driven internally

Both gates are part of ibkr_core_mcp itself. Downstream consumers such as ClaudIA can add further gates (e.g. a "Stage this order" button click in its Panel UI) before place_order/place_order_and_confirm is ever invoked.

GatewayManager runs the IBKR Client Portal Gateway as a Docker container bound to localhost:5055 only. The container has no privileged access and exposes no host filesystem mounts.

Web scraping (search_site, crawl_site, fetch_page) is SSRF-guarded at two independent layers — a pre-fetch URL check, plus a Playwright-level per-request check on every Crawl4AI fetch (initial navigation, redirects, and subresources) that closes DNS-rebinding and redirect-based bypasses the pre-fetch check alone can't. See SECURITY.md.

See SECURITY.md for the full security model.


Market Calendar

SQLiteStore.get_market_calendar_context() uses exchange_calendars to provide trading-day-aware context without any API calls:

from ibkr_core_mcp.store import SQLiteStore

# Default: 20 exchanges (full G20 + Eurex) — no Config needed for this call
cal = SQLiteStore.get_market_calendar_context()

# {
#   "today": "2026-06-24",
#   "is_trading_day": True,
#   "last_trading_day": "2026-06-23",
#   "next_trading_day": "2026-06-25",
#   "primary_exchange": "XNYS",
#   "holidays_by_exchange": {
#     "XNYS":  ["2026-01-01", "2026-01-19", "2026-02-16", ...],   # NYSE
#     "CME":   ["2026-01-01", "2026-07-04", ...],                  # CME Futures
#     "XLON":  ["2026-01-01", "2026-04-03", "2026-04-06", ...],   # LSE London
#     "XETR":  ["2026-01-01", "2026-04-03", ...],                  # Xetra Frankfurt
#     "XTKS":  ["2026-01-01", "2026-01-02", ...],                  # TSE Tokyo
#     "XHKG":  ["2026-01-01", "2026-01-28", ...],                  # HKEX Hong Kong
#     "XASX":  ["2026-01-01", "2026-01-26", ...],                  # ASX Sydney
#     "XTSE":  ["2026-01-01", "2026-02-16", ...]                   # TSX Toronto
#   }
# }

# Custom exchange list
cal = SQLiteStore.get_market_calendar_context(exchanges=["XNYS", "XKRX", "XBOM"])

Coverage: full current year + next year (past and future holidays) — ~10–28 per exchange, negligible payload.

Default 20 exchanges (full G20 + Eurex): NYSE (XNYS), CME Futures (CME), LSE London (XLON), Xetra Frankfurt (XETR), Eurex (XEUR), Euronext Paris (XPAR), Borsa Italiana (XMIL), TSE Tokyo (XTKS), HKEX Hong Kong (XHKG), SSE Shanghai (XSHG), BSE Mumbai (XBOM), KRX Seoul (XKRX), ASX Sydney (XASX), TSX Toronto (XTSE), B3 São Paulo (BVMF), BMV Mexico City (XMEX), JSE Johannesburg (XJSE), Tadawul Saudi Arabia (XSAU), IDX Jakarta (XIDX), Borsa Istanbul (XIST). Excludes Russia (XMOS — IBKR suspended most Russian securities since 2022) and Argentina (XBUE — capital controls, very limited IBKR access).

100+ supported markets including XNAS (NASDAQ), XPAR (Euronext Paris), XKRX (Korea), XBOM (Bombay), SSE (Shanghai), BVMF (Brazil), and more — full list.

Used for:

  • Staleness checkget_trade_date_coverage() uses the NYSE calendar to determine if Flex data is current. newest == last_trading_day means fully up to date, regardless of whether today is a weekend or holiday.
  • System prompt injection — ClaudIA receives today's trading status, last/next trading day, and full-year holidays for all 20 exchanges at session start. This lets it reason about order timing, settlement windows, cross-regional volume effects, and upcoming closures proactively — without any API calls or gateway dependency.

Why not the IBKR API? The Client Portal API has a per-contract trading schedule endpoint but no standalone market holiday calendar. exchange_calendars is lighter, faster, and works offline.

Performance: Designed for zero marginal cost at scale.

Call Time
First call per process (cold) ~3.4s — exchange_calendars loads numpy arrays for 20 exchanges once
Subsequent calls same day 0.01ms — process-level date-keyed cache hit
Next day / process restart Recomputes fresh automatically

The cache key is (date_str, tuple(exchange_codes)) — stored in a module-level dict (_market_calendar_cache). It auto-invalidates when the date changes; no manual expiry, no TTL logic needed. Correct by construction.


Flex Import Integrity

verify_flex_import is a manifest-based integrity check that proves every tradeID in the source XML archives is present in SQLite. It does not analyse activity patterns — use check_flex_coverage for that.

How it works

Drive account_data/
  ClaudIA_Full_Activity_2024.xml  ← manual (pre-validated by user)
  flex_U123_2024-06-15_REF.xml   ← auto (archived by sync_flex_trades)
  flex_U123_2024-06-20_REF2.xml  ← auto
  1. Manual archives (ClaudIA_Full_Activity_*.xml) — registered in the manifest on first encounter with source='manual' and verified_at already set. Never re-verified — user confirmed integrity at import time.
  2. Auto-synced archives (flex_U*.xml) — manifest row written at sync time with SHA-256 and verified_at=now (tradeIDs were just upserted, import is verified by definition). On re-check: SHA-256 compared to manifest. If hash matches, the full tradeID scan is skipped — file unchanged since sync. Hash mismatch (or first encounter) triggers a full cross-check.

Import manifest — flex_import_log table

Column Description
filename Drive filename (unique per file)
sha256 SHA-256 of XML bytes at log time
trade_id_count Unique tradeIDs in the XML
raw_trade_count Total <Trade> elements — if raw != unique, within-file duplicate tradeIDs detected
source 'manual' or 'auto'
imported_at UTC timestamp of first log
verified_at UTC timestamp of last successful integrity check (NULL until first check)

What it catches

Condition Result
tradeID in XML but missing from SQLite ✗ N missing — re-import required
raw_count != unique_count ⚠ within-file duplicate tradeIDs — flagged transparently (should never occur from IBKR)
Drive file modified after sync Hash mismatch → full cross-check triggered automatically

What it does NOT do

  • Never modifies trade data — IBKR XML is the authority; SQLite is never "corrected" against anything other than a fresh pull
  • Gaps in trade-date coverage are not flagged — inactivity (holding a position) appears as a gap; that is correct data, not a coverage hole

ClaudIA integration

ClaudIA is a Panel-based trading assistant that imports ibkr_core_mcp directly as a Python package and drives it via ClaudeToolkit. If you want a ready-made conversational UI on top of this library, start there.


Development

# Unit tests (no IBKR connection needed)
pytest -m "not integration"

# All tests (requires running IBKR gateway + credentials)
pytest

# Lint + type check
ruff check .            # includes pydocstyle D — every public definition needs a docstring
ruff format --check .
mypy

Docstring coverage is enforced in CI: ruff's pydocstyle (D) rules are enabled, so a new public module, class, method, function, or __init__ without a docstring fails the lint. Formatting-opinion codes (imperative mood, trailing periods, and similar) are deliberately disabled — see the annotated ignore list in pyproject.toml for what is off and why.


License

MIT

About

Standalone pip-installable Python package: IBKR Client Portal API, Drive cache, SQLite store, backtest sandbox, indicators, analytics, Claude tools, PineScript generation

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages