1Branch0Tags
GL
glucryptoFix exact Hyperliquid symbols
typescript
import { store } from "opentool/store"; import { wallet } from "opentool/wallet"; import type { WalletFullContext } from "opentool/wallet"; import { buildHyperliquidMarketIdentity, fetchHyperliquidClearinghouseState, fetchHyperliquidDexMetaAndAssetCtxs, fetchHyperliquidSizeDecimals, fetchHyperliquidSpotClearinghouseState, fetchHyperliquidTickSize, formatHyperliquidMarketablePrice, formatHyperliquidOrderSize, isHyperliquidSpotSymbol, normalizeHyperliquidBaseSymbol, normalizeHyperliquidMetaSymbol, placeHyperliquidOrder, planHyperliquidTrade, readHyperliquidPerpPositionSize, readHyperliquidSpotBalanceSize, resolveHyperliquidChainConfig, resolveHyperliquidErrorDetail, resolveHyperliquidLeverageMode, resolveHyperliquidPair, resolveHyperliquidSymbol, resolveHyperliquidTargetSize, updateHyperliquidLeverage, type HyperliquidEnvironment, type HyperliquidOrderResponse, } from "opentool/adapters/hyperliquid"; import { DEFAULT_EXECUTION_ENV, DEFAULT_EXECUTION_MODE, DEFAULT_SLIPPAGE_BPS, type IndicatorType, type SignalBotConfig, } from "../config"; import { extractOrderIds } from "./order-utils"; import { buildSignalTargetSizeConfig, buildSignalTargetSizeExecution } from "./target-size"; async function readOrderSizeDecimals(params: { environment: HyperliquidEnvironment; symbol: string; }) { if (!params.symbol.includes(":")) { return fetchHyperliquidSizeDecimals(params); } const [dexRaw] = params.symbol.split(":"); const dex = dexRaw?.trim().toLowerCase(); if (!dex) { return fetchHyperliquidSizeDecimals(params); } const data = (await fetchHyperliquidDexMetaAndAssetCtxs( params.environment, dex, )) as [{ universe?: Array<{ name?: string; szDecimals?: number | string }> }, Array<Record<string, unknown>>]; const universe = Array.isArray(data?.[0]?.universe) ? data[0].universe : []; const target = normalizeHyperliquidMetaSymbol(params.symbol).toUpperCase(); const entry = universe.find( (item) => normalizeHyperliquidMetaSymbol(item?.name ?? "").toUpperCase() === target, ) ?? null; const rawDecimals = entry?.szDecimals; const sizeDecimals = typeof rawDecimals === "number" ? rawDecimals : typeof rawDecimals === "string" ? Number.parseFloat(rawDecimals) : Number.NaN; if (!Number.isFinite(sizeDecimals)) { throw new Error(`No size decimals found for ${params.symbol}.`); } return sizeDecimals; } export async function executePriceSignal(params: { config: SignalBotConfig; currentPrice: number; tradeSignal: "buy" | "sell" | "hold" | "unknown"; indicator: IndicatorType; }): Promise<{ execution?: Record<string, unknown>; executionError: string | null }> { const { config, currentPrice, tradeSignal, indicator } = params; const environment = (config.execution?.environment ?? DEFAULT_EXECUTION_ENV) as HyperliquidEnvironment; const mode = config.execution?.mode ?? DEFAULT_EXECUTION_MODE; const slippageBps = config.execution?.slippageBps ?? 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 { targetSize, budgetUsd } = resolveHyperliquidTargetSize({ config: buildSignalTargetSizeConfig(config), execution: buildSignalTargetSizeExecution(config), accountValue: null, currentPrice, }); const plan = 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 tick = await fetchHyperliquidTickSize({ symbol: orderSymbol, environment, }); const resolvedSizeDecimals = await readOrderSizeDecimals({ symbol: orderSymbol, environment, }); sizeDecimals = resolvedSizeDecimals; const price = formatHyperliquidMarketablePrice({ mid: currentPrice, side: plan.side, slippageBps, tick, szDecimals: resolvedSizeDecimals, marketType: isSpot ? "spot" : "perp", }); 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] ?? `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, assetSymbols: undefined, side: plan.side, price, size, reduceOnly: plan.reduceOnly, ...(typeof leverage === "number" ? { leverage } : {}), environment, cloid: orderIds.cloids[0] ?? null, orderIds, orderResponse: response, strategy: "signal-bot", }, }); orderResponses.push(response); } return { executionError: null, execution: { enabled: true, indicator, 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) { const executionError = error instanceof Error ? error.message : "execution_failed"; const errorDetail = resolveHyperliquidErrorDetail(error); return { executionError, execution: { enabled: true, indicator, signal: tradeSignal, action: "error", environment, mode, symbol: baseSymbol, leverageMode, ...(typeof leverage === "number" ? { leverage } : {}), error: executionError, ...(errorDetail ? { errorDetail } : {}), }, }; } }