Sizing: the broker's largest and smallest order bind the leg's size

fireLeg clamped the order to the instrument's max and min size, but the
size the pick ranked by (and the Firms page shows) didn't: an instrument
whose largest order falls short of the ideal notional ranked on a hold it
can't reach. planLegSize now holds the notional to the margin cap and the
broker's largest order, then its smallest, so the ranking, the stats and
the trade agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-27 13:59:27 -05:00
co-authored by Claude Opus 5.5
parent 7f55e49755
commit 1782c16145
2 changed files with 47 additions and 4 deletions
+19 -4
View File
@@ -1398,6 +1398,9 @@ export function sizeNotional(o: {
financingPctPerDay?: number;
/** Hard cap on the position (see notionalCap); 0 = none. */
maxNotional: number;
/** The smallest order the broker takes; 0 = none. It wins over the cap,
* as the order does. */
minNotional?: number;
}): { notional: number; predictedHoldHours: number; winProbability: number | null; swapReserve: number } | null {
const sigma = o.volatilityPerHour ?? 0;
if (!(sigma > 0)) return null;
@@ -1428,7 +1431,7 @@ export function sizeNotional(o: {
n = Math.max(nTime, nCost);
}
if (!Number.isFinite(n) || n <= 0) return null;
const notional = o.maxNotional > 0 ? Math.min(n, o.maxNotional) : n;
const notional = Math.max(o.maxNotional > 0 ? Math.min(n, o.maxNotional) : n, o.minNotional ?? 0);
const { a, b, stopped, swap } = distances(notional);
return {
notional,
@@ -1448,10 +1451,19 @@ export function sizeNotional(o: {
* the far side is the firm's own breach. `cost` is what the leg is expected
* to spend: spread and both commissions on its notional, plus swap over its
* expected hold.
*
* The size is held to what the order can be — the margin cap and the
* broker's largest order, then its smallest — so the pick ranks each
* instrument by the hold it can actually reach. `cap` is the margin cap
* alone.
*/
export function planLegSize(o: {
leg: number;
contract: { stepNotional?: number; volatilityPerHour?: number; spreadFraction?: number; marginRate?: number };
contract: {
stepNotional?: number; volatilityPerHour?: number; spreadFraction?: number; marginRate?: number;
/** The broker's largest and smallest order, in steps. */
maxContracts?: number; minContracts?: number;
};
/** Per side, percent. */
commissionPct: number;
targetHoldHours: number;
@@ -1481,7 +1493,9 @@ export function planLegSize(o: {
: dllStopDollars({ room: o.dllRoom, dailyLossLimit: o.dailyLossLimit, entryFee: r * notional, commissionPct: o.commissionPct, notional }),
);
const cap = notionalCap({ marginRate: o.contract.marginRate, availableFunds: o.availableFunds });
const sizing = (o.contract.stepNotional ?? 0) > 0
const step = o.contract.stepNotional ?? 0;
const orderMax = (o.contract.maxContracts ?? 0) > 0 ? o.contract.maxContracts! * step : 0;
const sizing = step > 0
? sizeNotional({
leg: o.leg,
stopDollarsAt: walkAt,
@@ -1490,7 +1504,8 @@ export function planLegSize(o: {
commissionPct: o.commissionPct,
spreadFraction: o.contract.spreadFraction,
financingPctPerDay: o.financingPctPerDay,
maxNotional: cap,
maxNotional: cap > 0 && orderMax > 0 ? Math.min(cap, orderMax) : cap || orderMax,
minNotional: Math.max(1, o.contract.minContracts ?? 1) * step,
})
: null;
const cost = sizing
+28
View File
@@ -425,6 +425,34 @@ test('planLegSize: the stop, a last stand, the margin cap and the cost, as fireL
assert.equal(planLegSize({ ...base, maxLossRoom: null, contract: {} }).sizing, null);
});
test('planLegSize: the broker\'s order size limits bind the sizing, so the pick sees the real hold', () => {
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, maxLossRoom: 5757.39, obfuscationDollars: Infinity, availableFunds: 99757.39,
};
const free = planLegSize(base);
// A largest order well under the ideal size: the notional stops there and
// the hold grows past the target (the margin cap stays what it was).
const capped = planLegSize({ ...base, contract: { ...eth, maxContracts: 5000 } });
assert.ok(Math.abs(capped.sizing.notional - 5000 * 26.86) < 1e-6, `notional ${capped.sizing.notional}`);
assert.ok(capped.sizing.notional < free.sizing.notional);
assert.ok(capped.sizing.predictedHoldHours > 12, `hold ${capped.sizing.predictedHoldHours}`);
assert.equal(capped.cap, free.cap);
// One that doesn't bind changes nothing.
assert.equal(planLegSize({ ...base, contract: { ...eth, maxContracts: 1e9 } }).sizing.notional, free.sizing.notional);
// A smallest order above the ideal size: the order is that big.
const floored = planLegSize({ ...base, contract: { ...eth, minContracts: 20000 } });
assert.ok(Math.abs(floored.sizing.notional - 20000 * 26.86) < 1e-6);
assert.ok(floored.sizing.predictedHoldHours < 12);
// Ranked on the capped hold, a capped instrument no longer wins on cost.
const order = rankForTarget([
{ holdHours: capped.sizing.predictedHoldHours, cost: capped.cost },
{ holdHours: free.sizing.predictedHoldHours, cost: free.cost + 50 },
], 12);
assert.deepEqual(order, [1, 0]);
});
test('rankForTarget: reach the target hold first, cheapest; else quickest', () => {
const order = rankForTarget([
{ holdHours: 12, cost: 330 }, // ETH