2Branches0Tags
GL
glucryptoSync production with master template updates
dc969c112 days ago54Commits
typescript
import { clampHyperliquidAbs, fetchHyperliquidSpotTickSize, fetchHyperliquidTickSize, formatHyperliquidMarketablePrice, formatHyperliquidOrderSize, } from "opentool/adapters/hyperliquid"; import type { DeltaNeutralConfig } from "../config"; import { estimateCarryEdgeUsd, resolveHyperliquidBuilderFeeRate, resolveHyperliquidExchangeFeeRate, resolveHyperliquidTotalFeeRate, } from "./planner-fees"; import { buildDeltaNeutralMetrics } from "./planner-metrics"; import type { DeltaNeutralExecutionPlan, DeltaNeutralPlanInput, ResolvedPlannedOrder, } from "./types"; export const MIN_NOTIONAL_USD = 1; export { resolveHyperliquidBuilderFeeRate, resolveHyperliquidExchangeFeeRate, resolveHyperliquidTotalFeeRate, } from "./planner-fees"; export { buildDeltaNeutralMetrics } from "./planner-metrics"; export function planDeltaNeutralCycle( params: DeltaNeutralPlanInput, ): DeltaNeutralExecutionPlan { const metrics = buildDeltaNeutralMetrics({ 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 bootstrapSpotOrderUsd = hasSpotInventory ? 0 : clampHyperliquidAbs(params.config.targetNotionalUsd, params.maxPerRunUsd); const effectiveSpotValueUsd = metrics.spotValueUsd + bootstrapSpotOrderUsd; const desiredPerpOpenNotionalUsd = -effectiveSpotValueUsd * Math.max(params.config.hedgeRatio ?? 1, 0); if ( typeof params.config.basisMaxBps === "number" && metrics.basisBps != null && Math.abs(metrics.basisBps) > params.config.basisMaxBps && !hasPerpHedge ) { return { action: bootstrapSpotOrderUsd > 0 ? "delta-neutral-open" : "delta-neutral-hold", metrics, spotOrderUsd: bootstrapSpotOrderUsd, perpOrderUsd: 0, skipped: true, reason: bootstrapSpotOrderUsd > 0 ? "bootstrap-spot" : "basis-gate", }; } if (!hasPerpHedge && metrics.basisBps != null && metrics.basisBps < 0) { return { action: bootstrapSpotOrderUsd > 0 ? "delta-neutral-open" : "delta-neutral-hold", metrics, spotOrderUsd: bootstrapSpotOrderUsd, perpOrderUsd: 0, skipped: true, reason: bootstrapSpotOrderUsd > 0 ? "bootstrap-spot" : "basis-open-gate", }; } if ( typeof params.config.fundingMinBps === "number" && metrics.fundingRateBps != null ) { const fundingOpenThresholdBps = params.config.fundingMinBps; const fundingCloseThresholdBps = -Math.max(params.config.fundingMinBps * 3, 0.15); if (hasPerpHedge && metrics.fundingRateBps < fundingCloseThresholdBps) { return { action: "delta-neutral-close", metrics, spotOrderUsd: 0, perpOrderUsd: clampHyperliquidAbs(-metrics.perpNotionalUsd, params.maxPerRunUsd), skipped: false, reason: "funding-gate-close", }; } if (!hasPerpHedge && metrics.fundingRateBps < fundingOpenThresholdBps) { return { action: bootstrapSpotOrderUsd > 0 ? "delta-neutral-open" : "delta-neutral-hold", metrics, spotOrderUsd: bootstrapSpotOrderUsd, perpOrderUsd: 0, skipped: true, reason: bootstrapSpotOrderUsd > 0 ? "bootstrap-spot" : "funding-gate", }; } } if (!hasPerpHedge) { const carryEdge = estimateCarryEdgeUsd({ config: params.config, basisBps: metrics.basisBps, fundingRateBps: metrics.fundingRateBps, hedgeNotionalUsd: Math.abs(desiredPerpOpenNotionalUsd), }); if (carryEdge.netEdgeUsd <= 0) { return { action: bootstrapSpotOrderUsd > 0 ? "delta-neutral-open" : "delta-neutral-hold", metrics, spotOrderUsd: bootstrapSpotOrderUsd, perpOrderUsd: 0, skipped: true, reason: bootstrapSpotOrderUsd > 0 ? "bootstrap-spot" : "expected-edge-gate", }; } } const driftUsdThreshold = params.config.deltaDriftUsd ?? 0; const driftPctThreshold = params.config.deltaDriftPct ?? 0; const shouldRebalance = !hasPerpHedge || Math.abs(metrics.deltaUsd) >= driftUsdThreshold || (metrics.deltaPct != null && metrics.deltaPct >= driftPctThreshold); if (!shouldRebalance) { return { action: "delta-neutral-hold", metrics, spotOrderUsd: 0, perpOrderUsd: 0, }; } let spotOrderUsd = 0; let perpOrderUsd = 0; const action = !hasPerpHedge ? "delta-neutral-open" : "delta-neutral-rebalance"; if (!hasPerpHedge) { spotOrderUsd = bootstrapSpotOrderUsd; const desiredPerpNotional = desiredPerpOpenNotionalUsd; perpOrderUsd = clampHyperliquidAbs( desiredPerpNotional - metrics.perpNotionalUsd, params.maxPerRunUsd, ); } else { const desiredPerpNotional = -metrics.spotValueUsd * Math.max(params.config.hedgeRatio ?? 1, 0); perpOrderUsd = clampHyperliquidAbs( desiredPerpNotional - metrics.perpNotionalUsd, 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; currentPerpNotionalUsd: number; plan: DeltaNeutralExecutionPlan; }): ResolvedPlannedOrder[] { const orders: ResolvedPlannedOrder[] = []; 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: "perp", }); 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", 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: "spot", }); const fillPrice = Number.parseFloat(price); if (!Number.isFinite(fillPrice) || fillPrice <= 0) return; orders.push({ kind: "spot", symbol: params.spotSymbol, side, size, quantity, price, fillPrice, referencePrice: params.spotPrice, 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"; }