Skip to content

Commit df303f9

Browse files
authored
Merge pull request #98 from TraderAlice/dev
feat: IBKR-aligned order params + TPSL support + order summarization
2 parents c9241d6 + 1bba6af commit df303f9

22 files changed

Lines changed: 798 additions & 179 deletions

src/domain/trading/UnifiedTradingAccount.spec.ts

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ describe('UTA — getState', () => {
189189
broker.setPositions([makePosition()])
190190

191191
// Push a limit order to create a pending entry in git history
192-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'limit', qty: 5, price: 145 })
192+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', action: 'BUY', orderType: 'LMT', totalQuantity: 5, lmtPrice: 145 })
193193
uta.commit('limit buy')
194194
await uta.push()
195195

@@ -242,98 +242,126 @@ describe('UTA — stagePlaceOrder', () => {
242242
({ uta } = createUTA())
243243
})
244244

245-
it('maps buy side to BUY action', () => {
246-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
245+
it('sets BUY action', () => {
246+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
247247
const { order } = getStagedPlaceOrder(uta)
248248
expect(order.action).toBe('BUY')
249249
})
250250

251-
it('maps sell side to SELL action', () => {
252-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'market', qty: 10 })
251+
it('sets SELL action', () => {
252+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'SELL', orderType: 'MKT', totalQuantity: 10 })
253253
const { order } = getStagedPlaceOrder(uta)
254254
expect(order.action).toBe('SELL')
255255
})
256256

257-
it('maps order types correctly', () => {
258-
const cases: Array<[string, string]> = [
259-
['market', 'MKT'],
260-
['limit', 'LMT'],
261-
['stop', 'STP'],
262-
['stop_limit', 'STP LMT'],
263-
['trailing_stop', 'TRAIL'],
264-
]
265-
for (const [input, expected] of cases) {
257+
it('passes order types through', () => {
258+
const types = ['MKT', 'LMT', 'STP', 'STP LMT', 'TRAIL']
259+
for (const orderType of types) {
266260
const { uta: u } = createUTA()
267-
u.stagePlaceOrder({ aliceId: 'mock-paper|X', side: 'buy', type: input, qty: 1 })
261+
u.stagePlaceOrder({ aliceId: 'mock-paper|X', action: 'BUY', orderType, totalQuantity: 1 })
268262
const { order } = getStagedPlaceOrder(u)
269-
expect(order.orderType).toBe(expected)
263+
expect(order.orderType).toBe(orderType)
270264
}
271265
})
272266

273-
it('maps qty to totalQuantity as Decimal', () => {
274-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 42 })
267+
it('sets totalQuantity as Decimal', () => {
268+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 42 })
275269
const { order } = getStagedPlaceOrder(uta)
276270
expect(order.totalQuantity).toBeInstanceOf(Decimal)
277271
expect(order.totalQuantity.toNumber()).toBe(42)
278272
})
279273

280-
it('maps notional to cashQty', () => {
281-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', notional: 5000 })
274+
it('sets cashQty', () => {
275+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', cashQty: 5000 })
282276
const { order } = getStagedPlaceOrder(uta)
283277
expect(order.cashQty).toBe(5000)
284278
})
285279

286-
it('maps price to lmtPrice and stopPrice to auxPrice', () => {
287-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'stop_limit', qty: 10, price: 150, stopPrice: 145 })
280+
it('sets lmtPrice and auxPrice', () => {
281+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'STP LMT', totalQuantity: 10, lmtPrice: 150, auxPrice: 145 })
288282
const { order } = getStagedPlaceOrder(uta)
289283
expect(order.lmtPrice).toBe(150)
290284
expect(order.auxPrice).toBe(145)
291285
})
292286

293-
it('maps trailingAmount to trailStopPrice (not auxPrice)', () => {
294-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'trailing_stop', qty: 10, trailingAmount: 5 })
287+
it('auxPrice sets trailing offset for TRAIL orders', () => {
288+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'SELL', orderType: 'TRAIL', totalQuantity: 10, auxPrice: 5 })
295289
const { order } = getStagedPlaceOrder(uta)
296-
expect(order.trailStopPrice).toBe(5)
290+
expect(order.auxPrice).toBe(5)
297291
expect(order.orderType).toBe('TRAIL')
298292
})
299293

