Architecture¶
The same Python package runs in both modes. Only the host of each component changes.
Components¶
| Component | Role | Code |
|---|---|---|
| IB Gateway | IBKR's API endpoint. The bot talks to it over the TWS socket API (ib_async). |
external |
| Scheduler | Every 15 minutes: "is it a trading slot, and has this slot already run?" | scripts/scheduled_run.py |
| Trader run | Reconcile → price → kill switch → Claude → gate → execute | guardrail_trader/pipeline.py → run_once(), CLI scripts/run_bot.py |
| Agent | The Claude loop and its tools | guardrail_trader/agent.py |
| Risk gate | Pure functions that approve or block each proposal | guardrail_trader/risk/gate.py |
| Journal | SQLite: ledger, runs, proposals, orders, snapshots, LLM usage | guardrail_trader/journal.py |
| Dashboard | Read-only web page over the journal | scripts/dashboard.py |
| Gateway watchdog | Restarts IB Gateway when runs keep failing for gateway reasons (host, Docker mode) | scripts/gateway_watchdog.py |
| Keep-awake | Stops the Mac idle-sleeping on trading nights | scripts/keep_awake.py |
Native mode¶
flowchart TB
subgraph mac[macOS host]
subgraph la[launchd LaunchAgents]
SCH[com.guardrail-trader.scheduler<br/>every 15 min]
DSH[com.guardrail-trader.dashboard<br/>KeepAlive]
AWK[com.guardrail-trader.awake<br/>18:00 local by default]
end
GW[IB Gateway app<br/>127.0.0.1:4002]
SCH --> RUN[.venv python<br/>run_bot.py]
RUN <--> GW
RUN --> DB[(data/journal.sqlite)]
DSH --> DB
AWK --> CAF[caffeinate -i -s]
end
GW <--> IBKR[(IBKR servers)]
RUN <--> LLM[(Claude API)]
YOU[Browser] --> DSH
| What | Runs as |
|---|---|
| IB Gateway | Desktop app; you log in (paper) and it keeps a session. |
| Scheduler + trader | .venv/bin/python scripts/scheduled_run.py, started by launchd every 900 s |
| Dashboard | .venv/bin/python scripts/dashboard.py --no-browser, restarted by launchd if it exits |
| Keep-awake | .venv/bin/python scripts/keep_awake.py → caffeinate until 16:00 New York |
| Journal | data/journal.sqlite file |
Docker mode¶
flowchart TB
subgraph mac[macOS host]
AWK[launchd: com.guardrail-trader.awake] --> CAF[caffeinate]
subgraph dd[Docker Desktop VM · compose project guardrail-docker]
GW[ib-gateway<br/>IB Gateway + IBC<br/>socat :4004 → :4002]
BOT[bot<br/>supercronic → scheduled_run.py]
DSH[dashboard<br/>:8765]
BOT <--> GW
end
DATA[(./data bind mount<br/>journal.sqlite)]
BOT --> DATA
DSH --> DATA
end
GW <--> IBKR[(IBKR servers)]
BOT <--> LLM[(Claude API)]
YOU[Browser → 127.0.0.1:8765] --> DSH
| Service | What it runs | Ports (host, localhost only) |
|---|---|---|
ib-gateway |
gnzsnz/ib-gateway image: IB Gateway + IBC auto-login, daily restart | 14002 (API, diagnostics), 15900 (VNC) |
bot |
supercronic runs scheduled_run.py every 15 min (docker/crontab); healthcheck on the scheduler heartbeat |
none |
dashboard |
dashboard.py bound to 0.0.0.0 inside the container; no API key in its environment |
8765 |
Networks are split so that only the bot can reach the IB Gateway API, which has no authentication of its own:
| Network | Members | Purpose |
|---|---|---|
broker (internal, no route out) |
ib-gateway, bot |
the IB API on port 4004 |
gateway_egress |
ib-gateway |
IBKR servers, localhost-only host ports |
bot_egress |
bot |
Claude API |
web |
dashboard |
127.0.0.1:8765 on the host |
data/, logs/ and config/ (read-only) are bind-mounted from the project folder, so the journal
is the same file in both modes.
Why the port is 4004, not 4002
IB Gateway only accepts API connections from localhost. Inside the image, socat forwards
0.0.0.0:4004 to 127.0.0.1:4002, so the bot container can connect while the gateway still
sees localhost. The bot's paper/live port check treats 4004 as paper and 4003 as live
(config.py).
Data flow of one run¶
- Broker ready? the gateway must actually serve account data (a connected socket isn't enough).
- Reconcile: journal holdings must equal IBKR positions, with no stray open orders.
- Price: the whole universe plus FX to the base currency.
- Kill switch: if drawdown from peak ≥ the limit, sell everything and halt.
- Claude: the agent loop, only if the market is open and the monthly LLM cap has room.
- Gate: final, authoritative check of every proposal.
- Execute: place approved limit orders, wait for fills, cancel leftovers, book fills.
Every step writes to the journal, which the dashboard reads.