Skip to content

Commit f17659b

Browse files
authored
Merge pull request #114 from TraderAlice/dev
feat: currency-aware UTA + FX rates UI
2 parents f44c878 + 4a7d8bc commit f17659b

9 files changed

Lines changed: 230 additions & 69 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-alice",
3-
"version": "0.9.0-beta.9",
3+
"version": "0.9.0-beta.10",
44
"description": "File-based trading agent engine",
55
"type": "module",
66
"scripts": {

src/connectors/web/routes/trading.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,31 @@ export function createTradingRoutes(ctx: EngineContext) {
5858
return c.json(equity)
5959
})
6060

61+
// ==================== FX rates ====================
62+
63+
app.get('/fx-rates', async (c) => {
64+
// Collect all unique currencies from positions across all accounts
65+
const currencies = new Set<string>()
66+
for (const uta of ctx.accountManager.resolve()) {
67+
if (uta.health === 'offline') continue
68+
try {
69+
const positions = await uta.getPositions()
70+
for (const p of positions) {
71+
if (p.currency && p.currency !== 'USD') currencies.add(p.currency)
72+
}
73+
const account = await uta.getAccount()
74+
if (account.baseCurrency && account.baseCurrency !== 'USD') currencies.add(account.baseCurrency)
75+
} catch { /* skip unhealthy */ }
76+
}
77+
78+
const rates: Array<{ currency: string; rate: number; source: string; updatedAt: string }> = []
79+
for (const cur of currencies) {
80+
const fx = await ctx.fxService.getRate(cur)
81+
rates.push({ currency: cur, rate: fx.rate, source: fx.source, updatedAt: fx.updatedAt })
82+
}
83+
return c.json({ rates })
84+
})
85+
6186
// ==================== Per-account routes ====================
6287

6388
// Reconnect

src/core/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { QueryExecutor } from '@traderalice/opentypebb'
22
import type { AccountManager } from '../domain/trading/index.js'
3+
import type { FxService } from '../domain/trading/fx-service.js'
34
import type { SnapshotService } from '../domain/trading/snapshot/index.js'
45
import type { CronEngine } from '../task/cron/engine.js'
56
import type { Heartbeat } from '../task/heartbeat/index.js'
@@ -39,6 +40,7 @@ export interface EngineContext {
3940

4041
// Trading (unified account model)
4142
accountManager: AccountManager
43+
fxService: FxService
4244
snapshotService?: SnapshotService
4345
/** Reconnect connector plugins (Telegram, MCP-Ask, etc.). */
4446
reconnectConnectors: () => Promise<ReconnectResult>

src/domain/trading/__test__/e2e/ibkr-paper.e2e.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,38 @@ describe('IbkrBroker — connectivity', () => {
8383
})
8484
})
8585

86+
// ==================== Currency tracking (any time) ====================
87+
88+
describe('IbkrBroker — currency tracking', () => {
89+
beforeEach(({ skip }) => { if (!broker) skip('no IBKR paper account') })
90+
91+
it('getAccount returns baseCurrency field', async () => {
92+
const account = await broker!.getAccount()
93+
expect(account.baseCurrency).toBeDefined()
94+
expect(typeof account.baseCurrency).toBe('string')
95+
expect(account.baseCurrency.length).toBeGreaterThanOrEqual(3)
96+
console.log(` baseCurrency: ${account.baseCurrency}`)
97+
})
98+
99+
it('positions carry currency field matching contract.currency', async () => {
100+
const positions = await broker!.getPositions()
101+
if (positions.length === 0) {
102+
console.log(' no positions — skipping currency check')
103+
return
104+
}
105+
for (const p of positions) {
106+
expect(p.currency).toBeDefined()
107+
expect(typeof p.currency).toBe('string')
108+
expect(p.currency.length).toBeGreaterThanOrEqual(3)
109+
// currency should match what the contract says
110+
if (p.contract.currency) {
111+
expect(p.currency).toBe(p.contract.currency)
112+
}
113+
console.log(` ${p.contract.symbol}: currency=${p.currency}, avgCost=${p.avgCost}, marketPrice=${p.marketPrice}`)
114+
}
115+
})
116+
})
117+
86118
// ==================== Order lifecycle (any time — limit orders accepted outside market hours) ====================
87119

