openpondai/agents/rsi-signal-bot
OpenTool app
typescript
import { z } from "zod";
import {
buildHyperliquidProfileAssets,
clampHyperliquidInt,
parseHyperliquidJson,
normalizeHyperliquidBaseSymbol,
resolveHyperliquidIntervalCron,
resolveHyperliquidScheduleEvery,
resolveHyperliquidScheduleUnit,
} from "opentool/adapters/hyperliquid";
export type ScheduleConfig = {
cron: string;
enabled: boolean;
notifyEmail: boolean;
};
export type IndicatorType = "rsi";
export type ExecutionConfig = {
enabled?: boolean;
environment?: "testnet" | "mainnet";
symbol?: string;
mode?: "long-only" | "long-short";
size?: number;
leverage?: number;
slippageBps?: number;
indicator?: IndicatorType;
};
export type SignalBotConfig = {
configVersion?: number;
platform: "hyperliquid";
signalType: "price";
asset: string;
indicators: IndicatorType[];
cadence: "hourly";
hourlyInterval: number;
scheduleEvery: number;
scheduleUnit: "minutes" | "hours";
allocationMode: "percent_equity" | "fixed";
percentOfEquity: number;
maxPercentOfEquity: number;
amountUsd?: number;
schedule: ScheduleConfig;
resolution: "1" | "5" | "15" | "30" | "60" | "240" | "1D" | "1W";
countBack: number;
execution?: ExecutionConfig;
price?: {
rsiPreset?: string;
rsi?: { overbought: number; oversold: number };
};
};
const CONFIG_ENV = "OPENTOOL_PUBLIC_HL_RSI_SIGNAL_BOT_CONFIG";
const TEMPLATE_CONFIG_VERSION = 3;
const TEMPLATE_CONFIG_ENV_VAR = CONFIG_ENV;
const DEFAULT_ASSET = "BTC";
const DEFAULT_PERCENT_OF_EQUITY = 2;
const DEFAULT_MAX_PERCENT_OF_EQUITY = 10;
const DEFAULT_AMOUNT_USD = 200;
const DEFAULT_SCHEDULE_EVERY = 1;
const DEFAULT_SCHEDULE_UNIT: SignalBotConfig["scheduleUnit"] = "hours";
const DEFAULT_RESOLUTION: SignalBotConfig["resolution"] = "60";
const DEFAULT_COUNT_BACK = 240;
export const DEFAULT_EXECUTION_ENV: NonNullable<ExecutionConfig["environment"]> = "mainnet";
export const DEFAULT_EXECUTION_MODE: NonNullable<ExecutionConfig["mode"]> = "long-only";
export const DEFAULT_SLIPPAGE_BPS = 50;
export const DEFAULT_RSI_PRESET = "balanced";
export const DEFAULT_RSI_OVERBOUGHT = 70;
export const DEFAULT_RSI_OVERSOLD = 30;
export const RSI_PRESETS: Record<string, { overbought: number; oversold: number }> = {
balanced: { overbought: 70, oversold: 30 },
tighter: { overbought: 65, oversold: 35 },
wider: { overbought: 80, oversold: 20 },
};
const indicatorSchema = z.literal("rsi");
const configSchema = z
.object({
platform: z.literal("hyperliquid").optional(),
signalType: z.literal("price").optional(),
asset: z.string().min(1).optional(),
indicators: z.array(indicatorSchema).optional(),
scheduleEvery: z.number().int().min(1).max(59).optional(),
scheduleUnit: z.enum(["minutes", "hours"]).optional(),
cadence: z.literal("hourly").optional(),
hourlyInterval: z.number().int().min(1).max(24).optional(),
allocationMode: z.enum(["percent_equity", "fixed", "percent"]).optional(),
percentOfEquity: z.number().positive().optional(),
maxPercentOfEquity: z.number().positive().optional(),
amountUsd: z.number().positive().optional(),
schedule: z
.object({
cron: z.string().min(1).optional(),
enabled: z.boolean().optional(),
notifyEmail: z.boolean().optional(),
})
.optional(),
resolution: z.enum(["1", "5", "15", "30", "60", "240", "1D", "1W"]).optional(),
countBack: z.number().int().min(50).max(5000).optional(),
price: z
.object({
rsiPreset: z.string().optional(),
rsi: z
.object({
overbought: z.number().optional(),
oversold: z.number().optional(),
})
.optional(),
})
.optional(),
execution: z
.object({
enabled: z.boolean().optional(),
environment: z.enum(["testnet", "mainnet"]).optional(),
symbol: z.string().optional(),
mode: z.enum(["long-only", "long-short"]).optional(),
size: z.number().positive().optional(),
leverage: z.number().positive().optional(),
slippageBps: z.number().min(0).max(500).optional(),
indicator: indicatorSchema.optional(),
})
.optional(),
})
.partial();
const TEMPLATE_CONFIG_DEFAULTS: SignalBotConfig = {
configVersion: TEMPLATE_CONFIG_VERSION,
platform: "hyperliquid",
signalType: "price",
asset: DEFAULT_ASSET,
indicators: ["rsi"],
cadence: "hourly",
hourlyInterval: DEFAULT_SCHEDULE_EVERY,
scheduleEvery: DEFAULT_SCHEDULE_EVERY,
scheduleUnit: DEFAULT_SCHEDULE_UNIT,
allocationMode: "fixed",
percentOfEquity: DEFAULT_PERCENT_OF_EQUITY,
maxPercentOfEquity: DEFAULT_MAX_PERCENT_OF_EQUITY,
amountUsd: DEFAULT_AMOUNT_USD,
schedule: {
cron: resolveHyperliquidIntervalCron(DEFAULT_SCHEDULE_EVERY, DEFAULT_SCHEDULE_UNIT),
enabled: false,
notifyEmail: false,
},
resolution: DEFAULT_RESOLUTION,
countBack: DEFAULT_COUNT_BACK,
price: {
rsiPreset: DEFAULT_RSI_PRESET,
rsi: {
overbought: DEFAULT_RSI_OVERBOUGHT,
oversold: DEFAULT_RSI_OVERSOLD,
},
},
execution: {
enabled: false,
environment: DEFAULT_EXECUTION_ENV,
mode: DEFAULT_EXECUTION_MODE,
indicator: "rsi",
},
};
const TEMPLATE_CONFIG_SCHEMA = {
type: "object",
"x-budget": {
modeField: "allocationMode",
defaultMode: "fixed",
title: "Budget & allocation",
description: "Core exposure settings are shown first.",
modes: {
fixed: {
fields: ["amountUsd"],
},
percent_equity: {
fields: ["percentOfEquity", "maxPercentOfEquity"],
},
},
},
properties: {
configVersion: {
type: "number",
title: "Config version",
description: "Internal version for rsi-signal-bot defaults.",
readOnly: true,
"x-hidden": true,
"x-section": "Meta",
"x-order": 1000,
},
platform: {
type: "string",
enum: ["hyperliquid"],
title: "Platform",
readOnly: true,
"x-hidden": true,
"x-section": "Meta",
"x-order": 1001,
},
signalType: {
type: "string",
enum: ["price"],
title: "Signal type",
readOnly: true,
"x-hidden": true,
"x-section": "Meta",
"x-order": 1002,
},
asset: {
type: "string",
title: "Asset",
description:
"Default Hyperliquid perp symbol. Use execution symbol for spot pairs (BASE-QUOTE or BASE/QUOTE) or HIP-3 symbols (dex:BASE).",
"x-section": "Strategy",
"x-order": 1,
},
scheduleEvery: {
type: "number",
title: "Run every",
description: "How often the bot runs.",
minimum: 1,
maximum: 59,
"x-step": 1,
"x-section": "Strategy",
"x-order": 2,
},
scheduleUnit: {
type: "string",
enum: ["minutes", "hours"],
title: "Schedule unit",
description: "Run interval unit.",
"x-enumLabels": ["Minutes", "Hours"],
"x-section": "Strategy",
"x-order": 3,
},
allocationMode: {
type: "string",
enum: ["percent_equity", "fixed"],
title: "Allocation mode",
description: "Position sizing method.",
"x-enumLabels": ["Percent of equity", "Fixed USD"],
"x-section": "Risk",
"x-order": 1,
},
percentOfEquity: {
type: "number",
title: "Percent of equity",
description: "Target percent of account value per run.",
minimum: 0.01,
"x-unit": "%",
"x-format": "percent",
"x-step": 0.1,
"x-visibleIf": { field: "allocationMode", equals: "percent_equity" },
"x-section": "Risk",
"x-order": 2,
},
maxPercentOfEquity: {
type: "number",
title: "Max percent of equity",
description: "Upper cap for percent-based sizing.",
minimum: 0.01,
"x-unit": "%",
"x-format": "percent",
"x-step": 0.1,
"x-visibleIf": { field: "allocationMode", equals: "percent_equity" },
"x-section": "Risk",
"x-order": 3,
},
amountUsd: {
type: "number",
title: "Fixed amount",
description: "USD notional per run when allocation mode is fixed.",
minimum: 1,
"x-unit": "USD",
"x-format": "currency",
"x-step": 1,
"x-visibleIf": { field: "allocationMode", equals: "fixed" },
"x-section": "Risk",
"x-order": 4,
},
schedule: {
type: "object",
title: "Schedule",
description: "Cron and notification settings.",
"x-section": "Schedule",
properties: {
cron: {
type: "string",
title: "Cron expression",
description: "Cron expression for automated runs.",
"x-order": 1,
},
enabled: {
type: "boolean",
title: "Enabled",
description: "Enable scheduled runs.",
"x-order": 2,
},
notifyEmail: {
type: "boolean",
title: "Notify email",
description: "Send an email after scheduled runs.",
"x-order": 3,
},
},
},
resolution: {
type: "string",
enum: ["1", "5", "15", "30", "60", "240", "1D", "1W"],
title: "Resolution",
description: "Bar timeframe for price data.",
"x-section": "Price model",
"x-order": 1,
},
countBack: {
type: "number",
title: "Bars to fetch",
description: "Number of historical bars for RSI calculations.",
minimum: 50,
maximum: 5000,
"x-step": 1,
"x-section": "Price model",
"x-order": 2,
},
price: {
type: "object",
title: "RSI settings",
description: "RSI-specific tuning.",
"x-section": "Price model",
properties: {
rsiPreset: {
type: "string",
title: "RSI preset",
description: "Preset thresholds for RSI.",
"x-order": 1,
},
rsi: {
type: "object",
title: "RSI thresholds",
description: "Custom RSI overbought/oversold values.",
properties: {
overbought: {
type: "number",
title: "RSI overbought",
minimum: 1,
maximum: 100,
"x-order": 2,
},
oversold: {
type: "number",
title: "RSI oversold",
minimum: 1,
maximum: 100,
"x-order": 3,
},
},
},
},
},
execution: {
type: "object",
title: "Execution",
description: "Live trading controls and routing.",
"x-section": "Execution",
properties: {
enabled: {
type: "boolean",
title: "Execution enabled",
description: "Submit live orders when signals trigger.",
"x-order": 1,
},
environment: {
type: "string",
enum: ["testnet", "mainnet"],
title: "Environment",
description: "Execution environment for Hyperliquid.",
"x-enumLabels": ["Testnet", "Mainnet"],
"x-order": 2,
},
symbol: {
type: "string",
title: "Execution symbol",
description:
"Optional exact Hyperliquid symbol override. Use BASE-QUOTE or BASE/QUOTE for spot, bare BASE for perps, or dex:BASE for HIP-3.",
"x-order": 3,
},
mode: {
type: "string",
enum: ["long-only", "long-short"],
title: "Execution mode",
description: "Allow long-only or long-short positioning.",
"x-enumLabels": ["Long only", "Long + short"],
"x-order": 4,
},
size: {
type: "number",
title: "Explicit size",
description: "Optional explicit position size override.",
minimum: 0,
"x-step": 0.0001,
"x-order": 5,
},
leverage: {
type: "number",
title: "Leverage",
description: "Target leverage for perpetual execution.",
minimum: 1,
maximum: 50,
"x-unit": "x",
"x-step": 1,
"x-order": 6,
},
slippageBps: {
type: "number",
title: "Execution slippage",
description: "Max slippage for signal-driven orders.",
minimum: 0,
maximum: 500,
"x-unit": "bps",
"x-format": "bps",
"x-step": 1,
"x-order": 7,
},
},
},
},
};
export const RSI_SIGNAL_BOT_TEMPLATE_CONFIG = {
version: TEMPLATE_CONFIG_VERSION,
schema: TEMPLATE_CONFIG_SCHEMA,
defaults: TEMPLATE_CONFIG_DEFAULTS,
envVar: TEMPLATE_CONFIG_ENV_VAR,
};
export function readConfig(): SignalBotConfig {
const envConfig = parseHyperliquidJson(process.env[CONFIG_ENV] ?? null);
const parsed = configSchema.safeParse(envConfig);
const input = parsed.success ? parsed.data : {};
const scheduleEvery = resolveHyperliquidScheduleEvery(input.scheduleEvery, {
min: 1,
max: 59,
fallback: DEFAULT_SCHEDULE_EVERY,
});
const scheduleUnit = resolveHyperliquidScheduleUnit(input.scheduleUnit, DEFAULT_SCHEDULE_UNIT);
const hourlyInterval = scheduleUnit === "hours" ? scheduleEvery : 1;
const allocationMode: SignalBotConfig["allocationMode"] =
input.allocationMode === "percent_equity" || input.allocationMode === "percent"
? "percent_equity"
: "fixed";
const percentOfEquity = input.percentOfEquity ?? DEFAULT_PERCENT_OF_EQUITY;
const maxPercentOfEquity =
input.maxPercentOfEquity ?? DEFAULT_MAX_PERCENT_OF_EQUITY;
const amountUsd =
allocationMode === "fixed"
? input.amountUsd ?? DEFAULT_AMOUNT_USD
: undefined;
const schedule = {
cron: input.schedule?.cron ?? resolveHyperliquidIntervalCron(scheduleEvery, scheduleUnit),
enabled: input.schedule?.enabled ?? false,
notifyEmail: input.schedule?.notifyEmail ?? false,
};
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: input.execution.symbol } : {}),
...(input.execution.size ? { size: input.execution.size } : {}),
...(input.execution.leverage ? { leverage: input.execution.leverage } : {}),
indicator: "rsi" as const,
}
: undefined;
const price = input.price
? {
...(input.price.rsiPreset ? { rsiPreset: input.price.rsiPreset } : {}),
...(input.price.rsi || input.price.rsiPreset
? {
rsi: {
overbought: input.price.rsi?.overbought ?? DEFAULT_RSI_OVERBOUGHT,
oversold: input.price.rsi?.oversold ?? DEFAULT_RSI_OVERSOLD,
},
}
: {}),
}
: undefined;
return {
platform: "hyperliquid",
signalType: "price",
asset: (input.asset ?? DEFAULT_ASSET).toUpperCase(),
indicators: ["rsi"],
cadence: "hourly",
hourlyInterval,
scheduleEvery,
scheduleUnit,
allocationMode,
percentOfEquity,
maxPercentOfEquity,
...(amountUsd ? { amountUsd } : {}),
schedule,
resolution: input.resolution ?? DEFAULT_RESOLUTION,
countBack: input.countBack ?? DEFAULT_COUNT_BACK,
...(execution ? { execution } : {}),
...(price ? { price } : {}),
};
}
export function resolveScheduleConfig(config: SignalBotConfig): ScheduleConfig {
return config.schedule;
}
function resolveProfileAssetSymbols(config: SignalBotConfig): string[] {
const fallback = config.execution?.symbol ?? config.asset ?? DEFAULT_ASSET;
const normalizedFallback = normalizeHyperliquidBaseSymbol(fallback);
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 buildHyperliquidProfileAssets({
environment,
assets: [
{
assetSymbols: symbols,
leverage: config.execution?.leverage,
},
],
});
}