1use super::ma::MaData;
2use crate::utilities::data_loader::source_type;
3use crate::utilities::enums::Kernel;
4use std::collections::HashMap;
5use std::error::Error;
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum MaBatchDispatchError {
10 #[error("Unknown moving average type: {ma_type}")]
11 UnknownType { ma_type: String },
12 #[error(
13 "{indicator} does not support period-sweep batch dispatch; use the indicator directly"
14 )]
15 NotPeriodBased { indicator: &'static str },
16 #[error("{indicator} requires candles (timestamp/volume/OHLC); pass MaData::Candles")]
17 RequiresCandles { indicator: &'static str },
18 #[error("invalid param '{key}' for {indicator}: value={value} ({reason})")]
19 InvalidParam {
20 indicator: &'static str,
21 key: &'static str,
22 value: f64,
23 reason: &'static str,
24 },
25 #[error("invalid kernel for batch path: {0:?}")]
26 InvalidKernelForBatch(Kernel),
27}
28
29#[derive(Clone, Debug)]
30pub struct MaBatchOutput {
31 pub values: Vec<f64>,
32 pub periods: Vec<usize>,
33 pub rows: usize,
34 pub cols: usize,
35}
36
37impl MaBatchOutput {
38 pub fn row_for_period(&self, period: usize) -> Option<usize> {
39 self.periods.iter().position(|&p| p == period)
40 }
41
42 pub fn values_for_period(&self, period: usize) -> Option<&[f64]> {
43 self.row_for_period(period).map(|row| {
44 let start = row * self.cols;
45 &self.values[start..start + self.cols]
46 })
47 }
48}
49
50#[derive(Clone, Debug)]
51pub enum MaBatchParamValue<'a> {
52 Int(i64),
53 Float(f64),
54 Bool(bool),
55 EnumString(&'a str),
56}
57
58#[derive(Clone, Debug)]
59pub struct MaBatchParamKV<'a> {
60 pub key: &'a str,
61 pub value: MaBatchParamValue<'a>,
62}
63
64#[inline]
65fn to_batch_kernel(k: Kernel) -> Result<Kernel, MaBatchDispatchError> {
66 let out = match k {
67 Kernel::Auto => Kernel::Auto,
68 Kernel::Scalar => Kernel::ScalarBatch,
69 Kernel::Avx2 => Kernel::Avx2Batch,
70 Kernel::Avx512 => Kernel::Avx512Batch,
71 other if other.is_batch() => other,
72 other => return Err(MaBatchDispatchError::InvalidKernelForBatch(other)),
73 };
74 Ok(out)
75}
76
77#[inline]
78fn map_periods<T>(combos: &[T], get_period: impl Fn(&T) -> usize) -> Vec<usize> {
79 combos.iter().map(get_period).collect()
80}
81
82#[inline]
83fn expand_period_axis(range: (usize, usize, usize)) -> Result<Vec<usize>, MaBatchDispatchError> {
84 let (start, end, step) = range;
85 let periods = if step == 0 || start == end {
86 vec![start]
87 } else if start < end {
88 let s = step.max(1);
89 (start..=end).step_by(s).collect()
90 } else {
91 let s = step.max(1);
92 let mut v = Vec::new();
93 let mut cur = start;
94 while cur >= end {
95 v.push(cur);
96 if cur == 0 {
97 break;
98 }
99 let next = cur.saturating_sub(s);
100 if next == cur {
101 break;
102 }
103 cur = next;
104 if cur < end {
105 break;
106 }
107 }
108 v
109 };
110 if periods.is_empty() {
111 return Err(MaBatchDispatchError::InvalidParam {
112 indicator: "period_range",
113 key: "step",
114 value: step as f64,
115 reason: "invalid period range",
116 });
117 }
118 Ok(periods)
119}
120
121#[inline]
122pub fn ma_batch<'a>(
123 ma_type: &str,
124 data: MaData<'a>,
125 period_range: (usize, usize, usize),
126) -> Result<MaBatchOutput, Box<dyn Error>> {
127 ma_batch_with_kernel(ma_type, data, period_range, Kernel::Auto)
128}
129
130pub fn ma_batch_with_kernel<'a>(
131 ma_type: &str,
132 data: MaData<'a>,
133 period_range: (usize, usize, usize),
134 kernel: Kernel,
135) -> Result<MaBatchOutput, Box<dyn Error>> {
136 ma_batch_with_kernel_and_params(ma_type, data, period_range, kernel, None)
137}
138
139#[inline]
140pub fn ma_batch_with_params<'a>(
141 ma_type: &str,
142 data: MaData<'a>,
143 period_range: (usize, usize, usize),
144 params: &HashMap<String, f64>,
145) -> Result<MaBatchOutput, Box<dyn Error>> {
146 ma_batch_with_kernel_and_params(ma_type, data, period_range, Kernel::Auto, Some(params))
147}
148
149pub fn ma_batch_with_kernel_and_typed_params<'a>(
150 ma_type: &str,
151 data: MaData<'a>,
152 period_range: (usize, usize, usize),
153 kernel: Kernel,
154 params: &[MaBatchParamKV<'_>],
155) -> Result<MaBatchOutput, Box<dyn Error>> {
156 let mut numeric: HashMap<String, f64> = HashMap::with_capacity(params.len());
157 let mut text: HashMap<String, String> = HashMap::new();
158
159 for p in params {
160 match p.value {
161 MaBatchParamValue::Int(v) => {
162 numeric.insert(p.key.to_string(), v as f64);
163 }
164 MaBatchParamValue::Float(v) => {
165 if !v.is_finite() {
166 return Err(MaBatchDispatchError::InvalidParam {
167 indicator: "typed_params",
168 key: "float",
169 value: v,
170 reason: "expected finite number",
171 }
172 .into());
173 }
174 numeric.insert(p.key.to_string(), v);
175 }
176 MaBatchParamValue::Bool(v) => {
177 numeric.insert(p.key.to_string(), if v { 1.0 } else { 0.0 });
178 }
179 MaBatchParamValue::EnumString(v) => {
180 text.insert(p.key.to_string(), v.to_string());
181 }
182 }
183 }
184
185 if ma_type.eq_ignore_ascii_case("dma") && text.contains_key("hull_ma_type") {
186 let kernel = to_batch_kernel(kernel)?;
187 let (prices, _) = match data {
188 MaData::Slice(s) => (s, None),
189 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
190 };
191
192 let get_u = |key: &'static str, default_v: usize| -> Result<usize, MaBatchDispatchError> {
193 let Some(v) = numeric.get(key).copied() else {
194 return Ok(default_v);
195 };
196 if v < 0.0 {
197 return Err(MaBatchDispatchError::InvalidParam {
198 indicator: "dma",
199 key,
200 value: v,
201 reason: "expected >= 0",
202 });
203 }
204 let r = v.round();
205 if (v - r).abs() > 1e-9 {
206 return Err(MaBatchDispatchError::InvalidParam {
207 indicator: "dma",
208 key,
209 value: v,
210 reason: "expected integer",
211 });
212 }
213 Ok(r as usize)
214 };
215
216 let ema_length = get_u("ema_length", 20)?;
217 let ema_gain_limit = get_u("ema_gain_limit", 50)?;
218 let hull_ma_type = text
219 .get("hull_ma_type")
220 .cloned()
221 .unwrap_or_else(|| "WMA".to_string());
222 let sweep = super::dma::DmaBatchRange {
223 hull_length: period_range,
224 ema_length: (ema_length, ema_length, 0),
225 ema_gain_limit: (ema_gain_limit, ema_gain_limit, 0),
226 hull_ma_type,
227 };
228 let out = super::dma::dma_batch_with_kernel(prices, &sweep, kernel)?;
229 return Ok(MaBatchOutput {
230 periods: map_periods(&out.combos, |p| p.hull_length.unwrap_or(7)),
231 values: out.values,
232 rows: out.rows,
233 cols: out.cols,
234 });
235 }
236
237 if ma_type.eq_ignore_ascii_case("vwap")
238 && (text.contains_key("anchor")
239 || text.contains_key("anchor_start")
240 || text.contains_key("anchor_end"))
241 {
242 let kernel = to_batch_kernel(kernel)?;
243 let (prices, candles) = match data {
244 MaData::Slice(s) => (s, None),
245 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
246 };
247 let candles = candles.ok_or(MaBatchDispatchError::RequiresCandles { indicator: "vwap" })?;
248
249 let single_anchor = text.get("anchor").cloned();
250 let anchor_start = text
251 .get("anchor_start")
252 .cloned()
253 .or_else(|| single_anchor.clone())
254 .unwrap_or_else(|| "1d".to_string());
255 let anchor_end = text
256 .get("anchor_end")
257 .cloned()
258 .or_else(|| single_anchor.clone())
259 .unwrap_or_else(|| anchor_start.clone());
260 let anchor_step = numeric
261 .get("anchor_step")
262 .copied()
263 .map(|v| {
264 if v < 0.0 {
265 return Err(MaBatchDispatchError::InvalidParam {
266 indicator: "vwap",
267 key: "anchor_step",
268 value: v,
269 reason: "expected >= 0",
270 });
271 }
272 let r = v.round();
273 if (v - r).abs() > 1e-9 {
274 return Err(MaBatchDispatchError::InvalidParam {
275 indicator: "vwap",
276 key: "anchor_step",
277 value: v,
278 reason: "expected integer",
279 });
280 }
281 Ok(r as u32)
282 })
283 .transpose()?
284 .unwrap_or_else(|| if anchor_start == anchor_end { 0 } else { 1 });
285
286 let sweep = super::vwap::VwapBatchRange {
287 anchor: (anchor_start, anchor_end, anchor_step),
288 };
289 let out = super::vwap::vwap_batch_with_kernel(
290 &candles.timestamp,
291 &candles.volume,
292 prices,
293 &sweep,
294 kernel,
295 )?;
296 let periods = out
297 .combos
298 .iter()
299 .enumerate()
300 .map(|(i, p)| {
301 p.anchor
302 .as_deref()
303 .and_then(|a| super::vwap::parse_anchor(a).ok().map(|(n, _)| n as usize))
304 .unwrap_or(i + 1)
305 })
306 .collect();
307 return Ok(MaBatchOutput {
308 periods,
309 values: out.values,
310 rows: out.rows,
311 cols: out.cols,
312 });
313 }
314
315 if ma_type.eq_ignore_ascii_case("mama") {
316 let kernel = to_batch_kernel(kernel)?;
317 let (prices, _) = match data {
318 MaData::Slice(s) => (s, None),
319 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
320 };
321 let mut sweep = super::mama::MamaBatchRange::default();
322 if let Some(v) = numeric.get("fast_limit").copied() {
323 sweep.fast_limit = (v, v, 0.0);
324 } else {
325 if let Some(v) = numeric.get("fast_limit_start").copied() {
326 sweep.fast_limit.0 = v;
327 }
328 if let Some(v) = numeric.get("fast_limit_end").copied() {
329 sweep.fast_limit.1 = v;
330 }
331 if let Some(v) = numeric.get("fast_limit_step").copied() {
332 sweep.fast_limit.2 = v;
333 }
334 }
335 if let Some(v) = numeric.get("slow_limit").copied() {
336 sweep.slow_limit = (v, v, 0.0);
337 } else {
338 if let Some(v) = numeric.get("slow_limit_start").copied() {
339 sweep.slow_limit.0 = v;
340 }
341 if let Some(v) = numeric.get("slow_limit_end").copied() {
342 sweep.slow_limit.1 = v;
343 }
344 if let Some(v) = numeric.get("slow_limit_step").copied() {
345 sweep.slow_limit.2 = v;
346 }
347 }
348 let out = super::mama::mama_batch_with_kernel(prices, &sweep, kernel)?;
349 let output = text
350 .get("output")
351 .map(String::as_str)
352 .unwrap_or("mama")
353 .to_ascii_lowercase();
354 let values = match output.as_str() {
355 "mama" => out.mama_values,
356 "fama" => out.fama_values,
357 _ => {
358 return Err(MaBatchDispatchError::InvalidParam {
359 indicator: "mama",
360 key: "output",
361 value: f64::NAN,
362 reason: "expected 'mama' or 'fama'",
363 }
364 .into())
365 }
366 };
367 return Ok(MaBatchOutput {
368 periods: (1..=out.rows).collect(),
369 values,
370 rows: out.rows,
371 cols: out.cols,
372 });
373 }
374
375 if ma_type.eq_ignore_ascii_case("ehlers_pma") {
376 let kernel = to_batch_kernel(kernel)?;
377 let periods = expand_period_axis(period_range)?;
378 let rows = periods.len();
379 let (prices, _) = match data {
380 MaData::Slice(s) => (s, None),
381 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
382 };
383 let input = super::ehlers_pma::EhlersPmaInput::from_slice(
384 prices,
385 super::ehlers_pma::EhlersPmaParams::default(),
386 );
387 let out = super::ehlers_pma::ehlers_pma_with_kernel(&input, kernel)?;
388 let output = text
389 .get("output")
390 .map(String::as_str)
391 .unwrap_or("predict")
392 .to_ascii_lowercase();
393 let series = match output.as_str() {
394 "predict" => &out.predict,
395 "trigger" => &out.trigger,
396 _ => {
397 return Err(MaBatchDispatchError::InvalidParam {
398 indicator: "ehlers_pma",
399 key: "output",
400 value: f64::NAN,
401 reason: "expected 'predict' or 'trigger'",
402 }
403 .into())
404 }
405 };
406 let cols = series.len();
407 let mut values = Vec::with_capacity(rows.saturating_mul(cols));
408 for _ in 0..rows {
409 values.extend_from_slice(series);
410 }
411 return Ok(MaBatchOutput {
412 periods,
413 values,
414 rows,
415 cols,
416 });
417 }
418
419 if ma_type.eq_ignore_ascii_case("ema_deviation_corrected_t3") {
420 let kernel = to_batch_kernel(kernel)?;
421 let (prices, _) = match data {
422 MaData::Slice(s) => (s, None),
423 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
424 };
425 let sweep = super::ema_deviation_corrected_t3::EmaDeviationCorrectedT3BatchRange {
426 period: period_range,
427 hot: {
428 let v = numeric.get("hot").copied().unwrap_or(0.7);
429 (v, v, 0.0)
430 },
431 t3_mode: {
432 let v = numeric.get("t3_mode").copied().unwrap_or(0.0);
433 if v < 0.0 || (v - v.round()).abs() > 1e-9 {
434 return Err(MaBatchDispatchError::InvalidParam {
435 indicator: "ema_deviation_corrected_t3",
436 key: "t3_mode",
437 value: v,
438 reason: "expected integer >= 0",
439 }
440 .into());
441 }
442 let v = v.round() as usize;
443 (v, v, 0)
444 },
445 };
446 let out = super::ema_deviation_corrected_t3::ema_deviation_corrected_t3_batch_with_kernel(
447 prices, &sweep, kernel,
448 )?;
449 let output = text
450 .get("output")
451 .map(String::as_str)
452 .unwrap_or("corrected")
453 .to_ascii_lowercase();
454 let periods = map_periods(&out.combos, |p| p.period.unwrap_or(10));
455 let rows = out.rows;
456 let cols = out.cols;
457 let series = match output.as_str() {
458 "corrected" | "value" => out.corrected,
459 "t3" => out.t3,
460 _ => {
461 return Err(MaBatchDispatchError::InvalidParam {
462 indicator: "ema_deviation_corrected_t3",
463 key: "output",
464 value: f64::NAN,
465 reason: "expected 'corrected' or 't3'",
466 }
467 .into())
468 }
469 };
470 return Ok(MaBatchOutput {
471 periods,
472 values: series,
473 rows,
474 cols,
475 });
476 }
477
478 if ma_type.eq_ignore_ascii_case("ehlers_undersampled_double_moving_average") {
479 let kernel = to_batch_kernel(kernel)?;
480 let (prices, _) = match data {
481 MaData::Slice(s) => (s, None),
482 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
483 };
484
485 let get_u = |key: &'static str, default_v: usize| -> Result<usize, MaBatchDispatchError> {
486 let Some(v) = numeric.get(key).copied() else {
487 return Ok(default_v);
488 };
489 if v < 0.0 {
490 return Err(MaBatchDispatchError::InvalidParam {
491 indicator: "ehlers_undersampled_double_moving_average",
492 key,
493 value: v,
494 reason: "expected >= 0",
495 });
496 }
497 let r = v.round();
498 if (v - r).abs() > 1e-9 {
499 return Err(MaBatchDispatchError::InvalidParam {
500 indicator: "ehlers_undersampled_double_moving_average",
501 key,
502 value: v,
503 reason: "expected integer",
504 });
505 }
506 Ok(r as usize)
507 };
508
509 let mut sweep =
510 super::ehlers_undersampled_double_moving_average::EhlersUndersampledDoubleMovingAverageBatchRange::default();
511 sweep.fast_length = (
512 get_u("fast_length_start", get_u("fast_length", 6)?)?,
513 get_u("fast_length_end", get_u("fast_length", 6)?)?,
514 get_u("fast_length_step", 0)?,
515 );
516 sweep.slow_length = (
517 get_u("slow_length_start", get_u("slow_length", 12)?)?,
518 get_u("slow_length_end", get_u("slow_length", 12)?)?,
519 get_u("slow_length_step", 0)?,
520 );
521 sweep.sample_length = (
522 get_u("sample_length_start", get_u("sample_length", 5)?)?,
523 get_u("sample_length_end", get_u("sample_length", 5)?)?,
524 get_u("sample_length_step", 0)?,
525 );
526
527 let out =
528 super::ehlers_undersampled_double_moving_average::ehlers_undersampled_double_moving_average_batch_with_kernel(
529 prices, &sweep, kernel,
530 )?;
531 let output = text
532 .get("output")
533 .map(String::as_str)
534 .unwrap_or("fast")
535 .to_ascii_lowercase();
536 let values = match output.as_str() {
537 "fast" => out.fast_values,
538 "slow" => out.slow_values,
539 _ => {
540 return Err(MaBatchDispatchError::InvalidParam {
541 indicator: "ehlers_undersampled_double_moving_average",
542 key: "output",
543 value: f64::NAN,
544 reason: "expected 'fast' or 'slow'",
545 }
546 .into())
547 }
548 };
549
550 return Ok(MaBatchOutput {
551 periods: (1..=out.rows).collect(),
552 values,
553 rows: out.rows,
554 cols: out.cols,
555 });
556 }
557
558 if ma_type.eq_ignore_ascii_case("buff_averages") {
559 let kernel = to_batch_kernel(kernel)?;
560 let (prices, candles) = match data {
561 MaData::Slice(s) => (s, None),
562 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
563 };
564 let candles = candles.ok_or(MaBatchDispatchError::RequiresCandles {
565 indicator: "buff_averages",
566 })?;
567
568 let get_u = |key: &'static str| -> Result<Option<usize>, MaBatchDispatchError> {
569 let Some(v) = numeric.get(key).copied() else {
570 return Ok(None);
571 };
572 if v < 0.0 {
573 return Err(MaBatchDispatchError::InvalidParam {
574 indicator: "buff_averages",
575 key,
576 value: v,
577 reason: "expected >= 0",
578 });
579 }
580 let r = v.round();
581 if (v - r).abs() > 1e-9 {
582 return Err(MaBatchDispatchError::InvalidParam {
583 indicator: "buff_averages",
584 key,
585 value: v,
586 reason: "expected integer",
587 });
588 }
589 Ok(Some(r as usize))
590 };
591
592 let mut sweep = super::buff_averages::BuffAveragesBatchRange::default();
593 sweep.slow_period = period_range;
594
595 if let Some(v) = get_u("fast_period")? {
596 sweep.fast_period = (v, v, 0);
597 }
598 if let Some(v) = get_u("slow_period")? {
599 sweep.slow_period = (v, v, 0);
600 }
601 if let Some(v) = get_u("fast_period_start")? {
602 sweep.fast_period.0 = v;
603 }
604 if let Some(v) = get_u("fast_period_end")? {
605 sweep.fast_period.1 = v;
606 }
607 if let Some(v) = get_u("fast_period_step")? {
608 sweep.fast_period.2 = v;
609 }
610 if let Some(v) = get_u("slow_period_start")? {
611 sweep.slow_period.0 = v;
612 }
613 if let Some(v) = get_u("slow_period_end")? {
614 sweep.slow_period.1 = v;
615 }
616 if let Some(v) = get_u("slow_period_step")? {
617 sweep.slow_period.2 = v;
618 }
619
620 let out = super::buff_averages::buff_averages_batch_with_kernel(
621 prices,
622 &candles.volume,
623 &sweep,
624 kernel,
625 )?;
626
627 let output = text
628 .get("output")
629 .map(String::as_str)
630 .unwrap_or("fast")
631 .to_ascii_lowercase();
632 let values = match output.as_str() {
633 "fast" | "fast_buff" => out.fast,
634 "slow" | "slow_buff" => out.slow,
635 _ => {
636 return Err(MaBatchDispatchError::InvalidParam {
637 indicator: "buff_averages",
638 key: "output",
639 value: f64::NAN,
640 reason: "expected 'fast' or 'slow'",
641 }
642 .into())
643 }
644 };
645
646 let all_fast_same = out
647 .combos
648 .first()
649 .map(|c| out.combos.iter().all(|x| x.0 == c.0))
650 .unwrap_or(true);
651 let all_slow_same = out
652 .combos
653 .first()
654 .map(|c| out.combos.iter().all(|x| x.1 == c.1))
655 .unwrap_or(true);
656 let periods = if all_fast_same {
657 out.combos.iter().map(|c| c.1).collect()
658 } else if all_slow_same {
659 out.combos.iter().map(|c| c.0).collect()
660 } else {
661 (1..=out.rows).collect()
662 };
663
664 return Ok(MaBatchOutput {
665 periods,
666 values,
667 rows: out.rows,
668 cols: out.cols,
669 });
670 }
671
672 if ma_type.eq_ignore_ascii_case("n_order_ema") {
673 let kernel = to_batch_kernel(kernel)?;
674 let (prices, _) = match data {
675 MaData::Slice(s) => (s, None),
676 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
677 };
678
679 let order = match numeric.get("order").copied() {
680 Some(v) => {
681 if v <= 0.0 {
682 return Err(MaBatchDispatchError::InvalidParam {
683 indicator: "n_order_ema",
684 key: "order",
685 value: v,
686 reason: "expected integer > 0",
687 }
688 .into());
689 }
690 let r = v.round();
691 if (v - r).abs() > 1.0e-9 {
692 return Err(MaBatchDispatchError::InvalidParam {
693 indicator: "n_order_ema",
694 key: "order",
695 value: v,
696 reason: "expected integer",
697 }
698 .into());
699 }
700 r as usize
701 }
702 None => 1usize,
703 };
704
705 let ema_style = text
706 .get("ema_style")
707 .cloned()
708 .unwrap_or_else(|| "ema".to_string());
709 let iir_style = text
710 .get("iir_style")
711 .cloned()
712 .unwrap_or_else(|| "impulse_matched".to_string());
713
714 let out = super::n_order_ema::n_order_ema_batch_with_kernel(
715 prices,
716 &super::n_order_ema::NOrderEmaBatchRange {
717 period: (
718 period_range.0 as f64,
719 period_range.1 as f64,
720 period_range.2 as f64,
721 ),
722 order: (order, order, 0),
723 },
724 &super::n_order_ema::NOrderEmaParams {
725 period: None,
726 order: None,
727 ema_style: Some(ema_style),
728 iir_style: Some(iir_style),
729 },
730 kernel,
731 )?;
732 return Ok(MaBatchOutput {
733 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9.0).round() as usize),
734 values: out.values,
735 rows: out.rows,
736 cols: out.cols,
737 });
738 }
739
740 ma_batch_with_kernel_and_params(ma_type, data, period_range, kernel, Some(&numeric))
741}
742
743pub fn ma_batch_with_kernel_and_params<'a>(
744 ma_type: &str,
745 data: MaData<'a>,
746 period_range: (usize, usize, usize),
747 kernel: Kernel,
748 params: Option<&HashMap<String, f64>>,
749) -> Result<MaBatchOutput, Box<dyn Error>> {
750 let kernel = to_batch_kernel(kernel)?;
751 let (prices, candles) = match data {
752 MaData::Slice(s) => (s, None),
753 MaData::Candles { candles, source } => (source_type(candles, source), Some(candles)),
754 };
755
756 #[inline]
757 fn get_f64(
758 params: Option<&HashMap<String, f64>>,
759 indicator: &'static str,
760 key: &'static str,
761 ) -> Result<Option<f64>, MaBatchDispatchError> {
762 match params.and_then(|m| m.get(key).copied()) {
763 None => Ok(None),
764 Some(v) if v.is_finite() => Ok(Some(v)),
765 Some(v) => Err(MaBatchDispatchError::InvalidParam {
766 indicator,
767 key,
768 value: v,
769 reason: "expected finite number",
770 }),
771 }
772 }
773
774 #[inline]
775 fn get_usize(
776 params: Option<&HashMap<String, f64>>,
777 indicator: &'static str,
778 key: &'static str,
779 ) -> Result<Option<usize>, MaBatchDispatchError> {
780 let Some(v) = get_f64(params, indicator, key)? else {
781 return Ok(None);
782 };
783 if v < 0.0 {
784 return Err(MaBatchDispatchError::InvalidParam {
785 indicator,
786 key,
787 value: v,
788 reason: "expected >= 0",
789 });
790 }
791 let r = v.round();
792 if (v - r).abs() > 1e-9 {
793 return Err(MaBatchDispatchError::InvalidParam {
794 indicator,
795 key,
796 value: v,
797 reason: "expected integer",
798 });
799 }
800 if r > (usize::MAX as f64) {
801 return Err(MaBatchDispatchError::InvalidParam {
802 indicator,
803 key,
804 value: v,
805 reason: "too large for usize",
806 });
807 }
808 Ok(Some(r as usize))
809 }
810
811 #[inline]
812 fn get_u32(
813 params: Option<&HashMap<String, f64>>,
814 indicator: &'static str,
815 key: &'static str,
816 ) -> Result<Option<u32>, MaBatchDispatchError> {
817 let Some(v) = get_usize(params, indicator, key)? else {
818 return Ok(None);
819 };
820 if v > (u32::MAX as usize) {
821 return Err(MaBatchDispatchError::InvalidParam {
822 indicator,
823 key,
824 value: v as f64,
825 reason: "too large for u32",
826 });
827 }
828 Ok(Some(v as u32))
829 }
830
831 match ma_type.to_ascii_lowercase().as_str() {
832 "sma" => {
833 let sweep = super::sma::SmaBatchRange {
834 period: period_range,
835 };
836 let out = super::sma::sma_batch_with_kernel(prices, &sweep, kernel)?;
837 Ok(MaBatchOutput {
838 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
839 values: out.values,
840 rows: out.rows,
841 cols: out.cols,
842 })
843 }
844 "ema" => {
845 let sweep = super::ema::EmaBatchRange {
846 period: period_range,
847 };
848 let out = super::ema::ema_batch_with_kernel(prices, &sweep, kernel)?;
849 Ok(MaBatchOutput {
850 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
851 values: out.values,
852 rows: out.rows,
853 cols: out.cols,
854 })
855 }
856 "dema" => {
857 let sweep = super::dema::DemaBatchRange {
858 period: period_range,
859 };
860 let out = super::dema::dema_batch_with_kernel(prices, &sweep, kernel)?;
861 Ok(MaBatchOutput {
862 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
863 values: out.values,
864 rows: out.rows,
865 cols: out.cols,
866 })
867 }
868 "tema" => {
869 let sweep = super::tema::TemaBatchRange {
870 period: period_range,
871 };
872 let out = super::tema::tema_batch_with_kernel(prices, &sweep, kernel)?;
873 Ok(MaBatchOutput {
874 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
875 values: out.values,
876 rows: out.rows,
877 cols: out.cols,
878 })
879 }
880 "smma" => {
881 let sweep = super::smma::SmmaBatchRange {
882 period: period_range,
883 };
884 let out = super::smma::smma_batch_with_kernel(prices, &sweep, kernel)?;
885 Ok(MaBatchOutput {
886 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
887 values: out.values,
888 rows: out.rows,
889 cols: out.cols,
890 })
891 }
892 "zlema" => {
893 let sweep = super::zlema::ZlemaBatchRange {
894 period: period_range,
895 };
896 let out = super::zlema::zlema_batch_with_kernel(prices, &sweep, kernel)?;
897 Ok(MaBatchOutput {
898 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
899 values: out.values,
900 rows: out.rows,
901 cols: out.cols,
902 })
903 }
904 "wma" => {
905 let sweep = super::wma::WmaBatchRange {
906 period: period_range,
907 };
908 let out = super::wma::wma_with_kernel_batch(prices, &sweep, kernel)?;
909 Ok(MaBatchOutput {
910 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
911 values: out.values,
912 rows: out.rows,
913 cols: out.cols,
914 })
915 }
916 "alma" => {
917 let mut sweep = super::alma::AlmaBatchRange::default();
918 sweep.period = period_range;
919 if let Some(v) = get_f64(params, "alma", "offset")? {
920 sweep.offset = (v, v, 0.0);
921 }
922 if let Some(v) = get_f64(params, "alma", "sigma")? {
923 sweep.sigma = (v, v, 0.0);
924 }
925 let out = super::alma::alma_batch_with_kernel(prices, &sweep, kernel)?;
926 Ok(MaBatchOutput {
927 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
928 values: out.values,
929 rows: out.rows,
930 cols: out.cols,
931 })
932 }
933 "cwma" => {
934 let sweep = super::cwma::CwmaBatchRange {
935 period: period_range,
936 };
937 let out = super::cwma::cwma_batch_with_kernel(prices, &sweep, kernel)?;
938 Ok(MaBatchOutput {
939 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
940 values: out.values,
941 rows: out.rows,
942 cols: out.cols,
943 })
944 }
945 "corrected_moving_average" | "cma" => {
946 let sweep = super::corrected_moving_average::CorrectedMovingAverageBatchRange {
947 period: period_range,
948 };
949 let out = super::corrected_moving_average::corrected_moving_average_batch_with_kernel(
950 prices, &sweep, kernel,
951 )?;
952 Ok(MaBatchOutput {
953 periods: map_periods(&out.combos, |p| p.period.unwrap_or(35)),
954 values: out.values,
955 rows: out.rows,
956 cols: out.cols,
957 })
958 }
959 "cora_wave" => {
960 let mut sweep = crate::indicators::cora_wave::CoraWaveBatchRange {
961 period: period_range,
962 r_multi: (2.0, 2.0, 0.0),
963 smooth: true,
964 };
965 if let Some(v) = get_f64(params, "cora_wave", "r_multi")? {
966 if v < 0.0 {
967 return Err(MaBatchDispatchError::InvalidParam {
968 indicator: "cora_wave",
969 key: "r_multi",
970 value: v,
971 reason: "expected >= 0",
972 }
973 .into());
974 }
975 sweep.r_multi = (v, v, 0.0);
976 }
977 if let Some(v) = get_usize(params, "cora_wave", "smooth")? {
978 sweep.smooth = match v {
979 0 => false,
980 1 => true,
981 other => {
982 return Err(MaBatchDispatchError::InvalidParam {
983 indicator: "cora_wave",
984 key: "smooth",
985 value: other as f64,
986 reason: "expected 0 or 1",
987 }
988 .into());
989 }
990 };
991 }
992 let out =
993 crate::indicators::cora_wave::cora_wave_batch_with_kernel(prices, &sweep, kernel)?;
994 Ok(MaBatchOutput {
995 periods: map_periods(&out.combos, |p| p.period.unwrap_or(20)),
996 values: out.values,
997 rows: out.rows,
998 cols: out.cols,
999 })
1000 }
1001 "edcf" => {
1002 let sweep = super::edcf::EdcfBatchRange {
1003 period: period_range,
1004 };
1005 let out = super::edcf::edcf_batch_with_kernel(prices, &sweep, kernel)?;
1006 Ok(MaBatchOutput {
1007 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1008 values: out.values,
1009 rows: out.rows,
1010 cols: out.cols,
1011 })
1012 }
1013 "fwma" => {
1014 let sweep = super::fwma::FwmaBatchRange {
1015 period: period_range,
1016 };
1017 let out = super::fwma::fwma_batch_with_kernel(prices, &sweep, kernel)?;
1018 Ok(MaBatchOutput {
1019 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1020 values: out.values,
1021 rows: out.rows,
1022 cols: out.cols,
1023 })
1024 }
1025 "gaussian" => {
1026 let mut sweep = super::gaussian::GaussianBatchRange::default();
1027 sweep.period = period_range;
1028 if let Some(v) = get_usize(params, "gaussian", "poles")? {
1029 sweep.poles = (v, v, 0);
1030 }
1031 let out = super::gaussian::gaussian_batch_with_kernel(prices, &sweep, kernel)?;
1032 Ok(MaBatchOutput {
1033 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1034 values: out.values,
1035 rows: out.rows,
1036 cols: out.cols,
1037 })
1038 }
1039 "highpass" => {
1040 let sweep = super::highpass::HighPassBatchRange {
1041 period: period_range,
1042 };
1043 let out = super::highpass::highpass_batch_with_kernel(prices, &sweep, kernel)?;
1044 Ok(MaBatchOutput {
1045 periods: map_periods(&out.combos, |p| p.period.unwrap_or(48)),
1046 values: out.values,
1047 rows: out.rows,
1048 cols: out.cols,
1049 })
1050 }
1051 "highpass2" | "highpass_2_pole" => {
1052 let mut sweep = super::highpass_2_pole::HighPass2BatchRange::default();
1053 sweep.period = period_range;
1054 if let Some(v) = get_f64(params, "highpass_2_pole", "k")? {
1055 sweep.k = (v, v, 0.0);
1056 }
1057 let out =
1058 super::highpass_2_pole::highpass_2_pole_batch_with_kernel(prices, &sweep, kernel)?;
1059 Ok(MaBatchOutput {
1060 periods: map_periods(&out.combos, |p| p.period.unwrap_or(48)),
1061 values: out.values,
1062 rows: out.rows,
1063 cols: out.cols,
1064 })
1065 }
1066 "hma" => {
1067 let sweep = super::hma::HmaBatchRange {
1068 period: period_range,
1069 };
1070 let out = super::hma::hma_batch_with_kernel(prices, &sweep, kernel)?;
1071 Ok(MaBatchOutput {
1072 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1073 values: out.values,
1074 rows: out.rows,
1075 cols: out.cols,
1076 })
1077 }
1078 "jma" => {
1079 let mut sweep = super::jma::JmaBatchRange::default();
1080 sweep.period = period_range;
1081 if let Some(v) = get_f64(params, "jma", "phase")? {
1082 sweep.phase = (v, v, 0.0);
1083 }
1084 if let Some(v) = get_u32(params, "jma", "power")? {
1085 sweep.power = (v, v, 0);
1086 }
1087 let out = super::jma::jma_batch_with_kernel(prices, &sweep, kernel)?;
1088 Ok(MaBatchOutput {
1089 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1090 values: out.values,
1091 rows: out.rows,
1092 cols: out.cols,
1093 })
1094 }
1095 "jsa" => {
1096 let sweep = super::jsa::JsaBatchRange {
1097 period: period_range,
1098 };
1099 let out = super::jsa::jsa_batch_with_kernel(prices, &sweep, kernel)?;
1100 Ok(MaBatchOutput {
1101 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1102 values: out.values,
1103 rows: out.rows,
1104 cols: out.cols,
1105 })
1106 }
1107 "linreg" => {
1108 let sweep = super::linreg::LinRegBatchRange {
1109 period: period_range,
1110 };
1111 let out = super::linreg::linreg_batch_with_kernel(prices, &sweep, kernel)?;
1112 Ok(MaBatchOutput {
1113 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1114 values: out.values,
1115 rows: out.rows,
1116 cols: out.cols,
1117 })
1118 }
1119 "kama" => {
1120 let sweep = super::kama::KamaBatchRange {
1121 period: period_range,
1122 };
1123 let out = super::kama::kama_batch_with_kernel(prices, &sweep, kernel)?;
1124 Ok(MaBatchOutput {
1125 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1126 values: out.values,
1127 rows: out.rows,
1128 cols: out.cols,
1129 })
1130 }
1131 "ehlers_kama" => {
1132 let sweep = super::ehlers_kama::EhlersKamaBatchRange {
1133 period: period_range,
1134 };
1135 let out = super::ehlers_kama::ehlers_kama_batch_with_kernel(prices, &sweep, kernel)?;
1136 Ok(MaBatchOutput {
1137 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1138 values: out.values,
1139 rows: out.rows,
1140 cols: out.cols,
1141 })
1142 }
1143 "ehlers_itrend" => {
1144 let warmup = get_usize(params, "ehlers_itrend", "warmup_bars")?.unwrap_or(20);
1145 let sweep = super::ehlers_itrend::EhlersITrendBatchRange {
1146 warmup_bars: (warmup, warmup, 0),
1147 max_dc_period: period_range,
1148 };
1149 let out =
1150 super::ehlers_itrend::ehlers_itrend_batch_with_kernel(prices, &sweep, kernel)?;
1151 Ok(MaBatchOutput {
1152 periods: map_periods(&out.combos, |p| p.max_dc_period.unwrap_or(48)),
1153 values: out.values,
1154 rows: out.rows,
1155 cols: out.cols,
1156 })
1157 }
1158 "ehlers_ecema" => {
1159 let gain_limit = get_usize(params, "ehlers_ecema", "gain_limit")?.unwrap_or(50);
1160 let sweep = super::ehlers_ecema::EhlersEcemaBatchRange {
1161 length: period_range,
1162 gain_limit: (gain_limit, gain_limit, 0),
1163 };
1164 let out = super::ehlers_ecema::ehlers_ecema_batch_with_kernel(prices, &sweep, kernel)?;
1165 Ok(MaBatchOutput {
1166 periods: map_periods(&out.combos, |p| p.length.unwrap_or(20)),
1167 values: out.values,
1168 rows: out.rows,
1169 cols: out.cols,
1170 })
1171 }
1172 "ehma" => {
1173 let sweep = super::ehma::EhmaBatchRange {
1174 period: period_range,
1175 };
1176 let out = super::ehma::ehma_batch_with_kernel(prices, &sweep, kernel)?;
1177 Ok(MaBatchOutput {
1178 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1179 values: out.values,
1180 rows: out.rows,
1181 cols: out.cols,
1182 })
1183 }
1184 "nama" => {
1185 let sweep = super::nama::NamaBatchRange {
1186 period: period_range,
1187 };
1188 let out = super::nama::nama_batch_with_kernel(prices, &sweep, kernel)?;
1189 Ok(MaBatchOutput {
1190 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1191 values: out.values,
1192 rows: out.rows,
1193 cols: out.cols,
1194 })
1195 }
1196 "n_order_ema" => {
1197 let out = super::n_order_ema::n_order_ema_batch_with_kernel(
1198 prices,
1199 &super::n_order_ema::NOrderEmaBatchRange {
1200 period: (
1201 period_range.0 as f64,
1202 period_range.1 as f64,
1203 period_range.2 as f64,
1204 ),
1205 order: (1, 1, 0),
1206 },
1207 &super::n_order_ema::NOrderEmaParams {
1208 period: None,
1209 order: None,
1210 ema_style: Some("ema".to_string()),
1211 iir_style: Some("impulse_matched".to_string()),
1212 },
1213 kernel,
1214 )?;
1215 Ok(MaBatchOutput {
1216 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9.0).round() as usize),
1217 values: out.values,
1218 rows: out.rows,
1219 cols: out.cols,
1220 })
1221 }
1222 "nma" => {
1223 let sweep = super::nma::NmaBatchRange {
1224 period: period_range,
1225 };
1226 let out = super::nma::nma_batch_with_kernel(prices, &sweep, kernel)?;
1227 Ok(MaBatchOutput {
1228 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1229 values: out.values,
1230 rows: out.rows,
1231 cols: out.cols,
1232 })
1233 }
1234 "pwma" => {
1235 let sweep = super::pwma::PwmaBatchRange {
1236 period: period_range,
1237 };
1238 let out = super::pwma::pwma_batch_with_kernel(prices, &sweep, kernel)?;
1239 Ok(MaBatchOutput {
1240 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1241 values: out.values,
1242 rows: out.rows,
1243 cols: out.cols,
1244 })
1245 }
1246 "reflex" => {
1247 let sweep = super::reflex::ReflexBatchRange {
1248 period: period_range,
1249 };
1250 let out = super::reflex::reflex_batch_with_kernel(prices, &sweep, kernel)?;
1251 Ok(MaBatchOutput {
1252 periods: map_periods(&out.combos, |p| p.period.unwrap_or(48)),
1253 values: out.values,
1254 rows: out.rows,
1255 cols: out.cols,
1256 })
1257 }
1258 "sinwma" => {
1259 let sweep = super::sinwma::SinWmaBatchRange {
1260 period: period_range,
1261 };
1262 let out = super::sinwma::sinwma_batch_with_kernel(prices, &sweep, kernel)?;
1263 Ok(MaBatchOutput {
1264 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1265 values: out.values,
1266 rows: out.rows,
1267 cols: out.cols,
1268 })
1269 }
1270 "sqwma" => {
1271 let sweep = super::sqwma::SqwmaBatchRange {
1272 period: period_range,
1273 };
1274 let out = super::sqwma::sqwma_batch_with_kernel(prices, &sweep, kernel)?;
1275 Ok(MaBatchOutput {
1276 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1277 values: out.values,
1278 rows: out.rows,
1279 cols: out.cols,
1280 })
1281 }
1282 "srwma" => {
1283 let sweep = super::srwma::SrwmaBatchRange {
1284 period: period_range,
1285 };
1286 let out = super::srwma::srwma_batch_with_kernel(prices, &sweep, kernel)?;
1287 Ok(MaBatchOutput {
1288 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1289 values: out.values,
1290 rows: out.rows,
1291 cols: out.cols,
1292 })
1293 }
1294 "sgf" => {
1295 let poly_order = get_usize(params, "sgf", "poly_order")?.unwrap_or(2);
1296 let sweep = super::sgf::SgfBatchRange {
1297 period: period_range,
1298 poly_order: (poly_order, poly_order, 0),
1299 };
1300 let out = super::sgf::sgf_batch_with_kernel(prices, &sweep, kernel)?;
1301 Ok(MaBatchOutput {
1302 periods: map_periods(&out.combos, |p| p.period.unwrap_or(21)),
1303 values: out.values,
1304 rows: out.rows,
1305 cols: out.cols,
1306 })
1307 }
1308 "swma" => {
1309 let sweep = super::swma::SwmaBatchRange {
1310 period: period_range,
1311 };
1312 let out = super::swma::swma_batch_with_kernel(prices, &sweep, kernel)?;
1313 Ok(MaBatchOutput {
1314 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1315 values: out.values,
1316 rows: out.rows,
1317 cols: out.cols,
1318 })
1319 }
1320 "supersmoother" => {
1321 let sweep = super::supersmoother::SuperSmootherBatchRange {
1322 period: period_range,
1323 };
1324 let out =
1325 super::supersmoother::supersmoother_batch_with_kernel(prices, &sweep, kernel)?;
1326 Ok(MaBatchOutput {
1327 periods: map_periods(&out.combos, |p| p.period.unwrap_or(48)),
1328 values: out.values,
1329 rows: out.rows,
1330 cols: out.cols,
1331 })
1332 }
1333 "supersmoother_3_pole" => {
1334 let sweep = super::supersmoother_3_pole::SuperSmoother3PoleBatchRange {
1335 period: period_range,
1336 };
1337 let out = super::supersmoother_3_pole::supersmoother_3_pole_batch_with_kernel(
1338 prices, &sweep, kernel,
1339 )?;
1340 Ok(MaBatchOutput {
1341 periods: map_periods(&out.combos, |p| p.period.unwrap_or(48)),
1342 values: out.values,
1343 rows: out.rows,
1344 cols: out.cols,
1345 })
1346 }
1347 "tilson" => {
1348 let mut sweep = super::tilson::TilsonBatchRange::default();
1349 sweep.period = period_range;
1350 if let Some(v) = get_f64(params, "tilson", "volume_factor")? {
1351 sweep.volume_factor = (v, v, 0.0);
1352 }
1353 let out = super::tilson::tilson_batch_with_kernel(prices, &sweep, kernel)?;
1354 Ok(MaBatchOutput {
1355 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1356 values: out.values,
1357 rows: out.rows,
1358 cols: out.cols,
1359 })
1360 }
1361 "trendflex" => {
1362 let sweep = super::trendflex::TrendFlexBatchRange {
1363 period: period_range,
1364 };
1365 let out = super::trendflex::trendflex_batch_with_kernel(prices, &sweep, kernel)?;
1366 Ok(MaBatchOutput {
1367 periods: map_periods(&out.combos, |p| p.period.unwrap_or(48)),
1368 values: out.values,
1369 rows: out.rows,
1370 cols: out.cols,
1371 })
1372 }
1373 "corrected_moving_average" => {
1374 let sweep = super::corrected_moving_average::CorrectedMovingAverageBatchRange {
1375 period: period_range,
1376 };
1377 let out = super::corrected_moving_average::corrected_moving_average_batch_with_kernel(
1378 prices, &sweep, kernel,
1379 )?;
1380 Ok(MaBatchOutput {
1381 periods: map_periods(&out.combos, |p| p.period.unwrap_or(35)),
1382 values: out.values,
1383 rows: out.rows,
1384 cols: out.cols,
1385 })
1386 }
1387 "ema_deviation_corrected_t3" => {
1388 let sweep = super::ema_deviation_corrected_t3::EmaDeviationCorrectedT3BatchRange {
1389 period: period_range,
1390 hot: {
1391 let v = get_f64(params, "ema_deviation_corrected_t3", "hot")?.unwrap_or(0.7);
1392 (v, v, 0.0)
1393 },
1394 t3_mode: {
1395 let v =
1396 get_usize(params, "ema_deviation_corrected_t3", "t3_mode")?.unwrap_or(0);
1397 (v, v, 0)
1398 },
1399 };
1400 let out =
1401 super::ema_deviation_corrected_t3::ema_deviation_corrected_t3_batch_with_kernel(
1402 prices, &sweep, kernel,
1403 )?;
1404 let periods = map_periods(&out.combos, |p| p.period.unwrap_or(10));
1405 let rows = out.rows;
1406 let cols = out.cols;
1407 Ok(MaBatchOutput {
1408 periods,
1409 values: out.corrected,
1410 rows,
1411 cols,
1412 })
1413 }
1414 "wave_smoother" => {
1415 let sweep = super::wave_smoother::WaveSmootherBatchRange {
1416 period: period_range,
1417 phase: {
1418 let v = get_f64(params, "wave_smoother", "phase")?.unwrap_or(70.0);
1419 (v, v, 0.0)
1420 },
1421 };
1422 let out =
1423 super::wave_smoother::wave_smoother_batch_with_kernel(prices, &sweep, kernel)?;
1424 Ok(MaBatchOutput {
1425 periods: map_periods(&out.combos, |p| p.period.unwrap_or(20)),
1426 values: out.values,
1427 rows: out.rows,
1428 cols: out.cols,
1429 })
1430 }
1431 "trima" => {
1432 let sweep = super::trima::TrimaBatchRange {
1433 period: period_range,
1434 };
1435 let out = super::trima::trima_batch_with_kernel(prices, &sweep, kernel)?;
1436 Ok(MaBatchOutput {
1437 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1438 values: out.values,
1439 rows: out.rows,
1440 cols: out.cols,
1441 })
1442 }
1443 "wilders" => {
1444 let sweep = super::wilders::WildersBatchRange {
1445 period: period_range,
1446 };
1447 let out = super::wilders::wilders_batch_with_kernel(prices, &sweep, kernel)?;
1448 Ok(MaBatchOutput {
1449 periods: map_periods(&out.combos, |p| p.period.unwrap_or(14)),
1450 values: out.values,
1451 rows: out.rows,
1452 cols: out.cols,
1453 })
1454 }
1455 "vpwma" => {
1456 let sweep = super::vpwma::VpwmaBatchRange {
1457 period: period_range,
1458 power: {
1459 let v = get_f64(params, "vpwma", "power")?.unwrap_or(0.382);
1460 (v, v, 0.0)
1461 },
1462 };
1463 let out = super::vpwma::vpwma_batch_with_kernel(prices, &sweep, kernel)?;
1464 Ok(MaBatchOutput {
1465 periods: map_periods(&out.combos, |p| p.period.unwrap_or(14)),
1466 values: out.values,
1467 rows: out.rows,
1468 cols: out.cols,
1469 })
1470 }
1471 "vwma" => {
1472 let candles =
1473 candles.ok_or(MaBatchDispatchError::RequiresCandles { indicator: "vwma" })?;
1474 let sweep = super::vwma::VwmaBatchRange {
1475 period: period_range,
1476 };
1477 let out = super::vwma::vwma_batch_with_kernel(prices, &candles.volume, &sweep, kernel)?;
1478 Ok(MaBatchOutput {
1479 periods: map_periods(&out.combos, |p| p.period.unwrap_or(20)),
1480 values: out.values,
1481 rows: out.rows,
1482 cols: out.cols,
1483 })
1484 }
1485 "elastic_volume_weighted_moving_average" => {
1486 let candles = candles.ok_or(MaBatchDispatchError::RequiresCandles {
1487 indicator: "elastic_volume_weighted_moving_average",
1488 })?;
1489 let mut sweep =
1490 super::elastic_volume_weighted_moving_average::ElasticVolumeWeightedMovingAverageBatchRange::default();
1491 sweep.length = period_range;
1492 if let Some(v) = get_f64(
1493 params,
1494 "elastic_volume_weighted_moving_average",
1495 "absolute_volume_millions",
1496 )? {
1497 sweep.absolute_volume_millions = Some(v);
1498 }
1499 if let Some(v) = get_usize(params, "elastic_volume_weighted_moving_average", "length")?
1500 {
1501 sweep.length = (v, v, 0);
1502 }
1503 if let Some(v) = get_usize(
1504 params,
1505 "elastic_volume_weighted_moving_average",
1506 "use_volume_sum",
1507 )? {
1508 sweep.use_volume_sum = Some(match v {
1509 0 => false,
1510 1 => true,
1511 other => {
1512 return Err(MaBatchDispatchError::InvalidParam {
1513 indicator: "elastic_volume_weighted_moving_average",
1514 key: "use_volume_sum",
1515 value: other as f64,
1516 reason: "expected 0 or 1",
1517 }
1518 .into());
1519 }
1520 });
1521 } else {
1522 sweep.use_volume_sum = Some(true);
1523 }
1524 let out = super::elastic_volume_weighted_moving_average::elastic_volume_weighted_moving_average_batch_with_kernel(
1525 prices,
1526 &candles.volume,
1527 &sweep,
1528 kernel,
1529 )?;
1530 Ok(MaBatchOutput {
1531 periods: map_periods(&out.combos, |p| p.length.unwrap_or(30)),
1532 values: out.values,
1533 rows: out.rows,
1534 cols: out.cols,
1535 })
1536 }
1537 "tradjema" => {
1538 let candles = candles.ok_or(MaBatchDispatchError::RequiresCandles {
1539 indicator: "tradjema",
1540 })?;
1541 let mut sweep = super::tradjema::TradjemaBatchRange::default();
1542 sweep.length = period_range;
1543 if let Some(v) = get_f64(params, "tradjema", "mult")? {
1544 sweep.mult = (v, v, 0.0);
1545 }
1546 let out = super::tradjema::tradjema_batch_with_kernel(
1547 &candles.high,
1548 &candles.low,
1549 &candles.close,
1550 &sweep,
1551 kernel,
1552 )?;
1553 Ok(MaBatchOutput {
1554 periods: map_periods(&out.combos, |p| p.length.unwrap_or(40)),
1555 values: out.values,
1556 rows: out.rows,
1557 cols: out.cols,
1558 })
1559 }
1560 "uma" => {
1561 let mut sweep = super::uma::UmaBatchRange::default();
1562 sweep.max_length = period_range;
1563 if let Some(v) = get_f64(params, "uma", "accelerator")? {
1564 sweep.accelerator = (v, v, 0.0);
1565 }
1566 if let Some(v) = get_usize(params, "uma", "min_length")? {
1567 sweep.min_length = (v, v, 0);
1568 }
1569 if let Some(v) = get_usize(params, "uma", "max_length")? {
1570 sweep.max_length = (v, v, 0);
1571 }
1572 if let Some(v) = get_usize(params, "uma", "smooth_length")? {
1573 sweep.smooth_length = (v, v, 0);
1574 }
1575 let volumes = candles.map(|c| c.volume.as_slice());
1576 let out = super::uma::uma_batch_with_kernel(prices, volumes, &sweep, kernel)?;
1577 Ok(MaBatchOutput {
1578 periods: map_periods(&out.combos, |p| p.max_length.unwrap_or(50)),
1579 values: out.values,
1580 rows: out.rows,
1581 cols: out.cols,
1582 })
1583 }
1584 "volume_adjusted_ma" => {
1585 let candles = candles.ok_or(MaBatchDispatchError::RequiresCandles {
1586 indicator: "volume_adjusted_ma",
1587 })?;
1588 let mut sweep = super::volume_adjusted_ma::VolumeAdjustedMaBatchRange::default();
1589 sweep.length = period_range;
1590 if let Some(v) = get_f64(params, "volume_adjusted_ma", "vi_factor")? {
1591 sweep.vi_factor = (v, v, 0.0);
1592 }
1593 if let Some(v) = get_usize(params, "volume_adjusted_ma", "sample_period")? {
1594 sweep.sample_period = (v, v, 0);
1595 }
1596 if let Some(v) = get_usize(params, "volume_adjusted_ma", "strict")? {
1597 sweep.strict = Some(match v {
1598 0 => false,
1599 1 => true,
1600 other => {
1601 return Err(MaBatchDispatchError::InvalidParam {
1602 indicator: "volume_adjusted_ma",
1603 key: "strict",
1604 value: other as f64,
1605 reason: "expected 0 or 1",
1606 }
1607 .into());
1608 }
1609 });
1610 }
1611 let out = super::volume_adjusted_ma::VolumeAdjustedMa_batch_with_kernel(
1612 prices,
1613 &candles.volume,
1614 &sweep,
1615 kernel,
1616 )?;
1617 Ok(MaBatchOutput {
1618 periods: map_periods(&out.combos, |p| p.length.unwrap_or(13)),
1619 values: out.values,
1620 rows: out.rows,
1621 cols: out.cols,
1622 })
1623 }
1624 "hwma" => {
1625 let mut sweep = super::hwma::HwmaBatchRange::default();
1626 if let Some(v) = get_f64(params, "hwma", "na")? {
1627 sweep.na = (v, v, 0.0);
1628 }
1629 if let Some(v) = get_f64(params, "hwma", "nb")? {
1630 sweep.nb = (v, v, 0.0);
1631 }
1632 if let Some(v) = get_f64(params, "hwma", "nc")? {
1633 sweep.nc = (v, v, 0.0);
1634 }
1635 let out = super::hwma::hwma_batch_with_kernel(prices, &sweep, kernel)?;
1636 Ok(MaBatchOutput {
1637 periods: (1..=out.rows).collect(),
1638 values: out.values,
1639 rows: out.rows,
1640 cols: out.cols,
1641 })
1642 }
1643 "mama" => {
1644 let mut sweep = super::mama::MamaBatchRange::default();
1645 if let Some(v) = get_f64(params, "mama", "fast_limit")? {
1646 sweep.fast_limit = (v, v, 0.0);
1647 } else {
1648 if let Some(v) = get_f64(params, "mama", "fast_limit_start")? {
1649 sweep.fast_limit.0 = v;
1650 }
1651 if let Some(v) = get_f64(params, "mama", "fast_limit_end")? {
1652 sweep.fast_limit.1 = v;
1653 }
1654 if let Some(v) = get_f64(params, "mama", "fast_limit_step")? {
1655 sweep.fast_limit.2 = v;
1656 }
1657 }
1658 if let Some(v) = get_f64(params, "mama", "slow_limit")? {
1659 sweep.slow_limit = (v, v, 0.0);
1660 } else {
1661 if let Some(v) = get_f64(params, "mama", "slow_limit_start")? {
1662 sweep.slow_limit.0 = v;
1663 }
1664 if let Some(v) = get_f64(params, "mama", "slow_limit_end")? {
1665 sweep.slow_limit.1 = v;
1666 }
1667 if let Some(v) = get_f64(params, "mama", "slow_limit_step")? {
1668 sweep.slow_limit.2 = v;
1669 }
1670 }
1671 let out = super::mama::mama_batch_with_kernel(prices, &sweep, kernel)?;
1672 Ok(MaBatchOutput {
1673 periods: (1..=out.rows).collect(),
1674 values: out.mama_values,
1675 rows: out.rows,
1676 cols: out.cols,
1677 })
1678 }
1679 "ehlers_pma" => {
1680 let periods = expand_period_axis(period_range)?;
1681 let rows = periods.len();
1682 let input = super::ehlers_pma::EhlersPmaInput::from_slice(
1683 prices,
1684 super::ehlers_pma::EhlersPmaParams::default(),
1685 );
1686 let out = super::ehlers_pma::ehlers_pma_with_kernel(&input, kernel)?;
1687 let cols = out.predict.len();
1688 let mut values = Vec::with_capacity(rows.saturating_mul(cols));
1689 for _ in 0..rows {
1690 values.extend_from_slice(&out.predict);
1691 }
1692 Ok(MaBatchOutput {
1693 periods,
1694 values,
1695 rows,
1696 cols,
1697 })
1698 }
1699 "ehlers_undersampled_double_moving_average" => {
1700 let mut sweep =
1701 super::ehlers_undersampled_double_moving_average::EhlersUndersampledDoubleMovingAverageBatchRange::default();
1702 if let Some(v) = get_usize(
1703 params,
1704 "ehlers_undersampled_double_moving_average",
1705 "fast_length",
1706 )? {
1707 sweep.fast_length = (v, v, 0);
1708 } else {
1709 if let Some(v) = get_usize(
1710 params,
1711 "ehlers_undersampled_double_moving_average",
1712 "fast_length_start",
1713 )? {
1714 sweep.fast_length.0 = v;
1715 }
1716 if let Some(v) = get_usize(
1717 params,
1718 "ehlers_undersampled_double_moving_average",
1719 "fast_length_end",
1720 )? {
1721 sweep.fast_length.1 = v;
1722 }
1723 if let Some(v) = get_usize(
1724 params,
1725 "ehlers_undersampled_double_moving_average",
1726 "fast_length_step",
1727 )? {
1728 sweep.fast_length.2 = v;
1729 }
1730 }
1731 if let Some(v) = get_usize(
1732 params,
1733 "ehlers_undersampled_double_moving_average",
1734 "slow_length",
1735 )? {
1736 sweep.slow_length = (v, v, 0);
1737 } else {
1738 if let Some(v) = get_usize(
1739 params,
1740 "ehlers_undersampled_double_moving_average",
1741 "slow_length_start",
1742 )? {
1743 sweep.slow_length.0 = v;
1744 }
1745 if let Some(v) = get_usize(
1746 params,
1747 "ehlers_undersampled_double_moving_average",
1748 "slow_length_end",
1749 )? {
1750 sweep.slow_length.1 = v;
1751 }
1752 if let Some(v) = get_usize(
1753 params,
1754 "ehlers_undersampled_double_moving_average",
1755 "slow_length_step",
1756 )? {
1757 sweep.slow_length.2 = v;
1758 }
1759 }
1760 if let Some(v) = get_usize(
1761 params,
1762 "ehlers_undersampled_double_moving_average",
1763 "sample_length",
1764 )? {
1765 sweep.sample_length = (v, v, 0);
1766 } else {
1767 if let Some(v) = get_usize(
1768 params,
1769 "ehlers_undersampled_double_moving_average",
1770 "sample_length_start",
1771 )? {
1772 sweep.sample_length.0 = v;
1773 }
1774 if let Some(v) = get_usize(
1775 params,
1776 "ehlers_undersampled_double_moving_average",
1777 "sample_length_end",
1778 )? {
1779 sweep.sample_length.1 = v;
1780 }
1781 if let Some(v) = get_usize(
1782 params,
1783 "ehlers_undersampled_double_moving_average",
1784 "sample_length_step",
1785 )? {
1786 sweep.sample_length.2 = v;
1787 }
1788 }
1789 let out =
1790 super::ehlers_undersampled_double_moving_average::ehlers_undersampled_double_moving_average_batch_with_kernel(
1791 prices, &sweep, kernel,
1792 )?;
1793 Ok(MaBatchOutput {
1794 periods: (1..=out.rows).collect(),
1795 values: out.fast_values,
1796 rows: out.rows,
1797 cols: out.cols,
1798 })
1799 }
1800 "mwdx" => {
1801 let mut sweep = super::mwdx::MwdxBatchRange::default();
1802 if let Some(v) = get_f64(params, "mwdx", "factor")? {
1803 sweep.factor = (v, v, 0.0);
1804 } else {
1805 let fac_start = 2.0 / (period_range.0 as f64 + 1.0);
1806 let fac_end = 2.0 / (period_range.1 as f64 + 1.0);
1807 let next_period = if period_range.2 == 0 || period_range.0 == period_range.1 {
1808 period_range.0
1809 } else if period_range.0 < period_range.1 {
1810 period_range.0.saturating_add(period_range.2)
1811 } else {
1812 period_range.0.saturating_sub(period_range.2)
1813 };
1814 let fac_next = 2.0 / (next_period as f64 + 1.0);
1815 let fac_step = (fac_next - fac_start).abs();
1816 sweep.factor = (fac_start, fac_end, fac_step);
1817 }
1818 let out = super::mwdx::mwdx_batch_with_kernel(prices, &sweep, kernel)?;
1819 Ok(MaBatchOutput {
1820 periods: map_periods(&out.combos, |p| {
1821 let f = p.factor.unwrap_or(0.2);
1822 if f > 0.0 {
1823 ((2.0 / f) - 1.0).round().max(1.0) as usize
1824 } else {
1825 1
1826 }
1827 }),
1828 values: out.values,
1829 rows: out.rows,
1830 cols: out.cols,
1831 })
1832 }
1833 "vwap" => {
1834 let candles =
1835 candles.ok_or(MaBatchDispatchError::RequiresCandles { indicator: "vwap" })?;
1836 let sweep = super::vwap::VwapBatchRange {
1837 anchor: ("1d".to_string(), "1d".to_string(), 0),
1838 };
1839 let out = super::vwap::vwap_batch_with_kernel(
1840 &candles.timestamp,
1841 &candles.volume,
1842 prices,
1843 &sweep,
1844 kernel,
1845 )?;
1846 let periods = out
1847 .combos
1848 .iter()
1849 .enumerate()
1850 .map(|(i, p)| {
1851 p.anchor
1852 .as_deref()
1853 .and_then(|a| super::vwap::parse_anchor(a).ok().map(|(n, _)| n as usize))
1854 .unwrap_or(i + 1)
1855 })
1856 .collect();
1857 Ok(MaBatchOutput {
1858 periods,
1859 values: out.values,
1860 rows: out.rows,
1861 cols: out.cols,
1862 })
1863 }
1864 "dma" => {
1865 let ema_length = get_usize(params, "dma", "ema_length")?.unwrap_or(20);
1866 let ema_gain_limit = get_usize(params, "dma", "ema_gain_limit")?.unwrap_or(50);
1867 let sweep = super::dma::DmaBatchRange {
1868 hull_length: period_range,
1869 ema_length: (ema_length, ema_length, 0),
1870 ema_gain_limit: (ema_gain_limit, ema_gain_limit, 0),
1871 hull_ma_type: "WMA".to_string(),
1872 };
1873 let out = super::dma::dma_batch_with_kernel(prices, &sweep, kernel)?;
1874 Ok(MaBatchOutput {
1875 periods: map_periods(&out.combos, |p| p.hull_length.unwrap_or(7)),
1876 values: out.values,
1877 rows: out.rows,
1878 cols: out.cols,
1879 })
1880 }
1881 "epma" => {
1882 let offset = get_usize(params, "epma", "offset")?.unwrap_or(4);
1883 let sweep = super::epma::EpmaBatchRange {
1884 period: period_range,
1885 offset: (offset, offset, 0),
1886 };
1887 let out = super::epma::epma_batch_with_kernel(prices, &sweep, kernel)?;
1888 Ok(MaBatchOutput {
1889 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1890 values: out.values,
1891 rows: out.rows,
1892 cols: out.cols,
1893 })
1894 }
1895 "sama" => {
1896 let mut sweep = super::sama::SamaBatchRange::default();
1897 sweep.length = period_range;
1898 if let Some(v) = get_usize(params, "sama", "maj_length")? {
1899 sweep.maj_length = (v, v, 0);
1900 }
1901 if let Some(v) = get_usize(params, "sama", "min_length")? {
1902 sweep.min_length = (v, v, 0);
1903 }
1904 let out = super::sama::sama_batch_with_kernel(prices, &sweep, kernel)?;
1905 Ok(MaBatchOutput {
1906 periods: map_periods(&out.combos, |p| p.length.unwrap_or(10)),
1907 values: out.values,
1908 rows: out.rows,
1909 cols: out.cols,
1910 })
1911 }
1912 "volatility_adjusted_ma" | "vama" => {
1913 let vol_period = get_usize(params, "vama", "vol_period")?.unwrap_or(51);
1914 let sweep = super::volatility_adjusted_ma::VamaBatchRange {
1915 base_period: period_range,
1916 vol_period: (vol_period, vol_period, 0),
1917 };
1918 let out =
1919 super::volatility_adjusted_ma::vama_batch_with_kernel(prices, &sweep, kernel)?;
1920 Ok(MaBatchOutput {
1921 periods: map_periods(&out.combos, |p| p.base_period.unwrap_or(10)),
1922 values: out.values,
1923 rows: out.rows,
1924 cols: out.cols,
1925 })
1926 }
1927 "maaq" => {
1928 let mut sweep = super::maaq::MaaqBatchRange::default();
1929 sweep.period = period_range;
1930 if let Some(v) = get_usize(params, "maaq", "fast_period")? {
1931 sweep.fast_period = (v, v, 0);
1932 }
1933 if let Some(v) = get_usize(params, "maaq", "slow_period")? {
1934 sweep.slow_period = (v, v, 0);
1935 }
1936 let out = super::maaq::maaq_batch_with_kernel(prices, &sweep, kernel)?;
1937 Ok(MaBatchOutput {
1938 periods: map_periods(&out.combos, |p| p.period.unwrap_or(9)),
1939 values: out.values,
1940 rows: out.rows,
1941 cols: out.cols,
1942 })
1943 }
1944 "frama" => {
1945 let sc = get_usize(params, "frama", "sc")?.unwrap_or(300);
1946 let fc = get_usize(params, "frama", "fc")?.unwrap_or(1);
1947 let (high, low, close) = match candles {
1948 Some(c) => (&c.high[..], &c.low[..], &c.close[..]),
1949 None => (prices, prices, prices),
1950 };
1951 let sweep = super::frama::FramaBatchRange {
1952 window: period_range,
1953 sc: (sc, sc, 0),
1954 fc: (fc, fc, 0),
1955 };
1956 let out = super::frama::frama_batch_with_kernel(high, low, close, &sweep, kernel)?;
1957 Ok(MaBatchOutput {
1958 periods: map_periods(&out.combos, |p| p.window.unwrap_or(10)),
1959 values: out.values,
1960 rows: out.rows,
1961 cols: out.cols,
1962 })
1963 }
1964 "buff_averages" => {
1965 let candles = candles.ok_or(MaBatchDispatchError::RequiresCandles {
1966 indicator: "buff_averages",
1967 })?;
1968 let mut sweep = super::buff_averages::BuffAveragesBatchRange::default();
1969 sweep.slow_period = period_range;
1970
1971 if let Some(v) = get_usize(params, "buff_averages", "fast_period")? {
1972 sweep.fast_period = (v, v, 0);
1973 }
1974 if let Some(v) = get_usize(params, "buff_averages", "slow_period")? {
1975 sweep.slow_period = (v, v, 0);
1976 }
1977 if let Some(v) = get_usize(params, "buff_averages", "fast_period_start")? {
1978 sweep.fast_period.0 = v;
1979 }
1980 if let Some(v) = get_usize(params, "buff_averages", "fast_period_end")? {
1981 sweep.fast_period.1 = v;
1982 }
1983 if let Some(v) = get_usize(params, "buff_averages", "fast_period_step")? {
1984 sweep.fast_period.2 = v;
1985 }
1986 if let Some(v) = get_usize(params, "buff_averages", "slow_period_start")? {
1987 sweep.slow_period.0 = v;
1988 }
1989 if let Some(v) = get_usize(params, "buff_averages", "slow_period_end")? {
1990 sweep.slow_period.1 = v;
1991 }
1992 if let Some(v) = get_usize(params, "buff_averages", "slow_period_step")? {
1993 sweep.slow_period.2 = v;
1994 }
1995
1996 let out = super::buff_averages::buff_averages_batch_with_kernel(
1997 prices,
1998 &candles.volume,
1999 &sweep,
2000 kernel,
2001 )?;
2002
2003 let all_fast_same = out
2004 .combos
2005 .first()
2006 .map(|c| out.combos.iter().all(|x| x.0 == c.0))
2007 .unwrap_or(true);
2008 let all_slow_same = out
2009 .combos
2010 .first()
2011 .map(|c| out.combos.iter().all(|x| x.1 == c.1))
2012 .unwrap_or(true);
2013 let periods = if all_fast_same {
2014 out.combos.iter().map(|c| c.1).collect()
2015 } else if all_slow_same {
2016 out.combos.iter().map(|c| c.0).collect()
2017 } else {
2018 (1..=out.rows).collect()
2019 };
2020
2021 Ok(MaBatchOutput {
2022 periods,
2023 values: out.fast,
2024 rows: out.rows,
2025 cols: out.cols,
2026 })
2027 }
2028 other => Err(MaBatchDispatchError::UnknownType {
2029 ma_type: other.to_string(),
2030 }
2031 .into()),
2032 }
2033}
2034
2035#[cfg(test)]
2036mod tests {
2037 use super::*;
2038 use crate::indicators::moving_averages::ma::ma_with_kernel;
2039 use crate::utilities::data_loader::Candles;
2040 use crate::utilities::enums::Kernel;
2041
2042 fn sample_prices(len: usize) -> Vec<f64> {
2043 (0..len)
2044 .map(|i| ((i as f64) * 0.1).sin() + (i as f64) * 0.001 + 100.0)
2045 .collect()
2046 }
2047
2048 fn sample_candles(len: usize) -> Candles {
2049 let timestamp: Vec<i64> = (0..len)
2050 .map(|i| 1_700_000_000_000_i64 + (i as i64) * 60_000)
2051 .collect();
2052 let close = sample_prices(len);
2053 let open: Vec<f64> = close.iter().map(|v| v - 0.1).collect();
2054 let high: Vec<f64> = close
2055 .iter()
2056 .enumerate()
2057 .map(|(i, v)| v + 0.35 + ((i as f64) * 0.01).sin().abs())
2058 .collect();
2059 let low: Vec<f64> = close
2060 .iter()
2061 .enumerate()
2062 .map(|(i, v)| v - 0.35 - ((i as f64) * 0.01).sin().abs())
2063 .collect();
2064 let volume: Vec<f64> = (0..len)
2065 .map(|i| 1000.0 + ((i % 31) as f64) * 7.0 + (i as f64) * 0.1)
2066 .collect();
2067 Candles::new(timestamp, open, high, low, close, volume)
2068 }
2069
2070 fn assert_series_eq(a: &[f64], b: &[f64], tol: f64) {
2071 assert_eq!(a.len(), b.len());
2072 for (i, (&av, &bv)) in a.iter().zip(b.iter()).enumerate() {
2073 if av.is_nan() && bv.is_nan() {
2074 continue;
2075 }
2076 let d = (av - bv).abs();
2077 assert!(
2078 d <= tol,
2079 "series mismatch at index {i}: left={av}, right={bv}, abs_diff={d}"
2080 );
2081 }
2082 }
2083
2084 fn assert_series_eq_ctx(a: &[f64], b: &[f64], tol: f64, ctx: &str) {
2085 assert_eq!(a.len(), b.len(), "length mismatch for {ctx}");
2086 for (i, (&av, &bv)) in a.iter().zip(b.iter()).enumerate() {
2087 if av.is_nan() && bv.is_nan() {
2088 continue;
2089 }
2090 let d = (av - bv).abs();
2091 assert!(
2092 d <= tol,
2093 "series mismatch for {ctx} at index {i}: left={av}, right={bv}, abs_diff={d}"
2094 );
2095 }
2096 }
2097
2098 #[test]
2099 fn period_based_batch_matches_single_direct_for_many_ids() {
2100 let prices = sample_prices(320);
2101 let candles = sample_candles(320);
2102 let period_range = (18, 22, 2);
2103 let expected_periods = vec![18, 20, 22];
2104
2105 let slice_cases = [
2106 "sma",
2107 "ema",
2108 "dema",
2109 "tema",
2110 "smma",
2111 "zlema",
2112 "wma",
2113 "alma",
2114 "cwma",
2115 "corrected_moving_average",
2116 "cora_wave",
2117 "edcf",
2118 "fwma",
2119 "gaussian",
2120 "highpass",
2121 "highpass_2_pole",
2122 "hma",
2123 "jma",
2124 "jsa",
2125 "linreg",
2126 "kama",
2127 "ehlers_kama",
2128 "ehlers_ecema",
2129 "ehma",
2130 "nama",
2131 "nma",
2132 "pwma",
2133 "reflex",
2134 "sinwma",
2135 "sqwma",
2136 "srwma",
2137 "sgf",
2138 "swma",
2139 "supersmoother",
2140 "supersmoother_3_pole",
2141 "tilson",
2142 "trendflex",
2143 "corrected_moving_average",
2144 "ema_deviation_corrected_t3",
2145 "wave_smoother",
2146 "trima",
2147 "wilders",
2148 "epma",
2149 "sama",
2150 ];
2151
2152 for ma_type in slice_cases {
2153 let batch =
2154 ma_batch_with_kernel(ma_type, MaData::Slice(&prices), period_range, Kernel::Auto)
2155 .unwrap();
2156
2157 assert_eq!(batch.periods, expected_periods);
2158 assert_eq!(batch.rows, expected_periods.len());
2159 assert_eq!(batch.cols, prices.len());
2160
2161 for (row, period) in expected_periods.iter().copied().enumerate() {
2162 let direct =
2163 ma_with_kernel(ma_type, MaData::Slice(&prices), period, Kernel::Auto).unwrap();
2164 let start = row * batch.cols;
2165 let end = start + batch.cols;
2166 let ctx = format!("{ma_type} slice period={period}");
2167 assert_series_eq_ctx(&batch.values[start..end], &direct, 1e-10, &ctx);
2168 }
2169 }
2170
2171 let candle_cases = ["vpwma", "vwma", "frama"];
2172 for ma_type in candle_cases {
2173 let batch = ma_batch_with_kernel(
2174 ma_type,
2175 MaData::Candles {
2176 candles: &candles,
2177 source: "close",
2178 },
2179 period_range,
2180 Kernel::Auto,
2181 )
2182 .unwrap();
2183
2184 assert_eq!(batch.periods, expected_periods);
2185 assert_eq!(batch.rows, expected_periods.len());
2186 assert_eq!(batch.cols, candles.close.len());
2187
2188 for (row, period) in expected_periods.iter().copied().enumerate() {
2189 let direct = ma_with_kernel(
2190 ma_type,
2191 MaData::Candles {
2192 candles: &candles,
2193 source: "close",
2194 },
2195 period,
2196 Kernel::Auto,
2197 )
2198 .unwrap();
2199 let start = row * batch.cols;
2200 let end = start + batch.cols;
2201 let ctx = format!("{ma_type} candles period={period}");
2202 assert_series_eq_ctx(&batch.values[start..end], &direct, 1e-10, &ctx);
2203 }
2204 }
2205 }
2206
2207 #[test]
2208 fn mama_typed_output_selection_matches_direct() {
2209 let prices = sample_prices(256);
2210 let data = MaData::Slice(&prices);
2211 let params = [
2212 MaBatchParamKV {
2213 key: "fast_limit",
2214 value: MaBatchParamValue::Float(0.35),
2215 },
2216 MaBatchParamKV {
2217 key: "slow_limit",
2218 value: MaBatchParamValue::Float(0.06),
2219 },
2220 MaBatchParamKV {
2221 key: "output",
2222 value: MaBatchParamValue::EnumString("fama"),
2223 },
2224 ];
2225
2226 let got =
2227 ma_batch_with_kernel_and_typed_params("mama", data, (10, 10, 0), Kernel::Auto, ¶ms)
2228 .unwrap();
2229
2230 let direct = crate::indicators::moving_averages::mama::mama_batch_with_kernel(
2231 &prices,
2232 &crate::indicators::moving_averages::mama::MamaBatchRange {
2233 fast_limit: (0.35, 0.35, 0.0),
2234 slow_limit: (0.06, 0.06, 0.0),
2235 },
2236 Kernel::Auto,
2237 )
2238 .unwrap();
2239
2240 assert_eq!(got.rows, direct.rows);
2241 assert_eq!(got.cols, direct.cols);
2242 assert_eq!(got.periods, (1..=direct.rows).collect::<Vec<_>>());
2243 assert_series_eq(&got.values, &direct.fama_values, 1e-12);
2244 }
2245
2246 #[test]
2247 fn ehlers_pma_typed_output_selection_matches_direct() {
2248 let prices = sample_prices(300);
2249 let data = MaData::Slice(&prices);
2250 let params = [MaBatchParamKV {
2251 key: "output",
2252 value: MaBatchParamValue::EnumString("trigger"),
2253 }];
2254
2255 let got = ma_batch_with_kernel_and_typed_params(
2256 "ehlers_pma",
2257 data,
2258 (8, 10, 1),
2259 Kernel::Auto,
2260 ¶ms,
2261 )
2262 .unwrap();
2263
2264 let input = crate::indicators::moving_averages::ehlers_pma::EhlersPmaInput::from_slice(
2265 &prices,
2266 crate::indicators::moving_averages::ehlers_pma::EhlersPmaParams::default(),
2267 );
2268 let direct = crate::indicators::moving_averages::ehlers_pma::ehlers_pma_with_kernel(
2269 &input,
2270 Kernel::Auto,
2271 )
2272 .unwrap();
2273
2274 assert_eq!(got.rows, 3);
2275 assert_eq!(got.cols, prices.len());
2276 assert_eq!(got.periods, vec![8, 9, 10]);
2277 for row in 0..got.rows {
2278 let start = row * got.cols;
2279 let end = start + got.cols;
2280 assert_series_eq(&got.values[start..end], &direct.trigger, 1e-12);
2281 }
2282 }
2283
2284 #[test]
2285 fn invalid_output_selection_returns_error() {
2286 let prices = sample_prices(256);
2287 let data = MaData::Slice(&prices);
2288 let params = [MaBatchParamKV {
2289 key: "output",
2290 value: MaBatchParamValue::EnumString("bad_line"),
2291 }];
2292
2293 let err =
2294 ma_batch_with_kernel_and_typed_params("mama", data, (10, 10, 0), Kernel::Auto, ¶ms)
2295 .unwrap_err()
2296 .to_string();
2297
2298 assert!(err.contains("expected 'mama' or 'fama'"));
2299 }
2300
2301 #[test]
2302 fn mama_numeric_path_defaults_to_primary_output() {
2303 let prices = sample_prices(256);
2304 let mut params = HashMap::new();
2305 params.insert("fast_limit".to_string(), 0.4);
2306 params.insert("slow_limit".to_string(), 0.07);
2307
2308 let got = ma_batch_with_kernel_and_params(
2309 "mama",
2310 MaData::Slice(&prices),
2311 (12, 12, 0),
2312 Kernel::Auto,
2313 Some(¶ms),
2314 )
2315 .unwrap();
2316
2317 let direct = crate::indicators::moving_averages::mama::mama_batch_with_kernel(
2318 &prices,
2319 &crate::indicators::moving_averages::mama::MamaBatchRange {
2320 fast_limit: (0.4, 0.4, 0.0),
2321 slow_limit: (0.07, 0.07, 0.0),
2322 },
2323 Kernel::Auto,
2324 )
2325 .unwrap();
2326
2327 assert_eq!(got.rows, direct.rows);
2328 assert_eq!(got.cols, direct.cols);
2329 assert_series_eq(&got.values, &direct.mama_values, 1e-12);
2330 }
2331
2332 #[test]
2333 fn ehlers_pma_numeric_path_defaults_and_descending_periods() {
2334 let prices = sample_prices(300);
2335 let got = ma_batch_with_kernel_and_params(
2336 "ehlers_pma",
2337 MaData::Slice(&prices),
2338 (10, 8, 1),
2339 Kernel::Auto,
2340 None,
2341 )
2342 .unwrap();
2343
2344 let input = crate::indicators::moving_averages::ehlers_pma::EhlersPmaInput::from_slice(
2345 &prices,
2346 crate::indicators::moving_averages::ehlers_pma::EhlersPmaParams::default(),
2347 );
2348 let direct = crate::indicators::moving_averages::ehlers_pma::ehlers_pma_with_kernel(
2349 &input,
2350 Kernel::Auto,
2351 )
2352 .unwrap();
2353
2354 assert_eq!(got.periods, vec![10, 9, 8]);
2355 assert_eq!(got.rows, 3);
2356 assert_eq!(got.cols, prices.len());
2357 for row in 0..got.rows {
2358 let start = row * got.cols;
2359 let end = start + got.cols;
2360 assert_series_eq(&got.values[start..end], &direct.predict, 1e-12);
2361 }
2362 }
2363
2364 #[test]
2365 fn hwma_typed_params_match_direct() {
2366 let prices = sample_prices(256);
2367 let params = [
2368 MaBatchParamKV {
2369 key: "na",
2370 value: MaBatchParamValue::Float(0.23),
2371 },
2372 MaBatchParamKV {
2373 key: "nb",
2374 value: MaBatchParamValue::Float(0.11),
2375 },
2376 MaBatchParamKV {
2377 key: "nc",
2378 value: MaBatchParamValue::Float(0.17),
2379 },
2380 ];
2381 let got = ma_batch_with_kernel_and_typed_params(
2382 "hwma",
2383 MaData::Slice(&prices),
2384 (10, 10, 0),
2385 Kernel::Auto,
2386 ¶ms,
2387 )
2388 .unwrap();
2389 let direct = crate::indicators::moving_averages::hwma::hwma_batch_with_kernel(
2390 &prices,
2391 &crate::indicators::moving_averages::hwma::HwmaBatchRange {
2392 na: (0.23, 0.23, 0.0),
2393 nb: (0.11, 0.11, 0.0),
2394 nc: (0.17, 0.17, 0.0),
2395 },
2396 Kernel::Auto,
2397 )
2398 .unwrap();
2399 assert_eq!(got.rows, direct.rows);
2400 assert_eq!(got.cols, direct.cols);
2401 assert_series_eq(&got.values, &direct.values, 1e-12);
2402 }
2403
2404 #[test]
2405 fn mwdx_typed_factor_matches_direct() {
2406 let prices = sample_prices(256);
2407 let params = [MaBatchParamKV {
2408 key: "factor",
2409 value: MaBatchParamValue::Float(2.0 / 11.0),
2410 }];
2411 let got = ma_batch_with_kernel_and_typed_params(
2412 "mwdx",
2413 MaData::Slice(&prices),
2414 (10, 10, 0),
2415 Kernel::Auto,
2416 ¶ms,
2417 )
2418 .unwrap();
2419 let direct = crate::indicators::moving_averages::mwdx::mwdx_batch_with_kernel(
2420 &prices,
2421 &crate::indicators::moving_averages::mwdx::MwdxBatchRange {
2422 factor: (2.0 / 11.0, 2.0 / 11.0, 0.0),
2423 },
2424 Kernel::Auto,
2425 )
2426 .unwrap();
2427 assert_eq!(got.rows, direct.rows);
2428 assert_eq!(got.cols, direct.cols);
2429 assert_series_eq(&got.values, &direct.values, 1e-12);
2430 }
2431
2432 #[test]
2433 fn uma_typed_params_match_direct() {
2434 let prices = sample_prices(256);
2435 let params = [
2436 MaBatchParamKV {
2437 key: "accelerator",
2438 value: MaBatchParamValue::Float(1.0),
2439 },
2440 MaBatchParamKV {
2441 key: "min_length",
2442 value: MaBatchParamValue::Int(5),
2443 },
2444 MaBatchParamKV {
2445 key: "max_length",
2446 value: MaBatchParamValue::Int(35),
2447 },
2448 MaBatchParamKV {
2449 key: "smooth_length",
2450 value: MaBatchParamValue::Int(4),
2451 },
2452 ];
2453 let got = ma_batch_with_kernel_and_typed_params(
2454 "uma",
2455 MaData::Slice(&prices),
2456 (35, 35, 0),
2457 Kernel::Auto,
2458 ¶ms,
2459 )
2460 .unwrap();
2461 let direct = crate::indicators::moving_averages::uma::uma_batch_with_kernel(
2462 &prices,
2463 None,
2464 &crate::indicators::moving_averages::uma::UmaBatchRange {
2465 accelerator: (1.0, 1.0, 0.0),
2466 min_length: (5, 5, 0),
2467 max_length: (35, 35, 0),
2468 smooth_length: (4, 4, 0),
2469 },
2470 Kernel::Auto,
2471 )
2472 .unwrap();
2473 assert_eq!(got.rows, direct.rows);
2474 assert_eq!(got.cols, direct.cols);
2475 assert_series_eq(&got.values, &direct.values, 1e-12);
2476 }
2477
2478 #[test]
2479 fn tradjema_typed_params_match_direct() {
2480 let candles = sample_candles(300);
2481 let params = [MaBatchParamKV {
2482 key: "mult",
2483 value: MaBatchParamValue::Float(2.3),
2484 }];
2485 let got = ma_batch_with_kernel_and_typed_params(
2486 "tradjema",
2487 MaData::Candles {
2488 candles: &candles,
2489 source: "close",
2490 },
2491 (40, 40, 0),
2492 Kernel::Auto,
2493 ¶ms,
2494 )
2495 .unwrap();
2496 let direct = crate::indicators::moving_averages::tradjema::tradjema_batch_with_kernel(
2497 &candles.high,
2498 &candles.low,
2499 &candles.close,
2500 &crate::indicators::moving_averages::tradjema::TradjemaBatchRange {
2501 length: (40, 40, 0),
2502 mult: (2.3, 2.3, 0.0),
2503 },
2504 Kernel::Auto,
2505 )
2506 .unwrap();
2507 assert_eq!(got.rows, direct.rows);
2508 assert_eq!(got.cols, direct.cols);
2509 assert_series_eq(&got.values, &direct.values, 1e-12);
2510 }
2511
2512 #[test]
2513 fn volume_adjusted_ma_typed_params_match_direct() {
2514 let candles = sample_candles(300);
2515 let params = [
2516 MaBatchParamKV {
2517 key: "vi_factor",
2518 value: MaBatchParamValue::Float(2.0),
2519 },
2520 MaBatchParamKV {
2521 key: "sample_period",
2522 value: MaBatchParamValue::Int(30),
2523 },
2524 MaBatchParamKV {
2525 key: "strict",
2526 value: MaBatchParamValue::Bool(true),
2527 },
2528 ];
2529 let got = ma_batch_with_kernel_and_typed_params(
2530 "volume_adjusted_ma",
2531 MaData::Candles {
2532 candles: &candles,
2533 source: "close",
2534 },
2535 (20, 20, 0),
2536 Kernel::Auto,
2537 ¶ms,
2538 )
2539 .unwrap();
2540 let direct =
2541 crate::indicators::moving_averages::volume_adjusted_ma::VolumeAdjustedMa_batch_with_kernel(
2542 &candles.close,
2543 &candles.volume,
2544 &crate::indicators::moving_averages::volume_adjusted_ma::VolumeAdjustedMaBatchRange {
2545 length: (20, 20, 0),
2546 vi_factor: (2.0, 2.0, 0.0),
2547 sample_period: (30, 30, 0),
2548 strict: Some(true),
2549 },
2550 Kernel::Auto,
2551 )
2552 .unwrap();
2553 assert_eq!(got.rows, direct.rows);
2554 assert_eq!(got.cols, direct.cols);
2555 assert_series_eq(&got.values, &direct.values, 1e-12);
2556 }
2557
2558 #[test]
2559 fn vwap_typed_anchor_matches_direct() {
2560 let candles = sample_candles(300);
2561 let params = [MaBatchParamKV {
2562 key: "anchor",
2563 value: MaBatchParamValue::EnumString("1d"),
2564 }];
2565 let got = ma_batch_with_kernel_and_typed_params(
2566 "vwap",
2567 MaData::Candles {
2568 candles: &candles,
2569 source: "close",
2570 },
2571 (10, 10, 0),
2572 Kernel::Auto,
2573 ¶ms,
2574 )
2575 .unwrap();
2576 let direct = crate::indicators::moving_averages::vwap::vwap_batch_with_kernel(
2577 &candles.timestamp,
2578 &candles.volume,
2579 &candles.close,
2580 &crate::indicators::moving_averages::vwap::VwapBatchRange {
2581 anchor: ("1d".to_string(), "1d".to_string(), 0),
2582 },
2583 Kernel::Auto,
2584 )
2585 .unwrap();
2586 assert_eq!(got.rows, direct.rows);
2587 assert_eq!(got.cols, direct.cols);
2588 assert_series_eq(&got.values, &direct.values, 1e-12);
2589 }
2590
2591 #[test]
2592 fn dma_typed_hull_ma_type_matches_direct() {
2593 let prices = sample_prices(256);
2594 let params = [
2595 MaBatchParamKV {
2596 key: "ema_length",
2597 value: MaBatchParamValue::Int(20),
2598 },
2599 MaBatchParamKV {
2600 key: "ema_gain_limit",
2601 value: MaBatchParamValue::Int(50),
2602 },
2603 MaBatchParamKV {
2604 key: "hull_ma_type",
2605 value: MaBatchParamValue::EnumString("EMA"),
2606 },
2607 ];
2608 let got = ma_batch_with_kernel_and_typed_params(
2609 "dma",
2610 MaData::Slice(&prices),
2611 (14, 14, 0),
2612 Kernel::Auto,
2613 ¶ms,
2614 )
2615 .unwrap();
2616 let direct = crate::indicators::moving_averages::dma::dma_batch_with_kernel(
2617 &prices,
2618 &crate::indicators::moving_averages::dma::DmaBatchRange {
2619 hull_length: (14, 14, 0),
2620 ema_length: (20, 20, 0),
2621 ema_gain_limit: (50, 50, 0),
2622 hull_ma_type: "EMA".to_string(),
2623 },
2624 Kernel::Auto,
2625 )
2626 .unwrap();
2627 assert_eq!(got.rows, direct.rows);
2628 assert_eq!(got.cols, direct.cols);
2629 assert_series_eq(&got.values, &direct.values, 1e-12);
2630 }
2631
2632 #[test]
2633 fn ehlers_itrend_typed_params_match_direct() {
2634 let prices = sample_prices(320);
2635 let params = [MaBatchParamKV {
2636 key: "warmup_bars",
2637 value: MaBatchParamValue::Int(30),
2638 }];
2639 let got = ma_batch_with_kernel_and_typed_params(
2640 "ehlers_itrend",
2641 MaData::Slice(&prices),
2642 (48, 48, 0),
2643 Kernel::Auto,
2644 ¶ms,
2645 )
2646 .unwrap();
2647 let direct =
2648 crate::indicators::moving_averages::ehlers_itrend::ehlers_itrend_batch_with_kernel(
2649 &prices,
2650 &crate::indicators::moving_averages::ehlers_itrend::EhlersITrendBatchRange {
2651 warmup_bars: (30, 30, 0),
2652 max_dc_period: (48, 48, 0),
2653 },
2654 Kernel::Auto,
2655 )
2656 .unwrap();
2657 assert_eq!(got.rows, direct.rows);
2658 assert_eq!(got.cols, direct.cols);
2659 assert_series_eq(&got.values, &direct.values, 1e-12);
2660 }
2661
2662 #[test]
2663 fn vama_typed_params_match_direct() {
2664 let prices = sample_prices(320);
2665 let params = [MaBatchParamKV {
2666 key: "vol_period",
2667 value: MaBatchParamValue::Int(51),
2668 }];
2669 let got = ma_batch_with_kernel_and_typed_params(
2670 "vama",
2671 MaData::Slice(&prices),
2672 (18, 22, 2),
2673 Kernel::Auto,
2674 ¶ms,
2675 )
2676 .unwrap();
2677 let direct =
2678 crate::indicators::moving_averages::volatility_adjusted_ma::vama_batch_with_kernel(
2679 &prices,
2680 &crate::indicators::moving_averages::volatility_adjusted_ma::VamaBatchRange {
2681 base_period: (18, 22, 2),
2682 vol_period: (51, 51, 0),
2683 },
2684 Kernel::Auto,
2685 )
2686 .unwrap();
2687 assert_eq!(got.rows, direct.rows);
2688 assert_eq!(got.cols, direct.cols);
2689 assert_series_eq(&got.values, &direct.values, 1e-12);
2690 }
2691
2692 #[test]
2693 fn maaq_typed_params_match_direct() {
2694 let prices = sample_prices(320);
2695 let params = [
2696 MaBatchParamKV {
2697 key: "fast_period",
2698 value: MaBatchParamValue::Int(2),
2699 },
2700 MaBatchParamKV {
2701 key: "slow_period",
2702 value: MaBatchParamValue::Int(30),
2703 },
2704 ];
2705 let got = ma_batch_with_kernel_and_typed_params(
2706 "maaq",
2707 MaData::Slice(&prices),
2708 (18, 22, 2),
2709 Kernel::Auto,
2710 ¶ms,
2711 )
2712 .unwrap();
2713 let direct = crate::indicators::moving_averages::maaq::maaq_batch_with_kernel(
2714 &prices,
2715 &crate::indicators::moving_averages::maaq::MaaqBatchRange {
2716 period: (18, 22, 2),
2717 fast_period: (2, 2, 0),
2718 slow_period: (30, 30, 0),
2719 },
2720 Kernel::Auto,
2721 )
2722 .unwrap();
2723 assert_eq!(got.rows, direct.rows);
2724 assert_eq!(got.cols, direct.cols);
2725 assert_series_eq(&got.values, &direct.values, 1e-12);
2726 }
2727
2728 #[test]
2729 fn tradjema_requires_candles_error() {
2730 let prices = sample_prices(256);
2731 let err = ma_batch_with_kernel_and_typed_params(
2732 "tradjema",
2733 MaData::Slice(&prices),
2734 (40, 40, 0),
2735 Kernel::Auto,
2736 &[],
2737 )
2738 .unwrap_err()
2739 .to_string();
2740 assert!(err.contains("requires candles"));
2741 }
2742
2743 #[test]
2744 fn vwap_requires_candles_error() {
2745 let prices = sample_prices(256);
2746 let err = ma_batch_with_kernel_and_typed_params(
2747 "vwap",
2748 MaData::Slice(&prices),
2749 (10, 10, 0),
2750 Kernel::Auto,
2751 &[],
2752 )
2753 .unwrap_err()
2754 .to_string();
2755 assert!(err.contains("requires candles"));
2756 }
2757
2758 #[test]
2759 fn volume_adjusted_ma_requires_candles_error() {
2760 let prices = sample_prices(256);
2761 let err = ma_batch_with_kernel_and_typed_params(
2762 "volume_adjusted_ma",
2763 MaData::Slice(&prices),
2764 (20, 20, 0),
2765 Kernel::Auto,
2766 &[],
2767 )
2768 .unwrap_err()
2769 .to_string();
2770 assert!(err.contains("requires candles"));
2771 }
2772
2773 #[test]
2774 fn volume_adjusted_ma_invalid_strict_numeric_error() {
2775 let candles = sample_candles(300);
2776 let mut params = HashMap::new();
2777 params.insert("strict".to_string(), 2.0);
2778 let err = ma_batch_with_kernel_and_params(
2779 "volume_adjusted_ma",
2780 MaData::Candles {
2781 candles: &candles,
2782 source: "close",
2783 },
2784 (20, 20, 0),
2785 Kernel::Auto,
2786 Some(¶ms),
2787 )
2788 .unwrap_err()
2789 .to_string();
2790 assert!(err.contains("expected 0 or 1"));
2791 }
2792
2793 #[test]
2794 fn vwap_invalid_anchor_step_error() {
2795 let candles = sample_candles(300);
2796 let params = [
2797 MaBatchParamKV {
2798 key: "anchor",
2799 value: MaBatchParamValue::EnumString("1d"),
2800 },
2801 MaBatchParamKV {
2802 key: "anchor_step",
2803 value: MaBatchParamValue::Float(-1.0),
2804 },
2805 ];
2806 let err = ma_batch_with_kernel_and_typed_params(
2807 "vwap",
2808 MaData::Candles {
2809 candles: &candles,
2810 source: "close",
2811 },
2812 (10, 10, 0),
2813 Kernel::Auto,
2814 ¶ms,
2815 )
2816 .unwrap_err()
2817 .to_string();
2818 assert!(err.contains("expected >= 0"));
2819 }
2820
2821 #[test]
2822 fn ehlers_pma_invalid_output_selection_returns_error() {
2823 let prices = sample_prices(256);
2824 let params = [MaBatchParamKV {
2825 key: "output",
2826 value: MaBatchParamValue::EnumString("bad_line"),
2827 }];
2828 let err = ma_batch_with_kernel_and_typed_params(
2829 "ehlers_pma",
2830 MaData::Slice(&prices),
2831 (10, 10, 0),
2832 Kernel::Auto,
2833 ¶ms,
2834 )
2835 .unwrap_err()
2836 .to_string();
2837 assert!(err.contains("expected 'predict' or 'trigger'"));
2838 }
2839
2840 #[test]
2841 fn buff_averages_typed_output_selection_matches_direct() {
2842 let candles = sample_candles(300);
2843 let params = [
2844 MaBatchParamKV {
2845 key: "fast_period",
2846 value: MaBatchParamValue::Int(5),
2847 },
2848 MaBatchParamKV {
2849 key: "output",
2850 value: MaBatchParamValue::EnumString("slow"),
2851 },
2852 ];
2853 let got = ma_batch_with_kernel_and_typed_params(
2854 "buff_averages",
2855 MaData::Candles {
2856 candles: &candles,
2857 source: "close",
2858 },
2859 (20, 20, 0),
2860 Kernel::Auto,
2861 ¶ms,
2862 )
2863 .unwrap();
2864 let direct =
2865 crate::indicators::moving_averages::buff_averages::buff_averages_batch_with_kernel(
2866 &candles.close,
2867 &candles.volume,
2868 &crate::indicators::moving_averages::buff_averages::BuffAveragesBatchRange {
2869 fast_period: (5, 5, 0),
2870 slow_period: (20, 20, 0),
2871 },
2872 Kernel::Auto,
2873 )
2874 .unwrap();
2875 assert_eq!(got.rows, direct.rows);
2876 assert_eq!(got.cols, direct.cols);
2877 assert_series_eq(&got.values, &direct.slow, 1e-12);
2878 }
2879
2880 #[test]
2881 fn buff_averages_requires_candles_error() {
2882 let prices = sample_prices(256);
2883 let err = ma_batch_with_kernel_and_typed_params(
2884 "buff_averages",
2885 MaData::Slice(&prices),
2886 (20, 20, 0),
2887 Kernel::Auto,
2888 &[],
2889 )
2890 .unwrap_err()
2891 .to_string();
2892 assert!(err.contains("requires candles"));
2893 }
2894
2895 #[test]
2896 fn buff_averages_invalid_output_selection_returns_error() {
2897 let candles = sample_candles(300);
2898 let params = [MaBatchParamKV {
2899 key: "output",
2900 value: MaBatchParamValue::EnumString("bad_line"),
2901 }];
2902 let err = ma_batch_with_kernel_and_typed_params(
2903 "buff_averages",
2904 MaData::Candles {
2905 candles: &candles,
2906 source: "close",
2907 },
2908 (20, 20, 0),
2909 Kernel::Auto,
2910 ¶ms,
2911 )
2912 .unwrap_err()
2913 .to_string();
2914 assert!(err.contains("expected 'fast' or 'slow'"));
2915 }
2916
2917 #[test]
2918 fn buff_averages_numeric_params_match_direct_fast() {
2919 let candles = sample_candles(300);
2920 let mut params = HashMap::new();
2921 params.insert("fast_period_start".to_string(), 5.0);
2922 params.insert("fast_period_end".to_string(), 5.0);
2923 params.insert("fast_period_step".to_string(), 0.0);
2924 params.insert("slow_period_start".to_string(), 20.0);
2925 params.insert("slow_period_end".to_string(), 22.0);
2926 params.insert("slow_period_step".to_string(), 1.0);
2927
2928 let got = ma_batch_with_kernel_and_params(
2929 "buff_averages",
2930 MaData::Candles {
2931 candles: &candles,
2932 source: "close",
2933 },
2934 (20, 22, 1),
2935 Kernel::Auto,
2936 Some(¶ms),
2937 )
2938 .unwrap();
2939
2940 let direct =
2941 crate::indicators::moving_averages::buff_averages::buff_averages_batch_with_kernel(
2942 &candles.close,
2943 &candles.volume,
2944 &crate::indicators::moving_averages::buff_averages::BuffAveragesBatchRange {
2945 fast_period: (5, 5, 0),
2946 slow_period: (20, 22, 1),
2947 },
2948 Kernel::Auto,
2949 )
2950 .unwrap();
2951
2952 assert_eq!(got.periods, vec![20, 21, 22]);
2953 assert_eq!(got.rows, direct.rows);
2954 assert_eq!(got.cols, direct.cols);
2955 assert_series_eq(&got.values, &direct.fast, 1e-12);
2956 }
2957
2958 #[test]
2959 fn typed_non_finite_float_rejected() {
2960 let prices = sample_prices(256);
2961 let params = [MaBatchParamKV {
2962 key: "offset",
2963 value: MaBatchParamValue::Float(f64::NAN),
2964 }];
2965 let err = ma_batch_with_kernel_and_typed_params(
2966 "alma",
2967 MaData::Slice(&prices),
2968 (20, 20, 0),
2969 Kernel::Auto,
2970 ¶ms,
2971 )
2972 .unwrap_err()
2973 .to_string();
2974 assert!(err.contains("expected finite number"));
2975 }
2976
2977 #[test]
2978 fn uma_typed_integer_param_rejects_fractional_value() {
2979 let prices = sample_prices(256);
2980 let params = [MaBatchParamKV {
2981 key: "min_length",
2982 value: MaBatchParamValue::Float(7.25),
2983 }];
2984 let err = ma_batch_with_kernel_and_typed_params(
2985 "uma",
2986 MaData::Slice(&prices),
2987 (35, 35, 0),
2988 Kernel::Auto,
2989 ¶ms,
2990 )
2991 .unwrap_err()
2992 .to_string();
2993 assert!(err.contains("expected integer"));
2994 }
2995
2996 #[test]
2997 fn buff_averages_typed_integer_param_rejects_fractional_value() {
2998 let candles = sample_candles(300);
2999 let params = [MaBatchParamKV {
3000 key: "fast_period",
3001 value: MaBatchParamValue::Float(5.5),
3002 }];
3003 let err = ma_batch_with_kernel_and_typed_params(
3004 "buff_averages",
3005 MaData::Candles {
3006 candles: &candles,
3007 source: "close",
3008 },
3009 (20, 20, 0),
3010 Kernel::Auto,
3011 ¶ms,
3012 )
3013 .unwrap_err()
3014 .to_string();
3015 assert!(err.contains("expected integer"));
3016 }
3017
3018 #[test]
3019 fn vwap_typed_anchor_step_rejects_fractional_value() {
3020 let candles = sample_candles(300);
3021 let params = [
3022 MaBatchParamKV {
3023 key: "anchor",
3024 value: MaBatchParamValue::EnumString("1d"),
3025 },
3026 MaBatchParamKV {
3027 key: "anchor_step",
3028 value: MaBatchParamValue::Float(1.5),
3029 },
3030 ];
3031 let err = ma_batch_with_kernel_and_typed_params(
3032 "vwap",
3033 MaData::Candles {
3034 candles: &candles,
3035 source: "close",
3036 },
3037 (10, 10, 0),
3038 Kernel::Auto,
3039 ¶ms,
3040 )
3041 .unwrap_err()
3042 .to_string();
3043 assert!(err.contains("expected integer"));
3044 }
3045
3046 #[test]
3047 fn mwdx_typed_non_finite_factor_rejected() {
3048 let prices = sample_prices(256);
3049 let params = [MaBatchParamKV {
3050 key: "factor",
3051 value: MaBatchParamValue::Float(f64::INFINITY),
3052 }];
3053 let err = ma_batch_with_kernel_and_typed_params(
3054 "mwdx",
3055 MaData::Slice(&prices),
3056 (10, 10, 0),
3057 Kernel::Auto,
3058 ¶ms,
3059 )
3060 .unwrap_err()
3061 .to_string();
3062 assert!(err.contains("expected finite number"));
3063 }
3064
3065 #[test]
3066 fn hwma_typed_non_finite_param_rejected() {
3067 let prices = sample_prices(256);
3068 let params = [MaBatchParamKV {
3069 key: "na",
3070 value: MaBatchParamValue::Float(f64::NAN),
3071 }];
3072 let err = ma_batch_with_kernel_and_typed_params(
3073 "hwma",
3074 MaData::Slice(&prices),
3075 (10, 10, 0),
3076 Kernel::Auto,
3077 ¶ms,
3078 )
3079 .unwrap_err()
3080 .to_string();
3081 assert!(err.contains("expected finite number"));
3082 }
3083
3084 #[test]
3085 fn dma_typed_fractional_ema_length_rejected() {
3086 let prices = sample_prices(256);
3087 let params = [
3088 MaBatchParamKV {
3089 key: "ema_length",
3090 value: MaBatchParamValue::Float(20.5),
3091 },
3092 MaBatchParamKV {
3093 key: "hull_ma_type",
3094 value: MaBatchParamValue::EnumString("EMA"),
3095 },
3096 ];
3097 let err = ma_batch_with_kernel_and_typed_params(
3098 "dma",
3099 MaData::Slice(&prices),
3100 (14, 14, 0),
3101 Kernel::Auto,
3102 ¶ms,
3103 )
3104 .unwrap_err()
3105 .to_string();
3106 assert!(err.contains("expected integer"));
3107 }
3108
3109 #[test]
3110 fn tradjema_typed_non_finite_mult_rejected() {
3111 let candles = sample_candles(300);
3112 let params = [MaBatchParamKV {
3113 key: "mult",
3114 value: MaBatchParamValue::Float(f64::NAN),
3115 }];
3116 let err = ma_batch_with_kernel_and_typed_params(
3117 "tradjema",
3118 MaData::Candles {
3119 candles: &candles,
3120 source: "close",
3121 },
3122 (40, 40, 0),
3123 Kernel::Auto,
3124 ¶ms,
3125 )
3126 .unwrap_err()
3127 .to_string();
3128 assert!(err.contains("expected finite number"));
3129 }
3130
3131 #[test]
3132 fn uma_typed_negative_min_length_rejected() {
3133 let prices = sample_prices(256);
3134 let params = [MaBatchParamKV {
3135 key: "min_length",
3136 value: MaBatchParamValue::Int(-1),
3137 }];
3138 let err = ma_batch_with_kernel_and_typed_params(
3139 "uma",
3140 MaData::Slice(&prices),
3141 (35, 35, 0),
3142 Kernel::Auto,
3143 ¶ms,
3144 )
3145 .unwrap_err()
3146 .to_string();
3147 assert!(err.contains("expected >= 0"));
3148 }
3149
3150 #[test]
3151 fn volume_adjusted_ma_typed_fractional_sample_period_rejected() {
3152 let candles = sample_candles(300);
3153 let params = [MaBatchParamKV {
3154 key: "sample_period",
3155 value: MaBatchParamValue::Float(30.5),
3156 }];
3157 let err = ma_batch_with_kernel_and_typed_params(
3158 "volume_adjusted_ma",
3159 MaData::Candles {
3160 candles: &candles,
3161 source: "close",
3162 },
3163 (20, 20, 0),
3164 Kernel::Auto,
3165 ¶ms,
3166 )
3167 .unwrap_err()
3168 .to_string();
3169 assert!(err.contains("expected integer"));
3170 }
3171}