Skip to main content

hyper_ta/
dynamic.rs

1//! TaEngine-based dynamic indicator calculation.
2//!
3//! This module provides a bridge between the streaming `motosan-ta-stream` engine
4//! and the existing `TechnicalIndicators` struct. It enables two workflows:
5//!
6//! 1. **`calculate_snapshot`** -- feeds candles through a `TaEngine` and returns a
7//!    `TaSnapshot` containing all indicator values.
8//! 2. **`snapshot_to_indicators`** -- converts a `TaSnapshot` back into the legacy
9//!    `TechnicalIndicators` struct for backward compatibility with the rule engine.
10//!
11//! Helper functions `get_snapshot_value` / `get_snapshot_sub_value` provide direct
12//! access to individual indicator values inside a snapshot.
13
14use motosan_ta_stream::engine::TaEngine;
15use motosan_ta_stream::indicator::*;
16use motosan_ta_stream::snapshot::{IndicatorValue, TaSnapshot};
17use motosan_ta_stream::types::{Bar, Interval};
18
19use crate::technical_analysis::TechnicalIndicators;
20use crate::Candle;
21
22// ---------------------------------------------------------------------------
23// Engine construction
24// ---------------------------------------------------------------------------
25
26/// Build a `TaEngine` with the standard set of indicators used by strategy
27/// templates. The returned engine covers every indicator present in
28/// [`TechnicalIndicators`].
29pub fn build_default_engine(symbol: &str) -> TaEngine {
30    TaEngine::new(symbol, Interval::H1)
31        // Moving averages
32        .add("SMA_20", Sma::new(MaConfig { period: 20 }))
33        .add("SMA_50", Sma::new(MaConfig { period: 50 }))
34        .add("EMA_12", Ema::new(MaConfig { period: 12 }))
35        .add("EMA_20", Ema::new(MaConfig { period: 20 }))
36        .add("EMA_26", Ema::new(MaConfig { period: 26 }))
37        .add("EMA_50", Ema::new(MaConfig { period: 50 }))
38        // Momentum
39        .add("RSI_14", Rsi::new(RsiConfig { period: 14 }))
40        .add(
41            "MACD",
42            Macd::new(MacdConfig {
43                fast: 12,
44                slow: 26,
45                signal: 9,
46            }),
47        )
48        .add("CCI_20", Cci::new(CciConfig { period: 20 }))
49        .add(
50            "WILLIAMS_R_14",
51            WilliamsR::new(WilliamsRConfig { period: 14 }),
52        )
53        .add("ROC_12", Roc::new(RocConfig { period: 12 }))
54        .add("MFI_14", Mfi::new(MfiConfig { period: 14 }))
55        // Volatility
56        .add("ATR_14", Atr::new(AtrConfig { period: 14 }))
57        .add(
58            "BB_20",
59            Bbands::new(BbandsConfig {
60                period: 20,
61                std_dev: 2.0,
62            }),
63        )
64        .add("ADX_14", Adx::new(AdxConfig { period: 14 }))
65        .add(
66            "KC_20",
67            Keltner::new(KeltnerConfig {
68                period: 20,
69                multiplier: 1.5,
70            }),
71        )
72        // Trend
73        .add(
74            "SUPERTREND",
75            Supertrend::new(SupertrendConfig {
76                period: 10,
77                multiplier: 3.0,
78            }),
79        )
80        // Channel
81        .add("DONCHIAN_20", Donchian::new(DonchianConfig { period: 20 }))
82        .add("DONCHIAN_10", Donchian::new(DonchianConfig { period: 10 }))
83        // VWAP
84        .add("VWAP", Vwap::new(VwapConfig { auto_reset: false }))
85}
86
87// ---------------------------------------------------------------------------
88// Snapshot calculation
89// ---------------------------------------------------------------------------
90
91/// Feed a slice of candles through a `TaEngine` and return the final snapshot.
92///
93/// Returns `None` if the candle slice is empty or no indicators have warmed up.
94pub fn calculate_snapshot(candles: &[Candle], symbol: &str) -> Option<TaSnapshot> {
95    let mut engine = build_default_engine(symbol);
96    let mut last_snapshot = None;
97
98    for candle in candles {
99        let bar = Bar {
100            time: candle.time as i64 * 1000, // epoch seconds -> millis
101            open: candle.open,
102            high: candle.high,
103            low: candle.low,
104            close: candle.close,
105            volume: candle.volume,
106            is_closed: true,
107        };
108        if let Some(snapshot) = engine.feed(&bar) {
109            last_snapshot = Some(snapshot);
110        }
111    }
112
113    last_snapshot
114}
115
116// ---------------------------------------------------------------------------
117// Value accessors
118// ---------------------------------------------------------------------------
119
120/// Get a single (or primary) value from a `TaSnapshot` by indicator key.
121///
122/// For multi-value indicators the function tries common primary keys in order:
123/// `value`, `line`, `adx`, `k`.
124pub fn get_snapshot_value(snapshot: &TaSnapshot, key: &str) -> Option<f64> {
125    match snapshot.results.get(key)? {
126        IndicatorValue::Single(v) => Some(*v),
127        IndicatorValue::Multi(map) => map
128            .get("value")
129            .or_else(|| map.get("line"))
130            .or_else(|| map.get("adx"))
131            .or_else(|| map.get("k"))
132            .copied(),
133    }
134}
135
136/// Get a named sub-value from a multi-value indicator.
137pub fn get_snapshot_sub_value(snapshot: &TaSnapshot, key: &str, sub_key: &str) -> Option<f64> {
138    match snapshot.results.get(key)? {
139        IndicatorValue::Multi(map) => map.get(sub_key).copied(),
140        IndicatorValue::Single(v) => {
141            if sub_key == "value" {
142                Some(*v)
143            } else {
144                None
145            }
146        }
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Snapshot -> TechnicalIndicators conversion
152// ---------------------------------------------------------------------------
153
154/// Convert a `TaSnapshot` into the legacy `TechnicalIndicators` struct.
155///
156/// This enables backward compatibility: callers that still use
157/// `evaluate_rules(&rules, &indicators, ...)` can obtain a `TechnicalIndicators`
158/// from a snapshot without changing any downstream code.
159pub fn snapshot_to_indicators(snapshot: &TaSnapshot) -> TechnicalIndicators {
160    TechnicalIndicators {
161        sma_20: get_snapshot_value(snapshot, "SMA_20"),
162        sma_50: get_snapshot_value(snapshot, "SMA_50"),
163        ema_12: get_snapshot_value(snapshot, "EMA_12"),
164        ema_20: get_snapshot_value(snapshot, "EMA_20"),
165        ema_26: get_snapshot_value(snapshot, "EMA_26"),
166        ema_50: get_snapshot_value(snapshot, "EMA_50"),
167        rsi_14: get_snapshot_value(snapshot, "RSI_14"),
168        macd_line: get_snapshot_sub_value(snapshot, "MACD", "macd"),
169        macd_signal: get_snapshot_sub_value(snapshot, "MACD", "signal"),
170        macd_histogram: get_snapshot_sub_value(snapshot, "MACD", "histogram"),
171        bb_upper: get_snapshot_sub_value(snapshot, "BB_20", "upper"),
172        bb_middle: get_snapshot_sub_value(snapshot, "BB_20", "middle"),
173        bb_lower: get_snapshot_sub_value(snapshot, "BB_20", "lower"),
174        atr_14: get_snapshot_value(snapshot, "ATR_14"),
175        adx_14: get_snapshot_sub_value(snapshot, "ADX_14", "adx"),
176        stoch_k: None, // Stochastic not available in ta-stream
177        stoch_d: None,
178        cci_20: get_snapshot_value(snapshot, "CCI_20"),
179        williams_r_14: get_snapshot_value(snapshot, "WILLIAMS_R_14"),
180        obv: None, // OBV not available in ta-stream
181        mfi_14: get_snapshot_value(snapshot, "MFI_14"),
182        roc_12: get_snapshot_value(snapshot, "ROC_12"),
183        donchian_upper_20: get_snapshot_sub_value(snapshot, "DONCHIAN_20", "upper"),
184        donchian_lower_20: get_snapshot_sub_value(snapshot, "DONCHIAN_20", "lower"),
185        donchian_upper_10: get_snapshot_sub_value(snapshot, "DONCHIAN_10", "upper"),
186        donchian_lower_10: get_snapshot_sub_value(snapshot, "DONCHIAN_10", "lower"),
187        close_zscore_20: None, // Z-score not available in ta-stream
188        volume_zscore_20: None,
189        hv_20: None, // HV not available in ta-stream
190        hv_60: None,
191        kc_upper_20: get_snapshot_sub_value(snapshot, "KC_20", "upper"),
192        kc_lower_20: get_snapshot_sub_value(snapshot, "KC_20", "lower"),
193        supertrend_value: get_snapshot_sub_value(snapshot, "SUPERTREND", "value"),
194        supertrend_direction: get_snapshot_sub_value(snapshot, "SUPERTREND", "direction"),
195        vwap: get_snapshot_value(snapshot, "VWAP"),
196        plus_di_14: get_snapshot_sub_value(snapshot, "ADX_14", "di_plus"),
197        minus_di_14: get_snapshot_sub_value(snapshot, "ADX_14", "di_minus"),
198    }
199}