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:
co-authored by
Claude Opus 5.5
parent
5a4e85cc18
commit
934620e8e9
+12
-4
@@ -1540,6 +1540,14 @@ export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' |
|
|||||||
const acc = client.accountList.find((a) => a.id === accountId)!;
|
const acc = client.accountList.find((a) => a.id === accountId)!;
|
||||||
if (isInNoTradeWindow(firm)) throw new Error(`${firm.name} is outside its trading hours`);
|
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 tradeDate = currentTradeDateCT();
|
||||||
const ev = evaluateAccountForTrade(client, acc, tradeDate);
|
const ev = evaluateAccountForTrade(client, acc, tradeDate);
|
||||||
if (!ev || !ev.plan) throw new Error(ev?.reason || 'account has no account type');
|
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
|
// 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
|
// and side — what a batch of like accounts would have done — never the
|
||||||
// other side (that is hedging).
|
// other side (that is hedging).
|
||||||
const lane = laneOf(firm);
|
|
||||||
const held = inLane(firmClients().ready, lane).flatMap(({ client: c }) => c.accountList
|
const held = inLane(firmClients().ready, lane).flatMap(({ client: c }) => c.accountList
|
||||||
.map((a) => c.positions[a.id])
|
.map((a) => c.positions[a.id])
|
||||||
.filter((p): p is NonNullable<typeof p> => !!p));
|
.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];
|
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}`);
|
||||||
|
|
||||||
const lease = acquireTradeMutex('[manual-leg]', lane);
|
// The lookups above took long enough for a batch to presume this one
|
||||||
if (lease === null) throw new Error('another batch is in flight in this lane');
|
// wedged and take the lane: it traded on fresher numbers.
|
||||||
try {
|
if (!holdsTradeMutex(lease, lane)) throw new Error('superseded by a batch while choosing — not firing');
|
||||||
|
|
||||||
const item: LegItem = { ...sizingItem, contract, pv };
|
const item: LegItem = { ...sizingItem, contract, pv };
|
||||||
console.log(`[manual-leg] ${acc.name} (${firm.name}): firing one leg by hand`);
|
console.log(`[manual-leg] ${acc.name} (${firm.name}): firing one leg by hand`);
|
||||||
runtime().lastFireAt = Date.now();
|
runtime().lastFireAt = Date.now();
|
||||||
|
|||||||
Reference in New Issue
Block a user