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

sendOrder takes an optional intent. On Vest an 'open' is refused when the
account already holds the symbol, where it used to close that position as
the "entry"; the leg's exits then found nothing and its flatten opened a
fresh position with no TP or stop. A 'close' only closes or reduces, and on
a flat account is refused with NothingToCloseError, which flattenAndFail
reports as nothing to flatten instead of FLATTEN FAILED. Entries, the
extra-day round trip and flattens pass their intent; the manual order route
and the other platforms keep today's behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 14:03:14 -05:00
co-authored by Claude Opus 5.5
parent beddbdcf6c
commit e90f21ed91
5 changed files with 127 additions and 20 deletions
+13 -9
View File
@@ -46,14 +46,14 @@ import {
} from './db';
import { getCachedFirm } from './firm-catalog';
import { firmInstruments } from './instruments';
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, NothingToCloseError, isOrderNotPlaced, errorMessage } 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, VEST_STATUS_ACTIVE, VEST_TXN, VEST_STOP_SLIPPAGE_FRACTION,
parseHistoryPosition, jwtClaimStrings, accountScopedToken, vestOrderEffect, VEST_STATUS_ACTIVE, VEST_TXN, VEST_STOP_SLIPPAGE_FRACTION,
} from './vest-helpers';
import type { VestCapitalAccount, VestPosition, VestOpenOrder, VestTxn, VestTrade } from './vest-helpers';
@@ -1605,10 +1605,11 @@ 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.
* 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 (or reduce) of the position when it holds the opposite side —
* only the one `intent` names, when given (vestOrderEffect). 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,
@@ -1617,9 +1618,10 @@ export class VestClient 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;
@@ -1633,6 +1635,7 @@ export class VestClient implements PlatformClient {
action: 'Buy' | 'Sell',
orderType: 'Market' | 'Limit',
price?: number,
intent?: OrderIntent,
): Promise<Record<string, unknown>> {
const { uuid, symbol, step, spec } = this.orderContext(accountId, contractSymbol);
const side: 'long' | 'short' = action === 'Buy' ? 'long' : 'short';
@@ -1643,7 +1646,8 @@ export class VestClient implements PlatformClient {
throw new OrderNotPlacedError(`Vest 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 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 units = roundToStep(contracts * step, step);
const qty = (n: number) => formatDecimal(n, spec.sizeDecimals);