finance_query/models/indicators/
mod.rs

1//! Internal technical indicators calculation module.
2//!
3//! This module is NOT part of the public API. Users access indicators through
4//! `Ticker::indicators()` and `AsyncTicker::indicators()` methods only.
5
6mod momentum;
7mod moving_avg;
8mod trend;
9mod volatility;
10mod volume;
11
12use crate::models::chart::Candle;
13
14/// Calculate all technical indicators from candle data.
15///
16/// Returns the latest values for all implemented indicators.
17pub(crate) fn calculate_indicators(candles: &[Candle]) -> IndicatorsSummary {
18    if candles.is_empty() {
19        return IndicatorsSummary::default();
20    }
21
22    let (closes, highs, lows, opens, volumes) = prepare_data(candles);
23
24    IndicatorsSummary {
25        // === MOVING AVERAGES ===
26        // Simple Moving Averages
27        sma_10: moving_avg::sma(&closes, 10),
28        sma_20: moving_avg::sma(&closes, 20),
29        sma_50: moving_avg::sma(&closes, 50),
30        sma_100: moving_avg::sma(&closes, 100),
31        sma_200: moving_avg::sma(&closes, 200),
32        // Exponential Moving Averages
33        ema_10: moving_avg::ema(&closes, 10),
34        ema_20: moving_avg::ema(&closes, 20),
35        ema_50: moving_avg::ema(&closes, 50),
36        ema_100: moving_avg::ema(&closes, 100),
37        ema_200: moving_avg::ema(&closes, 200),
38        // Weighted Moving Averages
39        wma_10: moving_avg::wma(&closes, 10),
40        wma_20: moving_avg::wma(&closes, 20),
41        wma_50: moving_avg::wma(&closes, 50),
42        wma_100: moving_avg::wma(&closes, 100),
43        wma_200: moving_avg::wma(&closes, 200),
44        // Advanced Moving Averages
45        dema_20: moving_avg::dema(&closes, 20),
46        tema_20: moving_avg::tema(&closes, 20),
47        hma_20: moving_avg::hma(&closes, 20),
48        vwma_20: moving_avg::vwma(&closes, &volumes, 20),
49        alma_9: moving_avg::alma(&closes, 9, 0.85, 6.0),
50        mcginley_dynamic_20: moving_avg::mcginley_dynamic(&closes, 20),
51
52        // === MOMENTUM OSCILLATORS ===
53        rsi_14: momentum::rsi(&closes, 14),
54        stochastic: {
55            let (k, d) = momentum::stochastic(&highs, &lows, &closes, 14, 3);
56            Some(StochasticData { k, d })
57        },
58        stochastic_rsi: momentum::stochastic_rsi(&closes, 14, 14).map(|k| StochasticData {
59            k: Some(k),
60            d: None,
61        }),
62        cci_20: momentum::cci(&highs, &lows, &closes, 20),
63        williams_r_14: momentum::williams_r(&highs, &lows, &closes, 14),
64        roc_12: momentum::roc(&closes, 12),
65        momentum_10: momentum::momentum(&closes, 10),
66        cmo_14: momentum::cmo(&closes, 14),
67        awesome_oscillator: momentum::awesome_oscillator(&highs, &lows),
68        coppock_curve: momentum::coppock_curve(&closes),
69
70        // === TREND INDICATORS ===
71        macd: Some(trend::macd(&closes, 12, 26, 9)),
72        adx_14: trend::adx(&highs, &lows, &closes, 14),
73        aroon: Some(trend::aroon(&highs, &lows, 25)),
74        supertrend: Some(trend::supertrend(&highs, &lows, &closes, 10, 3.0)),
75        ichimoku: Some(trend::ichimoku(&highs, &lows, &closes)),
76        parabolic_sar: trend::parabolic_sar(&highs, &lows, &closes, 0.02, 0.2),
77        bull_bear_power: Some(trend::bull_bear_power(&highs, &lows, &closes)),
78        elder_ray_index: Some(trend::elder_ray(&highs, &lows, &closes)),
79
80        // === VOLATILITY INDICATORS ===
81        bollinger_bands: Some(volatility::bollinger_bands(&closes, 20, 2.0)),
82        keltner_channels: Some(volatility::keltner_channels(
83            &highs, &lows, &closes, 20, 10, 2.0,
84        )),
85        donchian_channels: Some(volatility::donchian_channels(&highs, &lows, 20)),
86        atr_14: volatility::atr(&highs, &lows, &closes, 14),
87        true_range: volatility::true_range(&highs, &lows, &closes),
88        choppiness_index_14: volatility::choppiness_index(&highs, &lows, &closes, 14),
89
90        // === VOLUME INDICATORS ===
91        obv: volume::obv(&closes, &volumes),
92        mfi_14: volume::mfi(&highs, &lows, &closes, &volumes, 14),
93        cmf_20: volume::cmf(&highs, &lows, &closes, &volumes, 20),
94        chaikin_oscillator: volume::chaikin_oscillator(&highs, &lows, &closes, &volumes),
95        accumulation_distribution: volume::accumulation_distribution(
96            &highs, &lows, &closes, &volumes,
97        ),
98        vwap: volume::vwap(&highs, &lows, &closes, &volumes),
99        balance_of_power: volume::balance_of_power(&opens, &highs, &lows, &closes),
100    }
101}
102
103/// Price data extracted from candles
104type PriceData = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>);
105
106/// Extract price data arrays from candles
107fn prepare_data(candles: &[Candle]) -> PriceData {
108    let closes: Vec<f64> = candles.iter().map(|c| c.close).collect();
109    let highs: Vec<f64> = candles.iter().map(|c| c.high).collect();
110    let lows: Vec<f64> = candles.iter().map(|c| c.low).collect();
111    let opens: Vec<f64> = candles.iter().map(|c| c.open).collect();
112    let volumes: Vec<f64> = candles.iter().map(|c| c.volume as f64).collect();
113    (closes, highs, lows, opens, volumes)
114}
115
116/// Summary of all calculated technical indicators
117#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
118#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
119#[serde(rename_all = "camelCase")]
120pub struct IndicatorsSummary {
121    // === MOVING AVERAGES ===
122    // Simple Moving Averages
123    /// Simple Moving Average (10-period)
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub sma_10: Option<f64>,
126    /// Simple Moving Average (20-period)
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub sma_20: Option<f64>,
129    /// Simple Moving Average (50-period)
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub sma_50: Option<f64>,
132    /// Simple Moving Average (100-period)
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub sma_100: Option<f64>,
135    /// Simple Moving Average (200-period)
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub sma_200: Option<f64>,
138
139    // Exponential Moving Averages
140    /// Exponential Moving Average (10-period)
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub ema_10: Option<f64>,
143    /// Exponential Moving Average (20-period)
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub ema_20: Option<f64>,
146    /// Exponential Moving Average (50-period)
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub ema_50: Option<f64>,
149    /// Exponential Moving Average (100-period)
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub ema_100: Option<f64>,
152    /// Exponential Moving Average (200-period)
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub ema_200: Option<f64>,
155
156    // Weighted Moving Averages
157    /// Weighted Moving Average (10-period)
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub wma_10: Option<f64>,
160    /// Weighted Moving Average (20-period)
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub wma_20: Option<f64>,
163    /// Weighted Moving Average (50-period)
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub wma_50: Option<f64>,
166    /// Weighted Moving Average (100-period)
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub wma_100: Option<f64>,
169    /// Weighted Moving Average (200-period)
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub wma_200: Option<f64>,
172
173    // Advanced Moving Averages
174    /// Double Exponential Moving Average (20-period)
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub dema_20: Option<f64>,
177    /// Triple Exponential Moving Average (20-period)
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub tema_20: Option<f64>,
180    /// Hull Moving Average (20-period)
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub hma_20: Option<f64>,
183    /// Volume Weighted Moving Average (20-period)
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub vwma_20: Option<f64>,
186    /// Arnaud Legoux Moving Average (9-period)
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub alma_9: Option<f64>,
189    /// McGinley Dynamic (20-period)
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub mcginley_dynamic_20: Option<f64>,
192
193    // === MOMENTUM OSCILLATORS ===
194    /// Relative Strength Index (14-period)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub rsi_14: Option<f64>,
197    /// Stochastic Oscillator (14, 3, 3)
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub stochastic: Option<StochasticData>,
200    /// Commodity Channel Index (20-period)
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub cci_20: Option<f64>,
203    /// Williams %R (14-period)
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub williams_r_14: Option<f64>,
206    /// Stochastic RSI (14, 14)
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub stochastic_rsi: Option<StochasticData>,
209    /// Rate of Change (12-period)
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub roc_12: Option<f64>,
212    /// Momentum (10-period)
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub momentum_10: Option<f64>,
215    /// Chande Momentum Oscillator (14-period)
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub cmo_14: Option<f64>,
218    /// Awesome Oscillator (5, 34)
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub awesome_oscillator: Option<f64>,
221    /// Coppock Curve (10, 11, 14)
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub coppock_curve: Option<f64>,
224
225    // === TREND INDICATORS ===
226    /// Moving Average Convergence Divergence (12, 26, 9)
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub macd: Option<MacdData>,
229    /// Average Directional Index (14-period)
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub adx_14: Option<f64>,
232    /// Aroon Indicator (25-period)
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub aroon: Option<AroonData>,
235    /// SuperTrend Indicator (10, 3.0)
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub supertrend: Option<SuperTrendData>,
238    /// Ichimoku Cloud (9, 26, 52, 26)
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub ichimoku: Option<IchimokuData>,
241    /// Parabolic SAR (0.02, 0.2)
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub parabolic_sar: Option<f64>,
244    /// Bull Bear Power (13-period EMA based)
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub bull_bear_power: Option<BullBearPowerData>,
247    /// Elder Ray Index (13-period EMA based)
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub elder_ray_index: Option<ElderRayData>,
250
251    // === VOLATILITY INDICATORS ===
252    /// Bollinger Bands (20, 2.0)
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub bollinger_bands: Option<BollingerBandsData>,
255    /// Average True Range (14-period)
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub atr_14: Option<f64>,
258    /// Keltner Channels (20, 10, 2.0)
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub keltner_channels: Option<KeltnerChannelsData>,
261    /// Donchian Channels (20-period)
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub donchian_channels: Option<DonchianChannelsData>,
264    /// True Range (current period)
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub true_range: Option<f64>,
267    /// Choppiness Index (14-period)
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub choppiness_index_14: Option<f64>,
270
271    // === VOLUME INDICATORS ===
272    /// On-Balance Volume
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub obv: Option<f64>,
275    /// Money Flow Index (14-period)
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub mfi_14: Option<f64>,
278    /// Chaikin Money Flow (20-period)
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub cmf_20: Option<f64>,
281    /// Chaikin Oscillator (3, 10)
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub chaikin_oscillator: Option<f64>,
284    /// Accumulation/Distribution Line
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub accumulation_distribution: Option<f64>,
287    /// Volume Weighted Average Price
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub vwap: Option<f64>,
290    /// Balance of Power
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub balance_of_power: Option<f64>,
293}
294
295/// Stochastic Oscillator data
296#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct StochasticData {
299    /// %K line value
300    #[serde(rename = "%K", skip_serializing_if = "Option::is_none")]
301    pub k: Option<f64>,
302    /// %D line value
303    #[serde(rename = "%D", skip_serializing_if = "Option::is_none")]
304    pub d: Option<f64>,
305}
306
307/// MACD indicator data
308#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct MacdData {
311    /// MACD line value
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub macd: Option<f64>,
314    /// Signal line value
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub signal: Option<f64>,
317    /// Histogram value
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub histogram: Option<f64>,
320}
321
322/// Aroon indicator data
323#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct AroonData {
326    /// Aroon Up value
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub aroon_up: Option<f64>,
329    /// Aroon Down value
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub aroon_down: Option<f64>,
332}
333
334/// Bollinger Bands data
335#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct BollingerBandsData {
338    /// Upper band value
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub upper: Option<f64>,
341    /// Middle band value
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub middle: Option<f64>,
344    /// Lower band value
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub lower: Option<f64>,
347}
348
349/// SuperTrend indicator data
350#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct SuperTrendData {
353    /// SuperTrend value
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub value: Option<f64>,
356    /// Trend direction
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub trend: Option<String>,
359}
360
361/// Ichimoku Cloud data
362#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct IchimokuData {
365    /// Conversion line (Tenkan-sen)
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub conversion_line: Option<f64>,
368    /// Base line (Kijun-sen)
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub base_line: Option<f64>,
371    /// Leading Span A (Senkou Span A)
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub leading_span_a: Option<f64>,
374    /// Leading Span B (Senkou Span B)
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub leading_span_b: Option<f64>,
377    /// Lagging Span (Chikou Span)
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub lagging_span: Option<f64>,
380}
381
382/// Keltner Channels data
383#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct KeltnerChannelsData {
386    /// Upper channel value
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub upper: Option<f64>,
389    /// Middle channel value
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub middle: Option<f64>,
392    /// Lower channel value
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub lower: Option<f64>,
395}
396
397/// Donchian Channels data
398#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct DonchianChannelsData {
401    /// Upper channel value
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub upper: Option<f64>,
404    /// Middle channel value
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub middle: Option<f64>,
407    /// Lower channel value
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub lower: Option<f64>,
410}
411
412/// Bull Bear Power indicator data
413#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
414#[serde(rename_all = "camelCase")]
415pub struct BullBearPowerData {
416    /// Bull power value
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub bull_power: Option<f64>,
419    /// Bear power value
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub bear_power: Option<f64>,
422}
423
424/// Elder Ray Index data
425#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
426#[serde(rename_all = "camelCase")]
427pub struct ElderRayData {
428    /// Bull power value
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub bull_power: Option<f64>,
431    /// Bear power value
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub bear_power: Option<f64>,
434}
435
436// === Helper Functions ===
437
438/// Round to 2 decimal places
439#[inline]
440pub(crate) fn round2(value: f64) -> f64 {
441    (value * 100.0).round() / 100.0
442}
443
444/// Get last value from vector, rounded to 2 decimals, handling NaN/Inf
445#[inline]
446#[allow(dead_code)]
447pub(crate) fn last(values: &[f64]) -> Option<f64> {
448    values
449        .last()
450        .and_then(|&v| if v.is_finite() { Some(round2(v)) } else { None })
451}