Skip to content

Commit 057eb79

Browse files
authored
Merge pull request #84 from TraderAlice/dev
feat: fill data capture, IBKR d.ts fix, E2E stability
2 parents 9a6d62c + 5c3791c commit 057eb79

24 files changed

+298
-176
lines changed

CLAUDE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ src/
2222
│ ├── tool-center.ts # Centralized tool registry (Vercel + MCP export)
2323
│ ├── session.ts # JSONL session store
2424
│ ├── compaction.ts # Auto-summarize long context windows
25-
│ ├── config.ts # Zod-validated config loader
25+
│ ├── config.ts # Zod-validated config loader (generic account schema with brokerConfig)
2626
│ ├── ai-config.ts # Runtime AI provider selection
2727
│ ├── event-log.ts # Append-only JSONL event log
2828
│ ├── connector-center.ts # ConnectorCenter — push delivery + last-interacted tracking
@@ -37,6 +37,14 @@ src/
3737
├── domain/
3838
│ ├── market-data/ # Structured data layer (typebb in-process + OpenBB API remote)
3939
│ ├── trading/ # Unified multi-account trading, guard pipeline, git-like commits
40+
│ │ ├── account-manager.ts # UTA lifecycle (init, reconnect, enable/disable) + registry
41+
│ │ ├── git-persistence.ts # Git state load/save
42+
│ │ └── brokers/
43+
│ │ ├── registry.ts # Broker self-registration (configSchema + configFields + fromConfig)
44+
│ │ ├── alpaca/ # Alpaca (US equities)
45+
│ │ ├── ccxt/ # CCXT (100+ crypto exchanges)
46+
│ │ ├── ibkr/ # Interactive Brokers (TWS/Gateway)
47+
│ │ └── mock/ # In-memory test broker
4048
│ ├── analysis/ # Indicators, technical analysis, sandbox
4149
│ ├── news/ # RSS collector + archive search
4250
│ ├── brain/ # Cognitive state (memory, emotion)

README.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Your one-person Wall Street. Alice is an AI trading agent that gives you your ow
2424
## Features
2525

2626
- **Multi-provider AI** — switch between Claude (via Agent SDK with OAuth or API key) and Vercel AI SDK at runtime, no restart needed
27-
- **Unified Trading Account (UTA)** — each trading account is a self-contained entity that owns its broker connection, git-like operation history, and guard pipeline. AI interacts with UTAs, never with brokers directly. All order types use IBKR's type system (`@traderalice/ibkr`) as the single source of truth, with Alpaca and CCXT adapting to it
27+
- **Unified Trading Account (UTA)** — each trading account is a self-contained entity that owns its broker connection, git-like operation history, and guard pipeline. AI interacts with UTAs, never with brokers directly. All order types use IBKR's type system (`@traderalice/ibkr`) as the single source of truth. Supported brokers: CCXT (100+ crypto exchanges), Alpaca (US equities), Interactive Brokers (stocks, options, futures, bonds via TWS/Gateway). Each broker self-registers its config schema and UI field descriptors — adding a new broker requires zero changes to the framework
2828
- **Trading-as-Git** — stage orders, commit with a message, push to execute. Every commit gets an 8-char hash. Full history reviewable via `tradingLog` / `tradingShow`
2929
- **Guard pipeline** — pre-execution safety checks (max position size, cooldown, symbol whitelist) that run inside each UTA before orders reach the broker
3030
- **Market data** — TypeScript-native OpenBB engine (`opentypebb`) with no external sidecar required. Covers equity, crypto, commodity, currency, and macro data with unified symbol search (`marketSearchForResearch`) and technical indicator calculator. Can also expose an embedded OpenBB-compatible HTTP API for external tools
@@ -43,7 +43,7 @@ Your one-person Wall Street. Alice is an AI trading agent that gives you your ow
4343

4444
**Extension** — A self-contained tool package registered in ToolCenter. Each extension owns its tools, state, and persistence. Examples: trading, brain, analysis-kit.
4545

