DXtrade: an entry never closes a hand trade, and a flatten never opens one

DXtrade dropped the intent the engine now passes, so on the live
Tradeify247 accounts an entry could still close a hand position as the
"entry" and the leg's flatten then OPEN a position with no exits. The
open/close decision moves out of vest-helpers into platform-client's
orderEffect, which both clients now follow to pick OPEN or CLOSE.

A flatten's flat read is "nothing to flatten" only when the client's own
record agrees. One empty read just after a fill the client saw may be the
gateway lagging, so it stays a plain refusal and keeps the FLATTEN FAILED
alarm. A check now fails if an engine order drops its intent or a client
stops going by orderEffect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 14:13:07 -05:00
co-authored by Claude Opus 5.5
parent e90f21ed91
commit 378530595e
7 changed files with 147 additions and 94 deletions
+44 -6
View File
@@ -171,9 +171,9 @@ export interface PlatformClient {
/** `intent`, where the caller knows what the order is for: an entry
* ('open') must never close a position it didn't know was there, a
* flatten ('close') must never open one on an account that turned out
* flat. A platform that checks it refuses the order otherwise
* (NothingToCloseError for a close with nothing to close); left out, or
* on a platform that doesn't check, the position decides. */
* flat. A platform that checks it (orderEffect) refuses the order
* otherwise (NothingToCloseError for a close with nothing to close);
* left out, or on a platform that doesn't check, the position decides. */
sendOrder(
accountId: number,
contractSymbol: string,
@@ -464,9 +464,9 @@ export function isOrderNotPlaced(err: unknown): boolean {
/**
* A close-only order (sendOrder's 'close') refused because a fresh read
* showed no position in the symbol to close: the account is flat there, so a
* flatten that gets this has nothing left to do — nothing rides without its
* stop.
* showed no position in the symbol to close, and the client's own record
* agrees: the account is flat there, so a flatten that gets this has nothing
* left to do — nothing rides without its stop.
*/
export class NothingToCloseError extends OrderNotPlacedError {
readonly nothingToClose = true;
@@ -481,6 +481,44 @@ export function isNothingToClose(err: unknown): boolean {
return !!err && typeof err === 'object' && (err as { nothingToClose?: unknown }).nothingToClose === true;
}
/**
* What an order on `side` does on a position-based platform (Vest, DXtrade),
* given a fresh read of the account's position in the symbol (`held`: its
* side, null when flat) and the client's own last record of it (`recorded`):
* open, or close (or reduce) — or it throws why nothing is sent. With no
* intent a flat account opens and the opposite side closes. An entry ('open')
* never closes: a hand trade the engine didn't know about would be closed in
* its place, and the leg's flatten would then open a position with no exits.
* A flatten ('close') never opens, and a flat read is NothingToCloseError only
* when the record agrees — one empty read just after a fill the client saw
* may be the gateway lagging, and a leg riding without its stop must still be
* shouted about. Adding to a position is never supported.
*/
export function orderEffect(o: {
symbol: string;
held: 'long' | 'short' | null;
recorded: 'long' | 'short' | null;
side: 'long' | 'short';
intent?: OrderIntent;
}): 'open' | 'close' {
const { symbol, held, recorded, side, intent } = o;
if (held === side) {
throw new OrderNotPlacedError(intent === 'close'
? `the account holds a ${held} ${symbol} position, not one to close — nothing sent`
: `adding to an open ${symbol} position is not supported`);
}
if (held && intent === 'open') {
throw new OrderNotPlacedError(`a ${held} ${symbol} position is already open — an entry never closes it, order not sent`);
}
if (!held && intent === 'close') {
if (recorded && recorded !== side) {
throw new OrderNotPlacedError(`a fresh read shows no ${symbol} position, but this client last saw a ${recorded} one open — nothing sent`);
}
throw new NothingToCloseError(`no ${symbol} position to close — nothing sent`);
}
return held ? 'close' : 'open';
}
/** An error's message without the rest of it. AxiosErrors carry the request
* config — Tradovate's login body (the password) and Bearer tokens — so they
* must never be logged whole. */