Skip to main content

quantwave_polars/
lib.rs

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