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
@@ -104,6 +104,9 @@ A DXtrade firm is set up on the master's **Firms** page like any other, plus:
|
||||
|
||||
- **Server URL** (`https://dx.tradeify247.co/dxsca-web`) and **login domain**
|
||||
(usually `default`). Changing the URL re-keys the firm's accounts.
|
||||
These settings only exist for DXtrade firms; futures firms keep their measured
|
||||
per-contract fees and the CME calendar.
|
||||
|
||||
- **Commission %** of notional per side (Tradeify: `0.04`). The engine prices
|
||||
every fee off the fill with it.
|
||||
- **Trades every day** — weekends and CME holidays included. Such a firm runs
|
||||
@@ -114,9 +117,9 @@ A DXtrade firm is set up on the master's **Firms** page like any other, plus:
|
||||
covers every root (drain and extra-day legs trade `MNQ`, so map it or use
|
||||
`*`). When a root lists several, the cheapest tradable one right now wins:
|
||||
spread plus both commissions, divided by its recent hourly volatility.
|
||||
- **Target hold hours** — size each leg's notional so its expected hold is
|
||||
about this long (0 = the symbol map's fixed notional per $1,000 of leg), and
|
||||
**financing %/day** if the firm charges swap.
|
||||
- **Target hold hours** — each leg's notional is sized from the instrument's
|
||||
recent volatility so its expected hold is about this long (blank = 12 h),
|
||||
and **financing %/day** if the firm charges swap.
|
||||
|
||||
Per account type: **Daily loss limit** (Tradeify: 3% of size — every leg then
|
||||
carries a stop that keeps the firm's day inside it, less a 10% reserve and
|
||||
|
||||
+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,
|
||||
|
||||
+9
-17
@@ -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;
|
||||
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. */
|
||||
|
||||
+26
-33
@@ -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).
|
||||
let n = o.leg * 20;
|
||||
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 { 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 (ok && Number.isFinite(n) && n > 0) {
|
||||
const notional = cap(n);
|
||||
return { notional, mode: 'volatility', ...describe(notional) };
|
||||
}
|
||||
}
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -10,7 +10,7 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
percentFee, takeProfitPlan, obfuscationStopDollars, dllRoom, dllStopDollars, dllViable, stopTicks,
|
||||
botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine,
|
||||
botStopsActive, sizeNotional, isNoTradeTime, isPastSessionClose, nextTradingDate, effectiveCombine, DEFAULT_TARGET_HOLD_HOURS,
|
||||
projectPerfectPath, cycleCompletion, DLL_RESERVE_FRACTION, DLL_MIN_STOP,
|
||||
} from '../lib/trading-logic.ts';
|
||||
|
||||
@@ -258,18 +258,17 @@ const btcSigma = 0.03 / Math.sqrt(24); // 3%/day
|
||||
|
||||
test('volatility sizing hits the target hold, and the example table', () => {
|
||||
const stop = () => 1350;
|
||||
const at = (hours) => sizeNotional({ leg: 600, stopDollarsAt: stop, volatilityPerHour: btcSigma, targetHoldHours: hours, commissionPct: 0, fixedNotional: 0, maxNotional: 0 });
|
||||
const at = (hours) => sizeNotional({ leg: 600, stopDollarsAt: stop, volatilityPerHour: btcSigma, targetHoldHours: hours, commissionPct: 0, maxNotional: 0 });
|
||||
for (const [hours, notional] of [[54, 20000], [24, 30000], [6, 60000]]) {
|
||||
const s = at(hours);
|
||||
assert.equal(s.mode, 'volatility');
|
||||
assert.ok(Math.abs(s.notional - notional) / notional < 0.001, `${hours}h → ${s.notional}`);
|
||||
assert.ok(Math.abs(s.predictedHoldHours - hours) / hours < 0.001);
|
||||
assert.ok(Math.abs(s.winProbability - 1350 / 1950) < 1e-9);
|
||||
}
|
||||
});
|
||||
|
||||
test('sizing: fees feed back, the cap binds, financing only raises N, fixed fallback', () => {
|
||||
const base = { leg: 600, volatilityPerHour: btcSigma, targetHoldHours: 6, commissionPct: 0.04, spreadFraction: 0.0002, fixedNotional: 20000, maxNotional: 0 };
|
||||
test('sizing: fees feed back, the cap binds, financing only raises N, defaults', () => {
|
||||
const base = { leg: 600, volatilityPerHour: btcSigma, targetHoldHours: 6, commissionPct: 0.04, spreadFraction: 0.0002, maxNotional: 0 };
|
||||
// Daily-loss stop shrinks as fees grow with N.
|
||||
const stopDollarsAt = (n) => dllStopDollars({ room: 1500, dailyLossLimit: 1500, entryFee: 0.0004 * n, commissionPct: 0.04 });
|
||||
const s = sizeNotional({ ...base, stopDollarsAt });
|
||||
@@ -286,11 +285,15 @@ test('sizing: fees feed back, the cap binds, financing only raises N, fixed fall
|
||||
const unfinanced = sizeNotional({ ...base, stopDollarsAt, targetHoldHours: 200 });
|
||||
assert.ok(financed.notional > unfinanced.notional);
|
||||
|
||||
const fixed = sizeNotional({ ...base, stopDollarsAt: () => Infinity, leg: 1700 });
|
||||
assert.equal(fixed.mode, 'fixed');
|
||||
assert.equal(fixed.notional, 40000);
|
||||
assert.equal(sizeNotional({ ...base, stopDollarsAt: () => Infinity, fixedNotional: 0 }), null);
|
||||
assert.equal(sizeNotional({ ...base, targetHoldHours: 0, stopDollarsAt }).mode, 'fixed');
|
||||
// No target hold = 12 h.
|
||||
const dflt = sizeNotional({ ...base, targetHoldHours: 0, stopDollarsAt });
|
||||
assert.ok(Math.abs(dflt.predictedHoldHours - DEFAULT_TARGET_HOLD_HOURS) < 0.1, `hold ${dflt.predictedHoldHours}`);
|
||||
// A stopless leg is sized as if the stop sat as far as the target.
|
||||
const stopless = sizeNotional({ ...base, stopDollarsAt: () => Infinity });
|
||||
assert.ok(Math.abs(stopless.predictedHoldHours - 6) < 0.05);
|
||||
assert.equal(stopless.winProbability, null);
|
||||
// No volatility, no size.
|
||||
assert.equal(sizeNotional({ ...base, volatilityPerHour: undefined, stopDollarsAt }), null);
|
||||
});
|
||||
|
||||
test('random-walk simulation agrees with the predicted hold and win rate', () => {
|
||||
@@ -461,15 +464,16 @@ test('the selector ranks the plan example: ETH, BTC, XYZ100, AAPL', () => {
|
||||
const k = Math.sqrt((600 * 1350) / 6);
|
||||
assert.ok(Math.abs(score(0.0003, 0.04) * k - 49.6) < 1);
|
||||
assert.ok(Math.abs(score(0.0002, 0.03) * k - 60) < 1);
|
||||
// Fixed sizing ignores volatility; volatility sizing needs it.
|
||||
assert.equal(scoreCandidate({ spreadFraction: 0.0002, commissionPct: 0.04, financingPctPerDay: 0, targetHoldHours: 0, volatilityPerHour: null }), 0.001);
|
||||
// No volatility, no score; financing over the hold adds to the cost.
|
||||
assert.equal(scoreCandidate({ spreadFraction: 0.0002, commissionPct: 0.04, financingPctPerDay: 0, targetHoldHours: 6, volatilityPerHour: null }), null);
|
||||
const withFinancing = scoreCandidate({ spreadFraction: 0.0002, commissionPct: 0.04, financingPctPerDay: 0.033, targetHoldHours: 12, volatilityPerHour: perHour(0.03) });
|
||||
assert.ok(withFinancing > score(0.0002, 0.03));
|
||||
});
|
||||
|
||||
test('the selector drops what cannot trade right now', () => {
|
||||
const ok = {
|
||||
tradingStatus: 'FULL', bid: 100, ask: 100.02, quoteAgeMs: 1000, inHours: true, candles: 49,
|
||||
needVolatility: true, minContractsOk: true, currency: 'USD', accountCurrency: 'USD',
|
||||
minContractsOk: true, currency: 'USD', accountCurrency: 'USD',
|
||||
};
|
||||
assert.equal(candidateRejection(ok), null);
|
||||
assert.match(candidateRejection({ ...ok, tradingStatus: 'CLOSE_ONLY' }), /CLOSE_ONLY/);
|
||||
@@ -478,6 +482,5 @@ test('the selector drops what cannot trade right now', () => {
|
||||
assert.match(candidateRejection({ ...ok, quoteAgeMs: 10 * 60_000 }), /old/);
|
||||
assert.match(candidateRejection({ ...ok, ask: 101 }), /spread/);
|
||||
assert.match(candidateRejection({ ...ok, candles: 10 }), /candles/);
|
||||
assert.equal(candidateRejection({ ...ok, candles: 10, needVolatility: false }), null);
|
||||
assert.match(candidateRejection({ ...ok, currency: 'EUR' }), /EUR/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user