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}