/** * An account's next leg, sized on every instrument its DXtrade client last * surveyed — the numbers the local dashboard's hold estimate and the * master's Firms page show. Same rule as the trade: planLegSize for the size, * rankForTarget for the order (the cheapest that reaches the target hold, * else the quickest), so what is shown is what a batch would pick. */ import { getAccountConfig } from './account-type-resolver'; import type { PlatformContract } from './platform-client'; import type { SymbolStats } from './master-protocol'; import { combineCmeSessions, computeDayPlan, resolveEffectiveConfig, obfuscationStopDollars, dllRoom as computeDllRoom, planLegSize, rankForTarget, maxLossRoom, lastStand, dllStopDollars, DEFAULT_TARGET_HOLD_HOURS, FALLBACK_COMMISSION_PER_CONTRACT, } from './trading-logic'; export type CandidateLeg = SymbolStats['symbols'][number] & { swapReserve: number; leverage: number | null; winProbabilityNoFees: number | null; }; interface FirmSizing { commissionPct?: number; targetHoldHours?: number; financingPctPerDay?: number; } /** The next leg of `leg` dollars on each surveyed candidate, best first. */ export function rankNextLeg(o: { // eslint-disable-next-line @typescript-eslint/no-explicit-any client: any; accountId: number; cfg: { accountSize: number; maxLoss?: number; dailyLossLimit?: number }; firm: FirmSizing; leg: number; /** null = daily loss limit set but its day start isn't known yet. */ dllRoom: number | null; obfuscationDollars: number; cash: { amount: number; availableFunds?: number }; }): CandidateLeg[] { const candidates: PlatformContract[] = o.client.lastCandidates ?? []; if (!(o.leg > 0) || candidates.length === 0) return []; const target = o.firm.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS; const floorRoom = maxLossRoom({ balance: o.cash.amount, accountSize: o.cfg.accountSize, maxLoss: o.cfg.maxLoss ?? 0, autoLiqThreshold: o.client.autoLiqThresholds?.[o.accountId], }); // The next leg opens flat: while one is open its margin is tied up, so // size against the balance then. const funds = o.client.positions?.[o.accountId] ? o.cash.amount : (o.cash.availableFunds ?? o.cash.amount); const sized = candidates.flatMap((c): CandidateLeg[] => { if (!c.volatilityPerHour) return []; const plan = (costs: boolean) => planLegSize({ leg: o.leg, contract: costs ? c : { ...c, spreadFraction: 0 }, commissionPct: costs ? (c.commissionPct ?? o.firm.commissionPct ?? 0) : 0, targetHoldHours: target, financingPctPerDay: costs ? (o.firm.financingPctPerDay ?? 0) : 0, dailyLossLimit: o.cfg.dailyLossLimit ?? 0, // Without fees the day's room would also have its fees back — but // only this leg's are known, so the room stays as it is. dllRoom: o.dllRoom ?? Infinity, maxLossRoom: floorRoom, obfuscationDollars: o.obfuscationDollars, availableFunds: funds, }); const p = plan(true); if (!p.sizing || p.cost === null) return []; const leverage = c.marginRate ? Math.round(10 / c.marginRate) / 10 : null; return [{ symbol: String(c.id), notional: Math.round(p.sizing.notional), multiplier: o.cfg.accountSize > 0 ? Math.round((p.sizing.notional / o.cfg.accountSize) * 100) / 100 : 0, maxMultiplier: leverage, leverage, holdHours: Math.round(p.sizing.predictedHoldHours * 10) / 10, cost: Math.round(p.cost), winProbability: p.sizing.winProbability, winProbabilityNoFees: plan(false).sizing?.winProbability ?? null, volatilityPerDay: c.volatilityPerHour * Math.sqrt(24), spreadFraction: c.spreadFraction ?? 0, swapReserve: p.sizing.swapReserve, }]; }); return rankForTarget(sized, target).map((i) => sized[i]); } /** An account's day plan and daily-loss room, worked out from scratch the * way the dashboard does. null without an account type or a plan. */ function accountPlan( // eslint-disable-next-line @typescript-eslint/no-explicit-any client: any, acc: { id: number }, tradeDate: string, ) { const cfg = getAccountConfig(acc.id).accountType; if (!cfg) return null; const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 }; const combined = combineCmeSessions(cfg.combineCmeHolidays === true, client.dailyPnL[acc.id] ?? [], tradeDate, cash.realizedPnL); const effective = resolveEffectiveConfig( cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages, client.priorProfit?.[acc.id] ?? 0, client.fundTransactions?.[acc.id] ?? [], cfg.minDayPnL, ); const plan = computeDayPlan( acc.id, combined.tradeDate, effective.profitTarget, effective.consistency, combined.dailyPnL, effective.minDayPnL, effective.minTradingDays, cash.amount - cfg.accountSize, combined.realizedPnL, cfg.splitEnabled, effective.drain, ); if (!plan) return null; const dll = cfg.dailyLossLimit ?? 0; let room: number | null = null; if (dll > 0) { const dayStart = client.dayStartBalance?.(acc.id); room = dayStart === null ? null : computeDllRoom({ dailyLossLimit: dll, closedBalance: cash.amount, dayStartBalance: dayStart, realizedToday: Math.min(cash.realizedPnL, combined.realizedPnL), }); } const obfuscation = obfuscationStopDollars(cfg, combined.dailyPnL, plan, combined.tradeDate, plan.remainingToday); return { cfg, cash, plan, dll, room, obfuscation }; } /** * The same for an account worked out from scratch — its day plan, daily-loss * room and stops as the dashboard computes them. null when it has no * account type, no plan, or nothing left to trade today. */ export function accountNextLeg( // eslint-disable-next-line @typescript-eslint/no-explicit-any client: any, acc: { id: number }, firm: FirmSizing, tradeDate: string, ): { leg: number; lastStand: boolean; candidates: CandidateLeg[] } | null { const p = accountPlan(client, acc, tradeDate); if (!p || !(p.plan.remainingToday > 0)) return null; const floorRoom = maxLossRoom({ balance: p.cash.amount, accountSize: p.cfg.accountSize, maxLoss: p.cfg.maxLoss ?? 0, autoLiqThreshold: client.autoLiqThresholds?.[acc.id], }); return { leg: p.plan.remainingToday, lastStand: p.room !== null && lastStand({ dllRoom: p.room, dailyLossLimit: p.dll, maxLossRoom: floorRoom }), candidates: rankNextLeg({ client, accountId: acc.id, cfg: p.cfg, firm, leg: p.plan.remainingToday, dllRoom: p.room, obfuscationDollars: p.obfuscation, cash: p.cash, }), }; } /** * A futures account's next leg as the Auto draw sizes its hold: the day's * remaining target, its stop (the tighter of the obfuscation and daily-loss * stops; Infinity = none) and the account type's max contracts. null with * nothing left to trade today. */ export function accountLegShape( // eslint-disable-next-line @typescript-eslint/no-explicit-any client: any, acc: { id: number }, tradeDate: string, ): { leg: number; stopDollars: number; maxPositionSize: number } | null { const p = accountPlan(client, acc, tradeDate); if (!p || !(p.plan.remainingToday > 0)) return null; const obf = Number.isFinite(p.obfuscation) && p.obfuscation > 0 ? p.obfuscation : Infinity; const hard = p.room !== null && p.dll > 0 ? dllStopDollars({ room: p.room, dailyLossLimit: p.dll, entryFee: FALLBACK_COMMISSION_PER_CONTRACT, commissionPct: 0 }) : Infinity; return { leg: p.plan.remainingToday, stopDollars: Math.min(obf, hard), maxPositionSize: p.cfg.maxPositionSize }; }