openpondai/agents/signal-bot
OpenTool app
typescript
import {
clampHyperliquidFloat,
clampHyperliquidInt,
} from "opentool/adapters/hyperliquid";
import {
DEFAULT_BB_PERIOD,
DEFAULT_BB_STD_DEV,
DEFAULT_DONCHIAN_PERIOD,
DEFAULT_EMA_PERIOD,
DEFAULT_MA_CROSS_FAST,
DEFAULT_MA_CROSS_SLOW,
DEFAULT_RSI_OVERBOUGHT,
DEFAULT_RSI_OVERSOLD,
DEFAULT_RSI_PRESET,
DEFAULT_SMA_PERIOD,
LIMITS,
RSI_PRESETS,
type IndicatorType,
type SignalBotConfig,
} from "../config";
import { computeBollinger } from "../indicators/computeBollinger";
import { computeDonchian } from "../indicators/computeDonchian";
import { computeEma } from "../indicators/computeEma";
import { computeMacd } from "../indicators/computeMacd";
import { computeMaCross } from "../indicators/computeMaCross";
import { computeRsi } from "../indicators/computeRsi";
import { computeSma } from "../indicators/computeSma";
import type { Bar } from "../types";
export function resolveRsiThresholds(config: SignalBotConfig) {
const priceConfig = config.price ?? {};
const rsiPreset = priceConfig.rsiPreset ?? DEFAULT_RSI_PRESET;
const rsiDefaults = RSI_PRESETS[rsiPreset] ?? RSI_PRESETS[DEFAULT_RSI_PRESET];
const overbought = clampHyperliquidFloat(
priceConfig.rsi?.overbought,
1,
100,
rsiDefaults?.overbought ?? DEFAULT_RSI_OVERBOUGHT,
);
const oversold = clampHyperliquidFloat(
priceConfig.rsi?.oversold,
1,
100,
rsiDefaults?.oversold ?? DEFAULT_RSI_OVERSOLD,
);
return { rsiPreset, overbought, oversold };
}
export function buildIndicatorDecisionOutput(params: {
config: SignalBotConfig;
bars: Bar[];
indicator: IndicatorType;
}): Record<string, unknown> {
const { config, bars, indicator } = params;
const closes = bars.map((bar) => bar.close);
const currentPrice = closes[closes.length - 1] ?? 0;
const output: Record<string, unknown> = {};
const priceConfig = config.price ?? {};
if (indicator === "rsi") {
const { overbought, oversold } = resolveRsiThresholds(config);
const value = computeRsi(closes);
const signal =
value === null
? "unknown"
: value >= overbought
? "overbought"
: value <= oversold
? "oversold"
: "neutral";
output.rsi = { value, signal, overbought, oversold };
return output;
}
if (indicator === "macd") {
const result = computeMacd(closes);
const signal =
result === null
? "unknown"
: result.histogram > 0
? "bullish"
: result.histogram < 0
? "bearish"
: "neutral";
output.macd = result
? { ...result, signal }
: { macd: null, signalLine: null, histogram: null, signal };
return output;
}
if (indicator === "bb") {
const period = clampHyperliquidInt(
priceConfig.bollinger?.period,
LIMITS.bollinger.periodMin,
LIMITS.bollinger.periodMax,
DEFAULT_BB_PERIOD,
);
const stdDev = clampHyperliquidFloat(
priceConfig.bollinger?.stdDev,
LIMITS.bollinger.stdDevMin,
LIMITS.bollinger.stdDevMax,
DEFAULT_BB_STD_DEV,
);
const result = computeBollinger(closes, period, stdDev);
const signal =
result === null
? "unknown"
: currentPrice > result.upper
? "overbought"
: currentPrice < result.lower
? "oversold"
: "neutral";
output.bb = result
? { ...result, signal, period, stdDev }
: { upper: null, middle: null, lower: null, signal, period, stdDev };
return output;
}
if (indicator === "sma") {
const period = clampHyperliquidInt(
priceConfig.movingAverage?.type === "sma"
? priceConfig.movingAverage?.period
: DEFAULT_SMA_PERIOD,
LIMITS.movingAverage.min,
LIMITS.movingAverage.max,
DEFAULT_SMA_PERIOD,
);
const value = computeSma(closes, period);
const signal =
value === null
? "unknown"
: currentPrice > value
? "above"
: currentPrice < value
? "below"
: "at";
output.sma = { value, period, signal };
return output;
}
if (indicator === "ema") {
const period = clampHyperliquidInt(
priceConfig.movingAverage?.type === "ema"
? priceConfig.movingAverage?.period
: DEFAULT_EMA_PERIOD,
LIMITS.movingAverage.min,
LIMITS.movingAverage.max,
DEFAULT_EMA_PERIOD,
);
const value = computeEma(closes, period);
const signal =
value === null
? "unknown"
: currentPrice > value
? "above"
: currentPrice < value
? "below"
: "at";
output.ema = { value, period, signal };
return output;
}
if (indicator === "ma-cross") {
const type = priceConfig.maCross?.type ?? "sma";
const slowPeriod = clampHyperliquidInt(
priceConfig.maCross?.slowPeriod,
LIMITS.maCross.slowMin,
LIMITS.maCross.slowMax,
DEFAULT_MA_CROSS_SLOW,
);
const fastCandidate = clampHyperliquidInt(
priceConfig.maCross?.fastPeriod,
LIMITS.maCross.fastMin,
LIMITS.maCross.fastMax,
DEFAULT_MA_CROSS_FAST,
);
const fastPeriod =
fastCandidate >= slowPeriod
? Math.max(
LIMITS.maCross.fastMin,
Math.min(slowPeriod - 1, LIMITS.maCross.fastMax),
)
: fastCandidate;
const result = computeMaCross(closes, type, fastPeriod, slowPeriod);
output.maCross = result
? { ...result, type, fastPeriod, slowPeriod }
: {
type,
fastPeriod,
slowPeriod,
fast: null,
slow: null,
signal: "unknown",
};
return output;
}
const period = clampHyperliquidInt(
priceConfig.donchian?.period,
LIMITS.donchian.min,
LIMITS.donchian.max,
DEFAULT_DONCHIAN_PERIOD,
);
const result = computeDonchian(bars, period);
const signal =
result === null
? "unknown"
: currentPrice > result.upper
? "bullish"
: currentPrice < result.lower
? "bearish"
: "neutral";
output.donchian = result
? { ...result, period, signal }
: { upper: null, lower: null, period, signal };
return output;
}
export function buildSignalIndicatorsOutput(config: SignalBotConfig, bars: Bar[]) {
const output: Record<string, unknown> = {};
for (const indicator of config.indicators) {
Object.assign(
output,
buildIndicatorDecisionOutput({
config,
bars,
indicator,
}),
);
}
return output;
}