DXtrade sizing: volatility only (12h default hold); fees and every-day only for DXtrade
The symbol map's fixed notional per contract is gone: every DXtrade leg is sized from the instrument's recent volatility for the firm's target hold (blank = 12 h), and a candidate without a volatility isn't picked. Percent commission and every-day trading are ignored on non-DXtrade firms, which keep measured per-contract fees and the CME calendar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
28f465508d
commit
d01f03b293
+7
-8
@@ -14,6 +14,7 @@ import { getAccountConfig } from './account-type-resolver';
|
||||
import {
|
||||
combineCmeSessions, computeDayPlan, groupLegFraction, pickLegAmount, resolveEffectiveConfig, currentTradeDateCT,
|
||||
obfuscationStopDollars, takeProfitPlan, percentFee, dllRoom, dllStopDollars, dllViable, stopTicks, sizeNotional,
|
||||
DEFAULT_TARGET_HOLD_HOURS,
|
||||
isNoTradeTime, isPastSessionClose,
|
||||
BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY, POINT_VALUES,
|
||||
FALLBACK_COMMISSION_PER_CONTRACT, MAX_COMMISSION_PER_CONTRACT, DLL_MIN_STOP,
|
||||
@@ -947,9 +948,9 @@ async function flattenAndFail(
|
||||
* the batch and copy-trade, so every fee, stop and sizing rule lives once.
|
||||
*
|
||||
* Size: futures trade one contract per $1,000 of leg; a platform that prices
|
||||
* positions (stepNotional) sizes by notional instead — volatility sizing for
|
||||
* the firm's target hold, or the symbol map's fixed notional — under the
|
||||
* account type's notional cap.
|
||||
* positions (stepNotional) sizes by notional instead — whatever makes the
|
||||
* leg's expected hold the firm's target, from the instrument's recent
|
||||
* volatility — under the account type's notional cap.
|
||||
*
|
||||
* Fees: a percent-of-notional firm's fee is computed off the fill; otherwise
|
||||
* it is read off the realized-P&L change a second after it.
|
||||
@@ -1016,15 +1017,14 @@ async function fireLeg(
|
||||
leg: legAmount,
|
||||
stopDollarsAt,
|
||||
volatilityPerHour: legContract.volatilityPerHour,
|
||||
targetHoldHours: firm.targetHoldHours ?? 0,
|
||||
targetHoldHours: firm.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS,
|
||||
commissionPct: pct,
|
||||
spreadFraction: legContract.spreadFraction,
|
||||
financingPctPerDay: firm.financingPctPerDay ?? 0,
|
||||
fixedNotional: legContract.fixedNotional ?? 0,
|
||||
maxNotional: cfg.maxNotional ?? 0,
|
||||
});
|
||||
if (!sizing) {
|
||||
throw new Error(`no position size for ${legContract.name} — set a notional per contract in the symbol map, or a target hold on the firm`);
|
||||
throw new Error(`no position size for ${legContract.name} — its recent volatility is unknown`);
|
||||
}
|
||||
contracts = Math.max(1, Math.round(sizing.notional / stepNotional));
|
||||
const maxNotional = cfg.maxNotional ?? 0;
|
||||
@@ -1118,8 +1118,7 @@ async function fireLeg(
|
||||
|
||||
const instrumentLabel = legContract.stepNotional || item.drain ? legContract.name : symbolLabel;
|
||||
const sizeNote = sizing
|
||||
? ` | notional $${Math.round(contracts * (legContract.stepNotional ?? 0))} (${sizing.mode}`
|
||||
+ `${sizing.predictedHoldHours !== null ? `, hold ~${sizing.predictedHoldHours.toFixed(1)}h` : ''}`
|
||||
? ` | notional $${Math.round(contracts * (legContract.stepNotional ?? 0))} (hold ~${sizing.predictedHoldHours.toFixed(1)}h`
|
||||
+ `${sizing.winProbability !== null ? `, win ${Math.round(sizing.winProbability * 100)}%` : ''})`
|
||||
: '';
|
||||
const stopNote = stopPrice !== null
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { currentTradeDateCT, FIRM_RESET_UTC_HOUR } from './trading-logic';
|
||||
import { currentTradeDateCT, FIRM_RESET_UTC_HOUR, DEFAULT_TARGET_HOLD_HOURS } from './trading-logic';
|
||||
import {
|
||||
saveDailyPnL, loadDailyPnL, loadAccountMeta, saveAccountMeta, loadFundTransactions, saveFundTransactions,
|
||||
saveEndedCycleStart,
|
||||
@@ -1102,8 +1102,8 @@ 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 when the
|
||||
* firm sizes by target hold (see scoreCandidate). Sticky while an account
|
||||
* 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.
|
||||
*/
|
||||
public async findFrontMonthContract(root: string): Promise<PlatformContract | null> {
|
||||
@@ -1134,13 +1134,13 @@ export class DXTradeClient implements PlatformClient {
|
||||
const ref = this.referenceAccount();
|
||||
if (!ref) return null;
|
||||
const firm = getCachedFirm(this.cfg.firmId);
|
||||
const targetHold = firm?.targetHoldHours ?? 0;
|
||||
const targetHold = firm?.targetHoldHours || DEFAULT_TARGET_HOLD_HOURS;
|
||||
const symbols = [...new Set(candidates.map((c) => c.symbol))];
|
||||
const [insts, dets, quotes, vols] = await Promise.all([
|
||||
this.instruments(symbols),
|
||||
this.accountInstruments(ref.code, symbols),
|
||||
this.quotes(ref.code, symbols),
|
||||
targetHold > 0 ? this.volatility(ref.code, symbols) : Promise.resolve(new Map<string, { vol: number | null; candles: number }>()),
|
||||
this.volatility(ref.code, symbols),
|
||||
]);
|
||||
|
||||
const now = new Date();
|
||||
@@ -1178,7 +1178,6 @@ export class DXTradeClient implements PlatformClient {
|
||||
quoteAgeMs: quote && Number.isFinite(quote.time) ? Date.now() - quote.time : null,
|
||||
inHours: inTradingHours(inst.tradingHours, now),
|
||||
candles: vol?.candles ?? 0,
|
||||
needVolatility: targetHold > 0,
|
||||
minContractsOk: maxContracts === undefined || maxContracts >= minContracts,
|
||||
currency: typeof inst.currency === 'string' ? inst.currency : undefined,
|
||||
accountCurrency: ref.currency,
|
||||
@@ -1204,7 +1203,6 @@ export class DXTradeClient implements PlatformClient {
|
||||
pointValue: step * multiplier,
|
||||
commissionPct: pct,
|
||||
stepNotional,
|
||||
fixedNotional: cand.notionalPerContract,
|
||||
volatilityPerHour: vol?.vol ?? undefined,
|
||||
spreadFraction: spread,
|
||||
maxContracts,
|
||||
|
||||
+10
-18
@@ -212,16 +212,13 @@ export function realizedVolPerHour(closes: number[]): number | null {
|
||||
/**
|
||||
* A candidate's cost score — lower is better.
|
||||
*
|
||||
* Volatility sizing (targetHoldHours > 0): a leg sized for a hold of T hours
|
||||
* needs notional ∝ 1/σ, so its cost — spread plus both commissions plus
|
||||
* financing over the hold, all proportional to notional — is proportional to
|
||||
* (spread + 2c + f·T/24) / σ. A volatile instrument reaches the take-profit
|
||||
* with less notional, which is what makes a tight spread alone not enough.
|
||||
* A leg sized for a hold of T hours needs notional ∝ 1/σ, so its cost —
|
||||
* spread plus both commissions plus financing over the hold, all
|
||||
* proportional to notional — is proportional to (spread + 2c + f·T/24) / σ.
|
||||
* A volatile instrument reaches the take-profit with less notional, which is
|
||||
* what makes a tight spread alone not enough.
|
||||
*
|
||||
* Fixed sizing: the notional is set by the symbol map, so only the costs per
|
||||
* dollar of notional matter.
|
||||
*
|
||||
* null = can't be scored (volatility sizing without a σ).
|
||||
* null = can't be scored (no σ).
|
||||
*/
|
||||
export function scoreCandidate(o: {
|
||||
spreadFraction: number;
|
||||
@@ -230,13 +227,9 @@ export function scoreCandidate(o: {
|
||||
targetHoldHours: number;
|
||||
volatilityPerHour: number | null;
|
||||
}): number | null {
|
||||
const perNotional = o.spreadFraction + 2 * (o.commissionPct / 100);
|
||||
if (o.targetHoldHours > 0) {
|
||||
if (!o.volatilityPerHour || !(o.volatilityPerHour > 0)) return null;
|
||||
const financing = (o.financingPctPerDay / 100) * (o.targetHoldHours / 24);
|
||||
return (perNotional + financing) / o.volatilityPerHour;
|
||||
}
|
||||
return perNotional;
|
||||
if (!o.volatilityPerHour || !(o.volatilityPerHour > 0)) return null;
|
||||
const financing = (o.financingPctPerDay / 100) * (Math.max(0, o.targetHoldHours) / 24);
|
||||
return (o.spreadFraction + 2 * (o.commissionPct / 100) + financing) / o.volatilityPerHour;
|
||||
}
|
||||
|
||||
/** Most a spread may be, as a share of the mid price, for a candidate to trade. */
|
||||
@@ -252,7 +245,6 @@ export function candidateRejection(o: {
|
||||
quoteAgeMs: number | null;
|
||||
inHours: boolean;
|
||||
candles: number;
|
||||
needVolatility: boolean;
|
||||
minContractsOk: boolean;
|
||||
currency: string | undefined;
|
||||
accountCurrency: string;
|
||||
@@ -263,7 +255,7 @@ export function candidateRejection(o: {
|
||||
if (o.quoteAgeMs !== null && o.quoteAgeMs > MAX_QUOTE_AGE_MS) return `quote ${Math.round(o.quoteAgeMs / 60_000)}m old`;
|
||||
const mid = (o.bid + o.ask) / 2;
|
||||
if ((o.ask - o.bid) / mid > MAX_SPREAD_FRACTION) return `spread ${(((o.ask - o.bid) / mid) * 100).toFixed(2)}%`;
|
||||
if (o.needVolatility && o.candles < MIN_VOL_CANDLES + 1) return `only ${o.candles} hourly candles`;
|
||||
if (o.candles < MIN_VOL_CANDLES + 1) return `only ${o.candles} hourly candles`;
|
||||
if (!o.minContractsOk) return 'minimum order size out of reach';
|
||||
if (o.currency && o.currency !== o.accountCurrency) return `priced in ${o.currency}`;
|
||||
return null;
|
||||
|
||||
+6
-4
@@ -156,10 +156,9 @@ function sanitizeSymbolMap(raw: unknown): SymbolMap {
|
||||
if (typeof c !== 'object' || c === null) return [];
|
||||
const cand = c as Record<string, unknown>;
|
||||
if (typeof cand.symbol !== 'string' || !cand.symbol || cand.symbol.includes('|')) return [];
|
||||
const notional = nonNegative(cand.notionalPerContract);
|
||||
const pct = typeof cand.commissionPct === 'number' && Number.isFinite(cand.commissionPct) && cand.commissionPct >= 0
|
||||
? cand.commissionPct : undefined;
|
||||
return [{ symbol: cand.symbol, notionalPerContract: notional, ...(pct !== undefined ? { commissionPct: pct } : {}) }];
|
||||
return [{ symbol: cand.symbol, ...(pct !== undefined ? { commissionPct: pct } : {}) }];
|
||||
});
|
||||
if (candidates.length > 0) map[root.toUpperCase()] = candidates;
|
||||
}
|
||||
@@ -200,7 +199,10 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
|
||||
warnUnknownPlatform(firm.id, firm.name, platform);
|
||||
return;
|
||||
}
|
||||
const tradesEveryDay = firm.tradesEveryDay === true;
|
||||
// Percent commission and every-day trading are DXtrade settings;
|
||||
// futures firms keep measured per-contract fees and the CME calendar.
|
||||
const dx = platform === 'dxtrade';
|
||||
const tradesEveryDay = dx && firm.tradesEveryDay === true;
|
||||
if (!Array.isArray(firm.accountTypes)) throw new Error(`firm ${firm.name} is missing accountTypes`);
|
||||
|
||||
const accountTypes: CatalogAccountType[] = firm.accountTypes.map((t: unknown, j: number) => {
|
||||
@@ -246,7 +248,7 @@ function validatePayload(raw: unknown): FirmCatalogPayload {
|
||||
accountTypes,
|
||||
serverUrl: typeof firm.serverUrl === 'string' ? firm.serverUrl : '',
|
||||
loginDomain: typeof firm.loginDomain === 'string' ? firm.loginDomain : '',
|
||||
commissionPct: nonNegative(firm.commissionPct),
|
||||
commissionPct: dx ? nonNegative(firm.commissionPct) : 0,
|
||||
tradesEveryDay,
|
||||
symbolMap: sanitizeSymbolMap(firm.symbolMap),
|
||||
targetHoldHours: nonNegative(firm.targetHoldHours),
|
||||
|
||||
@@ -228,13 +228,12 @@ export function isFirmPlatform(value: unknown): value is FirmPlatform {
|
||||
return typeof value === 'string' && (FIRM_PLATFORMS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** One symbol a DXTrade firm may trade for a CME root. */
|
||||
/** 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
|
||||
* master's notionalPerContract is ignored). */
|
||||
export interface SymbolMapCandidate {
|
||||
/** The platform's symbol, e.g. "BTC/USD". */
|
||||
symbol: string;
|
||||
/** Fixed sizing: notional per $1,000 of leg (like futures' one contract
|
||||
* per $1,000). Used when volatility sizing is off or can't run. */
|
||||
notionalPerContract: number;
|
||||
/** Per-side commission % for this symbol; absent = the firm's. */
|
||||
commissionPct?: number;
|
||||
}
|
||||
@@ -263,8 +262,8 @@ export interface CatalogFirm {
|
||||
tradesEveryDay?: boolean;
|
||||
/** DXTrade: which symbols each CME root may trade as. */
|
||||
symbolMap?: SymbolMap;
|
||||
/** DXTrade volatility sizing: aim each leg's expected hold at this many
|
||||
* hours (0 = fixed sizing from the symbol map). */
|
||||
/** DXTrade sizing: aim each leg's expected hold at this many hours
|
||||
* (0 = DEFAULT_TARGET_HOLD_HOURS, 12). */
|
||||
targetHoldHours?: number;
|
||||
/** Overnight financing the firm charges, % of notional per day (0 = none). */
|
||||
financingPctPerDay?: number;
|
||||
|
||||
@@ -30,8 +30,6 @@ export interface PlatformContract {
|
||||
/** Notional of one engine contract at the current mid — present where the
|
||||
* platform prices positions, which is what enables notional sizing. */
|
||||
stepNotional?: number;
|
||||
/** Fixed sizing: notional per $1,000 of leg (the symbol map's setting). */
|
||||
fixedNotional?: number;
|
||||
/** σ of log returns per √hour, from recent candles. */
|
||||
volatilityPerHour?: number;
|
||||
/** (ask − bid) / mid when the contract was picked. */
|
||||
|
||||
+31
-38
@@ -1266,6 +1266,9 @@ export function botStopsActive(cfg: { firstDayStop: boolean; minDayStop: boolean
|
||||
|
||||
// ── position size ────────────────────────────────────────────────────────────
|
||||
|
||||
/** The hold a leg is sized for when the firm doesn't set one. */
|
||||
export const DEFAULT_TARGET_HOLD_HOURS = 12;
|
||||
|
||||
/**
|
||||
* How much notional a leg should carry, on platforms that price it (DXTrade).
|
||||
*
|
||||
@@ -1278,63 +1281,53 @@ export function botStopsActive(cfg: { firstDayStop: boolean; minDayStop: boolean
|
||||
* financing adds a floor: below √(f·A·B/(σ²·(2c + s))), a bigger position is
|
||||
* both cheaper and faster.
|
||||
*
|
||||
* Fixed mode (no target hold, no σ, or a stopless leg): the symbol map's
|
||||
* notional per $1,000 of leg, like futures' one contract per $1,000.
|
||||
* Returns null when neither mode can size the leg.
|
||||
* A stopless leg (no daily loss limit, no obfuscation stop) is sized as if
|
||||
* its stop sat as far away as its target. Returns null without a volatility.
|
||||
*/
|
||||
export function sizeNotional(o: {
|
||||
leg: number;
|
||||
/** Stop distance in dollars at a given notional (fees move with it). */
|
||||
stopDollarsAt: (notional: number) => number;
|
||||
volatilityPerHour?: number;
|
||||
/** 0 = DEFAULT_TARGET_HOLD_HOURS. */
|
||||
targetHoldHours: number;
|
||||
/** Per side, percent. */
|
||||
commissionPct: number;
|
||||
spreadFraction?: number;
|
||||
financingPctPerDay?: number;
|
||||
/** Notional per $1,000 of leg for fixed sizing; 0 = none. */
|
||||
fixedNotional: number;
|
||||
/** Hard cap on the position; 0 = none. */
|
||||
maxNotional: number;
|
||||
}): { notional: number; mode: 'volatility' | 'fixed'; predictedHoldHours: number | null; winProbability: number | null } | null {
|
||||
}): { notional: number; predictedHoldHours: number; winProbability: number | null } | null {
|
||||
const sigma = o.volatilityPerHour ?? 0;
|
||||
if (!(sigma > 0)) return null;
|
||||
const hold = o.targetHoldHours > 0 ? o.targetHoldHours : DEFAULT_TARGET_HOLD_HOURS;
|
||||
const c = Math.max(0, o.commissionPct) / 100;
|
||||
const s = Math.max(0, o.spreadFraction ?? 0);
|
||||
const sigma = o.volatilityPerHour ?? 0;
|
||||
const fixed = o.fixedNotional > 0 ? Math.max(1, Math.ceil(o.leg / 1000)) * o.fixedNotional : 0;
|
||||
const cap = (n: number) => (o.maxNotional > 0 ? Math.min(n, o.maxNotional) : n);
|
||||
const describe = (n: number) => {
|
||||
const fh = Math.max(0, o.financingPctPerDay ?? 0) / 100 / 24;
|
||||
const distances = (n: number) => {
|
||||
const a = o.leg + 2 * c * n;
|
||||
const b = o.stopDollarsAt(n);
|
||||
if (!(sigma > 0) || !Number.isFinite(b) || b <= 0 || n <= 0) {
|
||||
return { predictedHoldHours: null, winProbability: Number.isFinite(b) && b > 0 ? b / (a + b) : null };
|
||||
}
|
||||
return { predictedHoldHours: (a / n) * (b / n) / (sigma * sigma), winProbability: b / (a + b) };
|
||||
const stop = o.stopDollarsAt(n);
|
||||
const stopped = Number.isFinite(stop) && stop > 0;
|
||||
return { a, b: stopped ? stop : a, stopped };
|
||||
};
|
||||
|
||||
if (o.targetHoldHours > 0 && sigma > 0) {
|
||||
let n = fixed > 0 ? fixed : o.leg * 20;
|
||||
let ok = true;
|
||||
// Fees depend on N and the daily-loss stop depends on fees: a couple of
|
||||
// fixed-point passes settle it (the correction is second order).
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
const a = o.leg + 2 * c * n;
|
||||
const b = o.stopDollarsAt(n);
|
||||
if (!Number.isFinite(b) || b <= 0) { ok = false; break; }
|
||||
const nTime = Math.sqrt((a * b) / (sigma * sigma * o.targetHoldHours));
|
||||
const fh = Math.max(0, o.financingPctPerDay ?? 0) / 100 / 24;
|
||||
const nCost = fh > 0 ? Math.sqrt((fh * a * b) / (sigma * sigma * (2 * c + s))) : 0;
|
||||
n = Math.max(nTime, nCost);
|
||||
}
|
||||
if (ok && Number.isFinite(n) && n > 0) {
|
||||
const notional = cap(n);
|
||||
return { notional, mode: 'volatility', ...describe(notional) };
|
||||
}
|
||||
// Fees depend on N and the daily-loss stop depends on fees: a couple of
|
||||
// fixed-point passes settle it (the correction is second order).
|
||||
let n = o.leg * 20;
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
const { a, b } = distances(n);
|
||||
const nTime = Math.sqrt((a * b) / (sigma * sigma * hold));
|
||||
const nCost = fh > 0 ? Math.sqrt((fh * a * b) / (sigma * sigma * (2 * c + s))) : 0;
|
||||
n = Math.max(nTime, nCost);
|
||||
}
|
||||
if (fixed > 0) {
|
||||
const notional = cap(fixed);
|
||||
return { notional, mode: 'fixed', ...describe(notional) };
|
||||
}
|
||||
return null;
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
const notional = o.maxNotional > 0 ? Math.min(n, o.maxNotional) : n;
|
||||
const { a, b, stopped } = distances(notional);
|
||||
return {
|
||||
notional,
|
||||
predictedHoldHours: (a / notional) * (b / notional) / (sigma * sigma),
|
||||
winProbability: stopped ? b / (a + b) : null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── trading hours ────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user