Skip to main content

easytradeview/
analysis.rs

1use super::*;
2use anyhow::{anyhow, Context, Result};
3use serde::Serialize;
4use std::str::FromStr;
5use std::{collections::HashMap, ops::Add};
6
7#[derive(Debug, PartialEq, Clone, Copy, Serialize)]
8pub enum Recommendation {
9    StrongSell,
10    Sell,
11    Neutral,
12    Buy,
13    StrongBuy,
14}
15
16impl Default for Recommendation {
17    fn default() -> Self {
18        Recommendation::Neutral
19    }
20}
21
22impl AsRef<str> for Recommendation {
23    fn as_ref(&self) -> &str {
24        match self {
25            Recommendation::StrongSell => "STRONG_SELL",
26            Recommendation::Sell => "SELL",
27            Recommendation::Neutral => "NEUTRAL",
28            Recommendation::Buy => "BUY",
29            Recommendation::StrongBuy => "STRONG_BUY",
30        }
31    }
32}
33
34impl std::fmt::Display for Recommendation {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "{}", self.as_ref())
37    }
38}
39
40impl FromStr for Recommendation {
41    type Err = anyhow::Error;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        match s {
45            "STRONG_SELL" => Ok(Recommendation::StrongSell),
46            "SELL" => Ok(Recommendation::Sell),
47            "NEUTRAL" => Ok(Recommendation::Neutral),
48            "BUY" => Ok(Recommendation::Buy),
49            "STRONG_BUF" => Ok(Recommendation::StrongBuy),
50            _ => Err(anyhow!("Invalid recommendation: {}", s)),
51        }
52    }
53}
54
55#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)]
56pub enum Signal {
57    RecommendOther,
58    RecommendAll,
59    RecommendMA,
60    RSI,
61    StochK,
62    CCI20,
63    ADX,
64    AO,
65    Mon,
66    MACD,
67    StochRsiK,
68    WR,
69    BBPower,
70    UO,
71    EMA10,
72    SMA10,
73    EMA20,
74    SMA20,
75    EMA30,
76    SMA30,
77    EMA50,
78    SMA50,
79    EMA100,
80    SMA100,
81    EMA200,
82    SMA200,
83    IchimokuBLine,
84    VWMA,
85    HullMA9,
86    PivotMClassicS3,
87    PivotMClassicS2,
88    PivotMClassicS1,
89    PivotMClassicMiddle,
90    PivotMClassicR1,
91    PivotMClassicR2,
92    PivotMClassicR3,
93    PivotMFibonacciS3,
94    PivotMFibonacciS2,
95    PivotMFibonacciS1,
96    PivotMFibonacciMiddle,
97    PivotMFibonacciR1,
98    PivotMFibonacciR2,
99    PivotMFibonacciR3,
100    PivotMCamarillaS3,
101    PivotMCamarillaS2,
102    PivotMCamarillaS1,
103    PivotMCamarillaMiddle,
104    PivotMCamarillaR1,
105    PivotMCamarillaR2,
106    PivotMCamarillaR3,
107    PivotMWoodieS3,
108    PivotMWoodieS2,
109    PivotMWoodieS1,
110    PivotMWoodieMiddle,
111    PivotMWoodieR1,
112    PivotMWoodieR2,
113    PivotMWoodieR3,
114    PivotMDemarkS1,
115    PivotMDemarkMiddle,
116    PivotMDemarkR1,
117}
118
119#[derive(Debug, Default, Clone, Copy, Serialize)]
120pub struct RecommendCounter {
121    pub strong_sell: u32,
122    pub sell: u32,
123    pub neutral: u32,
124    pub buy: u32,
125    pub strong_buy: u32,
126}
127
128impl RecommendCounter {
129    pub fn increase1(&mut self, recommend: Recommendation) {
130        match recommend {
131            Recommendation::StrongSell => self.strong_sell += 1,
132            Recommendation::Sell => self.sell += 1,
133            Recommendation::Neutral => self.neutral += 1,
134            Recommendation::Buy => self.buy += 1,
135            Recommendation::StrongBuy => self.strong_buy += 1,
136        }
137    }
138
139    pub fn get(&mut self, recommend: Recommendation) -> u32 {
140        match recommend {
141            Recommendation::StrongSell => self.strong_sell,
142            Recommendation::Sell => self.sell,
143            Recommendation::Neutral => self.neutral,
144            Recommendation::Buy => self.buy,
145            Recommendation::StrongBuy => self.strong_buy,
146        }
147    }
148
149    pub fn count(&self) -> u32 {
150        self.strong_sell + self.sell + self.neutral + self.buy + self.strong_buy
151    }
152}
153
154impl std::fmt::Display for RecommendCounter {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        write!(
157            f,
158            "{{ STRONG_SELL:{:>2} SELL:{:>2} NETURAL:{:>2} BUY:{:>2} STRONG_BUY:{:>2} }}",
159            self.strong_sell, self.sell, self.neutral, self.buy, self.strong_buy
160        )
161    }
162}
163
164impl Add for RecommendCounter {
165    type Output = RecommendCounter;
166
167    fn add(self, rhs: Self) -> Self::Output {
168        Self::Output {
169            strong_sell: self.strong_sell + rhs.strong_sell,
170            sell: self.sell + rhs.sell,
171            neutral: self.neutral + rhs.neutral,
172            buy: self.buy + rhs.buy,
173            strong_buy: self.strong_buy + rhs.strong_buy,
174        }
175    }
176}
177
178#[derive(Debug, Default, Clone, Serialize)]
179pub struct Analysis {
180    pub recommend_summary: Recommendation,
181    pub recommend_oscillators: Recommendation,
182    pub recommend_move_averages: Recommendation,
183
184    pub counter_summary: RecommendCounter,
185    pub counter_oscillators: RecommendCounter,
186    pub counter_move_averages: RecommendCounter,
187
188    pub signal_summary: f64,
189    pub signal_oscillators: f64,
190    pub signal_move_averages: f64,
191
192    pub signals: HashMap<Signal, Recommendation>,
193}
194
195impl Analysis {
196    /// Returns a static reference to an array of `Field` instances required for technical analysis.
197    pub fn ta_fields() -> &'static [Field] {
198        static VALUES: [Field; 43] = [
199            Field::RecommendAll,
200            Field::RecommendMA,
201            Field::RecommendOther,
202            Field::Close,
203            Field::RSI,
204            Field::RSI1,
205            Field::StochK,
206            Field::StochD,
207            Field::StochK1,
208            Field::StochD1,
209            Field::CCI20,
210            Field::CCI201,
211            Field::ADX,
212            Field::ADXplusDI,
213            Field::ADXminusDI,
214            Field::ADXplusDI1,
215            Field::ADXminusDI1,
216            Field::AO,
217            Field::AO1,
218            Field::AO2,
219            Field::Mom,
220            Field::Mom1,
221            Field::MACDmacd,
222            Field::MACDsignal,
223            Field::RecStochRSI,
224            Field::RecWR,
225            Field::RecBBPower,
226            Field::RecUO,
227            Field::EMA10,
228            Field::SMA10,
229            Field::EMA20,
230            Field::SMA20,
231            Field::EMA30,
232            Field::SMA30,
233            Field::EMA50,
234            Field::SMA50,
235            Field::EMA100,
236            Field::SMA100,
237            Field::EMA200,
238            Field::SMA200,
239            Field::RecIchimoku,
240            Field::RecVWMA,
241            Field::RecHullMA9,
242        ];
243        &VALUES
244    }
245
246    /// Retrieves symbol values for the given symbol from `tradingview` and computes technical analysis.
247    pub async fn get_technical_analysis<S1, S2>(
248        tradingview: &TradingView,
249        symbol: S1,
250        interval: S2,
251    ) -> Result<Analysis>
252    where
253        S1: AsRef<str>,
254        S2: AsRef<str>,
255    {
256        let values = tradingview
257            .get_symbol_fields(symbol, interval, Analysis::ta_fields())
258            .await
259            .context("get symbol fields error")?;
260        Ok(Analysis::compute(&values.get_f64_values()))
261    }
262
263    /// Compute technical analysis from symbol values.
264    pub fn compute(values: &HashMap<Field, f64>) -> Analysis {
265        let mut analysis = Analysis::default();
266
267        /* recommend signals */
268        {
269            if let Some(signal) = values.get(&Field::RecommendAll) {
270                analysis.recommend_summary = Analysis::compute_recommend_signal(*signal);
271                analysis.signal_summary = *signal;
272            }
273            if let Some(signal) = values.get(&Field::RecommendOther) {
274                analysis.recommend_oscillators = Analysis::compute_recommend_signal(*signal);
275                analysis.signal_oscillators = *signal;
276            }
277            if let Some(signal) = values.get(&Field::RecommendMA) {
278                analysis.recommend_move_averages = Analysis::compute_recommend_signal(*signal);
279                analysis.signal_move_averages = *signal;
280            }
281        }
282
283        /* oscillators */
284        {
285            // RSI
286            if let Some(vals) = get_all_values(values, &[Field::RSI, Field::RSI1]) {
287                analysis.add_oscillator_signal(
288                    Signal::RSI,
289                    Analysis::compute_rsi_signal(vals[0], vals[1]),
290                );
291            }
292            // Stoch.K
293            if let Some(vals) = get_all_values(
294                values,
295                &[Field::StochK, Field::StochD, Field::StochK1, Field::StochD1],
296            ) {
297                analysis.add_oscillator_signal(
298                    Signal::StochK,
299                    Analysis::compute_stoch_signal(vals[0], vals[1], vals[2], vals[3]),
300                );
301            }
302            // CCI20
303            if let Some(vals) = get_all_values(values, &[Field::CCI20, Field::CCI201]) {
304                analysis.add_oscillator_signal(
305                    Signal::CCI20,
306                    Analysis::compute_cci20_signal(vals[0], vals[1]),
307                );
308            }
309            // ADX
310            if let Some(vals) = get_all_values(
311                values,
312                &[
313                    Field::ADX,
314                    Field::ADXplusDI,
315                    Field::ADXminusDI,
316                    Field::ADXplusDI1,
317                    Field::ADXminusDI1,
318                ],
319            ) {
320                analysis.add_oscillator_signal(
321                    Signal::ADX,
322                    Analysis::compute_adx_signal(vals[0], vals[1], vals[2], vals[3], vals[4]),
323                );
324            }
325            // AO
326            if let Some(vals) = get_all_values(values, &[Field::AO, Field::AO1, Field::AO2]) {
327                analysis.add_oscillator_signal(
328                    Signal::AO,
329                    Analysis::compute_ao_signal(vals[0], vals[1], vals[2]),
330                );
331            }
332            // Mom
333            if let Some(vals) = get_all_values(values, &[Field::Mom, Field::Mom1]) {
334                analysis.add_oscillator_signal(
335                    Signal::Mon,
336                    Analysis::compute_mom_signal(vals[0], vals[1]),
337                );
338            }
339            // MACD
340            if let Some(vals) = get_all_values(values, &[Field::MACDmacd, Field::MACDsignal]) {
341                analysis.add_oscillator_signal(
342                    Signal::MACD,
343                    Analysis::compute_macd_signal(vals[0], vals[1]),
344                );
345            }
346            // Stoch.RSI.K
347            if let Some(vals) = get_all_values(values, &[Field::RecStochRSI]) {
348                analysis.add_oscillator_signal(
349                    Signal::StochRsiK,
350                    Analysis::compute_simple_signal(vals[0]),
351                );
352            }
353            // WR
354            if let Some(vals) = get_all_values(values, &[Field::RecWR]) {
355                analysis
356                    .add_oscillator_signal(Signal::WR, Analysis::compute_simple_signal(vals[0]));
357            }
358            // BBPower
359            if let Some(vals) = get_all_values(values, &[Field::RecBBPower]) {
360                analysis.add_oscillator_signal(
361                    Signal::BBPower,
362                    Analysis::compute_simple_signal(vals[0]),
363                );
364            }
365            // UO
366            if let Some(vals) = get_all_values(values, &[Field::RecUO]) {
367                analysis
368                    .add_oscillator_signal(Signal::UO, Analysis::compute_simple_signal(vals[0]));
369            }
370        }
371
372        /* move averages */
373        {
374            if let Some(close) = values.get(&Field::Close) {
375                if let Some(val) = values.get(&Field::SMA10) {
376                    analysis.add_move_average_signal(
377                        Signal::SMA10,
378                        Analysis::compute_ma_signal(*val, *close),
379                    );
380                }
381                if let Some(val) = values.get(&Field::EMA10) {
382                    analysis.add_move_average_signal(
383                        Signal::EMA10,
384                        Analysis::compute_ma_signal(*val, *close),
385                    );
386                }
387                if let Some(val) = values.get(&Field::SMA20) {
388                    analysis.add_move_average_signal(
389                        Signal::SMA20,
390                        Analysis::compute_ma_signal(*val, *close),
391                    );
392                }
393                if let Some(val) = values.get(&Field::EMA20) {
394                    analysis.add_move_average_signal(
395                        Signal::EMA20,
396                        Analysis::compute_ma_signal(*val, *close),
397                    );
398                }
399                if let Some(val) = values.get(&Field::SMA30) {
400                    analysis.add_move_average_signal(
401                        Signal::SMA30,
402                        Analysis::compute_ma_signal(*val, *close),
403                    );
404                }
405                if let Some(val) = values.get(&Field::EMA30) {
406                    analysis.add_move_average_signal(
407                        Signal::EMA30,
408                        Analysis::compute_ma_signal(*val, *close),
409                    );
410                }
411                if let Some(val) = values.get(&Field::SMA50) {
412                    analysis.add_move_average_signal(
413                        Signal::SMA50,
414                        Analysis::compute_ma_signal(*val, *close),
415                    );
416                }
417                if let Some(val) = values.get(&Field::EMA50) {
418                    analysis.add_move_average_signal(
419                        Signal::EMA50,
420                        Analysis::compute_ma_signal(*val, *close),
421                    );
422                }
423                if let Some(val) = values.get(&Field::SMA100) {
424                    analysis.add_move_average_signal(
425                        Signal::SMA100,
426                        Analysis::compute_ma_signal(*val, *close),
427                    );
428                }
429                if let Some(val) = values.get(&Field::EMA100) {
430                    analysis.add_move_average_signal(
431                        Signal::EMA100,
432                        Analysis::compute_ma_signal(*val, *close),
433                    );
434                }
435                if let Some(val) = values.get(&Field::SMA200) {
436                    analysis.add_move_average_signal(
437                        Signal::SMA200,
438                        Analysis::compute_ma_signal(*val, *close),
439                    );
440                }
441                if let Some(val) = values.get(&Field::EMA200) {
442                    analysis.add_move_average_signal(
443                        Signal::EMA200,
444                        Analysis::compute_ma_signal(*val, *close),
445                    );
446                }
447            }
448            if let Some(val) = values.get(&Field::RecIchimoku) {
449                analysis.add_move_average_signal(
450                    Signal::IchimokuBLine,
451                    Analysis::compute_simple_signal(*val),
452                );
453            }
454            if let Some(val) = values.get(&Field::RecVWMA) {
455                analysis
456                    .add_move_average_signal(Signal::VWMA, Analysis::compute_simple_signal(*val));
457            }
458            if let Some(val) = values.get(&Field::RecVWMA) {
459                analysis.add_move_average_signal(
460                    Signal::HullMA9,
461                    Analysis::compute_simple_signal(*val),
462                );
463            }
464        }
465
466        analysis
467    }
468
469    /// Adds a signal to the analysis.
470    fn add_signal(&mut self, signal: Signal, recommend: Recommendation) {
471        self.counter_summary.increase1(recommend);
472        self.signals.insert(signal, recommend);
473    }
474
475    /// Adds an oscillator signal to the analysis.
476    fn add_oscillator_signal(&mut self, signal: Signal, recommend: Recommendation) {
477        self.counter_oscillators.increase1(recommend);
478        self.counter_summary.increase1(recommend);
479        self.signals.insert(signal, recommend);
480    }
481
482    /// Adds a moving average signal to the analysis.
483    fn add_move_average_signal(&mut self, signal: Signal, recommend: Recommendation) {
484        self.counter_move_averages.increase1(recommend);
485        self.counter_summary.increase1(recommend);
486        self.signals.insert(signal, recommend);
487    }
488
489    fn compute_ma_signal(ma: f64, close: f64) -> Recommendation {
490        if ma < close {
491            Recommendation::Buy
492        } else if ma > close {
493            Recommendation::Sell
494        } else {
495            Recommendation::Neutral
496        }
497    }
498
499    fn compute_rsi_signal(rsi: f64, rsi1: f64) -> Recommendation {
500        if rsi < 30. && rsi1 < rsi {
501            Recommendation::Buy
502        } else if rsi > 70. && rsi1 > rsi {
503            Recommendation::Sell
504        } else {
505            Recommendation::Neutral
506        }
507    }
508
509    fn compute_stoch_signal(k: f64, d: f64, k1: f64, d1: f64) -> Recommendation {
510        if k < 20. && d < 20. && k > d && k1 < d1 {
511            Recommendation::Buy
512        } else if k > 80. && d > 80. && k < d && k1 > d1 {
513            Recommendation::Sell
514        } else {
515            Recommendation::Neutral
516        }
517    }
518
519    fn compute_cci20_signal(cci20: f64, cci201: f64) -> Recommendation {
520        if cci20 < -100. && cci20 > cci201 {
521            Recommendation::Buy
522        } else if cci20 > 100. && cci20 < cci201 {
523            Recommendation::Sell
524        } else {
525            Recommendation::Neutral
526        }
527    }
528
529    fn compute_adx_signal(
530        adx: f64,
531        adxpdi: f64,
532        adxndi: f64,
533        adxpdi1: f64,
534        adxndi1: f64,
535    ) -> Recommendation {
536        if adx > 20. && adxpdi1 < adxndi1 && adxpdi > adxndi {
537            Recommendation::Buy
538        } else if adx > 20. && adxpdi1 > adxndi1 && adxpdi < adxndi {
539            Recommendation::Sell
540        } else {
541            Recommendation::Neutral
542        }
543    }
544
545    fn compute_ao_signal(ao: f64, ao1: f64, ao2: f64) -> Recommendation {
546        if ao > 0. && ao1 < 0. || ao > 0. && ao1 > 0. && ao > ao1 && ao2 > ao1 {
547            Recommendation::Buy
548        } else if ao < 0. && ao1 > 0. || ao < 0. && ao1 < 0. && ao < ao1 && ao2 < ao1 {
549            Recommendation::Sell
550        } else {
551            Recommendation::Neutral
552        }
553    }
554
555    fn compute_mom_signal(mon: f64, mon1: f64) -> Recommendation {
556        if mon > mon1 {
557            Recommendation::Buy
558        } else if mon < mon1 {
559            Recommendation::Sell
560        } else {
561            Recommendation::Neutral
562        }
563    }
564
565    fn compute_macd_signal(macd: f64, signal: f64) -> Recommendation {
566        if macd > signal {
567            Recommendation::Buy
568        } else if macd < signal {
569            Recommendation::Sell
570        } else {
571            Recommendation::Neutral
572        }
573    }
574
575    fn compute_bbbuy_signal(close: f64, bblower: f64) -> Recommendation {
576        if close < bblower {
577            Recommendation::Buy
578        } else {
579            Recommendation::Neutral
580        }
581    }
582
583    fn compute_bbsell_signal(close: f64, bbupper: f64) -> Recommendation {
584        if close > bbupper {
585            Recommendation::Sell
586        } else {
587            Recommendation::Neutral
588        }
589    }
590
591    fn compute_psar_signal(psar: f64, open: f64) -> Recommendation {
592        if psar < open {
593            Recommendation::Buy
594        } else if psar > open {
595            Recommendation::Sell
596        } else {
597            Recommendation::Neutral
598        }
599    }
600
601    fn compute_recommend_signal(signal: f64) -> Recommendation {
602        if signal >= -1. && signal < -0.5 {
603            Recommendation::StrongSell
604        } else if signal >= -0.5 && signal < -0.1 {
605            Recommendation::Sell
606        } else if signal >= -0.1 && signal <= 0.1 {
607            Recommendation::Neutral
608        } else if signal > 0.1 && signal <= 0.5 {
609            Recommendation::Buy
610        } else if signal > 0.5 && signal <= 1. {
611            Recommendation::StrongBuy
612        } else {
613            Recommendation::default()
614        }
615    }
616
617    fn compute_simple_signal(signal: f64) -> Recommendation {
618        if signal == -1. {
619            Recommendation::Sell
620        } else if signal == 1. {
621            Recommendation::Buy
622        } else {
623            Recommendation::Neutral
624        }
625    }
626}
627
628impl std::fmt::Display for Analysis {
629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630        writeln!(
631            f,
632            "{:>13} :  {:>11}({:>5.2})  {}",
633            "SUMMARY", self.recommend_summary, self.signal_summary, self.counter_summary
634        )
635        .and_then(|_| {
636            writeln!(
637                f,
638                "{:>13} :  {:>11}({:>5.2})  {}",
639                "OSCILLATORS",
640                self.recommend_oscillators,
641                self.signal_oscillators,
642                self.counter_oscillators
643            )
644        })
645        .and_then(|_| {
646            writeln!(
647                f,
648                "{:>13} :  {:>11}({:>5.2})  {}",
649                "MOVE_AVERAGES",
650                self.recommend_move_averages,
651                self.signal_move_averages,
652                self.counter_move_averages
653            )
654        })
655    }
656}
657
658fn get_all_values<K, V>(map: &HashMap<K, V>, keys: &[K]) -> Option<Vec<V>>
659where
660    K: std::cmp::Eq + std::cmp::PartialEq + std::hash::Hash,
661    V: Clone,
662{
663    let vals: Vec<_> = keys.iter().filter_map(|x| map.get(x)).cloned().collect();
664    if keys.len() == vals.len() {
665        Some(vals)
666    } else {
667        None
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674
675    #[tokio::test]
676    async fn test_analysis_compute() -> Result<()> {
677        let tradingview = TradingView::new(Screener::Crypto, "OKX");
678        let symbol = "BTCUSDT.P";
679        let interval = Interval::Hour1;
680        let analysis = Analysis::get_technical_analysis(&tradingview, &symbol, &interval)
681            .await
682            .context("get technical analysis error")?;
683
684        assert!(analysis.counter_summary.count() > 0);
685        Ok(())
686    }
687}