openpondai/agents/hyperliquid-delta-neutral
OpenTool app
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",
},
};
}