use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndicatorCategory {
Overlay,
Oscillator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ParamKind {
Integer,
Float,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ParamSpec {
pub name: &'static str,
pub kind: ParamKind,
pub default: f64,
pub min: f64,
pub max: f64,
}
impl ParamSpec {
#[must_use]
pub const fn int(name: &'static str, default: f64, min: f64, max: f64) -> Self {
Self {
name,
kind: ParamKind::Integer,
default,
min,
max,
}
}
#[must_use]
pub const fn float(name: &'static str, default: f64, min: f64, max: f64) -> Self {
Self {
name,
kind: ParamKind::Float,
default,
min,
max,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IndicatorDescriptor {
pub id: &'static str,
pub display_name: &'static str,
pub category: IndicatorCategory,
pub params: Vec<ParamSpec>,
}
impl IndicatorDescriptor {
fn new(
id: &'static str,
display_name: &'static str,
category: IndicatorCategory,
params: Vec<ParamSpec>,
) -> Self {
Self {
id,
display_name,
category,
params,
}
}
}
#[must_use]
pub fn catalog() -> Vec<IndicatorDescriptor> {
use IndicatorCategory::{Oscillator, Overlay};
use ParamSpec as P;
const PMIN: f64 = 1.0;
const PMAX: f64 = 500.0;
vec![
IndicatorDescriptor::new(
"ema",
"EMA",
Overlay,
vec![P::int("period", 20.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"sma",
"SMA",
Overlay,
vec![P::int("period", 20.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"wma",
"WMA",
Overlay,
vec![P::int("period", 14.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"macd",
"MACD",
Oscillator,
vec![
P::int("fast_period", 12.0, PMIN, PMAX),
P::int("slow_period", 26.0, PMIN, PMAX),
P::int("signal_period", 9.0, PMIN, PMAX),
],
),
IndicatorDescriptor::new(
"atr",
"ATR",
Oscillator,
vec![P::int("period", 14.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"linearregression",
"LinearRegression",
Overlay,
vec![P::int("period", 14.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"parabolicsar",
"ParabolicSAR",
Overlay,
vec![
P::float("step", 0.02, 0.001, 0.5),
P::float("max_step", 0.2, 0.01, 1.0),
],
),
IndicatorDescriptor::new(
"rsi",
"RSI",
Oscillator,
vec![P::int("period", 14.0, 2.0, PMAX)],
),
IndicatorDescriptor::new(
"schafftrendcycle",
"SchaffTrendCycle",
Oscillator,
vec![
P::int("short_ema", 12.0, PMIN, PMAX),
P::int("long_ema", 26.0, PMIN, PMAX),
P::int("stoch_period", 10.0, PMIN, PMAX),
P::int("signal_period", 3.0, PMIN, PMAX),
],
),
IndicatorDescriptor::new(
"stochastic",
"Stochastic",
Oscillator,
vec![
P::int("k_period", 14.0, PMIN, PMAX),
P::int("smooth_k", 3.0, PMIN, PMAX),
P::int("d_period", 3.0, PMIN, PMAX),
],
),
IndicatorDescriptor::new(
"stochasticrsi",
"StochasticRSI",
Oscillator,
vec![
P::int("rsi_period", 14.0, 2.0, PMAX),
P::int("stoch_period", 14.0, PMIN, PMAX),
P::int("k_smooth", 3.0, PMIN, PMAX),
P::int("d_period", 3.0, PMIN, PMAX),
],
),
IndicatorDescriptor::new(
"williamsr",
"WilliamsR",
Oscillator,
vec![P::int("period", 14.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"bollingerbands",
"BollingerBands",
Overlay,
vec![
P::int("period", 20.0, PMIN, PMAX),
P::float("std_dev", 2.0, 0.1, 10.0),
],
),
IndicatorDescriptor::new(
"choppinessindex",
"ChoppinessIndex",
Oscillator,
vec![P::int("period", 14.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"elderrayindex",
"ElderRayIndex",
Oscillator,
vec![P::int("fast_period", 14.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"keltnerchannels",
"KeltnerChannels",
Overlay,
vec![
P::int("period", 20.0, PMIN, PMAX),
P::float("multiplier", 2.0, 0.1, 10.0),
],
),
IndicatorDescriptor::new(
"marketcycle",
"MarketCycle",
Oscillator,
vec![P::int("momentum_period", 1.0, PMIN, PMAX)],
),
IndicatorDescriptor::new("adl", "ADL", Oscillator, vec![]),
IndicatorDescriptor::new(
"chaikinmoneyflow",
"ChaikinMoneyFlow",
Oscillator,
vec![P::int("period", 20.0, PMIN, PMAX)],
),
IndicatorDescriptor::new(
"vwap",
"VWAP",
Overlay,
vec![P::int("period", 0.0, 0.0, PMAX)],
),
IndicatorDescriptor::new(
"vzo",
"VZO",
Oscillator,
vec![P::int("period", 14.0, PMIN, PMAX)],
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn catalog_is_non_empty() {
assert!(!catalog().is_empty());
}
#[test]
fn ids_are_unique() {
let cat = catalog();
let mut seen = HashSet::new();
for d in &cat {
assert!(seen.insert(d.id), "duplicate indicator id: {}", d.id);
}
}
#[test]
fn defaults_within_bounds() {
for d in catalog() {
for p in &d.params {
assert!(
p.min <= p.max,
"{}::{} has min {} > max {}",
d.id,
p.name,
p.min,
p.max
);
assert!(
p.default >= p.min && p.default <= p.max,
"{}::{} default {} out of [{}, {}]",
d.id,
p.name,
p.default,
p.min,
p.max
);
}
}
}
#[test]
fn ids_match_runtime_registry() {
let reg = crate::registry::registry();
for d in catalog() {
assert!(
reg.contains(d.id),
"descriptor id `{}` is not registered in the runtime registry",
d.id
);
}
}
#[test]
fn serializes_to_expected_json_shape() {
let rsi = catalog()
.into_iter()
.find(|d| d.id == "rsi")
.expect("rsi in catalog");
let json = serde_json::to_value(&rsi).unwrap();
assert_eq!(json["id"], "rsi");
assert_eq!(json["display_name"], "RSI");
assert_eq!(json["category"], "Oscillator");
assert_eq!(json["params"][0]["name"], "period");
assert_eq!(json["params"][0]["kind"], "Integer");
assert_eq!(json["params"][0]["default"], 14.0);
}
}