1Branch0Tags
GL
glucryptoRefresh package-lock for opentool 0.19.5
1e58acc14 hours ago9Commits
typescript
import { resolveBacktestAccountValueUsd, resolveBacktestWindow } from "opentool/backtest"; import { buildHyperliquidMarketIdentity, clampHyperliquidFloat, clampHyperliquidInt, extractHyperliquidOrderIds, fetchHyperliquidBars, fetchHyperliquidClearinghouseState, fetchHyperliquidSizeDecimals, fetchHyperliquidSpotAccountValue, fetchHyperliquidSpotClearinghouseState, formatHyperliquidMarketablePrice, formatHyperliquidOrderSize, isHyperliquidSpotSymbol, normalizeHyperliquidBaseSymbol, placeHyperliquidOrder, planHyperliquidTrade, readHyperliquidAccountValue, readHyperliquidPerpPositionSize, readHyperliquidSpotBalanceSize, resolveHyperliquidChainConfig, resolveHyperliquidErrorDetail, resolveHyperliquidLeverageMode, resolveHyperliquidPair, resolveHyperliquidSymbol, resolveHyperliquidTargetSize, updateHyperliquidLeverage, type HyperliquidEnvironment, type HyperliquidOrderResponse, } from "opentool/adapters/hyperliquid"; import { store } from "opentool/store"; import { wallet } from "opentool/wallet"; import type { WalletFullContext } from "opentool/wallet"; import { DEFAULT_EXECUTION_ENV, DEFAULT_EXECUTION_MODE, DEFAULT_RSI_OVERBOUGHT, DEFAULT_RSI_OVERSOLD, DEFAULT_RSI_PRESET, DEFAULT_SLIPPAGE_BPS, RSI_PRESETS, type ExecutionConfig, type IndicatorType, type SignalBotConfig, } from "./config"; import { computeRsi } from "./indicators/computeRsi"; type TradeSignal = "buy" | "sell" | "hold" | "unknown"; type TradePlan = { side: "buy" | "sell"; size: number; reduceOnly: boolean; targetSize: number; }; type BacktestDecisionPoint = { ts: string; price: number; signal: TradeSignal; targetSize: number; budgetUsd: number; indicator: IndicatorType; indicators: Record<string, unknown>; }; export type BacktestDecisionSeriesResult = { symbol: string; timeframeStart: string; timeframeEnd: string; barsEvaluated: number; resolution: SignalBotConfig["resolution"]; mode: NonNullable<ExecutionConfig["mode"]>; indicator: IndicatorType; decisions: BacktestDecisionPoint[]; }; function extractOrderIds( responses: HyperliquidOrderResponse[], ): ReturnType<typeof extractHyperliquidOrderIds> { return extractHyperliquidOrderIds( responses as unknown as Array<{ response?: { data?: { statuses?: Array<Record<string, unknown>>; }; }; }>, ); } function resolveTradeSignal(output: Record<string, unknown>): TradeSignal { const rsiRecord = output.rsi; if (!rsiRecord || typeof rsiRecord !== "object") return "unknown"; const signal = typeof (rsiRecord as { signal?: unknown }).signal === "string" ? ((rsiRecord as { signal: string }).signal as string) : ""; if (signal === "oversold") return "buy"; if (signal === "overbought") return "sell"; return signal ? "hold" : "unknown"; } function buildIndicatorDecisionOutput(params: { config: SignalBotConfig; bars: Array<{ close: number }>; }): Record<string, unknown> { const { config, bars } = params; const closes = bars.map((bar) => bar.close); const priceConfig = config.price ?? {}; const rsiPreset = priceConfig.rsiPreset ?? DEFAULT_RSI_PRESET; const rsiDefaults = RSI_PRESETS[rsiPreset] ?? RSI_PRESETS[DEFAULT_RSI_PRESET]; const overbought = clampHyperliquidFloat( priceConfig.rsi?.overbought, 1, 100, rsiDefaults?.overbought ?? DEFAULT_RSI_OVERBOUGHT, ); const oversold = clampHyperliquidFloat( priceConfig.rsi?.oversold, 1, 100, rsiDefaults?.oversold ?? DEFAULT_RSI_OVERSOLD, ); const value = computeRsi(closes); const signal = value === null ? "unknown" : value >= overbought ? "overbought" : value <= oversold ? "oversold" : "neutral"; return { rsi: { value, signal, overbought, oversold, preset: rsiPreset, }, }; } export async function buildBacktestDecisionSeries(params: { config: SignalBotConfig; symbol?: string; timeframeStart?: string; timeframeEnd?: string; from?: number; to?: number; lookbackDays?: number; accountValueUsd?: number; }): Promise<BacktestDecisionSeriesResult> { const symbolOverride = params.symbol?.trim(); const config: SignalBotConfig = symbolOverride ? { ...params.config, asset: symbolOverride } : params.config; const execution = config.execution ?? {}; 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 bars = 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)) } : {}), }); if (bars.length === 0) { throw new Error("No price data returned."); } const requestedAccountValue = resolveBacktestAccountValueUsd(params.accountValueUsd); const accountValue = requestedAccountValue != null ? requestedAccountValue : config.allocationMode === "percent_equity" ? 10_000 : 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, }); const signal = resolveTradeSignal(indicators); const { targetSize, budgetUsd } = resolveHyperliquidTargetSize({ config, execution, accountValue, currentPrice: bar.close, }); decisions.push({ ts: new Date(bar.time).toISOString(), price: bar.close, signal, targetSize, budgetUsd, indicator: "rsi", 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: "rsi", decisions, }; } export async function runSignalBot(config: SignalBotConfig) { const bars = await fetchHyperliquidBars({ symbol: config.asset, resolution: config.resolution, countBack: config.countBack, }); if (bars.length === 0) { return { ok: false, error: "No price data returned.", asset: config.asset, signalType: config.signalType, }; } const closes = bars.map((bar) => bar.close); const currentPrice = closes[closes.length - 1]; const asOf = new Date(bars[bars.length - 1].time).toISOString(); const priceConfig = config.price ?? {}; const rsiPreset = priceConfig.rsiPreset ?? DEFAULT_RSI_PRESET; const rsiDefaults = RSI_PRESETS[rsiPreset] ?? RSI_PRESETS[DEFAULT_RSI_PRESET]; const rsiOverbought = clampHyperliquidFloat( priceConfig.rsi?.overbought, 1, 100, rsiDefaults?.overbought ?? DEFAULT_RSI_OVERBOUGHT, ); const rsiOversold = clampHyperliquidFloat( priceConfig.rsi?.oversold, 1, 100, rsiDefaults?.oversold ?? DEFAULT_RSI_OVERSOLD, ); const rsiValue = computeRsi(closes); const rsiSignal = rsiValue === null ? "unknown" : rsiValue >= rsiOverbought ? "overbought" : rsiValue <= rsiOversold ? "oversold" : "neutral"; const output: Record<string, unknown> = { rsi: { value: rsiValue, signal: rsiSignal, overbought: rsiOverbought, oversold: rsiOversold, }, }; let execution: Record<string, unknown> | undefined; let executionError: string | null = null; if (config.execution?.enabled) { const tradeSignal = resolveTradeSignal(output); const environment = (config.execution.environment ?? DEFAULT_EXECUTION_ENV) as HyperliquidEnvironment; const mode = config.execution.mode ?? DEFAULT_EXECUTION_MODE; const slippageBps = clampHyperliquidInt( config.execution.slippageBps, 0, 500, DEFAULT_SLIPPAGE_BPS, ); const orderSymbol = resolveHyperliquidSymbol(config.asset, config.execution.symbol); const isSpot = isHyperliquidSpotSymbol(orderSymbol); const baseSymbol = normalizeHyperliquidBaseSymbol(orderSymbol); const pair = resolveHyperliquidPair(orderSymbol); const leverageMode = resolveHyperliquidLeverageMode(orderSymbol); const leverage = config.execution.leverage; try { const chain = resolveHyperliquidChainConfig(environment).chain; const ctx = await wallet({ chain }); if (isSpot && orderSymbol.startsWith("@")) { throw new Error("spot execution requires BASE-QUOTE or BASE/QUOTE symbols (no @ ids)."); } if (isSpot && typeof leverage === "number" && Number.isFinite(leverage)) { throw new Error("leverage is not supported for spot markets."); } const clearing = isSpot ? await fetchHyperliquidSpotClearinghouseState({ environment, user: ctx.address as `0x${string}`, }) : await fetchHyperliquidClearinghouseState({ environment, walletAddress: ctx.address as `0x${string}`, }); const currentSize = isSpot ? readHyperliquidSpotBalanceSize(clearing, orderSymbol) : readHyperliquidPerpPositionSize(clearing, orderSymbol, { prefixMatch: true }); const accountValue = isSpot ? await fetchHyperliquidSpotAccountValue({ environment, balances: (clearing as any)?.balances, }) : readHyperliquidAccountValue(clearing); const { targetSize, budgetUsd } = resolveHyperliquidTargetSize({ config, execution: config.execution, accountValue, currentPrice, }); const plan: TradePlan | null = planHyperliquidTrade({ signal: tradeSignal, mode, currentSize, targetSize, }); const orderResponses: HyperliquidOrderResponse[] = []; let sizeDecimals: number | undefined; if (!isSpot && typeof leverage === "number" && Number.isFinite(leverage)) { await updateHyperliquidLeverage({ wallet: ctx as WalletFullContext, environment, input: { symbol: orderSymbol, leverageMode, leverage, }, }); } if (plan) { const price = formatHyperliquidMarketablePrice({ mid: currentPrice, side: plan.side, slippageBps, }); const resolvedSizeDecimals = await fetchHyperliquidSizeDecimals({ symbol: orderSymbol, environment, }); sizeDecimals = resolvedSizeDecimals; const size = formatHyperliquidOrderSize(plan.size, resolvedSizeDecimals); if (size === "0") { throw new Error(`Order size too small for ${orderSymbol} (szDecimals=${sizeDecimals}).`); } const response = await placeHyperliquidOrder({ wallet: ctx as WalletFullContext, environment, orders: [ { symbol: orderSymbol, side: plan.side, price, size, tif: "FrontendMarket", reduceOnly: plan.reduceOnly, }, ], }); const orderIds = extractOrderIds([response]); const orderRef = orderIds.cloids[0] ?? orderIds.oids[0] ?? `rsi-signal-bot-${Date.now()}`; const marketIdentity = buildHyperliquidMarketIdentity({ environment, symbol: pair ?? orderSymbol, rawSymbol: orderSymbol, isSpot, base: baseSymbol ?? null, }); if (!marketIdentity) { throw new Error("Unable to resolve market identity for order."); } await store({ source: "hyperliquid", ref: orderRef, status: "submitted", walletAddress: ctx.address, action: "order", notional: size, network: environment === "mainnet" ? "hyperliquid" : "hyperliquid-testnet", market: marketIdentity, metadata: { symbol: baseSymbol, pair: pair ?? undefined, side: plan.side, price, size, reduceOnly: plan.reduceOnly, ...(typeof leverage === "number" ? { leverage } : {}), environment, cloid: orderIds.cloids[0] ?? null, orderIds, orderResponse: response, strategy: "rsi-signal-bot", }, }); orderResponses.push(response); } execution = { enabled: true, indicator: "rsi", signal: tradeSignal, action: plan ? "order" : "noop", environment, mode, symbol: baseSymbol, pair, leverageMode, ...(typeof leverage === "number" ? { leverage } : {}), slippageBps, sizeDecimals, budgetUsd, targetSize, currentSize, orderIds: extractOrderIds(orderResponses), orderResponses: orderResponses.length ? orderResponses : undefined, }; } catch (error) { executionError = error instanceof Error ? error.message : "execution_failed"; const errorDetail = resolveHyperliquidErrorDetail(error); execution = { enabled: true, indicator: "rsi", signal: resolveTradeSignal(output), action: "error", environment, mode, symbol: baseSymbol, leverageMode, ...(typeof leverage === "number" ? { leverage } : {}), error: executionError, ...(errorDetail ? { errorDetail } : {}), }; } } if (executionError) { return { ok: false, error: executionError, asset: config.asset, signalType: config.signalType, cadence: config.cadence, scheduleEvery: config.scheduleEvery, scheduleUnit: config.scheduleUnit, resolution: config.resolution, asOf, price: currentPrice, allocation: { mode: config.allocationMode, percentOfEquity: config.percentOfEquity, maxPercentOfEquity: config.maxPercentOfEquity, ...(config.amountUsd ? { amountUsd: config.amountUsd } : {}), }, indicators: output, rsiPreset, rsiThresholds: { overbought: rsiOverbought, oversold: rsiOversold }, ...(execution ? { execution } : {}), }; } return { ok: true, asset: config.asset, signalType: config.signalType, cadence: config.cadence, scheduleEvery: config.scheduleEvery, scheduleUnit: config.scheduleUnit, resolution: config.resolution, asOf, price: currentPrice, allocation: { mode: config.allocationMode, percentOfEquity: config.percentOfEquity, maxPercentOfEquity: config.maxPercentOfEquity, ...(config.amountUsd ? { amountUsd: config.amountUsd } : {}), }, indicators: output, rsiPreset, rsiThresholds: { overbought: rsiOverbought, oversold: rsiOversold }, ...(execution ? { execution } : {}), }; }