1Branch0Tags
GL
glucryptoFix Paragon perp market resolution
251fe3214 days ago5Commits
typescript
import { wallet } from "opentool/wallet"; import type { WalletFullContext } from "opentool/wallet"; import { extractHyperliquidOrderIds, fetchHyperliquidClearinghouseState, fetchHyperliquidSpotClearinghouseState, fetchHyperliquidSpotMarketInfo, fetchHyperliquidSpotTickSize, fetchHyperliquidTickSize, placeHyperliquidOrder, readHyperliquidPerpPosition, readHyperliquidSpotBalance, resolveHyperliquidChainConfig, resolveHyperliquidErrorDetail, resolveHyperliquidSpotSymbol, updateHyperliquidLeverage, type HyperliquidEnvironment, } from "opentool/adapters/hyperliquid"; import type { PairTradeConfig } from "../config"; import { fetchPairTradePerpMarketInfo, resolvePairTradePerpLeverageMode, resolvePairTradePerpSymbol, } from "./market"; import { buildResolvedOrders, planPairTradeCycle } from "./planner"; import { resolvePairTradeLegMarkets } from "./schedule"; import type { PairTradeMetrics, PairTradeResult, PlannedOrder } from "./types"; function buildEmptyMetrics(): PairTradeMetrics { return { spotValueUsd: 0, perpNotionalUsd: 0, deltaUsd: 0, deltaPct: null, basisBps: null, fundingRateBps: null, spotBalance: 0, perpSize: 0, spotEntryNtl: null, perpUnrealizedPnl: null, spotUnrealizedPnl: null, }; } function resolvePositiveLeverage(value: number) { return Number.isFinite(value) && value > 0 ? Math.max(1, Math.round(value)) : 1; } async function resolveMarketLeg(params: { environment: HyperliquidEnvironment; asset: string; marketType: "spot" | "perp"; }) { if (params.marketType === "spot") { const spot = resolveHyperliquidSpotSymbol(params.asset); const market = await fetchHyperliquidSpotMarketInfo({ environment: params.environment, base: spot.base, quote: spot.quote, }); const tick = await fetchHyperliquidSpotTickSize({ environment: params.environment, marketIndex: market.marketIndex, }); return { asset: params.asset, marketType: params.marketType, symbol: spot.symbol, base: spot.base, price: market.price, szDecimals: market.szDecimals, tick, fundingRateBps: null, }; } const symbol = resolvePairTradePerpSymbol(params.asset); const [market, tick] = await Promise.all([ fetchPairTradePerpMarketInfo({ environment: params.environment, symbol }), fetchHyperliquidTickSize({ environment: params.environment, symbol }), ]); return { asset: params.asset, marketType: params.marketType, symbol, base: symbol, price: market.price, szDecimals: market.szDecimals, tick, fundingRateBps: market.fundingRate != null ? market.fundingRate * 10_000 : null, }; } export async function runPairTrade(config: PairTradeConfig): Promise<PairTradeResult> { const environment = (config.environment ?? "mainnet") as HyperliquidEnvironment; const { longAsset, shortAsset, longMarketType, shortMarketType } = resolvePairTradeLegMarkets(config); const longSymbol = longMarketType === "spot" ? resolveHyperliquidSpotSymbol(longAsset).symbol : resolvePairTradePerpSymbol(longAsset); const shortSymbol = shortMarketType === "spot" ? resolveHyperliquidSpotSymbol(shortAsset).symbol : resolvePairTradePerpSymbol(shortAsset); const perpSymbol = shortSymbol; const spotSymbol = longSymbol; const legAPerpSymbol = config.legAMarketType === "perp" ? resolvePairTradePerpSymbol(config.legAAsset) : null; const legBPerpSymbol = config.legBMarketType === "perp" ? resolvePairTradePerpSymbol(config.legBAsset) : null; const leverageByPerpSymbol = new Map< string, { leverage: number; leverageMode: "cross" | "isolated" } >(); if (legAPerpSymbol) { leverageByPerpSymbol.set(legAPerpSymbol, { leverage: resolvePositiveLeverage(config.legALeverage), leverageMode: resolvePairTradePerpLeverageMode( legAPerpSymbol, config.legALeverageMode, ), }); } if (legBPerpSymbol) { leverageByPerpSymbol.set(legBPerpSymbol, { leverage: resolvePositiveLeverage(config.legBLeverage), leverageMode: resolvePairTradePerpLeverageMode( legBPerpSymbol, config.legBLeverageMode, ), }); } const slippageBps = Math.max(0, config.slippageBps); const maxPerRunUsd = Math.max(0, config.maxPerRunUsd); const chain = resolveHyperliquidChainConfig(environment).chain; const ctx = await wallet({ chain }); const walletAddress = ctx.address as string; if (shortMarketType === "spot") { return { ok: false, action: "pair-trade-hold", environment, walletAddress, asset: longAsset, longAsset, shortAsset, longMarketType, shortMarketType, perpSymbol, spotSymbol, metrics: buildEmptyMetrics(), plannedOrders: [], executedOrders: [], orderResponses: [], error: "Pair-trade does not support a spot short leg.", }; } const [longLeg, shortLeg, perpClearing, spotClearing] = await Promise.all([ resolveMarketLeg({ environment, asset: longAsset, marketType: longMarketType }), resolveMarketLeg({ environment, asset: shortAsset, marketType: shortMarketType }), fetchHyperliquidClearinghouseState({ environment, walletAddress: ctx.address as `0x${string}`, }), longMarketType === "spot" ? fetchHyperliquidSpotClearinghouseState({ environment, user: ctx.address as `0x${string}`, }) : Promise.resolve(null), ]); if (!perpClearing.ok || !perpClearing.data) { return { ok: false, action: "pair-trade-hold", environment, walletAddress, asset: longAsset, longAsset, shortAsset, longMarketType, shortMarketType, perpSymbol, spotSymbol, metrics: buildEmptyMetrics(), plannedOrders: [], executedOrders: [], orderResponses: [], error: "Failed to load Hyperliquid perp state.", errorDetail: perpClearing.data, }; } const longSpotBalance = longLeg.marketType === "spot" ? readHyperliquidSpotBalance(spotClearing, longLeg.base) : null; const longPerpPosition = longLeg.marketType === "perp" ? readHyperliquidPerpPosition(perpClearing.data, longLeg.symbol) : null; const longPosition = longLeg.marketType === "spot" ? { size: longSpotBalance?.total ?? 0, entryNtl: longSpotBalance?.entryNtl ?? null, unrealizedPnl: null, } : { size: longPerpPosition?.size ?? 0, entryNtl: null, unrealizedPnl: longPerpPosition?.unrealizedPnl ?? null, }; const shortPosition = readHyperliquidPerpPosition(perpClearing.data, shortLeg.symbol); const plan = planPairTradeCycle({ config, spotPrice: longLeg.price, perpPrice: shortLeg.price, fundingRateBps: shortLeg.fundingRateBps ?? longLeg.fundingRateBps ?? null, spotBalance: longPosition.size, spotEntryNtl: longPosition.entryNtl, perpSize: shortPosition.size, perpUnrealizedPnl: shortPosition.unrealizedPnl, maxPerRunUsd, }); const plannedOrders = buildResolvedOrders({ slippageBps, perpSymbol: shortLeg.symbol, spotSymbol: longLeg.symbol, perpPrice: shortLeg.price, spotPrice: longLeg.price, perpSzDecimals: shortLeg.szDecimals, spotSzDecimals: longLeg.szDecimals, perpTick: shortLeg.tick, spotTick: longLeg.tick, perpMarketType: shortLeg.marketType, spotMarketType: longLeg.marketType, currentSpotNotionalUsd: plan.metrics.spotValueUsd, currentPerpNotionalUsd: plan.metrics.perpNotionalUsd, plan, }).map((order) => ({ kind: order.kind, marketType: order.marketType, symbol: order.symbol, side: order.side, size: order.size, price: order.price, reduceOnly: order.reduceOnly, notionalUsd: order.notionalUsd, })) satisfies PlannedOrder[]; if (plannedOrders.length === 0) { return { ok: true, action: "pair-trade-hold", environment, walletAddress, asset: longAsset, longAsset, shortAsset, longMarketType, shortMarketType, perpSymbol, spotSymbol, metrics: plan.metrics, plannedOrders, executedOrders: [], orderResponses: [], ...(plan.reason ? { skipped: true, reason: plan.reason } : {}), ...(!plan.reason ? { skipped: true, reason: "order-too-small" } : {}), }; } const executionOrder = plan.action === "pair-trade-open" ? ["spot", "perp"] : ["perp", "spot"]; const executedOrders: PlannedOrder[] = []; const orderResponses: PairTradeResult["orderResponses"] = []; try { const perpSymbols = Array.from( new Set( plannedOrders .filter((entry) => entry.marketType === "perp") .map((entry) => entry.symbol), ), ); for (const symbol of perpSymbols) { const leverageConfig = leverageByPerpSymbol.get(symbol) ?? { leverage: 1, leverageMode: resolvePairTradePerpLeverageMode(symbol, "cross"), }; await updateHyperliquidLeverage({ wallet: ctx as WalletFullContext, environment, input: { symbol, leverageMode: leverageConfig.leverageMode, leverage: leverageConfig.leverage, }, }); } for (const kind of executionOrder) { const order = plannedOrders.find((entry) => entry.kind === kind); if (!order) continue; const response = await placeHyperliquidOrder({ wallet: ctx as WalletFullContext, environment, orders: [ { symbol: order.symbol, side: order.side, price: order.price, size: order.size, tif: "FrontendMarket", reduceOnly: order.reduceOnly, }, ], }); orderResponses.push(response); executedOrders.push(order); } } catch (error) { return { ok: false, action: plan.action, environment, walletAddress, asset: longAsset, longAsset, shortAsset, longMarketType, shortMarketType, perpSymbol, spotSymbol, metrics: plan.metrics, plannedOrders, executedOrders, orderResponses, error: error instanceof Error ? error.message : "Order submission failed", errorDetail: resolveHyperliquidErrorDetail(error), }; } const orderIds = extractHyperliquidOrderIds( orderResponses as unknown as Array<{ response?: { data?: { statuses?: Array<Record<string, unknown>> } }; }>, ); return { ok: true, action: plan.action, environment, walletAddress, asset: longAsset, longAsset, shortAsset, longMarketType, shortMarketType, perpSymbol, spotSymbol, metrics: plan.metrics, plannedOrders, executedOrders, orderResponses, ...(orderIds.cloids.length || orderIds.oids.length ? { orderIds } : {}), }; }