hyper-ta 0.1.0

Technical analysis indicators — RSI, MACD, Bollinger Bands, multi-timeframe
Documentation
//! TaEngine-based dynamic indicator calculation.
//!
//! This module provides a bridge between the streaming `motosan-ta-stream` engine
//! and the existing `TechnicalIndicators` struct. It enables two workflows:
//!
//! 1. **`calculate_snapshot`** -- feeds candles through a `TaEngine` and returns a
//!    `TaSnapshot` containing all indicator values.
//! 2. **`snapshot_to_indicators`** -- converts a `TaSnapshot` back into the legacy
//!    `TechnicalIndicators` struct for backward compatibility with the rule engine.
//!
//! Helper functions `get_snapshot_value` / `get_snapshot_sub_value` provide direct
//! access to individual indicator values inside a snapshot.

use motosan_ta_stream::engine::TaEngine;
use motosan_ta_stream::indicator::*;
use motosan_ta_stream::snapshot::{IndicatorValue, TaSnapshot};
use motosan_ta_stream::types::{Bar, Interval};

use crate::technical_analysis::TechnicalIndicators;
use crate::Candle;

// ---------------------------------------------------------------------------
// Engine construction
// ---------------------------------------------------------------------------

/// Build a `TaEngine` with the standard set of indicators used by strategy
/// templates. The returned engine covers every indicator present in
/// [`TechnicalIndicators`].
pub fn build_default_engine(symbol: &str) -> TaEngine {
    TaEngine::new(symbol, Interval::H1)
        // Moving averages
        .add("SMA_20", Sma::new(MaConfig { period: 20 }))
        .add("SMA_50", Sma::new(MaConfig { period: 50 }))
        .add("EMA_12", Ema::new(MaConfig { period: 12 }))
        .add("EMA_20", Ema::new(MaConfig { period: 20 }))
        .add("EMA_26", Ema::new(MaConfig { period: 26 }))
        .add("EMA_50", Ema::new(MaConfig { period: 50 }))
        // Momentum
        .add("RSI_14", Rsi::new(RsiConfig { period: 14 }))
        .add(
            "MACD",
            Macd::new(MacdConfig {
                fast: 12,
                slow: 26,
                signal: 9,
            }),
        )
        .add("CCI_20", Cci::new(CciConfig { period: 20 }))
        .add(
            "WILLIAMS_R_14",
            WilliamsR::new(WilliamsRConfig { period: 14 }),
        )
        .add("ROC_12", Roc::new(RocConfig { period: 12 }))
        .add("MFI_14", Mfi::new(MfiConfig { period: 14 }))
        // Volatility
        .add("ATR_14", Atr::new(AtrConfig { period: 14 }))
        .add(
            "BB_20",
            Bbands::new(BbandsConfig {
                period: 20,
                std_dev: 2.0,
            }),
        )
        .add("ADX_14", Adx::new(AdxConfig { period: 14 }))
        .add(
            "KC_20",
            Keltner::new(KeltnerConfig {
                period: 20,
                multiplier: 1.5,
            }),
        )
        // Trend
        .add(
            "SUPERTREND",
            Supertrend::new(SupertrendConfig {
                period: 10,
                multiplier: 3.0,
            }),
        )
        // Channel
        .add("DONCHIAN_20", Donchian::new(DonchianConfig { period: 20 }))
        .add("DONCHIAN_10", Donchian::new(DonchianConfig { period: 10 }))
        // VWAP
        .add("VWAP", Vwap::new(VwapConfig { auto_reset: false }))
}

// ---------------------------------------------------------------------------
// Snapshot calculation
// ---------------------------------------------------------------------------

/// Feed a slice of candles through a `TaEngine` and return the final snapshot.
///
/// Returns `None` if the candle slice is empty or no indicators have warmed up.
pub fn calculate_snapshot(candles: &[Candle], symbol: &str) -> Option<TaSnapshot> {
    let mut engine = build_default_engine(symbol);
    let mut last_snapshot = None;

    for candle in candles {
        let bar = Bar {
            time: candle.time as i64 * 1000, // epoch seconds -> millis
            open: candle.open,
            high: candle.high,
            low: candle.low,
            close: candle.close,
            volume: candle.volume,
            is_closed: true,
        };
        if let Some(snapshot) = engine.feed(&bar) {
            last_snapshot = Some(snapshot);
        }
    }

    last_snapshot
}

// ---------------------------------------------------------------------------
// Value accessors
// ---------------------------------------------------------------------------

