Tradeify247's API user may not read DXtrade market data (code 34), which skipped every batch at the instrument pick. Quotes and hourly candles then come from Hyperliquid's public API (BTC, ETH on the main dex; XYZ100, SP500 and tokenized stocks on trade[XYZ]) — for the pick and sizing only; orders, fills and exit prices stay DXtrade's. Closing STOP/LIMIT orders close the whole position by positionCode and must not carry a quantity (code 33) — the exit bracket was refused, and the daily-loss guard flattened both accounts. A close's fill price also falls back to its executions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
509 lines
28 KiB
JavaScript
509 lines
28 KiB
JavaScript
// Checks for the pure trading math in lib/trading-logic.ts.
|
||
//
|
||
// npm run check
|
||
//
|
||
// .mjs on purpose: Next's tsconfig includes every .ts file, and a .ts test
|
||
// importing another .ts by extension would trip the build's type check. Node
|
||
// strips the types from lib/trading-logic.ts at import time (it has no
|
||
// imports of its own).
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
percentFee, takeProfitPlan, obfuscationStopDollars, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||
botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS,
|
||
projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP,
|
||
} from '../lib/trading-logic.ts';
|
||
|
||
// Deterministic PRNG so a failure reproduces.
|
||
function rng(seed) {
|
||
let s = seed >>> 0;
|
||
return () => {
|
||
s = (s + 0x6d2b79f5) >>> 0;
|
||
let t = s;
|
||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||
};
|
||
}
|
||
|
||
// ── parity with the pre-refactor inline code ────────────────────────────────
|
||
|
||
/** The take-profit exactly as auto-trade.ts computed it before takeProfitPlan. */
|
||
function oldTakeProfit(side, entry, leg, pv, contracts, tickSize, entryCommission) {
|
||
const totalCommission = entryCommission * 2;
|
||
const grossTarget = leg + totalCommission;
|
||
const targetPoints = grossTarget / (pv * contracts);
|
||
const ticks = Math.ceil(targetPoints / tickSize);
|
||
const exitPrice = side === 'Buy' ? entry + ticks * tickSize : entry - ticks * tickSize;
|
||
return { exitPrice, ticks, totalCommission, grossTarget };
|
||
}
|
||
|
||
test('takeProfitPlan reproduces the old formula when fees are measured', () => {
|
||
const r = rng(1);
|
||
for (let i = 0; i < 2000; i++) {
|
||
const side = r() < 0.5 ? 'Buy' : 'Sell';
|
||
const [pv, tick] = [[20, 0.25], [2, 0.25], [100, 0.1], [1000, 0.01], [50, 0.25]][Math.floor(r() * 5)];
|
||
const entry = Math.round((1000 + r() * 20000) / tick) * tick;
|
||
const leg = Math.round(5 + r() * 2500);
|
||
const contracts = 1 + Math.floor(r() * 4);
|
||
const fee = Math.round(r() * 1000) / 100;
|
||
const got = takeProfitPlan({ side, entry, leg, pointValue: pv, contracts, tickSize: tick, commissionPct: 0, entryCommission: fee });
|
||
const want = oldTakeProfit(side, entry, leg, pv, contracts, tick, fee);
|
||
assert.equal(got.exitPrice, want.exitPrice);
|
||
assert.equal(got.ticks, want.ticks);
|
||
assert.equal(got.totalCommission, want.totalCommission);
|
||
assert.equal(got.grossTarget, want.grossTarget);
|
||
}
|
||
});
|
||
|
||
/** obfuscationStopDollars exactly as it lived in auto-trade.ts. */
|
||
function oldObfuscation(cfg, dailyPnL, plan, tradeDate, legAmount) {
|
||
if (!cfg.firstDayStop && !cfg.minDayStop) return Infinity;
|
||
const prior = dailyPnL.filter((d) => d.date < tradeDate && d.pnl !== 0).sort((a, b) => a.date.localeCompare(b.date));
|
||
if (prior.length > 0 && prior[prior.length - 1].pnl < 0) return Infinity;
|
||
let stop = Infinity;
|
||
const realizedToday = Math.round((plan.dayTarget - plan.remainingToday) * 100) / 100;
|
||
if (cfg.firstDayStop && prior.length === 0) stop = Math.max(0, 1000 + realizedToday);
|
||
if (cfg.minDayStop && plan.dayTarget <= 1700) stop = Math.min(stop, 2 * legAmount);
|
||
return stop;
|
||
}
|
||
|
||
test('obfuscationStopDollars is unchanged by the move', () => {
|
||
const r = rng(2);
|
||
for (let i = 0; i < 1000; i++) {
|
||
const cfg = { firstDayStop: r() < 0.5, minDayStop: r() < 0.5 };
|
||
const days = Math.floor(r() * 4);
|
||
const dailyPnL = Array.from({ length: days }, (_, d) => ({ date: `2026-09-0${d + 1}`, pnl: Math.round((r() - 0.4) * 2000) }));
|
||
const dayTarget = Math.round(r() * 3000);
|
||
const plan = { dayTarget, remainingToday: Math.round(dayTarget * r()), path: 'normal_day', splitAllowed: true };
|
||
const leg = Math.round(100 + r() * 1500);
|
||
assert.equal(obfuscationStopDollars(cfg, dailyPnL, plan, '2026-09-08', leg), oldObfuscation(cfg, dailyPnL, plan, '2026-09-08', leg));
|
||
}
|
||
});
|
||
|
||
/** isInNoTradeWindow as it was, with the clock and settings passed in. */
|
||
function oldNoTrade(now, mode, jitter) {
|
||
const centralParts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: 'numeric', hour12: false, timeZone: 'America/Chicago' }).formatToParts(now);
|
||
const hour = parseInt(centralParts.find((p) => p.type === 'hour').value, 10);
|
||
const minute = parseInt(centralParts.find((p) => p.type === 'minute').value, 10);
|
||
const day = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'America/Chicago' }).format(now);
|
||
const timeMinutes = hour * 60 + minute;
|
||
if (mode === 'equity_hours') {
|
||
if (day === 'Sat' || day === 'Sun') return true;
|
||
const open = 8 * 60 + 35 + jitter;
|
||
const close = 14 * 60 + 55;
|
||
return timeMinutes < open || timeMinutes >= close;
|
||
}
|
||
if (day === 'Sat') return true;
|
||
const cmeOpen = 17 * 60 + 5 + jitter;
|
||
if (day === 'Sun') return timeMinutes < cmeOpen;
|
||
const cmeClose = 14 * 60 + 55;
|
||
if (day === 'Fri' && timeMinutes >= cmeClose) return true;
|
||
if (timeMinutes >= cmeClose && timeMinutes < cmeOpen) return true;
|
||
return false;
|
||
}
|
||
|
||
function oldAfterClose(now) {
|
||
const parts = new Intl.DateTimeFormat('en-US', { weekday: 'short', hour: 'numeric', minute: 'numeric', hour12: false, timeZone: 'America/Chicago' }).formatToParts(now);
|
||
const get = (type) => parts.find((p) => p.type === type)?.value ?? '';
|
||
if (get('weekday') === 'Sat' || get('weekday') === 'Sun') return false;
|
||
const minutes = (parseInt(get('hour'), 10) % 24) * 60 + parseInt(get('minute'), 10);
|
||
return minutes >= 14 * 60 + 55 && minutes < 17 * 60;
|
||
}
|
||
|
||
test('CME trading hours are unchanged, across both DST regimes', () => {
|
||
for (const start of [Date.UTC(2026, 8, 20), Date.UTC(2026, 0, 11)]) { // a September and a January week
|
||
for (let m = 0; m < 7 * 24 * 60; m += 7) {
|
||
const now = new Date(start + m * 60_000);
|
||
for (const mode of ['full_cme', 'equity_hours']) {
|
||
for (const jitter of [1, 13, 20]) {
|
||
assert.equal(isNoTradeTime(now, { everyDay: false, mode, jitterMin: jitter }), oldNoTrade(now, mode, jitter), `${now.toISOString()} ${mode} ${jitter}`);
|
||
}
|
||
}
|
||
assert.equal(isPastSessionClose(now, false), oldAfterClose(now), now.toISOString());
|
||
}
|
||
}
|
||
});
|
||
|
||
// ── every-day hours ──────────────────────────────────────────────────────────
|
||
|
||
const ct = (iso, offsetHours) => new Date(Date.parse(`${iso}Z`) + offsetHours * 3600_000);
|
||
const cdt = (iso) => ct(iso, 5); // Central daylight: UTC−5
|
||
const cst = (iso) => ct(iso, 6); // Central standard: UTC−6
|
||
|
||
test('every-day firms close only around the roll (CDT)', () => {
|
||
const o = { everyDay: true, mode: 'full_cme', jitterMin: 7 };
|
||
assert.equal(isNoTradeTime(cdt('2026-09-26T12:00:00'), o), false); // Saturday noon
|
||
assert.equal(isNoTradeTime(cdt('2026-09-27T09:00:00'), o), false); // Sunday morning
|
||
assert.equal(isNoTradeTime(cdt('2026-09-28T16:54:00'), o), false);
|
||
assert.equal(isNoTradeTime(cdt('2026-09-28T16:55:00'), o), true);
|
||
assert.equal(isNoTradeTime(cdt('2026-09-28T17:11:00'), o), true);
|
||
assert.equal(isNoTradeTime(cdt('2026-09-28T17:12:00'), o), false);
|
||
assert.equal(isPastSessionClose(cdt('2026-09-26T16:56:00'), true), true);
|
||
assert.equal(isPastSessionClose(cdt('2026-09-26T17:01:00'), true), false);
|
||
});
|
||
|
||
test('every-day firms close from 3:55 PM in CST, covering the 4 PM firm reset', () => {
|
||
const o = { everyDay: true, mode: 'equity_hours', jitterMin: 1 };
|
||
assert.equal(isNoTradeTime(cst('2027-01-12T15:54:00'), o), false);
|
||
assert.equal(isNoTradeTime(cst('2027-01-12T15:55:00'), o), true);
|
||
assert.equal(isNoTradeTime(cst('2027-01-12T16:30:00'), o), true);
|
||
assert.equal(isNoTradeTime(cst('2027-01-12T17:05:00'), o), true);
|
||
assert.equal(isNoTradeTime(cst('2027-01-12T17:06:00'), o), false);
|
||
assert.equal(isNoTradeTime(cst('2027-01-16T12:00:00'), o), false); // Saturday
|
||
assert.equal(isPastSessionClose(cst('2027-01-12T16:10:00'), true), true);
|
||
});
|
||
|
||
// ── fees ─────────────────────────────────────────────────────────────────────
|
||
|
||
test('percentFee rounds each side up to the cent', () => {
|
||
assert.equal(percentFee(0.04, 60000, 1, 1), 24);
|
||
assert.equal(percentFee(0.04, 100000, 0.2, 1), 8);
|
||
assert.equal(percentFee(0.04, 123.45, 0.001, 7), 0.01); // 0.00035 → a cent
|
||
assert.equal(percentFee(0, 60000, 1, 1), 0);
|
||
});
|
||
|
||
test('percent-fee take-profits net at least the leg, and one tick less does not', () => {
|
||
const r = rng(3);
|
||
for (let i = 0; i < 3000; i++) {
|
||
const side = r() < 0.5 ? 'Buy' : 'Sell';
|
||
const tick = [0.01, 0.1, 0.5, 1][Math.floor(r() * 4)];
|
||
const entry = Math.round((50 + r() * 100000) / tick) * tick;
|
||
const step = [0.001, 0.01, 1][Math.floor(r() * 3)];
|
||
const contracts = 1 + Math.floor(r() * 400);
|
||
const pct = [0.04, 0.02, 0.1][Math.floor(r() * 3)];
|
||
const leg = Math.round(5 + r() * 2000);
|
||
const entryFee = percentFee(pct, entry, step, contracts);
|
||
const tp = takeProfitPlan({ side, entry, leg, pointValue: step, contracts, tickSize: tick, commissionPct: pct, entryCommission: entryFee });
|
||
const k = step * contracts;
|
||
const dir = side === 'Buy' ? 1 : -1;
|
||
const net = (ticks) => {
|
||
const x = entry + dir * ticks * tick;
|
||
return ticks * tick * k - entryFee - percentFee(pct, x, step, contracts);
|
||
};
|
||
assert.ok(net(tp.ticks) >= leg - 1e-6, `short of the leg: ${JSON.stringify({ side, entry, tick, step, contracts, pct, leg, tp })}`);
|
||
if (tp.ticks > 1) assert.ok(net(tp.ticks - 1) < leg + 0.02, `a tick further than needed: ${JSON.stringify(tp)}`);
|
||
assert.equal(tp.totalCommission, Math.round((entryFee + tp.exitFee) * 100) / 100);
|
||
}
|
||
});
|
||
|
||
// ── daily loss limit ─────────────────────────────────────────────────────────
|
||
|
||
test('dllRoom: snapshot, fallback, green-day cap, no limit', () => {
|
||
assert.equal(dllRoom({ dailyLossLimit: 0, closedBalance: 50000, realizedToday: -900 }), Infinity);
|
||
assert.equal(dllRoom({ dailyLossLimit: 1500, closedBalance: 49600, dayStartBalance: 50000, realizedToday: 0 }), 1100);
|
||
assert.equal(dllRoom({ dailyLossLimit: 1500, closedBalance: 50600, dayStartBalance: 50000, realizedToday: 0 }), 1500);
|
||
assert.equal(dllRoom({ dailyLossLimit: 1000, closedBalance: 50000, dayStartBalance: null, realizedToday: -200 }), 800);
|
||
assert.equal(dllRoom({ dailyLossLimit: 1000, closedBalance: 50000, realizedToday: 350 }), 1000);
|
||
});
|
||
|
||
test('the worked futures example: $1,000 limit, down $200, NQ at $2.14/side', () => {
|
||
const room = dllRoom({ dailyLossLimit: 1000, closedBalance: 49800, realizedToday: -200 });
|
||
const d = dllStopDollars({ room, dailyLossLimit: 1000, entryFee: 2.14, commissionPct: 0 });
|
||
const ticks = stopTicks({ obfuscationDollars: Infinity, dllDollars: d, pointValue: 20, contracts: 1, tickSize: 0.25 });
|
||
assert.equal(ticks, 139);
|
||
const loss = ticks * 0.25 * 20 + 2 * 2.14;
|
||
assert.ok(-200 - loss > -1000 + 100 - 1e-9);
|
||
});
|
||
|
||
test('stopTicks: obfuscation rounds out, the daily-loss stop rounds in, the tighter wins', () => {
|
||
const base = { pointValue: 20, contracts: 1, tickSize: 0.25 };
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: Infinity, dllDollars: Infinity }), null);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: 0, dllDollars: Infinity }), null);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: 1001, dllDollars: Infinity }), 201);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: Infinity, dllDollars: 1000 }), 200);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: Infinity, dllDollars: 999.99 }), 199);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: 600, dllDollars: 1000 }), 120);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: 1600, dllDollars: 1000 }), 200);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: Infinity, dllDollars: 3 }), 0);
|
||
assert.equal(stopTicks({ ...base, obfuscationDollars: Infinity, dllDollars: -40 }), 0);
|
||
});
|
||
|
||
test('worst case at the stop, both fees paid, never spends past room − reserve', () => {
|
||
const r = rng(4);
|
||
for (let i = 0; i < 20000; i++) {
|
||
const side = r() < 0.5 ? 'Buy' : 'Sell';
|
||
const pctFirm = r() < 0.6;
|
||
const pct = pctFirm ? [0.04, 0.1][Math.floor(r() * 2)] : 0;
|
||
const tick = pctFirm ? [0.01, 0.1, 1][Math.floor(r() * 3)] : 0.25;
|
||
const pv = pctFirm ? [0.001, 0.01, 1][Math.floor(r() * 3)] : [2, 20, 50][Math.floor(r() * 3)];
|
||
const contracts = pctFirm ? 1 + Math.floor(r() * 500) : 1 + Math.floor(r() * 3);
|
||
const entry = Math.round((100 + r() * 90000) / tick) * tick;
|
||
const dll = [300, 750, 1500, 3000][Math.floor(r() * 4)];
|
||
const room = dllRoom({ dailyLossLimit: dll, closedBalance: 50000, dayStartBalance: 50000 + Math.round((r() - 0.3) * dll), realizedToday: 0 });
|
||
const entryFee = pctFirm ? percentFee(pct, entry, pv, contracts) : Math.round(r() * 500) / 100 * contracts;
|
||
const d = dllStopDollars({ room, dailyLossLimit: dll, entryFee, commissionPct: pct });
|
||
const ticks = stopTicks({ obfuscationDollars: Infinity, dllDollars: d, pointValue: pv, contracts, tickSize: tick });
|
||
if (ticks === 0) continue; // flattened instead
|
||
const stopPrice = side === 'Buy' ? entry - ticks * tick : entry + ticks * tick;
|
||
const exitFee = pctFirm ? percentFee(pct, stopPrice, pv, contracts) : entryFee;
|
||
const loss = ticks * tick * pv * contracts + entryFee + exitFee;
|
||
assert.ok(loss <= room - DLL_RESERVE_FRACTION * dll + 1e-6, JSON.stringify({ side, pct, tick, pv, contracts, entry, dll, room, ticks, loss }));
|
||
}
|
||
});
|
||
|
||
test('dllViable and botStopsActive', () => {
|
||
assert.equal(dllViable(Infinity, 0, 0), true);
|
||
assert.equal(dllViable(1500, 1500, 20), true);
|
||
assert.equal(dllViable(150 + DLL_MIN_STOP + 20, 1500, 20), true);
|
||
assert.equal(dllViable(150 + DLL_MIN_STOP + 19.99, 1500, 20), false);
|
||
assert.equal(botStopsActive({ firstDayStop: false, minDayStop: false }), false);
|
||
assert.equal(botStopsActive({ firstDayStop: false, minDayStop: false, dailyLossLimit: 1500 }), true);
|
||
assert.equal(botStopsActive({ firstDayStop: true, minDayStop: false, dailyLossLimit: 0 }), true);
|
||
});
|
||
|
||
// ── sizing ───────────────────────────────────────────────────────────────────
|
||
|
||
const btcSigma = 0.03 / Math.sqrt(24); // 3%/day
|
||
|
||
test('volatility sizing hits the target hold, and the example table', () => {
|
||
const stop = () => 1350;
|
||
const at = (hours) => sizeNotional({ leg: 600, stopDollarsAt: stop, volatilityPerHour: btcSigma, targetHoldHours: hours, commissionPct: 0, maxNotional: 0 });
|
||
for (const [hours, notional] of [[54, 20000], [24, 30000], [6, 60000]]) {
|
||
const s = at(hours);
|
||
assert.ok(Math.abs(s.notional - notional) / notional < 0.001, `${hours}h → ${s.notional}`);
|
||
assert.ok(Math.abs(s.predictedHoldHours - hours) / hours < 0.001);
|
||
assert.ok(Math.abs(s.winProbability - 1350 / 1950) < 1e-9);
|
||
}
|
||
});
|
||
|
||
test('sizing: fees feed back, the cap binds, financing only raises N, defaults', () => {
|
||
const base = { leg: 600, volatilityPerHour: btcSigma, targetHoldHours: 6, commissionPct: 0.04, spreadFraction: 0.0002, maxNotional: 0 };
|
||
// Daily-loss stop shrinks as fees grow with N.
|
||
const stopDollarsAt = (n) => dllStopDollars({ room: 1500, dailyLossLimit: 1500, entryFee: 0.0004 * n, commissionPct: 0.04 });
|
||
const s = sizeNotional({ ...base, stopDollarsAt });
|
||
const a = 600 + 2 * 0.0004 * s.notional;
|
||
const b = stopDollarsAt(s.notional);
|
||
assert.ok(Math.abs(s.predictedHoldHours - 6) < 0.05, `hold ${s.predictedHoldHours}`);
|
||
assert.ok(Math.abs(s.winProbability - b / (a + b)) < 1e-9);
|
||
|
||
const capped = sizeNotional({ ...base, stopDollarsAt, maxNotional: 25000 });
|
||
assert.equal(capped.notional, 25000);
|
||
assert.ok(capped.predictedHoldHours > 6);
|
||
|
||
const financed = sizeNotional({ ...base, stopDollarsAt, targetHoldHours: 200, financingPctPerDay: 0.033 });
|
||
const unfinanced = sizeNotional({ ...base, stopDollarsAt, targetHoldHours: 200 });
|
||
assert.ok(financed.notional > unfinanced.notional);
|
||
|
||
// No target hold = 12 h.
|
||
const dflt = sizeNotional({ ...base, targetHoldHours: 0, stopDollarsAt });
|
||
assert.ok(Math.abs(dflt.predictedHoldHours - DEFAULT_TARGET_HOLD_HOURS) < 0.1, `hold ${dflt.predictedHoldHours}`);
|
||
// A stopless leg is sized as if the stop sat as far as the target.
|
||
const stopless = sizeNotional({ ...base, stopDollarsAt: () => Infinity });
|
||
assert.ok(Math.abs(stopless.predictedHoldHours - 6) < 0.05);
|
||
assert.equal(stopless.winProbability, null);
|
||
// No volatility, no size.
|
||
assert.equal(sizeNotional({ ...base, volatilityPerHour: undefined, stopDollarsAt }), null);
|
||
});
|
||
|
||
test('random-walk simulation agrees with the predicted hold and win rate', () => {
|
||
const r = rng(5);
|
||
const notional = 60000;
|
||
const A = 600;
|
||
const B = 1350;
|
||
const sigma = btcSigma;
|
||
const dtHours = 1 / 60; // one-minute steps
|
||
const stepDollars = notional * sigma * Math.sqrt(dtHours);
|
||
const predicted = (A / notional) * (B / notional) / (sigma * sigma);
|
||
let wins = 0;
|
||
let totalHours = 0;
|
||
const paths = 3000;
|
||
for (let p = 0; p < paths; p++) {
|
||
let x = 0;
|
||
let steps = 0;
|
||
while (x < A && x > -B) {
|
||
x += r() < 0.5 ? stepDollars : -stepDollars;
|
||
steps++;
|
||
}
|
||
if (x >= A) wins++;
|
||
totalHours += steps * dtHours;
|
||
}
|
||
const meanHold = totalHours / paths;
|
||
assert.ok(Math.abs(meanHold - predicted) / predicted < 0.1, `mean hold ${meanHold} vs predicted ${predicted}`);
|
||
assert.ok(Math.abs(wins / paths - B / (A + B)) < 0.03, `win rate ${wins / paths}`);
|
||
});
|
||
|
||
// ── every-day calendar ───────────────────────────────────────────────────────
|
||
|
||
test('nextTradingDate and effectiveCombine', () => {
|
||
assert.equal(nextTradingDate('2026-09-25'), '2026-09-28');
|
||
assert.equal(nextTradingDate('2026-09-25', true), '2026-09-26');
|
||
assert.equal(nextTradingDate('2026-12-24', true), '2026-12-25');
|
||
assert.equal(effectiveCombine({ combineCmeHolidays: true }), true);
|
||
assert.equal(effectiveCombine({ combineCmeHolidays: true, tradesEveryDay: true }), false);
|
||
});
|
||
|
||
const projCfg = {
|
||
profitTarget: 3000, consistency: 0, minDayPnL: 0, minTradingDays: 5, accountSize: 50000,
|
||
targetSameEquity: false, withdrawalStages: [], splitEnabled: true,
|
||
};
|
||
|
||
test('every-day projections use consecutive dates; CME ones still skip weekends', () => {
|
||
const now = cdt('2026-09-26T12:00:00'); // Saturday noon → trade date Saturday
|
||
const common = { accountId: 7, cycleDailyPnL: [], fundTransactions: [], balance: 50000, realizedPnL: 0, now };
|
||
const every = projectPerfectPath({ ...common, cfg: { ...projCfg, tradesEveryDay: true } });
|
||
const cme = projectPerfectPath({ ...common, cfg: projCfg });
|
||
assert.ok(every.length > 1);
|
||
for (let i = 1; i < every.length; i++) assert.equal(every[i].date, nextTradingDate(every[i - 1].date, true));
|
||
for (const d of cme) {
|
||
const dow = new Date(`${d.date}T00:00:00Z`).getUTCDay();
|
||
assert.ok(dow !== 0 && dow !== 6, `CME projection landed on ${d.date}`);
|
||
}
|
||
// Saturday itself is booked (silently) for the every-day firm: its first
|
||
// projected day is Sunday, the CME one's is Monday.
|
||
assert.equal(every[0].date, '2026-09-27');
|
||
assert.equal(cme[0].date, '2026-09-28');
|
||
});
|
||
|
||
test('an every-day firm can finish its cycle on a Saturday', () => {
|
||
const now = cdt('2026-09-26T12:00:00');
|
||
const cycleDailyPnL = [
|
||
{ date: '2026-09-21', pnl: 700 }, { date: '2026-09-22', pnl: 700 },
|
||
{ date: '2026-09-23', pnl: 700 }, { date: '2026-09-24', pnl: 700 },
|
||
];
|
||
const opts = {
|
||
accountId: 7, rawTradeDate: '2026-09-26', cycleDailyPnL, rawRealizedPnL: 400, combineCmeHolidays: false,
|
||
effective: { profitTarget: 3000, consistency: 0, minTradingDays: 5, minDayPnL: 0, drain: false },
|
||
equityProfit: 3200, splitEnabled: true, now,
|
||
};
|
||
assert.equal(cycleCompletion({ ...opts, tradesEveryDay: true }), 'today');
|
||
assert.equal(cycleCompletion(opts), 'none');
|
||
});
|
||
|
||
// ── DXTrade helpers (lib/dxtrade-helpers.ts) ─────────────────────────────────
|
||
|
||
import {
|
||
dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep,
|
||
lastUtcReset, ctTradeDayStart, parseDurationMs, ciNum, signedFundValue, inTradingHours,
|
||
realizedVolPerHour, scoreCandidate, candidateRejection, MIN_VOL_CANDLES,
|
||
} from '../lib/dxtrade-helpers.ts';
|
||
|
||
test('dxAccountId is stable, safe, far above broker ids, and host-scoped', () => {
|
||
const a = dxAccountId('dx.tradeify247.co', 'default:TFY-1');
|
||
assert.equal(a, dxAccountId('DX.Tradeify247.co', 'default:TFY-1'));
|
||
assert.ok(Number.isSafeInteger(a) && a >= 2 ** 50 && a < 2 ** 53);
|
||
assert.notEqual(a, dxAccountId('dx.other.co', 'default:TFY-1'));
|
||
const seen = new Set();
|
||
for (let i = 0; i < 20000; i++) seen.add(dxAccountId('dx.tradeify247.co', `default:${i}`));
|
||
assert.equal(seen.size, 20000);
|
||
});
|
||
|
||
test('account codes and contract names', () => {
|
||
assert.equal(accountDisplayName('default:TFY-50K-12'), 'TFY-50K-12');
|
||
assert.equal(accountDisplayName('plain'), 'plain');
|
||
assert.equal(accountCodeOf({ clearing: 'default', account: 'x1' }), 'default:x1');
|
||
assert.equal(accountCodeOf('default:x1'), 'default:x1');
|
||
assert.deepEqual(decodeContractName(encodeContractName('BTC/USD', 0.001)), { symbol: 'BTC/USD', step: 0.001 });
|
||
assert.equal(decodeContractName('NQZ5'), null);
|
||
assert.equal(roundToStep(0.1 + 0.2, 0.1), 0.3);
|
||
assert.equal(roundToStep(12 * 0.001, 0.001), 0.012);
|
||
assert.equal(roundToStep(101.234, 0.5, 'up'), 101.5);
|
||
assert.equal(roundToStep(101.234, 0.5, 'down'), 101);
|
||
});
|
||
|
||
test('the firm reset and the CT trade-date start', () => {
|
||
assert.equal(lastUtcReset(new Date('2026-09-26T21:59:00Z')).toISOString(), '2026-09-25T22:00:00.000Z');
|
||
assert.equal(lastUtcReset(new Date('2026-09-26T22:00:00Z')).toISOString(), '2026-09-26T22:00:00.000Z');
|
||
assert.equal(ctTradeDayStart(cdt('2026-09-26T18:30:00')).toISOString(), '2026-09-26T22:00:00.000Z');
|
||
assert.equal(ctTradeDayStart(cdt('2026-09-27T09:00:00')).toISOString(), '2026-09-26T22:00:00.000Z');
|
||
assert.equal(ctTradeDayStart(cst('2027-01-12T10:00:00')).toISOString(), '2027-01-11T23:00:00.000Z');
|
||
});
|
||
|
||
test('loose parsing', () => {
|
||
assert.equal(parseDurationMs('PT30M'), 30 * 60_000);
|
||
assert.equal(parseDurationMs('01:05:08'), (3600 + 300 + 8) * 1000);
|
||
assert.equal(parseDurationMs({ seconds: 900 }), 900_000);
|
||
assert.equal(parseDurationMs(undefined), 30 * 60_000);
|
||
assert.equal(ciNum({ openPL: 12.5 }, 'openPl'), 12.5);
|
||
assert.equal(ciNum({ openPL: '12' }, 'openPl'), undefined);
|
||
assert.equal(signedFundValue('WITHDRAWAL', 500), -500);
|
||
assert.equal(signedFundValue('WITHDRAWAL', -500), -500);
|
||
assert.equal(signedFundValue('DEPOSIT', 50000), 50000);
|
||
});
|
||
|
||
test('trading hours from UTC week sessions', () => {
|
||
const at = (dayOfWeek, hour, eventType) => ({ eventType, weekDay: { dayOfWeek, time: { hour, minute: 30 } } });
|
||
const weekdays = [at('MONDAY', 13, 'SESSION_OPEN'), at('MONDAY', 20, 'SESSION_CLOSE'), at('TUESDAY', 13, 'SESSION_OPEN'), at('TUESDAY', 20, 'SESSION_CLOSE')];
|
||
assert.equal(inTradingHours(weekdays, new Date('2026-09-28T15:00:00Z')), true); // Monday
|
||
assert.equal(inTradingHours(weekdays, new Date('2026-09-28T21:00:00Z')), false);
|
||
assert.equal(inTradingHours(weekdays, new Date('2026-09-27T15:00:00Z')), false); // Sunday wraps to Tuesday's close
|
||
assert.equal(inTradingHours([], new Date()), true);
|
||
assert.equal(inTradingHours([{ garbage: true }], new Date()), true);
|
||
});
|
||
|
||
test('realized volatility of hourly closes', () => {
|
||
assert.equal(realizedVolPerHour([100, 101, 102]), null);
|
||
const r = rng(6);
|
||
const sigma = 0.004;
|
||
const closes = [30000];
|
||
for (let i = 0; i < 2000; i++) {
|
||
// Box–Muller for a normal step
|
||
const z = Math.sqrt(-2 * Math.log(1 - r())) * Math.cos(2 * Math.PI * r());
|
||
closes.push(closes[closes.length - 1] * Math.exp(sigma * z));
|
||
}
|
||
const v = realizedVolPerHour(closes);
|
||
assert.ok(Math.abs(v - sigma) / sigma < 0.08, `σ ${v}`);
|
||
const flat = Array.from({ length: MIN_VOL_CANDLES + 1 }, () => 200);
|
||
assert.equal(realizedVolPerHour(flat), 0);
|
||
});
|
||
|
||
test('the selector ranks the plan example: ETH, BTC, XYZ100, AAPL', () => {
|
||
const perHour = (daily) => daily / Math.sqrt(24);
|
||
const score = (spread, daily) => scoreCandidate({
|
||
spreadFraction: spread, commissionPct: 0.04, financingPctPerDay: 0, targetHoldHours: 6, volatilityPerHour: perHour(daily),
|
||
});
|
||
const board = [
|
||
['BTC/USD', score(0.0002, 0.03)],
|
||
['XYZ100', score(0.0001, 0.012)],
|
||
['AAPL/USD (Sat)', score(0.0005, 0.004)],
|
||
['ETH/USD', score(0.0003, 0.04)],
|
||
['AAPL/USD (Tue)', score(0.0005, 0.015)],
|
||
].sort((a, b) => a[1] - b[1]).map((b) => b[0]);
|
||
assert.deepEqual(board, ['ETH/USD', 'BTC/USD', 'XYZ100', 'AAPL/USD (Tue)', 'AAPL/USD (Sat)']);
|
||
// Cost per leg = score × √(A·B/T): the example's $50 and $60.
|
||
const k = Math.sqrt((600 * 1350) / 6);
|
||
assert.ok(Math.abs(score(0.0003, 0.04) * k - 49.6) < 1);
|
||
assert.ok(Math.abs(score(0.0002, 0.03) * k - 60) < 1);
|
||
// No volatility, no score; financing over the hold adds to the cost.
|
||
assert.equal(scoreCandidate({ spreadFraction: 0.0002, commissionPct: 0.04, financingPctPerDay: 0, targetHoldHours: 6, volatilityPerHour: null }), null);
|
||
const withFinancing = scoreCandidate({ spreadFraction: 0.0002, commissionPct: 0.04, financingPctPerDay: 0.033, targetHoldHours: 12, volatilityPerHour: perHour(0.03) });
|
||
assert.ok(withFinancing > score(0.0002, 0.03));
|
||
});
|
||
|
||
test('the selector drops what cannot trade right now', () => {
|
||
const ok = {
|
||
tradingStatus: 'FULL', bid: 100, ask: 100.02, quoteAgeMs: 1000, inHours: true, candles: 49,
|
||
minContractsOk: true, currency: 'USD', accountCurrency: 'USD',
|
||
};
|
||
assert.equal(candidateRejection(ok), null);
|
||
assert.match(candidateRejection({ ...ok, tradingStatus: 'CLOSE_ONLY' }), /CLOSE_ONLY/);
|
||
assert.match(candidateRejection({ ...ok, inHours: false }), /hours/);
|
||
assert.match(candidateRejection({ ...ok, bid: undefined }), /quote/);
|
||
assert.match(candidateRejection({ ...ok, quoteAgeMs: 10 * 60_000 }), /old/);
|
||
assert.match(candidateRejection({ ...ok, ask: 101 }), /spread/);
|
||
assert.match(candidateRejection({ ...ok, candles: 10 }), /candles/);
|
||
assert.match(candidateRejection({ ...ok, currency: 'EUR' }), /EUR/);
|
||
});
|
||
|
||
// ── Hyperliquid price fallback (lib/hyperliquid-feed.ts) ─────────────────────
|
||
|
||
import { hyperliquidCoinFor, parseL2Book } from '../lib/hyperliquid-feed.ts';
|
||
|
||
test('DXtrade symbols map to Hyperliquid coins', () => {
|
||
const coins = new Set(['BTC', 'ETH', 'xyz:XYZ100', 'xyz:SP500', 'xyz:AAPL']);
|
||
assert.equal(hyperliquidCoinFor('BTC/USD', coins), 'BTC');
|
||
assert.equal(hyperliquidCoinFor('ETH/USD', coins), 'ETH');
|
||
assert.equal(hyperliquidCoinFor('XYZ100', coins), 'xyz:XYZ100');
|
||
assert.equal(hyperliquidCoinFor('SP500', coins), 'xyz:SP500');
|
||
assert.equal(hyperliquidCoinFor('AAPL/USD', coins), 'xyz:AAPL');
|
||
assert.equal(hyperliquidCoinFor('DOGE/USD', coins), null);
|
||
});
|
||
|
||
test('l2Book best bid/ask', () => {
|
||
const q = parseL2Book({ levels: [[{ px: '84310.0' }, { px: '84309' }], [{ px: '84311.0' }]], time: 1790461870878 });
|
||
assert.deepEqual(q, { bid: 84310, ask: 84311, time: 1790461870878 });
|
||
const empty = parseL2Book({ levels: [[], []] });
|
||
assert.equal(empty.bid, undefined);
|
||
assert.equal(empty.ask, undefined);
|
||
});
|