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
+83 -15
View File
@@ -1269,17 +1269,80 @@ export function botStopsActive(cfg: { firstDayStop: boolean; minDayStop: boolean
/** The hold a leg is sized for when the firm doesn't set one. */
export const DEFAULT_TARGET_HOLD_HOURS = 12;
/** Share of the broker's margin headroom a new position may use: the rest
* absorbs the price moving between sizing and the fill, and the entry fee. */
export const MARGIN_HEADROOM = 0.97;
/**
* The largest position an account may open: the account type's notional cap
* and, where the broker states the instrument's margin rate, what the
* account's available funds can margin (the firm's leverage — 5:1 on crypto,
* 2:1 on stocks at Tradeify247). 0 = no cap.
*/
export function notionalCap(o: { maxNotional: number; marginRate?: number; availableFunds?: number }): number {
const caps: number[] = [];
if (o.maxNotional > 0) caps.push(o.maxNotional);
if ((o.marginRate ?? 0) > 0 && Number.isFinite(o.availableFunds) && o.availableFunds! > 0) {
caps.push((MARGIN_HEADROOM * o.availableFunds!) / o.marginRate!);
}
return caps.length > 0 ? Math.min(...caps) : 0;
}
/** The swap reserve never grows past this share of the take-profit distance
* — beyond it the leg is sized too small to hold at all. */
const MAX_SWAP_RESERVE_FRACTION = 0.25;
/**
* The overnight swap a winning leg is expected to pay before its take-profit
* fills, so the take-profit can carry it and a win still clears the leg.
*
* Same driftless walk as sizeNotional, in dollars: σ$ = notional·σ per √hour,
* the take-profit A away, the stop B away. Given it wins, the walk takes
* (A² + 2AB)/(3σ$²) hours on average — longer than the unconditional hold,
* since the quick endings are mostly stop-outs. The swap is charged at each
* daily reset the position is held over, about one per 24 h held, on the
* notional. A includes the reserve itself, so it is solved as a fixed point.
* A stopless leg is taken as B = A.
*
* `distance` = the take-profit distance before swap: leg + round-trip fees.
*/
export function expectedSwapToWin(o: {
distance: number;
stopDollars: number;
notional: number;
volatilityPerHour?: number;
financingPctPerDay?: number;
}): number {
const daily = Math.max(0, o.financingPctPerDay ?? 0) / 100;
const sigma = o.volatilityPerHour ?? 0;
if (!(daily > 0) || !(sigma > 0) || !(o.notional > 0) || !(o.distance > 0)) return 0;
const variance = (o.notional * sigma) ** 2;
const stopped = Number.isFinite(o.stopDollars) && o.stopDollars > 0;
const cap = o.distance * MAX_SWAP_RESERVE_FRACTION;
let swap = 0;
for (let i = 0; i < 50; i++) {
const a = o.distance + swap;
const b = stopped ? o.stopDollars : a;
const winHours = (a * a + 2 * a * b) / (3 * variance);
const next = Math.min(cap, daily * o.notional * (winHours / 24));
const done = Math.abs(next - swap) < 0.005;
swap = next;
if (done) break;
}
return Math.ceil(swap * 100) / 100;
}
/**
* How much notional a leg should carry, on platforms that price it (DXTrade).
*
* Model price as a driftless random walk with σ per √hour. With the
* take-profit A dollars away (leg + fees) and the stop B dollars away, a
* position of notional N wins B/(A+B) of the time whatever N is, its expected
* hold is (A/N)(B/N)/σ² hours, and its expected P&L is minus fees and spread
* — proportional to N. Smaller is cheaper, bigger is faster, so the knob is
* the target hold T: N = √(A·B/(σ²·T)). A firm that charges overnight
* financing adds a floor: below √(f·A·B/(σ²·(2c + s))), a bigger position is
* both cheaper and faster.
* take-profit A dollars away (leg + fees + expected swap) and the stop B
* dollars away, a position of notional N wins B/(A+B) of the time whatever N
* is, its expected hold is (A/N)(B/N)/σ² hours, and its expected P&L is minus
* fees and spread — proportional to N. Smaller is cheaper, bigger is faster,
* so the knob is the target hold T: N = √(A·B/(σ²·T)). A firm that charges
* overnight financing adds a floor: below √(f·A·B/(σ²·(2c + s))), a bigger
* position is both cheaper and faster.
*
* A stopless leg (no daily loss limit, no obfuscation stop) is sized as if
* its stop sat as far away as its target. Returns null without a volatility.
@@ -1295,9 +1358,9 @@ export function sizeNotional(o: {
commissionPct: number;
spreadFraction?: number;
financingPctPerDay?: number;
/** Hard cap on the position; 0 = none. */
/** Hard cap on the position (see notionalCap); 0 = none. */
maxNotional: number;
}): { notional: number; predictedHoldHours: number; winProbability: number | null } | null {
}): { notional: number; predictedHoldHours: number; winProbability: number | null; swapReserve: number } | null {
const sigma = o.volatilityPerHour ?? 0;
if (!(sigma > 0)) return null;
const hold = o.targetHoldHours > 0 ? o.targetHoldHours : DEFAULT_TARGET_HOLD_HOURS;
@@ -1305,16 +1368,20 @@ export function sizeNotional(o: {
const s = Math.max(0, o.spreadFraction ?? 0);
const fh = Math.max(0, o.financingPctPerDay ?? 0) / 100 / 24;
const distances = (n: number) => {
const a = o.leg + 2 * c * n;
const stop = o.stopDollarsAt(n);
const stopped = Number.isFinite(stop) && stop > 0;
return { a, b: stopped ? stop : a, stopped };
const swap = expectedSwapToWin({
distance: o.leg + 2 * c * n, stopDollars: stop, notional: n,
volatilityPerHour: sigma, financingPctPerDay: o.financingPctPerDay,
});
const a = o.leg + 2 * c * n + swap;
return { a, b: stopped ? stop : a, stopped, swap };
};
// Fees depend on N and the daily-loss stop depends on fees: a couple of
// fixed-point passes settle it (the correction is second order).
// Fees and swap depend on N and the daily-loss stop depends on fees: a
// few fixed-point passes settle it (the correction is second order).
let n = o.leg * 20;
for (let pass = 0; pass < 3; pass++) {
for (let pass = 0; pass < 4; pass++) {
const { a, b } = distances(n);
const nTime = Math.sqrt((a * b) / (sigma * sigma * hold));
const nCost = fh > 0 ? Math.sqrt((fh * a * b) / (sigma * sigma * (2 * c + s))) : 0;
@@ -1322,11 +1389,12 @@ export function sizeNotional(o: {
}
if (!Number.isFinite(n) || n <= 0) return null;
const notional = o.maxNotional > 0 ? Math.min(n, o.maxNotional) : n;
const { a, b, stopped } = distances(notional);
const { a, b, stopped, swap } = distances(notional);
return {
notional,
predictedHoldHours: (a / notional) * (b / notional) / (sigma * sigma),
winProbability: stopped ? b / (a + b) : null,
swapReserve: swap,
};
}