3Branches0Tags
GL
glucryptoSync production with master template updates
4d3cf1112 days ago23Commits
typescript
import { store } from "opentool/store"; import { wallet } from "opentool/wallet"; import type { WalletFullContext } from "opentool/wallet"; import { buildHyperliquidMarketIdentity, fetchHyperliquidBars, fetchHyperliquidDexMetaAndAssetCtxs, fetchHyperliquidSizeDecimals, formatHyperliquidMarketablePrice, formatHyperliquidOrderSize, isHyperliquidSpotSymbol, normalizeHyperliquidBaseSymbol, normalizeHyperliquidDcaEntries, normalizeHyperliquidMetaSymbol, placeHyperliquidOrder, resolveHyperliquidBudgetUsd, resolveHyperliquidChainConfig, resolveHyperliquidErrorDetail, resolveHyperliquidLeverageMode, resolveHyperliquidPair, resolveHyperliquidSymbol, updateHyperliquidLeverage, type HyperliquidEnvironment, type HyperliquidOrderResponse, } from "opentool/adapters/hyperliquid"; import { DEFAULT_ASSET, DEFAULT_EXECUTION_ENV, DEFAULT_SLIPPAGE_BPS, type DcaAgentConfig, } from "../config"; import { extractOrderIds } from "./order-utils"; 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 executeDcaOrders(params: { config: DcaAgentConfig; dca: NonNullable<DcaAgentConfig["dca"]>; }): Promise<{ execution?: Record<string, unknown>; executionError: string | null }> { const { config, dca } = params; const environment = (config.execution?.environment ?? DEFAULT_EXECUTION_ENV) as HyperliquidEnvironment; const slippageBps = config.execution?.slippageBps ?? dca.slippageBps ?? DEFAULT_SLIPPAGE_BPS; const leverage = config.execution?.leverage; const entries = normalizeHyperliquidDcaEntries({ entries: config.dca?.symbols, fallbackSymbol: config.asset ?? DEFAULT_ASSET, }); const hasSpotEntries = entries.some((entry) => isHyperliquidSpotSymbol(resolveHyperliquidSymbol(entry.symbol)), ); try { const chain = resolveHyperliquidChainConfig(environment).chain; const ctx = await wallet({ chain }); if (hasSpotEntries && typeof leverage === "number" && Number.isFinite(leverage)) { throw new Error("leverage is not supported for spot markets."); } const budgetUsd = resolveHyperliquidBudgetUsd({ config, accountValue: null }); if (!Number.isFinite(budgetUsd) || budgetUsd <= 0) { throw new Error("DCA budget must be greater than zero."); } const orderResponses: HyperliquidOrderResponse[] = []; const results: Array<Record<string, unknown>> = []; const errors: Array<Record<string, unknown>> = []; for (const entry of entries) { const symbolBudget = budgetUsd * entry.normalizedWeight; if (!Number.isFinite(symbolBudget) || symbolBudget <= 0) { errors.push({ symbol: entry.symbol, error: "budget too small" }); continue; } const bars = await fetchHyperliquidBars({ symbol: entry.symbol, resolution: config.resolution, countBack: config.countBack, }); if (bars.length === 0) { errors.push({ symbol: entry.symbol, error: "no price data" }); continue; } const currentPrice = bars[bars.length - 1]!.close; const orderSymbol = resolveHyperliquidSymbol(entry.symbol); const isSpot = isHyperliquidSpotSymbol(orderSymbol); if (isSpot && orderSymbol.startsWith("@")) { errors.push({ symbol: entry.symbol, error: "spot requires BASE/QUOTE" }); continue; } const baseSymbol = normalizeHyperliquidBaseSymbol(orderSymbol); const pair = resolveHyperliquidPair(orderSymbol); const leverageMode = resolveHyperliquidLeverageMode(orderSymbol); if (!isSpot && typeof leverage === "number" && Number.isFinite(leverage)) { await updateHyperliquidLeverage({ wallet: ctx as WalletFullContext, environment, input: { symbol: orderSymbol, leverageMode, leverage, }, }); } const sizeDecimals = await readOrderSizeDecimals({ symbol: orderSymbol, environment }); const price = formatHyperliquidMarketablePrice({ mid: currentPrice, side: "buy", slippageBps, szDecimals: sizeDecimals, marketType: isSpot ? "spot" : "perp", }); const size = formatHyperliquidOrderSize(symbolBudget / currentPrice, sizeDecimals); if (size === "0") { errors.push({ symbol: entry.symbol, error: `Order size too small (szDecimals=${sizeDecimals}).`, }); continue; } const response = await placeHyperliquidOrder({ wallet: ctx as WalletFullContext, environment, orders: [ { symbol: orderSymbol, side: "buy", price, size, tif: "FrontendMarket", reduceOnly: false, }, ], }); const orderIds = extractOrderIds([response]); const orderRef = orderIds.cloids[0] ?? orderIds.oids[0] ?? `dca-agent-${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, market: orderSymbol, pair: pair ?? undefined, assetSymbols: entries.map((entry) => entry.symbol), side: "buy", price, size, reduceOnly: false, ...(typeof leverage === "number" ? { leverage } : {}), environment, weight: entry.weight, budgetUsd: symbolBudget, cloid: orderIds.cloids[0] ?? null, orderIds, orderResponse: response, strategy: "dca-agent", }, }); orderResponses.push(response); results.push({ symbol: entry.symbol, budgetUsd: symbolBudget, orderSymbol, size, price, orderIds, }); } return { executionError: null, execution: { enabled: true, environment, mode: "long-only", slippageBps, budgetUsd, orders: results, errors: errors.length > 0 ? errors : undefined, orderIds: extractOrderIds(orderResponses), }, }; } catch (error) { const executionError = error instanceof Error ? error.message : "execution_failed"; const errorDetail = resolveHyperliquidErrorDetail(error); return { executionError, execution: { enabled: true, environment, mode: "long-only", ...(typeof leverage === "number" ? { leverage } : {}), error: executionError, ...(errorDetail ? { errorDetail } : {}), }, }; } }