Manual leg: may join a lane on the same instrument and side, never the other

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-26 17:46:16 -05:00
co-authored by Claude Opus 5.5
parent b5ed423145
commit 675b4c89f8
+20 -5
View File
@@ -1373,8 +1373,9 @@ async function copyTradeLocked(lease: number, lane: string): Promise<unknown[]>
* The one rule it overrides is "one trade per day" for unsplit accounts: an * 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 * account that already booked something today may take another leg toward
* what's left of its target. Everything else still applies — flat, history * 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 * loaded, the daily-loss room, the firm's trading hours, no batch in flight
* no batch in flight there. * 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<LegResult> { export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' | 'Auto'): Promise<LegResult> {
const found = firmClients().ready.find(({ client }) => client.accountList.some((a) => a.id === accountId)); 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; 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 lane = laneOf(firm);
const open = countOpenPositions(inLane(firmClients().ready, lane)); const held = inLane(firmClients().ready, lane).flatMap(({ client: c }) => c.accountList
if (open > 0) throw new Error(`${open} position(s) still open in the ${laneName(lane)} lane`); .map((a) => c.positions[a.id])
.filter((p): p is NonNullable<typeof p> => !!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 state = getState();
const symbol = state.symbol !== 'Auto' ? state.symbol : 'NQ'; 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); const contract = await client.findFrontMonthContract(symbol);
if (!contract) throw new Error(`no instrument for ${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]; const pv = contract.pointValue ?? POINT_VALUES[symbol];
if (!pv) throw new Error(`no point value for ${symbol}`); if (!pv) throw new Error(`no point value for ${symbol}`);