use std::collections::HashMap;
use indicators::{error::IndicatorError, registry::registry, types::Candle};
fn rising_candles(n: usize) -> Vec<Candle> {
(0..n)
.map(|i| {
let c = 100.0 + i as f64 * 0.25;
Candle {
time: i64::try_from(i).unwrap() * 60_000,
open: c - 0.1,
high: c + 0.2,
low: c - 0.2,
close: c,
volume: 500.0 + (i % 7) as f64 * 100.0,
}
})
.collect()
}
#[test]
fn registry_is_non_empty() {
let names = registry().list();
assert!(
!names.is_empty(),
"registry must have at least one registered indicator"
);
}
#[test]
fn registry_contains_core_indicators() {
let reg = registry();
for name in &[
"sma",
"ema",
"wma",
"macd",
"atr",
"rsi",
"stochastic",
"williamsr",
"bollingerbands",
"keltnerchannels",
"vwap",
"adl",
"engine",
"signal",
] {
assert!(
reg.contains(name),
"registry is missing expected indicator: '{name}'"
);
}
}
#[test]
fn all_indicators_create_with_empty_params() {
let reg = registry();
let names = reg.list();
let empty: HashMap<String, String> = HashMap::new();
for name in &names {
let result = reg.create(name, &empty);
assert!(
result.is_ok(),
"indicator '{name}' failed to create with empty params: {:?}",
result.err()
);
}
}
#[test]
fn all_indicators_calculate_does_not_panic_on_required_len() {
let reg = registry();
let names = reg.list();
let empty: HashMap<String, String> = HashMap::new();
let candles = rising_candles(350);
for name in &names {
let Ok(indicator) = reg.create(name, &empty) else {
continue;
};
let needed = indicator.required_len();
let slice = if needed <= candles.len() {
&candles[..needed.max(1)]
} else {
&candles[..]
};
let _ = std::panic::catch_unwind(|| {
if let Ok(ind) = reg.create(name, &empty) {
let _ = ind.calculate(slice);
}
});
}
}
#[test]
fn all_indicators_calculate_succeeds_with_ample_data() {
let reg = registry();
let names = reg.list();
let empty: HashMap<String, String> = HashMap::new();
let candles = rising_candles(350);
for name in &names {
let Ok(indicator) = reg.create(name, &empty) else {
continue;
};
let needed = indicator.required_len();
let result = indicator.calculate(&candles[..needed.max(2)]);
assert!(
result.is_ok(),
"indicator '{name}' returned Err on {needed} candles: {:?}",
result.err()
);
}
}
#[test]
fn unknown_name_returns_unknown_indicator_error() {
let empty: HashMap<String, String> = HashMap::new();
let err = registry()
.create("this_indicator_does_not_exist_xyz", &empty)
.unwrap_err();
assert!(
matches!(err, IndicatorError::UnknownIndicator { .. }),
"expected UnknownIndicator, got {err:?}"
);
}
#[test]
fn empty_name_returns_unknown_indicator_error() {
let empty: HashMap<String, String> = HashMap::new();
let err = registry().create("", &empty).unwrap_err();
assert!(
matches!(err, IndicatorError::UnknownIndicator { .. }),
"expected UnknownIndicator for empty name, got {err:?}"
);
}
#[test]
fn registry_lookup_is_case_insensitive() {
let reg = registry();
let empty: HashMap<String, String> = HashMap::new();
for name in &["sma", "SMA", "Sma", "sMa", "SMA"] {
assert!(
reg.create(name, &empty).is_ok(),
"case-insensitive lookup failed for '{name}'"
);
}
}
#[test]
fn registry_contains_is_case_insensitive() {
let reg = registry();
assert!(reg.contains("sma"));
assert!(reg.contains("SMA"));
assert!(reg.contains("Ema"));
assert!(reg.contains("MACD"));
}
#[test]
fn non_numeric_period_returns_invalid_parameter_error() {
let bad_params: HashMap<String, String> =
std::iter::once(("period".to_string(), "not_a_number".to_string())).collect();
for name in &["sma", "ema", "wma", "rsi", "atr"] {
let result = registry().create(name, &bad_params);
assert!(
result.is_err(),
"indicator '{name}' should reject non-numeric 'period'"
);
let err = result.unwrap_err();
assert!(
matches!(err, IndicatorError::InvalidParameter { .. }),
"indicator '{name}': expected InvalidParameter, got {err:?}"
);
}
}
#[test]
fn empty_string_period_returns_invalid_parameter_error() {
let bad_params: HashMap<String, String> =
std::iter::once(("period".to_string(), String::new())).collect();
for name in &["sma", "ema", "rsi"] {
let result = registry().create(name, &bad_params);
assert!(
result.is_err(),
"indicator '{name}' should reject empty string 'period'"
);
}
}
#[test]
fn float_period_string_returns_error() {
let bad_params: HashMap<String, String> =
std::iter::once(("period".to_string(), "14.5".to_string())).collect();
for name in &["sma", "ema", "rsi"] {
let result = registry().create(name, &bad_params);
assert!(
result.is_err(),
"indicator '{name}' should reject float string '14.5' for a usize param"
);
}
}
#[test]
fn random_string_params_do_not_panic() {
let garbage_values = [
"abc",
"!@#$%",
" ",
"\t\n",
"9999999999999999999999999999", "-1",
"0",
"1e10",
"NaN",
"inf",
"-inf",
"true",
"null",
"[]",
"{}",
"''",
];
let reg = registry();
let names = reg.list();
for name in &names {
for val in &garbage_values {
let params: HashMap<String, String> =
std::iter::once(("period".to_string(), val.to_string())).collect();
let result = std::panic::catch_unwind(|| {
let _ = registry().create(name, ¶ms);
});
assert!(
result.is_ok(),
"indicator '{name}' panicked on param value '{val}'"
);
}
}
}
#[test]
fn unknown_param_keys_are_ignored() {
let extra_params: HashMap<String, String> = [
("this_key_does_not_exist".to_string(), "42".to_string()),
("another_bogus_key".to_string(), "hello".to_string()),
]
.into_iter()
.collect();
let reg = registry();
for name in &["sma", "ema", "rsi", "macd"] {
let result = reg.create(name, &extra_params);
assert!(
result.is_ok(),
"indicator '{name}' should ignore unknown param keys; got {result:?}"
);
}
}
#[test]
fn period_of_one_is_accepted() {
let params: HashMap<String, String> =
std::iter::once(("period".to_string(), "1".to_string())).collect();
let reg = registry();
for name in &["sma", "ema", "wma", "rsi"] {
let result = reg.create(name, ¶ms);
assert!(
result.is_ok(),
"indicator '{name}' should accept period=1; got {result:?}"
);
}
}
#[test]
fn large_period_does_not_panic() {
let params: HashMap<String, String> =
std::iter::once(("period".to_string(), "10000".to_string())).collect();
let reg = registry();
for name in ®.list() {
let result = std::panic::catch_unwind(|| {
let _ = registry().create(name, ¶ms);
});
assert!(
result.is_ok(),
"indicator '{name}' panicked with period=10000"
);
}
}
#[test]
fn all_indicators_output_at_least_one_column() {
let reg = registry();
let empty: HashMap<String, String> = HashMap::new();
let candles = rising_candles(350);
for name in ®.list() {
let Ok(indicator) = reg.create(name, &empty) else {
continue;
};
let needed = indicator.required_len();
if needed > candles.len() {
continue;
}
if let Ok(output) = indicator.calculate(&candles) {
let mut cols = output.columns();
assert!(
cols.next().is_some(),
"indicator '{name}' produced zero output columns"
);
}
}
}
#[test]
fn output_length_equals_input_length() {
let reg = registry();
let empty: HashMap<String, String> = HashMap::new();
let candles = rising_candles(350);
for name in ®.list() {
let Ok(indicator) = reg.create(name, &empty) else {
continue;
};
let needed = indicator.required_len();
if needed > candles.len() {
continue;
}
if let Ok(output) = indicator.calculate(&candles) {
for col in output.columns() {
assert_eq!(
output.get(col).unwrap().len(),
candles.len(),
"indicator '{name}' column '{col}' length mismatch"
);
}
}
}
}