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 quantwave_backtest::{run_param_sweep, single_param_variants, SweepVariant};
34    pub use crate::{QuantWaveExt, QuantWaveNamespace};
35    pub use crate::features::TaFeaturesNamespace; // .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
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 = StructChunked::from_series(
983                    "stochf_result".into(),
984                    s.len(),
985                    [s_k, s_d].iter(),
986                )?;
987                Ok(Some(Column::from(struct_series.into_series())))
988            },
989            GetOutput::from_type(DataType::Struct(vec![
990                Field::new("fastk".into(), DataType::Float64),
991                Field::new("fastd".into(), DataType::Float64),
992            ])),
993        )
994        .alias("stochf")])
995    }
996
997    pub fn stochrsi(
998        self,
999        name: &str,
1000        timeperiod: usize,
1001        fastk_period: usize,
1002        fastd_period: usize,
1003        fastd_matype: talib::MaType,
1004    ) -> LazyFrame {
1005        let name_str = name.to_string();
1006        self.0.clone().with_columns([col(&name_str)
1007            .map(
1008                move |s| {
1009                    let ca = s.f64()?;
1010                    let mut indicator = STOCHRSI::new(timeperiod, fastk_period, fastd_period, fastd_matype);
1011                    let mut k_vals = Vec::with_capacity(s.len());
1012                    let mut d_vals = Vec::with_capacity(s.len());
1013
1014                    for i in 0..s.len() {
1015                        let val = ca.get(i).unwrap_or(f64::NAN);
1016                        let (k, d) = indicator.next(val);
1017                        k_vals.push(k);
1018                        d_vals.push(d);
1019                    }
1020
1021                    let s_k = Series::new("fastk".into(), k_vals);
1022                    let s_d = Series::new("fastd".into(), d_vals);
1023
1024                    let struct_series = StructChunked::from_series(
1025                        "stochrsi_result".into(),
1026                        s.len(),
1027                        [s_k, s_d].iter(),
1028                    )?;
1029                    Ok(Some(Column::from(struct_series.into_series())))
1030                },
1031                GetOutput::from_type(DataType::Struct(vec![
1032                    Field::new("fastk".into(), DataType::Float64),
1033                    Field::new("fastd".into(), DataType::Float64),
1034                ])),
1035            )
1036            .alias("stochrsi")])
1037    }
1038
1039    pub fn apo(
1040        self,
1041        name: &str,
1042        fastperiod: usize,
1043        slowperiod: usize,
1044        matype: talib::MaType,
1045    ) -> LazyFrame {
1046        let name_str = name.to_string();
1047        self.0.clone().with_columns([col(&name_str)
1048            .map(
1049                move |s| {
1050                    let ca = s.f64()?;
1051                    let mut indicator = APO::new(fastperiod, slowperiod, matype);
1052                    let mut values = Vec::with_capacity(s.len());
1053                    for i in 0..s.len() {
1054                        let val = ca.get(i).unwrap_or(f64::NAN);
1055                        values.push(indicator.next(val));
1056                    }
1057                    Ok(Some(Column::from(Series::new("apo".into(), values))))
1058                },
1059                GetOutput::from_type(DataType::Float64),
1060            )
1061            .alias("apo")])
1062    }
1063
1064    pub fn ppo(
1065        self,
1066        name: &str,
1067        fastperiod: usize,
1068        slowperiod: usize,
1069        matype: talib::MaType,
1070    ) -> LazyFrame {
1071        let name_str = name.to_string();
1072        self.0.clone().with_columns([col(&name_str)
1073            .map(
1074                move |s| {
1075                    let ca = s.f64()?;
1076                    let mut indicator = PPO::new(fastperiod, slowperiod, matype);
1077                    let mut values = Vec::with_capacity(s.len());
1078                    for i in 0..s.len() {
1079                        let val = ca.get(i).unwrap_or(f64::NAN);
1080                        values.push(indicator.next(val));
1081                    }
1082                    Ok(Some(Column::from(Series::new("ppo".into(), values))))
1083                },
1084                GetOutput::from_type(DataType::Float64),
1085            )
1086            .alias("ppo")])
1087    }
1088
1089    pub fn bop(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
1090        self.ta_4_in_1_out_default::<BOP>(open, high, low, close, "bop")
1091    }
1092
1093    pub fn aroonosc(self, high: &str, low: &str, period: usize) -> LazyFrame {
1094        self.math_operator_2_in_1_out_period::<AROONOSC>(high, low, period, "aroonosc")
1095    }
1096
1097    pub fn mfi(
1098        self,
1099        high: &str,
1100        low: &str,
1101        close: &str,
1102        volume: &str,
1103        period: usize,
1104    ) -> LazyFrame {
1105        let high_str = high.to_string();
1106        let low_str = low.to_string();
1107        let close_str = close.to_string();
1108        let volume_str = volume.to_string();
1109        self.0.clone().with_columns([as_struct(vec![
1110            col(&high_str),
1111            col(&low_str),
1112            col(&close_str),
1113            col(&volume_str),
1114        ])
1115        .map(
1116            move |s| {
1117                let ca = s.struct_()?;
1118                let s_h = ca.field_by_name(&high_str)?;
1119                let s_l = ca.field_by_name(&low_str)?;
1120                let s_c = ca.field_by_name(&close_str)?;
1121                let s_v = ca.field_by_name(&volume_str)?;
1122
1123                let high = s_h.f64()?;
1124                let low = s_l.f64()?;
1125                let close = s_c.f64()?;
1126                let volume = s_v.f64()?;
1127
1128                let mut indicator = MFI::new(period);
1129                let mut values = Vec::with_capacity(s.len());
1130
1131                for i in 0..s.len() {
1132                    let h = high.get(i).unwrap_or(f64::NAN);
1133                    let l = low.get(i).unwrap_or(f64::NAN);
1134                    let c = close.get(i).unwrap_or(f64::NAN);
1135                    let v = volume.get(i).unwrap_or(f64::NAN);
1136                    values.push(indicator.next((h, l, c, v)));
1137                }
1138
1139                Ok(Some(Column::from(Series::new("mfi".into(), values))))
1140            },
1141            GetOutput::from_type(DataType::Float64),
1142        )
1143        .alias("mfi")])
1144    }
1145
1146    pub fn ultosc(
1147        self,
1148        high: &str,
1149        low: &str,
1150        close: &str,
1151        timeperiod1: usize,
1152        timeperiod2: usize,
1153        timeperiod3: usize,
1154    ) -> LazyFrame {
1155        let high_str = high.to_string();
1156        let low_str = low.to_string();
1157        let close_str = close.to_string();
1158        self.0.clone().with_columns([as_struct(vec![
1159            col(&high_str),
1160            col(&low_str),
1161            col(&close_str),
1162        ])
1163        .map(
1164            move |s| {
1165                let ca = s.struct_()?;
1166                let s_h = ca.field_by_name(&high_str)?;
1167                let s_l = ca.field_by_name(&low_str)?;
1168                let s_c = ca.field_by_name(&close_str)?;
1169                let high = s_h.f64()?;
1170                let low = s_l.f64()?;
1171                let close = s_c.f64()?;
1172
1173                let mut indicator = ULTOSC::new(timeperiod1, timeperiod2, timeperiod3);
1174                let mut values = Vec::with_capacity(s.len());
1175
1176                for i in 0..s.len() {
1177                    let h = high.get(i).unwrap_or(f64::NAN);
1178                    let l = low.get(i).unwrap_or(f64::NAN);
1179                    let c = close.get(i).unwrap_or(f64::NAN);
1180                    values.push(indicator.next((h, l, c)));
1181                }
1182
1183                Ok(Some(Column::from(Series::new("ultosc".into(), values))))
1184            },
1185            GetOutput::from_type(DataType::Float64),
1186        )
1187        .alias("ultosc")])
1188    }
1189
1190    pub fn plus_dm(self, high: &str, low: &str, period: usize) -> LazyFrame {
1191        self.math_operator_2_in_1_out_period::<PLUS_DM>(high, low, period, "plus_dm")
1192    }
1193
1194    pub fn minus_dm(self, high: &str, low: &str, period: usize) -> LazyFrame {
1195        self.math_operator_2_in_1_out_period::<MINUS_DM>(high, low, period, "minus_dm")
1196    }
1197
1198    pub fn t3(
1199        self,
1200        name: &str,
1201        period: usize,
1202        v_factor: f64,
1203    ) -> LazyFrame {
1204        let name_str = name.to_string();
1205        self.0.clone().with_columns([col(&name_str)
1206            .map(
1207                move |s| {
1208                    let ca = s.f64()?;
1209                    let mut indicator = T3::new(period, v_factor);
1210                    let mut values = Vec::with_capacity(s.len());
1211                    for i in 0..s.len() {
1212                        let val = ca.get(i).unwrap_or(f64::NAN);
1213                        values.push(indicator.next(val));
1214                    }
1215                    Ok(Some(Column::from(Series::new("t3".into(), values))))
1216                },
1217                GetOutput::from_type(DataType::Float64),
1218            )
1219            .alias("t3")])
1220    }
1221
1222    pub fn mama(
1223        self,
1224        name: &str,
1225        fastlimit: f64,
1226        slowlimit: f64,
1227    ) -> LazyFrame {
1228        let name_str = name.to_string();
1229        self.0.clone().with_columns([col(&name_str)
1230            .map(
1231                move |s| {
1232                    let ca = s.f64()?;
1233                    let mut indicator = MAMA::new(fastlimit, slowlimit);
1234                    let mut mama_vals = Vec::with_capacity(s.len());
1235                    let mut fama_vals = Vec::with_capacity(s.len());
1236
1237                    for i in 0..s.len() {
1238                        let val = ca.get(i).unwrap_or(f64::NAN);
1239                        let (m, f) = indicator.next(val);
1240                        mama_vals.push(m);
1241                        fama_vals.push(f);
1242                    }
1243
1244                    let s_mama = Series::new("mama".into(), mama_vals);
1245                    let s_fama = Series::new("fama".into(), fama_vals);
1246
1247                    let struct_series = StructChunked::from_series(
1248                        "mama_result".into(),
1249                        s.len(),
1250                        [s_mama, s_fama].iter(),
1251                    )?;
1252                    Ok(Some(Column::from(struct_series.into_series())))
1253                },
1254                GetOutput::from_type(DataType::Struct(vec![
1255                    Field::new("mama".into(), DataType::Float64),
1256                    Field::new("fama".into(), DataType::Float64),
1257                ])),
1258            )
1259            .alias("mama")])
1260    }
1261
1262    pub fn sar(
1263        self,
1264        high: &str,
1265        low: &str,
1266        acceleration: f64,
1267        maximum: f64,
1268    ) -> LazyFrame {
1269        let high_str = high.to_string();
1270        let low_str = low.to_string();
1271        self.0.clone().with_columns([as_struct(vec![
1272            col(&high_str),
1273            col(&low_str),
1274        ])
1275        .map(
1276            move |s| {
1277                let ca = s.struct_()?;
1278                let s_h = ca.field_by_name(&high_str)?;
1279                let s_l = ca.field_by_name(&low_str)?;
1280                let high = s_h.f64()?;
1281                let low = s_l.f64()?;
1282
1283                let mut indicator = SAR::new(acceleration, maximum);
1284                let mut values = Vec::with_capacity(s.len());
1285
1286                for i in 0..s.len() {
1287                    let h = high.get(i).unwrap_or(f64::NAN);
1288                    let l = low.get(i).unwrap_or(f64::NAN);
1289                    values.push(indicator.next((h, l)));
1290                }
1291
1292                Ok(Some(Column::from(Series::new("sar".into(), values))))
1293            },
1294            GetOutput::from_type(DataType::Float64),
1295        )
1296        .alias("sar")])
1297    }
1298
1299    #[allow(clippy::too_many_arguments)]
1300    pub fn sarext(
1301        self,
1302        high: &str,
1303        low: &str,
1304        startvalue: f64,
1305        offsetonreverse: f64,
1306        accelerationinitlong: f64,
1307        accelerationlong: f64,
1308        accelerationmaxlong: f64,
1309        accelerationinitshort: f64,
1310        accelerationshort: f64,
1311        accelerationmaxshort: f64,
1312    ) -> LazyFrame {
1313        let high_str = high.to_string();
1314        let low_str = low.to_string();
1315        self.0.clone().with_columns([as_struct(vec![
1316            col(&high_str),
1317            col(&low_str),
1318        ])
1319        .map(
1320            move |s| {
1321                let ca = s.struct_()?;
1322                let s_h = ca.field_by_name(&high_str)?;
1323                let s_l = ca.field_by_name(&low_str)?;
1324                let high = s_h.f64()?;
1325                let low = s_l.f64()?;
1326
1327                let mut indicator = SAREXT::new(
1328                    startvalue,
1329                    offsetonreverse,
1330                    accelerationinitlong,
1331                    accelerationlong,
1332                    accelerationmaxlong,
1333                    accelerationinitshort,
1334                    accelerationshort,
1335                    accelerationmaxshort,
1336                );
1337                let mut values = Vec::with_capacity(s.len());
1338
1339                for i in 0..s.len() {
1340                    let h = high.get(i).unwrap_or(f64::NAN);
1341                    let l = low.get(i).unwrap_or(f64::NAN);
1342                    values.push(indicator.next((h, l)));
1343                }
1344
1345                Ok(Some(Column::from(Series::new("sarext".into(), values))))
1346            },
1347            GetOutput::from_type(DataType::Float64),
1348        )
1349        .alias("sarext")])
1350    }
1351
1352    pub fn mavp(
1353        self,
1354        in1: &str,
1355        in2: &str,
1356        minperiod: usize,
1357        maxperiod: usize,
1358        matype: talib::MaType,
1359    ) -> LazyFrame {
1360        let in1_str = in1.to_string();
1361        let in2_str = in2.to_string();
1362        self.0.clone().with_columns([as_struct(vec![
1363            col(&in1_str),
1364            col(&in2_str),
1365        ])
1366        .map(
1367            move |s| {
1368                let ca = s.struct_()?;
1369                let s_1 = ca.field_by_name(&in1_str)?;
1370                let s_2 = ca.field_by_name(&in2_str)?;
1371                let in1_ca = s_1.f64()?;
1372                let in2_ca = s_2.f64()?;
1373
1374                let mut indicator = MAVP::new(minperiod, maxperiod, matype);
1375                let mut values = Vec::with_capacity(s.len());
1376
1377                for i in 0..s.len() {
1378                    let i1 = in1_ca.get(i).unwrap_or(f64::NAN);
1379                    let i2 = in2_ca.get(i).unwrap_or(f64::NAN);
1380                    values.push(indicator.next((i1, i2)));
1381                }
1382
1383                Ok(Some(Column::from(Series::new("mavp".into(), values))))
1384            },
1385            GetOutput::from_type(DataType::Float64),
1386        )
1387        .alias("mavp")])
1388    }
1389
1390    pub fn ht_phasor(self, name: &str) -> LazyFrame {
1391        let name_str = name.to_string();
1392        self.0.clone().with_columns([col(&name_str)
1393            .map(
1394                move |s| {
1395                    let ca = s.f64()?;
1396                    let mut indicator = HT_PHASOR::new();
1397                    let mut inphase_vals = Vec::with_capacity(s.len());
1398                    let mut quadrature_vals = Vec::with_capacity(s.len());
1399
1400                    for i in 0..s.len() {
1401                        let val = ca.get(i).unwrap_or(f64::NAN);
1402                        let (inp, q) = indicator.next(val);
1403                        inphase_vals.push(inp);
1404                        quadrature_vals.push(q);
1405                    }
1406
1407                    let s_inphase = Series::new("inphase".into(), inphase_vals);
1408                    let s_quadrature = Series::new("quadrature".into(), quadrature_vals);
1409
1410                    let struct_series = StructChunked::from_series(
1411                        "ht_phasor_result".into(),
1412                        s.len(),
1413                        [s_inphase, s_quadrature].iter(),
1414                    )?;
1415                    Ok(Some(Column::from(struct_series.into_series())))
1416                },
1417                GetOutput::from_type(DataType::Struct(vec![
1418                    Field::new("inphase".into(), DataType::Float64),
1419                    Field::new("quadrature".into(), DataType::Float64),
1420                ])),
1421            )
1422            .alias("ht_phasor")])
1423    }
1424
1425    pub fn ht_sine(self, name: &str) -> LazyFrame {
1426        let name_str = name.to_string();
1427        self.0.clone().with_columns([col(&name_str)
1428            .map(
1429                move |s| {
1430                    let ca = s.f64()?;
1431                    let mut indicator = HT_SINE::new();
1432                    let mut sine_vals = Vec::with_capacity(s.len());
1433                    let mut leadsine_vals = Vec::with_capacity(s.len());
1434
1435                    for i in 0..s.len() {
1436                        let val = ca.get(i).unwrap_or(f64::NAN);
1437                        let (si, ls) = indicator.next(val);
1438                        sine_vals.push(si);
1439                        leadsine_vals.push(ls);
1440                    }
1441
1442                    let s_sine = Series::new("sine".into(), sine_vals);
1443                    let s_leadsine = Series::new("leadsine".into(), leadsine_vals);
1444
1445                    let struct_series = StructChunked::from_series(
1446                        "ht_sine_result".into(),
1447                        s.len(),
1448                        [s_sine, s_leadsine].iter(),
1449                    )?;
1450                    Ok(Some(Column::from(struct_series.into_series())))
1451                },
1452                GetOutput::from_type(DataType::Struct(vec![
1453                    Field::new("sine".into(), DataType::Float64),
1454                    Field::new("leadsine".into(), DataType::Float64),
1455                ])),
1456            )
1457            .alias("ht_sine")])
1458    }
1459
1460    fn ta_3_in_1_out_period<I>(
1461        self,
1462        in1: &str,
1463        in2: &str,
1464        in3: &str,
1465        period: usize,
1466        output_name: &str,
1467    ) -> LazyFrame
1468    where
1469        I: Next<(f64, f64, f64), Output = f64> + Send + Sync + 'static,
1470        I: From<usize>,
1471    {
1472        let in1_str = in1.to_string();
1473        let in2_str = in2.to_string();
1474        let in3_str = in3.to_string();
1475        let output_name_str = output_name.to_string();
1476        let output_name_for_closure = output_name_str.clone();
1477        self.0.clone().with_columns(
1478            [as_struct(vec![col(&in1_str), col(&in2_str), col(&in3_str)])
1479                .map(
1480                    move |s| {
1481                        let ca = s.struct_()?;
1482                        let s1 = ca.field_by_name(&in1_str)?;
1483                        let s2 = ca.field_by_name(&in2_str)?;
1484                        let s3 = ca.field_by_name(&in3_str)?;
1485
1486                        let ca1 = s1.f64()?;
1487                        let ca2 = s2.f64()?;
1488                        let ca3 = s3.f64()?;
1489
1490                        let mut indicator = I::from(period);
1491                        let mut values = Vec::with_capacity(s.len());
1492
1493                        for i in 0..s.len() {
1494                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1495                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1496                            let v3 = ca3.get(i).unwrap_or(f64::NAN);
1497                            values.push(indicator.next((v1, v2, v3)));
1498                        }
1499
1500                        Ok(Some(Column::from(Series::new(
1501                            output_name_for_closure.clone().into(),
1502                            values,
1503                        ))))
1504                    },
1505                    GetOutput::from_type(DataType::Float64),
1506                )
1507                .alias(&output_name_str)],
1508        )
1509    }
1510
1511    fn ta_3_in_1_out_default<I>(
1512        self,
1513        in1: &str,
1514        in2: &str,
1515        in3: &str,
1516        output_name: &str,
1517    ) -> LazyFrame
1518    where
1519        I: Next<(f64, f64, f64), Output = f64> + Default + Send + Sync + 'static,
1520    {
1521        let in1_str = in1.to_string();
1522        let in2_str = in2.to_string();
1523        let in3_str = in3.to_string();
1524        let output_name_str = output_name.to_string();
1525        let output_name_for_closure = output_name_str.clone();
1526        self.0.clone().with_columns(
1527            [as_struct(vec![col(&in1_str), col(&in2_str), col(&in3_str)])
1528                .map(
1529                    move |s| {
1530                        let ca = s.struct_()?;
1531                        let s1 = ca.field_by_name(&in1_str)?;
1532                        let s2 = ca.field_by_name(&in2_str)?;
1533                        let s3 = ca.field_by_name(&in3_str)?;
1534
1535                        let ca1 = s1.f64()?;
1536                        let ca2 = s2.f64()?;
1537                        let ca3 = s3.f64()?;
1538
1539                        let mut indicator = I::default();
1540                        let mut values = Vec::with_capacity(s.len());
1541
1542                        for i in 0..s.len() {
1543                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1544                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1545                            let v3 = ca3.get(i).unwrap_or(f64::NAN);
1546                            values.push(indicator.next((v1, v2, v3)));
1547                        }
1548
1549                        Ok(Some(Column::from(Series::new(
1550                            output_name_for_closure.clone().into(),
1551                            values,
1552                        ))))
1553                    },
1554                    GetOutput::from_type(DataType::Float64),
1555                )
1556                .alias(&output_name_str)],
1557        )
1558    }
1559
1560    fn ta_4_in_1_out_default<I>(
1561        self,
1562        in1: &str,
1563        in2: &str,
1564        in3: &str,
1565        in4: &str,
1566        output_name: &str,
1567    ) -> LazyFrame
1568    where
1569        I: Next<(f64, f64, f64, f64), Output = f64> + Default + Send + Sync + 'static,
1570    {
1571        let in1_str = in1.to_string();
1572        let in2_str = in2.to_string();
1573        let in3_str = in3.to_string();
1574        let in4_str = in4.to_string();
1575        let output_name_str = output_name.to_string();
1576        let output_name_for_closure = output_name_str.clone();
1577        self.0.clone().with_columns([as_struct(vec![
1578            col(&in1_str),
1579            col(&in2_str),
1580            col(&in3_str),
1581            col(&in4_str),
1582        ])
1583        .map(
1584            move |s| {
1585                let ca = s.struct_()?;
1586                let s1 = ca.field_by_name(&in1_str)?;
1587                let s2 = ca.field_by_name(&in2_str)?;
1588                let s3 = ca.field_by_name(&in3_str)?;
1589                let s4 = ca.field_by_name(&in4_str)?;
1590
1591                let ca1 = s1.f64()?;
1592                let ca2 = s2.f64()?;
1593                let ca3 = s3.f64()?;
1594                let ca4 = s4.f64()?;
1595
1596                let mut indicator = I::default();
1597                let mut values = Vec::with_capacity(s.len());
1598
1599                for i in 0..s.len() {
1600                    let v1 = ca1.get(i).unwrap_or(f64::NAN);
1601                    let v2 = ca2.get(i).unwrap_or(f64::NAN);
1602                    let v3 = ca3.get(i).unwrap_or(f64::NAN);
1603                    let v4 = ca4.get(i).unwrap_or(f64::NAN);
1604                    values.push(indicator.next((v1, v2, v3, v4)));
1605                }
1606
1607                Ok(Some(Column::from(Series::new(
1608                    output_name_for_closure.clone().into(),
1609                    values,
1610                ))))
1611            },
1612            GetOutput::from_type(DataType::Float64),
1613        )
1614        .alias(&output_name_str)])
1615    }
1616
1617    fn math_transform_1_in_1_out<I>(self, name: &str, output_name: &str) -> LazyFrame
1618    where
1619        I: Next<f64, Output = f64> + Default + Send + Sync + 'static,
1620    {
1621        let name = name.to_string();
1622        let output_name_str = output_name.to_string();
1623        let output_name_for_closure = output_name_str.clone();
1624        self.0.clone().with_columns([col(&name)
1625            .map(
1626                move |s| {
1627                    let ca = s.f64()?;
1628                    let mut indicator = I::default();
1629                    let mut values = Vec::with_capacity(s.len());
1630
1631                    for i in 0..s.len() {
1632                        let val = ca.get(i).unwrap_or(f64::NAN);
1633                        values.push(indicator.next(val));
1634                    }
1635
1636                    Ok(Some(Column::from(Series::new(
1637                        output_name_for_closure.clone().into(),
1638                        values,
1639                    ))))
1640                },
1641                GetOutput::from_type(DataType::Float64),
1642            )
1643            .alias(&output_name_str)])
1644    }
1645
1646    fn math_operator_2_in_1_out<I>(self, in1: &str, in2: &str, output_name: &str) -> LazyFrame
1647    where
1648        I: Next<(f64, f64), Output = f64> + Default + Send + Sync + 'static,
1649    {
1650        let in1_str = in1.to_string();
1651        let in2_str = in2.to_string();
1652        let output_name_str = output_name.to_string();
1653        let output_name_for_closure = output_name_str.clone();
1654        self.0
1655            .clone()
1656            .with_columns([as_struct(vec![col(&in1_str), col(&in2_str)])
1657                .map(
1658                    move |s| {
1659                        let ca = s.struct_()?;
1660                        let s1 = ca.field_by_name(&in1_str)?;
1661                        let s2 = ca.field_by_name(&in2_str)?;
1662
1663                        let ca1 = s1.f64()?;
1664                        let ca2 = s2.f64()?;
1665
1666                        let mut indicator = I::default();
1667                        let mut values = Vec::with_capacity(s.len());
1668
1669                        for i in 0..s.len() {
1670                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1671                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1672                            values.push(indicator.next((v1, v2)));
1673                        }
1674
1675                        Ok(Some(Column::from(Series::new(
1676                            output_name_for_closure.clone().into(),
1677                            values,
1678                        ))))
1679                    },
1680                    GetOutput::from_type(DataType::Float64),
1681                )
1682                .alias(&output_name_str)])
1683    }
1684
1685    fn math_operator_1_in_1_out_period<I>(
1686        self,
1687        name: &str,
1688        period: usize,
1689        output_name: &str,
1690    ) -> LazyFrame
1691    where
1692        I: Next<f64, Output = f64> + Send + Sync + 'static,
1693        I: From<usize>,
1694    {
1695        let name = name.to_string();
1696        let output_name_str = output_name.to_string();
1697        let output_name_for_closure = output_name_str.clone();
1698        self.0.clone().with_columns([col(&name)
1699            .map(
1700                move |s| {
1701                    let ca = s.f64()?;
1702                    let mut indicator = I::from(period);
1703                    let mut values = Vec::with_capacity(s.len());
1704
1705                    for i in 0..s.len() {
1706                        let val = ca.get(i).unwrap_or(f64::NAN);
1707                        values.push(indicator.next(val));
1708                    }
1709
1710                    Ok(Some(Column::from(Series::new(
1711                        output_name_for_closure.clone().into(),
1712                        values,
1713                    ))))
1714                },
1715                GetOutput::from_type(DataType::Float64),
1716            )
1717            .alias(&output_name_str)])
1718    }
1719
1720    fn math_operator_2_in_1_out_period<I>(
1721        self,
1722        in1: &str,
1723        in2: &str,
1724        period: usize,
1725        output_name: &str,
1726    ) -> LazyFrame
1727    where
1728        I: Next<(f64, f64), Output = f64> + Send + Sync + 'static,
1729        I: From<usize>,
1730    {
1731        let in1_str = in1.to_string();
1732        let in2_str = in2.to_string();
1733        let output_name_str = output_name.to_string();
1734        let output_name_for_closure = output_name_str.clone();
1735        self.0
1736            .clone()
1737            .with_columns([as_struct(vec![col(&in1_str), col(&in2_str)])
1738                .map(
1739                    move |s| {
1740                        let ca = s.struct_()?;
1741                        let s1 = ca.field_by_name(&in1_str)?;
1742                        let s2 = ca.field_by_name(&in2_str)?;
1743
1744                        let ca1 = s1.f64()?;
1745                        let ca2 = s2.f64()?;
1746
1747                        let mut indicator = I::from(period);
1748                        let mut values = Vec::with_capacity(s.len());
1749
1750                        for i in 0..s.len() {
1751                            let v1 = ca1.get(i).unwrap_or(f64::NAN);
1752                            let v2 = ca2.get(i).unwrap_or(f64::NAN);
1753                            values.push(indicator.next((v1, v2)));
1754                        }
1755
1756                        Ok(Some(Column::from(Series::new(
1757                            output_name_for_closure.clone().into(),
1758                            values,
1759                        ))))
1760                    },
1761                    GetOutput::from_type(DataType::Float64),
1762                )
1763                .alias(&output_name_str)])
1764    }
1765
1766    pub fn supertrend(self, period: usize, multiplier: f64) -> LazyFrame {
1767        self.0
1768            .clone()
1769            .with_columns([as_struct(vec![col("high"), col("low"), col("close")])
1770                .map(
1771                    move |s| {
1772                        let ca = s.struct_()?;
1773                        let s_high = ca.field_by_name("high")?;
1774                        let s_low = ca.field_by_name("low")?;
1775                        let s_close = ca.field_by_name("close")?;
1776
1777                        let high = s_high.f64()?;
1778                        let low = s_low.f64()?;
1779                        let close = s_close.f64()?;
1780
1781                        let mut st = SuperTrend::new(period, multiplier);
1782                        let mut values = Vec::with_capacity(s.len());
1783                        let mut directions = Vec::with_capacity(s.len());
1784
1785                        for i in 0..s.len() {
1786                            let h = high.get(i).unwrap_or(0.0);
1787                            let l = low.get(i).unwrap_or(0.0);
1788                            let c = close.get(i).unwrap_or(0.0);
1789                            let (val, dir) = st.next((h, l, c));
1790                            values.push(val);
1791                            directions.push(dir as f64);
1792                        }
1793
1794                        let st_series = Series::new("supertrend".into(), values);
1795                        let dir_series = Series::new("supertrend_direction".into(), directions);
1796
1797                        let out = StructChunked::from_series(
1798                            "supertrend_output".into(),
1799                            s.len(),
1800                            [st_series, dir_series].iter(),
1801                        )?;
1802                        Ok(Some(Column::from(out.into_series())))
1803                    },
1804                    GetOutput::from_type(DataType::Struct(vec![
1805                        Field::new("supertrend".into(), DataType::Float64),
1806                        Field::new("supertrend_direction".into(), DataType::Float64),
1807                    ])),
1808                )
1809                .alias("supertrend_data")])
1810    }
1811
1812    /// Market Structure (swings + confirmed BOS flips) — rich PA event foundation.
1813    ///
1814    /// Returns a Struct column "market_structure" with rich metadata fields directly usable
1815    /// for event extraction (filter rows where has_current_flip=true), backtester signals,
1816    /// and ML (regime + feature joins at flip bars).
1817    ///
1818    /// This wires the core MarketStructure Next impl + PAEvent system into Polars.
1819    /// Emits as Struct series (per project convention for composites like supertrend/bbands).
1820    /// For exploded event log: after collect, filter on has_current_flip and construct PAEvent rows
1821    /// (or use core extract_pa_events on the state columns).
1822    ///
1823    /// Sources: see market_structure.rs (MQL5 Part 21 + Parts 66/69 lessons).
1824    pub fn anchored_vwap(self, price: &str, volume: &str, anchor: &str) -> LazyFrame {
1825        let price = price.to_string();
1826        let volume = volume.to_string();
1827        let anchor = anchor.to_string();
1828
1829        self.0
1830            .clone()
1831            .with_columns([as_struct(vec![col(&price), col(&volume), col(&anchor)])
1832                .map(
1833                    move |s| {
1834                        let ca = s.struct_()?;
1835                        let s_price = ca.field_by_name(&price)?;
1836                        let s_volume = ca.field_by_name(&volume)?;
1837                        let s_anchor = ca.field_by_name(&anchor)?;
1838
1839                        let price = s_price.f64()?;
1840                        let volume = s_volume.f64()?;
1841                        let anchor = s_anchor.bool()?;
1842
1843                        let mut avwap = quantwave_core::AnchoredVWAP::new();
1844                        let mut values = Vec::with_capacity(s.len());
1845
1846                        for i in 0..s.len() {
1847                            let p = price.get(i).unwrap_or(0.0);
1848                            let v = volume.get(i).unwrap_or(0.0);
1849                            let a = anchor.get(i).unwrap_or(false);
1850                            values.push(avwap.next((p, v, a)));
1851                        }
1852
1853                        Ok(Some(Column::from(Series::new(
1854                            "anchored_vwap".into(),
1855                            values,
1856                        ))))
1857                    },
1858                    GetOutput::from_type(DataType::Float64),
1859                )
1860                .alias("avwap")])
1861    }
1862
1863    pub fn hma(self, name: &str, period: usize) -> LazyFrame {
1864        let name = name.to_string();
1865        self.0.clone().with_columns([col(&name)
1866            .map(
1867                move |s| {
1868                    let ca = s.f64()?;
1869                    let mut hma = quantwave_core::HMA::new(period);
1870                    let mut values = Vec::with_capacity(s.len());
1871
1872                    for i in 0..s.len() {
1873                        let val = ca.get(i).unwrap_or(0.0);
1874                        values.push(hma.next(val));
1875                    }
1876
1877                    Ok(Some(Column::from(Series::new("hma".into(), values))))
1878                },
1879                GetOutput::from_type(DataType::Float64),
1880            )
1881            .alias("hma")])
1882    }
1883
1884    pub fn kalman(self, name: &str, q: f64, r: f64) -> LazyFrame {
1885        let name = name.to_string();
1886        self.0.clone().with_columns([col(&name)
1887            .map(
1888                move |s| {
1889                    let ca = s.f64()?;
1890                    let mut indicator =
1891                        quantwave_core::indicators::kalman::KalmanFilter::new(q, r);
1892                    let mut values = Vec::with_capacity(s.len());
1893
1894                    for i in 0..s.len() {
1895                        let val = ca.get(i).unwrap_or(f64::NAN);
1896                        values.push(indicator.next(val));
1897                    }
1898
1899                    Ok(Some(Column::from(Series::new("kalman".into(), values))))
1900                },
1901                GetOutput::from_type(DataType::Float64),
1902            )
1903            .alias("kalman")])
1904    }
1905
1906    pub fn kinematic_kalman(self, name: &str, q_pos: f64, q_vel: f64, r: f64) -> LazyFrame {
1907        let name = name.to_string();
1908        self.0.clone().with_columns([col(&name)
1909            .map(
1910                move |s| {
1911                    let ca = s.f64()?;
1912                    let mut indicator =
1913                        quantwave_core::indicators::kinematic_kalman::KinematicKalmanFilter::new(
1914                            q_pos, q_vel, r,
1915                        );
1916                    let mut values = Vec::with_capacity(s.len());
1917
1918                    for i in 0..s.len() {
1919                        let val = ca.get(i).unwrap_or(f64::NAN);
1920                        values.push(indicator.next(val));
1921                    }
1922
1923                    Ok(Some(Column::from(Series::new(
1924                        "kinematic_kalman".into(),
1925                        values,
1926                    ))))
1927                },
1928                GetOutput::from_type(DataType::Float64),
1929            )
1930            .alias("kinematic_kalman")])
1931    }
1932
1933    pub fn vpn(
1934        self,
1935        high: &str,
1936        low: &str,
1937        close: &str,
1938        volume: &str,
1939        period: usize,
1940        smooth_period: usize,
1941    ) -> LazyFrame {
1942        let high_str = high.to_string();
1943        let low_str = low.to_string();
1944        let close_str = close.to_string();
1945        let volume_str = volume.to_string();
1946
1947        self.0.clone().with_columns([as_struct(vec![
1948            col(&high_str),
1949            col(&low_str),
1950            col(&close_str),
1951            col(&volume_str),
1952        ])
1953        .map(
1954            move |s| {
1955                let ca = s.struct_()?;
1956                let s_h = ca.field_by_name(&high_str)?;
1957                let s_l = ca.field_by_name(&low_str)?;
1958                let s_c = ca.field_by_name(&close_str)?;
1959                let s_v = ca.field_by_name(&volume_str)?;
1960
1961                let high = s_h.f64()?;
1962                let low = s_l.f64()?;
1963                let close = s_c.f64()?;
1964                let volume = s_v.f64()?;
1965
1966                let mut indicator = quantwave_core::VPNIndicator::new(period, smooth_period);
1967                let mut values = Vec::with_capacity(s.len());
1968
1969                for i in 0..s.len() {
1970                    let h = high.get(i).unwrap_or(f64::NAN);
1971                    let l = low.get(i).unwrap_or(f64::NAN);
1972                    let c = close.get(i).unwrap_or(f64::NAN);
1973                    let v = volume.get(i).unwrap_or(f64::NAN);
1974                    values.push(indicator.next((h, l, c, v)));
1975                }
1976
1977                Ok(Some(Column::from(Series::new("vpn".into(), values))))
1978            },
1979            GetOutput::from_type(DataType::Float64),
1980        )
1981        .alias("vpn")])
1982    }
1983
1984    pub fn gap_momentum(
1985        self,
1986        open: &str,
1987        close: &str,
1988        period: usize,
1989        signal_period: usize,
1990    ) -> LazyFrame {
1991        let open_str = open.to_string();
1992        let close_str = close.to_string();
1993
1994        self.0.clone().with_columns([as_struct(vec![
1995            col(&open_str),
1996            col(&close_str),
1997        ])
1998        .map(
1999            move |s| {
2000                let ca = s.struct_()?;
2001                let s_o = ca.field_by_name(&open_str)?;
2002                let s_c = ca.field_by_name(&close_str)?;
2003
2004                let open = s_o.f64()?;
2005                let close = s_c.f64()?;
2006
2007                let mut indicator = quantwave_core::GapMomentum::new(period, signal_period);
2008                let mut ratio_vals = Vec::with_capacity(s.len());
2009                let mut signal_vals = Vec::with_capacity(s.len());
2010
2011                for i in 0..s.len() {
2012                    let o = open.get(i).unwrap_or(f64::NAN);
2013                    let c = close.get(i).unwrap_or(f64::NAN);
2014                    let (ratio, signal) = indicator.next((o, c));
2015                    ratio_vals.push(ratio);
2016                    signal_vals.push(signal);
2017                }
2018
2019                let s_ratio = Series::new("gap_ratio".into(), ratio_vals);
2020                let s_signal = Series::new("gap_signal".into(), signal_vals);
2021                let struct_series = StructChunked::from_series(
2022                    "gap_momentum_result".into(),
2023                    s.len(),
2024                    [s_ratio, s_signal].iter(),
2025                )?;
2026                Ok(Some(Column::from(struct_series.into_series())))
2027            },
2028            GetOutput::from_type(DataType::Struct(vec![
2029                Field::new("gap_ratio".into(), DataType::Float64),
2030                Field::new("gap_signal".into(), DataType::Float64),
2031            ])),
2032        )
2033        .alias("gap_momentum")])
2034    }
2035
2036    pub fn autotune_filter(self, name: &str, window: usize, bandwidth: f64) -> LazyFrame {
2037        let name_str = name.to_string();
2038        self.0.clone().with_columns([col(&name_str)
2039            .map(
2040                move |s| {
2041                    let ca = s.f64()?;
2042                    let mut indicator = quantwave_core::AutoTuneFilter::new(window, bandwidth);
2043                    let mut values = Vec::with_capacity(s.len());
2044
2045                    for i in 0..s.len() {
2046                        let val = ca.get(i).unwrap_or(f64::NAN);
2047                        values.push(indicator.next(val));
2048                    }
2049
2050                    Ok(Some(Column::from(Series::new("autotune".into(), values))))
2051                },
2052                GetOutput::from_type(DataType::Float64),
2053            )
2054            .alias("autotune")])
2055    }
2056
2057    pub fn adaptive_ema(
2058        self,
2059        high: &str,
2060        low: &str,
2061        close: &str,
2062        period: usize,
2063        pds: usize,
2064    ) -> LazyFrame {
2065        let h_str = high.to_string();
2066        let l_str = low.to_string();
2067        let c_str = close.to_string();
2068
2069        self.0.clone().with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2070            .map(
2071                move |s| {
2072                    let ca = s.struct_()?;
2073                    let f_h = ca.field_by_name(&h_str)?;
2074                    let high = f_h.f64()?;
2075                    let f_l = ca.field_by_name(&l_str)?;
2076                    let low = f_l.f64()?;
2077                    let f_c = ca.field_by_name(&c_str)?;
2078                    let close = f_c.f64()?;
2079
2080                    let mut indicator = quantwave_core::AdaptiveEMA::new(period, pds);
2081                    let mut values = Vec::with_capacity(s.len());
2082
2083                    for i in 0..s.len() {
2084                        let h = high.get(i).unwrap_or(f64::NAN);
2085                        let l = low.get(i).unwrap_or(f64::NAN);
2086                        let c = close.get(i).unwrap_or(f64::NAN);
2087                        values.push(indicator.next((h, l, c)));
2088                    }
2089
2090                    Ok(Some(Column::from(Series::new("adaptive_ema".into(), values))))
2091                },
2092                GetOutput::from_type(DataType::Float64),
2093            )
2094            .alias("adaptive_ema")])
2095    }
2096
2097    pub fn tradj_ema(
2098        self,
2099        high: &str,
2100        low: &str,
2101        close: &str,
2102        period: usize,
2103        pds: usize,
2104        mltp: f64,
2105    ) -> LazyFrame {
2106        let h_str = high.to_string();
2107        let l_str = low.to_string();
2108        let c_str = close.to_string();
2109
2110        self.0.clone().with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2111            .map(
2112                move |s| {
2113                    let ca = s.struct_()?;
2114                    let f_h = ca.field_by_name(&h_str)?;
2115                    let high = f_h.f64()?;
2116                    let f_l = ca.field_by_name(&l_str)?;
2117                    let low = f_l.f64()?;
2118                    let f_c = ca.field_by_name(&c_str)?;
2119                    let close = f_c.f64()?;
2120
2121                    let mut indicator = quantwave_core::TRAdjEMA::new(period, pds, mltp);
2122                    let mut values = Vec::with_capacity(s.len());
2123
2124                    for i in 0..s.len() {
2125                        let h = high.get(i).unwrap_or(f64::NAN);
2126                        let l = low.get(i).unwrap_or(f64::NAN);
2127                        let c = close.get(i).unwrap_or(f64::NAN);
2128                        values.push(indicator.next((h, l, c)));
2129                    }
2130
2131                    Ok(Some(Column::from(Series::new("tradj_ema".into(), values))))
2132                },
2133                GetOutput::from_type(DataType::Float64),
2134            )
2135            .alias("tradj_ema")])
2136    }
2137
2138    pub fn obvm(
2139        self,
2140        high: &str,
2141        low: &str,
2142        close: &str,
2143        volume: &str,
2144        obvm_period: usize,
2145        signal_period: usize,
2146    ) -> LazyFrame {
2147        let h_str = high.to_string();
2148        let l_str = low.to_string();
2149        let c_str = close.to_string();
2150        let v_str = volume.to_string();
2151
2152        self.0.clone().with_columns([as_struct(vec![
2153            col(&h_str),
2154            col(&l_str),
2155            col(&c_str),
2156            col(&v_str),
2157        ])
2158        .map(
2159            move |s| {
2160                let ca = s.struct_()?;
2161                let f_h = ca.field_by_name(&h_str)?;
2162                let high = f_h.f64()?;
2163                let f_l = ca.field_by_name(&l_str)?;
2164                let low = f_l.f64()?;
2165                let f_c = ca.field_by_name(&c_str)?;
2166                let close = f_c.f64()?;
2167                let f_v = ca.field_by_name(&v_str)?;
2168                let volume = f_v.f64()?;
2169
2170                let mut indicator = quantwave_core::Obvm::new(obvm_period, signal_period);
2171                let mut obvm_vals = Vec::with_capacity(s.len());
2172                let mut signal_vals = Vec::with_capacity(s.len());
2173
2174                for i in 0..s.len() {
2175                    let h = high.get(i).unwrap_or(f64::NAN);
2176                    let l = low.get(i).unwrap_or(f64::NAN);
2177                    let c = close.get(i).unwrap_or(f64::NAN);
2178                    let v = volume.get(i).unwrap_or(f64::NAN);
2179                    let (o, sig) = indicator.next((h, l, c, v));
2180                    obvm_vals.push(o);
2181                    signal_vals.push(sig);
2182                }
2183
2184                let s_obvm = Series::new("obvm".into(), obvm_vals);
2185                let s_signal = Series::new("signal".into(), signal_vals);
2186                let struct_series = StructChunked::from_series(
2187                    "obvm_data".into(),
2188                    s.len(),
2189                    [s_obvm, s_signal].iter(),
2190                )?;
2191                Ok(Some(Column::from(struct_series.into_series())))
2192            },
2193            GetOutput::from_type(DataType::Struct(vec![
2194                Field::new("obvm".into(), DataType::Float64),
2195                Field::new("signal".into(), DataType::Float64),
2196            ])),
2197        )
2198        .alias("obvm_data")])
2199    }
2200
2201    pub fn vfi(
2202        self,
2203        high: &str,
2204        low: &str,
2205        close: &str,
2206        volume: &str,
2207        period: usize,
2208        coef: f64,
2209        vcoef: f64,
2210        smoothing_period: usize,
2211    ) -> LazyFrame {
2212        let h_str = high.to_string();
2213        let l_str = low.to_string();
2214        let c_str = close.to_string();
2215        let v_str = volume.to_string();
2216
2217        self.0.clone().with_columns([as_struct(vec![
2218            col(&h_str),
2219            col(&l_str),
2220            col(&c_str),
2221            col(&v_str),
2222        ])
2223        .map(
2224            move |s| {
2225                let ca = s.struct_()?;
2226                let f_h = ca.field_by_name(&h_str)?;
2227                let high = f_h.f64()?;
2228                let f_l = ca.field_by_name(&l_str)?;
2229                let low = f_l.f64()?;
2230                let f_c = ca.field_by_name(&c_str)?;
2231                let close = f_c.f64()?;
2232                let f_v = ca.field_by_name(&v_str)?;
2233                let volume = f_v.f64()?;
2234
2235                let mut indicator = quantwave_core::Vfi::new(period, coef, vcoef, smoothing_period);
2236                let mut values = Vec::with_capacity(s.len());
2237
2238                for i in 0..s.len() {
2239                    let h = high.get(i).unwrap_or(f64::NAN);
2240                    let l = low.get(i).unwrap_or(f64::NAN);
2241                    let c = close.get(i).unwrap_or(f64::NAN);
2242                    let v = volume.get(i).unwrap_or(f64::NAN);
2243                    values.push(indicator.next((h, l, c, v)));
2244                }
2245
2246                Ok(Some(Column::from(Series::new("vfi".into(), values))))
2247            },
2248            GetOutput::from_type(DataType::Float64),
2249        )
2250        .alias("vfi")])
2251    }
2252
2253    pub fn sve_volatility_bands(
2254        self,
2255        high: &str,
2256        low: &str,
2257        close: &str,
2258        bands_period: usize,
2259        bands_deviation: f64,
2260        low_band_adjust: f64,
2261        mid_line_length: usize,
2262    ) -> LazyFrame {
2263        let h_str = high.to_string();
2264        let l_str = low.to_string();
2265        let c_str = close.to_string();
2266
2267        self.0.clone().with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2268            .map(
2269                move |s| {
2270                    let ca = s.struct_()?;
2271                    let f_h = ca.field_by_name(&h_str)?;
2272                    let high = f_h.f64()?;
2273                    let f_l = ca.field_by_name(&l_str)?;
2274                    let low = f_l.f64()?;
2275                    let f_c = ca.field_by_name(&c_str)?;
2276                    let close = f_c.f64()?;
2277
2278                    let mut indicator = quantwave_core::SVEVolatilityBands::new(
2279                        bands_period,
2280                        bands_deviation,
2281                        low_band_adjust,
2282                        mid_line_length,
2283                    );
2284                    let mut upper_vals = Vec::with_capacity(s.len());
2285                    let mut mid_vals = Vec::with_capacity(s.len());
2286                    let mut lower_vals = Vec::with_capacity(s.len());
2287
2288                    for i in 0..s.len() {
2289                        let h = high.get(i).unwrap_or(f64::NAN);
2290                        let l = low.get(i).unwrap_or(f64::NAN);
2291                        let c = close.get(i).unwrap_or(f64::NAN);
2292                        let (upper, mid, lower) = indicator.next((h, l, c));
2293                        upper_vals.push(upper);
2294                        mid_vals.push(mid);
2295                        lower_vals.push(lower);
2296                    }
2297
2298                    let s_upper = Series::new("upper".into(), upper_vals);
2299                    let s_mid = Series::new("middle".into(), mid_vals);
2300                    let s_lower = Series::new("lower".into(), lower_vals);
2301                    let struct_series = StructChunked::from_series(
2302                        "sve_bands_data".into(),
2303                        s.len(),
2304                        [s_upper, s_mid, s_lower].iter(),
2305                    )?;
2306                    Ok(Some(Column::from(struct_series.into_series())))
2307                },
2308                GetOutput::from_type(DataType::Struct(vec![
2309                    Field::new("upper".into(), DataType::Float64),
2310                    Field::new("middle".into(), DataType::Float64),
2311                    Field::new("lower".into(), DataType::Float64),
2312                ])),
2313            )
2314            .alias("sve_bands_data")])
2315    }
2316
2317    pub fn exp_dev_bands(
2318        self,
2319        name: &str,
2320        period: usize,
2321        multiplier: f64,
2322        use_sma: bool,
2323    ) -> LazyFrame {
2324        let name_str = name.to_string();
2325        self.0.clone().with_columns([col(&name_str)
2326            .map(
2327                move |s| {
2328                    let ca = s.f64()?;
2329                    let mut indicator = quantwave_core::ExpDevBands::new(period, multiplier, use_sma);
2330                    let mut upper_vals = Vec::with_capacity(s.len());
2331                    let mut mid_vals = Vec::with_capacity(s.len());
2332                    let mut lower_vals = Vec::with_capacity(s.len());
2333
2334                    for i in 0..s.len() {
2335                        let val = ca.get(i).unwrap_or(f64::NAN);
2336                        let (upper, mid, lower) = indicator.next(val);
2337                        upper_vals.push(upper);
2338                        mid_vals.push(mid);
2339                        lower_vals.push(lower);
2340                    }
2341
2342                    let s_upper = Series::new("upper".into(), upper_vals);
2343                    let s_mid = Series::new("middle".into(), mid_vals);
2344                    let s_lower = Series::new("lower".into(), lower_vals);
2345                    let struct_series = StructChunked::from_series(
2346                        "exp_dev_bands_data".into(),
2347                        s.len(),
2348                        [s_upper, s_mid, s_lower].iter(),
2349                    )?;
2350                    Ok(Some(Column::from(struct_series.into_series())))
2351                },
2352                GetOutput::from_type(DataType::Struct(vec![
2353                    Field::new("upper".into(), DataType::Float64),
2354                    Field::new("middle".into(), DataType::Float64),
2355                    Field::new("lower".into(), DataType::Float64),
2356                ])),
2357            )
2358            .alias("exp_dev_bands_data")])
2359    }
2360
2361    pub fn sdo(
2362        self,
2363        name: &str,
2364        lookback_period: usize,
2365        period: usize,
2366        ema_pds: usize,
2367    ) -> LazyFrame {
2368        let name_str = name.to_string();
2369        self.0.clone().with_columns([col(&name_str)
2370            .map(
2371                move |s| {
2372                    let ca = s.f64()?;
2373                    let mut indicator = quantwave_core::SDO::new(lookback_period, period, ema_pds);
2374                    let mut values = Vec::with_capacity(s.len());
2375
2376                    for i in 0..s.len() {
2377                        let val = ca.get(i).unwrap_or(f64::NAN);
2378                        values.push(indicator.next(val));
2379                    }
2380
2381                    Ok(Some(Column::from(Series::new("sdo".into(), values))))
2382                },
2383                GetOutput::from_type(DataType::Float64),
2384            )
2385            .alias("sdo")])
2386    }
2387
2388    pub fn rsmk(self, price: &str, benchmark: &str, length: usize, ema_length: usize) -> LazyFrame {
2389        let p_str = price.to_string();
2390        let b_str = benchmark.to_string();
2391
2392        self.0.clone().with_columns([as_struct(vec![col(&p_str), col(&b_str)])
2393            .map(
2394                move |s| {
2395                    let ca = s.struct_()?;
2396                    let f_p = ca.field_by_name(&p_str)?;
2397                    let price = f_p.f64()?;
2398                    let f_b = ca.field_by_name(&b_str)?;
2399                    let benchmark = f_b.f64()?;
2400
2401                    let mut indicator = quantwave_core::RSMK::new(length, ema_length);
2402                    let mut values = Vec::with_capacity(s.len());
2403
2404                    for i in 0..s.len() {
2405                        let p = price.get(i).unwrap_or(f64::NAN);
2406                        let b = benchmark.get(i).unwrap_or(f64::NAN);
2407                        values.push(indicator.next((p, b)));
2408                    }
2409
2410                    Ok(Some(Column::from(Series::new("rsmk".into(), values))))
2411                },
2412                GetOutput::from_type(DataType::Float64),
2413            )
2414            .alias("rsmk")])
2415    }
2416
2417    pub fn rodc(
2418        self,
2419        name: &str,
2420        window_size: usize,
2421        threshold: f64,
2422        smooth_period: usize,
2423    ) -> LazyFrame {
2424        let name_str = name.to_string();
2425        self.0.clone().with_columns([col(&name_str)
2426            .map(
2427                move |s| {
2428                    let ca = s.f64()?;
2429                    let mut indicator = quantwave_core::RODC::new(window_size, threshold, smooth_period);
2430                    let mut values = Vec::with_capacity(s.len());
2431
2432                    for i in 0..s.len() {
2433                        let val = ca.get(i).unwrap_or(f64::NAN);
2434                        values.push(indicator.next(val));
2435                    }
2436
2437                    Ok(Some(Column::from(Series::new("rodc".into(), values))))
2438                },
2439                GetOutput::from_type(DataType::Float64),
2440            )
2441            .alias("rodc")])
2442    }
2443
2444    pub fn reverse_ema(self, name: &str, alpha: f64) -> LazyFrame {
2445        let name_str = name.to_string();
2446        self.0.clone().with_columns([col(&name_str)
2447            .map(
2448                move |s| {
2449                    let ca = s.f64()?;
2450                    let mut indicator = quantwave_core::ReverseEMA::new(alpha);
2451                    let mut values = Vec::with_capacity(s.len());
2452
2453                    for i in 0..s.len() {
2454                        let val = ca.get(i).unwrap_or(f64::NAN);
2455                        values.push(indicator.next(val));
2456                    }
2457
2458                    Ok(Some(Column::from(Series::new("reverse_ema".into(), values))))
2459                },
2460                GetOutput::from_type(DataType::Float64),
2461            )
2462            .alias("reverse_ema")])
2463    }
2464
2465    pub fn harrington_adx(
2466        self,
2467        high: &str,
2468        low: &str,
2469        close: &str,
2470        adx_length: usize,
2471        adx_smooth_length: usize,
2472    ) -> LazyFrame {
2473        let h_str = high.to_string();
2474        let l_str = low.to_string();
2475        let c_str = close.to_string();
2476
2477        self.0.clone().with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
2478            .map(
2479                move |s| {
2480                    let ca = s.struct_()?;
2481                    let f_h = ca.field_by_name(&h_str)?;
2482                    let high = f_h.f64()?;
2483                    let f_l = ca.field_by_name(&l_str)?;
2484                    let low = f_l.f64()?;
2485                    let f_c = ca.field_by_name(&c_str)?;
2486                    let close = f_c.f64()?;
2487
2488                    let mut indicator = quantwave_core::HarringtonADXOscillator::new(adx_length, adx_smooth_length);
2489                    let mut values = Vec::with_capacity(s.len());
2490
2491                    for i in 0..s.len() {
2492                        let h = high.get(i).unwrap_or(f64::NAN);
2493                        let l = low.get(i).unwrap_or(f64::NAN);
2494                        let c = close.get(i).unwrap_or(f64::NAN);
2495                        values.push(indicator.next((h, l, c)));
2496                    }
2497
2498                    Ok(Some(Column::from(Series::new("harrington_adx".into(), values))))
2499                },
2500                GetOutput::from_type(DataType::Float64),
2501            )
2502            .alias("harrington_adx")])
2503    }
2504
2505    pub fn keltner_channels(
2506        self,
2507        high: &str,
2508        low: &str,
2509        close: &str,
2510        ema_period: usize,
2511        atr_period: usize,
2512        multiplier: f64,
2513    ) -> LazyFrame {
2514        let high = high.to_string();
2515        let low = low.to_string();
2516        let close = close.to_string();
2517
2518        self.0
2519            .clone()
2520            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2521                .map(
2522                    move |s| {
2523                        let ca = s.struct_()?;
2524                        let s_high = ca.field_by_name(&high)?;
2525                        let s_low = ca.field_by_name(&low)?;
2526                        let s_close = ca.field_by_name(&close)?;
2527
2528                        let high = s_high.f64()?;
2529                        let low = s_low.f64()?;
2530                        let close = s_close.f64()?;
2531
2532                        let mut kc = quantwave_core::KeltnerChannels::new(
2533                            ema_period, atr_period, multiplier,
2534                        );
2535                        let mut uppers = Vec::with_capacity(s.len());
2536                        let mut middles = Vec::with_capacity(s.len());
2537                        let mut lowers = Vec::with_capacity(s.len());
2538
2539                        for i in 0..s.len() {
2540                            let h = high.get(i).unwrap_or(0.0);
2541                            let l = low.get(i).unwrap_or(0.0);
2542                            let c = close.get(i).unwrap_or(0.0);
2543                            let (upper, middle, lower) = kc.next((h, l, c));
2544                            uppers.push(upper);
2545                            middles.push(middle);
2546                            lowers.push(lower);
2547                        }
2548
2549                        let upper_series = Series::new("upper".into(), uppers);
2550                        let middle_series = Series::new("middle".into(), middles);
2551                        let lower_series = Series::new("lower".into(), lowers);
2552
2553                        let out = StructChunked::from_series(
2554                            "keltner_output".into(),
2555                            s.len(),
2556                            [upper_series, middle_series, lower_series].iter(),
2557                        )?;
2558                        Ok(Some(Column::from(out.into_series())))
2559                    },
2560                    GetOutput::from_type(DataType::Struct(vec![
2561                        Field::new("upper".into(), DataType::Float64),
2562                        Field::new("middle".into(), DataType::Float64),
2563                        Field::new("lower".into(), DataType::Float64),
2564                    ])),
2565                )
2566                .alias("keltner_data")])
2567    }
2568
2569    pub fn alma(self, name: &str, period: usize, offset: f64, sigma: f64) -> LazyFrame {
2570        let name = name.to_string();
2571        self.0.clone().with_columns([col(&name)
2572            .map(
2573                move |s| {
2574                    let ca = s.f64()?;
2575                    let mut alma = quantwave_core::ALMA::new(period, offset, sigma);
2576                    let mut values = Vec::with_capacity(s.len());
2577
2578                    for i in 0..s.len() {
2579                        let val = ca.get(i).unwrap_or(0.0);
2580                        values.push(alma.next(val));
2581                    }
2582
2583                    Ok(Some(Column::from(Series::new("alma".into(), values))))
2584                },
2585                GetOutput::from_type(DataType::Float64),
2586            )
2587            .alias("alma")])
2588    }
2589
2590    pub fn donchian_channels(self, high: &str, low: &str, period: usize) -> LazyFrame {
2591        let high = high.to_string();
2592        let low = low.to_string();
2593
2594        self.0
2595            .clone()
2596            .with_columns([as_struct(vec![col(&high), col(&low)])
2597                .map(
2598                    move |s| {
2599                        let ca = s.struct_()?;
2600                        let s_high = ca.field_by_name(&high)?;
2601                        let s_low = ca.field_by_name(&low)?;
2602
2603                        let high = s_high.f64()?;
2604                        let low = s_low.f64()?;
2605
2606                        let mut dc = quantwave_core::DonchianChannels::new(period);
2607                        let mut uppers = Vec::with_capacity(s.len());
2608                        let mut middles = Vec::with_capacity(s.len());
2609                        let mut lowers = Vec::with_capacity(s.len());
2610
2611                        for i in 0..s.len() {
2612                            let h = high.get(i).unwrap_or(0.0);
2613                            let l = low.get(i).unwrap_or(0.0);
2614                            let (upper, middle, lower) = dc.next((h, l));
2615                            uppers.push(upper);
2616                            middles.push(middle);
2617                            lowers.push(lower);
2618                        }
2619
2620                        let upper_series = Series::new("upper".into(), uppers);
2621                        let middle_series = Series::new("middle".into(), middles);
2622                        let lower_series = Series::new("lower".into(), lowers);
2623
2624                        let out = StructChunked::from_series(
2625                            "donchian_output".into(),
2626                            s.len(),
2627                            [upper_series, middle_series, lower_series].iter(),
2628                        )?;
2629                        Ok(Some(Column::from(out.into_series())))
2630                    },
2631                    GetOutput::from_type(DataType::Struct(vec![
2632                        Field::new("upper".into(), DataType::Float64),
2633                        Field::new("middle".into(), DataType::Float64),
2634                        Field::new("lower".into(), DataType::Float64),
2635                    ])),
2636                )
2637                .alias("donchian_data")])
2638    }
2639
2640    pub fn ttm_squeeze(
2641        self,
2642        high: &str,
2643        low: &str,
2644        close: &str,
2645        period: usize,
2646        multiplier_bb: f64,
2647        multiplier_kc: f64,
2648    ) -> LazyFrame {
2649        let high = high.to_string();
2650        let low = low.to_string();
2651        let close = close.to_string();
2652
2653        self.0
2654            .clone()
2655            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2656                .map(
2657                    move |s| {
2658                        let ca = s.struct_()?;
2659                        let s_high = ca.field_by_name(&high)?;
2660                        let s_low = ca.field_by_name(&low)?;
2661                        let s_close = ca.field_by_name(&close)?;
2662
2663                        let high = s_high.f64()?;
2664                        let low = s_low.f64()?;
2665                        let close = s_close.f64()?;
2666
2667                        let mut ttm =
2668                            quantwave_core::TTMSqueeze::new(period, multiplier_bb, multiplier_kc);
2669                        let mut histograms = Vec::with_capacity(s.len());
2670                        let mut squeezed = Vec::with_capacity(s.len());
2671
2672                        for i in 0..s.len() {
2673                            let h = high.get(i).unwrap_or(0.0);
2674                            let l = low.get(i).unwrap_or(0.0);
2675                            let c = close.get(i).unwrap_or(0.0);
2676                            let (hist, is_sq) = ttm.next((h, l, c));
2677                            histograms.push(hist);
2678                            squeezed.push(is_sq);
2679                        }
2680
2681                        let hist_series = Series::new("histogram".into(), histograms);
2682                        let squeezed_series = Series::new("is_squeezed".into(), squeezed);
2683
2684                        let out = StructChunked::from_series(
2685                            "ttm_squeeze_output".into(),
2686                            s.len(),
2687                            [hist_series, squeezed_series].iter(),
2688                        )?;
2689                        Ok(Some(Column::from(out.into_series())))
2690                    },
2691                    GetOutput::from_type(DataType::Struct(vec![
2692                        Field::new("histogram".into(), DataType::Float64),
2693                        Field::new("is_squeezed".into(), DataType::Boolean),
2694                    ])),
2695                )
2696                .alias("ttm_squeeze_data")])
2697    }
2698
2699    pub fn vortex_indicator(self, high: &str, low: &str, close: &str, period: usize) -> LazyFrame {
2700        let high = high.to_string();
2701        let low = low.to_string();
2702        let close = close.to_string();
2703
2704        self.0
2705            .clone()
2706            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2707                .map(
2708                    move |s| {
2709                        let ca = s.struct_()?;
2710                        let s_high = ca.field_by_name(&high)?;
2711                        let s_low = ca.field_by_name(&low)?;
2712                        let s_close = ca.field_by_name(&close)?;
2713
2714                        let high = s_high.f64()?;
2715                        let low = s_low.f64()?;
2716                        let close = s_close.f64()?;
2717
2718                        let mut vi = quantwave_core::VortexIndicator::new(period);
2719                        let mut plus_vals = Vec::with_capacity(s.len());
2720                        let mut minus_vals = Vec::with_capacity(s.len());
2721
2722                        for i in 0..s.len() {
2723                            let h = high.get(i).unwrap_or(0.0);
2724                            let l = low.get(i).unwrap_or(0.0);
2725                            let c = close.get(i).unwrap_or(0.0);
2726                            let (plus, minus) = vi.next((h, l, c));
2727                            plus_vals.push(plus);
2728                            minus_vals.push(minus);
2729                        }
2730
2731                        let plus_series = Series::new("vi_plus".into(), plus_vals);
2732                        let minus_series = Series::new("vi_minus".into(), minus_vals);
2733
2734                        let out = StructChunked::from_series(
2735                            "vortex_output".into(),
2736                            s.len(),
2737                            [plus_series, minus_series].iter(),
2738                        )?;
2739                        Ok(Some(Column::from(out.into_series())))
2740                    },
2741                    GetOutput::from_type(DataType::Struct(vec![
2742                        Field::new("vi_plus".into(), DataType::Float64),
2743                        Field::new("vi_minus".into(), DataType::Float64),
2744                    ])),
2745                )
2746                .alias("vortex_data")])
2747    }
2748
2749    pub fn heikin_ashi(self, open: &str, high: &str, low: &str, close: &str) -> LazyFrame {
2750        let open = open.to_string();
2751        let high = high.to_string();
2752        let low = low.to_string();
2753        let close = close.to_string();
2754
2755        self.0.clone().with_columns([as_struct(vec![
2756            col(&open),
2757            col(&high),
2758            col(&low),
2759            col(&close),
2760        ])
2761        .map(
2762            move |s| {
2763                let ca = s.struct_()?;
2764                let s_open = ca.field_by_name(&open)?;
2765                let s_high = ca.field_by_name(&high)?;
2766                let s_low = ca.field_by_name(&low)?;
2767                let s_close = ca.field_by_name(&close)?;
2768
2769                let open = s_open.f64()?;
2770                let high = s_high.f64()?;
2771                let low = s_low.f64()?;
2772                let close = s_close.f64()?;
2773
2774                let mut ha = quantwave_core::HeikinAshi::new();
2775                let mut ha_opens = Vec::with_capacity(s.len());
2776                let mut ha_highs = Vec::with_capacity(s.len());
2777                let mut ha_lows = Vec::with_capacity(s.len());
2778                let mut ha_closes = Vec::with_capacity(s.len());
2779
2780                for i in 0..s.len() {
2781                    let o = open.get(i).unwrap_or(0.0);
2782                    let h = high.get(i).unwrap_or(0.0);
2783                    let l = low.get(i).unwrap_or(0.0);
2784                    let c = close.get(i).unwrap_or(0.0);
2785                    let (ha_o, ha_h, ha_l, ha_c) = ha.next((o, h, l, c));
2786                    ha_opens.push(ha_o);
2787                    ha_highs.push(ha_h);
2788                    ha_lows.push(ha_l);
2789                    ha_closes.push(ha_c);
2790                }
2791
2792                let o_series = Series::new("ha_open".into(), ha_opens);
2793                let h_series = Series::new("ha_high".into(), ha_highs);
2794                let l_series = Series::new("ha_low".into(), ha_lows);
2795                let c_series = Series::new("ha_close".into(), ha_closes);
2796
2797                let out = StructChunked::from_series(
2798                    "heikin_ashi_output".into(),
2799                    s.len(),
2800                    [o_series, h_series, l_series, c_series].iter(),
2801                )?;
2802                Ok(Some(Column::from(out.into_series())))
2803            },
2804            GetOutput::from_type(DataType::Struct(vec![
2805                Field::new("ha_open".into(), DataType::Float64),
2806                Field::new("ha_high".into(), DataType::Float64),
2807                Field::new("ha_low".into(), DataType::Float64),
2808                Field::new("ha_close".into(), DataType::Float64),
2809            ])),
2810        )
2811        .alias("heikin_ashi_data")])
2812    }
2813
2814    pub fn wavetrend(
2815        self,
2816        high: &str,
2817        low: &str,
2818        close: &str,
2819        n1: usize,
2820        n2: usize,
2821        n3: usize,
2822    ) -> LazyFrame {
2823        let high = high.to_string();
2824        let low = low.to_string();
2825        let close = close.to_string();
2826
2827        self.0
2828            .clone()
2829            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2830                .map(
2831                    move |s| {
2832                        let ca = s.struct_()?;
2833                        let s_high = ca.field_by_name(&high)?;
2834                        let s_low = ca.field_by_name(&low)?;
2835                        let s_close = ca.field_by_name(&close)?;
2836
2837                        let high = s_high.f64()?;
2838                        let low = s_low.f64()?;
2839                        let close = s_close.f64()?;
2840
2841                        let mut wt = quantwave_core::WaveTrend::new(n1, n2, n3);
2842                        let mut wt1_vals = Vec::with_capacity(s.len());
2843                        let mut wt2_vals = Vec::with_capacity(s.len());
2844
2845                        for i in 0..s.len() {
2846                            let h = high.get(i).unwrap_or(0.0);
2847                            let l = low.get(i).unwrap_or(0.0);
2848                            let c = close.get(i).unwrap_or(0.0);
2849                            let (wt1, wt2) = wt.next((h, l, c));
2850                            wt1_vals.push(wt1);
2851                            wt2_vals.push(wt2);
2852                        }
2853
2854                        let wt1_series = Series::new("wt1".into(), wt1_vals);
2855                        let wt2_series = Series::new("wt2".into(), wt2_vals);
2856
2857                        let out = StructChunked::from_series(
2858                            "wavetrend_output".into(),
2859                            s.len(),
2860                            [wt1_series, wt2_series].iter(),
2861                        )?;
2862                        Ok(Some(Column::from(out.into_series())))
2863                    },
2864                    GetOutput::from_type(DataType::Struct(vec![
2865                        Field::new("wt1".into(), DataType::Float64),
2866                        Field::new("wt2".into(), DataType::Float64),
2867                    ])),
2868                )
2869                .alias("wavetrend_data")])
2870    }
2871
2872    pub fn tema(self, name: &str, period: usize) -> LazyFrame {
2873        let name = name.to_string();
2874        self.0.clone().with_columns([col(&name)
2875            .map(
2876                move |s| {
2877                    let ca = s.f64()?;
2878                    let mut tema = quantwave_core::TEMA::new(period);
2879                    let mut values = Vec::with_capacity(s.len());
2880
2881                    for i in 0..s.len() {
2882                        let val = ca.get(i).unwrap_or(0.0);
2883                        values.push(tema.next(val));
2884                    }
2885
2886                    Ok(Some(Column::from(Series::new("tema".into(), values))))
2887                },
2888                GetOutput::from_type(DataType::Float64),
2889            )
2890            .alias("tema")])
2891    }
2892
2893    pub fn zlema(self, name: &str, period: usize) -> LazyFrame {
2894        let name = name.to_string();
2895        self.0.clone().with_columns([col(&name)
2896            .map(
2897                move |s| {
2898                    let ca = s.f64()?;
2899                    let mut zlema = quantwave_core::ZLEMA::new(period);
2900                    let mut values = Vec::with_capacity(s.len());
2901
2902                    for i in 0..s.len() {
2903                        let val = ca.get(i).unwrap_or(0.0);
2904                        values.push(zlema.next(val));
2905                    }
2906
2907                    Ok(Some(Column::from(Series::new("zlema".into(), values))))
2908                },
2909                GetOutput::from_type(DataType::Float64),
2910            )
2911            .alias("zlema")])
2912    }
2913
2914    pub fn atr_trailing_stop(
2915        self,
2916        high: &str,
2917        low: &str,
2918        close: &str,
2919        period: usize,
2920        multiplier: f64,
2921    ) -> LazyFrame {
2922        let high = high.to_string();
2923        let low = low.to_string();
2924        let close = close.to_string();
2925
2926        self.0
2927            .clone()
2928            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2929                .map(
2930                    move |s| {
2931                        let ca = s.struct_()?;
2932                        let s_high = ca.field_by_name(&high)?;
2933                        let s_low = ca.field_by_name(&low)?;
2934                        let s_close = ca.field_by_name(&close)?;
2935
2936                        let high = s_high.f64()?;
2937                        let low = s_low.f64()?;
2938                        let close = s_close.f64()?;
2939
2940                        let mut atr_ts = quantwave_core::ATRTrailingStop::new(period, multiplier);
2941                        let mut stops = Vec::with_capacity(s.len());
2942                        let mut directions = Vec::with_capacity(s.len());
2943
2944                        for i in 0..s.len() {
2945                            let h = high.get(i).unwrap_or(0.0);
2946                            let l = low.get(i).unwrap_or(0.0);
2947                            let c = close.get(i).unwrap_or(0.0);
2948                            let (stop, dir) = atr_ts.next((h, l, c));
2949                            stops.push(stop);
2950                            directions.push(dir as f64);
2951                        }
2952
2953                        let stop_series = Series::new("stop".into(), stops);
2954                        let dir_series = Series::new("direction".into(), directions);
2955
2956                        let out = StructChunked::from_series(
2957                            "atr_ts_output".into(),
2958                            s.len(),
2959                            [stop_series, dir_series].iter(),
2960                        )?;
2961                        Ok(Some(Column::from(out.into_series())))
2962                    },
2963                    GetOutput::from_type(DataType::Struct(vec![
2964                        Field::new("stop".into(), DataType::Float64),
2965                        Field::new("direction".into(), DataType::Float64),
2966                    ])),
2967                )
2968                .alias("atr_ts_data")])
2969    }
2970
2971    pub fn pivot_points(self, high: &str, low: &str, close: &str) -> LazyFrame {
2972        let high = high.to_string();
2973        let low = low.to_string();
2974        let close = close.to_string();
2975
2976        self.0
2977            .clone()
2978            .with_columns([as_struct(vec![col(&high), col(&low), col(&close)])
2979                .map(
2980                    move |s| {
2981                        let ca = s.struct_()?;
2982                        let s_high = ca.field_by_name(&high)?;
2983                        let s_low = ca.field_by_name(&low)?;
2984                        let s_close = ca.field_by_name(&close)?;
2985
2986                        let high = s_high.f64()?;
2987                        let low = s_low.f64()?;
2988                        let close = s_close.f64()?;
2989
2990                        let mut pivot = quantwave_core::PivotPoints::new();
2991                        let mut p_vals = Vec::with_capacity(s.len());
2992                        let mut r1_vals = Vec::with_capacity(s.len());
2993                        let mut s1_vals = Vec::with_capacity(s.len());
2994                        let mut r2_vals = Vec::with_capacity(s.len());
2995                        let mut s2_vals = Vec::with_capacity(s.len());
2996
2997                        for i in 0..s.len() {
2998                            let h = high.get(i).unwrap_or(0.0);
2999                            let l = low.get(i).unwrap_or(0.0);
3000                            let c = close.get(i).unwrap_or(0.0);
3001                            let (p, r1, s1, r2, s2) = pivot.next((h, l, c));
3002                            p_vals.push(p);
3003                            r1_vals.push(r1);
3004                            s1_vals.push(s1);
3005                            r2_vals.push(r2);
3006                            s2_vals.push(s2);
3007                        }
3008
3009                        let p_series = Series::new("p".into(), p_vals);
3010                        let r1_series = Series::new("r1".into(), r1_vals);
3011                        let s1_series = Series::new("s1".into(), s1_vals);
3012                        let r2_series = Series::new("r2".into(), r2_vals);
3013                        let s2_series = Series::new("s2".into(), s2_vals);
3014
3015                        let out = StructChunked::from_series(
3016                            "pivot_output".into(),
3017                            s.len(),
3018                            [p_series, r1_series, s1_series, r2_series, s2_series].iter(),
3019                        )?;
3020                        Ok(Some(Column::from(out.into_series())))
3021                    },
3022                    GetOutput::from_type(DataType::Struct(vec![
3023                        Field::new("p".into(), DataType::Float64),
3024                        Field::new("r1".into(), DataType::Float64),
3025                        Field::new("s1".into(), DataType::Float64),
3026                        Field::new("r2".into(), DataType::Float64),
3027                        Field::new("s2".into(), DataType::Float64),
3028                    ])),
3029                )
3030                .alias("pivot_points_data")])
3031    }
3032
3033    pub fn bill_williams_fractals(self, high: &str, low: &str) -> LazyFrame {
3034        let high = high.to_string();
3035        let low = low.to_string();
3036
3037        self.0
3038            .clone()
3039            .with_columns([as_struct(vec![col(&high), col(&low)])
3040                .map(
3041                    move |s| {
3042                        let ca = s.struct_()?;
3043                        let s_high = ca.field_by_name(&high)?;
3044                        let s_low = ca.field_by_name(&low)?;
3045
3046                        let high = s_high.f64()?;
3047                        let low = s_low.f64()?;
3048
3049                        let mut fractals = quantwave_core::BillWilliamsFractals::new();
3050                        let mut bearish_vals = Vec::with_capacity(s.len());
3051                        let mut bullish_vals = Vec::with_capacity(s.len());
3052
3053                        for i in 0..s.len() {
3054                            let h = high.get(i).unwrap_or(0.0);
3055                            let l = low.get(i).unwrap_or(0.0);
3056                            let (bear, bull) = fractals.next((h, l));
3057                            bearish_vals.push(bear);
3058                            bullish_vals.push(bull);
3059                        }
3060
3061                        let bearish_series = Series::new("bearish".into(), bearish_vals);
3062                        let bullish_series = Series::new("bullish".into(), bullish_vals);
3063
3064                        let out = StructChunked::from_series(
3065                            "fractals_output".into(),
3066                            s.len(),
3067                            [bearish_series, bullish_series].iter(),
3068                        )?;
3069                        Ok(Some(Column::from(out.into_series())))
3070                    },
3071                    GetOutput::from_type(DataType::Struct(vec![
3072                        Field::new("bearish".into(), DataType::Boolean),
3073                        Field::new("bullish".into(), DataType::Boolean),
3074                    ])),
3075                )
3076                .alias("fractals_data")])
3077    }
3078
3079    /// Market Structure (swings + confirmed BOS flips) Polars accessor.
3080    /// Returns a Struct column "market_structure" with rich per-bar state + flip metadata:
3081    ///   bias (0=Neutral,1=Bullish,2=Bearish), last_*_price/bar (0/NaN if none),
3082    ///   has_flip + flip_* fields (only meaningful when has_flip=true — these are the events),
3083    ///   swing_depth, bar_index.
3084    ///
3085    /// This directly emits the foundation for the standardized PAEvent system:
3086    /// - Use core `extract_pa_events(&state)` (or Python equivalent on the struct fields) to obtain
3087    ///   typed `PAEvent` / `PAEventKind::MarketStructureFlip` carrying strength, confidence=1.0, bar etc.
3088    /// - Filter/explode for events: `.filter(col("market_structure").struct_().field_by_name("has_flip"))`.
3089    /// - Rich meta (structure_strength etc) drives backtester sizing/attribution
3090    ///   and ML confluence (feature_values/regime_at_event slots filled by join).
3091    ///
3092    /// Delegates to quantwave_core::MarketStructure (Next<(f64,f64)> -> MarketStructureState + PAEvent adapters).
3093    /// Primary Polars surface for Part 21 PA foundation + rich event standardization.
3094    ///
3095    /// Matches project patterns (see fractals, supertrend, features.rs cyber_cycle).
3096    ///
3097    /// Sources: market_structure.rs (MQL5 Part 21 https://www.mql5.com/en/articles/17891 + 66/69 lessons).
3098    pub fn market_structure(
3099        self,
3100        high: &str,
3101        low: &str,
3102        swing_strength: usize,
3103    ) -> LazyFrame {
3104        let high_str = high.to_string();
3105        let low_str = low.to_string();
3106        let strength = swing_strength;
3107
3108        self.0.clone().with_columns([as_struct(vec![col(&high_str), col(&low_str)])
3109            .map(
3110                move |s| {
3111                    let ca = s.struct_()?;
3112                    let s_h = ca.field_by_name(&high_str)?;
3113                    let s_l = ca.field_by_name(&low_str)?;
3114                    let highs = s_h.f64()?;
3115                    let lows = s_l.f64()?;
3116
3117                    let mut ms = quantwave_core::MarketStructure::new(strength);
3118                    let n = s.len();
3119
3120                    let mut bias_vals: Vec<u32> = Vec::with_capacity(n);
3121                    let mut lh_p: Vec<f64> = Vec::with_capacity(n);
3122                    let mut lh_b: Vec<u64> = Vec::with_capacity(n);
3123                    let mut ll_p: Vec<f64> = Vec::with_capacity(n);
3124                    let mut ll_b: Vec<u64> = Vec::with_capacity(n);
3125                    let mut has_f: Vec<bool> = Vec::with_capacity(n);
3126                    let mut f_bear: Vec<bool> = Vec::with_capacity(n);
3127                    let mut f_p: Vec<f64> = Vec::with_capacity(n);
3128                    let mut f_ba: Vec<u64> = Vec::with_capacity(n);
3129                    let mut f_str: Vec<u32> = Vec::with_capacity(n);
3130                    let mut depths: Vec<u32> = Vec::with_capacity(n);
3131                    let mut bars: Vec<u64> = Vec::with_capacity(n);
3132
3133                    for i in 0..n {
3134                        let h = highs.get(i).unwrap_or(f64::NAN);
3135                        let l = lows.get(i).unwrap_or(f64::NAN);
3136                        // Guard ordering
3137                        let hh = if h.is_nan() || l.is_nan() { f64::NAN } else { h.max(l) };
3138                        let ll = if h.is_nan() || l.is_nan() { f64::NAN } else { l.min(h) };
3139                        let state = ms.next((hh, ll));
3140
3141                        let b = match state.bias {
3142                            quantwave_core::Bias::Neutral => 0u32,
3143                            quantwave_core::Bias::Bullish => 1,
3144                            quantwave_core::Bias::Bearish => 2,
3145                        };
3146                        bias_vals.push(b);
3147
3148                        match &state.last_swing_high {
3149                            Some(sh) => { lh_p.push(sh.price); lh_b.push(sh.bar as u64); }
3150                            None => { lh_p.push(f64::NAN); lh_b.push(0); }
3151                        }
3152                        match &state.last_swing_low {
3153                            Some(sl) => { ll_p.push(sl.price); ll_b.push(sl.bar as u64); }
3154                            None => { ll_p.push(f64::NAN); ll_b.push(0); }
3155                        }
3156
3157                        if let Some(f) = &state.current_flip {
3158                            has_f.push(true);
3159                            f_bear.push(f.is_bearish);
3160                            f_p.push(f.price);
3161                            f_ba.push(f.bar as u64);
3162                            f_str.push(f.structure_strength);
3163                        } else {
3164                            has_f.push(false);
3165                            f_bear.push(false);
3166                            f_p.push(f64::NAN);
3167                            f_ba.push(0);
3168                            f_str.push(0);
3169                        }
3170
3171                        depths.push(state.swing_depth_used as u32);
3172                        bars.push(state.bar_index as u64);
3173                    }
3174
3175                    let s_bias = Series::new("bias".into(), bias_vals);
3176                    let s_lhp = Series::new("last_high_price".into(), lh_p);
3177                    let s_lhb = Series::new("last_high_bar".into(), lh_b);
3178                    let s_llp = Series::new("last_low_price".into(), ll_p);
3179                    let s_llb = Series::new("last_low_bar".into(), ll_b);
3180                    let s_hasf = Series::new("has_flip".into(), has_f);
3181                    let s_fb = Series::new("flip_bearish".into(), f_bear);
3182                    let s_fp = Series::new("flip_price".into(), f_p);
3183                    let s_fba = Series::new("flip_bar".into(), f_ba);
3184                    let s_fstr = Series::new("flip_strength".into(), f_str);
3185                    let s_dep = Series::new("swing_depth".into(), depths);
3186                    let s_bar = Series::new("bar_index".into(), bars);
3187
3188                    let struct_series = StructChunked::from_series(
3189                        "market_structure_result".into(),
3190                        s.len(),
3191                        [
3192                            s_bias, s_lhp, s_lhb, s_llp, s_llb, s_hasf, s_fb, s_fp, s_fba, s_fstr,
3193                            s_dep, s_bar,
3194                        ]
3195                        .iter(),
3196                    )?;
3197                    Ok(Some(Column::from(struct_series.into_series())))
3198                },
3199                GetOutput::from_type(DataType::Struct(vec![
3200                    Field::new("bias".into(), DataType::UInt32),
3201                    Field::new("last_high_price".into(), DataType::Float64),
3202                    Field::new("last_high_bar".into(), DataType::UInt64),
3203                    Field::new("last_low_price".into(), DataType::Float64),
3204                    Field::new("last_low_bar".into(), DataType::UInt64),
3205                    Field::new("has_flip".into(), DataType::Boolean),
3206                    Field::new("flip_bearish".into(), DataType::Boolean),
3207                    Field::new("flip_price".into(), DataType::Float64),
3208                    Field::new("flip_bar".into(), DataType::UInt64),
3209                    Field::new("flip_strength".into(), DataType::UInt32),
3210                    Field::new("swing_depth".into(), DataType::UInt32),
3211                    Field::new("bar_index".into(), DataType::UInt64),
3212                ])),
3213            )
3214            .alias("market_structure")])
3215
3216    }
3217
3218    /// Geometric Pattern Scanner (Flags + H&S) Polars accessor, built on the MarketStructure foundation.
3219    /// Returns a Struct column "geometric_patterns" containing:
3220    ///   flag: Struct(id, is_bull, pole_length, pole_length_atr, breakout_confirmed, breakout_price)
3221    ///   hs:   Struct(id, is_bearish, height, height_atr, score, breakout_confirmed)
3222    /// (id==0 means no detection on that bar).
3223    ///
3224    /// Delegates to quantwave_core::GeometricPatternScanner (Part 69 flag + Part 66 H&S rules).
3225    /// Emits rich metadata (`pole_length_atr`, `score`, `breakout_confirmed`) for sizing and filters.
3226    pub fn geometric_patterns(
3227        self,
3228        high: &str,
3229        low: &str,
3230        swing_strength: usize,
3231    ) -> LazyFrame {
3232        let high_str = high.to_string();
3233        let low_str = low.to_string();
3234        let strength = swing_strength;
3235
3236        self.0.clone().with_columns([as_struct(vec![col(&high_str), col(&low_str)])
3237            .map(
3238                move |s| {
3239                    let ca = s.struct_()?;
3240                    let s_h = ca.field_by_name(&high_str)?;
3241                    let s_l = ca.field_by_name(&low_str)?;
3242                    let highs = s_h.f64()?;
3243                    let lows = s_l.f64()?;
3244
3245                    let mut scanner = quantwave_core::GeometricPatternScanner::new(strength);
3246                    let n = s.len();
3247
3248                    let mut flag_ids: Vec<u32> = Vec::with_capacity(n);
3249                    let mut flag_is_bull: Vec<bool> = Vec::with_capacity(n);
3250                    let mut flag_pole_len: Vec<f64> = Vec::with_capacity(n);
3251                    let mut flag_pole_atr: Vec<f64> = Vec::with_capacity(n);
3252                    let mut flag_breakout: Vec<bool> = Vec::with_capacity(n);
3253                    let mut flag_bp: Vec<f64> = Vec::with_capacity(n);
3254
3255                    let mut hs_ids: Vec<u32> = Vec::with_capacity(n);
3256                    let mut hs_bear: Vec<bool> = Vec::with_capacity(n);
3257                    let mut hs_height: Vec<f64> = Vec::with_capacity(n);
3258                    let mut hs_height_atr: Vec<f64> = Vec::with_capacity(n);
3259                    let mut hs_score: Vec<f64> = Vec::with_capacity(n);
3260                    let mut hs_breakout: Vec<bool> = Vec::with_capacity(n);
3261
3262                    for i in 0..n {
3263                        let h = highs.get(i).unwrap_or(f64::NAN);
3264                        let l = lows.get(i).unwrap_or(f64::NAN);
3265                        let hh = if h.is_nan() || l.is_nan() { f64::NAN } else { h.max(l) };
3266                        let ll = if h.is_nan() || l.is_nan() { f64::NAN } else { l.min(h) };
3267                        let (_state, flag, hs) = scanner.next((hh, ll));
3268
3269                        if let Some(f) = flag {
3270                            flag_ids.push(f.id);
3271                            flag_is_bull.push(f.is_bull);
3272                            flag_pole_len.push(f.pole_length);
3273                            flag_pole_atr.push(f.pole_length_atr);
3274                            flag_breakout.push(f.breakout_confirmed);
3275                            flag_bp.push(f.breakout_price);
3276                        } else {
3277                            flag_ids.push(0);
3278                            flag_is_bull.push(false);
3279                            flag_pole_len.push(f64::NAN);
3280                            flag_pole_atr.push(f64::NAN);
3281                            flag_breakout.push(false);
3282                            flag_bp.push(f64::NAN);
3283                        }
3284
3285                        if let Some(hp) = hs {
3286                            hs_ids.push(hp.id);
3287                            hs_bear.push(hp.is_bearish);
3288                            hs_height.push(hp.height);
3289                            hs_height_atr.push(hp.height_atr);
3290                            hs_score.push(hp.score);
3291                            hs_breakout.push(hp.breakout_confirmed);
3292                        } else {
3293                            hs_ids.push(0);
3294                            hs_bear.push(false);
3295                            hs_height.push(f64::NAN);
3296                            hs_height_atr.push(f64::NAN);
3297                            hs_score.push(f64::NAN);
3298                            hs_breakout.push(false);
3299                        }
3300                    }
3301
3302                    let s_fid = Series::new("id".into(), flag_ids);
3303                    let s_fbull = Series::new("is_bull".into(), flag_is_bull);
3304                    let s_fplen = Series::new("pole_length".into(), flag_pole_len);
3305                    let s_fpatr = Series::new("pole_length_atr".into(), flag_pole_atr);
3306                    let s_fbo = Series::new("breakout_confirmed".into(), flag_breakout);
3307                    let s_fbp = Series::new("breakout_price".into(), flag_bp);
3308
3309                    let flag_struct = StructChunked::from_series(
3310                        "flag".into(),
3311                        n,
3312                        [s_fid, s_fbull, s_fplen, s_fpatr, s_fbo, s_fbp].iter(),
3313                    )?;
3314
3315                    let s_hid = Series::new("id".into(), hs_ids);
3316                    let s_hbear = Series::new("is_bearish".into(), hs_bear);
3317                    let s_hh = Series::new("height".into(), hs_height);
3318                    let s_hhatr = Series::new("height_atr".into(), hs_height_atr);
3319                    let s_hsc = Series::new("score".into(), hs_score);
3320                    let s_hbo = Series::new("breakout_confirmed".into(), hs_breakout);
3321
3322                    let hs_struct = StructChunked::from_series(
3323                        "hs".into(),
3324                        n,
3325                        [s_hid, s_hbear, s_hh, s_hhatr, s_hsc, s_hbo].iter(),
3326                    )?;
3327
3328                    let combined = StructChunked::from_series(
3329                        "geo_patterns".into(),
3330                        n,
3331                        [flag_struct.into_series(), hs_struct.into_series()].iter(),
3332                    )?;
3333                    Ok(Some(Column::from(combined.into_series())))
3334                },
3335                GetOutput::from_type(DataType::Struct(vec![
3336                    Field::new("flag".into(), DataType::Struct(vec![
3337                        Field::new("id".into(), DataType::UInt32),
3338                        Field::new("is_bull".into(), DataType::Boolean),
3339                        Field::new("pole_length".into(), DataType::Float64),
3340                        Field::new("pole_length_atr".into(), DataType::Float64),
3341                        Field::new("breakout_confirmed".into(), DataType::Boolean),
3342                        Field::new("breakout_price".into(), DataType::Float64),
3343                    ])),
3344                    Field::new("hs".into(), DataType::Struct(vec![
3345                        Field::new("id".into(), DataType::UInt32),
3346                        Field::new("is_bearish".into(), DataType::Boolean),
3347                        Field::new("height".into(), DataType::Float64),
3348                        Field::new("height_atr".into(), DataType::Float64),
3349                        Field::new("score".into(), DataType::Float64),
3350                        Field::new("breakout_confirmed".into(), DataType::Boolean),
3351                    ])),
3352                ])),
3353            )
3354            .alias("geometric_patterns")])
3355    }
3356
3357    /// S/R Interaction Monitor (MQL5 Part 67) Polars accessor.
3358    /// Returns struct column "sr_monitor" with per-bar structure summary + first interaction (if any).
3359    /// Use `interaction_count > 0` to filter event bars; join with regimes/ML features for confluence.
3360    pub fn sr_monitor(
3361        self,
3362        high: &str,
3363        low: &str,
3364        close: &str,
3365        swing_strength: usize,
3366        touch_tolerance: f64,
3367        approach_zone: f64,
3368    ) -> LazyFrame {
3369        let high_str = high.to_string();
3370        let low_str = low.to_string();
3371        let close_str = close.to_string();
3372        let strength = swing_strength;
3373
3374        self.0.clone().with_columns([as_struct(vec![
3375            col(&high_str),
3376            col(&low_str),
3377            col(&close_str),
3378        ])
3379        .map(
3380            move |s| {
3381                let ca = s.struct_()?;
3382                let s_h = ca.field_by_name(&high_str)?;
3383                let s_l = ca.field_by_name(&low_str)?;
3384                let s_c = ca.field_by_name(&close_str)?;
3385                let highs = s_h.f64()?;
3386                let lows = s_l.f64()?;
3387                let closes = s_c.f64()?;
3388
3389                let mut mon = quantwave_core::SRInteractionMonitor::new(strength, touch_tolerance, approach_zone);
3390                let n = s.len();
3391
3392                let mut bias_vals: Vec<u32> = Vec::with_capacity(n);
3393                let mut active_levels: Vec<u32> = Vec::with_capacity(n);
3394                let mut interaction_counts: Vec<u32> = Vec::with_capacity(n);
3395                let mut has_interaction: Vec<bool> = Vec::with_capacity(n);
3396                let mut interaction_types: Vec<u32> = Vec::with_capacity(n);
3397                let mut level_prices: Vec<f64> = Vec::with_capacity(n);
3398                let mut is_support_vals: Vec<bool> = Vec::with_capacity(n);
3399                let mut strengths: Vec<f64> = Vec::with_capacity(n);
3400                let mut distances: Vec<f64> = Vec::with_capacity(n);
3401                let mut atr_vals: Vec<f64> = Vec::with_capacity(n);
3402
3403                for i in 0..n {
3404                    let h = highs.get(i).unwrap_or(f64::NAN);
3405                    let l = lows.get(i).unwrap_or(f64::NAN);
3406                    let c = closes.get(i).unwrap_or(f64::NAN);
3407                    let hh = if h.is_nan() || l.is_nan() { f64::NAN } else { h.max(l) };
3408                    let ll = if h.is_nan() || l.is_nan() { f64::NAN } else { l.min(h) };
3409                    let cc = if c.is_nan() { (hh + ll) / 2.0 } else { c.clamp(ll, hh) };
3410
3411                    let out = mon.next((hh, ll, cc));
3412
3413                    let b = match out.structure.bias {
3414                        quantwave_core::Bias::Neutral => 0u32,
3415                        quantwave_core::Bias::Bullish => 1,
3416                        quantwave_core::Bias::Bearish => 2,
3417                    };
3418                    bias_vals.push(b);
3419                    active_levels.push(mon.active_level_count() as u32);
3420                    interaction_counts.push(out.interactions.len() as u32);
3421
3422                    if let Some(first) = out.interactions.first() {
3423                        has_interaction.push(true);
3424                        interaction_types.push(first.interaction as u32);
3425                        level_prices.push(first.level_price);
3426                        is_support_vals.push(first.is_support);
3427                        strengths.push(first.strength);
3428                        distances.push(first.distance_at_event);
3429                    } else {
3430                        has_interaction.push(false);
3431                        interaction_types.push(0);
3432                        level_prices.push(f64::NAN);
3433                        is_support_vals.push(false);
3434                        strengths.push(f64::NAN);
3435                        distances.push(f64::NAN);
3436                    }
3437                    atr_vals.push(mon.current_atr());
3438                }
3439
3440                let struct_series = StructChunked::from_series(
3441                    "sr_monitor_result".into(),
3442                    n,
3443                    [
3444                        Series::new("bias".into(), bias_vals),
3445                        Series::new("active_levels".into(), active_levels),
3446                        Series::new("interaction_count".into(), interaction_counts),
3447                        Series::new("has_interaction".into(), has_interaction),
3448                        Series::new("interaction_type".into(), interaction_types),
3449                        Series::new("level_price".into(), level_prices),
3450                        Series::new("is_support".into(), is_support_vals),
3451                        Series::new("strength".into(), strengths),
3452                        Series::new("distance".into(), distances),
3453                        Series::new("atr".into(), atr_vals),
3454                    ]
3455                    .iter(),
3456                )?;
3457                Ok(Some(Column::from(struct_series.into_series())))
3458            },
3459            GetOutput::from_type(DataType::Struct(vec![
3460                Field::new("bias".into(), DataType::UInt32),
3461                Field::new("active_levels".into(), DataType::UInt32),
3462                Field::new("interaction_count".into(), DataType::UInt32),
3463                Field::new("has_interaction".into(), DataType::Boolean),
3464                Field::new("interaction_type".into(), DataType::UInt32),
3465                Field::new("level_price".into(), DataType::Float64),
3466                Field::new("is_support".into(), DataType::Boolean),
3467                Field::new("strength".into(), DataType::Float64),
3468                Field::new("distance".into(), DataType::Float64),
3469                Field::new("atr".into(), DataType::Float64),
3470            ])),
3471        )
3472        .alias("sr_monitor")])
3473    }
3474
3475    pub fn ichimoku_cloud(
3476        self,
3477        high: &str,
3478        low: &str,
3479        p1: usize,
3480        p2: usize,
3481        p3: usize,
3482    ) -> LazyFrame {
3483        let high = high.to_string();
3484        let low = low.to_string();
3485
3486        self.0
3487            .clone()
3488            .with_columns([as_struct(vec![col(&high), col(&low)])
3489                .map(
3490                    move |s| {
3491                        let ca = s.struct_()?;
3492                        let s_high = ca.field_by_name(&high)?;
3493                        let s_low = ca.field_by_name(&low)?;
3494
3495                        let high = s_high.f64()?;
3496                        let low = s_low.f64()?;
3497
3498                        let mut ic = quantwave_core::IchimokuCloud::new(p1, p2, p3);
3499                        let mut t_vals = Vec::with_capacity(s.len());
3500                        let mut k_vals = Vec::with_capacity(s.len());
3501                        let mut sa_vals = Vec::with_capacity(s.len());
3502                        let mut sb_vals = Vec::with_capacity(s.len());
3503
3504                        for i in 0..s.len() {
3505                            let h = high.get(i).unwrap_or(0.0);
3506                            let l = low.get(i).unwrap_or(0.0);
3507                            let (t, k, sa, sb) = ic.next((h, l));
3508                            t_vals.push(t);
3509                            k_vals.push(k);
3510                            sa_vals.push(sa);
3511                            sb_vals.push(sb);
3512                        }
3513
3514                        let t_series = Series::new("tenkan".into(), t_vals);
3515                        let k_series = Series::new("kijun".into(), k_vals);
3516                        let sa_series = Series::new("senkou_a".into(), sa_vals);
3517                        let sb_series = Series::new("senkou_b".into(), sb_vals);
3518
3519                        let out = StructChunked::from_series(
3520                            "ichimoku_output".into(),
3521                            s.len(),
3522                            [t_series, k_series, sa_series, sb_series].iter(),
3523                        )?;
3524                        Ok(Some(Column::from(out.into_series())))
3525                    },
3526                    GetOutput::from_type(DataType::Struct(vec![
3527                        Field::new("tenkan".into(), DataType::Float64),
3528                        Field::new("kijun".into(), DataType::Float64),
3529                        Field::new("senkou_a".into(), DataType::Float64),
3530                        Field::new("senkou_b".into(), DataType::Float64),
3531                    ])),
3532                )
3533                .alias("ichimoku_data")])
3534    }
3535
3536    pub fn volatility_clusterer(
3537        self,
3538        high: &str,
3539        low: &str,
3540        close: &str,
3541        atr_period: usize,
3542        window_size: usize,
3543        k: usize,
3544    ) -> LazyFrame {
3545        let h_str = high.to_string();
3546        let l_str = low.to_string();
3547        let c_str = close.to_string();
3548
3549        self.0.clone().with_columns([as_struct(vec![col(&h_str), col(&l_str), col(&c_str)])
3550            .map(
3551                move |s| {
3552                    let ca = s.struct_()?;
3553                    let f_h = ca.field_by_name(&h_str)?;
3554                    let high = f_h.f64()?;
3555                    let f_l = ca.field_by_name(&l_str)?;
3556                    let low = f_l.f64()?;
3557                    let f_c = ca.field_by_name(&c_str)?;
3558                    let close = f_c.f64()?;
3559
3560                    let mut clusterer =
3561                        quantwave_core::regimes::volatility_clustering::VolatilityClusterer::new(
3562                            atr_period,
3563                            window_size,
3564                            k,
3565                        );
3566                    let mut values = Vec::with_capacity(s.len());
3567
3568                    for i in 0..s.len() {
3569                        let h = high.get(i).unwrap_or(f64::NAN);
3570                        let l = low.get(i).unwrap_or(f64::NAN);
3571                        let c = close.get(i).unwrap_or(f64::NAN);
3572                        let regime = clusterer.next((h, l, c));
3573                        let val = match regime {
3574                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
3575                            quantwave_core::regimes::MarketRegime::Bull => 1,
3576                            quantwave_core::regimes::MarketRegime::Bear => 2,
3577                            quantwave_core::regimes::MarketRegime::Crisis => 3,
3578                            quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
3579                        };
3580                        values.push(val);
3581                    }
3582
3583                    Ok(Some(Column::from(Series::new("volatility_regime".into(), values))))
3584                },
3585                GetOutput::from_type(DataType::UInt32),
3586            )
3587            .alias("volatility_regime")])
3588    }
3589
3590    pub fn hmm_bull_bear(self, name: &str) -> LazyFrame {
3591        let name_str = name.to_string();
3592        self.0.clone().with_columns([col(&name_str)
3593            .map(
3594                move |s| {
3595                    let ca = s.f64()?;
3596                    let mut hmm = quantwave_core::regimes::hmm::HMM::bull_bear();
3597                    let mut values = Vec::with_capacity(s.len());
3598
3599                    for i in 0..s.len() {
3600                        let val = ca.get(i).unwrap_or(f64::NAN);
3601                        let regime = hmm.next(val);
3602                        let out = match regime {
3603                            quantwave_core::regimes::MarketRegime::Bull => 1u32,
3604                            quantwave_core::regimes::MarketRegime::Bear => 2,
3605                            _ => 0,
3606                        };
3607                        values.push(out);
3608                    }
3609
3610                    Ok(Some(Column::from(Series::new("hmm_regime".into(), values))))
3611                },
3612                GetOutput::from_type(DataType::UInt32),
3613            )
3614            .alias("hmm_regime")])
3615    }
3616
3617    /// Fit a generic HMM (EM) on a full observation column and decode regimes.
3618    ///
3619    /// Returns struct column `hmm_fit_data` with:
3620    /// - `hmm_fit_state` (UInt32): global Viterbi state index (0-based)
3621    /// - `hmm_fit_smooth_probs` (List[Float64]): smoothed state probabilities per bar
3622    ///
3623    /// Set `fit_lambdas` to enable lambda (ecld) emissions with per-state λ MLE (ldhmm parity).
3624    ///
3625    /// Non-finite observations are skipped for fitting/decoding; output rows at those
3626    /// indices receive state `0` and uniform probabilities.
3627    pub fn hmm_fit(self, name: &str, n_states: usize, max_iter: usize, fit_lambdas: bool) -> LazyFrame {
3628        let name_str = name.to_string();
3629        self.0.clone().with_columns([col(&name_str)
3630            .map(
3631                move |s| {
3632                    use quantwave_core::regimes::gaussian_hmm::{
3633                        fit_em, EmissionFamily, GaussianHmmFitConfig,
3634                    };
3635
3636                    let ca = s.f64()?;
3637                    let n = s.len();
3638                    let mut obs = Vec::new();
3639                    let mut index_map = Vec::new();
3640                    for i in 0..n {
3641                        if let Some(v) = ca.get(i) {
3642                            if v.is_finite() {
3643                                obs.push(v);
3644                                index_map.push(i);
3645                            }
3646                        }
3647                    }
3648
3649                    let m = n_states.max(2);
3650                    let uniform = 1.0 / m as f64;
3651                    let mut states = vec![0u32; n];
3652                    let mut probs_rows = vec![vec![uniform; m]; n];
3653
3654                    if obs.len() > m {
3655                        let config = GaussianHmmFitConfig {
3656                            n_states: m,
3657                            max_iter: max_iter.max(1),
3658                            emission_family: if fit_lambdas {
3659                                EmissionFamily::Lambda
3660                            } else {
3661                                EmissionFamily::Gaussian
3662                            },
3663                            fit_lambdas,
3664                            ..Default::default()
3665                        };
3666                        if let Ok(fit) = fit_em(&obs, &config) {
3667                            if let Ok(decode) = fit.params.decode(&obs) {
3668                                for (j, &row_idx) in index_map.iter().enumerate() {
3669                                    states[row_idx] = decode.viterbi_path[j] as u32;
3670                                    for st in 0..m {
3671                                        probs_rows[row_idx][st] = decode.smooth_probs[st][j];
3672                                    }
3673                                }
3674                            }
3675                        }
3676                    }
3677
3678                    let mut prob_builder = ListPrimitiveChunkedBuilder::<Float64Type>::new(
3679                        "hmm_fit_smooth_probs".into(),
3680                        n,
3681                        n * m,
3682                        DataType::Float64,
3683                    );
3684                    for row in &probs_rows {
3685                        prob_builder.append_slice(row);
3686                    }
3687
3688                    let state_series = Series::new("hmm_fit_state".into(), states);
3689                    let prob_series = prob_builder.finish().into_series();
3690                    let out = StructChunked::from_series(
3691                        "hmm_fit_data".into(),
3692                        n,
3693                        [state_series, prob_series].iter(),
3694                    )?;
3695                    Ok(Some(Column::from(out.into_series())))
3696                },
3697                GetOutput::from_type(DataType::Struct(vec![
3698                    Field::new("hmm_fit_state".into(), DataType::UInt32),
3699                    Field::new(
3700                        "hmm_fit_smooth_probs".into(),
3701                        DataType::List(Box::new(DataType::Float64)),
3702                    ),
3703                ])),
3704            )
3705            .alias("hmm_fit_data")])
3706    }
3707
3708    /// Pseudo-residuals from a fitted HMM (ldhmm `pseudo_residuals`).
3709    pub fn hmm_pseudo_residuals(
3710        self,
3711        name: &str,
3712        n_states: usize,
3713        max_iter: usize,
3714        fit_lambdas: bool,
3715    ) -> LazyFrame {
3716        let name_str = name.to_string();
3717        self.0.clone().with_columns([col(&name_str)
3718            .map(
3719                move |s| {
3720                    use quantwave_core::regimes::gaussian_hmm::{
3721                        fit_em, EmissionFamily, GaussianHmmFitConfig,
3722                    };
3723                    use quantwave_core::regimes::hmm_forecast::pseudo_residuals;
3724
3725                    let ca = s.f64()?;
3726                    let n = s.len();
3727                    let mut values = vec![f64::NAN; n];
3728                    let mut obs = Vec::new();
3729                    let mut index_map = Vec::new();
3730                    for i in 0..n {
3731                        if let Some(v) = ca.get(i) {
3732                            if v.is_finite() {
3733                                obs.push(v);
3734                                index_map.push(i);
3735                            }
3736                        }
3737                    }
3738                    let m = n_states.max(2);
3739                    if obs.len() > m {
3740                        let config = GaussianHmmFitConfig {
3741                            n_states: m,
3742                            max_iter: max_iter.max(1),
3743                            emission_family: if fit_lambdas {
3744                                EmissionFamily::Lambda
3745                            } else {
3746                                EmissionFamily::Gaussian
3747                            },
3748                            fit_lambdas,
3749                            ..Default::default()
3750                        };
3751                        if let Ok(fit) = fit_em(&obs, &config) {
3752                            if let Ok(decode) = fit.params.decode(&obs) {
3753                                if let Ok(residuals) =
3754                                    pseudo_residuals(&fit.params, &decode.forward_filter, &obs)
3755                                {
3756                                    for (j, &row_idx) in index_map.iter().enumerate() {
3757                                        values[row_idx] = residuals[j];
3758                                    }
3759                                }
3760                            }
3761                        }
3762                    }
3763                    Ok(Some(Column::from(Series::new("hmm_pseudo_residual".into(), values))))
3764                },
3765                GetOutput::from_type(DataType::Float64),
3766            )
3767            .alias("hmm_pseudo_residual")])
3768    }
3769
3770    /// Per-bar weighted decode statistics from a fitted HMM (ldhmm `decode_stats_history`).
3771    pub fn hmm_decode_stats(
3772        self,
3773        name: &str,
3774        n_states: usize,
3775        max_iter: usize,
3776        fit_lambdas: bool,
3777    ) -> LazyFrame {
3778        let name_str = name.to_string();
3779        self.0.clone().with_columns([col(&name_str)
3780            .map(
3781                move |s| {
3782                    use quantwave_core::regimes::gaussian_hmm::{
3783                        fit_em, EmissionFamily, GaussianHmmFitConfig,
3784                    };
3785                    use quantwave_core::regimes::hmm_forecast::decode_stats_history;
3786
3787                    let ca = s.f64()?;
3788                    let n = s.len();
3789                    let mut means = vec![f64::NAN; n];
3790                    let mut vols = vec![f64::NAN; n];
3791                    let mut lambdas = vec![f64::NAN; n];
3792                    let mut obs = Vec::new();
3793                    let mut index_map = Vec::new();
3794                    for i in 0..n {
3795                        if let Some(v) = ca.get(i) {
3796                            if v.is_finite() {
3797                                obs.push(v);
3798                                index_map.push(i);
3799                            }
3800                        }
3801                    }
3802                    let m = n_states.max(2);
3803                    if obs.len() > m {
3804                        let config = GaussianHmmFitConfig {
3805                            n_states: m,
3806                            max_iter: max_iter.max(1),
3807                            emission_family: if fit_lambdas {
3808                                EmissionFamily::Lambda
3809                            } else {
3810                                EmissionFamily::Gaussian
3811                            },
3812                            fit_lambdas,
3813                            ..Default::default()
3814                        };
3815                        if let Ok(fit) = fit_em(&obs, &config) {
3816                            if let Ok(decode) = fit.params.decode(&obs) {
3817                                if let Ok(stats) =
3818                                    decode_stats_history(&fit.params, &decode.smooth_probs)
3819                                {
3820                                    for (j, row) in stats.iter().enumerate() {
3821                                        let idx = index_map[j];
3822                                        means[idx] = row.weighted_mean;
3823                                        vols[idx] = row.weighted_vol;
3824                                        lambdas[idx] = row.weighted_lambda;
3825                                    }
3826                                }
3827                            }
3828                        }
3829                    }
3830                    let out = StructChunked::from_series(
3831                        "hmm_decode_stats_data".into(),
3832                        n,
3833                        [
3834                            Series::new("hmm_decode_weighted_mean".into(), means),
3835                            Series::new("hmm_decode_weighted_vol".into(), vols),
3836                            Series::new("hmm_decode_weighted_lambda".into(), lambdas),
3837                        ]
3838                        .iter(),
3839                    )?;
3840                    Ok(Some(Column::from(out.into_series())))
3841                },
3842                GetOutput::from_type(DataType::Struct(vec![
3843                    Field::new("hmm_decode_weighted_mean".into(), DataType::Float64),
3844                    Field::new("hmm_decode_weighted_vol".into(), DataType::Float64),
3845                    Field::new("hmm_decode_weighted_lambda".into(), DataType::Float64),
3846                ])),
3847            )
3848            .alias("hmm_decode_stats_data")])
3849    }
3850
3851    /// h-step mixture volatility forecast from each bar's forward filter (ldhmm `forecast_volatility`).
3852    pub fn hmm_forecast_vol(
3853        self,
3854        name: &str,
3855        n_states: usize,
3856        max_iter: usize,
3857        fit_lambdas: bool,
3858        horizon: usize,
3859    ) -> LazyFrame {
3860        let name_str = name.to_string();
3861        let h = horizon.max(1);
3862        self.0.clone().with_columns([col(&name_str)
3863            .map(
3864                move |s| {
3865                    use quantwave_core::regimes::gaussian_hmm::{
3866                        fit_em, EmissionFamily, GaussianHmmFitConfig,
3867                    };
3868                    use quantwave_core::regimes::hmm_forecast::forecast_volatility;
3869
3870                    let ca = s.f64()?;
3871                    let n = s.len();
3872                    let mut values = vec![f64::NAN; n];
3873                    let mut obs = Vec::new();
3874                    let mut index_map = Vec::new();
3875                    for i in 0..n {
3876                        if let Some(v) = ca.get(i) {
3877                            if v.is_finite() {
3878                                obs.push(v);
3879                                index_map.push(i);
3880                            }
3881                        }
3882                    }
3883                    let m = n_states.max(2);
3884                    if obs.len() > m {
3885                        let config = GaussianHmmFitConfig {
3886                            n_states: m,
3887                            max_iter: max_iter.max(1),
3888                            emission_family: if fit_lambdas {
3889                                EmissionFamily::Lambda
3890                            } else {
3891                                EmissionFamily::Gaussian
3892                            },
3893                            fit_lambdas,
3894                            ..Default::default()
3895                        };
3896                        if let Ok(fit) = fit_em(&obs, &config) {
3897                            if let Ok(decode) = fit.params.decode(&obs) {
3898                                for (j, &row_idx) in index_map.iter().enumerate() {
3899                                    let probs: Vec<f64> =
3900                                        (0..m).map(|st| decode.forward_filter[st][j]).collect();
3901                                    if let Ok(vol) =
3902                                        forecast_volatility(&fit.params, &probs, h)
3903                                    {
3904                                        values[row_idx] = vol;
3905                                    }
3906                                }
3907                            }
3908                        }
3909                    }
3910                    Ok(Some(Column::from(Series::new("hmm_forecast_vol".into(), values))))
3911                },
3912                GetOutput::from_type(DataType::Float64),
3913            )
3914            .alias("hmm_forecast_vol")])
3915    }
3916
3917    pub fn pelt(self, name: &str, penalty: f64, min_dist: usize) -> LazyFrame {
3918        let name_str = name.to_string();
3919        self.0.clone().with_columns([col(&name_str)
3920            .map(
3921                move |s| {
3922                    let ca = s.f64()?;
3923                    let data: Vec<f64> = ca.into_iter().map(|v| v.unwrap_or(f64::NAN)).collect();
3924                    let pelt = quantwave_core::regimes::pelt::PELT::new(penalty, min_dist);
3925                    let cps = pelt.detect(&data);
3926                    
3927                    let mut values = vec![0u32; s.len()];
3928                    for cp in cps {
3929                        if cp < values.len() {
3930                            values[cp] = 1;
3931                        }
3932                    }
3933
3934                    Ok(Some(Column::from(Series::new("changepoints".into(), values))))
3935                },
3936                GetOutput::from_type(DataType::UInt32),
3937            )
3938            .alias("changepoints")])
3939    }
3940
3941    pub fn gmm(self, columns: &[&str], _k: usize) -> LazyFrame {
3942        let cols: Vec<String> = columns.iter().map(|s| s.to_string()).collect();
3943        let col_exprs: Vec<Expr> = cols.iter().map(|c| col(c)).collect();
3944
3945        self.0.clone().with_columns([as_struct(col_exprs)
3946            .map(
3947                move |s| {
3948                    let ca = s.struct_()?;
3949                    let n_rows = s.len();
3950                    let n_dims = cols.len();
3951
3952                    let mut data = Vec::with_capacity(n_rows);
3953                    for i in 0..n_rows {
3954                        let mut row = Vec::with_capacity(n_dims);
3955                        for c_name in &cols {
3956                            let f = ca.field_by_name(c_name)?;
3957                            let val = f.f64()?.get(i).unwrap_or(f64::NAN);
3958                            row.push(val);
3959                        }
3960                        data.push(row);
3961                    }
3962
3963                    // Placeholder GMM: requires fitting or pre-trained params.
3964                    // For now, we'll use a simple default clusterer based on means.
3965                    let mut values = Vec::with_capacity(n_rows);
3966                    for _ in 0..n_rows {
3967                        values.push(0u32);
3968                    }
3969
3970                    Ok(Some(Column::from(Series::new("gmm_regime".into(), values))))
3971                },
3972                GetOutput::from_type(DataType::UInt32),
3973            )
3974            .alias("gmm_regime")])
3975    }
3976
3977    pub fn regimes_duration_stats(self, regime_col: &str, num_states: usize) -> LazyFrame {
3978        let name_str = regime_col.to_string();
3979        self.0.clone().with_columns([col(&name_str)
3980            .map(
3981                move |s| {
3982                    let ca = s.u32()?;
3983                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
3984                    let stats = quantwave_core::RegimeAnalytics::duration_stats(&states, num_states);
3985                    
3986                    // Convert stats to a Struct
3987                    let mut regime_ids = Vec::new();
3988                    let mut means = Vec::new();
3989                    let mut medians = Vec::new();
3990                    let mut stds = Vec::new();
3991                    let mut maxes = Vec::new();
3992                    let mut totals = Vec::new();
3993                    
3994                    for stat in stats {
3995                        regime_ids.push(stat.regime_id);
3996                        means.push(stat.mean_duration);
3997                        medians.push(stat.median_duration);
3998                        stds.push(stat.std_duration);
3999                        maxes.push(stat.max_duration as u32);
4000                        totals.push(stat.total_observations as u32);
4001                    }
4002                    
4003                    let s_id = Series::new("regime_id".into(), regime_ids);
4004                    let s_mean = Series::new("mean_duration".into(), means);
4005                    let s_median = Series::new("median_duration".into(), medians);
4006                    let s_std = Series::new("std_duration".into(), stds);
4007                    let s_max = Series::new("max_duration".into(), maxes);
4008                    let s_total = Series::new("total_observations".into(), totals);
4009                    
4010                    let struct_series = StructChunked::from_series(
4011                        "duration_stats".into(),
4012                        s_id.len(),
4013                        [s_id, s_mean, s_median, s_std, s_max, s_total].iter(),
4014                    )?;
4015                    Ok(Some(Column::from(struct_series.into_series())))
4016                },
4017                GetOutput::from_type(DataType::Struct(vec![
4018                    Field::new("regime_id".into(), DataType::UInt32),
4019                    Field::new("mean_duration".into(), DataType::Float64),
4020                    Field::new("median_duration".into(), DataType::Float64),
4021                    Field::new("std_duration".into(), DataType::Float64),
4022                    Field::new("max_duration".into(), DataType::UInt32),
4023                    Field::new("total_observations".into(), DataType::UInt32),
4024                ])),
4025            )
4026            .alias("regime_duration_stats")])
4027    }
4028
4029    pub fn regimes_transition_matrix(self, regime_col: &str, num_states: usize) -> LazyFrame {
4030        let name_str = regime_col.to_string();
4031        self.0.clone().with_columns([col(&name_str)
4032            .map(
4033                move |s| {
4034                    let ca = s.u32()?;
4035                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
4036                    let matrix = quantwave_core::RegimeAnalytics::transition_matrix(&states, num_states);
4037                    
4038                    // Return as a List of Lists (effectively a matrix)
4039                    let mut builders = ListPrimitiveChunkedBuilder::<Float64Type>::new(
4040                        "transition_matrix".into(),
4041                        matrix.len(),
4042                        matrix.len() * num_states,
4043                        DataType::Float64,
4044                    );
4045                    for row in matrix {
4046                        builders.append_slice(&row);
4047                    }
4048                    
4049                    let list_ca = builders.finish();
4050                    Ok(Some(Column::from(list_ca.into_series())))
4051                },
4052                GetOutput::from_type(DataType::List(Box::new(DataType::Float64))),
4053            )
4054            .alias("regime_transition_matrix")])
4055    }
4056
4057    pub fn regimes_stability_score(self, regime_col: &str) -> LazyFrame {
4058        let name_str = regime_col.to_string();
4059        self.0.clone().with_columns([col(&name_str)
4060            .map(
4061                move |s| {
4062                    let ca = s.u32()?;
4063                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
4064                    let score = quantwave_core::RegimeAnalytics::stability_score(&states);
4065                    
4066                    Ok(Some(Column::from(Series::new("stability_score".into(), vec![score; s.len()]))))
4067                },
4068                GetOutput::from_type(DataType::Float64),
4069            )
4070            .alias("regime_stability_score")])
4071    }
4072
4073    pub fn regimes_next_state_prob(self, regime_col: &str, num_states: usize, steps: usize) -> LazyFrame {
4074        let name_str = regime_col.to_string();
4075        self.0.clone().with_columns([col(&name_str)
4076            .map(
4077                move |s| {
4078                    let ca = s.u32()?;
4079                    let states: Vec<u32> = ca.into_iter().map(|v| v.unwrap_or(0)).collect();
4080                    let matrix = quantwave_core::RegimeAnalytics::transition_matrix(&states, num_states);
4081                    
4082                    let mut builders = ListPrimitiveChunkedBuilder::<Float64Type>::new(
4083                        "next_state_probs".into(),
4084                        s.len(),
4085                        s.len() * num_states,
4086                        DataType::Float64,
4087                    );
4088
4089                    for &current in &states {
4090                        let probs = quantwave_core::RegimeAnalytics::forecast_state(&matrix, current, steps);
4091                        builders.append_slice(&probs);
4092                    }
4093                    
4094                    let list_ca = builders.finish();
4095                    Ok(Some(Column::from(list_ca.into_series())))
4096                },
4097                GetOutput::from_type(DataType::List(Box::new(DataType::Float64))),
4098            )
4099            .alias("next_state_probs")])
4100    }
4101
4102    pub fn filter_by_regime(self, regime_col: &str, target_regime: u32) -> LazyFrame {
4103        self.0.clone().filter(col(regime_col).eq(lit(target_regime)))
4104    }
4105
4106    pub fn apply_regime_strategy(
4107        self,
4108        regime_col: &str,
4109        signal_col: &str,
4110        regime_weights: std::collections::HashMap<u32, f64>,
4111    ) -> LazyFrame {
4112        let mut case_expr = col(signal_col);
4113        for (regime, weight) in regime_weights {
4114            case_expr = when(col(regime_col).eq(lit(regime)))
4115                .then(col(signal_col) * lit(weight))
4116                .otherwise(case_expr);
4117        }
4118        
4119        self.0.clone().with_columns([case_expr.alias("regime_adjusted_signal")])
4120    }
4121
4122    pub fn regimes_ms_garch(self, returns_col: &str) -> LazyFrame {
4123        let name_str = returns_col.to_string();
4124        self.0.clone().with_columns([col(&name_str)
4125            .map(
4126                move |s| {
4127                    let ca = s.f64()?;
4128                    let mut model = quantwave_core::regimes::ms_garch::MSGarch::low_high_vol();
4129                    let mut regimes = Vec::with_capacity(s.len());
4130                    let mut vols = Vec::with_capacity(s.len());
4131
4132                    for i in 0..s.len() {
4133                        let ret = ca.get(i).unwrap_or(0.0);
4134                        let (regime, vol) = model.next(ret);
4135                        
4136                        let r_val = match regime {
4137                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4138                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4139                            quantwave_core::regimes::MarketRegime::Bull => 2,
4140                            quantwave_core::regimes::MarketRegime::Bear => 3,
4141                            quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
4142                        };
4143                        regimes.push(r_val);
4144                        vols.push(vol);
4145                    }
4146
4147                    let s_regime = Series::new("regime".into(), regimes);
4148                    let s_vol = Series::new("estimated_vol".into(), vols);
4149                    let struct_series = StructChunked::from_series(
4150                        "ms_garch_data".into(),
4151                        s.len(),
4152                        [s_regime, s_vol].iter(),
4153                    )?;
4154                    Ok(Some(Column::from(struct_series.into_series())))
4155                },
4156                GetOutput::from_type(DataType::Struct(vec![
4157                    Field::new("regime".into(), DataType::UInt32),
4158                    Field::new("estimated_vol".into(), DataType::Float64),
4159                ])),
4160            )
4161            .alias("ms_garch_data")])
4162    }
4163
4164    pub fn regimes_ensemble(self, columns: &[&str], weights: &[f64]) -> LazyFrame {
4165        let cols: Vec<String> = columns.iter().map(|s| s.to_string()).collect();
4166        let col_exprs: Vec<Expr> = cols.iter().map(|c| col(c)).collect();
4167        let w_vec = weights.to_vec();
4168
4169        self.0.clone().with_columns([as_struct(col_exprs)
4170            .map(
4171                move |s| {
4172                    let ca = s.struct_()?;
4173                    let n_rows = s.len();
4174                    let n_dims = cols.len();
4175                    let ensemble = quantwave_core::regimes::ensemble::RegimeEnsemble::new(w_vec.clone());
4176                    
4177                    let mut results = Vec::with_capacity(n_rows);
4178                    for i in 0..n_rows {
4179                        let mut row_regimes = Vec::with_capacity(n_dims);
4180                        for c_name in &cols {
4181                            let f = ca.field_by_name(c_name)?;
4182                            let val = f.u32()?.get(i).unwrap_or(0);
4183                            
4184                            let regime = match val {
4185                                0 => quantwave_core::regimes::MarketRegime::Steady,
4186                                1 => quantwave_core::regimes::MarketRegime::Crisis,
4187                                2 => quantwave_core::regimes::MarketRegime::Bull,
4188                                3 => quantwave_core::regimes::MarketRegime::Bear,
4189                                v if v >= 4 => quantwave_core::regimes::MarketRegime::Cluster((v - 4) as u8),
4190                                _ => quantwave_core::regimes::MarketRegime::Steady,
4191                            };
4192                            row_regimes.push(regime);
4193                        }
4194                        
4195                        let consensus = ensemble.vote(&row_regimes);
4196                        let out = match consensus {
4197                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4198                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4199                            quantwave_core::regimes::MarketRegime::Bull => 2,
4200                            quantwave_core::regimes::MarketRegime::Bear => 3,
4201                            quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
4202                        };
4203                        results.push(out);
4204                    }
4205
4206                    Ok(Some(Column::from(Series::new("ensemble_regime".into(), results))))
4207                },
4208                GetOutput::from_type(DataType::UInt32),
4209            )
4210            .alias("ensemble_regime")])
4211    }
4212
4213    pub fn regimes_tar(self, signal_col: &str, thresholds: Vec<f64>) -> LazyFrame {
4214        let name_str = signal_col.to_string();
4215        self.0.clone().with_columns([col(&name_str)
4216            .map(
4217                move |s| {
4218                    let ca = s.f64()?;
4219                    let mut model = quantwave_core::regimes::tar::TAR::multi(thresholds.clone());
4220                    let mut results = Vec::with_capacity(s.len());
4221
4222                    for i in 0..s.len() {
4223                        let val = ca.get(i).unwrap_or(f64::NAN);
4224                        let regime = model.next(val);
4225                        let out = match regime {
4226                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4227                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4228                            quantwave_core::regimes::MarketRegime::Bull => 2,
4229                            quantwave_core::regimes::MarketRegime::Bear => 3,
4230                            quantwave_core::regimes::MarketRegime::Cluster(c) => 4 + (c as u32),
4231                        };
4232                        results.push(out);
4233                    }
4234
4235                    Ok(Some(Column::from(Series::new("tar_regime".into(), results))))
4236                },
4237                GetOutput::from_type(DataType::UInt32),
4238            )
4239            .alias("tar_regime")])
4240    }
4241
4242    pub fn regimes_hsmm(self, name: &str) -> LazyFrame {
4243        let name_str = name.to_string();
4244        self.0.clone().with_columns([col(&name_str)
4245            .map(
4246                move |s| {
4247                    let ca = s.f64()?;
4248                    // Default 2-state HSMM: Poisson durations (5 days Bull, 2 days Bear)
4249                    let mut model = quantwave_core::regimes::hsmm::HSMM::new(
4250                        vec![vec![0.0, 1.0], vec![1.0, 0.0]], // Always switch
4251                        vec![0.001, -0.002],
4252                        vec![0.01, 0.02],
4253                        vec![
4254                            quantwave_core::regimes::hsmm::DurationDistribution::Poisson { lambda: 5.0 },
4255                            quantwave_core::regimes::hsmm::DurationDistribution::Poisson { lambda: 2.0 },
4256                        ],
4257                    );
4258                    let mut values = Vec::with_capacity(s.len());
4259
4260                    for i in 0..s.len() {
4261                        let val = ca.get(i).unwrap_or(f64::NAN);
4262                        let regime = model.next(val);
4263                        let out = match regime {
4264                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4265                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4266                            _ => 2, // Map others
4267                        };
4268                        values.push(out);
4269                    }
4270
4271                    Ok(Some(Column::from(Series::new("hsmm_regime".into(), values))))
4272                },
4273                GetOutput::from_type(DataType::UInt32),
4274            )
4275            .alias("hsmm_regime")])
4276    }
4277
4278    pub fn regimes_hmm_gas(self, name: &str) -> LazyFrame {
4279        let name_str = name.to_string();
4280        self.0.clone().with_columns([col(&name_str)
4281            .map(
4282                move |s| {
4283                    let ca = s.f64()?;
4284                    let mut model = quantwave_core::regimes::hmm_gas::HMMGAS::new(
4285                        [0.1, 0.05, 0.9], // p11 params
4286                        [0.1, 0.05, 0.9], // p22 params
4287                        [0.001, -0.002],
4288                        [0.01, 0.02],
4289                    );
4290                    let mut values = Vec::with_capacity(s.len());
4291
4292                    for i in 0..s.len() {
4293                        let val = ca.get(i).unwrap_or(f64::NAN);
4294                        let regime = model.next(val);
4295                        let out = match regime {
4296                            quantwave_core::regimes::MarketRegime::Steady => 0u32,
4297                            quantwave_core::regimes::MarketRegime::Crisis => 1,
4298                            _ => 2,
4299                        };
4300                        values.push(out);
4301                    }
4302
4303                    Ok(Some(Column::from(Series::new("hmm_gas_regime".into(), values))))
4304                },
4305                GetOutput::from_type(DataType::UInt32),
4306            )
4307            .alias("hmm_gas_regime")])
4308    }
4309
4310    pub fn regimes_conditioned_metrics(
4311        self,
4312        returns_col: &str,
4313        regime_col: &str,
4314        annualization_factor: f64,
4315    ) -> LazyFrame {
4316        let ret_str = returns_col.to_string();
4317        let reg_str = regime_col.to_string();
4318
4319        self.0
4320            .clone()
4321            .group_by([col(&reg_str)])
4322            .agg([
4323                col(&ret_str).mean().alias("mean_return"),
4324                col(&ret_str).std(1).alias("volatility"),
4325                (col(&ret_str).mean() / col(&ret_str).std(1) * lit(annualization_factor.sqrt()))
4326                    .alias("sharpe_ratio"),
4327                col(&ret_str).skew(true).alias("skewness"),
4328                col(&ret_str).kurtosis(true, true).alias("kurtosis"),
4329                // Sortino Ratio: Mean return / Downside Deviation
4330                (col(&ret_str).mean() 
4331                    / col(&ret_str).filter(col(&ret_str).lt(lit(0.0))).std(1) 
4332                    * lit(annualization_factor.sqrt()))
4333                .alias("sortino_ratio"),
4334                // For Max Drawdown and Ulcer Index in an AGG context, we can use map_groups if needed,
4335                // but for now let's stick to simpler metrics or use a robust multi-step approach.
4336                // We'll calculate drawdown stats by first expanding the groups.
4337            ])
4338    }
4339}
4340
4341
4342
4343
4344
4345#[cfg(test)]
4346mod tests {
4347    use super::*;
4348
4349    #[test]
4350    fn test_polars_heikin_ashi() -> PolarsResult<()> {
4351        let df = df![
4352            "open" => [10.0, 11.0],
4353            "high" => [12.0, 13.0],
4354            "low" => [8.0, 10.0],
4355            "close" => [11.0, 12.0]
4356        ]?;
4357
4358        let out = df
4359            .lazy()
4360            .ta()
4361            .heikin_ashi("open", "high", "low", "close")
4362            .collect()?;
4363
4364        let ha = out.column("heikin_ashi_data")?.struct_()?;
4365        assert_eq!(
4366            ha.field_by_name("ha_open".into())?.f64()?.get(0),
4367            Some(10.5)
4368        );
4369        assert_eq!(
4370            ha.field_by_name("ha_close".into())?.f64()?.get(0),
4371            Some(10.25)
4372        );
4373
4374        Ok(())
4375    }
4376
4377    #[test]
4378    fn test_polars_tema_zlema() -> PolarsResult<()> {
4379        let df = df![
4380            "price" => [1.0, 2.0, 3.0, 4.0, 5.0]
4381        ]?;
4382
4383        let out = df.clone().lazy().ta().tema("price", 3).collect()?;
4384
4385        let tema = out.column("tema")?.f64()?;
4386        assert!(tema.get(4).is_some());
4387
4388        let out2 = df.lazy().ta().zlema("price", 3).collect()?;
4389
4390        let zlema = out2.column("zlema")?.f64()?;
4391        assert!(zlema.get(4).is_some());
4392
4393        Ok(())
4394    }
4395
4396    #[test]
4397    fn test_polars_atr_ts() -> PolarsResult<()> {
4398        let df = df![
4399            "high" => [10.0, 12.0, 11.0],
4400            "low" => [8.0, 10.0, 9.0],
4401            "close" => [9.0, 11.0, 10.0]
4402        ]?;
4403
4404        let out = df
4405            .lazy()
4406            .ta()
4407            .atr_trailing_stop("high", "low", "close", 14, 2.5)
4408            .collect()?;
4409
4410        let atr_ts = out.column("atr_ts_data")?.struct_()?;
4411        assert!(atr_ts.field_by_name("stop".into())?.f64()?.get(0).is_some());
4412        assert!(
4413            atr_ts
4414                .field_by_name("direction".into())?
4415                .f64()?
4416                .get(0)
4417                .is_some()
4418        );
4419
4420        Ok(())
4421    }
4422
4423    #[test]
4424    fn test_polars_pivot_points() -> PolarsResult<()> {
4425        let df = df![
4426            "high" => [10.0, 12.0, 11.0],
4427            "low" => [8.0, 10.0, 9.0],
4428            "close" => [9.0, 11.0, 10.0]
4429        ]?;
4430
4431        let out = df
4432            .lazy()
4433            .ta()
4434            .pivot_points("high", "low", "close")
4435            .collect()?;
4436
4437        let pivot = out.column("pivot_points_data")?.struct_()?;
4438        assert!(pivot.field_by_name("p".into())?.f64()?.get(0).is_some());
4439        assert!(pivot.field_by_name("r1".into())?.f64()?.get(0).is_some());
4440
4441        Ok(())
4442    }
4443
4444    #[test]
4445    fn test_polars_fractals() -> PolarsResult<()> {
4446        let df = df![
4447            "high" => [10.0, 11.0, 15.0, 12.0, 10.0],
4448            "low" => [5.0, 6.0, 2.0, 6.0, 7.0]
4449        ]?;
4450
4451        let out = df
4452            .lazy()
4453            .ta()
4454            .bill_williams_fractals("high", "low")
4455            .collect()?;
4456
4457        let fractals = out.column("fractals_data")?.struct_()?;
4458        assert!(
4459            fractals
4460                .field_by_name("bearish".into())?
4461                .bool()?
4462                .get(4)
4463                .unwrap()
4464        );
4465        assert!(
4466            fractals
4467                .field_by_name("bullish".into())?
4468                .bool()?
4469                .get(4)
4470                .unwrap()
4471        );
4472
4473        Ok(())
4474    }
4475
4476    #[test]
4477    fn test_polars_ichimoku() -> PolarsResult<()> {
4478        let df = df![
4479            "high" => [10.0, 11.0, 15.0, 12.0, 10.0],
4480            "low" => [5.0, 6.0, 2.0, 6.0, 7.0]
4481        ]?;
4482
4483        let out = df
4484            .lazy()
4485            .ta()
4486            .ichimoku_cloud("high", "low", 9, 26, 52)
4487            .collect()?;
4488
4489        let ichimoku = out.column("ichimoku_data")?.struct_()?;
4490        assert!(
4491            ichimoku
4492                .field_by_name("tenkan".into())?
4493                .f64()?
4494                .get(4)
4495                .is_some()
4496        );
4497        assert!(
4498            ichimoku
4499                .field_by_name("kijun".into())?
4500                .f64()?
4501                .get(4)
4502                .is_some()
4503        );
4504
4505        Ok(())
4506    }
4507
4508    #[test]
4509    fn test_polars_wavetrend() -> PolarsResult<()> {
4510        let df = df![
4511            "high" => [10.0, 12.0, 11.0],
4512            "low" => [8.0, 10.0, 9.0],
4513            "close" => [9.0, 11.0, 10.0]
4514        ]?;
4515
4516        let out = df
4517            .lazy()
4518            .ta()
4519            .wavetrend("high", "low", "close", 10, 21, 4)
4520            .collect()?;
4521
4522        let wt = out.column("wavetrend_data")?.struct_()?;
4523        assert!(wt.field_by_name("wt1".into())?.f64()?.get(0).is_some());
4524        assert!(wt.field_by_name("wt2".into())?.f64()?.get(0).is_some());
4525
4526        Ok(())
4527    }
4528
4529    #[test]
4530    fn test_polars_vortex() -> PolarsResult<()> {
4531        let df = df![
4532            "high" => [10.0, 12.0, 11.0],
4533            "low" => [8.0, 10.0, 9.0],
4534            "close" => [9.0, 11.0, 10.0]
4535        ]?;
4536
4537        let out = df
4538            .lazy()
4539            .ta()
4540            .vortex_indicator("high", "low", "close", 14)
4541            .collect()?;
4542
4543        let vortex = out.column("vortex_data")?.struct_()?;
4544        assert!(
4545            vortex
4546                .field_by_name("vi_plus".into())?
4547                .f64()?
4548                .get(0)
4549                .is_some()
4550        );
4551        assert!(
4552            vortex
4553                .field_by_name("vi_minus".into())?
4554                .f64()?
4555                .get(0)
4556                .is_some()
4557        );
4558
4559        Ok(())
4560    }
4561
4562    #[test]
4563    fn test_polars_ttm_squeeze() -> PolarsResult<()> {
4564        let df = df![
4565            "high" => [11.0, 12.0, 13.0, 14.0],
4566            "low" => [9.0, 10.0, 11.0, 12.0],
4567            "close" => [10.0, 11.0, 12.0, 13.0]
4568        ]?;
4569
4570        let out = df
4571            .lazy()
4572            .ta()
4573            .ttm_squeeze("high", "low", "close", 20, 2.0, 1.5)
4574            .collect()?;
4575
4576        let ttm = out.column("ttm_squeeze_data")?.struct_()?;
4577        assert!(
4578            ttm.field_by_name("histogram".into())?
4579                .f64()?
4580                .get(0)
4581                .is_some()
4582        );
4583        assert!(
4584            ttm.field_by_name("is_squeezed".into())?
4585                .bool()?
4586                .get(0)
4587                .is_some()
4588        );
4589
4590        Ok(())
4591    }
4592
4593    #[test]
4594    fn test_polars_donchian() -> PolarsResult<()> {
4595        let df = df![
4596            "high" => [10.0, 12.0, 11.0, 13.0, 15.0],
4597            "low" => [8.0, 7.0, 9.0, 10.0, 12.0]
4598        ]?;
4599
4600        let out = df
4601            .lazy()
4602            .ta()
4603            .donchian_channels("high", "low", 3)
4604            .collect()?;
4605
4606        let donchian = out.column("donchian_data")?.struct_()?;
4607        // bar 4: H=13, L=10. Window (12,7), (11,9), (13,10). Upper=13, Lower=7, Middle=10
4608        assert_eq!(
4609            donchian.field_by_name("upper".into())?.f64()?.get(3),
4610            Some(13.0)
4611        );
4612        assert_eq!(
4613            donchian.field_by_name("middle".into())?.f64()?.get(3),
4614            Some(10.0)
4615        );
4616        assert_eq!(
4617            donchian.field_by_name("lower".into())?.f64()?.get(3),
4618            Some(7.0)
4619        );
4620
4621        Ok(())
4622    }
4623
4624    #[test]
4625    fn test_polars_alma() -> PolarsResult<()> {
4626        let df = df![
4627            "price" => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
4628        ]?;
4629
4630        let out = df.lazy().ta().alma("price", 9, 0.85, 6.0).collect()?;
4631
4632        let alma = out.column("alma")?.f64()?;
4633        assert!(alma.get(9).is_some());
4634
4635        Ok(())
4636    }
4637
4638    #[test]
4639    fn test_polars_keltner() -> PolarsResult<()> {
4640        let df = df![
4641            "high" => [12.0],
4642            "low" => [8.0],
4643            "close" => [10.0]
4644        ]?;
4645
4646        let out = df
4647            .lazy()
4648            .ta()
4649            .keltner_channels("high", "low", "close", 3, 3, 2.0)
4650            .collect()?;
4651
4652        let keltner = out.column("keltner_data")?.struct_()?;
4653        assert_eq!(
4654            keltner.field_by_name("middle".into())?.f64()?.get(0),
4655            Some(10.0)
4656        );
4657        assert_eq!(
4658            keltner.field_by_name("upper".into())?.f64()?.get(0),
4659            Some(18.0)
4660        );
4661        assert_eq!(
4662            keltner.field_by_name("lower".into())?.f64()?.get(0),
4663            Some(2.0)
4664        );
4665
4666        Ok(())
4667    }
4668
4669    #[test]
4670    fn test_polars_hma() -> PolarsResult<()> {
4671        let df = df![
4672            "price" => [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
4673        ]?;
4674
4675        let out = df.lazy().ta().hma("price", 4).collect()?;
4676
4677        let hma = out.column("hma")?.f64()?;
4678        assert!(hma.get(9).is_some());
4679
4680        Ok(())
4681    }
4682
4683    #[test]
4684    fn test_polars_anchored_vwap() -> PolarsResult<()> {
4685        let df = df![
4686            "price" => [10.0, 12.0, 15.0, 16.0],
4687            "volume" => [100.0, 200.0, 100.0, 100.0],
4688            "anchor" => [false, false, true, false]
4689        ]?;
4690
4691        let out = df
4692            .lazy()
4693            .ta()
4694            .anchored_vwap("price", "volume", "anchor")
4695            .collect()?;
4696
4697        let avwap = out.column("avwap")?.f64()?;
4698        assert_eq!(avwap.get(0), Some(10.0));
4699        assert_eq!(avwap.get(1), Some(11.333333333333334));
4700        assert_eq!(avwap.get(2), Some(15.0));
4701        assert_eq!(avwap.get(3), Some(15.5));
4702
4703        Ok(())
4704    }
4705
4706    #[test]
4707    fn test_polars_math_transforms() -> PolarsResult<()> {
4708        let df = df![
4709            "val" => [0.0, 1.5707963267948966] // 0, PI/2
4710        ]?;
4711
4712        let out = df.lazy().ta().sin("val").collect()?;
4713
4714        let sin = out.column("sin")?.f64()?;
4715        assert!((sin.get(0).unwrap() - 0.0).abs() < 1e-10);
4716        assert!((sin.get(1).unwrap() - 1.0).abs() < 1e-10);
4717
4718        Ok(())
4719    }
4720
4721    #[test]
4722    fn test_polars_math_operators() -> PolarsResult<()> {
4723        let df = df![
4724            "v1" => [10.0, 20.0],
4725            "v2" => [5.0, 30.0]
4726        ]?;
4727
4728        let out = df.lazy().ta().add("v1", "v2").ta().max("v1", 2).collect()?;
4729
4730        let add = out.column("add")?.f64()?;
4731        assert_eq!(add.get(0), Some(15.0));
4732        assert_eq!(add.get(1), Some(50.0));
4733
4734        let max = out.column("max")?.f64()?;
4735        assert_eq!(max.get(1), Some(20.0));
4736
4737        Ok(())
4738    }
4739
4740    #[test]
4741    fn test_polars_vpn() -> PolarsResult<()> {
4742        let df = df![
4743            "high" => [10.0, 11.0, 12.0],
4744            "low" => [9.0, 10.0, 11.0],
4745            "close" => [9.5, 10.5, 11.5],
4746            "volume" => [1000.0, 1100.0, 1200.0]
4747        ]?;
4748
4749        let out = df.lazy().ta().vpn("high", "low", "close", "volume", 30, 3).collect()?;
4750        let vpn = out.column("vpn")?.f64()?;
4751        assert!(vpn.get(2).is_some());
4752        Ok(())
4753    }
4754
4755    #[test]
4756    fn test_polars_gap_momentum() -> PolarsResult<()> {
4757        let df = df![
4758            "open" => [10.0, 11.0, 10.0],
4759            "close" => [10.5, 10.5, 9.5]
4760        ]?;
4761
4762        let out = df.lazy().ta().gap_momentum("open", "close", 10, 5).collect()?;
4763        let gm = out.column("gap_momentum")?.struct_()?;
4764        assert!(gm.field_by_name("gap_ratio".into())?.f64()?.get(2).is_some());
4765        assert!(gm.field_by_name("gap_signal".into())?.f64()?.get(2).is_some());
4766        Ok(())
4767    }
4768
4769    #[test]
4770    fn test_polars_autotune() -> PolarsResult<()> {
4771        let df = df![
4772            "price" => [100.0; 50]
4773        ]?;
4774
4775        let out = df.lazy().ta().autotune_filter("price", 20, 0.25).collect()?;
4776        let at = out.column("autotune")?.f64()?;
4777        assert!(at.get(49).is_some());
4778        Ok(())
4779    }
4780
4781    #[test]
4782    fn test_polars_adaptive_ema() -> PolarsResult<()> {
4783        let df = df!["h" => [10.0, 11.0, 10.5], "l" => [9.0, 10.0, 9.5], "c" => [9.5, 10.5, 10.0]]?;
4784        let out = df.lazy().ta().adaptive_ema("h", "l", "c", 10, 2).collect()?;
4785        assert!(out.column("adaptive_ema")?.f64()?.get(2).is_some());
4786        Ok(())
4787    }
4788
4789    #[test]
4790    fn test_polars_obvm() -> PolarsResult<()> {
4791        let df = df!["h" => [10.0, 11.0], "l" => [9.0, 10.0], "c" => [9.5, 10.5], "v" => [100.0, 200.0]]?;
4792        let out = df.lazy().ta().obvm("h", "l", "c", "v", 10, 3).collect()?;
4793        let data = out.column("obvm_data")?.struct_()?;
4794        assert!(data.field_by_name("obvm".into())?.f64()?.get(1).is_some());
4795        Ok(())
4796    }
4797
4798    #[test]
4799    fn test_polars_vfi() -> PolarsResult<()> {
4800        let df = df!["h" => [10.0, 11.0], "l" => [9.0, 10.0], "c" => [9.5, 10.5], "v" => [100.0, 200.0]]?;
4801        let out = df.lazy().ta().vfi("h", "l", "c", "v", 10, 0.2, 2.5, 3).collect()?;
4802        assert!(out.column("vfi")?.f64()?.get(1).is_some());
4803        Ok(())
4804    }
4805
4806    #[test]
4807    fn test_polars_sdo() -> PolarsResult<()> {
4808        let df = df!["p" => [10.0, 11.0, 12.0]]?;
4809        let out = df.lazy().ta().sdo("p", 2, 5, 3).collect()?;
4810        assert!(out.column("sdo")?.f64()?.get(2).is_some());
4811        Ok(())
4812    }
4813
4814    #[test]
4815    fn test_polars_rsmk() -> PolarsResult<()> {
4816        let df = df!["p" => [10.0, 11.0], "b" => [100.0, 101.0]]?;
4817        let out = df.lazy().ta().rsmk("p", "b", 90, 3).collect()?;
4818        assert!(out.column("rsmk")?.f64()?.get(1).is_some());
4819        Ok(())
4820    }
4821
4822    #[test]
4823    fn test_polars_rodc() -> PolarsResult<()> {
4824        let df = df!["p" => [10.0, 11.0, 10.0, 11.0, 12.0]]?;
4825        let out = df.lazy().ta().rodc("p", 10, 0.5, 3).collect()?;
4826        assert!(out.column("rodc")?.f64()?.get(4).is_some());
4827        Ok(())
4828    }
4829
4830    #[test]
4831    fn test_polars_reverse_ema() -> PolarsResult<()> {
4832        let df = df!["p" => [10.0, 11.0, 12.0]]?;
4833        let out = df.lazy().ta().reverse_ema("p", 0.1).collect()?;
4834        assert!(out.column("reverse_ema")?.f64()?.get(2).is_some());
4835        Ok(())
4836    }
4837
4838    #[test]
4839    fn test_polars_harrington_adx() -> PolarsResult<()> {
4840        let df = df!["h" => [10.0, 11.0, 12.0], "l" => [9.0, 10.0, 11.0], "c" => [9.5, 10.5, 11.5]]?;
4841        let out = df.lazy().ta().harrington_adx("h", "l", "c", 10, 1).collect()?;
4842        assert!(out.column("harrington_adx")?.f64()?.get(2).is_some());
4843        Ok(())
4844    }
4845
4846    #[test]
4847    fn test_polars_tradj_ema() -> PolarsResult<()> {
4848        let df = df!["h" => [10.0, 11.0, 10.5], "l" => [9.0, 10.0, 9.5], "c" => [9.5, 10.5, 10.0]]?;
4849        let out = df.lazy().ta().tradj_ema("h", "l", "c", 10, 2, 0.5).collect()?;
4850        assert!(out.column("tradj_ema")?.f64()?.get(2).is_some());
4851        Ok(())
4852    }
4853
4854    #[test]
4855    fn test_polars_sve_volatility_bands() -> PolarsResult<()> {
4856        let df = df!["h" => [10.0, 11.0, 10.5], "l" => [9.0, 10.0, 9.5], "c" => [9.5, 10.5, 10.0]]?;
4857        let out = df.lazy().ta().sve_volatility_bands("h", "l", "c", 10, 1.5, 1.0, 3).collect()?;
4858        let data = out.column("sve_bands_data")?.struct_()?;
4859        assert!(data.field_by_name("upper".into())?.f64()?.get(2).is_some());
4860        Ok(())
4861    }
4862
4863    #[test]
4864    fn test_polars_exp_dev_bands() -> PolarsResult<()> {
4865        let df = df!["p" => [10.0, 11.0, 12.0, 11.0, 10.0]]?;
4866        let out = df.lazy().ta().exp_dev_bands("p", 10, 2.0, true).collect()?;
4867        let data = out.column("exp_dev_bands_data")?.struct_()?;
4868        assert!(data.field_by_name("upper".into())?.f64()?.get(4).is_some());
4869        Ok(())
4870    }
4871
4872    #[test]
4873    fn test_polars_hmm_fit() -> PolarsResult<()> {
4874        let obs: Vec<f64> = vec![
4875            0.01, -0.008, 0.012, -0.015, 0.009, -0.011, 0.007, -0.013, 0.011, -0.009, 0.008,
4876            -0.014, 0.006, -0.01, 0.013, -0.012, 0.005, -0.007, 0.01, -0.016,
4877        ];
4878        let df = df!["returns" => obs.as_slice()]?;
4879        let out = df.lazy().ta().hmm_fit("returns", 2, 40, false).collect()?;
4880        let data = out.column("hmm_fit_data")?.struct_()?;
4881        let state_col = data.field_by_name("hmm_fit_state".into())?;
4882        let prob_col = data.field_by_name("hmm_fit_smooth_probs".into())?;
4883        let states = state_col.u32()?;
4884        let probs = prob_col.list()?;
4885        assert_eq!(states.len(), obs.len());
4886        assert_eq!(probs.len(), obs.len());
4887        assert!(states.get(0).is_some());
4888        Ok(())
4889    }
4890
4891    #[test]
4892    fn test_regimes_conditioned_metrics() -> PolarsResult<()> {
4893        let df = df![
4894            "returns" => [0.01, 0.02, -0.01, -0.02, 0.01],
4895            "regime" => [0u32, 0, 1, 1, 0]
4896        ]?;
4897
4898        let out = df
4899            .lazy()
4900            .ta()
4901            .regimes_conditioned_metrics("returns", "regime", 252.0)
4902            .collect()?;
4903
4904        assert_eq!(out.height(), 2);
4905        assert!(out.column("sharpe_ratio").is_ok());
4906        assert!(out.column("skewness").is_ok());
4907        assert!(out.column("kurtosis").is_ok());
4908        assert!(out.column("sortino_ratio").is_ok());
4909
4910        Ok(())
4911    }
4912
4913    #[test]
4914    fn test_polars_kalman_filters() -> PolarsResult<()> {
4915        let df = df![
4916            "price" => [100.0, 101.0, 102.0, 103.0, 104.0]
4917        ]?;
4918
4919        let out = df
4920            .clone()
4921            .lazy()
4922            .ta()
4923            .kalman("price", 0.01, 0.1)
4924            .collect()?;
4925
4926        let kalman = out.column("kalman")?.f64()?;
4927        assert!(kalman.get(0).is_some());
4928        assert_eq!(kalman.get(0).unwrap(), 100.0);
4929
4930        let out2 = df
4931            .lazy()
4932            .ta()
4933            .kinematic_kalman("price", 0.001, 0.0001, 0.1)
4934            .collect()?;
4935
4936        let kin_kalman = out2.column("kinematic_kalman")?.f64()?;
4937        assert!(kin_kalman.get(0).is_some());
4938        assert_eq!(kin_kalman.get(0).unwrap(), 100.0);
4939
4940        Ok(())
4941    }
4942
4943    /// Smoke test for the new PA foundation accessors.
4944    /// Verifies column presence + dtypes for rich structs (existing market_structure + new geometric_patterns).
4945    /// Uses the exact field names from the prior market_structure impl.
4946    #[test]
4947    fn smoke_ta_pa_foundation() -> PolarsResult<()> {
4948        let highs: Vec<f64> = (0..60).map(|i| 100.0 + (i as f64 * 0.8).sin() * 5.0 + (i as f64) * 0.3).collect();
4949        let lows: Vec<f64> = highs.iter().map(|&h| h - 1.5 - (h % 3.0) * 0.2).collect();
4950
4951        let df = df!["high" => highs, "low" => lows]?;
4952        let lf = df.lazy();
4953
4954        // market_structure (pre-existing impl in this file) -> rich struct with String bias etc.
4955        let out = lf
4956            .clone()
4957            .ta()
4958            .market_structure("high", "low", 3)
4959            .collect()?;
4960        let ms = out.column("market_structure")?;
4961        assert!(matches!(ms.dtype(), DataType::Struct(_)));
4962        let ca = ms.struct_()?;
4963        // bias is UInt32 per the new optimized impl
4964        assert_eq!(ca.field_by_name("bias".into())?.dtype().clone(), DataType::UInt32);
4965        assert!(ca.field_by_name("has_flip".into())?.bool()?.get(59).is_some());
4966
4967        // geometric_patterns -> Struct( flag: Struct(...), hs: Struct(...) )
4968        let out2 = out
4969            .lazy()
4970            .ta()
4971            .geometric_patterns("high", "low", 2)
4972            .collect()?;
4973        let gp = out2.column("geometric_patterns")?;
4974        assert!(matches!(gp.dtype(), DataType::Struct(_)));
4975        let gca = gp.struct_()?;
4976        let flag = gca.field_by_name("flag".into())?;
4977        assert!(matches!(flag.dtype(), DataType::Struct(_)));
4978        // pole_length_atr is the key rich field for sizing in the notebook strategy
4979        assert!(flag.struct_()?.field_by_name("pole_length_atr".into())?.f64()?.get(59).is_some());
4980
4981        Ok(())
4982    }
4983}
4984
4985impl QuantWaveExt for LazyFrame {
4986    fn ta(&self) -> QuantWaveNamespace<'_> {
4987        QuantWaveNamespace(self)
4988    }
4989}
4990
4991pub use bt::{BtNamespace, BtOptions, QuantWaveBtExt};
4992pub use quantwave_backtest::{run_param_sweep, single_param_variants, SweepVariant};