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
+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>;