Files
autofirmer-expanded/scripts/check-kept-rules.mjs
T
Brandon LiandClaude Opus 5.5 32081f3dcf Vest: claim the profit at the target, then trade again the same day
On Vest, a claim lowers the daily-loss floor by the amount claimed. Winning
$1,500 and claiming it leaves $3,000 of room the same day, the full
drawdown. The bot now uses that.

- Payout claims: a scheduler step (runClaims, under the lane mutex, before
  the flat gate) claims every payoutable account that reached its stage's
  target, via a platform that takes payouts by API. The claim is all
  profit above the starting balance. It then clears the account's legs and
  entry outcome for the day so it can trade its next stage. Failures retry
  after 5 minutes. The auto_claim setting 'off' disables it.
- VestClient.claimProfit: POST /v3/capital/withdraw to the login's main
  account (account_type 1 on /v3/accounts). The payout is booked at once:
  today's fund row and the cycle cut, with today's P&L counted from the
  post-claim balance. The history read later rebooks it from Vest's
  WITHDRAW row, and the books check counts a claim until that row shows.
- Same-day stages: an account type can pick its stage by the payouts since
  the firm's last daily reset (stagesSameDay, sameDayClaims). A stage can
  carry its own daily loss limit.
- That limit replaces the type's everywhere room, stops and last stands are
  worked out: the batch, the manual leg, copy trade, the dashboard and the
  report.