300-
it('trailingAmount and stopPrice use separate fields', () => {
301-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'trailing_stop', qty: 10, stopPrice: 145, trailingAmount: 5 })
294+
it('TRAIL order with trailStopPrice and auxPrice', () => {
295+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'SELL', orderType: 'TRAIL', totalQuantity: 10, trailStopPrice: 145, auxPrice: 5 })
302296
const { order } = getStagedPlaceOrder(uta)
303-
expect(order.auxPrice).toBe(145)
304-
expect(order.trailStopPrice).toBe(5)
297+
expect(order.trailStopPrice).toBe(145)
298+
expect(order.auxPrice).toBe(5)
305299
})
306300

307-
it('maps trailingPercent to trailingPercent', () => {
308-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'trailing_stop', qty: 10, trailingPercent: 2.5 })
301+
it('sets trailingPercent', () => {
302+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'SELL', orderType: 'TRAIL', totalQuantity: 10, trailingPercent: 2.5 })
309303
const { order } = getStagedPlaceOrder(uta)
310304
expect(order.trailingPercent).toBe(2.5)
311305
})
312306

313-
it('defaults timeInForce to DAY', () => {
314-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
307+
it('defaults tif to DAY', () => {
308+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
315309
const { order } = getStagedPlaceOrder(uta)
316310
expect(order.tif).toBe('DAY')
317311
})
318312

319-
it('allows overriding timeInForce', () => {
320-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'limit', qty: 10, price: 150, timeInForce: 'gtc' })
313+
it('allows overriding tif', () => {
314+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'LMT', totalQuantity: 10, lmtPrice: 150, tif: 'GTC' })
321315
const { order } = getStagedPlaceOrder(uta)
322316
expect(order.tif).toBe('GTC')
323317
})
324318

325-
it('maps extendedHours to outsideRth', () => {
326-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'limit', qty: 10, price: 150, extendedHours: true })
319+
it('sets outsideRth', () => {
320+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'LMT', totalQuantity: 10, lmtPrice: 150, outsideRth: true })
327321
const { order } = getStagedPlaceOrder(uta)
328322
expect(order.outsideRth).toBe(true)
329323
})
330324

331325
it('sets aliceId and symbol on contract', () => {
332-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
326+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
333327
const { contract } = getStagedPlaceOrder(uta)
334328
expect(contract.aliceId).toBe('mock-paper|AAPL')
335329
expect(contract.symbol).toBe('AAPL')
336330
})
331+
332+
it('sets tpsl with takeProfit only', () => {
333+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10, takeProfit: { price: '160' } })
334+
const staged = uta.status().staged
335+
const op = staged[0] as Extract<Operation, { action: 'placeOrder' }>
336+
expect(op.tpsl).toEqual({ takeProfit: { price: '160' }, stopLoss: undefined })
337+
})
338+
339+
it('sets tpsl with stopLoss only', () => {
340+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10, stopLoss: { price: '140' } })
341+
const staged = uta.status().staged
342+
const op = staged[0] as Extract<Operation, { action: 'placeOrder' }>
343+
expect(op.tpsl).toEqual({ takeProfit: undefined, stopLoss: { price: '140' } })
344+
})
345+
346+
it('sets tpsl with both TP and SL', () => {
347+
uta.stagePlaceOrder({
348+
aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10,
349+
takeProfit: { price: '160' }, stopLoss: { price: '140', limitPrice: '139.50' },
350+
})
351+
const staged = uta.status().staged
352+
const op = staged[0] as Extract<Operation, { action: 'placeOrder' }>
353+
expect(op.tpsl).toEqual({
354+
takeProfit: { price: '160' },
355+
stopLoss: { price: '140', limitPrice: '139.50' },
356+
})
357+
})
358+
359+
it('omits tpsl when neither TP nor SL provided', () => {
360+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
361+
const staged = uta.status().staged
362+
const op = staged[0] as Extract<Operation, { action: 'placeOrder' }>
363+
expect(op.tpsl).toBeUndefined()
364+
})
337365
})
338366

339367
// ==================== stageModifyOrder ====================
@@ -345,8 +373,8 @@ describe('UTA — stageModifyOrder', () => {
345373
({ uta } = createUTA())
346374
})
347375

