1Branch0Tags
GL
glucryptoReserve only trade targets in price trigger profil...
4a63a6312 days ago20Commits
typescript
import { DEFAULT_AMOUNT_USD, DEFAULT_ASSET, TEMPLATE_CONFIG_DEFAULTS, TEMPLATE_CONFIG_ENV_VAR, TEMPLATE_CONFIG_VERSION, } from "./defaults"; import { configSchema, TEMPLATE_CONFIG_SCHEMA } from "./schema"; import type { PriceTriggerConfig, PriceTriggerRule, PriceTriggerTarget, } from "./types"; function parseJson(value: string | null | undefined) { if (!value) return null; try { return JSON.parse(value) as unknown; } catch { return null; } } function sanitizeConfigCandidate(value: unknown): unknown { if (!value || typeof value !== "object" || Array.isArray(value)) { return value; } const record = value as Record<string, unknown>; const execution = record.execution && typeof record.execution === "object" && !Array.isArray(record.execution) ? (record.execution as Record<string, unknown>) : null; if (!execution) { return value; } const slippageBps = execution.slippageBps; if (typeof slippageBps !== "number" || !Number.isFinite(slippageBps)) { return value; } return { ...record, execution: { ...execution, // Keep known starter payloads inside schema bounds so one numeric field // cannot reset the full template config back to defaults. slippageBps: Math.max(0, Math.min(500, slippageBps)), }, }; } export function normalizeBaseAsset(value: string | null | undefined) { const trimmed = value?.trim().toUpperCase() ?? ""; if (!trimmed) return ""; const withoutDex = trimmed.includes(":") ? trimmed.split(":").slice(1).join(":") : trimmed; const base = withoutDex.split(/[-/]/)[0] ?? withoutDex; return base.trim().toUpperCase(); } export function normalizeSymbol(value: string | null | undefined) { const trimmed = value?.trim().toUpperCase() ?? ""; if (!trimmed) return ""; const withoutSpaces = trimmed.replace(/\s+/g, ""); if (withoutSpaces.includes("/")) return withoutSpaces; if (withoutSpaces.includes("-")) { const [base, ...rest] = withoutSpaces.split("-"); const quote = rest.join("-").trim(); return base && quote ? `${base}/${quote}` : withoutSpaces; } return withoutSpaces; } function normalizeTarget(target: Partial<PriceTriggerTarget> | null | undefined) { const symbol = normalizeSymbol(target?.symbol); if (!symbol) return null; const weight = typeof target?.weight === "number" && Number.isFinite(target.weight) && target.weight > 0 ? target.weight : 1; return { symbol, weight, }; } function normalizeTargets( targets: Array<Partial<PriceTriggerTarget>> | undefined, fallbackSymbol: string, ) { const normalizedTargets = (targets ?? []) .map((target) => normalizeTarget(target)) .filter((target): target is PriceTriggerTarget => Boolean(target)); if (normalizedTargets.length > 0) { return normalizedTargets; } return [{ symbol: normalizeSymbol(fallbackSymbol) || DEFAULT_ASSET, weight: 1 }]; } function normalizeRule( rule: Partial<PriceTriggerRule> | undefined, index: number, fallbackSymbol: string, ): PriceTriggerRule { const sourceSymbol = normalizeSymbol(rule?.sourceSymbol) || fallbackSymbol; return { id: rule?.id?.trim() || `rule-${index + 1}`, sourceSymbol, condition: rule?.condition ?? "crosses-above", threshold: typeof rule?.threshold === "number" && Number.isFinite(rule.threshold) && rule.threshold > 0 ? rule.threshold : TEMPLATE_CONFIG_DEFAULTS.rules[0]!.threshold, actionSide: rule?.actionSide ?? "buy", targets: normalizeTargets(rule?.targets, sourceSymbol), }; } export const PRICE_TRIGGER_BOT_TEMPLATE_CONFIG = { version: TEMPLATE_CONFIG_VERSION, schema: TEMPLATE_CONFIG_SCHEMA, defaults: TEMPLATE_CONFIG_DEFAULTS, envVar: TEMPLATE_CONFIG_ENV_VAR, }; export function readConfig(): PriceTriggerConfig { const parsed = configSchema.safeParse( sanitizeConfigCandidate(parseJson(process.env[TEMPLATE_CONFIG_ENV_VAR])), ); const input = parsed.success ? parsed.data : {}; const asset = normalizeBaseAsset(input.asset) || DEFAULT_ASSET; const schedule = { cron: input.schedule?.cron ?? TEMPLATE_CONFIG_DEFAULTS.schedule.cron, enabled: input.schedule?.enabled ?? TEMPLATE_CONFIG_DEFAULTS.schedule.enabled, notifyEmail: input.schedule?.notifyEmail ?? TEMPLATE_CONFIG_DEFAULTS.schedule.notifyEmail, }; const rules = input.rules?.length && Array.isArray(input.rules) ? input.rules.map((rule, index) => normalizeRule(rule, index, normalizeSymbol(asset)), ) : TEMPLATE_CONFIG_DEFAULTS.rules; return { configVersion: TEMPLATE_CONFIG_VERSION, platform: "hyperliquid", ruleType: "price-trigger", allocationMode: "fixed", asset, amountUsd: input.amountUsd ?? DEFAULT_AMOUNT_USD, maxPerRunUsd: input.amountUsd ?? DEFAULT_AMOUNT_USD, schedule, rules, execution: { enabled: input.execution?.enabled ?? TEMPLATE_CONFIG_DEFAULTS.execution!.enabled, environment: input.execution?.environment ?? TEMPLATE_CONFIG_DEFAULTS.execution!.environment, slippageBps: input.execution?.slippageBps ?? TEMPLATE_CONFIG_DEFAULTS.execution!.slippageBps, ...(typeof input.execution?.leverage === "number" ? { leverage: input.execution.leverage } : {}), }, }; } export function resolveScheduleConfig(config: PriceTriggerConfig) { return config.schedule; } function resolveProfileSymbols(config: PriceTriggerConfig) { const symbols = new Set<string>(); for (const rule of config.rules) { for (const target of rule.targets) { const targetSymbol = target.symbol.trim(); if (targetSymbol) symbols.add(targetSymbol); } } if (symbols.size === 0) { const fallback = config.asset.trim(); if (fallback) symbols.add(fallback); } return Array.from(symbols); } export function resolveProfileAssets(config: PriceTriggerConfig) { const assetSymbols = resolveProfileSymbols(config); if (assetSymbols.length === 0) { return []; } return [ { venue: "hyperliquid" as const, chain: config.execution?.environment === "testnet" ? ("hyperliquid-testnet" as const) : ("hyperliquid" as const), assetSymbols, ...(typeof config.execution?.leverage === "number" ? { leverage: config.execution.leverage } : {}), }, ]; }