openpondai/agents/signal-bot
OpenTool app
typescript
import {
clampHyperliquidInt,
parseHyperliquidJson,
} from "opentool/adapters/hyperliquid";
import {
CONFIG_ENV,
DEFAULT_AMOUNT_USD,
DEFAULT_ASSET,
DEFAULT_BB_PERIOD,
DEFAULT_BB_STD_DEV,
DEFAULT_COUNT_BACK,
DEFAULT_DONCHIAN_PERIOD,
DEFAULT_EMA_PERIOD,
DEFAULT_EXECUTION_ENV,
DEFAULT_EXECUTION_MODE,
DEFAULT_MA_CROSS_FAST,
DEFAULT_MA_CROSS_SLOW,
DEFAULT_RESOLUTION,
DEFAULT_RSI_OVERBOUGHT,
DEFAULT_RSI_OVERSOLD,
DEFAULT_SIGNAL_SCHEDULE_CRON,
DEFAULT_SLIPPAGE_BPS,
DEFAULT_SMA_PERIOD,
TEMPLATE_CONFIG_DEFAULTS,
TEMPLATE_CONFIG_ENV_VAR,
TEMPLATE_CONFIG_VERSION,
} from "./defaults";
import { configSchema, TEMPLATE_CONFIG_SCHEMA } from "./schema";
import type { ScheduleConfig, SignalBotConfig } from "./types";
export const SIGNAL_BOT_TEMPLATE_CONFIG = {
version: TEMPLATE_CONFIG_VERSION,
schema: TEMPLATE_CONFIG_SCHEMA,
defaults: TEMPLATE_CONFIG_DEFAULTS,
envVar: TEMPLATE_CONFIG_ENV_VAR,
};
export function normalizeConfigMarketSymbol(value: string | null | undefined): string {
const trimmed = (value ?? DEFAULT_ASSET).trim();
if (!trimmed) return DEFAULT_ASSET;
const dexSeparator = trimmed.indexOf(":");
if (dexSeparator > 0) {
const dex = trimmed.slice(0, dexSeparator).trim().toLowerCase();
const symbol = trimmed.slice(dexSeparator + 1).trim().toUpperCase();
return dex && symbol ? `${dex}:${symbol}` : trimmed;
}
return trimmed.toUpperCase();
}
export function readConfig(): SignalBotConfig {
const envConfig = parseHyperliquidJson(process.env[CONFIG_ENV] ?? null);
const parsed = configSchema.safeParse(envConfig);
const input = parsed.success ? parsed.data : {};
const resolution = input.resolution ?? DEFAULT_RESOLUTION;
const allocationMode: SignalBotConfig["allocationMode"] = "fixed";
const resolvedIndicators: SignalBotConfig["indicators"] =
input.indicators && input.indicators.length > 0
? (input.indicators as SignalBotConfig["indicators"])
: ["rsi"];
const amountUsd = input.amountUsd ?? DEFAULT_AMOUNT_USD;
const schedule = {
cron: input.schedule?.cron ?? DEFAULT_SIGNAL_SCHEDULE_CRON,
enabled: input.schedule?.enabled ?? false,
notifyEmail: input.schedule?.notifyEmail ?? false,
};
const base = {
platform: "hyperliquid" as const,
allocationMode,
amountUsd,
schedule,
resolution,
countBack: input.countBack ?? DEFAULT_COUNT_BACK,
};
const execution =
input.execution
? {
enabled: input.execution.enabled ?? false,
environment: input.execution.environment ?? DEFAULT_EXECUTION_ENV,
mode: input.execution.mode ?? DEFAULT_EXECUTION_MODE,
slippageBps: clampHyperliquidInt(
input.execution.slippageBps,
0,
500,
DEFAULT_SLIPPAGE_BPS,
),
...(input.execution.symbol ? { symbol: normalizeConfigMarketSymbol(input.execution.symbol) } : {}),
...(input.execution.size ? { size: input.execution.size } : {}),
...(input.execution.leverage
? { leverage: input.execution.leverage }
: {}),
...(input.execution.indicator
? { indicator: input.execution.indicator }
: {}),
}
: undefined;
const price = input.price
? {
...(input.price.rsiPreset ? { rsiPreset: input.price.rsiPreset } : {}),
...(input.price.rsi
? {
rsi: {
overbought: input.price.rsi?.overbought ?? DEFAULT_RSI_OVERBOUGHT,
oversold: input.price.rsi?.oversold ?? DEFAULT_RSI_OVERSOLD,
},
}
: {}),
...(input.price.movingAverage
? {
movingAverage: {
type: input.price.movingAverage.type ?? "sma",
period: input.price.movingAverage.period ?? DEFAULT_SMA_PERIOD,
},
}
: {}),
...(input.price.maCross
? {
maCross: {
type: input.price.maCross.type ?? "sma",
fastPeriod: input.price.maCross.fastPeriod ?? DEFAULT_MA_CROSS_FAST,
slowPeriod: input.price.maCross.slowPeriod ?? DEFAULT_MA_CROSS_SLOW,
},
}
: {}),
...(input.price.bollinger
? {
bollinger: {
period: input.price.bollinger.period ?? DEFAULT_BB_PERIOD,
stdDev: input.price.bollinger.stdDev ?? DEFAULT_BB_STD_DEV,
},
}
: {}),
...(input.price.donchian
? {
donchian: {
period: input.price.donchian.period ?? DEFAULT_DONCHIAN_PERIOD,
},
}
: {}),
}
: undefined;
return {
...base,
signalType: "price",
asset: normalizeConfigMarketSymbol(input.asset),
indicators: resolvedIndicators,
...(execution ? { execution } : {}),
...(price ? { price } : {}),
};
}
export function resolveScheduleConfig(config: SignalBotConfig): ScheduleConfig {
return config.schedule;
}
export function resolveProfileAssetSymbols(config: SignalBotConfig): string[] {
const fallback = config.execution?.symbol ?? config.asset ?? DEFAULT_ASSET;
const normalizedFallback = fallback.trim();
return normalizedFallback ? [normalizedFallback] : [];
}
export function resolveProfileAssets(config: SignalBotConfig): Array<{
venue: "hyperliquid";
chain: "hyperliquid" | "hyperliquid-testnet";
assetSymbols: string[];
pair?: string;
leverage?: number;
}> {
const environment = config.execution?.environment ?? DEFAULT_EXECUTION_ENV;
const symbols = resolveProfileAssetSymbols(config)
.map((symbol) => symbol.trim())
.filter((symbol) => symbol.length > 0);
if (symbols.length === 0) {
return [];
}
return [
{
venue: "hyperliquid",
chain:
environment === "testnet"
? ("hyperliquid-testnet" as const)
: ("hyperliquid" as const),
assetSymbols: Array.from(new Set(symbols)),
...(typeof config.execution?.leverage === "number"
? { leverage: config.execution.leverage }
: {}),
},
];
}