46-
**UTA (Unified Trading Account)** — The core business entity for trading. Each UTA owns a broker connection (`IBroker`), a git-like operation history (`TradingGit`), and a guard pipeline. Think of it as a git repository for trades — multiple UTAs are like a monorepo with independent histories. AI and the frontend interact with UTAs exclusively; brokers are internal implementation details. All types (Contract, Order, Execution, OrderState) come from IBKR's type system via `@traderalice/ibkr`.
46+
**UTA (Unified Trading Account)** — The core business entity for trading. Each UTA owns a broker connection (`IBroker`), a git-like operation history (`TradingGit`), and a guard pipeline. Think of it as a git repository for trades — multiple UTAs are like a monorepo with independent histories. AI and the frontend interact with UTAs exclusively; brokers are internal implementation details. All types (Contract, Order, Execution, OrderState) come from IBKR's type system via `@traderalice/ibkr`. `AccountManager` owns the full UTA lifecycle (create, reconnect, enable/disable, remove).
4747

4848
**Trading-as-Git** — The workflow inside each UTA. Stage operations (`stagePlaceOrder`, `stageClosePosition`, etc.), commit with a message, then push to execute. Push runs guards, dispatches to the broker, snapshots account state, and records a commit with an 8-char hash. Full history is reviewable via `tradingLog` / `tradingShow`.
4949

@@ -165,17 +165,14 @@ All config lives in `data/config/` as JSON files with Zod validation. Missing fi
165165

