2Branches0Tags
GL
glucryptoSync production with master template updates
dc969c112 days ago54Commits
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); }