Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ src/
│ ├── tool-center.ts # Centralized tool registry (Vercel + MCP export)
│ ├── session.ts # JSONL session store
│ ├── compaction.ts # Auto-summarize long context windows
│ ├── config.ts # Zod-validated config loader
│ ├── config.ts # Zod-validated config loader (generic account schema with brokerConfig)
│ ├── ai-config.ts # Runtime AI provider selection
│ ├── event-log.ts # Append-only JSONL event log
│ ├── connector-center.ts # ConnectorCenter — push delivery + last-interacted tracking
Expand All @@ -37,6 +37,14 @@ src/
├── domain/
│ ├── market-data/ # Structured data layer (typebb in-process + OpenBB API remote)
│ ├── trading/ # Unified multi-account trading, guard pipeline, git-like commits
│ │ ├── account-manager.ts # UTA lifecycle (init, reconnect, enable/disable) + registry
│ │ ├── git-persistence.ts # Git state load/save
│ │ └── brokers/
│ │ ├── registry.ts # Broker self-registration (configSchema + configFields + fromConfig)
│ │ ├── alpaca/ # Alpaca (US equities)
│ │ ├── ccxt/ # CCXT (100+ crypto exchanges)
│ │ ├── ibkr/ # Interactive Brokers (TWS/Gateway)
│ │ └── mock/ # In-memory test broker
│ ├── analysis/ # Indicators, technical analysis, sandbox
│ ├── news/ # RSS collector + archive search
│ ├── brain/ # Cognitive state (memory, emotion)
Expand Down
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Your one-person Wall Street. Alice is an AI trading agent that gives you your ow
## Features

- **Multi-provider AI** — switch between Claude (via Agent SDK with OAuth or API key) and Vercel AI SDK at runtime, no restart needed
- **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
- **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
- **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`
- **Guard pipeline** — pre-execution safety checks (max position size, cooldown, symbol whitelist) that run inside each UTA before orders reach the broker
- **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
Expand All @@ -43,7 +43,7 @@ Your one-person Wall Street. Alice is an AI trading agent that gives you your ow

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

**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`.
**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).