88120
describe('IbkrBroker — order lifecycle', () => {

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ async function main() {
406406
const ctx: EngineContext = {
407407
config, connectorCenter, agentCenter, eventLog, toolCallLog, heartbeat, cronEngine, toolCenter,
408408
bbEngine: getSDKExecutor(),
409-
accountManager, snapshotService,
409+
accountManager, fxService, snapshotService,
410410
reconnectConnectors,
411411
}
412412

ui/src/api/trading.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export const tradingApi = {
1818
return fetchJson('/api/trading/equity')
1919
},
2020

21+
// ==================== FX rates ====================
22+
23+
async fxRates(): Promise<{ rates: Array<{ currency: string; rate: number; source: string; updatedAt: string }> }> {
24+
return fetchJson('/api/trading/fx-rates')
25+
},
26+
2127
// ==================== Per-account ====================
2228

2329
async reconnectAccount(accountId: string): Promise<ReconnectResult> {

ui/src/api/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export interface TradingAccount {
202202
}
203203

204204
export interface AccountInfo {
205+
baseCurrency: string
205206
netLiquidation: number
206207
totalCashValue: number
207208
unrealizedPnL: number
@@ -224,6 +225,8 @@ export interface Position {
224225
multiplier?: number
225226
localSymbol?: string
226227
}
228+
/** Currency denomination of all monetary fields. */
229+
currency: string
227230
side: 'long' | 'short'
228231
quantity: string // Decimal serialized as string
229232
avgCost: number
@@ -354,6 +357,7 @@ export interface UTASnapshotSummary {
354357
timestamp: string
355358
trigger: string
356359
account: {
360+
baseCurrency: string
357361
netLiquidation: string
358362
totalCashValue: string
359363
unrealizedPnL: string
@@ -364,6 +368,7 @@ export interface UTASnapshotSummary {
364368
}
365369
positions: Array<{
366370
aliceId: string
371+
currency: string
367372
side: 'long' | 'short'
368373
quantity: string
369374
avgCost: string

ui/src/components/SnapshotDetail.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function SnapshotDetail({ snapshot, onClose }: SnapshotDetailProps) {
5151
<thead>
5252
<tr className="bg-bg text-text-muted text-left">
5353
<th className="px-2.5 py-1.5 font-medium">Symbol</th>
54+
<th className="px-2.5 py-1.5 font-medium text-center">Ccy</th>
5455
<th className="px-2.5 py-1.5 font-medium text-right">Qty</th>
5556
<th className="px-2.5 py-1.5 font-medium text-right">Avg Cost</th>
5657
<th className="px-2.5 py-1.5 font-medium text-right">Mkt Price</th>
@@ -70,11 +71,12 @@ export function SnapshotDetail({ snapshot, onClose }: SnapshotDetailProps) {
7071
</span>
7172
</td>
7273
<td className="px-2.5 py-1.5 text-right text-text tabular-nums">{p.quantity}</td>
73-
<td className="px-2.5 py-1.5 text-right text-text-muted tabular-nums">{fmtStr(p.avgCost)}</td>
74-
<td className="px-2.5 py-1.5 text-right text-text tabular-nums">{fmtStr(p.marketPrice)}</td>
75-
<td className="px-2.5 py-1.5 text-right text-text tabular-nums">{fmtStr(p.marketValue)}</td>
74+
<td className="px-2.5 py-1.5 text-center text-text-muted text-[10px] tabular-nums">{p.currency}</td>
75+
<td className="px-2.5 py-1.5 text-right text-text-muted tabular-nums">{fmtStr(p.avgCost, p.currency)}</td>
76+
<td className="px-2.5 py-1.5 text-right text-text tabular-nums">{fmtStr(p.marketPrice, p.currency)}</td>
77+
<td className="px-2.5 py-1.5 text-right text-text tabular-nums">{fmtStr(p.marketValue, p.currency)}</td>
7678
<td className={`px-2.5 py-1.5 text-right font-medium tabular-nums ${pnl >= 0 ? 'text-green' : 'text-red'}`}>
77-
{fmtPnlStr(p.unrealizedPnL)}
79+
{fmtPnlStr(p.unrealizedPnL, p.currency)}
7880
</td>
7981
</tr>
8082
)
@@ -153,15 +155,28 @@ function symbolFromAliceId(aliceId: string): string {
153155
return parts[parts.length - 1]
154156
}
155157

156-
function fmtStr(s: string): string {
158+
const CURRENCY_SYMBOLS: Record<string, string> = {
159+
USD: '$', HKD: 'HK$', EUR: '€', GBP: '£', JPY: '¥',
160+
CNY: '¥', CNH: '¥', CAD: 'C$', AUD: 'A$', CHF: 'CHF ',
161+
SGD: 'S$', KRW: '₩', INR: '₹', TWD: 'NT$', BRL: 'R$',
162+
}
163+
164+
function currencySymbol(currency?: string): string {
165+
if (!currency) return '$'
166+
return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? `${currency} `
167+
}
168+
169+
function fmtStr(s: string, currency?: string): string {
157170
const n = Number(s)
158171
if (isNaN(n)) return s
159-
return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
172+
const sym = currencySymbol(currency)
173+
return `${sym}${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
160174
}
161175

162-
function fmtPnlStr(s: string): string {
176+
function fmtPnlStr(s: string, currency?: string): string {
163177
const n = Number(s)
164178
if (isNaN(n)) return s
179+
const sym = currencySymbol(currency)
165180
const sign = n >= 0 ? '+' : ''
166-
return `${sign}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
181+
return `${sign}${sym}${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
167182
}

0 commit comments

Comments
 (0)