Skip to main content

vector_ta/indicators/
cyberpunk_value_trend_analyzer.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
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22#[cfg(not(target_arch = "wasm32"))]
23use rayon::prelude::*;
24use std::collections::VecDeque;
25use thiserror::Error;
26
27const DEFAULT_ENTRY_LEVEL: usize = 30;
28const DEFAULT_EXIT_LEVEL: usize = 75;
29const SMA13_WINDOW: usize = 13;
30const RANGE75_WINDOW: usize = 75;
31const OUTPUTS: usize = 6;
32
33#[derive(Debug, Clone)]
34pub enum CyberpunkValueTrendAnalyzerData<'a> {
35    Candles {
36        candles: &'a Candles,
37    },
38    Slices {
39        open: &'a [f64],
40        high: &'a [f64],
41        low: &'a [f64],
42        close: &'a [f64],
43    },
44}
45
46#[derive(Debug, Clone)]
47pub struct CyberpunkValueTrendAnalyzerOutput {
48    pub value_trend: Vec<f64>,
49    pub value_trend_lag: Vec<f64>,
50    pub deviation_index: Vec<f64>,
51    pub overbought_signal: Vec<f64>,
52    pub buy_signal: Vec<f64>,
53    pub sell_signal: Vec<f64>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg_attr(
58    all(target_arch = "wasm32", feature = "wasm"),
59    derive(Serialize, Deserialize)
60)]
61pub struct CyberpunkValueTrendAnalyzerParams {
62    pub entry_level: Option<usize>,
63    pub exit_level: Option<usize>,
64}
65
66impl Default for CyberpunkValueTrendAnalyzerParams {
67    fn default() -> Self {
68        Self {
69            entry_level: Some(DEFAULT_ENTRY_LEVEL),
70            exit_level: Some(DEFAULT_EXIT_LEVEL),
71        }
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct CyberpunkValueTrendAnalyzerInput<'a> {
77    pub data: CyberpunkValueTrendAnalyzerData<'a>,
78    pub params: CyberpunkValueTrendAnalyzerParams,
79}
80
81impl<'a> CyberpunkValueTrendAnalyzerInput<'a> {
82    #[inline]
83    pub fn from_candles(candles: &'a Candles, params: CyberpunkValueTrendAnalyzerParams) -> Self {
84        Self {
85            data: CyberpunkValueTrendAnalyzerData::Candles { candles },
86            params,
87        }
88    }
89
90    #[inline]
91    pub fn from_slices(
92        open: &'a [f64],
93        high: &'a [f64],
94        low: &'a [f64],
95        close: &'a [f64],
96        params: CyberpunkValueTrendAnalyzerParams,
97    ) -> Self {
98        Self {
99            data: CyberpunkValueTrendAnalyzerData::Slices {
100                open,
101                high,
102                low,
103                close,
104            },
105            params,
106        }
107    }
108
109    #[inline]
110    pub fn with_default_candles(candles: &'a Candles) -> Self {
111        Self::from_candles(candles, CyberpunkValueTrendAnalyzerParams::default())
112    }
113
114    #[inline]
115    pub fn get_entry_level(&self) -> usize {
116        self.params.entry_level.unwrap_or(DEFAULT_ENTRY_LEVEL)
117    }
118
119    #[inline]
120    pub fn get_exit_level(&self) -> usize {
121        self.params.exit_level.unwrap_or(DEFAULT_EXIT_LEVEL)
122    }
123}
124
125#[derive(Debug, Clone, Copy)]
126pub struct CyberpunkValueTrendAnalyzerBuilder {
127    entry_level: Option<usize>,
128    exit_level: Option<usize>,
129    kernel: Kernel,
130}
131
132impl Default for CyberpunkValueTrendAnalyzerBuilder {
133    fn default() -> Self {
134        Self {
135            entry_level: None,
136            exit_level: None,
137            kernel: Kernel::Auto,
138        }
139    }
140}
141
142impl CyberpunkValueTrendAnalyzerBuilder {
143    #[inline(always)]
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    #[inline(always)]
149    pub fn entry_level(mut self, value: usize) -> Self {
150        self.entry_level = Some(value);
151        self
152    }
153
154    #[inline(always)]
155    pub fn exit_level(mut self, value: usize) -> Self {
156        self.exit_level = Some(value);
157        self
158    }
159
160    #[inline(always)]
161    pub fn kernel(mut self, value: Kernel) -> Self {
162        self.kernel = value;
163        self
164    }
165}
166
167#[derive(Debug, Error)]
168pub enum CyberpunkValueTrendAnalyzerError {
169    #[error("cyberpunk_value_trend_analyzer: Input data slice is empty.")]
170    EmptyInputData,
171    #[error(
172        "cyberpunk_value_trend_analyzer: Input length mismatch: open = {open_len}, high = {high_len}, low = {low_len}, close = {close_len}"
173    )]
174    InputLengthMismatch {
175        open_len: usize,
176        high_len: usize,
177        low_len: usize,
178        close_len: usize,
179    },
180    #[error("cyberpunk_value_trend_analyzer: All values are NaN.")]
181    AllValuesNaN,
182    #[error("cyberpunk_value_trend_analyzer: Invalid entry_level: {entry_level}")]
183    InvalidEntryLevel { entry_level: usize },
184    #[error("cyberpunk_value_trend_analyzer: Invalid exit_level: {exit_level}")]
185    InvalidExitLevel { exit_level: usize },
186    #[error(
187        "cyberpunk_value_trend_analyzer: Not enough valid data: needed = {needed}, valid = {valid}"
188    )]
189    NotEnoughValidData { needed: usize, valid: usize },
190    #[error(
191        "cyberpunk_value_trend_analyzer: Output length mismatch: expected = {expected}, got = {got}"
192    )]
193    OutputLengthMismatch { expected: usize, got: usize },
194    #[error(
195        "cyberpunk_value_trend_analyzer: Output length mismatch: dst = {dst_len}, expected = {expected_len}"
196    )]
197    MismatchedOutputLen { dst_len: usize, expected_len: usize },
198    #[error(
199        "cyberpunk_value_trend_analyzer: Invalid range: start={start}, end={end}, step={step}"
200    )]
201    InvalidRange {
202        start: String,
203        end: String,
204        step: String,
205    },
206    #[error("cyberpunk_value_trend_analyzer: Invalid kernel: {0:?}")]
207    InvalidKernel(Kernel),
208    #[error("cyberpunk_value_trend_analyzer: Invalid kernel for batch: {0:?}")]
209    InvalidKernelForBatch(Kernel),
210    #[error("cyberpunk_value_trend_analyzer: Invalid input: {msg}")]
211    InvalidInput { msg: String },
212}
213
214#[derive(Debug, Clone)]
215struct RollingSum {
216    buf: [f64; SMA13_WINDOW],
217    pos: usize,
218    count: usize,
219    sum: f64,
220}
221
222impl Default for RollingSum {
223    fn default() -> Self {
224        Self {
225            buf: [0.0; SMA13_WINDOW],
226            pos: 0,
227            count: 0,
228            sum: 0.0,
229        }
230    }
231}
232
233impl RollingSum {
234    #[inline(always)]
235    fn reset(&mut self) {
236        self.pos = 0;
237        self.count = 0;
238        self.sum = 0.0;
239    }
240
241    #[inline(always)]
242    fn push(&mut self, value: f64) -> Option<f64> {
243        if self.count < SMA13_WINDOW {
244            self.buf[self.pos] = value;
245            self.pos = (self.pos + 1) % SMA13_WINDOW;
246            self.count += 1;
247            self.sum += value;
248            if self.count == SMA13_WINDOW {
249                Some(self.sum / SMA13_WINDOW as f64)
250            } else {
251                None
252            }
253        } else {
254            let old = self.buf[self.pos];
255            self.buf[self.pos] = value;
256            self.pos = (self.pos + 1) % SMA13_WINDOW;
257            self.sum += value - old;
258            Some(self.sum / SMA13_WINDOW as f64)
259        }
260    }
261}
262
263#[derive(Debug, Clone, Default)]
264struct MonotonicQueue {
265    data: VecDeque<(usize, f64)>,
266}
267
268impl MonotonicQueue {
269    #[inline(always)]
270    fn reset(&mut self) {
271        self.data.clear();
272    }
273
274    #[inline(always)]
275    fn push_min(&mut self, index: usize, value: f64) {
276        while let Some((_, back)) = self.data.back() {
277            if *back <= value {
278                break;
279            }
280            self.data.pop_back();
281        }
282        self.data.push_back((index, value));
283    }
284
285    #[inline(always)]
286    fn push_max(&mut self, index: usize, value: f64) {
287        while let Some((_, back)) = self.data.back() {
288            if *back >= value {
289                break;
290            }
291            self.data.pop_back();
292        }
293        self.data.push_back((index, value));
294    }
295
296    #[inline(always)]
297    fn prune(&mut self, min_index: usize) {
298        while let Some((front_index, _)) = self.data.front() {
299            if *front_index >= min_index {
300                break;
301            }
302            self.data.pop_front();
303        }
304    }
305
306    #[inline(always)]
307    fn current(&self) -> Option<f64> {
308        self.data.front().map(|(_, value)| *value)
309    }
310}
311
312#[derive(Debug, Clone, Copy)]
313struct WeightedSmaState {
314    alpha: f64,
315    value: Option<f64>,
316}
317
318impl WeightedSmaState {
319    #[inline(always)]
320    fn new(length: usize) -> Self {
321        Self {
322            alpha: 1.0 / length as f64,
323            value: None,
324        }
325    }
326
327    #[inline(always)]
328    fn reset(&mut self) {
329        self.value = None;
330    }
331
332    #[inline(always)]
333    fn update(&mut self, source: f64) -> f64 {
334        if !source.is_finite() {
335            self.value = None;
336            return f64::NAN;
337        }
338        let next = match self.value {
339            Some(prev) => self.alpha * source + (1.0 - self.alpha) * prev,
340            None => source,
341        };
342        self.value = Some(next);
343        next
344    }
345}
346
347#[inline(always)]
348fn is_valid_ohlc(open: f64, high: f64, low: f64, close: f64) -> bool {
349    open.is_finite() && high.is_finite() && low.is_finite() && close.is_finite()
350}
351
352#[inline(always)]
353fn longest_valid_run(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> usize {
354    let mut best = 0usize;
355    let mut current = 0usize;
356    for (((&o, &h), &l), &c) in open
357        .iter()
358        .zip(high.iter())
359        .zip(low.iter())
360        .zip(close.iter())
361    {
362        if is_valid_ohlc(o, h, l, c) {
363            current += 1;
364            best = best.max(current);
365        } else {
366            current = 0;
367        }
368    }
369    best
370}
371
372#[inline(always)]
373fn input_slices<'a>(
374    input: &'a CyberpunkValueTrendAnalyzerInput<'a>,
375) -> (&'a [f64], &'a [f64], &'a [f64], &'a [f64]) {
376    match &input.data {
377        CyberpunkValueTrendAnalyzerData::Candles { candles } => (
378            candles.open.as_slice(),
379            candles.high.as_slice(),
380            candles.low.as_slice(),
381            candles.close.as_slice(),
382        ),
383        CyberpunkValueTrendAnalyzerData::Slices {
384            open,
385            high,
386            low,
387            close,
388        } => (open, high, low, close),
389    }
390}
391
392#[inline(always)]
393fn validate_params_only(
394    entry_level: usize,
395    exit_level: usize,
396) -> Result<(), CyberpunkValueTrendAnalyzerError> {
397    if !(1..=100).contains(&entry_level) {
398        return Err(CyberpunkValueTrendAnalyzerError::InvalidEntryLevel { entry_level });
399    }
400    if !(1..=100).contains(&exit_level) {
401        return Err(CyberpunkValueTrendAnalyzerError::InvalidExitLevel { exit_level });
402    }
403    Ok(())
404}
405
406#[inline(always)]
407fn validate_common(
408    open: &[f64],
409    high: &[f64],
410    low: &[f64],
411    close: &[f64],
412    entry_level: usize,
413    exit_level: usize,
414) -> Result<(), CyberpunkValueTrendAnalyzerError> {
415    if open.is_empty() || high.is_empty() || low.is_empty() || close.is_empty() {
416        return Err(CyberpunkValueTrendAnalyzerError::EmptyInputData);
417    }
418    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
419        return Err(CyberpunkValueTrendAnalyzerError::InputLengthMismatch {
420            open_len: open.len(),
421            high_len: high.len(),
422            low_len: low.len(),
423            close_len: close.len(),
424        });
425    }
426    validate_params_only(entry_level, exit_level)?;
427    let longest = longest_valid_run(open, high, low, close);
428    if longest == 0 {
429        return Err(CyberpunkValueTrendAnalyzerError::AllValuesNaN);
430    }
431    if longest < RANGE75_WINDOW {
432        return Err(CyberpunkValueTrendAnalyzerError::NotEnoughValidData {
433            needed: RANGE75_WINDOW,
434            valid: longest,
435        });
436    }
437    Ok(())
438}
439
440#[inline(always)]
441fn normalize_single_kernel(kernel: Kernel) -> Result<Kernel, CyberpunkValueTrendAnalyzerError> {
442    match kernel {
443        Kernel::Auto => Ok(detect_best_kernel()),
444        Kernel::Scalar | Kernel::ScalarBatch => Ok(Kernel::Scalar),
445        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
446        Kernel::Avx2 | Kernel::Avx2Batch => Ok(Kernel::Avx2),
447        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
448        Kernel::Avx512 | Kernel::Avx512Batch => Ok(Kernel::Avx512),
449        other => Err(CyberpunkValueTrendAnalyzerError::InvalidKernel(other)),
450    }
451}
452
453#[inline(always)]
454fn normalize_batch_kernel(kernel: Kernel) -> Result<Kernel, CyberpunkValueTrendAnalyzerError> {
455    match kernel {
456        Kernel::Auto => Ok(detect_best_batch_kernel()),
457        Kernel::Scalar | Kernel::ScalarBatch => Ok(Kernel::ScalarBatch),
458        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
459        Kernel::Avx2 | Kernel::Avx2Batch => Ok(Kernel::Avx2Batch),
460        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
461        Kernel::Avx512 | Kernel::Avx512Batch => Ok(Kernel::Avx512Batch),
462        other => Err(CyberpunkValueTrendAnalyzerError::InvalidKernelForBatch(
463            other,
464        )),
465    }
466}
467
468#[inline(always)]
469fn axis_usize(
470    start: usize,
471    end: usize,
472    step: usize,
473) -> Result<Vec<usize>, CyberpunkValueTrendAnalyzerError> {
474    validate_params_only(start, start)?;
475    validate_params_only(end, end)?;
476    if step == 0 {
477        return Ok(vec![start]);
478    }
479    if start > end {
480        return Err(CyberpunkValueTrendAnalyzerError::InvalidRange {
481            start: start.to_string(),
482            end: end.to_string(),
483            step: step.to_string(),
484        });
485    }
486    let mut out = Vec::new();
487    let mut current = start;
488    loop {
489        out.push(current);
490        if current >= end {
491            break;
492        }
493        let next = current.checked_add(step).ok_or_else(|| {
494            CyberpunkValueTrendAnalyzerError::InvalidRange {
495                start: start.to_string(),
496                end: end.to_string(),
497                step: step.to_string(),
498            }
499        })?;
500        if next <= current {
501            return Err(CyberpunkValueTrendAnalyzerError::InvalidRange {
502                start: start.to_string(),
503                end: end.to_string(),
504                step: step.to_string(),
505            });
506        }
507        current = next.min(end);
508    }
509    Ok(out)
510}
511
512#[inline(always)]
513fn reset_core_state(
514    sma13: &mut RollingSum,
515    lowest75: &mut MonotonicQueue,
516    highest75: &mut MonotonicQueue,
517    close_norm_sma: &mut WeightedSmaState,
518    smooth5: &mut WeightedSmaState,
519    valid_run: &mut usize,
520    prev_value_trend: &mut f64,
521) {
522    sma13.reset();
523    lowest75.reset();
524    highest75.reset();
525    close_norm_sma.reset();
526    smooth5.reset();
527    *valid_run = 0;
528    *prev_value_trend = f64::NAN;
529}
530
531#[inline(always)]
532fn compute_row(
533    open: &[f64],
534    high: &[f64],
535    low: &[f64],
536    close: &[f64],
537    entry_level: usize,
538    exit_level: usize,
539    out_value_trend: &mut [f64],
540    out_value_trend_lag: &mut [f64],
541    out_deviation_index: &mut [f64],
542    out_overbought_signal: &mut [f64],
543    out_buy_signal: &mut [f64],
544    out_sell_signal: &mut [f64],
545) {
546    let entry_level = entry_level as f64;
547    let exit_level = exit_level as f64;
548    let mut sma13 = RollingSum::default();
549    let mut lowest75 = MonotonicQueue::default();
550    let mut highest75 = MonotonicQueue::default();
551    let mut close_norm_sma = WeightedSmaState::new(20);
552    let mut smooth5 = WeightedSmaState::new(5);
553    let mut valid_run = 0usize;
554    let mut prev_value_trend = f64::NAN;
555
556    for i in 0..close.len() {
557        let o = open[i];
558        let h = high[i];
559        let l = low[i];
560        let c = close[i];
561
562        if !is_valid_ohlc(o, h, l, c) {
563            reset_core_state(
564                &mut sma13,
565                &mut lowest75,
566                &mut highest75,
567                &mut close_norm_sma,
568                &mut smooth5,
569                &mut valid_run,
570                &mut prev_value_trend,
571            );
572            continue;
573        }
574
575        valid_run += 1;
576        let ma13 = sma13.push(c);
577        lowest75.push_min(i, l);
578        highest75.push_max(i, h);
579        let min_index = i.saturating_sub(RANGE75_WINDOW - 1);
580        lowest75.prune(min_index);
581        highest75.prune(min_index);
582
583        if prev_value_trend.is_finite() {
584            out_value_trend_lag[i] = prev_value_trend;
585        }
586
587        let mut current_value_trend = f64::NAN;
588        if valid_run >= RANGE75_WINDOW {
589            let range_low = lowest75.current().unwrap_or(f64::NAN);
590            let range_high = highest75.current().unwrap_or(f64::NAN);
591            let range = range_high - range_low;
592            if range.is_finite() && range > 0.0 {
593                let close_norm = (c - range_low) * 100.0 / range;
594                let close_norm_avg = close_norm_sma.update(close_norm);
595                let smooth = smooth5.update(close_norm_avg);
596                if close_norm_avg.is_finite() && smooth.is_finite() {
597                    current_value_trend = 3.0 * close_norm_avg - 2.0 * smooth;
598                    out_value_trend[i] = current_value_trend;
599                    out_buy_signal[i] = 0.0;
600                    out_sell_signal[i] = 0.0;
601                }
602            } else {
603                close_norm_sma.reset();
604                smooth5.reset();
605            }
606        }
607
608        if let Some(avg13) = ma13 {
609            if avg13.is_finite() && avg13 != 0.0 {
610                let deviation_index = 100.0 - (((c - avg13) / avg13) * 100.0).abs();
611                out_deviation_index[i] = deviation_index;
612                if current_value_trend.is_finite() && current_value_trend > deviation_index {
613                    out_overbought_signal[i] = deviation_index;
614                }
615            }
616        }
617
618        if current_value_trend.is_finite() && prev_value_trend.is_finite() {
619            if prev_value_trend <= entry_level && current_value_trend > entry_level {
620                out_buy_signal[i] = 1.0;
621            }
622            if prev_value_trend >= exit_level && current_value_trend < exit_level {
623                out_sell_signal[i] = 1.0;
624            }
625        }
626
627        prev_value_trend = current_value_trend;
628    }
629}
630
631pub fn cyberpunk_value_trend_analyzer(
632    input: &CyberpunkValueTrendAnalyzerInput,
633) -> Result<CyberpunkValueTrendAnalyzerOutput, CyberpunkValueTrendAnalyzerError> {
634    cyberpunk_value_trend_analyzer_with_kernel(input, Kernel::Auto)
635}
636
637pub fn cyberpunk_value_trend_analyzer_with_kernel(
638    input: &CyberpunkValueTrendAnalyzerInput,
639    kernel: Kernel,
640) -> Result<CyberpunkValueTrendAnalyzerOutput, CyberpunkValueTrendAnalyzerError> {
641    let (open, high, low, close) = input_slices(input);
642    let entry_level = input.get_entry_level();
643    let exit_level = input.get_exit_level();
644    validate_common(open, high, low, close, entry_level, exit_level)?;
645    let _chosen = normalize_single_kernel(kernel)?;
646
647    let mut value_trend = alloc_with_nan_prefix(close.len(), RANGE75_WINDOW - 1);
648    let mut value_trend_lag = alloc_with_nan_prefix(close.len(), RANGE75_WINDOW);
649    let mut deviation_index = alloc_with_nan_prefix(close.len(), SMA13_WINDOW - 1);
650    let mut overbought_signal = alloc_with_nan_prefix(close.len(), RANGE75_WINDOW - 1);
651    let mut buy_signal = alloc_with_nan_prefix(close.len(), RANGE75_WINDOW - 1);
652    let mut sell_signal = alloc_with_nan_prefix(close.len(), RANGE75_WINDOW - 1);
653    value_trend.fill(f64::NAN);
654    value_trend_lag.fill(f64::NAN);
655    deviation_index.fill(f64::NAN);
656    overbought_signal.fill(f64::NAN);
657    buy_signal.fill(f64::NAN);
658    sell_signal.fill(f64::NAN);
659
660    compute_row(
661        open,
662        high,
663        low,
664        close,
665        entry_level,
666        exit_level,
667        &mut value_trend,
668        &mut value_trend_lag,
669        &mut deviation_index,
670        &mut overbought_signal,
671        &mut buy_signal,
672        &mut sell_signal,
673    );
674
675    Ok(CyberpunkValueTrendAnalyzerOutput {
676        value_trend,
677        value_trend_lag,
678        deviation_index,
679        overbought_signal,
680        buy_signal,
681        sell_signal,
682    })
683}
684
685pub fn cyberpunk_value_trend_analyzer_into_slice(
686    out_value_trend: &mut [f64],
687    out_value_trend_lag: &mut [f64],
688    out_deviation_index: &mut [f64],
689    out_overbought_signal: &mut [f64],
690    out_buy_signal: &mut [f64],
691    out_sell_signal: &mut [f64],
692    input: &CyberpunkValueTrendAnalyzerInput,
693    kernel: Kernel,
694) -> Result<(), CyberpunkValueTrendAnalyzerError> {
695    let (open, high, low, close) = input_slices(input);
696    let entry_level = input.get_entry_level();
697    let exit_level = input.get_exit_level();
698    validate_common(open, high, low, close, entry_level, exit_level)?;
699    let _chosen = normalize_single_kernel(kernel)?;
700
701    for out in [
702        &mut *out_value_trend,
703        &mut *out_value_trend_lag,
704        &mut *out_deviation_index,
705        &mut *out_overbought_signal,
706        &mut *out_buy_signal,
707        &mut *out_sell_signal,
708    ] {
709        if out.len() != close.len() {
710            return Err(CyberpunkValueTrendAnalyzerError::OutputLengthMismatch {
711                expected: close.len(),
712                got: out.len(),
713            });
714        }
715        out.fill(f64::NAN);
716    }
717
718    compute_row(
719        open,
720        high,
721        low,
722        close,
723        entry_level,
724        exit_level,
725        out_value_trend,
726        out_value_trend_lag,
727        out_deviation_index,
728        out_overbought_signal,
729        out_buy_signal,
730        out_sell_signal,
731    );
732    Ok(())
733}
734
735#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
736pub fn cyberpunk_value_trend_analyzer_into(
737    input: &CyberpunkValueTrendAnalyzerInput,
738    out_value_trend: &mut [f64],
739    out_value_trend_lag: &mut [f64],
740    out_deviation_index: &mut [f64],
741    out_overbought_signal: &mut [f64],
742    out_buy_signal: &mut [f64],
743    out_sell_signal: &mut [f64],
744) -> Result<(), CyberpunkValueTrendAnalyzerError> {
745    cyberpunk_value_trend_analyzer_into_slice(
746        out_value_trend,
747        out_value_trend_lag,
748        out_deviation_index,
749        out_overbought_signal,
750        out_buy_signal,
751        out_sell_signal,
752        input,
753        Kernel::Auto,
754    )
755}
756
757#[derive(Debug, Clone, Copy)]
758pub struct CyberpunkValueTrendAnalyzerBatchRange {
759    pub entry_level: (usize, usize, usize),
760    pub exit_level: (usize, usize, usize),
761}
762
763impl Default for CyberpunkValueTrendAnalyzerBatchRange {
764    fn default() -> Self {
765        Self {
766            entry_level: (DEFAULT_ENTRY_LEVEL, DEFAULT_ENTRY_LEVEL, 0),
767            exit_level: (DEFAULT_EXIT_LEVEL, DEFAULT_EXIT_LEVEL, 0),
768        }
769    }
770}
771
772#[derive(Debug, Clone)]
773pub struct CyberpunkValueTrendAnalyzerBatchOutput {
774    pub value_trend: Vec<f64>,
775    pub value_trend_lag: Vec<f64>,
776    pub deviation_index: Vec<f64>,
777    pub overbought_signal: Vec<f64>,
778    pub buy_signal: Vec<f64>,
779    pub sell_signal: Vec<f64>,
780    pub combos: Vec<CyberpunkValueTrendAnalyzerParams>,
781    pub rows: usize,
782    pub cols: usize,
783}
784
785#[derive(Debug, Clone, Copy)]
786pub struct CyberpunkValueTrendAnalyzerBatchBuilder {
787    range: CyberpunkValueTrendAnalyzerBatchRange,
788    kernel: Kernel,
789}
790
791impl Default for CyberpunkValueTrendAnalyzerBatchBuilder {
792    fn default() -> Self {
793        Self {
794            range: CyberpunkValueTrendAnalyzerBatchRange::default(),
795            kernel: Kernel::Auto,
796        }
797    }
798}
799
800impl CyberpunkValueTrendAnalyzerBatchBuilder {
801    #[inline(always)]
802    pub fn new() -> Self {
803        Self::default()
804    }
805
806    #[inline(always)]
807    pub fn kernel(mut self, value: Kernel) -> Self {
808        self.kernel = value;
809        self
810    }
811
812    #[inline(always)]
813    pub fn entry_level_range(mut self, start: usize, end: usize, step: usize) -> Self {
814        self.range.entry_level = (start, end, step);
815        self
816    }
817
818    #[inline(always)]
819    pub fn entry_level_static(mut self, value: usize) -> Self {
820        self.range.entry_level = (value, value, 0);
821        self
822    }
823
824    #[inline(always)]
825    pub fn exit_level_range(mut self, start: usize, end: usize, step: usize) -> Self {
826        self.range.exit_level = (start, end, step);
827        self
828    }
829
830    #[inline(always)]
831    pub fn exit_level_static(mut self, value: usize) -> Self {
832        self.range.exit_level = (value, value, 0);
833        self
834    }
835
836    #[inline(always)]
837    pub fn apply_slices(
838        self,
839        open: &[f64],
840        high: &[f64],
841        low: &[f64],
842        close: &[f64],
843    ) -> Result<CyberpunkValueTrendAnalyzerBatchOutput, CyberpunkValueTrendAnalyzerError> {
844        cyberpunk_value_trend_analyzer_batch_with_kernel(
845            open,
846            high,
847            low,
848            close,
849            &self.range,
850            self.kernel,
851        )
852    }
853
854    #[inline(always)]
855    pub fn apply_candles(
856        self,
857        candles: &Candles,
858    ) -> Result<CyberpunkValueTrendAnalyzerBatchOutput, CyberpunkValueTrendAnalyzerError> {
859        cyberpunk_value_trend_analyzer_batch_with_kernel(
860            candles.open.as_slice(),
861            candles.high.as_slice(),
862            candles.low.as_slice(),
863            candles.close.as_slice(),
864            &self.range,
865            self.kernel,
866        )
867    }
868}
869
870#[inline(always)]
871fn expand_grid_checked(
872    range: &CyberpunkValueTrendAnalyzerBatchRange,
873) -> Result<Vec<CyberpunkValueTrendAnalyzerParams>, CyberpunkValueTrendAnalyzerError> {
874    let entry_levels = axis_usize(
875        range.entry_level.0,
876        range.entry_level.1,
877        range.entry_level.2,
878    )?;
879    let exit_levels = axis_usize(range.exit_level.0, range.exit_level.1, range.exit_level.2)?;
880    let mut combos = Vec::new();
881    for &entry_level in &entry_levels {
882        for &exit_level in &exit_levels {
883            validate_params_only(entry_level, exit_level)?;
884            combos.push(CyberpunkValueTrendAnalyzerParams {
885                entry_level: Some(entry_level),
886                exit_level: Some(exit_level),
887            });
888        }
889    }
890    Ok(combos)
891}
892
893pub fn expand_grid_cyberpunk_value_trend_analyzer(
894    range: &CyberpunkValueTrendAnalyzerBatchRange,
895) -> Vec<CyberpunkValueTrendAnalyzerParams> {
896    expand_grid_checked(range).unwrap_or_default()
897}
898
899pub fn cyberpunk_value_trend_analyzer_batch_with_kernel(
900    open: &[f64],
901    high: &[f64],
902    low: &[f64],
903    close: &[f64],
904    sweep: &CyberpunkValueTrendAnalyzerBatchRange,
905    kernel: Kernel,
906) -> Result<CyberpunkValueTrendAnalyzerBatchOutput, CyberpunkValueTrendAnalyzerError> {
907    let combos = expand_grid_checked(sweep)?;
908    let rows = combos.len();
909    let cols = close.len();
910    let total =
911        rows.checked_mul(cols)
912            .ok_or_else(|| CyberpunkValueTrendAnalyzerError::InvalidInput {
913                msg: "cyberpunk_value_trend_analyzer: rows*cols overflow in batch".to_string(),
914            })?;
915
916    let mut value_trend_mu = make_uninit_matrix(rows, cols);
917    let mut value_trend_lag_mu = make_uninit_matrix(rows, cols);
918    let mut deviation_index_mu = make_uninit_matrix(rows, cols);
919    let mut overbought_signal_mu = make_uninit_matrix(rows, cols);
920    let mut buy_signal_mu = make_uninit_matrix(rows, cols);
921    let mut sell_signal_mu = make_uninit_matrix(rows, cols);
922
923    let mut value_trend_guard = core::mem::ManuallyDrop::new(value_trend_mu);
924    let mut value_trend_lag_guard = core::mem::ManuallyDrop::new(value_trend_lag_mu);
925    let mut deviation_index_guard = core::mem::ManuallyDrop::new(deviation_index_mu);
926    let mut overbought_signal_guard = core::mem::ManuallyDrop::new(overbought_signal_mu);
927    let mut buy_signal_guard = core::mem::ManuallyDrop::new(buy_signal_mu);
928    let mut sell_signal_guard = core::mem::ManuallyDrop::new(sell_signal_mu);
929
930    let value_trend = unsafe {
931        std::slice::from_raw_parts_mut(value_trend_guard.as_mut_ptr() as *mut f64, total)
932    };
933    let value_trend_lag = unsafe {
934        std::slice::from_raw_parts_mut(value_trend_lag_guard.as_mut_ptr() as *mut f64, total)
935    };
936    let deviation_index = unsafe {
937        std::slice::from_raw_parts_mut(deviation_index_guard.as_mut_ptr() as *mut f64, total)
938    };
939    let overbought_signal = unsafe {
940        std::slice::from_raw_parts_mut(overbought_signal_guard.as_mut_ptr() as *mut f64, total)
941    };
942    let buy_signal =
943        unsafe { std::slice::from_raw_parts_mut(buy_signal_guard.as_mut_ptr() as *mut f64, total) };
944    let sell_signal = unsafe {
945        std::slice::from_raw_parts_mut(sell_signal_guard.as_mut_ptr() as *mut f64, total)
946    };
947
948    cyberpunk_value_trend_analyzer_batch_inner_into(
949        open,
950        high,
951        low,
952        close,
953        sweep,
954        kernel,
955        true,
956        value_trend,
957        value_trend_lag,
958        deviation_index,
959        overbought_signal,
960        buy_signal,
961        sell_signal,
962    )?;
963
964    let value_trend = unsafe {
965        Vec::from_raw_parts(
966            value_trend_guard.as_mut_ptr() as *mut f64,
967            value_trend_guard.len(),
968            value_trend_guard.capacity(),
969        )
970    };
971    let value_trend_lag = unsafe {
972        Vec::from_raw_parts(
973            value_trend_lag_guard.as_mut_ptr() as *mut f64,
974            value_trend_lag_guard.len(),
975            value_trend_lag_guard.capacity(),
976        )
977    };
978    let deviation_index = unsafe {
979        Vec::from_raw_parts(
980            deviation_index_guard.as_mut_ptr() as *mut f64,
981            deviation_index_guard.len(),
982            deviation_index_guard.capacity(),
983        )
984    };
985    let overbought_signal = unsafe {
986        Vec::from_raw_parts(
987            overbought_signal_guard.as_mut_ptr() as *mut f64,
988            overbought_signal_guard.len(),
989            overbought_signal_guard.capacity(),
990        )
991    };
992    let buy_signal = unsafe {
993        Vec::from_raw_parts(
994            buy_signal_guard.as_mut_ptr() as *mut f64,
995            buy_signal_guard.len(),
996            buy_signal_guard.capacity(),
997        )
998    };
999    let sell_signal = unsafe {
1000        Vec::from_raw_parts(
1001            sell_signal_guard.as_mut_ptr() as *mut f64,
1002            sell_signal_guard.len(),
1003            sell_signal_guard.capacity(),
1004        )
1005    };
1006
1007    Ok(CyberpunkValueTrendAnalyzerBatchOutput {
1008        value_trend,
1009        value_trend_lag,
1010        deviation_index,
1011        overbought_signal,
1012        buy_signal,
1013        sell_signal,
1014        combos,
1015        rows,
1016        cols,
1017    })
1018}
1019
1020pub fn cyberpunk_value_trend_analyzer_batch_slice(
1021    open: &[f64],
1022    high: &[f64],
1023    low: &[f64],
1024    close: &[f64],
1025    sweep: &CyberpunkValueTrendAnalyzerBatchRange,
1026    kernel: Kernel,
1027) -> Result<CyberpunkValueTrendAnalyzerBatchOutput, CyberpunkValueTrendAnalyzerError> {
1028    cyberpunk_value_trend_analyzer_batch_with_kernel(open, high, low, close, sweep, kernel)
1029}
1030
1031pub fn cyberpunk_value_trend_analyzer_batch_par_slice(
1032    open: &[f64],
1033    high: &[f64],
1034    low: &[f64],
1035    close: &[f64],
1036    sweep: &CyberpunkValueTrendAnalyzerBatchRange,
1037    kernel: Kernel,
1038) -> Result<CyberpunkValueTrendAnalyzerBatchOutput, CyberpunkValueTrendAnalyzerError> {
1039    cyberpunk_value_trend_analyzer_batch_with_kernel(open, high, low, close, sweep, kernel)
1040}
1041
1042fn cyberpunk_value_trend_analyzer_batch_inner_into(
1043    open: &[f64],
1044    high: &[f64],
1045    low: &[f64],
1046    close: &[f64],
1047    sweep: &CyberpunkValueTrendAnalyzerBatchRange,
1048    kernel: Kernel,
1049    parallel: bool,
1050    out_value_trend: &mut [f64],
1051    out_value_trend_lag: &mut [f64],
1052    out_deviation_index: &mut [f64],
1053    out_overbought_signal: &mut [f64],
1054    out_buy_signal: &mut [f64],
1055    out_sell_signal: &mut [f64],
1056) -> Result<Vec<CyberpunkValueTrendAnalyzerParams>, CyberpunkValueTrendAnalyzerError> {
1057    let combos = expand_grid_checked(sweep)?;
1058    let rows = combos.len();
1059    let cols = close.len();
1060    let total =
1061        rows.checked_mul(cols)
1062            .ok_or_else(|| CyberpunkValueTrendAnalyzerError::InvalidInput {
1063                msg: "cyberpunk_value_trend_analyzer: rows*cols overflow in batch_into".to_string(),
1064            })?;
1065    for out in [
1066        &mut *out_value_trend,
1067        &mut *out_value_trend_lag,
1068        &mut *out_deviation_index,
1069        &mut *out_overbought_signal,
1070        &mut *out_buy_signal,
1071        &mut *out_sell_signal,
1072    ] {
1073        if out.len() != total {
1074            return Err(CyberpunkValueTrendAnalyzerError::MismatchedOutputLen {
1075                dst_len: out.len(),
1076                expected_len: total,
1077            });
1078        }
1079    }
1080
1081    let max_entry = combos
1082        .iter()
1083        .map(|params| params.entry_level.unwrap_or(DEFAULT_ENTRY_LEVEL))
1084        .max()
1085        .unwrap_or(DEFAULT_ENTRY_LEVEL);
1086    let max_exit = combos
1087        .iter()
1088        .map(|params| params.exit_level.unwrap_or(DEFAULT_EXIT_LEVEL))
1089        .max()
1090        .unwrap_or(DEFAULT_EXIT_LEVEL);
1091    validate_common(open, high, low, close, max_entry, max_exit)?;
1092    let _chosen = normalize_batch_kernel(kernel)?;
1093
1094    let worker = |row: usize,
1095                  value_trend_row: &mut [f64],
1096                  value_trend_lag_row: &mut [f64],
1097                  deviation_index_row: &mut [f64],
1098                  overbought_signal_row: &mut [f64],
1099                  buy_signal_row: &mut [f64],
1100                  sell_signal_row: &mut [f64]| {
1101        value_trend_row.fill(f64::NAN);
1102        value_trend_lag_row.fill(f64::NAN);
1103        deviation_index_row.fill(f64::NAN);
1104        overbought_signal_row.fill(f64::NAN);
1105        buy_signal_row.fill(f64::NAN);
1106        sell_signal_row.fill(f64::NAN);
1107        let params = &combos[row];
1108        compute_row(
1109            open,
1110            high,
1111            low,
1112            close,
1113            params.entry_level.unwrap_or(DEFAULT_ENTRY_LEVEL),
1114            params.exit_level.unwrap_or(DEFAULT_EXIT_LEVEL),
1115            value_trend_row,
1116            value_trend_lag_row,
1117            deviation_index_row,
1118            overbought_signal_row,
1119            buy_signal_row,
1120            sell_signal_row,
1121        );
1122    };
1123
1124    #[cfg(not(target_arch = "wasm32"))]
1125    if parallel && rows > 1 {
1126        out_value_trend
1127            .par_chunks_mut(cols)
1128            .zip(out_value_trend_lag.par_chunks_mut(cols))
1129            .zip(out_deviation_index.par_chunks_mut(cols))
1130            .zip(out_overbought_signal.par_chunks_mut(cols))
1131            .zip(out_buy_signal.par_chunks_mut(cols))
1132            .zip(out_sell_signal.par_chunks_mut(cols))
1133            .enumerate()
1134            .for_each(
1135                |(
1136                    row,
1137                    (
1138                        (
1139                            (
1140                                ((value_trend_row, value_trend_lag_row), deviation_index_row),
1141                                overbought_signal_row,
1142                            ),
1143                            buy_signal_row,
1144                        ),
1145                        sell_signal_row,
1146                    ),
1147                )| {
1148                    worker(
1149                        row,
1150                        value_trend_row,
1151                        value_trend_lag_row,
1152                        deviation_index_row,
1153                        overbought_signal_row,
1154                        buy_signal_row,
1155                        sell_signal_row,
1156                    );
1157                },
1158            );
1159    } else {
1160        for (
1161            row,
1162            (
1163                (
1164                    (
1165                        ((value_trend_row, value_trend_lag_row), deviation_index_row),
1166                        overbought_signal_row,
1167                    ),
1168                    buy_signal_row,
1169                ),
1170                sell_signal_row,
1171            ),
1172        ) in out_value_trend
1173            .chunks_mut(cols)
1174            .zip(out_value_trend_lag.chunks_mut(cols))
1175            .zip(out_deviation_index.chunks_mut(cols))
1176            .zip(out_overbought_signal.chunks_mut(cols))
1177            .zip(out_buy_signal.chunks_mut(cols))
1178            .zip(out_sell_signal.chunks_mut(cols))
1179            .enumerate()
1180        {
1181            worker(
1182                row,
1183                value_trend_row,
1184                value_trend_lag_row,
1185                deviation_index_row,
1186                overbought_signal_row,
1187                buy_signal_row,
1188                sell_signal_row,
1189            );
1190        }
1191    }
1192
1193    #[cfg(target_arch = "wasm32")]
1194    {
1195        let _ = parallel;
1196        for (
1197            row,
1198            (
1199                (
1200                    (
1201                        ((value_trend_row, value_trend_lag_row), deviation_index_row),
1202                        overbought_signal_row,
1203                    ),
1204                    buy_signal_row,
1205                ),
1206                sell_signal_row,
1207            ),
1208        ) in out_value_trend
1209            .chunks_mut(cols)
1210            .zip(out_value_trend_lag.chunks_mut(cols))
1211            .zip(out_deviation_index.chunks_mut(cols))
1212            .zip(out_overbought_signal.chunks_mut(cols))
1213            .zip(out_buy_signal.chunks_mut(cols))
1214            .zip(out_sell_signal.chunks_mut(cols))
1215            .enumerate()
1216        {
1217            worker(
1218                row,
1219                value_trend_row,
1220                value_trend_lag_row,
1221                deviation_index_row,
1222                overbought_signal_row,
1223                buy_signal_row,
1224                sell_signal_row,
1225            );
1226        }
1227    }
1228
1229    Ok(combos)
1230}
1231
1232#[derive(Debug, Clone)]
1233pub struct CyberpunkValueTrendAnalyzerStream {
1234    entry_level: usize,
1235    exit_level: usize,
1236    index: usize,
1237    valid_run: usize,
1238    sma13: RollingSum,
1239    lowest75: MonotonicQueue,
1240    highest75: MonotonicQueue,
1241    close_norm_sma: WeightedSmaState,
1242    smooth5: WeightedSmaState,
1243    prev_value_trend: f64,
1244}
1245
1246impl CyberpunkValueTrendAnalyzerStream {
1247    pub fn try_new(
1248        params: CyberpunkValueTrendAnalyzerParams,
1249    ) -> Result<Self, CyberpunkValueTrendAnalyzerError> {
1250        let entry_level = params.entry_level.unwrap_or(DEFAULT_ENTRY_LEVEL);
1251        let exit_level = params.exit_level.unwrap_or(DEFAULT_EXIT_LEVEL);
1252        validate_params_only(entry_level, exit_level)?;
1253        Ok(Self {
1254            entry_level,
1255            exit_level,
1256            index: 0,
1257            valid_run: 0,
1258            sma13: RollingSum::default(),
1259            lowest75: MonotonicQueue::default(),
1260            highest75: MonotonicQueue::default(),
1261            close_norm_sma: WeightedSmaState::new(20),
1262            smooth5: WeightedSmaState::new(5),
1263            prev_value_trend: f64::NAN,
1264        })
1265    }
1266
1267    fn reset(&mut self) {
1268        self.valid_run = 0;
1269        self.sma13.reset();
1270        self.lowest75.reset();
1271        self.highest75.reset();
1272        self.close_norm_sma.reset();
1273        self.smooth5.reset();
1274        self.prev_value_trend = f64::NAN;
1275    }
1276
1277    pub fn update(
1278        &mut self,
1279        open: f64,
1280        high: f64,
1281        low: f64,
1282        close: f64,
1283    ) -> Option<(f64, f64, f64, f64, f64, f64)> {
1284        let idx = self.index;
1285        self.index = self.index.saturating_add(1);
1286
1287        if !is_valid_ohlc(open, high, low, close) {
1288            self.reset();
1289            return None;
1290        }
1291
1292        self.valid_run += 1;
1293        let ma13 = self.sma13.push(close);
1294        self.lowest75.push_min(idx, low);
1295        self.highest75.push_max(idx, high);
1296        let min_index = idx.saturating_sub(RANGE75_WINDOW - 1);
1297        self.lowest75.prune(min_index);
1298        self.highest75.prune(min_index);
1299
1300        let value_trend_lag = self.prev_value_trend;
1301        let mut value_trend = f64::NAN;
1302        let mut deviation_index = f64::NAN;
1303        let mut overbought_signal = f64::NAN;
1304        let mut buy_signal = if self.valid_run >= RANGE75_WINDOW {
1305            0.0
1306        } else {
1307            f64::NAN
1308        };
1309        let mut sell_signal = buy_signal;
1310
1311        if self.valid_run >= RANGE75_WINDOW {
1312            let range_low = self.lowest75.current().unwrap_or(f64::NAN);
1313            let range_high = self.highest75.current().unwrap_or(f64::NAN);
1314            let range = range_high - range_low;
1315            if range.is_finite() && range > 0.0 {
1316                let close_norm = (close - range_low) * 100.0 / range;
1317                let close_norm_avg = self.close_norm_sma.update(close_norm);
1318                let smooth = self.smooth5.update(close_norm_avg);
1319                if close_norm_avg.is_finite() && smooth.is_finite() {
1320                    value_trend = 3.0 * close_norm_avg - 2.0 * smooth;
1321                } else {
1322                    buy_signal = f64::NAN;
1323                    sell_signal = f64::NAN;
1324                }
1325            } else {
1326                self.close_norm_sma.reset();
1327                self.smooth5.reset();
1328                buy_signal = f64::NAN;
1329                sell_signal = f64::NAN;
1330            }
1331        }
1332
1333        if let Some(avg13) = ma13 {
1334            if avg13.is_finite() && avg13 != 0.0 {
1335                deviation_index = 100.0 - (((close - avg13) / avg13) * 100.0).abs();
1336                if value_trend.is_finite() && value_trend > deviation_index {
1337                    overbought_signal = deviation_index;
1338                }
1339            }
1340        }
1341
1342        if value_trend.is_finite() && self.prev_value_trend.is_finite() {
1343            if self.prev_value_trend <= self.entry_level as f64
1344                && value_trend > self.entry_level as f64
1345            {
1346                buy_signal = 1.0;
1347            }
1348            if self.prev_value_trend >= self.exit_level as f64
1349                && value_trend < self.exit_level as f64
1350            {
1351                sell_signal = 1.0;
1352            }
1353        }
1354
1355        self.prev_value_trend = value_trend;
1356        Some((
1357            value_trend,
1358            value_trend_lag,
1359            deviation_index,
1360            overbought_signal,
1361            buy_signal,
1362            sell_signal,
1363        ))
1364    }
1365}
1366
1367impl CyberpunkValueTrendAnalyzerBuilder {
1368    #[inline(always)]
1369    pub fn apply(
1370        self,
1371        candles: &Candles,
1372    ) -> Result<CyberpunkValueTrendAnalyzerOutput, CyberpunkValueTrendAnalyzerError> {
1373        cyberpunk_value_trend_analyzer_with_kernel(
1374            &CyberpunkValueTrendAnalyzerInput::from_candles(
1375                candles,
1376                CyberpunkValueTrendAnalyzerParams {
1377                    entry_level: self.entry_level,
1378                    exit_level: self.exit_level,
1379                },
1380            ),
1381            self.kernel,
1382        )
1383    }
1384
1385    #[inline(always)]
1386    pub fn apply_slices(
1387        self,
1388        open: &[f64],
1389        high: &[f64],
1390        low: &[f64],
1391        close: &[f64],
1392    ) -> Result<CyberpunkValueTrendAnalyzerOutput, CyberpunkValueTrendAnalyzerError> {
1393        cyberpunk_value_trend_analyzer_with_kernel(
1394            &CyberpunkValueTrendAnalyzerInput::from_slices(
1395                open,
1396                high,
1397                low,
1398                close,
1399                CyberpunkValueTrendAnalyzerParams {
1400                    entry_level: self.entry_level,
1401                    exit_level: self.exit_level,
1402                },
1403            ),
1404            self.kernel,
1405        )
1406    }
1407
1408    #[inline(always)]
1409    pub fn into_stream(
1410        self,
1411    ) -> Result<CyberpunkValueTrendAnalyzerStream, CyberpunkValueTrendAnalyzerError> {
1412        CyberpunkValueTrendAnalyzerStream::try_new(CyberpunkValueTrendAnalyzerParams {
1413            entry_level: self.entry_level,
1414            exit_level: self.exit_level,
1415        })
1416    }
1417}
1418
1419#[cfg(feature = "python")]
1420#[pyfunction(
1421    name = "cyberpunk_value_trend_analyzer",
1422    signature = (open, high, low, close, entry_level=DEFAULT_ENTRY_LEVEL, exit_level=DEFAULT_EXIT_LEVEL, kernel=None)
1423)]
1424pub fn cyberpunk_value_trend_analyzer_py<'py>(
1425    py: Python<'py>,
1426    open: PyReadonlyArray1<'py, f64>,
1427    high: PyReadonlyArray1<'py, f64>,
1428    low: PyReadonlyArray1<'py, f64>,
1429    close: PyReadonlyArray1<'py, f64>,
1430    entry_level: usize,
1431    exit_level: usize,
1432    kernel: Option<&str>,
1433) -> PyResult<(
1434    Bound<'py, PyArray1<f64>>,
1435    Bound<'py, PyArray1<f64>>,
1436    Bound<'py, PyArray1<f64>>,
1437    Bound<'py, PyArray1<f64>>,
1438    Bound<'py, PyArray1<f64>>,
1439    Bound<'py, PyArray1<f64>>,
1440)> {
1441    let open = open.as_slice()?;
1442    let high = high.as_slice()?;
1443    let low = low.as_slice()?;
1444    let close = close.as_slice()?;
1445    let kern = validate_kernel(kernel, false)?;
1446    let out = py
1447        .allow_threads(|| {
1448            cyberpunk_value_trend_analyzer_with_kernel(
1449                &CyberpunkValueTrendAnalyzerInput::from_slices(
1450                    open,
1451                    high,
1452                    low,
1453                    close,
1454                    CyberpunkValueTrendAnalyzerParams {
1455                        entry_level: Some(entry_level),
1456                        exit_level: Some(exit_level),
1457                    },
1458                ),
1459                kern,
1460            )
1461        })
1462        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1463    Ok((
1464        out.value_trend.into_pyarray(py),
1465        out.value_trend_lag.into_pyarray(py),
1466        out.deviation_index.into_pyarray(py),
1467        out.overbought_signal.into_pyarray(py),
1468        out.buy_signal.into_pyarray(py),
1469        out.sell_signal.into_pyarray(py),
1470    ))
1471}
1472
1473#[cfg(feature = "python")]
1474#[pyfunction(
1475    name = "cyberpunk_value_trend_analyzer_batch",
1476    signature = (open, high, low, close, entry_level_range=(DEFAULT_ENTRY_LEVEL, DEFAULT_ENTRY_LEVEL, 0), exit_level_range=(DEFAULT_EXIT_LEVEL, DEFAULT_EXIT_LEVEL, 0), kernel=None)
1477)]
1478pub fn cyberpunk_value_trend_analyzer_batch_py<'py>(
1479    py: Python<'py>,
1480    open: PyReadonlyArray1<'py, f64>,
1481    high: PyReadonlyArray1<'py, f64>,
1482    low: PyReadonlyArray1<'py, f64>,
1483    close: PyReadonlyArray1<'py, f64>,
1484    entry_level_range: (usize, usize, usize),
1485    exit_level_range: (usize, usize, usize),
1486    kernel: Option<&str>,
1487) -> PyResult<Bound<'py, PyDict>> {
1488    let open = open.as_slice()?;
1489    let high = high.as_slice()?;
1490    let low = low.as_slice()?;
1491    let close = close.as_slice()?;
1492    let kern = validate_kernel(kernel, true)?;
1493    let out = py
1494        .allow_threads(|| {
1495            cyberpunk_value_trend_analyzer_batch_with_kernel(
1496                open,
1497                high,
1498                low,
1499                close,
1500                &CyberpunkValueTrendAnalyzerBatchRange {
1501                    entry_level: entry_level_range,
1502                    exit_level: exit_level_range,
1503                },
1504                kern,
1505            )
1506        })
1507        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1508
1509    let dict = PyDict::new(py);
1510    dict.set_item(
1511        "value_trend",
1512        out.value_trend
1513            .into_pyarray(py)
1514            .reshape((out.rows, out.cols))?,
1515    )?;
1516    dict.set_item(
1517        "value_trend_lag",
1518        out.value_trend_lag
1519            .into_pyarray(py)
1520            .reshape((out.rows, out.cols))?,
1521    )?;
1522    dict.set_item(
1523        "deviation_index",
1524        out.deviation_index
1525            .into_pyarray(py)
1526            .reshape((out.rows, out.cols))?,
1527    )?;
1528    dict.set_item(
1529        "overbought_signal",
1530        out.overbought_signal
1531            .into_pyarray(py)
1532            .reshape((out.rows, out.cols))?,
1533    )?;
1534    dict.set_item(
1535        "buy_signal",
1536        out.buy_signal
1537            .into_pyarray(py)
1538            .reshape((out.rows, out.cols))?,
1539    )?;
1540    dict.set_item(
1541        "sell_signal",
1542        out.sell_signal
1543            .into_pyarray(py)
1544            .reshape((out.rows, out.cols))?,
1545    )?;
1546    dict.set_item(
1547        "entry_levels",
1548        out.combos
1549            .iter()
1550            .map(|params| params.entry_level.unwrap_or(DEFAULT_ENTRY_LEVEL) as u64)
1551            .collect::<Vec<_>>()
1552            .into_pyarray(py),
1553    )?;
1554    dict.set_item(
1555        "exit_levels",
1556        out.combos
1557            .iter()
1558            .map(|params| params.exit_level.unwrap_or(DEFAULT_EXIT_LEVEL) as u64)
1559            .collect::<Vec<_>>()
1560            .into_pyarray(py),
1561    )?;
1562    dict.set_item("rows", out.rows)?;
1563    dict.set_item("cols", out.cols)?;
1564    Ok(dict)
1565}
1566
1567#[cfg(feature = "python")]
1568#[pyclass(name = "CyberpunkValueTrendAnalyzerStream")]
1569pub struct CyberpunkValueTrendAnalyzerStreamPy {
1570    stream: CyberpunkValueTrendAnalyzerStream,
1571}
1572
1573#[cfg(feature = "python")]
1574#[pymethods]
1575impl CyberpunkValueTrendAnalyzerStreamPy {
1576    #[new]
1577    #[pyo3(signature = (entry_level=DEFAULT_ENTRY_LEVEL, exit_level=DEFAULT_EXIT_LEVEL))]
1578    fn new(entry_level: usize, exit_level: usize) -> PyResult<Self> {
1579        let stream =
1580            CyberpunkValueTrendAnalyzerStream::try_new(CyberpunkValueTrendAnalyzerParams {
1581                entry_level: Some(entry_level),
1582                exit_level: Some(exit_level),
1583            })
1584            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1585        Ok(Self { stream })
1586    }
1587
1588    fn update(
1589        &mut self,
1590        open: f64,
1591        high: f64,
1592        low: f64,
1593        close: f64,
1594    ) -> Option<(f64, f64, f64, f64, f64, f64)> {
1595        self.stream.update(open, high, low, close)
1596    }
1597}
1598
1599#[cfg(feature = "python")]
1600pub fn register_cyberpunk_value_trend_analyzer_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1601    m.add_function(wrap_pyfunction!(cyberpunk_value_trend_analyzer_py, m)?)?;
1602    m.add_function(wrap_pyfunction!(
1603        cyberpunk_value_trend_analyzer_batch_py,
1604        m
1605    )?)?;
1606    m.add_class::<CyberpunkValueTrendAnalyzerStreamPy>()?;
1607    Ok(())
1608}
1609
1610#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1611#[derive(Debug, Clone, Serialize, Deserialize)]
1612pub struct CyberpunkValueTrendAnalyzerBatchConfig {
1613    pub entry_level_range: Vec<usize>,
1614    pub exit_level_range: Vec<usize>,
1615}
1616
1617#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1618#[wasm_bindgen(js_name = cyberpunk_value_trend_analyzer_js)]
1619pub fn cyberpunk_value_trend_analyzer_js(
1620    open: &[f64],
1621    high: &[f64],
1622    low: &[f64],
1623    close: &[f64],
1624    entry_level: usize,
1625    exit_level: usize,
1626) -> Result<JsValue, JsValue> {
1627    let out = cyberpunk_value_trend_analyzer_with_kernel(
1628        &CyberpunkValueTrendAnalyzerInput::from_slices(
1629            open,
1630            high,
1631            low,
1632            close,
1633            CyberpunkValueTrendAnalyzerParams {
1634                entry_level: Some(entry_level),
1635                exit_level: Some(exit_level),
1636            },
1637        ),
1638        Kernel::Auto,
1639    )
1640    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1641
1642    let obj = js_sys::Object::new();
1643    for (key, value) in [
1644        (
1645            "value_trend",
1646            serde_wasm_bindgen::to_value(&out.value_trend).unwrap(),
1647        ),
1648        (
1649            "value_trend_lag",
1650            serde_wasm_bindgen::to_value(&out.value_trend_lag).unwrap(),
1651        ),
1652        (
1653            "deviation_index",
1654            serde_wasm_bindgen::to_value(&out.deviation_index).unwrap(),
1655        ),
1656        (
1657            "overbought_signal",
1658            serde_wasm_bindgen::to_value(&out.overbought_signal).unwrap(),
1659        ),
1660        (
1661            "buy_signal",
1662            serde_wasm_bindgen::to_value(&out.buy_signal).unwrap(),
1663        ),
1664        (
1665            "sell_signal",
1666            serde_wasm_bindgen::to_value(&out.sell_signal).unwrap(),
1667        ),
1668    ] {
1669        js_sys::Reflect::set(&obj, &JsValue::from_str(key), &value)?;
1670    }
1671    Ok(JsValue::from(obj))
1672}
1673
1674#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1675#[wasm_bindgen(js_name = cyberpunk_value_trend_analyzer_batch_js)]
1676pub fn cyberpunk_value_trend_analyzer_batch_js(
1677    open: &[f64],
1678    high: &[f64],
1679    low: &[f64],
1680    close: &[f64],
1681    config: JsValue,
1682) -> Result<JsValue, JsValue> {
1683    let cfg: CyberpunkValueTrendAnalyzerBatchConfig =
1684        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1685    if cfg.entry_level_range.len() != 3 || cfg.exit_level_range.len() != 3 {
1686        return Err(JsValue::from_str(
1687            "cyberpunk_value_trend_analyzer_batch_js: range vectors must have length 3",
1688        ));
1689    }
1690    let out = cyberpunk_value_trend_analyzer_batch_with_kernel(
1691        open,
1692        high,
1693        low,
1694        close,
1695        &CyberpunkValueTrendAnalyzerBatchRange {
1696            entry_level: (
1697                cfg.entry_level_range[0],
1698                cfg.entry_level_range[1],
1699                cfg.entry_level_range[2],
1700            ),
1701            exit_level: (
1702                cfg.exit_level_range[0],
1703                cfg.exit_level_range[1],
1704                cfg.exit_level_range[2],
1705            ),
1706        },
1707        Kernel::Auto,
1708    )
1709    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1710
1711    let obj = js_sys::Object::new();
1712    for (key, value) in [
1713        (
1714            "value_trend",
1715            serde_wasm_bindgen::to_value(&out.value_trend).unwrap(),
1716        ),
1717        (
1718            "value_trend_lag",
1719            serde_wasm_bindgen::to_value(&out.value_trend_lag).unwrap(),
1720        ),
1721        (
1722            "deviation_index",
1723            serde_wasm_bindgen::to_value(&out.deviation_index).unwrap(),
1724        ),
1725        (
1726            "overbought_signal",
1727            serde_wasm_bindgen::to_value(&out.overbought_signal).unwrap(),
1728        ),
1729        (
1730            "buy_signal",
1731            serde_wasm_bindgen::to_value(&out.buy_signal).unwrap(),
1732        ),
1733        (
1734            "sell_signal",
1735            serde_wasm_bindgen::to_value(&out.sell_signal).unwrap(),
1736        ),
1737    ] {
1738        js_sys::Reflect::set(&obj, &JsValue::from_str(key), &value)?;
1739    }
1740    js_sys::Reflect::set(
1741        &obj,
1742        &JsValue::from_str("rows"),
1743        &JsValue::from_f64(out.rows as f64),
1744    )?;
1745    js_sys::Reflect::set(
1746        &obj,
1747        &JsValue::from_str("cols"),
1748        &JsValue::from_f64(out.cols as f64),
1749    )?;
1750
1751    let combos = out
1752        .combos
1753        .iter()
1754        .map(|params| {
1755            let item = js_sys::Object::new();
1756            let _ = js_sys::Reflect::set(
1757                &item,
1758                &JsValue::from_str("entry_level"),
1759                &JsValue::from_f64(params.entry_level.unwrap_or(DEFAULT_ENTRY_LEVEL) as f64),
1760            );
1761            let _ = js_sys::Reflect::set(
1762                &item,
1763                &JsValue::from_str("exit_level"),
1764                &JsValue::from_f64(params.exit_level.unwrap_or(DEFAULT_EXIT_LEVEL) as f64),
1765            );
1766            item
1767        })
1768        .collect::<js_sys::Array>();
1769    js_sys::Reflect::set(&obj, &JsValue::from_str("combos"), &combos)?;
1770    Ok(JsValue::from(obj))
1771}
1772
1773#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1774#[wasm_bindgen]
1775pub fn cyberpunk_value_trend_analyzer_alloc(len: usize) -> *mut f64 {
1776    let mut vec = Vec::<f64>::with_capacity(OUTPUTS * len);
1777    let ptr = vec.as_mut_ptr();
1778    std::mem::forget(vec);
1779    ptr
1780}
1781
1782#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1783#[wasm_bindgen]
1784pub fn cyberpunk_value_trend_analyzer_free(ptr: *mut f64, len: usize) {
1785    if !ptr.is_null() {
1786        unsafe {
1787            let _ = Vec::from_raw_parts(ptr, OUTPUTS * len, OUTPUTS * len);
1788        }
1789    }
1790}
1791
1792#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1793#[wasm_bindgen]
1794pub fn cyberpunk_value_trend_analyzer_into(
1795    open_ptr: *const f64,
1796    high_ptr: *const f64,
1797    low_ptr: *const f64,
1798    close_ptr: *const f64,
1799    out_ptr: *mut f64,
1800    len: usize,
1801    entry_level: usize,
1802    exit_level: usize,
1803) -> Result<(), JsValue> {
1804    if open_ptr.is_null()
1805        || high_ptr.is_null()
1806        || low_ptr.is_null()
1807        || close_ptr.is_null()
1808        || out_ptr.is_null()
1809    {
1810        return Err(JsValue::from_str(
1811            "null pointer passed to cyberpunk_value_trend_analyzer_into",
1812        ));
1813    }
1814    unsafe {
1815        let open = std::slice::from_raw_parts(open_ptr, len);
1816        let high = std::slice::from_raw_parts(high_ptr, len);
1817        let low = std::slice::from_raw_parts(low_ptr, len);
1818        let close = std::slice::from_raw_parts(close_ptr, len);
1819        let out = std::slice::from_raw_parts_mut(out_ptr, OUTPUTS * len);
1820        let (value_trend, tail) = out.split_at_mut(len);
1821        let (value_trend_lag, tail) = tail.split_at_mut(len);
1822        let (deviation_index, tail) = tail.split_at_mut(len);
1823        let (overbought_signal, tail) = tail.split_at_mut(len);
1824        let (buy_signal, sell_signal) = tail.split_at_mut(len);
1825        cyberpunk_value_trend_analyzer_into_slice(
1826            value_trend,
1827            value_trend_lag,
1828            deviation_index,
1829            overbought_signal,
1830            buy_signal,
1831            sell_signal,
1832            &CyberpunkValueTrendAnalyzerInput::from_slices(
1833                open,
1834                high,
1835                low,
1836                close,
1837                CyberpunkValueTrendAnalyzerParams {
1838                    entry_level: Some(entry_level),
1839                    exit_level: Some(exit_level),
1840                },
1841            ),
1842            Kernel::Auto,
1843        )
1844        .map_err(|e| JsValue::from_str(&e.to_string()))
1845    }
1846}
1847
1848#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1849#[wasm_bindgen]
1850pub fn cyberpunk_value_trend_analyzer_batch_into(
1851    open_ptr: *const f64,
1852    high_ptr: *const f64,
1853    low_ptr: *const f64,
1854    close_ptr: *const f64,
1855    out_ptr: *mut f64,
1856    len: usize,
1857    entry_start: usize,
1858    entry_end: usize,
1859    entry_step: usize,
1860    exit_start: usize,
1861    exit_end: usize,
1862    exit_step: usize,
1863) -> Result<usize, JsValue> {
1864    if open_ptr.is_null()
1865        || high_ptr.is_null()
1866        || low_ptr.is_null()
1867        || close_ptr.is_null()
1868        || out_ptr.is_null()
1869    {
1870        return Err(JsValue::from_str(
1871            "null pointer passed to cyberpunk_value_trend_analyzer_batch_into",
1872        ));
1873    }
1874
1875    let sweep = CyberpunkValueTrendAnalyzerBatchRange {
1876        entry_level: (entry_start, entry_end, entry_step),
1877        exit_level: (exit_start, exit_end, exit_step),
1878    };
1879    let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1880    let rows = combos.len();
1881    let split = rows.checked_mul(len).ok_or_else(|| {
1882        JsValue::from_str("rows*cols overflow in cyberpunk_value_trend_analyzer_batch_into")
1883    })?;
1884    let total = split.checked_mul(OUTPUTS).ok_or_else(|| {
1885        JsValue::from_str("outputs*rows*cols overflow in cyberpunk_value_trend_analyzer_batch_into")
1886    })?;
1887
1888    unsafe {
1889        let open = std::slice::from_raw_parts(open_ptr, len);
1890        let high = std::slice::from_raw_parts(high_ptr, len);
1891        let low = std::slice::from_raw_parts(low_ptr, len);
1892        let close = std::slice::from_raw_parts(close_ptr, len);
1893        let out = std::slice::from_raw_parts_mut(out_ptr, total);
1894        let (value_trend, tail) = out.split_at_mut(split);
1895        let (value_trend_lag, tail) = tail.split_at_mut(split);
1896        let (deviation_index, tail) = tail.split_at_mut(split);
1897        let (overbought_signal, tail) = tail.split_at_mut(split);
1898        let (buy_signal, sell_signal) = tail.split_at_mut(split);
1899        cyberpunk_value_trend_analyzer_batch_inner_into(
1900            open,
1901            high,
1902            low,
1903            close,
1904            &sweep,
1905            Kernel::Auto,
1906            false,
1907            value_trend,
1908            value_trend_lag,
1909            deviation_index,
1910            overbought_signal,
1911            buy_signal,
1912            sell_signal,
1913        )
1914        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1915    }
1916    Ok(rows)
1917}
1918
1919#[cfg(test)]
1920mod tests {
1921    use super::*;
1922
1923    fn sample_ohlc(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1924        let mut open = Vec::with_capacity(len);
1925        let mut high = Vec::with_capacity(len);
1926        let mut low = Vec::with_capacity(len);
1927        let mut close = Vec::with_capacity(len);
1928        for i in 0..len {
1929            let trend = 100.0 + i as f64 * 0.18;
1930            let wave = ((i % 17) as f64 - 8.0) * 0.9 + ((i % 9) as f64 - 4.0) * 0.35;
1931            let o = trend + wave - if i % 2 == 0 { 0.7 } else { -0.4 };
1932            let c = trend + wave + ((i * 7 % 11) as f64 - 5.0) * 0.22;
1933            let h = o.max(c) + 1.25 + (i % 5) as f64 * 0.08;
1934            let l = o.min(c) - 1.15 - (i % 7) as f64 * 0.07;
1935            open.push(o);
1936            high.push(h);
1937            low.push(l);
1938            close.push(c);
1939        }
1940        (open, high, low, close)
1941    }
1942
1943    fn assert_vec_eq_nan(lhs: &[f64], rhs: &[f64]) {
1944        assert_eq!(lhs.len(), rhs.len());
1945        for (a, b) in lhs.iter().zip(rhs.iter()) {
1946            if a.is_nan() && b.is_nan() {
1947                continue;
1948            }
1949            assert!((*a - *b).abs() <= 1e-12, "mismatch: left={a:?} right={b:?}");
1950        }
1951    }
1952
1953    #[test]
1954    fn output_contract() {
1955        let (open, high, low, close) = sample_ohlc(240);
1956        let out = cyberpunk_value_trend_analyzer(&CyberpunkValueTrendAnalyzerInput::from_slices(
1957            &open,
1958            &high,
1959            &low,
1960            &close,
1961            CyberpunkValueTrendAnalyzerParams::default(),
1962        ))
1963        .unwrap();
1964        assert_eq!(out.value_trend.len(), close.len());
1965        assert_eq!(out.sell_signal.len(), close.len());
1966        assert!(out.value_trend.iter().any(|v| v.is_finite()));
1967    }
1968
1969    #[test]
1970    fn invalid_params() {
1971        let (open, high, low, close) = sample_ohlc(120);
1972        let err = cyberpunk_value_trend_analyzer(&CyberpunkValueTrendAnalyzerInput::from_slices(
1973            &open,
1974            &high,
1975            &low,
1976            &close,
1977            CyberpunkValueTrendAnalyzerParams {
1978                entry_level: Some(0),
1979                exit_level: Some(75),
1980            },
1981        ))
1982        .unwrap_err();
1983        assert!(matches!(
1984            err,
1985            CyberpunkValueTrendAnalyzerError::InvalidEntryLevel { .. }
1986        ));
1987    }
1988
1989    #[test]
1990    fn into_slice_matches_safe_api() {
1991        let (open, high, low, close) = sample_ohlc(220);
1992        let input = CyberpunkValueTrendAnalyzerInput::from_slices(
1993            &open,
1994            &high,
1995            &low,
1996            &close,
1997            CyberpunkValueTrendAnalyzerParams::default(),
1998        );
1999        let safe = cyberpunk_value_trend_analyzer(&input).unwrap();
2000        let mut value_trend = vec![0.0; close.len()];
2001        let mut value_trend_lag = vec![0.0; close.len()];
2002        let mut deviation_index = vec![0.0; close.len()];
2003        let mut overbought_signal = vec![0.0; close.len()];
2004        let mut buy_signal = vec![0.0; close.len()];
2005        let mut sell_signal = vec![0.0; close.len()];
2006        cyberpunk_value_trend_analyzer_into_slice(
2007            &mut value_trend,
2008            &mut value_trend_lag,
2009            &mut deviation_index,
2010            &mut overbought_signal,
2011            &mut buy_signal,
2012            &mut sell_signal,
2013            &input,
2014            Kernel::Auto,
2015        )
2016        .unwrap();
2017        assert_vec_eq_nan(&safe.value_trend, &value_trend);
2018        assert_vec_eq_nan(&safe.value_trend_lag, &value_trend_lag);
2019        assert_vec_eq_nan(&safe.deviation_index, &deviation_index);
2020        assert_vec_eq_nan(&safe.overbought_signal, &overbought_signal);
2021        assert_vec_eq_nan(&safe.buy_signal, &buy_signal);
2022        assert_vec_eq_nan(&safe.sell_signal, &sell_signal);
2023    }
2024
2025    #[test]
2026    fn batch_single_matches_safe_api() {
2027        let (open, high, low, close) = sample_ohlc(210);
2028        let batch = cyberpunk_value_trend_analyzer_batch_with_kernel(
2029            &open,
2030            &high,
2031            &low,
2032            &close,
2033            &CyberpunkValueTrendAnalyzerBatchRange::default(),
2034            Kernel::Auto,
2035        )
2036        .unwrap();
2037        let safe = cyberpunk_value_trend_analyzer(&CyberpunkValueTrendAnalyzerInput::from_slices(
2038            &open,
2039            &high,
2040            &low,
2041            &close,
2042            CyberpunkValueTrendAnalyzerParams::default(),
2043        ))
2044        .unwrap();
2045        assert_eq!(batch.rows, 1);
2046        assert_eq!(batch.cols, close.len());
2047        assert_vec_eq_nan(&batch.value_trend, &safe.value_trend);
2048        assert_vec_eq_nan(&batch.value_trend_lag, &safe.value_trend_lag);
2049        assert_vec_eq_nan(&batch.deviation_index, &safe.deviation_index);
2050        assert_vec_eq_nan(&batch.overbought_signal, &safe.overbought_signal);
2051        assert_vec_eq_nan(&batch.buy_signal, &safe.buy_signal);
2052        assert_vec_eq_nan(&batch.sell_signal, &safe.sell_signal);
2053    }
2054
2055    #[test]
2056    fn stream_matches_safe_api() {
2057        let (open, high, low, close) = sample_ohlc(200);
2058        let safe = cyberpunk_value_trend_analyzer(&CyberpunkValueTrendAnalyzerInput::from_slices(
2059            &open,
2060            &high,
2061            &low,
2062            &close,
2063            CyberpunkValueTrendAnalyzerParams::default(),
2064        ))
2065        .unwrap();
2066        let mut stream = CyberpunkValueTrendAnalyzerStream::try_new(
2067            CyberpunkValueTrendAnalyzerParams::default(),
2068        )
2069        .unwrap();
2070        let mut value_trend = Vec::with_capacity(close.len());
2071        let mut value_trend_lag = Vec::with_capacity(close.len());
2072        let mut deviation_index = Vec::with_capacity(close.len());
2073        let mut overbought_signal = Vec::with_capacity(close.len());
2074        let mut buy_signal = Vec::with_capacity(close.len());
2075        let mut sell_signal = Vec::with_capacity(close.len());
2076        for (((&o, &h), &l), &c) in open
2077            .iter()
2078            .zip(high.iter())
2079            .zip(low.iter())
2080            .zip(close.iter())
2081        {
2082            let point = stream.update(o, h, l, c).unwrap();
2083            value_trend.push(point.0);
2084            value_trend_lag.push(point.1);
2085            deviation_index.push(point.2);
2086            overbought_signal.push(point.3);
2087            buy_signal.push(point.4);
2088            sell_signal.push(point.5);
2089        }
2090        assert_vec_eq_nan(&safe.value_trend, &value_trend);
2091        assert_vec_eq_nan(&safe.value_trend_lag, &value_trend_lag);
2092        assert_vec_eq_nan(&safe.deviation_index, &deviation_index);
2093        assert_vec_eq_nan(&safe.overbought_signal, &overbought_signal);
2094        assert_vec_eq_nan(&safe.buy_signal, &buy_signal);
2095        assert_vec_eq_nan(&safe.sell_signal, &sell_signal);
2096    }
2097
2098    #[test]
2099    fn batch_multi_param_contract() {
2100        let (open, high, low, close) = sample_ohlc(180);
2101        let batch = cyberpunk_value_trend_analyzer_batch_with_kernel(
2102            &open,
2103            &high,
2104            &low,
2105            &close,
2106            &CyberpunkValueTrendAnalyzerBatchRange {
2107                entry_level: (25, 35, 10),
2108                exit_level: (70, 80, 10),
2109            },
2110            Kernel::Auto,
2111        )
2112        .unwrap();
2113        assert_eq!(batch.rows, 4);
2114        assert_eq!(batch.cols, close.len());
2115        assert_eq!(batch.combos.len(), 4);
2116        assert_eq!(batch.value_trend.len(), 4 * close.len());
2117    }
2118}