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
+7 -5
View File
@@ -260,9 +260,10 @@ function unrecordLeg(accountId: number, tradeDate: string, legAmount: number): v
* (OrderNotPlacedError). Anything ambiguous, like an ack or fill timeout,
* keeps the leg: the order may well have filled, and a leg counted that never
* happened only costs a leg, while one forgotten that did can trade the
* account twice. Open-only: where the platform checks (Vest), an account that
* already holds the symbol — a hand trade the synced positions don't show
* yet — is refused rather than having that position closed as the "entry".
* account twice. Open-only: where the platform checks (Vest, DXtrade), an
* account that already holds the symbol — a hand trade the synced positions
* don't show yet — is refused rather than having that position closed as the
* "entry".
*/
async function sendEntry(
tag: string,
@@ -1304,8 +1305,9 @@ const round2 = (n: number) => Math.round(n * 100) / 100;
* report the leg as failed. A flatten that fails too is the worst case: a
* position with no stop, so it is shouted about. Close-only: on an account
* that turned out flat it must not open a fresh position with no exits, and
* where the platform checks (Vest) that flat account has nothing riding — the
* leg still fails, but without the alarm.
* where the platform checks (Vest, DXtrade) and its own record agrees, that
* flat account has nothing riding — the leg still fails, but without the
* alarm. A flat read the record contradicts still gets the alarm.
*/
async function flattenAndFail(
tag: string,
+16 -9
View File
@@ -42,8 +42,8 @@ import { getAccountConfig } from './account-type-resolver';
import { getCachedFirm } from './firm-catalog';
import { firmInstruments } from './instruments';
import type { SymbolMapCandidate } from './master-protocol';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext, OrderIntent } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage, orderEffect } from './platform-client';
import {
dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep,
lastUtcReset, ctTradeDayStart, firmDayOf, timeOf, parseDurationMs, ciNum, isFundType, signedFundValue, newOrderCode,
@@ -1557,10 +1557,10 @@ export class DXTradeClient implements PlatformClient {
/**
* Place an order and wait for its fill: an OPEN when the account is flat,
* a CLOSE of the position when it holds the opposite side. Adding to a
* position is refused, and so is an OPEN against another account's
* opposite position in the same symbol (no hedging). Resolves with the
* fill `price`.
* a CLOSE of the position when it holds the opposite side — only the one
* `intent` names, when given (orderEffect). Adding to a position is
* refused, and so is an OPEN against another account's opposite position
* in the same symbol (no hedging). Resolves with the fill `price`.
*/
public async sendOrder(
accountId: number,
@@ -1569,9 +1569,10 @@ export class DXTradeClient implements PlatformClient {
action: 'Buy' | 'Sell',
orderType: 'Market' | 'Limit',
price?: number,
intent?: OrderIntent,
): Promise<Record<string, unknown>> {
try {
return await this.sendOrderAndWait(accountId, contractSymbol, quantity, action, orderType, price);
return await this.sendOrderAndWait(accountId, contractSymbol, quantity, action, orderType, price, intent);
} catch (err) {
if (!isOrderNotPlaced(err)) this.suspectAccounts.set(accountId, Date.now());
throw err;
@@ -1585,6 +1586,7 @@ export class DXTradeClient implements PlatformClient {
action: 'Buy' | 'Sell',
orderType: 'Market' | 'Limit',
price?: number,
intent?: OrderIntent,
): Promise<Record<string, unknown>> {
const { account, symbol, step } = this.orderContext(accountId, contractSymbol);
const side: 'BUY' | 'SELL' = action === 'Buy' ? 'BUY' : 'SELL';
@@ -1594,8 +1596,13 @@ export class DXTradeClient implements PlatformClient {
} catch (err) {
throw new OrderNotPlacedError(`DXtrade position check failed — order not sent: ${errorMessage(err)}`);
}
const existing = positions.find((p) => p.symbol === symbol);
if (existing && existing.side === side) throw new OrderNotPlacedError(`adding to an open ${symbol} position is not supported`);
const held = positions.find((p) => p.symbol === symbol);
const sideOf = (s: 'BUY' | 'SELL'): 'long' | 'short' => (s === 'BUY' ? 'long' : 'short');
const mine = this.positionsDetail.get(accountId)?.find((p) => p.symbol === symbol);
// orderEffect picks OPEN or CLOSE; `existing` is the position a CLOSE closes.
const existing = orderEffect({
symbol, held: held ? sideOf(held.side) : null, recorded: mine ? sideOf(mine.side) : null, side: sideOf(side), intent,
}) === 'close' ? held : undefined;
const code = newOrderCode(existing ? 'x' : 'e');
let body: Record<string, unknown>;
+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. */
+7 -6
View File
@@ -47,13 +47,13 @@ import {
import { getCachedFirm } from './firm-catalog';
import { firmInstruments } from './instruments';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext, OrderIntent } from './platform-client';
import { OrderNotPlacedError, NothingToCloseError, isOrderNotPlaced, errorMessage } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage, orderEffect } from './platform-client';
import { encodeContractName, decodeContractName, roundToStep, scoreCandidate } from './dxtrade-helpers';
import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from './vest-session';
import {
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal, resetSafeStop,
parseHistoryPosition, jwtClaimStrings, accountScopedToken, vestOrderEffect, VEST_STATUS_ACTIVE, VEST_TXN, VEST_STOP_SLIPPAGE_FRACTION,
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_TXN, VEST_STOP_SLIPPAGE_FRACTION,
} from './vest-helpers';
import type { VestCapitalAccount, VestPosition, VestOpenOrder, VestTxn, VestTrade } from './vest-helpers';
@@ -1606,7 +1606,7 @@ export class VestClient implements PlatformClient {
/**
* Place an order and wait for its fill: an open when the account is flat,
* a close (or reduce) of the position when it holds the opposite side —
* only the one `intent` names, when given (vestOrderEffect). Adding to a
* only the one `intent` names, when given (orderEffect). Adding to a
* position is refused, and so is an open against another account's
* opposite position in the same symbol (no hedging). Resolves with the
* fill `price`.
@@ -1645,9 +1645,10 @@ export class VestClient implements PlatformClient {
} catch (err) {
throw new OrderNotPlacedError(`Vest position check failed — order not sent: ${errorMessage(err)}`);
}
const existing = positions.find((p) => p.symbol === symbol);
const route = vestOrderEffect({ symbol, held: existing?.side ?? null, side, intent });
if ('refused' in route) throw route.nothingToClose ? new NothingToCloseError(route.refused) : new OrderNotPlacedError(route.refused);
const held = positions.find((p) => p.symbol === symbol);
const recorded = this.positionsDetail.get(accountId)?.find((p) => p.symbol === symbol)?.side ?? null;
// orderEffect picks open or close; `existing` is the position a close closes.
const existing = orderEffect({ symbol, held: held?.side ?? null, recorded, side, intent }) === 'close' ? held : undefined;
const units = roundToStep(contracts * step, step);
const qty = (n: number) => formatDecimal(n, spec.sizeDecimals);
-28
View File
@@ -421,34 +421,6 @@ export function unrealizedPnl(p: Pick<VestPosition, 'side' | 'quantity' | 'openP
return (p.side === 'long' ? 1 : -1) * p.quantity * (mark - p.openPrice);
}
/**
* What an order on `side` does to the account's position in the symbol
* (`held`: its side, null when flat), or why nothing is sent. With no intent
* a flat account opens and the opposite side closes or reduces. 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: nothingToClose says the account
* is flat in the symbol. Adding to a position is never supported.
*/
export function vestOrderEffect(o: {
symbol: string;
held: 'long' | 'short' | null;
side: 'long' | 'short';
intent?: 'open' | 'close';
}): { effect: 'open' | 'close' } | { refused: string; nothingToClose?: true } {
const { symbol, held, side, intent } = o;
if (held === side) {
return {
refused: 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') return { refused: `a ${held} ${symbol} position is already open — an entry never closes it, order not sent` };
if (!held && intent === 'close') return { refused: `no ${symbol} position to close — nothing sent`, nothingToClose: true };
return { effect: held ? 'close' : 'open' };
}
// ── transactions ────────────────────────────────────────────────────────────
/**