348-
it('maps provided fields to Partial<Order>', () => {
349-
uta.stageModifyOrder({ orderId: 'ord-1', qty: 20, price: 155, type: 'limit', timeInForce: 'gtc' })
376+
it('sets provided fields on Partial<Order>', () => {
377+
uta.stageModifyOrder({ orderId: 'ord-1', totalQuantity: 20, lmtPrice: 155, orderType: 'LMT', tif: 'GTC' })
350378
const staged = uta.status().staged
351379
expect(staged).toHaveLength(1)
352380
const op = staged[0] as Extract<Operation, { action: 'modifyOrder' }>
@@ -360,7 +388,7 @@ describe('UTA — stageModifyOrder', () => {
360388
})
361389

362390
it('omits fields not provided', () => {
363-
uta.stageModifyOrder({ orderId: 'ord-1', price: 160 })
391+
uta.stageModifyOrder({ orderId: 'ord-1', lmtPrice: 160 })
364392
const staged = uta.status().staged
365393
const op = staged[0] as Extract<Operation, { action: 'modifyOrder' }>
366394
expect(op.changes.lmtPrice).toBe(160)
@@ -425,23 +453,23 @@ describe('UTA — git flow', () => {
425453
})
426454

427455
it('push throws when not committed', async () => {
428-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
456+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
429457
await expect(uta.push()).rejects.toThrow('please commit first')
430458
})
431459

432460
it('executes multiple operations in a single push', async () => {
433461
const { uta: u, broker: b } = createUTA()
434462
const spy = vi.spyOn(b, 'placeOrder')
435-
u.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
436-
u.stagePlaceOrder({ aliceId: 'mock-paper|MSFT', symbol: 'MSFT', side: 'buy', type: 'market', qty: 5 })
463+
u.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
464+
u.stagePlaceOrder({ aliceId: 'mock-paper|MSFT', symbol: 'MSFT', action: 'BUY', orderType: 'MKT', totalQuantity: 5 })
437465
u.commit('buy both')
438466
await u.push()
439467

440468
expect(spy).toHaveBeenCalledTimes(2)
441469
})
442470

443471
it('clears staging area after push', async () => {
444-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
472+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
445473
uta.commit('buy')
446474
await uta.push()
447475

@@ -462,7 +490,7 @@ describe('UTA — sync', () => {
462490
const { uta, broker } = createUTA()
463491

464492
// Limit order → MockBroker keeps it pending naturally
465-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'limit', qty: 10, price: 150 })
493+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', action: 'BUY', orderType: 'LMT', totalQuantity: 10, lmtPrice: 150 })
466494
uta.commit('limit buy')
467495
const pushResult = await uta.push()
468496
const orderId = pushResult.submitted[0]?.orderId
@@ -481,7 +509,7 @@ describe('UTA — sync', () => {
481509
const { uta, broker } = createUTA()
482510

483511
// Limit order → pending
484-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'limit', qty: 10, price: 150 })
512+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', action: 'BUY', orderType: 'LMT', totalQuantity: 10, lmtPrice: 150 })
485513
uta.commit('limit buy')
486514
const pushResult = await uta.push()
487515
const orderId = pushResult.submitted[0]?.orderId
@@ -503,7 +531,7 @@ describe('UTA — guards', () => {
503531
})
504532
const spy = vi.spyOn(broker, 'placeOrder')
505533

506-
uta.stagePlaceOrder({ aliceId: 'mock-paper|TSLA', symbol: 'TSLA', side: 'buy', type: 'market', qty: 10 })
534+
uta.stagePlaceOrder({ aliceId: 'mock-paper|TSLA', symbol: 'TSLA', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
507535
uta.commit('buy TSLA (should be blocked)')
508536
const result = await uta.push()
509537

@@ -518,7 +546,7 @@ describe('UTA — guards', () => {
518546
})
519547
const spy = vi.spyOn(broker, 'placeOrder')
520548

521-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
549+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
522550
uta.commit('buy AAPL (allowed)')
523551
await uta.push()
524552

@@ -532,7 +560,7 @@ describe('UTA — constructor', () => {
532560
it('restores from savedState', async () => {
533561
// Create a UTA, push a commit, export state
534562
const { uta: original } = createUTA()
535-
original.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
563+
original.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
536564
original.commit('initial buy')
537565
await original.push()
538566

@@ -660,7 +688,7 @@ describe('UTA — health tracking', () => {
660688
await expect(uta.getAccount()).rejects.toThrow()
661689
}
662690

663-
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
691+
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', action: 'BUY', orderType: 'MKT', totalQuantity: 10 })
664692
uta.commit('buy AAPL')
665693
await expect(uta.push()).rejects.toThrow(/offline/)
666694
await uta.close()

0 commit comments

Comments
 (0)