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;
}