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, * (OrderNotPlacedError). Anything ambiguous, like an ack or fill timeout,
* keeps the leg: the order may well have filled, and a leg counted that never * 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 * 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 * account twice. Open-only: where the platform checks (Vest, DXtrade), an
* already holds the symbol — a hand trade the synced positions don't show * account that already holds the symbol — a hand trade the synced positions
* yet — is refused rather than having that position closed as the "entry". * don't show yet — is refused rather than having that position closed as the
* "entry".
*/ */
async function sendEntry( async function sendEntry(
tag: string, 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 * 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 * 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 * 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 * where the platform checks (Vest, DXtrade) and its own record agrees, that
* leg still fails, but without the alarm. * 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( async function flattenAndFail(
tag: string, tag: string,
+16 -9
View File
@@ -42,8 +42,8 @@ import { getAccountConfig } from './account-type-resolver';
import { getCachedFirm } from './firm-catalog'; import { getCachedFirm } from './firm-catalog';
import { firmInstruments } from './instruments'; import { firmInstruments } from './instruments';
import type { SymbolMapCandidate } from './master-protocol'; import type { SymbolMapCandidate } from './master-protocol';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client'; import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext, OrderIntent } from './platform-client';
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client'; import { OrderNotPlacedError, isOrderNotPlaced, errorMessage, orderEffect } from './platform-client';
import { import {
dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep, dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep,
lastUtcReset, ctTradeDayStart, firmDayOf, timeOf, parseDurationMs, ciNum, isFundType, signedFundValue, newOrderCode, 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, * 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 * a CLOSE of the position when it holds the opposite side — only the one
* position is refused, and so is an OPEN against another account's * `intent` names, when given (orderEffect). Adding to a position is
* opposite position in the same symbol (no hedging). Resolves with the * refused, and so is an OPEN against another account's opposite position
* fill `price`. * in the same symbol (no hedging). Resolves with the fill `price`.
*/ */
public async sendOrder( public async sendOrder(
accountId: number, accountId: number,
@@ -1569,9 +1569,10 @@ export class DXTradeClient implements PlatformClient {
action: 'Buy' | 'Sell', action: 'Buy' | 'Sell',
orderType: 'Market' | 'Limit', orderType: 'Market' | 'Limit',
price?: number, price?: number,
intent?: OrderIntent,
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
try { try {
return await this.sendOrderAndWait(accountId, contractSymbol, quantity, action, orderType, price); return await this.sendOrderAndWait(accountId, contractSymbol, quantity, action, orderType, price, intent);
} catch (err) { } catch (err) {
if (!isOrderNotPlaced(err)) this.suspectAccounts.set(accountId, Date.now()); if (!isOrderNotPlaced(err)) this.suspectAccounts.set(accountId, Date.now());
throw err; throw err;
@@ -1585,6 +1586,7 @@ export class DXTradeClient implements PlatformClient {
action: 'Buy' | 'Sell', action: 'Buy' | 'Sell',
orderType: 'Market' | 'Limit', orderType: 'Market' | 'Limit',
price?: number, price?: number,
intent?: OrderIntent,
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const { account, symbol, step } = this.orderContext(accountId, contractSymbol); const { account, symbol, step } = this.orderContext(accountId, contractSymbol);
const side: 'BUY' | 'SELL' = action === 'Buy' ? 'BUY' : 'SELL'; const side: 'BUY' | 'SELL' = action === 'Buy' ? 'BUY' : 'SELL';
@@ -1594,8 +1596,13 @@ export class DXTradeClient implements PlatformClient {
} catch (err) { } catch (err) {
throw new OrderNotPlacedError(`DXtrade position check failed — order not sent: ${errorMessage(err)}`); throw new OrderNotPlacedError(`DXtrade position check failed — order not sent: ${errorMessage(err)}`);
} }
const existing = positions.find((p) => p.symbol === symbol); const held = positions.find((p) => p.symbol === symbol);
if (existing && existing.side === side) throw new OrderNotPlacedError(`adding to an open ${symbol} position is not supported`); 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'); const code = newOrderCode(existing ? 'x' : 'e');
let body: Record<string, unknown>; 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 /** `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 * ('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 * flatten ('close') must never open one on an account that turned out
* flat. A platform that checks it refuses the order otherwise * flat. A platform that checks it (orderEffect) refuses the order
* (NothingToCloseError for a close with nothing to close); left out, or * otherwise (NothingToCloseError for a close with nothing to close);
* on a platform that doesn't check, the position decides. */ * left out, or on a platform that doesn't check, the position decides. */
sendOrder( sendOrder(
accountId: number, accountId: number,
contractSymbol: string, contractSymbol: string,
@@ -464,9 +464,9 @@ export function isOrderNotPlaced(err: unknown): boolean {
/** /**
* A close-only order (sendOrder's 'close') refused because a fresh read * 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 * showed no position in the symbol to close, and the client's own record
* flatten that gets this has nothing left to do — nothing rides without its * agrees: the account is flat there, so a flatten that gets this has nothing
* stop. * left to do — nothing rides without its stop.
*/ */
export class NothingToCloseError extends OrderNotPlacedError { export class NothingToCloseError extends OrderNotPlacedError {
readonly nothingToClose = true; readonly nothingToClose = true;
@@ -481,6 +481,44 @@ export function isNothingToClose(err: unknown): boolean {
return !!err && typeof err === 'object' && (err as { nothingToClose?: unknown }).nothingToClose === true; 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 /** 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 * config — Tradovate's login body (the password) and Bearer tokens — so they
* must never be logged whole. */ * must never be logged whole. */
+7 -6
View File
@@ -47,13 +47,13 @@ import {
import { getCachedFirm } from './firm-catalog'; import { getCachedFirm } from './firm-catalog';
import { firmInstruments } from './instruments'; import { firmInstruments } from './instruments';
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext, OrderIntent } from './platform-client'; 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 { encodeContractName, decodeContractName, roundToStep, scoreCandidate } from './dxtrade-helpers';
import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from './vest-session'; import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from './vest-session';
import { import {
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder, vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal, resetSafeStop, 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'; } from './vest-helpers';
import type { VestCapitalAccount, VestPosition, VestOpenOrder, VestTxn, VestTrade } 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, * 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 — * 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 * position is refused, and so is an open against another account's
* opposite position in the same symbol (no hedging). Resolves with the * opposite position in the same symbol (no hedging). Resolves with the
* fill `price`. * fill `price`.
@@ -1645,9 +1645,10 @@ export class VestClient implements PlatformClient {
} catch (err) { } catch (err) {
throw new OrderNotPlacedError(`Vest position check failed — order not sent: ${errorMessage(err)}`); throw new OrderNotPlacedError(`Vest position check failed — order not sent: ${errorMessage(err)}`);
} }
const existing = positions.find((p) => p.symbol === symbol); const held = positions.find((p) => p.symbol === symbol);
const route = vestOrderEffect({ symbol, held: existing?.side ?? null, side, intent }); const recorded = this.positionsDetail.get(accountId)?.find((p) => p.symbol === symbol)?.side ?? null;
if ('refused' in route) throw route.nothingToClose ? new NothingToCloseError(route.refused) : new OrderNotPlacedError(route.refused); // 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 units = roundToStep(contracts * step, step);
const qty = (n: number) => formatDecimal(n, spec.sizeDecimals); 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); 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 ──────────────────────────────────────────────────────────── // ── transactions ────────────────────────────────────────────────────────────
/** /**
+70
View File
@@ -1131,3 +1131,73 @@ test('the day after a claim: a fresh cycle that aims at the next stage\'s target
assert.ok(plan.dayTarget >= 3000 && plan.dayTarget <= 3025, `dayTarget ${plan.dayTarget}`); assert.ok(plan.dayTarget >= 3000 && plan.dayTarget <= 3025, `dayTarget ${plan.dayTarget}`);
assert.equal(plan.remainingToday, plan.dayTarget); assert.equal(plan.remainingToday, plan.dayTarget);
}); });
// ── what an order is for (lib/platform-client.ts orderEffect) ────────────────
import { readFileSync } from 'node:fs';
import { orderEffect, OrderNotPlacedError, NothingToCloseError, isOrderNotPlaced, isNothingToClose } from '../lib/platform-client.ts';
test('an entry never closes a position, a flatten never opens one (Vest, DXtrade)', () => {
/** The route, or the refusal thrown. `recorded`: the client's own record
* of the symbol, by default what the fresh read shows. */
const effect = (held, side, intent, recorded = held) => {
try {
return orderEffect({ symbol: 'NDX-USD-PERP', held, recorded, side, intent });
} catch (err) {
return err;
}
};
// No intent: the position decides, as before (the manual order route).
assert.equal(effect(null, 'long'), 'open');
assert.equal(effect('short', 'long'), 'close');
assert.match(effect('long', 'long').message, /adding to an open NDX-USD-PERP position/);
// An entry on an account that already holds the symbol — a hand short the
// engine's synced positions didn't show yet — is refused (the leg is
// un-counted), not turned into a close of that short.
assert.equal(effect(null, 'long', 'open'), 'open');
for (const recorded of ['short', null]) {
const entry = effect('short', 'long', 'open', recorded);
assert.equal(isOrderNotPlaced(entry), true);
assert.equal(isNothingToClose(entry), false);
assert.match(entry.message, /short NDX-USD-PERP position is already open/);
}
assert.match(effect('long', 'long', 'open').message, /adding to an open/);
// A flatten closes the leg it was sent for; on a flat account it opens
// nothing, and says there is nothing to close when the record agrees.
assert.equal(effect('long', 'short', 'close'), 'close');
const flat = effect(null, 'short', 'close');
assert.ok(flat instanceof NothingToCloseError);
assert.equal(isOrderNotPlaced(flat), true);
assert.equal(isNothingToClose(flat), true);
assert.match(flat.message, /no NDX-USD-PERP position to close/);
// A flat read just after a fill the client saw (the leg still on record)
// may be the gateway lagging: nothing sent, but not "flat" — the flatten
// keeps its alarm.
const lagging = effect(null, 'short', 'close', 'long');
assert.ok(lagging instanceof OrderNotPlacedError);
assert.equal(isNothingToClose(lagging), false);
assert.match(lagging.message, /fresh read shows no NDX-USD-PERP position, but this client last saw a long one open/);
// Holding the flatten's own side is not the leg: refused, and not "flat".
const wrong = effect('short', 'short', 'close');
assert.ok(wrong instanceof OrderNotPlacedError);
assert.equal(isNothingToClose(wrong), false);
assert.match(wrong.message, /holds a short NDX-USD-PERP position, not one to close/);
assert.equal(isNothingToClose(new OrderNotPlacedError('Vest refused the order: 400')), false);
assert.equal(isNothingToClose(new Error('outcome unknown')), false);
assert.equal(isNothingToClose(undefined), false);
});
test('the engine says what each order is for, and both position-based clients go by it', () => {
const source = (file) => readFileSync(new URL(`../lib/${file}`, import.meta.url), 'utf8');
// An order sent without its intent lets the position decide again: an
// entry closing a hand trade, a flatten opening a position with no exits.
const calls = [...source('auto-trade.ts').matchAll(/client\.sendOrder\(([^;]+?)\);/g)].map((m) => m[1].trim());
assert.ok(calls.length >= 4, `found ${calls.length} sendOrder calls`);
for (const args of calls) assert.match(args, /, '(open|close)'$/, `sendOrder(${args}) names no intent`);
for (const file of ['vest-class.ts', 'dxtrade-class.ts']) {
const src = source(file);
assert.match(src, /return await this\.sendOrderAndWait\([^)]*, intent\);/, `${file}: sendOrder drops the intent`);
assert.match(src, /= orderEffect\(\{[^}]*\bintent\b[^}]*\}\) === 'close' \? held : undefined;/, `${file}: orderEffect doesn't pick the route`);
}
});
+3 -40
View File
@@ -1,6 +1,5 @@
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts), the // Checks for the Vest client's pure helpers (lib/vest-helpers.ts) and the
// relayed-session store (lib/vest-session.ts) and the order refusals it // relayed-session store (lib/vest-session.ts).
// throws (lib/platform-client.ts).
// //
// npm run check // npm run check
// //
@@ -13,10 +12,9 @@ import {
isVestWalletAddress, vestAccountId, num, toMs, formatDecimal, vestDayOf, lastVestResetMs, parseCapitalAccount, isVestWalletAddress, vestAccountId, num, toMs, formatDecimal, vestDayOf, lastVestResetMs, parseCapitalAccount,
vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN, vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN,
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings, hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
accountScopedToken, vestOrderEffect, accountScopedToken,
} from '../lib/vest-helpers.ts'; } from '../lib/vest-helpers.ts';
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts'; import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
import { OrderNotPlacedError, NothingToCloseError, isOrderNotPlaced, isNothingToClose } from '../lib/platform-client.ts';
const iso = (s) => Date.parse(s); const iso = (s) => Date.parse(s);
@@ -144,41 +142,6 @@ test('positions, their legs and unrealized P&L', () => {
assert.equal(parseOpenOrder({ symbol: 'x' }), null); assert.equal(parseOpenOrder({ symbol: 'x' }), null);
}); });
test('an entry never closes a position, a flatten never opens one', () => {
const effect = (held, side, intent) => vestOrderEffect({ symbol: 'NDX-USD-PERP', held, side, intent });
// No intent: the position decides, as before (the manual order route).
assert.deepEqual(effect(null, 'long'), { effect: 'open' });
assert.deepEqual(effect('short', 'long'), { effect: 'close' });
assert.match(effect('long', 'long').refused, /adding to an open NDX-USD-PERP position/);
// An entry on an account that already holds the symbol — a hand short the
// engine's synced positions didn't show yet — is refused, not turned into
// a close of that short.
assert.deepEqual(effect(null, 'long', 'open'), { effect: 'open' });
const entry = effect('short', 'long', 'open');
assert.match(entry.refused, /short NDX-USD-PERP position is already open/);
assert.equal(entry.nothingToClose, undefined);
assert.match(effect('long', 'long', 'open').refused, /adding to an open/);
// A flatten closes the leg it was sent for; on a flat account it opens
// nothing and says there is nothing to close.
assert.deepEqual(effect('long', 'short', 'close'), { effect: 'close' });
const flat = effect(null, 'short', 'close');
assert.match(flat.refused, /no NDX-USD-PERP position to close/);
assert.equal(flat.nothingToClose, true);
// Holding the flatten's own side is not the leg: refused, and not "flat".
const wrong = effect('short', 'short', 'close');
assert.match(wrong.refused, /holds a short NDX-USD-PERP position, not one to close/);
assert.equal(wrong.nothingToClose, undefined);
// The refusal the client throws for it: still "never placed" (the leg is
// un-counted), and told apart from every other refusal.
const nothing = new NothingToCloseError('no NDX-USD-PERP position to close — nothing sent');
assert.equal(isOrderNotPlaced(nothing), true);
assert.equal(isNothingToClose(nothing), true);
assert.equal(isNothingToClose(new OrderNotPlacedError('Vest refused the order: 400')), false);
assert.equal(isNothingToClose(new Error('outcome unknown')), false);
assert.equal(isNothingToClose(undefined), false);
});
test('transactions book into daily P&L and fund rows by Vest day', () => { test('transactions book into daily P&L and fund rows by Vest day', () => {
assert.equal(txnKind(VEST_TXN.FEE), 'trade'); assert.equal(txnKind(VEST_TXN.FEE), 'trade');
assert.equal(txnKind(VEST_TXN.REALIZED_PNL), 'trade'); assert.equal(txnKind(VEST_TXN.REALIZED_PNL), 'trade');