Files
autofirmer-expanded/lib/leg-estimate.ts
T
Brandon LiandClaude Opus 5.5 02544fbdf5 Instruments: each firm's list from the master, and a session-aware futures draw
- lib/instrument-specs.ts (pure, tested): the CME specs — point value,
  tick size, one-tick spread, Yahoo ticker — replacing POINT_VALUES; the
  draw's maths; the 12-hour hold cap; the micro for drain and extra-day
  legs; reading a firm's list (the master's, or the old rules against an
  older master: this machine's enabled set less bans, the symbol map).
- lib/instruments.ts: the one module the trading paths ask. Measures every
  future's daily move from Yahoo's hourly candles once per trade date, and
  shows entry cost (a tick of spread plus Tradovate's fee parameters or
  TopstepX's last measured fill) without ever choosing by it.
- Futures Auto draws one root per batch from the batch firm's list: movement
  per dollar of spread early in the session with a 10% floor each, sliding
  to the fastest movers by the last entry; nothing expected to hold 12h or
  more; micros only if nothing else is listed. Every draw logs its odds.
- The batch is chosen before its instrument, and a group that can't trade
  anything yields to the next. Copy trade matches the held contract against
  the listed roots and won't copy an instrument it can't identify. Manual
  legs, manual orders and drain/extra-day legs follow the lists.
- DXtrade firms rank their own list whatever root was drawn.
- Capabilities, the dashboard's trade bar and contract resolves follow the
  lists; the trade bar and scheduler default to Auto. The master link
  reconnects when master_dashboard_url changes, and the catalog drops
  ?since after a switch so two masters' versions can't collide.
- Reports carry each futures firm's instrumentStats for the master's Firms
  page; GET /api/instruments shows them (?simulate=N runs the draw, no
  orders); the Settings page shows the lists read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:47:32 -05:00

173 lines
7.8 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, 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 };
}