openpondai/agents/pair-trade
OpenTool app
typescript
import {
resolveHyperliquidMaxPerRunUsd,
} from "opentool/adapters/hyperliquid";
import {
TEMPLATE_CONFIG_DEFAULTS,
TEMPLATE_CONFIG_ENV_VAR,
TEMPLATE_CONFIG_VERSION,
} from "./defaults";
import { configSchema, TEMPLATE_CONFIG_SCHEMA } from "./schema";
import type { PairTradeConfig, PairTradeMarketType } from "./types";
export const PAIR_TRADE_TEMPLATE_CONFIG = {
version: TEMPLATE_CONFIG_VERSION,
schema: TEMPLATE_CONFIG_SCHEMA,
defaults: TEMPLATE_CONFIG_DEFAULTS,
envVar: TEMPLATE_CONFIG_ENV_VAR,
};
function normalizeAsset(value: string | null | undefined) {
return value?.trim().toUpperCase() ?? "";
}
function normalizeMarketType(
value: PairTradeMarketType | null | undefined,
fallback: PairTradeMarketType,
): PairTradeMarketType {
return value === "spot" || value === "perp" ? value : fallback;
}
function resolveLegConfig(input: {
legAAsset?: string;
legBAsset?: string;
legAMarketType?: PairTradeMarketType;
legBMarketType?: PairTradeMarketType;
legASide?: "long" | "short";
longAsset?: string;
shortAsset?: string;
longMarketType?: PairTradeMarketType;
shortMarketType?: PairTradeMarketType;
targetNotionalUsd: number;
hedgeRatio: number;
}) {
const legacyLongAsset = normalizeAsset(input.longAsset);
const legacyShortAsset = normalizeAsset(input.shortAsset);
const legASide = input.legASide ?? "long";
const legAAsset =
normalizeAsset(input.legAAsset) ||
(legASide === "long" ? legacyLongAsset : legacyShortAsset) ||
normalizeAsset(TEMPLATE_CONFIG_DEFAULTS.legAAsset);
const legBAsset =
normalizeAsset(input.legBAsset) ||
(legASide === "long" ? legacyShortAsset : legacyLongAsset) ||
normalizeAsset(TEMPLATE_CONFIG_DEFAULTS.legBAsset);
const legacyLongMarketType = normalizeMarketType(
input.longMarketType,
TEMPLATE_CONFIG_DEFAULTS.longMarketType,
);
const legacyShortMarketType = normalizeMarketType(
input.shortMarketType,
TEMPLATE_CONFIG_DEFAULTS.shortMarketType,
);
const legAMarketType = normalizeMarketType(
input.legAMarketType,
legASide === "long" ? legacyLongMarketType : legacyShortMarketType,
);
const legBMarketType = normalizeMarketType(
input.legBMarketType,
legASide === "long" ? legacyShortMarketType : legacyLongMarketType,
);
const longAsset = legASide === "long" ? legAAsset : legBAsset;
const shortAsset = legASide === "long" ? legBAsset : legAAsset;
const longMarketType = legASide === "long" ? legAMarketType : legBMarketType;
const shortMarketType = legASide === "long" ? legBMarketType : legAMarketType;
const longTargetNotionalUsd =
legASide === "long"
? input.targetNotionalUsd
: input.targetNotionalUsd * input.hedgeRatio;
const shortTargetNotionalUsd =
legASide === "long"
? input.targetNotionalUsd * input.hedgeRatio
: input.targetNotionalUsd;
return {
legAAsset,
legBAsset,
legAMarketType,
legBMarketType,
legASide,
longAsset,
shortAsset,
longMarketType,
shortMarketType,
longTargetNotionalUsd,
shortTargetNotionalUsd,
};
}
export function readConfig(): PairTradeConfig {
const defaultSchedule = TEMPLATE_CONFIG_DEFAULTS.schedule;
const raw = process.env[TEMPLATE_CONFIG_ENV_VAR];
if (!raw) {
const targetNotionalUsd = TEMPLATE_CONFIG_DEFAULTS.targetNotionalUsd;
const hedgeRatio = TEMPLATE_CONFIG_DEFAULTS.hedgeRatio;
const legs = resolveLegConfig({
...TEMPLATE_CONFIG_DEFAULTS,
targetNotionalUsd,
hedgeRatio,
});
const maxPerRunUsd = resolveHyperliquidMaxPerRunUsd(targetNotionalUsd, hedgeRatio);
const rebalanceDriftUsd =
(targetNotionalUsd * TEMPLATE_CONFIG_DEFAULTS.rebalanceDriftPct) / 100;
return {
...TEMPLATE_CONFIG_DEFAULTS,
...legs,
maxPerRunUsd,
rebalanceDriftUsd,
};
}
try {
const parsed = configSchema.parse(JSON.parse(raw));
const schedule =
parsed.schedule === null || (!parsed.schedule && !defaultSchedule)
? undefined
: {
cron: parsed.schedule?.cron ?? defaultSchedule?.cron ?? "",
enabled: parsed.schedule?.enabled ?? defaultSchedule?.enabled ?? false,
notifyEmail:
parsed.schedule?.notifyEmail ??
defaultSchedule?.notifyEmail ??
false,
};
const merged = {
...TEMPLATE_CONFIG_DEFAULTS,
...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 safeHedgeRatio = hedgeRatio > 0 ? hedgeRatio : TEMPLATE_CONFIG_DEFAULTS.hedgeRatio;
const rebalanceDriftPct = Number.isFinite(merged.rebalanceDriftPct)
? merged.rebalanceDriftPct
: TEMPLATE_CONFIG_DEFAULTS.rebalanceDriftPct;
const computedRebalanceDriftUsd =
(targetNotionalUsd * rebalanceDriftPct) / 100;
const legs = resolveLegConfig({
...merged,
targetNotionalUsd,
hedgeRatio: safeHedgeRatio,
});
return {
...merged,
allocationMode: "target_notional",
hedgeRatio: safeHedgeRatio,
...legs,
rebalanceDriftPct,
maxPerRunUsd: resolveHyperliquidMaxPerRunUsd(targetNotionalUsd, safeHedgeRatio),
rebalanceDriftUsd: computedRebalanceDriftUsd,
};
} catch {
const targetNotionalUsd = TEMPLATE_CONFIG_DEFAULTS.targetNotionalUsd;
const hedgeRatio = TEMPLATE_CONFIG_DEFAULTS.hedgeRatio;
const legs = resolveLegConfig({
...TEMPLATE_CONFIG_DEFAULTS,
targetNotionalUsd,
hedgeRatio,
});
const maxPerRunUsd = resolveHyperliquidMaxPerRunUsd(targetNotionalUsd, hedgeRatio);
const rebalanceDriftUsd =
(targetNotionalUsd * TEMPLATE_CONFIG_DEFAULTS.rebalanceDriftPct) / 100;
return {
...TEMPLATE_CONFIG_DEFAULTS,
...legs,
maxPerRunUsd,
rebalanceDriftUsd,
};
}
}
export function resolveScheduleConfig(config: PairTradeConfig) {
const schedule = config.schedule;
if (!schedule || !schedule.cron) return undefined;
return {
cron: schedule.cron,
enabled: schedule.enabled,
notifyEmail: schedule.notifyEmail,
};
}
export function resolveProfileAssets(config: PairTradeConfig) {
const toProfileSymbol = (asset: string, marketType: PairTradeMarketType) => {
const normalized = asset.trim().toUpperCase();
if (!normalized) return "";
if (marketType === "perp" || normalized.includes("/") || normalized.includes("-")) {
return normalized;
}
return `${normalized}/USDC`;
};
const assetSymbols = Array.from(
new Set(
[
toProfileSymbol(config.legAAsset, config.legAMarketType),
toProfileSymbol(config.legBAsset, config.legBMarketType),
toProfileSymbol(config.longAsset, config.longMarketType),
toProfileSymbol(config.shortAsset, config.shortMarketType),
]
.map((value) => value.trim())
.filter((value) => value.length > 0),
),
);
if (assetSymbols.length === 0) {
return [];
}
return [
{
venue: "hyperliquid" as const,
chain:
config.environment === "testnet"
? ("hyperliquid-testnet" as const)
: ("hyperliquid" as const),
assetSymbols,
},
];
}