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