openpondai/agents/signal-bot
OpenTool app
typescript
import { resolveBacktestAccountValueUsd, resolveBacktestWindow } from "opentool/backtest";
import {
fetchHyperliquidBars,
normalizeHyperliquidBaseSymbol,
normalizeHyperliquidIndicatorBars,
resolveHyperliquidTargetSize,
} from "opentool/adapters/hyperliquid";
import {
DEFAULT_EXECUTION_MODE,
normalizeConfigMarketSymbol,
type IndicatorType,
type SignalBotConfig,
} from "../config";
import { buildIndicatorDecisionOutput } from "./indicator-output";
import { buildSignalTargetSizeConfig, buildSignalTargetSizeExecution } from "./target-size";
import { resolveTradeSignal } from "./trade-signal";
import type { BacktestDecisionPoint } from "./types";
export type BacktestDecisionSeriesResult = {
symbol: string;
timeframeStart: string;
timeframeEnd: string;
barsEvaluated: number;
resolution: SignalBotConfig["resolution"];
mode: "long-only" | "long-short";
indicator: IndicatorType;
decisions: BacktestDecisionPoint[];
};
export async function buildBacktestDecisionSeries(params: {
config: SignalBotConfig;
symbol?: string;
timeframeStart?: string;
timeframeEnd?: string;
from?: number;
to?: number;
lookbackDays?: number;
accountValueUsd?: number;
}) {
const symbolOverride = params.symbol ? normalizeConfigMarketSymbol(params.symbol) : "";
const config: SignalBotConfig = symbolOverride
? { ...params.config, asset: symbolOverride }
: params.config;
const execution = config.execution ?? {};
const indicator = execution.indicator ?? config.indicators[0] ?? "rsi";
const mode = execution.mode ?? DEFAULT_EXECUTION_MODE;
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 countBack = window.countBack;
const rawBars = await fetchHyperliquidBars({
symbol: config.asset,
resolution: config.resolution,
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 accountValue = resolveBacktestAccountValueUsd(params.accountValueUsd) ?? null;
const decisions: BacktestDecisionPoint[] = [];
for (let index = 0; index < bars.length; index += 1) {
const snapshotBars = bars.slice(0, index + 1);
const bar = snapshotBars[snapshotBars.length - 1]!;
const indicators = buildIndicatorDecisionOutput({
config,
bars: snapshotBars,
indicator,
});
const signal = resolveTradeSignal(indicator, indicators);
const { targetSize, budgetUsd } = resolveHyperliquidTargetSize({
config: buildSignalTargetSizeConfig(config),
execution: buildSignalTargetSizeExecution(config),
accountValue,
currentPrice: bar.close,
});
decisions.push({
ts: new Date(bar.time).toISOString(),
price: bar.close,
signal,
targetSize,
budgetUsd,
indicator,
indicators,
});
}
return {
symbol: (normalizeHyperliquidBaseSymbol(config.asset) ?? config.asset).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,
indicator,
decisions,
};
}