Legs: cheapest symbol that reaches the target hold, last stands, a reserve that grows with size
- A batch sizes its leg on every mapped candidate, each at the leverage the broker states for it, and trades the cheapest (spread, commissions, expected swap) that reaches the target hold — else the quickest. The dashboard ranks the same way from a half-hourly candidate survey. - Last stand: when a daily-loss stop-out would leave the account within 15% of that loss above its max-drawdown floor (the account type's static max loss, or the broker's liquidation floor), the leg carries no daily-loss stop and is sized against the firm's own breach. - The daily-loss reserve is 10% of the limit or 0.1% of the position, whichever is more, since a stop's slippage grows with the position. - The account type's max notional is gone: the broker's margin rates cap each symbol. planLegSize holds the one sizing rule fireLeg, the pick and the dashboard share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a2d9df5ba4
commit
a2cd3a94c9
+51
-22
@@ -31,7 +31,7 @@
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { currentTradeDateCT, FIRM_RESET_UTC_HOUR, DEFAULT_TARGET_HOLD_HOURS } from './trading-logic';
|
||||
import { currentTradeDateCT, FIRM_RESET_UTC_HOUR, DEFAULT_TARGET_HOLD_HOURS, rankForTarget } from './trading-logic';
|
||||
import {
|
||||
saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions,
|
||||
saveEndedCycleStart,
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
import { getAccountConfig } from './account-type-resolver';
|
||||
import { getCachedFirm } from './firm-catalog';
|
||||
import type { SymbolMapCandidate } from './master-protocol';
|
||||
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth } from './platform-client';
|
||||
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
|
||||
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
|
||||
import {
|
||||
dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep,
|
||||
@@ -78,6 +78,8 @@ const OCO_POLL_MS = 5_000;
|
||||
/** A pick is reused this long, so a batch's lookups (Phase 1, drain legs)
|
||||
* all land on the same symbol. */
|
||||
const PICK_TTL_MS = 45_000;
|
||||
/** How often the dashboard's candidates (quotes, spreads) are refreshed. */
|
||||
const SURVEY_INTERVAL_MS = 30 * 60_000;
|
||||
const INSTRUMENT_TTL_MS = 60 * 60_000;
|
||||
const ACCOUNT_INSTRUMENT_TTL_MS = 10 * 60_000;
|
||||
const CANDLE_TTL_MS = 15 * 60_000;
|
||||
@@ -304,10 +306,11 @@ export class DXTradeClient implements PlatformClient {
|
||||
private accountInstrumentCache = new Map<string, { det: Record<string, unknown>; 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 = '';
|
||||
/** Every mapped instrument that could trade at the last full look — what
|
||||
* the dashboard ranks each account's next leg across. */
|
||||
public lastCandidates: PlatformContract[] = [];
|
||||
private lastSurveyTradeDate = '';
|
||||
private lastSurveyAt = 0;
|
||||
private unmappedLoggedAt = new Map<string, number>();
|
||||
|
||||
constructor(cfg: DXTradeConfig, onSync: () => Promise<void>) {
|
||||
@@ -676,12 +679,14 @@ 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)}`));
|
||||
// Measure the day's volatility at the first good sync of each trade
|
||||
// date, and refresh the candidates the dashboard sizes against
|
||||
// every half hour, whether or not a batch fires.
|
||||
if (this.historyLoaded && (this.lastSurveyTradeDate !== currentTradeDateCT() || Date.now() - this.lastSurveyAt >= SURVEY_INTERVAL_MS)) {
|
||||
this.lastSurveyTradeDate = currentTradeDateCT();
|
||||
this.lastSurveyAt = Date.now();
|
||||
void this.surveyCandidates().catch((err) =>
|
||||
console.warn(`${this.tag} candidate survey failed: ${errorMessage(err)}`));
|
||||
}
|
||||
|
||||
this.syncComplete = true;
|
||||
@@ -1157,12 +1162,15 @@ export class DXTradeClient implements PlatformClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* The contract a CME root trades as on this firm right now: the cheapest
|
||||
* tradable candidate from the symbol map, per unit of volatility (see
|
||||
* scoreCandidate). Sticky while an account
|
||||
* holds one of the candidates, cached briefly so a batch's lookups agree.
|
||||
* The contract a CME root trades as on this firm right now, from the
|
||||
* symbol map. With a pick context (the batch's leg), each candidate is
|
||||
* sized as the leg would be — at its own leverage — and the cheapest one
|
||||
* that reaches the target hold wins (else the quickest); without one, the
|
||||
* cheapest per unit of volatility (see scoreCandidate). Sticky while an
|
||||
* account holds one of the candidates; a batch's pick is reused briefly
|
||||
* so its other lookups (drain legs) agree.
|
||||
*/
|
||||
public async findFrontMonthContract(root: string): Promise<PlatformContract | null> {
|
||||
public async findFrontMonthContract(root: string, ctx?: PickContext): Promise<PlatformContract | null> {
|
||||
if (!this.token) return null;
|
||||
const firm = getCachedFirm(this.cfg.firmId);
|
||||
const map = firm?.symbolMap ?? {};
|
||||
@@ -1177,16 +1185,26 @@ export class DXTradeClient implements PlatformClient {
|
||||
}
|
||||
const held = new Set([...this.positionsDetail.values()].flat().map((p) => p.symbol));
|
||||
const sticky = candidates.find((c) => held.has(c.symbol));
|
||||
if (!sticky) {
|
||||
if (!sticky && !ctx) {
|
||||
const cached = this.pickCache.get(root);
|
||||
if (cached && now - cached.at < PICK_TTL_MS) return cached.contract;
|
||||
}
|
||||
const pick = await this.pickCandidate(root, sticky ? [sticky] : candidates, sticky !== undefined);
|
||||
const pick = await this.pickCandidate(root, sticky ? [sticky] : candidates, sticky !== undefined, ctx);
|
||||
if (pick && !sticky) this.pickCache.set(root, { contract: pick, at: now });
|
||||
return pick;
|
||||
}
|
||||
|
||||
private async pickCandidate(root: string, candidates: SymbolMapCandidate[], sticky: boolean): Promise<PlatformContract | null> {
|
||||
/** Every mapped symbol, looked at afresh whatever the accounts hold: the
|
||||
* day's volatility, and the candidates the dashboard ranks. */
|
||||
private async surveyCandidates(): Promise<void> {
|
||||
const all = new Map<string, SymbolMapCandidate>();
|
||||
for (const list of Object.values(getCachedFirm(this.cfg.firmId)?.symbolMap ?? {})) {
|
||||
for (const c of list) if (!all.has(c.symbol)) all.set(c.symbol, c);
|
||||
}
|
||||
if (all.size > 0) await this.pickCandidate('survey', [...all.values()], false);
|
||||
}
|
||||
|
||||
private async pickCandidate(root: string, candidates: SymbolMapCandidate[], sticky: boolean, ctx?: PickContext): Promise<PlatformContract | null> {
|
||||
const ref = this.referenceAccount();
|
||||
if (!ref) return null;
|
||||
const firm = getCachedFirm(this.cfg.firmId);
|
||||
@@ -1270,14 +1288,25 @@ export class DXTradeClient implements PlatformClient {
|
||||
});
|
||||
}
|
||||
|
||||
const ranked = board.filter((b) => b.contract).sort((a, b) => a.score! - b.score!);
|
||||
let ranked = board.filter((b) => b.contract).sort((a, b) => a.score! - b.score!);
|
||||
if (ctx) {
|
||||
// Size the batch's leg on each: cheapest that reaches the target
|
||||
// hold, else quickest.
|
||||
const sized = ranked.flatMap((b) => {
|
||||
const ev = ctx.evaluate(b.contract!);
|
||||
if (!ev) { b.note += ', can\'t size'; return []; }
|
||||
b.note += `, $${Math.round(ev.notional / 1000)}k → ${ev.holdHours.toFixed(1)}h, ~$${Math.round(ev.cost)}`;
|
||||
return [{ b, holdHours: ev.holdHours, cost: ev.cost }];
|
||||
});
|
||||
ranked = rankForTarget(sized, ctx.targetHoldHours).map((i) => sized[i].b);
|
||||
}
|
||||
const winner = ranked[0];
|
||||
const summary = board
|
||||
.map((b) => `${b.symbol}${b.score !== null ? ` ${b.score.toFixed(4)}` : ''} (${b.note})`)
|
||||
.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;
|
||||
if (!sticky) this.lastCandidates = board.flatMap((b) => (b.contract ? [b.contract] : []));
|
||||
return winner?.contract ?? null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user