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:
co-authored by
Claude Opus 5.5
parent
a2d9df5ba4
commit
a2cd3a94c9
+137
-13
@@ -1182,6 +1182,14 @@ export function obfuscationStopDollars(
|
||||
/** Share of a hard daily loss limit the bot never spends — room for a stop
|
||||
* that fills worse than its price (gaps, thin weekend books). */
|
||||
export const DLL_RESERVE_FRACTION = 0.10;
|
||||
/** …or this share of the position's notional, when that is more: a stop's
|
||||
* slippage grows with the position, a flat reserve doesn't. */
|
||||
export const DLL_SLIPPAGE_FRACTION = 0.001;
|
||||
|
||||
/** The part of the day's room a stop leaves unspent. */
|
||||
export function dllReserve(dailyLossLimit: number, notional = 0): number {
|
||||
return Math.max(DLL_RESERVE_FRACTION * dailyLossLimit, DLL_SLIPPAGE_FRACTION * Math.max(0, notional));
|
||||
}
|
||||
/** Smallest daily-loss stop worth trading. Under a fair walk a leg of L with a
|
||||
* stop of S wins S/(S + L) of the time — about 9% for $50 against $500 — so
|
||||
* anything tighter just spends the day's room on fees. */
|
||||
@@ -1218,11 +1226,55 @@ export function dllRoom(o: {
|
||||
* entryFee + r × (stop distance) — a short's stop sits above its entry — so
|
||||
* the worst loss is d(1 + r) + 2·entryFee, plus a cent of fee rounding.
|
||||
* Infinity when there is no limit. Can go negative: no room at all.
|
||||
*
|
||||
* Overnight swap needs no reserve of its own while a day's swap is below the
|
||||
* commission rate (Tradeify: 0.033% < 0.04%): a position carried past the
|
||||
* reset has its entry fee on the old day's books, and the swap takes no more
|
||||
* than that slot on the new day's.
|
||||
*/
|
||||
export function dllStopDollars(o: { room: number; dailyLossLimit: number; entryFee: number; commissionPct: number }): number {
|
||||
export function dllStopDollars(o: { room: number; dailyLossLimit: number; entryFee: number; commissionPct: number; notional?: number }): number {
|
||||
if (!Number.isFinite(o.room)) return Infinity;
|
||||
const r = Math.max(0, o.commissionPct) / 100;
|
||||
return (o.room - DLL_RESERVE_FRACTION * o.dailyLossLimit - 2 * o.entryFee - 0.01) / (1 + r);
|
||||
return (o.room - dllReserve(o.dailyLossLimit, o.notional) - 2 * o.entryFee - 0.01) / (1 + r);
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the account's balance is above its max-drawdown floor, or null when
|
||||
* it isn't known: the broker's liquidation floor where it reports one, else
|
||||
* the account type's static max loss below the starting size — whichever is
|
||||
* higher.
|
||||
*/
|
||||
export function maxLossRoom(o: { balance: number; accountSize: number; maxLoss: number; autoLiqThreshold?: number }): number | null {
|
||||
const floors: number[] = [];
|
||||
if ((o.autoLiqThreshold ?? 0) > 0) floors.push(o.autoLiqThreshold!);
|
||||
if (Math.abs(o.maxLoss) > 0 && o.accountSize > 0) floors.push(o.accountSize - Math.abs(o.maxLoss));
|
||||
return floors.length > 0 ? round2(o.balance - Math.max(...floors)) : null;
|
||||
}
|
||||
|
||||
/** A daily-loss stop is kept only while a stop-out would leave at least this
|
||||
* share of its loss above the max-drawdown floor. */
|
||||
export const LAST_STAND_FRACTION = 0.15;
|
||||
|
||||
/**
|
||||
* Whether a leg should ride without its daily-loss stop. The stop is there to
|
||||
* keep the account alive for another day; when a stop-out would leave it
|
||||
* within 15% of that loss of its max-drawdown floor — or the floor is nearer
|
||||
* than the stop — there is no useful day after, and the stop only gives up
|
||||
* room (the reserve, and whatever the floor allows past it) for nothing. The
|
||||
* leg then runs until its take-profit or the firm's own breach.
|
||||
*/
|
||||
export function lastStand(o: { dllRoom: number; dailyLossLimit: number; maxLossRoom: number | null }): boolean {
|
||||
if (o.maxLossRoom === null || !(o.dailyLossLimit > 0) || !Number.isFinite(o.dllRoom)) return false;
|
||||
const stopOut = o.dllRoom - DLL_RESERVE_FRACTION * o.dailyLossLimit;
|
||||
if (!(stopOut > 0)) return false;
|
||||
return o.maxLossRoom - stopOut <= LAST_STAND_FRACTION * stopOut;
|
||||
}
|
||||
|
||||
/** On a last stand: how far price can move against the leg before the firm's
|
||||
* nearer floor breaches the account — its equity carries the entry fee, not
|
||||
* the exit's. What sizing and the swap estimate take as the walk's far side. */
|
||||
export function breachDistance(o: { dllRoom: number; maxLossRoom: number | null; entryFee: number }): number {
|
||||
return Math.min(o.dllRoom, o.maxLossRoom ?? Infinity) - o.entryFee;
|
||||
}
|
||||
|
||||
/** Is there enough daily-loss room left to trade at all? */
|
||||
@@ -1274,18 +1326,13 @@ export const DEFAULT_TARGET_HOLD_HOURS = 12;
|
||||
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.
|
||||
* The largest position an account may open: what its available funds can
|
||||
* margin at the broker's rate for the instrument — the firm's leverage per
|
||||
* symbol (5:1 crypto, 2:1 stocks at Tradeify247). 0 = no cap known.
|
||||
*/
|
||||
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;
|
||||
export function notionalCap(o: { marginRate?: number; availableFunds?: number }): number {
|
||||
if (!((o.marginRate ?? 0) > 0) || !Number.isFinite(o.availableFunds) || !(o.availableFunds! > 0)) return 0;
|
||||
return (MARGIN_HEADROOM * o.availableFunds!) / o.marginRate!;
|
||||
}
|
||||
|
||||
/** The swap reserve never grows past this share of the take-profit distance
|
||||
@@ -1398,6 +1445,83 @@ export function sizeNotional(o: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One leg's size on one instrument for one account — the rule fireLeg
|
||||
* trades, the instrument pick ranks by and the dashboard shows.
|
||||
*
|
||||
* The walk's far side (what sizing, the win chance and the swap estimate
|
||||
* use) is the stop the leg carries: the tighter of the obfuscation stop and
|
||||
* the hard daily-loss stop. On a last stand there is no daily-loss stop and
|
||||
* 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.
|
||||
*/
|
||||
export function planLegSize(o: {
|
||||
leg: number;
|
||||
contract: { stepNotional?: number; volatilityPerHour?: number; spreadFraction?: number; marginRate?: number };
|
||||
/** Per side, percent. */
|
||||
commissionPct: number;
|
||||
targetHoldHours: number;
|
||||
financingPctPerDay: number;
|
||||
dailyLossLimit: number;
|
||||
/** Infinity = no hard daily limit. */
|
||||
dllRoom: number;
|
||||
maxLossRoom: number | null;
|
||||
/** Infinity (or ≤ 0) = no obfuscation stop. */
|
||||
obfuscationDollars: number;
|
||||
availableFunds?: number;
|
||||
}): {
|
||||
lastStand: boolean;
|
||||
cap: number;
|
||||
walkAt: (notional: number) => number;
|
||||
sizing: ReturnType<typeof sizeNotional>;
|
||||
cost: number | null;
|
||||
} {
|
||||
const dllOn = Number.isFinite(o.dllRoom) && o.dailyLossLimit > 0;
|
||||
const stand = dllOn && lastStand({ dllRoom: o.dllRoom, dailyLossLimit: o.dailyLossLimit, maxLossRoom: o.maxLossRoom });
|
||||
const obf = Number.isFinite(o.obfuscationDollars) && o.obfuscationDollars > 0 ? o.obfuscationDollars : Infinity;
|
||||
const r = Math.max(0, o.commissionPct) / 100;
|
||||
const walkAt = (notional: number) => Math.min(
|
||||
obf,
|
||||
!dllOn ? Infinity
|
||||
: stand ? breachDistance({ dllRoom: o.dllRoom, maxLossRoom: o.maxLossRoom, entryFee: r * notional })
|
||||
: 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
|
||||
? sizeNotional({
|
||||
leg: o.leg,
|
||||
stopDollarsAt: walkAt,
|
||||
volatilityPerHour: o.contract.volatilityPerHour,
|
||||
targetHoldHours: o.targetHoldHours,
|
||||
commissionPct: o.commissionPct,
|
||||
spreadFraction: o.contract.spreadFraction,
|
||||
financingPctPerDay: o.financingPctPerDay,
|
||||
maxNotional: cap,
|
||||
})
|
||||
: null;
|
||||
const cost = sizing
|
||||
? sizing.notional * (Math.max(0, o.contract.spreadFraction ?? 0) + 2 * r)
|
||||
+ (Math.max(0, o.financingPctPerDay) / 100) * sizing.notional * (sizing.predictedHoldHours / 24)
|
||||
: null;
|
||||
return { lastStand: stand, cap, walkAt, sizing, cost };
|
||||
}
|
||||
|
||||
/**
|
||||
* Order candidates for a leg: those that reach the target hold (within a
|
||||
* few percent) first, cheapest first; then those whose leverage cap keeps
|
||||
* them short of it, quickest first. Returns indices into `options`.
|
||||
*/
|
||||
export function rankForTarget(options: { holdHours: number; cost: number }[], targetHoldHours: number): number[] {
|
||||
const reaches = (h: number) => h <= targetHoldHours * 1.02 + 0.01;
|
||||
return options
|
||||
.map((opt, i) => ({ ...opt, i, reach: reaches(opt.holdHours) }))
|
||||
.sort((a, b) => (a.reach === b.reach
|
||||
? (a.reach ? a.cost - b.cost : a.holdHours - b.holdHours) || a.i - b.i
|
||||
: a.reach ? -1 : 1))
|
||||
.map((x) => x.i);
|
||||
}
|
||||
|
||||
// ── trading hours ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Tradeify247 (and DXtrade brokers generally) roll the trading day at 22:00
|
||||
|
||||
Reference in New Issue
Block a user