DXtrade: count closed-trade P&L, week-long volatility, broker leverage caps, swap in the take-profit

- Transfers: a close books its SETTLEMENT and COMMISSION under one
  transaction code, and keying by the code alone let the commission
  overwrite the P&L. Keys now carry the type, so the day-start balance,
  daily-loss room, realized P&L, daily history and books check all see
  closed-trade profit and loss.
- Volatility from a week of hourly closes (was 48 h), still measured once
  per trade date, so one quiet weekend can't triple a hold estimate.
- Each symbol's margin rate from the account's instrument details caps
  the position at available funds / rate (Tradeify: 5:1 crypto, 2:1
  stocks); the account type's max notional stays as an optional ceiling.
- Firms with overnight financing add the swap a winning leg is expected
  to pay (the winners' mean hold, (A²+2AB)/3σ²) to its take-profit, and
  the sizing and hold estimate count it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-26 18:11:42 -05:00
co-authored by Claude Opus 5.5
parent 675b4c89f8
commit a2d9df5ba4
10 changed files with 309 additions and 81 deletions
+87 -3
View File
@@ -11,7 +11,7 @@ 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,
projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP, notionalCap, expectedSwapToWin, MARGIN_HEADROOM,
} from '../lib/trading-logic.ts';
// Deterministic PRNG so a failure reproduces.
@@ -307,6 +307,7 @@ test('random-walk simulation agrees with the predicted hold and win rate', () =>
const predicted = (A / notional) * (B / notional) / (sigma * sigma);
let wins = 0;
let totalHours = 0;
let winHours = 0;
const paths = 3000;
for (let p = 0; p < paths; p++) {
let x = 0;
@@ -315,12 +316,59 @@ test('random-walk simulation agrees with the predicted hold and win rate', () =>
x += r() < 0.5 ? stepDollars : -stepDollars;
steps++;
}
if (x >= A) wins++;
if (x >= A) { wins++; winHours += steps * dtHours; }
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}`);
// Winners take longer than the average leg: (A² + 2AB) / 3σ$² — what
// expectedSwapToWin charges swap over.
const predictedWin = (A * A + 2 * A * B) / (3 * (notional * sigma) ** 2);
assert.ok(Math.abs(winHours / wins - predictedWin) / predictedWin < 0.1, `winners ${winHours / wins}h vs ${predictedWin}h`);
});
test('expected swap to win: a fixed point over the winners\' hold, zero without swap, capped', () => {
const sigma = 0.022 / Math.sqrt(24); // ETH over a week, ~2.2%/day
const o = { distance: 12400, stopDollars: 2300, notional: 200000, volatilityPerHour: sigma, financingPctPerDay: 0.033 };
const swap = expectedSwapToWin(o);
const a = o.distance + swap;
const b = o.stopDollars;
const winHours = (a * a + 2 * a * b) / (3 * (o.notional * sigma) ** 2);
assert.ok(Math.abs(swap - 0.00033 * o.notional * (winHours / 24)) < 0.02, `swap ${swap} over ${winHours}h`);
assert.ok(swap > 200 && swap < 400, `swap ${swap}`);
// Quieter means a longer hold and more swap; none without a rate or σ.
assert.ok(expectedSwapToWin({ ...o, volatilityPerHour: sigma * 0.6 }) > swap);
assert.equal(expectedSwapToWin({ ...o, financingPctPerDay: 0 }), 0);
assert.equal(expectedSwapToWin({ ...o, volatilityPerHour: undefined }), 0);
// Stopless reads as B = A: a longer walk than with the stop.
assert.ok(expectedSwapToWin({ ...o, stopDollars: Infinity }) > swap);
// A leg that can barely move is capped at a quarter of its distance.
assert.equal(expectedSwapToWin({ ...o, volatilityPerHour: sigma / 50 }), 3100);
});
test('the notional cap: the account type, the margin the funds allow, the tighter', () => {
assert.equal(notionalCap({ maxNotional: 200000 }), 200000);
assert.equal(notionalCap({ maxNotional: 0 }), 0);
assert.equal(notionalCap({ maxNotional: 0, marginRate: 0.2, availableFunds: 100000 }), MARGIN_HEADROOM * 500000);
assert.equal(notionalCap({ maxNotional: 0, marginRate: 0.5, availableFunds: 99757.39 }), MARGIN_HEADROOM * 99757.39 * 2);
assert.equal(notionalCap({ maxNotional: 200000, marginRate: 0.2, availableFunds: 100000 }), 200000);
assert.equal(notionalCap({ maxNotional: 0, marginRate: 0.2, availableFunds: undefined }), 0);
});
test('sizing carries the swap reserve into the take-profit distance', () => {
const sigma = 0.0124 / Math.sqrt(24);
const stopDollarsAt = (n) => dllStopDollars({ room: 2760.15, dailyLossLimit: 3000, entryFee: 0.0004 * n, commissionPct: 0.04 });
const base = { leg: 12239, stopDollarsAt, volatilityPerHour: sigma, targetHoldHours: 12, commissionPct: 0.04, spreadFraction: 0.0001 };
const withSwap = sizeNotional({ ...base, financingPctPerDay: 0.033, maxNotional: 200000 });
const noSwap = sizeNotional({ ...base, financingPctPerDay: 0, maxNotional: 200000 });
assert.equal(noSwap.swapReserve, 0);
assert.ok(withSwap.swapReserve > 500, `swap ${withSwap.swapReserve}`);
const a = 12239 + 2 * 0.0004 * 200000 + withSwap.swapReserve;
const b = stopDollarsAt(200000);
assert.ok(Math.abs(withSwap.predictedHoldHours - (a / 200000) * (b / 200000) / (sigma * sigma)) < 1e-6);
assert.ok(withSwap.predictedHoldHours > noSwap.predictedHoldHours);
assert.ok(withSwap.winProbability < noSwap.winProbability);
});
// ── every-day calendar ───────────────────────────────────────────────────────
@@ -375,9 +423,45 @@ test('an every-day firm can finish its cycle on a Saturday', () => {
import {
dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep,
lastUtcReset, ctTradeDayStart, parseDurationMs, ciNum, signedFundValue, inTradingHours,
realizedVolPerHour, scoreCandidate, candidateRejection, MIN_VOL_CANDLES,
realizedVolPerHour, scoreCandidate, candidateRejection, MIN_VOL_CANDLES, parseTransfers, marginRateOf,
} from '../lib/dxtrade-helpers.ts';
test('transfers: a close\'s SETTLEMENT and COMMISSION share a code and both count', () => {
// As Tradeify247 answered for a flatten.
const body = { cashTransfers: [
{ account: 'default:1S100K2166398', transferCode: '2354051:1417097', transactionTime: '2026-09-26T22:33:39.615Z', cashTransactions: [
{ account: 'default:1S100K2166398', transactionCode: '2354051:1417097', type: 'COMMISSION', value: -79.95, transactionTime: '2026-09-26T22:33:39.615Z', orderCode: 'x:af-1-e' },
] },
{ account: 'default:1S100K2166398', transferCode: '2354051:1417102', transactionTime: '2026-09-26T22:33:42.197Z', cashTransactions: [
{ account: 'default:1S100K2166398', transactionCode: '2354051:1417102', type: 'SETTLEMENT', value: -5.43, transactionTime: '2026-09-26T22:33:42.197Z', orderCode: 'x:af-2-x' },
{ account: 'default:1S100K2166398', transactionCode: '2354051:1417102', type: 'COMMISSION', value: -79.95, transactionTime: '2026-09-26T22:33:42.197Z', orderCode: 'x:af-2-x' },
] },
] };
const txns = parseTransfers(body);
assert.equal(txns.length, 3);
const byKey = new Map(txns.map((t) => [t.key, t]));
assert.equal(byKey.size, 3, 'every transaction keeps its own key');
assert.equal(Math.round([...byKey.values()].reduce((s, t) => s + t.value, 0) * 100) / 100, -165.33);
// The same answer again (the next sync, another page) keys identically.
assert.deepEqual(parseTransfers(body).map((t) => t.key), txns.map((t) => t.key));
// Two of one type in one transfer stay apart too.
const twice = { cashTransfers: [{ transferCode: 't', cashTransactions: [
{ account: 'default:A', transactionCode: 'c', type: 'COMMISSION', value: -1, transactionTime: '2026-09-26T00:00:00Z' },
{ account: 'default:A', transactionCode: 'c', type: 'COMMISSION', value: -2, transactionTime: '2026-09-26T00:00:00Z' },
] }] };
assert.equal(new Set(parseTransfers(twice).map((t) => t.key)).size, 2);
});
test('margin rates: flat and tiered, the steepest tier, junk ignored', () => {
assert.equal(marginRateOf({ marginRate: { rateType: 'FLAT', tiers: [{ endVolume: 'Infinity', value: 0.2 }] } }), 0.2);
assert.equal(marginRateOf({ marginRate: { rateType: 'FLAT', tiers: [{ endVolume: 'Infinity', value: 0.5 }] } }), 0.5);
assert.equal(marginRateOf({ marginRate: { tiers: [{ endVolume: 1e6, value: 0.1 }, { endVolume: 'Infinity', value: 0.25 }] } }), 0.25);
assert.equal(marginRateOf({ marginRate: { value: 0.05 } }), 0.05);
assert.equal(marginRateOf({}), undefined);
assert.equal(marginRateOf(undefined), undefined);
assert.equal(marginRateOf({ marginRate: { tiers: [{ value: 'x' }, { value: 0 }, { value: 3 }] } }), undefined);
});
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'));