1Branch0Tags
GL
glucryptoFix Paragon perp market resolution
251fe3214 days ago5Commits
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, }, ]; }