1pub mod analysis_generator;
10pub mod indicators;
11
12pub use analysis_generator::{
14 lookup_series_code, AnalysisGenerator, AnalysisOptions, AnalysisSummary, BbPosition,
15 FullAnalysis,
16};
17pub use indicators::{
18 adx, atr, bollinger_bands, bollinger_bands_with_multiplier, choppiness_index, rsi, AdxResult,
19 BollingerBands,
20};
21
22#[derive(Debug, Clone, Copy)]
27pub struct Candle {
28 pub time: u64,
29 pub open: f64,
30 pub high: f64,
31 pub low: f64,
32 pub close: f64,
33}
34
35#[derive(Debug, Clone, Copy)]
36pub struct ValueAtTime {
37 pub time: u64,
38 pub value: f64,
39}
40
41#[derive(Debug, Clone)]
42pub struct EmaAnalysis {
43 pub time_candle: u64,
44 pub index: usize,
45 pub color_candle: String,
46 pub next_color_candle: String,
47
48 pub ema_short_value: f64,
50 pub ema_short_slope_value: f64,
51 pub ema_short_slope_direction: String,
52 pub is_ema_short_turn_type: String, pub ema_short_cut_position: String, pub ema_medium_value: f64,
57 pub ema_medium_slope_direction: String,
58
59 pub ema_long_value: f64,
61 pub ema_long_slope_direction: String,
62
63 pub ema_above: String, pub ema_long_above: String, pub macd_12: f64, pub macd_23: f64, pub previous_ema_short_value: f64,
73 pub previous_ema_medium_value: f64,
74 pub previous_ema_long_value: f64,
75 pub previous_macd_12: f64,
76 pub previous_macd_23: f64,
77
78 pub ema_convergence_type: String, pub ema_long_convergence_type: String, pub ema_cut_short_type: String, pub candles_since_short_cut: usize,
85
86 pub ema_cut_long_type: String, pub candles_since_ema_cut: usize,
89
90 pub previous_color_back1: String,
92 pub previous_color_back3: String,
93}
94
95#[derive(Debug, Clone, Copy)]
100pub enum MaType {
101 EMA,
102 HMA,
103 WMA,
104 SMA,
105 EHMA,
106}
107
108#[derive(Debug, Clone, Copy)]
109pub enum CutStrategy {
110 ShortCut, LongCut, }
113
114fn extract_close(candles: &[Candle]) -> Vec<f64> {
119 candles.iter().map(|c| c.close).collect()
120}
121
122fn wrap_output(candles: &[Candle], values: Vec<f64>) -> Vec<ValueAtTime> {
123 candles
124 .iter()
125 .zip(values.iter())
126 .map(|(c, v)| ValueAtTime {
127 time: c.time,
128 value: *v,
129 })
130 .collect()
131}
132
133pub fn sma(candles: &[Candle], period: usize) -> Vec<ValueAtTime> {
138 let prices = extract_close(candles);
139 let mut out = vec![f64::NAN; prices.len()];
140
141 if period == 0 || prices.len() < period {
142 return wrap_output(candles, out);
143 }
144
145 for i in period - 1..prices.len() {
146 let sum: f64 = prices[i - period + 1..=i].iter().sum();
147 out[i] = sum / period as f64;
148 }
149
150 wrap_output(candles, out)
151}
152
153pub fn ema(candles: &[Candle], period: usize) -> Vec<ValueAtTime> {
154 let prices = extract_close(candles);
155 let mut out = vec![f64::NAN; prices.len()];
156
157 if period == 0 || prices.is_empty() {
158 return wrap_output(candles, out);
159 }
160
161 let k = 2.0 / (period as f64 + 1.0);
162 let mut prev = 0.0;
175
176 for i in 0..prices.len() {
177 if i < period - 1 {
178 out[i] = f64::NAN;
179 } else if i == period - 1 {
180 let sma_val: f64 = prices[0..period].iter().sum::<f64>() / period as f64;
181 out[i] = sma_val;
182 prev = sma_val;
183 } else {
184 prev = prices[i] * k + prev * (1.0 - k);
185 out[i] = prev;
186 }
187 }
188
189 wrap_output(candles, out)
190}
191
192pub fn wma(candles: &[Candle], period: usize) -> Vec<ValueAtTime> {
193 let prices = extract_close(candles);
194 let mut out = vec![f64::NAN; prices.len()];
195
196 if period == 0 || prices.len() < period {
197 return wrap_output(candles, out);
198 }
199
200 let denom = (period * (period + 1) / 2) as f64;
201
202 for i in period - 1..prices.len() {
203 let mut sum = 0.0;
204 for j in 0..period {
205 sum += prices[i - j] * (period - j) as f64;
206 }
207 out[i] = sum / denom;
208 }
209
210 wrap_output(candles, out)
211}
212
213fn wma_values(values: &[f64], period: usize) -> Vec<f64> {
214 let mut out = vec![f64::NAN; values.len()];
215 if period == 0 || values.len() < period {
216 return out;
217 }
218
219 let denom = (period * (period + 1) / 2) as f64;
220
221 for i in period - 1..values.len() {
222 let mut sum = 0.0;
223 for j in 0..period {
224 sum += values[i - j] * (period - j) as f64;
225 }
226 out[i] = sum / denom;
227 }
228
229 out
230}
231
232pub fn hma(candles: &[Candle], period: usize) -> Vec<ValueAtTime> {
233 if period < 2 {
234 return wrap_output(candles, vec![f64::NAN; candles.len()]);
235 }
236
237 let prices = extract_close(candles);
238 let half = period / 2;
239 let sqrt_n = (period as f64).sqrt().round() as usize;
240
241 let w1 = wma_values(&prices, half);
242 let w2 = wma_values(&prices, period);
243
244 let diff: Vec<f64> = w1.iter().zip(w2.iter()).map(|(a, b)| 2.0 * a - b).collect();
246 let h = wma_values(&diff, sqrt_n);
247
248 wrap_output(candles, h)
249}
250
251pub fn ehma(candles: &[Candle], period: usize) -> Vec<ValueAtTime> {
252 let ema_full = ema(candles, period);
259 let ema_half = ema(candles, period / 2);
260
261 let raw: Vec<Candle> = candles
262 .iter()
263 .enumerate()
264 .map(|(i, c)| {
265 let val_full = ema_full[i].value;
266 let val_half = ema_half[i].value;
267 let res = if val_full.is_nan() || val_half.is_nan() {
269 f64::NAN
270 } else {
271 2.0 * val_half - val_full
272 };
273 Candle {
274 time: c.time,
275 open: c.open,
276 high: c.high,
277 low: c.low,
278 close: res,
279 }
280 })
281 .collect();
282
283 let sqrt_n = (period as f64).sqrt().round() as usize;
284 ema(&raw, sqrt_n)
285}
286
287fn slope_direction(v: f64) -> String {
294 if v > 0.0001 {
295 "Up".to_string()
296 } else if v < -0.0001 {
297 "Down".to_string()
298 } else {
299 "Flat".to_string()
300 }
301}
302
303fn turn_type(prev_diff: f64, curr_diff: f64) -> String {
304 let prev_dir = if prev_diff > 0.0001 {
305 "Up"
306 } else if prev_diff < -0.0001 {
307 "Down"
308 } else {
309 "Flat"
310 };
311 let curr_dir = if curr_diff > 0.0001 {
312 "Up"
313 } else if curr_diff < -0.0001 {
314 "Down"
315 } else {
316 "Flat"
317 };
318
319 if curr_dir == "Up" && prev_dir == "Down" {
320 "TurnUp".to_string()
321 } else if curr_dir == "Down" && prev_dir == "Up" {
322 "TurnDown".to_string()
323 } else {
324 "None".to_string()
325 }
326}
327
328fn get_ema_cut_position(c: &Candle, v: f64) -> String {
329 if v.is_nan() {
330 return "Unknown".to_string();
331 }
332
333 let body_top = c.close.max(c.open);
334 let body_bottom = c.close.min(c.open);
335
336 if v > c.high {
337 return "1".to_string(); }
339 if v >= body_top {
340 return "2".to_string(); }
342 if v >= body_bottom {
343 let height = body_top - body_bottom;
345 if height == 0.0 {
346 return "B2".to_string();
347 } let ratio = (v - body_bottom) / height;
350 if ratio >= 0.66 {
351 return "B1".to_string();
352 } else if ratio >= 0.33 {
353 return "B2".to_string();
354 } else {
355 return "B3".to_string();
356 }
357 }
358 if v >= c.low {
359 return "3".to_string(); }
361
362 "4".to_string() }
364
365fn get_color(open: f64, close: f64) -> String {
366 if close > open {
367 "Green".to_string()
368 } else if close < open {
369 "Red".to_string()
370 } else {
371 "Equal".to_string()
372 }
373}
374
375fn calculate_ma(candles: &[Candle], period: usize, ma_type: MaType) -> Vec<ValueAtTime> {
376 match ma_type {
377 MaType::EMA => ema(candles, period),
378 MaType::HMA => hma(candles, period),
379 MaType::WMA => wma(candles, period),
380 MaType::SMA => sma(candles, period),
381 MaType::EHMA => ehma(candles, period),
382 }
383}
384
385pub fn generate_analysis_data(
392 candles: &[Candle],
393 short_p: usize,
394 medium_p: usize,
395 long_p: usize,
396 short_type: MaType,
397 medium_type: MaType,
398 long_type: MaType,
399) -> Vec<EmaAnalysis> {
400 let ma_short = calculate_ma(candles, short_p, short_type);
401 let ma_medium = calculate_ma(candles, medium_p, medium_type);
402 let ma_long = calculate_ma(candles, long_p, long_type);
403
404 let mut out = Vec::new();
405
406 let mut last_ema_cut_short_index: Option<usize> = None;
407 let mut last_ema_cut_long_index: Option<usize> = None;
408
409 for i in 0..candles.len() {
410 let c = &candles[i];
411 let next_c = if i < candles.len() - 1 {
412 Some(&candles[i + 1])
413 } else {
414 None
415 };
416
417 let color_candle = get_color(c.open, c.close);
419 let next_color_candle = if let Some(nc) = next_c {
420 get_color(nc.open, nc.close)
421 } else {
422 "Unknown".to_string()
423 };
424
425 let short_val = ma_short[i].value;
427 let medium_val = ma_medium[i].value;
428 let long_val = ma_long[i].value;
429
430 let (prev_short, prev_medium, prev_long) = if i > 0 {
432 (
433 ma_short[i - 1].value,
434 ma_medium[i - 1].value,
435 ma_long[i - 1].value,
436 )
437 } else {
438 (f64::NAN, f64::NAN, f64::NAN)
439 };
440
441 let short_diff = if !short_val.is_nan() && !prev_short.is_nan() {
443 short_val - prev_short
444 } else {
445 0.0
446 };
447 let short_slope_dir = slope_direction(short_diff);
448
449 let mut short_turn = "None".to_string();
451 if i >= 2 {
452 let val_i2 = ma_short[i - 2].value; let val_i1 = prev_short; if !val_i2.is_nan() && !val_i1.is_nan() && !short_val.is_nan() {
456 let prev_diff = val_i1 - val_i2;
457 let curr_diff = short_val - val_i1;
458 short_turn = turn_type(prev_diff, curr_diff);
459 }
460 }
461
462 let medium_diff = if !medium_val.is_nan() && !prev_medium.is_nan() {
464 medium_val - prev_medium
465 } else {
466 0.0
467 };
468 let medium_slope_dir = slope_direction(medium_diff);
469
470 let long_diff = if !long_val.is_nan() && !prev_long.is_nan() {
472 long_val - prev_long
473 } else {
474 0.0
475 };
476 let long_slope_dir = slope_direction(long_diff);
477
478 let ema_above = if !short_val.is_nan() && !medium_val.is_nan() {
480 if short_val > medium_val {
481 "ShortAbove".to_string()
482 } else {
483 "MediumAbove".to_string()
484 }
485 } else {
486 "Unknown".to_string()
487 };
488
489 let ema_long_above = if !medium_val.is_nan() && !long_val.is_nan() {
490 if medium_val > long_val {
491 "MediumAbove".to_string()
492 } else {
493 "LongAbove".to_string()
494 }
495 } else {
496 "Unknown".to_string()
497 };
498
499 let macd_12 = if !short_val.is_nan() && !medium_val.is_nan() {
501 (short_val - medium_val).abs()
502 } else {
503 f64::NAN
504 };
505 let macd_23 = if !medium_val.is_nan() && !long_val.is_nan() {
506 (medium_val - long_val).abs()
507 } else {
508 f64::NAN
509 };
510
511 let prev_macd_12 = if !prev_short.is_nan() && !prev_medium.is_nan() {
513 (prev_short - prev_medium).abs()
514 } else {
515 f64::NAN
516 };
517 let prev_macd_23 = if !prev_medium.is_nan() && !prev_long.is_nan() {
518 (prev_medium - prev_long).abs()
519 } else {
520 f64::NAN
521 };
522
523 let mut ema_convergence_type = "Neutral".to_string();
525 if !macd_12.is_nan() && !prev_macd_12.is_nan() {
526 if macd_12 > prev_macd_12 {
527 ema_convergence_type = "divergence".to_string();
528 } else if macd_12 < prev_macd_12 {
529 ema_convergence_type = "convergence".to_string();
530 }
531 }
532
533 let mut ema_long_convergence_type = "Neutral".to_string();
534 if !macd_23.is_nan() && !prev_macd_23.is_nan() {
535 if macd_23 > prev_macd_23 {
536 ema_long_convergence_type = "divergence".to_string();
537 } else if macd_23 < prev_macd_23 {
538 ema_long_convergence_type = "convergence".to_string();
539 }
540 }
541
542 let mut ema_cut_short_type = "None".to_string();
544 if i > 0
545 && !short_val.is_nan()
546 && !medium_val.is_nan()
547 && !prev_short.is_nan()
548 && !prev_medium.is_nan()
549 {
550 let curr_short_above = short_val > medium_val;
551 let prev_short_above = prev_short > prev_medium;
552
553 if curr_short_above != prev_short_above {
554 if curr_short_above {
555 ema_cut_short_type = "UpTrend".to_string();
556 } else {
557 ema_cut_short_type = "DownTrend".to_string();
558 }
559 }
560 }
561
562 if ema_cut_short_type != "None" {
563 last_ema_cut_short_index = Some(i);
564 }
565
566 let candles_since_short_cut = if let Some(idx) = last_ema_cut_short_index {
567 i - idx
568 } else {
569 0
570 };
571
572 let mut ema_cut_long_type = "None".to_string();
574 if i > 0
576 && !medium_val.is_nan()
577 && !long_val.is_nan()
578 && !prev_medium.is_nan()
579 && !prev_long.is_nan()
580 {
581 let curr_medium_above = medium_val > long_val;
582 let prev_medium_above = prev_medium > prev_long;
583
584 if curr_medium_above != prev_medium_above {
585 if curr_medium_above {
586 ema_cut_long_type = "UpTrend".to_string(); } else {
588 ema_cut_long_type = "DownTrend".to_string(); }
590 }
591 }
592
593 if ema_cut_long_type != "None" {
594 last_ema_cut_long_index = Some(i);
595 }
596
597 let candles_since_ema_cut = if let Some(idx) = last_ema_cut_long_index {
598 i - idx
599 } else {
600 0
601 };
602
603 let cut_pos = get_ema_cut_position(c, short_val);
605
606 let prev_color_1 = if i >= 1 {
608 get_color(candles[i - 1].open, candles[i - 1].close)
609 } else {
610 "Unknown".to_string()
611 };
612 let prev_color_3 = if i >= 3 {
613 get_color(candles[i - 3].open, candles[i - 3].close)
614 } else {
615 "Unknown".to_string()
616 };
617
618 out.push(EmaAnalysis {
619 time_candle: c.time,
620 index: i,
621 color_candle,
622 next_color_candle,
623
624 ema_short_value: short_val,
625 ema_short_slope_value: short_diff,
626 ema_short_slope_direction: short_slope_dir,
627 is_ema_short_turn_type: short_turn,
628 ema_short_cut_position: cut_pos,
629
630 ema_medium_value: medium_val,
631 ema_medium_slope_direction: medium_slope_dir,
632
633 ema_long_value: long_val,
634 ema_long_slope_direction: long_slope_dir,
635
636 ema_above,
637 ema_long_above,
638
639 macd_12,
640 macd_23,
641
642 previous_ema_short_value: prev_short,
643 previous_ema_medium_value: prev_medium,
644 previous_ema_long_value: prev_long,
645 previous_macd_12: prev_macd_12,
646 previous_macd_23: prev_macd_23,
647
648 ema_convergence_type,
649 ema_long_convergence_type,
650
651 ema_cut_short_type,
652 candles_since_short_cut,
653
654 ema_cut_long_type,
655 candles_since_ema_cut,
656
657 previous_color_back1: prev_color_1,
658 previous_color_back3: prev_color_3,
659 });
660 }
661
662 out
663}
664
665pub fn get_action_by_simple(results: &[EmaAnalysis], index: usize) -> &'static str {
670 if let Some(analysis) = results.get(index) {
671 match analysis.ema_above.as_str() {
672 "ShortAbove" => "call",
673 "MediumAbove" => "put", _ => "hold",
675 }
676 } else {
677 "none"
678 }
679}
680
681pub fn get_action_by_cut_type(
682 results: &[EmaAnalysis],
683 index: usize,
684 use_cut_type: CutStrategy,
685) -> &'static str {
686 if let Some(analysis) = results.get(index) {
687 match use_cut_type {
688 CutStrategy::ShortCut => {
689 let trend = analysis.ema_cut_short_type.as_str();
691 let slope = analysis.ema_short_slope_direction.as_str();
692
693 if trend == "UpTrend" && slope == "Up" {
694 return "call";
695 }
696 if trend == "DownTrend" && slope == "Down" {
697 return "put";
698 }
699 "hold"
700 }
701 CutStrategy::LongCut => {
702 let trend = analysis.ema_cut_long_type.as_str();
704 let slope = analysis.ema_medium_slope_direction.as_str();
705
706 if trend == "UpTrend" && slope == "Up" {
707 return "call";
708 }
709 if trend == "DownTrend" && slope == "Down" {
710 return "put";
711 }
712 "hold"
713 }
714 }
715 } else {
716 "none"
717 }
718}