Manual leg: take the lane's lock before reading the account

It checked the account, then awaited the instrument draw and the contract
lookup, and only then locked the lane. A batch that fired in that gap left
it trading on numbers from before: a second leg, or on DXtrade the other
side of the position the batch had just opened. Now it locks first, as the
batch and copy trade do, and checks the lease still holds before firing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-27 13:59:23 -05:00
co-authored by Claude Opus 5.5
parent 5a4e85cc18
commit 934620e8e9
+12 -4
View File
@@ -1540,6 +1540,14 @@ export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' |
const acc = client.accountList.find((a) => a.id === accountId)!;
if (isInNoTradeWindow(firm)) throw new Error(`${firm.name} is outside its trading hours`);
// The lane's lock before anything is read, as a batch takes it: a batch
// that fired between this leg's checks and its entry would leave it
// trading on numbers from before — a second leg, or the other side of
// the position the batch just opened.
const lane = laneOf(firm);
const lease = acquireTradeMutex('[manual-leg]', lane);
if (lease === null) throw new Error('another batch is in flight in this lane');
try {
const tradeDate = currentTradeDateCT();
const ev = evaluateAccountForTrade(client, acc, tradeDate);
if (!ev || !ev.plan) throw new Error(ev?.reason || 'account has no account type');
@@ -1558,7 +1566,6 @@ export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' |
// 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 held = inLane(firmClients().ready, lane).flatMap(({ client: c }) => c.accountList
.map((a) => c.positions[a.id])
.filter((p): p is NonNullable<typeof p> => !!p));
@@ -1597,9 +1604,10 @@ export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' |
const pv = contract.pointValue ?? POINT_VALUES[symbol];
if (!pv) throw new Error(`no point value for ${symbol}`);
const lease = acquireTradeMutex('[manual-leg]', lane);
if (lease === null) throw new Error('another batch is in flight in this lane');
try {
// The lookups above took long enough for a batch to presume this one
// wedged and take the lane: it traded on fresher numbers.
if (!holdsTradeMutex(lease, lane)) throw new Error('superseded by a batch while choosing — not firing');
const item: LegItem = { ...sizingItem, contract, pv };
console.log(`[manual-leg] ${acc.name} (${firm.name}): firing one leg by hand`);
runtime().lastFireAt = Date.now();