Skip to content

Commit 534f693

Browse files
committed
feat(livetrading) - adding ibkr and collective2
1 parent 6d852d8 commit 534f693

37 files changed

Lines changed: 1331 additions & 199 deletions
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Plan: Make IB livetrade copier cron-friendly
2+
3+
## Context
4+
5+
The copier was just verified end-to-end against an IB paper account during pre-market hours. It connects, sizes orders correctly, and submits them — but the run revealed four issues that make it unusable as a scheduled cron job:
6+
7+
1. **Per-order 30s blocking wait** ([interactive_brokers.py:131-138](tradingbot/livetrade/interactive_brokers.py#L131-L138)). With ~40 sells, that's ~20 minutes of dead time during which IB just queues the order anyway. Outside RTH the wait is purely wasted — the order can't possibly fill until next open.
8+
2. **Misleading "Executed" log** ([copier.py:169](tradingbot/livetrade/copier.py#L169)). Logs `Executed SELL …` purely on no-exception, so `PreSubmitted` (queued for next open) and `Cancelled` (e.g. fractional rejected) both look like success in the run output.
9+
3. **Fractional shares rejected** by IB API (error 10243): `VSNT 2.16 shares: Fractional-sized order cannot be placed via API`. Anything that came in via DRIP or corporate action will fail.
10+
4. **No de-duplication across cron runs.** Cron is scheduled post-close ([cronjob-livetrade-interactivebrokers.yaml schedule `20 21 * * 1-5`](helm/tradingbots/templates/cronjob-livetrade-interactivebrokers.yaml)); orders queue as `PreSubmitted` until next open. If the next cron fires (e.g. weekend → Monday) before yesterday's queued orders have filled, the diff still sees the position and submits a *second* order. Duplicates.
11+
12+
The intended cron model is: post-close run computes target diff, submits market orders, IB queues them, they fill at next open. The code needs to match that model.
13+
14+
## Approach
15+
16+
Adopt **submit-and-walk-away with idempotent re-submission**:
17+
18+
- IB's `place_order` fires the order and returns immediately. No 30s wait.
19+
- Before each sync, cancel any open orders we previously submitted (from prior runs that haven't filled). Then re-issue against the latest target. This makes each cron run self-correcting — yesterday's queued sell of 10 shares is replaced by today's queued sell of 12 if the target changed, without doubling up.
20+
- Stocks must be integer-quantity; fractional residuals are rounded down with a warning. Crypto/forex keep their existing precision.
21+
- Logs say what actually happened: `Submitted` (with status), not a fake `Executed`.
22+
23+
This keeps the cron container exit time bounded (~seconds, not minutes) and makes successive runs idempotent.
24+
25+
## Files to modify
26+
27+
### 1. [tradingbot/livetrade/broker.py](tradingbot/livetrade/broker.py)
28+
29+
Add a non-abstract default-no-op method to `LiveBroker`:
30+
31+
```python
32+
def cancel_open_orders(self) -> int:
33+
"""Cancel any orders this broker session has previously submitted that
34+
are still open. Override per-broker. Returns count cancelled."""
35+
return 0
36+
```
37+
38+
C2 inherits the no-op (REST: orders submitted are immediately accepted or rejected; no working-order concept on our side).
39+
40+
### 2. [tradingbot/livetrade/interactive_brokers.py](tradingbot/livetrade/interactive_brokers.py)
41+
42+
**Replace** `place_order` ([interactive_brokers.py:119-146](tradingbot/livetrade/interactive_brokers.py#L119-L146)):
43+
44+
- Remove the `while not trade.isDone()` loop and 30s timeout.
45+
- For `STK` contracts, round quantity to `int(quantity)` (floor, not banker's-round). If 0, log warning and return without submitting.
46+
- Submit via `ib.placeOrder`. Single short `ib.sleep(0.3)` to let the first ack come back, then log final status (`PendingSubmit`/`PreSubmitted`/`Submitted`/`Filled`/`Cancelled`).
47+
- No exception on `Cancelled` — IB cancellation includes valid rejections (e.g. fractional 10243); the next run will re-evaluate.
48+
49+
**Add** `cancel_open_orders` override:
50+
51+
```python
52+
def cancel_open_orders(self) -> int:
53+
self.connect(readonly=False)
54+
open_trades = [t for t in self.ib.openTrades() if not t.isDone()]
55+
for t in open_trades:
56+
self.ib.cancelOrder(t.order)
57+
if open_trades:
58+
self.ib.sleep(1) # let cancellations propagate
59+
return len(open_trades)
60+
```
61+
62+
Note: `ib.openTrades()` returns trades visible to *this* clientId only (default behavior unless a master client ID is configured), so this won't touch orders the user placed manually with a different ID. Safe.
63+
64+
### 3. [tradingbot/livetrade/copier.py](tradingbot/livetrade/copier.py)
65+
66+
In `sync()`, after computing target weights and before calling `_execute_orders`:
67+
68+
```python
69+
cancelled = self.broker.cancel_open_orders()
70+
if cancelled:
71+
logger.info(f"Cancelled {cancelled} stale open orders before sync")
72+
```
73+
74+
In `_execute_orders` ([copier.py:158-183](tradingbot/livetrade/copier.py)):
75+
76+
- Change the per-order log from `Executed {side}` to `Submitted {side}` to match reality. The broker logs the actual order status separately.
77+
78+
The existing `LIVETRADE_SETTLE_DELAY_SECONDS` between sells and buys stays — it's only meaningful intraday, and the dry-run/empty-batch guard already makes it a no-op when there are no sells.
79+
80+
### 4. [tests/test_livetrade_ib.py](tests/test_livetrade_ib.py)
81+
82+
Update `test_ib_place_order`:
83+
84+
- The test currently mocks `trade.isDone() == True` and expects the loop to exit. With the loop gone, the test needs to be relaxed — just assert `ib.placeOrder` was called with the right contract+order, drop the orderStatus assertions about `Filled`.
85+
86+
Add `test_ib_place_order_floors_fractional_stock`:
87+
88+
- Quantity `2.16`, `STK` contract → `placeOrder` called with `totalQuantity == 2`. Quantity `0.4``placeOrder` NOT called (warned and skipped).
89+
90+
Add `test_ib_cancel_open_orders`:
91+
92+
- Mock `ib.openTrades()` to return two non-done trades; assert `cancelOrder` called twice and method returns `2`.
93+
94+
Existing 8 tests stay valid.
95+
96+
## Files NOT changing
97+
98+
- [tradingbot/livetrade/collective2.py](tradingbot/livetrade/collective2.py) — REST broker, no fractional / no working-order concept. Inherits the no-op `cancel_open_orders`.
99+
- [tradingbot/livetrade_interactive_brokers.py](tradingbot/livetrade_interactive_brokers.py) entry point — copier orchestrates everything.
100+
- Helm cronjobs / values — same env vars, same schedule. The fix is purely behavioral.
101+
102+
## Risks
103+
104+
1. **`ib.openTrades()` scope** — if the user runs Gateway with a Master Client ID configured, it will see *every* order in the account, including manual ones. Document this in [docs/guides/live-trading.md](docs/guides/live-trading.md) under the IB section: "Run the cron and any other scripts with distinct clientIds and **without** Master Client ID set, otherwise `cancel_open_orders` may cancel manual orders too." Code-side, we could filter by `t.order.clientId == self.client_id` for belt-and-braces; cheap, do it.
105+
106+
2. **Cancel race** — between cancelling and re-submitting, a partial fill could occur. For market orders queued outside RTH this is essentially impossible. Inside RTH it's possible but unlikely in the 1s window. Acceptable.
107+
108+
3. **Fractional floor loses value**`2.16 → 2` strands `0.16` shares. The user must liquidate fractional residuals manually via desktop. Surface a clear log line so they know.
109+
110+
## Verification
111+
112+
1. `POSTGRES_URI=… PYTHONPATH=. uv run pytest tests/test_livetrade.py tests/test_livetrade_ib.py -q` — all pass (existing 8 + 2 new).
113+
2. **Local dry-run** via VSCode "LiveTrade: Interactive Brokers (paper)" with `LIVETRADE_DRY_RUN=true` — confirm log says `[DRY RUN] Would SELL …` and exits in seconds, not minutes.
114+
3. **Live paper run** during after-hours with `LIVETRADE_DRY_RUN=false`:
115+
- First run: confirm 40 orders submit in ~10s total (no 30s waits). Log shows `Submitted SELL …` lines plus broker-side `PreSubmitted` status. Container exits cleanly.
116+
- Second run (immediately after first): confirm log says `Cancelled N stale open orders before sync`, then orders are re-submitted with up-to-date sizes. Final IB account state shows N working orders, not 2N.
117+
- Confirm fractional `VSNT 2.16` is rounded to `2` and submits cleanly (no error 10243).
118+
4. **Next-day check** (after market opens): orders should fill. Confirm subsequent cron run sees the new positions and computes new diffs correctly.

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,12 @@ COLLECTIVE2_SYSTEM_ID=155809898
3838
LIVETRADE_BOT_WEIGHTS={"AdaptiveMeanReversionBot": 1.0}
3939
LIVETRADE_COPY_OPEN_TRADES=true
4040
LIVETRADE_MIN_ORDER_USD=50
41-
LIVETRADE_DRY_RUN=false
41+
LIVETRADE_DRY_RUN=false
42+
43+
# Interactive Brokers (IB Gateway). See docs/guides/live-trading.md
44+
# Paper account ID looks like DU1234567; live looks like U1234567.
45+
# IB_CLIENT_ID must be unique per concurrent connection (cron uses 17, vscode debug uses 18).
46+
IB_GATEWAY_HOST=localhost
47+
IB_GATEWAY_PORT=4002
48+
IB_CLIENT_ID=17
49+
IB_ACCOUNT_ID=

.vscode/launch.json

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
"program": "${file}",
1212
"console": "integratedTerminal",
1313
"cwd": "${workspaceFolder}/tradingbot/",
14-
"envFile": "${workspaceFolder}/.env"
14+
"envFile": "${workspaceFolder}/.env",
15+
"env": {
16+
"PYTHONPATH": "${workspaceFolder}/tradingbot"
17+
}
1518
},
1619
{
1720
"name": "LiveTrade: Collective2 (LIVE)",
@@ -22,7 +25,49 @@
2225
"cwd": "${workspaceFolder}/tradingbot/",
2326
"envFile": "${workspaceFolder}/.env",
2427
"env": {
25-
"LIVETRADE_DRY_RUN": "false"
28+
"LIVETRADE_DRY_RUN": "false",
29+
"PYTHONPATH": "${workspaceFolder}/tradingbot"
30+
}
31+
},
32+
{
33+
"name": "LiveTrade: Interactive Brokers (paper)",
34+
"type": "debugpy",
35+
"request": "launch",
36+
"program": "${workspaceFolder}/tradingbot/livetrade_interactive_brokers.py",
37+
"console": "integratedTerminal",
38+
"cwd": "${workspaceFolder}/tradingbot/",
39+
"envFile": "${workspaceFolder}/.env",
40+
"env": {
41+
"LIVETRADE_DRY_RUN": "false",
42+
"IB_GATEWAY_PORT": "4004",
43+
"IB_CLIENT_ID": "18",
44+
"PYTHONPATH": "${workspaceFolder}/tradingbot"
45+
}
46+
},
47+
{
48+
"name": "Print Portfolio: Collective2",
49+
"type": "debugpy",
50+
"request": "launch",
51+
"program": "${workspaceFolder}/tradingbot/livetrade/collective2.py",
52+
"console": "integratedTerminal",
53+
"cwd": "${workspaceFolder}/tradingbot/",
54+
"envFile": "${workspaceFolder}/.env",
55+
"env": {
56+
"PYTHONPATH": "${workspaceFolder}/tradingbot"
57+
}
58+
},
59+
{
60+
"name": "Print Portfolio: Interactive Brokers",
61+
"type": "debugpy",
62+
"request": "launch",
63+
"program": "${workspaceFolder}/tradingbot/livetrade/interactive_brokers.py",
64+
"console": "integratedTerminal",
65+
"cwd": "${workspaceFolder}/tradingbot/",
66+
"envFile": "${workspaceFolder}/.env",
67+
"env": {
68+
"IB_GATEWAY_PORT": "4004",
69+
"IB_CLIENT_ID": "19",
70+
"PYTHONPATH": "${workspaceFolder}/tradingbot"
2671
}
2772
}
2873
]

AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,17 @@ with get_db_session() as s:
767767
```
768768
Only use `create_or_get_bot` when the caller genuinely owns the bot's identity (i.e., the bot itself, registering on first run).
769769

770+
### 9. `POSTGRES_URI` Required Even for Non-DB Tests
771+
**Problem**: `pytest tests/...` fails with `KeyError: 'Set POSTGRES_URI or (POSTGRES_HOST + POSTGRES_PASSWORD) for database connection'` even when running tests that don't touch the DB (e.g. pure-mock tests under `tests/test_livetrade.py`).
772+
773+
**Cause**: `tradingbot/utils/__init__.py` imports `botclass``bot_repository``db`, and `db.py` resolves `DATABASE_URL` at module import time. Any test that imports anything from `tradingbot.utils` (or transitively, like the livetrade copier) triggers this.
774+
775+
**Solution**: Pass a stub URI for non-DB test runs:
776+
```bash
777+
POSTGRES_URI="postgresql://x:x@localhost:5432/x" PYTHONPATH=. uv run pytest tests/ -q
778+
```
779+
The connection isn't opened until a session is actually used, so a syntactically-valid bogus URI is enough to satisfy import.
780+
770781
## Technical Analysis Indicators
771782

772783
After calling `getYFDataWithTA()`, the DataFrame includes indicators from the `ta` library:

README.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,21 +375,47 @@ telegramMonitor:
375375

376376
See [Telegram Monitor Guide](docs/guides/telegram-monitor.md) for full setup instructions.
377377

378-
## 📈 Live Trading (Collective2)
378+
## 📈 Live Trading (Collective2 & Interactive Brokers)
379379

380-
The framework can mirror your paper-bot portfolios to a live brokerage account. Currently, it supports **Collective2** (World API v4), with Interactive Brokers support coming soon.
380+
> [!WARNING]
381+
> **DISCLAIMER:** This software is for educational and research purposes only. Trading involves significant risk of loss and is not suitable for all investors. Use of "Live Trading" features is strictly at your own risk. The authors and contributors are not liable for any financial losses, damages, or unintended trades incurred. Always test strategies thoroughly in a paper-trading environment before deploying real capital.
382+
383+
The framework can mirror your paper-bot portfolios to a live brokerage account. Supported brokers: **Collective2** (World API v4) and **Interactive Brokers** (via IB Gateway / `ib_async`).
381384

382385
### 1. Configure Environment
383386

384387
Add these to your `.env` or Kubernetes secrets:
385388
```bash
389+
# Collective2
386390
COLLECTIVE2_API_KEY=your_api_key
387391
COLLECTIVE2_SYSTEM_ID=12345678
392+
393+
# Interactive Brokers (IB Gateway must be running)
394+
IB_GATEWAY_HOST=127.0.0.1
395+
IB_GATEWAY_PORT=4004
396+
IB_CLIENT_ID=17
397+
IB_ACCOUNT_ID=DU1234567 # paper accounts start with DU; live with U
398+
399+
# Shared
388400
LIVETRADE_BOT_WEIGHTS='{"adaptivemeanreversionbot": 1.0}'
389-
LIVETRADE_COPY_OPEN_TRADES=true
390401
LIVETRADE_DRY_RUN=false
391402
```
392403

404+
### Inspect Account & Portfolio
405+
406+
Each broker module is runnable directly to print the account summary and current
407+
positions — useful for sanity-checking credentials, account IDs, and mappings
408+
before running the copier:
409+
410+
```bash
411+
# Collective2
412+
uv run python tradingbot/livetrade/collective2.py
413+
414+
# Interactive Brokers (read-only connection; uses IB_CLIENT_ID=19 by default
415+
# so it won't collide with the cron client id 17 or vscode debug 18)
416+
uv run python tradingbot/livetrade/interactive_brokers.py
417+
```
418+
393419
### 2. Map Your Tickers
394420

395421
Yfinance symbols often differ from broker symbols (e.g., `EURUSD=X` vs `EURUSD`). The framework includes an **Assisted Ticker Discovery** script to help you map them:
@@ -407,11 +433,17 @@ uv run python -m tradingbot.livetrade.discover_symbols --apply
407433

408434
### 3. Deploy the Copier
409435

410-
The copier runs as a standalone script. Deploy it as a CronJob to run shortly after your trading bots:
436+
The copier runs as a standalone script per broker. Deploy as a CronJob to run shortly after your trading bots:
411437
```bash
438+
# Collective2
412439
uv run python tradingbot/livetrade_collective2.py
440+
441+
# Interactive Brokers
442+
uv run python tradingbot/livetrade_interactive_brokers.py
413443
```
414444

445+
Each broker is its own Helm CronJob gated by an independent flag in `values.yaml` — enable them separately (`liveTrade.enabled` for Collective2, `liveTradeIB.enabled` for IBKR), so you can run only one, both, or neither. Both default to `false`. You can also cap how much of the account each broker mirrors via `LIVETRADE_PORTFOLIO_FRACTION` (default `1.0` = full account; e.g. `0.5` = half).
446+
415447
See the [Live Trading Guide](docs/guides/live-trading.md) for advanced configuration and mapping rules.
416448

417449
## 🎯 Example Bots

docs/architecture/bot-class-system.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Bot Class System
22

3+
> [!WARNING]
4+
> **DISCLAIMER:** This software is for educational and research purposes only. Trading involves significant risk of loss. Use of this framework for live trading is strictly at your own risk.
5+
36
The `Bot` class is the foundation of the trading bot system. All bots inherit from it and implement trading strategies.
47

58
## Implementation Approaches

docs/architecture/overview.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Architecture Overview
22

3+
> [!WARNING]
4+
> **DISCLAIMER:** This architecture is designed for educational and research purposes. Use of this system for live trading is strictly at your own risk. Trading involves significant risk of loss.
5+
36
## System Components
47

58
The Trading Bot System consists of several key components:

docs/deployment/helm.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Helm Charts
22

3+
> [!WARNING]
4+
> **DISCLAIMER:** Use of this framework for live trading in a production environment is strictly at your own risk. Trading involves significant risk of loss.
5+
36
The system uses Helm charts for Kubernetes deployment.
47

58
## Chart Structure

docs/deployment/kubernetes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Kubernetes Deployment
22

3+
> [!WARNING]
4+
> **DISCLAIMER:** Use of this framework for live trading in a production environment is strictly at your own risk. Trading involves significant risk of loss.
5+
36
## Prerequisites
47

58
- Kubernetes cluster (1.20+)

docs/deployment/overview.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Deployment Overview
22

3+
> [!WARNING]
4+
> **DISCLAIMER:** This software is for educational and research purposes only. Trading involves significant risk of loss. Deploying bots to run on a schedule or for live trading is strictly at your own risk. The authors and contributors are not liable for any financial losses or damages incurred.
5+
36
The trading bot system supports two deployment approaches:
47

58
1. **Kubernetes with Helm** (Production) - Deploys PostgreSQL and bots as CronJobs

0 commit comments

Comments
 (0)