use crate::catalog::{
ParamValue, ParamConstraint, ConstraintSet, ParamError,
};
use crate::bar_indicators::bar_indicator_id::BarIndicatorId;
use crate::bar_indicators::indicator_value::IndicatorValueKind;
use crate::data_loader::stream_kind::StreamKind;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SourceType {
#[default]
PriceOnly,
VolumeOnly,
PriceAndVolume,
}
impl SourceType {
pub fn requires_source_selection(&self) -> bool {
matches!(self, SourceType::PriceOnly)
}
pub fn as_str(&self) -> &'static str {
match self {
SourceType::PriceOnly => "price_only",
SourceType::VolumeOnly => "volume_only",
SourceType::PriceAndVolume => "price_and_volume",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"price_only" | "price" => Some(SourceType::PriceOnly),
"volume_only" | "volume" => Some(SourceType::VolumeOnly),
"price_and_volume" | "both" => Some(SourceType::PriceAndVolume),
_ => None,
}
}
}
impl fmt::Display for SourceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IndicatorCategory {
Momentum, SignalProcessing, Channels, Volatility, Average, Levels, Adaptive, Entropy, Volume, Kalman, TrendStop, Accumulation, Regression, Chaos, Ratio, Candles, Zigzag, Trend, Divergence, Clusters, Book, Position, Statistics, StatisticalScoring,
FundingAdvanced, OpenInterest, MarkPriceAdvanced, TickerAdvanced, Liquidations, TickAdvanced, BookAdvanced, Composites, Sentiment, IndexBasis, VolatilityAdvanced, Greeks, Stress, Microstructure, RiskFunding,
Custom,
Composite,
Experimental,
Unknown,
}
impl IndicatorCategory {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"momentum" => Self::Momentum,
"signal_processing" | "signalprocessing" => Self::SignalProcessing,
"channels" => Self::Channels,
"volatility" => Self::Volatility,
"average" | "averages" => Self::Average,
"levels" => Self::Levels,
"adaptive" => Self::Adaptive,
"entropy" => Self::Entropy,
"volume" => Self::Volume,
"kalman" => Self::Kalman,
"trend_stop" | "trendstop" => Self::TrendStop,
"accumulation" => Self::Accumulation,
"regression" => Self::Regression,
"chaos" => Self::Chaos,
"ratio" => Self::Ratio,
"candles" => Self::Candles,
"zigzag" => Self::Zigzag,
"trend" => Self::Trend,
"divergence" => Self::Divergence,
"clusters" => Self::Clusters,
"book" => Self::Book,
"position" => Self::Position,
"statistics" | "stats" => Self::Statistics,
"statistical_scoring" | "statisticalscoring" | "scoring" => Self::StatisticalScoring,
"funding_advanced" | "fundingadvanced" => Self::FundingAdvanced,
"open_interest" | "openinterest" => Self::OpenInterest,
"mark_price_advanced" | "markpriceadvanced" => Self::MarkPriceAdvanced,
"ticker_advanced" | "tickeradvanced" => Self::TickerAdvanced,
"liquidations" => Self::Liquidations,
"tick_advanced" | "tickadvanced" => Self::TickAdvanced,
"book_advanced" | "bookadvanced" => Self::BookAdvanced,
"composites" => Self::Composites,
"sentiment" => Self::Sentiment,
"index_basis" | "indexbasis" => Self::IndexBasis,
"volatility_advanced" | "volatilityadvanced" => Self::VolatilityAdvanced,
"greeks" => Self::Greeks,
"stress" => Self::Stress,
"microstructure" => Self::Microstructure,
"risk_funding" | "riskfunding" => Self::RiskFunding,
"custom" => Self::Custom,
"composite" => Self::Composite,
"experimental" => Self::Experimental,
_ => Self::Unknown,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Momentum => "momentum",
Self::SignalProcessing => "signal_processing",
Self::Channels => "channels",
Self::Volatility => "volatility",
Self::Average => "average",
Self::Levels => "levels",
Self::Adaptive => "adaptive",
Self::Entropy => "entropy",
Self::Volume => "volume",
Self::Kalman => "kalman",
Self::TrendStop => "trend_stop",
Self::Accumulation => "accumulation",
Self::Regression => "regression",
Self::Chaos => "chaos",
Self::Ratio => "ratio",
Self::Candles => "candles",
Self::Zigzag => "zigzag",
Self::Trend => "trend",
Self::Divergence => "divergence",
Self::Clusters => "clusters",
Self::Book => "book",
Self::Position => "position",
Self::Statistics => "statistics",
Self::StatisticalScoring => "statistical_scoring",
Self::FundingAdvanced => "funding_advanced",
Self::OpenInterest => "open_interest",
Self::MarkPriceAdvanced => "mark_price_advanced",
Self::TickerAdvanced => "ticker_advanced",
Self::Liquidations => "liquidations",
Self::TickAdvanced => "tick_advanced",
Self::BookAdvanced => "book_advanced",
Self::Composites => "composites",
Self::Sentiment => "sentiment",
Self::IndexBasis => "index_basis",
Self::VolatilityAdvanced => "volatility_advanced",
Self::Greeks => "greeks",
Self::Stress => "stress",
Self::Microstructure => "microstructure",
Self::RiskFunding => "risk_funding",
Self::Custom => "custom",
Self::Composite => "composite",
Self::Experimental => "experimental",
Self::Unknown => "unknown",
}
}
}
impl fmt::Display for IndicatorCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IndicatorRoleKind {
Smoother,
OscillatorBounded,
OscillatorUnbounded,
Channel,
TrendStop,
Volatility,
Volume,
Level,
Regime,
Pattern,
Statistical,
Other,
}
#[derive(Debug, Clone)]
pub struct IndicatorSignature {
pub id: String,
pub name: String,
pub category: IndicatorCategory,
pub description: String,
pub constraints: ConstraintSet,
pub source_type: SourceType,
pub metadata: HashMap<String, String>,
pub machine_id: Option<BarIndicatorId>,
pub aliases: Vec<String>,
pub role_kind: Option<IndicatorRoleKind>,
pub output_bounds: Option<(f64, f64)>,
pub default_thresholds: Option<(f64, f64)>,
pub warmup_bars: Option<usize>,
pub validated: bool,
pub requires_l2: bool,
pub output_kind: Option<IndicatorValueKind>,
pub input_stream: StreamKind,
pub aux_streams: &'static [StreamKind],
}
impl IndicatorSignature {
pub fn builder(id: impl Into<String>, category: IndicatorCategory) -> IndicatorSignatureBuilder {
IndicatorSignatureBuilder::new(id, category)
}
pub fn validate_params(&self, params: &[(&str, ParamValue)]) -> Result<(), ParamError> {
self.constraints.validate_all(params)
}
pub fn effective_role_kind(&self) -> IndicatorRoleKind {
if let Some(rk) = self.role_kind {
return rk;
}
match self.category {
IndicatorCategory::Average | IndicatorCategory::Adaptive
| IndicatorCategory::Kalman | IndicatorCategory::SignalProcessing
| IndicatorCategory::Regression => IndicatorRoleKind::Smoother,
IndicatorCategory::Channels => IndicatorRoleKind::Channel,
IndicatorCategory::TrendStop => IndicatorRoleKind::TrendStop,
IndicatorCategory::Volatility => IndicatorRoleKind::Volatility,
IndicatorCategory::Volume | IndicatorCategory::Accumulation => IndicatorRoleKind::Volume,
IndicatorCategory::Levels | IndicatorCategory::Zigzag => IndicatorRoleKind::Level,
IndicatorCategory::Candles => IndicatorRoleKind::Pattern,
IndicatorCategory::Statistics => IndicatorRoleKind::Statistical,
IndicatorCategory::StatisticalScoring => IndicatorRoleKind::Statistical,
IndicatorCategory::Momentum => IndicatorRoleKind::OscillatorUnbounded,
IndicatorCategory::Trend => IndicatorRoleKind::Regime,
_ => IndicatorRoleKind::Other,
}
}
pub fn cache_key(&self, params: &[(&str, ParamValue)]) -> String {
let mut key = self.id.clone();
let mut sorted_params: Vec<_> = params.iter().collect();
sorted_params.sort_by_key(|(name, _)| *name);
for (_, value) in sorted_params {
key.push('_');
key.push_str(&value.to_string());
}
key
}
pub fn resolve_params(&self, params: &[(&str, ParamValue)]) -> Result<HashMap<String, ParamValue>, ParamError> {
self.validate_params(params)?;
let mut resolved = HashMap::new();
for (name, value) in params {
resolved.insert(name.to_string(), value.clone());
}
for constraint in &self.constraints.constraints {
if !resolved.contains_key(&constraint.name) {
if let Some(default) = &constraint.default {
resolved.insert(constraint.name.clone(), default.clone());
}
}
}
Ok(resolved)
}
pub fn required_params(&self) -> Vec<&str> {
self.constraints.required_params()
}
pub fn param_names(&self) -> Vec<&str> {
self.constraints.param_names()
}
pub fn get_metadata(&self, key: &str) -> Option<&str> {
self.metadata.get(key).map(|s| s.as_str())
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
}
impl fmt::Display for IndicatorSignature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Indicator: {} ({})", self.name, self.id)?;
writeln!(f, "Category: {}", self.category)?;
if !self.description.is_empty() {
writeln!(f, "Description: {}", self.description)?;
}
writeln!(f, "\nParameters:")?;
for constraint in &self.constraints.constraints {
writeln!(f, " {}", constraint)?;
}
Ok(())
}
}
pub struct IndicatorSignatureBuilder {
id: String,
name: Option<String>,
category: IndicatorCategory,
description: Option<String>,
constraints: Vec<ParamConstraint>,
source_type: SourceType,
metadata: HashMap<String, String>,
machine_id: Option<BarIndicatorId>,
aliases: Vec<String>,
role_kind: Option<IndicatorRoleKind>,
output_bounds: Option<(f64, f64)>,
default_thresholds: Option<(f64, f64)>,
warmup_bars: Option<usize>,
validated: bool,
requires_l2: bool,
output_kind: Option<IndicatorValueKind>,
input_stream: StreamKind,
aux_streams: &'static [StreamKind],
}
impl IndicatorSignatureBuilder {
pub fn new(id: impl Into<String>, category: IndicatorCategory) -> Self {
Self {
id: id.into(),
name: None,
category,
description: None,
constraints: Vec::new(),
source_type: SourceType::default(),
metadata: HashMap::new(),
machine_id: None,
aliases: Vec::new(),
role_kind: None,
output_bounds: None,
default_thresholds: None,
warmup_bars: None,
validated: false,
requires_l2: false,
output_kind: None,
input_stream: StreamKind::Bar,
aux_streams: &[],
}
}
pub fn input_stream(mut self, stream: StreamKind) -> Self {
self.input_stream = stream;
self
}
pub fn aux_streams(mut self, streams: &'static [StreamKind]) -> Self {
self.aux_streams = streams;
self
}
pub fn output_kind(mut self, kind: IndicatorValueKind) -> Self {
self.output_kind = Some(kind);
self
}
pub fn role_kind(mut self, kind: IndicatorRoleKind) -> Self {
self.role_kind = Some(kind);
self
}
pub fn output_bounds(mut self, lo: f64, hi: f64) -> Self {
self.output_bounds = Some((lo, hi));
self
}
pub fn default_thresholds(mut self, lower: f64, upper: f64) -> Self {
self.default_thresholds = Some((lower, upper));
self
}
pub fn warmup_bars(mut self, bars: usize) -> Self {
self.warmup_bars = Some(bars);
self
}
pub fn validated(mut self) -> Self {
self.validated = true;
self
}
pub fn requires_l2(mut self) -> Self {
self.requires_l2 = true;
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn source_type(mut self, source_type: SourceType) -> Self {
self.source_type = source_type;
self
}
pub fn add_constraint(mut self, constraint: ParamConstraint) -> Self {
self.constraints.push(constraint);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn machine_id(mut self, id: BarIndicatorId) -> Self {
self.machine_id = Some(id);
self
}
pub fn alias(mut self, alias: impl Into<String>) -> Self {
self.aliases.push(alias.into());
self
}
pub fn build(self) -> IndicatorSignature {
let name = self.name.unwrap_or_else(|| self.id.clone());
let mut constraints = ConstraintSet::new(&self.id);
constraints.add_all(self.constraints);
IndicatorSignature {
id: self.id,
name,
category: self.category,
description: self.description.unwrap_or_default(),
constraints,
source_type: self.source_type,
metadata: self.metadata,
machine_id: self.machine_id,
aliases: self.aliases,
role_kind: self.role_kind,
output_bounds: self.output_bounds,
default_thresholds: self.default_thresholds,
warmup_bars: self.warmup_bars,
validated: self.validated,
requires_l2: self.requires_l2,
output_kind: self.output_kind,
input_stream: self.input_stream,
aux_streams: self.aux_streams,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bar_indicators::average::moving_average::MovingAverageType;
#[test]
fn test_indicator_category_from_str() {
assert_eq!(IndicatorCategory::from_str("momentum"), IndicatorCategory::Momentum);
assert_eq!(IndicatorCategory::from_str("Momentum"), IndicatorCategory::Momentum);
assert_eq!(IndicatorCategory::from_str("MOMENTUM"), IndicatorCategory::Momentum);
assert_eq!(IndicatorCategory::from_str("signal_processing"), IndicatorCategory::SignalProcessing);
assert_eq!(IndicatorCategory::from_str("signalprocessing"), IndicatorCategory::SignalProcessing);
assert_eq!(IndicatorCategory::from_str("unknown_category"), IndicatorCategory::Unknown);
}
#[test]
fn test_indicator_category_as_str() {
assert_eq!(IndicatorCategory::Momentum.as_str(), "momentum");
assert_eq!(IndicatorCategory::SignalProcessing.as_str(), "signal_processing");
assert_eq!(IndicatorCategory::Channels.as_str(), "channels");
}
#[test]
fn test_builder_minimal() {
let sig = IndicatorSignature::builder("SMA", IndicatorCategory::Average)
.build();
assert_eq!(sig.id, "SMA");
assert_eq!(sig.name, "SMA"); assert_eq!(sig.category, IndicatorCategory::Average);
}
#[test]
fn test_builder_complete() {
let sig = IndicatorSignature::builder("RSI", IndicatorCategory::Momentum)
.name("Relative Strength Index")
.description("Momentum oscillator measuring speed and magnitude of price changes")
.add_constraint(ParamConstraint::period(2, 200, 14))
.add_constraint(ParamConstraint::flag("use_wilder", true))
.metadata("author", "J. Welles Wilder")
.metadata("year", "1978")
.build();
assert_eq!(sig.id, "RSI");
assert_eq!(sig.name, "Relative Strength Index");
assert_eq!(sig.category, IndicatorCategory::Momentum);
assert!(sig.description.contains("Momentum oscillator"));
assert_eq!(sig.constraints.constraints.len(), 2);
assert_eq!(sig.get_metadata("author"), Some("J. Welles Wilder"));
assert_eq!(sig.get_metadata("year"), Some("1978"));
}
#[test]
fn test_validate_params() {
let sig = IndicatorSignature::builder("SMA", IndicatorCategory::Average)
.add_constraint(ParamConstraint::period(2, 200, 20))
.build();
let params = vec![("period", ParamValue::USize(20))];
assert!(sig.validate_params(¶ms).is_ok());
let params = vec![("period", ParamValue::USize(1))];
assert!(sig.validate_params(¶ms).is_err());
let params = vec![("period", ParamValue::F64(20.0))];
assert!(sig.validate_params(¶ms).is_err());
}
#[test]
fn test_cache_key() {
let sig = IndicatorSignature::builder("MACD", IndicatorCategory::Momentum)
.add_constraint(ParamConstraint::period(2, 50, 12))
.add_constraint(ParamConstraint::period(2, 50, 26))
.add_constraint(ParamConstraint::period(2, 20, 9))
.build();
let params = vec![
("fast", ParamValue::USize(12)),
("slow", ParamValue::USize(26)),
("signal", ParamValue::USize(9)),
];
let key = sig.cache_key(¶ms);
assert!(key.starts_with("MACD_"));
assert!(key.contains("12"));
assert!(key.contains("26"));
assert!(key.contains("9"));
}
#[test]
fn test_cache_key_consistency() {
let sig = IndicatorSignature::builder("TEST", IndicatorCategory::Custom).build();
let params1 = vec![
("a", ParamValue::USize(1)),
("b", ParamValue::USize(2)),
];
let params2 = vec![
("b", ParamValue::USize(2)),
("a", ParamValue::USize(1)),
];
let key1 = sig.cache_key(¶ms1);
let key2 = sig.cache_key(¶ms2);
assert_eq!(key1, key2);
}
#[test]
fn test_resolve_params_with_defaults() {
use crate::catalog::param_value::ParamType;
let sig = IndicatorSignature::builder("BB", IndicatorCategory::Channels)
.add_constraint(ParamConstraint::period(2, 200, 20))
.add_constraint(
ParamConstraint::new("multiplier", ParamType::F64)
.with_min(ParamValue::F64(0.5))
.with_max(ParamValue::F64(5.0))
.with_default(ParamValue::F64(2.0))
)
.build();
let params = vec![("period", ParamValue::USize(20))];
let resolved = sig.resolve_params(¶ms).unwrap();
assert_eq!(resolved.get("period"), Some(&ParamValue::USize(20)));
assert_eq!(resolved.get("multiplier"), Some(&ParamValue::F64(2.0))); }
#[test]
fn test_resolve_params_override_defaults() {
let sig = IndicatorSignature::builder("BB", IndicatorCategory::Channels)
.add_constraint(ParamConstraint::period(2, 200, 20))
.add_constraint(ParamConstraint::multiplier(0.5, 5.0, 2.0))
.build();
let params = vec![
("period", ParamValue::USize(20)),
("multiplier", ParamValue::F64(3.0)),
];
let resolved = sig.resolve_params(¶ms).unwrap();
assert_eq!(resolved.get("multiplier"), Some(&ParamValue::F64(3.0)));
}
#[test]
fn test_required_params() {
let sig = IndicatorSignature::builder("RSI", IndicatorCategory::Momentum)
.add_constraint(ParamConstraint::period(2, 200, 14)) .add_constraint(ParamConstraint::flag("use_wilder", true)) .build();
let required = sig.required_params();
assert_eq!(required.len(), 1);
assert!(required.contains(&"period"));
}
#[test]
fn test_param_names() {
let sig = IndicatorSignature::builder("MACD", IndicatorCategory::Momentum)
.add_constraint(ParamConstraint::period(2, 50, 12))
.add_constraint(ParamConstraint::period(2, 50, 26))
.add_constraint(ParamConstraint::period(2, 20, 9))
.build();
let names = sig.param_names();
assert_eq!(names.len(), 3);
}
#[test]
fn test_metadata() {
let mut sig = IndicatorSignature::builder("RSI", IndicatorCategory::Momentum).build();
sig.set_metadata("version", "1.0");
sig.set_metadata("source", "ta-lib");
assert_eq!(sig.get_metadata("version"), Some("1.0"));
assert_eq!(sig.get_metadata("source"), Some("ta-lib"));
assert_eq!(sig.get_metadata("nonexistent"), None);
}
#[test]
fn test_display() {
let sig = IndicatorSignature::builder("RSI", IndicatorCategory::Momentum)
.name("Relative Strength Index")
.description("Momentum oscillator")
.add_constraint(ParamConstraint::period(2, 200, 14))
.build();
let display = format!("{}", sig);
assert!(display.contains("RSI"));
assert!(display.contains("Relative Strength Index"));
assert!(display.contains("momentum"));
assert!(display.contains("period"));
}
#[test]
fn test_ma_type_parameter() {
let sig = IndicatorSignature::builder("EMA", IndicatorCategory::Average)
.add_constraint(ParamConstraint::period(2, 200, 20))
.add_constraint(ParamConstraint::ma_type(MovingAverageType::EMA))
.build();
let params = vec![
("period", ParamValue::USize(20)),
("ma_type", ParamValue::MaType(MovingAverageType::EMA)),
];
assert!(sig.validate_params(¶ms).is_ok());
}
#[test]
fn test_source_type_default() {
let sig = IndicatorSignature::builder("SMA", IndicatorCategory::Average)
.build();
assert_eq!(sig.source_type, SourceType::PriceOnly);
assert!(sig.source_type.requires_source_selection());
}
#[test]
fn test_source_type_explicit() {
let sig = IndicatorSignature::builder("OBV", IndicatorCategory::Volume)
.source_type(SourceType::VolumeOnly)
.build();
assert_eq!(sig.source_type, SourceType::VolumeOnly);
assert!(!sig.source_type.requires_source_selection());
}
#[test]
fn test_source_type_price_and_volume() {
let sig = IndicatorSignature::builder("MFI", IndicatorCategory::Momentum)
.source_type(SourceType::PriceAndVolume)
.build();
assert_eq!(sig.source_type, SourceType::PriceAndVolume);
assert!(!sig.source_type.requires_source_selection());
}
#[test]
fn test_source_type_from_str() {
assert_eq!(SourceType::from_str("price_only"), Some(SourceType::PriceOnly));
assert_eq!(SourceType::from_str("price"), Some(SourceType::PriceOnly));
assert_eq!(SourceType::from_str("volume_only"), Some(SourceType::VolumeOnly));
assert_eq!(SourceType::from_str("volume"), Some(SourceType::VolumeOnly));
assert_eq!(SourceType::from_str("price_and_volume"), Some(SourceType::PriceAndVolume));
assert_eq!(SourceType::from_str("both"), Some(SourceType::PriceAndVolume));
assert_eq!(SourceType::from_str("PRICE_ONLY"), Some(SourceType::PriceOnly));
assert_eq!(SourceType::from_str("invalid"), None);
}
#[test]
fn test_source_type_as_str() {
assert_eq!(SourceType::PriceOnly.as_str(), "price_only");
assert_eq!(SourceType::VolumeOnly.as_str(), "volume_only");
assert_eq!(SourceType::PriceAndVolume.as_str(), "price_and_volume");
}
#[test]
fn test_source_type_display() {
assert_eq!(format!("{}", SourceType::PriceOnly), "price_only");
assert_eq!(format!("{}", SourceType::VolumeOnly), "volume_only");
assert_eq!(format!("{}", SourceType::PriceAndVolume), "price_and_volume");
}
}