166166
**AI Provider** — The default provider is Claude (Agent SDK), which uses your local Claude Code login — no API key needed. To use the [Vercel AI SDK](https://sdk.vercel.ai/docs) instead (Anthropic, OpenAI, Google, etc.), switch `ai-provider.json` to `vercel-ai-sdk` and add your API key. Both can be switched at runtime via the Web UI.
167167

168-
**Trading** — Unified Trading Account (UTA) architecture. Define platforms in `platforms.json` (CCXT exchanges, Alpaca), then create accounts in `accounts.json` referencing a platform. Each account becomes a UTA with its own git history and guard config. Legacy `crypto.json` and `securities.json` are still supported.
168+
**Trading** — Unified Trading Account (UTA) architecture. Each account in `accounts.json` becomes a UTA with its own broker connection, git history, and guard config. Broker-specific settings live in the `brokerConfig` field — each broker type declares its own schema and validates it internally.
169169

170170
| File | Purpose |
171171
|------|---------|
172172
| `engine.json` | Trading pairs, tick interval, timeframe |
173173
| `agent.json` | Max agent steps, evolution mode toggle, Claude Code tool permissions |
174174
| `ai-provider.json` | Active AI provider (`agent-sdk` or `vercel-ai-sdk`), login method, switchable at runtime |
175-
| `platforms.json` | Trading platform definitions (CCXT exchanges, Alpaca) |
176-
| `accounts.json` | Trading account credentials and guard config, references platforms |
177-
| `crypto.json` | CCXT exchange config + API keys, allowed symbols, guards |
178-
| `securities.json` | Alpaca broker config + API keys, allowed symbols, guards |
175+
| `accounts.json` | Trading accounts with `type`, `enabled`, `guards`, and `brokerConfig` (broker-specific settings) |
179176
| `connectors.json` | Web/MCP server ports, MCP Ask enable |
180177
| `telegram.json` | Telegram bot credentials + enable |
181178
| `web-subchannels.json` | Web UI sub-channel definitions with per-channel AI provider overrides |
@@ -225,11 +222,17 @@ src/
225222
news/ # RSS collector, archive search tools
226223
trading/ # Unified Trading Account (UTA): brokers, git-like commits, guards, AI tool adapter
227224
UnifiedTradingAccount.ts # UTA class — owns broker + git + guards
228-
brokers/ # IBroker interface + Alpaca/CCXT implementations
225+
account-manager.ts # UTA lifecycle management (init, reconnect, enable/disable, remove) + registry
226+
git-persistence.ts # Git state load/save (commit history to disk)
227+
brokers/ # IBroker interface + implementations
228+
registry.ts # Broker type registry (self-registration with config schema + UI fields)
229+
factory.ts # AccountConfig → IBroker (delegates to registry)
230+
alpaca/ # Alpaca broker (US equities)
231+
ccxt/ # CCXT broker (100+ crypto exchanges)
232+
ibkr/ # Interactive Brokers (TWS/Gateway, callback→Promise bridge)
233+
mock/ # In-memory test broker
229234
git/ # Trading-as-Git engine (stage → commit → push)
230235
guards/ # Pre-execution safety checks (position size, cooldown, whitelist)
231-
adapter.ts # AI tool definitions (Zod schemas → UTA methods)
232-
account-manager.ts # Multi-UTA registry and routing
233236
thinking-kit/ # Reasoning and calculation tools
234237
brain/ # Cognitive state (memory, emotion)
235238
browser/ # Browser automation bridge (via OpenClaw)
@@ -273,7 +276,7 @@ Open Alice is in pre-release. The following items must land before the first sta
273276

274277
- [ ] **Tool confirmation** — sensitive tools (order placement, cancellation, position close) require explicit user confirmation before execution, with a per-tool bypass mechanism for trusted workflows
275278
- [ ] **Trading-as-Git stable interface** — the UTA class and git workflow are functional; remaining work is serialization format (FIX-like tag-value encoding for Operation persistence) and the `tradingSync` polling loop
276-
- [ ] **IBKR broker** — Interactive Brokers integration via TWS API. The `@traderalice/ibkr` TypeScript SDK (full TWS protocol port) is complete; remaining work is implementing `IBroker` against it
279+
- [x] **IBKR broker** — Interactive Brokers integration via TWS/Gateway. `IbkrBroker` bridges the callback-based `@traderalice/ibkr` SDK to the Promise-based `IBroker` interface via `RequestBridge`. Supports all IBroker methods including conId-based contract resolution
277280
- [ ] **Account snapshot & analytics** — unified trading account snapshots with P&L breakdown, exposure analysis, and historical performance tracking
278281

279282
## Star History

packages/ibkr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"directory": "packages/ibkr"
1919
},
2020
"scripts": {
21-
"build": "tsup",
21+
"build": "rm -rf dist && tsc",
2222
"test": "vitest run --config vitest.config.ts",
2323
"test:e2e": "vitest run --config vitest.e2e.config.ts",
2424
"test:all": "vitest run --config vitest.config.ts && vitest run --config vitest.e2e.config.ts",

packages/ibkr/src/client/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import { applyAccount } from './account.js'
1313
import { applyOrders } from './orders.js'
1414
import { applyHistorical } from './historical.js'
1515

16+
// Force d.ts to reference mixin files so declare module augmentations are loaded
17+
import './market-data.js'
18+
import './account.js'
19+
import './orders.js'
20+
import './historical.js'
21+
1622
// Apply all method groups to EClient.prototype
1723
applyMarketData(EClient)
1824
applyAccount(EClient)

packages/ibkr/tsconfig.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,12 @@
66
"esModuleInterop": true,
77
"strict": true,
88
"outDir": "dist",
9-
"rootDir": ".",
9+
"rootDir": "src",
1010
"skipLibCheck": true,
1111
"resolveJsonModule": true,
1212
"declaration": true,
13-
"sourceMap": true,
14-
"paths": {
15-
"@/*": ["./src/*"]
16-
}
13+
"sourceMap": true
1714
},
1815
"include": ["src"],
19-
"exclude": ["node_modules", "dist", "ref", "**/*.test.ts"]
16+
"exclude": ["node_modules", "dist", "ref", "**/*.test.ts", "**/*.spec.ts"]
2017
}

src/connectors/web/routes/trading.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import type { UnifiedTradingAccount } from '../../../domain/trading/UnifiedTradi
66

77
/** Resolve account by :id param, return 404 if not found. */
88
function resolveAccount(ctx: EngineContext, c: Context): UnifiedTradingAccount | null {
9-
return ctx.accountManager.get(c.req.param('id')) ?? null
9+
const id = c.req.param('id')
10+
if (!id) return null
11+
return ctx.accountManager.get(id) ?? null
1012
}
1113

