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
+20
-39
@@ -3,13 +3,13 @@ import { loadDailyPnL } from '@/lib/db';
|
|||||||
import { getClients } from '@/lib/clients';
|
import { getClients } from '@/lib/clients';
|
||||||
import { getCachedCatalog } from '@/lib/firm-catalog';
|
import { getCachedCatalog } from '@/lib/firm-catalog';
|
||||||
import { getAccountConfig, getInferredSize, matchAccountType } from '@/lib/account-type-resolver';
|
import { getAccountConfig, getInferredSize, matchAccountType } from '@/lib/account-type-resolver';
|
||||||
import { historyFresh, type PlatformContract } from '@/lib/platform-client';
|
import { historyFresh } from '@/lib/platform-client';
|
||||||
|
import { rankNextLeg } from '@/lib/leg-estimate';
|
||||||
import type { AccountState } from '@/types';
|
import type { AccountState } from '@/types';
|
||||||
import {
|
import {
|
||||||
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, currentTradeDateCT,
|
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, currentTradeDateCT,
|
||||||
obfuscationStopDollars, dllRoom as computeDllRoom, dllStopDollars, botStopsActive, planLegSize, rankForTarget,
|
obfuscationStopDollars, dllRoom as computeDllRoom, dllStopDollars, botStopsActive,
|
||||||
maxLossRoom, lastStand as isLastStand,
|
maxLossRoom, lastStand as isLastStand,
|
||||||
DEFAULT_TARGET_HOLD_HOURS,
|
|
||||||
LEG_COMPLETION_EPSILON,
|
LEG_COMPLETION_EPSILON,
|
||||||
} from '@/lib/trading-logic';
|
} from '@/lib/trading-logic';
|
||||||
|
|
||||||
@@ -133,44 +133,25 @@ export async function GET() {
|
|||||||
if (Number.isFinite(stop) && stop > 0) nextStop = Math.round(stop * 100) / 100;
|
if (Number.isFinite(stop) && stop > 0) nextStop = Math.round(stop * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DXtrade: size the next leg the way fireLeg would on each
|
// DXtrade: the next leg sized on every surveyed instrument as
|
||||||
// instrument the client last surveyed — at its leverage and
|
// fireLeg would, and the one a batch would pick.
|
||||||
// measured volatility — and take the one the batch would:
|
if (plan && plan.remainingToday > 0) {
|
||||||
// the cheapest that reaches the target hold. The next leg
|
const best = rankNextLeg({
|
||||||
// opens flat, so while one is open (its margin tied up) it
|
client, accountId: acc.id, cfg, firm: f, leg: plan.remainingToday, dllRoom,
|
||||||
// sizes against the balance.
|
obfuscationDollars: obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday),
|
||||||
const candidates = (client as { lastCandidates?: PlatformContract[] }).lastCandidates ?? [];
|
cash,
|
||||||
if (plan && plan.remainingToday > 0 && candidates.length > 0) {
|
})[0];
|
||||||
const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday);
|
|
||||||
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 }]
|
|
||||||
: [];
|
|
||||||
});
|
|
||||||
const best = sized[rankForTarget(sized, target)[0]];
|
|
||||||
if (best) {
|
if (best) {
|
||||||
holdEstimate = {
|
holdEstimate = {
|
||||||
hours: Math.round(best.holdHours * 10) / 10,
|
hours: best.holdHours,
|
||||||
symbol: String(best.c.id),
|
symbol: best.symbol,
|
||||||
notional: Math.round(best.sizing.notional),
|
notional: best.notional,
|
||||||
volatilityPerDay: best.volatility * Math.sqrt(24),
|
multiplier: best.multiplier,
|
||||||
winProbability: best.sizing.winProbability,
|
volatilityPerDay: best.volatilityPerDay,
|
||||||
swapReserve: best.sizing.swapReserve,
|
winProbability: best.winProbability,
|
||||||
leverage: best.c.marginRate ? Math.round(10 / best.c.marginRate) / 10 : null,
|
swapReserve: best.swapReserve,
|
||||||
cost: Math.round(best.cost),
|
leverage: best.leverage,
|
||||||
|
cost: best.cost,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -396,8 +396,8 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
|
|||||||
{account.holdEstimate && (
|
{account.holdEstimate && (
|
||||||
<span
|
<span
|
||||||
className="text-slate-500"
|
className="text-slate-500"
|
||||||
title={`Next leg on ${account.holdEstimate.symbol}: $${Math.round(account.holdEstimate.notional).toLocaleString()} notional`
|
title={`Next leg on ${account.holdEstimate.symbol}: $${Math.round(account.holdEstimate.notional).toLocaleString()} notional, ${account.holdEstimate.multiplier}× the account`
|
||||||
+ (account.holdEstimate.leverage !== null ? ` (${account.holdEstimate.leverage}:1 max)` : '')
|
+ (account.holdEstimate.leverage !== null ? ` (${account.holdEstimate.leverage}× max)` : '')
|
||||||
+ ` at a measured volatility of ${(account.holdEstimate.volatilityPerDay * 100).toFixed(2)}%/day over the past week`
|
+ ` at a measured volatility of ${(account.holdEstimate.volatilityPerDay * 100).toFixed(2)}%/day over the past week`
|
||||||
+ (account.holdEstimate.winProbability !== null ? `, wins ~${Math.round(account.holdEstimate.winProbability * 100)}% of the time` : '')
|
+ (account.holdEstimate.winProbability !== null ? `, wins ~${Math.round(account.holdEstimate.winProbability * 100)}% of the time` : '')
|
||||||
+ (account.holdEstimate.swapReserve > 0 ? `; its take-profit carries ~$${fmt(account.holdEstimate.swapReserve)} of expected swap` : '')
|
+ (account.holdEstimate.swapReserve > 0 ? `; its take-profit carries ~$${fmt(account.holdEstimate.swapReserve)} of expected swap` : '')
|
||||||
|
|||||||
@@ -1258,7 +1258,7 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
accountCurrency: ref.currency,
|
accountCurrency: ref.currency,
|
||||||
});
|
});
|
||||||
if (rejection) { board.push({ symbol: cand.symbol, score: null, note: rejection }); continue; }
|
if (rejection) { board.push({ symbol: cand.symbol, score: null, note: rejection }); continue; }
|
||||||
const pct = cand.commissionPct ?? firm?.commissionPct ?? 0;
|
const pct = firm?.commissionPct ?? 0;
|
||||||
const score = scoreCandidate({
|
const score = scoreCandidate({
|
||||||
spreadFraction: spread,
|
spreadFraction: spread,
|
||||||
commissionPct: pct,
|
commissionPct: pct,
|
||||||
|
|||||||
+1
-3
@@ -156,9 +156,7 @@ function sanitizeSymbolMap(raw: unknown): SymbolMap {
|
|||||||
if (typeof c !== 'object' || c === null) return [];
|
if (typeof c !== 'object' || c === null) return [];
|
||||||
const cand = c as Record<string, unknown>;
|
const cand = c as Record<string, unknown>;
|
||||||
if (typeof cand.symbol !== 'string' || !cand.symbol || cand.symbol.includes('|')) return [];
|
if (typeof cand.symbol !== 'string' || !cand.symbol || cand.symbol.includes('|')) return [];
|
||||||
const pct = typeof cand.commissionPct === 'number' && Number.isFinite(cand.commissionPct) && cand.commissionPct >= 0
|
return [{ symbol: cand.symbol }];
|
||||||
? cand.commissionPct : undefined;
|
|
||||||
return [{ symbol: cand.symbol, ...(pct !== undefined ? { commissionPct: pct } : {}) }];
|
|
||||||
});
|
});
|
||||||
if (candidates.length > 0) map[root.toUpperCase()] = candidates;
|
if (candidates.length > 0) map[root.toUpperCase()] = candidates;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
+33
-4
@@ -112,6 +112,36 @@ export interface FirmStats {
|
|||||||
/** Per account type, summing to the firm's own totals. Optional so a master
|
/** Per account type, summing to the firm's own totals. Optional so a master
|
||||||
* on an older build simply ignores it. */
|
* on an older build simply ignores it. */
|
||||||
accountTypes?: AccountTypeStats[];
|
accountTypes?: AccountTypeStats[];
|
||||||
|
/** DXtrade firms: one account's next leg sized on every symbol-map
|
||||||
|
* candidate, best first — what the master's Firms page shows beside each
|
||||||
|
* symbol. Optional. */
|
||||||
|
symbolStats?: SymbolStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How one account's next leg sizes on each candidate (see planLegSize). */
|
||||||
|
export interface SymbolStats {
|
||||||
|
account: string;
|
||||||
|
/** The leg's dollar target, before fees and swap. */
|
||||||
|
leg: number;
|
||||||
|
targetHoldHours: number;
|
||||||
|
/** The leg rides without a daily-loss stop (see lastStand). */
|
||||||
|
lastStand: boolean;
|
||||||
|
/** ISO time the stats were computed. */
|
||||||
|
at: string;
|
||||||
|
symbols: {
|
||||||
|
symbol: string;
|
||||||
|
notional: number;
|
||||||
|
/** Notional ÷ account size. */
|
||||||
|
multiplier: number;
|
||||||
|
/** The broker's leverage on the symbol (5 = 5:1); null when unstated. */
|
||||||
|
maxMultiplier: number | null;
|
||||||
|
holdHours: number;
|
||||||
|
/** Expected spread, commissions and swap. */
|
||||||
|
cost: number;
|
||||||
|
winProbability: number | null;
|
||||||
|
volatilityPerDay: number;
|
||||||
|
spreadFraction: number;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SchedulerState {
|
export interface SchedulerState {
|
||||||
@@ -226,13 +256,12 @@ export function isFirmPlatform(value: unknown): value is FirmPlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** One symbol a DXTrade firm may trade for a CME root. Position size comes
|
/** One symbol a DXTrade firm may trade for a CME root. Position size comes
|
||||||
* from volatility and the firm's target hold, never from the map (an older
|
* from volatility and the firm's target hold, and the commission is the
|
||||||
* master's notionalPerContract is ignored). */
|
* firm's (an older master's notionalPerContract or per-symbol commissionPct
|
||||||
|
* is ignored). */
|
||||||
export interface SymbolMapCandidate {
|
export interface SymbolMapCandidate {
|
||||||
/** The platform's symbol, e.g. "BTC/USD". */
|
/** The platform's symbol, e.g. "BTC/USD". */
|
||||||
symbol: string;
|
symbol: string;
|
||||||
/** Per-side commission % for this symbol; absent = the firm's. */
|
|
||||||
commissionPct?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CME root the batch drew (NQ, MNQ, GC…) or '*' for any root not listed →
|
/** CME root the batch drew (NQ, MNQ, GC…) or '*' for any root not listed →
|
||||||
|
|||||||
+48
-2
@@ -18,8 +18,9 @@ import { getClients } from './clients';
|
|||||||
import { getTradableFirms } from './firm-catalog';
|
import { getTradableFirms } from './firm-catalog';
|
||||||
import { getAccountConfig } from './account-type-resolver';
|
import { getAccountConfig } from './account-type-resolver';
|
||||||
import { getSchedulerStatus } from './auto-trade';
|
import { getSchedulerStatus } from './auto-trade';
|
||||||
import { botStopsActive, combineCmeSessions, computeDayPlan, currentTradeDateCT, cycleCompletion, endedCycleCompletion, LEG_COMPLETION_EPSILON, projectPerfectPath, resolveEffectiveConfig } from './trading-logic';
|
import { botStopsActive, combineCmeSessions, computeDayPlan, currentTradeDateCT, cycleCompletion, endedCycleCompletion, LEG_COMPLETION_EPSILON, projectPerfectPath, resolveEffectiveConfig, DEFAULT_TARGET_HOLD_HOURS } from './trading-logic';
|
||||||
import type { AccountTypeStats, FirmStats } from './master-protocol';
|
import type { AccountTypeStats, FirmStats, SymbolStats } from './master-protocol';
|
||||||
|
import { accountNextLeg } from './leg-estimate';
|
||||||
import { historyFresh } from './platform-client';
|
import { historyFresh } from './platform-client';
|
||||||
|
|
||||||
function collectFirmStats() {
|
function collectFirmStats() {
|
||||||
@@ -387,10 +388,55 @@ function collectFirmStats() {
|
|||||||
// Unknown until the firm has synced, not empty.
|
// Unknown until the firm has synced, not empty.
|
||||||
...(client.syncComplete ? { accounts } : {}),
|
...(client.syncComplete ? { accounts } : {}),
|
||||||
accountTypes,
|
accountTypes,
|
||||||
|
...symbolStatsFor(f, client, rawToday),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DXtrade firm's next leg on each symbol-map candidate, for the master's
|
||||||
|
* Firms page: one live account from the firm's largest prefix group — the
|
||||||
|
* group a batch fires — with something left to trade today. Nothing for
|
||||||
|
* other platforms, or when no account has a leg to size.
|
||||||
|
*/
|
||||||
|
function symbolStatsFor(
|
||||||
|
f: { name: string; commissionPct?: number; targetHoldHours?: number; financingPctPerDay?: number },
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
client: any,
|
||||||
|
tradeDate: string,
|
||||||
|
): { symbolStats?: SymbolStats } {
|
||||||
|
if (!(client.lastCandidates?.length > 0)) return {};
|
||||||
|
const groups = new Map<string, { id: number; name: string }[]>();
|
||||||
|
for (const acc of client.accountList as { id: number; name: string; active: boolean }[]) {
|
||||||
|
const cfg = getAccountConfig(acc.id).accountType;
|
||||||
|
const floor = client.autoLiqThresholds?.[acc.id] ?? 0;
|
||||||
|
const cash = client.accountCashBalances?.[acc.id];
|
||||||
|
if (!cfg || !acc.active || !cash || (floor !== 0 && cash.amount <= floor)) continue;
|
||||||
|
groups.set(cfg.prefix, [...(groups.get(cfg.prefix) ?? []), acc]);
|
||||||
|
}
|
||||||
|
for (const group of [...groups.values()].sort((a, b) => b.length - a.length)) {
|
||||||
|
for (const acc of group) {
|
||||||
|
const next = accountNextLeg(client, acc, f, tradeDate);
|
||||||
|
if (!next || next.candidates.length === 0) continue;
|
||||||
|
return {
|
||||||
|
symbolStats: {
|
||||||
|
account: acc.name,
|
||||||
|
leg: next.leg,
|
||||||
|
targetHoldHours: f.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS,
|
||||||
|
lastStand: next.lastStand,
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
symbols: next.candidates.map((c) => ({
|
||||||
|
symbol: c.symbol, notional: c.notional, multiplier: c.multiplier, maxMultiplier: c.maxMultiplier,
|
||||||
|
holdHours: c.holdHours, cost: c.cost, winProbability: c.winProbability,
|
||||||
|
volatilityPerDay: c.volatilityPerDay, spreadFraction: c.spreadFraction,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
// HMR-safe global to avoid duplicate intervals
|
// HMR-safe global to avoid duplicate intervals
|
||||||
const _g = globalThis as typeof globalThis & { __reporterInterval?: ReturnType<typeof setInterval> };
|
const _g = globalThis as typeof globalThis & { __reporterInterval?: ReturnType<typeof setInterval> };
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export interface AccountState {
|
|||||||
hours: number;
|
hours: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
notional: number;
|
notional: number;
|
||||||
|
/** Notional ÷ account size. */
|
||||||
|
multiplier: number;
|
||||||
volatilityPerDay: number;
|
volatilityPerDay: number;
|
||||||
winProbability: number | null;
|
winProbability: number | null;
|
||||||
/** Expected swap a winning leg pays, added to its take-profit. */
|
/** Expected swap a winning leg pays, added to its take-profit. */
|
||||||
|
|||||||
Reference in New Issue
Block a user