openpondai/agents/hyperliquid-delta-neutral
OpenTool app
typescript
import { wallet } from "opentool/wallet";
import type { WalletFullContext } from "opentool/wallet";
import {
extractHyperliquidOrderIds,
fetchHyperliquidClearinghouseState,
fetchHyperliquidPerpMarketInfo,
fetchHyperliquidSpotClearinghouseState,
fetchHyperliquidSpotMarketInfo,
fetchHyperliquidSpotTickSize,
fetchHyperliquidTickSize,
placeHyperliquidOrder,
readHyperliquidPerpPosition,
readHyperliquidSpotBalance,
resolveHyperliquidChainConfig,
resolveHyperliquidErrorDetail,
resolveHyperliquidPerpSymbol,
resolveHyperliquidSpotSymbol,
updateHyperliquidLeverage,
type HyperliquidEnvironment,
} from "opentool/adapters/hyperliquid";
import type { DeltaNeutralConfig } from "../config";
import { buildResolvedOrders, planDeltaNeutralCycle } from "./planner";
import { resolveDeltaNeutralLegAssets } from "./schedule";
import type { DeltaNeutralMetrics, DeltaNeutralResult, PlannedOrder } from "./types";
function buildEmptyMetrics(): DeltaNeutralMetrics {
return {
spotValueUsd: 0,
perpNotionalUsd: 0,
deltaUsd: 0,
deltaPct: null,
basisBps: null,
fundingRateBps: null,
spotBalance: 0,
perpSize: 0,
spotEntryNtl: null,
perpUnrealizedPnl: null,
spotUnrealizedPnl: null,
};
}
export async function runDeltaNeutral(config: DeltaNeutralConfig): Promise<DeltaNeutralResult> {
const environment = (config.environment ?? "mainnet") as HyperliquidEnvironment;
const { spotAsset, perpAsset } = resolveDeltaNeutralLegAssets(config);
const perpSymbol = resolveHyperliquidPerpSymbol(perpAsset);
const spot = resolveHyperliquidSpotSymbol(spotAsset);
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;
const [perpMarket, spotMarket, perpClearing, spotClearing] = await Promise.all([
fetchHyperliquidPerpMarketInfo({ environment, symbol: perpSymbol }),
fetchHyperliquidSpotMarketInfo({ 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: spotAsset,
spotAsset,
perpAsset,
perpSymbol,
spotSymbol: spot.symbol,
metrics: buildEmptyMetrics(),
plannedOrders: [],
executedOrders: [],
orderResponses: [],
error: "Failed to load Hyperliquid perp state.",
errorDetail: perpClearing.data,
};
}
const perpPosition = readHyperliquidPerpPosition(perpClearing.data, perpSymbol);
const spotPosition = readHyperliquidSpotBalance(spotClearing, spotMarket.base);
const plan = planDeltaNeutralCycle({
config,
spotPrice: spotMarket.price,
perpPrice: perpMarket.price,
fundingRateBps:
perpMarket.fundingRate != null ? perpMarket.fundingRate * 10_000 : null,
spotBalance: spotPosition.total,
spotEntryNtl: spotPosition.entryNtl,
perpSize: perpPosition.size,
perpUnrealizedPnl: perpPosition.unrealizedPnl,
maxPerRunUsd,
});
const plannedOrders = buildResolvedOrders({
slippageBps,
perpSymbol,
spotSymbol: spot.symbol,
perpPrice: perpMarket.price,
spotPrice: spotMarket.price,
perpSzDecimals: perpMarket.szDecimals,
spotSzDecimals: spotMarket.szDecimals,
perpTick,
spotTick,
currentPerpNotionalUsd: plan.metrics.perpNotionalUsd,
plan,
}).map((order) => ({
kind: order.kind,
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: "delta-neutral-hold",
environment,
walletAddress,
asset: spotAsset,
spotAsset,
perpAsset,
perpSymbol,
spotSymbol: spot.symbol,
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 === "delta-neutral-open" ? ["spot", "perp"] : ["perp", "spot"];
const executedOrders: PlannedOrder[] = [];
const orderResponses: DeltaNeutralResult["orderResponses"] = [];
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: plan.action,
environment,
walletAddress,
asset: spotAsset,
spotAsset,
perpAsset,
perpSymbol,
spotSymbol: spot.symbol,
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: spotAsset,
spotAsset,
perpAsset,
perpSymbol,
spotSymbol: spot.symbol,
metrics: plan.metrics,
plannedOrders,
executedOrders,
orderResponses,
...(orderIds.cloids.length || orderIds.oids.length ? { orderIds } : {}),
};
}