1214
/**

src/core/config.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ describe('readAccountsConfig', () => {
255255

256256
describe('writeAccountsConfig', () => {
257257
it('writes validated accounts to accounts.json', async () => {
258-
await writeAccountsConfig([{ id: 'acc-1', type: 'alpaca', guards: [], brokerConfig: { paper: true } }])
258+
await writeAccountsConfig([{ id: 'acc-1', type: 'alpaca', enabled: true, guards: [], brokerConfig: { paper: true } }])
259259
const filePath = mockWriteFile.mock.calls[0][0] as string
260260
expect(filePath).toMatch(/accounts\.json$/)
261261
})

src/domain/trading/UnifiedTradingAccount.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import Decimal from 'decimal.js'
11-
import { Contract, Order, ContractDescription, ContractDetails } from '@traderalice/ibkr'
11+
import { Contract, Order, ContractDescription, ContractDetails, UNSET_DECIMAL } from '@traderalice/ibkr'
1212
import { BrokerError, type IBroker, type AccountInfo, type Position, type OpenOrder, type PlaceOrderResult, type Quote, type MarketClock, type AccountCapabilities, type BrokerHealth, type BrokerHealthInfo } from './brokers/types.js'
1313
import { TradingGit } from './git/TradingGit.js'
1414
import type {
@@ -461,11 +461,19 @@ export class UnifiedTradingAccount {
461461

462462
const status = brokerOrder.orderState.status
463463
if (status !== 'Submitted' && status !== 'PreSubmitted') {
464+
// Extract fill data when available
465+
const orderFilledQty = brokerOrder.order.filledQuantity
466+
const filledQty = orderFilledQty && !orderFilledQty.equals(UNSET_DECIMAL)
467+
? orderFilledQty.toNumber()
468+
: undefined
469+
464470
updates.push({
465471
orderId,
466472
symbol,
467473
previousStatus: 'submitted',
468474
currentStatus: status === 'Filled' ? 'filled' : status === 'Cancelled' ? 'cancelled' : 'rejected',
475+
filledQty,
476+
filledPrice: brokerOrder.avgFillPrice,
469477
})
470478
}
471479
}

src/domain/trading/__test__/e2e/ccxt-bybit.e2e.spec.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,35 +30,38 @@ beforeAll(async () => {
3030
describe('CcxtBroker — Bybit e2e', () => {
3131
beforeEach(({ skip }) => { if (!broker) skip('no Bybit account') })
3232

33+
/** Narrow broker type — beforeEach guarantees non-null via skip(). */
34+
function b(): IBroker { return broker! }
35+
3336
it('fetches account info with positive equity', async () => {
34-
const account = await broker.getAccount()
37+
const account = await b().getAccount()
3538
expect(account.netLiquidation).toBeGreaterThan(0)
3639
console.log(` equity: $${account.netLiquidation.toFixed(2)}, cash: $${account.totalCashValue.toFixed(2)}`)
3740
})
3841

3942
it('fetches positions', async () => {
4043

41-
const positions = await broker.getPositions()
44+
const positions = await b().getPositions()
4245
expect(Array.isArray(positions)).toBe(true)
4346
console.log(` ${positions.length} open positions`)
4447
})
4548

4649
it('searches ETH contracts', async () => {
4750

48-
const results = await broker.searchContracts('ETH')
51+
const results = await b().searchContracts('ETH')
4952
expect(results.length).toBeGreaterThan(0)
5053
const perp = results.find(r => r.contract.localSymbol?.includes('USDT:USDT'))
5154
expect(perp).toBeDefined()
5255
console.log(` found ${results.length} ETH contracts, perp: ${perp!.contract.localSymbol}`)
5356
})
5457

