1Branch0Tags
GL
glucryptoFix Paragon perp market resolution
251fe3214 days ago5Commits
typescript
import { clampHyperliquidAbs, fetchHyperliquidSpotTickSize, fetchHyperliquidTickSize, formatHyperliquidMarketablePrice, formatHyperliquidOrderSize, } from "opentool/adapters/hyperliquid"; import type { PairTradeConfig } from "../config"; import type { PairTradeMarketType } from "../config"; import { resolveHyperliquidBuilderFeeRate, resolveHyperliquidExchangeFeeRate, resolveHyperliquidTotalFeeRate, } from "./planner-fees"; import { buildPairTradeMetrics } from "./planner-metrics"; import type { PairTradeExecutionPlan, PairTradePlanInput, ResolvedPlannedOrder, } from "./types"; export const MIN_NOTIONAL_USD = 1; export { resolveHyperliquidBuilderFeeRate, resolveHyperliquidExchangeFeeRate, resolveHyperliquidTotalFeeRate, } from "./planner-fees"; export { buildPairTradeMetrics } from "./planner-metrics"; export function planPairTradeCycle( params: PairTradePlanInput, ): PairTradeExecutionPlan { const metrics = buildPairTradeMetrics({ config: params.config, spotPrice: params.spotPrice, perpPrice: params.perpPrice, fundingRateBps: params.fundingRateBps, spotBalance: params.spotBalance, spotEntryNtl: params.spotEntryNtl, perpSize: params.perpSize, perpUnrealizedPnl: params.perpUnrealizedPnl, }); const hasSpotInventory = Math.abs(metrics.spotValueUsd) >= MIN_NOTIONAL_USD; const hasPerpHedge = Math.abs(metrics.perpNotionalUsd) >= MIN_NOTIONAL_USD; const targetSpotValueUsd = params.config.longTargetNotionalUsd; const targetPerpNotionalUsd = -params.config.shortTargetNotionalUsd; const spotDiffUsd = targetSpotValueUsd - metrics.spotValueUsd; const perpDiffUsd = targetPerpNotionalUsd - metrics.perpNotionalUsd; const driftUsdThreshold = params.config.rebalanceDriftUsd ?? 0; const driftPctThreshold = params.config.rebalanceDriftPct ?? 0; const spotDriftPct = targetSpotValueUsd > 0 ? (Math.abs(spotDiffUsd) / targetSpotValueUsd) * 100 : 0; const perpDriftPct = Math.abs(targetPerpNotionalUsd) > 0 ? (Math.abs(perpDiffUsd) / Math.abs(targetPerpNotionalUsd)) * 100 : 0; const shouldRebalance = !hasSpotInventory || !hasPerpHedge || Math.abs(spotDiffUsd) >= driftUsdThreshold || Math.abs(perpDiffUsd) >= driftUsdThreshold || spotDriftPct >= driftPctThreshold || perpDriftPct >= driftPctThreshold; if (!shouldRebalance) { return { action: "pair-trade-hold", metrics, spotOrderUsd: 0, perpOrderUsd: 0, }; } let spotOrderUsd = 0; let perpOrderUsd = 0; const action = !hasSpotInventory || !hasPerpHedge ? "pair-trade-open" : "pair-trade-rebalance"; spotOrderUsd = clampHyperliquidAbs(spotDiffUsd, params.maxPerRunUsd); perpOrderUsd = clampHyperliquidAbs(perpDiffUsd, params.maxPerRunUsd); return { action, metrics, spotOrderUsd, perpOrderUsd, }; } export function buildResolvedOrders(params: { slippageBps: number; perpSymbol: string; spotSymbol: string; perpPrice: number; spotPrice: number; perpSzDecimals: number; spotSzDecimals: number; perpTick: ReturnType<typeof fetchHyperliquidTickSize> extends Promise<infer T> ? T : never; spotTick: ReturnType<typeof fetchHyperliquidSpotTickSize> extends Promise<infer T> ? T : never; perpMarketType?: PairTradeMarketType; spotMarketType?: PairTradeMarketType; currentSpotNotionalUsd: number; currentPerpNotionalUsd: number; plan: PairTradeExecutionPlan; }): ResolvedPlannedOrder[] { const orders: ResolvedPlannedOrder[] = []; const perpMarketType = params.perpMarketType ?? "perp"; const spotMarketType = params.spotMarketType ?? "spot"; const addPerpOrder = (usdDelta: number) => { if (Math.abs(usdDelta) < MIN_NOTIONAL_USD) return; const side: "buy" | "sell" = usdDelta > 0 ? "buy" : "sell"; const size = formatHyperliquidOrderSize( Math.abs(usdDelta) / params.perpPrice, params.perpSzDecimals, ); const quantity = Number.parseFloat(size); if (!Number.isFinite(quantity) || quantity <= 0) return; const price = formatHyperliquidMarketablePrice({ mid: params.perpPrice, side, slippageBps: params.slippageBps, tick: params.perpTick, szDecimals: params.perpSzDecimals, marketType: perpMarketType, }); const fillPrice = Number.parseFloat(price); if (!Number.isFinite(fillPrice) || fillPrice <= 0) return; const reduceOnly = params.currentPerpNotionalUsd !== 0 && Math.sign(usdDelta) !== Math.sign(params.currentPerpNotionalUsd); orders.push({ kind: "perp", marketType: perpMarketType, symbol: params.perpSymbol, side, size, quantity, price, fillPrice, referencePrice: params.perpPrice, reduceOnly, notionalUsd: Math.abs(quantity * fillPrice), }); }; const addSpotOrder = (usdDelta: number) => { if (Math.abs(usdDelta) < MIN_NOTIONAL_USD) return; const side: "buy" | "sell" = usdDelta > 0 ? "buy" : "sell"; const size = formatHyperliquidOrderSize( Math.abs(usdDelta) / params.spotPrice, params.spotSzDecimals, ); const quantity = Number.parseFloat(size); if (!Number.isFinite(quantity) || quantity <= 0) return; const price = formatHyperliquidMarketablePrice({ mid: params.spotPrice, side, slippageBps: params.slippageBps, tick: params.spotTick, szDecimals: params.spotSzDecimals, marketType: spotMarketType, }); const fillPrice = Number.parseFloat(price); if (!Number.isFinite(fillPrice) || fillPrice <= 0) return; const reduceOnly = spotMarketType === "perp" && params.currentSpotNotionalUsd !== 0 && Math.sign(usdDelta) !== Math.sign(params.currentSpotNotionalUsd); orders.push({ kind: "spot", marketType: spotMarketType, symbol: params.spotSymbol, side, size, quantity, price, fillPrice, referencePrice: params.spotPrice, reduceOnly, notionalUsd: Math.abs(quantity * fillPrice), }); }; addPerpOrder(params.plan.perpOrderUsd); addSpotOrder(params.plan.spotOrderUsd); return orders; } export function resolveDecisionSignal(params: { spotOrderUsd: number; perpOrderUsd: number; orders: ResolvedPlannedOrder[]; }): "buy" | "sell" | "hold" | "unknown" { if (params.orders.length === 0) return "hold"; if (params.spotOrderUsd > 0) return "buy"; if (params.spotOrderUsd < 0) return "sell"; if (params.perpOrderUsd < 0) return "buy"; if (params.perpOrderUsd > 0) return "sell"; return "unknown"; }