2Branches0Tags
GL
glucryptoSync production with master template updates
dc969c112 days ago54Commits
typescript
import { resolveHyperliquidTotalFeeRate } from "./planner"; import { computePeriodsPerYear } from "./schedule"; import type { DeltaNeutralBacktestReplayArtifacts, DeltaNeutralBacktestState, ResolvedPlannedOrder, } from "./types"; function calculateStdDev(values: number[]): number | null { if (values.length < 2) return null; const mean = values.reduce((sum, value) => sum + value, 0) / values.length; const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (values.length - 1); if (!Number.isFinite(variance) || variance < 0) return null; return Math.sqrt(variance); } export function applySpotExecution(params: { state: DeltaNeutralBacktestState; order: ResolvedPlannedOrder; ts: Date; }) { const nextState: DeltaNeutralBacktestState = { ...params.state }; const notionalUsd = params.order.quantity * params.order.fillPrice; const { exchangeFeeRate, builderFeeRate, totalFeeRate } = resolveHyperliquidTotalFeeRate({ kind: "spot", side: params.order.side, }); const feeUsd = notionalUsd * totalFeeRate; const exchangeFeeUsd = notionalUsd * exchangeFeeRate; const builderFeeUsd = notionalUsd * builderFeeRate; let realizedPnlUsd = 0; let holdingMinutes: number | null = null; if (params.order.side === "buy") { nextState.cashUsd -= notionalUsd + feeUsd; nextState.spotBalance += params.order.quantity; nextState.spotCostBasisUsd += notionalUsd; if (!nextState.spotOpenedAt) { nextState.spotOpenedAt = params.ts; } } else { const quantity = Math.min(params.order.quantity, Math.max(nextState.spotBalance, 0)); const avgCost = nextState.spotBalance > 0 ? nextState.spotCostBasisUsd / nextState.spotBalance : params.order.fillPrice; const costBasis = avgCost * quantity; realizedPnlUsd = quantity * params.order.fillPrice - costBasis; nextState.cashUsd += quantity * params.order.fillPrice - feeUsd; nextState.spotBalance = Math.max(0, nextState.spotBalance - quantity); nextState.spotCostBasisUsd = Math.max(0, nextState.spotCostBasisUsd - costBasis); if (nextState.spotBalance <= 0) { if (nextState.spotOpenedAt) { holdingMinutes = Math.max( 0, (params.ts.getTime() - nextState.spotOpenedAt.getTime()) / 60_000, ); } nextState.spotBalance = 0; nextState.spotCostBasisUsd = 0; nextState.spotOpenedAt = null; } } return { nextState, notionalUsd, feeUsd, exchangeFeeUsd, builderFeeUsd, realizedPnlUsd, holdingMinutes, slippageImpactUsd: Math.abs(params.order.fillPrice - params.order.referencePrice) * params.order.quantity, }; } export function applyPerpExecution(params: { state: DeltaNeutralBacktestState; order: ResolvedPlannedOrder; ts: Date; }) { const nextState: DeltaNeutralBacktestState = { ...params.state }; const notionalUsd = params.order.quantity * params.order.fillPrice; const { exchangeFeeRate, builderFeeRate, totalFeeRate } = resolveHyperliquidTotalFeeRate({ kind: "perp", side: params.order.side, }); const feeUsd = notionalUsd * totalFeeRate; const exchangeFeeUsd = notionalUsd * exchangeFeeRate; const builderFeeUsd = notionalUsd * builderFeeRate; const signedQuantity = params.order.side === "buy" ? params.order.quantity : -params.order.quantity; const currentSize = nextState.perpSize; let realizedPnlUsd = 0; let holdingMinutes: number | null = null; if (currentSize === 0 || Math.sign(currentSize) === Math.sign(signedQuantity)) { const absCurrent = Math.abs(currentSize); const absNext = Math.abs(currentSize + signedQuantity); const baseline = nextState.perpEntryPrice ?? params.order.fillPrice; nextState.perpEntryPrice = absNext > 0 ? (baseline * absCurrent + params.order.fillPrice * Math.abs(signedQuantity)) / absNext : null; nextState.perpSize = currentSize + signedQuantity; if (!nextState.perpOpenedAt && nextState.perpSize !== 0) { nextState.perpOpenedAt = params.ts; } } else { const closedQuantity = Math.min(Math.abs(currentSize), Math.abs(signedQuantity)); const entryPrice = nextState.perpEntryPrice ?? params.order.fillPrice; realizedPnlUsd = currentSize > 0 ? (params.order.fillPrice - entryPrice) * closedQuantity : (entryPrice - params.order.fillPrice) * closedQuantity; const remainingQuantity = Math.abs(signedQuantity) - closedQuantity; nextState.perpSize = currentSize + signedQuantity; if (remainingQuantity <= 0) { if (nextState.perpSize === 0) { nextState.perpEntryPrice = null; if (nextState.perpOpenedAt) { holdingMinutes = Math.max( 0, (params.ts.getTime() - nextState.perpOpenedAt.getTime()) / 60_000, ); } nextState.perpOpenedAt = null; } } else { nextState.perpEntryPrice = params.order.fillPrice; if (nextState.perpOpenedAt) { holdingMinutes = Math.max( 0, (params.ts.getTime() - nextState.perpOpenedAt.getTime()) / 60_000, ); } nextState.perpOpenedAt = params.ts; } } if (nextState.perpSize === 0) { nextState.perpEntryPrice = null; nextState.perpOpenedAt = null; } nextState.cashUsd += realizedPnlUsd - feeUsd; return { nextState, notionalUsd, feeUsd, exchangeFeeUsd, builderFeeUsd, realizedPnlUsd, holdingMinutes, slippageImpactUsd: Math.abs(params.order.fillPrice - params.order.referencePrice) * params.order.quantity, }; } export function buildBacktestMetrics(params: { resolution: string; initialEquityUsd: number; equityPoints: DeltaNeutralBacktestReplayArtifacts["equityPoints"]; realizedPnls: number[]; holdingMinutes: number[]; turnoverUsd: number; totalFeesUsd: number; totalExchangeFeesUsd: number; totalBuilderFeesUsd: number; totalSlippageImpactUsd: number; tradeCount: number; cumulativeFundingUsd: number; fundingPointsApplied: number; spotProxyBarsUsed?: number; }): DeltaNeutralBacktestReplayArtifacts["metrics"] { const finalEquity = params.equityPoints[params.equityPoints.length - 1]?.equityUsd ?? params.initialEquityUsd; const totalReturnPct = params.initialEquityUsd > 0 ? ((finalEquity - params.initialEquityUsd) / params.initialEquityUsd) * 100 : null; const maxDrawdownPct = params.equityPoints.reduce((max, point) => { const drawdown = point.drawdownPct ?? 0; return drawdown > max ? drawdown : max; }, 0); const periodicReturns: number[] = []; for (let i = 1; i < params.equityPoints.length; i += 1) { const previous = params.equityPoints[i - 1]?.equityUsd ?? 0; const current = params.equityPoints[i]?.equityUsd ?? 0; if (previous > 0) { periodicReturns.push((current - previous) / previous); } } const periodsPerYear = computePeriodsPerYear(params.resolution); const returnsStd = calculateStdDev(periodicReturns); const avgReturn = periodicReturns.length > 0 ? periodicReturns.reduce((sum, value) => sum + value, 0) / periodicReturns.length : null; const downsideReturns = periodicReturns.filter((value) => value < 0); const downsideStd = calculateStdDev(downsideReturns); const annualizedReturnPct = params.initialEquityUsd > 0 && params.equityPoints.length > 1 ? (Math.pow( finalEquity / params.initialEquityUsd, periodsPerYear / (params.equityPoints.length - 1), ) - 1) * 100 : null; const volatilityPct = returnsStd != null ? returnsStd * Math.sqrt(periodsPerYear) * 100 : null; const sharpe = avgReturn != null && returnsStd != null && returnsStd > 0 ? (avgReturn / returnsStd) * Math.sqrt(periodsPerYear) : null; const sortino = avgReturn != null && downsideStd != null && downsideStd > 0 ? (avgReturn / downsideStd) * Math.sqrt(periodsPerYear) : null; const grossProfit = params.realizedPnls .filter((value) => value > 0) .reduce((sum, value) => sum + value, 0); const grossLoss = params.realizedPnls .filter((value) => value < 0) .reduce((sum, value) => sum + value, 0); const winRatePct = params.realizedPnls.length > 0 ? (params.realizedPnls.filter((value) => value > 0).length / params.realizedPnls.length) * 100 : null; const profitFactor = grossLoss < 0 ? grossProfit / Math.abs(grossLoss) : null; const expectancyUsd = params.realizedPnls.length > 0 ? params.realizedPnls.reduce((sum, value) => sum + value, 0) / params.realizedPnls.length : null; const avgHoldingMinutes = params.holdingMinutes.length > 0 ? params.holdingMinutes.reduce((sum, value) => sum + value, 0) / params.holdingMinutes.length : null; return { totalReturnPct, annualizedReturnPct, maxDrawdownPct, volatilityPct, sharpe, sortino, winRatePct, profitFactor, expectancyUsd, turnoverUsd: params.turnoverUsd, avgHoldingMinutes, totalFeesUsd: params.totalFeesUsd, slippageImpactUsd: params.totalSlippageImpactUsd, tradeCount: params.tradeCount, metadata: { metricMode: "delta-neutral-replay", includesFundingPnl: params.fundingPointsApplied > 0, totalFundingPnlUsd: params.cumulativeFundingUsd, fundingPointsApplied: params.fundingPointsApplied, totalExchangeFeesUsd: params.totalExchangeFeesUsd, totalBuilderFeesUsd: params.totalBuilderFeesUsd, ...(typeof params.spotProxyBarsUsed === "number" ? { spotProxyBarsUsed: params.spotProxyBarsUsed } : {}), replaySource: "template-shared-planner", }, }; }