openpondai/agents/price-trigger-bot
OpenTool app
1Branch0Tags
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 }
: {}),
},
];
}