Skip to main content

quantwave_polars/
lib.rs

1//! # quantwave-polars
2//!
3//! Polars integration for [QuantWave](https://lavs9.github.io/quantwave/): the
4//! [`QuantWaveExt::ta()`](QuantWaveExt::ta) namespace on [`LazyFrame`] for **218**
5//! vectorized indicators, plus [`bt`](bt) for backtest helpers.
6//!
7//! ## Quick start
8//!
9//! ```rust,no_run
10//! use polars::prelude::*;
11//! use quantwave_polars::QuantWaveExt;
12//!
13//! # fn demo(df: LazyFrame) -> PolarsResult<LazyFrame> {
14//! df.ta().rsi("close", 14)
15//! # }
16//! ```
17//!
18//! Under the hood, expression plugins call the same `quantwave-core` math as
19//! streaming `Next<T>` structs — see [parity guide](https://lavs9.github.io/quantwave/guides/rust/next-trait/).
20//!
21//! User guides: <https://lavs9.github.io/quantwave/guides/rust/crate-map/>
22//! Full API: <https://docs.rs/quantwave-polars>
23
24use polars::prelude::*;
25use quantwave_core::traits::Next;
26use quantwave_core::*;
27
28pub mod bt;
29pub mod features; // implements .ta.features.hurst / cyber_cycle / griffiths_dominant_cycle / regime_features (see features.rs)
30
31pub mod prelude {
32    pub use crate::bt::{BtNamespace, BtOptions, QuantWaveBtExt};
33    pub use crate::features::TaFeaturesNamespace;
34    pub use crate::{QuantWaveExt, QuantWaveNamespace};
35    pub use quantwave_backtest::{SweepVariant, run_param_sweep, single_param_variants}; // .ta().features() sub-namespace (locked minimal surface for 4ps cross-epic deliverable)
36}
37
38pub trait QuantWaveExt {
39    fn ta(&self) -> QuantWaveNamespace<'_>;
40}
41
42pub struct QuantWaveNamespace<'a>(&'a LazyFrame);
43
44impl<'a> QuantWaveNamespace<'a> {
45    pub fn acos(self, name: &str) -> LazyFrame {
46        self.math_transform_1_in_1_out::<ACOS>(name, "acos")
47    }
48    pub fn asin(self, name: &str) -> LazyFrame {
49        self.math_transform_1_in_1_out::<ASIN>(name, "asin")
50    }
51    pub fn atan(self, name: &str) -> LazyFrame {
52        self.math_transform_1_in_1_out::<ATAN>(name, "atan")
53    }
54    pub fn ceil(self, name: &str) -> LazyFrame {
55        self.math_transform_1_in_1_out::<CEIL>(name, "ceil")
56    }
57    pub fn cos(self, name: &str) -> LazyFrame {
58        self.math_transform_1_in_1_out::<COS>(name, "cos")
59    }
60    pub fn cosh(self, name: &str) -> LazyFrame {
61        self.math_transform_1_in_1_out::<COSH>(name, "cosh")
62    }
63    pub fn exp(self, name: &str) -> LazyFrame {
64        self.math_transform_1_in_1_out::<EXP>(name, "exp")
65    }
66    pub fn floor(self, name: &str) -> LazyFrame {
67        self.math_transform_1_in_1_out::<FLOOR>(name, "floor")
68    }
69    pub fn ln(self, name: &str) -> LazyFrame {
70        self.math_transform_1_in_1_out::<LN>(name, "ln")
71    }
72    pub fn log10(self, name: &str) -> LazyFrame {
73        self.math_transform_1_in_1_out::<LOG10>(name, "log10")
74    }
75    pub fn sin(self, name: &str) -> LazyFrame {
76        self.math_transform_1_in_1_out::<SIN>(name, "sin")
77    }
78    pub fn sinh(self, name: &str) -> LazyFrame {
79        self.math_transform_1_in_1_out::<SINH>(name, "sinh")
80    }
81    pub fn sqrt(self, name: &str) -> LazyFrame {
82        self.math_transform_1_in_1_out::<SQRT>(name, "sqrt")
83    }
84    pub fn tan(self, name: &str) -> LazyFrame {
85        self.math_transform_1_in_1_out::<TAN>(name, "tan")
86    }
87    pub fn tanh(self, name: &str) -> LazyFrame {
88        self.math_transform_1_in_1_out::<TANH>(name, "tanh")
89    }
90
91    pub fn add(self, in1: &str, in2: &str) -> LazyFrame {
92        self.math_operator_2_in_1_out::<ADD>(in1, in2, "add")
93    }
94    pub fn sub(self, in1: &str, in2: &str) -> LazyFrame {
95        self.math_operator_2_in_1_out::<SUB>(in1, in2, "sub")
96    }
97    pub fn mult(self, in1: &str, in2: &str) -> LazyFrame {
98        self.math_operator_2_in_1_out::<MULT>(in1, in2, "mult")
99    }
100    pub fn div(self, in1: &str, in2: &str) -> LazyFrame {
101        self.math_operator_2_in_1_out::<DIV>(in1, in2, "div")
102    }
103
104    pub fn max(self, name: &str, period: usize) -> LazyFrame {
105        self.math_operator_1_in_1_out_period::<MAX>(name, period, "max")
106    }
107    pub fn maxindex(self, name: &str, period: usize) -> LazyFrame {
108        self.math_operator_1_in_1_out_period::<MAXINDEX>(name, period, "maxindex")
109    }
110    pub fn min(self, name: &str, period: usize) -> LazyFrame {
111        self.math_operator_1_in_1_out_period::<MIN>(name, period, "min")
112    }
113    pub fn minindex(self, name: &str, period: usize) -> LazyFrame {
114        self.math_operator_1_in_1_out_period::<MININDEX>(name, period, "minindex")
115    }
116    pub fn sum(self, name: &str, period: usize) -> LazyFrame {
117        self.math_operator_1_in_1_out_period::<SUM>(name, period, "sum")
118    }
119
120    pub fn sma(self, name: &str, period: usize) -> LazyFrame {
121        self.math_operator_1_in_1_out_period::<SMA>(name, period, "sma")
122    }
123    pub fn ema(self, name: &str, period: usize) -> LazyFrame {
124        self.math_operator_1_in_1_out_period::<EMA>(name, period, "ema")
125    }
126    pub fn wma(self, name: &str, period: usize) -> LazyFrame {
127        self.math_operator_1_in_1_out_period::<WMA>(name, period, "wma")
128    }
129    pub fn dema(self, name: &str, period: usize) -> LazyFrame {
130        self.math_operator_1_in_1_out_period::<DEMA>(name, period, "dema")
131    }
132    pub fn trima(self, name: &str, period: usize) -> LazyFrame {
133        self.math_operator_1_in_1_out_period::<TRIMA>(name, period, "trima")
134    }
135    pub fn kama(self, name: &str, period: usize) -> LazyFrame {
136        self.math_operator_1_in_1_out_period::<KAMA>(name, period, "kama")
137    }
138    pub fn midpoint(self, name: &str, period: usize) -> LazyFrame {
139        self.math_operator_1_in_1_out_period::<MIDPOINT>(name, period, "midpoint")
140    }
141    pub fn ht_trendline(self, name: &str) -> LazyFrame {
142        self.math_transform_1_in_1_out::<HT_TRENDLINE>(name, "ht_trendline")
143    }
144    pub fn midprice(self, high: &str, low: &str, period: usize) -> LazyFrame {
145        self.math_operator_2_in_1_out_period::<MIDPRICE>(high, low, period, "midprice")
146    }
147
148    pub fn rsi(self, name: &str, period: usize) -> LazyFrame {
149        self.math_operator_1_in_1_out_period::<RSI>(name, period, "rsi")
150    }
151    pub fn mom(self, name: &str, period: usize) -> LazyFrame {
152        self.math_operator_1_in_1_out_period::<MOM>(name, period, "mom")
153    }
154    pub fn roc(self, name: &str, period: usize) -> LazyFrame {
155        self.math_operator_1_in_1_out_period::<ROC>(name, period, "roc")
156    }
157    pub fn rocp(self, name: &str, period: usize) -> LazyFrame {
158        self.math_operator_1_in_1_out_period::<ROCP>(name, period, "rocp")
159    }
160    pub fn rocr(self, name: &str, period: usize) -> LazyFrame {
161        self.math_operator_1_in_1_out_period::<ROCR>(name, period, "rocr")
162    }
163    pub fn rocr100(self, name: &str, period: usize) -> LazyFrame {
164        self.math_operator_1_in_1_out_period::<ROCR100>(name, period, "rocr100")
165    }
166    pub fn trix(self, name: &str, period: usize) -> LazyFrame {
167        self.math_operator_1_in_1_out_period::<TRIX>(name, period, "trix")
168    }
169    pub fn cmo(self, name: &str, period: usize) -> LazyFrame {
170        self.math_operator_1_in_1_out_period::<CMO>(name, period, "cmo")
171    }
172
173    /// Prado fractional differentiation (`d` order, weight truncation `threshold`).
174    pub fn frac_diff(self, name: &str, d: f64, threshold: f64) -> LazyFrame {
175        let name = name.to_string();
176        self.0.clone().with_columns([col(&name)
177            .map(
178                move |s| {
179                    let ca = s.f64()?;
180                    let mut indicator = FracDiff::new(d, threshold);
181                    let mut values = Vec::with_capacity(s.len());
182                    for i in 0..s.len() {
183                        let val = ca.get(i).unwrap_or(f64::NAN);
184                        values.push(indicator.next(val));
185                    }
186                    Ok(Some(Column::from(Series::new("frac_diff".into(), values))))
187                },
188                GetOutput::from_type(DataType::Float64),
189            )
190            .alias("frac_diff")])
191    }
192
193    pub fn adx(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
194        self.ta_3_in_1_out_period::<ADX>(high, low, close, period, "adx")
195    }
196    pub fn adxr(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
197        self.ta_3_in_1_out_period::<ADXR>(high, low, close, period, "adxr")
198    }
199    pub fn cci(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
200        self.ta_3_in_1_out_period::<CCI>(high, low, close, period, "cci")
201    }
202    pub fn willr(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
203        self.ta_3_in_1_out_period::<WILLR>(high, low, close, period, "willr")
204    }
205    pub fn dx(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
206        self.ta_3_in_1_out_period::<DX>(high, low, close, period, "dx")
207    }
208    pub fn plus_di(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
209        self.ta_3_in_1_out_period::<PLUS_DI>(high, low, close, period, "plus_di")
210    }
211    pub fn minus_di(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
212        self.ta_3_in_1_out_period::<MINUS_DI>(high, low, close, period, "minus_di")
213    }
214
215    pub fn ta_atr(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
216        self.ta_3_in_1_out_period::<TaATR>(high, low, close, period, "ta_atr")
217    }
218    pub fn ta_natr(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
219        self.ta_3_in_1_out_period::<TaNATR>(high, low, close, period, "ta_natr")
220    }
221    pub fn ta_trange(self, high: &str, low: &str, close: &str) -> LazyFrame {
222        self.ta_3_in_1_out_default::<TaTRANGE>(high, low, close, "ta_trange")
223    }
224
225    pub fn obv(self, close: &str, volume: &str) -> LazyFrame {
226        self.math_operator_2_in_1_out::<OBV>(close, volume, "obv")
227    }
228    pub fn ad(self, high: &str, low: &str, close: &str, volume: &str) -> LazyFrame {
229        self.ta_4_in_1_out_default::<AD>(high, low, close, volume, "ad")
230    }
231    pub fn adosc(
232        self,
233        high: &str,
234        low: &str,
235        close: &str,
236        volume: &str,
237        fast: usize,
238        slow: usize,
239    ) -> LazyFrame {
240        let high_str = high.to_string();
241        let low_str = low.to_string();
242        let close_str = close.to_string();
243        let volume_str = volume.to_string();
244        self.0.clone().with_columns([as_struct(vec![
245            col(&high_str),
246            col(&low_str),
247            col(&close_str),
248            col(&volume_str),
249        ])
250        .map(
251            move |s| {
252                let ca = s.struct_()?;
253                let s_h = ca.field_by_name(&high_str)?;
254                let s_l = ca.field_by_name(&low_str)?;
255                let s_c = ca.field_by_name(&close_str)?;
256                let s_v = ca.field_by_name(&volume_str)?;
257
258                let high = s_h.f64()?;
259                let low = s_l.f64()?;
260                let close = s_c.f64()?;
261                let volume = s_v.f64()?;
262
263                let mut indicator = ADOSC::new(fast, slow);
264                let mut values = Vec::with_capacity(s.len());
265
266                for i in 0..s.len() {
267                    let h = high.get(i).unwrap_or(f64::NAN);
268                    let l = low.get(i).unwrap_or(f64::NAN);
269                    let c = close.get(i).unwrap_or(f64::NAN);
270                    let v = volume.get(i).unwrap_or(f64::NAN);
271                    values.push(indicator.next((h, l, c, v)));
272                }
273
274                Ok(Some(Column::from(Series::new("adosc".into(), values))))
275            },
276            GetOutput::from_type(DataType::Float64),
277        )
278        .alias("adosc")])
279    }
280
281    pub fn aroon(self, high: &str, low: &str, period: usize) -> LazyFrame {
282        let high_str = high.to_string();
283        let low_str = low.to_string();
284        self.0
285            .clone()
286            .with_columns([as_struct(vec![col(&high_str), col(&low_str)])
287                .map(
288                    move |s| {
289                        let ca = s.struct_()?;
290                        let s_h = ca.field_by_name(&high_str)?;
291                        let s_l = ca.field_by_name(&low_str)?;
292                        let high = s_h.f64()?;
293                        let low = s_l.f64()?;
294
295                        let mut indicator = AROON::new(period);
296                        let mut up_vals = Vec::with_capacity(s.len());
297                        let mut down_vals = Vec::with_capacity(s.len());
298
299                        for i in 0..s.len() {
300                            let h = high.get(i).unwrap_or(f64::NAN);
301                            let l = low.get(i).unwrap_or(f64::NAN);
302                            let (up, down) = indicator.next((h, l));
303                            up_vals.push(up);
304                            down_vals.push(down);
305                        }
306
307                        let s_up = Series::new("aroon_up".into(), up_vals);
308                        let s_down = Series::new("aroon_down".into(), down_vals);
309                        let struct_series = StructChunked::from_series(
310                            "aroon_result".into(),
311                            s.len(),
312                            [s_up, s_down].iter(),
313                        )?;
314                        Ok(Some(Column::from(struct_series.into_series())))
315                    },
316                    GetOutput::from_type(DataType::Struct(vec![
317                        Field::new("aroon_up".into(), DataType::Float64),
318                        Field::new("aroon_down".into(), DataType::Float64),
319                    ])),
320                )
321                .alias("aroon")])
322    }
323
324    #[allow(clippy::too_many_arguments)]
325    pub fn stoch(
326        self,
327        high: &str,
328        low: &str,
329        close: &str,
330        fastk: usize,
331        slowk: usize,
332        slowk_matype: talib::MaType,
333        slowd: usize,
334        slowd_matype: talib::MaType,
335    ) -> LazyFrame {
336        let high_str = high.to_string();
337        let low_str = low.to_string();
338        let close_str = close.to_string();
339        self.0.clone().with_columns([as_struct(vec![
340            col(&high_str),
341            col(&low_str),
342            col(&close_str),
343        ])
344        .map(
345            move |s| {
346                let ca = s.struct_()?;
347                let s_h = ca.field_by_name(&high_str)?;
348                let s_l = ca.field_by_name(&low_str)?;
349                let s_c = ca.field_by_name(&close_str)?;
350                let high = s_h.f64()?;
351                let low = s_l.f64()?;
352                let close = s_c.f64()?;
353
354                let mut indicator = STOCH::new(fastk, slowk, slowk_matype, slowd, slowd_matype);
355                let mut k_vals = Vec::with_capacity(s.len());
356                let mut d_vals = Vec::with_capacity(s.len());
357
358                for i in 0..s.len() {
359                    let h = high.get(i).unwrap_or(f64::NAN);
360                    let l = low.get(i).unwrap_or(f64::NAN);
361                    let c = close.get(i).unwrap_or(f64::NAN);
362                    let (k, d) = indicator.next((h, l, c));
363                    k_vals.push(k);
364                    d_vals.push(d);
365                }
366
367                let s_k = Series::new("slowk".into(), k_vals);
368                let s_d = Series::new("slowd".into(), d_vals);
369                let struct_series =
370                    StructChunked::from_series("stoch_result".into(), s.len(), [s_k, s_d].iter())?;
371                Ok(Some(Column::from(struct_series.into_series())))
372            },
373            GetOutput::from_type(DataType::Struct(vec![
374                Field::new("slowk".into(), DataType::Float64),
375                Field::new("slowd".into(), DataType::Float64),
376            ])),
377        )
378        .alias("stoch")])
379    }
380
381    pub fn avgprice(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
382        self.ta_4_in_1_out_default::<AVGPRICE>(open, high, low, close, "avgprice")
383    }
384    pub fn medprice(self, high: &str, low: &str) -> LazyFrame {
385        self.math_operator_2_in_1_out::<MEDPRICE>(high, low, "medprice")
386    }
387    pub fn typprice(self, high: &str, low: &str, close: &str) -> LazyFrame {
388        self.ta_3_in_1_out_default::<TYPPRICE>(high, low, close, "typprice")
389    }
390    pub fn wclprice(self, high: &str, low: &str, close: &str) -> LazyFrame {
391        self.ta_3_in_1_out_default::<WCLPRICE>(high, low, close, "wclprice")
392    }
393
394    pub fn ht_dcperiod(self, name: &str) -> LazyFrame {
395        self.math_transform_1_in_1_out::<HT_DCPERIOD>(name, "ht_dcperiod")
396    }
397    pub fn ht_dcphase(self, name: &str) -> LazyFrame {
398        self.math_transform_1_in_1_out::<HT_DCPHASE>(name, "ht_dcphase")
399    }
400    pub fn ht_trendmode(self, name: &str) -> LazyFrame {
401        self.math_transform_1_in_1_out::<HT_TRENDMODE>(name, "ht_trendmode")
402    }
403
404    pub fn ta_stddev(self, name: &str, period: usize, nbdev: f64) -> LazyFrame {
405        let name_str = name.to_string();
406        self.0.clone().with_columns([col(&name_str)
407            .map(
408                move |s| {
409                    let ca = s.f64()?;
410                    let mut indicator = TaSTDDEV::new(period, nbdev);
411                    let mut values = Vec::with_capacity(s.len());
412                    for i in 0..s.len() {
413                        let val = ca.get(i).unwrap_or(f64::NAN);
414                        values.push(indicator.next(val));
415                    }
416                    Ok(Some(Column::from(Series::new("ta_stddev".into(), values))))
417                },
418                GetOutput::from_type(DataType::Float64),
419            )
420            .alias("ta_stddev")])
421    }
422    pub fn ta_var(self, name: &str, period: usize, nbdev: f64) -> LazyFrame {
423        let name_str = name.to_string();
424        self.0.clone().with_columns([col(&name_str)
425            .map(
426                move |s| {
427                    let ca = s.f64()?;
428                    let mut indicator = TaVAR::new(period, nbdev);
429                    let mut values = Vec::with_capacity(s.len());
430                    for i in 0..s.len() {
431                        let val = ca.get(i).unwrap_or(f64::NAN);
432                        values.push(indicator.next(val));
433                    }
434                    Ok(Some(Column::from(Series::new("ta_var".into(), values))))
435                },
436                GetOutput::from_type(DataType::Float64),
437            )
438            .alias("ta_var")])
439    }
440    pub fn ta_beta(self, in1: &str, in2: &str, period: usize) -> LazyFrame {
441        self.math_operator_2_in_1_out_period::<TaBETA>(in1, in2, period, "ta_beta")
442    }
443    pub fn ta_correl(self, in1: &str, in2: &str, period: usize) -> LazyFrame {
444        self.math_operator_2_in_1_out_period::<TaCORREL>(in1, in2, period, "ta_correl")
445    }
446    pub fn ta_linearreg(self, name: &str, period: usize) -> LazyFrame {
447        self.math_operator_1_in_1_out_period::<TaLINEARREG>(name, period, "ta_linearreg")
448    }
449    pub fn ta_linearreg_slope(self, name: &str, period: usize) -> LazyFrame {
450        self.math_operator_1_in_1_out_period::<TaLINEARREG_SLOPE>(
451            name,
452            period,
453            "ta_linearreg_slope",
454        )
455    }
456    pub fn ta_linearreg_intercept(self, name: &str, period: usize) -> LazyFrame {
457        self.math_operator_1_in_1_out_period::<TaLINEARREG_INTERCEPT>(
458            name,
459            period,
460            "ta_linearreg_intercept",
461        )
462    }
463    pub fn ta_linearreg_angle(self, name: &str, period: usize) -> LazyFrame {
464        self.math_operator_1_in_1_out_period::<TaLINEARREG_ANGLE>(
465            name,
466            period,
467            "ta_linearreg_angle",
468        )
469    }
470    pub fn ta_tsf(self, name: &str, period: usize) -> LazyFrame {
471        self.math_operator_1_in_1_out_period::<TaTSF>(name, period, "ta_tsf")
472    }
473
474    pub fn cdl_2crows(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
475        self.ta_4_in_1_out_default::<CDL2CROWS>(open, high, low, close, "cdl_2crows")
476    }
477    pub fn cdl_3blackcrows(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
478        self.ta_4_in_1_out_default::<CDL3BLACKCROWS>(open, high, low, close, "cdl_3blackcrows")
479    }
480    pub fn cdl_3inside(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
481        self.ta_4_in_1_out_default::<CDL3INSIDE>(open, high, low, close, "cdl_3inside")
482    }
483    pub fn cdl_3linestrike(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
484        self.ta_4_in_1_out_default::<CDL3LINESTRIKE>(open, high, low, close, "cdl_3linestrike")
485    }
486    pub fn cdl_3outside(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
487        self.ta_4_in_1_out_default::<CDL3OUTSIDE>(open, high, low, close, "cdl_3outside")
488    }
489    pub fn cdl_3starsinsouth(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
490        self.ta_4_in_1_out_default::<CDL3STARSINSOUTH>(open, high, low, close, "cdl_3starsinsouth")
491    }
492    pub fn cdl_3whitesoldiers(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
493        self.ta_4_in_1_out_default::<CDL3WHITESOLDIERS>(
494            open,
495            high,
496            low,
497            close,
498            "cdl_3whitesoldiers",
499        )
500    }
501    pub fn cdl_abandonedbaby(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
502        self.ta_4_in_1_out_default::<CDLABANDONEDBABY>(open, high, low, close, "cdl_abandonedbaby")
503    }
504    pub fn cdl_advanceblock(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
505        self.ta_4_in_1_out_default::<CDLADVANCEBLOCK>(open, high, low, close, "cdl_advanceblock")
506    }
507    pub fn cdl_belthold(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
508        self.ta_4_in_1_out_default::<CDLBELTHOLD>(open, high, low, close, "cdl_belthold")
509    }
510    pub fn cdl_breakaway(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
511        self.ta_4_in_1_out_default::<CDLBREAKAWAY>(open, high, low, close, "cdl_breakaway")
512    }
513    pub fn cdl_closingmarubozu(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
514        self.ta_4_in_1_out_default::<CDLCLOSINGMARUBOZU>(
515            open,
516            high,
517            low,
518            close,
519            "cdl_closingmarubozu",
520        )
521    }
522    pub fn cdl_concealbabyswall(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
523        self.ta_4_in_1_out_default::<CDLCONCEALBABYSWALL>(
524            open,
525            high,
526            low,
527            close,
528            "cdl_concealbabyswall",
529        )
530    }
531    pub fn cdl_counterattack(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
532        self.ta_4_in_1_out_default::<CDLCOUNTERATTACK>(open, high, low, close, "cdl_counterattack")
533    }
534    pub fn cdl_darkcloudcover(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
535        self.ta_4_in_1_out_default::<CDLDARKCLOUDCOVER>(
536            open,
537            high,
538            low,
539            close,
540            "cdl_darkcloudcover",
541        )
542    }
543    pub fn cdl_doji(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
544        self.ta_4_in_1_out_default::<CDLDOJI>(open, high, low, close, "cdl_doji")
545    }
546    pub fn cdl_dojistar(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
547        self.ta_4_in_1_out_default::<CDLDOJISTAR>(open, high, low, close, "cdl_dojistar")
548    }
549    pub fn cdl_dragonflydoji(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
550        self.ta_4_in_1_out_default::<CDLDRAGONFLYDOJI>(open, high, low, close, "cdl_dragonflydoji")
551    }
552    pub fn cdl_engulfing(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
553        self.ta_4_in_1_out_default::<CDLENGULFING>(open, high, low, close, "cdl_engulfing")
554    }
555    pub fn cdl_eveningdojistar(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
556        self.ta_4_in_1_out_default::<CDLEVENINGDOJISTAR>(
557            open,
558            high,
559            low,
560            close,
561            "cdl_eveningdojistar",
562        )
563    }
564    pub fn cdl_eveningstar(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
565        self.ta_4_in_1_out_default::<CDLEVENINGSTAR>(open, high, low, close, "cdl_eveningstar")
566    }
567    pub fn cdl_gapsidesidewhite(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
568        self.ta_4_in_1_out_default::<CDLGAPSIDESIDEWHITE>(
569            open,
570            high,
571            low,
572            close,
573            "cdl_gapsidesidewhite",
574        )
575    }
576    pub fn cdl_gravestonedoji(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
577        self.ta_4_in_1_out_default::<CDLGRAVESTONEDOJI>(
578            open,
579            high,
580            low,
581            close,
582            "cdl_gravestonedoji",
583        )
584    }
585    pub fn cdl_hammer(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
586        self.ta_4_in_1_out_default::<CDLHAMMER>(open, high, low, close, "cdl_hammer")
587    }
588    pub fn cdl_hangingman(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
589        self.ta_4_in_1_out_default::<CDLHANGINGMAN>(open, high, low, close, "cdl_hangingman")
590    }
591    pub fn cdl_harami(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
592        self.ta_4_in_1_out_default::<CDLHARAMI>(open, high, low, close, "cdl_harami")
593    }
594    pub fn cdl_haramicross(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
595        self.ta_4_in_1_out_default::<CDLHARAMICROSS>(open, high, low, close, "cdl_haramicross")
596    }
597    pub fn cdl_highwave(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
598        self.ta_4_in_1_out_default::<CDLHIGHWAVE>(open, high, low, close, "cdl_highwave")
599    }
600    pub fn cdl_hikkake(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
601        self.ta_4_in_1_out_default::<CDLHIKKAKE>(open, high, low, close, "cdl_hikkake")
602    }
603    pub fn cdl_hikkakemod(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
604        self.ta_4_in_1_out_default::<CDLHIKKAKEMOD>(open, high, low, close, "cdl_hikkakemod")
605    }
606    pub fn cdl_homingpigeon(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
607        self.ta_4_in_1_out_default::<CDLHOMINGPIGEON>(open, high, low, close, "cdl_homingpigeon")
608    }
609    pub fn cdl_identical3crows(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
610        self.ta_4_in_1_out_default::<CDLIDENTICAL3CROWS>(
611            open,
612            high,
613            low,
614            close,
615            "cdl_identical3crows",
616        )
617    }
618    pub fn cdl_inneck(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
619        self.ta_4_in_1_out_default::<CDLINNECK>(open, high, low, close, "cdl_inneck")
620    }
621    pub fn cdl_invertedhammer(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
622        self.ta_4_in_1_out_default::<CDLINVERTEDHAMMER>(
623            open,
624            high,
625            low,
626            close,
627            "cdl_invertedhammer",
628        )
629    }
630    pub fn cdl_kicking(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
631        self.ta_4_in_1_out_default::<CDLKICKING>(open, high, low, close, "cdl_kicking")
632    }
633    pub fn cdl_kickingbylength(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
634        self.ta_4_in_1_out_default::<CDLKICKINGBYLENGTH>(
635            open,
636            high,
637            low,
638            close,
639            "cdl_kickingbylength",
640        )
641    }
642    pub fn cdl_ladderbottom(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
643        self.ta_4_in_1_out_default::<CDLLADDERBOTTOM>(open, high, low, close, "cdl_ladderbottom")
644    }
645    pub fn cdl_longleggeddoji(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
646        self.ta_4_in_1_out_default::<CDLLONGLEGGEDDOJI>(
647            open,
648            high,
649            low,
650            close,
651            "cdl_longleggeddoji",
652        )
653    }
654    pub fn cdl_longline(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
655        self.ta_4_in_1_out_default::<CDLLONGLINE>(open, high, low, close, "cdl_longline")
656    }
657    pub fn cdl_marubozu(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
658        self.ta_4_in_1_out_default::<CDLMARUBOZU>(open, high, low, close, "cdl_marubozu")
659    }
660    pub fn cdl_matchinglow(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
661        self.ta_4_in_1_out_default::<CDLMATCHINGLOW>(open, high, low, close, "cdl_matchinglow")
662    }
663    pub fn cdl_mathold(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
664        self.ta_4_in_1_out_default::<CDLMATHOLD>(open, high, low, close, "cdl_mathold")
665    }
666    pub fn cdl_morningdojistar(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
667        self.ta_4_in_1_out_default::<CDLMORNINGDOJISTAR>(
668            open,
669            high,
670            low,
671            close,
672            "cdl_morningdojistar",
673        )
674    }
675    pub fn cdl_morningstar(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
676        self.ta_4_in_1_out_default::<CDLMORNINGSTAR>(open, high, low, close, "cdl_morningstar")
677    }
678    pub fn cdl_onneck(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
679        self.ta_4_in_1_out_default::<CDLONNECK>(open, high, low, close, "cdl_onneck")
680    }
681    pub fn cdl_piercing(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
682        self.ta_4_in_1_out_default::<CDLPIERCING>(open, high, low, close, "cdl_piercing")
683    }
684    pub fn cdl_rickshawman(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
685        self.ta_4_in_1_out_default::<CDLRICKSHAWMAN>(open, high, low, close, "cdl_rickshawman")
686    }
687    pub fn cdl_risefall3methods(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
688        self.ta_4_in_1_out_default::<CDLRISEFALL3METHODS>(
689            open,
690            high,
691            low,
692            close,
693            "cdl_risefall3methods",
694        )
695    }
696    pub fn cdl_separatinglines(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
697        self.ta_4_in_1_out_default::<CDLSEPARATINGLINES>(
698            open,
699            high,
700            low,
701            close,
702            "cdl_separatinglines",
703        )
704    }
705    pub fn cdl_shootingstar(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
706        self.ta_4_in_1_out_default::<CDLSHOOTINGSTAR>(open, high, low, close, "cdl_shootingstar")
707    }
708    pub fn cdl_shortline(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
709        self.ta_4_in_1_out_default::<CDLSHORTLINE>(open, high, low, close, "cdl_shortline")
710    }
711    pub fn cdl_spinningtop(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
712        self.ta_4_in_1_out_default::<CDLSPINNINGTOP>(open, high, low, close, "cdl_spinningtop")
713    }
714    pub fn cdl_stalledpattern(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
715        self.ta_4_in_1_out_default::<CDLSTALLEDPATTERN>(
716            open,
717            high,
718            low,
719            close,
720            "cdl_stalledpattern",
721        )
722    }
723    pub fn cdl_sticksandwich(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
724        self.ta_4_in_1_out_default::<CDLSTICKSANDWICH>(open, high, low, close, "cdl_sticksandwich")
725    }
726    pub fn cdl_takuri(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
727        self.ta_4_in_1_out_default::<CDLTAKURI>(open, high, low, close, "cdl_takuri")
728    }
729    pub fn cdl_tasukigap(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
730        self.ta_4_in_1_out_default::<CDLTASUKIGAP>(open, high, low, close, "cdl_tasukigap")
731    }
732    pub fn cdl_thrusting(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
733        self.ta_4_in_1_out_default::<CDLTHRUSTING>(open, high, low, close, "cdl_thrusting")
734    }
735    pub fn cdl_tristar(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
736        self.ta_4_in_1_out_default::<CDLTRISTAR>(open, high, low, close, "cdl_tristar")
737    }
738    pub fn cdl_unique3river(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
739        self.ta_4_in_1_out_default::<CDLUNIQUE3RIVER>(open, high, low, close, "cdl_unique3river")
740    }
741    pub fn cdl_upsidegap2crows(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
742        self.ta_4_in_1_out_default::<CDLUPSIDEGAP2CROWS>(
743            open,
744            high,
745            low,
746            close,
747            "cdl_upsidegap2crows",
748        )
749    }
750    pub fn cdl_xsidegap3methods(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
751        self.ta_4_in_1_out_default::<CDLXSIDEGAP3METHODS>(
752            open,
753            high,
754            low,
755            close,
756            "cdl_xsidegap3methods",
757        )
758    }
759
760    pub fn macd(self, name: &str, fast: usize, slow: usize, signal: usize) -> LazyFrame {
761        let name_str = name.to_string();
762        self.0.clone().with_columns([col(&name_str)
763            .map(
764                move |s| {
765                    let ca = s.f64()?;
766                    let mut indicator = MACD::new(fast, slow, signal);
767                    let mut macd_vals = Vec::with_capacity(s.len());
768                    let mut signal_vals = Vec::with_capacity(s.len());
769                    let mut hist_vals = Vec::with_capacity(s.len());
770
771                    for i in 0..s.len() {
772                        let val = ca.get(i).unwrap_or(f64::NAN);
773                        let (m, s_val, h) = indicator.next(val);
774                        macd_vals.push(m);
775                        signal_vals.push(s_val);
776                        hist_vals.push(h);
777                    }
778
779                    let s_macd = Series::new("macd".into(), macd_vals);
780                    let s_signal = Series::new("macd_signal".into(), signal_vals);
781                    let s_hist = Series::new("macd_hist".into(), hist_vals);
782
783                    let struct_series = StructChunked::from_series(
784                        "macd_result".into(),
785                        s.len(),
786                        [s_macd, s_signal, s_hist].iter(),
787                    )?;
788                    Ok(Some(Column::from(struct_series.into_series())))
789                },
790                GetOutput::from_type(DataType::Struct(vec![
791                    Field::new("macd".into(), DataType::Float64),
792                    Field::new("macd_signal".into(), DataType::Float64),
793                    Field::new("macd_hist".into(), DataType::Float64),
794                ])),
795            )
796            .alias("macd")])
797    }
798
799    pub fn bbands(
800        self,
801        name: &str,
802        period: usize,
803        nbdevup: f64,
804        nbdevdn: f64,
805        matype: talib::MaType,
806    ) -> LazyFrame {
807        let name_str = name.to_string();
808        self.0.clone().with_columns([col(&name_str)
809            .map(
810                move |s| {
811                    let ca = s.f64()?;
812                    let mut indicator = BBANDS::new(period, nbdevup, nbdevdn, matype);
813                    let mut upper_vals = Vec::with_capacity(s.len());
814                    let mut middle_vals = Vec::with_capacity(s.len());
815                    let mut lower_vals = Vec::with_capacity(s.len());
816
817                    for i in 0..s.len() {
818                        let val = ca.get(i).unwrap_or(f64::NAN);
819                        let (u, m, l) = indicator.next(val);
820                        upper_vals.push(u);
821                        middle_vals.push(m);
822                        lower_vals.push(l);
823                    }
824
825                    let s_upper = Series::new("upper".into(), upper_vals);
826                    let s_middle = Series::new("middle".into(), middle_vals);
827                    let s_lower = Series::new("lower".into(), lower_vals);
828
829                    let struct_series = StructChunked::from_series(
830                        "bbands_result".into(),
831                        s.len(),
832                        [s_upper, s_middle, s_lower].iter(),
833                    )?;
834                    Ok(Some(Column::from(struct_series.into_series())))
835                },
836                GetOutput::from_type(DataType::Struct(vec![
837                    Field::new("upper".into(), DataType::Float64),
838                    Field::new("middle".into(), DataType::Float64),
839                    Field::new("lower".into(), DataType::Float64),
840                ])),
841            )
842            .alias("bbands")])
843    }
844
845    #[allow(clippy::too_many_arguments)]
846    pub fn macdext(
847        self,
848        name: &str,
849        fastperiod: usize,
850        fastmatype: talib::MaType,
851        slowperiod: usize,
852        slowmatype: talib::MaType,
853        signalperiod: usize,
854        signalmatype: talib::MaType,
855    ) -> LazyFrame {
856        let name_str = name.to_string();
857        self.0.clone().with_columns([col(&name_str)
858            .map(
859                move |s| {
860                    let ca = s.f64()?;
861                    let mut indicator = MACDEXT::new(
862                        fastperiod,
863                        fastmatype,
864                        slowperiod,
865                        slowmatype,
866                        signalperiod,
867                        signalmatype,
868                    );
869                    let mut macd_vals = Vec::with_capacity(s.len());
870                    let mut signal_vals = Vec::with_capacity(s.len());
871                    let mut hist_vals = Vec::with_capacity(s.len());
872
873                    for i in 0..s.len() {
874                        let val = ca.get(i).unwrap_or(f64::NAN);
875                        let (m, s_val, h) = indicator.next(val);
876                        macd_vals.push(m);
877                        signal_vals.push(s_val);
878                        hist_vals.push(h);
879                    }
880
881                    let s_macd = Series::new("macd".into(), macd_vals);
882                    let s_signal = Series::new("macd_signal".into(), signal_vals);
883                    let s_hist = Series::new("macd_hist".into(), hist_vals);
884
885                    let struct_series = StructChunked::from_series(
886                        "macdext_result".into(),
887                        s.len(),
888                        [s_macd, s_signal, s_hist].iter(),
889                    )?;
890                    Ok(Some(Column::from(struct_series.into_series())))
891                },
892                GetOutput::from_type(DataType::Struct(vec![
893                    Field::new("macd".into(), DataType::Float64),
894                    Field::new("macd_signal".into(), DataType::Float64),
895                    Field::new("macd_hist".into(), DataType::Float64),
896                ])),
897            )
898            .alias("macdext")])
899    }
900
901    pub fn macdfix(self, name: &str, signalperiod: usize) -> LazyFrame {
902        let name_str = name.to_string();
903        self.0.clone().with_columns([col(&name_str)
904            .map(
905                move |s| {
906                    let ca = s.f64()?;
907                    let mut indicator = MACDFIX::new(signalperiod);
908                    let mut macd_vals = Vec::with_capacity(s.len());
909                    let mut signal_vals = Vec::with_capacity(s.len());
910                    let mut hist_vals = Vec::with_capacity(s.len());
911
912                    for i in 0..s.len() {
913                        let val = ca.get(i).unwrap_or(f64::NAN);
914                        let (m, s_val, h) = indicator.next(val);
915                        macd_vals.push(m);
916                        signal_vals.push(s_val);
917                        hist_vals.push(h);
918                    }
919
920                    let s_macd = Series::new("macd".into(), macd_vals);
921                    let s_signal = Series::new("macd_signal".into(), signal_vals);
922                    let s_hist = Series::new("macd_hist".into(), hist_vals);
923
924                    let struct_series = StructChunked::from_series(
925                        "macdfix_result".into(),
926                        s.len(),
927                        [s_macd, s_signal, s_hist].iter(),
928                    )?;
929                    Ok(Some(Column::from(struct_series.into_series())))
930                },
931                GetOutput::from_type(DataType::Struct(vec![
932                    Field::new("macd".into(), DataType::Float64),
933                    Field::new("macd_signal".into(), DataType::Float64),
934                    Field::new("macd_hist".into(), DataType::Float64),
935                ])),
936            )
937            .alias("macdfix")])
938    }
939
940    pub fn stochf(
941        self,
942        high: &str,
943        low: &str,
944        close: &str,
945        fastk_period: usize,
946        fastd_period: usize,
947        fastd_matype: talib::MaType,
948    ) -> LazyFrame {
949        let high_str = high.to_string();
950        let low_str = low.to_string();
951        let close_str = close.to_string();
952        self.0.clone().with_columns([as_struct(vec![
953            col(&high_str),
954            col(&low_str),
955            col(&close_str),
956        ])
957        .map(
958            move |s| {
959                let ca = s.struct_()?;
960                let s_h = ca.field_by_name(&high_str)?;
961                let s_l = ca.field_by_name(&low_str)?;
962                let s_c = ca.field_by_name(&close_str)?;
963                let high = s_h.f64()?;
964                let low = s_l.f64()?;
965                let close = s_c.f64()?;
966
967                let mut indicator = STOCHF::new(fastk_period, fastd_period, fastd_matype);
968                let mut k_vals = Vec::with_capacity(s.len());
969                let mut d_vals = Vec::with_capacity(s.len());
970
971                for i in 0..s.len() {
972                    let h = high.get(i).unwrap_or(f64::NAN);
973                    let l = low.get(i).unwrap_or(f64::NAN);
974                    let c = close.get(i).unwrap_or(f64::NAN);
975                    let (k, d) = indicator.next((h, l, c));
976                    k_vals.push(k);
977                    d_vals.push(d);
978                }
979
980                let s_k = Series::new("fastk".into(), k_vals);
981                let s_d = Series::new("fastd".into(), d_vals);
982                let struct_series =
983                    StructChunked::from_series("stochf_result".into(), s.len(), [s_k, s_d].iter())?;
984                Ok(Some(Column::from(struct_series.into_series())))
985            },
986            GetOutput::from_type(DataType::Struct(vec![
987                Field::new("fastk".into(), DataType::Float64),
988                Field::new("fastd".into(), DataType::Float64),
989            ])),
990        )
991        .alias("stochf")])
992    }
993
994    pub fn stochrsi(
995        self,
996        name: &str,
997        timeperiod: usize,
998        fastk_period: usize,
999        fastd_period: usize,
1000        fastd_matype: talib::MaType,
1001    ) -> LazyFrame {
1002        let name_str = name.to_string();
1003        self.0.clone().with_columns([col(&name_str)
1004            .map(
1005                move |s| {
1006                    let ca = s.f64()?;
1007                    let mut indicator =
1008                        STOCHRSI::new(timeperiod, fastk_period, fastd_period, fastd_matype);
1009                    let mut k_vals = Vec::with_capacity(s.len());
1010                    let mut d_vals = Vec::with_capacity(s.len());
1011
1012                    for i in 0..s.len() {
1013                        let val = ca.get(i).unwrap_or(f64::NAN);
1014                        let (k, d) = indicator.next(val);
1015                        k_vals.push(k);
1016                        d_vals.push(d);
1017                    }
1018
1019                    let s_k = Series::new("fastk".into(), k_vals);
1020                    let s_d = Series::new("fastd".into(), d_vals);
1021
1022                    let struct_series = StructChunked::from_series(
1023                        "stochrsi_result".into(),
1024                        s.len(),
1025                        [s_k, s_d].iter(),
1026                    )?;
1027                    Ok(Some(Column::from(struct_series.into_series())))
1028                },
1029                GetOutput::from_type(DataType::Struct(vec![
1030                    Field::new("fastk".into(), DataType::Float64),
1031                    Field::new("fastd".into(), DataType::Float64),
1032                ])),
1033            )
1034            .alias("stochrsi")])
1035    }
1036
1037    pub fn apo(
1038        self,
1039        name: &str,
1040        fastperiod: usize,
1041        slowperiod: usize,
1042        matype: talib::MaType,
1043    ) -> LazyFrame {
1044        let name_str = name.to_string();
1045        self.0.clone().with_columns([col(&name_str)
1046            .map(
1047                move |s| {
1048                    let ca = s.f64()?;
1049                    let mut indicator = APO::new(fastperiod, slowperiod, matype);
1050                    let mut values = Vec::with_capacity(s.len());
1051                    for i in 0..s.len() {
1052                        let val = ca.get(i).unwrap_or(f64::NAN);
1053                        values.push(indicator.next(val));
1054                    }
1055                    Ok(Some(Column::from(Series::new("apo".into(), values))))
1056                },
1057                GetOutput::from_type(DataType::Float64),
1058            )
1059            .alias("apo")])
1060    }
1061
1062    pub fn ppo(
1063        self,
1064        name: &str,
1065        fastperiod: usize,
1066        slowperiod: usize,
1067        matype: talib::MaType,
1068    ) -> LazyFrame {
1069        let name_str = name.to_string();
1070        self.0.clone().with_columns([col(&name_str)
1071            .map(
1072                move |s| {
1073                    let ca = s.f64()?;
1074                    let mut indicator = PPO::new(fastperiod, slowperiod, matype);
1075                    let mut values = Vec::with_capacity(s.len());
1076                    for i in 0..s.len() {
1077                        let val = ca.get(i).unwrap_or(f64::NAN);
1078                        values.push(indicator.next(val));
1079                    }
1080                    Ok(Some(Column::from(Series::new("ppo".into(), values))))
1081                },
1082                GetOutput::from_type(DataType::Float64),
1083            )
1084            .alias("ppo")])
1085    }
1086
1087    pub fn bop(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
1088        self.ta_4_in_1_out_default::<BOP>(open, high, low, close, "bop")
1089    }
1090
1091    pub fn aroonosc(self, high: &str, low: &str, period: usize) -> LazyFrame {
1092        self.math_operator_2_in_1_out_period::<AROONOSC>(high, low, period, "aroonosc")
1093    }
1094
1095    pub fn mfi(self, high: &str, low: &str, close: &str, volume: &str, period: usize) -> LazyFrame {
1096        let high_str = high.to_string();
1097        let low_str = low.to_string();
1098        let close_str = close.to_string();
1099        let volume_str = volume.to_string();
1100        self.0.clone().with_columns([as_struct(vec![
1101            col(&high_str),
1102            col(&low_str),
1103            col(&close_str),
1104            col(&volume_str),
1105        ])
1106        .map(
1107            move |s| {
1108                let ca = s.struct_()?;
1109                let s_h = ca.field_by_name(&high_str)?;
1110                let s_l = ca.field_by_name(&low_str)?;
1111                let s_c = ca.field_by_name(&close_str)?;
1112                let s_v = ca.field_by_name(&volume_str)?;
1113
1114                let high = s_h.f64()?;
1115                let low = s_l.f64()?;
1116                let close = s_c.f64()?;
1117                let volume = s_v.f64()?;
1118
1119                let mut indicator = MFI::new(period);
1120                let mut values = Vec::with_capacity(s.len());
1121
1122                for i in 0..s.len() {
1123                    let h = high.get(i).unwrap_or(f64::NAN);
1124                    let l = low.get(i).unwrap_or(f64::NAN);
1125                    let c = close.get(i).unwrap_or(f64::NAN);
1126                    let v = volume.get(i).unwrap_or(f64::NAN);
1127                    values.push(indicator.next((h, l, c, v)));
1128                }
1129
1130                Ok(Some(Column::from(Series::new("mfi".into(), values))))
1131            },
1132            GetOutput::from_type(DataType::Float64),
1133        )
1134        .alias("mfi")])
1135    }
1136
1137    pub fn ultosc(
1138        self,
1139        high: &str,
1140        low: &str,
1141        close: &str,
1142        timeperiod1: usize,
1143        timeperiod2: usize,
1144        timeperiod3: usize,
1145    ) -> LazyFrame {
1146        let high_str = high.to_string();
1147        let low_str = low.to_string();
1148        let close_str = close.to_string();
1149        self.0.clone().with_columns([as_struct(vec![
1150            col(&high_str),
1151            col(&low_str),
1152            col(&close_str),
1153        ])
1154        .map(
1155            move |s| {
1156                let ca = s.struct_()?;
1157                let s_h = ca.field_by_name(&high_str)?;
1158                let s_l = ca.field_by_name(&low_str)?;
1159                let s_c = ca.field_by_name(&close_str)?;
1160                let high = s_h.f64()?;
1161                let low = s_l.f64()?;
1162                let close = s_c.f64()?;
1163
1164                let mut indicator = ULTOSC::new(timeperiod1, timeperiod2, timeperiod3);
1165                let mut values = Vec::with_capacity(s.len());
1166
1167                for i in 0..s.len() {
1168                    let h = high.get(i).unwrap_or(f64::NAN);
1169                    let l = low.get(i).unwrap_or(f64::NAN);
1170                    let c = close.get(i).unwrap_or(f64::NAN);
1171                    values.push(indicator.next((h, l, c)));
1172                }
1173
1174                Ok(Some(Column::from(Series::new("ultosc".into(), values))))
1175            },
1176            GetOutput::from_type(DataType::Float64),
1177        )
1178        .alias("ultosc")])
1179    }
1180
1181    pub fn plus_dm(self, high: &str, low: &str, period: usize) -> LazyFrame {
1182        self.math_operator_2_in_1_out_period::<PLUS_DM>(high, low, period, "plus_dm")
1183    }
1184
1185    pub fn minus_dm(self, high: &str, low: &str, period: usize) -> LazyFrame {
1186        self.math_operator_2_in_1_out_period::<MINUS_DM>(high, low, period, "minus_dm")
1187    }
1188
1189    pub fn t3(self, name: &str, period: usize, v_factor: f64) -> LazyFrame {
1190        let name_str = name.to_string();
1191        self.0.clone().with_columns([col(&name_str)
1192            .map(
1193                move |s| {
1194                    let ca = s.f64()?;
1195                    let mut indicator = T3::new(period, v_factor);
1196                    let mut values = Vec::with_capacity(s.len());
1197                    for i in 0..s.len() {
1198                        let val = ca.get(i).unwrap_or(f64::NAN);
1199                        values.push(indicator.next(val));
1200                    }
1201                    Ok(Some(Column::from(Series::new("t3".into(), values))))
1202                },
1203                GetOutput::from_type(DataType::Float64),
1204            )
1205            .alias("t3")])
1206    }
1207
1208    pub fn mama(self, name: &str, fastlimit: f64, slowlimit: f64) -> LazyFrame {
1209        let name_str = name.to_string();
1210        self.0.clone().with_columns([col(&name_str)
1211            .map(
1212                move |s| {
1213                    let ca = s.f64()?;
1214                    let mut indicator = MAMA::new(fastlimit, slowlimit);
1215                    let mut mama_vals = Vec::with_capacity(s.len());
1216                    let mut fama_vals = Vec::with_capacity(s.len());
1217
1218                    for i in 0..s.len() {
1219                        let val = ca.get(i).unwrap_or(f64::NAN);
1220                        let (m, f) = indicator.next(val);
1221                        mama_vals.push(m);
1222                        fama_vals.push(f);
1223                    }
1224
1225                    let s_mama = Series::new("mama".into(), mama_vals);
1226                    let s_fama = Series::new("fama".into(), fama_vals);
1227
1228                    let struct_series = StructChunked::from_series(
1229                        "mama_result".into(),
1230                        s.len(),
1231                        [s_mama, s_fama].iter(),
1232                    )?;
1233                    Ok(Some(Column::from(struct_series.into_series())))
1234                },
1235                GetOutput::from_type(DataType::Struct(vec![
1236                    Field::new("mama".into(), DataType::Float64),
1237                    Field::new("fama".into(), DataType::Float64),
1238                ])),
1239            )
1240            .alias("mama")])
1241    }
1242
1243    pub fn sar(self, high: &str, low: &str, acceleration: f64, maximum: f64) -> LazyFrame {
1244        let high_str = high.to_string();
1245        let low_str = low.to_string();
1246        self.0
1247            .clone()
1248            .with_columns([as_struct(vec![col(&high_str), col(&low_str)])
1249                .map(
1250                    move |s| {
1251                        let ca = s.struct_()?;
1252                        let s_h = ca.field_by_name(&high_str)?;
1253                        let s_l = ca.field_by_name(&low_str)?;
1254                        let high = s_h.f64()?;
1255                        let low = s_l.f64()?;
1256
1257                        let mut indicator = SAR::new(acceleration, maximum);
1258                        let mut values = Vec::with_capacity(s.len());
1259
1260                        for i in 0..s.len() {
1261                            let h = high.get(i).unwrap_or(f64::NAN);
1262                            let l = low.get(i).unwrap_or(f64::NAN);
1263                            values.push(indicator.next((h, l)));
1264                        }
1265
1266                        Ok(Some(Column::from(Series::new("sar".into(), values))))
1267                    },
1268                    GetOutput::from_type(DataType::Float64),
1269                )
1270                .alias("sar")])
1271    }
1272
1273    #[allow(clippy::too_many_arguments)]
1274    pub fn sarext(
1275        self,
1276        high: &str,
1277        low: &str,
1278        startvalue: f64,
1279        offsetonreverse: f64,
1280        accelerationinitlong: f64,
1281        accelerationlong: f64,
1282        accelerationmaxlong: f64,
1283        accelerationinitshort: f64,
1284        accelerationshort: f64,
1285        accelerationmaxshort: f64,
1286    ) -> LazyFrame {
1287        let high_str = high.to_string();
1288        let low_str = low.to_string();
1289        self.0
1290            .clone()
1291            .with_columns([as_struct(vec![col(&high_str), col(&low_str)])
1292                .map(
1293                    move |s| {
1294                        let ca = s.struct_()?;
1295                        let s_h = ca.field_by_name(&high_str)?;
1296                        let s_l = ca.field_by_name(&low_str)?;
1297                        let high = s_h.f64()?;
1298                        let low = s_l.f64()?;
1299
1300                        let mut indicator = SAREXT::new(
1301                            startvalue,
1302                            offsetonreverse,
1303                            accelerationinitlong,
1304                            accelerationlong,
1305                            accelerationmaxlong,
1306                            accelerationinitshort,
1307                            accelerationshort,
1308                            accelerationmaxshort,
1309                        );
1310                        let mut values = Vec::with_capacity(s.len());
1311
1312                        for i in 0..s.len() {
1313                            let h = high.get(i).unwrap_or(f64::NAN);
1314                            let l = low.get(i).unwrap_or(f64::NAN);
1315                            values.push(indicator.next((h, l)));
1316                        }
1317
1318                        Ok(Some(Column::from(Series::new("sarext".into(), values))))
1319                    },
1320                    GetOutput::from_type(DataType::Float64),
1321                )
1322                .alias("sarext")])
1323    }
1324
1325    pub fn mavp(
1326        self,
1327        in1: &str,
1328        in2: &str,
1329        minperiod: usize,
1330        maxperiod: usize,
1331        matype: talib::MaType,
1332    ) -> LazyFrame {
1333        let in1_str = in1.to_string();
1334        let in2_str = in2.to_string();
1335        self.0
1336            .clone()
1337            .with_columns([as_struct(vec![col(&in1_str), col(&in2_str)])
1338                .map(
1339                    move |s| {
1340                        let ca = s.struct_()?;
1341                        let s_1 = ca.field_by_name(&in1_str)?;
1342                        let s_2 = ca.field_by_name(&in2_str)?;
1343                        let in1_ca = s_1.f64()?;
1344                        let in2_ca = s_2.f64()?;
1345
1346                        let mut indicator = MAVP::new(minperiod, maxperiod, matype);
1347                        let mut values = Vec::with_capacity(s.len());
1348
1349                        for i in 0..s.len() {
1350                            let i1 = in1_ca.get(i).unwrap_or(f64::NAN);
1351                            let i2 = in2_ca.get(i).unwrap_or(f64::NAN);
1352                            values.push(indicator.next((i1, i2)));
1353                        }
1354
1355                        Ok(Some(Column::from(Series::new("mavp".into(), values))))
1356                    },
1357                    GetOutput::from_type(DataType::Float64),
1358                )
1359                .alias("mavp")])
1360    }
1361
1362    pub fn ht_phasor(self, name: &str) -> LazyFrame {
1363        let name_str = name.to_string();
1364        self.0.clone().with_columns([col(&name_str)
1365            .map(
1366                move |s| {
1367                    let ca = s.f64()?;
1368                    let mut indicator = HT_PHASOR::new();
1369                    let mut inphase_vals = Vec::with_capacity(s.len());
1370                    let mut quadrature_vals = Vec::with_capacity(s.len());
1371
1372                    for i in 0..s.len() {
1373                        let val = ca.get(i).unwrap_or(f64::NAN);
1374                        let (inp, q) = indicator.next(val);
1375                        inphase_vals.push(inp);
1376                        quadrature_vals.push(q);
1377                    }
1378
1379                    let s_inphase = Series::new("inphase".into(), inphase_vals);
1380                    let s_quadrature = Series::new("quadrature".into(), quadrature_vals);
1381
1382                    let struct_series = StructChunked::from_series(
1383                        "ht_phasor_result".into(),
1384                        s.len(),
1385                        [s_inphase, s_quadrature].iter(),
1386                    )?;
1387                    Ok(Some(Column::from(struct_series.into_series())))
1388                },
1389                GetOutput::from_type(DataType::Struct(vec![
1390                    Field::new("inphase".into(), DataType::Float64),
1391                    Field::new("quadrature".into(), DataType::Float64),
1392                ])),
1393            )
1394            .alias("ht_phasor")])
1395    }
1396
1397    pub fn ht_sine(self, name: &str) -> LazyFrame {
1398        let name_str = name.to_string();
1399        self.0.clone().with_columns([col(&name_str)
1400            .map(
1401                move |s| {
1402                    let ca = s.f64()?;
1403                    let mut indicator = HT_SINE::new();
1404                    let mut sine_vals = Vec::with_capacity(s.len());
1405                    let mut leadsine_vals = Vec::with_capacity(s.len());
1406
1407                    for i in 0..s.len() {
1408                        let val = ca.get(i).unwrap_or(f64::NAN);
1409                        let (si, ls) = indicator.next(val);
1410                        sine_vals.push(si);
1411                        leadsine_vals.push(ls);
1412                    }
1413
1414                    let s_sine = Series::new("sine".into(), sine_vals);
1415                    let s_leadsine = Series::new("leadsine".into(), leadsine_vals);
1416
1417                    let struct_series = StructChunked::from_series(
1418                        "ht_sine_result".into(),
1419                        s.len(),
1420                        [s_sine, s_leadsine].iter(),
1421                    )?;
1422                    Ok(Some(Column::from(struct_series.into_series())))
1423                },
1424                GetOutput::from_type(DataType::Struct(vec![
1425                    Field::new("sine".into(), DataType::Float64),
1426                    Field::new("leadsine".into(), DataType::Float64),
1427                ])),
1428            )
1429            .alias("ht_sine")])
1430    }
1431
1432    fn ta_3_in_1_out_period<I>(
1433        self,
1434        in1: &str,
1435        in2: &str,
1436        in3: &str,
1437        period: usize,
1438        output_name: &str,
1439    ) -> LazyFrame
1440    where
1441        I: Next<(f64, f64, f64), Output = f64> + Send + Sync + 'static,
1442        I: From<usize>,
1443    {
1444        let in1_str = in1.to_string();
1445        let in2_str = in2.to_string();
1446        let in3_str = in3.to_string();
1447        let output_name_str = output_name.to_string();
1448        let output_name_for_closure = output_name_str.clone();
1449        self.0.clone().with_columns(
1450            [as_struct(vec![col(&in1_str), col(&in2_str), col(&in3_str)])
1451                .map(
1452                    move |s| {
1453                        let ca = s.struct_()?;
1454                        let s1 = ca.field_by_name(&in1_str)?;
1455                        let s2 = ca.field_by_name(&in2_str)?;
1456                        let s3 = ca.field_by_name(&in3_str)?;
1457
1458                        let ca1 = s1.f64()?;
1459                        let ca2 = s2.f64()?;
1460                        let ca3 = s3.f64()?;
1461
1462                        let mut indicator = I::from(period);
1463                        let mut values = Vec::with_capacity(s.len());
1464
1465                        for i in 0..s.len() {
1466                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1467                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1468                            let v3 = ca3.get(i).unwrap_or(f64::NAN);
1469                            values.push(indicator.next((v1, v2, v3)));
1470                        }
1471
1472                        Ok(Some(Column::from(Series::new(
1473                            output_name_for_closure.clone().into(),
1474                            values,
1475                        ))))
1476                    },
1477                    GetOutput::from_type(DataType::Float64),
1478                )
1479                .alias(&output_name_str)],
1480        )
1481    }
1482
1483    fn ta_3_in_1_out_default<I>(
1484        self,
1485        in1: &str,
1486        in2: &str,
1487        in3: &str,
1488        output_name: &str,
1489    ) -> LazyFrame
1490    where
1491        I: Next<(f64, f64, f64), Output = f64> + Default + Send + Sync + 'static,
1492    {
1493        let in1_str = in1.to_string();
1494        let in2_str = in2.to_string();
1495        let in3_str = in3.to_string();
1496        let output_name_str = output_name.to_string();
1497        let output_name_for_closure = output_name_str.clone();
1498        self.0.clone().with_columns(
1499            [as_struct(vec![col(&in1_str), col(&in2_str), col(&in3_str)])
1500                .map(
1501                    move |s| {
1502                        let ca = s.struct_()?;
1503                        let s1 = ca.field_by_name(&in1_str)?;
1504                        let s2 = ca.field_by_name(&in2_str)?;
1505                        let s3 = ca.field_by_name(&in3_str)?;
1506
1507                        let ca1 = s1.f64()?;
1508                        let ca2 = s2.f64()?;
1509                        let ca3 = s3.f64()?;
1510
1511                        let mut indicator = I::default();
1512                        let mut values = Vec::with_capacity(s.len());
1513
1514                        for i in 0..s.len() {
1515                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1516                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1517                            let v3 = ca3.get(i).unwrap_or(f64::NAN);
1518                            values.push(indicator.next((v1, v2, v3)));
1519                        }
1520
1521                        Ok(Some(Column::from(Series::new(
1522                            output_name_for_closure.clone().into(),
1523                            values,
1524                        ))))
1525                    },
1526                    GetOutput::from_type(DataType::Float64),
1527                )
1528                .alias(&output_name_str)],
1529        )
1530    }
1531
1532    fn ta_4_in_1_out_default<I>(
1533        self,
1534        in1: &str,
1535        in2: &str,
1536        in3: &str,
1537        in4: &str,
1538        output_name: &str,
1539    ) -> LazyFrame
1540    where
1541        I: Next<(f64, f64, f64, f64), Output = f64> + Default + Send + Sync + 'static,
1542    {
1543        let in1_str = in1.to_string();
1544        let in2_str = in2.to_string();
1545        let in3_str = in3.to_string();
1546        let in4_str = in4.to_string();
1547        let output_name_str = output_name.to_string();
1548        let output_name_for_closure = output_name_str.clone();
1549        self.0.clone().with_columns([as_struct(vec![
1550            col(&in1_str),
1551            col(&in2_str),
1552            col(&in3_str),
1553            col(&in4_str),
1554        ])
1555        .map(
1556            move |s| {
1557                let ca = s.struct_()?;
1558                let s1 = ca.field_by_name(&in1_str)?;
1559                let s2 = ca.field_by_name(&in2_str)?;
1560                let s3 = ca.field_by_name(&in3_str)?;
1561                let s4 = ca.field_by_name(&in4_str)?;
1562
1563                let ca1 = s1.f64()?;
1564                let ca2 = s2.f64()?;
1565                let ca3 = s3.f64()?;
1566                let ca4 = s4.f64()?;
1567
1568                let mut indicator = I::default();
1569                let mut values = Vec::with_capacity(s.len());
1570
1571                for i in 0..s.len() {
1572                    let v1 = ca1.get(i).unwrap_or(f64::NAN);
1573                    let v2 = ca2.get(i).unwrap_or(f64::NAN);
1574                    let v3 = ca3.get(i).unwrap_or(f64::NAN);
1575                    let v4 = ca4.get(i).unwrap_or(f64::NAN);
1576                    values.push(indicator.next((v1, v2, v3, v4)));
1577                }
1578
1579                Ok(Some(Column::from(Series::new(
1580                    output_name_for_closure.clone().into(),
1581                    values,
1582                ))))
1583            },
1584            GetOutput::from_type(DataType::Float64),
1585        )
1586        .alias(&output_name_str)])
1587    }
1588
1589    fn math_transform_1_in_1_out<I>(self, name: &str, output_name: &str) -> LazyFrame
1590    where
1591        I: Next<f64, Output = f64> + Default + Send + Sync + 'static,
1592    {
1593        let name = name.to_string();
1594        let output_name_str = output_name.to_string();
1595        let output_name_for_closure = output_name_str.clone();
1596        self.0.clone().with_columns([col(&name)
1597            .map(
1598                move |s| {
1599                    let ca = s.f64()?;
1600                    let mut indicator = I::default();
1601                    let mut values = Vec::with_capacity(s.len());
1602
1603                    for i in 0..s.len() {
1604                        let val = ca.get(i).unwrap_or(f64::NAN);
1605                        values.push(indicator.next(val));
1606                    }
1607
1608                    Ok(Some(Column::from(Series::new(
1609                        output_name_for_closure.clone().into(),
1610                        values,
1611                    ))))
1612                },
1613                GetOutput::from_type(DataType::Float64),
1614            )
1615            .alias(&output_name_str)])
1616    }
1617
1618    fn math_operator_2_in_1_out<I>(self, in1: &str, in2: &str, output_name: &str) -> LazyFrame
1619    where
1620        I: Next<(f64, f64), Output = f64> + Default + Send + Sync + 'static,
1621    {
1622        let in1_str = in1.to_string();
1623        let in2_str = in2.to_string();
1624        let output_name_str = output_name.to_string();
1625        let output_name_for_closure = output_name_str.clone();
1626        self.0
1627            .clone()
1628            .with_columns([as_struct(vec![col(&in1_str), col(&in2_str)])
1629                .map(
1630                    move |s| {
1631                        let ca = s.struct_()?;
1632                        let s1 = ca.field_by_name(&in1_str)?;
1633                        let s2 = ca.field_by_name(&in2_str)?;
1634
1635                        let ca1 = s1.f64()?;
1636                        let ca2 = s2.f64()?;
1637
1638                        let mut indicator = I::default();
1639                        let mut values = Vec::with_capacity(s.len());
1640
1641                        for i in 0..s.len() {
1642                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1643                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1644                            values.push(indicator.next((v1, v2)));
1645                        }
1646
1647                        Ok(Some(Column::from(Series::new(
1648                            output_name_for_closure.clone().into(),
1649                            values,
1650                        ))))
1651                    },
1652                    GetOutput::from_type(DataType::Float64),
1653                )
1654                .alias(&output_name_str)])
1655    }
1656
1657    fn math_operator_1_in_1_out_period<I>(
1658        self,
1659        name: &str,
1660        period: usize,
1661        output_name: &str,
1662    ) -> LazyFrame
1663    where
1664        I: Next<f64, Output = f64> + Send + Sync + 'static,
1665        I: From<usize>,
1666    {
1667        let name = name.to_string();
1668        let output_name_str = output_name.to_string();
1669        let output_name_for_closure = output_name_str.clone();
1670        self.0.clone().with_columns([col(&name)
1671            .map(
1672                move |s| {
1673                    let ca = s.f64()?;
1674                    let mut indicator = I::from(period);
1675                    let mut values = Vec::with_capacity(s.len());
1676
1677                    for i in 0..s.len() {
1678                        let val = ca.get(i).unwrap_or(f64::NAN);
1679                        values.push(indicator.next(val));
1680                    }
1681
1682                    Ok(Some(Column::from(Series::new(
1683                        output_name_for_closure.clone().into(),
1684                        values,
1685                    ))))
1686                },
1687                GetOutput::from_type(DataType::Float64),
1688            )
1689            .alias(&output_name_str)])
1690    }
1691
1692    fn math_operator_2_in_1_out_period<I>(
1693        self,
1694        in1: &str,
1695        in2: &str,
1696        period: usize,
1697        output_name: &str,
1698    ) -> LazyFrame
1699    where
1700        I: Next<(f64, f64), Output = f64> + Send + Sync + 'static,
1701        I: From<usize>,
1702    {
1703        let in1_str = in1.to_string();
1704        let in2_str = in2.to_string();
1705        let output_name_str = output_name.to_string();
1706        let output_name_for_closure = output_name_str.clone();
1707        self.0
1708            .clone()
1709            .with_columns([as_struct(vec![col(&in1_str), col(&in2_str)])
1710                .map(
1711                    move |s| {
1712                        let ca = s.struct_()?;
1713                        let s1 = ca.field_by_name(&in1_str)?;
1714                        let s2 = ca.field_by_name(&in2_str)?;
1715
1716                        let ca1 = s1.f64()?;
1717                        let ca2 = s2.f64()?;
1718
1719                        let mut indicator = I::from(period);
1720                        let mut values = Vec::with_capacity(s.len());
1721
1722                        for i in 0..s.len() {
1723                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1724                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1725                            values.push(indicator.next((v1, v2)));
1726                        }
1727
1728                        Ok(Some(Column::from(Series::new(
1729                            output_name_for_closure.clone().into(),
1730                            values,
1731                        ))))
1732                    },
1733                    GetOutput::from_type(DataType::Float64),
1734                )
1735                .alias(&output_name_str)])
1736    }
1737
1738    pub fn supertrend(self, period: usize, multiplier: f64) -> LazyFrame {
1739        self.0
1740            .clone()
1741            .with_columns([as_struct(vec![col("high"), col("low"), col("close")])
1742                .map(
1743                    move |s| {
1744                        let ca = s.struct_()?;
1745                        let s_high = ca.field_by_name("high")?;
1746                        let s_low = ca.field_by_name("low")?;
1747                        let s_close = ca.field_by_name("close")?;
1748
1749                        let high = s_high.f64()?;
1750                        let low = s_low.f64()?;
1751                        let close = s_close.f64()?;
1752
1753                        let mut st = SuperTrend::new(period, multiplier);
1754                        let mut values = Vec::with_capacity(s.len());
1755                        let mut directions = Vec::with_capacity(s.len());
1756
1757                        for i in 0..s.len() {
1758                            let h = high.get(i).unwrap_or(0.0);
1759                            let l = low.get(i).unwrap_or(0.0);
1760                            let c = close.get(i).unwrap_or(0.0);
1761                            let (val, dir) = st.next((h, l, c));
1762                            values.push(val);
1763                            directions.push(dir as f64);
1764                        }
1765
1766                        let st_series = Series::new("supertrend".into(), values);
1767                        let dir_series = Series::new("supertrend_direction".into(), directions);
1768
1769                        let out = StructChunked::from_series(
1770                            "supertrend_output".into(),
1771                            s.len(),
1772                            [st_series, dir_series].iter(),
1773                        )?;
1774                        Ok(Some(Column::from(out.into_series())))
1775                    },
1776                    GetOutput::from_type(DataType::Struct(vec![
1777                        Field::new("supertrend".into(), DataType::Float64),
1778                        Field::new("supertrend_direction".into(), DataType::Float64),
1779                    ])),
1780                )
1781                .alias("supertrend_data")])
1782    }
1783
1784    /// Market Structure (swings + confirmed BOS flips) — rich PA event foundation.
1785    ///
1786    /// Returns a Struct column "market_structure" with rich metadata fields directly usable
1787    /// for event extraction (filter rows where has_current_flip=true), backtester signals,
1788    /// and ML (regime + feature joins at flip bars).
1789    ///
1790    /// This wires the core MarketStructure Next impl + PAEvent system into Polars.
1791    /// Emits as Struct series (per project convention for composites like supertrend/bbands).
1792    /// For exploded event log: after collect, filter on has_current_flip and construct PAEvent rows
1793    /// (or use core extract_pa_events on the state columns).
1794    ///
1795    /// Sources: see market_structure.rs (MQL5 Part 21 + Parts 66/69 lessons).
1796    pub fn anchored_vwap(self, price: &str, volume: &str, anchor: &str) -> LazyFrame {
1797        let price = price.to_string();
1798        let volume = volume.to_string();
1799        let anchor = anchor.to_string();
1800
1801        self.0
1802            .clone()
1803            .with_columns([as_struct(vec![col(&price), col(&volume), col(&anchor)])
1804                .map(
1805                    move |s| {
1806                        let ca = s.struct_()?;
1807                        let s_price = ca.field_by_name(&price)?;
1808                        let s_volume = ca.field_by_name(&volume)?;
1809                        let s_anchor = ca.field_by_name(&anchor)?;
1810
1811                        let price = s_price.f64()?;
1812                        let volume = s_volume.f64()?;
1813                        let anchor = s_anchor.bool()?;
1814
1815                        let mut avwap = quantwave_core::AnchoredVWAP::new();
1816                        let mut values = Vec::with_capacity(s.len());
1817
1818                        for i in 0..s.len() {
1819                            let p = price.get(i).unwrap_or(0.0);
1820                            let v = volume.get(i).unwrap_or(0.0);
1821                            let a = anchor.get(i).unwrap_or(false);
1822                            values.push(avwap.next((p, v, a)));
1823                        }
1824
1825                        Ok(Some(Column::from(Series::new(
1826                            "anchored_vwap".into(),
1827                            values,
1828                        ))))
1829                    },
1830                    GetOutput::from_type(DataType::Float64),
1831                )
1832                .alias("avwap")])
1833    }
1834
1835    pub fn hma(self, name: &str, period: usize) -> LazyFrame {
1836        let name = name.to_string();
1837        self.0.clone().with_columns([col(&name)
1838            .map(
1839                move |s| {
1840                    let ca = s.f64()?;
1841                    let mut hma = quantwave_core::HMA::new(period);
1842                    let mut values = Vec::with_capacity(s.len());
1843
1844                    for i in 0..s.len() {
1845                        let val = ca.get(i).unwrap_or(0.0);
1846                        values.push(hma.next(val));
1847                    }
1848
1849                    Ok(Some(Column::from(Series::new("hma".into(), values))))
1850                },
1851                GetOutput::from_type(DataType::Float64),
1852            )
1853            .alias("hma")])
1854    }
1855
1856    pub fn kalman(self, name: &str, q: f64, r: f64) -> LazyFrame {
1857        let name = name.to_string();
1858        self.0.clone().with_columns([col(&name)
1859            .map(
1860                move |s| {
1861                    let ca = s.f64()?;
1862                    let mut indicator = quantwave_core::indicators::kalman::KalmanFilter::new(q, r);
1863                    let mut values = Vec::with_capacity(s.len());
1864
1865                    for i in 0..s.len() {
1866                        let val = ca.get(i).unwrap_or(f64::NAN);
1867                        values.push(indicator.next(val));
1868                    }
1869
1870                    Ok(Some(Column::from(Series::new("kalman".into(), values))))
1871                },
1872                GetOutput::from_type(DataType::Float64),
1873            )
1874            .alias("kalman")])
1875    }
1876
1877    pub fn kinematic_kalman(self, name: &str, q_pos: f64, q_vel: f64, r: f64) -> LazyFrame {
1878        let name = name.to_string();
1879        self.0.clone().with_columns([col(&name)
1880            .map(
1881                move |s| {
1882                    let ca = s.f64()?;
1883                    let mut indicator =
1884                        quantwave_core::indicators::kinematic_kalman::KinematicKalmanFilter::new(
1885                            q_pos, q_vel, r,
1886                        );
1887                    let mut values = Vec::with_capacity(s.len());
1888
1889                    for i in 0..s.len() {
1890                        let val = ca.get(i).unwrap_or(f64::NAN);
1891                        values.push(indicator.next(val));
1892                    }
1893
1894                    Ok(Some(Column::from(Series::new(
1895                        "kinematic_kalman".into(),
1896                        values,
1897                    ))))
1898                },
1899                GetOutput::from_type(DataType::Float64),
1900            )
1901            .alias("kinematic_kalman")])
1902    }
1903
1904    pub fn vpn(
1905        self,
1906        high: &str,
1907        low: &str,
1908        close: &str,
1909        volume: &str,
1910        period: usize,
1911        smooth_period: usize,
1912    ) -> LazyFrame {
1913        let high_str = high.to_string();
1914        let low_str = low.to_string();
1915        let close_str = close.to_string();
1916        let volume_str = volume.to_string();
1917
1918        self.0.clone().with_columns([as_struct(vec![
1919            col(&high_str),
1920            col(&low_str),
1921            col(&close_str),
1922            col(&volume_str),
1923        ])
1924        .map(
1925            move |s| {
1926                let ca = s.struct_()?;
1927                let s_h = ca.field_by_name(&high_str)?;
1928                let s_l = ca.field_by_name(&low_str)?;
1929                let s_c = ca.field_by_name(&close_str)?;
1930                let s_v = ca.field_by_name(&volume_str)?;
1931
1932                let high = s_h.f64()?;
1933                let low = s_l.f64()?;
1934                let close = s_c.f64()?;
1935                let volume = s_v.f64()?;
1936
1937                let mut indicator = quantwave_core::VPNIndicator::new(period, smooth_period);
1938                let mut values = Vec::with_capacity(s.len());
1939
1940                for i in 0..s.len() {
1941                    let h = high.get(i).unwrap_or(f64::NAN);
1942                    let l = low.get(i).unwrap_or(f64::NAN);
1943                    let c = close.get(i).unwrap_or(f64::NAN);
1944                    let v = volume.get(i).unwrap_or(f64::NAN);
1945                    values.push(indicator.next((h, l, c, v)));
1946                }
1947
1948                Ok(Some(Column::from(Series::new("vpn".into(), values))))
1949            },
1950            GetOutput::from_type(DataType::Float64),
1951        )
1952        .alias("vpn")])
1953    }
1954
1955    pub fn gap_momentum(
1956        self,
1957        open: &str,
1958        close: &str,
1959        period: usize,
1960        signal_period: usize,
1961    ) -> LazyFrame {
1962        let open_str = open.to_string();
1963        let close_str = close.to_string();
1964
1965        self.0
1966            .clone()
1967            .with_columns([as_struct(vec![col(&open_str), col(&close_str)])
1968                .map(
1969                    move |s| {
1970                        let ca = s.struct_()?;
1971                        let s_o = ca.field_by_name(&open_str)?;
1972                        let s_c = ca.field_by_name(&close_str)?;
1973
1974                        let open = s_o.f64()?;
1975                        let close = s_c.f64()?;
1976
1977                        let mut indicator = quantwave_core::GapMomentum::new(period, signal_period);
1978                        let mut ratio_vals = Vec::with_capacity(s.len());
1979                        let mut signal_vals = Vec::with_capacity(s.len());
1980
1981                        for i in 0..s.len() {
1982                            let o = open.get(i).unwrap_or(f64::NAN);
1983                            let c = close.get(i).unwrap_or(f64::NAN);
1984                            let (ratio, signal) = indicator.next((o, c));
1985                            ratio_vals.push(ratio);
1986                            signal_vals.push(signal);
1987                        }
1988
1989                        let s_ratio = Series::new("gap_ratio".into(), ratio_vals);
1990                        let s_signal = Series::new("gap_signal".into(), signal_vals);
1991                        let struct_series = StructChunked::from_series(
1992                            "gap_momentum_result".into(),
1993                            s.len(),
1994                            [s_ratio, s_signal].iter(),
1995                        )?;
1996                        Ok(Some(Column::from(struct_series.into_series())))
1997                    },
1998                    GetOutput::from_type(DataType::Struct(vec![
1999                        Field::new("gap_ratio".into(), DataType::Float64),
2000                        Field::new("gap_signal".into(), DataType::Float64),
2001                    ])),
2002                )
2003                .alias("gap_momentum")])
2004    }
2005
2006    pub fn autotune_filter(self, name: &str, window: usize, bandwidth: f64) -> LazyFrame {
2007        let name_str = name.to_string();
2008        self.0.clone().with_columns([col(&name_str)
2009            .map(
2010                move |s| {
2011                    let ca = s.f64()?;
2012                    let mut indicator = quantwave_core::AutoTuneFilter::new(window, bandwidth);
2013                    let mut values = Vec::with_capacity(s.len());
2014
2015                    for i in 0..s.len() {
2016                        let val = ca.get(i).unwrap_or(f64::NAN);
2017                        values.push(indicator.next(val));
2018                    }
2019
2020                    Ok(Some(Column::from(Series::new("autotune".into(), values))))
2021                },
2022                GetOutput::from_type(DataType::Float64),
2023            )
2024            .alias("autotune")])
2025    }
2026
2027    pub fn adaptive_ema(
2028        self,
2029        high: &str,
2030        low: &str,
2031        close: &str,
2032        period: usize,
2033        pds: usize,
2034    ) -> LazyFrame {
2035        let h_str = high.to_string();
2036        let l_str = low.to_string();
2037        let c_str = close.to_string();
2038
2039        self.0
2040            .clone()
2041            .with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2042                .map(
2043                    move |s| {
2044                        let ca = s.struct_()?;
2045                        let f_h = ca.field_by_name(&h_str)?;
2046                        let high = f_h.f64()?;
2047                        let f_l = ca.field_by_name(&l_str)?;
2048                        let low = f_l.f64()?;
2049                        let f_c = ca.field_by_name(&c_str)?;
2050                        let close = f_c.f64()?;
2051
2052                        let mut indicator = quantwave_core::AdaptiveEMA::new(period, pds);
2053                        let mut values = Vec::with_capacity(s.len());
2054
2055                        for i in 0..s.len() {
2056                            let h = high.get(i).unwrap_or(f64::NAN);
2057                            let l = low.get(i).unwrap_or(f64::NAN);
2058                            let c = close.get(i).unwrap_or(f64::NAN);
2059                            values.push(indicator.next((h, l, c)));
2060                        }
2061
2062                        Ok(Some(Column::from(Series::new(
2063                            "adaptive_ema".into(),
2064                            values,
2065                        ))))
2066                    },
2067                    GetOutput::from_type(DataType::Float64),
2068                )
2069                .alias("adaptive_ema")])
2070    }
2071
2072    pub fn tradj_ema(
2073        self,
2074        high: &str,
2075        low: &str,
2076        close: &str,
2077        period: usize,
2078        pds: usize,
2079        mltp: f64,
2080    ) -> LazyFrame {
2081        let h_str = high.to_string();
2082        let l_str = low.to_string();
2083        let c_str = close.to_string();
2084
2085        self.0
2086            .clone()
2087            .with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2088                .map(
2089                    move |s| {
2090                        let ca = s.struct_()?;
2091                        let f_h = ca.field_by_name(&h_str)?;
2092                        let high = f_h.f64()?;
2093                        let f_l = ca.field_by_name(&l_str)?;
2094                        let low = f_l.f64()?;
2095                        let f_c = ca.field_by_name(&c_str)?;
2096                        let close = f_c.f64()?;
2097
2098                        let mut indicator = quantwave_core::TRAdjEMA::new(period, pds, mltp);
2099                        let mut values = Vec::with_capacity(s.len());
2100
2101                        for i in 0..s.len() {
2102                            let h = high.get(i).unwrap_or(f64::NAN);
2103                            let l = low.get(i).unwrap_or(f64::NAN);
2104                            let c = close.get(i).unwrap_or(f64::NAN);
2105                            values.push(indicator.next((h, l, c)));
2106                        }
2107
2108                        Ok(Some(Column::from(Series::new("tradj_ema".into(), values))))
2109                    },
2110                    GetOutput::from_type(DataType::Float64),
2111                )
2112                .alias("tradj_ema")])
2113    }
2114
2115    pub fn obvm(
2116        self,
2117        high: &str,
2118        low: &str,
2119        close: &str,
2120        volume: &str,
2121        obvm_period: usize,
2122        signal_period: usize,
2123    ) -> LazyFrame {
2124        let h_str = high.to_string();
2125        let l_str = low.to_string();
2126        let c_str = close.to_string();
2127        let v_str = volume.to_string();
2128
2129        self.0.clone().with_columns([as_struct(vec![
2130            col(&h_str),
2131            col(&l_str),
2132            col(&c_str),
2133            col(&v_str),
2134        ])
2135        .map(
2136            move |s| {
2137                let ca = s.struct_()?;
2138                let f_h = ca.field_by_name(&h_str)?;
2139                let high = f_h.f64()?;
2140                let f_l = ca.field_by_name(&l_str)?;
2141                let low = f_l.f64()?;
2142                let f_c = ca.field_by_name(&c_str)?;
2143                let close = f_c.f64()?;
2144                let f_v = ca.field_by_name(&v_str)?;
2145                let volume = f_v.f64()?;
2146
2147                let mut indicator = quantwave_core::Obvm::new(obvm_period, signal_period);
2148                let mut obvm_vals = Vec::with_capacity(s.len());
2149                let mut signal_vals = Vec::with_capacity(s.len());
2150
2151                for i in 0..s.len() {
2152                    let h = high.get(i).unwrap_or(f64::NAN);
2153                    let l = low.get(i).unwrap_or(f64::NAN);
2154                    let c = close.get(i).unwrap_or(f64::NAN);
2155                    let v = volume.get(i).unwrap_or(f64::NAN);
2156                    let (o, sig) = indicator.next((h, l, c, v));
2157                    obvm_vals.push(o);
2158                    signal_vals.push(sig);
2159                }
2160
2161                let s_obvm = Series::new("obvm".into(), obvm_vals);
2162                let s_signal = Series::new("signal".into(), signal_vals);
2163                let struct_series = StructChunked::from_series(
2164                    "obvm_data".into(),
2165                    s.len(),
2166                    [s_obvm, s_signal].iter(),
2167                )?;
2168                Ok(Some(Column::from(struct_series.into_series())))
2169            },
2170            GetOutput::from_type(DataType::Struct(vec![
2171                Field::new("obvm".into(), DataType::Float64),
2172                Field::new("signal".into(), DataType::Float64),
2173            ])),
2174        )
2175        .alias("obvm_data")])
2176    }
2177
2178    #[allow(clippy::too_many_arguments)]
2179    pub fn vfi(
2180        self,
2181        high: &str,
2182        low: &str,
2183        close: &str,
2184        volume: &str,
2185        period: usize,
2186        coef: f64,
2187        vcoef: f64,
2188        smoothing_period: usize,
2189    ) -> LazyFrame {
2190        let h_str = high.to_string();
2191        let l_str = low.to_string();
2192        let c_str = close.to_string();
2193        let v_str = volume.to_string();
2194
2195        self.0.clone().with_columns([as_struct(vec![
2196            col(&h_str),
2197            col(&l_str),
2198            col(&c_str),
2199            col(&v_str),
2200        ])
2201        .map(
2202            move |s| {
2203                let ca = s.struct_()?;
2204                let f_h = ca.field_by_name(&h_str)?;
2205                let high = f_h.f64()?;
2206                let f_l = ca.field_by_name(&l_str)?;
2207                let low = f_l.f64()?;
2208                let f_c = ca.field_by_name(&c_str)?;
2209                let close = f_c.f64()?;
2210                let f_v = ca.field_by_name(&v_str)?;
2211                let volume = f_v.f64()?;
2212
2213                let mut indicator = quantwave_core::Vfi::new(period, coef, vcoef, smoothing_period);
2214                let mut values = Vec::with_capacity(s.len());
2215
2216                for i in 0..s.len() {
2217                    let h = high.get(i).unwrap_or(f64::NAN);
2218                    let l = low.get(i).unwrap_or(f64::NAN);
2219                    let c = close.get(i).unwrap_or(f64::NAN);
2220                    let v = volume.get(i).unwrap_or(f64::NAN);
2221                    values.push(indicator.next((h, l, c, v)));
2222                }
2223
2224                Ok(Some(Column::from(Series::new("vfi".into(), values))))
2225            },
2226            GetOutput::from_type(DataType::Float64),
2227        )
2228        .alias("vfi")])
2229    }
2230
2231    #[allow(clippy::too_many_arguments)]
2232    pub fn sve_volatility_bands(
2233        self,
2234        high: &str,
2235        low: &str,
2236        close: &str,
2237        bands_period: usize,
2238        bands_deviation: f64,
2239        low_band_adjust: f64,
2240        mid_line_length: usize,
2241    ) -> LazyFrame {
2242        let h_str = high.to_string();
2243        let l_str = low.to_string();
2244        let c_str = close.to_string();
2245
2246        self.0
2247            .clone()
2248            .with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2249                .map(
2250                    move |s| {
2251                        let ca = s.struct_()?;
2252                        let f_h = ca.field_by_name(&h_str)?;
2253                        let high = f_h.f64()?;
2254                        let f_l = ca.field_by_name(&l_str)?;
2255                        let low = f_l.f64()?;
2256                        let f_c = ca.field_by_name(&c_str)?;
2257                        let close = f_c.f64()?;
2258
2259                        let mut indicator = quantwave_core::SVEVolatilityBands::new(
2260                            bands_period,
2261                            bands_deviation,
2262                            low_band_adjust,
2263                            mid_line_length,
2264                        );
2265                        let mut upper_vals = Vec::with_capacity(s.len());
2266                        let mut mid_vals = Vec::with_capacity(s.len());
2267                        let mut lower_vals = Vec::with_capacity(s.len());
2268
2269                        for i in 0..s.len() {
2270                            let h = high.get(i).unwrap_or(f64::NAN);
2271                            let l = low.get(i).unwrap_or(f64::NAN);
2272                            let c = close.get(i).unwrap_or(f64::NAN);
2273                            let (upper, mid, lower) = indicator.next((h, l, c));
2274                            upper_vals.push(upper);
2275                            mid_vals.push(mid);
2276                            lower_vals.push(lower);
2277                        }
2278
2279                        let s_upper = Series::new("upper".into(), upper_vals);
2280                        let s_mid = Series::new("middle".into(), mid_vals);
2281                        let s_lower = Series::new("lower".into(), lower_vals);
2282                        let struct_series = StructChunked::from_series(
2283                            "sve_bands_data".into(),
2284                            s.len(),
2285                            [s_upper, s_mid, s_lower].iter(),
2286                        )?;
2287                        Ok(Some(Column::from(struct_series.into_series())))
2288                    },
2289                    GetOutput::from_type(DataType::Struct(vec![
2290                        Field::new("upper".into(), DataType::Float64),
2291                        Field::new("middle".into(), DataType::Float64),
2292                        Field::new("lower".into(), DataType::Float64),
2293                    ])),
2294                )
2295                .alias("sve_bands_data")])
2296    }
2297
2298    pub fn exp_dev_bands(
2299        self,
2300        name: &str,
2301        period: usize,
2302        multiplier: f64,
2303        use_sma: bool,
2304    ) -> LazyFrame {
2305        let name_str = name.to_string();
2306        self.0.clone().with_columns([col(&name_str)
2307            .map(
2308                move |s| {
2309                    let ca = s.f64()?;
2310                    let mut indicator =
2311                        quantwave_core::ExpDevBands::new(period, multiplier, use_sma);
2312                    let mut upper_vals = Vec::with_capacity(s.len());
2313                    let mut mid_vals = Vec::with_capacity(s.len());
2314                    let mut lower_vals = Vec::with_capacity(s.len());
2315
2316                    for i in 0..s.len() {
2317                        let val = ca.get(i).unwrap_or(f64::NAN);
2318                        let (upper, mid, lower) = indicator.next(val);
2319                        upper_vals.push(upper);
2320                        mid_vals.push(mid);
2321                        lower_vals.push(lower);
2322                    }
2323
2324                    let s_upper = Series::new("upper".into(), upper_vals);
2325                    let s_mid = Series::new("middle".into(), mid_vals);
2326                    let s_lower = Series::new("lower".into(), lower_vals);
2327                    let struct_series = StructChunked::from_series(
2328                        "exp_dev_bands_data".into(),
2329                        s.len(),
2330                        [s_upper, s_mid, s_lower].iter(),
2331                    )?;
2332                    Ok(Some(Column::from(struct_series.into_series())))
2333                },
2334                GetOutput::from_type(DataType::Struct(vec![
2335                    Field::new("upper".into(), DataType::Float64),
2336                    Field::new("middle".into(), DataType::Float64),
2337                    Field::new("lower".into(), DataType::Float64),
2338                ])),
2339            )
2340            .alias("exp_dev_bands_data")])
2341    }
2342
2343    pub fn sdo(
2344        self,
2345        name: &str,
2346        lookback_period: usize,
2347        period: usize,
2348        ema_pds: usize,
2349    ) -> LazyFrame {
2350        let name_str = name.to_string();
2351        self.0.clone().with_columns([col(&name_str)
2352            .map(
2353                move |s| {
2354                    let ca = s.f64()?;
2355                    let mut indicator = quantwave_core::SDO::new(lookback_period, period, ema_pds);
2356                    let mut values = Vec::with_capacity(s.len());
2357
2358                    for i in 0..s.len() {
2359                        let val = ca.get(i).unwrap_or(f64::NAN);
2360                        values.push(indicator.next(val));
2361                    }
2362
2363                    Ok(Some(Column::from(Series::new("sdo".into(), values))))
2364                },
2365                GetOutput::from_type(DataType::Float64),
2366            )
2367            .alias("sdo")])
2368    }
2369
2370    pub fn rsmk(self, price: &str, benchmark: &str, length: usize, ema_length: usize) -> LazyFrame {
2371        let p_str = price.to_string();
2372        let b_str = benchmark.to_string();
2373
2374        self.0
2375            .clone()
2376            .with_columns([as_struct(vec![col(&p_str), col(&b_str)])
2377                .map(
2378                    move |s| {
2379                        let ca = s.struct_()?;
2380                        let f_p = ca.field_by_name(&p_str)?;
2381                        let price = f_p.f64()?;
2382                        let f_b = ca.field_by_name(&b_str)?;
2383                        let benchmark = f_b.f64()?;
2384
2385                        let mut indicator = quantwave_core::RSMK::new(length, ema_length);
2386                        let mut values = Vec::with_capacity(s.len());
2387
2388                        for i in 0..s.len() {
2389                            let p = price.get(i).unwrap_or(f64::NAN);
2390                            let b = benchmark.get(i).unwrap_or(f64::NAN);
2391                            values.push(indicator.next((p, b)));
2392                        }
2393
2394                        Ok(Some(Column::from(Series::new("rsmk".into(), values))))
2395                    },
2396                    GetOutput::from_type(DataType::Float64),
2397                )
2398                .alias("rsmk")])
2399    }
2400
2401    pub fn rodc(
2402        self,
2403        name: &str,
2404        window_size: usize,
2405        threshold: f64,
2406        smooth_period: usize,
2407    ) -> LazyFrame {
2408        let name_str = name.to_string();
2409        self.0.clone().with_columns([col(&name_str)
2410            .map(
2411                move |s| {
2412                    let ca = s.f64()?;
2413                    let mut indicator =
2414                        quantwave_core::RODC::new(window_size, threshold, smooth_period);
2415                    let mut values = Vec::with_capacity(s.len());
2416
2417                    for i in 0..s.len() {
2418                        let val = ca.get(i).unwrap_or(f64::NAN);
2419                        values.push(indicator.next(val));
2420                    }
2421
2422                    Ok(Some(Column::from(Series::new("rodc".into(), values))))
2423                },
2424                GetOutput::from_type(DataType::Float64),
2425            )
2426            .alias("rodc")])
2427    }
2428
2429    pub fn reverse_ema(self, name: &str, alpha: f64) -> LazyFrame {
2430        let name_str = name.to_string();
2431        self.0.clone().with_columns([col(&name_str)
2432            .map(
2433                move |s| {
2434                    let ca = s.f64()?;
2435                    let mut indicator = quantwave_core::ReverseEMA::new(alpha);
2436                    let mut values = Vec::with_capacity(s.len());
2437
2438                    for i in 0..s.len() {
2439                        let val = ca.get(i).unwrap_or(f64::NAN);
2440                        values.push(indicator.next(val));
2441                    }
2442
2443                    Ok(Some(Column::from(Series::new(
2444                        "reverse_ema".into(),
2445                        values,
2446                    ))))
2447                },
2448                GetOutput::from_type(DataType::Float64),
2449            )
2450            .alias("reverse_ema")])
2451    }
2452
2453    pub fn harrington_adx(
2454        self,
2455        high: &str,
2456        low: &str,
2457        close: &str,
2458        adx_length: usize,
2459        adx_smooth_length: usize,
2460    ) -> LazyFrame {
2461        let h_str = high.to_string();
2462        let l_str = low.to_string();
2463        let c_str = close.to_string();
2464
2465        self.0
2466            .clone()
2467            .with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2468                .map(
2469                    move |s| {
2470                        let ca = s.struct_()?;
2471                        let f_h = ca.field_by_name(&h_str)?;
2472                        let high = f_h.f64()?;
2473                        let f_l = ca.field_by_name(&l_str)?;
2474                        let low = f_l.f64()?;
2475                        let f_c = ca.field_by_name(&c_str)?;
2476                        let close = f_c.f64()?;
2477
2478                        let mut indicator = quantwave_core::HarringtonADXOscillator::new(
2479                            adx_length,
2480                            adx_smooth_length,
2481                        );
2482                        let mut values = Vec::with_capacity(s.len());
2483
2484                        for i in 0..s.len() {
2485                            let h = high.get(i).unwrap_or(f64::NAN);
2486                            let l = low.get(i).unwrap_or(f64::NAN);
2487                            let c = close.get(i).unwrap_or(f64::NAN);
2488                            values.push(indicator.next((h, l, c)));
2489                        }
2490
2491                        Ok(Some(Column::from(Series::new(
2492                            "harrington_adx".into(),
2493                            values,
2494                        ))))
2495                    },
2496                    GetOutput::from_type(DataType::Float64),
2497                )
2498                .alias("harrington_adx")])
2499    }
2500
2501    pub fn keltner_channels(
2502        self,
2503        high: &str,
2504        low: &str,
2505        close: &str,
2506        ema_period: usize,
2507        atr_period: usize,
2508        multiplier: f64,
2509    ) -> LazyFrame {
2510        let high = high.to_string();
2511        let low = low.to_string();
2512        let close = close.to_string();
2513
2514        self.0
2515            .clone()
2516            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2517                .map(
2518                    move |s| {
2519                        let ca = s.struct_()?;
2520                        let s_high = ca.field_by_name(&high)?;
2521                        let s_low = ca.field_by_name(&low)?;
2522                        let s_close = ca.field_by_name(&close)?;
2523
2524                        let high = s_high.f64()?;
2525                        let low = s_low.f64()?;
2526                        let close = s_close.f64()?;
2527
2528                        let mut kc = quantwave_core::KeltnerChannels::new(
2529                            ema_period, atr_period, multiplier,
2530                        );
2531                        let mut uppers = Vec::with_capacity(s.len());
2532                        let mut middles = Vec::with_capacity(s.len());
2533                        let mut lowers = Vec::with_capacity(s.len());
2534
2535                        for i in 0..s.len() {
2536                            let h = high.get(i).unwrap_or(0.0);
2537                            let l = low.get(i).unwrap_or(0.0);
2538                            let c = close.get(i).unwrap_or(0.0);
2539                            let (upper, middle, lower) = kc.next((h, l, c));
2540                            uppers.push(upper);
2541                            middles.push(middle);
2542                            lowers.push(lower);
2543                        }
2544
2545                        let upper_series = Series::new("upper".into(), uppers);
2546                        let middle_series = Series::new("middle".into(), middles);
2547                        let lower_series = Series::new("lower".into(), lowers);
2548
2549                        let out = StructChunked::from_series(
2550                            "keltner_output".into(),
2551                            s.len(),
2552                            [upper_series, middle_series, lower_series].iter(),
2553                        )?;
2554                        Ok(Some(Column::from(out.into_series())))
2555                    },
2556                    GetOutput::from_type(DataType::Struct(vec![
2557                        Field::new("upper".into(), DataType::Float64),
2558                        Field::new("middle".into(), DataType::Float64),
2559                        Field::new("lower".into(), DataType::Float64),
2560                    ])),
2561                )
2562                .alias("keltner_data")])
2563    }
2564
2565    pub fn alma(self, name: &str, period: usize, offset: f64, sigma: f64) -> LazyFrame {
2566        let name = name.to_string();
2567        self.0.clone().with_columns([col(&name)
2568            .map(
2569                move |s| {
2570                    let ca = s.f64()?;
2571                    let mut alma = quantwave_core::ALMA::new(period, offset, sigma);
2572                    let mut values = Vec::with_capacity(s.len());
2573
2574                    for i in 0..s.len() {
2575                        let val = ca.get(i).unwrap_or(0.0);
2576                        values.push(alma.next(val));
2577                    }
2578
2579                    Ok(Some(Column::from(Series::new("alma".into(), values))))
2580                },
2581                GetOutput::from_type(DataType::Float64),
2582            )
2583            .alias("alma")])
2584    }
2585
2586    pub fn donchian_channels(self, high: &str, low: &str, period: usize) -> LazyFrame {
2587        let high = high.to_string();
2588        let low = low.to_string();
2589
2590        self.0
2591            .clone()
2592            .with_columns([as_struct(vec![col(&high), col(&low)])
2593                .map(
2594                    move |s| {
2595                        let ca = s.struct_()?;
2596                        let s_high = ca.field_by_name(&high)?;
2597                        let s_low = ca.field_by_name(&low)?;
2598
2599                        let high = s_high.f64()?;
2600                        let low = s_low.f64()?;
2601
2602                        let mut dc = quantwave_core::DonchianChannels::new(period);
2603                        let mut uppers = Vec::with_capacity(s.len());
2604                        let mut middles = Vec::with_capacity(s.len());
2605                        let mut lowers = Vec::with_capacity(s.len());
2606
2607                        for i in 0..s.len() {
2608                            let h = high.get(i).unwrap_or(0.0);
2609                            let l = low.get(i).unwrap_or(0.0);
2610                            let (upper, middle, lower) = dc.next((h, l));
2611                            uppers.push(upper);
2612                            middles.push(middle);
2613                            lowers.push(lower);
2614                        }
2615
2616                        let upper_series = Series::new("upper".into(), uppers);
2617                        let middle_series = Series::new("middle".into(), middles);
2618                        let lower_series = Series::new("lower".into(), lowers);
2619
2620                        let out = StructChunked::from_series(
2621                            "donchian_output".into(),
2622                            s.len(),
2623                            [upper_series, middle_series, lower_series].iter(),
2624                        )?;
2625                        Ok(Some(Column::from(out.into_series())))
2626                    },
2627                    GetOutput::from_type(DataType::Struct(vec![
2628                        Field::new("upper".into(), DataType::Float64),
2629                        Field::new("middle".into(), DataType::Float64),
2630                        Field::new("lower".into(), DataType::Float64),
2631                    ])),
2632                )
2633                .alias("donchian_data")])
2634    }
2635
2636    pub fn ttm_squeeze(
2637        self,
2638        high: &str,
2639        low: &str,
2640        close: &str,
2641        period: usize,
2642        multiplier_bb: f64,
2643        multiplier_kc: f64,
2644    ) -> LazyFrame {
2645        let high = high.to_string();
2646        let low = low.to_string();
2647        let close = close.to_string();
2648
2649        self.0
2650            .clone()
2651            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2652                .map(
2653                    move |s| {
2654                        let ca = s.struct_()?;
2655                        let s_high = ca.field_by_name(&high)?;
2656                        let s_low = ca.field_by_name(&low)?;
2657                        let s_close = ca.field_by_name(&close)?;
2658
2659                        let high = s_high.f64()?;
2660                        let low = s_low.f64()?;
2661                        let close = s_close.f64()?;
2662
2663                        let mut ttm =
2664                            quantwave_core::TTMSqueeze::new(period, multiplier_bb, multiplier_kc);
2665                        let mut histograms = Vec::with_capacity(s.len());
2666                        let mut squeezed = Vec::with_capacity(s.len());
2667
2668                        for i in 0..s.len() {
2669                            let h = high.get(i).unwrap_or(0.0);
2670                            let l = low.get(i).unwrap_or(0.0);
2671                            let c = close.get(i).unwrap_or(0.0);
2672                            let (hist, is_sq) = ttm.next((h, l, c));
2673                            histograms.push(hist);
2674                            squeezed.push(is_sq);
2675                        }
2676
2677                        let hist_series = Series::new("histogram".into(), histograms);
2678                        let squeezed_series = Series::new("is_squeezed".into(), squeezed);
2679
2680                        let out = StructChunked::from_series(
2681                            "ttm_squeeze_output".into(),
2682                            s.len(),
2683                            [hist_series, squeezed_series].iter(),
2684                        )?;
2685                        Ok(Some(Column::from(out.into_series())))
2686                    },
2687                    GetOutput::from_type(DataType::Struct(vec![
2688                        Field::new("histogram".into(), DataType::Float64),
2689                        Field::new("is_squeezed".into(), DataType::Boolean),
2690                    ])),
2691                )
2692                .alias("ttm_squeeze_data")])
2693    }
2694
2695    pub fn vortex_indicator(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
2696        let high = high.to_string();
2697        let low = low.to_string();
2698        let close = close.to_string();
2699
2700        self.0
2701            .clone()
2702            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2703                .map(
2704                    move |s| {
2705                        let ca = s.struct_()?;
2706                        let s_high = ca.field_by_name(&high)?;
2707                        let s_low = ca.field_by_name(&low)?;
2708                        let s_close = ca.field_by_name(&close)?;
2709
2710                        let high = s_high.f64()?;
2711                        let low = s_low.f64()?;
2712                        let close = s_close.f64()?;
2713
2714                        let mut vi = quantwave_core::VortexIndicator::new(period);
2715                        let mut plus_vals = Vec::with_capacity(s.len());
2716                        let mut minus_vals = Vec::with_capacity(s.len());
2717
2718                        for i in 0..s.len() {
2719                            let h = high.get(i).unwrap_or(0.0);
2720                            let l = low.get(i).unwrap_or(0.0);
2721                            let c = close.get(i).unwrap_or(0.0);
2722                            let (plus, minus) = vi.next((h, l, c));
2723                            plus_vals.push(plus);
2724                            minus_vals.push(minus);
2725                        }
2726
2727                        let plus_series = Series::new("vi_plus".into(), plus_vals);
2728                        let minus_series = Series::new("vi_minus".into(), minus_vals);
2729
2730                        let out = StructChunked::from_series(
2731                            "vortex_output".into(),
2732                            s.len(),
2733                            [plus_series, minus_series].iter(),
2734                        )?;
2735                        Ok(Some(Column::from(out.into_series())))
2736                    },
2737                    GetOutput::from_type(DataType::Struct(vec![
2738                        Field::new("vi_plus".into(), DataType::Float64),
2739                        Field::new("vi_minus".into(), DataType::Float64),
2740                    ])),
2741                )
2742                .alias("vortex_data")])
2743    }
2744
2745    pub fn heikin_ashi(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
2746        let open = open.to_string();
2747        let high = high.to_string();
2748        let low = low.to_string();
2749        let close = close.to_string();
2750
2751        self.0.clone().with_columns([as_struct(vec![
2752            col(&open),
2753            col(&high),
2754            col(&low),
2755            col(&close),
2756        ])
2757        .map(
2758            move |s| {
2759                let ca = s.struct_()?;
2760                let s_open = ca.field_by_name(&open)?;
2761                let s_high = ca.field_by_name(&high)?;
2762                let s_low = ca.field_by_name(&low)?;
2763                let s_close = ca.field_by_name(&close)?;
2764
2765                let open = s_open.f64()?;
2766                let high = s_high.f64()?;
2767                let low = s_low.f64()?;
2768                let close = s_close.f64()?;
2769
2770                let mut ha = quantwave_core::HeikinAshi::new();
2771                let mut ha_opens = Vec::with_capacity(s.len());
2772                let mut ha_highs = Vec::with_capacity(s.len());
2773                let mut ha_lows = Vec::with_capacity(s.len());
2774                let mut ha_closes = Vec::with_capacity(s.len());
2775
2776                for i in 0..s.len() {
2777                    let o = open.get(i).unwrap_or(0.0);
2778                    let h = high.get(i).unwrap_or(0.0);
2779                    let l = low.get(i).unwrap_or(0.0);
2780                    let c = close.get(i).unwrap_or(0.0);
2781                    let (ha_o, ha_h, ha_l, ha_c) = ha.next((o, h, l, c));
2782                    ha_opens.push(ha_o);
2783                    ha_highs.push(ha_h);
2784                    ha_lows.push(ha_l);
2785                    ha_closes.push(ha_c);
2786                }
2787
2788                let o_series = Series::new("ha_open".into(), ha_opens);
2789                let h_series = Series::new("ha_high".into(), ha_highs);
2790                let l_series = Series::new("ha_low".into(), ha_lows);
2791                let c_series = Series::new("ha_close".into(), ha_closes);
2792
2793                let out = StructChunked::from_series(
2794                    "heikin_ashi_output".into(),
2795                    s.len(),
2796                    [o_series, h_series, l_series, c_series].iter(),
2797                )?;
2798                Ok(Some(Column::from(out.into_series())))
2799            },
2800            GetOutput::from_type(DataType::Struct(vec![
2801                Field::new("ha_open".into(), DataType::Float64),
2802                Field::new("ha_high".into(), DataType::Float64),
2803                Field::new("ha_low".into(), DataType::Float64),
2804                Field::new("ha_close".into(), DataType::Float64),
2805            ])),
2806        )
2807        .alias("heikin_ashi_data")])
2808    }
2809
2810    pub fn wavetrend(
2811        self,
2812        high: &str,
2813        low: &str,
2814        close: &str,
2815        n1: usize,
2816        n2: usize,
2817        n3: usize,
2818    ) -> LazyFrame {
2819        let high = high.to_string();
2820        let low = low.to_string();
2821        let close = close.to_string();
2822
2823        self.0
2824            .clone()
2825            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2826                .map(
2827                    move |s| {
2828                        let ca = s.struct_()?;
2829                        let s_high = ca.field_by_name(&high)?;
2830                        let s_low = ca.field_by_name(&low)?;
2831                        let s_close = ca.field_by_name(&close)?;
2832
2833                        let high = s_high.f64()?;
2834                        let low = s_low.f64()?;
2835                        let close = s_close.f64()?;
2836
2837                        let mut wt = quantwave_core::WaveTrend::new(n1, n2, n3);
2838                        let mut wt1_vals = Vec::with_capacity(s.len());
2839                        let mut wt2_vals = Vec::with_capacity(s.len());
2840
2841                        for i in 0..s.len() {
2842                            let h = high.get(i).unwrap_or(0.0);
2843                            let l = low.get(i).unwrap_or(0.0);
2844                            let c = close.get(i).unwrap_or(0.0);
2845                            let (wt1, wt2) = wt.next((h, l, c));
2846                            wt1_vals.push(wt1);
2847                            wt2_vals.push(wt2);
2848                        }
2849
2850                        let wt1_series = Series::new("wt1".into(), wt1_vals);
2851                        let wt2_series = Series::new("wt2".into(), wt2_vals);
2852
2853                        let out = StructChunked::from_series(
2854                            "wavetrend_output".into(),
2855                            s.len(),
2856                            [wt1_series, wt2_series].iter(),
2857                        )?;
2858                        Ok(Some(Column::from(out.into_series())))
2859                    },
2860                    GetOutput::from_type(DataType::Struct(vec![
2861                        Field::new("wt1".into(), DataType::Float64),
2862                        Field::new("wt2".into(), DataType::Float64),
2863                    ])),
2864                )
2865                .alias("wavetrend_data")])
2866    }
2867
2868    pub fn tema(self, name: &str, period: usize) -> LazyFrame {
2869        let name = name.to_string();
2870        self.0.clone().with_columns([col(&name)
2871            .map(
2872                move |s| {
2873                    let ca = s.f64()?;
2874                    let mut tema = quantwave_core::TEMA::new(period);
2875                    let mut values = Vec::with_capacity(s.len());
2876
2877                    for i in 0..s.len() {
2878                        let val = ca.get(i).unwrap_or(0.0);
2879                        values.push(tema.next(val));
2880                    }
2881
2882                    Ok(Some(Column::from(Series::new("tema".into(), values))))
2883                },
2884                GetOutput::from_type(DataType::Float64),
2885            )
2886            .alias("tema")])
2887    }
2888
2889    pub fn zlema(self, name: &str, period: usize) -> LazyFrame {
2890        let name = name.to_string();
2891        self.0.clone().with_columns([col(&name)
2892            .map(
2893                move |s| {
2894                    let ca = s.f64()?;
2895                    let mut zlema = quantwave_core::ZLEMA::new(period);
2896                    let mut values = Vec::with_capacity(s.len());
2897
2898                    for i in 0..s.len() {
2899                        let val = ca.get(i).unwrap_or(0.0);
2900                        values.push(zlema.next(val));
2901                    }
2902
2903                    Ok(Some(Column::from(Series::new("zlema".into(), values))))
2904                },
2905                GetOutput::from_type(DataType::Float64),
2906            )
2907            .alias("zlema")])
2908    }
2909
2910    pub fn atr_trailing_stop(
2911        self,
2912        high: &str,
2913        low: &str,
2914        close: &str,
2915        period: usize,
2916        multiplier: f64,
2917    ) -> LazyFrame {
2918        let high = high.to_string();
2919        let low = low.to_string();
2920        let close = close.to_string();
2921
2922        self.0
2923            .clone()
2924            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2925                .map(
2926                    move |s| {
2927                        let ca = s.struct_()?;
2928                        let s_high = ca.field_by_name(&high)?;
2929                        let s_low = ca.field_by_name(&low)?;
2930                        let s_close = ca.field_by_name(&close)?;
2931
2932                        let high = s_high.f64()?;
2933                        let low = s_low.f64()?;
2934                        let close = s_close.f64()?;
2935
2936                        let mut atr_ts = quantwave_core::ATRTrailingStop::new(period, multiplier);
2937                        let mut stops = Vec::with_capacity(s.len());
2938                        let mut directions = Vec::with_capacity(s.len());
2939
2940                        for i in 0..s.len() {
2941                            let h = high.get(i).unwrap_or(0.0);
2942                            let l = low.get(i).unwrap_or(0.0);
2943                            let c = close.get(i).unwrap_or(0.0);
2944                            let (stop, dir) = atr_ts.next((h, l, c));
2945                            stops.push(stop);
2946                            directions.push(dir as f64);
2947                        }
2948
2949                        let stop_series = Series::new("stop".into(), stops);
2950                        let dir_series = Series::new("direction".into(), directions);
2951
2952                        let out = StructChunked::from_series(
2953                            "atr_ts_output".into(),
2954                            s.len(),
2955                            [stop_series, dir_series].iter(),
2956                        )?;
2957                        Ok(Some(Column::from(out.into_series())))
2958                    },
2959                    GetOutput::from_type(DataType::Struct(vec![
2960                        Field::new("stop".into(), DataType::Float64),
2961                        Field::new("direction".into(), DataType::Float64),
2962                    ])),
2963                )
2964                .alias("atr_ts_data")])
2965    }
2966
2967    pub fn pivot_points(self, high: &str, low: &str, close: &str) -> LazyFrame {
2968        let high = high.to_string();
2969        let low = low.to_string();
2970        let close = close.to_string();
2971
2972        self.0
2973            .clone()
2974            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2975                .map(
2976                    move |s| {
2977                        let ca = s.struct_()?;
2978                        let s_high = ca.field_by_name(&high)?;
2979                        let s_low = ca.field_by_name(&low)?;
2980                        let s_close = ca.field_by_name(&close)?;
2981
2982                        let high = s_high.f64()?;
2983                        let low = s_low.f64()?;
2984                        let close = s_close.f64()?;
2985
2986                        let mut pivot = quantwave_core::PivotPoints::new();
2987                        let mut p_vals = Vec::with_capacity(s.len());
2988                        let mut r1_vals = Vec::with_capacity(s.len());
2989                        let mut s1_vals = Vec::with_capacity(s.len());
2990                        let mut r2_vals = Vec::with_capacity(s.len());
2991                        let mut s2_vals = Vec::with_capacity(s.len());
2992
2993                        for i in 0..s.len() {
2994                            let h = high.get(i).unwrap_or(0.0);
2995                            let l = low.get(i).unwrap_or(0.0);
2996                            let c = close.get(i).unwrap_or(0.0);
2997                            let (p, r1, s1, r2, s2) = pivot.next((h, l, c));
2998                            p_vals.push(p);
2999                            r1_vals.push(r1);
3000                            s1_vals.push(s1);
3001                            r2_vals.push(r2);
3002                            s2_vals.push(s2);
3003                        }
3004
3005                        let p_series = Series::new("p".into(), p_vals);
3006                        let r1_series = Series::new("r1".into(), r1_vals);
3007                        let s1_series = Series::new("s1".into(), s1_vals);
3008                        let r2_series = Series::new("r2".into(), r2_vals);
3009                        let s2_series = Series::new("s2".into(), s2_vals);
3010
3011                        let out = StructChunked::from_series(
3012                            "pivot_output".into(),
3013                            s.len(),
3014                            [p_series, r1_series, s1_series, r2_series, s2_series].iter(),
3015                        )?;
3016                        Ok(Some(Column::from(out.into_series())))
3017                    },
3018                    GetOutput::from_type(DataType::Struct(vec![
3019                        Field::new("p".into(), DataType::Float64),
3020                        Field::new("r1".into(), DataType::Float64),
3021                        Field::new("s1".into(), DataType::Float64),
3022                        Field::new("r2".into(), DataType::Float64),
3023                        Field::new("s2".into(), DataType::Float64),
3024                    ])),
3025                )
3026                .alias("pivot_points_data")])
3027    }
3028
3029    pub fn bill_williams_fractals(self, high: &str, low: &str) -> LazyFrame {
3030        let high = high.to_string();
3031        let low = low.to_string();
3032
3033        self.0
3034            .clone()
3035            .with_columns([as_struct(vec![col(&high), col(&low)])
3036                .map(
3037                    move |s| {
3038                        let ca = s.struct_()?;
3039                        let s_high = ca.field_by_name(&high)?;
3040                        let s_low = ca.field_by_name(&low)?;
3041
3042                        let high = s_high.f64()?;
3043                        let low = s_low.f64()?;
3044
3045                        let mut fractals = quantwave_core::BillWilliamsFractals::new();
3046                        let mut bearish_vals = Vec::with_capacity(s.len());
3047                        let mut bullish_vals = Vec::with_capacity(s.len());
3048
3049                        for i in 0..s.len() {
3050                            let h = high.get(i).unwrap_or(0.0);
3051                            let l = low.get(i).unwrap_or(0.0);
3052                            let (bear, bull) = fractals.next((h, l));
3053                            bearish_vals.push(bear);
3054                            bullish_vals.push(bull);
3055                        }
3056
3057                        let bearish_series = Series::new("bearish".into(), bearish_vals);
3058                        let bullish_series = Series::new("bullish".into(), bullish_vals);
3059
3060                        let out = StructChunked::from_series(
3061                            "fractals_output".into(),
3062                            s.len(),
3063                            [bearish_series, bullish_series].iter(),
3064                        )?;
3065                        Ok(Some(Column::from(out.into_series())))
3066                    },
3067                    GetOutput::from_type(DataType::Struct(vec![
3068                        Field::new("bearish".into(), DataType::Boolean),
3069                        Field::new("bullish".into(), DataType::Boolean),
3070                    ])),
3071                )
3072                .alias("fractals_data")])
3073    }
3074
3075    /// Market Structure (swings + confirmed BOS flips) Polars accessor.
3076    /// Returns a Struct column "market_structure" with rich per-bar state + flip metadata:
3077    ///   bias (0=Neutral,1=Bullish,2=Bearish), last_*_price/bar (0/NaN if none),
3078    ///   has_flip + flip_* fields (only meaningful when has_flip=true — these are the events),
3079    ///   swing_depth, bar_index.
3080    ///
3081    /// This directly emits the foundation for the standardized PAEvent system:
3082    /// - Use core `extract_pa_events(&state)` (or Python equivalent on the struct fields) to obtain
3083    ///   typed `PAEvent` / `PAEventKind::MarketStructureFlip` carrying strength, confidence=1.0, bar etc.
3084    /// - Filter/explode for events: `.filter(col("market_structure").struct_().field_by_name("has_flip"))`.
3085    /// - Rich meta (structure_strength etc) drives backtester sizing/attribution
3086    ///   and ML confluence (feature_values/regime_at_event slots filled by join).
3087    ///
3088    /// Delegates to quantwave_core::MarketStructure (Next<(f64,f64)> -> MarketStructureState + PAEvent adapters).
3089    /// Primary Polars surface for Part 21 PA foundation + rich event standardization.
3090    ///
3091    /// Matches project patterns (see fractals, supertrend, features.rs cyber_cycle).
3092    ///
3093    /// Sources: market_structure.rs (MQL5 Part 21 https://www.mql5.com/en/articles/17891 + 66/69 lessons).
3094    pub fn market_structure(self, high: &str, low: &str, swing_strength: usize) -> LazyFrame {
3095        let high_str = high.to_string();
3096        let low_str = low.to_string();
3097        let strength = swing_strength;
3098
3099        self.0
3100            .clone()
3101            .with_columns([as_struct(vec![col(&high_str), col(&low_str)])
3102                .map(
3103                    move |s| {
3104                        let ca = s.struct_()?;
3105                        let s_h = ca.field_by_name(&high_str)?;
3106                        let s_l = ca.field_by_name(&low_str)?;
3107                        let highs = s_h.f64()?;
3108                        let lows = s_l.f64()?;
3109
3110                        let mut ms = quantwave_core::MarketStructure::new(strength);
3111                        let n = s.len();
3112
3113                        let mut bias_vals: Vec<u32> = Vec::with_capacity(n);
3114                        let mut lh_p: Vec<f64> = Vec::with_capacity(n);
3115                        let mut lh_b: Vec<u64> = Vec::with_capacity(n);
3116                        let mut ll_p: Vec<f64> = Vec::with_capacity(n);
3117                        let mut ll_b: Vec<u64> = Vec::with_capacity(n);
3118                        let mut has_f: Vec<bool> = Vec::with_capacity(n);
3119                        let mut f_bear: Vec<bool> = Vec::with_capacity(n);
3120                        let mut f_p: Vec<f64> = Vec::with_capacity(n);
3121                        let mut f_ba: Vec<u64> = Vec::with_capacity(n);
3122                        let mut f_str: Vec<u32> = Vec::with_capacity(n);
3123                        let mut depths: Vec<u32> = Vec::with_capacity(n);
3124                        let mut bars: Vec<u64> = Vec::with_capacity(n);
3125
3126                        for i in 0..n {
3127                            let h = highs.get(i).unwrap_or(f64::NAN);
3128                            let l = lows.get(i).unwrap_or(f64::NAN);
3129                            // Guard ordering
3130                            let hh = if h.is_nan() || l.is_nan() {
3131                                f64::NAN
3132                            } else {
3133                                h.max(l)
3134                            };
3135                            let ll = if h.is_nan() || l.is_nan() {
3136                                f64::NAN
3137                            } else {
3138                                l.min(h)
3139                            };
3140                            let state = ms.next((hh, ll));
3141
3142                            let b = match state.bias {
3143                                quantwave_core::Bias::Neutral => 0u32,
3144                                quantwave_core::Bias::Bullish => 1,
3145                                quantwave_core::Bias::Bearish => 2,
3146                            };
3147                            bias_vals.push(b);
3148
3149                            match &state.last_swing_high {
3150                                Some(sh) => {
3151                                    lh_p.push(sh.price);
3152                                    lh_b.push(sh.bar as u64);
3153                                }
3154                                None => {
3155                                    lh_p.push(f64::NAN);
3156                                    lh_b.push(0);
3157                                }
3158                            }
3159                            match &state.last_swing_low {
3160                                Some(sl) => {
3161                                    ll_p.push(sl.price);
3162                                    ll_b.push(sl.bar as u64);
3163                                }
3164                                None => {
3165                                    ll_p.push(f64::NAN);
3166                                    ll_b.push(0);
3167                                }
3168                            }
3169
3170                            if let Some(f) = &state.current_flip {
3171                                has_f.push(true);
3172                                f_bear.push(f.is_bearish);
3173                                f_p.push(f.price);
3174                                f_ba.push(f.bar as u64);
3175                                f_str.push(f.structure_strength);
3176                            } else {
3177                                has_f.push(false);
3178                                f_bear.push(false);
3179                                f_p.push(f64::NAN);
3180                                f_ba.push(0);
3181                                f_str.push(0);
3182                            }
3183
3184                            depths.push(state.swing_depth_used as u32);
3185                            bars.push(state.bar_index as u64);
3186                        }
3187
3188                        let s_bias = Series::new("bias".into(), bias_vals);
3189                        let s_lhp = Series::new("last_high_price".into(), lh_p);
3190                        let s_lhb = Series::new("last_high_bar".into(), lh_b);
3191                        let s_llp = Series::new("last_low_price".into(), ll_p);
3192                        let s_llb = Series::new("last_low_bar".into(), ll_b);
3193                        let s_hasf = Series::new("has_flip".into(), has_f);
3194                        let s_fb = Series::new("flip_bearish".into(), f_bear);
3195                        let s_fp = Series::new("flip_price".into(), f_p);
3196                        let s_fba = Series::new("flip_bar".into(), f_ba);
3197                        let s_fstr = Series::new("flip_strength".into(), f_str);
3198                        let s_dep = Series::new("swing_depth".into(), depths);
3199                        let s_bar = Series::new("bar_index".into(), bars);
3200
3201                        let struct_series = StructChunked::from_series(
3202                            "market_structure_result".into(),
3203                            s.len(),
3204                            [
3205                                s_bias, s_lhp, s_lhb, s_llp, s_llb, s_hasf, s_fb, s_fp, s_fba,
3206                                s_fstr, s_dep, s_bar,
3207                            ]
3208                            .iter(),
3209                        )?;
3210                        Ok(Some(Column::from(struct_series.into_series())))
3211                    },
3212                    GetOutput::from_type(DataType::Struct(vec![
3213                        Field::new("bias".into(), DataType::UInt32),
3214                        Field::new("last_high_price".into(), DataType::Float64),
3215                        Field::new("last_high_bar".into(), DataType::UInt64),
3216                        Field::new("last_low_price".into(), DataType::Float64),
3217                        Field::new("last_low_bar".into(), DataType::UInt64),
3218                        Field::new("has_flip".into(), DataType::Boolean),
3219                        Field::new("flip_bearish".into(), DataType::Boolean),
3220                        Field::new("flip_price".into(), DataType::Float64),
3221                        Field::new("flip_bar".into(), DataType::UInt64),
3222                        Field::new("flip_strength".into(), DataType::UInt32),
3223                        Field::new("swing_depth".into(), DataType::UInt32),
3224                        Field::new("bar_index".into(), DataType::UInt64),
3225                    ])),
3226                )
3227                .alias("market_structure")])
3228    }
3229
3230    /// Geometric Pattern Scanner (Flags + H&S) Polars accessor, built on the MarketStructure foundation.
3231    /// Returns a Struct column "geometric_patterns" containing:
3232    ///   flag: Struct(id, is_bull, pole_length, pole_length_atr, breakout_confirmed, breakout_price)
3233    ///   hs:   Struct(id, is_bearish, height, height_atr, score, breakout_confirmed)
3234    /// (id==0 means no detection on that bar).
3235    ///
3236    /// Delegates to quantwave_core::GeometricPatternScanner (Part 69 flag + Part 66 H&S rules).
3237    /// Emits rich metadata (`pole_length_atr`, `score`, `breakout_confirmed`) for sizing and filters.
3238    pub fn geometric_patterns(self, high: &str, low: &str, swing_strength: usize) -> LazyFrame {
3239        let high_str = high.to_string();
3240        let low_str = low.to_string();
3241        let strength = swing_strength;
3242
3243        self.0
3244            .clone()
3245            .with_columns([as_struct(vec![col(&high_str), col(&low_str)])
3246                .map(
3247                    move |s| {
3248                        let ca = s.struct_()?;
3249                        let s_h = ca.field_by_name(&high_str)?;
3250                        let s_l = ca.field_by_name(&low_str)?;
3251                        let highs = s_h.f64()?;
3252                        let lows = s_l.f64()?;
3253
3254                        let mut scanner = quantwave_core::GeometricPatternScanner::new(strength);
3255                        let n = s.len();
3256
3257                        let mut flag_ids: Vec<u32> = Vec::with_capacity(n);
3258                        let mut flag_is_bull: Vec<bool> = Vec::with_capacity(n);
3259                        let mut flag_pole_len: Vec<f64> = Vec::with_capacity(n);
3260                        let mut flag_pole_atr: Vec<f64> = Vec::with_capacity(n);
3261                        let mut flag_breakout: Vec<bool> = Vec::with_capacity(n);
3262                        let mut flag_bp: Vec<f64> = Vec::with_capacity(n);
3263
3264                        let mut hs_ids: Vec<u32> = Vec::with_capacity(n);
3265                        let mut hs_bear: Vec<bool> = Vec::with_capacity(n);
3266                        let mut hs_height: Vec<f64> = Vec::with_capacity(n);
3267                        let mut hs_height_atr: Vec<f64> = Vec::with_capacity(n);
3268                        let mut hs_score: Vec<f64> = Vec::with_capacity(n);
3269                        let mut hs_breakout: Vec<bool> = Vec::with_capacity(n);
3270
3271                        for i in 0..n {
3272                            let h = highs.get(i).unwrap_or(f64::NAN);
3273                            let l = lows.get(i).unwrap_or(f64::NAN);
3274                            let hh = if h.is_nan() || l.is_nan() {
3275                                f64::NAN
3276                            } else {
3277                                h.max(l)
3278                            };
3279                            let ll = if h.is_nan() || l.is_nan() {
3280                                f64::NAN
3281                            } else {
3282                                l.min(h)
3283                            };
3284                            let (_state, flag, hs) = scanner.next((hh, ll));
3285
3286                            if let Some(f) = flag {
3287                                flag_ids.push(f.id);
3288                                flag_is_bull.push(f.is_bull);
3289                                flag_pole_len.push(f.pole_length);
3290                                flag_pole_atr.push(f.pole_length_atr);
3291                                flag_breakout.push(f.breakout_confirmed);
3292                                flag_bp.push(f.breakout_price);
3293                            } else {
3294                                flag_ids.push(0);
3295                                flag_is_bull.push(false);
3296                                flag_pole_len.push(f64::NAN);
3297                                flag_pole_atr.push(f64::NAN);
3298                                flag_breakout.push(false);
3299                                flag_bp.push(f64::NAN);
3300                            }
3301
3302                            if let Some(hp) = hs {
3303                                hs_ids.push(hp.id);
3304                                hs_bear.push(hp.is_bearish);
3305                                hs_height.push(hp.height);
3306                                hs_height_atr.push(hp.height_atr);
3307                                hs_score.push(hp.score);
3308                                hs_breakout.push(hp.breakout_confirmed);
3309                            } else {
3310                                hs_ids.push(0);
3311                                hs_bear.push(false);
3312                                hs_height.push(f64::NAN);
3313                                hs_height_atr.push(f64::NAN);
3314                                hs_score.push(f64::NAN);
3315                                hs_breakout.push(false);
3316                            }
3317                        }
3318
3319                        let s_fid = Series::new("id".into(), flag_ids);
3320                        let s_fbull = Series::new("is_bull".into(), flag_is_bull);
3321                        let s_fplen = Series::new("pole_length".into(), flag_pole_len);
3322                        let s_fpatr = Series::new("pole_length_atr".into(), flag_pole_atr);
3323                        let s_fbo = Series::new("breakout_confirmed".into(), flag_breakout);
3324                        let s_fbp = Series::new("breakout_price".into(), flag_bp);
3325
3326                        let flag_struct = StructChunked::from_series(
3327                            "flag".into(),
3328                            n,
3329                            [s_fid, s_fbull, s_fplen, s_fpatr, s_fbo, s_fbp].iter(),
3330                        )?;
3331
3332                        let s_hid = Series::new("id".into(), hs_ids);
3333                        let s_hbear = Series::new("is_bearish".into(), hs_bear);
3334                        let s_hh = Series::new("height".into(), hs_height);
3335                        let s_hhatr = Series::new("height_atr".into(), hs_height_atr);
3336                        let s_hsc = Series::new("score".into(), hs_score);
3337                        let s_hbo = Series::new("breakout_confirmed".into(), hs_breakout);
3338
3339                        let hs_struct = StructChunked::from_series(
3340                            "hs".into(),
3341                            n,
3342                            [s_hid, s_hbear, s_hh, s_hhatr, s_hsc, s_hbo].iter(),
3343                        )?;
3344
3345                        let combined = StructChunked::from_series(
3346                            "geo_patterns".into(),
3347                            n,
3348                            [flag_struct.into_series(), hs_struct.into_series()].iter(),
3349                        )?;
3350                        Ok(Some(Column::from(combined.into_series())))
3351                    },
3352                    GetOutput::from_type(DataType::Struct(vec![
3353                        Field::new(
3354                            "flag".into(),
3355                            DataType::Struct(vec![
3356                                Field::new("id".into(), DataType::UInt32),
3357                                Field::new("is_bull".into(), DataType::Boolean),
3358                                Field::new("pole_length".into(), DataType::Float64),
3359                                Field::new("pole_length_atr".into(), DataType::Float64),
3360                                Field::new("breakout_confirmed".into(), DataType::Boolean),
3361                                Field::new("breakout_price".into(), DataType::Float64),
3362                            ]),
3363                        ),
3364                        Field::new(
3365                            "hs".into(),
3366                            DataType::Struct(vec![
3367                                Field::new("id".into(), DataType::UInt32),
3368                                Field::new("is_bearish".into(), DataType::Boolean),
3369                                Field::new("height".into(), DataType::Float64),
3370                                Field::new("height_atr".into(), DataType::Float64),
3371                                Field::new("score".into(), DataType::Float64),
3372                                Field::new("breakout_confirmed".into(), DataType::Boolean),
3373                            ]),
3374                        ),
3375                    ])),
3376                )
3377                .alias("geometric_patterns")])
3378    }
3379
3380    /// S/R Interaction Monitor (MQL5 Part 67) Polars accessor.
3381    /// Returns struct column "sr_monitor" with per-bar structure summary + first interaction (if any).
3382    /// Use `interaction_count > 0` to filter event bars; join with regimes/ML features for confluence.
3383    pub fn sr_monitor(
3384        self,
3385        high: &str,
3386        low: &str,
3387        close: &str,
3388        swing_strength: usize,
3389        touch_tolerance: f64,
3390        approach_zone: f64,
3391    ) -> LazyFrame {
3392        let high_str = high.to_string();
3393        let low_str = low.to_string();
3394        let close_str = close.to_string();
3395        let strength = swing_strength;
3396
3397        self.0.clone().with_columns([as_struct(vec![
3398            col(&high_str),
3399            col(&low_str),
3400            col(&close_str),
3401        ])
3402        .map(
3403            move |s| {
3404                let ca = s.struct_()?;
3405                let s_h = ca.field_by_name(&high_str)?;
3406                let s_l = ca.field_by_name(&low_str)?;
3407                let s_c = ca.field_by_name(&close_str)?;
3408                let highs = s_h.f64()?;
3409                let lows = s_l.f64()?;
3410                let closes = s_c.f64()?;
3411
3412                let mut mon = quantwave_core::SRInteractionMonitor::new(
3413                    strength,
3414                    touch_tolerance,
3415                    approach_zone,
3416                );
3417                let n = s.len();
3418
3419                let mut bias_vals: Vec<u32> = Vec::with_capacity(n);
3420                let mut active_levels: Vec<u32> = Vec::with_capacity(n);
3421                let mut interaction_counts: Vec<u32> = Vec::with_capacity(n);
3422                let mut has_interaction: Vec<bool> = Vec::with_capacity(n);
3423                let mut interaction_types: Vec<u32> = Vec::with_capacity(n);
3424                let mut level_prices: Vec<f64> = Vec::with_capacity(n);
3425                let mut is_support_vals: Vec<bool> = Vec::with_capacity(n);
3426                let mut strengths: Vec<f64> = Vec::with_capacity(n);
3427                let mut distances: Vec<f64> = Vec::with_capacity(n);
3428                let mut atr_vals: Vec<f64> = Vec::with_capacity(n);
3429
3430                for i in 0..n {
3431                    let h = highs.get(i).unwrap_or(f64::NAN);
3432                    let l = lows.get(i).unwrap_or(f64::NAN);
3433                    let c = closes.get(i).unwrap_or(f64::NAN);
3434                    let hh = if h.is_nan() || l.is_nan() {
3435                        f64::NAN
3436                    } else {
3437                        h.max(l)
3438                    };
3439                    let ll = if h.is_nan() || l.is_nan() {
3440                        f64::NAN
3441                    } else {
3442                        l.min(h)
3443                    };
3444                    let cc = if c.is_nan() {
3445                        (hh + ll) / 2.0
3446                    } else {
3447                        c.clamp(ll, hh)
3448                    };
3449
3450                    let out = mon.next((hh, ll, cc));
3451
3452                    let b = match out.structure.bias {
3453                        quantwave_core::Bias::Neutral => 0u32,
3454                        quantwave_core::Bias::Bullish => 1,
3455                        quantwave_core::Bias::Bearish => 2,
3456                    };
3457                    bias_vals.push(b);
3458                    active_levels.push(mon.active_level_count() as u32);
3459                    interaction_counts.push(out.interactions.len() as u32);
3460
3461                    if let Some(first) = out.interactions.first() {
3462                        has_interaction.push(true);
3463                        interaction_types.push(first.interaction as u32);
3464                        level_prices.push(first.level_price);
3465                        is_support_vals.push(first.is_support);
3466                        strengths.push(first.strength);
3467                        distances.push(first.distance_at_event);
3468                    } else {
3469                        has_interaction.push(false);
3470                        interaction_types.push(0);
3471                        level_prices.push(f64::NAN);
3472                        is_support_vals.push(false);
3473                        strengths.push(f64::NAN);
3474                        distances.push(f64::NAN);
3475                    }
3476                    atr_vals.push(mon.current_atr());
3477                }
3478
3479                let struct_series = StructChunked::from_series(
3480                    "sr_monitor_result".into(),
3481                    n,
3482                    [
3483                        Series::new("bias".into(), bias_vals),
3484                        Series::new("active_levels".into(), active_levels),
3485                        Series::new("interaction_count".into(), interaction_counts),
3486                        Series::new("has_interaction".into(), has_interaction),
3487                        Series::new("interaction_type".into(), interaction_types),
3488                        Series::new("level_price".into(), level_prices),
3489                        Series::new("is_support".into(), is_support_vals),
3490                        Series::new("strength".into(), strengths),
3491                        Series::new("distance".into(), distances),
3492                        Series::new("atr".into(), atr_vals),
3493                    ]
3494                    .iter(),
3495                )?;
3496                Ok(Some(Column::from(struct_series.into_series())))
3497            },
3498            GetOutput::from_type(DataType::Struct(vec![
3499                Field::new("bias".into(), DataType::UInt32),
3500                Field::new("active_levels".into(), DataType::UInt32),
3501                Field::new("interaction_count".into(), DataType::UInt32),
3502                Field::new("has_interaction".into(), DataType::Boolean),
3503                Field::new("interaction_type".into(), DataType::UInt32),
3504                Field::new("level_price".into(), DataType::Float64),
3505                Field::new("is_support".into(), DataType::Boolean),
3506                Field::new("strength".into(), DataType::Float64),
3507                Field::new("distance".into(), DataType::Float64),
3508                Field::new("atr".into(), DataType::Float64),
3509            ])),
3510        )
3511        .alias("sr_monitor")])
3512    }
3513
3514    pub fn ichimoku_cloud(
3515        self,
3516        high: &str,
3517        low: &str,
3518        p1: usize,
3519        p2: usize,
3520        p3: usize,
3521    ) -> LazyFrame {
3522        let high = high.to_string();
3523        let low = low.to_string();
3524
3525        self.0
3526            .clone()
3527            .with_columns([as_struct(vec![col(&high), col(&low)])
3528                .map(
3529                    move |s| {
3530                        let ca = s.struct_()?;
3531                        let s_high = ca.field_by_name(&high)?;
3532                        let s_low = ca.field_by_name(&low)?;
3533
3534                        let high = s_high.f64()?;
3535                        let low = s_low.f64()?;
3536
3537                        let mut ic = quantwave_core::IchimokuCloud::new(p1, p2, p3);
3538                        let mut t_vals = Vec::with_capacity(s.len());
3539                        let mut k_vals = Vec::with_capacity(s.len());
3540                        let mut sa_vals = Vec::with_capacity(s.len());
3541                        let mut sb_vals = Vec::with_capacity(s.len());
3542
3543                        for i in 0..s.len() {
3544                            let h = high.get(i).unwrap_or(0.0);
3545                            let l = low.get(i).unwrap_or(0.0);
3546                            let (t, k, sa, sb) = ic.next((h, l));
3547                            t_vals.push(t);
3548                            k_vals.push(k);
3549                            sa_vals.push(sa);
3550                            sb_vals.push(sb);
3551                        }
3552
3553                        let t_series = Series::new("tenkan".into(), t_vals);
3554                        let k_series = Series::new("kijun".into(), k_vals);
3555                        let sa_series = Series::new("senkou_a".into(), sa_vals);
3556                        let sb_series = Series::new("senkou_b".into(), sb_vals);
3557
3558                        let out = StructChunked::from_series(
3559                            "ichimoku_output".into(),
3560                            s.len(),
3561                            [t_series, k_series, sa_series, sb_series].iter(),
3562                        )?;
3563                        Ok(Some(Column::from(out.into_series())))
3564                    },
3565                    GetOutput::from_type(DataType::Struct(vec![
3566                        Field::new("tenkan".into(), DataType::Float64),
3567                        Field::new("kijun".into(), DataType::Float64),
3568                        Field::new("senkou_a".into(), DataType::Float64),
3569                        Field::new("senkou_b".into(), DataType::Float64),
3570                    ])),
3571                )
3572                .alias("ichimoku_data")])
3573    }
3574
3575    pub fn volatility_clusterer(
3576        self,
3577        high: &str,
3578        low: &str,
3579        close: &str,
3580        atr_period: usize,
3581        window_size: usize,
3582        k: usize,
3583    ) -> LazyFrame {
3584        let h_str = high.to_string();
3585        let l_str = low.to_string();
3586        let c_str = close.to_string();
3587
3588        self.0
3589            .clone()
3590            .with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
3591                .map(
3592                    move |s| {
3593                        let ca = s.struct_()?;
3594                        let f_h = ca.field_by_name(&h_str)?;
3595                        let high = f_h.f64()?;
3596                        let f_l = ca.field_by_name(&l_str)?;
3597                        let low = f_l.f64()?;
3598                        let f_c = ca.field_by_name(&c_str)?;
3599                        let close = f_c.f64()?;
3600
3601                        let mut clusterer =
3602                        quantwave_core::regimes::volatility_clustering::VolatilityClusterer::new(
3603                            atr_period,
3604                            window_size,
3605                            k,
3606                        );
3607                        let mut values = Vec::with_capacity(s.len());
3608
3609                        for i in 0..s.len() {
3610                            let h = high.get(i).unwrap_or(f64::NAN);
3611                            let l = low.get(i).unwrap_or(f64::NAN);
3612                            let c = close.get(i).unwrap_or(f64::NAN);
3613                            let regime = clusterer.next((h, l, c));
3614                            let val = match regime {
3615                                quantwave_core::regimes::MarketRegime::Steady => 0u32,
3616                                quantwave_core::regimes::MarketRegime::Bull => 1,
3617                                quantwave_core::regimes::MarketRegime::Bear => 2,
3618                                quantwave_core::regimes::MarketRegime::Crisis => 3,
3619                                quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
3620                            };
3621                            values.push(val);
3622                        }
3623
3624                        Ok(Some(Column::from(Series::new(
3625                            "volatility_regime".into(),
3626                            values,
3627                        ))))
3628                    },
3629                    GetOutput::from_type(DataType::UInt32),
3630                )
3631                .alias("volatility_regime")])
3632    }
3633
3634    pub fn hmm_bull_bear(self, name: &str) -> LazyFrame {
3635        let name_str = name.to_string();
3636        self.0.clone().with_columns([col(&name_str)
3637            .map(
3638                move |s| {
3639                    let ca = s.f64()?;
3640                    let mut hmm = quantwave_core::regimes::hmm::HMM::bull_bear();
3641                    let mut values = Vec::with_capacity(s.len());
3642
3643                    for i in 0..s.len() {
3644                        let val = ca.get(i).unwrap_or(f64::NAN);
3645                        let regime = hmm.next(val);
3646                        let out = match regime {
3647                            quantwave_core::regimes::MarketRegime::Bull => 1u32,
3648                            quantwave_core::regimes::MarketRegime::Bear => 2,
3649                            _ => 0,
3650                        };
3651                        values.push(out);
3652                    }
3653
3654                    Ok(Some(Column::from(Series::new("hmm_regime".into(), values))))
3655                },
3656                GetOutput::from_type(DataType::UInt32),
3657            )
3658            .alias("hmm_regime")])
3659    }
3660
3661    /// Fit a generic HMM (EM) on a full observation column and decode regimes.
3662    ///
3663    /// Returns struct column `hmm_fit_data` with:
3664    /// - `hmm_fit_state` (UInt32): global Viterbi state index (0-based)
3665    /// - `hmm_fit_smooth_probs` (List[Float64]): smoothed state probabilities per bar
3666    ///
3667    /// Set `fit_lambdas` to enable lambda (ecld) emissions with per-state λ MLE (ldhmm parity).
3668    ///
3669    /// Non-finite observations are skipped for fitting/decoding; output rows at those
3670    /// indices receive state `0` and uniform probabilities.
3671    pub fn hmm_fit(
3672        self,
3673        name: &str,
3674        n_states: usize,
3675        max_iter: usize,
3676        fit_lambdas: bool,
3677    ) -> LazyFrame {
3678        let name_str = name.to_string();
3679        self.0.clone().with_columns([col(&name_str)
3680            .map(
3681                move |s| {
3682                    use quantwave_core::regimes::gaussian_hmm::{
3683                        EmissionFamily, GaussianHmmFitConfig, fit_em,
3684                    };
3685
3686                    let ca = s.f64()?;
3687                    let n = s.len();
3688                    let mut obs = Vec::new();
3689                    let mut index_map = Vec::new();
3690                    for i in 0..n {
3691                        if let Some(v) = ca.get(i)
3692                            && v.is_finite()
3693                        {
3694                            obs.push(v);
3695                            index_map.push(i);
3696                        }
3697                    }
3698
3699                    let m = n_states.max(2);
3700                    let uniform = 1.0 / m as f64;
3701                    let mut states = vec![0u32; n];
3702                    let mut probs_rows = vec![vec![uniform; m]; n];
3703
3704                    if obs.len() > m {
3705                        let config = GaussianHmmFitConfig {
3706                            n_states: m,
3707                            max_iter: max_iter.max(1),
3708                            emission_family: if fit_lambdas {
3709                                EmissionFamily::Lambda
3710                            } else {
3711                                EmissionFamily::Gaussian
3712                            },
3713                            fit_lambdas,
3714                            ..Default::default()
3715                        };
3716                        if let Ok(fit) = fit_em(&obs, &config)
3717                            && let Ok(decode) = fit.params.decode(&obs)
3718                        {
3719                            for (j, &row_idx) in index_map.iter().enumerate() {
3720                                states[row_idx] = decode.viterbi_path[j] as u32;
3721                                for st in 0..m {
3722                                    probs_rows[row_idx][st] = decode.smooth_probs[st][j];
3723                                }
3724                            }
3725                        }
3726                    }
3727
3728                    let mut prob_builder = ListPrimitiveChunkedBuilder::<Float64Type>::new(
3729                        "hmm_fit_smooth_probs".into(),
3730                        n,
3731                        n * m,
3732                        DataType::Float64,
3733                    );
3734                    for row in &probs_rows {
3735                        prob_builder.append_slice(row);
3736                    }
3737
3738                    let state_series = Series::new("hmm_fit_state".into(), states);
3739                    let prob_series = prob_builder.finish().into_series();
3740                    let out = StructChunked::from_series(
3741                        "hmm_fit_data".into(),
3742                        n,
3743                        [state_series, prob_series].iter(),
3744                    )?;
3745                    Ok(Some(Column::from(out.into_series())))
3746                },
3747                GetOutput::from_type(DataType::Struct(vec![
3748                    Field::new("hmm_fit_state".into(), DataType::UInt32),
3749                    Field::new(
3750                        "hmm_fit_smooth_probs".into(),
3751                        DataType::List(Box::new(DataType::Float64)),
3752                    ),
3753                ])),
3754            )
3755            .alias("hmm_fit_data")])
3756    }
3757
3758    /// Pseudo-residuals from a fitted HMM (ldhmm `pseudo_residuals`).
3759    pub fn hmm_pseudo_residuals(
3760        self,
3761        name: &str,
3762        n_states: usize,
3763        max_iter: usize,
3764        fit_lambdas: bool,
3765    ) -> LazyFrame {
3766        let name_str = name.to_string();
3767        self.0.clone().with_columns([col(&name_str)
3768            .map(
3769                move |s| {
3770                    use quantwave_core::regimes::gaussian_hmm::{
3771                        EmissionFamily, GaussianHmmFitConfig, fit_em,
3772                    };
3773                    use quantwave_core::regimes::hmm_forecast::pseudo_residuals;
3774
3775                    let ca = s.f64()?;
3776                    let n = s.len();
3777                    let mut values = vec![f64::NAN; n];
3778                    let mut obs = Vec::new();
3779                    let mut index_map = Vec::new();
3780                    for i in 0..n {
3781                        if let Some(v) = ca.get(i)
3782                            && v.is_finite()
3783                        {
3784                            obs.push(v);
3785                            index_map.push(i);
3786                        }
3787                    }
3788                    let m = n_states.max(2);
3789                    if obs.len() > m {
3790                        let config = GaussianHmmFitConfig {
3791                            n_states: m,
3792                            max_iter: max_iter.max(1),
3793                            emission_family: if fit_lambdas {
3794                                EmissionFamily::Lambda
3795                            } else {
3796                                EmissionFamily::Gaussian
3797                            },
3798                            fit_lambdas,
3799                            ..Default::default()
3800                        };
3801                        if let Ok(fit) = fit_em(&obs, &config)
3802                            && let Ok(decode) = fit.params.decode(&obs)
3803                            && let Ok(residuals) =
3804                                pseudo_residuals(&fit.params, &decode.forward_filter, &obs)
3805                        {
3806                            for (j, &row_idx) in index_map.iter().enumerate() {
3807                                values[row_idx] = residuals[j];
3808                            }
3809                        }
3810                    }
3811                    Ok(Some(Column::from(Series::new(
3812                        "hmm_pseudo_residual".into(),
3813                        values,
3814                    ))))
3815                },
3816                GetOutput::from_type(DataType::Float64),
3817            )
3818            .alias("hmm_pseudo_residual")])
3819    }
3820
3821    /// Per-bar weighted decode statistics from a fitted HMM (ldhmm `decode_stats_history`).
3822    pub fn hmm_decode_stats(
3823        self,
3824        name: &str,
3825        n_states: usize,
3826        max_iter: usize,
3827        fit_lambdas: bool,
3828    ) -> LazyFrame {
3829        let name_str = name.to_string();
3830        self.0.clone().with_columns([col(&name_str)
3831            .map(
3832                move |s| {
3833                    use quantwave_core::regimes::gaussian_hmm::{
3834                        EmissionFamily, GaussianHmmFitConfig, fit_em,
3835                    };
3836                    use quantwave_core::regimes::hmm_forecast::decode_stats_history;
3837
3838                    let ca = s.f64()?;
3839                    let n = s.len();
3840                    let mut means = vec![f64::NAN; n];
3841                    let mut vols = vec![f64::NAN; n];
3842                    let mut lambdas = vec![f64::NAN; n];
3843                    let mut obs = Vec::new();
3844                    let mut index_map = Vec::new();
3845                    for i in 0..n {
3846                        if let Some(v) = ca.get(i)
3847                            && v.is_finite()
3848                        {
3849                            obs.push(v);
3850                            index_map.push(i);
3851                        }
3852                    }
3853                    let m = n_states.max(2);
3854                    if obs.len() > m {
3855                        let config = GaussianHmmFitConfig {
3856                            n_states: m,
3857                            max_iter: max_iter.max(1),
3858                            emission_family: if fit_lambdas {
3859                                EmissionFamily::Lambda
3860                            } else {
3861                                EmissionFamily::Gaussian
3862                            },
3863                            fit_lambdas,
3864                            ..Default::default()
3865                        };
3866                        if let Ok(fit) = fit_em(&obs, &config)
3867                            && let Ok(decode) = fit.params.decode(&obs)
3868                            && let Ok(stats) =
3869                                decode_stats_history(&fit.params, &decode.smooth_probs)
3870                        {
3871                            for (j, row) in stats.iter().enumerate() {
3872                                let idx = index_map[j];
3873                                means[idx] = row.weighted_mean;
3874                                vols[idx] = row.weighted_vol;
3875                                lambdas[idx] = row.weighted_lambda;
3876                            }
3877                        }
3878                    }
3879                    let out = StructChunked::from_series(
3880                        "hmm_decode_stats_data".into(),
3881                        n,
3882                        [
3883                            Series::new("hmm_decode_weighted_mean".into(), means),
3884                            Series::new("hmm_decode_weighted_vol".into(), vols),
3885                            Series::new("hmm_decode_weighted_lambda".into(), lambdas),
3886                        ]
3887                        .iter(),
3888                    )?;
3889                    Ok(Some(Column::from(out.into_series())))
3890                },
3891                GetOutput::from_type(DataType::Struct(vec![
3892                    Field::new("hmm_decode_weighted_mean".into(), DataType::Float64),
3893                    Field::new("hmm_decode_weighted_vol".into(), DataType::Float64),
3894                    Field::new("hmm_decode_weighted_lambda".into(), DataType::Float64),
3895                ])),
3896            )
3897            .alias("hmm_decode_stats_data")])
3898    }
3899
3900    /// h-step mixture volatility forecast from each bar's forward filter (ldhmm `forecast_volatility`).
3901    pub fn hmm_forecast_vol(
3902        self,
3903        name: &str,
3904        n_states: usize,
3905        max_iter: usize,
3906        fit_lambdas: bool,
3907        horizon: usize,
3908    ) -> LazyFrame {
3909        let name_str = name.to_string();
3910        let h = horizon.max(1);
3911        self.0.clone().with_columns([col(&name_str)
3912            .map(
3913                move |s| {
3914                    use quantwave_core::regimes::gaussian_hmm::{
3915                        EmissionFamily, GaussianHmmFitConfig, fit_em,
3916                    };
3917                    use quantwave_core::regimes::hmm_forecast::forecast_volatility;
3918
3919                    let ca = s.f64()?;
3920                    let n = s.len();
3921                    let mut values = vec![f64::NAN; n];
3922                    let mut obs = Vec::new();
3923                    let mut index_map = Vec::new();
3924                    for i in 0..n {
3925                        if let Some(v) = ca.get(i)
3926                            && v.is_finite()
3927                        {
3928                            obs.push(v);
3929                            index_map.push(i);
3930                        }
3931                    }
3932                    let m = n_states.max(2);
3933                    if obs.len() > m {
3934                        let config = GaussianHmmFitConfig {
3935                            n_states: m,
3936                            max_iter: max_iter.max(1),
3937                            emission_family: if fit_lambdas {
3938                                EmissionFamily::Lambda
3939                            } else {
3940                                EmissionFamily::Gaussian
3941                            },
3942                            fit_lambdas,
3943                            ..Default::default()
3944                        };
3945                        if let Ok(fit) = fit_em(&obs, &config)
3946                            && let Ok(decode) = fit.params.decode(&obs)
3947                        {
3948                            for (j, &row_idx) in index_map.iter().enumerate() {
3949                                let probs: Vec<f64> =
3950                                    (0..m).map(|st| decode.forward_filter[st][j]).collect();
3951                                if let Ok(vol) = forecast_volatility(&fit.params, &probs, h) {
3952                                    values[row_idx] = vol;
3953                                }
3954                            }
3955                        }
3956                    }
3957                    Ok(Some(Column::from(Series::new(
3958                        "hmm_forecast_vol".into(),
3959                        values,
3960                    ))))
3961                },
3962                GetOutput::from_type(DataType::Float64),
3963            )
3964            .alias("hmm_forecast_vol")])
3965    }
3966
3967    pub fn pelt(self, name: &str, penalty: f64, min_dist: usize) -> LazyFrame {
3968        let name_str = name.to_string();
3969        self.0.clone().with_columns([col(&name_str)
3970            .map(
3971                move |s| {
3972                    let ca = s.f64()?;
3973                    let data: Vec<f64> = ca.into_iter().map(|v| v.unwrap_or(f64::NAN)).collect();
3974                    let pelt = quantwave_core::regimes::pelt::PELT::new(penalty, min_dist);
3975                    let cps = pelt.detect(&data);
3976
3977                    let mut values = vec![0u32; s.len()];
3978                    for cp in cps {
3979                        if cp < values.len() {
3980                            values[cp] = 1;
3981                        }
3982                    }
3983
3984                    Ok(Some(Column::from(Series::new(
3985                        "changepoints".into(),
3986                        values,
3987                    ))))
3988                },
3989                GetOutput::from_type(DataType::UInt32),
3990            )
3991            .alias("changepoints")])
3992    }
3993
3994    pub fn gmm(self, columns: &[&str], _k: usize) -> LazyFrame {
3995        let cols: Vec<String> = columns.iter().map(|s| s.to_string()).collect();
3996        let col_exprs: Vec<Expr> = cols.iter().map(col).collect();
3997
3998        self.0.clone().with_columns([as_struct(col_exprs)
3999            .map(
4000                move |s| {
4001                    let ca = s.struct_()?;
4002                    let n_rows = s.len();
4003                    let n_dims = cols.len();
4004
4005                    let mut data = Vec::with_capacity(n_rows);
4006                    for i in 0..n_rows {
4007                        let mut row = Vec::with_capacity(n_dims);
4008                        for c_name in &cols {
4009                            let f = ca.field_by_name(c_name)?;
4010                            let val = f.f64()?.get(i).unwrap_or(f64::NAN);
4011                            row.push(val);
4012                        }
4013                        data.push(row);
4014                    }
4015
4016                    // Placeholder GMM: requires fitting or pre-trained params.
4017                    // For now, we'll use a simple default clusterer based on means.
4018                    let values = vec![0u32; n_rows];
4019
4020                    Ok(Some(Column::from(Series::new("gmm_regime".into(), values))))
4021                },
4022                GetOutput::from_type(DataType::UInt32),
4023            )
4024            .alias("gmm_regime")])
4025    }
4026
4027    pub fn regimes_duration_stats(self, regime_col: &str, num_states: usize) -> LazyFrame {
4028        let name_str = regime_col.to_string();
4029        self.0.clone().with_columns([col(&name_str)
4030            .map(
4031                move |s| {
4032                    let ca = s.u32()?;
4033                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
4034                    let stats =
4035                        quantwave_core::RegimeAnalytics::duration_stats(&states, num_states);
4036
4037                    // Convert stats to a Struct
4038                    let mut regime_ids = Vec::new();
4039                    let mut means = Vec::new();
4040                    let mut medians = Vec::new();
4041                    let mut stds = Vec::new();
4042                    let mut maxes = Vec::new();
4043                    let mut totals = Vec::new();
4044
4045                    for stat in stats {
4046                        regime_ids.push(stat.regime_id);
4047                        means.push(stat.mean_duration);
4048                        medians.push(stat.median_duration);
4049                        stds.push(stat.std_duration);
4050                        maxes.push(stat.max_duration as u32);
4051                        totals.push(stat.total_observations as u32);
4052                    }
4053
4054                    let s_id = Series::new("regime_id".into(), regime_ids);
4055                    let s_mean = Series::new("mean_duration".into(), means);
4056                    let s_median = Series::new("median_duration".into(), medians);
4057                    let s_std = Series::new("std_duration".into(), stds);
4058                    let s_max = Series::new("max_duration".into(), maxes);
4059                    let s_total = Series::new("total_observations".into(), totals);
4060
4061                    let struct_series = StructChunked::from_series(
4062                        "duration_stats".into(),
4063                        s_id.len(),
4064                        [s_id, s_mean, s_median, s_std, s_max, s_total].iter(),
4065                    )?;
4066                    Ok(Some(Column::from(struct_series.into_series())))
4067                },
4068                GetOutput::from_type(DataType::Struct(vec![
4069                    Field::new("regime_id".into(), DataType::UInt32),
4070                    Field::new("mean_duration".into(), DataType::Float64),
4071                    Field::new("median_duration".into(), DataType::Float64),
4072                    Field::new("std_duration".into(), DataType::Float64),
4073                    Field::new("max_duration".into(), DataType::UInt32),
4074                    Field::new("total_observations".into(), DataType::UInt32),
4075                ])),
4076            )
4077            .alias("regime_duration_stats")])
4078    }
4079
4080    pub fn regimes_transition_matrix(self, regime_col: &str, num_states: usize) -> LazyFrame {
4081        let name_str = regime_col.to_string();
4082        self.0.clone().with_columns([col(&name_str)
4083            .map(
4084                move |s| {
4085                    let ca = s.u32()?;
4086                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
4087                    let matrix =
4088                        quantwave_core::RegimeAnalytics::transition_matrix(&states, num_states);
4089
4090                    // Return as a List of Lists (effectively a matrix)
4091                    let mut builders = ListPrimitiveChunkedBuilder::<Float64Type>::new(
4092                        "transition_matrix".into(),
4093                        matrix.len(),
4094                        matrix.len() * num_states,
4095                        DataType::Float64,
4096                    );
4097                    for row in matrix {
4098                        builders.append_slice(&row);
4099                    }
4100
4101                    let list_ca = builders.finish();
4102                    Ok(Some(Column::from(list_ca.into_series())))
4103                },
4104                GetOutput::from_type(DataType::List(Box::new(DataType::Float64))),
4105            )
4106            .alias("regime_transition_matrix")])
4107    }
4108
4109    pub fn regimes_stability_score(self, regime_col: &str) -> LazyFrame {
4110        let name_str = regime_col.to_string();
4111        self.0.clone().with_columns([col(&name_str)
4112            .map(
4113                move |s| {
4114                    let ca = s.u32()?;
4115                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
4116                    let score = quantwave_core::RegimeAnalytics::stability_score(&states);
4117
4118                    Ok(Some(Column::from(Series::new(
4119                        "stability_score".into(),
4120                        vec![score; s.len()],
4121                    ))))
4122                },
4123                GetOutput::from_type(DataType::Float64),
4124            )
4125            .alias("regime_stability_score")])
4126    }
4127
4128    pub fn regimes_next_state_prob(
4129        self,
4130        regime_col: &str,
4131        num_states: usize,
4132        steps: usize,
4133    ) -> LazyFrame {
4134        let name_str = regime_col.to_string();
4135        self.0.clone().with_columns([col(&name_str)
4136            .map(
4137                move |s| {
4138                    let ca = s.u32()?;
4139                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
4140                    let matrix =
4141                        quantwave_core::RegimeAnalytics::transition_matrix(&states, num_states);
4142
4143                    let mut builders = ListPrimitiveChunkedBuilder::<Float64Type>::new(
4144                        "next_state_probs".into(),
4145                        s.len(),
4146                        s.len() * num_states,
4147                        DataType::Float64,
4148                    );
4149
4150                    for &current in &states {
4151                        let probs = quantwave_core::RegimeAnalytics::forecast_state(
4152                            &matrix, current, steps,
4153                        );
4154                        builders.append_slice(&probs);
4155                    }
4156
4157                    let list_ca = builders.finish();
4158                    Ok(Some(Column::from(list_ca.into_series())))
4159                },
4160                GetOutput::from_type(DataType::List(Box::new(DataType::Float64))),
4161            )
4162            .alias("next_state_probs")])
4163    }
4164
4165    pub fn filter_by_regime(self, regime_col: &str, target_regime: u32) -> LazyFrame {
4166        self.0
4167            .clone()
4168            .filter(col(regime_col).eq(lit(target_regime)))
4169    }
4170
4171    pub fn apply_regime_strategy(
4172        self,
4173        regime_col: &str,
4174        signal_col: &str,
4175        regime_weights: std::collections::HashMap<u32, f64>,
4176    ) -> LazyFrame {
4177        let mut case_expr = col(signal_col);
4178        for (regime, weight) in regime_weights {
4179            case_expr = when(col(regime_col).eq(lit(regime)))
4180                .then(col(signal_col) * lit(weight))
4181                .otherwise(case_expr);
4182        }
4183
4184        self.0
4185            .clone()
4186            .with_columns([case_expr.alias("regime_adjusted_signal")])
4187    }
4188
4189    pub fn regimes_ms_garch(self, returns_col: &str) -> LazyFrame {
4190        let name_str = returns_col.to_string();
4191        self.0.clone().with_columns([col(&name_str)
4192            .map(
4193                move |s| {
4194                    let ca = s.f64()?;
4195                    let mut model = quantwave_core::regimes::ms_garch::MSGarch::low_high_vol();
4196                    let mut regimes = Vec::with_capacity(s.len());
4197                    let mut vols = Vec::with_capacity(s.len());
4198
4199                    for i in 0..s.len() {
4200                        let ret = ca.get(i).unwrap_or(0.0);
4201                        let (regime, vol) = model.next(ret);
4202
4203                        let r_val = match regime {
4204                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4205                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4206                            quantwave_core::regimes::MarketRegime::Bull => 2,
4207                            quantwave_core::regimes::MarketRegime::Bear => 3,
4208                            quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
4209                        };
4210                        regimes.push(r_val);
4211                        vols.push(vol);
4212                    }
4213
4214                    let s_regime = Series::new("regime".into(), regimes);
4215                    let s_vol = Series::new("estimated_vol".into(), vols);
4216                    let struct_series = StructChunked::from_series(
4217                        "ms_garch_data".into(),
4218                        s.len(),
4219                        [s_regime, s_vol].iter(),
4220                    )?;
4221                    Ok(Some(Column::from(struct_series.into_series())))
4222                },
4223                GetOutput::from_type(DataType::Struct(vec![
4224                    Field::new("regime".into(), DataType::UInt32),
4225                    Field::new("estimated_vol".into(), DataType::Float64),
4226                ])),
4227            )
4228            .alias("ms_garch_data")])
4229    }
4230
4231    pub fn regimes_ensemble(self, columns: &[&str], weights: &[f64]) -> LazyFrame {
4232        let cols: Vec<String> = columns.iter().map(|s| s.to_string()).collect();
4233        let col_exprs: Vec<Expr> = cols.iter().map(col).collect();
4234        let w_vec = weights.to_vec();
4235
4236        self.0.clone().with_columns([as_struct(col_exprs)
4237            .map(
4238                move |s| {
4239                    let ca = s.struct_()?;
4240                    let n_rows = s.len();
4241                    let n_dims = cols.len();
4242                    let ensemble =
4243                        quantwave_core::regimes::ensemble::RegimeEnsemble::new(w_vec.clone());
4244
4245                    let mut results = Vec::with_capacity(n_rows);
4246                    for i in 0..n_rows {
4247                        let mut row_regimes = Vec::with_capacity(n_dims);
4248                        for c_name in &cols {
4249                            let f = ca.field_by_name(c_name)?;
4250                            let val = f.u32()?.get(i).unwrap_or(0);
4251
4252                            let regime = match val {
4253                                0 => quantwave_core::regimes::MarketRegime::Steady,
4254                                1 => quantwave_core::regimes::MarketRegime::Crisis,
4255                                2 => quantwave_core::regimes::MarketRegime::Bull,
4256                                3 => quantwave_core::regimes::MarketRegime::Bear,
4257                                v if v >= 4 => {
4258                                    quantwave_core::regimes::MarketRegime::Cluster((v - 4) as u8)
4259                                }
4260                                _ => quantwave_core::regimes::MarketRegime::Steady,
4261                            };
4262                            row_regimes.push(regime);
4263                        }
4264
4265                        let consensus = ensemble.vote(&row_regimes);
4266                        let out = match consensus {
4267                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4268                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4269                            quantwave_core::regimes::MarketRegime::Bull => 2,
4270                            quantwave_core::regimes::MarketRegime::Bear => 3,
4271                            quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
4272                        };
4273                        results.push(out);
4274                    }
4275
4276                    Ok(Some(Column::from(Series::new(
4277                        "ensemble_regime".into(),
4278                        results,
4279                    ))))
4280                },
4281                GetOutput::from_type(DataType::UInt32),
4282            )
4283            .alias("ensemble_regime")])
4284    }
4285
4286    pub fn regimes_tar(self, signal_col: &str, thresholds: Vec<f64>) -> LazyFrame {
4287        let name_str = signal_col.to_string();
4288        self.0.clone().with_columns([col(&name_str)
4289            .map(
4290                move |s| {
4291                    let ca = s.f64()?;
4292                    let mut model = quantwave_core::regimes::tar::TAR::multi(thresholds.clone());
4293                    let mut results = Vec::with_capacity(s.len());
4294
4295                    for i in 0..s.len() {
4296                        let val = ca.get(i).unwrap_or(f64::NAN);
4297                        let regime = model.next(val);
4298                        let out = match regime {
4299                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4300                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4301                            quantwave_core::regimes::MarketRegime::Bull => 2,
4302                            quantwave_core::regimes::MarketRegime::Bear => 3,
4303                            quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
4304                        };
4305                        results.push(out);
4306                    }
4307
4308                    Ok(Some(Column::from(Series::new(
4309                        "tar_regime".into(),
4310                        results,
4311                    ))))
4312                },
4313                GetOutput::from_type(DataType::UInt32),
4314            )
4315            .alias("tar_regime")])
4316    }
4317
4318    pub fn regimes_hsmm(self, name: &str) -> LazyFrame {
4319        let name_str = name.to_string();
4320        self.0.clone().with_columns([col(&name_str)
4321            .map(
4322                move |s| {
4323                    let ca = s.f64()?;
4324                    // Default 2-state HSMM: Poisson durations (5 days Bull, 2 days Bear)
4325                    let mut model = quantwave_core::regimes::hsmm::HSMM::new(
4326                        vec![vec![0.0, 1.0], vec![1.0, 0.0]], // Always switch
4327                        vec![0.001, -0.002],
4328                        vec![0.01, 0.02],
4329                        vec![
4330                            quantwave_core::regimes::hsmm::DurationDistribution::Poisson {
4331                                lambda: 5.0,
4332                            },
4333                            quantwave_core::regimes::hsmm::DurationDistribution::Poisson {
4334                                lambda: 2.0,
4335                            },
4336                        ],
4337                    );
4338                    let mut values = Vec::with_capacity(s.len());
4339
4340                    for i in 0..s.len() {
4341                        let val = ca.get(i).unwrap_or(f64::NAN);
4342                        let regime = model.next(val);
4343                        let out = match regime {
4344                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4345                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4346                            _ => 2, // Map others
4347                        };
4348                        values.push(out);
4349                    }
4350
4351                    Ok(Some(Column::from(Series::new(
4352                        "hsmm_regime".into(),
4353                        values,
4354                    ))))
4355                },
4356                GetOutput::from_type(DataType::UInt32),
4357            )
4358            .alias("hsmm_regime")])
4359    }
4360
4361    pub fn regimes_hmm_gas(self, name: &str) -> LazyFrame {
4362        let name_str = name.to_string();
4363        self.0.clone().with_columns([col(&name_str)
4364            .map(
4365                move |s| {
4366                    let ca = s.f64()?;
4367                    let mut model = quantwave_core::regimes::hmm_gas::HMMGAS::new(
4368                        [0.1, 0.05, 0.9], // p11 params
4369                        [0.1, 0.05, 0.9], // p22 params
4370                        [0.001, -0.002],
4371                        [0.01, 0.02],
4372                    );
4373                    let mut values = Vec::with_capacity(s.len());
4374
4375                    for i in 0..s.len() {
4376                        let val = ca.get(i).unwrap_or(f64::NAN);
4377                        let regime = model.next(val);
4378                        let out = match regime {
4379                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4380                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4381                            _ => 2,
4382                        };
4383                        values.push(out);
4384                    }
4385
4386                    Ok(Some(Column::from(Series::new(
4387                        "hmm_gas_regime".into(),
4388                        values,
4389                    ))))
4390                },
4391                GetOutput::from_type(DataType::UInt32),
4392            )
4393            .alias("hmm_gas_regime")])
4394    }
4395
4396    pub fn regimes_conditioned_metrics(
4397        self,
4398        returns_col: &str,
4399        regime_col: &str,
4400        annualization_factor: f64,
4401    ) -> LazyFrame {
4402        let ret_str = returns_col.to_string();
4403        let reg_str = regime_col.to_string();
4404
4405        self.0.clone().group_by([col(&reg_str)]).agg([
4406            col(&ret_str).mean().alias("mean_return"),
4407            col(&ret_str).std(1).alias("volatility"),
4408            (col(&ret_str).mean() / col(&ret_str).std(1) * lit(annualization_factor.sqrt()))
4409                .alias("sharpe_ratio"),
4410            col(&ret_str).skew(true).alias("skewness"),
4411            col(&ret_str).kurtosis(true, true).alias("kurtosis"),
4412            // Sortino Ratio: Mean return / Downside Deviation
4413            (col(&ret_str).mean() / col(&ret_str).filter(col(&ret_str).lt(lit(0.0))).std(1)
4414                * lit(annualization_factor.sqrt()))
4415            .alias("sortino_ratio"),
4416            // For Max Drawdown and Ulcer Index in an AGG context, we can use map_groups if needed,
4417            // but for now let's stick to simpler metrics or use a robust multi-step approach.
4418            // We'll calculate drawdown stats by first expanding the groups.
4419        ])
4420    }
4421}
4422
4423#[cfg(test)]
4424mod tests {
4425    use super::*;
4426
4427    #[test]
4428    fn test_polars_heikin_ashi() -> PolarsResult<()> {
4429        let df = df![
4430            "open" => [10.0, 11.0],
4431            "high" => [12.0, 13.0],
4432            "low" => [8.0, 10.0],
4433            "close" => [11.0, 12.0]
4434        ]?;
4435
4436        let out = df
4437            .lazy()
4438            .ta()
4439            .heikin_ashi("open", "high", "low", "close")
4440            .collect()?;
4441
4442        let ha = out.column("heikin_ashi_data")?.struct_()?;
4443        assert_eq!(ha.field_by_name("ha_open")?.f64()?.get(0), Some(10.5));
4444        assert_eq!(ha.field_by_name("ha_close")?.f64()?.get(0), Some(10.25));
4445
4446        Ok(())
4447    }
4448
4449    #[test]
4450    fn test_polars_tema_zlema() -> PolarsResult<()> {
4451        let df = df![
4452            "price" => [1.0, 2.0, 3.0, 4.0, 5.0]
4453        ]?;
4454
4455        let out = df.clone().lazy().ta().tema("price", 3).collect()?;
4456
4457        let tema = out.column("tema")?.f64()?;
4458        assert!(tema.get(4).is_some());
4459
4460        let out2 = df.lazy().ta().zlema("price", 3).collect()?;
4461
4462        let zlema = out2.column("zlema")?.f64()?;
4463        assert!(zlema.get(4).is_some());
4464
4465        Ok(())
4466    }
4467
4468    #[test]
4469    fn test_polars_atr_ts() -> PolarsResult<()> {
4470        let df = df![
4471            "high" => [10.0, 12.0, 11.0],
4472            "low" => [8.0, 10.0, 9.0],
4473            "close" => [9.0, 11.0, 10.0]
4474        ]?;
4475
4476        let out = df
4477            .lazy()
4478            .ta()
4479            .atr_trailing_stop("high", "low", "close", 14, 2.5)
4480            .collect()?;
4481
4482        let atr_ts = out.column("atr_ts_data")?.struct_()?;
4483        assert!(atr_ts.field_by_name("stop")?.f64()?.get(0).is_some());
4484        assert!(atr_ts.field_by_name("direction")?.f64()?.get(0).is_some());
4485
4486        Ok(())
4487    }
4488
4489    #[test]
4490    fn test_polars_pivot_points() -> PolarsResult<()> {
4491        let df = df![
4492            "high" => [10.0, 12.0, 11.0],
4493            "low" => [8.0, 10.0, 9.0],
4494            "close" => [9.0, 11.0, 10.0]
4495        ]?;
4496
4497        let out = df
4498            .lazy()
4499            .ta()
4500            .pivot_points("high", "low", "close")
4501            .collect()?;
4502
4503        let pivot = out.column("pivot_points_data")?.struct_()?;
4504        assert!(pivot.field_by_name("p")?.f64()?.get(0).is_some());
4505        assert!(pivot.field_by_name("r1")?.f64()?.get(0).is_some());
4506
4507        Ok(())
4508    }
4509
4510    #[test]
4511    fn test_polars_fractals() -> PolarsResult<()> {
4512        let df = df![
4513            "high" => [10.0, 11.0, 15.0, 12.0, 10.0],
4514            "low" => [5.0, 6.0, 2.0, 6.0, 7.0]
4515        ]?;
4516
4517        let out = df
4518            .lazy()
4519            .ta()
4520            .bill_williams_fractals("high", "low")
4521            .collect()?;
4522
4523        let fractals = out.column("fractals_data")?.struct_()?;
4524        assert!(fractals.field_by_name("bearish")?.bool()?.get(4).unwrap());
4525        assert!(fractals.field_by_name("bullish")?.bool()?.get(4).unwrap());
4526
4527        Ok(())
4528    }
4529
4530    #[test]
4531    fn test_polars_ichimoku() -> PolarsResult<()> {
4532        let df = df![
4533            "high" => [10.0, 11.0, 15.0, 12.0, 10.0],
4534            "low" => [5.0, 6.0, 2.0, 6.0, 7.0]
4535        ]?;
4536
4537        let out = df
4538            .lazy()
4539            .ta()
4540            .ichimoku_cloud("high", "low", 9, 26, 52)
4541            .collect()?;
4542
4543        let ichimoku = out.column("ichimoku_data")?.struct_()?;
4544        assert!(ichimoku.field_by_name("tenkan")?.f64()?.get(4).is_some());
4545        assert!(ichimoku.field_by_name("kijun")?.f64()?.get(4).is_some());
4546
4547        Ok(())
4548    }
4549
4550    #[test]
4551    fn test_polars_wavetrend() -> PolarsResult<()> {
4552        let df = df![
4553            "high" => [10.0, 12.0, 11.0],
4554            "low" => [8.0, 10.0, 9.0],
4555            "close" => [9.0, 11.0, 10.0]
4556        ]?;
4557
4558        let out = df
4559            .lazy()
4560            .ta()
4561            .wavetrend("high", "low", "close", 10, 21, 4)
4562            .collect()?;
4563
4564        let wt = out.column("wavetrend_data")?.struct_()?;
4565        assert!(wt.field_by_name("wt1")?.f64()?.get(0).is_some());
4566        assert!(wt.field_by_name("wt2")?.f64()?.get(0).is_some());
4567
4568        Ok(())
4569    }
4570
4571    #[test]
4572    fn test_polars_vortex() -> PolarsResult<()> {
4573        let df = df![
4574            "high" => [10.0, 12.0, 11.0],
4575            "low" => [8.0, 10.0, 9.0],
4576            "close" => [9.0, 11.0, 10.0]
4577        ]?;
4578
4579        let out = df
4580            .lazy()
4581            .ta()
4582            .vortex_indicator("high", "low", "close", 14)
4583            .collect()?;
4584
4585        let vortex = out.column("vortex_data")?.struct_()?;
4586        assert!(vortex.field_by_name("vi_plus")?.f64()?.get(0).is_some());
4587        assert!(vortex.field_by_name("vi_minus")?.f64()?.get(0).is_some());
4588
4589        Ok(())
4590    }
4591
4592    #[test]
4593    fn test_polars_ttm_squeeze() -> PolarsResult<()> {
4594        let df = df![
4595            "high" => [11.0, 12.0, 13.0, 14.0],
4596            "low" => [9.0, 10.0, 11.0, 12.0],
4597            "close" => [10.0, 11.0, 12.0, 13.0]
4598        ]?;
4599
4600        let out = df
4601            .lazy()
4602            .ta()
4603            .ttm_squeeze("high", "low", "close", 20, 2.0, 1.5)
4604            .collect()?;
4605
4606        let ttm = out.column("ttm_squeeze_data")?.struct_()?;
4607        assert!(ttm.field_by_name("histogram")?.f64()?.get(0).is_some());
4608        assert!(ttm.field_by_name("is_squeezed")?.bool()?.get(0).is_some());
4609
4610        Ok(())
4611    }
4612
4613    #[test]
4614    fn test_polars_donchian() -> PolarsResult<()> {
4615        let df = df![
4616            "high" => [10.0, 12.0, 11.0, 13.0, 15.0],
4617            "low" => [8.0, 7.0, 9.0, 10.0, 12.0]
4618        ]?;
4619
4620        let out = df
4621            .lazy()
4622            .ta()
4623            .donchian_channels("high", "low", 3)
4624            .collect()?;
4625
4626        let donchian = out.column("donchian_data")?.struct_()?;
4627        // bar 4: H=13, L=10. Window (12,7), (11,9), (13,10). Upper=13, Lower=7, Middle=10
4628        assert_eq!(donchian.field_by_name("upper")?.f64()?.get(3), Some(13.0));
4629        assert_eq!(donchian.field_by_name("middle")?.f64()?.get(3), Some(10.0));
4630        assert_eq!(donchian.field_by_name("lower")?.f64()?.get(3), Some(7.0));
4631
4632        Ok(())
4633    }
4634
4635    #[test]
4636    fn test_polars_alma() -> PolarsResult<()> {
4637        let df = df![
4638            "price" => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
4639        ]?;
4640
4641        let out = df.lazy().ta().alma("price", 9, 0.85, 6.0).collect()?;
4642
4643        let alma = out.column("alma")?.f64()?;
4644        assert!(alma.get(9).is_some());
4645
4646        Ok(())
4647    }
4648
4649    #[test]
4650    fn test_polars_keltner() -> PolarsResult<()> {
4651        let df = df![
4652            "high" => [12.0],
4653            "low" => [8.0],
4654            "close" => [10.0]
4655        ]?;
4656
4657        let out = df
4658            .lazy()
4659            .ta()
4660            .keltner_channels("high", "low", "close", 3, 3, 2.0)
4661            .collect()?;
4662
4663        let keltner = out.column("keltner_data")?.struct_()?;
4664        assert_eq!(keltner.field_by_name("middle")?.f64()?.get(0), Some(10.0));
4665        assert_eq!(keltner.field_by_name("upper")?.f64()?.get(0), Some(18.0));
4666        assert_eq!(keltner.field_by_name("lower")?.f64()?.get(0), Some(2.0));
4667
4668        Ok(())
4669    }
4670
4671    #[test]
4672    fn test_polars_hma() -> PolarsResult<()> {
4673        let df = df![
4674            "price" => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
4675        ]?;
4676
4677        let out = df.lazy().ta().hma("price", 4).collect()?;
4678
4679        let hma = out.column("hma")?.f64()?;
4680        assert!(hma.get(9).is_some());
4681
4682        Ok(())
4683    }
4684
4685    #[test]
4686    fn test_polars_anchored_vwap() -> PolarsResult<()> {
4687        let df = df![
4688            "price" => [10.0, 12.0, 15.0, 16.0],
4689            "volume" => [100.0, 200.0, 100.0, 100.0],
4690            "anchor" => [false, false, true, false]
4691        ]?;
4692
4693        let out = df
4694            .lazy()
4695            .ta()
4696            .anchored_vwap("price", "volume", "anchor")
4697            .collect()?;
4698
4699        let avwap = out.column("avwap")?.f64()?;
4700        assert_eq!(avwap.get(0), Some(10.0));
4701        assert_eq!(avwap.get(1), Some(11.333333333333334));
4702        assert_eq!(avwap.get(2), Some(15.0));
4703        assert_eq!(avwap.get(3), Some(15.5));
4704
4705        Ok(())
4706    }
4707
4708    #[test]
4709    fn test_polars_math_transforms() -> PolarsResult<()> {
4710        let df = df![
4711            "val" => [0.0, 1.5707963267948966] // 0, PI/2
4712        ]?;
4713
4714        let out = df.lazy().ta().sin("val").collect()?;
4715
4716        let sin = out.column("sin")?.f64()?;
4717        assert!((sin.get(0).unwrap() - 0.0).abs() < 1e-10);
4718        assert!((sin.get(1).unwrap() - 1.0).abs() < 1e-10);
4719
4720        Ok(())
4721    }
4722
4723    #[test]
4724    fn test_polars_math_operators() -> PolarsResult<()> {
4725        let df = df![
4726            "v1" => [10.0, 20.0],
4727            "v2" => [5.0, 30.0]
4728        ]?;
4729
4730        let out = df.lazy().ta().add("v1", "v2").ta().max("v1", 2).collect()?;
4731
4732        let add = out.column("add")?.f64()?;
4733        assert_eq!(add.get(0), Some(15.0));
4734        assert_eq!(add.get(1), Some(50.0));
4735
4736        let max = out.column("max")?.f64()?;
4737        assert_eq!(max.get(1), Some(20.0));
4738
4739        Ok(())
4740    }
4741
4742    #[test]
4743    fn test_polars_vpn() -> PolarsResult<()> {
4744        let df = df![
4745            "high" => [10.0, 11.0, 12.0],
4746            "low" => [9.0, 10.0, 11.0],
4747            "close" => [9.5, 10.5, 11.5],
4748            "volume" => [1000.0, 1100.0, 1200.0]
4749        ]?;
4750
4751        let out = df
4752            .lazy()
4753            .ta()
4754            .vpn("high", "low", "close", "volume", 30, 3)
4755            .collect()?;
4756        let vpn = out.column("vpn")?.f64()?;
4757        assert!(vpn.get(2).is_some());
4758        Ok(())
4759    }
4760
4761    #[test]
4762    fn test_polars_gap_momentum() -> PolarsResult<()> {
4763        let df = df![
4764            "open" => [10.0, 11.0, 10.0],
4765            "close" => [10.5, 10.5, 9.5]
4766        ]?;
4767
4768        let out = df
4769            .lazy()
4770            .ta()
4771            .gap_momentum("open", "close", 10, 5)
4772            .collect()?;
4773        let gm = out.column("gap_momentum")?.struct_()?;
4774        assert!(gm.field_by_name("gap_ratio")?.f64()?.get(2).is_some());
4775        assert!(gm.field_by_name("gap_signal")?.f64()?.get(2).is_some());
4776        Ok(())
4777    }
4778
4779    #[test]
4780    fn test_polars_autotune() -> PolarsResult<()> {
4781        let df = df![
4782            "price" => [100.0; 50]
4783        ]?;
4784
4785        let out = df
4786            .lazy()
4787            .ta()
4788            .autotune_filter("price", 20, 0.25)
4789            .collect()?;
4790        let at = out.column("autotune")?.f64()?;
4791        assert!(at.get(49).is_some());
4792        Ok(())
4793    }
4794
4795    #[test]
4796    fn test_polars_adaptive_ema() -> PolarsResult<()> {
4797        let df = df!["h" => [10.0, 11.0, 10.5], "l" => [9.0, 10.0, 9.5], "c" => [9.5, 10.5, 10.0]]?;
4798        let out = df
4799            .lazy()
4800            .ta()
4801            .adaptive_ema("h", "l", "c", 10, 2)
4802            .collect()?;
4803        assert!(out.column("adaptive_ema")?.f64()?.get(2).is_some());
4804        Ok(())
4805    }
4806
4807    #[test]
4808    fn test_polars_obvm() -> PolarsResult<()> {
4809        let df = df!["h" => [10.0, 11.0], "l" => [9.0, 10.0], "c" => [9.5, 10.5], "v" => [100.0, 200.0]]?;
4810        let out = df.lazy().ta().obvm("h", "l", "c", "v", 10, 3).collect()?;
4811        let data = out.column("obvm_data")?.struct_()?;
4812        assert!(data.field_by_name("obvm")?.f64()?.get(1).is_some());
4813        Ok(())
4814    }
4815
4816    #[test]
4817    fn test_polars_vfi() -> PolarsResult<()> {
4818        let df = df!["h" => [10.0, 11.0], "l" => [9.0, 10.0], "c" => [9.5, 10.5], "v" => [100.0, 200.0]]?;
4819        let out = df
4820            .lazy()
4821            .ta()
4822            .vfi("h", "l", "c", "v", 10, 0.2, 2.5, 3)
4823            .collect()?;
4824        assert!(out.column("vfi")?.f64()?.get(1).is_some());
4825        Ok(())
4826    }
4827
4828    #[test]
4829    fn test_polars_sdo() -> PolarsResult<()> {
4830        let df = df!["p" => [10.0, 11.0, 12.0]]?;
4831        let out = df.lazy().ta().sdo("p", 2, 5, 3).collect()?;
4832        assert!(out.column("sdo")?.f64()?.get(2).is_some());
4833        Ok(())
4834    }
4835
4836    #[test]
4837    fn test_polars_rsmk() -> PolarsResult<()> {
4838        let df = df!["p" => [10.0, 11.0], "b" => [100.0, 101.0]]?;
4839        let out = df.lazy().ta().rsmk("p", "b", 90, 3).collect()?;
4840        assert!(out.column("rsmk")?.f64()?.get(1).is_some());
4841        Ok(())
4842    }
4843
4844    #[test]
4845    fn test_polars_rodc() -> PolarsResult<()> {
4846        let df = df!["p" => [10.0, 11.0, 10.0, 11.0, 12.0]]?;
4847        let out = df.lazy().ta().rodc("p", 10, 0.5, 3).collect()?;
4848        assert!(out.column("rodc")?.f64()?.get(4).is_some());
4849        Ok(())
4850    }
4851
4852    #[test]
4853    fn test_polars_reverse_ema() -> PolarsResult<()> {
4854        let df = df!["p" => [10.0, 11.0, 12.0]]?;
4855        let out = df.lazy().ta().reverse_ema("p", 0.1).collect()?;
4856        assert!(out.column("reverse_ema")?.f64()?.get(2).is_some());
4857        Ok(())
4858    }
4859
4860    #[test]
4861    fn test_polars_harrington_adx() -> PolarsResult<()> {
4862        let df =
4863            df!["h" => [10.0, 11.0, 12.0], "l" => [9.0, 10.0, 11.0], "c" => [9.5, 10.5, 11.5]]?;
4864        let out = df
4865            .lazy()
4866            .ta()
4867            .harrington_adx("h", "l", "c", 10, 1)
4868            .collect()?;
4869        assert!(out.column("harrington_adx")?.f64()?.get(2).is_some());
4870        Ok(())
4871    }
4872
4873    #[test]
4874    fn test_polars_tradj_ema() -> PolarsResult<()> {
4875        let df = df!["h" => [10.0, 11.0, 10.5], "l" => [9.0, 10.0, 9.5], "c" => [9.5, 10.5, 10.0]]?;
4876        let out = df
4877            .lazy()
4878            .ta()
4879            .tradj_ema("h", "l", "c", 10, 2, 0.5)
4880            .collect()?;
4881        assert!(out.column("tradj_ema")?.f64()?.get(2).is_some());
4882        Ok(())
4883    }
4884
4885    #[test]
4886    fn test_polars_sve_volatility_bands() -> PolarsResult<()> {
4887        let df = df!["h" => [10.0, 11.0, 10.5], "l" => [9.0, 10.0, 9.5], "c" => [9.5, 10.5, 10.0]]?;
4888        let out = df
4889            .lazy()
4890            .ta()
4891            .sve_volatility_bands("h", "l", "c", 10, 1.5, 1.0, 3)
4892            .collect()?;
4893        let data = out.column("sve_bands_data")?.struct_()?;
4894        assert!(data.field_by_name("upper")?.f64()?.get(2).is_some());
4895        Ok(())
4896    }
4897
4898    #[test]
4899    fn test_polars_exp_dev_bands() -> PolarsResult<()> {
4900        let df = df!["p" => [10.0, 11.0, 12.0, 11.0, 10.0]]?;
4901        let out = df.lazy().ta().exp_dev_bands("p", 10, 2.0, true).collect()?;
4902        let data = out.column("exp_dev_bands_data")?.struct_()?;
4903        assert!(data.field_by_name("upper")?.f64()?.get(4).is_some());
4904        Ok(())
4905    }
4906
4907    #[test]
4908    fn test_polars_hmm_fit() -> PolarsResult<()> {
4909        let obs: Vec<f64> = vec![
4910            0.01, -0.008, 0.012, -0.015, 0.009, -0.011, 0.007, -0.013, 0.011, -0.009, 0.008,
4911            -0.014, 0.006, -0.01, 0.013, -0.012, 0.005, -0.007, 0.01, -0.016,
4912        ];
4913        let df = df!["returns" => obs.as_slice()]?;
4914        let out = df.lazy().ta().hmm_fit("returns", 2, 40, false).collect()?;
4915        let data = out.column("hmm_fit_data")?.struct_()?;
4916        let state_col = data.field_by_name("hmm_fit_state")?;
4917        let prob_col = data.field_by_name("hmm_fit_smooth_probs")?;
4918        let states = state_col.u32()?;
4919        let probs = prob_col.list()?;
4920        assert_eq!(states.len(), obs.len());
4921        assert_eq!(probs.len(), obs.len());
4922        assert!(states.get(0).is_some());
4923        Ok(())
4924    }
4925
4926    #[test]
4927    fn test_regimes_conditioned_metrics() -> PolarsResult<()> {
4928        let df = df![
4929            "returns" => [0.01, 0.02, -0.01, -0.02, 0.01],
4930            "regime" => [0u32, 0, 1, 1, 0]
4931        ]?;
4932
4933        let out = df
4934            .lazy()
4935            .ta()
4936            .regimes_conditioned_metrics("returns", "regime", 252.0)
4937            .collect()?;
4938
4939        assert_eq!(out.height(), 2);
4940        assert!(out.column("sharpe_ratio").is_ok());
4941        assert!(out.column("skewness").is_ok());
4942        assert!(out.column("kurtosis").is_ok());
4943        assert!(out.column("sortino_ratio").is_ok());
4944
4945        Ok(())
4946    }
4947
4948    #[test]
4949    fn test_polars_kalman_filters() -> PolarsResult<()> {
4950        let df = df![
4951            "price" => [100.0, 101.0, 102.0, 103.0, 104.0]
4952        ]?;
4953
4954        let out = df
4955            .clone()
4956            .lazy()
4957            .ta()
4958            .kalman("price", 0.01, 0.1)
4959            .collect()?;
4960
4961        let kalman = out.column("kalman")?.f64()?;
4962        assert!(kalman.get(0).is_some());
4963        assert_eq!(kalman.get(0).unwrap(), 100.0);
4964
4965        let out2 = df
4966            .lazy()
4967            .ta()
4968            .kinematic_kalman("price", 0.001, 0.0001, 0.1)
4969            .collect()?;
4970
4971        let kin_kalman = out2.column("kinematic_kalman")?.f64()?;
4972        assert!(kin_kalman.get(0).is_some());
4973        assert_eq!(kin_kalman.get(0).unwrap(), 100.0);
4974
4975        Ok(())
4976    }
4977
4978    /// Smoke test for the new PA foundation accessors.
4979    /// Verifies column presence + dtypes for rich structs (existing market_structure + new geometric_patterns).
4980    /// Uses the exact field names from the prior market_structure impl.
4981    #[test]
4982    fn smoke_ta_pa_foundation() -> PolarsResult<()> {
4983        let highs: Vec<f64> = (0..60)
4984            .map(|i| 100.0 + (i as f64 * 0.8).sin() * 5.0 + (i as f64) * 0.3)
4985            .collect();
4986        let lows: Vec<f64> = highs.iter().map(|&h| h - 1.5 - (h % 3.0) * 0.2).collect();
4987
4988        let df = df!["high" => highs, "low" => lows]?;
4989        let lf = df.lazy();
4990
4991        // market_structure (pre-existing impl in this file) -> rich struct with String bias etc.
4992        let out = lf
4993            .clone()
4994            .ta()
4995            .market_structure("high", "low", 3)
4996            .collect()?;
4997        let ms = out.column("market_structure")?;
4998        assert!(matches!(ms.dtype(), DataType::Struct(_)));
4999        let ca = ms.struct_()?;
5000        // bias is UInt32 per the new optimized impl
5001        assert_eq!(ca.field_by_name("bias")?.dtype().clone(), DataType::UInt32);
5002        assert!(ca.field_by_name("has_flip")?.bool()?.get(59).is_some());
5003
5004        // geometric_patterns -> Struct( flag: Struct(...), hs: Struct(...) )
5005        let out2 = out
5006            .lazy()
5007            .ta()
5008            .geometric_patterns("high", "low", 2)
5009            .collect()?;
5010        let gp = out2.column("geometric_patterns")?;
5011        assert!(matches!(gp.dtype(), DataType::Struct(_)));
5012        let gca = gp.struct_()?;
5013        let flag = gca.field_by_name("flag")?;
5014        assert!(matches!(flag.dtype(), DataType::Struct(_)));
5015        // pole_length_atr is the key rich field for sizing in the notebook strategy
5016        assert!(
5017            flag.struct_()?
5018                .field_by_name("pole_length_atr")?
5019                .f64()?
5020                .get(59)
5021                .is_some()
5022        );
5023
5024        Ok(())
5025    }
5026}
5027
5028impl QuantWaveExt for LazyFrame {
5029    fn ta(&self) -> QuantWaveNamespace<'_> {
5030        QuantWaveNamespace(self)
5031    }
5032}
5033
5034pub use bt::{BtNamespace, BtOptions, QuantWaveBtExt};
5035pub use quantwave_backtest::{SweepVariant, run_param_sweep, single_param_variants};