Vest: a crypto leg's stop holds back the engine's default slippage, not NDX's
Crypto (exchangeInfo asset "crypto": BTC, ETH, SOL, HYPE) trades on weekends when NDX, SPX and GC are shut, but its stop slippage on Vest is unmeasured, and its book is a fraction of NDX's: about $10M an hour on a weekend against $600M, with a batch's stops firing together. Its stops now leave 0.1% of the position unspent (the engine's default) instead of the 0.02% measured on NDX stop-outs; every other symbol keeps 0.02%. The share rides on the picked contract and is used wherever a Vest leg's stop is sized: the batch's plan, the leg's own stop, the dashboard's next stop, and the 7:55 PM ET reset guard, which now sums each open position's own share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
618976bf5f
commit
10da0ca83e
+10
-3
@@ -4,7 +4,7 @@ import { getClients } from '@/lib/clients';
|
|||||||
import { getAccountTypeById, getCachedCatalog } from '@/lib/firm-catalog';
|
import { getAccountTypeById, getCachedCatalog } from '@/lib/firm-catalog';
|
||||||
import { ruleDiffs } from '@/lib/kept-rules';
|
import { ruleDiffs } from '@/lib/kept-rules';
|
||||||
import { getAccountConfig, getInferredSize, matchAccountType } from '@/lib/account-type-resolver';
|
import { getAccountConfig, getInferredSize, matchAccountType } from '@/lib/account-type-resolver';
|
||||||
import { historyFresh } from '@/lib/platform-client';
|
import { historyFresh, type PlatformContract } from '@/lib/platform-client';
|
||||||
import { rankNextLeg, dayLossRoom } from '@/lib/leg-estimate';
|
import { rankNextLeg, dayLossRoom } from '@/lib/leg-estimate';
|
||||||
import { obfuscationDayBooked } from '@/lib/auto-trade';
|
import { obfuscationDayBooked } from '@/lib/auto-trade';
|
||||||
import type { AccountState } from '@/types';
|
import type { AccountState } from '@/types';
|
||||||
@@ -176,9 +176,16 @@ export async function GET() {
|
|||||||
if (plan && !dayBooked && botStopsActive(cfg, effective)) {
|
if (plan && !dayBooked && botStopsActive(cfg, effective)) {
|
||||||
const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday);
|
const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday);
|
||||||
// The reserve grows with the position (dllReserve): at
|
// The reserve grows with the position (dllReserve): at
|
||||||
// the size the next leg would trade, where it's known.
|
// the size the next leg would trade, on its instrument,
|
||||||
|
// where it's known.
|
||||||
|
const nextContract = holdEstimate
|
||||||
|
? (client as { lastCandidates?: PlatformContract[] }).lastCandidates?.find((c) => String(c.id) === holdEstimate!.symbol)
|
||||||
|
: undefined;
|
||||||
const hard = dllRoom !== null && !lastStandNext
|
const hard = dllRoom !== null && !lastStandNext
|
||||||
? dllStopDollars({ room: dllRoom, dailyLossLimit: dll, entryFee: 0, commissionPct: 0, notional: holdEstimate?.notional, slippageFraction: client.stopSlippageFraction })
|
? dllStopDollars({
|
||||||
|
room: dllRoom, dailyLossLimit: dll, entryFee: 0, commissionPct: 0, notional: holdEstimate?.notional,
|
||||||
|
slippageFraction: nextContract?.stopSlippageFraction ?? client.stopSlippageFraction,
|
||||||
|
})
|
||||||
: Infinity;
|
: Infinity;
|
||||||
const stop = Math.min(Number.isFinite(obf) && obf > 0 ? obf : Infinity, hard);
|
const stop = Math.min(Number.isFinite(obf) && obf > 0 ? obf : Infinity, hard);
|
||||||
if (Number.isFinite(stop) && stop > 0) nextStop = Math.round(stop * 100) / 100;
|
if (Number.isFinite(stop) && stop > 0) nextStop = Math.round(stop * 100) / 100;
|
||||||
|
|||||||
+2
-2
@@ -1426,7 +1426,7 @@ function sizeLeg(item: SizingItem, contract: LegContract, legAmount: number, tra
|
|||||||
maxLossRoom: floorRoom,
|
maxLossRoom: floorRoom,
|
||||||
obfuscationDollars: obfuscation,
|
obfuscationDollars: obfuscation,
|
||||||
availableFunds: live.availableFunds ?? live.amount,
|
availableFunds: live.availableFunds ?? live.amount,
|
||||||
slippageFraction: item.client.stopSlippageFraction,
|
slippageFraction: contract.stopSlippageFraction ?? item.client.stopSlippageFraction,
|
||||||
});
|
});
|
||||||
return { ...plan, pct, obfuscation, maxLossRoom: floorRoom };
|
return { ...plan, pct, obfuscation, maxLossRoom: floorRoom };
|
||||||
}
|
}
|
||||||
@@ -1577,7 +1577,7 @@ async function fireLeg(
|
|||||||
const dllFee = pct > 0 ? entryFee : (measured ?? MAX_COMMISSION_PER_CONTRACT * contracts);
|
const dllFee = pct > 0 ? entryFee : (measured ?? MAX_COMMISSION_PER_CONTRACT * contracts);
|
||||||
const notional = fill.price * pv * contracts;
|
const notional = fill.price * pv * contracts;
|
||||||
const dllDollars = dllOn && !plan.lastStand
|
const dllDollars = dllOn && !plan.lastStand
|
||||||
? dllStopDollars({ room: item.dllRoom, dailyLossLimit: dll, entryFee: dllFee, commissionPct: pct, notional, slippageFraction: client.stopSlippageFraction })
|
? dllStopDollars({ room: item.dllRoom, dailyLossLimit: dll, entryFee: dllFee, commissionPct: pct, notional, slippageFraction: legContract.stopSlippageFraction ?? client.stopSlippageFraction })
|
||||||
: Infinity;
|
: Infinity;
|
||||||
const ticks = stopTicks({ obfuscationDollars: obfuscation, dllDollars, pointValue: pv, contracts, tickSize: legContract.tickSize });
|
const ticks = stopTicks({ obfuscationDollars: obfuscation, dllDollars, pointValue: pv, contracts, tickSize: legContract.tickSize });
|
||||||
if (ticks === 0) {
|
if (ticks === 0) {
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ export function rankNextLeg(o: {
|
|||||||
maxLossRoom: floorRoom,
|
maxLossRoom: floorRoom,
|
||||||
obfuscationDollars: o.obfuscationDollars,
|
obfuscationDollars: o.obfuscationDollars,
|
||||||
availableFunds: funds,
|
availableFunds: funds,
|
||||||
slippageFraction: o.client.stopSlippageFraction,
|
slippageFraction: c.stopSlippageFraction ?? o.client.stopSlippageFraction,
|
||||||
});
|
});
|
||||||
const p = plan(true);
|
const p = plan(true);
|
||||||
if (!p.sizing || p.cost === null) return [];
|
if (!p.sizing || p.cost === null) return [];
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ export interface PlatformContract {
|
|||||||
* says: no leg starts that wouldn't be done by then (clearsClose), each
|
* says: no leg starts that wouldn't be done by then (clearsClose), each
|
||||||
* account's at its own size (fireLeg). */
|
* account's at its own size (fireLeg). */
|
||||||
closesAt?: number;
|
closesAt?: number;
|
||||||
|
/** This instrument's share of notional a stop may fill past its price,
|
||||||
|
* where it differs from the client's stopSlippageFraction (Vest: crypto,
|
||||||
|
* unmeasured, keeps the engine's default). */
|
||||||
|
stopSlippageFraction?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What an order is for, where the caller knows (see sendOrder). */
|
/** What an order is for, where the caller knows (see sendOrder). */
|
||||||
|
|||||||
+12
-5
@@ -53,7 +53,7 @@ import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession }
|
|||||||
import {
|
import {
|
||||||
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
|
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
|
||||||
parseTxn, bookTxns, awaitingLegs, unknownOpenFill, unrealizedPnl, hourlyVolatility, marketOpen, longClosure, num, toMs, formatDecimal, resetSafeStop,
|
parseTxn, bookTxns, awaitingLegs, unknownOpenFill, unrealizedPnl, hourlyVolatility, marketOpen, longClosure, num, toMs, formatDecimal, resetSafeStop,
|
||||||
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_STOP_SLIPPAGE_FRACTION,
|
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_STOP_SLIPPAGE_FRACTION, vestStopSlippageFraction,
|
||||||
vestStatusName, awaitingActivation,
|
vestStatusName, awaitingActivation,
|
||||||
VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, settleClaims, readClaims, payoutDaysOf, payoutsSince,
|
VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, settleClaims, readClaims, payoutDaysOf, payoutsSince,
|
||||||
} from './vest-helpers';
|
} from './vest-helpers';
|
||||||
@@ -155,6 +155,9 @@ interface SymbolSpec {
|
|||||||
marginRate: number;
|
marginRate: number;
|
||||||
tradingStatus: string;
|
tradingStatus: string;
|
||||||
capitalTradingStatus: string;
|
capitalTradingStatus: string;
|
||||||
|
/** exchangeInfo's asset class: "crypto", "forex", "stock" (indices and
|
||||||
|
* commodities too). */
|
||||||
|
asset: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VestConfig {
|
export interface VestConfig {
|
||||||
@@ -558,6 +561,7 @@ export class VestClient implements PlatformClient {
|
|||||||
marginRate: capitalIm && capitalIm > 0 ? capitalIm : 0,
|
marginRate: capitalIm && capitalIm > 0 ? capitalIm : 0,
|
||||||
tradingStatus: String(s.tradingStatus ?? ''),
|
tradingStatus: String(s.tradingStatus ?? ''),
|
||||||
capitalTradingStatus: String(s.capitalTradingStatus ?? s.tradingStatus ?? ''),
|
capitalTradingStatus: String(s.capitalTradingStatus ?? s.tradingStatus ?? ''),
|
||||||
|
asset: String(s.asset ?? ''),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (next.size > 0) {
|
if (next.size > 0) {
|
||||||
@@ -1230,13 +1234,15 @@ export class VestClient implements PlatformClient {
|
|||||||
|| (x.currentStop !== null && y.currentStop !== null ? lossAt(x.pos, x.currentStop) - lossAt(y.pos, y.currentStop) : 0)
|
|| (x.currentStop !== null && y.currentStop !== null ? lossAt(x.pos, x.currentStop) - lossAt(y.pos, y.currentStop) : 0)
|
||||||
|| x.pos.openedAt - y.pos.openedAt || x.pos.positionId.localeCompare(y.pos.positionId));
|
|| x.pos.openedAt - y.pos.openedAt || x.pos.positionId.localeCompare(y.pos.positionId));
|
||||||
let committed = 0;
|
let committed = 0;
|
||||||
let notionalSoFar = 0;
|
// The slippage reserve the positions so far hold together, in
|
||||||
|
// dollars: each its own symbol's share of its notional.
|
||||||
|
let slippageSoFar = 0;
|
||||||
for (const { pos, currentStop } of held) {
|
for (const { pos, currentStop } of held) {
|
||||||
const spec = specs.get(pos.symbol);
|
const spec = specs.get(pos.symbol);
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
// Can't be fitted without its tick, but it still holds
|
// Can't be fitted without its tick, but it still holds
|
||||||
// its loss against the room the others share.
|
// its loss against the room the others share.
|
||||||
notionalSoFar += pos.quantity * pos.openPrice;
|
slippageSoFar += this.stopSlippageFraction * pos.quantity * pos.openPrice;
|
||||||
if (currentStop !== null) committed += lossAt(pos, currentStop);
|
if (currentStop !== null) committed += lossAt(pos, currentStop);
|
||||||
else if (!guard.unfittable.has(pos.positionId)) {
|
else if (!guard.unfittable.has(pos.positionId)) {
|
||||||
guard.unfittable.add(pos.positionId);
|
guard.unfittable.add(pos.positionId);
|
||||||
@@ -1248,13 +1254,13 @@ export class VestClient implements PlatformClient {
|
|||||||
const mark = this.marks.get(pos.symbol)?.mark ?? NaN;
|
const mark = this.marks.get(pos.symbol)?.mark ?? NaN;
|
||||||
const stopLeg = pos.stopLosses.find((l) => l.status === 'awaiting') ?? null;
|
const stopLeg = pos.stopLosses.find((l) => l.status === 'awaiting') ?? null;
|
||||||
const notional = pos.quantity * (Number.isFinite(mark) ? mark : pos.openPrice);
|
const notional = pos.quantity * (Number.isFinite(mark) ? mark : pos.openPrice);
|
||||||
notionalSoFar += notional;
|
slippageSoFar += vestStopSlippageFraction(spec.asset) * notional;
|
||||||
const exitFee = spec.takerFee * notional;
|
const exitFee = spec.takerFee * notional;
|
||||||
const decision = resetSafeStop({
|
const decision = resetSafeStop({
|
||||||
side: pos.side, quantity: pos.quantity, openPrice: pos.openPrice, mark,
|
side: pos.side, quantity: pos.quantity, openPrice: pos.openPrice, mark,
|
||||||
balance: cash.amount - committed, dailyLossPct: a.maxDailyLossPct, maxDrawdownFloor: a.maxDrawdownLimit,
|
balance: cash.amount - committed, dailyLossPct: a.maxDailyLossPct, maxDrawdownFloor: a.maxDrawdownLimit,
|
||||||
floorBalance: cash.amount,
|
floorBalance: cash.amount,
|
||||||
reserve: dllReserve(cash.amount * a.maxDailyLossPct, notionalSoFar, this.stopSlippageFraction),
|
reserve: dllReserve(cash.amount * a.maxDailyLossPct, slippageSoFar, 1),
|
||||||
exitFeeRate: spec.takerFee, currentStop, tick: spec.tick,
|
exitFeeRate: spec.takerFee, currentStop, tick: spec.tick,
|
||||||
});
|
});
|
||||||
const stopAt = decision.action === 'stop' ? decision.price : decision.action === 'none' ? currentStop : null;
|
const stopAt = decision.action === 'stop' ? decision.price : decision.action === 'none' ? currentStop : null;
|
||||||
@@ -2022,6 +2028,7 @@ export class VestClient implements PlatformClient {
|
|||||||
minContracts: 1,
|
minContracts: 1,
|
||||||
marginRate: spec.marginRate || undefined,
|
marginRate: spec.marginRate || undefined,
|
||||||
closesAt: gateAt ?? undefined,
|
closesAt: gateAt ?? undefined,
|
||||||
|
stopSlippageFraction: vestStopSlippageFraction(spec.asset),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,18 @@ export function nextVestResetMs(now: number): number {
|
|||||||
* past the trigger on average, 2.25 at worst — the 0.1% default held back
|
* past the trigger on average, 2.25 at worst — the 0.1% default held back
|
||||||
* about 30. The flat 10% of the limit still applies under it. */
|
* about 30. The flat 10% of the limit still applies under it. */
|
||||||
export const VEST_STOP_SLIPPAGE_FRACTION = 0.0002;
|
export const VEST_STOP_SLIPPAGE_FRACTION = 0.0002;
|
||||||
|
/** Crypto's (asset "crypto" in exchangeInfo: BTC, ETH, SOL, HYPE): unmeasured,
|
||||||
|
* so the engine's default share (trading-logic's DLL_SLIPPAGE_FRACTION).
|
||||||
|
* Vest's BTC book trades ~$10M an hour on a weekend against NDX's ~$600M, and
|
||||||
|
* a batch's stops fire together: a square-root impact estimate scaled to the
|
||||||
|
* NDX measurement puts the worst fill near 7bp. To come down once crypto
|
||||||
|
* stop-outs are measured, as NDX's was. */
|
||||||
|
export const VEST_CRYPTO_STOP_SLIPPAGE_FRACTION = 0.001;
|
||||||
|
|
||||||
|
/** The stop-slippage share for a symbol of this exchangeInfo asset class. */
|
||||||
|
export function vestStopSlippageFraction(asset: string | undefined): number {
|
||||||
|
return asset === 'crypto' ? VEST_CRYPTO_STOP_SLIPPAGE_FRACTION : VEST_STOP_SLIPPAGE_FRACTION;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What to do with an open position before the daily reset so the reset can't
|
* What to do with an open position before the daily reset so the reset can't
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const { putVestSession, vestSessionFor, dropVestSession } = await import('../lib
|
|||||||
const {
|
const {
|
||||||
botOpenedPositions, loadAccountMeta, saveAccountMeta, saveFundTransactions, wipeAccountData, loadClaimedCycles, saveClaimedCycles,
|
botOpenedPositions, loadAccountMeta, saveAccountMeta, saveFundTransactions, wipeAccountData, loadClaimedCycles, saveClaimedCycles,
|
||||||
} = await import('../lib/db.ts');
|
} = await import('../lib/db.ts');
|
||||||
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts');
|
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE, resetSafeStop } = await import('../lib/vest-helpers.ts');
|
||||||
const { encodeContractName } = await import('../lib/dxtrade-helpers.ts');
|
const { encodeContractName } = await import('../lib/dxtrade-helpers.ts');
|
||||||
const { tradeDateFor, stageInputsFor, resolveEffectiveConfig, claimedCycle, endedCycleCompletion } = await import('../lib/trading-logic.ts');
|
const { tradeDateFor, stageInputsFor, resolveEffectiveConfig, claimedCycle, endedCycleCompletion } = await import('../lib/trading-logic.ts');
|
||||||
const { isClientReady, isOrderNotPlaced } = await import('../lib/platform-client.ts');
|
const { isClientReady, isOrderNotPlaced } = await import('../lib/platform-client.ts');
|
||||||
@@ -93,10 +93,10 @@ const week = [
|
|||||||
shut('2026-10-30T21:00:00Z', '2026-11-01T23:00:00Z', 'weekly'),
|
shut('2026-10-30T21:00:00Z', '2026-11-01T23:00:00Z', 'weekly'),
|
||||||
open('2026-11-01T23:00:00Z', '2026-11-02T22:00:00Z'),
|
open('2026-11-01T23:00:00Z', '2026-11-02T22:00:00Z'),
|
||||||
];
|
];
|
||||||
const spec = (symbol, assetId) => ({
|
const spec = (symbol, assetId, asset = 'stock') => ({
|
||||||
symbol, assetId, sizeDecimals: 3, priceDecimals: 2, tick: 0.01, takerFee: 0.0001, marginRate: 0.05, tradingStatus: 'TRADING', capitalTradingStatus: 'TRADING',
|
symbol, assetId, sizeDecimals: 3, priceDecimals: 2, tick: 0.01, takerFee: 0.0001, marginRate: 0.05, tradingStatus: 'TRADING', capitalTradingStatus: 'TRADING', asset,
|
||||||
});
|
});
|
||||||
const specs = new Map([['NDX-USD', spec('NDX-USD', 7)], ['BTC-PERP', spec('BTC-PERP', 9)]]);
|
const specs = new Map([['NDX-USD', spec('NDX-USD', 7)], ['BTC-PERP', spec('BTC-PERP', 9, 'crypto')]]);
|
||||||
|
|
||||||
/** A client with no sync running, its gateway answered by `answer`, holding
|
/** A client with no sync running, its gateway answered by `answer`, holding
|
||||||
* acc-1 and acc-2 under `ids` (the ids a sync gives them: A and B). */
|
* acc-1 and acc-2 under `ids` (the ids a sync gives them: A and B). */
|
||||||
@@ -221,6 +221,9 @@ test('a symbol the close gate holds back stays on the board, and isn\'t picked;
|
|||||||
assert.deepEqual(client.lastCandidates.map((c) => c.id).sort(), ['BTC-PERP', 'NDX-USD']);
|
assert.deepEqual(client.lastCandidates.map((c) => c.id).sort(), ['BTC-PERP', 'NDX-USD']);
|
||||||
const ndx = client.lastCandidates.find((c) => c.id === 'NDX-USD');
|
const ndx = client.lastCandidates.find((c) => c.id === 'NDX-USD');
|
||||||
assert.equal(ndx.closesAt, iso('2026-10-30T21:00:00Z'));
|
assert.equal(ndx.closesAt, iso('2026-10-30T21:00:00Z'));
|
||||||
|
// Crypto's stops keep the engine's slippage share; NDX's is measured.
|
||||||
|
assert.equal(pick.stopSlippageFraction, 0.001);
|
||||||
|
assert.equal(ndx.stopSlippageFraction, 0.0002);
|
||||||
// The survey keeps it up for the dashboard, though nothing is picked.
|
// The survey keeps it up for the dashboard, though nothing is picked.
|
||||||
assert.equal(await client.pickCandidate('survey', ['NDX-USD'], false), null);
|
assert.equal(await client.pickCandidate('survey', ['NDX-USD'], false), null);
|
||||||
assert.deepEqual(client.lastCandidates.map((c) => c.id), ['NDX-USD']);
|
assert.deepEqual(client.lastCandidates.map((c) => c.id), ['NDX-USD']);
|
||||||
@@ -769,3 +772,42 @@ test('exits wait for the login session to be relayed again; an open goes now or
|
|||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the reset guard holds each position's own slippage reserve: crypto's wider than NDX's", async () => {
|
||||||
|
// 7:56 PM ET: four minutes to Vest's reset. acc-1 holds BTC, acc-2 NDX,
|
||||||
|
// neither with a stop.
|
||||||
|
clockAt('2026-10-29T23:56:00Z');
|
||||||
|
const stops = [];
|
||||||
|
const positions = [
|
||||||
|
rawPosition('9201', 'acc-1', iso('2026-10-29T22:00:00Z'), { symbol: 'BTC-PERP', quantity: '5', openPrice: '70000' }),
|
||||||
|
rawPosition('9202', 'acc-2', iso('2026-10-29T22:00:00Z'), { quantity: '15', openPrice: '20000' }),
|
||||||
|
];
|
||||||
|
const client = testClient((method, path, opts) => {
|
||||||
|
if (method === 'GET' && path === '/v3/positions/opened') return { data: { positions } };
|
||||||
|
if (method === 'POST' && path === '/v3/positions/stop-loss') { stops.push({ account: opts.account, ...opts.body }); return { data: {} }; }
|
||||||
|
throw new Error(`no gateway in this test: ${method} ${path}`);
|
||||||
|
});
|
||||||
|
client.refreshMarks = async () => {};
|
||||||
|
client.marks.set('BTC-PERP', { mark: 70000, at: Date.now() });
|
||||||
|
client.marks.set('NDX-USD', { mark: 20000, at: Date.now() });
|
||||||
|
for (const id of [1, 2]) {
|
||||||
|
client.capital.set(id, { status: VEST_STATUS_ACTIVE, maxDailyLossPct: 0.03, maxDrawdownLimit: 47000 });
|
||||||
|
client.accountCashBalances[id] = { amount: 50000, realizedPnL: 0 };
|
||||||
|
}
|
||||||
|
client.syncComplete = true;
|
||||||
|
const fitted = (quantity, openPrice, reserve) => resetSafeStop({
|
||||||
|
side: 'long', quantity, openPrice, mark: openPrice, balance: 50000, floorBalance: 50000, dailyLossPct: 0.03,
|
||||||
|
maxDrawdownFloor: 47000, reserve, exitFeeRate: 0.0001, currentStop: null, tick: 0.01,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await client.guardReset();
|
||||||
|
const by = Object.fromEntries(stops.map((s) => [s.account, Number(s.triggerPrice)]));
|
||||||
|
// BTC: 0.1% of $350k = $350 held back, over the flat $150.
|
||||||
|
assert.equal(by['acc-1'], fitted(5, 70000, 350).price);
|
||||||
|
assert.notEqual(by['acc-1'], fitted(5, 70000, 150).price);
|
||||||
|
// NDX: 0.02% of $300k is $60, so the flat $150 stands.
|
||||||
|
assert.equal(by['acc-2'], fitted(15, 20000, 150).price);
|
||||||
|
} finally {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
|
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
|
||||||
accountScopedToken, VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, claimWithdrawal, settleClaims,
|
accountScopedToken, VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, claimWithdrawal, settleClaims,
|
||||||
claimFundRows, readClaims, payoutDaysOf, payoutsSince, longClosure, unknownOpenFill, awaitingActivation, vestStatusName,
|
claimFundRows, readClaims, payoutDaysOf, payoutsSince, longClosure, unknownOpenFill, awaitingActivation, vestStatusName,
|
||||||
VEST_STATUS,
|
VEST_STATUS, vestStopSlippageFraction, VEST_STOP_SLIPPAGE_FRACTION, VEST_CRYPTO_STOP_SLIPPAGE_FRACTION,
|
||||||
} 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 { computeDayPlan, currentTradeDateCT, tradeDateFor, cycleCompletion } from '../lib/trading-logic.ts';
|
import { computeDayPlan, currentTradeDateCT, tradeDateFor, cycleCompletion } from '../lib/trading-logic.ts';
|
||||||
@@ -664,3 +664,9 @@ test('a refused account token falls back to the login\'s and stays refused', ()
|
|||||||
refuseVestSession(wallet2, account.replace('acct', 'accu'), now);
|
refuseVestSession(wallet2, account.replace('acct', 'accu'), now);
|
||||||
assert.equal(vestSessionFor(wallet2, now), null);
|
assert.equal(vestSessionFor(wallet2, now), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a crypto stop's slippage share is the engine default; everything else keeps NDX's measured one", () => {
|
||||||
|
assert.equal(VEST_CRYPTO_STOP_SLIPPAGE_FRACTION, 0.001);
|
||||||
|
assert.equal(vestStopSlippageFraction('crypto'), VEST_CRYPTO_STOP_SLIPPAGE_FRACTION);
|
||||||
|
for (const asset of ['stock', 'forex', '', undefined]) assert.equal(vestStopSlippageFraction(asset), VEST_STOP_SLIPPAGE_FRACTION);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user