use crate::catalog::{
IndicatorSignature, IndicatorCategory, IndicatorRoleKind, ParamConstraint, ParamType, ParamValue,
};
use crate::bar_indicators::indicator_value::IndicatorValueKind;
use crate::data_loader::stream_kind::StreamKind;
use super::super::bar_indicator_id::BarIndicatorId;
use once_cell::sync::Lazy;
use std::collections::HashMap;
pub const CATEGORY: IndicatorCategory = IndicatorCategory::Clusters;
pub fn signature_market_microstructure() -> IndicatorSignature {
IndicatorSignature::builder("MARKET_MICRO", CATEGORY)
.name("Market Microstructure")
.description("Analyzes market microstructure: liquidity, efficiency, execution quality")
.add_constraint(ParamConstraint::period(5, 512, 50))
.metadata("outputs", "liquidity_score, efficiency_score, execution_score, microstructure_score")
.metadata("metrics", "spread, depth, price_impact, discovery_speed, volatility_clustering")
.machine_id(BarIndicatorId::MarketMicro)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::OrderBook)
.output_kind(IndicatorValueKind::Single)
.requires_l2()
.alias("MarketMicro")
.alias("market_micro")
.alias("MARKETMICROSTRUCTURE")
.alias("MarketMicrostructure")
.alias("marketmicrostructure")
.alias("market_microstructure")
.alias("MARKET_MICROSTRUCTURE")
.alias("Market_Microstructure")
.build()
}
pub fn signature_order_book_slope() -> IndicatorSignature {
IndicatorSignature::builder("ORDER_BOOK_SLOPE", CATEGORY)
.name("Order Book Slope")
.description("Approximates order book slope using OHLCV data")
.metadata("formula", "ln(volume) / (high - low)")
.metadata("proxy", "true")
.machine_id(BarIndicatorId::OrderBookSlope)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::OrderBook)
.output_kind(IndicatorValueKind::Single)
.requires_l2()
.alias("OrderBookSlope")
.alias("order_book_slope")
.alias("ORDERBOOKSLOPE")
.alias("orderbookslope")
.alias("Order_Book_Slope")
.build()
}
pub fn signature_order_flow_imbalance() -> IndicatorSignature {
IndicatorSignature::builder("ORDER_FLOW_IMB", CATEGORY)
.name("Order Flow Imbalance")
.description("Analyzes imbalance between buy and sell orders at price levels")
.add_constraint(ParamConstraint::period(5, 512, 50))
.add_constraint(
ParamConstraint::new("tick_size", ParamType::F64)
.with_min(ParamValue::F64(0.0001))
.with_max(ParamValue::F64(1.0))
.with_default(ParamValue::F64(0.01))
)
.metadata("outputs", "total_imbalance, avg_imbalance, dominant_side, imbalance_strength")
.metadata("uses_volume", "true")
.machine_id(BarIndicatorId::OrderFlowImb)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::OrderBook)
.output_kind(IndicatorValueKind::Single)
.requires_l2()
.alias("OrderFlowImb")
.alias("order_flow_imb")
.alias("ORDERFLOWIMBALANCE")
.alias("OrderFlowImbalance")
.alias("orderflowimbalance")
.alias("order_flow_imbalance")
.alias("ORDER_FLOW_IMBALANCE")
.alias("Order_Flow_Imbalance")
.build()
}
pub fn signature_queue_imbalance() -> IndicatorSignature {
IndicatorSignature::builder("CL_QUEUE_IMB", CATEGORY)
.name("Queue Imbalance")
.description("Level-1 proxy queue imbalance from OHLCV")
.metadata("formula", "(close - mid) / range")
.metadata("proxy", "true")
.machine_id(BarIndicatorId::ClQueueImb)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::OrderBook)
.output_kind(IndicatorValueKind::Single)
.requires_l2()
.alias("ClQueueImb")
.alias("cl_queue_imb")
.alias("QUEUEIMBALANCE")
.alias("QueueImbalance")
.alias("queueimbalance")
.alias("queue_imbalance")
.alias("QUEUE_IMBALANCE")
.alias("Queue_Imbalance")
.build()
}
pub fn signature_tick_volume_analyzer() -> IndicatorSignature {
IndicatorSignature::builder("TICK_VOLUME", CATEGORY)
.name("Tick Volume Analyzer")
.description("Detailed analysis of tick volume and microstructure")
.add_constraint(ParamConstraint::period(10, 1024, 100))
.metadata("outputs", "volume_delta, volume_ratio, buy_pct, avg_spread, market_pressure")
.metadata("uses_volume", "true")
.metadata("uses_ticks", "true")
.machine_id(BarIndicatorId::TickVolume)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::Tick)
.output_kind(IndicatorValueKind::Single)
.requires_l2()
.alias("TickVolume")
.alias("tick_volume")
.alias("TICKVOLUMEANALYZER")
.alias("TickVolumeAnalyzer")
.alias("tickvolumeanalyzer")
.alias("tick_volume_analyzer")
.alias("TICK_VOLUME_ANALYZER")
.alias("Tick_Volume_Analyzer")
.build()
}
pub fn signature_footprint_chart() -> IndicatorSignature {
IndicatorSignature::builder("FOOTPRINT_CHART", CATEGORY)
.name("Footprint Chart")
.description("Aggregates buy/sell volume per price bucket; outputs net_delta, POC price, total_volume")
.add_constraint(
ParamConstraint::new("price_bucket", ParamType::F64)
.with_min(ParamValue::F64(1e-9))
.with_max(ParamValue::F64(10000.0))
.with_default(ParamValue::F64(0.01))
)
.metadata("outputs", "net_delta, poc_price, total_volume")
.metadata("uses_ticks", "true")
.machine_id(BarIndicatorId::FootprintChart)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::Tick)
.output_kind(IndicatorValueKind::Triple)
.requires_l2()
.alias("FootprintChart")
.alias("footprint_chart")
.alias("FOOTPRINT")
.alias("footprint")
.build()
}
pub fn signature_footprint_imbalance() -> IndicatorSignature {
IndicatorSignature::builder("FOOTPRINT_IMB", CATEGORY)
.name("Footprint Imbalance")
.description("Detects price levels with extreme buy/sell skew above threshold_pct")
.add_constraint(
ParamConstraint::new("price_bucket", ParamType::F64)
.with_min(ParamValue::F64(1e-9))
.with_max(ParamValue::F64(10000.0))
.with_default(ParamValue::F64(0.01))
)
.add_constraint(
ParamConstraint::new("threshold_pct", ParamType::F64)
.with_min(ParamValue::F64(0.0))
.with_max(ParamValue::F64(100.0))
.with_default(ParamValue::F64(75.0))
)
.metadata("outputs", "signal, imb_price, imb_pct")
.metadata("uses_ticks", "true")
.machine_id(BarIndicatorId::FootprintImbalance)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::Tick)
.output_kind(IndicatorValueKind::Triple)
.requires_l2()
.alias("FootprintImbalance")
.alias("footprint_imbalance")
.alias("FOOTPRINT_IMBALANCE")
.build()
}
pub fn signature_footprint_poc() -> IndicatorSignature {
IndicatorSignature::builder("FOOTPRINT_POC", CATEGORY)
.name("Footprint POC")
.description("Outputs the price level with maximum total volume in the current bar")
.add_constraint(
ParamConstraint::new("price_bucket", ParamType::F64)
.with_min(ParamValue::F64(1e-9))
.with_max(ParamValue::F64(10000.0))
.with_default(ParamValue::F64(0.01))
)
.metadata("outputs", "poc_price")
.metadata("uses_ticks", "true")
.machine_id(BarIndicatorId::FootprintPoc)
.role_kind(IndicatorRoleKind::Smoother)
.input_stream(StreamKind::Tick)
.output_kind(IndicatorValueKind::Single)
.requires_l2()
.alias("FootprintPoc")
.alias("footprint_poc")
.alias("FOOTPRINT_POC")
.build()
}
pub fn signature_absorption_detector() -> IndicatorSignature {
IndicatorSignature::builder("ABSORPTION_DETECTOR", CATEGORY)
.name("Absorption Detector")
.description("Detects buy/sell absorption: large volume with minimal price movement")
.add_constraint(ParamConstraint::period(5, 1000, 50))
.metadata("outputs", "absorption_score, signal (+1=buy abs, -1=sell abs, 0=none)")
.metadata("uses_ticks", "true")
.machine_id(BarIndicatorId::AbsorptionDetector)
.role_kind(IndicatorRoleKind::OscillatorUnbounded)
.input_stream(StreamKind::Tick)
.output_kind(IndicatorValueKind::Double)
.requires_l2()
.alias("AbsorptionDetector")
.alias("absorption_detector")
.alias("ABSORPTIONDETECTOR")
.build()
}
pub fn signature_trade_cluster_detector() -> IndicatorSignature {
IndicatorSignature::builder("TRADE_CLUSTER_DETECTOR", CATEGORY)
.name("Trade Cluster Detector")
.description("Detects iceberg orders: N+ trades at the same price bucket within a time window")
.add_constraint(
ParamConstraint::new("price_bucket", ParamType::F64)
.with_min(ParamValue::F64(1e-9))
.with_max(ParamValue::F64(1000.0))
.with_default(ParamValue::F64(0.01))
)
.add_constraint(
ParamConstraint::new("cluster_threshold", ParamType::F64)
.with_min(ParamValue::F64(2.0))
.with_max(ParamValue::F64(100.0))
.with_default(ParamValue::F64(3.0))
)
.add_constraint(
ParamConstraint::new("window_ms", ParamType::F64)
.with_min(ParamValue::F64(100.0))
.with_max(ParamValue::F64(300_000.0))
.with_default(ParamValue::F64(5000.0))
)
.metadata("outputs", "signal (+1/-1/0), cluster_price, cluster_size")
.metadata("uses_ticks", "true")
.machine_id(BarIndicatorId::TradeClusterDetector)
.role_kind(IndicatorRoleKind::Pattern)
.input_stream(StreamKind::Tick)
.output_kind(IndicatorValueKind::Triple)
.requires_l2()
.alias("TradeClusterDetector")
.alias("trade_cluster_detector")
.alias("TRADECLUSTERDETECTOR")
.build()
}
pub fn signature_volume_weighted_price_levels() -> IndicatorSignature {
IndicatorSignature::builder("VWAP_LEVELS", CATEGORY)
.name("Volume Weighted Price Levels")
.description("Identifies key support/resistance levels based on volume analysis")
.add_constraint(ParamConstraint::period(10, 512, 100))
.add_constraint(
ParamConstraint::new("price_precision", ParamType::F64)
.with_min(ParamValue::F64(0.0001))
.with_max(ParamValue::F64(10.0))
.with_default(ParamValue::F64(0.01))
)
.metadata("outputs", "vwap, support_levels, resistance_levels, high_volume_nodes")
.metadata("uses_volume", "true")
.machine_id(BarIndicatorId::VwapLevels)
.role_kind(IndicatorRoleKind::Volume)
.input_stream(StreamKind::Bar)
.output_kind(IndicatorValueKind::Single)
.alias("VwapLevels")
.alias("vwap_levels")
.alias("VOLUMEWEIGHTEDPRICELEVELS")
.alias("VolumeWeightedPriceLevels")
.alias("volumeweightedpricelevels")
.alias("volume_weighted_price_levels")
.alias("VOLUME_WEIGHTED_PRICE_LEVELS")
.alias("Volume_Weighted_Price_Levels")
.build()
}
const BASE_CATALOG: &[(&str, fn() -> IndicatorSignature)] = &[
("MARKET_MICRO", signature_market_microstructure as fn() -> IndicatorSignature),
("ORDER_BOOK_SLOPE", signature_order_book_slope as fn() -> IndicatorSignature),
("ORDER_FLOW_IMB", signature_order_flow_imbalance as fn() -> IndicatorSignature),
("CL_QUEUE_IMB", signature_queue_imbalance as fn() -> IndicatorSignature),
("TICK_VOLUME", signature_tick_volume_analyzer as fn() -> IndicatorSignature),
("VWAP_LEVELS", signature_volume_weighted_price_levels as fn() -> IndicatorSignature),
("FOOTPRINT_CHART", signature_footprint_chart as fn() -> IndicatorSignature),
("FOOTPRINT_IMB", signature_footprint_imbalance as fn() -> IndicatorSignature),
("FOOTPRINT_POC", signature_footprint_poc as fn() -> IndicatorSignature),
("ABSORPTION_DETECTOR", signature_absorption_detector as fn() -> IndicatorSignature),
("TRADE_CLUSTER_DETECTOR", signature_trade_cluster_detector as fn() -> IndicatorSignature),
];
pub static CLUSTERS_CATALOG: Lazy<HashMap<String, fn() -> IndicatorSignature>> = Lazy::new(|| {
let mut m = HashMap::new();
for &(main_id, func) in BASE_CATALOG {
let sig = func();
m.insert(main_id.to_string(), func);
for alias in &sig.aliases {
m.insert(alias.clone(), func);
}
}
m
});
pub fn get_signature(id: &str) -> Option<IndicatorSignature> {
CLUSTERS_CATALOG.get(id).map(|f| f())
}
pub fn all_indicator_ids() -> Vec<&'static str> {
BASE_CATALOG.iter().map(|(id, _)| *id).collect()
}
pub fn count() -> usize {
BASE_CATALOG.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_market_microstructure_signature() {
let sig = get_signature("MARKET_MICRO").unwrap();
assert_eq!(sig.id, "MARKET_MICRO");
assert_eq!(sig.category, CATEGORY);
}
#[test]
fn test_get_order_flow_imbalance_signature() {
let sig = get_signature("ORDER_FLOW_IMB").unwrap();
assert_eq!(sig.id, "ORDER_FLOW_IMB");
assert_eq!(sig.required_params().len(), 1); }
#[test]
fn test_get_tick_volume_signature() {
let sig = get_signature("TICK_VOLUME").unwrap();
assert_eq!(sig.id, "TICK_VOLUME");
assert!(sig.metadata.contains_key("uses_volume"));
}
#[test]
fn test_all_signatures_valid() {
for id in all_indicator_ids() {
let sig = get_signature(id).unwrap();
assert_eq!(sig.id, id);
assert_eq!(sig.category, CATEGORY);
}
}
#[test]
fn test_count() {
assert_eq!(count(), 11); }
#[test]
fn test_order_flow_validation() {
let sig = get_signature("ORDER_FLOW_IMB").unwrap();
let params = vec![
("period", ParamValue::USize(50)),
("tick_size", ParamValue::F64(0.01)),
];
assert!(sig.validate_params(¶ms).is_ok());
let params = vec![
("period", ParamValue::USize(2)),
];
assert!(sig.validate_params(¶ms).is_err());
let params = vec![
("period", ParamValue::USize(50)),
("tick_size", ParamValue::F64(10.0)),
];
assert!(sig.validate_params(¶ms).is_err());
}
#[test]
fn test_cache_key_generation() {
let sig = get_signature("ORDER_FLOW_IMB").unwrap();
let params = vec![
("period", ParamValue::USize(50)),
("tick_size", ParamValue::F64(0.01)),
];
let key = sig.cache_key(¶ms);
assert!(key.contains("ORDER_FLOW_IMB"));
assert!(key.contains("50"));
}
#[test]
fn test_vwap_levels_signature() {
let sig = get_signature("VWAP_LEVELS").unwrap();
assert_eq!(sig.id, "VWAP_LEVELS");
assert_eq!(sig.required_params().len(), 1); }
#[test]
fn test_order_book_slope_signature() {
let sig = get_signature("ORDER_BOOK_SLOPE").unwrap();
assert_eq!(sig.id, "ORDER_BOOK_SLOPE");
assert_eq!(sig.required_params().len(), 0); assert!(sig.metadata.contains_key("proxy"));
}
#[test]
fn test_queue_imbalance_signature() {
if let Some(sig) = get_signature("QUEUE_IMB") {
assert_eq!(sig.id, "QUEUE_IMB");
assert!(sig.metadata.contains_key("formula"));
}
}
}