Skip to main content

finance_query/indicators/
summary.rs

1//! Indicators summary module.
2//!
3//! Provides the `IndicatorsSummary` type which calculates and returns the latest
4//! values for all 52+ technical indicators at once.
5//!
6//! This module reuses the main indicator implementations and extracts the last value,
7//! ensuring consistency and eliminating code duplication.
8
9use super::last_value;
10use crate::Candle;
11use crate::indicators::{
12    accumulation_distribution, adx, alma, aroon, atr, atr::atr_raw, awesome_oscillator,
13    balance_of_power, bollinger_bands, bull_bear_power, cci, chaikin_oscillator, choppiness_index,
14    cmf, cmo, coppock_curve, dema, donchian_channels, elder_ray, ema::ema_raw, hma, ichimoku,
15    keltner_channels::keltner_with_atr_dense, macd, mcginley_dynamic, mfi, momentum, obv,
16    parabolic_sar, roc, rsi::rsi_raw, sma::sma_raw, stochastic,
17    stochastic_rsi::stochastic_rsi_from_rsi_dense, supertrend::supertrend_with_atr_dense, tema,
18    true_range, vwap, vwma, williams_r, wma::wma_raw,
19};
20
21/// Helper to extract last value from Result-returning indicators
22#[inline]
23fn last_from_result(result: crate::indicators::Result<Vec<Option<f64>>>) -> Option<f64> {
24    result.ok().and_then(|v| last_value(&v))
25}
26
27/// Calculate all technical indicators from candle data.
28///
29/// Returns the latest values for all implemented indicators.
30/// Reuses the main indicator implementations for consistency.
31pub(crate) fn calculate_indicators(candles: &[Candle]) -> IndicatorsSummary {
32    if candles.is_empty() {
33        return IndicatorsSummary::default();
34    }
35
36    // Extract price data from candles in a single pass (avoids 5 separate iterations)
37    let len = candles.len();
38    let mut closes = Vec::with_capacity(len);
39    let mut highs = Vec::with_capacity(len);
40    let mut lows = Vec::with_capacity(len);
41    let mut opens = Vec::with_capacity(len);
42    let mut volumes = Vec::with_capacity(len);
43    for c in candles {
44        closes.push(c.close);
45        highs.push(c.high);
46        lows.push(c.low);
47        opens.push(c.open);
48        volumes.push(c.volume as f64);
49    }
50
51    // Pre-compute shared intermediates (avoids redundant passes)
52    let rsi_14_dense = rsi_raw(&closes, 14).ok();
53    let atr_10_dense = atr_raw(&highs, &lows, &closes, 10).ok();
54
55    IndicatorsSummary {
56        // === MOVING AVERAGES ===
57        sma_10: sma_raw(&closes, 10).last().copied(),
58        sma_20: sma_raw(&closes, 20).last().copied(),
59        sma_50: sma_raw(&closes, 50).last().copied(),
60        sma_100: sma_raw(&closes, 100).last().copied(),
61        sma_200: sma_raw(&closes, 200).last().copied(),
62
63        ema_10: ema_raw(&closes, 10).last().copied(),
64        ema_20: ema_raw(&closes, 20).last().copied(),
65        ema_50: ema_raw(&closes, 50).last().copied(),
66        ema_100: ema_raw(&closes, 100).last().copied(),
67        ema_200: ema_raw(&closes, 200).last().copied(),
68
69        wma_10: wma_raw(&closes, 10).last().copied(),
70        wma_20: wma_raw(&closes, 20).last().copied(),
71        wma_50: wma_raw(&closes, 50).last().copied(),
72        wma_100: wma_raw(&closes, 100).last().copied(),
73        wma_200: wma_raw(&closes, 200).last().copied(),
74
75        // Advanced Moving Averages (Result types)
76        dema_20: dema(&closes, 20).ok().and_then(|v| last_value(&v)),
77        tema_20: tema(&closes, 20).ok().and_then(|v| last_value(&v)),
78        hma_20: hma(&closes, 20).ok().and_then(|v| last_value(&v)),
79        vwma_20: vwma(&closes, &volumes, 20)
80            .ok()
81            .and_then(|v| last_value(&v)),
82        alma_9: alma(&closes, 9, 0.85, 6.0)
83            .ok()
84            .and_then(|v| last_value(&v)),
85        mcginley_dynamic_20: mcginley_dynamic(&closes, 20)
86            .ok()
87            .and_then(|v| last_value(&v)),
88
89        // === MOMENTUM OSCILLATORS ===
90        rsi_14: rsi_14_dense.as_deref().and_then(|v| v.last().copied()),
91        stochastic: {
92            stochastic(&highs, &lows, &closes, 14, 1, 3)
93                .ok()
94                .map(|result| StochasticData {
95                    k: last_value(&result.k),
96                    d: last_value(&result.d),
97                })
98        },
99        stochastic_rsi: {
100            rsi_14_dense.as_deref().and_then(|rsi_dense| {
101                stochastic_rsi_from_rsi_dense(rsi_dense, len, 14, 14, 3, 3)
102                    .ok()
103                    .map(|result| StochasticData {
104                        k: last_value(&result.k),
105                        d: last_value(&result.d),
106                    })
107            })
108        },
109        cci_20: last_from_result(cci(&highs, &lows, &closes, 20)),
110        williams_r_14: last_from_result(williams_r(&highs, &lows, &closes, 14)),
111        roc_12: last_from_result(roc(&closes, 12)),
112        momentum_10: last_from_result(momentum(&closes, 10)),
113        cmo_14: last_from_result(cmo(&closes, 14)),
114        awesome_oscillator: last_from_result(awesome_oscillator(&highs, &lows, 5, 34)),
115        coppock_curve: last_from_result(coppock_curve(&closes, 14, 11, 10)),
116
117        // === TREND INDICATORS ===
118        macd: {
119            macd(&closes, 12, 26, 9).ok().map(|result| MacdData {
120                macd: last_value(&result.macd_line),
121                signal: last_value(&result.signal_line),
122                histogram: last_value(&result.histogram),
123            })
124        },
125        adx_14: last_from_result(adx(&highs, &lows, &closes, 14)),
126        aroon: {
127            aroon(&highs, &lows, 25).ok().map(|result| AroonData {
128                aroon_up: last_value(&result.aroon_up),
129                aroon_down: last_value(&result.aroon_down),
130            })
131        },
132        supertrend: {
133            atr_10_dense.as_deref().and_then(|atr_dense| {
134                supertrend_with_atr_dense(&highs, &lows, &closes, atr_dense, 10, 3.0)
135                    .ok()
136                    .map(|result| SuperTrendData {
137                        value: last_value(&result.value),
138                        trend: result.is_uptrend.last().and_then(|&v| v).map(|v| {
139                            if v {
140                                "up".to_string()
141                            } else {
142                                "down".to_string()
143                            }
144                        }),
145                    })
146            })
147        },
148        ichimoku: {
149            ichimoku(&highs, &lows, &closes, 9, 26, 26, 26)
150                .ok()
151                .map(|result| IchimokuData {
152                    conversion_line: last_value(&result.conversion_line),
153                    base_line: last_value(&result.base_line),
154                    leading_span_a: last_value(&result.leading_span_a),
155                    leading_span_b: last_value(&result.leading_span_b),
156                    lagging_span: last_value(&result.lagging_span),
157                })
158        },
159        parabolic_sar: last_from_result(parabolic_sar(&highs, &lows, &closes, 0.02, 0.2)),
160        bull_bear_power: {
161            bull_bear_power(&highs, &lows, &closes, 13)
162                .ok()
163                .map(|result| BullBearPowerData {
164                    bull_power: last_value(&result.bull_power),
165                    bear_power: last_value(&result.bear_power),
166                })
167        },
168        elder_ray_index: {
169            elder_ray(&highs, &lows, &closes, 13)
170                .ok()
171                .map(|result| ElderRayData {
172                    bull_power: last_value(&result.bull_power),
173                    bear_power: last_value(&result.bear_power),
174                })
175        },
176
177        // === VOLATILITY INDICATORS ===
178        bollinger_bands: {
179            bollinger_bands(&closes, 20, 2.0)
180                .ok()
181                .map(|result| BollingerBandsData {
182                    upper: last_value(&result.upper),
183                    middle: last_value(&result.middle),
184                    lower: last_value(&result.lower),
185                })
186        },
187        keltner_channels: {
188            atr_10_dense.as_deref().and_then(|atr_dense| {
189                keltner_with_atr_dense(&closes, 20, atr_dense, 10, 2.0)
190                    .ok()
191                    .map(|result| KeltnerChannelsData {
192                        upper: last_value(&result.upper),
193                        middle: last_value(&result.middle),
194                        lower: last_value(&result.lower),
195                    })
196            })
197        },
198        donchian_channels: {
199            donchian_channels(&highs, &lows, 20)
200                .ok()
201                .map(|result| DonchianChannelsData {
202                    upper: last_value(&result.upper),
203                    middle: last_value(&result.middle),
204                    lower: last_value(&result.lower),
205                })
206        },
207        atr_14: last_from_result(atr(&highs, &lows, &closes, 14)),
208        true_range: last_from_result(true_range(&highs, &lows, &closes)),
209        choppiness_index_14: last_from_result(choppiness_index(&highs, &lows, &closes, 14)),
210
211        // === VOLUME INDICATORS ===
212        obv: last_from_result(obv(&closes, &volumes)),
213        mfi_14: last_from_result(mfi(&highs, &lows, &closes, &volumes, 14)),
214        cmf_20: last_from_result(cmf(&highs, &lows, &closes, &volumes, 20)),
215        chaikin_oscillator: last_from_result(chaikin_oscillator(&highs, &lows, &closes, &volumes)),
216        accumulation_distribution: last_from_result(accumulation_distribution(
217            &highs, &lows, &closes, &volumes,
218        )),
219        vwap: last_from_result(vwap(&highs, &lows, &closes, &volumes)),
220        balance_of_power: last_from_result(balance_of_power(&opens, &highs, &lows, &closes, None)),
221    }
222}
223
224/// Summary of all calculated technical indicators
225#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
226#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
227#[serde(rename_all = "camelCase")]
228pub struct IndicatorsSummary {
229    // === MOVING AVERAGES ===
230    // Simple Moving Averages
231    /// Simple Moving Average (10-period)
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub sma_10: Option<f64>,
234    /// Simple Moving Average (20-period)
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub sma_20: Option<f64>,
237    /// Simple Moving Average (50-period)
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub sma_50: Option<f64>,
240    /// Simple Moving Average (100-period)
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub sma_100: Option<f64>,
243    /// Simple Moving Average (200-period)
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub sma_200: Option<f64>,
246
247    // Exponential Moving Averages
248    /// Exponential Moving Average (10-period)
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub ema_10: Option<f64>,
251    /// Exponential Moving Average (20-period)
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub ema_20: Option<f64>,
254    /// Exponential Moving Average (50-period)
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub ema_50: Option<f64>,
257    /// Exponential Moving Average (100-period)
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub ema_100: Option<f64>,
260    /// Exponential Moving Average (200-period)
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub ema_200: Option<f64>,
263
264    // Weighted Moving Averages
265    /// Weighted Moving Average (10-period)
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub wma_10: Option<f64>,
268    /// Weighted Moving Average (20-period)
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub wma_20: Option<f64>,
271    /// Weighted Moving Average (50-period)
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub wma_50: Option<f64>,
274    /// Weighted Moving Average (100-period)
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub wma_100: Option<f64>,
277    /// Weighted Moving Average (200-period)
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub wma_200: Option<f64>,
280
281    // Advanced Moving Averages
282    /// Double Exponential Moving Average (20-period)
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub dema_20: Option<f64>,
285    /// Triple Exponential Moving Average (20-period)
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub tema_20: Option<f64>,
288    /// Hull Moving Average (20-period)
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub hma_20: Option<f64>,
291    /// Volume Weighted Moving Average (20-period)
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub vwma_20: Option<f64>,
294    /// Arnaud Legoux Moving Average (9-period)
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub alma_9: Option<f64>,
297    /// McGinley Dynamic (20-period)
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub mcginley_dynamic_20: Option<f64>,
300
301    // === MOMENTUM OSCILLATORS ===
302    /// Relative Strength Index (14-period)
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub rsi_14: Option<f64>,
305    /// Stochastic Oscillator (14, 3, 3)
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub stochastic: Option<StochasticData>,
308    /// Commodity Channel Index (20-period)
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub cci_20: Option<f64>,
311    /// Williams %R (14-period)
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub williams_r_14: Option<f64>,
314    /// Stochastic RSI (14, 14)
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub stochastic_rsi: Option<StochasticData>,
317    /// Rate of Change (12-period)
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub roc_12: Option<f64>,
320    /// Momentum (10-period)
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub momentum_10: Option<f64>,
323    /// Chande Momentum Oscillator (14-period)
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub cmo_14: Option<f64>,
326    /// Awesome Oscillator (5, 34)
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub awesome_oscillator: Option<f64>,
329    /// Coppock Curve (10, 11, 14)
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub coppock_curve: Option<f64>,
332
333    // === TREND INDICATORS ===
334    /// Moving Average Convergence Divergence (12, 26, 9)
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub macd: Option<MacdData>,
337    /// Average Directional Index (14-period)
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub adx_14: Option<f64>,
340    /// Aroon Indicator (25-period)
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub aroon: Option<AroonData>,
343    /// SuperTrend Indicator (10, 3.0)
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub supertrend: Option<SuperTrendData>,
346    /// Ichimoku Cloud (9, 26, 52, 26)
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub ichimoku: Option<IchimokuData>,
349    /// Parabolic SAR (0.02, 0.2)
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub parabolic_sar: Option<f64>,
352    /// Bull Bear Power (13-period EMA based)
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub bull_bear_power: Option<BullBearPowerData>,
355    /// Elder Ray Index (13-period EMA based)
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub elder_ray_index: Option<ElderRayData>,
358
359    // === VOLATILITY INDICATORS ===
360    /// Bollinger Bands (20, 2.0)
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub bollinger_bands: Option<BollingerBandsData>,
363    /// Average True Range (14-period)
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub atr_14: Option<f64>,
366    /// Keltner Channels (20, 10, 2.0)
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub keltner_channels: Option<KeltnerChannelsData>,
369    /// Donchian Channels (20-period)
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub donchian_channels: Option<DonchianChannelsData>,
372    /// True Range (current period)
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub true_range: Option<f64>,
375    /// Choppiness Index (14-period)
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub choppiness_index_14: Option<f64>,
378
379    // === VOLUME INDICATORS ===
380    /// On-Balance Volume
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub obv: Option<f64>,
383    /// Money Flow Index (14-period)
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub mfi_14: Option<f64>,
386    /// Chaikin Money Flow (20-period)
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub cmf_20: Option<f64>,
389    /// Chaikin Oscillator (3, 10)
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub chaikin_oscillator: Option<f64>,
392    /// Accumulation/Distribution Line
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub accumulation_distribution: Option<f64>,
395    /// Volume Weighted Average Price
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub vwap: Option<f64>,
398    /// Balance of Power
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub balance_of_power: Option<f64>,
401}
402
403/// Stochastic Oscillator data
404#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
405#[serde(rename_all = "camelCase")]
406pub struct StochasticData {
407    /// %K line value
408    #[serde(rename = "%K", skip_serializing_if = "Option::is_none")]
409    pub k: Option<f64>,
410    /// %D line value
411    #[serde(rename = "%D", skip_serializing_if = "Option::is_none")]
412    pub d: Option<f64>,
413}
414
415/// MACD indicator data
416#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
417#[serde(rename_all = "camelCase")]
418pub struct MacdData {
419    /// MACD line value
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub macd: Option<f64>,
422    /// Signal line value
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub signal: Option<f64>,
425    /// Histogram value
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub histogram: Option<f64>,
428}
429
430/// Aroon indicator data
431#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
432#[serde(rename_all = "camelCase")]
433pub struct AroonData {
434    /// Aroon Up value
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub aroon_up: Option<f64>,
437    /// Aroon Down value
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub aroon_down: Option<f64>,
440}
441
442/// Bollinger Bands data
443#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
444#[serde(rename_all = "camelCase")]
445pub struct BollingerBandsData {
446    /// Upper band value
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub upper: Option<f64>,
449    /// Middle band value
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub middle: Option<f64>,
452    /// Lower band value
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub lower: Option<f64>,
455}
456
457/// SuperTrend indicator data
458#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
459#[serde(rename_all = "camelCase")]
460pub struct SuperTrendData {
461    /// SuperTrend value
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub value: Option<f64>,
464    /// Trend direction
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub trend: Option<String>,
467}
468
469/// Ichimoku Cloud data
470#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub struct IchimokuData {
473    /// Conversion line (Tenkan-sen)
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub conversion_line: Option<f64>,
476    /// Base line (Kijun-sen)
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub base_line: Option<f64>,
479    /// Leading Span A (Senkou Span A)
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub leading_span_a: Option<f64>,
482    /// Leading Span B (Senkou Span B)
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub leading_span_b: Option<f64>,
485    /// Lagging Span (Chikou Span)
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub lagging_span: Option<f64>,
488}
489
490/// Keltner Channels data
491#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
492#[serde(rename_all = "camelCase")]
493pub struct KeltnerChannelsData {
494    /// Upper channel value
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub upper: Option<f64>,
497    /// Middle channel value
498    #[serde(skip_serializing_if = "Option::is_none")]
499    pub middle: Option<f64>,
500    /// Lower channel value
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub lower: Option<f64>,
503}
504
505/// Donchian Channels data
506#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
507#[serde(rename_all = "camelCase")]
508pub struct DonchianChannelsData {
509    /// Upper channel value
510    #[serde(skip_serializing_if = "Option::is_none")]
511    pub upper: Option<f64>,
512    /// Middle channel value
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub middle: Option<f64>,
515    /// Lower channel value
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub lower: Option<f64>,
518}
519
520/// Bull Bear Power indicator data
521#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
522#[serde(rename_all = "camelCase")]
523pub struct BullBearPowerData {
524    /// Bull power value
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub bull_power: Option<f64>,
527    /// Bear power value
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub bear_power: Option<f64>,
530}
531
532/// Elder Ray Index data
533#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct ElderRayData {
536    /// Bull power value
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub bull_power: Option<f64>,
539    /// Bear power value
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub bear_power: Option<f64>,
542}