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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user