DXtrade: volatility measured once a day, next-leg hold on the dashboard, closed accounts dead
Each instrument's volatility is measured at the first good sync (or pick) of
a trade date and held for the day, so a day's sizing and hold estimate stay
put. The dashboard shows each DXtrade account's next-leg hold ("hold ~Nh")
with its symbol, notional, volatility and win odds. Accounts the firm has
closed (TERMINATED, EXPIRED, NO_TRADING) read as dead instead of inactive,
and skip the books check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9fc1ed6908
commit
6876223fa4
+40
-2
@@ -3,10 +3,12 @@ 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 } from '@/lib/platform-client';
|
import { historyFresh, type PlatformContract } from '@/lib/platform-client';
|
||||||
|
import type { AccountState } from '@/types';
|
||||||
import {
|
import {
|
||||||
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, currentTradeDateCT,
|
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, currentTradeDateCT,
|
||||||
obfuscationStopDollars, dllRoom as computeDllRoom, dllStopDollars, botStopsActive,
|
obfuscationStopDollars, dllRoom as computeDllRoom, dllStopDollars, botStopsActive, sizeNotional,
|
||||||
|
DEFAULT_TARGET_HOLD_HOURS,
|
||||||
LEG_COMPLETION_EPSILON,
|
LEG_COMPLETION_EPSILON,
|
||||||
} from '@/lib/trading-logic';
|
} from '@/lib/trading-logic';
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ export async function GET() {
|
|||||||
let effectiveProfitTarget: number | null = null;
|
let effectiveProfitTarget: number | null = null;
|
||||||
let nextStop: number | null = null;
|
let nextStop: number | null = null;
|
||||||
let dllRoom: number | null = null;
|
let dllRoom: number | null = null;
|
||||||
|
let holdEstimate: AccountState['holdEstimate'] = null;
|
||||||
if (cfg && effective && !isDead) {
|
if (cfg && effective && !isDead) {
|
||||||
const equityProfit = cash.amount - cfg.accountSize;
|
const equityProfit = cash.amount - cfg.accountSize;
|
||||||
const plan = computeDayPlan(
|
const plan = computeDayPlan(
|
||||||
@@ -121,6 +124,40 @@ 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 the
|
||||||
|
// instrument the selector last picked and today's measured
|
||||||
|
// volatility, to show how long it is expected to hold.
|
||||||
|
const pick = (client as { lastPickedContract?: PlatformContract | null }).lastPickedContract;
|
||||||
|
if (plan && plan.remainingToday > 0 && pick?.stepNotional && pick.volatilityPerHour) {
|
||||||
|
const pct = pick.commissionPct ?? f.commissionPct ?? 0;
|
||||||
|
const obf = obfuscationStopDollars(cfg, dailyPnL, plan, combined.tradeDate, plan.remainingToday);
|
||||||
|
const room = dllRoom;
|
||||||
|
const sizing = sizeNotional({
|
||||||
|
leg: plan.remainingToday,
|
||||||
|
stopDollarsAt: (n) => Math.min(
|
||||||
|
Number.isFinite(obf) && obf > 0 ? obf : Infinity,
|
||||||
|
room !== null && dll > 0
|
||||||
|
? dllStopDollars({ room, dailyLossLimit: dll, entryFee: (pct / 100) * n, commissionPct: pct })
|
||||||
|
: Infinity,
|
||||||
|
),
|
||||||
|
volatilityPerHour: pick.volatilityPerHour,
|
||||||
|
targetHoldHours: f.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS,
|
||||||
|
commissionPct: pct,
|
||||||
|
spreadFraction: pick.spreadFraction,
|
||||||
|
financingPctPerDay: f.financingPctPerDay ?? 0,
|
||||||
|
maxNotional: cfg.maxNotional ?? 0,
|
||||||
|
});
|
||||||
|
if (sizing) {
|
||||||
|
holdEstimate = {
|
||||||
|
hours: Math.round(sizing.predictedHoldHours * 10) / 10,
|
||||||
|
symbol: String(pick.id),
|
||||||
|
notional: Math.round(sizing.notional),
|
||||||
|
volatilityPerDay: pick.volatilityPerHour * Math.sqrt(24),
|
||||||
|
winProbability: sizing.winProbability,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Effective profit target = max(stage target, consistency realTarget).
|
// Effective profit target = max(stage target, consistency realTarget).
|
||||||
// If a big day forces the consistency rule, the account must reach maxDay/consistency
|
// If a big day forces the consistency rule, the account must reach maxDay/consistency
|
||||||
// in total trading profit for the stage, not just profitTarget.
|
// in total trading profit for the stage, not just profitTarget.
|
||||||
@@ -228,6 +265,7 @@ export async function GET() {
|
|||||||
dailyTarget,
|
dailyTarget,
|
||||||
nextStop,
|
nextStop,
|
||||||
dllRoom,
|
dllRoom,
|
||||||
|
holdEstimate,
|
||||||
remainingToday,
|
remainingToday,
|
||||||
effectiveProfitTarget,
|
effectiveProfitTarget,
|
||||||
dailyPnL,
|
dailyPnL,
|
||||||
|
|||||||
@@ -390,6 +390,15 @@ function AccountRow({ account, firm, hideDead, privacy, onAssigned }: {
|
|||||||
{typeof account.dllRoom === 'number' && (
|
{typeof account.dllRoom === 'number' && (
|
||||||
<span className="text-slate-500" title="Hard daily loss limit: what the firm's day may still lose before a breach. 10% of the limit is never spent."> · DLL room ${fmt(account.dllRoom)}</span>
|
<span className="text-slate-500" title="Hard daily loss limit: what the firm's day may still lose before a breach. 10% of the limit is never spent."> · DLL room ${fmt(account.dllRoom)}</span>
|
||||||
)}
|
)}
|
||||||
|
{account.holdEstimate && (
|
||||||
|
<span
|
||||||
|
className="text-slate-500"
|
||||||
|
title={`Next leg on ${account.holdEstimate.symbol}: $${Math.round(account.holdEstimate.notional).toLocaleString()} notional at today's measured volatility of ${(account.holdEstimate.volatilityPerDay * 100).toFixed(2)}%/day`
|
||||||
|
+ (account.holdEstimate.winProbability !== null ? `, wins ~${Math.round(account.holdEstimate.winProbability * 100)}% of the time` : '')}
|
||||||
|
>
|
||||||
|
{' · hold ~'}{account.holdEstimate.hours < 48 ? `${Math.round(account.holdEstimate.hours)}h` : `${(account.holdEstimate.hours / 24).toFixed(1)}d`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+35
-5
@@ -93,6 +93,11 @@ const REAP_AFTER_MS = 60_000;
|
|||||||
/** A position this old with no working order is flagged as unprotected. */
|
/** A position this old with no working order is flagged as unprotected. */
|
||||||
const NAKED_AFTER_MS = 2 * 60_000;
|
const NAKED_AFTER_MS = 2 * 60_000;
|
||||||
|
|
||||||
|
/** accountStatus values that mean the firm has closed the account for good
|
||||||
|
* (TERMINATED after a breach, EXPIRED, NO_TRADING when nothing may trade).
|
||||||
|
* CLOSE_ONLY is left inactive rather than dead: it can still unwind. */
|
||||||
|
const DEAD_STATUSES = new Set(['TERMINATED', 'EXPIRED', 'NO_TRADING']);
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||||
const round2 = (n: number) => Math.round(n * 100) / 100;
|
const round2 = (n: number) => Math.round(n * 100) / 100;
|
||||||
|
|
||||||
@@ -329,7 +334,12 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
private pickCache = new Map<string, { contract: PlatformContract; at: number }>();
|
private pickCache = new Map<string, { contract: PlatformContract; at: number }>();
|
||||||
private instrumentCache = new Map<string, { inst: Record<string, unknown>; at: number }>();
|
private instrumentCache = new Map<string, { inst: Record<string, unknown>; at: number }>();
|
||||||
private accountInstrumentCache = new Map<string, { det: Record<string, unknown>; at: number }>();
|
private accountInstrumentCache = new Map<string, { det: Record<string, unknown>; at: number }>();
|
||||||
private candleCache = new Map<string, { vol: number | null; candles: number; at: number }>();
|
/** Volatility per symbol, measured once per trade date (see volatility). */
|
||||||
|
private candleCache = new Map<string, { vol: number | null; candles: number; at: number; tradeDate: string }>();
|
||||||
|
/** The last instrument the selector picked — what the dashboard's hold
|
||||||
|
* estimate sizes against. */
|
||||||
|
public lastPickedContract: PlatformContract | null = null;
|
||||||
|
private lastPickTradeDate = '';
|
||||||
private unmappedLoggedAt = new Map<string, number>();
|
private unmappedLoggedAt = new Map<string, number>();
|
||||||
|
|
||||||
constructor(cfg: DXTradeConfig, onSync: () => Promise<void>) {
|
constructor(cfg: DXTradeConfig, onSync: () => Promise<void>) {
|
||||||
@@ -680,7 +690,10 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
if (!isFundType(t.type) && currentTradeDateCT(new Date(t.time)) === today) realized += t.value;
|
if (!isFundType(t.type) && currentTradeDateCT(new Date(t.time)) === today) realized += t.value;
|
||||||
}
|
}
|
||||||
this.accountCashBalances[acc.id] = { amount: balance, realizedPnL: round2(realized), equity: ciNum(m, 'equity') };
|
this.accountCashBalances[acc.id] = { amount: balance, realizedPnL: round2(realized), equity: ciNum(m, 'equity') };
|
||||||
this.autoLiqThresholds[acc.id] = 0;
|
// No floor to report — except for an account the firm has
|
||||||
|
// closed for good (a breach): the balance itself, so the
|
||||||
|
// dashboards and the reporter count it dead, not just inactive.
|
||||||
|
this.autoLiqThresholds[acc.id] = DEAD_STATUSES.has(this.accountMeta.get(acc.id)?.status ?? '') ? balance : 0;
|
||||||
this.dayFresh.add(acc.id);
|
this.dayFresh.add(acc.id);
|
||||||
// Keep the reset snapshot handy for the debug view.
|
// Keep the reset snapshot handy for the debug view.
|
||||||
this.lastFetchRaw[`dayStart:${acc.name}`] = String(this.dayStartFromTxns(acc.id, balance, resetMs));
|
this.lastFetchRaw[`dayStart:${acc.name}`] = String(this.dayStartFromTxns(acc.id, balance, resetMs));
|
||||||
@@ -693,6 +706,13 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
|
|
||||||
console.log(`${this.tag} sync — ${list.length} account(s), ${Object.keys(this.positions).length} open position(s)`);
|
console.log(`${this.tag} sync — ${list.length} account(s), ${Object.keys(this.positions).length} open position(s)`);
|
||||||
this.maybeFetchDaysTraded();
|
this.maybeFetchDaysTraded();
|
||||||
|
// Measure the day's volatility (and so the hold estimate) at the
|
||||||
|
// first good sync of each trade date, whether or not a batch fires.
|
||||||
|
if (this.historyLoaded && this.lastPickTradeDate !== currentTradeDateCT()) {
|
||||||
|
this.lastPickTradeDate = currentTradeDateCT();
|
||||||
|
void this.findFrontMonthContract('*').catch((err) =>
|
||||||
|
console.warn(`${this.tag} daily volatility check failed: ${errorMessage(err)}`));
|
||||||
|
}
|
||||||
|
|
||||||
this.syncComplete = true;
|
this.syncComplete = true;
|
||||||
if (!fresh || this.dayFresh.size < list.length) {
|
if (!fresh || this.dayFresh.size < list.length) {
|
||||||
@@ -964,7 +984,8 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
const size = getAccountConfig(account.id).accountType?.accountSize ?? 0;
|
const size = getAccountConfig(account.id).accountType?.accountSize ?? 0;
|
||||||
const hasDeposit = mine.some((t) => t.type === 'DEPOSIT');
|
const hasDeposit = mine.some((t) => t.type === 'DEPOSIT');
|
||||||
const expected = mine.reduce((s, t) => s + (isFundType(t.type) ? signedFundValue(t.type, t.value) : t.value), 0) + (hasDeposit ? 0 : size);
|
const expected = mine.reduce((s, t) => s + (isFundType(t.type) ? signedFundValue(t.type, t.value) : t.value), 0) + (hasDeposit ? 0 : size);
|
||||||
if (cash && !this.positions[account.id] && Math.abs(cash.amount - expected) >= BOOKS_TOLERANCE) {
|
const tradable = this.accountMeta.get(account.id)?.status === 'FULL_TRADING';
|
||||||
|
if (tradable && cash && !this.positions[account.id] && Math.abs(cash.amount - expected) >= BOOKS_TOLERANCE) {
|
||||||
this.booksHeld.add(account.id);
|
this.booksHeld.add(account.id);
|
||||||
this.lastFetchErrors[account.name] = `balance ${round2(cash.amount)} vs transactions ${round2(expected)} — held until they agree`;
|
this.lastFetchErrors[account.name] = `balance ${round2(cash.amount)} vs transactions ${round2(expected)} — held until they agree`;
|
||||||
console.warn(`${this.tag} ${account.name}: ${this.lastFetchErrors[account.name]}`);
|
console.warn(`${this.tag} ${account.name}: ${this.lastFetchErrors[account.name]}`);
|
||||||
@@ -1120,7 +1141,13 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
|
|
||||||
private async volatility(account: string, symbols: string[]): Promise<Map<string, { vol: number | null; candles: number }>> {
|
private async volatility(account: string, symbols: string[]): Promise<Map<string, { vol: number | null; candles: number }>> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const missing = symbols.filter((s) => !(now - (this.candleCache.get(s)?.at ?? 0) < CANDLE_TTL_MS));
|
// Measured once per trade date, so a day's sizing and hold estimate
|
||||||
|
// stay put; a measurement that came back empty retries after a while.
|
||||||
|
const today = currentTradeDateCT();
|
||||||
|
const missing = symbols.filter((s) => {
|
||||||
|
const c = this.candleCache.get(s);
|
||||||
|
return !c || c.tradeDate !== today || (c.vol === null && now - c.at >= CANDLE_TTL_MS);
|
||||||
|
});
|
||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
const closes = new Map<string, number[]>();
|
const closes = new Map<string, number[]>();
|
||||||
let fromDx = false;
|
let fromDx = false;
|
||||||
@@ -1151,7 +1178,9 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
}
|
}
|
||||||
for (const s of missing) {
|
for (const s of missing) {
|
||||||
const list = closes.get(s) ?? [];
|
const list = closes.get(s) ?? [];
|
||||||
this.candleCache.set(s, { vol: realizedVolPerHour(list), candles: list.length, at: now });
|
const vol = realizedVolPerHour(list);
|
||||||
|
this.candleCache.set(s, { vol, candles: list.length, at: now, tradeDate: today });
|
||||||
|
if (vol !== null) console.log(`${this.tag} ${s} volatility for ${today}: ${(vol * Math.sqrt(24) * 100).toFixed(2)}%/day (${list.length} hourly closes)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Map(symbols.map((s) => [s, this.candleCache.get(s) ?? { vol: null, candles: 0 }]));
|
return new Map(symbols.map((s) => [s, this.candleCache.get(s) ?? { vol: null, candles: 0 }]));
|
||||||
@@ -1275,6 +1304,7 @@ export class DXTradeClient implements PlatformClient {
|
|||||||
.join('; ');
|
.join('; ');
|
||||||
this.lastFetchRaw[`pick:${root}`] = `${new Date().toISOString()} → ${winner?.symbol ?? 'none'} — ${summary}`;
|
this.lastFetchRaw[`pick:${root}`] = `${new Date().toISOString()} → ${winner?.symbol ?? 'none'} — ${summary}`;
|
||||||
console.log(`${this.tag} ${root}${sticky ? ' (held)' : ''} → ${winner?.symbol ?? 'nothing tradable'} — ${summary}`);
|
console.log(`${this.tag} ${root}${sticky ? ' (held)' : ''} → ${winner?.symbol ?? 'nothing tradable'} — ${summary}`);
|
||||||
|
if (winner?.contract) this.lastPickedContract = winner.contract;
|
||||||
return winner?.contract ?? null;
|
return winner?.contract ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ export interface AccountState {
|
|||||||
/** What the firm's day may still lose before a hard daily-loss breach.
|
/** What the firm's day may still lose before a hard daily-loss breach.
|
||||||
* null = no daily loss limit (or its day-start balance isn't known yet). */
|
* null = no daily loss limit (or its day-start balance isn't known yet). */
|
||||||
dllRoom: number | null;
|
dllRoom: number | null;
|
||||||
|
/** DXtrade: how the next leg would be sized and how long it is expected to
|
||||||
|
* hold, from today's measured volatility of the last-picked instrument.
|
||||||
|
* null on other platforms, or before the day's first pick. */
|
||||||
|
holdEstimate: {
|
||||||
|
hours: number;
|
||||||
|
symbol: string;
|
||||||
|
notional: number;
|
||||||
|
volatilityPerDay: number;
|
||||||
|
winProbability: number | null;
|
||||||
|
} | null;
|
||||||
/** Actual profit target to hit — max(stage profit target, consistency realTarget = maxDay/consistency). */
|
/** Actual profit target to hit — max(stage profit target, consistency realTarget = maxDay/consistency). */
|
||||||
effectiveProfitTarget: number | null;
|
effectiveProfitTarget: number | null;
|
||||||
/** Current cycle daily P&L — used for target calculations. */
|
/** Current cycle daily P&L — used for target calculations. */
|
||||||
|
|||||||
Reference in New Issue
Block a user