OpenPond
1Branch0Tags
GL
glucryptoMerge commit 'refs/tmp/prod-master'
typescript
import { z } from "zod"; import { wallet } from "opentool/wallet"; import type { WalletFullContext } from "opentool/wallet"; import { buildHyperliquidProfileAssets, extractHyperliquidOrderIds, formatHyperliquidMarketablePrice, formatHyperliquidOrderSize, fetchHyperliquidClearinghouseState, fetchHyperliquidSpotClearinghouseState, placeHyperliquidOrder, readHyperliquidPerpPosition, readHyperliquidSpotBalance, resolveHyperliquidErrorDetail, updateHyperliquidLeverage, type HyperliquidEnvironment, type HyperliquidOrderResponse, } from "opentool/adapters/hyperliquid"; import { fetchPerpMarketInfo, fetchSpotMarketInfo, fetchHyperliquidTickSize, fetchHyperliquidSpotTickSize, type TickSize, resolveChainConfig, resolvePerpSymbol, resolveSpotSymbol, } from "./hyperliquid"; export type DeltaNeutralConfig = { configVersion?: number; platform: "hyperliquid"; allocationMode: "target_notional"; asset: string; targetNotionalUsd: number; hedgeRatio: number; perpLeverage: number; perpLeverageMode: "cross" | "isolated"; maxPerRunUsd: number; slippageBps: number; rebalanceCadenceMinutes: number; deltaDriftUsd?: number; deltaDriftPct: number; basisMaxBps?: number; fundingMinBps?: number; environment: "mainnet" | "testnet"; schedule?: { cron: string; enabled: boolean; notifyEmail: boolean; }; }; const TEMPLATE_CONFIG_VERSION = 4; const TEMPLATE_CONFIG_ENV_VAR = "OPENTOOL_PUBLIC_HL_DELTA_NEUTRAL_CONFIG"; const TEMPLATE_CONFIG_DEFAULTS: DeltaNeutralConfig = { configVersion: TEMPLATE_CONFIG_VERSION, platform: "hyperliquid", allocationMode: "target_notional", asset: "BTC", targetNotionalUsd: 1000, hedgeRatio: 1, perpLeverage: 1, perpLeverageMode: "cross", maxPerRunUsd: 1000, slippageBps: 20, rebalanceCadenceMinutes: 30, deltaDriftPct: 2, basisMaxBps: 50, fundingMinBps: 0, environment: "mainnet", schedule: { cron: "*/30 * * * *", enabled: false, notifyEmail: false, }, }; const TEMPLATE_CONFIG_SCHEMA = { type: "object", "x-budget": { modeField: "allocationMode", defaultMode: "target_notional", title: "Budget & allocation", description: "Core exposure settings are shown first.", modes: { target_notional: { fields: ["targetNotionalUsd", "hedgeRatio"], }, }, }, required: [ "platform", "allocationMode", "asset", "targetNotionalUsd", "hedgeRatio", "slippageBps", "rebalanceCadenceMinutes", "deltaDriftPct", "environment", ], properties: { configVersion: { type: "number", title: "Config version", description: "Internal version for delta-neutral config defaults.", readOnly: true, "x-hidden": true, "x-section": "Meta", "x-order": 1000, }, platform: { type: "string", enum: ["hyperliquid"], title: "Platform", description: "Execution venue for spot + perps.", readOnly: true, "x-section": "Execution", "x-order": 1, }, allocationMode: { type: "string", enum: ["target_notional"], title: "Allocation mode", description: "Canonical sizing mode for delta-neutral templates.", readOnly: true, "x-hidden": true, "x-section": "Meta", "x-order": 1001, }, asset: { type: "string", title: "Asset", description: "Base asset to hedge (example: BTC).", "x-section": "Strategy", "x-order": 1, }, targetNotionalUsd: { type: "number", title: "Target notional", description: "Total USD notional for each leg.", minimum: 1, "x-unit": "USD", "x-format": "currency", "x-step": 1, "x-section": "Strategy", "x-order": 2, }, hedgeRatio: { type: "number", title: "Hedge ratio", description: "Multiplier for the hedge leg notional.", minimum: 0.01, "x-unit": "x", "x-step": 0.01, "x-section": "Strategy", "x-order": 3, }, perpLeverage: { type: "number", title: "Perp leverage", description: "Target leverage for the perp hedge leg.", minimum: 1, maximum: 40, "x-unit": "x", "x-step": 1, "x-section": "Execution", "x-order": 3, }, perpLeverageMode: { type: "string", enum: ["cross", "isolated"], title: "Perp leverage mode", description: "Margin mode for the perp hedge leg.", "x-enumLabels": ["Cross", "Isolated"], "x-section": "Execution", "x-order": 4, }, slippageBps: { type: "number", title: "Slippage", description: "Maximum slippage for marketable orders.", minimum: 0, maximum: 5000, "x-unit": "bps", "x-format": "bps", "x-step": 1, "x-section": "Risk limits", "x-order": 1, }, rebalanceCadenceMinutes: { type: "number", title: "Rebalance cadence", description: "Minimum minutes between rebalances.", minimum: 5, maximum: 720, "x-unit": "minutes", "x-format": "duration", "x-step": 1, "x-section": "Rebalance", "x-order": 1, }, deltaDriftPct: { type: "number", title: "Delta drift", description: "Percent of target notional before rebalancing.", minimum: 0.1, maximum: 50, "x-unit": "%", "x-format": "percent", "x-step": 0.1, "x-section": "Risk limits", "x-order": 2, }, basisMaxBps: { type: "number", title: "Max basis", description: "Max basis spread in bps before skipping trades.", minimum: 0, "x-unit": "bps", "x-format": "bps", "x-step": 1, "x-section": "Risk limits", "x-order": 5, }, fundingMinBps: { type: "number", title: "Min funding", description: "Minimum funding rate in bps to keep the position open.", minimum: 0, "x-unit": "bps", "x-format": "bps", "x-step": 1, "x-section": "Risk limits", "x-order": 6, }, environment: { type: "string", enum: ["mainnet", "testnet"], title: "Environment", description: "Hyperliquid environment for execution.", "x-enumLabels": ["Mainnet", "Testnet"], "x-section": "Execution", "x-order": 2, }, schedule: { type: "object", title: "Schedule", description: "Cron schedule for rebalancing runs.", "x-section": "Schedule", properties: { cron: { type: "string", title: "Cron expression", description: "Standard cron expression for the run cadence.", "x-order": 1, }, enabled: { type: "boolean", title: "Enabled", description: "Enable the scheduled rebalance.", "x-order": 2, }, notifyEmail: { type: "boolean", title: "Notify email", description: "Send an email after each scheduled run.", "x-order": 3, }, }, }, }, }; export const DELTA_NEUTRAL_TEMPLATE_CONFIG = { version: TEMPLATE_CONFIG_VERSION, schema: TEMPLATE_CONFIG_SCHEMA, defaults: TEMPLATE_CONFIG_DEFAULTS, envVar: TEMPLATE_CONFIG_ENV_VAR, }; const scheduleSchema = z .object({ cron: z.string().min(1).optional(), enabled: z.boolean().optional(), notifyEmail: z.boolean().optional(), }) .optional(); const configSchema = z.object({ platform: z.literal("hyperliquid").optional(), allocationMode: z.literal("target_notional").optional(), asset: z.string().min(1).optional(), targetNotionalUsd: z.number().positive().optional(), hedgeRatio: z.number().positive().optional(), perpLeverage: z.number().positive().optional(), perpLeverageMode: z.enum(["cross", "isolated"]).optional(), slippageBps: z.number().int().min(0).max(5000).optional(), rebalanceCadenceMinutes: z.number().int().min(5).max(720).optional(), deltaDriftUsd: z.number().positive().optional(), deltaDriftPct: z.number().positive().max(100).optional(), basisMaxBps: z.number().int().min(0).optional(), fundingMinBps: z.number().int().min(0).optional(), environment: z.enum(["mainnet", "testnet"]).optional(), schedule: scheduleSchema, }); const resolveMaxPerRunUsd = (targetNotionalUsd: number, hedgeRatio: number) => { if (!Number.isFinite(targetNotionalUsd) || targetNotionalUsd <= 0) return 0; const ratio = Number.isFinite(hedgeRatio) && hedgeRatio > 0 ? hedgeRatio : 1; return Math.max(targetNotionalUsd, targetNotionalUsd * ratio); }; export function readConfig(): DeltaNeutralConfig { const raw = process.env[TEMPLATE_CONFIG_ENV_VAR]; if (!raw) { const targetNotionalUsd = TEMPLATE_CONFIG_DEFAULTS.targetNotionalUsd; const hedgeRatio = TEMPLATE_CONFIG_DEFAULTS.hedgeRatio; const maxPerRunUsd = resolveMaxPerRunUsd(targetNotionalUsd, hedgeRatio); const deltaDriftUsd = (targetNotionalUsd * TEMPLATE_CONFIG_DEFAULTS.deltaDriftPct) / 100; return { ...TEMPLATE_CONFIG_DEFAULTS, maxPerRunUsd, deltaDriftUsd, }; } try { const parsed = configSchema.parse(JSON.parse(raw)); const merged = { ...TEMPLATE_CONFIG_DEFAULTS, ...parsed, schedule: { ...TEMPLATE_CONFIG_DEFAULTS.schedule, ...(parsed.schedule ?? {}), }, }; const targetNotionalUsd = Number.isFinite(merged.targetNotionalUsd) ? merged.targetNotionalUsd : TEMPLATE_CONFIG_DEFAULTS.targetNotionalUsd; const hedgeRatio = Number.isFinite(merged.hedgeRatio) ? merged.hedgeRatio : TEMPLATE_CONFIG_DEFAULTS.hedgeRatio; const deltaDriftPct = Number.isFinite(merged.deltaDriftPct) ? merged.deltaDriftPct : TEMPLATE_CONFIG_DEFAULTS.deltaDriftPct; const computedDeltaDriftUsd = (targetNotionalUsd * deltaDriftPct) / 100; return { ...merged, allocationMode: "target_notional", deltaDriftPct, maxPerRunUsd: resolveMaxPerRunUsd(targetNotionalUsd, hedgeRatio), deltaDriftUsd: Number.isFinite(merged.deltaDriftUsd) && merged.deltaDriftUsd > 0 ? merged.deltaDriftUsd : computedDeltaDriftUsd, }; } catch { const targetNotionalUsd = TEMPLATE_CONFIG_DEFAULTS.targetNotionalUsd; const hedgeRatio = TEMPLATE_CONFIG_DEFAULTS.hedgeRatio; const maxPerRunUsd = resolveMaxPerRunUsd(targetNotionalUsd, hedgeRatio); const deltaDriftUsd = (targetNotionalUsd * TEMPLATE_CONFIG_DEFAULTS.deltaDriftPct) / 100; return { ...TEMPLATE_CONFIG_DEFAULTS, maxPerRunUsd, deltaDriftUsd, }; } } export function resolveScheduleConfig(config: DeltaNeutralConfig) { const schedule = config.schedule ?? TEMPLATE_CONFIG_DEFAULTS.schedule; if (!schedule || !schedule.cron) return undefined; return { cron: schedule.cron, enabled: schedule.enabled, notifyEmail: schedule.notifyEmail, }; } export function resolveProfileAssets(config: DeltaNeutralConfig) { return buildHyperliquidProfileAssets({ environment: config.environment, assets: [{ assetSymbols: [config.asset] }], }); } type DeltaNeutralAction = | "delta-neutral-open" | "delta-neutral-rebalance" | "delta-neutral-hold" | "delta-neutral-close"; type PlannedOrder = { kind: "spot" | "perp"; symbol: string; side: "buy" | "sell"; size: string; price: string; reduceOnly?: boolean; notionalUsd: number; }; type DeltaNeutralResult = { ok: boolean; action: DeltaNeutralAction; environment: HyperliquidEnvironment; walletAddress: string; asset: string; perpSymbol: string; spotSymbol: string; metrics: { spotValueUsd: number; perpNotionalUsd: number; deltaUsd: number; deltaPct: number | null; basisBps: number | null; fundingRateBps: number | null; spotBalance: number; perpSize: number; spotEntryNtl: number | null; perpUnrealizedPnl: number | null; spotUnrealizedPnl: number | null; }; plannedOrders: PlannedOrder[]; executedOrders: PlannedOrder[]; orderResponses: HyperliquidOrderResponse[]; orderIds?: { cloids: string[]; oids: string[]; }; skipped?: boolean; reason?: string; error?: string; errorDetail?: unknown; }; const MIN_NOTIONAL_USD = 1; const clampAbs = (value: number, limit: number): number => { if (!Number.isFinite(value) || limit <= 0) return 0; const capped = Math.min(Math.abs(value), limit); return Math.sign(value) * capped; }; function formatMarketablePrice( mid: number, side: "buy" | "sell", slippageBps: number, tick?: TickSize | null ): string { return formatHyperliquidMarketablePrice({ mid, side, slippageBps, tick, }); } export async function runDeltaNeutral(config: DeltaNeutralConfig) { const environment = (config.environment ?? "mainnet") as HyperliquidEnvironment; const asset = config.asset.trim().toUpperCase(); const perpSymbol = resolvePerpSymbol(asset); const spot = resolveSpotSymbol(asset); const slippageBps = Math.max(0, config.slippageBps); const maxPerRunUsd = Math.max(0, config.maxPerRunUsd); const chain = resolveChainConfig(environment).chain; const ctx = await wallet({ chain }); const walletAddress = ctx.address as string; const [perpMarket, spotMarket, perpClearing, spotClearing] = await Promise.all([ fetchPerpMarketInfo({ environment, symbol: perpSymbol }), fetchSpotMarketInfo({ environment, base: spot.base, quote: spot.quote }), fetchHyperliquidClearinghouseState({ environment, walletAddress: ctx.address as `0x${string}`, }), fetchHyperliquidSpotClearinghouseState({ environment, user: ctx.address as `0x${string}`, }), ]); const [perpTick, spotTick] = await Promise.all([ fetchHyperliquidTickSize({ environment, symbol: perpSymbol }), fetchHyperliquidSpotTickSize({ environment, marketIndex: spotMarket.marketIndex, }), ]); if (!perpClearing.ok || !perpClearing.data) { return { ok: false, action: "delta-neutral-hold", environment, walletAddress, asset, perpSymbol, spotSymbol: spot.symbol, metrics: { spotValueUsd: 0, perpNotionalUsd: 0, deltaUsd: 0, deltaPct: null, basisBps: null, fundingRateBps: null, spotBalance: 0, perpSize: 0, spotEntryNtl: null, perpUnrealizedPnl: null, spotUnrealizedPnl: null, }, plannedOrders: [], executedOrders: [], orderResponses: [], error: "Failed to load Hyperliquid perp state.", errorDetail: perpClearing.data, } satisfies DeltaNeutralResult; } const perpPosition = readHyperliquidPerpPosition(perpClearing.data, perpSymbol); const spotPosition = readHyperliquidSpotBalance(spotClearing, spotMarket.base); const spotValueUsd = spotPosition.total * spotMarket.price; const perpNotionalUsd = perpPosition.size * perpMarket.price; const deltaUsd = spotValueUsd + perpNotionalUsd; const deltaPct = config.targetNotionalUsd > 0 ? (Math.abs(deltaUsd) / config.targetNotionalUsd) * 100 : null; const basisBps = spotMarket.price > 0 ? ((perpMarket.price - spotMarket.price) / spotMarket.price) * 10_000 : null; const fundingRateBps = perpMarket.fundingRate != null ? perpMarket.fundingRate * 10_000 : null; const metrics = { spotValueUsd, perpNotionalUsd, deltaUsd, deltaPct, basisBps, fundingRateBps, spotBalance: spotPosition.total, perpSize: perpPosition.size, spotEntryNtl: spotPosition.entryNtl, perpUnrealizedPnl: perpPosition.unrealizedPnl, spotUnrealizedPnl: spotPosition.entryNtl != null ? spotValueUsd - spotPosition.entryNtl : null, }; if ( typeof config.basisMaxBps === "number" && basisBps != null && Math.abs(basisBps) > config.basisMaxBps ) { return { ok: true, action: "delta-neutral-hold", environment, walletAddress, asset, perpSymbol, spotSymbol: spot.symbol, metrics, plannedOrders: [], executedOrders: [], orderResponses: [], skipped: true, reason: "basis-gate", } satisfies DeltaNeutralResult; } if ( typeof config.fundingMinBps === "number" && fundingRateBps != null && fundingRateBps < config.fundingMinBps ) { return { ok: true, action: "delta-neutral-hold", environment, walletAddress, asset, perpSymbol, spotSymbol: spot.symbol, metrics, plannedOrders: [], executedOrders: [], orderResponses: [], skipped: true, reason: "funding-gate", } satisfies DeltaNeutralResult; } const driftUsdThreshold = config.deltaDriftUsd ?? 0; const driftPctThreshold = config.deltaDriftPct ?? 0; const isOpen = Math.abs(spotValueUsd) < MIN_NOTIONAL_USD && Math.abs(perpNotionalUsd) < MIN_NOTIONAL_USD; const shouldRebalance = isOpen || Math.abs(deltaUsd) >= driftUsdThreshold || (deltaPct != null && deltaPct >= driftPctThreshold); if (!shouldRebalance) { return { ok: true, action: "delta-neutral-hold", environment, walletAddress, asset, perpSymbol, spotSymbol: spot.symbol, metrics, plannedOrders: [], executedOrders: [], orderResponses: [], } satisfies DeltaNeutralResult; } let spotOrderUsd = 0; let perpOrderUsd = 0; let action: DeltaNeutralAction = isOpen ? "delta-neutral-open" : "delta-neutral-rebalance"; const targetSpotNotional = config.targetNotionalUsd; if (isOpen) { spotOrderUsd = clampAbs(targetSpotNotional, maxPerRunUsd); const expectedSpotValue = spotValueUsd + spotOrderUsd; const desiredPerpNotional = -expectedSpotValue * config.hedgeRatio; perpOrderUsd = clampAbs(desiredPerpNotional - perpNotionalUsd, maxPerRunUsd); } else { const spotDriftUsd = targetSpotNotional - spotValueUsd; if (Math.abs(spotDriftUsd) >= driftUsdThreshold) { spotOrderUsd = clampAbs(spotDriftUsd, maxPerRunUsd); } const expectedSpotValue = spotValueUsd + spotOrderUsd; const desiredPerpNotional = -expectedSpotValue * config.hedgeRatio; perpOrderUsd = clampAbs(desiredPerpNotional - perpNotionalUsd, maxPerRunUsd); } const plannedOrders: PlannedOrder[] = []; 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) / perpMarket.price, perpMarket.szDecimals ); if (size === "0") return; const price = formatMarketablePrice( perpMarket.price, side, slippageBps, perpTick ); const reduceOnly = perpNotionalUsd !== 0 && Math.sign(usdDelta) !== Math.sign(perpNotionalUsd); plannedOrders.push({ kind: "perp", symbol: perpSymbol, side, size, price, reduceOnly, notionalUsd: Math.abs(usdDelta), }); }; 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) / spotMarket.price, spotMarket.szDecimals ); if (size === "0") return; const price = formatMarketablePrice( spotMarket.price, side, slippageBps, spotTick ); plannedOrders.push({ kind: "spot", symbol: spot.symbol, side, size, price, notionalUsd: Math.abs(usdDelta), }); }; addPerpOrder(perpOrderUsd); addSpotOrder(spotOrderUsd); if (plannedOrders.length === 0) { return { ok: true, action: "delta-neutral-hold", environment, walletAddress, asset, perpSymbol, spotSymbol: spot.symbol, metrics, plannedOrders, executedOrders: [], orderResponses: [], skipped: true, reason: "order-too-small", } satisfies DeltaNeutralResult; } const executionOrder = action === "delta-neutral-open" ? ["spot", "perp"] : ["perp", "spot"]; const executedOrders: PlannedOrder[] = []; const orderResponses: HyperliquidOrderResponse[] = []; try { const perpOrder = plannedOrders.find((entry) => entry.kind === "perp"); if (perpOrder) { await updateHyperliquidLeverage({ wallet: ctx as WalletFullContext, environment, input: { symbol: perpSymbol, leverageMode: config.perpLeverageMode ?? "cross", leverage: Number.isFinite(config.perpLeverage) && config.perpLeverage > 0 ? config.perpLeverage : 1, }, }); } 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, environment, walletAddress, asset, perpSymbol, spotSymbol: spot.symbol, metrics, plannedOrders, executedOrders, orderResponses, error: error instanceof Error ? error.message : "Order submission failed", errorDetail: resolveHyperliquidErrorDetail(error), } satisfies DeltaNeutralResult; } const orderIds = extractHyperliquidOrderIds( orderResponses as unknown as Array<{ response?: { data?: { statuses?: Array<Record<string, unknown>> } }; }> ); return { ok: true, action, environment, walletAddress, asset, perpSymbol, spotSymbol: spot.symbol, metrics, plannedOrders, executedOrders, orderResponses, ...(orderIds.cloids.length || orderIds.oids.length ? { orderIds } : {}), } as DeltaNeutralResult; }