DXtrade: count closed-trade P&L, week-long volatility, broker leverage caps, swap in the take-profit
- Transfers: a close books its SETTLEMENT and COMMISSION under one transaction code, and keying by the code alone let the commission overwrite the P&L. Keys now carry the type, so the day-start balance, daily-loss room, realized P&L, daily history and books check all see closed-trade profit and loss. - Volatility from a week of hourly closes (was 48 h), still measured once per trade date, so one quiet weekend can't triple a hold estimate. - Each symbol's margin rate from the account's instrument details caps the position at available funds / rate (Tradeify: 5:1 crypto, 2:1 stocks); the account type's max notional stays as an optional ceiling. - Firms with overnight financing add the swap a winning leg is expected to pay (the winners' mean hold, (A²+2AB)/3σ²) to its take-profit, and the sizing and hold estimate count it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
675b4c89f8
commit
a2d9df5ba4
+29
-11
@@ -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,
|
||||
notionalCap, expectedSwapToWin,
|
||||
DEFAULT_TARGET_HOLD_HOURS,
|
||||
isNoTradeTime, isPastSessionClose,
|
||||
BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY, POINT_VALUES,
|
||||
@@ -968,10 +969,13 @@ async function flattenAndFail(
|
||||
* Size: futures trade one contract per $1,000 of leg; a platform that prices
|
||||
* 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.
|
||||
* volatility — under the account type's notional cap and the instrument's
|
||||
* margin rate (the firm's leverage).
|
||||
*
|
||||
* 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.
|
||||
* it is read off the realized-P&L change a second after it. A firm that
|
||||
* charges overnight swap has the swap a winning leg is expected to pay added
|
||||
* to its take-profit.
|
||||
*
|
||||
* Stop: the tighter of the obfuscation stop and the hard daily-loss stop.
|
||||
* With a daily loss limit, no position is left without one — no room for a
|
||||
@@ -1026,6 +1030,12 @@ async function fireLeg(
|
||||
|
||||
const stepNotional = legContract.stepNotional ?? 0;
|
||||
if (stepNotional > 0) {
|
||||
const liveCash = client.accountCashBalances[acc.id] ?? item.cash;
|
||||
const cap = notionalCap({
|
||||
maxNotional: cfg.maxNotional ?? 0,
|
||||
marginRate: legContract.marginRate,
|
||||
availableFunds: liveCash.availableFunds ?? liveCash.amount,
|
||||
});
|
||||
const obfStop = Number.isFinite(obfuscation) && obfuscation > 0 ? obfuscation : Infinity;
|
||||
const stopDollarsAt = (notional: number) => Math.min(
|
||||
obfStop,
|
||||
@@ -1039,14 +1049,13 @@ async function fireLeg(
|
||||
commissionPct: pct,
|
||||
spreadFraction: legContract.spreadFraction,
|
||||
financingPctPerDay: firm.financingPctPerDay ?? 0,
|
||||
maxNotional: cfg.maxNotional ?? 0,
|
||||
maxNotional: cap,
|
||||
});
|
||||
if (!sizing) {
|
||||
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;
|
||||
if (maxNotional > 0) contracts = Math.min(contracts, Math.max(1, Math.floor(maxNotional / stepNotional)));
|
||||
if (cap > 0) contracts = Math.min(contracts, Math.max(1, Math.floor(cap / stepNotional)));
|
||||
if ((legContract.maxContracts ?? 0) > 0) contracts = Math.min(contracts, legContract.maxContracts!);
|
||||
contracts = Math.max(contracts, legContract.minContracts ?? 1);
|
||||
// The real size is known now: re-check the daily-loss room with it.
|
||||
@@ -1085,11 +1094,6 @@ async function fireLeg(
|
||||
measured = entryCommissionMeasured(item.cash.realizedPnL, updatedCash.realizedPnL, contracts);
|
||||
entryFee = measured ?? FALLBACK_COMMISSION_PER_CONTRACT * contracts;
|
||||
}
|
||||
const tp = takeProfitPlan({
|
||||
side: action, entry: fill.price, leg: legAmount, pointValue: pv, contracts,
|
||||
tickSize: legContract.tickSize, commissionPct: pct, entryCommission: entryFee,
|
||||
});
|
||||
|
||||
// The stop. On a measured-fee firm whose fee didn't show, the daily-loss
|
||||
// stop reserves the worst per-contract fee, not the $2.50 fallback.
|
||||
const dllFee = pct > 0 ? entryFee : (measured ?? MAX_COMMISSION_PER_CONTRACT * contracts);
|
||||
@@ -1100,6 +1104,20 @@ async function fireLeg(
|
||||
`daily-loss room ($${round2(item.dllRoom)}) doesn't cover a one-tick stop`);
|
||||
}
|
||||
|
||||
// The take-profit clears the leg, both fees and — where the firm charges
|
||||
// overnight swap — what a winning leg is expected to pay in it.
|
||||
const swapReserve = expectedSwapToWin({
|
||||
distance: legAmount + 2 * entryFee,
|
||||
stopDollars: ticks !== null ? ticks * legContract.tickSize * pv * contracts : Infinity,
|
||||
notional: fill.price * pv * contracts,
|
||||
volatilityPerHour: legContract.volatilityPerHour,
|
||||
financingPctPerDay: firm.financingPctPerDay ?? 0,
|
||||
});
|
||||
const tp = takeProfitPlan({
|
||||
side: action, entry: fill.price, leg: legAmount + swapReserve, pointValue: pv, contracts,
|
||||
tickSize: legContract.tickSize, commissionPct: pct, entryCommission: entryFee,
|
||||
});
|
||||
|
||||
let exitOrder: { orderId?: number };
|
||||
let stopPrice: number | null = null;
|
||||
if (ticks !== null) {
|
||||
@@ -1143,7 +1161,7 @@ async function fireLeg(
|
||||
? ` / stop @ ${stopPrice}`
|
||||
+ (dllOn ? ` (dll room $${round2(item.dllRoom)} → $${round2(dllDollars)}${Number.isFinite(obfuscation) && obfuscation > 0 ? `, obf $${round2(obfuscation)}` : ''})` : '')
|
||||
: '';
|
||||
console.log(`${tag} ${acc.name} (${item.firmName}) ${action} ${contracts}x${instrumentLabel} @ ${fill.price} | leg $${legAmount} of day $${item.plan.dayTarget} (remaining $${item.plan.remainingToday}) [${item.plan.path}${item.drain ? ' drain' : ''}] (+$${tp.totalCommission.toFixed(2)} comm)${sizeNote} | exit @ ${tp.exitPrice}${stopNote} (orderId=${exitOrder.orderId})`);
|
||||
console.log(`${tag} ${acc.name} (${item.firmName}) ${action} ${contracts}x${instrumentLabel} @ ${fill.price} | leg $${legAmount} of day $${item.plan.dayTarget} (remaining $${item.plan.remainingToday}) [${item.plan.path}${item.drain ? ' drain' : ''}] (+$${tp.totalCommission.toFixed(2)} comm${swapReserve > 0 ? `, +$${swapReserve.toFixed(2)} expected swap` : ''})${sizeNote} | exit @ ${tp.exitPrice}${stopNote} (orderId=${exitOrder.orderId})`);
|
||||
|
||||
return {
|
||||
account: acc.name,
|
||||
|
||||
+13
-40
@@ -44,8 +44,9 @@ import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-
|
||||
import {
|
||||
dxAccountId, accountDisplayName, accountCodeOf, encodeContractName, decodeContractName, roundToStep,
|
||||
lastUtcReset, ctTradeDayStart, timeOf, parseDurationMs, ciNum, isFundType, signedFundValue, newOrderCode,
|
||||
inTradingHours, realizedVolPerHour, scoreCandidate, candidateRejection,
|
||||
inTradingHours, realizedVolPerHour, scoreCandidate, candidateRejection, parseTransfers, marginRateOf,
|
||||
} from './dxtrade-helpers';
|
||||
import type { DxTxn } from './dxtrade-helpers';
|
||||
import { hyperliquidCoins, hyperliquidCoinFor, hyperliquidQuote, hyperliquidHourlyCloses } from './hyperliquid-feed';
|
||||
|
||||
// ── timings ──────────────────────────────────────────────────────────────────
|
||||
@@ -80,7 +81,9 @@ const PICK_TTL_MS = 45_000;
|
||||
const INSTRUMENT_TTL_MS = 60 * 60_000;
|
||||
const ACCOUNT_INSTRUMENT_TTL_MS = 10 * 60_000;
|
||||
const CANDLE_TTL_MS = 15 * 60_000;
|
||||
const CANDLE_HOURS = 48;
|
||||
/** Volatility lookback: a week, so one quiet weekend (or one busy day) can't
|
||||
* swing a multi-day hold estimate — the week's rhythm is priced in. */
|
||||
const CANDLE_HOURS = 7 * 24;
|
||||
/** History is fetched this far back at most (or from registration). */
|
||||
const HISTORY_MAX_DAYS = 180;
|
||||
/** Balance the books can't explain by this much holds the account. */
|
||||
@@ -129,15 +132,6 @@ interface DxOrder {
|
||||
rejectReason: string | undefined;
|
||||
}
|
||||
|
||||
interface DxTxn {
|
||||
key: string;
|
||||
account: string;
|
||||
type: string;
|
||||
value: number;
|
||||
time: number;
|
||||
orderCode: string | undefined;
|
||||
}
|
||||
|
||||
/** DXtrade answered with an error body. */
|
||||
class DxApiError extends Error {
|
||||
constructor(message: string, readonly status: number, readonly code: number | undefined) {
|
||||
@@ -191,32 +185,6 @@ function parseOrder(raw: unknown): DxOrder | null {
|
||||
};
|
||||
}
|
||||
|
||||
function parseTransfers(body: unknown): DxTxn[] {
|
||||
const transfers = (body as { cashTransfers?: unknown[] })?.cashTransfers ?? [];
|
||||
const out: DxTxn[] = [];
|
||||
for (const t of transfers) {
|
||||
const transfer = t as Record<string, unknown>;
|
||||
const txns = Array.isArray(transfer.cashTransactions) ? transfer.cashTransactions : [];
|
||||
for (const x of txns) {
|
||||
const tx = x as Record<string, unknown>;
|
||||
const account = accountCodeOf(tx.account) ?? accountCodeOf(transfer.account);
|
||||
const value = ciNum(tx, 'value');
|
||||
const time = timeOf(tx.transactionTime ?? transfer.transactionTime);
|
||||
if (!account || value === undefined || !Number.isFinite(time) || typeof tx.type !== 'string') continue;
|
||||
const code = typeof tx.transactionCode === 'string' ? tx.transactionCode : '';
|
||||
out.push({
|
||||
key: code || `${transfer.transferCode ?? ''}|${tx.type}|${time}|${value}|${tx.orderCode ?? ''}|${tx.tradeCode ?? ''}`,
|
||||
account,
|
||||
type: tx.type,
|
||||
value,
|
||||
time,
|
||||
orderCode: typeof tx.orderCode === 'string' ? tx.orderCode : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function chunks<T>(items: T[], size: number): T[][] {
|
||||
const out: T[][] = [];
|
||||
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
||||
@@ -249,7 +217,7 @@ export class DXTradeClient implements PlatformClient {
|
||||
private userPath: string | null = null;
|
||||
|
||||
public accountList: PlatformAccount[] = [];
|
||||
public accountCashBalances: { [accountId: number]: { amount: number; realizedPnL: number; equity?: number } } = {};
|
||||
public accountCashBalances: { [accountId: number]: { amount: number; realizedPnL: number; equity?: number; availableFunds?: number } } = {};
|
||||
public positions: {
|
||||
[accountId: number]: { contractId: string; netPos: number; netPrice: number; timestamp: Date };
|
||||
} = {};
|
||||
@@ -689,7 +657,9 @@ export class DXTradeClient implements PlatformClient {
|
||||
for (const t of map.values()) {
|
||||
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'), availableFunds: ciNum(m, 'availableFunds'),
|
||||
};
|
||||
// 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.
|
||||
@@ -1256,6 +1226,7 @@ export class DXTradeClient implements PlatformClient {
|
||||
: Math.floor((maxOrder.value * (ciNum(inst, 'lotSize') || 1)) / step);
|
||||
}
|
||||
const minContracts = minOrder > 0 ? Math.ceil(minOrder / step - 1e-9) : 1;
|
||||
const marginRate = marginRateOf(det);
|
||||
const rejection = !(tick > 0)
|
||||
? 'no price increment'
|
||||
: candidateRejection({
|
||||
@@ -1281,7 +1252,8 @@ export class DXTradeClient implements PlatformClient {
|
||||
board.push({
|
||||
symbol: cand.symbol,
|
||||
score,
|
||||
note: `spread ${(spread * 100).toFixed(3)}%${vol?.vol ? `, σ ${(vol.vol * Math.sqrt(24) * 100).toFixed(2)}%/day` : ''}`,
|
||||
note: `spread ${(spread * 100).toFixed(3)}%${vol?.vol ? `, σ ${(vol.vol * Math.sqrt(24) * 100).toFixed(2)}%/day` : ''}`
|
||||
+ (marginRate ? `, ${+(1 / marginRate).toFixed(1)}:1` : ''),
|
||||
contract: {
|
||||
id: cand.symbol,
|
||||
name: encodeContractName(cand.symbol, step),
|
||||
@@ -1293,6 +1265,7 @@ export class DXTradeClient implements PlatformClient {
|
||||
spreadFraction: spread,
|
||||
maxContracts,
|
||||
minContracts,
|
||||
marginRate,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,6 +155,65 @@ export function signedFundValue(type: string, value: number): number {
|
||||
return type === 'WITHDRAWAL' ? -Math.abs(value) : Math.abs(value);
|
||||
}
|
||||
|
||||
export interface DxTxn {
|
||||
key: string;
|
||||
account: string;
|
||||
type: string;
|
||||
value: number;
|
||||
time: number;
|
||||
orderCode: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The cash transactions in a /transfers answer, flattened. A close books its
|
||||
* SETTLEMENT (the trade's P&L) and its COMMISSION under one transaction code,
|
||||
* so the key carries the type — and a running count, should a transfer ever
|
||||
* hold two of one type — or one would overwrite the other.
|
||||
*/
|
||||
export function parseTransfers(body: unknown): DxTxn[] {
|
||||
const transfers = (body as { cashTransfers?: unknown[] })?.cashTransfers ?? [];
|
||||
const out: DxTxn[] = [];
|
||||
for (const t of transfers) {
|
||||
const transfer = t as Record<string, unknown>;
|
||||
const txns = Array.isArray(transfer.cashTransactions) ? transfer.cashTransactions : [];
|
||||
const seen = new Map<string, number>();
|
||||
for (const x of txns) {
|
||||
const tx = x as Record<string, unknown>;
|
||||
const account = accountCodeOf(tx.account) ?? accountCodeOf(transfer.account);
|
||||
const value = ciNum(tx, 'value');
|
||||
const time = timeOf(tx.transactionTime ?? transfer.transactionTime);
|
||||
if (!account || value === undefined || !Number.isFinite(time) || typeof tx.type !== 'string') continue;
|
||||
const code = typeof tx.transactionCode === 'string' ? tx.transactionCode : '';
|
||||
const base = code
|
||||
? `${code}|${tx.type}`
|
||||
: `${transfer.transferCode ?? ''}|${tx.type}|${time}|${value}|${tx.orderCode ?? ''}|${tx.tradeCode ?? ''}`;
|
||||
const n = seen.get(base) ?? 0;
|
||||
seen.set(base, n + 1);
|
||||
out.push({
|
||||
key: n === 0 ? base : `${base}|${n}`,
|
||||
account,
|
||||
type: tx.type,
|
||||
value,
|
||||
time,
|
||||
orderCode: typeof tx.orderCode === 'string' ? tx.orderCode : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The margin an account posts per dollar of notional on an instrument — its
|
||||
* leverage cap, inverted (0.2 = 5:1). A tiered rate reads as its steepest
|
||||
* tier. undefined when the details don't say.
|
||||
*/
|
||||
export function marginRateOf(details: unknown): number | undefined {
|
||||
const rate = (details as { marginRate?: { tiers?: { value?: unknown }[]; value?: unknown } } | undefined)?.marginRate;
|
||||
const values = [...(rate?.tiers ?? []).map((t) => Number(t?.value)), Number(rate?.value)]
|
||||
.filter((v) => Number.isFinite(v) && v > 0 && v <= 1);
|
||||
return values.length > 0 ? Math.max(...values) : undefined;
|
||||
}
|
||||
|
||||
/** A client order code: unique, ≤ 64 chars, and recognisably ours. */
|
||||
export function newOrderCode(tag: string): string {
|
||||
const rand = Math.random().toString(36).slice(2, 8);
|
||||
|
||||
@@ -38,6 +38,9 @@ export interface PlatformContract {
|
||||
maxContracts?: number;
|
||||
/** Smallest order the broker takes, in engine contracts. */
|
||||
minContracts?: number;
|
||||
/** Margin posted per dollar of notional (0.2 = 5:1 leverage), where the
|
||||
* broker says — caps a position at available funds ÷ this. */
|
||||
marginRate?: number;
|
||||
}
|
||||
|
||||
export interface PlatformClient {
|
||||
@@ -45,8 +48,9 @@ export interface PlatformClient {
|
||||
/** realizedPnL is today's. pendingRoll (Tradovate): the broker still shows
|
||||
* the ended day's record and today's share of it can't be told apart —
|
||||
* the account waits for the broker to roll it. equity: balance plus open
|
||||
* P&L, where the platform reports it. */
|
||||
accountCashBalances: { [accountId: number]: { amount: number; realizedPnL: number; pendingRoll?: boolean; equity?: number } };
|
||||
* P&L, where the platform reports it; availableFunds: what margin can
|
||||
* still draw on, where the platform reports it. */
|
||||
accountCashBalances: { [accountId: number]: { amount: number; realizedPnL: number; pendingRoll?: boolean; equity?: number; availableFunds?: number } };
|
||||
positions: {
|
||||
[accountId: number]: {
|
||||
contractId: number | string;
|
||||
|
||||
+83
-15
@@ -1269,17 +1269,80 @@ export function botStopsActive(cfg: { firstDayStop: boolean; minDayStop: boolean
|
||||
/** The hold a leg is sized for when the firm doesn't set one. */
|
||||
export const DEFAULT_TARGET_HOLD_HOURS = 12;
|
||||
|
||||
/** Share of the broker's margin headroom a new position may use: the rest
|
||||
* absorbs the price moving between sizing and the fill, and the entry fee. */
|
||||
export const MARGIN_HEADROOM = 0.97;
|
||||
|
||||
/**
|
||||
* The largest position an account may open: the account type's notional cap
|
||||
* and, where the broker states the instrument's margin rate, what the
|
||||
* account's available funds can margin (the firm's leverage — 5:1 on crypto,
|
||||
* 2:1 on stocks at Tradeify247). 0 = no cap.
|
||||
*/
|
||||
export function notionalCap(o: { maxNotional: number; marginRate?: number; availableFunds?: number }): number {
|
||||
const caps: number[] = [];
|
||||
if (o.maxNotional > 0) caps.push(o.maxNotional);
|
||||
if ((o.marginRate ?? 0) > 0 && Number.isFinite(o.availableFunds) && o.availableFunds! > 0) {
|
||||
caps.push((MARGIN_HEADROOM * o.availableFunds!) / o.marginRate!);
|
||||
}
|
||||
return caps.length > 0 ? Math.min(...caps) : 0;
|
||||
}
|
||||
|
||||
/** The swap reserve never grows past this share of the take-profit distance
|
||||
* — beyond it the leg is sized too small to hold at all. */
|
||||
const MAX_SWAP_RESERVE_FRACTION = 0.25;
|
||||
|
||||
/**
|
||||
* The overnight swap a winning leg is expected to pay before its take-profit
|
||||
* fills, so the take-profit can carry it and a win still clears the leg.
|
||||
*
|
||||
* Same driftless walk as sizeNotional, in dollars: σ$ = notional·σ per √hour,
|
||||
* the take-profit A away, the stop B away. Given it wins, the walk takes
|
||||
* (A² + 2AB)/(3σ$²) hours on average — longer than the unconditional hold,
|
||||
* since the quick endings are mostly stop-outs. The swap is charged at each
|
||||
* daily reset the position is held over, about one per 24 h held, on the
|
||||
* notional. A includes the reserve itself, so it is solved as a fixed point.
|
||||
* A stopless leg is taken as B = A.
|
||||
*
|
||||
* `distance` = the take-profit distance before swap: leg + round-trip fees.
|
||||
*/
|
||||
export function expectedSwapToWin(o: {
|
||||
distance: number;
|
||||
stopDollars: number;
|
||||
notional: number;
|
||||
volatilityPerHour?: number;
|
||||
financingPctPerDay?: number;
|
||||
}): number {
|
||||
const daily = Math.max(0, o.financingPctPerDay ?? 0) / 100;
|
||||
const sigma = o.volatilityPerHour ?? 0;
|
||||
if (!(daily > 0) || !(sigma > 0) || !(o.notional > 0) || !(o.distance > 0)) return 0;
|
||||
const variance = (o.notional * sigma) ** 2;
|
||||
const stopped = Number.isFinite(o.stopDollars) && o.stopDollars > 0;
|
||||
const cap = o.distance * MAX_SWAP_RESERVE_FRACTION;
|
||||
let swap = 0;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const a = o.distance + swap;
|
||||
const b = stopped ? o.stopDollars : a;
|
||||
const winHours = (a * a + 2 * a * b) / (3 * variance);
|
||||
const next = Math.min(cap, daily * o.notional * (winHours / 24));
|
||||
const done = Math.abs(next - swap) < 0.005;
|
||||
swap = next;
|
||||
if (done) break;
|
||||
}
|
||||
return Math.ceil(swap * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* How much notional a leg should carry, on platforms that price it (DXTrade).
|
||||
*
|
||||
* Model price as a driftless random walk with σ per √hour. With the
|
||||
* take-profit A dollars away (leg + fees) and the stop B dollars away, a
|
||||
* position of notional N wins B/(A+B) of the time whatever N is, its expected
|
||||
* hold is (A/N)(B/N)/σ² hours, and its expected P&L is minus fees and spread
|
||||
* — proportional to N. Smaller is cheaper, bigger is faster, so the knob is
|
||||
* the target hold T: N = √(A·B/(σ²·T)). A firm that charges overnight
|
||||
* financing adds a floor: below √(f·A·B/(σ²·(2c + s))), a bigger position is
|
||||
* both cheaper and faster.
|
||||
* take-profit A dollars away (leg + fees + expected swap) and the stop B
|
||||
* dollars away, a position of notional N wins B/(A+B) of the time whatever N
|
||||
* is, its expected hold is (A/N)(B/N)/σ² hours, and its expected P&L is minus
|
||||
* fees and spread — proportional to N. Smaller is cheaper, bigger is faster,
|
||||
* so the knob is the target hold T: N = √(A·B/(σ²·T)). A firm that charges
|
||||
* overnight financing adds a floor: below √(f·A·B/(σ²·(2c + s))), a bigger
|
||||
* position is both cheaper and faster.
|
||||
*
|
||||
* 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.
|
||||
@@ -1295,9 +1358,9 @@ export function sizeNotional(o: {
|
||||
commissionPct: number;
|
||||
spreadFraction?: number;
|
||||
financingPctPerDay?: number;
|
||||
/** Hard cap on the position; 0 = none. */
|
||||
/** Hard cap on the position (see notionalCap); 0 = none. */
|
||||
maxNotional: number;
|
||||
}): { notional: number; predictedHoldHours: number; winProbability: number | null } | null {
|
||||
}): { notional: number; predictedHoldHours: number; winProbability: number | null; swapReserve: number } | null {
|
||||
const sigma = o.volatilityPerHour ?? 0;
|
||||
if (!(sigma > 0)) return null;
|
||||
const hold = o.targetHoldHours > 0 ? o.targetHoldHours : DEFAULT_TARGET_HOLD_HOURS;
|
||||
@@ -1305,16 +1368,20 @@ export function sizeNotional(o: {
|
||||
const s = Math.max(0, o.spreadFraction ?? 0);
|
||||
const fh = Math.max(0, o.financingPctPerDay ?? 0) / 100 / 24;
|
||||
const distances = (n: number) => {
|
||||
const a = o.leg + 2 * c * n;
|
||||
const stop = o.stopDollarsAt(n);
|
||||
const stopped = Number.isFinite(stop) && stop > 0;
|
||||
return { a, b: stopped ? stop : a, stopped };
|
||||
const swap = expectedSwapToWin({
|
||||
distance: o.leg + 2 * c * n, stopDollars: stop, notional: n,
|
||||
volatilityPerHour: sigma, financingPctPerDay: o.financingPctPerDay,
|
||||
});
|
||||
const a = o.leg + 2 * c * n + swap;
|
||||
return { a, b: stopped ? stop : a, stopped, swap };
|
||||
};
|
||||
|
||||
// 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).
|
||||
// Fees and swap depend on N and the daily-loss stop depends on fees: a
|
||||
// few fixed-point passes settle it (the correction is second order).
|
||||
let n = o.leg * 20;
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
for (let pass = 0; pass < 4; 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;
|
||||
@@ -1322,11 +1389,12 @@ export function sizeNotional(o: {
|
||||
}
|
||||
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);
|
||||
const { a, b, stopped, swap } = distances(notional);
|
||||
return {
|
||||
notional,
|
||||
predictedHoldHours: (a / notional) * (b / notional) / (sigma * sigma),
|
||||
winProbability: stopped ? b / (a + b) : null,
|
||||
swapReserve: swap,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user