Skip to main content

kestrel_chartkit/evaluation/
exporter.rs

1use crate::model::MarketRegime;
2use crate::signal::{CompositeSignal, TriggerAction};
3use std::collections::HashMap;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// Exportable snapshot record of features, regime, and signal outputs for research or logging sinks.
9#[derive(Debug, Clone, PartialEq)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11pub struct FeatureRecord {
12    pub timestamp: i64,
13    pub symbol: String,
14    pub regime: MarketRegime,
15    pub indicator_values: HashMap<String, f64>,
16    pub subscores: HashMap<String, f64>,
17    pub trigger: TriggerAction,
18    pub score: f64,
19    pub agreement: f64,
20}
21
22/// Feature and research dataset exporter.
23#[derive(Debug, Clone, Default)]
24pub struct FeatureExporter;
25
26impl FeatureExporter {
27    pub fn new() -> Self {
28        Self
29    }
30
31    /// Builds a sink-independent `FeatureRecord` snapshot.
32    pub fn export_record(
33        &self,
34        timestamp: i64,
35        symbol: &str,
36        regime: MarketRegime,
37        indicator_values: HashMap<String, f64>,
38        signal: &CompositeSignal,
39    ) -> FeatureRecord {
40        let mut subscores = HashMap::new();
41        for sub in &signal.per_indicator {
42            subscores.insert(sub.indicator.clone(), sub.score);
43        }
44
45        FeatureRecord {
46            timestamp,
47            symbol: symbol.to_string(),
48            regime,
49            indicator_values,
50            subscores,
51            trigger: signal.trigger,
52            score: signal.score,
53            agreement: signal.agreement,
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::signal::PermissionGrade;
62
63    #[test]
64    fn test_feature_exporter() {
65        let exporter = FeatureExporter::new();
66        let signal = CompositeSignal {
67            score: 0.85,
68            direction: crate::signal::SignalDirection::Bullish,
69            permission: PermissionGrade::ClearToTrade,
70            trigger: TriggerAction::Buy,
71            target_zone: None,
72            invalidation_zone: None,
73            setup_duration: None,
74            risk_plan: None,
75            heat_score: 0.5,
76            sr_zones: Vec::new(),
77            per_indicator: Vec::new(),
78            reasons: Vec::new(),
79            explanation: "test".to_string(),
80            agreement: 0.9,
81            regime: MarketRegime::BullishExpansion,
82        };
83
84        let rec = exporter.export_record(
85            1000,
86            "BTCUSD",
87            MarketRegime::BullishExpansion,
88            HashMap::from([("rsi".to_string(), 65.0)]),
89            &signal,
90        );
91
92        assert_eq!(rec.symbol, "BTCUSD");
93        assert_eq!(rec.indicator_values["rsi"], 65.0);
94    }
95}