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
+35
-5
@@ -93,6 +93,11 @@ const REAP_AFTER_MS = 60_000;
|
||||
/** A position this old with no working order is flagged as unprotected. */
|
||||
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 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 instrumentCache = new Map<string, { inst: 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>();
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
// Keep the reset snapshot handy for the debug view.
|
||||
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)`);
|
||||
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;
|
||||
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 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);
|
||||
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.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]}`);
|
||||
@@ -1120,7 +1141,13 @@ export class DXTradeClient implements PlatformClient {
|
||||
|
||||
private async volatility(account: string, symbols: string[]): Promise<Map<string, { vol: number | null; candles: number }>> {
|
||||
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) {
|
||||
const closes = new Map<string, number[]>();
|
||||
let fromDx = false;
|
||||
@@ -1151,7 +1178,9 @@ export class DXTradeClient implements PlatformClient {
|
||||
}
|
||||
for (const s of missing) {
|
||||
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 }]));
|
||||
@@ -1275,6 +1304,7 @@ export class DXTradeClient implements PlatformClient {
|
||||
.join('; ');
|
||||
this.lastFetchRaw[`pick:${root}`] = `${new Date().toISOString()} → ${winner?.symbol ?? 'none'} — ${summary}`;
|
||||
console.log(`${this.tag} ${root}${sticky ? ' (held)' : ''} → ${winner?.symbol ?? 'nothing tradable'} — ${summary}`);
|
||||
if (winner?.contract) this.lastPickedContract = winner.contract;
|
||||
return winner?.contract ?? null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user