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