1Branch0Tags
GL
glucryptoAdopt opentool 0.19.8
1f37f4912 days ago17Commits
typescript
import { resolveBacktestWindow, type BacktestResolution } from "opentool/backtest"; import { fetchHyperliquidBars, normalizeHyperliquidIndicatorBars, } from "opentool/adapters/hyperliquid"; import { normalizeMarketSymbol, type TwapBotConfig } from "../config"; import { resolutionToMinutes, resolveTwapBacktestResolution, resolveTwapScheduleMinutes, } from "./schedule"; import type { TradeSignal, TwapBacktestDecisionPoint, TwapBacktestDecisionSeriesResult, } from "./types"; type ActiveTwapWindow = { startMs: number; totalSize: number; }; function resolveBacktestSymbol(config: TwapBotConfig, symbolOverride?: string) { const normalizedOverride = normalizeMarketSymbol(symbolOverride); if (normalizedOverride) { return normalizedOverride; } return normalizeMarketSymbol(config.marketSymbol || config.asset); } function resolveFallbackCountBack(resolution: BacktestResolution) { if (resolution === "1") return 24 * 60; if (resolution === "5") return 24 * 12 * 14; if (resolution === "15") return 24 * 4 * 30; if (resolution === "30") return 24 * 2 * 45; if (resolution === "60") return 24 * 60; if (resolution === "240") return 6 * 120; return 365; } function resolveSignal(deltaSize: number): TradeSignal { const epsilon = 1e-9; if (deltaSize > epsilon) return "buy"; if (deltaSize < -epsilon) return "sell"; return "hold"; } export async function buildBacktestDecisionSeries(params: { config: TwapBotConfig; symbol?: string; timeframeStart?: string; timeframeEnd?: string; from?: number; to?: number; lookbackDays?: number; }): Promise<TwapBacktestDecisionSeriesResult> { const marketSymbol = resolveBacktestSymbol(params.config, params.symbol); const config: TwapBotConfig = { ...params.config, asset: params.config.asset, marketSymbol, }; const scheduleMinutes = resolveTwapScheduleMinutes(config); const twapMinutes = Math.max(1, Math.trunc(config.twap.minutes)); const resolution = resolveTwapBacktestResolution({ scheduleMinutes, twapMinutes, }); const window = resolveBacktestWindow({ fallbackCountBack: resolveFallbackCountBack(resolution), lookbackDays: params.lookbackDays, resolution, from: params.from, to: params.to, timeframeStart: params.timeframeStart, timeframeEnd: params.timeframeEnd, }); const resolvedFrom = window.fromSeconds; const resolvedTo = window.toSeconds; const rawBars = await fetchHyperliquidBars({ symbol: marketSymbol, resolution, countBack: window.countBack, ...(resolvedFrom != null && Number.isFinite(resolvedFrom) ? { fromSeconds: Math.max(0, Math.trunc(resolvedFrom)) } : {}), ...(resolvedTo != null && Number.isFinite(resolvedTo) ? { toSeconds: Math.max(0, Math.trunc(resolvedTo)) } : {}), }); const bars = normalizeHyperliquidIndicatorBars(rawBars); if (bars.length === 0) { throw new Error("No TWAP price data returned."); } const barStepMs = resolutionToMinutes(resolution) * 60 * 1000; const scheduleIntervalMs = scheduleMinutes * 60 * 1000; const twapDurationMs = Math.max(barStepMs, twapMinutes * 60 * 1000); const mode = config.twap.side === "sell" ? "long-short" : "long-only"; const sideDirection = config.twap.side === "sell" ? -1 : 1; const decisions: TwapBacktestDecisionPoint[] = []; const activeWindows: ActiveTwapWindow[] = []; let completedTargetSize = 0; let previousTargetSize = 0; let lastStartMs: number | null = null; for (const bar of bars) { const barTimeMs = bar.time; if (lastStartMs == null || barTimeMs - lastStartMs >= scheduleIntervalMs) { const totalSize = (config.amountUsd / bar.close) * Math.abs(sideDirection); activeWindows.push({ startMs: barTimeMs, totalSize, }); lastStartMs = barTimeMs; } let inFlightTargetSize = 0; const nextWindows: ActiveTwapWindow[] = []; for (const windowEntry of activeWindows) { const progress = Math.max( 0, Math.min(1, (barTimeMs - windowEntry.startMs + barStepMs) / twapDurationMs), ); const contribution = windowEntry.totalSize * progress; inFlightTargetSize += contribution; if (progress >= 1) { completedTargetSize += windowEntry.totalSize; } else { nextWindows.push(windowEntry); } } activeWindows.length = 0; activeWindows.push(...nextWindows); const targetSize = completedTargetSize + inFlightTargetSize; const deltaSize = targetSize - previousTargetSize; const signal = resolveSignal(sideDirection * deltaSize); const budgetUsd = Math.abs(deltaSize) * bar.close; decisions.push({ ts: new Date(barTimeMs).toISOString(), price: bar.close, signal, targetSize, budgetUsd, indicator: "twap", indicators: { marketSymbol, side: config.twap.side, scheduleCron: config.schedule.cron, scheduleMinutes, twapMinutes, randomize: config.twap.randomize, activeWindowCount: activeWindows.length, }, }); previousTargetSize = targetSize; } return { symbol: marketSymbol.toUpperCase(), timeframeStart: resolvedFrom != null ? new Date(resolvedFrom * 1000).toISOString() : new Date(bars[0]!.time).toISOString(), timeframeEnd: resolvedTo != null ? new Date(resolvedTo * 1000).toISOString() : new Date(bars[bars.length - 1]!.time).toISOString(), barsEvaluated: bars.length, resolution, mode, indicator: "twap", decisions, }; }