- Room from the platform's own floor: dailyLossFloor (Vest) makes the room
  min(stage limit, balance − Vest's floor). A same-day claim that lowers
  the floor therefore frees room, and the room is never more than Vest
  allows. VestClient.dayStartBalance is gone.
- Before the 8 PM ET reset: from 7:55 PM ET, each open Vest position on an
  account with a daily limit is fitted to the room the reset will leave
  (resetSafeStop). Its stop comes in via PUT/POST stop-loss, or the
  position closes when there is no room.
- Checks for same-day stages, the stage limit, floorRoom, the post-claim
  plan, the next reset across both time changes, and resetSafeStop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 15:56:00 -05:00

518 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Checks for kept rules — which of an account type's rule sets an account
// trades on (lib/kept-rules.ts).
//
// npm run check
//
// lib/kept-rules.ts has only `import type`s, so Node strips its types and
// loads it directly; .mjs keeps this file out of Next's type check.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
RULE_FIELDS, IDENTITY_FIELDS, PROJECTION_FIELDS, normalizeAccountType, pickKeptRules, overlayKept, ruleDiffs,
stampRulesDate, evidenceRulesDate, lowersRulesDate, sortableIso, chicagoDay, chicagoDayStart, evidenceDay,
openingEvidence, identityStillMatches, pickedDayRulesDate, unchangedDaySaves,
} from '../lib/kept-rules.ts';
const LIVE = {
id: 'live-x', firmId: 'firm-1', label: '50K Eval', prefix: 'TDFY', accountSize: 50_000,
profitTarget: 2500, consistency: 0.4, minDayPnL: 150, minTradingDays: 3, maxLoss: 2000, maxPositionSize: 5,
targetSameEquity: false, splitEnabled: true, minLegs: 2, firstDayStop: true, minDayStop: false, combineCmeHolidays: false,
withdrawalStages: [{ profit: 2000, consistency: 0.4, minTradingDays: 5, drain: false, minDayPnL: 0 }],
payoutable: true, trailingDrawdown: 2000, expectedPayout: 1000, withdrawalPct: 0.5, payoutCap: 3000, profitSplit: 0.9,
dailyLossLimit: 1000, stagesSameDay: false, tradesEveryDay: false, updatedAt: '2026-10-18T14:05:00.000Z',
};
/** `obj` without `keys`. */
const omit = (obj, keys) => Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k)));
/** A kept set as the master builds it: the live type, the old rules on top. */
function keptOf(rules, window, id = 'kept-1') {
return {
...LIVE, ...rules, id, keptFrom: LIVE.id, createdAt: '2026-01-01T00:00:00.000Z',
openedAt: window.openedAt, closedAt: window.closedAt,
};
}
// ── field classes ───────────────────────────────────────────────────────────
test('field classes: the spec\'s lists, identical to the master\'s, and disjoint', () => {
assert.deepEqual([...RULE_FIELDS], [
'profitTarget', 'consistency', 'minDayPnL', 'minTradingDays', 'maxLoss', 'maxPositionSize', 'targetSameEquity',
'splitEnabled', 'minLegs', 'firstDayStop', 'minDayStop', 'combineCmeHolidays', 'withdrawalStages', 'payoutable',
'trailingDrawdown', 'dailyLossLimit', 'stagesSameDay',
]);
assert.deepEqual([...IDENTITY_FIELDS], ['prefix', 'accountSize']);
assert.deepEqual([...PROJECTION_FIELDS], ['expectedPayout', 'withdrawalPct', 'payoutCap', 'profitSplit']);
const all = [...RULE_FIELDS, ...IDENTITY_FIELDS, ...PROJECTION_FIELDS, 'label'];
assert.equal(new Set(all).size, all.length);
// Every field of a catalog account type is in exactly one class, or is
// bookkeeping the live type always owns.
const bookkeeping = ['id', 'firmId', 'updatedAt', 'tradesEveryDay'];
assert.deepEqual(Object.keys(LIVE).filter((k) => !all.includes(k) && !bookkeeping.includes(k)), []);
});
// ── normalizeAccountType: exactly the old validatePayload callback ──────────
/** lib/firm-catalog.ts validatePayload's per-type callback before the move
* (git show HEAD:lib/firm-catalog.ts), verbatim but for the types. */
function legacyNormalize(t, j, tradesEveryDay) {
const nonNegative = (value) => (typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0);
const isStages = (value) => Array.isArray(value) && value.every((s) =>
typeof s === 'object' && s !== null
&& typeof s.profit === 'number'
&& typeof s.consistency === 'number'
&& typeof s.minTradingDays === 'number');
if (typeof t !== 'object' || t === null) throw new Error(`account type ${j} is not an object`);
const type = t;
for (const key of ['id', 'firmId', 'label', 'prefix', 'updatedAt']) {
if (typeof type[key] !== 'string') throw new Error(`account type ${j} is missing ${key}`);
}
for (const key of [
'accountSize', 'profitTarget', 'consistency', 'minDayPnL',
'minTradingDays', 'maxLoss', 'maxPositionSize',
]) {
if (typeof type[key] !== 'number') throw new Error(`account type ${j} is missing ${key}`);
}
if (!isStages(type.withdrawalStages)) throw new Error(`account type ${j} has bad withdrawalStages`);
return {
...type,
splitEnabled: type.splitEnabled !== false,
combineCmeHolidays: type.combineCmeHolidays === true && !tradesEveryDay,
minLegs: typeof type.minLegs === 'number' ? type.minLegs : 0,
firstDayStop: type.firstDayStop === true,
minDayStop: type.minDayStop === true,
dailyLossLimit: nonNegative(type.dailyLossLimit),
stagesSameDay: type.stagesSameDay === true,
tradesEveryDay,
};
}
function outcome(fn) {
try {
return { ok: JSON.stringify(fn()) };
} catch (err) {
return { error: err.message };
}
}
test('normalizeAccountType is the old per-type validation, byte for byte (values, key order, errors)', () => {
const wire = omit(LIVE, ['tradesEveryDay']);
const variants = [
wire,
// An old master: none of the lenient fields.
omit(wire, ['splitEnabled', 'minLegs', 'firstDayStop', 'minDayStop', 'combineCmeHolidays', 'dailyLossLimit']),
{ ...wire, splitEnabled: false, combineCmeHolidays: true, minLegs: '3', firstDayStop: 'yes', dailyLossLimit: -5 },
{ ...wire, dailyLossLimit: NaN, minDayStop: true, extra: { nested: [1, 2] } },
{ ...wire, withdrawalStages: [] },
// Malformed: each must throw the same message.
null, 'type', 42,
{ ...wire, id: 7 }, { ...wire, label: undefined }, { ...wire, updatedAt: null },
{ ...wire, accountSize: '50000' }, { ...wire, maxPositionSize: undefined },
{ ...wire, withdrawalStages: [{ profit: 1, consistency: 0.5 }] }, { ...wire, withdrawalStages: 'none' },
{ ...wire, withdrawalStages: [null] },
];
let errors = 0;
for (const tradesEveryDay of [false, true]) {
variants.forEach((v, j) => {
const legacy = outcome(() => legacyNormalize(v, j, tradesEveryDay));
const now = outcome(() => normalizeAccountType(v, { tradesEveryDay, where: `account type ${j}` }));
assert.deepEqual(now, legacy, `variant ${j}, every day ${tradesEveryDay}`);
if (legacy.error) errors++;
});
}
assert.equal(errors, 2 * 11);
// The kept-set wording only changes the name in the message.
assert.throws(() => normalizeAccountType({ ...wire, prefix: 1 }, { tradesEveryDay: false, where: 'kept rules 0 of Tradeify' }),
{ message: 'kept rules 0 of Tradeify is missing prefix' });
});
// ── pickKeptRules ───────────────────────────────────────────────────────────
const G1 = { id: 'g1', openedAt: null, closedAt: '2026-10-02T15:00:00.000Z' };
const G2 = { id: 'g2', openedAt: '2026-10-02T15:00:00.000Z', closedAt: '2026-10-18T14:05:00.000Z' };
const G3 = { id: 'g3', openedAt: '2026-10-18T14:05:00.000Z', closedAt: '2026-10-18T19:30:00.000Z' };
const GENERATIONS = [G1, G2, G3];
test('pickKeptRules: the window holding the date, [openedAt, closedAt)', () => {
const pick = (d, sets = GENERATIONS) => pickKeptRules(sets, d)?.id ?? null;
// No lower bound on the first generation.
assert.equal(pick('2020-01-01T00:00:00.000Z'), 'g1');
assert.equal(pick('2026-10-02T14:59:59.999Z'), 'g1');
// A date exactly at a cutoff is on the newer side: closedAt excluded,
// openedAt included.
assert.equal(pick('2026-10-02T15:00:00.000Z'), 'g2');
assert.equal(pick('2026-10-18T14:04:59.999Z'), 'g2');
assert.equal(pick('2026-10-18T14:05:00.000Z'), 'g3');
assert.equal(pick('2026-10-18T19:29:59.999Z'), 'g3');
// From the last cutoff on: the current rules.
assert.equal(pick('2026-10-18T19:30:00.000Z'), null);
assert.equal(pick('2027-01-01T00:00:00.000Z'), null);
// Compared as times, not strings: other ISO spellings of the same instant.
assert.equal(pick('2026-10-02T10:00:00-05:00'), 'g2');
assert.equal(pick('2026-10-02T09:59:59.999-05:00'), 'g1');
// Order of the list doesn't matter for disjoint windows.
assert.equal(pick('2026-10-10T00:00:00.000Z', [G3, G1, G2]), 'g2');
// Returns the set itself (the generic carries through).
assert.equal(pickKeptRules(GENERATIONS, '2026-10-10T00:00:00.000Z'), G2);
});
test('pickKeptRules: no date, no sets, or an unreadable date — the current rules', () => {
assert.equal(pickKeptRules(GENERATIONS, null), null);
assert.equal(pickKeptRules(GENERATIONS, ''), null);
assert.equal(pickKeptRules(GENERATIONS, 'not a date'), null);
assert.equal(pickKeptRules(undefined, '2026-10-10T00:00:00.000Z'), null);
assert.equal(pickKeptRules([], '2026-10-10T00:00:00.000Z'), null);
// An unreadable bound never matches (the catalog parse rejects them anyway).
assert.equal(pickKeptRules([{ id: 'bad', openedAt: null, closedAt: 'soon' }], '2020-01-01T00:00:00.000Z'), null);
assert.equal(pickKeptRules([{ id: 'bad', openedAt: 'once', closedAt: '2030-01-01T00:00:00.000Z' }], '2026-01-01T00:00:00.000Z'), null);
// An empty window (openedAt == closedAt) holds nothing.
assert.equal(pickKeptRules([{ id: 'empty', openedAt: G2.closedAt, closedAt: G2.closedAt }], G2.closedAt), null);
});
// ── overlayKept ─────────────────────────────────────────────────────────────
test('overlayKept: the kept rules on top; identity, label, projections and bookkeeping stay live', () => {
const oldRules = {
profitTarget: 3000, consistency: 0.5, minDayPnL: 200, minTradingDays: 5, maxLoss: 2500, maxPositionSize: 3,
targetSameEquity: true, splitEnabled: false, minLegs: 0, firstDayStop: false, minDayStop: true, combineCmeHolidays: true,
withdrawalStages: [], payoutable: false, trailingDrawdown: 2500, dailyLossLimit: 0, stagesSameDay: false,
};
// A master that sent other identity/projection values on the kept set:
// they must not leak through.
const kept = keptOf({ ...oldRules, label: 'OLD', prefix: 'OLD', accountSize: 1, expectedPayout: 1, withdrawalPct: 1,
payoutCap: 1, profitSplit: 1, firmId: 'other', updatedAt: '1999-01-01T00:00:00.000Z', tradesEveryDay: true }, G2);
const liveBefore = structuredClone(LIVE);
const keptBefore = structuredClone(kept);
const eff = overlayKept(LIVE, kept);
for (const field of RULE_FIELDS) assert.deepEqual(eff[field], oldRules[field], field);
for (const field of ['id', 'firmId', 'label', 'prefix', 'accountSize', 'updatedAt', 'tradesEveryDay', ...PROJECTION_FIELDS]) {
assert.deepEqual(eff[field], LIVE[field], field);
}
assert.deepEqual(eff.keptRules, { id: 'kept-1', openedAt: G2.openedAt, closedAt: G2.closedAt });
// Nothing of the kept set's own bookkeeping comes along.
for (const key of ['keptFrom', 'openedAt', 'closedAt', 'createdAt']) assert.equal(key in eff, false, key);
assert.deepEqual(Object.keys(eff).sort(), [...Object.keys(LIVE), 'keptRules'].sort());
// Inputs untouched.
assert.deepEqual(LIVE, liveBefore);
assert.deepEqual(kept, keptBefore);
});
test('overlayKept: a rule field the kept set lacks keeps the live value', () => {
const kept = keptOf({ consistency: 0.5 }, G1);
delete kept.payoutable;
kept.trailingDrawdown = undefined;
const eff = overlayKept(LIVE, kept);
assert.equal(eff.consistency, 0.5);
assert.equal(eff.payoutable, LIVE.payoutable);
assert.equal(eff.trailingDrawdown, LIVE.trailingDrawdown);
});
// ── ruleDiffs ───────────────────────────────────────────────────────────────
test('ruleDiffs: the rule fields that differ, raw values both sides', () => {
assert.deepEqual(ruleDiffs(LIVE, LIVE), []);
const eff = overlayKept(LIVE, keptOf({ consistency: 0.5, profitTarget: 3000 }, G1));
assert.deepEqual(ruleDiffs(eff, LIVE), [
{ field: 'profitTarget', kept: 3000, now: 2500 },
{ field: 'consistency', kept: 0.5, now: 0.4 },
]);
// Non-rule fields never count.
assert.deepEqual(ruleDiffs({ ...LIVE, label: 'x', prefix: 'y', profitSplit: 0.1 }, LIVE), []);
const stages = [{ profit: 3000, consistency: 0.4, minTradingDays: 5, drain: false, minDayPnL: 0 }];
assert.deepEqual(ruleDiffs({ ...LIVE, withdrawalStages: stages }, LIVE), [
{ field: 'withdrawalStages', kept: stages, now: LIVE.withdrawalStages },
]);
});
test('ruleDiffs: stored JSON from before a field existed is not a change', () => {
// A stage without drain / minDayPnL is what parseAccountTypeInput fills
// with drain false, minDayPnL 0 (5 for a drain stage); key order is free.
const bare = { ...LIVE, withdrawalStages: [{ minTradingDays: 5, consistency: 0.4, profit: 2000 }] };
assert.deepEqual(ruleDiffs(bare, LIVE), []);
const drainFilled = { ...LIVE, withdrawalStages: [{ profit: 0, consistency: 0, minTradingDays: 5, drain: true, minDayPnL: 5 }] };
const drainBare = { ...LIVE, withdrawalStages: [{ profit: 0, consistency: 0, minTradingDays: 5, drain: true }] };
assert.deepEqual(ruleDiffs(drainBare, drainFilled), []);
// A per-stage payout override is a rule.
assert.equal(ruleDiffs({ ...LIVE, withdrawalStages: [{ ...LIVE.withdrawalStages[0], payoutCap: 1500 }] }, LIVE).length, 1);
// An absent optional flag or amount is its default.
const noOptionals = omit(LIVE, ['payoutable', 'trailingDrawdown']);
assert.deepEqual(ruleDiffs(noOptionals, { ...LIVE, payoutable: false, trailingDrawdown: 0 }), []);
assert.deepEqual(ruleDiffs({ ...LIVE, splitEnabled: undefined }, LIVE), []);
assert.deepEqual(ruleDiffs({ ...LIVE, splitEnabled: undefined }, { ...LIVE, splitEnabled: false }).map((d) => d.field), ['splitEnabled']);
});
// ── rules dates ─────────────────────────────────────────────────────────────
test('stampRulesDate: the latest of this clock, the master\'s and the catalog\'s last change', () => {
const now = Date.parse('2026-10-18T14:00:00.000Z');
assert.equal(stampRulesDate(now, null, null), '2026-10-18T14:00:00.000Z');
// This clock 5 minutes behind the master's.
assert.equal(stampRulesDate(now, 5 * 60_000, null), '2026-10-18T14:05:00.000Z');
// This clock ahead: its own time stands.
assert.equal(stampRulesDate(now, -5 * 60_000, null), '2026-10-18T14:00:00.000Z');
// The catalog changed after both: no new account dates before it.
assert.equal(stampRulesDate(now, 60_000, '2026-10-18T14:07:30.000Z'), '2026-10-18T14:07:30.000Z');
assert.equal(stampRulesDate(now, null, '2026-10-01T00:00:00.000Z'), '2026-10-18T14:00:00.000Z');
// Unreadable inputs are ignored.
assert.equal(stampRulesDate(now, NaN, 'never'), '2026-10-18T14:00:00.000Z');
assert.equal(stampRulesDate(now, null, ''), '2026-10-18T14:00:00.000Z');
});
test('stampRulesDate: a machine whose clock is behind never dates a new account inside the set just closed', () => {
// The owner saves "new accounts only" at 14:05 master time; this machine's
// clock reads 14:01 when it first sees a brand-new account at 14:05.
const cutoff = '2026-10-18T14:05:00.000Z';
const kept = [{ id: 'g', openedAt: null, closedAt: cutoff }];
const local = Date.parse('2026-10-18T14:01:00.000Z');
// Unclamped, the account would land in the old rules.
assert.equal(pickKeptRules(kept, new Date(local).toISOString())?.id, 'g');
// With the master's offset, or just the catalog's updatedAt, it doesn't.
assert.equal(pickKeptRules(kept, stampRulesDate(local, 4 * 60_000, null)), null);
assert.equal(pickKeptRules(kept, stampRulesDate(local, null, cutoff)), null);
// A machine whose master offset isn't known yet still clamps to the catalog.
assert.equal(pickKeptRules(kept, stampRulesDate(local, null, '2026-10-18T14:05:00.250Z')), null);
});
test('evidenceRulesDate: the end of the first fund day in Chicago — CDT and CST', () => {
// CDT (UTC−5): 23:59:59.999 CDT is 04:59:59.999Z the next day.
assert.equal(evidenceRulesDate('2026-10-18'), '2026-10-19T04:59:59.999Z');
assert.equal(evidenceRulesDate('2026-07-04'), '2026-07-05T04:59:59.999Z');
// CST (UTC−6): 05:59:59.999Z the next day.
assert.equal(evidenceRulesDate('2026-01-15'), '2026-01-16T05:59:59.999Z');
assert.equal(evidenceRulesDate('2026-12-31'), '2027-01-01T05:59:59.999Z');
assert.equal(evidenceRulesDate('2028-02-29'), '2028-03-01T05:59:59.999Z');
for (const bad of [null, undefined, '', '2026-10-18T00:00:00Z', '18/10/2026', '2026-1-8', '2026-02-31', '2026-13-01', 20261018]) {
assert.equal(evidenceRulesDate(bad), null, String(bad));
}
});
test('evidenceRulesDate: DST boundary days end on the right side of the change', () => {
// 2026: CST→CDT at 2 AM on Sunday Mar 8, CDT→CST at 2 AM on Sunday Nov 1.
assert.equal(evidenceRulesDate('2026-03-07'), '2026-03-08T05:59:59.999Z'); // last CST day
assert.equal(evidenceRulesDate('2026-03-08'), '2026-03-09T04:59:59.999Z'); // springs forward: ends in CDT
assert.equal(evidenceRulesDate('2026-03-09'), '2026-03-10T04:59:59.999Z');
assert.equal(evidenceRulesDate('2026-10-31'), '2026-11-01T04:59:59.999Z'); // last CDT day
assert.equal(evidenceRulesDate('2026-11-01'), '2026-11-02T05:59:59.999Z'); // falls back: ends in CST
assert.equal(evidenceRulesDate('2026-11-02'), '2026-11-03T05:59:59.999Z');
// The day before each boundary and the boundary day are 23 h / 25 h long
// in Chicago, measured end to end.
const len = (a, b) => (Date.parse(evidenceRulesDate(b)) - Date.parse(evidenceRulesDate(a))) / 3_600_000;
assert.equal(len('2026-03-07', '2026-03-08'), 23);
assert.equal(len('2026-10-31', '2026-11-01'), 25);
assert.equal(len('2026-06-01', '2026-06-02'), 24);
});
test('evidenceRulesDate: every day of 2026–2027 ends 1 ms before the next one starts, in Chicago', () => {
for (let ms = Date.UTC(2026, 0, 1); ms < Date.UTC(2028, 0, 1); ms += 86_400_000) {
const day = new Date(ms).toISOString().slice(0, 10);
const next = new Date(ms + 86_400_000).toISOString().slice(0, 10);
const end = evidenceRulesDate(day);
assert.equal(Date.parse(chicagoDayStart(next)) - Date.parse(end), 1, day);
// Both read back as their own Chicago day, and the end as evidence.
assert.equal(chicagoDay(end), day);
assert.equal(chicagoDay(chicagoDayStart(next)), next);
assert.equal(evidenceDay(end), day);
// Under either offset the end is 04:59:59.999Z or 05:59:59.999Z the next UTC day.
assert.match(end, new RegExp(`^${next}T0[45]:59:59\\.999Z$`));
}
});
test('evidenceRulesDate: an account opened on the day of a change — even that evening — counts as after it', () => {
// A change at 14:05 CDT on Oct 18: an account first funded that day is
// after it; one funded the day before keeps the old rules.
const afternoon = [{ id: 'g', openedAt: null, closedAt: '2026-10-18T19:05:00.000Z' }];
assert.equal(pickKeptRules(afternoon, evidenceRulesDate('2026-10-18')), null);
assert.equal(pickKeptRules(afternoon, evidenceRulesDate('2026-10-17'))?.id, 'g');
// A change at 8 PM CDT (01:00Z the next UTC day): a UTC end of day
// (Oct 18 23:59:59.999Z = 6:59 PM CDT) would have put an account that
// opened that evening before it.
const evening = [{ id: 'g', openedAt: null, closedAt: '2026-10-19T01:00:00.000Z' }];
assert.equal(pickKeptRules(evening, evidenceRulesDate('2026-10-18')), null);
assert.equal(pickKeptRules(evening, evidenceRulesDate('2026-10-17'))?.id, 'g');
// Same under CST: a change at 11:30 PM CST Jan 15 (05:30Z Jan 16).
const winter = [{ id: 'g', openedAt: null, closedAt: '2026-01-16T05:30:00.000Z' }];
assert.equal(pickKeptRules(winter, evidenceRulesDate('2026-01-15')), null);
assert.equal(pickKeptRules(winter, evidenceRulesDate('2026-01-14'))?.id, 'g');
});
test('chicagoDayStart / chicagoDay / evidenceDay', () => {
assert.equal(chicagoDayStart('2026-10-18'), '2026-10-18T05:00:00.000Z');
assert.equal(chicagoDayStart('2026-01-15'), '2026-01-15T06:00:00.000Z');
assert.equal(chicagoDayStart('2026-03-08'), '2026-03-08T06:00:00.000Z'); // midnight is still CST
assert.equal(chicagoDayStart('2026-11-01'), '2026-11-01T05:00:00.000Z'); // midnight is still CDT
assert.equal(chicagoDayStart('2026-02-31'), null);
assert.equal(chicagoDayStart('1999-12-31'), null); // before 2000: not a rules date
assert.equal(chicagoDay('2026-10-19T04:59:59.999Z'), '2026-10-18');
assert.equal(chicagoDay('2026-10-19T05:00:00.000Z'), '2026-10-19');
assert.equal(chicagoDay('nope'), null);
// Only the exact end of a Chicago day reads as evidence.
assert.equal(evidenceDay('2026-10-19T04:59:59.999Z'), '2026-10-18');
assert.equal(evidenceDay('2026-10-19T04:59:59.998Z'), null);
assert.equal(evidenceDay('2026-10-18T23:59:59.999Z'), null); // the old UTC shape
assert.equal(evidenceDay('2026-10-18T19:05:00.000Z'), null);
});
test('sortableIso: plain four-digit-year ISO from 2000 on, else null', () => {
assert.equal(sortableIso(Date.parse('2026-10-18T14:05:00Z')), '2026-10-18T14:05:00.000Z');
assert.equal(sortableIso(Date.UTC(2000, 0, 1)), '2000-01-01T00:00:00.000Z');
assert.equal(sortableIso(Date.UTC(2000, 0, 1) - 1), null);
assert.equal(sortableIso(0), null);
assert.equal(sortableIso(Date.UTC(9999, 11, 31, 23, 59, 59, 999)), '9999-12-31T23:59:59.999Z');
assert.equal(sortableIso(Date.parse('+010000-01-01T00:00:00Z')), null); // would print as +010000-…
assert.equal(sortableIso(8.64e15 + 1), null);
assert.equal(sortableIso(NaN), null);
assert.equal(sortableIso(Infinity), null);
});
test('openingEvidence: a fund day ends in Chicago; the registration fallback is the instant itself', () => {
// A positive fund row's day: the end of it in Chicago, whatever the registration.
assert.equal(openingEvidence('2026-10-02', Date.parse('2026-09-01T00:00:00Z')), '2026-10-03T04:59:59.999Z');
// Positive rows present but unreadable: no evidence (never the registration).
assert.equal(openingEvidence('2026-02-31', Date.parse('2026-09-01T00:00:00Z')), null);
// No positive rows: the registration instant, exactly.
assert.equal(openingEvidence(undefined, Date.parse('2026-10-19T02:00:00Z')), '2026-10-19T02:00:00.000Z');
// Unreadable or implausible registrations: none.
for (const bad of [undefined, null, NaN, 0, 5, -1, Infinity, Date.parse('1999-12-31T23:00:00Z'), 9e15]) {
assert.equal(openingEvidence(undefined, bad), null, String(bad));
}
});
test('openingEvidence: a DXtrade account registered after an evening cutoff is never lowered before it', () => {
// "New accounts only" saved at 8 PM CDT Oct 18 (01:00Z Oct 19); a DX
// account registered at 9 PM CDT (02:00Z) with no DEPOSIT row.
const kept = [{ id: 'g', openedAt: null, closedAt: '2026-10-19T01:00:00.000Z' }];
const stamped = '2026-10-19T02:00:05.000Z';
const evidence = openingEvidence(undefined, Date.parse('2026-10-19T02:00:00Z'));
const lowered = lowersRulesDate({ rules_date: stamped, rules_date_manual: 0 }, evidence) ? evidence : stamped;
assert.equal(pickKeptRules(kept, lowered), null);
// And at 9 PM CDT Oct 20 against an 8 PM cutoff that day.
const kept2 = [{ id: 'g', openedAt: null, closedAt: '2026-10-21T01:00:00.000Z' }];
assert.equal(pickKeptRules(kept2, openingEvidence(undefined, Date.parse('2026-10-21T02:00:00Z'))), null);
// One registered before the cutoff, first seen after it, goes back to the old rules.
const before = openingEvidence(undefined, Date.parse('2026-10-19T00:30:00Z'));
assert.equal(lowersRulesDate({ rules_date: stamped, rules_date_manual: 0 }, before), true);
assert.equal(pickKeptRules(kept, before)?.id, 'g');
});
test('identityStillMatches: prefix and the cached size against the live type', () => {
const live = { prefix: 'TDFY', accountSize: 50_000 };
assert.equal(identityStillMatches('TDFY1234', '50000', live), true);
assert.equal(identityStillMatches('TDFY1234', '50000.004', live), true);
// A size edit on the type (50K → 100K, or → $0-start): no longer its size.
assert.equal(identityStillMatches('TDFY1234', '50000', { ...live, accountSize: 100_000 }), false);
assert.equal(identityStillMatches('TDFY1234', '50000', { ...live, accountSize: 0 }), false);
// A prefix edit.
assert.equal(identityStillMatches('TDFY1234', '50000', { ...live, prefix: 'TDFX' }), false);
// A $0-start type caches 0 when it matches.
assert.equal(identityStillMatches('XFA77', '0', { prefix: 'XFA', accountSize: 0 }), true);
assert.equal(identityStillMatches('XFA77', '150000', { prefix: 'XFA', accountSize: 0 }), false);
// Nothing cached, or junk: not provably the same account.
assert.equal(identityStillMatches('TDFY1234', null, live), false);
assert.equal(identityStillMatches('TDFY1234', 'abc', live), false);
assert.equal(identityStillMatches('TDFY1234', '', { ...live, accountSize: 0 }), false);
});
test('pickedDayRulesDate: today is now; another day its start, never before Chicago\'s', () => {
const nowMs = Date.parse('2026-10-18T19:30:00.000Z');
const at = (localMidnightMs, today = '2026-10-18') => ({ today, nowMs, localMidnightMs });
// Today: now, whatever the zone.
assert.equal(pickedDayRulesDate('2026-10-18', at(Date.parse('2026-10-18T05:00:00Z'))), '2026-10-18T19:30:00.000Z');
// Chicago browser: its own midnight.
assert.equal(pickedDayRulesDate('2026-10-15', at(Date.parse('2026-10-15T05:00:00Z'))), '2026-10-15T05:00:00.000Z');
// Pacific browser: local midnight is later than Chicago's — stands.
assert.equal(pickedDayRulesDate('2026-10-15', at(Date.parse('2026-10-15T07:00:00Z'))), '2026-10-15T07:00:00.000Z');
// London (BST), Eastern and Karachi browsers: raised to Chicago's midnight.
for (const local of ['2026-10-14T23:00:00Z', '2026-10-15T04:00:00Z', '2026-10-14T19:00:00Z']) {
assert.equal(pickedDayRulesDate('2026-10-15', at(Date.parse(local))), '2026-10-15T05:00:00.000Z', local);
}
assert.equal(pickedDayRulesDate('2026-02-31', at(0)), null);
assert.equal(pickedDayRulesDate('2026-10-15', at(NaN)), null);
});
test('pickedDayRulesDate: a picked day never lands inside the evidence of the day before', () => {
// A browser at UTC+1 picks Oct 15 for a set closing at Chicago midnight
// of Oct 15: an account first funded Oct 14 stays on the old rules, the
// hand-dated one on Oct 15 does not.
const kept = [{ id: 'g', openedAt: null, closedAt: chicagoDayStart('2026-10-15') }];
const picked = pickedDayRulesDate('2026-10-15', {
today: '2026-10-18', nowMs: Date.parse('2026-10-18T12:00:00Z'), localMidnightMs: Date.parse('2026-10-14T23:00:00Z'),
});
assert.equal(pickKeptRules(kept, evidenceRulesDate('2026-10-14'))?.id, 'g');
assert.equal(pickKeptRules(kept, picked), null);
for (let ms = Date.UTC(2026, 0, 2); ms < Date.UTC(2027, 0, 1); ms += 86_400_000) {
const day = new Date(ms).toISOString().slice(0, 10);
const prev = new Date(ms - 86_400_000).toISOString().slice(0, 10);
for (const offsetH of [-10, -8, -6, -5, -4, 0, 1, 5, 9, 14]) {
const localMidnightMs = Date.parse(`${day}T00:00:00Z`) - offsetH * 3_600_000;
const stored = pickedDayRulesDate(day, { today: '2030-01-01', nowMs: 0, localMidnightMs });
assert.ok(stored > evidenceRulesDate(prev), `${day} at UTC${offsetH}`);
}
}
});
test('unchangedDaySaves: an untouched day saves only when it moves the account to other rules', () => {
// A change at 2:05 PM CDT today (19:05Z) with the default cutoff "now".
const kept = [{ id: 'g', openedAt: null, closedAt: '2026-10-18T19:05:00.000Z' }];
const at = { today: '2026-10-18', nowMs: Date.parse('2026-10-18T19:30:00Z'), localMidnightMs: Date.parse('2026-10-18T05:00:00Z') };
// Dated this morning (old rules): ✓ on today stores now → current rules.
const now = pickedDayRulesDate('2026-10-18', at);
assert.equal(unchangedDaySaves(kept, '2026-10-18T15:00:00.000Z', now), true);
assert.equal(pickKeptRules(kept, now), null);
// Already after the change: nothing to save.
assert.equal(unchangedDaySaves(kept, '2026-10-18T19:10:00.000Z', now), false);
// A past day's start lands where the date already is: nothing to save.
const oct2 = pickedDayRulesDate('2026-10-02', at);
assert.equal(unchangedDaySaves(kept, evidenceRulesDate('2026-10-02'), oct2), false);
// Windows not known (still loading, no firm): never.
assert.equal(unchangedDaySaves(null, '2026-10-18T15:00:00.000Z', now), false);
// The editor opened on Oct 18 (current rules) and a poll since lowered the
// date to Oct 2: an untouched ✓ compares against the opened date, so it
// doesn't pin Oct 18 by hand over the evidence.
const sets = [{ id: 'g2', openedAt: null, closedAt: '2026-10-10T00:00:00.000Z' }];
const oct18 = pickedDayRulesDate('2026-10-18', { ...at, today: '2026-10-19' });
assert.equal(unchangedDaySaves(sets, '2026-10-18T15:00:00.000Z', oct18), false);
});
test('lowersRulesDate: only earlier, only auto dates — the test lowerAutoRulesDate runs in SQL', () => {
const evidence = '2026-10-01T23:59:59.999Z';
assert.equal(lowersRulesDate({ rules_date: '2026-10-18T14:00:00.000Z', rules_date_manual: 0 }, evidence), true);
assert.equal(lowersRulesDate({ rules_date: null, rules_date_manual: 0 }, evidence), true);
assert.equal(lowersRulesDate({ rules_date: evidence, rules_date_manual: 0 }, evidence), false);
assert.equal(lowersRulesDate({ rules_date: '2026-09-01T00:00:00.000Z', rules_date_manual: 0 }, evidence), false);
// Set by hand: never moved.
assert.equal(lowersRulesDate({ rules_date: '2026-10-18T14:00:00.000Z', rules_date_manual: 1 }, evidence), false);
assert.equal(lowersRulesDate({ rules_date: null, rules_date_manual: 1 }, evidence), false);
// String order is time order for the two formats that are ever stored.
assert.equal('2026-10-01T23:59:59.999Z' < '2026-10-02T00:00:00.000Z', true);
assert.equal(evidenceRulesDate('2026-10-02') > new Date(Date.parse('2026-10-02T23:59:59.998Z')).toISOString(), true);
});
// ── the whole selection, as getAccountConfig runs it ────────────────────────
test('selection: generations, boundaries and clamps end to end', () => {
const sets = [
keptOf({ profitTarget: 3000, consistency: 0.5 }, G1, 'g1'),
keptOf({ profitTarget: 2800, consistency: 0.45 }, G2, 'g2'),
];
const rulesFor = (rulesDate) => {
const kept = pickKeptRules(sets, rulesDate);
return kept ? overlayKept(LIVE, kept) : LIVE;
};
const cases = [
[null, 2500, undefined],
['2026-09-01T00:00:00.000Z', 3000, 'g1'],
[G1.closedAt, 2800, 'g2'],
[G2.closedAt, 2500, undefined],
[evidenceRulesDate('2026-10-02'), 2800, 'g2'],
[evidenceRulesDate('2026-10-01'), 3000, 'g1'],
];
for (const [date, target, keptId] of cases) {
const eff = rulesFor(date);
assert.equal(eff.profitTarget, target, String(date));
assert.equal(eff.keptRules?.id, keptId, String(date));
// Always reported as the live type.
assert.equal(eff.id, LIVE.id);
}
});