DXtrade sizing: volatility only (12h default hold); fees and every-day only for DXtrade

The symbol map's fixed notional per contract is gone: every DXtrade leg is
sized from the instrument's recent volatility for the firm's target hold
(blank = 12 h), and a candidate without a volatility isn't picked. Percent
commission and every-day trading are ignored on non-DXtrade firms, which keep
measured per-contract fees and the CME calendar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-26 17:29:32 -05:00
co-authored by Claude Opus 5.5
parent 28f465508d
commit d01f03b293
9 changed files with 87 additions and 100 deletions
+17 -14
View File
@@ -10,7 +10,7 @@ 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,
botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS,
projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP,
} from '../lib/trading-logic.ts';
@@ -258,18 +258,17 @@ 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, fixedNotional: 0, maxNotional: 0 });
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.equal(s.mode, 'volatility');
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, fixed fallback', () => {
const base = { leg: 600, volatilityPerHour: btcSigma, targetHoldHours: 6, commissionPct: 0.04, spreadFraction: 0.0002, fixedNotional: 20000, maxNotional: 0 };
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 });
@@ -286,11 +285,15 @@ test('sizing: fees feed back, the cap binds, financing only raises N, fixed fall
const unfinanced = sizeNotional({ ...base, stopDollarsAt, targetHoldHours: 200 });
assert.ok(financed.notional > unfinanced.notional);
const fixed = sizeNotional({ ...base, stopDollarsAt: () => Infinity, leg: 1700 });
assert.equal(fixed.mode, 'fixed');
assert.equal(fixed.notional, 40000);
assert.equal(sizeNotional({ ...base, stopDollarsAt: () => Infinity, fixedNotional: 0 }), null);
assert.equal(sizeNotional({ ...base, targetHoldHours: 0, stopDollarsAt }).mode, 'fixed');
// 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', () => {
@@ -461,15 +464,16 @@ test('the selector ranks the plan example: ETH, BTC, XYZ100, AAPL', () => {
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);
// Fixed sizing ignores volatility; volatility sizing needs it.
assert.equal(scoreCandidate({ spreadFraction: 0.0002, commissionPct: 0.04, financingPctPerDay: 0, targetHoldHours: 0, volatilityPerHour: null }), 0.001);
// 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,
needVolatility: true, minContractsOk: true, currency: 'USD', accountCurrency: 'USD',
minContractsOk: true, currency: 'USD', accountCurrency: 'USD',
};
assert.equal(candidateRejection(ok), null);
assert.match(candidateRejection({ ...ok, tradingStatus: 'CLOSE_ONLY' }), /CLOSE_ONLY/);
@@ -478,6 +482,5 @@ test('the selector drops what cannot trade right now', () => {
assert.match(candidateRejection({ ...ok, quoteAgeMs: 10 * 60_000 }), /old/);
assert.match(candidateRejection({ ...ok, ask: 101 }), /spread/);
assert.match(candidateRejection({ ...ok, candles: 10 }), /candles/);
assert.equal(candidateRejection({ ...ok, candles: 10, needVolatility: false }), null);
assert.match(candidateRejection({ ...ok, currency: 'EUR' }), /EUR/);
});