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