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
2 changes: 1 addition & 1 deletion src/connectors/web/__tests__/chat-streaming.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ describe('Web UI chat streaming', () => {
const provider = new FakeProvider([
toolUseEvent('t1', 'getAccount', {}),
toolResultEvent('t1', '{"cash": 100000}'),
toolUseEvent('t2', 'getQuote', { aliceId: 'alpaca-AAPL' }),
toolUseEvent('t2', 'getQuote', { aliceId: 'mock-paper|AAPL' }),
toolResultEvent('t2', '{"last": 255.71}'),
textEvent('Account has $100k, AAPL at $255.71'),
doneEvent('Account has $100k, AAPL at $255.71'),
Expand Down
68 changes: 44 additions & 24 deletions src/domain/trading/UnifiedTradingAccount.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('UTA — operation dispatch', () => {
it('passes aliceId and extra contract fields', async () => {
const spy = vi.spyOn(broker, 'placeOrder')
const contract = makeContract({
aliceId: 'alpaca-AAPL',
aliceId: 'mock-paper|AAPL',
symbol: 'AAPL',
secType: 'STK',
currency: 'USD',
Expand All @@ -74,7 +74,7 @@ describe('UTA — operation dispatch', () => {
await uta.push()

const [passedContract, passedOrder] = spy.mock.calls[0]
expect(passedContract.aliceId).toBe('alpaca-AAPL')
expect(passedContract.aliceId).toBe('mock-paper|AAPL')
expect(passedContract.secType).toBe('STK')
expect(passedContract.currency).toBe('USD')
expect(passedContract.exchange).toBe('NASDAQ')
Expand Down Expand Up @@ -243,13 +243,13 @@ describe('UTA — stagePlaceOrder', () => {
})

it('maps buy side to BUY action', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
const { order } = getStagedPlaceOrder(uta)
expect(order.action).toBe('BUY')
})

it('maps sell side to SELL action', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'sell', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'market', qty: 10 })
const { order } = getStagedPlaceOrder(uta)
expect(order.action).toBe('SELL')
})
Expand All @@ -264,54 +264,74 @@ describe('UTA — stagePlaceOrder', () => {
]
for (const [input, expected] of cases) {
const { uta: u } = createUTA()
u.stagePlaceOrder({ aliceId: 'a-X', side: 'buy', type: input, qty: 1 })
u.stagePlaceOrder({ aliceId: 'mock-paper|X', side: 'buy', type: input, qty: 1 })
const { order } = getStagedPlaceOrder(u)
expect(order.orderType).toBe(expected)
}
})

it('maps qty to totalQuantity as Decimal', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 42 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 42 })
const { order } = getStagedPlaceOrder(uta)
expect(order.totalQuantity).toBeInstanceOf(Decimal)
expect(order.totalQuantity.toNumber()).toBe(42)
})

it('maps notional to cashQty', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', notional: 5000 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', notional: 5000 })
const { order } = getStagedPlaceOrder(uta)
expect(order.cashQty).toBe(5000)
})

it('maps price to lmtPrice and stopPrice to auxPrice', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'stop_limit', qty: 10, price: 150, stopPrice: 145 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'stop_limit', qty: 10, price: 150, stopPrice: 145 })
const { order } = getStagedPlaceOrder(uta)
expect(order.lmtPrice).toBe(150)
expect(order.auxPrice).toBe(145)
})

it('maps trailingAmount to trailStopPrice (not auxPrice)', () => {
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'trailing_stop', qty: 10, trailingAmount: 5 })
const { order } = getStagedPlaceOrder(uta)
expect(order.trailStopPrice).toBe(5)
expect(order.orderType).toBe('TRAIL')
})

it('trailingAmount and stopPrice use separate fields', () => {
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'trailing_stop', qty: 10, stopPrice: 145, trailingAmount: 5 })
const { order } = getStagedPlaceOrder(uta)
expect(order.auxPrice).toBe(145)
expect(order.trailStopPrice).toBe(5)
})