/// Get a single (or primary) value from a `TaSnapshot` by indicator key.
///
/// For multi-value indicators the function tries common primary keys in order:
/// `value`, `line`, `adx`, `k`.
pub fn get_snapshot_value(snapshot: &TaSnapshot, key: &str) -> Option<f64> {
    match snapshot.results.get(key)? {
        IndicatorValue::Single(v) => Some(*v),
        IndicatorValue::Multi(map) => map
            .get("value")
            .or_else(|| map.get("line"))
            .or_else(|| map.get("adx"))
            .or_else(|| map.get("k"))
            .copied(),
    }
}

/// Get a named sub-value from a multi-value indicator.
pub fn get_snapshot_sub_value(snapshot: &TaSnapshot, key: &str, sub_key: &str) -> Option<f64> {
    match snapshot.results.get(key)? {
        IndicatorValue::Multi(map) => map.get(sub_key).copied(),
        IndicatorValue::Single(v) => {
            if sub_key == "value" {
                Some(*v)
            } else {
                None
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Snapshot -> TechnicalIndicators conversion
// ---------------------------------------------------------------------------

/// Convert a `TaSnapshot` into the legacy `TechnicalIndicators` struct.
///
/// This enables backward compatibility: callers that still use
/// `evaluate_rules(&rules, &indicators, ...)` can obtain a `TechnicalIndicators`
/// from a snapshot without changing any downstream code.
pub fn snapshot_to_indicators(snapshot: &TaSnapshot) -> TechnicalIndicators {
    TechnicalIndicators {
        sma_20: get_snapshot_value(snapshot, "SMA_20"),
        sma_50: get_snapshot_value(snapshot, "SMA_50"),
        ema_12: get_snapshot_value(snapshot, "EMA_12"),
        ema_20: get_snapshot_value(snapshot, "EMA_20"),
        ema_26: get_snapshot_value(snapshot, "EMA_26"),
        ema_50: get_snapshot_value(snapshot, "EMA_50"),
        rsi_14: get_snapshot_value(snapshot, "RSI_14"),
        macd_line: get_snapshot_sub_value(snapshot, "MACD", "macd"),
        macd_signal: get_snapshot_sub_value(snapshot, "MACD", "signal"),
        macd_histogram: get_snapshot_sub_value(snapshot, "MACD", "histogram"),
        bb_upper: get_snapshot_sub_value(snapshot, "BB_20", "upper"),
        bb_middle: get_snapshot_sub_value(snapshot, "BB_20", "middle"),
        bb_lower: get_snapshot_sub_value(snapshot, "BB_20", "lower"),
        atr_14: get_snapshot_value(snapshot, "ATR_14"),
        adx_14: get_snapshot_sub_value(snapshot, "ADX_14", "adx"),
        stoch_k: None, // Stochastic not available in ta-stream
        stoch_d: None,
        cci_20: get_snapshot_value(snapshot, "CCI_20"),
        williams_r_14: get_snapshot_value(snapshot, "WILLIAMS_R_14"),
        obv: None, // OBV not available in ta-stream
        mfi_14: get_snapshot_value(snapshot, "MFI_14"),
        roc_12: get_snapshot_value(snapshot, "ROC_12"),
        donchian_upper_20: get_snapshot_sub_value(snapshot, "DONCHIAN_20", "upper"),
        donchian_lower_20: get_snapshot_sub_value(snapshot, "DONCHIAN_20", "lower"),
        donchian_upper_10: get_snapshot_sub_value(snapshot, "DONCHIAN_10", "upper"),
        donchian_lower_10: get_snapshot_sub_value(snapshot, "DONCHIAN_10", "lower"),
        close_zscore_20: None, // Z-score not available in ta-stream
        volume_zscore_20: None,
        hv_20: None, // HV not available in ta-stream
        hv_60: None,
        kc_upper_20: get_snapshot_sub_value(snapshot, "KC_20", "upper"),
        kc_lower_20: get_snapshot_sub_value(snapshot, "KC_20", "lower"),
        supertrend_value: get_snapshot_sub_value(snapshot, "SUPERTREND", "value"),
        supertrend_direction: get_snapshot_sub_value(snapshot, "SUPERTREND", "direction"),
        vwap: get_snapshot_value(snapshot, "VWAP"),
        plus_di_14: get_snapshot_sub_value(snapshot, "ADX_14", "di_plus"),
        minus_di_14: get_snapshot_sub_value(snapshot, "ADX_14", "di_minus"),
    }
}