Report each symbol's next-leg stats to the master; drop per-symbol commission
- lib/leg-estimate: an account's next leg sized on every surveyed candidate (planLegSize, ranked by rankForTarget) — notional, its multiple of the account and the broker's maximum, expected hold, cost and win chance. The dashboard's hold estimate and the reporter share it. - The reporter sends it per DXtrade firm as symbolStats, for one live account from the largest prefix group with a leg left to trade. - The symbol map's per-symbol commission is gone: the firm's applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a2cd3a94c9
commit
169fe19fad
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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, DEFAULT_TARGET_HOLD_HOURS,
|
||||
} from './trading-logic';
|
||||
|
||||
export type CandidateLeg = SymbolStats['symbols'][number] & { swapReserve: number; leverage: 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 p = planLegSize({
|
||||
leg: o.leg,
|
||||
contract: c,
|
||||
commissionPct: c.commissionPct ?? o.firm.commissionPct ?? 0,
|
||||
targetHoldHours: target,
|
||||
financingPctPerDay: o.firm.financingPctPerDay ?? 0,
|
||||
dailyLossLimit: o.cfg.dailyLossLimit ?? 0,
|
||||
dllRoom: o.dllRoom ?? Infinity,
|
||||
maxLossRoom: floorRoom,
|
||||
obfuscationDollars: o.obfuscationDollars,
|
||||
availableFunds: funds,
|
||||
});
|
||||
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,
|
||||
volatilityPerDay: c.volatilityPerHour * Math.sqrt(24),
|
||||
spreadFraction: c.spreadFraction ?? 0,
|
||||
swapReserve: p.sizing.swapReserve,
|
||||
}];
|
||||
});
|
||||
return rankForTarget(sized, target).map((i) => sized[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 || !(plan.remainingToday > 0)) 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 floorRoom = maxLossRoom({
|
||||
balance: cash.amount, accountSize: cfg.accountSize, maxLoss: cfg.maxLoss ?? 0,
|
||||
autoLiqThreshold: client.autoLiqThresholds?.[acc.id],
|
||||
});
|
||||
return {
|
||||
leg: plan.remainingToday,
|
||||
lastStand: room !== null && lastStand({ dllRoom: room, dailyLossLimit: dll, maxLossRoom: floorRoom }),
|
||||
candidates: rankNextLeg({
|
||||
client, accountId: acc.id, cfg, firm, leg: plan.remainingToday, dllRoom: room,
|
||||
obfuscationDollars: obfuscationStopDollars(cfg, combined.dailyPnL, plan, combined.tradeDate, plan.remainingToday),
|
||||
cash,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user