diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index 9a890c3..161192b 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -1373,8 +1373,9 @@ async function copyTradeLocked(lease: number, lane: string): Promise * The one rule it overrides is "one trade per day" for unsplit accounts: an * account that already booked something today may take another leg toward * what's left of its target. Everything else still applies — flat, history - * loaded, the daily-loss room, the firm's trading hours, its lane flat and - * no batch in flight there. + * loaded, the daily-loss room, the firm's trading hours, no batch in flight + * in its lane — and a lane with positions open is only joined on the same + * instrument and side. */ export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' | 'Auto'): Promise { const found = firmClients().ready.find(({ client }) => client.accountList.some((a) => a.id === accountId)); @@ -1398,14 +1399,28 @@ export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' | room = gate.room; } + // A lane with positions open may only be joined on the same instrument + // and side — what a batch of like accounts would have done — never the + // other side (that is hedging). const lane = laneOf(firm); - const open = countOpenPositions(inLane(firmClients().ready, lane)); - if (open > 0) throw new Error(`${open} position(s) still open in the ${laneName(lane)} lane`); + const held = inLane(firmClients().ready, lane).flatMap(({ client: c }) => c.accountList + .map((a) => c.positions[a.id]) + .filter((p): p is NonNullable => !!p)); + const heldSides = new Set(held.map((p) => (p.netPos > 0 ? 'Buy' : 'Sell'))); + const heldContracts = new Set(held.map((p) => String(p.contractId))); + if (heldSides.size > 1 || heldContracts.size > 1) throw new Error(`the ${laneName(lane)} lane holds mixed positions — not adding to it`); + const heldSide: 'Buy' | 'Sell' | null = held.length > 0 ? (held[0].netPos > 0 ? 'Buy' : 'Sell') : null; + if (heldSide && action !== 'Auto' && action !== heldSide) { + throw new Error(`the ${laneName(lane)} lane is ${heldSide === 'Buy' ? 'long' : 'short'} — a ${action} would hedge it`); + } const state = getState(); const symbol = state.symbol !== 'Auto' ? state.symbol : 'NQ'; - const resolvedAction: 'Buy' | 'Sell' = action === 'Auto' ? (Math.random() < 0.5 ? 'Buy' : 'Sell') : action; + const resolvedAction: 'Buy' | 'Sell' = heldSide ?? (action === 'Auto' ? (Math.random() < 0.5 ? 'Buy' : 'Sell') : action); const contract = await client.findFrontMonthContract(symbol); if (!contract) throw new Error(`no instrument for ${symbol}`); + if (held.length > 0 && String(contract.id) !== [...heldContracts][0]) { + throw new Error(`the ${laneName(lane)} lane holds ${[...heldContracts][0]}, not ${contract.id} — not adding to it`); + } const pv = contract.pointValue ?? POINT_VALUES[symbol]; if (!pv) throw new Error(`no point value for ${symbol}`);