it('maps trailingPercent to trailingPercent', () => {
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'sell', type: 'trailing_stop', qty: 10, trailingPercent: 2.5 })
const { order } = getStagedPlaceOrder(uta)
expect(order.trailingPercent).toBe(2.5)
})

it('defaults timeInForce to DAY', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
const { order } = getStagedPlaceOrder(uta)
expect(order.tif).toBe('DAY')
})

it('allows overriding timeInForce', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'limit', qty: 10, price: 150, timeInForce: 'gtc' })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'limit', qty: 10, price: 150, timeInForce: 'gtc' })
const { order } = getStagedPlaceOrder(uta)
expect(order.tif).toBe('GTC')
})

it('maps extendedHours to outsideRth', () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'limit', qty: 10, price: 150, extendedHours: true })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'limit', qty: 10, price: 150, extendedHours: true })
const { order } = getStagedPlaceOrder(uta)
expect(order.outsideRth).toBe(true)
})

it('sets aliceId and symbol on contract', () => {
uta.stagePlaceOrder({ aliceId: 'alpaca-AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
const { contract } = getStagedPlaceOrder(uta)
expect(contract.aliceId).toBe('alpaca-AAPL')
expect(contract.aliceId).toBe('mock-paper|AAPL')
expect(contract.symbol).toBe('AAPL')
})
})
Expand Down Expand Up @@ -360,17 +380,17 @@ describe('UTA — stageClosePosition', () => {
})

it('stages with Decimal quantity when qty provided', () => {
uta.stageClosePosition({ aliceId: 'a-AAPL', qty: 5 })
uta.stageClosePosition({ aliceId: 'mock-paper|AAPL', qty: 5 })
const staged = uta.status().staged
const op = staged[0] as Extract<Operation, { action: 'closePosition' }>
expect(op.action).toBe('closePosition')
expect(op.contract.aliceId).toBe('a-AAPL')
expect(op.contract.aliceId).toBe('mock-paper|AAPL')
expect(op.quantity).toBeInstanceOf(Decimal)
expect(op.quantity!.toNumber()).toBe(5)
})

