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 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 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 pub fn compute(values: &HashMap<Field, f64>) -> Analysis {
265 let mut analysis = Analysis::default();
266
267 {
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 {
285 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 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 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 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 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 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 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 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 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 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 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 {
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 fn add_signal(&mut self, signal: Signal, recommend: Recommendation) {
471 self.counter_summary.increase1(recommend);
472 self.signals.insert(signal, recommend);
473 }
474
475 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 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}