1Branch0Tags
GL
glucryptoRefresh package-lock for opentool 0.19.5
01acdef14 hours ago17Commits
typescript
import { buildBacktestDecisionSeriesInput, resolveBacktestAccountValueUsd, resolveBacktestWindow, type BacktestDecisionRequest, } from "opentool/backtest"; import { fetchHyperliquidBars, resolveHyperliquidTargetSize, } from "opentool/adapters/hyperliquid"; import { DEFAULT_EXECUTION_MODE, type NewsTradeSignal, type SimpleNewsBotBacktest, type SimpleNewsBotConfig } from "./config"; import { resolveExecutableMarketSymbol } from "./market-symbol"; import { buildBacktestCheckpoints, evaluateGate, fetchSignal, type SimpleNewsBotBacktestPoint } from "./news-core"; import { resolveNewsTradeDecision } from "./news-trade"; type BacktestDecisionPoint = { ts: string; price: number; signal: NewsTradeSignal; targetSize: number; budgetUsd: number; indicator: "news"; indicators: Record<string, unknown>; }; export type NewsBacktestDecisionSeriesResult = { symbol: string; timeframeStart: string; timeframeEnd: string; barsEvaluated: number; resolution: SimpleNewsBotConfig["resolution"]; mode: NonNullable<NonNullable<SimpleNewsBotConfig["execution"]>["mode"]>; indicator: "news"; decisions: BacktestDecisionPoint[]; }; const DEFAULT_BACKTEST_DECISION_STEP_HOURS = 24; const DEFAULT_BACKTEST_CANDIDATE_LIMIT = 2; // Preview backtests only need to prove the strategy tool can resolve a valid // decision path. Keep this to a single checkpoint so news+Polymarket replay // does not timeout during create/change preview validation. const DEFAULT_BACKTEST_DECISION_MAX_CHECKPOINTS = 1; function resolveBacktestConfig(config: SimpleNewsBotConfig): SimpleNewsBotConfig { const includePredictionMarkets = config.predictionMarketSignal != null ? config.includePredictionMarkets : false; if (config.mode === "proposition") { return { ...config, includePredictionMarkets, candidateLimit: Math.min(config.candidateLimit, DEFAULT_BACKTEST_CANDIDATE_LIMIT), }; } return { ...config, includePredictionMarkets, }; } export type SimpleNewsBotBacktestResult = { ok: true; mode: SimpleNewsBotConfig["mode"]; fetchedAt: string; backtest: { startDate: string; endDate: string; stepHours: number; points: SimpleNewsBotBacktestPoint[]; decisions: BacktestDecisionPoint[]; }; }; function findCheckpointPrice( bars: Array<{ time: number; close: number }>, checkpointMs: number, startIndex: number, ): { price: number; nextIndex: number } { let index = startIndex; while (index + 1 < bars.length && bars[index + 1]!.time <= checkpointMs) { index += 1; } const bar = bars[index] ?? bars[0]; if (!bar) { throw new Error("No price data returned."); } return { price: bar.close, nextIndex: index }; } export function downsampleBacktestCheckpoints( checkpoints: string[], maxCheckpoints: number, ): string[] { if (checkpoints.length <= maxCheckpoints) return checkpoints; if (maxCheckpoints <= 1) { const lastCheckpoint = checkpoints[checkpoints.length - 1]; return lastCheckpoint ? [lastCheckpoint] : []; } const selectedIndexes = new Set<number>(); for (let index = 0; index < maxCheckpoints; index += 1) { const ratio = index / (maxCheckpoints - 1); const scaledIndex = Math.round(ratio * (checkpoints.length - 1)); selectedIndexes.add(scaledIndex); } return [...selectedIndexes] .sort((left, right) => left - right) .map((index) => checkpoints[index]!) .filter(Boolean); } async function buildReplay(params: { config: SimpleNewsBotConfig; startDate: string; endDate: string; stepHours: number; accountValueUsd?: number; checkpoints?: string[]; }): Promise<SimpleNewsBotBacktestResult["backtest"]> { const { config, startDate, endDate, stepHours, accountValueUsd } = params; const executableSymbol = await resolveExecutableMarketSymbol(config); const window = resolveBacktestWindow({ fallbackCountBack: config.countBack, resolution: config.resolution, timeframeStart: startDate, timeframeEnd: endDate, }); const bars = await fetchHyperliquidBars({ symbol: executableSymbol, resolution: config.resolution, countBack: window.countBack, ...(window.fromSeconds != null ? { fromSeconds: window.fromSeconds } : {}), ...(window.toSeconds != null ? { toSeconds: window.toSeconds } : {}), }); if (bars.length === 0) { throw new Error("No price data returned."); } const checkpoints = params.checkpoints ?? buildBacktestCheckpoints({ startDate, endDate, stepHours }); const points: SimpleNewsBotBacktestPoint[] = []; const decisions: BacktestDecisionPoint[] = []; const execution = config.execution ?? {}; const resolvedAccountValue = resolveBacktestAccountValueUsd(accountValueUsd) ?? (config.allocationMode === "percent_equity" ? 10_000 : null); let barIndex = 0; for (const checkpoint of checkpoints) { const signal = await fetchSignal(config, checkpoint); const gate = evaluateGate(config, signal); const checkpointMs = new Date(checkpoint).getTime(); const { price, nextIndex } = findCheckpointPrice(bars, checkpointMs, barIndex); barIndex = nextIndex; const decision = resolveNewsTradeDecision(config, signal, gate); const { targetSize, budgetUsd } = resolveHyperliquidTargetSize({ config, execution, accountValue: resolvedAccountValue, currentPrice: price, }); points.push({ asOf: checkpoint, signal, gate, }); decisions.push({ ts: checkpoint, price, signal: decision.signal, targetSize, budgetUsd, indicator: "news", indicators: { confidence: decision.confidence, reason: decision.reason, blockedByGate: decision.blockedByGate, eventState: decision.eventState, propositionAnswer: decision.propositionAnswer, gate, signal, }, }); } return { startDate, endDate, stepHours, points, decisions, }; } export async function runSimpleNewsBotBacktest( config: SimpleNewsBotConfig, backtest: SimpleNewsBotBacktest, ): Promise<SimpleNewsBotBacktestResult> { const replay = await buildReplay({ config, startDate: backtest.startDate, endDate: backtest.endDate ?? new Date().toISOString().slice(0, 10), stepHours: backtest.stepHours ?? 24, }); return { ok: true, mode: config.mode, fetchedAt: new Date().toISOString(), backtest: replay, }; } export async function buildBacktestDecisionSeries(params: { config: SimpleNewsBotConfig; request: Partial<BacktestDecisionRequest>; }): Promise<NewsBacktestDecisionSeriesResult> { const input = buildBacktestDecisionSeriesInput(params.request); const window = resolveBacktestWindow({ fallbackCountBack: params.config.countBack, lookbackDays: input.lookbackDays, resolution: params.config.resolution, from: input.from, to: input.to, timeframeStart: input.timeframeStart, timeframeEnd: input.timeframeEnd, }); const effectiveConfig = resolveBacktestConfig( input.symbol?.trim() ? { ...params.config, asset: input.symbol.trim(), marketSymbol: input.symbol.trim(), } : params.config, ); const startDate = new Date( (window.fromSeconds ?? Math.floor(Date.now() / 1000) - 86400) * 1000, ).toISOString(); const endDate = new Date( (window.toSeconds ?? Math.floor(Date.now() / 1000)) * 1000, ).toISOString(); const decisionSeriesCheckpoints = downsampleBacktestCheckpoints( buildBacktestCheckpoints({ startDate, endDate, stepHours: DEFAULT_BACKTEST_DECISION_STEP_HOURS, }), DEFAULT_BACKTEST_DECISION_MAX_CHECKPOINTS, ); const replay = await buildReplay({ config: effectiveConfig, startDate, endDate, stepHours: DEFAULT_BACKTEST_DECISION_STEP_HOURS, accountValueUsd: input.accountValueUsd, checkpoints: decisionSeriesCheckpoints, }); const executableSymbol = await resolveExecutableMarketSymbol(effectiveConfig); const lastDecision = replay.decisions[replay.decisions.length - 1]; return { symbol: executableSymbol, timeframeStart: replay.startDate, timeframeEnd: replay.endDate, barsEvaluated: replay.decisions.length, resolution: params.config.resolution, mode: params.config.execution?.mode ?? DEFAULT_EXECUTION_MODE, indicator: "news", decisions: replay.decisions.map((decision) => ({ ...decision, budgetUsd: decision.budgetUsd, targetSize: decision.targetSize, price: decision.price, signal: decision.signal, ts: decision.ts, indicator: "news", indicators: { ...decision.indicators, latestSignal: lastDecision?.signal ?? null, }, })), }; }