1Branch0Tags
GL
glucryptoFix Paragon perp market resolution
251fe3214 days ago5Commits
typescript
import { store } from "opentool/store"; import type { ToolProfile } from "opentool"; import { z } from "zod"; import { buildBacktestDecisionSeriesInput, backtestDecisionRequestSchema, resolveBacktestMode, } from "opentool/backtest"; import { buildHyperliquidMarketIdentity, HyperliquidApiError, type HyperliquidOrderResponse, } from "opentool/adapters/hyperliquid"; import { PAIR_TRADE_TEMPLATE_CONFIG, readConfig, resolveProfileAssets, resolveScheduleConfig, } from "../src/config"; import { buildBacktestDecisionSeries, runPairTrade } from "../src/pair-trade"; const config = readConfig(); const fundingRatePointSchema = z.object({ time: z.string().min(1), rateBps: z.number(), }); const pairTradeBacktestRequestSchema = backtestDecisionRequestSchema .extend({ fundingRates: z.array(fundingRatePointSchema).optional(), }) .partial(); export const schema = pairTradeBacktestRequestSchema; type BacktestAwareToolProfile = ToolProfile & { backtest?: { mode: "decisions"; }; }; function buildFailureMetadata(error: unknown) { const message = error instanceof Error ? error.message : "unknown"; return { ok: false, error: message, errorType: error instanceof Error ? error.name : typeof error, ...(error instanceof HyperliquidApiError ? { errorResponse: error.response } : {}), }; } function resolveOrderRef(params: { response: HyperliquidOrderResponse | undefined; fallbackCloid: string | undefined; fallbackOid: string | undefined; index: number; }): string { const statuses = params.response && typeof params.response === "object" && params.response.response && typeof params.response.response === "object" && params.response.response.data && typeof params.response.response.data === "object" && Array.isArray((params.response.response.data as any).statuses) ? ((params.response.response.data as any).statuses as Array<Record<string, unknown>>) : []; for (const status of statuses) { const filled = status && typeof status.filled === "object" ? (status.filled as Record<string, unknown>) : null; if (filled) { if (typeof filled.cloid === "string" && filled.cloid.trim().length > 0) { return filled.cloid; } if ( typeof filled.oid === "number" || (typeof filled.oid === "string" && filled.oid.trim().length > 0) ) { return String(filled.oid); } } const resting = status && typeof status.resting === "object" ? (status.resting as Record<string, unknown>) : null; if (resting) { if (typeof resting.cloid === "string" && resting.cloid.trim().length > 0) { return resting.cloid; } if ( typeof resting.oid === "number" || (typeof resting.oid === "string" && resting.oid.trim().length > 0) ) { return String(resting.oid); } } } if (params.fallbackCloid && params.fallbackCloid.trim().length > 0) { return params.fallbackCloid; } if (params.fallbackOid && params.fallbackOid.trim().length > 0) { return params.fallbackOid; } return `pair-trade-order-${Date.now()}-${params.index}`; } export const profile: BacktestAwareToolProfile = { description: "Pair-trade strategy (long + short legs) on Hyperliquid.", category: "strategy", backtest: { mode: "decisions", }, templatePreview: { subtitle: "Long and short balancing with a configurable hedge ratio.", description: `Monitors configured markets and computes hedge requirements each cycle. Builds coordinated long and short orders to express relative exposure. Uses standardized Hyperliquid execution and normalization helpers in OpenTool. Includes risk-aware sizing controls for budget, leverage, and allocation limits. Optimized for repeated autonomous operation with transparent execution logs.`, }, schedule: resolveScheduleConfig(config), assets: resolveProfileAssets(config), templateConfig: PAIR_TRADE_TEMPLATE_CONFIG, }; async function executeLive() { const snapshot = readConfig(); try { const result = await runPairTrade(snapshot); const network = result.environment === "testnet" ? "hyperliquid-testnet" : "hyperliquid"; const decisionAction = result.action ?? "pair-trade"; const decisionRef = `pair-trade-${Date.now()}`; const { orderResponses: _orderResponses, orderIds: _orderIds, executedOrders: _executedOrders, ...decisionResult } = result; await store({ source: "pair-trade", ref: decisionRef, status: result.ok ? "info" : "failed", eventLevel: "decision", action: decisionAction, network, walletAddress: result.walletAddress ? (result.walletAddress as `0x${string}`) : undefined, metadata: { ...decisionResult, decisionRef, plannedOrderCount: result.plannedOrders.length, executedOrderCount: result.executedOrders.length, }, }); if (result.ok && result.executedOrders.length > 0) { for (let index = 0; index < result.executedOrders.length; index += 1) { const order = result.executedOrders[index]; if (!order) continue; const response = result.orderResponses[index]; const market = buildHyperliquidMarketIdentity({ environment: result.environment, symbol: order.symbol, rawSymbol: order.symbol, isSpot: order.marketType === "spot", base: order.kind === "spot" ? result.longAsset : result.shortAsset, quote: order.marketType === "spot" ? "USDC" : null, }); if (!market) continue; const ref = resolveOrderRef({ response, fallbackCloid: result.orderIds?.cloids[index], fallbackOid: result.orderIds?.oids[index], index, }); await store({ source: "pair-trade", ref, status: "submitted", eventLevel: "execution", action: "order", network, walletAddress: result.walletAddress ? (result.walletAddress as `0x${string}`) : undefined, notional: order.size, market, metadata: { strategy: "pair-trade", parentDecisionRef: decisionRef, executionRef: ref, kind: order.kind, marketType: order.marketType, side: order.side, size: order.size, price: order.price, reduceOnly: Boolean(order.reduceOnly), notionalUsd: order.notionalUsd, orderResponse: response ?? null, }, }); } } return Response.json(result); } catch (error) { const metadata = buildFailureMetadata(error); await store({ source: "pair-trade", ref: `pair-trade-${Date.now()}`, status: "failed", eventLevel: "decision", action: "pair-trade", metadata, }); return new Response(JSON.stringify(metadata), { status: 400, headers: { "content-type": "application/json" }, }); } } export async function POST(req: Request) { const snapshot = readConfig(); const payload = await req.json().catch(() => null); const mode = payload && typeof payload === "object" && !Array.isArray(payload) ? (payload as { mode?: unknown }).mode : null; const normalizedMode = resolveBacktestMode(mode); if (normalizedMode === "backtest_decisions") { try { const parsed = pairTradeBacktestRequestSchema.safeParse(payload); if (!parsed.success) { return new Response( JSON.stringify({ ok: false, error: parsed.error.issues[0]?.message ?? "invalid backtest request payload", }), { status: 400, headers: { "content-type": "application/json" }, } ); } const decisionSeries = await buildBacktestDecisionSeries({ config: snapshot, ...buildBacktestDecisionSeriesInput(parsed.data), fundingRates: parsed.data.fundingRates, }); return Response.json({ ok: true, mode: normalizedMode, backtest: decisionSeries, }); } catch (error) { const message = error instanceof Error ? error.message : "unknown"; return new Response(JSON.stringify({ ok: false, error: message }), { status: 400, headers: { "content-type": "application/json" }, }); } } return executeLive(); }