1use serde::{Deserialize, Serialize};
2
3use crate::candle::Candle;
4
5fn extract_closes(candles: &[Candle]) -> Vec<f64> {
10 candles.iter().map(|c| c.close).collect()
11}
12fn extract_highs(candles: &[Candle]) -> Vec<f64> {
13 candles.iter().map(|c| c.high).collect()
14}
15fn extract_lows(candles: &[Candle]) -> Vec<f64> {
16 candles.iter().map(|c| c.low).collect()
17}
18fn extract_volumes(candles: &[Candle]) -> Vec<f64> {
19 candles.iter().map(|c| c.volume).collect()
20}
21
22fn last_finite(values: &[f64]) -> Option<f64> {
23 values.last().filter(|v| v.is_finite()).copied()
24}
25
26fn to_option_series(values: Vec<f64>) -> Vec<Option<f64>> {
27 values
28 .into_iter()
29 .map(|v| if v.is_finite() { Some(v) } else { None })
30 .collect()
31}
32
33#[derive(Serialize, Deserialize, Clone, Debug)]
39#[serde(rename_all = "camelCase")]
40pub struct TechnicalIndicators {
41 pub sma_20: Option<f64>,
42 pub sma_50: Option<f64>,
43 pub ema_12: Option<f64>,
44 pub ema_20: Option<f64>,
45 pub ema_26: Option<f64>,
46 pub ema_50: Option<f64>,
47 pub rsi_14: Option<f64>,
48 pub macd_line: Option<f64>,
49 pub macd_signal: Option<f64>,
50 pub macd_histogram: Option<f64>,
51 pub bb_upper: Option<f64>,
52 pub bb_middle: Option<f64>,
53 pub bb_lower: Option<f64>,
54 pub atr_14: Option<f64>,
55 pub adx_14: Option<f64>,
57 pub stoch_k: Option<f64>,
59 pub stoch_d: Option<f64>,
60 pub cci_20: Option<f64>,
61 pub williams_r_14: Option<f64>,
62 pub obv: Option<f64>,
64 pub mfi_14: Option<f64>,
65 pub roc_12: Option<f64>,
67 pub donchian_upper_20: Option<f64>,
69 pub donchian_lower_20: Option<f64>,
70 pub donchian_upper_10: Option<f64>,
71 pub donchian_lower_10: Option<f64>,
72 pub close_zscore_20: Option<f64>,
74 pub volume_zscore_20: Option<f64>,
75 pub hv_20: Option<f64>,
77 pub hv_60: Option<f64>,
78 pub kc_upper_20: Option<f64>,
80 pub kc_lower_20: Option<f64>,
81 pub supertrend_value: Option<f64>,
83 pub supertrend_direction: Option<f64>,
84 pub vwap: Option<f64>,
86 pub plus_di_14: Option<f64>,
88 pub minus_di_14: Option<f64>,
89}
90
91impl TechnicalIndicators {
92 pub fn empty() -> Self {
94 Self {
95 sma_20: None,
96 sma_50: None,
97 ema_12: None,
98 ema_20: None,
99 ema_26: None,
100 ema_50: None,
101 rsi_14: None,
102 macd_line: None,
103 macd_signal: None,
104 macd_histogram: None,
105 bb_upper: None,
106 bb_middle: None,
107 bb_lower: None,
108 atr_14: None,
109 adx_14: None,
110 stoch_k: None,
111 stoch_d: None,
112 cci_20: None,
113 williams_r_14: None,
114 obv: None,
115 mfi_14: None,
116 roc_12: None,
117 donchian_upper_20: None,
118 donchian_lower_20: None,
119 donchian_upper_10: None,
120 donchian_lower_10: None,
121 close_zscore_20: None,
122 volume_zscore_20: None,
123 hv_20: None,
124 hv_60: None,
125 kc_upper_20: None,
126 kc_lower_20: None,
127 supertrend_value: None,
128 supertrend_direction: None,
129 vwap: None,
130 plus_di_14: None,
131 minus_di_14: None,
132 }
133 }
134}
135
136#[deprecated(note = "Use hyper_ta::dynamic::calculate_snapshot() with TaEngine instead")]
145pub fn calculate_indicators(candles: &[Candle]) -> TechnicalIndicators {
146 if candles.is_empty() {
147 return TechnicalIndicators::empty();
148 }
149
150 let sma_20 = compute_sma(candles, 20);
151 let sma_50 = compute_sma(candles, 50);
152 let ema_12 = compute_ema(candles, 12);
153 let ema_20 = compute_ema(candles, 20);
154 let ema_26 = compute_ema(candles, 26);
155 let ema_50 = compute_ema(candles, 50);
156 let rsi_14 = compute_rsi(candles, 14);
157 let (macd_line, macd_signal, macd_histogram) = compute_macd(candles, 12, 26, 9);
158 let (bb_upper, bb_middle, bb_lower) = compute_bollinger_bands(candles, 20, 2.0);
159 let atr_14 = compute_atr(candles, 14);
160 let adx_14 = compute_adx(candles, 14);
161 let (stoch_k, stoch_d) = compute_stochastic(candles, 14, 3, 3);
162 let cci_20 = compute_cci(candles, 20);
163 let williams_r_14 = compute_williams_r(candles, 14);
164 let obv = compute_obv(candles);
165 let mfi_14 = compute_mfi(candles, 14);
166 let roc_12 = compute_roc(candles, 12);
167 let (donchian_upper_20, donchian_lower_20) = compute_donchian(candles, 20);
168 let (donchian_upper_10, donchian_lower_10) = compute_donchian(candles, 10);
169 let close_zscore_20 = compute_zscore_close(candles, 20);
170 let volume_zscore_20 = compute_zscore_volume(candles, 20);
171 let hv_20 = compute_hv(candles, 20);
172 let hv_60 = compute_hv(candles, 60);
173 let (kc_upper_20, kc_lower_20) = compute_keltner(candles, 20, 1.5);
174 let (supertrend_value, supertrend_direction) = compute_supertrend(candles, 10, 3.0);
175 let vwap = compute_vwap(candles);
176 let (plus_di_14, minus_di_14) = compute_di(candles, 14);
177
178 TechnicalIndicators {
179 sma_20,
180 sma_50,
181 ema_12,
182 ema_20,
183 ema_26,
184 ema_50,
185 rsi_14,
186 macd_line,
187 macd_signal,
188 macd_histogram,
189 bb_upper,
190 bb_middle,
191 bb_lower,
192 atr_14,
193 adx_14,
194 stoch_k,
195 stoch_d,
196 cci_20,
197 williams_r_14,
198 obv,
199 mfi_14,
200 roc_12,
201 donchian_upper_20,
202 donchian_lower_20,
203 donchian_upper_10,
204 donchian_lower_10,
205 close_zscore_20,
206 volume_zscore_20,
207 hv_20,
208 hv_60,
209 kc_upper_20,
210 kc_lower_20,
211 supertrend_value,
212 supertrend_direction,
213 vwap,
214 plus_di_14,
215 minus_di_14,
216 }
217}
218
219fn compute_sma(candles: &[Candle], period: usize) -> Option<f64> {
225 if candles.len() < period {
226 return None;
227 }
228 let closes = extract_closes(candles);
229 let result = motosan_ta_math::indicators::sma(&closes, period);
230 last_finite(&result)
231}
232
233fn compute_ema(candles: &[Candle], period: usize) -> Option<f64> {
235 if candles.len() < period {
236 return None;
237 }
238 let closes = extract_closes(candles);
239 let result = motosan_ta_math::indicators::ema(&closes, period);
240 last_finite(&result)
241}
242
243fn compute_rsi(candles: &[Candle], period: usize) -> Option<f64> {
245 if candles.len() < period + 1 {
246 return None;
247 }
248 let closes = extract_closes(candles);
249 let result = motosan_ta_math::indicators::rsi(&closes, period);
250 last_finite(&result)
251}
252
253fn compute_macd(
255 candles: &[Candle],
256 fast: usize,
257 slow: usize,
258 signal: usize,
259) -> (Option<f64>, Option<f64>, Option<f64>) {
260 if candles.len() < slow {
261 return (None, None, None);
262 }
263 let closes = extract_closes(candles);
264 let (line, sig, hist) = motosan_ta_math::indicators::macd(&closes, fast, slow, signal);
265 (last_finite(&line), last_finite(&sig), last_finite(&hist))
266}
267
268fn compute_bollinger_bands(
270 candles: &[Candle],
271 period: usize,
272 sigma: f64,
273) -> (Option<f64>, Option<f64>, Option<f64>) {
274 if candles.len() < period {
275 return (None, None, None);
276 }
277 let closes = extract_closes(candles);
278 let (upper, middle, lower) =
279 motosan_ta_math::indicators::bollinger_bands(&closes, period, sigma);
280 (
281 last_finite(&upper),
282 last_finite(&middle),
283 last_finite(&lower),
284 )
285}
286
287fn compute_atr(candles: &[Candle], period: usize) -> Option<f64> {
289 if candles.len() < period + 1 {
290 return None;
291 }
292 let highs = extract_highs(candles);
293 let lows = extract_lows(candles);
294 let closes = extract_closes(candles);
295 let result = motosan_ta_math::indicators::atr(&highs, &lows, &closes, period);
296 last_finite(&result)
297}
298
299fn compute_adx(candles: &[Candle], period: usize) -> Option<f64> {
301 if candles.len() < period * 2 + 1 {
302 return None;
303 }
304 let highs = extract_highs(candles);
305 let lows = extract_lows(candles);
306 let closes = extract_closes(candles);
307 let result = motosan_ta_math::indicators::adx(&highs, &lows, &closes, period);
308 last_finite(&result)
309}
310
311fn compute_stochastic(
317 candles: &[Candle],
318 period: usize,
319 k_smooth: usize,
320 d_period: usize,
321) -> (Option<f64>, Option<f64>) {
322 if candles.len() < period + k_smooth + d_period - 2 {
323 return (None, None);
324 }
325 let highs = extract_highs(candles);
326 let lows = extract_lows(candles);
327 let closes = extract_closes(candles);
328 let (raw_k, _) =
330 motosan_ta_math::indicators::stochastic(&highs, &lows, &closes, period, d_period);
331
332 let finite_k: Vec<f64> = raw_k.iter().copied().filter(|v| v.is_finite()).collect();
334 if finite_k.len() < k_smooth {
335 return (None, None);
336 }
337
338 let smoothed_k = motosan_ta_math::indicators::sma(&finite_k, k_smooth);
340 let finite_smoothed_k: Vec<f64> = smoothed_k
341 .iter()
342 .copied()
343 .filter(|v| v.is_finite())
344 .collect();
345 if finite_smoothed_k.is_empty() {
346 return (None, None);
347 }
348
349 let last_k = *finite_smoothed_k.last().unwrap();
350
351 if finite_smoothed_k.len() < d_period {
353 return (Some(last_k), None);
354 }
355 let d_window = &finite_smoothed_k[(finite_smoothed_k.len() - d_period)..];
356 let last_d = d_window.iter().sum::<f64>() / d_period as f64;
357
358 (Some(last_k), Some(last_d))
359}
360
361fn compute_cci(candles: &[Candle], period: usize) -> Option<f64> {
363 if candles.len() < period {
364 return None;
365 }
366 let highs = extract_highs(candles);
367 let lows = extract_lows(candles);
368 let closes = extract_closes(candles);
369 let result = motosan_ta_math::indicators::cci(&highs, &lows, &closes, period);
370 last_finite(&result)
371}
372
373fn compute_williams_r(candles: &[Candle], period: usize) -> Option<f64> {
375 if candles.len() < period {
376 return None;
377 }
378 let highs = extract_highs(candles);
379 let lows = extract_lows(candles);
380 let closes = extract_closes(candles);
381 let result = motosan_ta_math::indicators::williams_r(&highs, &lows, &closes, period);
382 last_finite(&result)
383}
384
385fn compute_obv(candles: &[Candle]) -> Option<f64> {
387 if candles.len() < 2 {
388 return None;
389 }
390 let closes = extract_closes(candles);
391 let volumes = extract_volumes(candles);
392 let result = motosan_ta_math::indicators::obv(&closes, &volumes);
393 last_finite(&result)
394}
395
396fn compute_mfi(candles: &[Candle], period: usize) -> Option<f64> {
398 if candles.len() < period + 1 {
399 return None;
400 }
401 let highs = extract_highs(candles);
402 let lows = extract_lows(candles);
403 let closes = extract_closes(candles);
404 let volumes = extract_volumes(candles);
405 let result = motosan_ta_math::indicators::mfi(&highs, &lows, &closes, &volumes, period);
406 last_finite(&result)
407}
408
409fn compute_roc(candles: &[Candle], period: usize) -> Option<f64> {
411 if candles.len() < period + 1 {
412 return None;
413 }
414 let closes = extract_closes(candles);
415 let result = motosan_ta_math::indicators::roc(&closes, period);
416 last_finite(&result)
417}
418
419fn compute_donchian(candles: &[Candle], period: usize) -> (Option<f64>, Option<f64>) {
421 if candles.len() < period {
422 return (None, None);
423 }
424 let highs = extract_highs(candles);
425 let lows = extract_lows(candles);
426 let (upper, lower) = motosan_ta_math::indicators::donchian(&highs, &lows, period);
427 (last_finite(&upper), last_finite(&lower))
428}
429
430fn compute_zscore_close(candles: &[Candle], period: usize) -> Option<f64> {
432 if candles.len() < period {
433 return None;
434 }
435 let closes = extract_closes(candles);
436 let result = motosan_ta_math::indicators::statistics::zscore(&closes, period);
437 last_finite(&result)
438}
439
440fn compute_zscore_volume(candles: &[Candle], period: usize) -> Option<f64> {
442 if candles.len() < period {
443 return None;
444 }
445 let volumes = extract_volumes(candles);
446 let result = motosan_ta_math::indicators::statistics::zscore(&volumes, period);
447 last_finite(&result)
448}
449
450fn compute_hv(candles: &[Candle], period: usize) -> Option<f64> {
452 if candles.len() < period + 1 {
453 return None;
454 }
455 let closes = extract_closes(candles);
456 let result = motosan_ta_math::indicators::statistics::hv(&closes, period);
457 last_finite(&result)
458}
459
460fn compute_keltner(candles: &[Candle], period: usize, mult: f64) -> (Option<f64>, Option<f64>) {
462 if candles.len() < period + 1 {
463 return (None, None);
464 }
465 let highs = extract_highs(candles);
466 let lows = extract_lows(candles);
467 let closes = extract_closes(candles);
468 let (upper, lower) = motosan_ta_math::indicators::keltner(&highs, &lows, &closes, period, mult);
469 (last_finite(&upper), last_finite(&lower))
470}
471
472fn compute_supertrend(candles: &[Candle], period: usize, mult: f64) -> (Option<f64>, Option<f64>) {
474 if candles.len() < period + 1 {
475 return (None, None);
476 }
477 let highs = extract_highs(candles);
478 let lows = extract_lows(candles);
479 let closes = extract_closes(candles);
480 let (value, direction) =
481 motosan_ta_math::indicators::supertrend(&highs, &lows, &closes, period, mult);
482 (last_finite(&value), last_finite(&direction))
483}
484
485fn compute_vwap(candles: &[Candle]) -> Option<f64> {
487 if candles.is_empty() {
488 return None;
489 }
490 let highs = extract_highs(candles);
491 let lows = extract_lows(candles);
492 let closes = extract_closes(candles);
493 let volumes = extract_volumes(candles);
494 let result = motosan_ta_math::indicators::vwap(&highs, &lows, &closes, &volumes);
495 last_finite(&result)
496}
497
498fn compute_di(candles: &[Candle], period: usize) -> (Option<f64>, Option<f64>) {
500 if candles.len() < period * 2 + 1 {
501 return (None, None);
502 }
503 let highs = extract_highs(candles);
504 let lows = extract_lows(candles);
505 let closes = extract_closes(candles);
506 let (plus_di, minus_di) = motosan_ta_math::indicators::di(&highs, &lows, &closes, period);
507 (last_finite(&plus_di), last_finite(&minus_di))
508}
509
510pub fn compute_sma_series(candles: &[Candle], period: usize) -> Vec<Option<f64>> {
516 if candles.len() < period {
517 return vec![None; candles.len()];
518 }
519 let closes = extract_closes(candles);
520 to_option_series(motosan_ta_math::indicators::sma(&closes, period))
521}
522
523pub fn compute_ema_series(candles: &[Candle], period: usize) -> Vec<Option<f64>> {
525 if candles.len() < period {
526 return vec![None; candles.len()];
527 }
528 let closes = extract_closes(candles);
529 to_option_series(motosan_ta_math::indicators::ema(&closes, period))
530}
531
532#[allow(clippy::type_complexity)]
534pub fn compute_bb_series(
535 candles: &[Candle],
536 period: usize,
537 sigma: f64,
538) -> (Vec<Option<f64>>, Vec<Option<f64>>, Vec<Option<f64>>) {
539 let n = candles.len();
540 if n < period {
541 return (vec![None; n], vec![None; n], vec![None; n]);
542 }
543 let closes = extract_closes(candles);
544 let (upper, middle, lower) =
545 motosan_ta_math::indicators::bollinger_bands(&closes, period, sigma);
546 (
547 to_option_series(upper),
548 to_option_series(middle),
549 to_option_series(lower),
550 )
551}
552
553pub fn compute_rsi_series(candles: &[Candle], period: usize) -> Vec<Option<f64>> {
555 if candles.len() < period + 1 {
556 return vec![None; candles.len()];
557 }
558 let closes = extract_closes(candles);
559 to_option_series(motosan_ta_math::indicators::rsi(&closes, period))
560}
561
562#[allow(clippy::type_complexity)]
564pub fn compute_macd_series(
565 candles: &[Candle],
566 fast: usize,
567 slow: usize,
568 signal: usize,
569) -> (Vec<Option<f64>>, Vec<Option<f64>>, Vec<Option<f64>>) {
570 let n = candles.len();
571 if n < slow {
572 return (vec![None; n], vec![None; n], vec![None; n]);
573 }
574 let closes = extract_closes(candles);
575 let (line, sig, hist) = motosan_ta_math::indicators::macd(&closes, fast, slow, signal);
576 (
577 to_option_series(line),
578 to_option_series(sig),
579 to_option_series(hist),
580 )
581}
582
583pub fn compute_stochastic_series(
585 candles: &[Candle],
586 period: usize,
587 k_smooth: usize,
588 d_period: usize,
589) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
590 let n = candles.len();
591 if n < period + k_smooth + d_period - 2 {
592 return (vec![None; n], vec![None; n]);
593 }
594 let highs = extract_highs(candles);
595 let lows = extract_lows(candles);
596 let closes = extract_closes(candles);
597
598 let (raw_k, _) =
600 motosan_ta_math::indicators::stochastic(&highs, &lows, &closes, period, d_period);
601
602 let mut k_result = vec![None; n];
605 let mut d_result = vec![None; n];
606
607 for i in (period - 1 + k_smooth - 1)..n {
609 let window = &raw_k[(i + 1 - k_smooth)..=i];
610 if window.iter().all(|v| v.is_finite()) {
611 let sk = window.iter().sum::<f64>() / k_smooth as f64;
612 k_result[i] = Some(sk);
613 }
614 }
615
616 let smoothed_k_vals: Vec<(usize, f64)> = k_result
618 .iter()
619 .enumerate()
620 .filter_map(|(i, v)| v.map(|val| (i, val)))
621 .collect();
622
623 for window_end in (d_period - 1)..smoothed_k_vals.len() {
624 let window_start = window_end + 1 - d_period;
625 let d_val: f64 = smoothed_k_vals[window_start..=window_end]
626 .iter()
627 .map(|(_, v)| v)
628 .sum::<f64>()
629 / d_period as f64;
630 let candle_idx = smoothed_k_vals[window_end].0;
631 d_result[candle_idx] = Some(d_val);
632 }
633
634 (k_result, d_result)
635}
636
637pub fn compute_cci_series(candles: &[Candle], period: usize) -> Vec<Option<f64>> {
639 if candles.len() < period {
640 return vec![None; candles.len()];
641 }
642 let highs = extract_highs(candles);
643 let lows = extract_lows(candles);
644 let closes = extract_closes(candles);
645 to_option_series(motosan_ta_math::indicators::cci(
646 &highs, &lows, &closes, period,
647 ))
648}
649
650pub fn compute_williams_r_series(candles: &[Candle], period: usize) -> Vec<Option<f64>> {
652 if candles.len() < period {
653 return vec![None; candles.len()];
654 }
655 let highs = extract_highs(candles);
656 let lows = extract_lows(candles);
657 let closes = extract_closes(candles);
658 to_option_series(motosan_ta_math::indicators::williams_r(
659 &highs, &lows, &closes, period,
660 ))
661}
662
663pub fn compute_obv_series(candles: &[Candle]) -> Vec<Option<f64>> {
665 if candles.len() < 2 {
666 return vec![None; candles.len()];
667 }
668 let closes = extract_closes(candles);
669 let volumes = extract_volumes(candles);
670 to_option_series(motosan_ta_math::indicators::obv(&closes, &volumes))
671}
672
673pub fn compute_mfi_series(candles: &[Candle], period: usize) -> Vec<Option<f64>> {
675 if candles.len() < period + 1 {
676 return vec![None; candles.len()];
677 }
678 let highs = extract_highs(candles);
679 let lows = extract_lows(candles);
680 let closes = extract_closes(candles);
681 let volumes = extract_volumes(candles);
682 to_option_series(motosan_ta_math::indicators::mfi(
683 &highs, &lows, &closes, &volumes, period,
684 ))
685}
686
687pub fn compute_donchian_series(
689 candles: &[Candle],
690 period: usize,
691) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
692 let n = candles.len();
693 if n < period {
694 return (vec![None; n], vec![None; n]);
695 }
696 let highs = extract_highs(candles);
697 let lows = extract_lows(candles);
698 let (upper, lower) = motosan_ta_math::indicators::donchian(&highs, &lows, period);
699 (to_option_series(upper), to_option_series(lower))
700}
701
702pub fn compute_keltner_series(
704 candles: &[Candle],
705 period: usize,
706 mult: f64,
707) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
708 let n = candles.len();
709 if n < period + 1 {
710 return (vec![None; n], vec![None; n]);
711 }
712 let highs = extract_highs(candles);
713 let lows = extract_lows(candles);
714 let closes = extract_closes(candles);
715 let (upper, lower) = motosan_ta_math::indicators::keltner(&highs, &lows, &closes, period, mult);
716 (to_option_series(upper), to_option_series(lower))
717}
718
719pub fn compute_supertrend_series(
721 candles: &[Candle],
722 period: usize,
723 mult: f64,
724) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
725 let n = candles.len();
726 if n < period + 1 {
727 return (vec![None; n], vec![None; n]);
728 }
729 let highs = extract_highs(candles);
730 let lows = extract_lows(candles);
731 let closes = extract_closes(candles);
732 let (value, direction) =
733 motosan_ta_math::indicators::supertrend(&highs, &lows, &closes, period, mult);
734 (to_option_series(value), to_option_series(direction))
735}
736
737pub fn compute_vwap_series(candles: &[Candle]) -> Vec<Option<f64>> {
739 if candles.is_empty() {
740 return vec![];
741 }
742 let highs = extract_highs(candles);
743 let lows = extract_lows(candles);
744 let closes = extract_closes(candles);
745 let volumes = extract_volumes(candles);
746 to_option_series(motosan_ta_math::indicators::vwap(
747 &highs, &lows, &closes, &volumes,
748 ))
749}
750
751pub fn calculate_chart_indicator_series(
764 candles: &[Candle],
765 indicators: &[String],
766) -> std::collections::HashMap<String, Vec<Option<f64>>> {
767 let mut result = std::collections::HashMap::new();
768
769 for indicator in indicators {
770 match indicator.as_str() {
771 "ema_20" => {
772 result.insert("ema_20".to_string(), compute_ema_series(candles, 20));
773 }
774 "ema_50" => {
775 result.insert("ema_50".to_string(), compute_ema_series(candles, 50));
776 }
777 "sma_20" => {
778 result.insert("sma_20".to_string(), compute_sma_series(candles, 20));
779 }
780 "sma_50" => {
781 result.insert("sma_50".to_string(), compute_sma_series(candles, 50));
782 }
783 "bb_20_2" => {
784 let (upper, middle, lower) = compute_bb_series(candles, 20, 2.0);
785 result.insert("bb_upper".to_string(), upper);
786 result.insert("bb_middle".to_string(), middle);
787 result.insert("bb_lower".to_string(), lower);
788 }
789 "rsi_14" => {
790 result.insert("rsi_14".to_string(), compute_rsi_series(candles, 14));
791 }
792 "macd" => {
793 let (line, signal, hist) = compute_macd_series(candles, 12, 26, 9);
794 result.insert("macd_line".to_string(), line);
795 result.insert("macd_signal".to_string(), signal);
796 result.insert("macd_histogram".to_string(), hist);
797 }
798 "stochastic" => {
799 let (k, d) = compute_stochastic_series(candles, 14, 3, 3);
800 result.insert("stoch_k".to_string(), k);
801 result.insert("stoch_d".to_string(), d);
802 }
803 "cci_20" => {
804 result.insert("cci_20".to_string(), compute_cci_series(candles, 20));
805 }
806 "williams_r_14" => {
807 result.insert(
808 "williams_r_14".to_string(),
809 compute_williams_r_series(candles, 14),
810 );
811 }
812 "obv" => {
813 result.insert("obv".to_string(), compute_obv_series(candles));
814 }
815 "mfi_14" => {
816 result.insert("mfi_14".to_string(), compute_mfi_series(candles, 14));
817 }
818 "donchian_20" => {
819 let (upper, lower) = compute_donchian_series(candles, 20);
820 result.insert("donchian_upper_20".to_string(), upper);
821 result.insert("donchian_lower_20".to_string(), lower);
822 }
823 "donchian_10" => {
824 let (upper, lower) = compute_donchian_series(candles, 10);
825 result.insert("donchian_upper_10".to_string(), upper);
826 result.insert("donchian_lower_10".to_string(), lower);
827 }
828 "keltner_20" => {
829 let (upper, lower) = compute_keltner_series(candles, 20, 1.5);
830 result.insert("kc_upper_20".to_string(), upper);
831 result.insert("kc_lower_20".to_string(), lower);
832 }
833 "supertrend" => {
834 let (value, direction) = compute_supertrend_series(candles, 10, 3.0);
835 result.insert("supertrend_value".to_string(), value);
836 result.insert("supertrend_direction".to_string(), direction);
837 }
838 "vwap" => {
839 result.insert("vwap".to_string(), compute_vwap_series(candles));
840 }
841 _ => {
842 }
844 }
845 }
846
847 result
848}
849
850fn rsi_zone(rsi: f64) -> &'static str {
856 if rsi >= 70.0 {
857 "overbought"
858 } else if rsi <= 30.0 {
859 "oversold"
860 } else if rsi >= 60.0 {
861 "bullish"
862 } else if rsi <= 40.0 {
863 "bearish"
864 } else {
865 "neutral"
866 }
867}
868
869fn stoch_zone(k: f64) -> &'static str {
871 if k >= 80.0 {
872 "overbought zone"
873 } else if k <= 20.0 {
874 "oversold zone"
875 } else {
876 "neutral"
877 }
878}
879
880fn macd_cross_signal(histogram: f64) -> &'static str {
882 if histogram > 0.0 {
883 "bullish"
884 } else if histogram < 0.0 {
885 "bearish"
886 } else {
887 "neutral"
888 }
889}
890
891fn bb_position_label(price: f64, upper: f64, lower: f64) -> &'static str {
893 if price >= upper {
894 "at upper band"
895 } else if price <= lower {
896 "at lower band"
897 } else {
898 "within bands"
899 }
900}
901
902pub fn format_technical_summary(
917 symbol: &str,
918 indicators: &TechnicalIndicators,
919 current_price: Option<f64>,
920) -> String {
921 let mut sections = String::with_capacity(512);
922
923 {
925 let mut parts: Vec<String> = Vec::new();
926
927 if let Some(v) = indicators.sma_20 {
928 parts.push(format!("SMA20={}", format_price(v)));
929 }
930 if let Some(v) = indicators.ema_12 {
931 parts.push(format!("EMA12={}", format_price(v)));
932 }
933 if let Some(hist) = indicators.macd_histogram {
934 let sign = if hist >= 0.0 { "+" } else { "" };
935 let cross = macd_cross_signal(hist);
936 parts.push(format!("MACD={}{} ({})", sign, format_price(hist), cross));
937 }
938 if let Some(v) = indicators.adx_14 {
939 let strength = if v >= 25.0 { "strong" } else { "weak" };
940 parts.push(format!("ADX={:.0} ({})", v, strength));
941 }
942
943 if !parts.is_empty() {
944 sections.push_str(&format!(" Trend: {}\n", parts.join(" ")));
945 }
946 }
947
948 {
950 let mut parts: Vec<String> = Vec::new();
951
952 if let Some(v) = indicators.rsi_14 {
953 parts.push(format!("RSI={:.0} ({})", v, rsi_zone(v)));
954 }
955 if let Some(k) = indicators.stoch_k {
956 parts.push(format!("Stoch={:.0} ({})", k, stoch_zone(k)));
957 }
958 if let Some(v) = indicators.cci_20 {
959 let label = if v > 100.0 {
960 "overbought"
961 } else if v < -100.0 {
962 "oversold"
963 } else {
964 "neutral"
965 };
966 parts.push(format!("CCI={:.0} ({})", v, label));
967 }
968 if let Some(v) = indicators.williams_r_14 {
969 let label = if v > -20.0 {
970 "overbought"
971 } else if v < -80.0 {
972 "oversold"
973 } else {
974 "neutral"
975 };
976 parts.push(format!("WR={:.0} ({})", v, label));
977 }
978 if let Some(v) = indicators.mfi_14 {
979 let label = if v >= 80.0 {
980 "overbought"
981 } else if v <= 20.0 {
982 "oversold"
983 } else {
984 "neutral"
985 };
986 parts.push(format!("MFI={:.0} ({})", v, label));
987 }
988
989 if !parts.is_empty() {
990 sections.push_str(&format!(" Momentum: {}\n", parts.join(" ")));
991 }
992 }
993
994 {
996 let mut parts: Vec<String> = Vec::new();
997
998 if let (Some(bl), Some(bm), Some(bu)) = (
999 indicators.bb_lower,
1000 indicators.bb_middle,
1001 indicators.bb_upper,
1002 ) {
1003 let mut bb = format!(
1004 "BB[{} - {} - {}]",
1005 format_price(bl),
1006 format_price(bm),
1007 format_price(bu)
1008 );
1009 if let Some(price) = current_price {
1010 bb.push_str(&format!(" ({})", bb_position_label(price, bu, bl)));
1011 }
1012 parts.push(bb);
1013 }
1014 if let Some(v) = indicators.atr_14 {
1015 parts.push(format!("ATR={}", format_price(v)));
1016 }
1017 if let Some(v) = indicators.hv_20 {
1018 parts.push(format!("HV20={:.1}%", v * 100.0));
1019 }
1020 if let (Some(kcu), Some(kcl)) = (indicators.kc_upper_20, indicators.kc_lower_20) {
1021 parts.push(format!("KC[{} - {}]", format_price(kcl), format_price(kcu)));
1022 }
1023
1024 if !parts.is_empty() {
1025 sections.push_str(&format!(" Volatility: {}\n", parts.join(" ")));
1026 }
1027 }
1028
1029 {
1031 let mut parts: Vec<String> = Vec::new();
1032
1033 if let (Some(du), Some(dl)) = (indicators.donchian_upper_20, indicators.donchian_lower_20) {
1034 parts.push(format!(
1035 "Donchian20[{} - {}]",
1036 format_price(dl),
1037 format_price(du)
1038 ));
1039 }
1040 if let (Some(sv), Some(sd)) = (indicators.supertrend_value, indicators.supertrend_direction)
1041 {
1042 let dir_label = if sd > 0.0 { "bullish" } else { "bearish" };
1043 parts.push(format!("SuperTrend={} ({})", format_price(sv), dir_label));
1044 }
1045 if let Some(v) = indicators.vwap {
1046 parts.push(format!("VWAP={}", format_price(v)));
1047 }
1048 if let Some(v) = indicators.roc_12 {
1049 parts.push(format!("ROC={:.1}%", v));
1050 }
1051 if let (Some(pdi), Some(mdi)) = (indicators.plus_di_14, indicators.minus_di_14) {
1052 parts.push(format!("+DI={:.0} -DI={:.0}", pdi, mdi));
1053 }
1054
1055 if !parts.is_empty() {
1056 sections.push_str(&format!(" Channels: {}\n", parts.join(" ")));
1057 }
1058 }
1059
1060 if sections.is_empty() {
1062 return String::new();
1063 }
1064
1065 format!("Technical Analysis ({}):\n{}", symbol, sections)
1066}
1067
1068fn format_price(v: f64) -> String {
1071 let abs = v.abs();
1072 if abs >= 1000.0 {
1073 let sign = if v < 0.0 { "-" } else { "" };
1075 let rounded = abs.round() as u64;
1076 let s = rounded.to_string();
1077 let mut result = String::new();
1078 for (i, c) in s.chars().rev().enumerate() {
1079 if i > 0 && i % 3 == 0 {
1080 result.push(',');
1081 }
1082 result.push(c);
1083 }
1084 format!("{}{}", sign, result.chars().rev().collect::<String>())
1085 } else if abs >= 1.0 {
1086 format!("{:.2}", v)
1087 } else {
1088 format!("{:.4}", v)
1090 }
1091}
1092
1093#[cfg(test)]
1098#[allow(deprecated)]
1099mod tests {
1100 use super::*;
1101
1102 fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
1104 Candle {
1105 time: 1735689600, open,
1107 high,
1108 low,
1109 close,
1110 volume,
1111 }
1112 }
1113
1114 fn candles_from_closes(closes: &[f64]) -> Vec<Candle> {
1116 closes.iter().map(|&c| candle(c, c, c, c, 1000.0)).collect()
1117 }
1118
1119 fn realistic_candles(n: usize) -> Vec<Candle> {
1121 let mut candles = Vec::with_capacity(n);
1122 let mut price = 100.0;
1123 for i in 0..n {
1124 let change = ((i as f64) * 0.7).sin() * 2.0;
1125 price += change;
1126 let high = price + 1.5;
1127 let low = price - 1.5;
1128 candles.push(candle(
1129 price - 0.5,
1130 high,
1131 low,
1132 price,
1133 1000.0 + i as f64 * 10.0,
1134 ));
1135 }
1136 candles
1137 }
1138
1139 #[test]
1144 fn test_empty_candles_returns_all_none() {
1145 let result = calculate_indicators(&[]);
1146 assert!(result.sma_20.is_none());
1147 assert!(result.sma_50.is_none());
1148 assert!(result.ema_12.is_none());
1149 assert!(result.ema_26.is_none());
1150 assert!(result.rsi_14.is_none());
1151 assert!(result.macd_line.is_none());
1152 assert!(result.macd_signal.is_none());
1153 assert!(result.macd_histogram.is_none());
1154 assert!(result.bb_upper.is_none());
1155 assert!(result.bb_middle.is_none());
1156 assert!(result.bb_lower.is_none());
1157 assert!(result.atr_14.is_none());
1158 assert!(result.adx_14.is_none());
1159 assert!(result.stoch_k.is_none());
1160 assert!(result.stoch_d.is_none());
1161 assert!(result.cci_20.is_none());
1162 assert!(result.williams_r_14.is_none());
1163 assert!(result.obv.is_none());
1164 assert!(result.mfi_14.is_none());
1165 assert!(result.roc_12.is_none());
1166 assert!(result.donchian_upper_20.is_none());
1167 assert!(result.donchian_lower_20.is_none());
1168 assert!(result.donchian_upper_10.is_none());
1169 assert!(result.donchian_lower_10.is_none());
1170 assert!(result.close_zscore_20.is_none());
1171 assert!(result.volume_zscore_20.is_none());
1172 assert!(result.hv_20.is_none());
1173 assert!(result.hv_60.is_none());
1174 assert!(result.kc_upper_20.is_none());
1175 assert!(result.kc_lower_20.is_none());
1176 assert!(result.supertrend_value.is_none());
1177 assert!(result.supertrend_direction.is_none());
1178 assert!(result.vwap.is_none());
1179 assert!(result.plus_di_14.is_none());
1180 assert!(result.minus_di_14.is_none());
1181 }
1182
1183 #[test]
1184 fn test_single_candle_returns_all_none() {
1185 let candles = candles_from_closes(&[100.0]);
1186 let result = calculate_indicators(&candles);
1187 assert!(result.sma_20.is_none());
1188 assert!(result.ema_12.is_none());
1189 assert!(result.rsi_14.is_none());
1190 assert!(result.macd_line.is_none());
1191 assert!(result.bb_upper.is_none());
1192 assert!(result.atr_14.is_none());
1193 }
1194
1195 #[test]
1196 fn test_insufficient_data_for_sma50() {
1197 let candles = candles_from_closes(&vec![100.0; 30]);
1198 let result = calculate_indicators(&candles);
1199 assert!(result.sma_20.is_some());
1201 assert!(result.sma_50.is_none());
1202 }
1203
1204 #[test]
1209 fn test_sma_constant_prices() {
1210 let candles = candles_from_closes(&vec![100.0; 25]);
1212 let result = calculate_indicators(&candles);
1213 assert!((result.sma_20.unwrap() - 100.0).abs() < 1e-6);
1214 }
1215
1216 #[test]
1217 fn test_sma_20_known_values() {
1218 let closes: Vec<f64> = (1..=20).map(|x| x as f64).collect();
1220 let candles = candles_from_closes(&closes);
1221 let result = calculate_indicators(&candles);
1222 assert!((result.sma_20.unwrap() - 10.5).abs() < 1e-4);
1223 }
1224
1225 #[test]
1226 fn test_sma_50_known_values() {
1227 let closes: Vec<f64> = (1..=50).map(|x| x as f64).collect();
1229 let candles = candles_from_closes(&closes);
1230 let result = calculate_indicators(&candles);
1231 assert!((result.sma_50.unwrap() - 25.5).abs() < 1e-4);
1232 }
1233
1234 #[test]
1239 fn test_ema_constant_prices() {
1240 let candles = candles_from_closes(&vec![50.0; 30]);
1241 let result = calculate_indicators(&candles);
1242 assert!((result.ema_12.unwrap() - 50.0).abs() < 1e-6);
1243 assert!((result.ema_26.unwrap() - 50.0).abs() < 1e-6);
1244 }
1245
1246 #[test]
1247 fn test_ema_12_faster_than_ema_26_on_uptrend() {
1248 let closes: Vec<f64> = (1..=30).map(|x| x as f64).collect();
1250 let candles = candles_from_closes(&closes);
1251 let result = calculate_indicators(&candles);
1252 let ema12 = result.ema_12.unwrap();
1253 let ema26 = result.ema_26.unwrap();
1254 assert!(
1255 ema12 > ema26,
1256 "EMA-12 ({}) should be > EMA-26 ({}) on uptrend",
1257 ema12,
1258 ema26
1259 );
1260 }
1261
1262 #[test]
1267 fn test_rsi_all_gains() {
1268 let closes: Vec<f64> = (0..20).map(|x| 100.0 + x as f64).collect();
1270 let candles = candles_from_closes(&closes);
1271 let result = calculate_indicators(&candles);
1272 let rsi = result.rsi_14.unwrap();
1273 assert!(
1274 rsi > 95.0,
1275 "RSI should be near 100 for all-gains, got {}",
1276 rsi
1277 );
1278 }
1279
1280 #[test]
1281 fn test_rsi_all_losses() {
1282 let closes: Vec<f64> = (0..20).map(|x| 200.0 - x as f64).collect();
1284 let candles = candles_from_closes(&closes);
1285 let result = calculate_indicators(&candles);
1286 let rsi = result.rsi_14.unwrap();
1287 assert!(
1288 rsi < 5.0,
1289 "RSI should be near 0 for all-losses, got {}",
1290 rsi
1291 );
1292 }
1293
1294 #[test]
1295 fn test_rsi_flat_market() {
1296 let candles = candles_from_closes(&vec![100.0; 20]);
1299 let result = calculate_indicators(&candles);
1300 let rsi = result.rsi_14.unwrap();
1301 assert!(
1302 (rsi - 100.0).abs() < 1e-6 || rsi.is_nan() == false,
1303 "RSI for flat market should be 100 or NaN, got {}",
1304 rsi
1305 );
1306 }
1307
1308 #[test]
1309 fn test_rsi_range() {
1310 let candles = realistic_candles(50);
1312 let result = calculate_indicators(&candles);
1313 let rsi = result.rsi_14.unwrap();
1314 assert!(rsi >= 0.0 && rsi <= 100.0, "RSI out of range: {}", rsi);
1315 }
1316
1317 #[test]
1322 fn test_macd_with_enough_data() {
1323 let candles = realistic_candles(50);
1324 let result = calculate_indicators(&candles);
1325 assert!(result.macd_line.is_some(), "MACD line should be computed");
1326 assert!(
1327 result.macd_signal.is_some(),
1328 "MACD signal should be computed"
1329 );
1330 assert!(
1331 result.macd_histogram.is_some(),
1332 "MACD histogram should be computed"
1333 );
1334 }
1335
1336 #[test]
1337 fn test_macd_histogram_is_line_minus_signal() {
1338 let candles = realistic_candles(50);
1339 let result = calculate_indicators(&candles);
1340 let line = result.macd_line.unwrap();
1341 let signal = result.macd_signal.unwrap();
1342 let histogram = result.macd_histogram.unwrap();
1343 assert!(
1344 (histogram - (line - signal)).abs() < 1e-4,
1345 "Histogram ({}) should equal line ({}) - signal ({})",
1346 histogram,
1347 line,
1348 signal
1349 );
1350 }
1351
1352 #[test]
1353 fn test_macd_constant_price() {
1354 let candles = candles_from_closes(&vec![100.0; 50]);
1356 let result = calculate_indicators(&candles);
1357 let line = result.macd_line.unwrap();
1358 let signal = result.macd_signal.unwrap();
1359 assert!(line.abs() < 1e-4, "MACD line should be ~0, got {}", line);
1360 assert!(
1361 signal.abs() < 1e-4,
1362 "MACD signal should be ~0, got {}",
1363 signal
1364 );
1365 }
1366
1367 #[test]
1372 fn test_bb_with_enough_data() {
1373 let candles = realistic_candles(30);
1374 let result = calculate_indicators(&candles);
1375 assert!(result.bb_upper.is_some());
1376 assert!(result.bb_middle.is_some());
1377 assert!(result.bb_lower.is_some());
1378 }
1379
1380 #[test]
1381 fn test_bb_upper_gt_middle_gt_lower() {
1382 let candles = realistic_candles(30);
1383 let result = calculate_indicators(&candles);
1384 let upper = result.bb_upper.unwrap();
1385 let middle = result.bb_middle.unwrap();
1386 let lower = result.bb_lower.unwrap();
1387 assert!(
1388 upper >= middle && middle >= lower,
1389 "Expected upper ({}) >= middle ({}) >= lower ({})",
1390 upper,
1391 middle,
1392 lower
1393 );
1394 }
1395
1396 #[test]
1397 fn test_bb_constant_price_bands_converge() {
1398 let candles = candles_from_closes(&vec![100.0; 30]);
1400 let result = calculate_indicators(&candles);
1401 let upper = result.bb_upper.unwrap();
1402 let middle = result.bb_middle.unwrap();
1403 let lower = result.bb_lower.unwrap();
1404 assert!((upper - middle).abs() < 1e-4, "Upper should == middle");
1405 assert!((middle - lower).abs() < 1e-4, "Middle should == lower");
1406 }
1407
1408 #[test]
1413 fn test_atr_with_enough_data() {
1414 let candles = realistic_candles(20);
1415 let result = calculate_indicators(&candles);
1416 assert!(
1417 result.atr_14.is_some(),
1418 "ATR should be computed with 20 candles"
1419 );
1420 }
1421
1422 #[test]
1423 fn test_atr_positive() {
1424 let candles = realistic_candles(30);
1425 let result = calculate_indicators(&candles);
1426 let atr = result.atr_14.unwrap();
1427 assert!(atr > 0.0, "ATR should be positive, got {}", atr);
1428 }
1429
1430 #[test]
1431 fn test_atr_constant_price() {
1432 let candles = candles_from_closes(&vec![100.0; 20]);
1434 let result = calculate_indicators(&candles);
1435 let atr = result.atr_14.unwrap();
1436 assert!(
1437 atr.abs() < 1e-6,
1438 "ATR should be 0 for constant prices, got {}",
1439 atr
1440 );
1441 }
1442
1443 #[test]
1448 fn test_technical_indicators_serialization() {
1449 let candles = realistic_candles(60);
1450 let result = calculate_indicators(&candles);
1451 let json = serde_json::to_value(&result).unwrap();
1452
1453 assert!(json.get("sma20").is_some());
1455 assert!(json.get("sma50").is_some());
1456 assert!(json.get("ema12").is_some());
1457 assert!(json.get("ema26").is_some());
1458 assert!(json.get("rsi14").is_some());
1459 assert!(json.get("macdLine").is_some());
1460 assert!(json.get("macdSignal").is_some());
1461 assert!(json.get("macdHistogram").is_some());
1462 assert!(json.get("bbUpper").is_some());
1463 assert!(json.get("bbMiddle").is_some());
1464 assert!(json.get("bbLower").is_some());
1465 assert!(json.get("atr14").is_some());
1466 }
1467
1468 #[test]
1469 fn test_technical_indicators_empty_serialization() {
1470 let result = TechnicalIndicators::empty();
1471 let json = serde_json::to_value(&result).unwrap();
1472 assert!(json.get("sma20").unwrap().is_null());
1474 assert!(json.get("rsi14").unwrap().is_null());
1475 assert!(json.get("macdLine").unwrap().is_null());
1476 }
1477
1478 #[test]
1483 fn test_calculate_indicators_all_populated_with_enough_data() {
1484 let candles = realistic_candles(60);
1485 let result = calculate_indicators(&candles);
1486 assert!(result.sma_20.is_some(), "sma_20 should be present");
1488 assert!(result.sma_50.is_some(), "sma_50 should be present");
1489 assert!(result.ema_12.is_some(), "ema_12 should be present");
1490 assert!(result.ema_26.is_some(), "ema_26 should be present");
1491 assert!(result.rsi_14.is_some(), "rsi_14 should be present");
1492 assert!(result.macd_line.is_some(), "macd_line should be present");
1493 assert!(
1494 result.macd_signal.is_some(),
1495 "macd_signal should be present"
1496 );
1497 assert!(
1498 result.macd_histogram.is_some(),
1499 "macd_histogram should be present"
1500 );
1501 assert!(result.bb_upper.is_some(), "bb_upper should be present");
1502 assert!(result.bb_middle.is_some(), "bb_middle should be present");
1503 assert!(result.bb_lower.is_some(), "bb_lower should be present");
1504 assert!(result.atr_14.is_some(), "atr_14 should be present");
1505 }
1506
1507 #[test]
1512 fn test_adx_with_enough_data() {
1513 let candles = realistic_candles(60);
1514 let result = calculate_indicators(&candles);
1515 assert!(
1516 result.adx_14.is_some(),
1517 "ADX should be computed with 60 candles"
1518 );
1519 }
1520
1521 #[test]
1522 fn test_adx_range() {
1523 let candles = realistic_candles(60);
1524 let result = calculate_indicators(&candles);
1525 let adx = result.adx_14.unwrap();
1526 assert!(
1527 adx >= 0.0 && adx <= 100.0,
1528 "ADX should be between 0 and 100, got {}",
1529 adx
1530 );
1531 }
1532
1533 #[test]
1534 fn test_adx_insufficient_data() {
1535 let candles = realistic_candles(20);
1536 let result = calculate_indicators(&candles);
1537 assert!(
1539 result.adx_14.is_none(),
1540 "ADX should be None with only 20 candles"
1541 );
1542 }
1543
1544 #[test]
1549 fn test_stochastic_with_enough_data() {
1550 let candles = realistic_candles(30);
1551 let result = calculate_indicators(&candles);
1552 assert!(result.stoch_k.is_some(), "Stoch %K should be computed");
1553 assert!(result.stoch_d.is_some(), "Stoch %D should be computed");
1554 }
1555
1556 #[test]
1557 fn test_stochastic_range() {
1558 let candles = realistic_candles(30);
1559 let result = calculate_indicators(&candles);
1560 let k = result.stoch_k.unwrap();
1561 let d = result.stoch_d.unwrap();
1562 assert!(
1563 k >= 0.0 && k <= 100.0,
1564 "Stoch %K should be 0-100, got {}",
1565 k
1566 );
1567 assert!(
1568 d >= 0.0 && d <= 100.0,
1569 "Stoch %D should be 0-100, got {}",
1570 d
1571 );
1572 }
1573
1574 #[test]
1575 fn test_stochastic_at_high() {
1576 let mut candles = realistic_candles(20);
1578 for c in candles.iter_mut().rev().take(3) {
1580 c.close = 200.0;
1581 c.high = 200.0;
1582 }
1583 let result = calculate_indicators(&candles);
1584 let k = result.stoch_k.unwrap();
1585 assert!(
1586 k > 90.0,
1587 "Stoch %K should be high when recent closes are at top, got {}",
1588 k
1589 );
1590 }
1591
1592 #[test]
1597 fn test_cci_with_enough_data() {
1598 let candles = realistic_candles(30);
1599 let result = calculate_indicators(&candles);
1600 assert!(
1601 result.cci_20.is_some(),
1602 "CCI should be computed with 30 candles"
1603 );
1604 }
1605
1606 #[test]
1607 fn test_cci_constant_price() {
1608 let candles = candles_from_closes(&vec![100.0; 30]);
1609 let result = calculate_indicators(&candles);
1610 let cci = result.cci_20.unwrap();
1611 assert!(
1612 cci.abs() < 1e-6,
1613 "CCI should be 0 for constant prices, got {}",
1614 cci
1615 );
1616 }
1617
1618 #[test]
1619 fn test_cci_insufficient_data() {
1620 let candles = candles_from_closes(&vec![100.0; 15]);
1621 let result = calculate_indicators(&candles);
1622 assert!(
1623 result.cci_20.is_none(),
1624 "CCI-20 should be None with 15 candles"
1625 );
1626 }
1627
1628 #[test]
1633 fn test_williams_r_with_enough_data() {
1634 let candles = realistic_candles(20);
1635 let result = calculate_indicators(&candles);
1636 assert!(
1637 result.williams_r_14.is_some(),
1638 "Williams %R should be computed"
1639 );
1640 }
1641
1642 #[test]
1643 fn test_williams_r_range() {
1644 let candles = realistic_candles(30);
1645 let result = calculate_indicators(&candles);
1646 let wr = result.williams_r_14.unwrap();
1647 assert!(
1648 wr >= -100.0 && wr <= 0.0,
1649 "Williams %R should be -100 to 0, got {}",
1650 wr
1651 );
1652 }
1653
1654 #[test]
1655 fn test_williams_r_at_high() {
1656 let mut candles = realistic_candles(20);
1658 let window_highest = candles[(candles.len() - 14)..]
1660 .iter()
1661 .map(|c| c.high)
1662 .fold(f64::NEG_INFINITY, f64::max);
1663 let last = candles.last_mut().unwrap();
1664 last.close = window_highest + 10.0;
1665 last.high = window_highest + 10.0;
1666 let result = calculate_indicators(&candles);
1667 let wr = result.williams_r_14.unwrap();
1668 assert!(
1669 wr.abs() < 1e-4,
1670 "Williams %R should be ~0 at high, got {}",
1671 wr
1672 );
1673 }
1674
1675 #[test]
1680 fn test_obv_with_data() {
1681 let candles = realistic_candles(20);
1682 let result = calculate_indicators(&candles);
1683 assert!(result.obv.is_some(), "OBV should be computed");
1684 }
1685
1686 #[test]
1687 fn test_obv_uptrend_positive() {
1688 let closes: Vec<f64> = (1..=20).map(|x| 100.0 + x as f64).collect();
1690 let candles: Vec<Candle> = closes
1691 .iter()
1692 .map(|&c| candle(c, c + 1.0, c - 1.0, c, 1000.0))
1693 .collect();
1694 let result = calculate_indicators(&candles);
1695 let obv = result.obv.unwrap();
1696 assert!(obv > 0.0, "OBV should be positive in uptrend, got {}", obv);
1697 }
1698
1699 #[test]
1700 fn test_obv_downtrend_negative() {
1701 let closes: Vec<f64> = (1..=20).map(|x| 200.0 - x as f64).collect();
1703 let candles: Vec<Candle> = closes
1704 .iter()
1705 .map(|&c| candle(c, c + 1.0, c - 1.0, c, 1000.0))
1706 .collect();
1707 let result = calculate_indicators(&candles);
1708 let obv = result.obv.unwrap();
1709 assert!(
1710 obv < 0.0,
1711 "OBV should be negative in downtrend, got {}",
1712 obv
1713 );
1714 }
1715
1716 #[test]
1717 fn test_obv_flat_is_zero() {
1718 let candles = candles_from_closes(&vec![100.0; 20]);
1720 let result = calculate_indicators(&candles);
1721 let obv = result.obv.unwrap();
1722 assert!(
1723 obv.abs() < 1e-6,
1724 "OBV should be 0 for flat prices, got {}",
1725 obv
1726 );
1727 }
1728
1729 #[test]
1734 fn test_mfi_with_enough_data() {
1735 let candles = realistic_candles(20);
1736 let result = calculate_indicators(&candles);
1737 assert!(
1738 result.mfi_14.is_some(),
1739 "MFI should be computed with 20 candles"
1740 );
1741 }
1742
1743 #[test]
1744 fn test_mfi_range() {
1745 let candles = realistic_candles(30);
1746 let result = calculate_indicators(&candles);
1747 let mfi = result.mfi_14.unwrap();
1748 assert!(
1749 mfi >= 0.0 && mfi <= 100.0,
1750 "MFI should be 0-100, got {}",
1751 mfi
1752 );
1753 }
1754
1755 #[test]
1756 fn test_mfi_all_up() {
1757 let closes: Vec<f64> = (1..=20).map(|x| 100.0 + x as f64).collect();
1759 let candles: Vec<Candle> = closes
1760 .iter()
1761 .map(|&c| candle(c - 0.5, c + 1.0, c - 1.0, c, 1000.0))
1762 .collect();
1763 let result = calculate_indicators(&candles);
1764 let mfi = result.mfi_14.unwrap();
1765 assert!(mfi > 95.0, "MFI should be near 100 for all-up, got {}", mfi);
1766 }
1767
1768 #[test]
1769 fn test_mfi_insufficient_data() {
1770 let candles = realistic_candles(10);
1771 let result = calculate_indicators(&candles);
1772 assert!(
1773 result.mfi_14.is_none(),
1774 "MFI-14 should be None with 10 candles"
1775 );
1776 }
1777
1778 #[test]
1783 fn test_new_indicators_serialization() {
1784 let candles = realistic_candles(60);
1785 let result = calculate_indicators(&candles);
1786 let json = serde_json::to_value(&result).unwrap();
1787
1788 assert!(json.get("adx14").is_some(), "adx14 should be in JSON");
1789 assert!(json.get("stochK").is_some(), "stochK should be in JSON");
1790 assert!(json.get("stochD").is_some(), "stochD should be in JSON");
1791 assert!(json.get("cci20").is_some(), "cci20 should be in JSON");
1792 assert!(
1793 json.get("williamsR14").is_some(),
1794 "williamsR14 should be in JSON"
1795 );
1796 assert!(json.get("obv").is_some(), "obv should be in JSON");
1797 assert!(json.get("mfi14").is_some(), "mfi14 should be in JSON");
1798 }
1799
1800 #[test]
1805 fn test_all_new_indicators_populated_with_enough_data() {
1806 let candles = realistic_candles(60);
1807 let result = calculate_indicators(&candles);
1808 assert!(result.adx_14.is_some(), "adx_14 should be present");
1809 assert!(result.stoch_k.is_some(), "stoch_k should be present");
1810 assert!(result.stoch_d.is_some(), "stoch_d should be present");
1811 assert!(result.cci_20.is_some(), "cci_20 should be present");
1812 assert!(
1813 result.williams_r_14.is_some(),
1814 "williams_r_14 should be present"
1815 );
1816 assert!(result.obv.is_some(), "obv should be present");
1817 assert!(result.mfi_14.is_some(), "mfi_14 should be present");
1818 }
1819
1820 #[test]
1825 fn test_format_technical_summary_empty_indicators() {
1826 let indicators = TechnicalIndicators::empty();
1827 let result = format_technical_summary("BTC-PERP", &indicators, None);
1828 assert!(result.is_empty());
1830 }
1831
1832 #[test]
1833 fn test_format_technical_summary_full_indicators() {
1834 let indicators = TechnicalIndicators {
1835 sma_20: Some(67450.0),
1836 sma_50: Some(66000.0),
1837 ema_12: Some(67320.0),
1838 ema_20: Some(67100.0),
1839 ema_26: Some(66800.0),
1840 ema_50: Some(65500.0),
1841 rsi_14: Some(62.0),
1842 macd_line: Some(520.0),
1843 macd_signal: Some(400.0),
1844 macd_histogram: Some(120.0),
1845 bb_upper: Some(68200.0),
1846 bb_middle: Some(67500.0),
1847 bb_lower: Some(66800.0),
1848 atr_14: Some(350.0),
1849 adx_14: Some(28.0),
1850 stoch_k: Some(78.0),
1851 stoch_d: Some(72.0),
1852 cci_20: Some(45.0),
1853 williams_r_14: Some(-35.0),
1854 obv: Some(1000000.0),
1855 mfi_14: Some(55.0),
1856 roc_12: Some(5.2),
1857 donchian_upper_20: Some(68500.0),
1858 donchian_lower_20: Some(65500.0),
1859 donchian_upper_10: Some(68000.0),
1860 donchian_lower_10: Some(66000.0),
1861 close_zscore_20: Some(1.2),
1862 volume_zscore_20: Some(0.5),
1863 hv_20: Some(0.35),
1864 hv_60: Some(0.40),
1865 kc_upper_20: Some(68100.0),
1866 kc_lower_20: Some(66900.0),
1867 supertrend_value: Some(66500.0),
1868 supertrend_direction: Some(1.0),
1869 vwap: Some(67400.0),
1870 plus_di_14: Some(25.0),
1871 minus_di_14: Some(18.0),
1872 };
1873 let result = format_technical_summary("BTC-PERP", &indicators, Some(67600.0));
1874
1875 assert!(result.contains("Technical Analysis (BTC-PERP):"));
1876 assert!(result.contains("SMA20=67,450"));
1878 assert!(result.contains("EMA12=67,320"));
1879 assert!(result.contains("MACD=+120"));
1880 assert!(result.contains("(bullish)"));
1881 assert!(result.contains("ADX=28 (strong)"));
1882 assert!(result.contains("RSI=62 (bullish)"));
1884 assert!(result.contains("Stoch=78 (neutral)"));
1885 assert!(result.contains("CCI=45 (neutral)"));
1886 assert!(result.contains("WR=-35 (neutral)"));
1887 assert!(result.contains("MFI=55 (neutral)"));
1888 assert!(result.contains("BB[66,800 - 67,500 - 68,200]"));
1890 assert!(result.contains("within bands"));
1891 assert!(result.contains("ATR=350"));
1892 }
1893
1894 #[test]
1895 fn test_format_technical_summary_rsi_zones() {
1896 let mut ind = TechnicalIndicators::empty();
1897
1898 ind.rsi_14 = Some(75.0);
1900 let result = format_technical_summary("ETH-PERP", &ind, None);
1901 assert!(result.contains("RSI=75 (overbought)"));
1902
1903 ind.rsi_14 = Some(25.0);
1905 let result = format_technical_summary("ETH-PERP", &ind, None);
1906 assert!(result.contains("RSI=25 (oversold)"));
1907
1908 ind.rsi_14 = Some(63.0);
1910 let result = format_technical_summary("ETH-PERP", &ind, None);
1911 assert!(result.contains("RSI=63 (bullish)"));
1912
1913 ind.rsi_14 = Some(35.0);
1915 let result = format_technical_summary("ETH-PERP", &ind, None);
1916 assert!(result.contains("RSI=35 (bearish)"));
1917 }
1918
1919 #[test]
1920 fn test_format_technical_summary_macd_bearish() {
1921 let mut ind = TechnicalIndicators::empty();
1922 ind.macd_histogram = Some(-50.0);
1923 let result = format_technical_summary("SOL-PERP", &ind, None);
1924 assert!(result.contains("(bearish)"));
1925 }
1926
1927 #[test]
1928 fn test_format_technical_summary_stoch_zones() {
1929 let mut ind = TechnicalIndicators::empty();
1930
1931 ind.stoch_k = Some(85.0);
1932 let result = format_technical_summary("BTC-PERP", &ind, None);
1933 assert!(result.contains("Stoch=85 (overbought zone)"));
1934
1935 ind.stoch_k = Some(15.0);
1936 let result = format_technical_summary("BTC-PERP", &ind, None);
1937 assert!(result.contains("Stoch=15 (oversold zone)"));
1938 }
1939
1940 #[test]
1941 fn test_format_technical_summary_bb_position() {
1942 let mut ind = TechnicalIndicators::empty();
1943 ind.bb_upper = Some(100.0);
1944 ind.bb_middle = Some(95.0);
1945 ind.bb_lower = Some(90.0);
1946
1947 let result = format_technical_summary("X", &ind, Some(101.0));
1949 assert!(result.contains("at upper band"));
1950
1951 let result = format_technical_summary("X", &ind, Some(89.0));
1953 assert!(result.contains("at lower band"));
1954
1955 let result = format_technical_summary("X", &ind, Some(95.0));
1957 assert!(result.contains("within bands"));
1958 }
1959
1960 #[test]
1961 fn test_format_technical_summary_adx_weak() {
1962 let mut ind = TechnicalIndicators::empty();
1963 ind.adx_14 = Some(15.0);
1964 let result = format_technical_summary("BTC-PERP", &ind, None);
1965 assert!(result.contains("ADX=15 (weak)"));
1966 }
1967
1968 #[test]
1969 fn test_format_price_formatting() {
1970 assert_eq!(format_price(67450.0), "67,450");
1971 assert_eq!(format_price(1234567.0), "1,234,567");
1972 assert_eq!(format_price(350.0), "350.00");
1973 assert_eq!(format_price(0.0045), "0.0045");
1974 assert_eq!(format_price(-1500.0), "-1,500");
1975 }
1976
1977 #[test]
1978 fn test_format_technical_summary_with_real_candles() {
1979 let candles = realistic_candles(60);
1981 let indicators = calculate_indicators(&candles);
1982 let last_price = candles.last().unwrap().close;
1983 let result = format_technical_summary("TEST-PERP", &indicators, Some(last_price));
1984
1985 assert!(result.contains("Technical Analysis (TEST-PERP):"));
1987 assert!(result.contains("Trend:"));
1988 assert!(result.contains("Momentum:"));
1989 assert!(result.contains("Volatility:"));
1990 }
1991
1992 #[test]
1997 fn test_roc_known_value() {
1998 let mut closes = vec![100.0; 13];
2000 closes[12] = 110.0;
2001 let candles = candles_from_closes(&closes);
2002 let result = compute_roc(&candles, 12);
2003 assert!((result.unwrap() - 10.0).abs() < 1e-6);
2004 }
2005
2006 #[test]
2007 fn test_roc_negative() {
2008 let mut closes = vec![100.0; 13];
2009 closes[12] = 90.0;
2010 let candles = candles_from_closes(&closes);
2011 let result = compute_roc(&candles, 12);
2012 assert!((result.unwrap() - (-10.0)).abs() < 1e-6);
2013 }
2014
2015 #[test]
2016 fn test_roc_insufficient_data() {
2017 let candles = candles_from_closes(&vec![100.0; 5]);
2018 assert!(compute_roc(&candles, 12).is_none());
2019 }
2020
2021 #[test]
2022 fn test_roc_in_calculate_indicators() {
2023 let candles = realistic_candles(20);
2024 let result = calculate_indicators(&candles);
2025 assert!(
2026 result.roc_12.is_some(),
2027 "ROC-12 should be computed with 20 candles"
2028 );
2029 }
2030
2031 #[test]
2036 fn test_donchian_known_values() {
2037 let candles: Vec<Candle> = (1..=20)
2038 .map(|i| candle(i as f64, i as f64 + 0.5, i as f64 - 0.5, i as f64, 1000.0))
2039 .collect();
2040 let (upper, lower) = compute_donchian(&candles, 20);
2041 assert!((upper.unwrap() - 20.5).abs() < 1e-6); assert!((lower.unwrap() - 0.5).abs() < 1e-6); }
2044
2045 #[test]
2046 fn test_donchian_insufficient_data() {
2047 let candles = realistic_candles(5);
2048 let (u, l) = compute_donchian(&candles, 20);
2049 assert!(u.is_none());
2050 assert!(l.is_none());
2051 }
2052
2053 #[test]
2054 fn test_donchian_in_calculate_indicators() {
2055 let candles = realistic_candles(25);
2056 let result = calculate_indicators(&candles);
2057 assert!(result.donchian_upper_20.is_some());
2058 assert!(result.donchian_lower_20.is_some());
2059 assert!(result.donchian_upper_10.is_some());
2060 assert!(result.donchian_lower_10.is_some());
2061 }
2062
2063 #[test]
2068 fn test_zscore_close_constant() {
2069 let candles = candles_from_closes(&vec![100.0; 20]);
2070 let result = compute_zscore_close(&candles, 20);
2071 assert!(
2072 (result.unwrap()).abs() < 1e-6,
2073 "Z-Score should be 0 for constant prices"
2074 );
2075 }
2076
2077 #[test]
2078 fn test_zscore_close_known() {
2079 let closes: Vec<f64> = (1..=20).map(|x| x as f64).collect();
2082 let candles = candles_from_closes(&closes);
2083 let z = compute_zscore_close(&candles, 20).unwrap();
2084 assert!(
2086 z > 0.0,
2087 "Z-Score should be positive when last > mean, got {}",
2088 z
2089 );
2090 }
2091
2092 #[test]
2093 fn test_zscore_volume_constant() {
2094 let candles = candles_from_closes(&vec![100.0; 20]);
2095 let result = compute_zscore_volume(&candles, 20);
2097 assert!(
2098 (result.unwrap()).abs() < 1e-6,
2099 "Volume Z-Score should be 0 for constant volume"
2100 );
2101 }
2102
2103 #[test]
2104 fn test_zscore_insufficient_data() {
2105 let candles = candles_from_closes(&vec![100.0; 5]);
2106 assert!(compute_zscore_close(&candles, 20).is_none());
2107 assert!(compute_zscore_volume(&candles, 20).is_none());
2108 }
2109
2110 #[test]
2115 fn test_hv_constant_price() {
2116 let candles = candles_from_closes(&vec![100.0; 25]);
2117 let result = compute_hv(&candles, 20);
2118 assert!(
2120 (result.unwrap()).abs() < 1e-6,
2121 "HV should be 0 for constant prices"
2122 );
2123 }
2124
2125 #[test]
2126 fn test_hv_positive_for_varying() {
2127 let candles = realistic_candles(25);
2128 let result = compute_hv(&candles, 20);
2129 assert!(
2130 result.unwrap() > 0.0,
2131 "HV should be positive for varying prices"
2132 );
2133 }
2134
2135 #[test]
2136 fn test_hv_insufficient_data() {
2137 let candles = candles_from_closes(&vec![100.0; 15]);
2138 assert!(compute_hv(&candles, 20).is_none());
2139 }
2140
2141 #[test]
2142 fn test_hv_in_calculate_indicators() {
2143 let candles = realistic_candles(65);
2144 let result = calculate_indicators(&candles);
2145 assert!(result.hv_20.is_some(), "HV-20 should be present");
2146 assert!(result.hv_60.is_some(), "HV-60 should be present");
2147 }
2148
2149 #[test]
2154 fn test_keltner_with_enough_data() {
2155 let candles = realistic_candles(30);
2156 let (upper, lower) = compute_keltner(&candles, 20, 1.5);
2157 assert!(upper.is_some(), "KC upper should be computed");
2158 assert!(lower.is_some(), "KC lower should be computed");
2159 }
2160
2161 #[test]
2162 fn test_keltner_upper_gt_lower() {
2163 let candles = realistic_candles(30);
2164 let (upper, lower) = compute_keltner(&candles, 20, 1.5);
2165 assert!(
2166 upper.unwrap() > lower.unwrap(),
2167 "KC upper should be > lower"
2168 );
2169 }
2170
2171 #[test]
2172 fn test_keltner_constant_price() {
2173 let candles = candles_from_closes(&vec![100.0; 25]);
2175 let (upper, lower) = compute_keltner(&candles, 20, 1.5);
2176 let u = upper.unwrap();
2177 let l = lower.unwrap();
2178 assert!(
2179 (u - l).abs() < 1e-4,
2180 "KC bands should converge for constant prices"
2181 );
2182 }
2183
2184 #[test]
2185 fn test_keltner_insufficient_data() {
2186 let candles = candles_from_closes(&vec![100.0; 10]);
2187 let (u, l) = compute_keltner(&candles, 20, 1.5);
2188 assert!(u.is_none());
2189 assert!(l.is_none());
2190 }
2191
2192 #[test]
2197 fn test_supertrend_with_enough_data() {
2198 let candles = realistic_candles(30);
2199 let (value, direction) = compute_supertrend(&candles, 10, 3.0);
2200 assert!(value.is_some(), "SuperTrend value should be computed");
2201 assert!(
2202 direction.is_some(),
2203 "SuperTrend direction should be computed"
2204 );
2205 }
2206
2207 #[test]
2208 fn test_supertrend_direction_valid() {
2209 let candles = realistic_candles(30);
2210 let (_, direction) = compute_supertrend(&candles, 10, 3.0);
2211 let dir = direction.unwrap();
2212 assert!(
2213 dir == 1.0 || dir == -1.0,
2214 "SuperTrend direction should be 1.0 or -1.0, got {}",
2215 dir
2216 );
2217 }
2218
2219 #[test]
2220 fn test_supertrend_insufficient_data() {
2221 let candles = realistic_candles(5);
2222 let (v, d) = compute_supertrend(&candles, 10, 3.0);
2223 assert!(v.is_none());
2224 assert!(d.is_none());
2225 }
2226
2227 #[test]
2228 fn test_supertrend_uptrend_bullish() {
2229 let closes: Vec<f64> = (0..30).map(|i| 100.0 + i as f64 * 2.0).collect();
2231 let candles: Vec<Candle> = closes
2232 .iter()
2233 .map(|&c| candle(c - 0.5, c + 1.0, c - 1.0, c, 1000.0))
2234 .collect();
2235 let (_, direction) = compute_supertrend(&candles, 10, 3.0);
2236 assert_eq!(
2237 direction.unwrap(),
2238 1.0,
2239 "SuperTrend should be bullish in uptrend"
2240 );
2241 }
2242
2243 #[test]
2248 fn test_vwap_constant_price() {
2249 let candles = candles_from_closes(&vec![100.0; 20]);
2250 let result = compute_vwap(&candles);
2251 assert!((result.unwrap() - 100.0).abs() < 1e-6);
2253 }
2254
2255 #[test]
2256 fn test_vwap_empty() {
2257 let result = compute_vwap(&[]);
2258 assert!(result.is_none());
2259 }
2260
2261 #[test]
2262 fn test_vwap_single_candle() {
2263 let c = candle(100.0, 110.0, 90.0, 105.0, 1000.0);
2264 let result = compute_vwap(&[c]);
2265 let expected = (110.0 + 90.0 + 105.0) / 3.0;
2267 assert!((result.unwrap() - expected).abs() < 1e-4);
2268 }
2269
2270 #[test]
2271 fn test_vwap_in_calculate_indicators() {
2272 let candles = realistic_candles(20);
2273 let result = calculate_indicators(&candles);
2274 assert!(result.vwap.is_some(), "VWAP should be computed");
2275 }
2276
2277 #[test]
2282 fn test_di_with_enough_data() {
2283 let candles = realistic_candles(60);
2284 let (plus_di, minus_di) = compute_di(&candles, 14);
2285 assert!(plus_di.is_some(), "+DI should be computed");
2286 assert!(minus_di.is_some(), "-DI should be computed");
2287 }
2288
2289 #[test]
2290 fn test_di_range() {
2291 let candles = realistic_candles(60);
2292 let (plus_di, minus_di) = compute_di(&candles, 14);
2293 let pdi = plus_di.unwrap();
2294 let mdi = minus_di.unwrap();
2295 assert!(pdi >= 0.0, "+DI should be >= 0, got {}", pdi);
2296 assert!(mdi >= 0.0, "-DI should be >= 0, got {}", mdi);
2297 }
2298
2299 #[test]
2300 fn test_di_insufficient_data() {
2301 let candles = realistic_candles(20);
2302 let (p, m) = compute_di(&candles, 14);
2303 assert!(p.is_none());
2304 assert!(m.is_none());
2305 }
2306
2307 #[test]
2308 fn test_di_in_calculate_indicators() {
2309 let candles = realistic_candles(60);
2310 let result = calculate_indicators(&candles);
2311 assert!(result.plus_di_14.is_some(), "plus_di_14 should be present");
2312 assert!(
2313 result.minus_di_14.is_some(),
2314 "minus_di_14 should be present"
2315 );
2316 }
2317
2318 #[test]
2323 fn test_all_phase1_indicators_populated_with_enough_data() {
2324 let candles = realistic_candles(80);
2325 let result = calculate_indicators(&candles);
2326 assert!(result.roc_12.is_some(), "roc_12");
2327 assert!(result.donchian_upper_20.is_some(), "donchian_upper_20");
2328 assert!(result.donchian_lower_20.is_some(), "donchian_lower_20");
2329 assert!(result.donchian_upper_10.is_some(), "donchian_upper_10");
2330 assert!(result.donchian_lower_10.is_some(), "donchian_lower_10");
2331 assert!(result.close_zscore_20.is_some(), "close_zscore_20");
2332 assert!(result.volume_zscore_20.is_some(), "volume_zscore_20");
2333 assert!(result.hv_20.is_some(), "hv_20");
2334 assert!(result.hv_60.is_some(), "hv_60");
2335 assert!(result.kc_upper_20.is_some(), "kc_upper_20");
2336 assert!(result.kc_lower_20.is_some(), "kc_lower_20");
2337 assert!(result.supertrend_value.is_some(), "supertrend_value");
2338 assert!(
2339 result.supertrend_direction.is_some(),
2340 "supertrend_direction"
2341 );
2342 assert!(result.vwap.is_some(), "vwap");
2343 assert!(result.plus_di_14.is_some(), "plus_di_14");
2344 assert!(result.minus_di_14.is_some(), "minus_di_14");
2345 }
2346
2347 #[test]
2348 fn test_phase1_indicators_serialization() {
2349 let candles = realistic_candles(80);
2350 let result = calculate_indicators(&candles);
2351 let json = serde_json::to_value(&result).unwrap();
2352
2353 assert!(json.get("roc12").is_some(), "roc12 in JSON");
2354 assert!(
2355 json.get("donchianUpper20").is_some(),
2356 "donchianUpper20 in JSON"
2357 );
2358 assert!(
2359 json.get("donchianLower20").is_some(),
2360 "donchianLower20 in JSON"
2361 );
2362 assert!(json.get("closeZscore20").is_some(), "closeZscore20 in JSON");
2363 assert!(
2364 json.get("volumeZscore20").is_some(),
2365 "volumeZscore20 in JSON"
2366 );
2367 assert!(json.get("hv20").is_some(), "hv20 in JSON");
2368 assert!(json.get("hv60").is_some(), "hv60 in JSON");
2369 assert!(json.get("kcUpper20").is_some(), "kcUpper20 in JSON");
2370 assert!(json.get("kcLower20").is_some(), "kcLower20 in JSON");
2371 assert!(
2372 json.get("supertrendValue").is_some(),
2373 "supertrendValue in JSON"
2374 );
2375 assert!(
2376 json.get("supertrendDirection").is_some(),
2377 "supertrendDirection in JSON"
2378 );
2379 assert!(json.get("vwap").is_some(), "vwap in JSON");
2380 assert!(json.get("plusDi14").is_some(), "plusDi14 in JSON");
2381 assert!(json.get("minusDi14").is_some(), "minusDi14 in JSON");
2382 }
2383
2384 #[test]
2389 fn test_rsi_alternating_gains_losses() {
2390 let mut closes = vec![100.0];
2392 for i in 1..30 {
2393 if i % 2 == 0 {
2394 closes.push(closes[i - 1] + 1.0);
2395 } else {
2396 closes.push(closes[i - 1] - 1.0);
2397 }
2398 }
2399 let candles = candles_from_closes(&closes);
2400 let result = calculate_indicators(&candles);
2401 let rsi = result.rsi_14.unwrap();
2402 assert!(
2403 rsi > 30.0 && rsi < 70.0,
2404 "RSI for alternating market should be near 50, got {}",
2405 rsi
2406 );
2407 }
2408
2409 #[test]
2410 fn test_rsi_exactly_period_plus_one() {
2411 let closes: Vec<f64> = (0..15).map(|x| 100.0 + x as f64).collect();
2413 let candles = candles_from_closes(&closes);
2414 let result = calculate_indicators(&candles);
2415 assert!(
2416 result.rsi_14.is_some(),
2417 "RSI should compute with exactly period+1 candles"
2418 );
2419 }
2420
2421 #[test]
2422 fn test_rsi_period_candles_insufficient() {
2423 let closes: Vec<f64> = (0..14).map(|x| 100.0 + x as f64).collect();
2425 let candles = candles_from_closes(&closes);
2426 let result = calculate_indicators(&candles);
2427 assert!(
2428 result.rsi_14.is_none(),
2429 "RSI should be None with only 14 candles"
2430 );
2431 }
2432
2433 #[test]
2434 fn test_rsi_large_spike_then_flat() {
2435 let mut closes = vec![100.0, 200.0]; for _ in 2..25 {
2438 closes.push(200.0); }
2440 let candles = candles_from_closes(&closes);
2441 let result = calculate_indicators(&candles);
2442 let rsi = result.rsi_14.unwrap();
2443 assert!(
2445 rsi > 50.0,
2446 "RSI after spike then flat should still be elevated, got {}",
2447 rsi
2448 );
2449 }
2450
2451 #[test]
2456 fn test_macd_uptrend_positive_line() {
2457 let closes: Vec<f64> = (0..50).map(|x| 100.0 + x as f64 * 2.0).collect();
2459 let candles = candles_from_closes(&closes);
2460 let result = calculate_indicators(&candles);
2461 let line = result.macd_line.unwrap();
2462 assert!(
2463 line > 0.0,
2464 "MACD line should be positive in uptrend, got {}",
2465 line
2466 );
2467 }
2468
2469 #[test]
2470 fn test_macd_downtrend_negative_line() {
2471 let closes: Vec<f64> = (0..50).map(|x| 200.0 - x as f64 * 2.0).collect();
2473 let candles = candles_from_closes(&closes);
2474 let result = calculate_indicators(&candles);
2475 let line = result.macd_line.unwrap();
2476 assert!(
2477 line < 0.0,
2478 "MACD line should be negative in downtrend, got {}",
2479 line
2480 );
2481 }
2482
2483 #[test]
2484 fn test_macd_insufficient_data_boundary() {
2485 let closes: Vec<f64> = (0..25).map(|x| 100.0 + x as f64).collect();
2487 let candles = candles_from_closes(&closes);
2488 let result = calculate_indicators(&candles);
2489 assert!(
2490 result.macd_line.is_none(),
2491 "MACD should be None with only 25 candles"
2492 );
2493 }
2494
2495 #[test]
2496 fn test_macd_exactly_slow_period() {
2497 let closes: Vec<f64> = (0..26).map(|x| 100.0 + x as f64).collect();
2499 let candles = candles_from_closes(&closes);
2500 let result = calculate_indicators(&candles);
2501 assert!(
2502 result.macd_line.is_some(),
2503 "MACD should compute with exactly 26 candles"
2504 );
2505 }
2506
2507 #[test]
2512 fn test_bollinger_bands_high_volatility() {
2513 let mut closes = Vec::new();
2515 for i in 0..30 {
2516 if i % 2 == 0 {
2517 closes.push(100.0 + 20.0);
2518 } else {
2519 closes.push(100.0 - 20.0);
2520 }
2521 }
2522 let candles = candles_from_closes(&closes);
2523 let result = calculate_indicators(&candles);
2524 let upper = result.bb_upper.unwrap();
2525 let lower = result.bb_lower.unwrap();
2526 let bandwidth = upper - lower;
2527 assert!(
2528 bandwidth > 10.0,
2529 "BB bandwidth should be wide for volatile data, got {}",
2530 bandwidth
2531 );
2532 }
2533
2534 #[test]
2535 fn test_atr_high_volatility_vs_low() {
2536 let high_vol: Vec<Candle> = (0..20)
2538 .map(|i| Candle {
2539 time: i as u64,
2540 open: 100.0,
2541 high: 120.0,
2542 low: 80.0,
2543 close: 100.0,
2544 volume: 1000.0,
2545 })
2546 .collect();
2547 let low_vol: Vec<Candle> = (0..20)
2548 .map(|i| Candle {
2549 time: i as u64,
2550 open: 100.0,
2551 high: 101.0,
2552 low: 99.0,
2553 close: 100.0,
2554 volume: 1000.0,
2555 })
2556 .collect();
2557
2558 let atr_high = calculate_indicators(&high_vol).atr_14.unwrap();
2559 let atr_low = calculate_indicators(&low_vol).atr_14.unwrap();
2560 assert!(
2561 atr_high > atr_low,
2562 "ATR for high-vol ({}) should exceed ATR for low-vol ({})",
2563 atr_high,
2564 atr_low
2565 );
2566 }
2567
2568 #[test]
2569 fn test_obv_single_candle_insufficient() {
2570 let candles = candles_from_closes(&[100.0]);
2571 let result = calculate_indicators(&candles);
2572 assert!(result.obv.is_none(), "OBV needs at least 2 candles");
2573 }
2574
2575 #[test]
2576 fn test_vwap_zero_volume() {
2577 let candles: Vec<Candle> = (0..10)
2579 .map(|i| Candle {
2580 time: i as u64,
2581 open: 100.0,
2582 high: 105.0,
2583 low: 95.0,
2584 close: 100.0,
2585 volume: 0.0,
2586 })
2587 .collect();
2588 let result = calculate_indicators(&candles);
2589 assert!(
2590 result.vwap.is_none(),
2591 "VWAP should be None when total volume is 0"
2592 );
2593 }
2594
2595 #[test]
2596 fn test_williams_r_at_lowest_low() {
2597 let candles = vec![
2599 Candle {
2600 time: 0,
2601 open: 110.0,
2602 high: 120.0,
2603 low: 100.0,
2604 close: 100.0,
2605 volume: 1000.0,
2606 },
2607 Candle {
2608 time: 1,
2609 open: 110.0,
2610 high: 120.0,
2611 low: 100.0,
2612 close: 100.0,
2613 volume: 1000.0,
2614 },
2615 Candle {
2616 time: 2,
2617 open: 110.0,
2618 high: 120.0,
2619 low: 100.0,
2620 close: 100.0,
2621 volume: 1000.0,
2622 },
2623 Candle {
2624 time: 3,
2625 open: 110.0,
2626 high: 120.0,
2627 low: 100.0,
2628 close: 100.0,
2629 volume: 1000.0,
2630 },
2631 Candle {
2632 time: 4,
2633 open: 110.0,
2634 high: 120.0,
2635 low: 100.0,
2636 close: 100.0,
2637 volume: 1000.0,
2638 },
2639 Candle {
2640 time: 5,
2641 open: 110.0,
2642 high: 120.0,
2643 low: 100.0,
2644 close: 100.0,
2645 volume: 1000.0,
2646 },
2647 Candle {
2648 time: 6,
2649 open: 110.0,
2650 high: 120.0,
2651 low: 100.0,
2652 close: 100.0,
2653 volume: 1000.0,
2654 },
2655 Candle {
2656 time: 7,
2657 open: 110.0,
2658 high: 120.0,
2659 low: 100.0,
2660 close: 100.0,
2661 volume: 1000.0,
2662 },
2663 Candle {
2664 time: 8,
2665 open: 110.0,
2666 high: 120.0,
2667 low: 100.0,
2668 close: 100.0,
2669 volume: 1000.0,
2670 },
2671 Candle {
2672 time: 9,
2673 open: 110.0,
2674 high: 120.0,
2675 low: 100.0,
2676 close: 100.0,
2677 volume: 1000.0,
2678 },
2679 Candle {
2680 time: 10,
2681 open: 110.0,
2682 high: 120.0,
2683 low: 100.0,
2684 close: 100.0,
2685 volume: 1000.0,
2686 },
2687 Candle {
2688 time: 11,
2689 open: 110.0,
2690 high: 120.0,
2691 low: 100.0,
2692 close: 100.0,
2693 volume: 1000.0,
2694 },
2695 Candle {
2696 time: 12,
2697 open: 110.0,
2698 high: 120.0,
2699 low: 100.0,
2700 close: 100.0,
2701 volume: 1000.0,
2702 },
2703 Candle {
2704 time: 13,
2705 open: 110.0,
2706 high: 120.0,
2707 low: 100.0,
2708 close: 100.0,
2709 volume: 1000.0,
2710 },
2711 ];
2712 let result = calculate_indicators(&candles);
2713 let wr = result.williams_r_14.unwrap();
2714 assert!(
2715 (wr - (-100.0)).abs() < 1e-6,
2716 "Williams %R should be -100 when close equals lowest low, got {}",
2717 wr
2718 );
2719 }
2720
2721 #[test]
2722 fn test_cci_zero_mean_deviation() {
2723 let candles: Vec<Candle> = (0..25)
2725 .map(|i| Candle {
2726 time: i as u64,
2727 open: 100.0,
2728 high: 100.0,
2729 low: 100.0,
2730 close: 100.0,
2731 volume: 1000.0,
2732 })
2733 .collect();
2734 let result = calculate_indicators(&candles);
2735 let cci = result.cci_20.unwrap();
2736 assert!(
2737 cci.abs() < 1e-6,
2738 "CCI should be 0 when all typical prices are equal, got {}",
2739 cci
2740 );
2741 }
2742
2743 #[test]
2744 fn test_roc_zero_past_price() {
2745 let mut candles: Vec<Candle> = (0..15)
2747 .map(|i| Candle {
2748 time: i as u64,
2749 open: 0.0,
2750 high: 0.0,
2751 low: 0.0,
2752 close: 0.0,
2753 volume: 1000.0,
2754 })
2755 .collect();
2756 candles.last_mut().unwrap().close = 100.0;
2758 let result = calculate_indicators(&candles);
2759 assert!(
2762 result.roc_12.is_none(),
2763 "ROC should be None when past price is 0"
2764 );
2765 }
2766}