openpondai/agents/pair-trade
OpenTool app
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();
}