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
-1
View File
@@ -45,7 +45,6 @@ export async function GET() {
combineCmeHolidays: a.combineCmeHolidays,
withdrawalStages: a.withdrawalStages,
dailyLossLimit: a.dailyLossLimit ?? 0,
maxNotional: a.maxNotional ?? 0,
tradesEveryDay: a.tradesEveryDay === true,
})),
catalogUpdatedAt: firm.updatedAt,
+46 -37
View File
@@ -7,7 +7,8 @@ import { historyFresh, type PlatformContract } from '@/lib/platform-client';
import type { AccountState } from '@/types';
import {
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, currentTradeDateCT,
obfuscationStopDollars, dllRoom as computeDllRoom, dllStopDollars, botStopsActive, sizeNotional, notionalCap,
obfuscationStopDollars, dllRoom as computeDllRoom, dllStopDollars, botStopsActive, planLegSize, rankForTarget,
maxLossRoom, lastStand as isLastStand,
DEFAULT_TARGET_HOLD_HOURS,
LEG_COMPLETION_EPSILON,
} from '@/lib/trading-logic';
@@ -89,6 +90,7 @@ export async function GET() {
let nextStop: number | null = null;
let dllRoom: number | null = null;
let holdEstimate: AccountState['holdEstimate'] = null;
let lastStandNext = false;
if (cfg && effective && !isDead) {
const equityProfit = cash.amount - cfg.accountSize;
const plan = computeDayPlan(
@@ -115,54 +117,60 @@ export async function GET() {
realizedToday: Math.min(cash.realizedPnL, combined.realizedPnL),
});
}
// The max-drawdown floor, and whether the next leg is a last
// stand — no daily-loss stop — as fireLeg decides it.
const floorRoom = maxLossRoom({
balance: cash.amount, accountSize: cfg.accountSize, maxLoss: cfg.maxLoss ?? 0,
autoLiqThreshold: client.autoLiqThresholds?.[acc.id],
});
lastStandNext = dllRoom !== null && isLastStand({ dllRoom, dailyLossLimit: dll, maxLossRoom: floorRoom });
if (plan && (botStopsActive(cfg))) {
const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday);
const hard = dllRoom !== null
const hard = dllRoom !== null && !lastStandNext
? dllStopDollars({ room: dllRoom, dailyLossLimit: dll, entryFee: 0, commissionPct: 0 })
: Infinity;
const stop = Math.min(Number.isFinite(obf) && obf > 0 ? obf : Infinity, hard);
if (Number.isFinite(stop) && stop > 0) nextStop = Math.round(stop * 100) / 100;
}
// DXtrade: size the next leg the way fireLeg would, on the
// instrument the selector last picked and today's measured
// volatility — under the same notional and margin caps —
// to show how long it is expected to hold.
const pick = (client as { lastPickedContract?: PlatformContract | null }).lastPickedContract;
if (plan && plan.remainingToday > 0 && pick?.stepNotional && pick.volatilityPerHour) {
const pct = pick.commissionPct ?? f.commissionPct ?? 0;
// DXtrade: size the next leg the way fireLeg would on each
// instrument the client last surveyed — at its leverage and
// measured volatility — and take the one the batch would:
// the cheapest that reaches the target hold. The next leg
// opens flat, so while one is open (its margin tied up) it
// sizes against the balance.
const candidates = (client as { lastCandidates?: PlatformContract[] }).lastCandidates ?? [];
if (plan && plan.remainingToday > 0 && candidates.length > 0) {
const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday);
const room = dllRoom;
const sizing = sizeNotional({
leg: plan.remainingToday,
stopDollarsAt: (n) => Math.min(
Number.isFinite(obf) && obf > 0 ? obf : Infinity,
room !== null && dll > 0
? dllStopDollars({ room, dailyLossLimit: dll, entryFee: (pct / 100) * n, commissionPct: pct })
: Infinity,
),
volatilityPerHour: pick.volatilityPerHour,
targetHoldHours: f.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS,
commissionPct: pct,
spreadFraction: pick.spreadFraction,
financingPctPerDay: f.financingPctPerDay ?? 0,
// The next leg opens flat: while this one is open its
// margin is tied up, so size against the balance.
maxNotional: notionalCap({
maxNotional: cfg.maxNotional ?? 0,
marginRate: pick.marginRate,
const target = f.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS;
const sized = candidates.flatMap((c) => {
const p = planLegSize({
leg: plan.remainingToday,
contract: c,
commissionPct: c.commissionPct ?? f.commissionPct ?? 0,
targetHoldHours: target,
financingPctPerDay: f.financingPctPerDay ?? 0,
dailyLossLimit: dll,
dllRoom: dllRoom ?? Infinity,
maxLossRoom: floorRoom,
obfuscationDollars: obf,
availableFunds: client.positions[acc.id] ? cash.amount : (cash.availableFunds ?? cash.amount),
}),
});
return p.sizing && p.cost !== null && c.volatilityPerHour
? [{ c, sizing: p.sizing, holdHours: p.sizing.predictedHoldHours, cost: p.cost, volatility: c.volatilityPerHour }]
: [];
});
if (sizing) {
const best = sized[rankForTarget(sized, target)[0]];
if (best) {
holdEstimate = {
hours: Math.round(sizing.predictedHoldHours * 10) / 10,
symbol: String(pick.id),
notional: Math.round(sizing.notional),
volatilityPerDay: pick.volatilityPerHour * Math.sqrt(24),
winProbability: sizing.winProbability,
swapReserve: sizing.swapReserve,
leverage: pick.marginRate ? Math.round(10 / pick.marginRate) / 10 : null,
hours: Math.round(best.holdHours * 10) / 10,
symbol: String(best.c.id),
notional: Math.round(best.sizing.notional),
volatilityPerDay: best.volatility * Math.sqrt(24),
winProbability: best.sizing.winProbability,
swapReserve: best.sizing.swapReserve,
leverage: best.c.marginRate ? Math.round(10 / best.c.marginRate) / 10 : null,
cost: Math.round(best.cost),
};
}
}
@@ -275,6 +283,7 @@ export async function GET() {
nextStop,
dllRoom,
holdEstimate,
lastStand: lastStandNext,
remainingToday,
effectiveProfitTarget,
dailyPnL,
+6 -2
View File
@@ -387,8 +387,11 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
{account.nextStop !== null && (
<span className="text-amber-600" title="The next trade's stop — dollars of price movement it may lose before booking the day (fees come on top; the daily-loss stop already reserves for them)"> · stop ${fmt(account.nextStop)}</span>
)}
{account.lastStand && (
<span className="text-red-600" title="Last stand: a daily-loss stop-out would leave this account within 15% of that loss of its max-drawdown floor — nothing left to trade on — so the next leg carries no daily-loss stop and runs to its take-profit or the firm's breach"> · no stop (last stand)</span>
)}
{typeof account.dllRoom === 'number' && (
<span className="text-slate-500" title="Hard daily loss limit: what the firm's day may still lose before a breach. 10% of the limit is never spent."> · DLL room ${fmt(account.dllRoom)}</span>
<span className="text-slate-500" title="Hard daily loss limit: what the firm's day may still lose before a breach. A reserve — 10% of the limit, or 0.1% of the position if larger — is never spent."> · DLL room ${fmt(account.dllRoom)}</span>
)}
{account.holdEstimate && (
<span
@@ -397,7 +400,8 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
+ (account.holdEstimate.leverage !== null ? ` (${account.holdEstimate.leverage}:1 max)` : '')
+ ` at a measured volatility of ${(account.holdEstimate.volatilityPerDay * 100).toFixed(2)}%/day over the past week`
+ (account.holdEstimate.winProbability !== null ? `, wins ~${Math.round(account.holdEstimate.winProbability * 100)}% of the time` : '')
+ (account.holdEstimate.swapReserve > 0 ? `; its take-profit carries ~$${fmt(account.holdEstimate.swapReserve)} of expected swap` : '')}
+ (account.holdEstimate.swapReserve > 0 ? `; its take-profit carries ~$${fmt(account.holdEstimate.swapReserve)} of expected swap` : '')
+ `. Expected cost ~$${fmt(account.holdEstimate.cost)} (spread, commissions, swap) — the cheapest candidate that reaches the target hold`}
>
{' · hold ~'}{account.holdEstimate.hours < 48 ? `${Math.round(account.holdEstimate.hours)}h` : `${(account.holdEstimate.hours / 24).toFixed(1)}d`}
</span>