use super::indicator_signature::{IndicatorSignature, IndicatorCategory};
use super::rendering::RenderingMetadata;
use super::master_catalog::{MasterIndicatorCatalog, CatalogError, MASTER_CATALOG};
use super::rendering_catalog::{RENDERING_CATALOG, get_rendering};
#[derive(Debug, Clone)]
pub struct UnifiedIndicatorInfo {
pub signature: IndicatorSignature,
pub rendering: Option<RenderingMetadata>,
}
impl UnifiedIndicatorInfo {
pub fn new(signature: IndicatorSignature, rendering: Option<RenderingMetadata>) -> Self {
Self { signature, rendering }
}
pub fn id(&self) -> &str {
&self.signature.id
}
pub fn name(&self) -> &str {
&self.signature.name
}
pub fn category(&self) -> IndicatorCategory {
self.signature.category
}
pub fn is_overlay(&self) -> bool {
self.rendering.as_ref().map(|r| r.overlay).unwrap_or(false)
}
pub fn has_rendering(&self) -> bool {
self.rendering.is_some()
}
pub fn bounds(&self) -> Option<(f64, f64)> {
self.rendering.as_ref().and_then(|r| r.bounds)
}
pub fn output_count(&self) -> usize {
self.rendering
.as_ref()
.map(|r| r.outputs.len())
.unwrap_or(1)
}
}
pub struct UnifiedIndicatorCatalog {
master: &'static MasterIndicatorCatalog,
}
impl UnifiedIndicatorCatalog {
pub fn new() -> Self {
Self {
master: &MASTER_CATALOG,
}
}
pub fn get(&self, id: &str) -> Result<UnifiedIndicatorInfo, CatalogError> {
let signature = self.master.get_signature(id)?;
let rendering = signature.machine_id.and_then(|mid| get_rendering(mid).cloned());
Ok(UnifiedIndicatorInfo::new(signature, rendering))
}
pub fn get_by_category(
&self,
category: IndicatorCategory,
id: &str,
) -> Result<UnifiedIndicatorInfo, CatalogError> {
let signature = self.master.get_by_category(category, id)?;
let rendering = signature.machine_id.and_then(|mid| get_rendering(mid).cloned());
Ok(UnifiedIndicatorInfo::new(signature, rendering))
}
pub fn get_category_indicators(
&self,
category: IndicatorCategory,
) -> Result<Vec<UnifiedIndicatorInfo>, CatalogError> {
let signatures = self.master.get_category_indicators(category)?;
Ok(signatures
.into_iter()
.map(|sig| {
let rendering = sig.machine_id.and_then(|mid| get_rendering(mid).cloned());
UnifiedIndicatorInfo::new(sig, rendering)
})
.collect())
}
pub fn get_overlay_indicators(&self) -> Vec<UnifiedIndicatorInfo> {
self.get_all()
.into_iter()
.filter(|info| info.is_overlay())
.collect()
}
pub fn get_subpane_indicators(&self) -> Vec<UnifiedIndicatorInfo> {
self.get_all()
.into_iter()
.filter(|info| !info.is_overlay())
.collect()
}
pub fn get_all_with_rendering(&self) -> Vec<UnifiedIndicatorInfo> {
self.get_all()
.into_iter()
.filter(|info| info.has_rendering())
.collect()
}
pub fn get_all(&self) -> Vec<UnifiedIndicatorInfo> {
let mut result = Vec::new();
for category in [
IndicatorCategory::Average,
IndicatorCategory::Momentum,
IndicatorCategory::Channels,
IndicatorCategory::Volatility,
IndicatorCategory::Volume,
IndicatorCategory::Trend,
IndicatorCategory::Levels,
IndicatorCategory::Entropy,
IndicatorCategory::Kalman,
IndicatorCategory::SignalProcessing,
IndicatorCategory::Chaos,
IndicatorCategory::Regression,
IndicatorCategory::Adaptive,
IndicatorCategory::Accumulation,
IndicatorCategory::Book,
IndicatorCategory::Candles,
IndicatorCategory::Clusters,
IndicatorCategory::Divergence,
IndicatorCategory::Ratio,
IndicatorCategory::TrendStop,
IndicatorCategory::Position,
IndicatorCategory::Statistics,
IndicatorCategory::StatisticalScoring,
] {
if let Ok(indicators) = self.get_category_indicators(category) {
result.extend(indicators);
}
}
result
}
pub fn search(&self, query: &str) -> Vec<UnifiedIndicatorInfo> {
self.master
.search(query)
.into_iter()
.map(|sig| {
let rendering = sig.machine_id.and_then(|mid| get_rendering(mid).cloned());
UnifiedIndicatorInfo::new(sig, rendering)
})
.collect()
}
pub fn contains(&self, id: &str) -> bool {
self.master.contains(id)
}
pub fn total_count(&self) -> usize {
self.master.total_count()
}
pub fn rendering_count(&self) -> usize {
RENDERING_CATALOG.len()
}
pub fn stats(&self) -> UnifiedCatalogStats {
let all = self.get_all();
let with_rendering = all.iter().filter(|i| i.has_rendering()).count();
let overlay_count = all.iter().filter(|i| i.is_overlay()).count();
UnifiedCatalogStats {
total_indicators: all.len(),
with_rendering,
without_rendering: all.len() - with_rendering,
overlay_count,
subpane_count: all.len() - overlay_count,
}
}
}
impl Default for UnifiedIndicatorCatalog {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct UnifiedCatalogStats {
pub total_indicators: usize,
pub with_rendering: usize,
pub without_rendering: usize,
pub overlay_count: usize,
pub subpane_count: usize,
}
impl UnifiedCatalogStats {
pub fn report(&self) -> String {
let mut report = String::new();
report.push_str("═══════════════════════════════════════════════════════════\n");
report.push_str(" UNIFIED INDICATOR CATALOG STATISTICS\n");
report.push_str("═══════════════════════════════════════════════════════════\n\n");
report.push_str(&format!("Total Indicators: {}\n", self.total_indicators));
report.push_str(&format!("With Rendering: {}\n", self.with_rendering));
report.push_str(&format!("Without Rendering: {}\n", self.without_rendering));
report.push_str(&format!("Overlay Indicators: {}\n", self.overlay_count));
report.push_str(&format!("Sub-pane Indicators: {}\n", self.subpane_count));
let coverage = if self.total_indicators > 0 {
(self.with_rendering as f64 / self.total_indicators as f64) * 100.0
} else {
0.0
};
report.push_str(&format!("\nRendering Coverage: {:.1}%\n", coverage));
report.push_str("═══════════════════════════════════════════════════════════\n");
report
}
}
use once_cell::sync::Lazy;
pub static UNIFIED_CATALOG: Lazy<UnifiedIndicatorCatalog> = Lazy::new(UnifiedIndicatorCatalog::new);
pub fn catalog() -> &'static UnifiedIndicatorCatalog {
&UNIFIED_CATALOG
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unified_catalog_creation() {
let catalog = UnifiedIndicatorCatalog::new();
assert!(catalog.total_count() > 400, "Should have 400+ indicators");
}
#[test]
fn test_get_unified_info() {
let catalog = UnifiedIndicatorCatalog::new();
let rsi = catalog.get("RSI").unwrap();
assert_eq!(rsi.id(), "RSI");
assert!(rsi.has_rendering());
assert!(!rsi.is_overlay());
}
#[test]
fn test_overlay_detection() {
let catalog = UnifiedIndicatorCatalog::new();
let sma = catalog.get("SMA").unwrap();
assert!(sma.is_overlay());
let rsi = catalog.get("RSI").unwrap();
assert!(!rsi.is_overlay());
}
#[test]
fn test_get_overlay_indicators() {
let catalog = UnifiedIndicatorCatalog::new();
let overlays = catalog.get_overlay_indicators();
assert!(!overlays.is_empty());
for ind in &overlays {
assert!(ind.is_overlay(), "{} should be overlay", ind.id());
}
}
#[test]
fn test_get_subpane_indicators() {
let catalog = UnifiedIndicatorCatalog::new();
let subpanes = catalog.get_subpane_indicators();
assert!(!subpanes.is_empty());
for ind in &subpanes {
assert!(!ind.is_overlay(), "{} should be sub-pane", ind.id());
}
}
#[test]
fn test_stats() {
let catalog = UnifiedIndicatorCatalog::new();
let stats = catalog.stats();
assert!(stats.total_indicators > 400);
assert!(stats.with_rendering > 0);
assert!(stats.overlay_count > 0);
assert!(stats.subpane_count > 0);
}
#[test]
fn test_search() {
let catalog = UnifiedIndicatorCatalog::new();
let results = catalog.search("moving average");
assert!(!results.is_empty());
}
#[test]
fn test_global_catalog() {
let catalog = catalog();
assert!(catalog.total_count() > 400);
}
#[test]
fn test_unified_info_bounds() {
let catalog = UnifiedIndicatorCatalog::new();
let rsi = catalog.get("RSI").unwrap();
assert_eq!(rsi.bounds(), Some((0.0, 100.0)));
let sma = catalog.get("SMA").unwrap();
assert_eq!(sma.bounds(), None);
}
}