it('stages with undefined quantity for full close', () => {
uta.stageClosePosition({ aliceId: 'a-AAPL' })
uta.stageClosePosition({ aliceId: 'mock-paper|AAPL' })
const staged = uta.status().staged
const op = staged[0] as Extract<Operation, { action: 'closePosition' }>
expect(op.quantity).toBeUndefined()
Expand Down Expand Up @@ -405,23 +425,23 @@ describe('UTA — git flow', () => {
})

it('push throws when not committed', async () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
await expect(uta.push()).rejects.toThrow('please commit first')
})

it('executes multiple operations in a single push', async () => {
const { uta: u, broker: b } = createUTA()
const spy = vi.spyOn(b, 'placeOrder')
u.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
u.stagePlaceOrder({ aliceId: 'a-MSFT', symbol: 'MSFT', side: 'buy', type: 'market', qty: 5 })
u.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
u.stagePlaceOrder({ aliceId: 'mock-paper|MSFT', symbol: 'MSFT', side: 'buy', type: 'market', qty: 5 })
u.commit('buy both')
await u.push()

expect(spy).toHaveBeenCalledTimes(2)
})

it('clears staging area after push', async () => {
uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
uta.commit('buy')
await uta.push()

Expand Down Expand Up @@ -483,7 +503,7 @@ describe('UTA — guards', () => {
})
const spy = vi.spyOn(broker, 'placeOrder')

uta.stagePlaceOrder({ aliceId: 'a-TSLA', symbol: 'TSLA', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|TSLA', symbol: 'TSLA', side: 'buy', type: 'market', qty: 10 })
uta.commit('buy TSLA (should be blocked)')
const result = await uta.push()

Expand All @@ -498,7 +518,7 @@ describe('UTA — guards', () => {
})
const spy = vi.spyOn(broker, 'placeOrder')

uta.stagePlaceOrder({ aliceId: 'a-AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
uta.commit('buy AAPL (allowed)')
await uta.push()

Expand All @@ -512,7 +532,7 @@ describe('UTA — constructor', () => {
it('restores from savedState', async () => {
// Create a UTA, push a commit, export state
const { uta: original } = createUTA()
original.stagePlaceOrder({ aliceId: 'a-AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
original.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', symbol: 'AAPL', side: 'buy', type: 'market', qty: 10 })
original.commit('initial buy')
await original.push()

Expand Down Expand Up @@ -640,7 +660,7 @@ describe('UTA — health tracking', () => {
await expect(uta.getAccount()).rejects.toThrow()
}

uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
uta.commit('buy AAPL')
await expect(uta.push()).rejects.toThrow(/offline/)
await uta.close()
Expand Down
18 changes: 10 additions & 8 deletions src/domain/trading/UnifiedTradingAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,10 @@ export class UnifiedTradingAccount {
stagePlaceOrder(params: StagePlaceOrderParams): AddResult {
// Resolve aliceId → full contract via broker (fills secType, exchange, currency, conId, etc.)
const parsed = UnifiedTradingAccount.parseAliceId(params.aliceId)
const contract = parsed
? this.broker.resolveNativeKey(parsed.nativeKey)
: new Contract()
if (!parsed) {
throw new Error(`Invalid aliceId "${params.aliceId}". Use searchContracts to get a valid contract identifier (expected format: "accountId|nativeKey").`)
}
const contract = this.broker.resolveNativeKey(parsed.nativeKey)
contract.aliceId = params.aliceId
if (params.symbol) contract.symbol = params.symbol

Expand All @@ -373,7 +374,7 @@ export class UnifiedTradingAccount {
if (params.notional != null) order.cashQty = params.notional
if (params.price != null) order.lmtPrice = params.price
if (params.stopPrice != null) order.auxPrice = params.stopPrice
if (params.trailingAmount != null) order.auxPrice = params.trailingAmount
if (params.trailingAmount != null) order.trailStopPrice = params.trailingAmount
if (params.trailingPercent != null) order.trailingPercent = params.trailingPercent
if (params.goodTillDate != null) order.goodTillDate = params.goodTillDate
if (params.extendedHours) order.outsideRth = true
Expand All @@ -388,7 +389,7 @@ export class UnifiedTradingAccount {
if (params.qty != null) changes.totalQuantity = new Decimal(String(params.qty))
if (params.price != null) changes.lmtPrice = params.price
if (params.stopPrice != null) changes.auxPrice = params.stopPrice
if (params.trailingAmount != null) changes.auxPrice = params.trailingAmount
if (params.trailingAmount != null) changes.trailStopPrice = params.trailingAmount
if (params.trailingPercent != null) changes.trailingPercent = params.trailingPercent
if (params.type != null) changes.orderType = toIbkrOrderType(params.type)
if (params.timeInForce != null) changes.tif = toIbkrTif(params.timeInForce)
Expand All @@ -399,9 +400,10 @@ export class UnifiedTradingAccount {

stageClosePosition(params: StageClosePositionParams): AddResult {
const parsed = UnifiedTradingAccount.parseAliceId(params.aliceId)
const contract = parsed
? this.broker.resolveNativeKey(parsed.nativeKey)
: new Contract()
if (!parsed) {
throw new Error(`Invalid aliceId "${params.aliceId}". Use searchContracts to get a valid contract identifier (expected format: "accountId|nativeKey").`)
}
const contract = this.broker.resolveNativeKey(parsed.nativeKey)
contract.aliceId = params.aliceId
if (params.symbol) contract.symbol = params.symbol

Expand Down
10 changes: 5 additions & 5 deletions src/domain/trading/__test__/e2e/alpaca-paper.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('AlpacaBroker — fill + position (market hours)', () => {

it('fetches AAPL quote with valid prices', async () => {
const contract = new Contract()
contract.aliceId = 'alpaca-AAPL'
contract.aliceId = 'alpaca-paper|AAPL'
contract.symbol = 'AAPL'

const quote = await broker!.getQuote(contract)
Expand All @@ -131,7 +131,7 @@ describe('AlpacaBroker — fill + position (market hours)', () => {

it('places market buy 1 AAPL → success with UUID orderId', async () => {
const contract = new Contract()
contract.aliceId = 'alpaca-AAPL'
contract.aliceId = 'alpaca-paper|AAPL'
contract.symbol = 'AAPL'
contract.secType = 'STK'

Expand All @@ -151,7 +151,7 @@ describe('AlpacaBroker — fill + position (market hours)', () => {

it('queries order by ID after place', async () => {
const contract = new Contract()
contract.aliceId = 'alpaca-AAPL'
contract.aliceId = 'alpaca-paper|AAPL'
contract.symbol = 'AAPL'
contract.secType = 'STK'

Expand Down Expand Up @@ -187,7 +187,7 @@ describe('AlpacaBroker — fill + position (market hours)', () => {

it('closes AAPL position', async () => {
const contract = new Contract()
contract.aliceId = 'alpaca-AAPL'
contract.aliceId = 'alpaca-paper|AAPL'
contract.symbol = 'AAPL'

const result = await broker!.closePosition(contract)
Expand All @@ -197,7 +197,7 @@ describe('AlpacaBroker — fill + position (market hours)', () => {

it('getOrders with known IDs', async () => {
const contract = new Contract()
contract.aliceId = 'alpaca-AAPL'
contract.aliceId = 'alpaca-paper|AAPL'
contract.symbol = 'AAPL'
contract.secType = 'STK'

Expand Down
4 changes: 2 additions & 2 deletions src/domain/trading/__test__/uta-health.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ describe('UTA health — offline behavior', () => {
await expect(uta.getAccount()).rejects.toThrow()
}

uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
uta.commit('buy AAPL')
await expect(uta.push()).rejects.toThrow(/offline/)
await uta.close()
Expand All @@ -231,7 +231,7 @@ describe('UTA health — offline behavior', () => {
}

// Staging is a local operation — should work even when offline
const result = uta.stagePlaceOrder({ aliceId: 'a-AAPL', side: 'buy', type: 'market', qty: 10 })
const result = uta.stagePlaceOrder({ aliceId: 'mock-paper|AAPL', side: 'buy', type: 'market', qty: 10 })
expect(result.staged).toBe(true)

const commit = uta.commit('buy while offline')
Expand Down
8 changes: 4 additions & 4 deletions src/domain/trading/account-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@ describe('AccountManager', () => {
it('searches all accounts by default', async () => {
const a1 = new MockBroker({ id: 'a1' })
const desc1 = new ContractDescription()
desc1.contract = makeContract({ aliceId: 'a1-AAPL' })
desc1.contract = makeContract({ aliceId: 'a1|AAPL' })
vi.spyOn(a1, 'searchContracts').mockResolvedValue([desc1])

const a2 = new MockBroker({ id: 'a2' })
const desc2 = new ContractDescription()
desc2.contract = makeContract({ aliceId: 'a2-AAPL' })
desc2.contract = makeContract({ aliceId: 'a2|AAPL' })
vi.spyOn(a2, 'searchContracts').mockResolvedValue([desc2])

manager.add(makeUta(a1))
Expand All @@ -145,12 +145,12 @@ describe('AccountManager', () => {
it('scopes search to specific accountId', async () => {
const a1 = new MockBroker({ id: 'a1' })
const desc1 = new ContractDescription()
desc1.contract = makeContract({ aliceId: 'a1-AAPL' })
desc1.contract = makeContract({ aliceId: 'a1|AAPL' })
vi.spyOn(a1, 'searchContracts').mockResolvedValue([desc1])

const a2 = new MockBroker({ id: 'a2' })
const desc2 = new ContractDescription()
desc2.contract = makeContract({ aliceId: 'a2-AAPL' })
desc2.contract = makeContract({ aliceId: 'a2|AAPL' })
vi.spyOn(a2, 'searchContracts').mockResolvedValue([desc2])

manager.add(makeUta(a1))
Expand Down
Loading
Loading