1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use serde_wasm_bindgen;
16#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
17use wasm_bindgen::prelude::*;
18
19use crate::utilities::data_loader::{source_type, Candles};
20use crate::utilities::enums::Kernel;
21use crate::utilities::helpers::{alloc_with_nan_prefix, detect_best_batch_kernel};
22#[cfg(feature = "python")]
23use crate::utilities::kernel_validation::validate_kernel;
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::collections::VecDeque;
27use thiserror::Error;
28
29const DEFAULT_PERIOD: usize = 100;
30const DEFAULT_STEEPNESS: f64 = 2.5;
31const DEFAULT_MA_TYPE: &str = "ema";
32const DEFAULT_SMOOTH: usize = 10;
33const DEFAULT_MOMENTUM_WEIGHT: f64 = 1.2;
34const DEFAULT_LONG_THRESHOLD: f64 = 0.5;
35const DEFAULT_SHORT_THRESHOLD: f64 = -0.5;
36const SLOPE_LOOKBACK: usize = 10;
37const ANNUALIZATION: f64 = 252.0;
38
39#[derive(Debug, Clone)]
40pub enum LogarithmicMovingAverageData<'a> {
41 Candles {
42 candles: &'a Candles,
43 source: &'a str,
44 },
45 Slice {
46 data: &'a [f64],
47 volume: Option<&'a [f64]>,
48 },
49}
50
51#[derive(Debug, Clone)]
52pub struct LogarithmicMovingAverageOutput {
53 pub lma: Vec<f64>,
54 pub signal: Vec<f64>,
55 pub position: Vec<f64>,
56 pub momentum_confirmed: Vec<f64>,
57}
58
59#[derive(Debug, Clone)]
60#[cfg_attr(
61 all(target_arch = "wasm32", feature = "wasm"),
62 derive(Serialize, Deserialize)
63)]
64pub struct LogarithmicMovingAverageParams {
65 pub period: Option<usize>,
66 pub steepness: Option<f64>,
67 pub ma_type: Option<String>,
68 pub smooth: Option<usize>,
69 pub momentum_weight: Option<f64>,
70 pub long_threshold: Option<f64>,
71 pub short_threshold: Option<f64>,
72}
73
74impl Default for LogarithmicMovingAverageParams {
75 fn default() -> Self {
76 Self {
77 period: Some(DEFAULT_PERIOD),
78 steepness: Some(DEFAULT_STEEPNESS),
79 ma_type: Some(DEFAULT_MA_TYPE.to_string()),
80 smooth: Some(DEFAULT_SMOOTH),
81 momentum_weight: Some(DEFAULT_MOMENTUM_WEIGHT),
82 long_threshold: Some(DEFAULT_LONG_THRESHOLD),
83 short_threshold: Some(DEFAULT_SHORT_THRESHOLD),
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
89pub struct LogarithmicMovingAverageInput<'a> {
90 pub data: LogarithmicMovingAverageData<'a>,
91 pub params: LogarithmicMovingAverageParams,
92}
93
94impl<'a> LogarithmicMovingAverageInput<'a> {
95 #[inline]
96 pub fn from_candles(
97 candles: &'a Candles,
98 source: &'a str,
99 params: LogarithmicMovingAverageParams,
100 ) -> Self {
101 Self {
102 data: LogarithmicMovingAverageData::Candles { candles, source },
103 params,
104 }
105 }
106
107 #[inline]
108 pub fn from_slice(data: &'a [f64], params: LogarithmicMovingAverageParams) -> Self {
109 Self {
110 data: LogarithmicMovingAverageData::Slice { data, volume: None },
111 params,
112 }
113 }
114
115 #[inline]
116 pub fn from_slice_with_volume(
117 data: &'a [f64],
118 volume: &'a [f64],
119 params: LogarithmicMovingAverageParams,
120 ) -> Self {
121 Self {
122 data: LogarithmicMovingAverageData::Slice {
123 data,
124 volume: Some(volume),
125 },
126 params,
127 }
128 }
129
130 #[inline]
131 pub fn with_default_candles(candles: &'a Candles) -> Self {
132 Self::from_candles(candles, "close", LogarithmicMovingAverageParams::default())
133 }
134
135 #[inline(always)]
136 fn prices(&self) -> &'a [f64] {
137 match &self.data {
138 LogarithmicMovingAverageData::Candles { candles, source } => {
139 source_type(candles, source)
140 }
141 LogarithmicMovingAverageData::Slice { data, .. } => data,
142 }
143 }
144
145 #[inline(always)]
146 fn volumes(&self) -> Option<&'a [f64]> {
147 match &self.data {
148 LogarithmicMovingAverageData::Candles { candles, .. } => Some(&candles.volume),
149 LogarithmicMovingAverageData::Slice { volume, .. } => *volume,
150 }
151 }
152
153 #[inline]
154 pub fn get_period(&self) -> usize {
155 self.params.period.unwrap_or(DEFAULT_PERIOD)
156 }
157
158 #[inline]
159 pub fn get_steepness(&self) -> f64 {
160 self.params.steepness.unwrap_or(DEFAULT_STEEPNESS)
161 }
162
163 #[inline]
164 pub fn ma_type_str(&self) -> &str {
165 self.params.ma_type.as_deref().unwrap_or(DEFAULT_MA_TYPE)
166 }
167
168 #[inline]
169 pub fn get_smooth(&self) -> usize {
170 self.params.smooth.unwrap_or(DEFAULT_SMOOTH)
171 }
172
173 #[inline]
174 pub fn get_momentum_weight(&self) -> f64 {
175 self.params
176 .momentum_weight
177 .unwrap_or(DEFAULT_MOMENTUM_WEIGHT)
178 }
179
180 #[inline]
181 pub fn get_long_threshold(&self) -> f64 {
182 self.params.long_threshold.unwrap_or(DEFAULT_LONG_THRESHOLD)
183 }
184
185 #[inline]
186 pub fn get_short_threshold(&self) -> f64 {
187 self.params
188 .short_threshold
189 .unwrap_or(DEFAULT_SHORT_THRESHOLD)
190 }
191}
192
193#[derive(Copy, Clone, Debug)]
194pub struct LogarithmicMovingAverageBuilder {
195 period: Option<usize>,
196 steepness: Option<f64>,
197 smooth: Option<usize>,
198 momentum_weight: Option<f64>,
199 long_threshold: Option<f64>,
200 short_threshold: Option<f64>,
201 kernel: Kernel,
202}
203
204impl Default for LogarithmicMovingAverageBuilder {
205 fn default() -> Self {
206 Self {
207 period: None,
208 steepness: None,
209 smooth: None,
210 momentum_weight: None,
211 long_threshold: None,
212 short_threshold: None,
213 kernel: Kernel::Auto,
214 }
215 }
216}
217
218impl LogarithmicMovingAverageBuilder {
219 #[inline(always)]
220 pub fn new() -> Self {
221 Self::default()
222 }
223
224 #[inline(always)]
225 pub fn period(mut self, value: usize) -> Self {
226 self.period = Some(value);
227 self
228 }
229
230 #[inline(always)]
231 pub fn steepness(mut self, value: f64) -> Self {
232 self.steepness = Some(value);
233 self
234 }
235
236 #[inline(always)]
237 pub fn smooth(mut self, value: usize) -> Self {
238 self.smooth = Some(value);
239 self
240 }
241
242 #[inline(always)]
243 pub fn momentum_weight(mut self, value: f64) -> Self {
244 self.momentum_weight = Some(value);
245 self
246 }
247
248 #[inline(always)]
249 pub fn long_threshold(mut self, value: f64) -> Self {
250 self.long_threshold = Some(value);
251 self
252 }
253
254 #[inline(always)]
255 pub fn short_threshold(mut self, value: f64) -> Self {
256 self.short_threshold = Some(value);
257 self
258 }
259
260 #[inline(always)]
261 pub fn kernel(mut self, value: Kernel) -> Self {
262 self.kernel = value;
263 self
264 }
265
266 #[inline(always)]
267 pub fn apply(
268 self,
269 candles: &Candles,
270 ) -> Result<LogarithmicMovingAverageOutput, LogarithmicMovingAverageError> {
271 let input = LogarithmicMovingAverageInput::from_candles(
272 candles,
273 "close",
274 LogarithmicMovingAverageParams {
275 period: self.period,
276 steepness: self.steepness,
277 ma_type: Some(DEFAULT_MA_TYPE.to_string()),
278 smooth: self.smooth,
279 momentum_weight: self.momentum_weight,
280 long_threshold: self.long_threshold,
281 short_threshold: self.short_threshold,
282 },
283 );
284 logarithmic_moving_average_with_kernel(&input, self.kernel)
285 }
286
287 #[inline(always)]
288 pub fn apply_slice(
289 self,
290 data: &[f64],
291 ) -> Result<LogarithmicMovingAverageOutput, LogarithmicMovingAverageError> {
292 let input = LogarithmicMovingAverageInput::from_slice(
293 data,
294 LogarithmicMovingAverageParams {
295 period: self.period,
296 steepness: self.steepness,
297 ma_type: Some(DEFAULT_MA_TYPE.to_string()),
298 smooth: self.smooth,
299 momentum_weight: self.momentum_weight,
300 long_threshold: self.long_threshold,
301 short_threshold: self.short_threshold,
302 },
303 );
304 logarithmic_moving_average_with_kernel(&input, self.kernel)
305 }
306
307 #[inline(always)]
308 pub fn apply_slice_with_volume(
309 self,
310 data: &[f64],
311 volume: &[f64],
312 ma_type: &str,
313 ) -> Result<LogarithmicMovingAverageOutput, LogarithmicMovingAverageError> {
314 let input = LogarithmicMovingAverageInput::from_slice_with_volume(
315 data,
316 volume,
317 LogarithmicMovingAverageParams {
318 period: self.period,
319 steepness: self.steepness,
320 ma_type: Some(ma_type.to_string()),
321 smooth: self.smooth,
322 momentum_weight: self.momentum_weight,
323 long_threshold: self.long_threshold,
324 short_threshold: self.short_threshold,
325 },
326 );
327 logarithmic_moving_average_with_kernel(&input, self.kernel)
328 }
329
330 #[inline(always)]
331 pub fn into_stream<T: Into<String>>(
332 self,
333 ma_type: T,
334 ) -> Result<LogarithmicMovingAverageStream, LogarithmicMovingAverageError> {
335 LogarithmicMovingAverageStream::try_new(LogarithmicMovingAverageParams {
336 period: self.period,
337 steepness: self.steepness,
338 ma_type: Some(ma_type.into()),
339 smooth: self.smooth,
340 momentum_weight: self.momentum_weight,
341 long_threshold: self.long_threshold,
342 short_threshold: self.short_threshold,
343 })
344 }
345}
346
347#[derive(Debug, Error)]
348pub enum LogarithmicMovingAverageError {
349 #[error("logarithmic_moving_average: Input data slice is empty.")]
350 EmptyInputData,
351 #[error("logarithmic_moving_average: All values are NaN.")]
352 AllValuesNaN,
353 #[error(
354 "logarithmic_moving_average: Invalid period: period = {period}, data length = {data_len}"
355 )]
356 InvalidPeriod { period: usize, data_len: usize },
357 #[error(
358 "logarithmic_moving_average: Invalid smooth length: smooth = {smooth}, data length = {data_len}"
359 )]
360 InvalidSmooth { smooth: usize, data_len: usize },
361 #[error("logarithmic_moving_average: Invalid steepness: {steepness}")]
362 InvalidSteepness { steepness: f64 },
363 #[error("logarithmic_moving_average: Invalid MA type: {ma_type}")]
364 InvalidMaType { ma_type: String },
365 #[error("logarithmic_moving_average: Invalid momentum_weight: {momentum_weight}")]
366 InvalidMomentumWeight { momentum_weight: f64 },
367 #[error(
368 "logarithmic_moving_average: Invalid thresholds: long_threshold = {long_threshold}, short_threshold = {short_threshold}"
369 )]
370 InvalidThresholds {
371 long_threshold: f64,
372 short_threshold: f64,
373 },
374 #[error(
375 "logarithmic_moving_average: Data length mismatch: data = {data_len}, volume = {volume_len}"
376 )]
377 DataLengthMismatch { data_len: usize, volume_len: usize },
378 #[error("logarithmic_moving_average: VWMA smoothing requires volume data.")]
379 MissingVolumeForVwma,
380 #[error(
381 "logarithmic_moving_average: Not enough valid data: needed = {needed}, valid = {valid}"
382 )]
383 NotEnoughValidData { needed: usize, valid: usize },
384 #[error(
385 "logarithmic_moving_average: Output length mismatch: expected = {expected}, got = {got}"
386 )]
387 OutputLengthMismatch { expected: usize, got: usize },
388 #[error("logarithmic_moving_average: Invalid range: start={start}, end={end}, step={step}")]
389 InvalidRange {
390 start: String,
391 end: String,
392 step: String,
393 },
394 #[error("logarithmic_moving_average: Invalid kernel for batch path: {0:?}")]
395 InvalidKernelForBatch(Kernel),
396}
397
398#[derive(Clone, Debug)]
399struct PreparedParams {
400 period: usize,
401 steepness: f64,
402 ma_type: String,
403 smooth: usize,
404 momentum_weight: f64,
405 long_threshold: f64,
406 short_threshold: f64,
407}
408
409#[derive(Debug, Clone)]
410pub struct LogarithmicMovingAverageBatchRange {
411 pub period: (usize, usize, usize),
412 pub steepness: (f64, f64, f64),
413 pub smooth: (usize, usize, usize),
414 pub momentum_weight: (f64, f64, f64),
415 pub long_threshold: (f64, f64, f64),
416 pub short_threshold: (f64, f64, f64),
417 pub ma_type: String,
418}
419
420impl Default for LogarithmicMovingAverageBatchRange {
421 fn default() -> Self {
422 Self {
423 period: (DEFAULT_PERIOD, DEFAULT_PERIOD, 0),
424 steepness: (DEFAULT_STEEPNESS, DEFAULT_STEEPNESS, 0.0),
425 smooth: (DEFAULT_SMOOTH, DEFAULT_SMOOTH, 0),
426 momentum_weight: (DEFAULT_MOMENTUM_WEIGHT, DEFAULT_MOMENTUM_WEIGHT, 0.0),
427 long_threshold: (DEFAULT_LONG_THRESHOLD, DEFAULT_LONG_THRESHOLD, 0.0),
428 short_threshold: (DEFAULT_SHORT_THRESHOLD, DEFAULT_SHORT_THRESHOLD, 0.0),
429 ma_type: DEFAULT_MA_TYPE.to_string(),
430 }
431 }
432}
433
434#[derive(Debug, Clone)]
435pub struct LogarithmicMovingAverageBatchOutput {
436 pub lma: Vec<f64>,
437 pub signal: Vec<f64>,
438 pub position: Vec<f64>,
439 pub momentum_confirmed: Vec<f64>,
440 pub rows: usize,
441 pub cols: usize,
442 pub combos: Vec<LogarithmicMovingAverageParams>,
443}
444
445#[derive(Clone, Debug)]
446pub struct LogarithmicMovingAverageBatchBuilder {
447 range: LogarithmicMovingAverageBatchRange,
448 kernel: Kernel,
449}
450
451impl Default for LogarithmicMovingAverageBatchBuilder {
452 fn default() -> Self {
453 Self {
454 range: LogarithmicMovingAverageBatchRange::default(),
455 kernel: Kernel::Auto,
456 }
457 }
458}
459
460impl LogarithmicMovingAverageBatchBuilder {
461 #[inline(always)]
462 pub fn new() -> Self {
463 Self::default()
464 }
465
466 #[inline(always)]
467 pub fn period(mut self, start: usize, end: usize, step: usize) -> Self {
468 self.range.period = (start, end, step);
469 self
470 }
471
472 #[inline(always)]
473 pub fn steepness(mut self, start: f64, end: f64, step: f64) -> Self {
474 self.range.steepness = (start, end, step);
475 self
476 }
477
478 #[inline(always)]
479 pub fn smooth(mut self, start: usize, end: usize, step: usize) -> Self {
480 self.range.smooth = (start, end, step);
481 self
482 }
483
484 #[inline(always)]
485 pub fn momentum_weight(mut self, start: f64, end: f64, step: f64) -> Self {
486 self.range.momentum_weight = (start, end, step);
487 self
488 }
489
490 #[inline(always)]
491 pub fn long_threshold(mut self, start: f64, end: f64, step: f64) -> Self {
492 self.range.long_threshold = (start, end, step);
493 self
494 }
495
496 #[inline(always)]
497 pub fn short_threshold(mut self, start: f64, end: f64, step: f64) -> Self {
498 self.range.short_threshold = (start, end, step);
499 self
500 }
501
502 #[inline(always)]
503 pub fn ma_type<T: Into<String>>(mut self, value: T) -> Self {
504 self.range.ma_type = value.into();
505 self
506 }
507
508 #[inline(always)]
509 pub fn kernel(mut self, value: Kernel) -> Self {
510 self.kernel = value;
511 self
512 }
513
514 #[inline(always)]
515 pub fn apply_slice(
516 self,
517 data: &[f64],
518 ) -> Result<LogarithmicMovingAverageBatchOutput, LogarithmicMovingAverageError> {
519 logarithmic_moving_average_batch_with_kernel(data, None, &self.range, self.kernel)
520 }
521
522 #[inline(always)]
523 pub fn apply_slice_with_volume(
524 self,
525 data: &[f64],
526 volume: &[f64],
527 ) -> Result<LogarithmicMovingAverageBatchOutput, LogarithmicMovingAverageError> {
528 logarithmic_moving_average_batch_with_kernel(data, Some(volume), &self.range, self.kernel)
529 }
530
531 #[inline(always)]
532 pub fn apply(
533 self,
534 candles: &Candles,
535 ) -> Result<LogarithmicMovingAverageBatchOutput, LogarithmicMovingAverageError> {
536 logarithmic_moving_average_batch_with_kernel(
537 &candles.close,
538 Some(&candles.volume),
539 &self.range,
540 self.kernel,
541 )
542 }
543}
544
545#[derive(Debug, Clone)]
546pub struct LogarithmicMovingAverageStream {
547 params: LogarithmicMovingAverageParams,
548 data: Vec<f64>,
549 volume: Vec<f64>,
550}
551
552impl LogarithmicMovingAverageStream {
553 pub fn try_new(
554 params: LogarithmicMovingAverageParams,
555 ) -> Result<Self, LogarithmicMovingAverageError> {
556 let _ = prepare_param_values(
557 params.period.unwrap_or(DEFAULT_PERIOD),
558 params.steepness.unwrap_or(DEFAULT_STEEPNESS),
559 params.ma_type.as_deref().unwrap_or(DEFAULT_MA_TYPE),
560 params.smooth.unwrap_or(DEFAULT_SMOOTH),
561 params.momentum_weight.unwrap_or(DEFAULT_MOMENTUM_WEIGHT),
562 params.long_threshold.unwrap_or(DEFAULT_LONG_THRESHOLD),
563 params.short_threshold.unwrap_or(DEFAULT_SHORT_THRESHOLD),
564 )?;
565 Ok(Self {
566 params,
567 data: Vec::new(),
568 volume: Vec::new(),
569 })
570 }
571
572 pub fn update(&mut self, value: f64, volume: Option<f64>) -> Option<(f64, f64, f64, f64)> {
573 self.data.push(value);
574 self.volume.push(volume.unwrap_or(f64::NAN));
575 let input = if self.volume.iter().all(|v| v.is_nan()) {
576 LogarithmicMovingAverageInput::from_slice(&self.data, self.params.clone())
577 } else {
578 LogarithmicMovingAverageInput::from_slice_with_volume(
579 &self.data,
580 &self.volume,
581 self.params.clone(),
582 )
583 };
584 let out = logarithmic_moving_average(&input).ok()?;
585 let idx = out.signal.len().checked_sub(1)?;
586 let lma = *out.lma.get(idx)?;
587 let signal = *out.signal.get(idx)?;
588 let position = *out.position.get(idx)?;
589 let momentum_confirmed = *out.momentum_confirmed.get(idx)?;
590 if lma.is_nan() || signal.is_nan() || position.is_nan() || momentum_confirmed.is_nan() {
591 None
592 } else {
593 Some((lma, signal, position, momentum_confirmed))
594 }
595 }
596}
597
598#[inline]
599pub fn logarithmic_moving_average(
600 input: &LogarithmicMovingAverageInput,
601) -> Result<LogarithmicMovingAverageOutput, LogarithmicMovingAverageError> {
602 logarithmic_moving_average_with_kernel(input, Kernel::Auto)
603}
604
605#[inline(always)]
606fn longest_finite_run(data: &[f64]) -> usize {
607 let mut best = 0usize;
608 let mut cur = 0usize;
609 for &value in data {
610 if value.is_finite() {
611 cur += 1;
612 best = best.max(cur);
613 } else {
614 cur = 0;
615 }
616 }
617 best
618}
619
620#[inline(always)]
621fn normalize_ma_type(value: &str) -> Result<String, LogarithmicMovingAverageError> {
622 let normalized = value.trim().to_ascii_lowercase();
623 match normalized.as_str() {
624 "ema" | "sma" | "wma" | "rma" | "vwma" => Ok(normalized),
625 _ => Err(LogarithmicMovingAverageError::InvalidMaType {
626 ma_type: value.to_string(),
627 }),
628 }
629}
630
631fn prepare_input(
632 input: &LogarithmicMovingAverageInput,
633) -> Result<PreparedParams, LogarithmicMovingAverageError> {
634 let prepared = prepare_param_values(
635 input.get_period(),
636 input.get_steepness(),
637 input.ma_type_str(),
638 input.get_smooth(),
639 input.get_momentum_weight(),
640 input.get_long_threshold(),
641 input.get_short_threshold(),
642 )?;
643 let prices = input.prices();
644 if prices.is_empty() {
645 return Err(LogarithmicMovingAverageError::EmptyInputData);
646 }
647 if prices.iter().all(|x| x.is_nan()) {
648 return Err(LogarithmicMovingAverageError::AllValuesNaN);
649 }
650
651 if prepared.period > prices.len() {
652 return Err(LogarithmicMovingAverageError::InvalidPeriod {
653 period: prepared.period,
654 data_len: prices.len(),
655 });
656 }
657 if prepared.smooth > prices.len() {
658 return Err(LogarithmicMovingAverageError::InvalidSmooth {
659 smooth: prepared.smooth,
660 data_len: prices.len(),
661 });
662 }
663 if prepared.ma_type == "vwma" {
664 let volume = input
665 .volumes()
666 .ok_or(LogarithmicMovingAverageError::MissingVolumeForVwma)?;
667 if volume.len() != prices.len() {
668 return Err(LogarithmicMovingAverageError::DataLengthMismatch {
669 data_len: prices.len(),
670 volume_len: volume.len(),
671 });
672 }
673 } else if let Some(volume) = input.volumes() {
674 if !volume.is_empty() && volume.len() != prices.len() {
675 return Err(LogarithmicMovingAverageError::DataLengthMismatch {
676 data_len: prices.len(),
677 volume_len: volume.len(),
678 });
679 }
680 }
681
682 let longest = longest_finite_run(prices);
683 if longest < prepared.period {
684 return Err(LogarithmicMovingAverageError::NotEnoughValidData {
685 needed: prepared.period,
686 valid: longest,
687 });
688 }
689
690 Ok(prepared)
691}
692
693fn prepare_param_values(
694 period: usize,
695 steepness: f64,
696 ma_type: &str,
697 smooth: usize,
698 momentum_weight: f64,
699 long_threshold: f64,
700 short_threshold: f64,
701) -> Result<PreparedParams, LogarithmicMovingAverageError> {
702 if period == 0 {
703 return Err(LogarithmicMovingAverageError::InvalidPeriod {
704 period,
705 data_len: 0,
706 });
707 }
708 if smooth == 0 {
709 return Err(LogarithmicMovingAverageError::InvalidSmooth {
710 smooth,
711 data_len: 0,
712 });
713 }
714 if !steepness.is_finite() || steepness <= 0.0 {
715 return Err(LogarithmicMovingAverageError::InvalidSteepness { steepness });
716 }
717 if !momentum_weight.is_finite() || momentum_weight <= 0.0 {
718 return Err(LogarithmicMovingAverageError::InvalidMomentumWeight { momentum_weight });
719 }
720 if !long_threshold.is_finite()
721 || !short_threshold.is_finite()
722 || long_threshold <= short_threshold
723 {
724 return Err(LogarithmicMovingAverageError::InvalidThresholds {
725 long_threshold,
726 short_threshold,
727 });
728 }
729 Ok(PreparedParams {
730 period,
731 steepness,
732 ma_type: normalize_ma_type(ma_type)?,
733 smooth,
734 momentum_weight,
735 long_threshold,
736 short_threshold,
737 })
738}
739
740#[inline(always)]
741fn compute_weights(period: usize, steepness: f64) -> (Vec<f64>, f64) {
742 let mut weights = Vec::with_capacity(period);
743 let mut total = 0.0;
744 for i in 0..period {
745 let log_arg = ((i as f64) + steepness).max(2.0);
746 let weight = 1.0 / log_arg.ln().powi(2);
747 weights.push(weight);
748 total += weight;
749 }
750 (weights, total)
751}
752
753fn compute_lma(prices: &[f64], period: usize, steepness: f64, out: &mut [f64]) {
754 let (weights, total_weight) = compute_weights(period, steepness);
755 let mut run = 0usize;
756 for (i, &price) in prices.iter().enumerate() {
757 if price.is_finite() {
758 run += 1;
759 } else {
760 run = 0;
761 continue;
762 }
763 if run < period {
764 continue;
765 }
766 let mut acc = 0.0;
767 for k in 0..period {
768 acc += prices[i - k] * weights[k];
769 }
770 out[i] = acc / total_weight;
771 }
772}
773
774fn compute_log_momentum(prices: &[f64], period: usize, out: &mut [f64]) {
775 let mut ring = vec![0.0; period];
776 let mut head = 0usize;
777 let mut count = 0usize;
778 let mut sum = 0.0;
779
780 for i in 1..prices.len() {
781 let prev = prices[i - 1];
782 let curr = prices[i];
783 if !prev.is_finite() || !curr.is_finite() || prev <= 0.0 || curr <= 0.0 {
784 head = 0;
785 count = 0;
786 sum = 0.0;
787 continue;
788 }
789 let ret = (curr / prev).ln();
790 if count < period {
791 ring[count] = ret;
792 count += 1;
793 sum += ret;
794 if count < period {
795 continue;
796 }
797 head = 0;
798 } else {
799 let old = ring[head];
800 sum -= old;
801 ring[head] = ret;
802 sum += ret;
803 head += 1;
804 if head == period {
805 head = 0;
806 }
807 }
808 out[i] = sum * ANNUALIZATION / (period as f64);
809 }
810}
811
812fn compute_r_squared(prices: &[f64], period: usize, out: &mut [f64]) {
813 let sum_x = (period * (period - 1) / 2) as f64;
814 let sum_x2 = ((period - 1) * period * (2 * period - 1) / 6) as f64;
815 let mut window: VecDeque<f64> = VecDeque::with_capacity(period);
816 let mut sum_y = 0.0;
817 let mut sum_y2 = 0.0;
818 let mut sum_xy = 0.0;
819
820 for (i, &price) in prices.iter().enumerate() {
821 if !price.is_finite() || price <= 0.0 {
822 window.clear();
823 sum_y = 0.0;
824 sum_y2 = 0.0;
825 sum_xy = 0.0;
826 continue;
827 }
828
829 let y = price.ln();
830 if window.len() < period {
831 window.push_back(y);
832 let idx = (window.len() - 1) as f64;
833 sum_y += y;
834 sum_y2 += y * y;
835 sum_xy += idx * y;
836 if window.len() < period {
837 continue;
838 }
839 } else {
840 let oldest = window.pop_front().unwrap();
841 let prev_sum_y = sum_y;
842 sum_y -= oldest;
843 sum_y2 -= oldest * oldest;
844 sum_xy = sum_xy - prev_sum_y + oldest + ((period - 1) as f64) * y;
845 window.push_back(y);
846 sum_y += y;
847 sum_y2 += y * y;
848 }
849
850 if period <= 10 {
851 out[i] = 0.0;
852 continue;
853 }
854
855 let n = period as f64;
856 let denom_y = n * sum_y2 - sum_y * sum_y;
857 let denom = ((n * sum_x2 - sum_x * sum_x) * denom_y).sqrt();
858 let correlation = if denom.is_finite() && denom != 0.0 {
859 ((n * sum_xy - sum_x * sum_y) / denom).clamp(-1.0, 1.0)
860 } else {
861 0.0
862 };
863 out[i] = (correlation * correlation).clamp(0.0, 1.0);
864 }
865}
866
867fn compute_raw_signal(
868 lma: &[f64],
869 log_momentum: &[f64],
870 r_squared: &[f64],
871 momentum_weight: f64,
872 out: &mut [f64],
873) {
874 for i in 0..lma.len() {
875 if i < SLOPE_LOOKBACK {
876 continue;
877 }
878 let current = lma[i];
879 let prev = lma[i - SLOPE_LOOKBACK];
880 let momentum = log_momentum[i];
881 let quality = r_squared[i];
882 if !current.is_finite()
883 || !prev.is_finite()
884 || prev == 0.0
885 || !momentum.is_finite()
886 || !quality.is_finite()
887 {
888 continue;
889 }
890 let slope = ((current - prev) / prev) * 100.0;
891 let mut signal = slope * (0.5 + quality * 0.5);
892 if signal.signum() == momentum.signum() && momentum.abs() > 0.01 {
893 signal *= momentum_weight;
894 }
895 out[i] = signal;
896 }
897}
898
899fn smooth_sma(signal: &[f64], smooth: usize, out: &mut [f64]) {
900 let mut ring = vec![0.0; smooth];
901 let mut head = 0usize;
902 let mut count = 0usize;
903 let mut sum = 0.0;
904 for (i, &value) in signal.iter().enumerate() {
905 if !value.is_finite() {
906 head = 0;
907 count = 0;
908 sum = 0.0;
909 continue;
910 }
911 if count < smooth {
912 ring[count] = value;
913 count += 1;
914 sum += value;
915 if count < smooth {
916 continue;
917 }
918 head = 0;
919 } else {
920 let old = ring[head];
921 sum -= old;
922 ring[head] = value;
923 sum += value;
924 head += 1;
925 if head == smooth {
926 head = 0;
927 }
928 }
929 out[i] = sum / (smooth as f64);
930 }
931}
932
933fn smooth_ema_like(signal: &[f64], smooth: usize, alpha: f64, out: &mut [f64]) {
934 let mut window = VecDeque::with_capacity(smooth);
935 let mut sum = 0.0;
936 let mut seeded = false;
937 let mut prev = f64::NAN;
938 for (i, &value) in signal.iter().enumerate() {
939 if !value.is_finite() {
940 window.clear();
941 sum = 0.0;
942 seeded = false;
943 prev = f64::NAN;
944 continue;
945 }
946 if !seeded {
947 window.push_back(value);
948 sum += value;
949 if window.len() < smooth {
950 continue;
951 }
952 if window.len() > smooth {
953 let old = window.pop_front().unwrap();
954 sum -= old;
955 }
956 prev = sum / (smooth as f64);
957 out[i] = prev;
958 seeded = true;
959 continue;
960 }
961 prev += alpha * (value - prev);
962 out[i] = prev;
963 }
964}
965
966fn smooth_wma(signal: &[f64], smooth: usize, out: &mut [f64]) {
967 let mut ring = vec![0.0; smooth];
968 let mut head = 0usize;
969 let mut count = 0usize;
970 let mut sum = 0.0;
971 let mut weighted = 0.0;
972 let denom = (smooth * (smooth + 1) / 2) as f64;
973
974 for (i, &value) in signal.iter().enumerate() {
975 if !value.is_finite() {
976 head = 0;
977 count = 0;
978 sum = 0.0;
979 weighted = 0.0;
980 continue;
981 }
982 if count < smooth {
983 ring[count] = value;
984 count += 1;
985 sum += value;
986 weighted += (count as f64) * value;
987 if count < smooth {
988 continue;
989 }
990 head = 0;
991 } else {
992 let old = ring[head];
993 let prev_sum = sum;
994 sum -= old;
995 ring[head] = value;
996 sum += value;
997 weighted = weighted - prev_sum + old + (smooth as f64) * value;
998 head += 1;
999 if head == smooth {
1000 head = 0;
1001 }
1002 }
1003 out[i] = weighted / denom;
1004 }
1005}
1006
1007fn smooth_vwma(
1008 signal: &[f64],
1009 volume: &[f64],
1010 smooth: usize,
1011 out: &mut [f64],
1012) -> Result<(), LogarithmicMovingAverageError> {
1013 if signal.len() != volume.len() {
1014 return Err(LogarithmicMovingAverageError::DataLengthMismatch {
1015 data_len: signal.len(),
1016 volume_len: volume.len(),
1017 });
1018 }
1019
1020 let mut ring_sv = vec![0.0; smooth];
1021 let mut ring_v = vec![0.0; smooth];
1022 let mut head = 0usize;
1023 let mut count = 0usize;
1024 let mut sum_sv = 0.0;
1025 let mut sum_v = 0.0;
1026
1027 for i in 0..signal.len() {
1028 let s = signal[i];
1029 let v = volume[i];
1030 if !s.is_finite() || !v.is_finite() {
1031 head = 0;
1032 count = 0;
1033 sum_sv = 0.0;
1034 sum_v = 0.0;
1035 continue;
1036 }
1037 let sv = s * v;
1038 if count < smooth {
1039 ring_sv[count] = sv;
1040 ring_v[count] = v;
1041 count += 1;
1042 sum_sv += sv;
1043 sum_v += v;
1044 if count < smooth {
1045 continue;
1046 }
1047 head = 0;
1048 } else {
1049 sum_sv -= ring_sv[head];
1050 sum_v -= ring_v[head];
1051 ring_sv[head] = sv;
1052 ring_v[head] = v;
1053 sum_sv += sv;
1054 sum_v += v;
1055 head += 1;
1056 if head == smooth {
1057 head = 0;
1058 }
1059 }
1060 if sum_v != 0.0 {
1061 out[i] = sum_sv / sum_v;
1062 }
1063 }
1064 Ok(())
1065}
1066
1067fn finalize_outputs(
1068 signal: &[f64],
1069 log_momentum: &[f64],
1070 long_threshold: f64,
1071 short_threshold: f64,
1072 out_position: &mut [f64],
1073 out_momentum_confirmed: &mut [f64],
1074) {
1075 for i in 0..signal.len() {
1076 let value = signal[i];
1077 if !value.is_finite() {
1078 continue;
1079 }
1080 out_position[i] = if value > long_threshold {
1081 1.0
1082 } else if value < short_threshold {
1083 -1.0
1084 } else {
1085 0.0
1086 };
1087 let momentum = log_momentum[i];
1088 if momentum.is_finite() {
1089 out_momentum_confirmed[i] = if value.signum() == momentum.signum()
1090 && value.abs() > long_threshold.abs() * 0.5
1091 {
1092 1.0
1093 } else {
1094 0.0
1095 };
1096 }
1097 }
1098}
1099
1100fn logarithmic_moving_average_compute_into(
1101 prices: &[f64],
1102 volume: Option<&[f64]>,
1103 params: &PreparedParams,
1104 out_lma: &mut [f64],
1105 out_signal: &mut [f64],
1106 out_position: &mut [f64],
1107 out_momentum_confirmed: &mut [f64],
1108) -> Result<(), LogarithmicMovingAverageError> {
1109 out_lma.fill(f64::NAN);
1110 out_signal.fill(f64::NAN);
1111 out_position.fill(f64::NAN);
1112 out_momentum_confirmed.fill(f64::NAN);
1113
1114 let mut log_momentum = alloc_with_nan_prefix(prices.len(), prices.len());
1115 let mut r_squared = alloc_with_nan_prefix(prices.len(), prices.len());
1116 let mut raw_signal = alloc_with_nan_prefix(prices.len(), prices.len());
1117
1118 compute_lma(prices, params.period, params.steepness, out_lma);
1119 compute_log_momentum(prices, params.period, &mut log_momentum);
1120 compute_r_squared(prices, params.period, &mut r_squared);
1121 compute_raw_signal(
1122 out_lma,
1123 &log_momentum,
1124 &r_squared,
1125 params.momentum_weight,
1126 &mut raw_signal,
1127 );
1128
1129 match params.ma_type.as_str() {
1130 "ema" => smooth_ema_like(
1131 &raw_signal,
1132 params.smooth,
1133 2.0 / ((params.smooth as f64) + 1.0),
1134 out_signal,
1135 ),
1136 "sma" => smooth_sma(&raw_signal, params.smooth, out_signal),
1137 "wma" => smooth_wma(&raw_signal, params.smooth, out_signal),
1138 "rma" => smooth_ema_like(
1139 &raw_signal,
1140 params.smooth,
1141 1.0 / (params.smooth as f64),
1142 out_signal,
1143 ),
1144 "vwma" => smooth_vwma(
1145 &raw_signal,
1146 volume.ok_or(LogarithmicMovingAverageError::MissingVolumeForVwma)?,
1147 params.smooth,
1148 out_signal,
1149 )?,
1150 other => {
1151 return Err(LogarithmicMovingAverageError::InvalidMaType {
1152 ma_type: other.to_string(),
1153 });
1154 }
1155 }
1156
1157 finalize_outputs(
1158 out_signal,
1159 &log_momentum,
1160 params.long_threshold,
1161 params.short_threshold,
1162 out_position,
1163 out_momentum_confirmed,
1164 );
1165 Ok(())
1166}
1167
1168pub fn logarithmic_moving_average_into_slice(
1169 out_lma: &mut [f64],
1170 out_signal: &mut [f64],
1171 out_position: &mut [f64],
1172 out_momentum_confirmed: &mut [f64],
1173 input: &LogarithmicMovingAverageInput,
1174 _kernel: Kernel,
1175) -> Result<(), LogarithmicMovingAverageError> {
1176 let prices = input.prices();
1177 let volume = input.volumes();
1178 let params = prepare_input(input)?;
1179
1180 let expected = prices.len();
1181 if out_lma.len() != expected
1182 || out_signal.len() != expected
1183 || out_position.len() != expected
1184 || out_momentum_confirmed.len() != expected
1185 {
1186 return Err(LogarithmicMovingAverageError::OutputLengthMismatch {
1187 expected,
1188 got: out_lma
1189 .len()
1190 .max(out_signal.len())
1191 .max(out_position.len())
1192 .max(out_momentum_confirmed.len()),
1193 });
1194 }
1195
1196 logarithmic_moving_average_compute_into(
1197 prices,
1198 volume,
1199 ¶ms,
1200 out_lma,
1201 out_signal,
1202 out_position,
1203 out_momentum_confirmed,
1204 )
1205}
1206
1207#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1208#[inline]
1209pub fn logarithmic_moving_average_into(
1210 input: &LogarithmicMovingAverageInput,
1211 out_lma: &mut [f64],
1212 out_signal: &mut [f64],
1213 out_position: &mut [f64],
1214 out_momentum_confirmed: &mut [f64],
1215) -> Result<(), LogarithmicMovingAverageError> {
1216 logarithmic_moving_average_into_slice(
1217 out_lma,
1218 out_signal,
1219 out_position,
1220 out_momentum_confirmed,
1221 input,
1222 Kernel::Auto,
1223 )
1224}
1225
1226pub fn logarithmic_moving_average_with_kernel(
1227 input: &LogarithmicMovingAverageInput,
1228 kernel: Kernel,
1229) -> Result<LogarithmicMovingAverageOutput, LogarithmicMovingAverageError> {
1230 let len = input.prices().len();
1231 let mut lma = alloc_with_nan_prefix(len, len);
1232 let mut signal = alloc_with_nan_prefix(len, len);
1233 let mut position = alloc_with_nan_prefix(len, len);
1234 let mut momentum_confirmed = alloc_with_nan_prefix(len, len);
1235 logarithmic_moving_average_into_slice(
1236 &mut lma,
1237 &mut signal,
1238 &mut position,
1239 &mut momentum_confirmed,
1240 input,
1241 kernel,
1242 )?;
1243 Ok(LogarithmicMovingAverageOutput {
1244 lma,
1245 signal,
1246 position,
1247 momentum_confirmed,
1248 })
1249}
1250
1251fn expand_axis_usize(
1252 range: (usize, usize, usize),
1253) -> Result<Vec<usize>, LogarithmicMovingAverageError> {
1254 let (start, end, step) = range;
1255 if start > end {
1256 return Err(LogarithmicMovingAverageError::InvalidRange {
1257 start: start.to_string(),
1258 end: end.to_string(),
1259 step: step.to_string(),
1260 });
1261 }
1262 if start == end {
1263 return Ok(vec![start]);
1264 }
1265 if step == 0 {
1266 return Err(LogarithmicMovingAverageError::InvalidRange {
1267 start: start.to_string(),
1268 end: end.to_string(),
1269 step: step.to_string(),
1270 });
1271 }
1272 let mut out = Vec::new();
1273 let mut value = start;
1274 while value <= end {
1275 out.push(value);
1276 match value.checked_add(step) {
1277 Some(next) if next > value => value = next,
1278 _ => break,
1279 }
1280 }
1281 Ok(out)
1282}
1283
1284fn expand_axis_f64(range: (f64, f64, f64)) -> Result<Vec<f64>, LogarithmicMovingAverageError> {
1285 let (start, end, step) = range;
1286 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1287 return Err(LogarithmicMovingAverageError::InvalidRange {
1288 start: start.to_string(),
1289 end: end.to_string(),
1290 step: step.to_string(),
1291 });
1292 }
1293 if start > end {
1294 return Err(LogarithmicMovingAverageError::InvalidRange {
1295 start: start.to_string(),
1296 end: end.to_string(),
1297 step: step.to_string(),
1298 });
1299 }
1300 if (start - end).abs() <= f64::EPSILON {
1301 return Ok(vec![start]);
1302 }
1303 if step <= 0.0 {
1304 return Err(LogarithmicMovingAverageError::InvalidRange {
1305 start: start.to_string(),
1306 end: end.to_string(),
1307 step: step.to_string(),
1308 });
1309 }
1310 let mut out = Vec::new();
1311 let mut value = start;
1312 let limit = end + step * 1e-9;
1313 while value <= limit {
1314 out.push(value.min(end));
1315 value += step;
1316 }
1317 Ok(out)
1318}
1319
1320pub fn expand_grid_logarithmic_moving_average(
1321 range: &LogarithmicMovingAverageBatchRange,
1322) -> Result<Vec<LogarithmicMovingAverageParams>, LogarithmicMovingAverageError> {
1323 let periods = expand_axis_usize(range.period)?;
1324 let steepnesses = expand_axis_f64(range.steepness)?;
1325 let smooths = expand_axis_usize(range.smooth)?;
1326 let momentum_weights = expand_axis_f64(range.momentum_weight)?;
1327 let long_thresholds = expand_axis_f64(range.long_threshold)?;
1328 let short_thresholds = expand_axis_f64(range.short_threshold)?;
1329 let ma_type = normalize_ma_type(&range.ma_type)?;
1330
1331 let mut out = Vec::new();
1332 for &period in &periods {
1333 for &steepness in &steepnesses {
1334 for &smooth in &smooths {
1335 for &momentum_weight in &momentum_weights {
1336 for &long_threshold in &long_thresholds {
1337 for &short_threshold in &short_thresholds {
1338 out.push(LogarithmicMovingAverageParams {
1339 period: Some(period),
1340 steepness: Some(steepness),
1341 ma_type: Some(ma_type.clone()),
1342 smooth: Some(smooth),
1343 momentum_weight: Some(momentum_weight),
1344 long_threshold: Some(long_threshold),
1345 short_threshold: Some(short_threshold),
1346 });
1347 }
1348 }
1349 }
1350 }
1351 }
1352 }
1353 Ok(out)
1354}
1355
1356fn logarithmic_moving_average_batch_inner_into(
1357 prices: &[f64],
1358 volume: Option<&[f64]>,
1359 sweep: &LogarithmicMovingAverageBatchRange,
1360 kernel: Kernel,
1361 parallel: bool,
1362 out_lma: &mut [f64],
1363 out_signal: &mut [f64],
1364 out_position: &mut [f64],
1365 out_momentum_confirmed: &mut [f64],
1366) -> Result<Vec<LogarithmicMovingAverageParams>, LogarithmicMovingAverageError> {
1367 if prices.is_empty() {
1368 return Err(LogarithmicMovingAverageError::EmptyInputData);
1369 }
1370 let combos = expand_grid_logarithmic_moving_average(sweep)?;
1371 let cols = prices.len();
1372 let rows = combos.len();
1373 let total =
1374 rows.checked_mul(cols)
1375 .ok_or(LogarithmicMovingAverageError::OutputLengthMismatch {
1376 expected: usize::MAX,
1377 got: 0,
1378 })?;
1379 if out_lma.len() != total
1380 || out_signal.len() != total
1381 || out_position.len() != total
1382 || out_momentum_confirmed.len() != total
1383 {
1384 return Err(LogarithmicMovingAverageError::OutputLengthMismatch {
1385 expected: total,
1386 got: out_lma
1387 .len()
1388 .max(out_signal.len())
1389 .max(out_position.len())
1390 .max(out_momentum_confirmed.len()),
1391 });
1392 }
1393 let _kernel = kernel;
1394
1395 for params in &combos {
1396 let probe = LogarithmicMovingAverageInput {
1397 data: LogarithmicMovingAverageData::Slice {
1398 data: prices,
1399 volume,
1400 },
1401 params: params.clone(),
1402 };
1403 let _ = prepare_input(&probe)?;
1404 }
1405
1406 let do_row = |row: usize,
1407 lma_row: &mut [f64],
1408 signal_row: &mut [f64],
1409 position_row: &mut [f64],
1410 momentum_row: &mut [f64]| {
1411 let row_input = LogarithmicMovingAverageInput {
1412 data: LogarithmicMovingAverageData::Slice {
1413 data: prices,
1414 volume,
1415 },
1416 params: combos[row].clone(),
1417 };
1418 let prepared = prepare_input(&row_input).unwrap();
1419 logarithmic_moving_average_compute_into(
1420 prices,
1421 volume,
1422 &prepared,
1423 lma_row,
1424 signal_row,
1425 position_row,
1426 momentum_row,
1427 )
1428 .unwrap();
1429 };
1430
1431 if parallel {
1432 #[cfg(not(target_arch = "wasm32"))]
1433 out_lma
1434 .par_chunks_mut(cols)
1435 .zip(out_signal.par_chunks_mut(cols))
1436 .zip(out_position.par_chunks_mut(cols))
1437 .zip(out_momentum_confirmed.par_chunks_mut(cols))
1438 .enumerate()
1439 .for_each(
1440 |(row, (((lma_row, signal_row), position_row), momentum_row))| {
1441 do_row(row, lma_row, signal_row, position_row, momentum_row)
1442 },
1443 );
1444
1445 #[cfg(target_arch = "wasm32")]
1446 for (row, (((lma_row, signal_row), position_row), momentum_row)) in out_lma
1447 .chunks_mut(cols)
1448 .zip(out_signal.chunks_mut(cols))
1449 .zip(out_position.chunks_mut(cols))
1450 .zip(out_momentum_confirmed.chunks_mut(cols))
1451 .enumerate()
1452 {
1453 do_row(row, lma_row, signal_row, position_row, momentum_row);
1454 }
1455 } else {
1456 for (row, (((lma_row, signal_row), position_row), momentum_row)) in out_lma
1457 .chunks_mut(cols)
1458 .zip(out_signal.chunks_mut(cols))
1459 .zip(out_position.chunks_mut(cols))
1460 .zip(out_momentum_confirmed.chunks_mut(cols))
1461 .enumerate()
1462 {
1463 do_row(row, lma_row, signal_row, position_row, momentum_row);
1464 }
1465 }
1466
1467 Ok(combos)
1468}
1469
1470pub fn logarithmic_moving_average_batch_slice(
1471 prices: &[f64],
1472 volume: Option<&[f64]>,
1473 sweep: &LogarithmicMovingAverageBatchRange,
1474 kernel: Kernel,
1475) -> Result<LogarithmicMovingAverageBatchOutput, LogarithmicMovingAverageError> {
1476 let combos = expand_grid_logarithmic_moving_average(sweep)?;
1477 let rows = combos.len();
1478 let cols = prices.len();
1479 let total =
1480 rows.checked_mul(cols)
1481 .ok_or(LogarithmicMovingAverageError::OutputLengthMismatch {
1482 expected: usize::MAX,
1483 got: 0,
1484 })?;
1485 let mut lma = alloc_with_nan_prefix(total, total);
1486 let mut signal = alloc_with_nan_prefix(total, total);
1487 let mut position = alloc_with_nan_prefix(total, total);
1488 let mut momentum_confirmed = alloc_with_nan_prefix(total, total);
1489 let combos = logarithmic_moving_average_batch_inner_into(
1490 prices,
1491 volume,
1492 sweep,
1493 kernel,
1494 false,
1495 &mut lma,
1496 &mut signal,
1497 &mut position,
1498 &mut momentum_confirmed,
1499 )?;
1500 Ok(LogarithmicMovingAverageBatchOutput {
1501 lma,
1502 signal,
1503 position,
1504 momentum_confirmed,
1505 rows,
1506 cols,
1507 combos,
1508 })
1509}
1510
1511pub fn logarithmic_moving_average_batch_par_slice(
1512 prices: &[f64],
1513 volume: Option<&[f64]>,
1514 sweep: &LogarithmicMovingAverageBatchRange,
1515 kernel: Kernel,
1516) -> Result<LogarithmicMovingAverageBatchOutput, LogarithmicMovingAverageError> {
1517 let combos = expand_grid_logarithmic_moving_average(sweep)?;
1518 let rows = combos.len();
1519 let cols = prices.len();
1520 let total =
1521 rows.checked_mul(cols)
1522 .ok_or(LogarithmicMovingAverageError::OutputLengthMismatch {
1523 expected: usize::MAX,
1524 got: 0,
1525 })?;
1526 let mut lma = alloc_with_nan_prefix(total, total);
1527 let mut signal = alloc_with_nan_prefix(total, total);
1528 let mut position = alloc_with_nan_prefix(total, total);
1529 let mut momentum_confirmed = alloc_with_nan_prefix(total, total);
1530 let combos = logarithmic_moving_average_batch_inner_into(
1531 prices,
1532 volume,
1533 sweep,
1534 kernel,
1535 true,
1536 &mut lma,
1537 &mut signal,
1538 &mut position,
1539 &mut momentum_confirmed,
1540 )?;
1541 Ok(LogarithmicMovingAverageBatchOutput {
1542 lma,
1543 signal,
1544 position,
1545 momentum_confirmed,
1546 rows,
1547 cols,
1548 combos,
1549 })
1550}
1551
1552pub fn logarithmic_moving_average_batch_with_kernel(
1553 prices: &[f64],
1554 volume: Option<&[f64]>,
1555 sweep: &LogarithmicMovingAverageBatchRange,
1556 kernel: Kernel,
1557) -> Result<LogarithmicMovingAverageBatchOutput, LogarithmicMovingAverageError> {
1558 match kernel {
1559 Kernel::Scalar
1560 | Kernel::ScalarBatch
1561 | Kernel::Auto
1562 | Kernel::Avx2
1563 | Kernel::Avx512
1564 | Kernel::Avx2Batch
1565 | Kernel::Avx512Batch => {}
1566 other => return Err(LogarithmicMovingAverageError::InvalidKernelForBatch(other)),
1567 }
1568 #[cfg(not(target_arch = "wasm32"))]
1569 {
1570 logarithmic_moving_average_batch_par_slice(prices, volume, sweep, kernel)
1571 }
1572 #[cfg(target_arch = "wasm32")]
1573 {
1574 logarithmic_moving_average_batch_slice(prices, volume, sweep, kernel)
1575 }
1576}
1577
1578#[cfg(feature = "python")]
1579#[pyfunction(name = "logarithmic_moving_average")]
1580#[pyo3(signature = (data, period=DEFAULT_PERIOD, steepness=DEFAULT_STEEPNESS, ma_type=DEFAULT_MA_TYPE, smooth=DEFAULT_SMOOTH, momentum_weight=DEFAULT_MOMENTUM_WEIGHT, long_threshold=DEFAULT_LONG_THRESHOLD, short_threshold=DEFAULT_SHORT_THRESHOLD, volume=None, kernel=None))]
1581pub fn logarithmic_moving_average_py<'py>(
1582 py: Python<'py>,
1583 data: PyReadonlyArray1<'py, f64>,
1584 period: usize,
1585 steepness: f64,
1586 ma_type: &str,
1587 smooth: usize,
1588 momentum_weight: f64,
1589 long_threshold: f64,
1590 short_threshold: f64,
1591 volume: Option<PyReadonlyArray1<'py, f64>>,
1592 kernel: Option<&str>,
1593) -> PyResult<(
1594 Bound<'py, PyArray1<f64>>,
1595 Bound<'py, PyArray1<f64>>,
1596 Bound<'py, PyArray1<f64>>,
1597 Bound<'py, PyArray1<f64>>,
1598)> {
1599 let data = data.as_slice()?;
1600 let volume_slice = volume.as_ref().map(|v| v.as_slice()).transpose()?;
1601 let input = match volume_slice {
1602 Some(v) => LogarithmicMovingAverageInput::from_slice_with_volume(
1603 data,
1604 v,
1605 LogarithmicMovingAverageParams {
1606 period: Some(period),
1607 steepness: Some(steepness),
1608 ma_type: Some(ma_type.to_string()),
1609 smooth: Some(smooth),
1610 momentum_weight: Some(momentum_weight),
1611 long_threshold: Some(long_threshold),
1612 short_threshold: Some(short_threshold),
1613 },
1614 ),
1615 None => LogarithmicMovingAverageInput::from_slice(
1616 data,
1617 LogarithmicMovingAverageParams {
1618 period: Some(period),
1619 steepness: Some(steepness),
1620 ma_type: Some(ma_type.to_string()),
1621 smooth: Some(smooth),
1622 momentum_weight: Some(momentum_weight),
1623 long_threshold: Some(long_threshold),
1624 short_threshold: Some(short_threshold),
1625 },
1626 ),
1627 };
1628 let kernel = validate_kernel(kernel, false)?;
1629 let out = py
1630 .allow_threads(|| logarithmic_moving_average_with_kernel(&input, kernel))
1631 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1632 Ok((
1633 out.lma.into_pyarray(py),
1634 out.signal.into_pyarray(py),
1635 out.position.into_pyarray(py),
1636 out.momentum_confirmed.into_pyarray(py),
1637 ))
1638}
1639
1640#[cfg(feature = "python")]
1641#[pyclass(name = "LogarithmicMovingAverageStream")]
1642pub struct LogarithmicMovingAverageStreamPy {
1643 stream: LogarithmicMovingAverageStream,
1644}
1645
1646#[cfg(feature = "python")]
1647#[pymethods]
1648impl LogarithmicMovingAverageStreamPy {
1649 #[new]
1650 #[pyo3(signature = (period=DEFAULT_PERIOD, steepness=DEFAULT_STEEPNESS, ma_type=DEFAULT_MA_TYPE, smooth=DEFAULT_SMOOTH, momentum_weight=DEFAULT_MOMENTUM_WEIGHT, long_threshold=DEFAULT_LONG_THRESHOLD, short_threshold=DEFAULT_SHORT_THRESHOLD))]
1651 fn new(
1652 period: usize,
1653 steepness: f64,
1654 ma_type: &str,
1655 smooth: usize,
1656 momentum_weight: f64,
1657 long_threshold: f64,
1658 short_threshold: f64,
1659 ) -> PyResult<Self> {
1660 let stream = LogarithmicMovingAverageStream::try_new(LogarithmicMovingAverageParams {
1661 period: Some(period),
1662 steepness: Some(steepness),
1663 ma_type: Some(ma_type.to_string()),
1664 smooth: Some(smooth),
1665 momentum_weight: Some(momentum_weight),
1666 long_threshold: Some(long_threshold),
1667 short_threshold: Some(short_threshold),
1668 })
1669 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1670 Ok(Self { stream })
1671 }
1672
1673 #[pyo3(signature = (value, volume=None))]
1674 fn update(&mut self, value: f64, volume: Option<f64>) -> Option<(f64, f64, f64, f64)> {
1675 self.stream.update(value, volume)
1676 }
1677}
1678
1679#[cfg(feature = "python")]
1680#[pyfunction(name = "logarithmic_moving_average_batch")]
1681#[pyo3(signature = (data, period_range, steepness_range, smooth_range, momentum_weight_range, long_threshold_range, short_threshold_range, ma_type=DEFAULT_MA_TYPE, volume=None, kernel=None))]
1682pub fn logarithmic_moving_average_batch_py<'py>(
1683 py: Python<'py>,
1684 data: PyReadonlyArray1<'py, f64>,
1685 period_range: (usize, usize, usize),
1686 steepness_range: (f64, f64, f64),
1687 smooth_range: (usize, usize, usize),
1688 momentum_weight_range: (f64, f64, f64),
1689 long_threshold_range: (f64, f64, f64),
1690 short_threshold_range: (f64, f64, f64),
1691 ma_type: &str,
1692 volume: Option<PyReadonlyArray1<'py, f64>>,
1693 kernel: Option<&str>,
1694) -> PyResult<Bound<'py, PyDict>> {
1695 let data = data.as_slice()?;
1696 let volume = volume.as_ref().map(|v| v.as_slice()).transpose()?;
1697 let sweep = LogarithmicMovingAverageBatchRange {
1698 period: period_range,
1699 steepness: steepness_range,
1700 smooth: smooth_range,
1701 momentum_weight: momentum_weight_range,
1702 long_threshold: long_threshold_range,
1703 short_threshold: short_threshold_range,
1704 ma_type: ma_type.to_string(),
1705 };
1706 let combos = expand_grid_logarithmic_moving_average(&sweep)
1707 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1708 let rows = combos.len();
1709 let cols = data.len();
1710 let total = rows
1711 .checked_mul(cols)
1712 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1713 let lma_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1714 let signal_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1715 let position_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1716 let momentum_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1717 let out_lma = unsafe { lma_arr.as_slice_mut()? };
1718 let out_signal = unsafe { signal_arr.as_slice_mut()? };
1719 let out_position = unsafe { position_arr.as_slice_mut()? };
1720 let out_momentum = unsafe { momentum_arr.as_slice_mut()? };
1721 let kernel = validate_kernel(kernel, true)?;
1722
1723 py.allow_threads(|| {
1724 let batch_kernel = match kernel {
1725 Kernel::Auto => detect_best_batch_kernel(),
1726 other => other,
1727 };
1728 logarithmic_moving_average_batch_inner_into(
1729 data,
1730 volume,
1731 &sweep,
1732 batch_kernel.to_non_batch(),
1733 true,
1734 out_lma,
1735 out_signal,
1736 out_position,
1737 out_momentum,
1738 )
1739 })
1740 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1741
1742 let periods: Vec<u64> = combos
1743 .iter()
1744 .map(|params| params.period.unwrap_or(DEFAULT_PERIOD) as u64)
1745 .collect();
1746 let steepnesses: Vec<f64> = combos
1747 .iter()
1748 .map(|params| params.steepness.unwrap_or(DEFAULT_STEEPNESS))
1749 .collect();
1750 let smooths: Vec<u64> = combos
1751 .iter()
1752 .map(|params| params.smooth.unwrap_or(DEFAULT_SMOOTH) as u64)
1753 .collect();
1754 let momentum_weights: Vec<f64> = combos
1755 .iter()
1756 .map(|params| params.momentum_weight.unwrap_or(DEFAULT_MOMENTUM_WEIGHT))
1757 .collect();
1758 let long_thresholds: Vec<f64> = combos
1759 .iter()
1760 .map(|params| params.long_threshold.unwrap_or(DEFAULT_LONG_THRESHOLD))
1761 .collect();
1762 let short_thresholds: Vec<f64> = combos
1763 .iter()
1764 .map(|params| params.short_threshold.unwrap_or(DEFAULT_SHORT_THRESHOLD))
1765 .collect();
1766
1767 let dict = PyDict::new(py);
1768 dict.set_item("lma", lma_arr.reshape((rows, cols))?)?;
1769 dict.set_item("signal", signal_arr.reshape((rows, cols))?)?;
1770 dict.set_item("position", position_arr.reshape((rows, cols))?)?;
1771 dict.set_item("momentum_confirmed", momentum_arr.reshape((rows, cols))?)?;
1772 dict.set_item("rows", rows)?;
1773 dict.set_item("cols", cols)?;
1774 dict.set_item("periods", periods.into_pyarray(py))?;
1775 dict.set_item("steepnesses", steepnesses.into_pyarray(py))?;
1776 dict.set_item("smooths", smooths.into_pyarray(py))?;
1777 dict.set_item("momentum_weights", momentum_weights.into_pyarray(py))?;
1778 dict.set_item("long_thresholds", long_thresholds.into_pyarray(py))?;
1779 dict.set_item("short_thresholds", short_thresholds.into_pyarray(py))?;
1780 dict.set_item("ma_type", ma_type)?;
1781 Ok(dict)
1782}
1783
1784#[cfg(feature = "python")]
1785pub fn register_logarithmic_moving_average_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1786 m.add_function(wrap_pyfunction!(logarithmic_moving_average_py, m)?)?;
1787 m.add_function(wrap_pyfunction!(logarithmic_moving_average_batch_py, m)?)?;
1788 m.add_class::<LogarithmicMovingAverageStreamPy>()?;
1789 Ok(())
1790}
1791
1792#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1793#[derive(Debug, Clone, Serialize, Deserialize)]
1794struct LogarithmicMovingAverageJsOutput {
1795 lma: Vec<f64>,
1796 signal: Vec<f64>,
1797 position: Vec<f64>,
1798 momentum_confirmed: Vec<f64>,
1799}
1800
1801#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1802#[derive(Debug, Clone, Serialize, Deserialize)]
1803struct LogarithmicMovingAverageBatchConfig {
1804 period_range: Vec<usize>,
1805 steepness_range: Vec<f64>,
1806 smooth_range: Vec<usize>,
1807 momentum_weight_range: Vec<f64>,
1808 long_threshold_range: Vec<f64>,
1809 short_threshold_range: Vec<f64>,
1810 ma_type: String,
1811}
1812
1813#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1814#[derive(Debug, Clone, Serialize, Deserialize)]
1815struct LogarithmicMovingAverageBatchJsOutput {
1816 lma: Vec<f64>,
1817 signal: Vec<f64>,
1818 position: Vec<f64>,
1819 momentum_confirmed: Vec<f64>,
1820 rows: usize,
1821 cols: usize,
1822 combos: Vec<LogarithmicMovingAverageParams>,
1823}
1824
1825#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1826#[wasm_bindgen(js_name = "logarithmic_moving_average")]
1827pub fn logarithmic_moving_average_js(
1828 data: &[f64],
1829 volume: &[f64],
1830 period: usize,
1831 steepness: f64,
1832 ma_type: &str,
1833 smooth: usize,
1834 momentum_weight: f64,
1835 long_threshold: f64,
1836 short_threshold: f64,
1837) -> Result<JsValue, JsValue> {
1838 let input = if volume.is_empty() {
1839 LogarithmicMovingAverageInput::from_slice(
1840 data,
1841 LogarithmicMovingAverageParams {
1842 period: Some(period),
1843 steepness: Some(steepness),
1844 ma_type: Some(ma_type.to_string()),
1845 smooth: Some(smooth),
1846 momentum_weight: Some(momentum_weight),
1847 long_threshold: Some(long_threshold),
1848 short_threshold: Some(short_threshold),
1849 },
1850 )
1851 } else {
1852 LogarithmicMovingAverageInput::from_slice_with_volume(
1853 data,
1854 volume,
1855 LogarithmicMovingAverageParams {
1856 period: Some(period),
1857 steepness: Some(steepness),
1858 ma_type: Some(ma_type.to_string()),
1859 smooth: Some(smooth),
1860 momentum_weight: Some(momentum_weight),
1861 long_threshold: Some(long_threshold),
1862 short_threshold: Some(short_threshold),
1863 },
1864 )
1865 };
1866 let out = logarithmic_moving_average(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1867 serde_wasm_bindgen::to_value(&LogarithmicMovingAverageJsOutput {
1868 lma: out.lma,
1869 signal: out.signal,
1870 position: out.position,
1871 momentum_confirmed: out.momentum_confirmed,
1872 })
1873 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1874}
1875
1876#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1877#[wasm_bindgen]
1878pub fn logarithmic_moving_average_into(
1879 data_ptr: *const f64,
1880 volume_ptr: *const f64,
1881 volume_len: usize,
1882 out_ptr: *mut f64,
1883 len: usize,
1884 period: usize,
1885 steepness: f64,
1886 ma_type: &str,
1887 smooth: usize,
1888 momentum_weight: f64,
1889 long_threshold: f64,
1890 short_threshold: f64,
1891) -> Result<(), JsValue> {
1892 if data_ptr.is_null() || out_ptr.is_null() {
1893 return Err(JsValue::from_str(
1894 "null pointer passed to logarithmic_moving_average_into",
1895 ));
1896 }
1897 unsafe {
1898 let data = std::slice::from_raw_parts(data_ptr, len);
1899 let volume = if volume_ptr.is_null() || volume_len == 0 {
1900 None
1901 } else {
1902 Some(std::slice::from_raw_parts(volume_ptr, volume_len))
1903 };
1904 let out = std::slice::from_raw_parts_mut(out_ptr, len * 4);
1905 let (out_lma, rest) = out.split_at_mut(len);
1906 let (out_signal, rest) = rest.split_at_mut(len);
1907 let (out_position, out_momentum) = rest.split_at_mut(len);
1908 let input = match volume {
1909 Some(v) => LogarithmicMovingAverageInput::from_slice_with_volume(
1910 data,
1911 v,
1912 LogarithmicMovingAverageParams {
1913 period: Some(period),
1914 steepness: Some(steepness),
1915 ma_type: Some(ma_type.to_string()),
1916 smooth: Some(smooth),
1917 momentum_weight: Some(momentum_weight),
1918 long_threshold: Some(long_threshold),
1919 short_threshold: Some(short_threshold),
1920 },
1921 ),
1922 None => LogarithmicMovingAverageInput::from_slice(
1923 data,
1924 LogarithmicMovingAverageParams {
1925 period: Some(period),
1926 steepness: Some(steepness),
1927 ma_type: Some(ma_type.to_string()),
1928 smooth: Some(smooth),
1929 momentum_weight: Some(momentum_weight),
1930 long_threshold: Some(long_threshold),
1931 short_threshold: Some(short_threshold),
1932 },
1933 ),
1934 };
1935 logarithmic_moving_average_into_slice(
1936 out_lma,
1937 out_signal,
1938 out_position,
1939 out_momentum,
1940 &input,
1941 Kernel::Auto,
1942 )
1943 .map_err(|e| JsValue::from_str(&e.to_string()))
1944 }
1945}
1946
1947#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1948#[wasm_bindgen(js_name = "logarithmic_moving_average_into_host")]
1949pub fn logarithmic_moving_average_into_host(
1950 data: &[f64],
1951 volume: &[f64],
1952 out_ptr: *mut f64,
1953 period: usize,
1954 steepness: f64,
1955 ma_type: &str,
1956 smooth: usize,
1957 momentum_weight: f64,
1958 long_threshold: f64,
1959 short_threshold: f64,
1960) -> Result<(), JsValue> {
1961 if out_ptr.is_null() {
1962 return Err(JsValue::from_str(
1963 "null pointer passed to logarithmic_moving_average_into_host",
1964 ));
1965 }
1966 unsafe {
1967 let out = std::slice::from_raw_parts_mut(out_ptr, data.len() * 4);
1968 let (out_lma, rest) = out.split_at_mut(data.len());
1969 let (out_signal, rest) = rest.split_at_mut(data.len());
1970 let (out_position, out_momentum) = rest.split_at_mut(data.len());
1971 let input = if volume.is_empty() {
1972 LogarithmicMovingAverageInput::from_slice(
1973 data,
1974 LogarithmicMovingAverageParams {
1975 period: Some(period),
1976 steepness: Some(steepness),
1977 ma_type: Some(ma_type.to_string()),
1978 smooth: Some(smooth),
1979 momentum_weight: Some(momentum_weight),
1980 long_threshold: Some(long_threshold),
1981 short_threshold: Some(short_threshold),
1982 },
1983 )
1984 } else {
1985 LogarithmicMovingAverageInput::from_slice_with_volume(
1986 data,
1987 volume,
1988 LogarithmicMovingAverageParams {
1989 period: Some(period),
1990 steepness: Some(steepness),
1991 ma_type: Some(ma_type.to_string()),
1992 smooth: Some(smooth),
1993 momentum_weight: Some(momentum_weight),
1994 long_threshold: Some(long_threshold),
1995 short_threshold: Some(short_threshold),
1996 },
1997 )
1998 };
1999 logarithmic_moving_average_into_slice(
2000 out_lma,
2001 out_signal,
2002 out_position,
2003 out_momentum,
2004 &input,
2005 Kernel::Auto,
2006 )
2007 .map_err(|e| JsValue::from_str(&e.to_string()))
2008 }
2009}
2010
2011#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2012#[wasm_bindgen]
2013pub fn logarithmic_moving_average_alloc(len: usize) -> *mut f64 {
2014 let mut buf = vec![0.0_f64; len * 4];
2015 let ptr = buf.as_mut_ptr();
2016 std::mem::forget(buf);
2017 ptr
2018}
2019
2020#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2021#[wasm_bindgen]
2022pub fn logarithmic_moving_average_free(ptr: *mut f64, len: usize) {
2023 if ptr.is_null() {
2024 return;
2025 }
2026 unsafe {
2027 let _ = Vec::from_raw_parts(ptr, len * 4, len * 4);
2028 }
2029}
2030
2031#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2032#[wasm_bindgen(js_name = "logarithmic_moving_average_batch")]
2033pub fn logarithmic_moving_average_batch_js(
2034 data: &[f64],
2035 volume: &[f64],
2036 config: JsValue,
2037) -> Result<JsValue, JsValue> {
2038 let config: LogarithmicMovingAverageBatchConfig = serde_wasm_bindgen::from_value(config)
2039 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2040 if config.period_range.len() != 3
2041 || config.steepness_range.len() != 3
2042 || config.smooth_range.len() != 3
2043 || config.momentum_weight_range.len() != 3
2044 || config.long_threshold_range.len() != 3
2045 || config.short_threshold_range.len() != 3
2046 {
2047 return Err(JsValue::from_str(
2048 "Invalid config: ranges must have exactly 3 elements [start, end, step]",
2049 ));
2050 }
2051 let sweep = LogarithmicMovingAverageBatchRange {
2052 period: (
2053 config.period_range[0],
2054 config.period_range[1],
2055 config.period_range[2],
2056 ),
2057 steepness: (
2058 config.steepness_range[0],
2059 config.steepness_range[1],
2060 config.steepness_range[2],
2061 ),
2062 smooth: (
2063 config.smooth_range[0],
2064 config.smooth_range[1],
2065 config.smooth_range[2],
2066 ),
2067 momentum_weight: (
2068 config.momentum_weight_range[0],
2069 config.momentum_weight_range[1],
2070 config.momentum_weight_range[2],
2071 ),
2072 long_threshold: (
2073 config.long_threshold_range[0],
2074 config.long_threshold_range[1],
2075 config.long_threshold_range[2],
2076 ),
2077 short_threshold: (
2078 config.short_threshold_range[0],
2079 config.short_threshold_range[1],
2080 config.short_threshold_range[2],
2081 ),
2082 ma_type: config.ma_type,
2083 };
2084 let batch = logarithmic_moving_average_batch_slice(
2085 data,
2086 if volume.is_empty() {
2087 None
2088 } else {
2089 Some(volume)
2090 },
2091 &sweep,
2092 Kernel::Scalar,
2093 )
2094 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2095 serde_wasm_bindgen::to_value(&LogarithmicMovingAverageBatchJsOutput {
2096 lma: batch.lma,
2097 signal: batch.signal,
2098 position: batch.position,
2099 momentum_confirmed: batch.momentum_confirmed,
2100 rows: batch.rows,
2101 cols: batch.cols,
2102 combos: batch.combos,
2103 })
2104 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
2105}
2106
2107#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2108#[wasm_bindgen]
2109pub fn logarithmic_moving_average_batch_into(
2110 data_ptr: *const f64,
2111 volume_ptr: *const f64,
2112 volume_len: usize,
2113 lma_ptr: *mut f64,
2114 signal_ptr: *mut f64,
2115 position_ptr: *mut f64,
2116 momentum_confirmed_ptr: *mut f64,
2117 len: usize,
2118 period_start: usize,
2119 period_end: usize,
2120 period_step: usize,
2121 steepness_start: f64,
2122 steepness_end: f64,
2123 steepness_step: f64,
2124 smooth_start: usize,
2125 smooth_end: usize,
2126 smooth_step: usize,
2127 momentum_weight_start: f64,
2128 momentum_weight_end: f64,
2129 momentum_weight_step: f64,
2130 long_threshold_start: f64,
2131 long_threshold_end: f64,
2132 long_threshold_step: f64,
2133 short_threshold_start: f64,
2134 short_threshold_end: f64,
2135 short_threshold_step: f64,
2136 ma_type: &str,
2137) -> Result<usize, JsValue> {
2138 if data_ptr.is_null()
2139 || lma_ptr.is_null()
2140 || signal_ptr.is_null()
2141 || position_ptr.is_null()
2142 || momentum_confirmed_ptr.is_null()
2143 {
2144 return Err(JsValue::from_str(
2145 "null pointer passed to logarithmic_moving_average_batch_into",
2146 ));
2147 }
2148 unsafe {
2149 let data = std::slice::from_raw_parts(data_ptr, len);
2150 let volume = if volume_ptr.is_null() || volume_len == 0 {
2151 None
2152 } else {
2153 Some(std::slice::from_raw_parts(volume_ptr, volume_len))
2154 };
2155 let sweep = LogarithmicMovingAverageBatchRange {
2156 period: (period_start, period_end, period_step),
2157 steepness: (steepness_start, steepness_end, steepness_step),
2158 smooth: (smooth_start, smooth_end, smooth_step),
2159 momentum_weight: (
2160 momentum_weight_start,
2161 momentum_weight_end,
2162 momentum_weight_step,
2163 ),
2164 long_threshold: (
2165 long_threshold_start,
2166 long_threshold_end,
2167 long_threshold_step,
2168 ),
2169 short_threshold: (
2170 short_threshold_start,
2171 short_threshold_end,
2172 short_threshold_step,
2173 ),
2174 ma_type: ma_type.to_string(),
2175 };
2176 let combos = expand_grid_logarithmic_moving_average(&sweep)
2177 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2178 let rows = combos.len();
2179 let total = rows
2180 .checked_mul(len)
2181 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
2182 let out_lma = std::slice::from_raw_parts_mut(lma_ptr, total);
2183 let out_signal = std::slice::from_raw_parts_mut(signal_ptr, total);
2184 let out_position = std::slice::from_raw_parts_mut(position_ptr, total);
2185 let out_momentum = std::slice::from_raw_parts_mut(momentum_confirmed_ptr, total);
2186 logarithmic_moving_average_batch_inner_into(
2187 data,
2188 volume,
2189 &sweep,
2190 Kernel::Scalar,
2191 false,
2192 out_lma,
2193 out_signal,
2194 out_position,
2195 out_momentum,
2196 )
2197 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2198 Ok(rows)
2199 }
2200}
2201
2202#[cfg(test)]
2203mod tests {
2204 use super::*;
2205 use crate::indicators::dispatch::{
2206 compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
2207 ParamValue,
2208 };
2209
2210 fn sample_series(len: usize) -> (Vec<f64>, Vec<f64>) {
2211 let data: Vec<f64> = (0..len)
2212 .map(|i| 100.0 + (i as f64) * 0.15 + ((i as f64) * 0.07).sin())
2213 .collect();
2214 let volume: Vec<f64> = (0..len).map(|i| 1000.0 + (i % 17) as f64 * 25.0).collect();
2215 (data, volume)
2216 }
2217
2218 fn assert_close(a: &[f64], b: &[f64], tol: f64) {
2219 assert_eq!(a.len(), b.len());
2220 for (idx, (&x, &y)) in a.iter().zip(b.iter()).enumerate() {
2221 if x.is_nan() && y.is_nan() {
2222 continue;
2223 }
2224 assert!(
2225 (x - y).abs() <= tol,
2226 "mismatch at index {idx}: {x} vs {y} with tol {tol}"
2227 );
2228 }
2229 }
2230
2231 #[test]
2232 fn logarithmic_moving_average_output_contract() -> Result<(), Box<dyn std::error::Error>> {
2233 let (data, _) = sample_series(256);
2234 let input = LogarithmicMovingAverageInput::from_slice(
2235 &data,
2236 LogarithmicMovingAverageParams::default(),
2237 );
2238 let out = logarithmic_moving_average(&input)?;
2239 assert_eq!(out.lma.len(), data.len());
2240 assert_eq!(out.signal.len(), data.len());
2241 assert!(out.lma[..99].iter().all(|v| v.is_nan()));
2242 assert!(out.signal.iter().any(|v| v.is_finite()));
2243 Ok(())
2244 }
2245
2246 #[test]
2247 fn logarithmic_moving_average_vwma_requires_volume() {
2248 let (data, _) = sample_series(128);
2249 let input = LogarithmicMovingAverageInput::from_slice(
2250 &data,
2251 LogarithmicMovingAverageParams {
2252 ma_type: Some("vwma".to_string()),
2253 ..Default::default()
2254 },
2255 );
2256 let err = logarithmic_moving_average(&input).unwrap_err();
2257 assert!(matches!(
2258 err,
2259 LogarithmicMovingAverageError::MissingVolumeForVwma
2260 ));
2261 }
2262
2263 #[test]
2264 fn logarithmic_moving_average_stream_matches_batch() -> Result<(), Box<dyn std::error::Error>> {
2265 let (data, volume) = sample_series(220);
2266 let params = LogarithmicMovingAverageParams {
2267 ma_type: Some("vwma".to_string()),
2268 ..Default::default()
2269 };
2270 let input =
2271 LogarithmicMovingAverageInput::from_slice_with_volume(&data, &volume, params.clone());
2272 let batch = logarithmic_moving_average(&input)?;
2273 let mut stream = LogarithmicMovingAverageStream::try_new(params)?;
2274 let mut streamed_signal = Vec::with_capacity(data.len());
2275 for i in 0..data.len() {
2276 let value = stream.update(data[i], Some(volume[i]));
2277 streamed_signal.push(value.map(|(_, signal, _, _)| signal).unwrap_or(f64::NAN));
2278 }
2279 assert_close(&batch.signal, &streamed_signal, 1e-10);
2280 Ok(())
2281 }
2282
2283 #[test]
2284 fn logarithmic_moving_average_batch_single_param_matches_single(
2285 ) -> Result<(), Box<dyn std::error::Error>> {
2286 let (data, volume) = sample_series(196);
2287 let single =
2288 logarithmic_moving_average(&LogarithmicMovingAverageInput::from_slice_with_volume(
2289 &data,
2290 &volume,
2291 LogarithmicMovingAverageParams {
2292 ma_type: Some("vwma".to_string()),
2293 ..Default::default()
2294 },
2295 ))?;
2296 let batch = logarithmic_moving_average_batch_with_kernel(
2297 &data,
2298 Some(&volume),
2299 &LogarithmicMovingAverageBatchRange {
2300 ma_type: "vwma".to_string(),
2301 ..Default::default()
2302 },
2303 Kernel::Auto,
2304 )?;
2305 assert_eq!(batch.rows, 1);
2306 assert_eq!(batch.cols, data.len());
2307 assert_close(&single.signal, &batch.signal[..data.len()], 1e-10);
2308 Ok(())
2309 }
2310
2311 #[test]
2312 fn logarithmic_moving_average_dispatch_matches_direct() -> Result<(), Box<dyn std::error::Error>>
2313 {
2314 let (data, volume) = sample_series(192);
2315 let direct =
2316 logarithmic_moving_average(&LogarithmicMovingAverageInput::from_slice_with_volume(
2317 &data,
2318 &volume,
2319 LogarithmicMovingAverageParams {
2320 ma_type: Some("vwma".to_string()),
2321 ..Default::default()
2322 },
2323 ))?;
2324 let params = vec![
2325 ParamKV {
2326 key: "ma_type",
2327 value: ParamValue::EnumString("vwma"),
2328 },
2329 ParamKV {
2330 key: "period",
2331 value: ParamValue::Int(DEFAULT_PERIOD as i64),
2332 },
2333 ParamKV {
2334 key: "steepness",
2335 value: ParamValue::Float(DEFAULT_STEEPNESS),
2336 },
2337 ParamKV {
2338 key: "smooth",
2339 value: ParamValue::Int(DEFAULT_SMOOTH as i64),
2340 },
2341 ParamKV {
2342 key: "momentum_weight",
2343 value: ParamValue::Float(DEFAULT_MOMENTUM_WEIGHT),
2344 },
2345 ParamKV {
2346 key: "long_threshold",
2347 value: ParamValue::Float(DEFAULT_LONG_THRESHOLD),
2348 },
2349 ParamKV {
2350 key: "short_threshold",
2351 value: ParamValue::Float(DEFAULT_SHORT_THRESHOLD),
2352 },
2353 ];
2354 let combos = [IndicatorParamSet { params: ¶ms }];
2355 let out = compute_cpu_batch(IndicatorBatchRequest {
2356 indicator_id: "logarithmic_moving_average",
2357 output_id: Some("signal"),
2358 data: IndicatorDataRef::CloseVolume {
2359 close: &data,
2360 volume: &volume,
2361 },
2362 combos: &combos,
2363 kernel: Kernel::Auto,
2364 })?;
2365 let got = out.values_f64.expect("expected f64 output");
2366 assert_close(&direct.signal, &got, 1e-10);
2367 Ok(())
2368 }
2369}