projectPerfectPath takes the raw trade date (default: the Central one): the reporter passes its tradeDateFor day and the account page the one /api/state now gives each account. From 5 PM CT to 20:00 ET it took tomorrow as today while every input was Vest's, so the day's claim moved a day late, the next day's stage-1 payout dropped, and an open Vest-day row counted twice. Without same-day stages, the stage comes from the client's payout list (payoutDays, one per payout) when it has read one, not the negative fund rows: Vest books a day's two claims as one row, so the engine, claimDue, the stage label and the projection stayed a stage behind the report. StageInputs.payouts carries the count; a client with no list counts rows as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
420 lines
22 KiB
JavaScript
420 lines
22 KiB
JavaScript
// Checks for the Vest client's own wiring (lib/vest-class.ts), past what its
|
|
// pure helpers show: the market-close guard closing a leg whose open came back
|
|
// unknown, and the candidate board keeping a symbol the close gate holds back;
|
|
// a sync older than the bot's own fill, an open a restart cut off, an outside
|
|
// trade beside an open of unknown outcome, and the client's day at 8 PM ET.
|
|
//
|
|
// npm run check
|
|
//
|
|
// A scratch database, a fake clock, and every gateway call answered here —
|
|
// nothing reaches Vest.
|
|
import { after, test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { registerHooks, stripTypeScriptTypes } from 'node:module';
|
|
|
|
// Resolve the extensionless local imports used by Next, without a build.
|
|
function resolveTypes(specifier, context, nextResolve) {
|
|
try {
|
|
return nextResolve(specifier, context);
|
|
} catch (err) {
|
|
if (err.code !== 'ERR_MODULE_NOT_FOUND' || !specifier.startsWith('.')) throw err;
|
|
return nextResolve(`${specifier}.ts`, context);
|
|
}
|
|
}
|
|
// The client's classes use parameter properties, which plain type
|
|
// stripping refuses: transform them instead.
|
|
function loadTypes(url, context, nextLoad) {
|
|
if (!url.startsWith('file:') || !url.endsWith('.ts')) return nextLoad(url, context);
|
|
const source = stripTypeScriptTypes(readFileSync(fileURLToPath(url), 'utf8'), { mode: 'transform' });
|
|
return { format: 'module', source, shortCircuit: true };
|
|
}
|
|
registerHooks({ resolve: resolveTypes, load: loadTypes });
|
|
|
|
const directory = mkdtempSync(join(tmpdir(), 'autofirmer-vest-client-'));
|
|
process.env.AUTOFIRMER_DB = join(directory, 'test.sqlite');
|
|
delete process.env.NEXT_PHASE;
|
|
after(() => rmSync(directory, { recursive: true, force: true }));
|
|
|
|
// No request leaves this process: axios refuses every one, and each client's
|
|
// gateway is a function in this file — from construction on, since the
|
|
// constructor starts a sync.
|
|
const { default: axios } = await import('../lib/http-client.ts');
|
|
axios.defaults.adapter = (config) => Promise.reject(new Error(`no network in this test: ${config.method} ${config.url}`));
|
|
const { VestClient } = await import('../lib/vest-class.ts');
|
|
let gateway = null;
|
|
VestClient.prototype.request = async function (bucket, method, path, opts = {}) {
|
|
if (!gateway) throw new Error(`no gateway in this test: ${method} ${path}`);
|
|
return gateway(method, path, opts);
|
|
};
|
|
const { putVestSession } = await import('../lib/vest-session.ts');
|
|
const { botOpenedPositions, loadAccountMeta, saveAccountMeta } = await import('../lib/db.ts');
|
|
const { vestDayOf, vestAccountId, VestApiError, VEST_STATUS_ACTIVE } = await import('../lib/vest-helpers.ts');
|
|
const { encodeContractName } = await import('../lib/dxtrade-helpers.ts');
|
|
const { tradeDateFor, stageInputsFor, resolveEffectiveConfig } = await import('../lib/trading-logic.ts');
|
|
const { isClientReady } = await import('../lib/platform-client.ts');
|
|
|
|
const iso = (s) => Date.parse(s);
|
|
const WALLET = '0x7ce10771819f80130e204ddb5ca1522e48a9b451';
|
|
|
|
// The clock the client reads (Date.now), set to an instant and running on.
|
|
const realNow = Date.now;
|
|
after(() => { Date.now = realNow; });
|
|
let tokens = 0;
|
|
function clockAt(when) {
|
|
const start = realNow();
|
|
const t0 = iso(when);
|
|
Date.now = () => t0 + (realNow() - start);
|
|
tokens += 1;
|
|
assert.equal(putVestSession({ accessToken: `eyJ.test${tokens}.sig`, expiresAt: Date.now() + 3_600_000, primaryAddr: WALLET, userId: 'u' }), null);
|
|
}
|
|
|
|
// The week of NDX hours from check-vest.mjs: maintenance at 21:00 UTC
|
|
// (17:00 ET) Thursday, the weekly close at 21:00 UTC Friday.
|
|
const open = (from, to) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } });
|
|
const shut = (from, to, closure) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'unspecified', closure, tradingStatus: closure === 'weekly' ? 'CLOSE' : 'OPEN', price: { allow: closure !== 'weekly' } } });
|
|
const week = [
|
|
open('2026-10-28T22:00:00Z', '2026-10-29T21:00:00Z'),
|
|
shut('2026-10-29T21:00:00Z', '2026-10-29T22:00:00Z', 'maintenance'),
|
|
open('2026-10-29T22:00:00Z', '2026-10-30T21:00:00Z'),
|
|
shut('2026-10-30T21:00:00Z', '2026-11-01T23:00:00Z', 'weekly'),
|
|
open('2026-11-01T23:00:00Z', '2026-11-02T22:00:00Z'),
|
|
];
|
|
const spec = (symbol, assetId) => ({
|
|
symbol, assetId, sizeDecimals: 3, priceDecimals: 2, tick: 0.01, takerFee: 0.0001, marginRate: 0.05, tradingStatus: 'TRADING', capitalTradingStatus: 'TRADING',
|
|
});
|
|
const specs = new Map([['NDX-USD', spec('NDX-USD', 7)], ['BTC-PERP', spec('BTC-PERP', 9)]]);
|
|
|
|
/** 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). */
|
|
function testClient(answer, ids = [1, 2]) {
|
|
gateway = null;
|
|
const client = new VestClient({ firmId: 'vest-test', firmName: 'Vest test', wallet: WALLET }, async () => {});
|
|
clearInterval(client.syncInterval);
|
|
clearInterval(client.watchdogInterval);
|
|
gateway = answer;
|
|
client.specs = specs;
|
|
client.symbolSpecs = async () => specs;
|
|
client.marketHours = async (assetId) => (assetId === 7 ? week : []);
|
|
client.accountList = ids.map((id, i) => ({ id, name: `V50K-F-D3-${i + 1}`, active: true }));
|
|
ids.forEach((id, i) => {
|
|
client.uuidById.set(id, `acc-${i + 1}`);
|
|
client.capital.set(id, { status: VEST_STATUS_ACTIVE });
|
|
});
|
|
return client;
|
|
}
|
|
|
|
const A = vestAccountId('acc-1');
|
|
const B = vestAccountId('acc-2');
|
|
const NDX = encodeContractName('NDX-USD', 0.001);
|
|
const rawPosition = (positionId, accountId, openDate, extra = {}) => ({
|
|
positionId, accountId, symbol: 'NDX-USD', side: 'LONG', quantity: '0.500', openPrice: '20000', collateral: '500', leverage: '20', openDate,
|
|
stopLosses: [], takeProfits: [], ...extra,
|
|
});
|
|
|
|
/** What a sync reads (acc-1 and acc-2 funded, $50k each, and the login's
|
|
* main account; `positions` open), and the history reads after it. */
|
|
function syncReads(method, path, positions) {
|
|
if (method !== 'GET') return null;
|
|
if (path === '/v3/capital/accounts/active') {
|
|
return { data: { accounts: ['acc-1', 'acc-2'].map((id, i) => ({
|
|
id, status: VEST_STATUS_ACTIVE, attempt_index: i + 1, initial_capital: '50000', max_daily_loss_pct: '0.03', stage: 1, stages: [{ ordinal: 1, kind: 'funded' }],
|
|
})) } };
|
|
}
|
|
if (path === '/v3/accounts') {
|
|
return { data: { accounts: [
|
|
{ account_id: 'acc-1', amount: '50000', account_type: 2 }, { account_id: 'acc-2', amount: '50000', account_type: 2 }, { account_id: 'main', amount: '0', account_type: 1 },
|
|
] } };
|
|
}
|
|
if (path === '/v3/positions/opened') return { data: { positions } };
|
|
if (path === '/v3/positions/opened-orders') return { data: { orders: [] } };
|
|
if (path === '/v3/ticker/latest') return { data: { tickers: [] } };
|
|
if (path === '/v3/positions') return { data: { positions: [] } };
|
|
return null;
|
|
}
|
|
|
|
/** testClient on the ids a sync gives, once the constructor's own sync
|
|
* (which reaches no gateway) is over, with its history taken as read — so
|
|
* each requestSync here is one good sync, and starts nothing else. */
|
|
async function syncingClient(answer) {
|
|
const client = testClient(answer, [A, B]);
|
|
await client.syncRunning;
|
|
client.historyLoaded = true;
|
|
client.historyLoadedIds = new Set([A, B]);
|
|
client.lastDaysFetchStartedAt = Date.now();
|
|
client.lastDaysFetchSucceededAt = Infinity;
|
|
return client;
|
|
}
|
|
|
|
const unanswered = (method, path) => { throw new Error(`no gateway in this test: ${method} ${path}`); };
|
|
|
|
test('the market-close guard closes a bot leg whose open came back unknown, not a hand trade', async () => {
|
|
let positions = [];
|
|
const closes = [];
|
|
const client = testClient((method, path, opts) => {
|
|
if (method === 'GET' && path === '/v3/positions/opened') return { data: { positions } };
|
|
if (method === 'POST' && path === '/v3/positions/open') throw new Error('socket hang up');
|
|
if (method === 'POST' && path === '/v3/positions/close') { closes.push({ account: opts.account, ...opts.body }); return { data: {} }; }
|
|
throw new Error(`no gateway in this test: ${method} ${path}`);
|
|
});
|
|
try {
|
|
// Friday 8:30 AM ET: the entry's send fails in transit, but it filled.
|
|
clockAt('2026-10-30T12:30:00Z');
|
|
await assert.rejects(client.sendOrder(1, encodeContractName('NDX-USD', 0.001), 500, 'Buy', 'Market'), /outcome unknown/);
|
|
const raw = (positionId, accountId, openDate) => ({
|
|
positionId, accountId, symbol: 'NDX-USD', side: 'LONG', quantity: '0.500', openPrice: '20000', collateral: '500', leverage: '20', openDate,
|
|
stopLosses: [], takeProfits: [],
|
|
});
|
|
positions = [raw('9001', 'acc-1', iso('2026-10-30T12:30:01Z')), raw('9002', 'acc-2', iso('2026-10-30T11:00:00Z'))];
|
|
client.positionsDetail.set(1, []);
|
|
client.positionsDetail.set(2, [{ positionId: '9002', accountId: 'acc-2', symbol: 'NDX-USD', side: 'long', quantity: 0.5 }]);
|
|
client.syncComplete = true;
|
|
|
|
// 4:56 PM ET: four minutes to the weekly close.
|
|
clockAt('2026-10-30T20:56:00Z');
|
|
await client.guardClose();
|
|
assert.deepEqual(closes, [{ account: 'acc-1', positionId: '9001', symbol: 'NDX-USD', orderType: 'market' }]);
|
|
assert.deepEqual([...botOpenedPositions(['9001', '9002'])], ['9001']);
|
|
// A close just sent isn't sent again while a read may not show it.
|
|
await client.guardClose();
|
|
assert.equal(closes.length, 1);
|
|
} finally {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
test('a symbol the close gate holds back stays on the board, and isn\'t picked; the maintenance hour is ridden', async () => {
|
|
const client = testClient((method, path) => {
|
|
throw new Error(`no gateway in this test: ${method} ${path}`);
|
|
});
|
|
client.refreshMarks = async () => {};
|
|
client.volatility = async (symbols) => new Map(symbols.map((s) => [s, { vol: 0.004, candles: 168 }]));
|
|
const marked = () => { for (const s of ['NDX-USD', 'BTC-PERP']) client.marks.set(s, { mark: s === 'NDX-USD' ? 20000 : 70000, at: Date.now() }); };
|
|
try {
|
|
// Thursday 2 PM ET: NDX's maintenance hour is 3 h off, and reopens
|
|
// before the reset — no gate.
|
|
clockAt('2026-10-29T18:00:00Z');
|
|
marked();
|
|
await client.pickCandidate('Auto', ['NDX-USD', 'BTC-PERP'], false);
|
|
const thursday = client.lastCandidates.find((c) => c.id === 'NDX-USD');
|
|
assert.equal(thursday.closesAt, undefined);
|
|
|
|
// Friday 2 PM ET: the weekly close is 3 h off; crypto never closes.
|
|
clockAt('2026-10-30T18:00:00Z');
|
|
marked();
|
|
const pick = await client.pickCandidate('Auto', ['NDX-USD', 'BTC-PERP'], false);
|
|
assert.equal(pick.id, 'BTC-PERP');
|
|
assert.equal(pick.closesAt, undefined);
|
|
assert.deepEqual(client.lastCandidates.map((c) => c.id).sort(), ['BTC-PERP', 'NDX-USD']);
|
|
const ndx = client.lastCandidates.find((c) => c.id === 'NDX-USD');
|
|
assert.equal(ndx.closesAt, iso('2026-10-30T21:00:00Z'));
|
|
// The survey keeps it up for the dashboard, though nothing is picked.
|
|
assert.equal(await client.pickCandidate('survey', ['NDX-USD'], false), null);
|
|
assert.deepEqual(client.lastCandidates.map((c) => c.id), ['NDX-USD']);
|
|
// And the client's day is Vest's.
|
|
assert.equal(client.tradeDate(iso('2026-10-27T22:30:00Z')), vestDayOf(iso('2026-10-27T22:30:00Z')));
|
|
} finally {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
test("a sync whose positions read predates the bot's own fill leaves the fill's record alone", async () => {
|
|
let positions = [];
|
|
let holdRead = false;
|
|
let releaseRead = null;
|
|
clockAt('2026-10-28T14:00:00Z');
|
|
const client = await syncingClient((method, path) => {
|
|
if (method === 'GET' && path === '/v3/positions/opened' && holdRead) {
|
|
// Read now, flat — answered once released.
|
|
holdRead = false;
|
|
const served = positions;
|
|
return new Promise((resolve) => { releaseRead = () => resolve({ data: { positions: served } }); });
|
|
}
|
|
const read = syncReads(method, path, positions);
|
|
if (read) return read;
|
|
if (method === 'POST' && path === '/v3/positions/open') {
|
|
positions = [rawPosition('9101', 'acc-1', Date.now() + 300)];
|
|
return { data: { orderId: 'o-9101', positionId: '9101' } };
|
|
}
|
|
return unanswered(method, path);
|
|
});
|
|
try {
|
|
await client.requestSync();
|
|
// A sync reads acc-1 flat; the entry fills while it is still out.
|
|
holdRead = true;
|
|
const sync = client.requestSync();
|
|
await client.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open');
|
|
// Filled: the note kept against a restart goes.
|
|
assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null');
|
|
releaseRead();
|
|
await sync;
|
|
assert.deepEqual(client.positionsDetail.get(A).map((p) => p.positionId), ['9101']);
|
|
assert.equal(client.positions[A].netPos, 0.5);
|
|
// A read lagging the fill: the leg's flatten is refused as a position
|
|
// still out there (FLATTEN FAILED), not taken as nothing to flatten.
|
|
positions = [];
|
|
await assert.rejects(client.sendOrder(A, NDX, 500, 'Sell', 'Market', undefined, 'close'), /last saw a long one open/);
|
|
// A sync that went out after the fill writes what it reads.
|
|
await client.requestSync();
|
|
assert.deepEqual(client.positionsDetail.get(A), []);
|
|
assert.equal(client.positions[A], undefined);
|
|
} finally {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
test("an open a restart cut off is marked the bot's once a read after the restart shows it", async () => {
|
|
let positions = [];
|
|
let posted;
|
|
const postedAt = new Promise((resolve) => { posted = resolve; });
|
|
clockAt('2026-10-28T15:00:00Z');
|
|
const before = await syncingClient((method, path) => {
|
|
const read = syncReads(method, path, positions);
|
|
if (read) return read;
|
|
// The app stops with the open out.
|
|
if (method === 'POST' && path === '/v3/positions/open') { posted(Date.now()); return new Promise(() => {}); }
|
|
return unanswered(method, path);
|
|
});
|
|
void before.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open').catch(() => {});
|
|
const sentAt = await postedAt;
|
|
before.disconnect();
|
|
|
|
// Vest filled it, and an outside long opened on acc-2 with it. acc-2 has
|
|
// an older note, of a short that never showed.
|
|
positions = [rawPosition('9201', 'acc-1', sentAt + 400), rawPosition('9202', 'acc-2', sentAt + 400)];
|
|
saveAccountMeta(B, 'vest_pending_open', JSON.stringify({ accountId: 'acc-2', symbol: 'NDX-USD', side: 'short', sentAt: sentAt - 10 * 60_000 }));
|
|
clockAt('2026-10-28T15:02:00Z');
|
|
let refuse = false;
|
|
const after = await syncingClient((method, path) => {
|
|
const read = syncReads(method, path, positions);
|
|
if (read) return read;
|
|
if (method === 'POST' && path === '/v3/positions/open' && refuse) throw new VestApiError('/v3/positions/open: refused (HTTP 400)', 400, 'REFUSED');
|
|
return unanswered(method, path);
|
|
});
|
|
try {
|
|
await after.requestSync();
|
|
assert.deepEqual([...botOpenedPositions(['9201', '9202'])], ['9201']);
|
|
assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null');
|
|
assert.equal(loadAccountMeta(B, 'vest_pending_open'), 'null');
|
|
// An open Vest refuses leaves no note behind.
|
|
positions = [];
|
|
refuse = true;
|
|
await assert.rejects(after.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open'), /refused/);
|
|
assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null');
|
|
} finally {
|
|
after.disconnect();
|
|
}
|
|
});
|
|
|
|
test("an open of unknown outcome isn't matched to an outside trade after its error, nor to one of several", async () => {
|
|
let positions = [];
|
|
let open = null;
|
|
let onRead = () => {};
|
|
clockAt('2026-10-28T16:00:00Z');
|
|
const client = await syncingClient((method, path) => {
|
|
if (method === 'GET' && path === '/v3/positions/opened') onRead();
|
|
const read = syncReads(method, path, positions);
|
|
if (read) return read;
|
|
if (method === 'POST' && path === '/v3/positions/open') return open();
|
|
return unanswered(method, path);
|
|
});
|
|
try {
|
|
await client.requestSync();
|
|
// The entry's send fails in transit, and it never went in. Half a
|
|
// minute on, an outside copy-trader opens NDX long on every account.
|
|
open = () => { throw new Error('socket hang up'); };
|
|
await assert.rejects(client.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open'), /outcome unknown/);
|
|
const errorAt = Date.now();
|
|
positions = [rawPosition('9301', 'acc-1', errorAt + 30_000), rawPosition('9302', 'acc-2', errorAt + 30_000)];
|
|
clockAt('2026-10-28T16:01:00Z');
|
|
await client.requestSync();
|
|
assert.deepEqual([...botOpenedPositions(['9301', '9302'])], []);
|
|
assert.equal(loadAccountMeta(A, 'vest_pending_open'), 'null');
|
|
|
|
// Another on acc-2, and two positions show from that moment: either
|
|
// could be the bot's, so neither is marked.
|
|
positions = [];
|
|
clockAt('2026-10-28T16:05:00Z');
|
|
open = () => {
|
|
positions = [rawPosition('9303', 'acc-2', Date.now() + 100), rawPosition('9304', 'acc-2', Date.now() + 150)];
|
|
throw new Error('socket hang up');
|
|
};
|
|
await assert.rejects(client.sendOrder(B, NDX, 500, 'Buy', 'Market', undefined, 'open'), /outcome unknown/);
|
|
await client.requestSync();
|
|
assert.deepEqual([...botOpenedPositions(['9303', '9304'])], []);
|
|
|
|
// An open whose answer named its position, though no read showed it
|
|
// within the wait: that position is the bot's, even with a stop a
|
|
// hand has put on it since.
|
|
positions = [];
|
|
clockAt('2026-10-28T16:10:00Z');
|
|
let answered = false;
|
|
open = () => { answered = true; return { data: { orderId: 'o-9305', positionId: '9305' } }; };
|
|
// The wait's first read comes back past its deadline.
|
|
onRead = () => { if (answered) { answered = false; clockAt('2026-10-28T16:10:45Z'); } };
|
|
await assert.rejects(client.sendOrder(A, NDX, 500, 'Buy', 'Market', undefined, 'open'), /not seen filled/);
|
|
const stop = { id: 'sl-9305', triggerPrice: '19900', executionType: 'market', status: 'awaiting' };
|
|
positions = [rawPosition('9305', 'acc-1', iso('2026-10-28T16:10:00Z') + 500, { stopLosses: [stop] })];
|
|
await client.requestSync();
|
|
assert.deepEqual([...botOpenedPositions(['9305'])], ['9305']);
|
|
} finally {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
test("the client's Vest day turns with its numbers, and a sync that stalls stops the trading it holds back", async () => {
|
|
clockAt('2026-10-27T23:58:00Z');
|
|
const client = await syncingClient((method, path) => syncReads(method, path, []) ?? unanswered(method, path));
|
|
// The day the engine plans the lane on, at this test's clock.
|
|
const engineDay = () => tradeDateFor(client, new Date(Date.now()));
|
|
try {
|
|
// 7:58 PM ET, before any sync: the day now.
|
|
assert.equal(engineDay(), '2026-10-27');
|
|
await client.requestSync();
|
|
// 8:00:20 PM ET: Vest's day has turned, but the numbers the last sync
|
|
// worked out are still the ended day's — and so is the engine's day.
|
|
clockAt('2026-10-28T00:00:20Z');
|
|
assert.equal(vestDayOf(Date.now()), '2026-10-28');
|
|
assert.equal(engineDay(), '2026-10-27');
|
|
assert.equal(isClientReady(client), true);
|
|
await client.requestSync();
|
|
assert.equal(engineDay(), '2026-10-28');
|
|
// No sync after the next reset: the day holds back only while the
|
|
// scheduler still trades on the last sync's numbers.
|
|
clockAt('2026-10-28T23:59:00Z');
|
|
await client.requestSync();
|
|
clockAt('2026-10-29T00:03:00Z');
|
|
assert.equal(engineDay(), '2026-10-28');
|
|
assert.equal(isClientReady(client), true);
|
|
clockAt('2026-10-29T00:05:00Z');
|
|
assert.equal(engineDay(), '2026-10-28');
|
|
assert.equal(isClientReady(client), false);
|
|
} finally {
|
|
client.disconnect();
|
|
}
|
|
});
|
|
|
|
test('two payouts on one Vest day share its fund row, and still count two toward the stage', async () => {
|
|
clockAt('2026-10-27T23:00:00Z');
|
|
const client = await syncingClient(unanswered);
|
|
try {
|
|
// $1,500 then $3,000 taken on Vest day 10-27: Vest's two WITHDRAWs, one row.
|
|
client.withdrawals.set(A, [{ at: iso('2026-10-27T15:00:00Z'), amount: -1500 }, { at: iso('2026-10-27T19:00:00Z'), amount: -3000 }]);
|
|
client.fundTransactions[A] = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -4500 }];
|
|
assert.deepEqual(client.payoutDays(A), ['2026-10-27', '2026-10-27']);
|
|
// A type whose stages don't reset each day trades stage 3's $2,000, not stage 2's again.
|
|
const stages = [{ profit: 3000, consistency: 0, minTradingDays: 0 }, { profit: 2000, consistency: 0, minTradingDays: 0 }];
|
|
const inputs = stageInputsFor(client, A, { stagesSameDay: false });
|
|
assert.equal(inputs.payouts, 2);
|
|
assert.equal(resolveEffectiveConfig(1500, 0, 0, false, stages, 0, client.fundTransactions[A], 0, inputs).profitTarget, 2000);
|
|
// Before its history is read: one per negative fund row.
|
|
client.historyLoadedIds.delete(A);
|
|
assert.equal(stageInputsFor(client, A, {}).payouts, undefined);
|
|
} finally {
|
|
client.disconnect();
|
|
}
|
|
});
|