**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`.

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

**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.

**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.
**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.

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

- [ ] **Tool confirmation** — sensitive tools (order placement, cancellation, position close) require explicit user confirmation before execution, with a per-tool bypass mechanism for trusted workflows
- [ ] **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
- [ ] **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
- [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
- [ ] **Account snapshot & analytics** — unified trading account snapshots with P&L breakdown, exposure analysis, and historical performance tracking

## Star History
Expand Down
2 changes: 1 addition & 1 deletion packages/ibkr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"directory": "packages/ibkr"
},
"scripts": {
"build": "tsup",
"build": "rm -rf dist && tsc",
"test": "vitest run --config vitest.config.ts",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:all": "vitest run --config vitest.config.ts && vitest run --config vitest.e2e.config.ts",
Expand Down
6 changes: 6 additions & 0 deletions packages/ibkr/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import { applyAccount } from './account.js'
import { applyOrders } from './orders.js'
import { applyHistorical } from './historical.js'

// Force d.ts to reference mixin files so declare module augmentations are loaded
import './market-data.js'
import './account.js'
import './orders.js'
import './historical.js'

// Apply all method groups to EClient.prototype
applyMarketData(EClient)
applyAccount(EClient)
Expand Down
9 changes: 3 additions & 6 deletions packages/ibkr/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": ".",
"rootDir": "src",
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true,
"paths": {
"@/*": ["./src/*"]
}
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "ref", "**/*.test.ts"]
"exclude": ["node_modules", "dist", "ref", "**/*.test.ts", "**/*.spec.ts"]
}
4 changes: 3 additions & 1 deletion src/connectors/web/routes/trading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import type { UnifiedTradingAccount } from '../../../domain/trading/UnifiedTradi

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

/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ describe('readAccountsConfig', () => {

describe('writeAccountsConfig', () => {
it('writes validated accounts to accounts.json', async () => {
await writeAccountsConfig([{ id: 'acc-1', type: 'alpaca', guards: [], brokerConfig: { paper: true } }])
await writeAccountsConfig([{ id: 'acc-1', type: 'alpaca', enabled: true, guards: [], brokerConfig: { paper: true } }])
const filePath = mockWriteFile.mock.calls[0][0] as string
expect(filePath).toMatch(/accounts\.json$/)
})
Expand Down
10 changes: 9 additions & 1 deletion src/domain/trading/UnifiedTradingAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import Decimal from 'decimal.js'
import { Contract, Order, ContractDescription, ContractDetails } from '@traderalice/ibkr'
import { Contract, Order, ContractDescription, ContractDetails, UNSET_DECIMAL } from '@traderalice/ibkr'
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'
import { TradingGit } from './git/TradingGit.js'
import type {
Expand Down Expand Up @@ -461,11 +461,19 @@ export class UnifiedTradingAccount {

const status = brokerOrder.orderState.status
if (status !== 'Submitted' && status !== 'PreSubmitted') {
// Extract fill data when available
const orderFilledQty = brokerOrder.order.filledQuantity
const filledQty = orderFilledQty && !orderFilledQty.equals(UNSET_DECIMAL)
? orderFilledQty.toNumber()
: undefined

updates.push({
orderId,
symbol,
previousStatus: 'submitted',
currentStatus: status === 'Filled' ? 'filled' : status === 'Cancelled' ? 'cancelled' : 'rejected',
filledQty,
filledPrice: brokerOrder.avgFillPrice,
})
}
}
Expand Down
39 changes: 21 additions & 18 deletions src/domain/trading/__test__/e2e/ccxt-bybit.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,38 @@ beforeAll(async () => {
describe('CcxtBroker — Bybit e2e', () => {
beforeEach(({ skip }) => { if (!broker) skip('no Bybit account') })

/** Narrow broker type — beforeEach guarantees non-null via skip(). */
function b(): IBroker { return broker! }

it('fetches account info with positive equity', async () => {
const account = await broker.getAccount()
const account = await b().getAccount()
expect(account.netLiquidation).toBeGreaterThan(0)
console.log(` equity: $${account.netLiquidation.toFixed(2)}, cash: $${account.totalCashValue.toFixed(2)}`)
})

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

const positions = await broker.getPositions()
const positions = await b().getPositions()
expect(Array.isArray(positions)).toBe(true)
console.log(` ${positions.length} open positions`)
})

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

const results = await broker.searchContracts('ETH')
const results = await b().searchContracts('ETH')
expect(results.length).toBeGreaterThan(0)
const perp = results.find(r => r.contract.localSymbol?.includes('USDT:USDT'))
expect(perp).toBeDefined()
console.log(` found ${results.length} ETH contracts, perp: ${perp!.contract.localSymbol}`)
})

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

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

// Clean up diagnostic order
await broker.closePosition(ethPerp.contract, new Decimal('0.01'))
await b().closePosition(ethPerp.contract, new Decimal('0.01'))

// Now test through our placeOrder
const order = new Order()
order.action = 'BUY'
order.orderType = 'MKT'
order.totalQuantity = new Decimal('0.01')

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

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

const positions = await broker.getPositions()
const positions = await b().getPositions()
const ethPos = positions.find(p => p.contract.symbol === 'ETH')
expect(ethPos).toBeDefined()
console.log(` ETH position: ${ethPos!.quantity} ${ethPos!.side}`)
})

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

const result = await broker.closePosition(ethPerp.contract, new Decimal('0.01'))
const result = await b().closePosition(ethPerp.contract, new Decimal('0.01'))
expect(result.success).toBe(true)
console.log(` close orderId=${result.orderId}, success=${result.success}`)
}, 15_000)

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

const order = new Order()
order.action = 'BUY'
order.orderType = 'MKT'
order.totalQuantity = new Decimal('0.01')

const placed = await broker!.placeOrder(ethPerp!.contract, order)
if (!placed.orderId) skip('no orderId returned')
const placed = await b().placeOrder(ethPerp.contract, order)
if (!placed.orderId) return skip('no orderId returned')

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

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

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

// Clean up
await broker.closePosition(ethPerp.contract, new Decimal('0.01'))
await b().closePosition(ethPerp.contract, new Decimal('0.01'))
}, 15_000)
})
Loading
Loading