An LLM-native algorithmic trading platform for Interactive Brokers
MMR is a Python trading platform built to be operated by both humans and LLMs. It connects to Interactive Brokers via ib_async, uses ZeroMQ for inter-service messaging, DuckDB for storage, and exposes every operation as a JSON-returning CLI command — making it a natural fit for LLM agents that trade autonomously.
Most trading platforms are built for humans staring at charts. MMR is built for an LLM sitting in a loop:
- Propose → Review → Approve pipeline — the LLM never places a trade directly. It creates proposals with confidence scores and reasoning, reviews the auto-computed position sizing, checks portfolio risk, then approves or rejects. A human can review pending proposals at any time.
- Every command returns JSON (
--jsonflag) — structured data the LLM can parse without scraping Rich tables. - ATR-inverse position sizing — the LLM doesn't need to reason about how much to buy. Volatile stocks automatically get smaller positions; stable stocks get larger ones. The sizing pipeline:
base × risk_multiplier × confidence × ATR_volatility_adjustment. - Portfolio snapshots & diffs — compact (~500 token) state summaries designed for context-window efficiency. The LLM calls
portfolio-snapshotevery cycle andportfolio-diffto see what changed — if nothing moved, it skips analysis entirely. - Risk reports before and after —
portfolio-riskreturns concentration (HHI), group budget compliance, correlation clusters, and plain-English warnings. Run it before proposing to catch existing issues; run it after to catch issues the new trade would create. - Position groups with budgets — tag trades into named groups (e.g. "mining" at 20% max allocation) so the LLM can manage sector exposure without manual tracking.
The fastest way to get running is Docker — one command builds the image, starts IB Gateway, prompts for your credentials, and SSH's you in:
git clone https://github.com/9600dev/mmr.git
cd mmr
./docker.sh -g./docker.sh -g handles everything: builds the Docker image, prompts for your IB username/password/account, writes credentials to .env (gitignored), starts the IB Gateway sidecar + MMR container, and drops you into an SSH session. From there, run ./start_mmr.sh to launch all services.
# Once inside the container:
./start_mmr.sh # Paper trading (default)
./start_mmr.sh --live # Live trading
./start_mmr.sh --cli # CLI only (services already running)./docker.sh -u # Start containers
./docker.sh -d # Stop containers
./docker.sh -s # Sync code to running container
./docker.sh -e # SSH into container
./docker.sh -l # Tail logs
./docker.sh -c # Clean images/volumes
./docker.sh -i # IB Gateway only (for local dev)pip install -e ".[test]"
./start_mmr.sh --setup # Interactive wizard: IB connection, accounts, API keys
./start_mmr.sh # Start servicesOn first run, start_mmr.sh auto-launches the setup wizard to configure IB Gateway host/port, account numbers, Massive.com API key, and trading mode. Re-run anytime with ./start_mmr.sh --setup. Settings are saved to ~/.config/mmr/trader.yaml.
- Python >= 3.12
- Interactive Brokers account with IB Gateway or TWS
- Market data subscriptions for target exchanges
┌─────────────────────────────────────────────────────────────────────┐
│ Claude Code / LLM Agent │
│ CLAUDE.md (context) + mmr CLI --json (tools) │
│ MONITOR → ANALYZE → PROPOSE → DIGEST → sleep → repeat │
└────────────────────────────┬────────────────────────────────────────┘
│ Bash: mmr --json <command>
┌────────────────────────────▼────────────────────────────────────────┐
│ mmr_cli / sdk.py │
│ 80+ commands, JSON output, prompt_toolkit REPL │
└──────┬──────────────────┬──────────────────┬────────────────────────┘
│ ZMQ RPC │ ZMQ RPC │ ZMQ PubSub
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
│trader_service│ │ data_service │ │ strategy_service │
│ port 42001 │ │ port 42003 │ │ port 42005 │
│ │ │ │ │ │
│ IBAIORx ─────┼──┼──► IB Gateway│ │ Strategy runtime │
│ Executioner │ │ Massive.com │ │ RxPY pipelines │
│ Portfolio │ │ DuckDB │ │ MessageBus (42006) │
│ Risk Gate │ │ │ │ │
│ Book Subject │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────────────┘
│ │
│ ┌──────────────┐ │
└──────────┤ IB Gateway ├─────────────┘
│ (ib_async) │
└──────────────┘
| Service | Port | Role |
|---|---|---|
| trader_service | 42001 (RPC), 42002 (PubSub) | Trading runtime — order execution, portfolio, market data streaming, risk gate. Detects IB Gateway upstream connectivity loss and exposes it via status |
| data_service | 42003 (RPC) | Historical data downloads (Massive.com + IB), DuckDB storage |
| strategy_service | 42005 (RPC), 42006 (MessageBus) | Loads and runs strategies, receives tick streams, emits signals. Reconciliation loop (30s) auto-detects new strategies and portfolio changes |
| mmr_cli | — | Interactive REPL or one-shot commands, connects to services via ZMQ |
All inter-service communication uses ZeroMQ with msgpack serialization — no HTTP, no web frameworks. Three patterns:
- RPC (DEALER/ROUTER) — synchronous request/reply. CLI calls
trader_servicemethods via@rpcmethoddecorated handlers.RPCClient[T]uses__getattr__chaining so calls look likeclient.rpc().place_order(...). Server-side exceptions are preserved across the wire — stdlib types reconstruct as themselves, custom types go through a caller-suppliederror_table, and unknown types surface asRPCErrorthat still carries the original type name and args. - PubSub (PUB/SUB) — one-way broadcast of live ticker data. Topic filtering at the socket level, zero server-side bookkeeping. Ideal for high-frequency market data.
- MessageBus (DEALER/ROUTER with subscription tracking) — targeted routing for strategy signals. The server knows who subscribes to which topics and routes accordingly.
The msgpack EXT_OBJECT fallback uses dill, which can execute arbitrary Python on untrusted input. Set MMR_DILL_STRICT=1 to refuse the fallback entirely, or call set_dill_whitelist([Type1, Type2, ...]) to allow only specific classes.
Every query runs through a per-database lock that opens, executes, and closes the connection atomically (execute_atomic, or execute(query, params, fetch='all'|'one'|'df'|'none') for the common case). This lets multiple services share the same file without leaking connections or interleaving writes. Tables:
- tick_data — time-series OHLCV bars
- object_store — dill-serialized Python objects
- event_store — trading event audit trail (signals, orders, fills, rejections)
- proposal_store — trade proposals with enforced state-machine transitions (PENDING → APPROVED → EXECUTED; terminal states immutable)
- position_groups — named groups with allocation budgets
| Source | Coverage | Speed | Use Case |
|---|---|---|---|
| Massive.com (Polygon.io) | US equities | ~4s full scan | Primary for US — server-side indicators, batch snapshots, fundamentals, news with sentiment |
| IB APIs | International (ASX, TSE, SEHK, EU, etc.) | ~30-90s | Scanner, snapshots, reqHistoricalData. Falls back here for non-US markets |
Yahoo Finance is explicitly not used.
MMR is designed to be operated by Claude Code as an autonomous trading agent. The CLAUDE.md file provides Claude with complete platform context — architecture, all 80+ CLI commands, the propose/approve pipeline, risk management, and the LLM trading loop workflow. Claude interacts with MMR entirely through the mmr CLI with --json output via Bash.
Claude Code reads CLAUDE.md on startup, giving it full knowledge of the platform. It operates MMR by running CLI commands:
# Claude runs these via Bash tool
mmr --json portfolio-snapshot # ~500 tokens — compact state
mmr --json portfolio-diff # what changed since last check
mmr --json ideas momentum # scan for opportunities
mmr --json propose AAPL BUY --market --confidence 0.7 --group tech \
--reasoning "Strong momentum, RSI 65"
mmr --json portfolio-risk # check risk before approving
mmr approve 42 # execute the tradeNo special SDK or helper library needed — Claude Code's Bash tool is the entire integration surface.
The recommended workflow for autonomous trading follows a phased cycle:
MONITOR → ANALYZE → PROPOSE → DIGEST → (sleep) → repeat
MONITOR — portfolio-snapshot + portfolio-diff. If nothing moved (all positions unchanged), skip to DIGEST. ~500 tokens per cycle.
ANALYZE — portfolio-risk for warnings, ideas with rotating presets (momentum → mean-reversion → breakout → volatile → gap-down), news for held positions.
PROPOSE — propose with auto-sizing, group tagging, confidence scores, and reasoning. Never auto-executes — proposals wait for review.
DIGEST — save observations to Claude Code memory, sleep until next cycle.
The loop can be run using Claude Code's /loop command or by asking Claude to monitor on an interval. Claude can be interrupted at any time to approve/reject proposals, answer questions, or adjust strategy.
Every command in the loop is designed for minimal token usage:
| Command | Tokens | Purpose |
|---|---|---|
portfolio-snapshot |
~500 | Total value, P&L, top movers |
portfolio-diff |
~200-500 | Only what changed since last cycle |
portfolio-risk |
~800 | HHI, warnings, group budgets |
session |
~300 | Remaining capacity, sizing config |
ideas momentum |
~1000 | Top 10 ranked opportunities |
Compare to portfolio which returns ~2000+ tokens of full position detail. The snapshot/diff commands exist specifically so the LLM can monitor efficiently without burning context.
The LLM (or human) never places trades directly. Instead:
# 1. Create a proposal with reasoning
mmr propose AAPL BUY --market --confidence 0.7 --group tech \
--reasoning "Strong momentum, RSI 65, above 200-day MA"
# 2. Review — check auto-computed sizing
mmr proposals show 42
# Shows: base $5K × 1.0 risk × 0.7 confidence × 0.85 ATR_adj = $2,975
# 3. Check risk impact
mmr portfolio-risk
# 4. Approve or reject
mmr approve 42
mmr reject 42 --reason "Group over budget"Position sizing is automatic: base_position × risk_multiplier × confidence_scale × ATR_volatility_adjustment. Volatile stocks (high ATR%) get smaller positions; stable stocks get larger ones. Configured in config_defaults/position_sizing.yaml.
Proposal statuses follow a strict state machine: PENDING → APPROVED | REJECTED | EXPIRED | FAILED, then APPROVED → EXECUTED | FAILED | REJECTED. Terminal statuses are immutable — a proposal can't be executed twice, resurrected after rejection, or approved after it's already been executed. Illegal transitions raise InvalidProposalTransition instead of silently clobbering the row.
The mmr CLI is an interactive REPL (via prompt_toolkit) or accepts one-shot commands. All commands support --json for machine-readable output.
portfolio # Positions with P&L
portfolio-snapshot # Compact JSON (~500 tokens) — alias: psnap
portfolio-diff # Delta since last snapshot — alias: pdiff
portfolio-risk # Concentration, correlation, group budgets — alias: prisk
orders # Open orders
status # Service health + PnL
session # Sizing config, remaining capacitybuy AMD --market --amount 100.0
buy EUR --sectype CASH --market --quantity 20000
sell AMD --market --quantity 10
cancel 123 # Cancel order by ID
cancel-all # Cancel all orders
close 1 # Close position by row numberpropose AMD BUY --market --quantity 100 --bracket 180 150
propose AAPL BUY --limit 165 --amount 5000 --trailing-stop-pct 2.0
propose BHP BUY --market --confidence 0.7 --group mining --exchange ASX --currency AUD
proposals # List pending
proposals --all # All statuses
proposals show 3 # Full detail
approve 3 # Execute
reject 3 --reason "Thesis changed"group list # Groups with members + allocation %
group create mining --budget 20
group add mining BHP RIO FMG
group set mining --budget 25
group show mining
group remove mining BHP
group delete mining# US via Massive.com (~4s)
ideas # Momentum (default)
ideas gap-up # Gap-up preset
ideas mean-reversion # Mean-reversion preset
ideas breakout # Breakout preset
ideas volatile # Volatile/scalping
ideas momentum --tickers AAPL MSFT AMD NVDA
ideas momentum --universe sp500
ideas momentum --fundamentals --news --detail
# International via IB (~30-90s)
ideas momentum --location STK.AU.ASX --tickers BHP CBA CSL
ideas gap-up --location STK.HK.SEHK --tickers 0700 0005
# IB scanner
scan # Top gainers (default)
scan losers / scan active / scan hot-volume
scan --instrument ETF --location STK.US
# Market movers
movers # Stock gainers
movers --market crypto --losersresolve AMD # Symbol → conId
resolve EURUSD --sectype CASH
snapshot AMD # Price snapshot
depth AAPL # Level 2 + PNG chart
depth BHP --exchange ASX --currency AUD
depth AAPL --rows 10 --smart
listen AMD # Stream live ticks
watch # Live portfolio monitor
stream AAPL MSFT AMD # Massive.com stream
stream EURUSD --feed forex --quoteshistory list # Downloaded data inventory
history massive --symbol AAPL --bar_size "1 day" --prev_days 30
history ib --symbol AAPL --bar_size "1 min" --prev_days 5options expirations AAPL
options chain AAPL --expiration 2026-03-20 --type call
options chain AAPL -e 2026-03-20 --strike-min 200 --strike-max 250
options snapshot O:AAPL260320C00250000
options implied AAPL -e 2026-03-20
options buy AAPL -e 2026-03-20 -s 250 -r C -q 5 --marketnews AAPL --detail # Headlines + sentiment
movers # Market moversforex snapshot EURUSD
forex quote EUR USD
forex snapshot-all
forex movers / forex movers --losers
forex convert EUR USD 1000universe list # All universes with counts
universe show sp500
universe create my_universe
universe add my_universe AAPL MSFT AMD
universe import my_universe symbols.csv
universe remove my_universe MSFT
universe delete my_universestrategies # List deployed strategies
strategies inspect # AST scan of strategies/: classes, dispatch mode, tunable params
strategies enable my_strat
strategies disable my_strat
strategies reload # Reload YAML config + re-subscribe instrumentsresize-positions --max-bound 500000 # Trim to $500K
resize-positions --min-bound 300000 # Grow to $300K
resize-positions --max-bound 500000 --dry-run# Discover what's backtest-able and what knobs exist (AST scan, no execution)
strategies inspect
# Single run — summary-only is the default (no multi-MB trade arrays in the response)
backtest -s strategies/keltner_breakout.py --class KeltnerBreakout --conids 756733 --days 365
backtest -s ... --param EMA_PERIOD=15 --param BAND_MULT=2.5 # override class attrs
backtest -s ... --params '{"EMA_PERIOD": 15, "BAND_MULT": 2.5}' # JSON form
# Single-strategy parameter sweep — cartesian product, composite-score leaderboard
bt-sweep -s strategies/orb.py --class OpeningRangeBreakout --conids 756733 --days 365 \
--grid '{"RANGE_MINUTES":[15,30,45],"VOLUME_MULT":[1.2,1.3,1.5]}'
# Declarative, cron-able nightly sweep (multi-strategy)
sweep run ~/mmr-sweeps/nightly.yaml --dry-run # expand + estimate wall time
sweep run ~/mmr-sweeps/nightly.yaml # kick off
sweep list # history of past sweeps
sweep show 7 # leaderboard of sweep #7 + digest path
# Review results
backtests # ranked by composite quality score
backtests --sweep 7 # runs from a specific sweep
backtests confidence 42 43 44 # compact PSR/t-test/CI for N runs
backtests show 42 # full detail + statistical-confidence block
backtests archive 42 43 # soft-delete; reversible via `unarchive`Every run is persisted with its strategy source hash, params, conids, date range, 12+ summary metrics (return, Sharpe, Sortino, Calmar, profit factor, expectancy bps, max drawdown, time-in-market, etc.), the full trade list + equity curve, and — on backtests show / confidence — statistical-confidence tests (Probabilistic Sharpe Ratio, t-test, bootstrap CIs, P&L skew/kurtosis, Monte-Carlo losing-streak test) that answer "is this edge real or noise?" beyond what the headline metrics tell you.
Nightly sweeps drop a markdown digest to ~/.local/share/mmr/reports/sweep_<id>_<name>_<ts>.md with strong-candidate leaderboards, risk flags (negative skew + fat tails), and pointers back into the CLI. Cron-compatible — 0 2 * * * mmr sweep run ~/mmr-sweeps/nightly.yaml runs at 2am and you wake up to a verdict.
Subclass Strategy and implement on_prices():
from trader.trading.strategy import Strategy, Signal
from trader.objects import Action
class MyStrategy(Strategy):
def on_prices(self, prices):
# prices: DataFrame with open, high, low, close, volume, vwap (DatetimeIndex)
if some_buy_condition(prices):
return Signal(source_name=self.name, action=Action.BUY, probability=0.8)
return NoneRegister in config_defaults/strategy_runtime.yaml:
strategies:
- name: my_strategy
module: strategies/my_strategy.py # must live under strategies_directory
class_name: MyStrategy
bar_size: "1 min"
conids: [265598] # Verify with: mmr resolve AAPL
historical_days_prior: 5The strategy loader is sandboxed: the resolved module path must live under strategies_directory. Absolute paths or ../ traversal are rejected, and the YAML config is parsed with yaml.safe_load so Python-object tags (!!python/object/apply:...) cannot be used to execute arbitrary code. Each strategy gets a unique sys.modules key derived from its name, so two strategies sharing a filename (e.g. strategies/a/shared.py and strategies/b/shared.py) don't clobber each other and a reload actually re-imports fresh source.
MMR has multiple layers of risk controls:
- Risk Gate (
risk_gate.py) — pre-trade enforcement of max position size, daily loss limit, open order count, and signal rate limits via event store lookback - Position Sizing (
position_sizing.py) — ATR-inverse volatility adjustment, confidence scaling, liquidity checks (max 2% of ADV, spread penalty) - Portfolio Risk (
portfolio_risk.py) — gross + signed exposure breakdown (gross_exposure_pct,net_exposure_pct,long_exposure_pct,short_exposure_pct), HHI concentration on gross weights, position warnings (>10% warning, >15% critical), group budget compliance, and direction-aware correlation cluster detection (a correlated long/short pair nets to zero and correctly fires no cluster warning; a stacked long/long cluster fires at >30% combined net exposure). The summary explicitly notes "hedged" when gross and |net| diverge. - Trading Filter (
trading_filter.py) — symbol/exchange denylist and allowlist enforcement - Position Groups (
position_groups.py) — named groups with allocation budgets (e.g. "mining" at 20% max) - Proposal State Machine — terminal states (
EXECUTED,REJECTED,EXPIRED,FAILED) are immutable; re-approving an executed proposal or resurrecting a rejected one raisesInvalidProposalTransition - Bracket Order Transactionality —
BRACKETexits are all-or-nothing: entry is staged withtransmit=False, TP and SL are placed before SL'stransmit=Trueflushes the whole bundle. If any leg fails, earlier staged legs are cancelled before any market exposure exists.
User configs live in ~/.config/mmr/ (auto-copied from config_defaults/ on first run):
| File | Purpose |
|---|---|
trader.yaml |
IB connection, DuckDB path, ZMQ ports |
position_sizing.yaml |
Base size, risk level, ATR params, hard limits |
trading_filters.yaml |
Symbol/exchange denylist and allowlist |
strategy_runtime.yaml |
Strategy definitions (module, class, conids, bar_size) |
pycron.yaml |
Service scheduling and auto-restart |
logging.yaml |
Rich console + rotating file handlers |
Environment variables override config values (uppercased parameter name). The TRADER_CONFIG env var overrides the config file path.
The IB Gateway container (ghcr.io/gnzsnz/ib-gateway) is configured via environment variables in docker-compose.yml and .env:
| Variable | Default | Purpose |
|---|---|---|
TWS_SETTINGS_PATH |
/home/ibgateway/tws_settings |
Persist runtime settings (auto-restart state, sessions) to a bind mount without overwriting Jts template files |
BYPASS_WARNING |
yes |
Auto-dismiss precaution dialogs that IBC can't handle (e.g. "restart now?" prompt) |
SAVE_TWS_SETTINGS |
"08:00 17:00" |
Periodic settings saves — without this, settings only persist on clean shutdown |
AUTO_RESTART_TIME |
11:59 PM |
IB Gateway daily restart time (avoids 2FA re-authentication) |
VNC_SERVER_PASSWORD |
(from .env) |
VNC password for gateway GUI access at vnc://localhost:5901 |
See the ib-gateway-docker README for the full list of environment variables.
mmr/
├── trader/ # Core library
│ ├── trader_service.py # Trading runtime entry point
│ ├── strategy_service.py # Strategy runtime entry point
│ ├── data_service.py # Data service entry point
│ ├── mmr_cli.py # CLI REPL (80+ commands)
│ ├── sdk.py # ZMQ RPC client wrapper
│ ├── container.py # DI container
│ ├── config.py # Typed config dataclasses
│ ├── objects.py # Domain enums (Action, BarSize, etc.)
│ ├── common/ # Logging, helpers, RxPY utilities
│ ├── data/ # DuckDB stores, universe, market data
│ ├── listeners/ # IB + Massive.com data adapters
│ ├── messaging/ # ZMQ RPC, PubSub, MessageBus
│ ├── trading/ # Runtime, executioner, risk, sizing, proposals
│ ├── strategy/ # Strategy runtime
│ ├── simulation/ # Backtester + statistical-confidence tests + lookahead checker
│ └── tools/ # Idea scanner, depth chart, options chain
├── strategies/ # User strategy implementations
├── config_defaults/ # Bundled defaults
├── skills/ # Claude skills (mmr, mmr-loop, news)
├── CLAUDE.md # Claude Code context (architecture, commands, workflows)
├── tests/ # 1000+ tests (pytest, no IB required)
├── docker-compose.yml # IB Gateway + MMR containers
├── Dockerfile # Debian bookworm + Python venv
├── docker.sh # Docker/Podman build helper
├── start_mmr.sh # Service startup (tmux + health checks)
└── pyproject.toml # Package config + 58 dependencies
All tests are unit tests using temporary DuckDB databases — no IB connection required. The suite runs in ~47s with zero failures:
pytest tests/ --timeout=30 -q --ignore=tests/test_ibrx_async.py
# → 880 passedtest_ibrx_async.py is excluded because it spins up long-lived asyncio tasks that flake in the full suite; it still runs cleanly on its own.
Coverage highlights:
- Core logic:
test_backtester*,test_backtest_stats,test_backtest_params,test_backtest_store,test_sweep,test_position_sizing,test_portfolio_risk,test_risk_gate,test_idea_scanner - Stores:
test_duckdb_store(including concurrent-writer),test_event_store,test_proposal_store(state machine),test_position_groups - Messaging:
test_clientserver_rpc(error-type preservation, dill policy, threaded round-trip) - Runtime:
test_trading_runtime(PnL race, portfolio routing, bracket rollback),test_executioner(filter + gate rejection paths),test_strategy_runtime_reconcile(sandbox,yaml.safe_load, partial-write mtime recovery) - Integration:
test_propose_approve_integration(end-to-end propose → approve → execute, plus failure paths) - Correctness:
test_backtester.py::test_no_lookahead_fill_at_next_bar_openhand-crafts bars that prove fills come from bart+1's open, not bart's close
Core: ib_async, duckdb, pyzmq, msgpack, reactivex, pandas, numpy, pyarrow, rich, massive (Polygon.io), scikit-learn, vectorbt, exchange-calendars, matplotlib
Full list in pyproject.toml. Install with pip install -e .
Fair-code: Apache 2.0 with Commons Clause.