5558
it('places market buy 0.01 ETH → execution returned', async ({ skip }) => {
56-
const matches = await broker!.searchContracts('ETH')
59+
const matches = await b().searchContracts('ETH')
5760
const ethPerp = matches.find(m => m.contract.localSymbol?.includes('USDT:USDT'))
58-
if (!ethPerp) skip('ETH/USDT perp not found')
61+
if (!ethPerp) return skip('ETH/USDT perp not found')
5962

6063
// Diagnostic: see raw CCXT createOrder response
61-
const exchange = (broker as any).exchange
64+
const exchange = (b() as any).exchange
6265
const rawOrder = await exchange.createOrder('ETH/USDT:USDT', 'market', 'buy', 0.01)
6366
console.log(' CCXT raw createOrder:', JSON.stringify({
6467
id: rawOrder.id, status: rawOrder.status, filled: rawOrder.filled,
@@ -67,15 +70,15 @@ describe('CcxtBroker — Bybit e2e', () => {
6770
}))
6871

6972
// Clean up diagnostic order
70-
await broker.closePosition(ethPerp.contract, new Decimal('0.01'))
73+
await b().closePosition(ethPerp.contract, new Decimal('0.01'))
7174

7275
// Now test through our placeOrder
7376
const order = new Order()
7477
order.action = 'BUY'
7578
order.orderType = 'MKT'
7679
order.totalQuantity = new Decimal('0.01')
7780

78-
const result = await broker.placeOrder(ethPerp.contract, order)
81+
const result = await b().placeOrder(ethPerp.contract, order)
7982
expect(result.success).toBe(true)
8083
expect(result.orderId).toBeDefined()
8184
console.log(` placeOrder result: orderId=${result.orderId}, execution=${!!result.execution}, orderState=${result.orderState?.status}`)
@@ -89,40 +92,40 @@ describe('CcxtBroker — Bybit e2e', () => {
8992

9093
it('verifies ETH position exists after buy', async () => {
9194

92-
const positions = await broker.getPositions()
95+
const positions = await b().getPositions()
9396
const ethPos = positions.find(p => p.contract.symbol === 'ETH')
9497
expect(ethPos).toBeDefined()
9598
console.log(` ETH position: ${ethPos!.quantity} ${ethPos!.side}`)
9699
})
97100

98101
it('closes ETH position with reduceOnly', async ({ skip }) => {
99-
const matches = await broker!.searchContracts('ETH')
102+
const matches = await b().searchContracts('ETH')
100103
const ethPerp = matches.find(m => m.contract.localSymbol?.includes('USDT:USDT'))
101-
if (!ethPerp) skip('ETH/USDT perp not found')
104+
if (!ethPerp) return skip('ETH/USDT perp not found')
102105

103-
const result = await broker.closePosition(ethPerp.contract, new Decimal('0.01'))
106+
const result = await b().closePosition(ethPerp.contract, new Decimal('0.01'))
104107
expect(result.success).toBe(true)
105108
console.log(` close orderId=${result.orderId}, success=${result.success}`)
106109
}, 15_000)
107110

108111
it('queries order by ID', async ({ skip }) => {
109112
// Place a small order to get an orderId
110-
const matches = await broker!.searchContracts('ETH')
113+
const matches = await b().searchContracts('ETH')
111114
const ethPerp = matches.find(m => m.contract.localSymbol?.includes('USDT:USDT'))
112-
if (!ethPerp) skip('ETH/USDT perp not found')
115+
if (!ethPerp) return skip('ETH/USDT perp not found')
113116

114117
const order = new Order()
115118
order.action = 'BUY'
116119
order.orderType = 'MKT'
117120
order.totalQuantity = new Decimal('0.01')
118121

119-
const placed = await broker!.placeOrder(ethPerp!.contract, order)
120-
if (!placed.orderId) skip('no orderId returned')
122+
const placed = await b().placeOrder(ethPerp.contract, order)
123+
if (!placed.orderId) return skip('no orderId returned')
121124

122125
// Wait for exchange to settle — Bybit needs time before order appears in closed list
123126
await new Promise(r => setTimeout(r, 5000))
124127

125-
const detail = await broker.getOrder(placed.orderId)
128+
const detail = await b().getOrder(placed.orderId)
126129
console.log(` getOrder(${placed.orderId}): ${detail ? `status=${detail.orderState.status}` : 'null'}`)
127130

128131
expect(detail).not.toBeNull()
@@ -131,6 +134,6 @@ describe('CcxtBroker — Bybit e2e', () => {
131134
}
132135

133136
// Clean up
134-
await broker.closePosition(ethPerp.contract, new Decimal('0.01'))
137+
await b().closePosition(ethPerp.contract, new Decimal('0.01'))
135138
}, 15_000)
136139
})

0 commit comments

Comments
 (0)