openpondai/agents/hyperliquid-delta-neutral
OpenTool app
typescript
import {
fetchHyperliquidBars,
normalizeHyperliquidIndicatorBars,
} from "opentool/adapters/hyperliquid";
import type {
DeltaNeutralHistoricalFundingRatePoint,
NormalizedHistoricalFundingRatePoint,
} from "./types";
import { resolutionToMinutes } from "./schedule";
const MAX_BACKTEST_BARS_PER_FETCH = 400;
type HyperliquidBars = Awaited<ReturnType<typeof fetchHyperliquidBars>>;
type NormalizedBars = ReturnType<typeof normalizeHyperliquidIndicatorBars>;
export async function fetchHyperliquidBarsForBacktestWindow(params: {
symbol: string;
resolution: "60" | "240" | "1D";
countBack: number;
fromSeconds?: number;
toSeconds?: number;
}): Promise<HyperliquidBars> {
if (
params.fromSeconds == null ||
params.toSeconds == null ||
!Number.isFinite(params.fromSeconds) ||
!Number.isFinite(params.toSeconds) ||
params.toSeconds <= params.fromSeconds
) {
return fetchHyperliquidBars(params);
}
const resolutionSeconds = resolutionToMinutes(params.resolution) * 60;
const chunkSpanSeconds = resolutionSeconds * MAX_BACKTEST_BARS_PER_FETCH;
const merged = new Map<number, HyperliquidBars[number]>();
let chunkStart = Math.max(0, Math.trunc(params.fromSeconds));
const finalTo = Math.max(chunkStart + resolutionSeconds, Math.trunc(params.toSeconds));
while (chunkStart < finalTo) {
const chunkEnd = Math.min(finalTo, chunkStart + chunkSpanSeconds);
const chunkCountBack = Math.max(
1,
Math.ceil((chunkEnd - chunkStart) / resolutionSeconds) + 5,
);
const bars = await fetchHyperliquidBars({
symbol: params.symbol,
resolution: params.resolution,
countBack: chunkCountBack,
fromSeconds: chunkStart,
toSeconds: chunkEnd,
});
for (const bar of bars) {
merged.set(bar.time, bar);
}
chunkStart = chunkEnd;
}
return Array.from(merged.values()).sort((a, b) => a.time - b.time);
}
function buildBacktestResolutionCandidates(
preferredResolution: "60" | "240" | "1D",
): Array<"60" | "240" | "1D"> {
const candidates: Array<"60" | "240" | "1D"> = [preferredResolution];
if (!candidates.includes("240")) candidates.push("240");
if (!candidates.includes("1D")) candidates.push("1D");
return candidates;
}
export function computeWindowCountBack(params: {
fallbackCountBack: number;
resolution: "60" | "240" | "1D";
fromSeconds?: number;
toSeconds?: number;
}): number {
if (
params.fromSeconds != null &&
params.toSeconds != null &&
Number.isFinite(params.fromSeconds) &&
Number.isFinite(params.toSeconds) &&
params.toSeconds > params.fromSeconds
) {
const resolutionSeconds = resolutionToMinutes(params.resolution) * 60;
return Math.max(
50,
Math.ceil((params.toSeconds - params.fromSeconds) / resolutionSeconds) + 5,
);
}
return params.fallbackCountBack;
}
export async function fetchPerpBarsWithResolutionFallback(params: {
symbol: string;
preferredResolution: "60" | "240" | "1D";
fallbackCountBack: number;
fromSeconds?: number;
toSeconds?: number;
}) {
const candidates = buildBacktestResolutionCandidates(params.preferredResolution);
let lastBars: HyperliquidBars = [];
for (const resolution of candidates) {
const bars = await fetchHyperliquidBarsForBacktestWindow({
symbol: params.symbol,
resolution,
countBack: computeWindowCountBack({
fallbackCountBack: params.fallbackCountBack,
resolution,
fromSeconds: params.fromSeconds,
toSeconds: params.toSeconds,
}),
...(params.fromSeconds != null ? { fromSeconds: params.fromSeconds } : {}),
...(params.toSeconds != null ? { toSeconds: params.toSeconds } : {}),
});
lastBars = bars;
if (bars.length === 0) continue;
if (params.fromSeconds == null) {
return { bars, resolution };
}
const earliestBarSeconds = Math.trunc((bars[0]?.time ?? 0) / 1000);
const toleranceSeconds = resolutionToMinutes(resolution) * 60 * 2;
if (earliestBarSeconds <= params.fromSeconds + toleranceSeconds) {
return { bars, resolution };
}
}
return { bars: lastBars, resolution: candidates[candidates.length - 1]! };
}
export function mergeSpotBarsWithPerpProxy(perpBars: NormalizedBars, spotBars: NormalizedBars) {
if (perpBars.length === 0) return { bars: spotBars, proxyBarsUsed: 0 };
if (spotBars.length === 0) {
return { bars: perpBars, proxyBarsUsed: perpBars.length };
}
const firstSpotTime = spotBars[0]?.time ?? Number.POSITIVE_INFINITY;
const merged = new Map<number, NormalizedBars[number]>();
let proxyBarsUsed = 0;
for (const perpBar of perpBars) {
if (perpBar.time < firstSpotTime) {
merged.set(perpBar.time, perpBar);
proxyBarsUsed += 1;
}
}
for (const spotBar of spotBars) {
merged.set(spotBar.time, spotBar);
}
return {
bars: Array.from(merged.values()).sort((a, b) => a.time - b.time),
proxyBarsUsed,
};
}
export function normalizeHistoricalFundingRates(
points: DeltaNeutralHistoricalFundingRatePoint[] | undefined,
): NormalizedHistoricalFundingRatePoint[] {
if (!Array.isArray(points) || points.length === 0) return [];
return points
.map((point) => {
if (!point || typeof point.time !== "string") return null;
const tsMs = Date.parse(point.time);
if (Number.isNaN(tsMs) || !Number.isFinite(point.rateBps)) return null;
return {
tsMs,
time: new Date(tsMs).toISOString(),
rateBps: point.rateBps,
};
})
.filter((point): point is NormalizedHistoricalFundingRatePoint => Boolean(point))
.sort((a, b) => a.tsMs - b.tsMs);
}