Skip to main content

vector_ta/indicators/moving_averages/
logarithmic_moving_average.rs

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        &params,
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: &params }];
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}