A platform may now state its own stop slippage share (PlatformClient.stopSlippageFraction); the daily-loss reserve is the larger of 10% of the limit and that share of the position. Vest's 9 measured stop-outs filled 0.9 points past the trigger on average and 2.25 at worst, against the ~30 points 0.1% held back. On a $1,400 day with ~$495k of NDX the stop goes from ~$880 to ~$1,230, and the leg sizes up for the same hold. Sizing, the placed stop, the dashboard's next stop and the reset guard all use it; other platforms keep 0.1%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
9.2 KiB
TypeScript
203 lines
9.2 KiB
TypeScript
/**
|
|
* 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, stageInputsFor, floorRoom, shortestNextLeg,
|
|
FALLBACK_COMMISSION_PER_CONTRACT,
|
|
} from './trading-logic';
|
|
|
|
/**
|
|
* The day's hard-loss room as the engine's dailyLossGate works it out, for
|
|
* the dashboard paths: against the platform's own floor where it states one
|
|
* (Vest), else the day's P&L. null = not known yet (or no limit, dll ≤ 0).
|
|
*/
|
|
export function dayLossRoom(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
client: any,
|
|
accountId: number,
|
|
dll: number,
|
|
cash: { amount: number; realizedPnL: number },
|
|
combinedRealized: number,
|
|
/** The shortest take-profit the next leg may have (shortestNextLeg). */
|
|
nextLeg: number,
|
|
): number | null {
|
|
if (!(dll > 0)) return null;
|
|
const floor: number | null | undefined = client.dailyLossFloor?.(accountId);
|
|
if (floor === null) return null;
|
|
if (typeof floor === 'number') {
|
|
return floorRoom({
|
|
dailyLossLimit: dll, closedBalance: cash.amount, floor,
|
|
realizedToday: Math.min(cash.realizedPnL, combinedRealized),
|
|
gain: client.dayGainForRoom?.(accountId, { leg: nextLeg, limit: dll }) ?? 0,
|
|
});
|
|
}
|
|
const dayStart = client.dayStartBalance?.(accountId);
|
|
return dayStart === null ? null : computeDllRoom({
|
|
dailyLossLimit: dll, closedBalance: cash.amount, dayStartBalance: dayStart,
|
|
realizedToday: Math.min(cash.realizedPnL, combinedRealized),
|
|
});
|
|
}
|
|
import { sizingHoldHours } from './person-size';
|
|
|
|
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 };
|
|
/** The trade date the hold is drawn for (sizingHoldHours); default today. */
|
|
tradeDate?: string;
|
|
}): CandidateLeg[] {
|
|
const candidates: PlatformContract[] = o.client.lastCandidates ?? [];
|
|
if (!(o.leg > 0) || candidates.length === 0) return [];
|
|
const target = sizingHoldHours(o.firm, o.tradeDate);
|
|
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,
|
|
slippageFraction: o.client.stopSlippageFraction,
|
|
});
|
|
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,
|
|
stageInputsFor(client, acc.id, cfg),
|
|
);
|
|
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 = effective.dailyLossLimit;
|
|
const room = dayLossRoom(client, acc.id, dll, cash, combined.realizedPnL, shortestNextLeg(plan));
|
|
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, dailyLossLimit: p.dll }, firm, leg: p.plan.remainingToday, dllRoom: p.room,
|
|
obfuscationDollars: p.obfuscation, cash: p.cash, tradeDate,
|
|
}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 };
|
|
}
|