Legs: cheapest symbol that reaches the target hold, last stands, a reserve that grows with size

- A batch sizes its leg on every mapped candidate, each at the leverage
  the broker states for it, and trades the cheapest (spread, commissions,
  expected swap) that reaches the target hold — else the quickest. The
  dashboard ranks the same way from a half-hourly candidate survey.
- Last stand: when a daily-loss stop-out would leave the account within
  15% of that loss above its max-drawdown floor (the account type's static
  max loss, or the broker's liquidation floor), the leg carries no
  daily-loss stop and is sized against the firm's own breach.
- The daily-loss reserve is 10% of the limit or 0.1% of the position,
  whichever is more, since a stop's slippage grows with the position.
- The account type's max notional is gone: the broker's margin rates cap
  each symbol. planLegSize holds the one sizing rule fireLeg, the pick and
  the dashboard share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-26 18:26:12 -05:00
co-authored by Claude Opus 5.5
parent a2d9df5ba4
commit a2cd3a94c9
12 changed files with 477 additions and 172 deletions
+84 -7
View File
@@ -12,6 +12,7 @@ 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, notionalCap, expectedSwapToWin, MARGIN_HEADROOM,
dllReserve, DLL_SLIPPAGE_FRACTION, maxLossRoom, lastStand, breachDistance, LAST_STAND_FRACTION, planLegSize, rankForTarget,
} from '../lib/trading-logic.ts';
// Deterministic PRNG so a failure reproduces.
@@ -347,13 +348,89 @@ test('expected swap to win: a fixed point over the winners\' hold, zero without
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('the notional cap: what the available funds can margin at the symbol\'s rate', () => {
assert.equal(notionalCap({ marginRate: 0.2, availableFunds: 100000 }), MARGIN_HEADROOM * 500000);
assert.equal(notionalCap({ marginRate: 0.5, availableFunds: 99757.39 }), MARGIN_HEADROOM * 99757.39 * 2);
assert.equal(notionalCap({ marginRate: 0.2, availableFunds: undefined }), 0);
assert.equal(notionalCap({ availableFunds: 100000 }), 0);
});
test('the daily-loss reserve grows with the position past 10% of the limit', () => {
assert.equal(dllReserve(3000), 300);
assert.equal(dllReserve(3000, 200000), 300);
assert.equal(dllReserve(3000, 485000), 485000 * DLL_SLIPPAGE_FRACTION);
const at = (notional) => dllStopDollars({ room: 3000, dailyLossLimit: 3000, entryFee: 0.0004 * notional, commissionPct: 0.04, notional });
// Below $300k the flat 10% binds; above, 0.1% of notional does.
assert.ok(Math.abs(at(200000) - (3000 - 300 - 160 - 0.01) / 1.0004) < 1e-9);
assert.ok(Math.abs(at(485000) - (3000 - 485 - 388 - 0.01) / 1.0004) < 1e-9);
// No notional given: the old flat reserve.
assert.equal(dllStopDollars({ room: 3000, dailyLossLimit: 3000, entryFee: 0, commissionPct: 0 }), 2699.99);
});
test('max-drawdown room: the static floor, the broker floor, the higher', () => {
assert.equal(maxLossRoom({ balance: 99757.39, accountSize: 100000, maxLoss: 6000 }), 5757.39);
assert.equal(maxLossRoom({ balance: 99757.39, accountSize: 100000, maxLoss: -6000 }), 5757.39);
assert.equal(maxLossRoom({ balance: 51000, accountSize: 50000, maxLoss: 0, autoLiqThreshold: 48500 }), 2500);
assert.equal(maxLossRoom({ balance: 51000, accountSize: 50000, maxLoss: 2000, autoLiqThreshold: 49000 }), 2000);
assert.equal(maxLossRoom({ balance: 99000, accountSize: 100000, maxLoss: 0 }), null);
});
test('last stand: no daily-loss stop once a stop-out would leave under 15% of it', () => {
// Full day: a stop-out spends $2,700 (room less the $300 reserve).
const at = (maxRoom) => lastStand({ dllRoom: 3000, dailyLossLimit: 3000, maxLossRoom: maxRoom });
assert.equal(LAST_STAND_FRACTION, 0.15);
assert.equal(at(5757.39), false, 'a fresh 1-Step keeps its stop');
assert.equal(at(2700 * 1.15 + 1), false, 'just over 15% left: keep the stop');
assert.equal(at(2700 * 1.15), true, '15% left: last stand');
assert.equal(at(3000), true, 'two stopped days in: last stand');
assert.equal(at(800), true, 'floor nearer than the stop: last stand');
assert.equal(at(null), false, 'no floor known');
assert.equal(lastStand({ dllRoom: Infinity, dailyLossLimit: 0, maxLossRoom: 500 }), false, 'no daily limit, no daily stop to drop');
// The far side of the walk is the nearer floor, net of the entry fee.
assert.equal(breachDistance({ dllRoom: 3000, maxLossRoom: 2900, entryFee: 120 }), 2780);
assert.equal(breachDistance({ dllRoom: 2757.39, maxLossRoom: null, entryFee: 0 }), 2757.39);
});
test('planLegSize: the stop, a last stand, the margin cap and the cost, as fireLeg trades', () => {
const eth = { stepNotional: 26.86, volatilityPerHour: 0.022 / Math.sqrt(24), spreadFraction: 0.00004, marginRate: 0.2 };
const base = {
leg: 12239, contract: eth, commissionPct: 0.04, targetHoldHours: 12, financingPctPerDay: 0.033,
dailyLossLimit: 3000, dllRoom: 2757.39, obfuscationDollars: Infinity, availableFunds: 99757.39,
};
const fresh = planLegSize({ ...base, maxLossRoom: 5757.39 });
assert.equal(fresh.lastStand, false);
assert.ok(Math.abs(fresh.sizing.predictedHoldHours - 12) < 0.1, `hold ${fresh.sizing.predictedHoldHours}`);
const n = fresh.sizing.notional;
assert.ok(Math.abs(fresh.walkAt(n) - dllStopDollars({ room: 2757.39, dailyLossLimit: 3000, entryFee: 0.0004 * n, commissionPct: 0.04, notional: n })) < 1e-9);
const expectCost = n * (0.00004 + 0.0008) + 0.00033 * n * (fresh.sizing.predictedHoldHours / 24);
assert.ok(Math.abs(fresh.cost - expectCost) < 1e-6);
// Two days stopped out: a stop-out would leave ~$300 above the floor.
const stand = planLegSize({ ...base, dllRoom: 3000, maxLossRoom: 3000 });
assert.equal(stand.lastStand, true);
assert.equal(stand.walkAt(300000), 3000 - 120);
assert.ok(stand.sizing.winProbability > fresh.sizing.winProbability, 'the whole room, no reserve: a better chance');
// An obfuscation stop still counts on a last stand.
assert.equal(planLegSize({ ...base, dllRoom: 3000, maxLossRoom: 3000, obfuscationDollars: 1000 }).walkAt(300000), 1000);
// A 2:1 stock can't reach 12 h in a quiet market: the cap binds.
const stock = planLegSize({ ...base, maxLossRoom: 5757.39, contract: { ...eth, marginRate: 0.5, volatilityPerHour: 0.0076 / Math.sqrt(24) } });
assert.ok(Math.abs(stock.sizing.notional - stock.cap) < 1e-6);
assert.ok(stock.sizing.predictedHoldHours > 12);
// Futures (no stepNotional) aren't sized by notional.
assert.equal(planLegSize({ ...base, maxLossRoom: null, contract: {} }).sizing, null);
});
test('rankForTarget: reach the target hold first, cheapest; else quickest', () => {
const order = rankForTarget([
{ holdHours: 12, cost: 330 }, // ETH
{ holdHours: 12.1, cost: 300 }, // BTC — within the 2% tolerance
{ holdHours: 40, cost: 250 }, // XYZ100, capped at 2:1
{ holdHours: 20, cost: 400 }, // SP500, capped
], 12);
assert.deepEqual(order, [1, 0, 3, 2]);
assert.deepEqual(rankForTarget([{ holdHours: 30, cost: 1 }, { holdHours: 25, cost: 9 }], 12), [1, 0]);
assert.deepEqual(rankForTarget([], 12), []);
});
test('sizing carries the swap reserve into the take-profit distance', () => {