3Branches0Tags
GL
glucryptoSync production with master template updates
4d3cf1112 days ago23Commits
typescript
import { resolveBacktestAccountValueUsd, resolveBacktestWindow } from "opentool/backtest"; import { fetchHyperliquidBars, normalizeHyperliquidBaseSymbol, normalizeHyperliquidDcaEntries, normalizeHyperliquidIndicatorBars, resolveHyperliquidBudgetUsd, } from "opentool/adapters/hyperliquid"; import { DEFAULT_ASSET, type DcaAgentConfig, } from "../config"; import { resolveDcaScheduleSeconds } from "./backtest-schedule"; import type { DcaBacktestDecisionPoint, DcaBacktestDecisionSeriesResult } from "./types"; function resolveBacktestSymbolConfig(params: { config: DcaAgentConfig; symbolOverride?: string; }) { const normalizedOverride = params.symbolOverride?.trim().toUpperCase(); if (!normalizedOverride) { return params.config; } return { ...params.config, asset: normalizedOverride, dca: { preset: normalizedOverride, symbols: [{ symbol: normalizedOverride, weight: 1 }], slippageBps: params.config.dca?.slippageBps ?? 50, }, } satisfies DcaAgentConfig; } export async function buildBacktestDecisionSeries(params: { config: DcaAgentConfig; symbol?: string; timeframeStart?: string; timeframeEnd?: string; from?: number; to?: number; lookbackDays?: number; accountValueUsd?: number; }): Promise<DcaBacktestDecisionSeriesResult> { const config = resolveBacktestSymbolConfig({ config: params.config, symbolOverride: params.symbol, }); const entries = normalizeHyperliquidDcaEntries({ entries: config.dca?.symbols, fallbackSymbol: config.asset ?? DEFAULT_ASSET, }); const primaryEntry = entries[0]; if (!primaryEntry) { throw new Error("No DCA symbols configured."); } const window = resolveBacktestWindow({ fallbackCountBack: config.countBack, lookbackDays: params.lookbackDays, resolution: config.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: primaryEntry.symbol, resolution: config.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 price data returned."); } const requestedAccountValue = resolveBacktestAccountValueUsd(params.accountValueUsd); const accountValue = requestedAccountValue ?? null; const scheduleIntervalSeconds = resolveDcaScheduleSeconds(config); const totalBudgetUsd = resolveHyperliquidBudgetUsd({ config, accountValue }); const symbolBudgetUsd = totalBudgetUsd * primaryEntry.normalizedWeight; let cumulativeTargetSize = 0; let lastBuySeconds: number | null = null; const decisions: DcaBacktestDecisionPoint[] = []; for (const bar of bars) { const barSeconds = Math.trunc(bar.time / 1000); const shouldBuy = lastBuySeconds == null || barSeconds - lastBuySeconds >= scheduleIntervalSeconds; if (shouldBuy && Number.isFinite(symbolBudgetUsd) && symbolBudgetUsd > 0 && bar.close > 0) { cumulativeTargetSize += symbolBudgetUsd / bar.close; lastBuySeconds = barSeconds; } decisions.push({ ts: new Date(bar.time).toISOString(), price: bar.close, signal: shouldBuy ? "buy" : "hold", targetSize: cumulativeTargetSize, budgetUsd: shouldBuy ? symbolBudgetUsd : 0, indicator: "dca", indicators: { scheduleCron: config.schedule.cron, allocationMode: config.allocationMode, asset: primaryEntry.symbol, weight: primaryEntry.weight, }, }); } return { symbol: (normalizeHyperliquidBaseSymbol(primaryEntry.symbol) ?? primaryEntry.symbol).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: config.resolution, mode: "long-only", indicator: "dca", decisions, }; }