Skip to main content

vector_ta/indicators/dispatch/
cpu_batch.rs

1use super::{
2    IndicatorBatchOutput, IndicatorBatchRequest, IndicatorDataRef, IndicatorDispatchError,
3    IndicatorParamSet, ParamKV, ParamValue,
4};
5use crate::indicators::acosc::{acosc_with_kernel, AcoscInput, AcoscParams};
6use crate::indicators::ad::{ad_with_kernel, AdInput, AdParams};
7use crate::indicators::adosc::{adosc_with_kernel, AdoscInput, AdoscParams};
8use crate::indicators::adx::{adx_with_kernel, AdxInput, AdxParams};
9use crate::indicators::alligator::{alligator_with_kernel, AlligatorInput, AlligatorParams};
10use crate::indicators::alphatrend::{alphatrend_with_kernel, AlphaTrendInput, AlphaTrendParams};
11use crate::indicators::ao::{ao_into_slice, AoInput, AoParams};
12use crate::indicators::apo::{apo_with_kernel, ApoInput, ApoParams};
13use crate::indicators::aroon::{aroon_with_kernel, AroonInput, AroonParams};
14use crate::indicators::aso::{aso_with_kernel, AsoInput, AsoParams};
15use crate::indicators::atr::{atr_with_kernel, AtrInput, AtrParams};
16use crate::indicators::bandpass::{bandpass_with_kernel, BandPassInput, BandPassParams};
17use crate::indicators::bollinger_bands::{
18    bollinger_bands_with_kernel, BollingerBandsInput, BollingerBandsParams,
19};
20use crate::indicators::bop::{bop_with_kernel, BopInput, BopParams};
21use crate::indicators::cci::{cci_with_kernel, CciInput, CciParams};
22use crate::indicators::cfo::{cfo_with_kernel, CfoInput, CfoParams};
23use crate::indicators::chandelier_exit::{
24    chandelier_exit_with_kernel, ChandelierExitInput, ChandelierExitParams,
25};
26use crate::indicators::cksp::{cksp_with_kernel, CkspInput, CkspParams};
27use crate::indicators::cmo::{cmo_with_kernel, CmoInput, CmoParams};
28use crate::indicators::correlation_cycle::{
29    correlation_cycle_with_kernel, CorrelationCycleInput, CorrelationCycleParams,
30};
31use crate::indicators::damiani_volatmeter::{
32    damiani_volatmeter_with_kernel, DamianiVolatmeterInput, DamianiVolatmeterParams,
33};
34use crate::indicators::deviation::{deviation_with_kernel, DeviationInput, DeviationParams};
35use crate::indicators::di::{di_with_kernel, DiInput, DiParams};
36use crate::indicators::dm::{dm_with_kernel, DmInput, DmParams};
37use crate::indicators::donchian::{donchian_with_kernel, DonchianInput, DonchianParams};
38use crate::indicators::dpo::{dpo_with_kernel, DpoInput, DpoParams};
39use crate::indicators::dvdiqqe::{dvdiqqe_with_kernel, DvdiqqeInput, DvdiqqeParams};
40use crate::indicators::dx::{dx_batch_with_kernel, dx_into_slice, DxBatchRange, DxInput, DxParams};
41use crate::indicators::efi::{efi_with_kernel, EfiInput, EfiParams};
42use crate::indicators::emd::{emd_with_kernel, EmdInput, EmdParams};
43use crate::indicators::emv::{emv_with_kernel, EmvInput};
44use crate::indicators::er::{er_with_kernel, ErInput, ErParams};
45use crate::indicators::eri::{eri_with_kernel, EriInput, EriParams};
46use crate::indicators::fisher::{fisher_with_kernel, FisherInput, FisherParams};
47use crate::indicators::fosc::{fosc_with_kernel, FoscInput, FoscParams};
48use crate::indicators::fvg_trailing_stop::{
49    fvg_trailing_stop_with_kernel, FvgTrailingStopInput, FvgTrailingStopParams,
50};
51use crate::indicators::gatorosc::{gatorosc_with_kernel, GatorOscInput, GatorOscParams};
52use crate::indicators::halftrend::{halftrend_with_kernel, HalfTrendInput, HalfTrendParams};
53use crate::indicators::ift_rsi::{ift_rsi_with_kernel, IftRsiInput, IftRsiParams};
54use crate::indicators::kdj::{kdj_with_kernel, KdjInput, KdjParams};
55use crate::indicators::keltner::{keltner_with_kernel, KeltnerInput, KeltnerParams};
56use crate::indicators::kst::{kst_with_kernel, KstInput, KstParams};
57use crate::indicators::kurtosis::{kurtosis_with_kernel, KurtosisInput, KurtosisParams};
58use crate::indicators::kvo::{kvo_with_kernel, KvoInput, KvoParams};
59use crate::indicators::linearreg_angle::{
60    linearreg_angle_with_kernel, Linearreg_angleInput, Linearreg_angleParams,
61};
62use crate::indicators::linearreg_intercept::{
63    linearreg_intercept_with_kernel, LinearRegInterceptInput, LinearRegInterceptParams,
64};
65use crate::indicators::linearreg_slope::{
66    linearreg_slope_with_kernel, LinearRegSlopeInput, LinearRegSlopeParams,
67};
68use crate::indicators::lpc::{lpc_with_kernel, LpcInput, LpcParams};
69use crate::indicators::mab::{mab_with_kernel, MabInput, MabParams};
70use crate::indicators::macd::{macd_with_kernel, MacdInput, MacdParams};
71use crate::indicators::macz::{macz_with_kernel, MaczInput, MaczParams};
72use crate::indicators::mass::{mass_with_kernel, MassInput, MassParams};
73use crate::indicators::mean_ad::{mean_ad_with_kernel, MeanAdInput, MeanAdParams};
74use crate::indicators::medium_ad::{medium_ad_with_kernel, MediumAdInput, MediumAdParams};
75use crate::indicators::medprice::{medprice_with_kernel, MedpriceInput, MedpriceParams};
76use crate::indicators::mfi::{
77    mfi_batch_with_kernel, mfi_into_slice, MfiBatchRange, MfiInput, MfiParams,
78};
79use crate::indicators::midpoint::{midpoint_with_kernel, MidpointInput, MidpointParams};
80use crate::indicators::midprice::{midprice_with_kernel, MidpriceInput, MidpriceParams};
81use crate::indicators::minmax::{minmax_with_kernel, MinmaxInput, MinmaxParams};
82use crate::indicators::mom::{mom_with_kernel, MomInput, MomParams};
83use crate::indicators::moving_averages::ma::MaData;
84use crate::indicators::moving_averages::ma_batch::{
85    ma_batch_with_kernel_and_typed_params, MaBatchParamKV, MaBatchParamValue,
86};
87use crate::indicators::moving_averages::registry::list_moving_averages;
88use crate::indicators::msw::{msw_with_kernel, MswInput, MswParams};
89use crate::indicators::nadaraya_watson_envelope::{
90    nadaraya_watson_envelope_with_kernel, NweInput, NweParams,
91};
92use crate::indicators::natr::{natr_with_kernel, NatrInput, NatrParams};
93use crate::indicators::nvi::{nvi_with_kernel, NviInput, NviParams};
94use crate::indicators::obv::{obv_with_kernel, ObvInput, ObvParams};
95use crate::indicators::otto::{otto_with_kernel, OttoInput, OttoParams};
96use crate::indicators::percentile_nearest_rank::{
97    percentile_nearest_rank_with_kernel, PercentileNearestRankInput, PercentileNearestRankParams,
98};
99use crate::indicators::pfe::{pfe_with_kernel, PfeInput, PfeParams};
100use crate::indicators::pivot::{pivot_with_kernel, PivotInput, PivotParams};
101use crate::indicators::pma::{pma_with_kernel, PmaInput, PmaParams};
102use crate::indicators::ppo::{ppo_with_kernel, PpoInput, PpoParams};
103use crate::indicators::prb::{prb_with_kernel, PrbInput, PrbParams};
104use crate::indicators::pvi::{pvi_with_kernel, PviInput, PviParams};
105use crate::indicators::qqe::{qqe_with_kernel, QqeInput, QqeParams};
106use crate::indicators::range_filter::{
107    range_filter_with_kernel, RangeFilterInput, RangeFilterParams,
108};
109use crate::indicators::registry::{
110    get_indicator, IndicatorInfo, IndicatorInputKind, ParamValueStatic,
111};
112use crate::indicators::roc::{roc_with_kernel, RocInput, RocParams};
113use crate::indicators::rocp::{rocp_with_kernel, RocpInput, RocpParams};
114use crate::indicators::rocr::{rocr_with_kernel, RocrInput, RocrParams};
115use crate::indicators::rsi::{rsi_with_kernel, RsiInput, RsiParams};
116use crate::indicators::rsmk::{rsmk_with_kernel, RsmkInput, RsmkParams};
117use crate::indicators::squeeze_momentum::{
118    squeeze_momentum_with_kernel, SqueezeMomentumInput, SqueezeMomentumParams,
119};
120use crate::indicators::srsi::{srsi_with_kernel, SrsiInput, SrsiParams};
121use crate::indicators::stddev::{stddev_with_kernel, StdDevInput, StdDevParams};
122use crate::indicators::stoch::{stoch_with_kernel, StochInput, StochParams};
123use crate::indicators::stochf::{stochf_with_kernel, StochfInput, StochfParams};
124use crate::indicators::supertrend::{supertrend_with_kernel, SuperTrendInput, SuperTrendParams};
125use crate::indicators::trix::{
126    trix_batch_with_kernel, trix_into_slice, trix_with_kernel, TrixBatchRange, TrixInput,
127    TrixParams,
128};
129use crate::indicators::tsf::{tsf_with_kernel, TsfInput, TsfParams};
130use crate::indicators::tsi::{tsi_with_kernel, TsiInput, TsiParams};
131use crate::indicators::ttm_squeeze::{ttm_squeeze_with_kernel, TtmSqueezeInput, TtmSqueezeParams};
132use crate::indicators::ttm_trend::{ttm_trend_with_kernel, TtmTrendInput, TtmTrendParams};
133use crate::indicators::ui::{ui_with_kernel, UiInput, UiParams};
134use crate::indicators::ultosc::{ultosc_with_kernel, UltOscInput, UltOscParams};
135use crate::indicators::var::{var_with_kernel, VarInput, VarParams};
136use crate::indicators::vi::{vi_with_kernel, ViInput, ViParams};
137use crate::indicators::vosc::{vosc_with_kernel, VoscInput, VoscParams};
138use crate::indicators::voss::{voss_with_kernel, VossInput, VossParams};
139use crate::indicators::vpci::{vpci_with_kernel, VpciInput, VpciParams};
140use crate::indicators::vpt::{vpt_with_kernel, VptInput};
141use crate::indicators::vwmacd::{vwmacd_with_kernel, VwmacdInput, VwmacdParams};
142use crate::indicators::wavetrend::{wavetrend_with_kernel, WavetrendInput, WavetrendParams};
143use crate::indicators::wclprice::{wclprice_with_kernel, WclpriceInput};
144use crate::indicators::willr::{willr_with_kernel, WillrInput, WillrParams};
145use crate::indicators::wto::{wto_with_kernel, WtoInput, WtoParams};
146use crate::indicators::zscore::{zscore_with_kernel, ZscoreInput, ZscoreParams};
147use crate::indicators::{cg::cg_with_kernel, cg::CgInput, cg::CgParams};
148use crate::utilities::data_loader::source_type;
149use crate::utilities::enums::Kernel;
150use std::collections::HashMap;
151
152pub fn compute_cpu_batch(
153    req: IndicatorBatchRequest<'_>,
154) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
155    compute_cpu_batch_internal(req, false)
156}
157
158pub fn compute_cpu_batch_strict(
159    req: IndicatorBatchRequest<'_>,
160) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
161    compute_cpu_batch_internal(req, true)
162}
163
164fn compute_cpu_batch_internal(
165    req: IndicatorBatchRequest<'_>,
166    strict_inputs: bool,
167) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
168    if !strict_inputs {
169        if let Some(out) = try_fast_dispatch_non_strict(req) {
170            return out;
171        }
172    }
173
174    let info = get_indicator(req.indicator_id).ok_or_else(|| {
175        IndicatorDispatchError::UnknownIndicator {
176            id: req.indicator_id.to_string(),
177        }
178    })?;
179
180    if !info.capabilities.supports_cpu_batch {
181        return Err(IndicatorDispatchError::UnsupportedCapability {
182            indicator: info.id.to_string(),
183            capability: "cpu_batch",
184        });
185    }
186
187    if strict_inputs {
188        validate_input_kind_strict(info.id, info.input_kind, req.data)?;
189    }
190
191    let output_id = resolve_output_id(info, req.output_id)?;
192
193    dispatch_cpu_batch_by_indicator(req, info, output_id)
194}
195
196fn try_fast_dispatch_non_strict(
197    req: IndicatorBatchRequest<'_>,
198) -> Option<Result<IndicatorBatchOutput, IndicatorDispatchError>> {
199    let id = req.indicator_id;
200    let output_id = req.output_id;
201
202    if !id.as_bytes().iter().any(|b| b.is_ascii_uppercase()) {
203        return match id {
204            "bop" => Some(compute_bop_batch(req, output_id.unwrap_or("value"))),
205            "dpo" => Some(compute_dpo_batch(req, output_id.unwrap_or("value"))),
206            "cmo" => Some(compute_cmo_batch(req, output_id.unwrap_or("value"))),
207            "fosc" => Some(compute_fosc_batch(req, output_id.unwrap_or("value"))),
208            "emv" => Some(compute_emv_batch(req, output_id.unwrap_or("value"))),
209            "cfo" => Some(compute_cfo_batch(req, output_id.unwrap_or("value"))),
210            "nvi" => Some(compute_nvi_batch(req, output_id.unwrap_or("value"))),
211            "mom" => Some(compute_mom_batch(req, output_id.unwrap_or("value"))),
212            "vi" => {
213                if let Some(out) = output_id {
214                    Some(compute_vi_batch(req, out))
215                } else {
216                    None
217                }
218            }
219            "wto" => {
220                if let Some(out) = output_id {
221                    Some(compute_wto_batch(req, out))
222                } else {
223                    None
224                }
225            }
226            "voss" => {
227                if let Some(out) = output_id {
228                    Some(compute_voss_batch(req, out))
229                } else {
230                    None
231                }
232            }
233            "acosc" => {
234                if let Some(out) = output_id {
235                    Some(compute_acosc_batch(req, out))
236                } else {
237                    None
238                }
239            }
240            _ => None,
241        };
242    }
243
244    if id.eq_ignore_ascii_case("bop") {
245        return Some(compute_bop_batch(req, output_id.unwrap_or("value")));
246    }
247    if id.eq_ignore_ascii_case("dpo") {
248        return Some(compute_dpo_batch(req, output_id.unwrap_or("value")));
249    }
250    if id.eq_ignore_ascii_case("cmo") {
251        return Some(compute_cmo_batch(req, output_id.unwrap_or("value")));
252    }
253    if id.eq_ignore_ascii_case("fosc") {
254        return Some(compute_fosc_batch(req, output_id.unwrap_or("value")));
255    }
256    if id.eq_ignore_ascii_case("emv") {
257        return Some(compute_emv_batch(req, output_id.unwrap_or("value")));
258    }
259    if id.eq_ignore_ascii_case("cfo") {
260        return Some(compute_cfo_batch(req, output_id.unwrap_or("value")));
261    }
262    if id.eq_ignore_ascii_case("nvi") {
263        return Some(compute_nvi_batch(req, output_id.unwrap_or("value")));
264    }
265    if id.eq_ignore_ascii_case("mom") {
266        return Some(compute_mom_batch(req, output_id.unwrap_or("value")));
267    }
268    if id.eq_ignore_ascii_case("vi") {
269        if let Some(out) = output_id {
270            return Some(compute_vi_batch(req, out));
271        }
272        return None;
273    }
274    if id.eq_ignore_ascii_case("wto") {
275        if let Some(out) = output_id {
276            return Some(compute_wto_batch(req, out));
277        }
278        return None;
279    }
280    if id.eq_ignore_ascii_case("voss") {
281        if let Some(out) = output_id {
282            return Some(compute_voss_batch(req, out));
283        }
284        return None;
285    }
286    if id.eq_ignore_ascii_case("acosc") {
287        if let Some(out) = output_id {
288            return Some(compute_acosc_batch(req, out));
289        }
290        return None;
291    }
292
293    None
294}
295
296fn dispatch_cpu_batch_by_indicator(
297    req: IndicatorBatchRequest<'_>,
298    info: &IndicatorInfo,
299    output_id: &str,
300) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
301    if is_moving_average(info.id) {
302        return compute_ma_batch(req, info, output_id);
303    }
304
305    match info.id {
306        "ad" => compute_ad_batch(req, output_id),
307        "adosc" => compute_adosc_batch(req, output_id),
308        "ao" => compute_ao_batch(req, output_id),
309        "emv" => compute_emv_batch(req, output_id),
310        "efi" => compute_efi_batch(req, output_id),
311        "mfi" => compute_mfi_batch(req, output_id),
312        "mass" => compute_mass_batch(req, output_id),
313        "kvo" => compute_kvo_batch(req, output_id),
314        "vosc" => compute_vosc_batch(req, output_id),
315        "dx" => compute_dx_batch(req, output_id),
316        "fosc" => compute_fosc_batch(req, output_id),
317        "ift_rsi" => compute_ift_rsi_batch(req, output_id),
318        "linearreg_angle" => compute_linearreg_angle_batch(req, output_id),
319        "linearreg_intercept" => compute_linearreg_intercept_batch(req, output_id),
320        "linearreg_slope" => compute_linearreg_slope_batch(req, output_id),
321        "cg" => compute_cg_batch(req, output_id),
322        "rsi" => compute_rsi_batch(req, output_id),
323        "roc" => compute_roc_batch(req, output_id),
324        "apo" => compute_apo_batch(req, output_id),
325        "bop" => compute_bop_batch(req, output_id),
326        "cci" => compute_cci_batch(req, output_id),
327        "cfo" => compute_cfo_batch(req, output_id),
328        "er" => compute_er_batch(req, output_id),
329        "kurtosis" => compute_kurtosis_batch(req, output_id),
330        "natr" => compute_natr_batch(req, output_id),
331        "mean_ad" => compute_mean_ad_batch(req, output_id),
332        "medium_ad" => compute_medium_ad_batch(req, output_id),
333        "deviation" => compute_deviation_batch(req, output_id),
334        "dpo" => compute_dpo_batch(req, output_id),
335        "pfe" => compute_pfe_batch(req, output_id),
336        "percentile_nearest_rank" => compute_percentile_nearest_rank_batch(req, output_id),
337        "obv" => compute_obv_batch(req, output_id),
338        "vpt" => compute_vpt_batch(req, output_id),
339        "nvi" => compute_nvi_batch(req, output_id),
340        "pvi" => compute_pvi_batch(req, output_id),
341        "wclprice" => compute_wclprice_batch(req, output_id),
342        "ui" => compute_ui_batch(req, output_id),
343        "zscore" => compute_zscore_batch(req, output_id),
344        "medprice" => compute_medprice_batch(req, output_id),
345        "midpoint" => compute_midpoint_batch(req, output_id),
346        "midprice" => compute_midprice_batch(req, output_id),
347        "mom" => compute_mom_batch(req, output_id),
348        "cmo" => compute_cmo_batch(req, output_id),
349        "rocp" => compute_rocp_batch(req, output_id),
350        "rocr" => compute_rocr_batch(req, output_id),
351        "ppo" => compute_ppo_batch(req, output_id),
352        "tsf" => compute_tsf_batch(req, output_id),
353        "trix" => compute_trix_batch(req, output_id),
354        "tsi" => compute_tsi_batch(req, output_id),
355        "var" => compute_var_batch(req, output_id),
356        "stddev" => compute_stddev_batch(req, output_id),
357        "willr" => compute_willr_batch(req, output_id),
358        "ultosc" => compute_ultosc_batch(req, output_id),
359        "adx" => compute_adx_batch(req, output_id),
360        "atr" => compute_atr_batch(req, output_id),
361        "macd" => compute_macd_batch(req, output_id),
362        "bollinger_bands" => compute_bollinger_batch(req, output_id),
363        "stoch" => compute_stoch_batch(req, output_id),
364        "stochf" => compute_stochf_batch(req, output_id),
365        "vwmacd" => compute_vwmacd_batch(req, output_id),
366        "vpci" => compute_vpci_batch(req, output_id),
367        "ttm_trend" => compute_ttm_trend_batch(req, output_id),
368        "ttm_squeeze" => compute_ttm_squeeze_batch(req, output_id),
369        "aroon" => compute_aroon_batch(req, output_id),
370        "di" => compute_di_batch(req, output_id),
371        "dm" => compute_dm_batch(req, output_id),
372        "donchian" => compute_donchian_batch(req, output_id),
373        "kdj" => compute_kdj_batch(req, output_id),
374        "keltner" => compute_keltner_batch(req, output_id),
375        "squeeze_momentum" => compute_squeeze_momentum_batch(req, output_id),
376        "srsi" => compute_srsi_batch(req, output_id),
377        "supertrend" => compute_supertrend_batch(req, output_id),
378        "vi" => compute_vi_batch(req, output_id),
379        "wavetrend" => compute_wavetrend_batch(req, output_id),
380        "wto" => compute_wto_batch(req, output_id),
381        "acosc" => compute_acosc_batch(req, output_id),
382        "alligator" => compute_alligator_batch(req, output_id),
383        "alphatrend" => compute_alphatrend_batch(req, output_id),
384        "aso" => compute_aso_batch(req, output_id),
385        "bandpass" => compute_bandpass_batch(req, output_id),
386        "chandelier_exit" => compute_chandelier_exit_batch(req, output_id),
387        "cksp" => compute_cksp_batch(req, output_id),
388        "correlation_cycle" => compute_correlation_cycle_batch(req, output_id),
389        "damiani_volatmeter" => compute_damiani_volatmeter_batch(req, output_id),
390        "dvdiqqe" => compute_dvdiqqe_batch(req, output_id),
391        "emd" => compute_emd_batch(req, output_id),
392        "eri" => compute_eri_batch(req, output_id),
393        "fisher" => compute_fisher_batch(req, output_id),
394        "fvg_trailing_stop" => compute_fvg_trailing_stop_batch(req, output_id),
395        "gatorosc" => compute_gatorosc_batch(req, output_id),
396        "halftrend" => compute_halftrend_batch(req, output_id),
397        "kst" => compute_kst_batch(req, output_id),
398        "lpc" => compute_lpc_batch(req, output_id),
399        "mab" => compute_mab_batch(req, output_id),
400        "macz" => compute_macz_batch(req, output_id),
401        "minmax" => compute_minmax_batch(req, output_id),
402        "msw" => compute_msw_batch(req, output_id),
403        "nadaraya_watson_envelope" => compute_nadaraya_watson_envelope_batch(req, output_id),
404        "otto" => compute_otto_batch(req, output_id),
405        "pma" => compute_pma_batch(req, output_id),
406        "prb" => compute_prb_batch(req, output_id),
407        "qqe" => compute_qqe_batch(req, output_id),
408        "range_filter" => compute_range_filter_batch(req, output_id),
409        "rsmk" => compute_rsmk_batch(req, output_id),
410        "voss" => compute_voss_batch(req, output_id),
411        "pivot" => compute_pivot_batch(req, output_id),
412        _ => Err(IndicatorDispatchError::UnsupportedCapability {
413            indicator: info.id.to_string(),
414            capability: "cpu_batch",
415        }),
416    }
417}
418
419fn validate_input_kind_strict(
420    indicator: &str,
421    expected: IndicatorInputKind,
422    data: IndicatorDataRef<'_>,
423) -> Result<(), IndicatorDispatchError> {
424    let expected = strict_expected_input_kind(indicator, expected);
425    let matches = matches!(
426        (expected, data),
427        (IndicatorInputKind::Slice, IndicatorDataRef::Slice { .. })
428            | (
429                IndicatorInputKind::Candles,
430                IndicatorDataRef::Candles { .. }
431            )
432            | (IndicatorInputKind::Ohlc, IndicatorDataRef::Ohlc { .. })
433            | (IndicatorInputKind::Ohlcv, IndicatorDataRef::Ohlcv { .. })
434            | (
435                IndicatorInputKind::HighLow,
436                IndicatorDataRef::HighLow { .. }
437            )
438            | (
439                IndicatorInputKind::CloseVolume,
440                IndicatorDataRef::CloseVolume { .. }
441            )
442    );
443
444    if matches {
445        Ok(())
446    } else {
447        Err(IndicatorDispatchError::MissingRequiredInput {
448            indicator: indicator.to_string(),
449            input: expected,
450        })
451    }
452}
453
454fn strict_expected_input_kind(indicator: &str, fallback: IndicatorInputKind) -> IndicatorInputKind {
455    if indicator.eq_ignore_ascii_case("ao") {
456        return IndicatorInputKind::Slice;
457    }
458    if indicator.eq_ignore_ascii_case("ttm_trend") {
459        return IndicatorInputKind::Candles;
460    }
461    fallback
462}
463
464fn resolve_output_id<'a>(
465    info: &'a IndicatorInfo,
466    requested: Option<&str>,
467) -> Result<&'a str, IndicatorDispatchError> {
468    if info.outputs.is_empty() {
469        return Err(IndicatorDispatchError::ComputeFailed {
470            indicator: info.id.to_string(),
471            details: "indicator has no registered outputs".to_string(),
472        });
473    }
474
475    if info.outputs.len() == 1 {
476        let only = info.outputs[0].id;
477        if let Some(req) = requested {
478            if req == only {
479                return Ok(only);
480            }
481            if !req.eq_ignore_ascii_case(only) {
482                return Err(IndicatorDispatchError::UnknownOutput {
483                    indicator: info.id.to_string(),
484                    output: req.to_string(),
485                });
486            }
487        }
488        return Ok(only);
489    }
490
491    let req = requested.ok_or_else(|| IndicatorDispatchError::InvalidParam {
492        indicator: info.id.to_string(),
493        key: "output_id".to_string(),
494        reason: "output_id is required for multi-output indicators".to_string(),
495    })?;
496
497    if let Some(out) = info.outputs.iter().find(|o| o.id == req) {
498        return Ok(out.id);
499    }
500    info.outputs
501        .iter()
502        .find(|o| o.id.eq_ignore_ascii_case(req))
503        .map(|o| o.id)
504        .ok_or_else(|| IndicatorDispatchError::UnknownOutput {
505            indicator: info.id.to_string(),
506            output: req.to_string(),
507        })
508}
509
510fn is_moving_average(id: &str) -> bool {
511    list_moving_averages()
512        .iter()
513        .any(|ma| ma.id.eq_ignore_ascii_case(id))
514}
515
516fn ma_is_period_based(info: &IndicatorInfo) -> bool {
517    info.params
518        .iter()
519        .any(|p| p.key.eq_ignore_ascii_case("period"))
520}
521
522fn compute_ma_batch(
523    req: IndicatorBatchRequest<'_>,
524    info: &IndicatorInfo,
525    output_id: &str,
526) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
527    let data = ma_data_from_req(info.id, req.data)?;
528    let cols = ma_len_from_req(info.id, req.data)?;
529    let period_based = ma_is_period_based(info);
530    if period_based {
531        if let Some(out) = try_compute_ma_batch_fast(req, info, output_id, data.clone(), cols)? {
532            return Ok(out);
533        }
534    }
535    let rows = req.combos.len();
536    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
537
538    for combo in req.combos {
539        let period = ma_period_for_combo(info, combo.params)?;
540        let mut params = convert_ma_params(combo.params, info.id, output_id)?;
541        if info.outputs.len() > 1 && !has_key(combo.params, "output") {
542            params.push(MaBatchParamKV {
543                key: "output",
544                value: MaBatchParamValue::EnumString(output_id),
545            });
546        }
547        let out = ma_batch_with_kernel_and_typed_params(
548            info.id,
549            data.clone(),
550            (period, period, 0),
551            req.kernel,
552            &params,
553        )
554        .map_err(|e| IndicatorDispatchError::ComputeFailed {
555            indicator: info.id.to_string(),
556            details: e.to_string(),
557        })?;
558        ensure_len(info.id, cols, out.cols)?;
559        let row_values = if out.rows == 1 {
560            out.values
561        } else {
562            reorder_or_take_f64_matrix_by_period(
563                info.id,
564                &[period],
565                &out.periods,
566                out.cols,
567                out.values,
568            )?
569        };
570        ensure_len(info.id, cols, row_values.len())?;
571        matrix.extend_from_slice(&row_values);
572    }
573
574    Ok(f64_output(output_id, rows, cols, matrix))
575}
576
577fn try_compute_ma_batch_fast(
578    req: IndicatorBatchRequest<'_>,
579    info: &IndicatorInfo,
580    output_id: &str,
581    data: MaData<'_>,
582    cols: usize,
583) -> Result<Option<IndicatorBatchOutput>, IndicatorDispatchError> {
584    if req.combos.is_empty() {
585        return Ok(Some(f64_output(output_id, 0, cols, Vec::new())));
586    }
587    if !ma_is_period_based(info) {
588        return Ok(None);
589    }
590
591    let mut periods = Vec::with_capacity(req.combos.len());
592    let mut shared_params: Option<Vec<MaBatchParamKV<'_>>> = None;
593
594    for combo in req.combos {
595        periods.push(ma_period_for_combo(info, combo.params)?);
596        let mut params = convert_ma_params(combo.params, info.id, output_id)?;
597        if info.outputs.len() > 1 && !has_key(combo.params, "output") {
598            params.push(MaBatchParamKV {
599                key: "output",
600                value: MaBatchParamValue::EnumString(output_id),
601            });
602        }
603        match &shared_params {
604            None => shared_params = Some(params),
605            Some(existing) => {
606                if !ma_params_equal(existing, &params) {
607                    return Ok(None);
608                }
609            }
610        }
611    }
612
613    let Some((start, end, step)) = derive_period_sweep(&periods) else {
614        return Ok(None);
615    };
616
617    let out = ma_batch_with_kernel_and_typed_params(
618        info.id,
619        data,
620        (start, end, step),
621        req.kernel,
622        shared_params.as_deref().unwrap_or(&[]),
623    )
624    .map_err(|e| IndicatorDispatchError::ComputeFailed {
625        indicator: info.id.to_string(),
626        details: e.to_string(),
627    })?;
628    ensure_len(info.id, cols, out.cols)?;
629
630    let values = reorder_or_take_f64_matrix_by_period(
631        info.id,
632        &periods,
633        &out.periods,
634        out.cols,
635        out.values,
636    )?;
637    Ok(Some(f64_output(output_id, periods.len(), cols, values)))
638}
639
640fn ma_params_equal(a: &[MaBatchParamKV<'_>], b: &[MaBatchParamKV<'_>]) -> bool {
641    if a.len() != b.len() {
642        return false;
643    }
644
645    for (lhs, rhs) in a.iter().zip(b.iter()) {
646        if !lhs.key.eq_ignore_ascii_case(rhs.key) {
647            return false;
648        }
649        let same = match (&lhs.value, &rhs.value) {
650            (MaBatchParamValue::Int(x), MaBatchParamValue::Int(y)) => x == y,
651            (MaBatchParamValue::Float(x), MaBatchParamValue::Float(y)) => x == y,
652            (MaBatchParamValue::Bool(x), MaBatchParamValue::Bool(y)) => x == y,
653            (MaBatchParamValue::EnumString(x), MaBatchParamValue::EnumString(y)) => {
654                x.eq_ignore_ascii_case(y)
655            }
656            _ => false,
657        };
658        if !same {
659            return false;
660        }
661    }
662    true
663}
664
665fn collect_f64(
666    indicator: &str,
667    output_id: &str,
668    combos: &[IndicatorParamSet<'_>],
669    cols: usize,
670    mut eval: impl FnMut(&[ParamKV<'_>]) -> Result<Vec<f64>, IndicatorDispatchError>,
671) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
672    let rows = combos.len();
673    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
674    for combo in combos {
675        let series = eval(combo.params)?;
676        ensure_len(indicator, cols, series.len())?;
677        matrix.extend_from_slice(&series);
678    }
679    Ok(f64_output(output_id, rows, cols, matrix))
680}
681
682fn collect_bool(
683    indicator: &str,
684    output_id: &str,
685    combos: &[IndicatorParamSet<'_>],
686    cols: usize,
687    mut eval: impl FnMut(&[ParamKV<'_>]) -> Result<Vec<bool>, IndicatorDispatchError>,
688) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
689    let rows = combos.len();
690    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
691    for combo in combos {
692        let series = eval(combo.params)?;
693        ensure_len(indicator, cols, series.len())?;
694        matrix.extend_from_slice(&series);
695    }
696    Ok(bool_output(output_id, rows, cols, matrix))
697}
698
699fn collect_f64_into_rows(
700    indicator: &str,
701    output_id: &str,
702    combos: &[IndicatorParamSet<'_>],
703    cols: usize,
704    mut eval_into: impl FnMut(&[ParamKV<'_>], &mut [f64]) -> Result<(), IndicatorDispatchError>,
705) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
706    let rows = combos.len();
707    let total = rows
708        .checked_mul(cols)
709        .ok_or_else(|| IndicatorDispatchError::ComputeFailed {
710            indicator: indicator.to_string(),
711            details: "rows*cols overflow".to_string(),
712        })?;
713    let mut matrix = vec![f64::NAN; total];
714    for (row, combo) in combos.iter().enumerate() {
715        let start = row * cols;
716        let end = start + cols;
717        eval_into(combo.params, &mut matrix[start..end])?;
718    }
719    Ok(f64_output(output_id, rows, cols, matrix))
720}
721
722fn to_batch_kernel(kernel: Kernel) -> Kernel {
723    match kernel {
724        Kernel::Auto => Kernel::Auto,
725        Kernel::Scalar => Kernel::ScalarBatch,
726        Kernel::Avx2 => Kernel::Avx2Batch,
727        Kernel::Avx512 => Kernel::Avx512Batch,
728        other => other,
729    }
730}
731
732fn combo_periods(
733    indicator: &str,
734    combos: &[IndicatorParamSet<'_>],
735    key: &str,
736    default: usize,
737) -> Result<Vec<usize>, IndicatorDispatchError> {
738    let mut out = Vec::with_capacity(combos.len());
739    for combo in combos {
740        out.push(get_usize_param(indicator, combo.params, key, default)?);
741    }
742    Ok(out)
743}
744
745fn derive_period_sweep(periods: &[usize]) -> Option<(usize, usize, usize)> {
746    if periods.is_empty() {
747        return None;
748    }
749    if periods.len() == 1 {
750        return Some((periods[0], periods[0], 0));
751    }
752    if periods.windows(2).all(|w| w[0] == w[1]) {
753        return Some((periods[0], periods[0], 0));
754    }
755
756    let diff = periods[1] as isize - periods[0] as isize;
757    if diff == 0 {
758        return None;
759    }
760    if !periods
761        .windows(2)
762        .all(|w| (w[1] as isize - w[0] as isize) == diff)
763    {
764        return None;
765    }
766
767    Some((
768        periods[0],
769        *periods.last().unwrap_or(&periods[0]),
770        diff.unsigned_abs(),
771    ))
772}
773
774fn reorder_or_take_f64_matrix_by_period(
775    indicator: &str,
776    requested_periods: &[usize],
777    produced_periods: &[usize],
778    cols: usize,
779    values: Vec<f64>,
780) -> Result<Vec<f64>, IndicatorDispatchError> {
781    ensure_len(
782        indicator,
783        produced_periods.len().saturating_mul(cols),
784        values.len(),
785    )?;
786
787    if requested_periods.len() == produced_periods.len() && requested_periods == produced_periods {
788        return Ok(values);
789    }
790
791    let period_to_row: HashMap<usize, usize> = produced_periods
792        .iter()
793        .copied()
794        .enumerate()
795        .map(|(row, period)| (period, row))
796        .collect();
797
798    let mut out = Vec::with_capacity(requested_periods.len().saturating_mul(cols));
799    for period in requested_periods {
800        let row = period_to_row.get(period).copied().ok_or_else(|| {
801            IndicatorDispatchError::ComputeFailed {
802                indicator: indicator.to_string(),
803                details: format!("batch output did not contain requested period {period}"),
804            }
805        })?;
806        let start = row * cols;
807        let end = start + cols;
808        out.extend_from_slice(&values[start..end]);
809    }
810    Ok(out)
811}
812
813fn compute_ad_batch(
814    req: IndicatorBatchRequest<'_>,
815    output_id: &str,
816) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
817    expect_value_output("ad", output_id)?;
818    let (high, low, close, volume) = extract_hlcv_input("ad", req.data)?;
819    let kernel = req.kernel.to_non_batch();
820    collect_f64("ad", output_id, req.combos, close.len(), |_params| {
821        let input = AdInput::from_slices(high, low, close, volume, AdParams::default());
822        let out =
823            ad_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
824                indicator: "ad".to_string(),
825                details: e.to_string(),
826            })?;
827        Ok(out.values)
828    })
829}
830
831fn compute_adosc_batch(
832    req: IndicatorBatchRequest<'_>,
833    output_id: &str,
834) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
835    expect_value_output("adosc", output_id)?;
836    let (high, low, close, volume) = extract_hlcv_input("adosc", req.data)?;
837    let kernel = req.kernel.to_non_batch();
838    collect_f64("adosc", output_id, req.combos, close.len(), |params| {
839        let short_period = get_usize_param("adosc", params, "short_period", 3)?;
840        let long_period = get_usize_param("adosc", params, "long_period", 10)?;
841        let input = AdoscInput::from_slices(
842            high,
843            low,
844            close,
845            volume,
846            AdoscParams {
847                short_period: Some(short_period),
848                long_period: Some(long_period),
849            },
850        );
851        let out = adosc_with_kernel(&input, kernel).map_err(|e| {
852            IndicatorDispatchError::ComputeFailed {
853                indicator: "adosc".to_string(),
854                details: e.to_string(),
855            }
856        })?;
857        Ok(out.values)
858    })
859}
860
861fn compute_ao_batch(
862    req: IndicatorBatchRequest<'_>,
863    output_id: &str,
864) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
865    expect_value_output("ao", output_id)?;
866    let mut derived_source: Option<Vec<f64>> = None;
867    let source: &[f64] = match req.data {
868        IndicatorDataRef::Slice { values } => values,
869        IndicatorDataRef::Candles { candles, source } => {
870            source_type(candles, source.unwrap_or("hl2"))
871        }
872        IndicatorDataRef::HighLow { high, low } => {
873            ensure_same_len_2("ao", high.len(), low.len())?;
874            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
875            derived_source.as_deref().unwrap_or(high)
876        }
877        IndicatorDataRef::Ohlc {
878            open,
879            high,
880            low,
881            close,
882        } => {
883            ensure_same_len_4("ao", open.len(), high.len(), low.len(), close.len())?;
884            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
885            derived_source.as_deref().unwrap_or(close)
886        }
887        IndicatorDataRef::Ohlcv {
888            open,
889            high,
890            low,
891            close,
892            volume,
893        } => {
894            ensure_same_len_5(
895                "ao",
896                open.len(),
897                high.len(),
898                low.len(),
899                close.len(),
900                volume.len(),
901            )?;
902            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
903            derived_source.as_deref().unwrap_or(close)
904        }
905        IndicatorDataRef::CloseVolume { .. } => {
906            return Err(IndicatorDispatchError::MissingRequiredInput {
907                indicator: "ao".to_string(),
908                input: IndicatorInputKind::HighLow,
909            })
910        }
911    };
912    let kernel = req.kernel.to_non_batch();
913    collect_f64_into_rows("ao", output_id, req.combos, source.len(), |params, row| {
914        let short_period = get_usize_param("ao", params, "short_period", 5)?;
915        let long_period = get_usize_param("ao", params, "long_period", 34)?;
916        let input = AoInput::from_slice(
917            source,
918            AoParams {
919                short_period: Some(short_period),
920                long_period: Some(long_period),
921            },
922        );
923        ao_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
924            indicator: "ao".to_string(),
925            details: e.to_string(),
926        })
927    })
928}
929
930fn compute_bop_batch(
931    req: IndicatorBatchRequest<'_>,
932    output_id: &str,
933) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
934    expect_value_output("bop", output_id)?;
935    let (open, high, low, close): (&[f64], &[f64], &[f64], &[f64]) = match req.data {
936        IndicatorDataRef::Candles { candles, .. } => (
937            candles.open.as_slice(),
938            candles.high.as_slice(),
939            candles.low.as_slice(),
940            candles.close.as_slice(),
941        ),
942        IndicatorDataRef::Ohlc {
943            open,
944            high,
945            low,
946            close,
947        } => {
948            ensure_same_len_4("bop", open.len(), high.len(), low.len(), close.len())?;
949            (open, high, low, close)
950        }
951        IndicatorDataRef::Ohlcv {
952            open,
953            high,
954            low,
955            close,
956            volume,
957        } => {
958            ensure_same_len_5(
959                "bop",
960                open.len(),
961                high.len(),
962                low.len(),
963                close.len(),
964                volume.len(),
965            )?;
966            (open, high, low, close)
967        }
968        _ => {
969            return Err(IndicatorDispatchError::MissingRequiredInput {
970                indicator: "bop".to_string(),
971                input: IndicatorInputKind::Ohlc,
972            })
973        }
974    };
975    let kernel = req.kernel.to_non_batch();
976    collect_f64("bop", output_id, req.combos, close.len(), |_params| {
977        let input = BopInput::from_slices(open, high, low, close, BopParams::default());
978        let out =
979            bop_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
980                indicator: "bop".to_string(),
981                details: e.to_string(),
982            })?;
983        Ok(out.values)
984    })
985}
986
987fn compute_emv_batch(
988    req: IndicatorBatchRequest<'_>,
989    output_id: &str,
990) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
991    expect_value_output("emv", output_id)?;
992    let (high, low, close, volume) = extract_hlcv_input("emv", req.data)?;
993    let kernel = req.kernel.to_non_batch();
994    collect_f64("emv", output_id, req.combos, close.len(), |_params| {
995        let input = EmvInput::from_slices(high, low, close, volume);
996        let out =
997            emv_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
998                indicator: "emv".to_string(),
999                details: e.to_string(),
1000            })?;
1001        Ok(out.values)
1002    })
1003}
1004
1005fn compute_efi_batch(
1006    req: IndicatorBatchRequest<'_>,
1007    output_id: &str,
1008) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1009    expect_value_output("efi", output_id)?;
1010    let (price, volume) = extract_close_volume_input("efi", req.data, "close")?;
1011    let kernel = req.kernel.to_non_batch();
1012    collect_f64("efi", output_id, req.combos, price.len(), |params| {
1013        let period = get_usize_param("efi", params, "period", 13)?;
1014        let input = EfiInput::from_slices(
1015            price,
1016            volume,
1017            EfiParams {
1018                period: Some(period),
1019            },
1020        );
1021        let out =
1022            efi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1023                indicator: "efi".to_string(),
1024                details: e.to_string(),
1025            })?;
1026        Ok(out.values)
1027    })
1028}
1029
1030fn compute_mfi_batch(
1031    req: IndicatorBatchRequest<'_>,
1032    output_id: &str,
1033) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1034    expect_value_output("mfi", output_id)?;
1035    let mut derived_typical_price: Option<Vec<f64>> = None;
1036    let (typical_price, volume): (&[f64], &[f64]) = match req.data {
1037        IndicatorDataRef::Candles { candles, source } => (
1038            source_type(candles, source.unwrap_or("hlc3")),
1039            candles.volume.as_slice(),
1040        ),
1041        IndicatorDataRef::Ohlcv {
1042            open,
1043            high,
1044            low,
1045            close,
1046            volume,
1047        } => {
1048            ensure_same_len_5(
1049                "mfi",
1050                open.len(),
1051                high.len(),
1052                low.len(),
1053                close.len(),
1054                volume.len(),
1055            )?;
1056            derived_typical_price = Some(
1057                high.iter()
1058                    .zip(low)
1059                    .zip(close)
1060                    .map(|((h, l), c)| (h + l + c) / 3.0)
1061                    .collect(),
1062            );
1063            (derived_typical_price.as_deref().unwrap_or(close), volume)
1064        }
1065        IndicatorDataRef::CloseVolume { close, volume } => {
1066            ensure_same_len_2("mfi", close.len(), volume.len())?;
1067            (close, volume)
1068        }
1069        _ => {
1070            return Err(IndicatorDispatchError::MissingRequiredInput {
1071                indicator: "mfi".to_string(),
1072                input: IndicatorInputKind::CloseVolume,
1073            })
1074        }
1075    };
1076
1077    let periods = combo_periods("mfi", req.combos, "period", 14)?;
1078    if let Some((start, end, step)) = derive_period_sweep(&periods) {
1079        let out = mfi_batch_with_kernel(
1080            typical_price,
1081            volume,
1082            &MfiBatchRange {
1083                period: (start, end, step),
1084            },
1085            to_batch_kernel(req.kernel),
1086        )
1087        .map_err(|e| IndicatorDispatchError::ComputeFailed {
1088            indicator: "mfi".to_string(),
1089            details: e.to_string(),
1090        })?;
1091        ensure_len("mfi", typical_price.len(), out.cols)?;
1092        let produced_periods: Vec<usize> = out
1093            .combos
1094            .iter()
1095            .map(|combo| combo.period.unwrap_or(14))
1096            .collect();
1097        let values = reorder_or_take_f64_matrix_by_period(
1098            "mfi",
1099            &periods,
1100            &produced_periods,
1101            out.cols,
1102            out.values,
1103        )?;
1104        return Ok(f64_output(output_id, periods.len(), out.cols, values));
1105    }
1106
1107    let kernel = req.kernel.to_non_batch();
1108    collect_f64_into_rows(
1109        "mfi",
1110        output_id,
1111        req.combos,
1112        typical_price.len(),
1113        |params, row| {
1114            let period = get_usize_param("mfi", params, "period", 14)?;
1115            let input = MfiInput::from_slices(
1116                typical_price,
1117                volume,
1118                MfiParams {
1119                    period: Some(period),
1120                },
1121            );
1122            mfi_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1123                indicator: "mfi".to_string(),
1124                details: e.to_string(),
1125            })
1126        },
1127    )
1128}
1129
1130fn compute_mass_batch(
1131    req: IndicatorBatchRequest<'_>,
1132    output_id: &str,
1133) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1134    expect_value_output("mass", output_id)?;
1135    let (high, low) = extract_high_low_input("mass", req.data)?;
1136    let kernel = req.kernel.to_non_batch();
1137    collect_f64("mass", output_id, req.combos, high.len(), |params| {
1138        let period = get_usize_param("mass", params, "period", 5)?;
1139        let input = MassInput::from_slices(
1140            high,
1141            low,
1142            MassParams {
1143                period: Some(period),
1144            },
1145        );
1146        let out = mass_with_kernel(&input, kernel).map_err(|e| {
1147            IndicatorDispatchError::ComputeFailed {
1148                indicator: "mass".to_string(),
1149                details: e.to_string(),
1150            }
1151        })?;
1152        Ok(out.values)
1153    })
1154}
1155
1156fn compute_kvo_batch(
1157    req: IndicatorBatchRequest<'_>,
1158    output_id: &str,
1159) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1160    expect_value_output("kvo", output_id)?;
1161    let (high, low, close, volume) = extract_hlcv_input("kvo", req.data)?;
1162    let kernel = req.kernel.to_non_batch();
1163    collect_f64("kvo", output_id, req.combos, close.len(), |params| {
1164        let short_period = get_usize_param("kvo", params, "short_period", 2)?;
1165        let long_period = get_usize_param("kvo", params, "long_period", 5)?;
1166        let input = KvoInput::from_slices(
1167            high,
1168            low,
1169            close,
1170            volume,
1171            KvoParams {
1172                short_period: Some(short_period),
1173                long_period: Some(long_period),
1174            },
1175        );
1176        let out =
1177            kvo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1178                indicator: "kvo".to_string(),
1179                details: e.to_string(),
1180            })?;
1181        Ok(out.values)
1182    })
1183}
1184
1185fn compute_vosc_batch(
1186    req: IndicatorBatchRequest<'_>,
1187    output_id: &str,
1188) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1189    expect_value_output("vosc", output_id)?;
1190    let volume = extract_volume_input("vosc", req.data)?;
1191    let kernel = req.kernel.to_non_batch();
1192    collect_f64("vosc", output_id, req.combos, volume.len(), |params| {
1193        let short_period = get_usize_param("vosc", params, "short_period", 2)?;
1194        let long_period = get_usize_param("vosc", params, "long_period", 5)?;
1195        let input = VoscInput::from_slice(
1196            volume,
1197            VoscParams {
1198                short_period: Some(short_period),
1199                long_period: Some(long_period),
1200            },
1201        );
1202        let out = vosc_with_kernel(&input, kernel).map_err(|e| {
1203            IndicatorDispatchError::ComputeFailed {
1204                indicator: "vosc".to_string(),
1205                details: e.to_string(),
1206            }
1207        })?;
1208        Ok(out.values)
1209    })
1210}
1211
1212fn compute_dx_batch(
1213    req: IndicatorBatchRequest<'_>,
1214    output_id: &str,
1215) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1216    expect_value_output("dx", output_id)?;
1217    let (high, low, close) = extract_ohlc_input("dx", req.data)?;
1218
1219    let periods = combo_periods("dx", req.combos, "period", 14)?;
1220    if let Some((start, end, step)) = derive_period_sweep(&periods) {
1221        let out = dx_batch_with_kernel(
1222            high,
1223            low,
1224            close,
1225            &DxBatchRange {
1226                period: (start, end, step),
1227            },
1228            to_batch_kernel(req.kernel),
1229        )
1230        .map_err(|e| IndicatorDispatchError::ComputeFailed {
1231            indicator: "dx".to_string(),
1232            details: e.to_string(),
1233        })?;
1234        ensure_len("dx", close.len(), out.cols)?;
1235        let produced_periods: Vec<usize> = out
1236            .combos
1237            .iter()
1238            .map(|combo| combo.period.unwrap_or(14))
1239            .collect();
1240        let values = reorder_or_take_f64_matrix_by_period(
1241            "dx",
1242            &periods,
1243            &produced_periods,
1244            out.cols,
1245            out.values,
1246        )?;
1247        return Ok(f64_output(output_id, periods.len(), out.cols, values));
1248    }
1249
1250    let kernel = req.kernel.to_non_batch();
1251    collect_f64_into_rows("dx", output_id, req.combos, close.len(), |params, row| {
1252        let period = get_usize_param("dx", params, "period", 14)?;
1253        let input = DxInput::from_hlc_slices(
1254            high,
1255            low,
1256            close,
1257            DxParams {
1258                period: Some(period),
1259            },
1260        );
1261        dx_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1262            indicator: "dx".to_string(),
1263            details: e.to_string(),
1264        })
1265    })
1266}
1267
1268fn compute_fosc_batch(
1269    req: IndicatorBatchRequest<'_>,
1270    output_id: &str,
1271) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1272    expect_value_output("fosc", output_id)?;
1273    let data = extract_slice_input("fosc", req.data, "close")?;
1274    let kernel = req.kernel.to_non_batch();
1275    collect_f64("fosc", output_id, req.combos, data.len(), |params| {
1276        let period = get_usize_param("fosc", params, "period", 5)?;
1277        let input = FoscInput::from_slice(
1278            data,
1279            FoscParams {
1280                period: Some(period),
1281            },
1282        );
1283        let out = fosc_with_kernel(&input, kernel).map_err(|e| {
1284            IndicatorDispatchError::ComputeFailed {
1285                indicator: "fosc".to_string(),
1286                details: e.to_string(),
1287            }
1288        })?;
1289        Ok(out.values)
1290    })
1291}
1292
1293fn compute_ift_rsi_batch(
1294    req: IndicatorBatchRequest<'_>,
1295    output_id: &str,
1296) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1297    expect_value_output("ift_rsi", output_id)?;
1298    let data = extract_slice_input("ift_rsi", req.data, "close")?;
1299    let kernel = req.kernel.to_non_batch();
1300    collect_f64("ift_rsi", output_id, req.combos, data.len(), |params| {
1301        let rsi_period = get_usize_param("ift_rsi", params, "rsi_period", 5)?;
1302        let wma_period = get_usize_param("ift_rsi", params, "wma_period", 9)?;
1303        let input = IftRsiInput::from_slice(
1304            data,
1305            IftRsiParams {
1306                rsi_period: Some(rsi_period),
1307                wma_period: Some(wma_period),
1308            },
1309        );
1310        let out = ift_rsi_with_kernel(&input, kernel).map_err(|e| {
1311            IndicatorDispatchError::ComputeFailed {
1312                indicator: "ift_rsi".to_string(),
1313                details: e.to_string(),
1314            }
1315        })?;
1316        Ok(out.values)
1317    })
1318}
1319
1320fn compute_linearreg_angle_batch(
1321    req: IndicatorBatchRequest<'_>,
1322    output_id: &str,
1323) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1324    expect_value_output("linearreg_angle", output_id)?;
1325    let data = extract_slice_input("linearreg_angle", req.data, "close")?;
1326    let kernel = req.kernel.to_non_batch();
1327    collect_f64(
1328        "linearreg_angle",
1329        output_id,
1330        req.combos,
1331        data.len(),
1332        |params| {
1333            let period = get_usize_param("linearreg_angle", params, "period", 14)?;
1334            let input = Linearreg_angleInput::from_slice(
1335                data,
1336                Linearreg_angleParams {
1337                    period: Some(period),
1338                },
1339            );
1340            let out = linearreg_angle_with_kernel(&input, kernel).map_err(|e| {
1341                IndicatorDispatchError::ComputeFailed {
1342                    indicator: "linearreg_angle".to_string(),
1343                    details: e.to_string(),
1344                }
1345            })?;
1346            Ok(out.values)
1347        },
1348    )
1349}
1350
1351fn compute_linearreg_intercept_batch(
1352    req: IndicatorBatchRequest<'_>,
1353    output_id: &str,
1354) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1355    expect_value_output("linearreg_intercept", output_id)?;
1356    let data = extract_slice_input("linearreg_intercept", req.data, "close")?;
1357    let kernel = req.kernel.to_non_batch();
1358    collect_f64(
1359        "linearreg_intercept",
1360        output_id,
1361        req.combos,
1362        data.len(),
1363        |params| {
1364            let period = get_usize_param("linearreg_intercept", params, "period", 14)?;
1365            let input = LinearRegInterceptInput::from_slice(
1366                data,
1367                LinearRegInterceptParams {
1368                    period: Some(period),
1369                },
1370            );
1371            let out = linearreg_intercept_with_kernel(&input, kernel).map_err(|e| {
1372                IndicatorDispatchError::ComputeFailed {
1373                    indicator: "linearreg_intercept".to_string(),
1374                    details: e.to_string(),
1375                }
1376            })?;
1377            Ok(out.values)
1378        },
1379    )
1380}
1381
1382fn compute_linearreg_slope_batch(
1383    req: IndicatorBatchRequest<'_>,
1384    output_id: &str,
1385) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1386    expect_value_output("linearreg_slope", output_id)?;
1387    let data = extract_slice_input("linearreg_slope", req.data, "close")?;
1388    let kernel = req.kernel.to_non_batch();
1389    collect_f64(
1390        "linearreg_slope",
1391        output_id,
1392        req.combos,
1393        data.len(),
1394        |params| {
1395            let period = get_usize_param("linearreg_slope", params, "period", 14)?;
1396            let input = LinearRegSlopeInput::from_slice(
1397                data,
1398                LinearRegSlopeParams {
1399                    period: Some(period),
1400                },
1401            );
1402            let out = linearreg_slope_with_kernel(&input, kernel).map_err(|e| {
1403                IndicatorDispatchError::ComputeFailed {
1404                    indicator: "linearreg_slope".to_string(),
1405                    details: e.to_string(),
1406                }
1407            })?;
1408            Ok(out.values)
1409        },
1410    )
1411}
1412
1413fn compute_cg_batch(
1414    req: IndicatorBatchRequest<'_>,
1415    output_id: &str,
1416) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1417    expect_value_output("cg", output_id)?;
1418    let data = extract_slice_input("cg", req.data, "close")?;
1419    let kernel = req.kernel.to_non_batch();
1420    collect_f64("cg", output_id, req.combos, data.len(), |params| {
1421        let period = get_usize_param("cg", params, "period", 10)?;
1422        let input = CgInput::from_slice(
1423            data,
1424            CgParams {
1425                period: Some(period),
1426            },
1427        );
1428        let out =
1429            cg_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1430                indicator: "cg".to_string(),
1431                details: e.to_string(),
1432            })?;
1433        Ok(out.values)
1434    })
1435}
1436
1437fn compute_rsi_batch(
1438    req: IndicatorBatchRequest<'_>,
1439    output_id: &str,
1440) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1441    expect_value_output("rsi", output_id)?;
1442    let data = extract_slice_input("rsi", req.data, "close")?;
1443    let kernel = req.kernel.to_non_batch();
1444    collect_f64("rsi", output_id, req.combos, data.len(), |params| {
1445        let period = get_usize_param("rsi", params, "period", 14)?;
1446        let input = RsiInput::from_slice(
1447            data,
1448            RsiParams {
1449                period: Some(period),
1450            },
1451        );
1452        let out =
1453            rsi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1454                indicator: "rsi".to_string(),
1455                details: e.to_string(),
1456            })?;
1457        Ok(out.values)
1458    })
1459}
1460
1461fn compute_roc_batch(
1462    req: IndicatorBatchRequest<'_>,
1463    output_id: &str,
1464) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1465    expect_value_output("roc", output_id)?;
1466    let data = extract_slice_input("roc", req.data, "close")?;
1467    let kernel = req.kernel.to_non_batch();
1468    collect_f64("roc", output_id, req.combos, data.len(), |params| {
1469        let period = get_usize_param("roc", params, "period", 9)?;
1470        let input = RocInput::from_slice(
1471            data,
1472            RocParams {
1473                period: Some(period),
1474            },
1475        );
1476        let out =
1477            roc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1478                indicator: "roc".to_string(),
1479                details: e.to_string(),
1480            })?;
1481        Ok(out.values)
1482    })
1483}
1484
1485fn compute_apo_batch(
1486    req: IndicatorBatchRequest<'_>,
1487    output_id: &str,
1488) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1489    expect_value_output("apo", output_id)?;
1490    let data = extract_slice_input("apo", req.data, "close")?;
1491    let kernel = req.kernel.to_non_batch();
1492    collect_f64("apo", output_id, req.combos, data.len(), |params| {
1493        let short_period = get_usize_param("apo", params, "short_period", 10)?;
1494        let long_period = get_usize_param("apo", params, "long_period", 20)?;
1495        let input = ApoInput::from_slice(
1496            data,
1497            ApoParams {
1498                short_period: Some(short_period),
1499                long_period: Some(long_period),
1500            },
1501        );
1502        let out =
1503            apo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1504                indicator: "apo".to_string(),
1505                details: e.to_string(),
1506            })?;
1507        Ok(out.values)
1508    })
1509}
1510
1511fn compute_cci_batch(
1512    req: IndicatorBatchRequest<'_>,
1513    output_id: &str,
1514) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1515    expect_value_output("cci", output_id)?;
1516    let data = extract_slice_input("cci", req.data, "hlc3")?;
1517    let kernel = req.kernel.to_non_batch();
1518    collect_f64("cci", output_id, req.combos, data.len(), |params| {
1519        let period = get_usize_param("cci", params, "period", 14)?;
1520        let input = CciInput::from_slice(
1521            data,
1522            CciParams {
1523                period: Some(period),
1524            },
1525        );
1526        let out =
1527            cci_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1528                indicator: "cci".to_string(),
1529                details: e.to_string(),
1530            })?;
1531        Ok(out.values)
1532    })
1533}
1534
1535fn compute_cfo_batch(
1536    req: IndicatorBatchRequest<'_>,
1537    output_id: &str,
1538) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1539    expect_value_output("cfo", output_id)?;
1540    let data = extract_slice_input("cfo", req.data, "close")?;
1541    let kernel = req.kernel.to_non_batch();
1542    collect_f64("cfo", output_id, req.combos, data.len(), |params| {
1543        let period = get_usize_param("cfo", params, "period", 14)?;
1544        let scalar = get_f64_param("cfo", params, "scalar", 100.0)?;
1545        let input = CfoInput::from_slice(
1546            data,
1547            CfoParams {
1548                period: Some(period),
1549                scalar: Some(scalar),
1550            },
1551        );
1552        let out =
1553            cfo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1554                indicator: "cfo".to_string(),
1555                details: e.to_string(),
1556            })?;
1557        Ok(out.values)
1558    })
1559}
1560
1561fn compute_er_batch(
1562    req: IndicatorBatchRequest<'_>,
1563    output_id: &str,
1564) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1565    expect_value_output("er", output_id)?;
1566    let data = extract_slice_input("er", req.data, "close")?;
1567    let kernel = req.kernel.to_non_batch();
1568    collect_f64("er", output_id, req.combos, data.len(), |params| {
1569        let period = get_usize_param("er", params, "period", 5)?;
1570        let input = ErInput::from_slice(
1571            data,
1572            ErParams {
1573                period: Some(period),
1574            },
1575        );
1576        let out =
1577            er_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1578                indicator: "er".to_string(),
1579                details: e.to_string(),
1580            })?;
1581        Ok(out.values)
1582    })
1583}
1584
1585fn compute_kurtosis_batch(
1586    req: IndicatorBatchRequest<'_>,
1587    output_id: &str,
1588) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1589    expect_value_output("kurtosis", output_id)?;
1590    let data = extract_slice_input("kurtosis", req.data, "hl2")?;
1591    let kernel = req.kernel.to_non_batch();
1592    collect_f64("kurtosis", output_id, req.combos, data.len(), |params| {
1593        let period = get_usize_param("kurtosis", params, "period", 5)?;
1594        let input = KurtosisInput::from_slice(
1595            data,
1596            KurtosisParams {
1597                period: Some(period),
1598            },
1599        );
1600        let out = kurtosis_with_kernel(&input, kernel).map_err(|e| {
1601            IndicatorDispatchError::ComputeFailed {
1602                indicator: "kurtosis".to_string(),
1603                details: e.to_string(),
1604            }
1605        })?;
1606        Ok(out.values)
1607    })
1608}
1609
1610fn compute_natr_batch(
1611    req: IndicatorBatchRequest<'_>,
1612    output_id: &str,
1613) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1614    expect_value_output("natr", output_id)?;
1615    let (high, low, close) = extract_ohlc_input("natr", req.data)?;
1616    let kernel = req.kernel.to_non_batch();
1617    collect_f64("natr", output_id, req.combos, close.len(), |params| {
1618        let period = get_usize_param("natr", params, "period", 14)?;
1619        let input = NatrInput::from_slices(
1620            high,
1621            low,
1622            close,
1623            NatrParams {
1624                period: Some(period),
1625            },
1626        );
1627        let out = natr_with_kernel(&input, kernel).map_err(|e| {
1628            IndicatorDispatchError::ComputeFailed {
1629                indicator: "natr".to_string(),
1630                details: e.to_string(),
1631            }
1632        })?;
1633        Ok(out.values)
1634    })
1635}
1636
1637fn compute_mean_ad_batch(
1638    req: IndicatorBatchRequest<'_>,
1639    output_id: &str,
1640) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1641    expect_value_output("mean_ad", output_id)?;
1642    let data = extract_slice_input("mean_ad", req.data, "close")?;
1643    let kernel = req.kernel.to_non_batch();
1644    collect_f64("mean_ad", output_id, req.combos, data.len(), |params| {
1645        let period = get_usize_param("mean_ad", params, "period", 5)?;
1646        let input = MeanAdInput::from_slice(
1647            data,
1648            MeanAdParams {
1649                period: Some(period),
1650            },
1651        );
1652        let out = mean_ad_with_kernel(&input, kernel).map_err(|e| {
1653            IndicatorDispatchError::ComputeFailed {
1654                indicator: "mean_ad".to_string(),
1655                details: e.to_string(),
1656            }
1657        })?;
1658        Ok(out.values)
1659    })
1660}
1661
1662fn compute_medium_ad_batch(
1663    req: IndicatorBatchRequest<'_>,
1664    output_id: &str,
1665) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1666    expect_value_output("medium_ad", output_id)?;
1667    let data = extract_slice_input("medium_ad", req.data, "close")?;
1668    let kernel = req.kernel.to_non_batch();
1669    collect_f64("medium_ad", output_id, req.combos, data.len(), |params| {
1670        let period = get_usize_param("medium_ad", params, "period", 5)?;
1671        let input = MediumAdInput::from_slice(
1672            data,
1673            MediumAdParams {
1674                period: Some(period),
1675            },
1676        );
1677        let out = medium_ad_with_kernel(&input, kernel).map_err(|e| {
1678            IndicatorDispatchError::ComputeFailed {
1679                indicator: "medium_ad".to_string(),
1680                details: e.to_string(),
1681            }
1682        })?;
1683        Ok(out.values)
1684    })
1685}
1686
1687fn compute_deviation_batch(
1688    req: IndicatorBatchRequest<'_>,
1689    output_id: &str,
1690) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1691    expect_value_output("deviation", output_id)?;
1692    let data = extract_slice_input("deviation", req.data, "close")?;
1693    let kernel = req.kernel.to_non_batch();
1694    collect_f64("deviation", output_id, req.combos, data.len(), |params| {
1695        let period = get_usize_param("deviation", params, "period", 9)?;
1696        let devtype = get_usize_param("deviation", params, "devtype", 0)?;
1697        let input = DeviationInput::from_slice(
1698            data,
1699            DeviationParams {
1700                period: Some(period),
1701                devtype: Some(devtype),
1702            },
1703        );
1704        let out = deviation_with_kernel(&input, kernel).map_err(|e| {
1705            IndicatorDispatchError::ComputeFailed {
1706                indicator: "deviation".to_string(),
1707                details: e.to_string(),
1708            }
1709        })?;
1710        Ok(out.values)
1711    })
1712}
1713
1714fn compute_dpo_batch(
1715    req: IndicatorBatchRequest<'_>,
1716    output_id: &str,
1717) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1718    expect_value_output("dpo", output_id)?;
1719    let data = extract_slice_input("dpo", req.data, "close")?;
1720    let kernel = req.kernel.to_non_batch();
1721    collect_f64("dpo", output_id, req.combos, data.len(), |params| {
1722        let period = get_usize_param("dpo", params, "period", 5)?;
1723        let input = DpoInput::from_slice(
1724            data,
1725            DpoParams {
1726                period: Some(period),
1727            },
1728        );
1729        let out =
1730            dpo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1731                indicator: "dpo".to_string(),
1732                details: e.to_string(),
1733            })?;
1734        Ok(out.values)
1735    })
1736}
1737
1738fn compute_pfe_batch(
1739    req: IndicatorBatchRequest<'_>,
1740    output_id: &str,
1741) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1742    expect_value_output("pfe", output_id)?;
1743    let data = extract_slice_input("pfe", req.data, "close")?;
1744    let kernel = req.kernel.to_non_batch();
1745    collect_f64("pfe", output_id, req.combos, data.len(), |params| {
1746        let period = get_usize_param("pfe", params, "period", 10)?;
1747        let smoothing = get_usize_param("pfe", params, "smoothing", 5)?;
1748        let input = PfeInput::from_slice(
1749            data,
1750            PfeParams {
1751                period: Some(period),
1752                smoothing: Some(smoothing),
1753            },
1754        );
1755        let out =
1756            pfe_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1757                indicator: "pfe".to_string(),
1758                details: e.to_string(),
1759            })?;
1760        Ok(out.values)
1761    })
1762}
1763
1764fn compute_percentile_nearest_rank_batch(
1765    req: IndicatorBatchRequest<'_>,
1766    output_id: &str,
1767) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1768    expect_value_output("percentile_nearest_rank", output_id)?;
1769    let data = extract_slice_input("percentile_nearest_rank", req.data, "close")?;
1770    let kernel = req.kernel.to_non_batch();
1771    collect_f64(
1772        "percentile_nearest_rank",
1773        output_id,
1774        req.combos,
1775        data.len(),
1776        |params| {
1777            let length = get_usize_param("percentile_nearest_rank", params, "length", 15)?;
1778            let percentage = get_f64_param("percentile_nearest_rank", params, "percentage", 50.0)?;
1779            let input = PercentileNearestRankInput::from_slice(
1780                data,
1781                PercentileNearestRankParams {
1782                    length: Some(length),
1783                    percentage: Some(percentage),
1784                },
1785            );
1786            let out = percentile_nearest_rank_with_kernel(&input, kernel).map_err(|e| {
1787                IndicatorDispatchError::ComputeFailed {
1788                    indicator: "percentile_nearest_rank".to_string(),
1789                    details: e.to_string(),
1790                }
1791            })?;
1792            Ok(out.values)
1793        },
1794    )
1795}
1796
1797fn compute_obv_batch(
1798    req: IndicatorBatchRequest<'_>,
1799    output_id: &str,
1800) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1801    expect_value_output("obv", output_id)?;
1802    let (close, volume) = extract_close_volume_input("obv", req.data, "close")?;
1803    let kernel = req.kernel.to_non_batch();
1804    collect_f64("obv", output_id, req.combos, close.len(), |_params| {
1805        let input = ObvInput::from_slices(close, volume, ObvParams::default());
1806        let out =
1807            obv_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1808                indicator: "obv".to_string(),
1809                details: e.to_string(),
1810            })?;
1811        Ok(out.values)
1812    })
1813}
1814
1815fn compute_vpt_batch(
1816    req: IndicatorBatchRequest<'_>,
1817    output_id: &str,
1818) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1819    expect_value_output("vpt", output_id)?;
1820    let (close, volume) = extract_close_volume_input("vpt", req.data, "close")?;
1821    let kernel = req.kernel.to_non_batch();
1822    collect_f64("vpt", output_id, req.combos, close.len(), |_params| {
1823        let input = VptInput::from_slices(close, volume);
1824        let out =
1825            vpt_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1826                indicator: "vpt".to_string(),
1827                details: e.to_string(),
1828            })?;
1829        Ok(out.values)
1830    })
1831}
1832
1833fn compute_nvi_batch(
1834    req: IndicatorBatchRequest<'_>,
1835    output_id: &str,
1836) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1837    expect_value_output("nvi", output_id)?;
1838    let (close, volume) = extract_close_volume_input("nvi", req.data, "close")?;
1839    let kernel = req.kernel.to_non_batch();
1840    collect_f64("nvi", output_id, req.combos, close.len(), |_params| {
1841        let input = NviInput::from_slices(close, volume, NviParams::default());
1842        let out =
1843            nvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1844                indicator: "nvi".to_string(),
1845                details: e.to_string(),
1846            })?;
1847        Ok(out.values)
1848    })
1849}
1850
1851fn compute_pvi_batch(
1852    req: IndicatorBatchRequest<'_>,
1853    output_id: &str,
1854) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1855    expect_value_output("pvi", output_id)?;
1856    let (close, volume) = extract_close_volume_input("pvi", req.data, "close")?;
1857    let kernel = req.kernel.to_non_batch();
1858    collect_f64("pvi", output_id, req.combos, close.len(), |params| {
1859        let initial_value = get_f64_param("pvi", params, "initial_value", 1000.0)?;
1860        let input = PviInput::from_slices(
1861            close,
1862            volume,
1863            PviParams {
1864                initial_value: Some(initial_value),
1865            },
1866        );
1867        let out =
1868            pvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1869                indicator: "pvi".to_string(),
1870                details: e.to_string(),
1871            })?;
1872        Ok(out.values)
1873    })
1874}
1875
1876fn compute_wclprice_batch(
1877    req: IndicatorBatchRequest<'_>,
1878    output_id: &str,
1879) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1880    expect_value_output("wclprice", output_id)?;
1881    let (high, low, close) = extract_ohlc_input("wclprice", req.data)?;
1882    let kernel = req.kernel.to_non_batch();
1883    collect_f64("wclprice", output_id, req.combos, close.len(), |_params| {
1884        let input = WclpriceInput::from_slices(high, low, close);
1885        let out = wclprice_with_kernel(&input, kernel).map_err(|e| {
1886            IndicatorDispatchError::ComputeFailed {
1887                indicator: "wclprice".to_string(),
1888                details: e.to_string(),
1889            }
1890        })?;
1891        Ok(out.values)
1892    })
1893}
1894
1895fn compute_ui_batch(
1896    req: IndicatorBatchRequest<'_>,
1897    output_id: &str,
1898) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1899    expect_value_output("ui", output_id)?;
1900    let data = extract_slice_input("ui", req.data, "close")?;
1901    let kernel = req.kernel.to_non_batch();
1902    collect_f64("ui", output_id, req.combos, data.len(), |params| {
1903        let period = get_usize_param("ui", params, "period", 14)?;
1904        let scalar = get_f64_param("ui", params, "scalar", 100.0)?;
1905        let input = UiInput::from_slice(
1906            data,
1907            UiParams {
1908                period: Some(period),
1909                scalar: Some(scalar),
1910            },
1911        );
1912        let out =
1913            ui_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
1914                indicator: "ui".to_string(),
1915                details: e.to_string(),
1916            })?;
1917        Ok(out.values)
1918    })
1919}
1920
1921fn compute_zscore_batch(
1922    req: IndicatorBatchRequest<'_>,
1923    output_id: &str,
1924) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1925    expect_value_output("zscore", output_id)?;
1926    let data = extract_slice_input("zscore", req.data, "close")?;
1927    let kernel = req.kernel.to_non_batch();
1928    collect_f64("zscore", output_id, req.combos, data.len(), |params| {
1929        let period = get_usize_param("zscore", params, "period", 14)?;
1930        let ma_type = get_enum_param("zscore", params, "ma_type", "sma")?;
1931        let nbdev = get_f64_param("zscore", params, "nbdev", 1.0)?;
1932        let devtype = get_usize_param("zscore", params, "devtype", 0)?;
1933        let input = ZscoreInput::from_slice(
1934            data,
1935            ZscoreParams {
1936                period: Some(period),
1937                ma_type: Some(ma_type),
1938                nbdev: Some(nbdev),
1939                devtype: Some(devtype),
1940            },
1941        );
1942        let out = zscore_with_kernel(&input, kernel).map_err(|e| {
1943            IndicatorDispatchError::ComputeFailed {
1944                indicator: "zscore".to_string(),
1945                details: e.to_string(),
1946            }
1947        })?;
1948        Ok(out.values)
1949    })
1950}
1951
1952fn compute_medprice_batch(
1953    req: IndicatorBatchRequest<'_>,
1954    output_id: &str,
1955) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1956    expect_value_output("medprice", output_id)?;
1957    let (high, low) = extract_high_low_input("medprice", req.data)?;
1958    let kernel = req.kernel.to_non_batch();
1959    collect_f64("medprice", output_id, req.combos, high.len(), |_params| {
1960        let input = MedpriceInput::from_slices(high, low, MedpriceParams::default());
1961        let out = medprice_with_kernel(&input, kernel).map_err(|e| {
1962            IndicatorDispatchError::ComputeFailed {
1963                indicator: "medprice".to_string(),
1964                details: e.to_string(),
1965            }
1966        })?;
1967        Ok(out.values)
1968    })
1969}
1970
1971fn compute_midpoint_batch(
1972    req: IndicatorBatchRequest<'_>,
1973    output_id: &str,
1974) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1975    expect_value_output("midpoint", output_id)?;
1976    let data = extract_slice_input("midpoint", req.data, "close")?;
1977    let kernel = req.kernel.to_non_batch();
1978    collect_f64("midpoint", output_id, req.combos, data.len(), |params| {
1979        let period = get_usize_param("midpoint", params, "period", 14)?;
1980        let input = MidpointInput::from_slice(
1981            data,
1982            MidpointParams {
1983                period: Some(period),
1984            },
1985        );
1986        let out = midpoint_with_kernel(&input, kernel).map_err(|e| {
1987            IndicatorDispatchError::ComputeFailed {
1988                indicator: "midpoint".to_string(),
1989                details: e.to_string(),
1990            }
1991        })?;
1992        Ok(out.values)
1993    })
1994}
1995
1996fn compute_midprice_batch(
1997    req: IndicatorBatchRequest<'_>,
1998    output_id: &str,
1999) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2000    expect_value_output("midprice", output_id)?;
2001    let (high, low) = extract_high_low_input("midprice", req.data)?;
2002    let kernel = req.kernel.to_non_batch();
2003    collect_f64("midprice", output_id, req.combos, high.len(), |params| {
2004        let period = get_usize_param("midprice", params, "period", 14)?;
2005        let input = MidpriceInput::from_slices(
2006            high,
2007            low,
2008            MidpriceParams {
2009                period: Some(period),
2010            },
2011        );
2012        let out = midprice_with_kernel(&input, kernel).map_err(|e| {
2013            IndicatorDispatchError::ComputeFailed {
2014                indicator: "midprice".to_string(),
2015                details: e.to_string(),
2016            }
2017        })?;
2018        Ok(out.values)
2019    })
2020}
2021
2022fn compute_mom_batch(
2023    req: IndicatorBatchRequest<'_>,
2024    output_id: &str,
2025) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2026    expect_value_output("mom", output_id)?;
2027    let data = extract_slice_input("mom", req.data, "close")?;
2028    let kernel = req.kernel.to_non_batch();
2029    collect_f64("mom", output_id, req.combos, data.len(), |params| {
2030        let period = get_usize_param("mom", params, "period", 10)?;
2031        let input = MomInput::from_slice(
2032            data,
2033            MomParams {
2034                period: Some(period),
2035            },
2036        );
2037        let out =
2038            mom_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2039                indicator: "mom".to_string(),
2040                details: e.to_string(),
2041            })?;
2042        Ok(out.values)
2043    })
2044}
2045
2046fn compute_cmo_batch(
2047    req: IndicatorBatchRequest<'_>,
2048    output_id: &str,
2049) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2050    expect_value_output("cmo", output_id)?;
2051    let data = extract_slice_input("cmo", req.data, "close")?;
2052    let kernel = req.kernel.to_non_batch();
2053    collect_f64("cmo", output_id, req.combos, data.len(), |params| {
2054        let period = get_usize_param("cmo", params, "period", 14)?;
2055        let input = CmoInput::from_slice(
2056            data,
2057            CmoParams {
2058                period: Some(period),
2059            },
2060        );
2061        let out =
2062            cmo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2063                indicator: "cmo".to_string(),
2064                details: e.to_string(),
2065            })?;
2066        Ok(out.values)
2067    })
2068}
2069
2070fn compute_rocp_batch(
2071    req: IndicatorBatchRequest<'_>,
2072    output_id: &str,
2073) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2074    expect_value_output("rocp", output_id)?;
2075    let data = extract_slice_input("rocp", req.data, "close")?;
2076    let kernel = req.kernel.to_non_batch();
2077    collect_f64("rocp", output_id, req.combos, data.len(), |params| {
2078        let period = get_usize_param("rocp", params, "period", 10)?;
2079        let input = RocpInput::from_slice(
2080            data,
2081            RocpParams {
2082                period: Some(period),
2083            },
2084        );
2085        let out = rocp_with_kernel(&input, kernel).map_err(|e| {
2086            IndicatorDispatchError::ComputeFailed {
2087                indicator: "rocp".to_string(),
2088                details: e.to_string(),
2089            }
2090        })?;
2091        Ok(out.values)
2092    })
2093}
2094
2095fn compute_rocr_batch(
2096    req: IndicatorBatchRequest<'_>,
2097    output_id: &str,
2098) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2099    expect_value_output("rocr", output_id)?;
2100    let data = extract_slice_input("rocr", req.data, "close")?;
2101    let kernel = req.kernel.to_non_batch();
2102    collect_f64("rocr", output_id, req.combos, data.len(), |params| {
2103        let period = get_usize_param("rocr", params, "period", 10)?;
2104        let input = RocrInput::from_slice(
2105            data,
2106            RocrParams {
2107                period: Some(period),
2108            },
2109        );
2110        let out = rocr_with_kernel(&input, kernel).map_err(|e| {
2111            IndicatorDispatchError::ComputeFailed {
2112                indicator: "rocr".to_string(),
2113                details: e.to_string(),
2114            }
2115        })?;
2116        Ok(out.values)
2117    })
2118}
2119
2120fn compute_ppo_batch(
2121    req: IndicatorBatchRequest<'_>,
2122    output_id: &str,
2123) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2124    expect_value_output("ppo", output_id)?;
2125    let data = extract_slice_input("ppo", req.data, "close")?;
2126    let kernel = req.kernel.to_non_batch();
2127    collect_f64("ppo", output_id, req.combos, data.len(), |params| {
2128        let fast_period = get_usize_param("ppo", params, "fast_period", 12)?;
2129        let slow_period = get_usize_param("ppo", params, "slow_period", 26)?;
2130        let ma_type = get_enum_param("ppo", params, "ma_type", "sma")?;
2131        let input = PpoInput::from_slice(
2132            data,
2133            PpoParams {
2134                fast_period: Some(fast_period),
2135                slow_period: Some(slow_period),
2136                ma_type: Some(ma_type),
2137            },
2138        );
2139        let out =
2140            ppo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2141                indicator: "ppo".to_string(),
2142                details: e.to_string(),
2143            })?;
2144        Ok(out.values)
2145    })
2146}
2147
2148fn compute_trix_batch(
2149    req: IndicatorBatchRequest<'_>,
2150    output_id: &str,
2151) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2152    expect_value_output("trix", output_id)?;
2153    let data = extract_slice_input("trix", req.data, "close")?;
2154    let periods = combo_periods("trix", req.combos, "period", 18)?;
2155    if let Some((start, end, step)) = derive_period_sweep(&periods) {
2156        let out = trix_batch_with_kernel(
2157            data,
2158            &TrixBatchRange {
2159                period: (start, end, step),
2160            },
2161            to_batch_kernel(req.kernel),
2162        )
2163        .map_err(|e| IndicatorDispatchError::ComputeFailed {
2164            indicator: "trix".to_string(),
2165            details: e.to_string(),
2166        })?;
2167        ensure_len("trix", data.len(), out.cols)?;
2168        let produced_periods: Vec<usize> = out
2169            .combos
2170            .iter()
2171            .map(|combo| combo.period.unwrap_or(18))
2172            .collect();
2173        let values = reorder_or_take_f64_matrix_by_period(
2174            "trix",
2175            &periods,
2176            &produced_periods,
2177            out.cols,
2178            out.values,
2179        )?;
2180        return Ok(f64_output(output_id, periods.len(), out.cols, values));
2181    }
2182
2183    let kernel = req.kernel.to_non_batch();
2184    collect_f64_into_rows("trix", output_id, req.combos, data.len(), |params, row| {
2185        let period = get_usize_param("trix", params, "period", 18)?;
2186        let input = TrixInput::from_slice(
2187            data,
2188            TrixParams {
2189                period: Some(period),
2190            },
2191        );
2192        trix_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2193            indicator: "trix".to_string(),
2194            details: e.to_string(),
2195        })
2196    })
2197}
2198
2199fn compute_tsi_batch(
2200    req: IndicatorBatchRequest<'_>,
2201    output_id: &str,
2202) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2203    expect_value_output("tsi", output_id)?;
2204    let data = extract_slice_input("tsi", req.data, "close")?;
2205    let kernel = req.kernel.to_non_batch();
2206    collect_f64("tsi", output_id, req.combos, data.len(), |params| {
2207        let long_period = get_usize_param("tsi", params, "long_period", 25)?;
2208        let short_period = get_usize_param("tsi", params, "short_period", 13)?;
2209        let input = TsiInput::from_slice(
2210            data,
2211            TsiParams {
2212                long_period: Some(long_period),
2213                short_period: Some(short_period),
2214            },
2215        );
2216        let out =
2217            tsi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2218                indicator: "tsi".to_string(),
2219                details: e.to_string(),
2220            })?;
2221        Ok(out.values)
2222    })
2223}
2224
2225fn compute_tsf_batch(
2226    req: IndicatorBatchRequest<'_>,
2227    output_id: &str,
2228) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2229    expect_value_output("tsf", output_id)?;
2230    let data = extract_slice_input("tsf", req.data, "close")?;
2231    let kernel = req.kernel.to_non_batch();
2232    collect_f64("tsf", output_id, req.combos, data.len(), |params| {
2233        let period = get_usize_param("tsf", params, "period", 14)?;
2234        let input = TsfInput::from_slice(
2235            data,
2236            TsfParams {
2237                period: Some(period),
2238            },
2239        );
2240        let out =
2241            tsf_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2242                indicator: "tsf".to_string(),
2243                details: e.to_string(),
2244            })?;
2245        Ok(out.values)
2246    })
2247}
2248
2249fn compute_stddev_batch(
2250    req: IndicatorBatchRequest<'_>,
2251    output_id: &str,
2252) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2253    expect_value_output("stddev", output_id)?;
2254    let data = extract_slice_input("stddev", req.data, "close")?;
2255    let kernel = req.kernel.to_non_batch();
2256    collect_f64("stddev", output_id, req.combos, data.len(), |params| {
2257        let period = get_usize_param("stddev", params, "period", 5)?;
2258        let nbdev = get_f64_param("stddev", params, "nbdev", 1.0)?;
2259        let input = StdDevInput::from_slice(
2260            data,
2261            StdDevParams {
2262                period: Some(period),
2263                nbdev: Some(nbdev),
2264            },
2265        );
2266        let out = stddev_with_kernel(&input, kernel).map_err(|e| {
2267            IndicatorDispatchError::ComputeFailed {
2268                indicator: "stddev".to_string(),
2269                details: e.to_string(),
2270            }
2271        })?;
2272        Ok(out.values)
2273    })
2274}
2275
2276fn compute_var_batch(
2277    req: IndicatorBatchRequest<'_>,
2278    output_id: &str,
2279) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2280    expect_value_output("var", output_id)?;
2281    let data = extract_slice_input("var", req.data, "close")?;
2282    let kernel = req.kernel.to_non_batch();
2283    collect_f64("var", output_id, req.combos, data.len(), |params| {
2284        let period = get_usize_param("var", params, "period", 14)?;
2285        let nbdev = get_f64_param("var", params, "nbdev", 1.0)?;
2286        let input = VarInput::from_slice(
2287            data,
2288            VarParams {
2289                period: Some(period),
2290                nbdev: Some(nbdev),
2291            },
2292        );
2293        let out =
2294            var_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2295                indicator: "var".to_string(),
2296                details: e.to_string(),
2297            })?;
2298        Ok(out.values)
2299    })
2300}
2301
2302fn compute_willr_batch(
2303    req: IndicatorBatchRequest<'_>,
2304    output_id: &str,
2305) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2306    expect_value_output("willr", output_id)?;
2307    let (high, low, close) = extract_ohlc_input("willr", req.data)?;
2308    let kernel = req.kernel.to_non_batch();
2309    collect_f64("willr", output_id, req.combos, close.len(), |params| {
2310        let period = get_usize_param("willr", params, "period", 14)?;
2311        let input = WillrInput::from_slices(
2312            high,
2313            low,
2314            close,
2315            WillrParams {
2316                period: Some(period),
2317            },
2318        );
2319        let out = willr_with_kernel(&input, kernel).map_err(|e| {
2320            IndicatorDispatchError::ComputeFailed {
2321                indicator: "willr".to_string(),
2322                details: e.to_string(),
2323            }
2324        })?;
2325        Ok(out.values)
2326    })
2327}
2328
2329fn compute_ultosc_batch(
2330    req: IndicatorBatchRequest<'_>,
2331    output_id: &str,
2332) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2333    expect_value_output("ultosc", output_id)?;
2334    let (high, low, close) = extract_ohlc_input("ultosc", req.data)?;
2335    let kernel = req.kernel.to_non_batch();
2336    collect_f64("ultosc", output_id, req.combos, close.len(), |params| {
2337        let timeperiod1 = get_usize_param("ultosc", params, "timeperiod1", 7)?;
2338        let timeperiod2 = get_usize_param("ultosc", params, "timeperiod2", 14)?;
2339        let timeperiod3 = get_usize_param("ultosc", params, "timeperiod3", 28)?;
2340        let input = UltOscInput::from_slices(
2341            high,
2342            low,
2343            close,
2344            UltOscParams {
2345                timeperiod1: Some(timeperiod1),
2346                timeperiod2: Some(timeperiod2),
2347                timeperiod3: Some(timeperiod3),
2348            },
2349        );
2350        let out = ultosc_with_kernel(&input, kernel).map_err(|e| {
2351            IndicatorDispatchError::ComputeFailed {
2352                indicator: "ultosc".to_string(),
2353                details: e.to_string(),
2354            }
2355        })?;
2356        Ok(out.values)
2357    })
2358}
2359
2360fn compute_adx_batch(
2361    req: IndicatorBatchRequest<'_>,
2362    output_id: &str,
2363) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2364    expect_value_output("adx", output_id)?;
2365    let (high, low, close) = extract_ohlc_input("adx", req.data)?;
2366    let kernel = req.kernel.to_non_batch();
2367    collect_f64("adx", output_id, req.combos, close.len(), |params| {
2368        let period = get_usize_param("adx", params, "period", 14)?;
2369        let input = AdxInput::from_slices(
2370            high,
2371            low,
2372            close,
2373            AdxParams {
2374                period: Some(period),
2375            },
2376        );
2377        let out =
2378            adx_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2379                indicator: "adx".to_string(),
2380                details: e.to_string(),
2381            })?;
2382        Ok(out.values)
2383    })
2384}
2385
2386fn compute_atr_batch(
2387    req: IndicatorBatchRequest<'_>,
2388    output_id: &str,
2389) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2390    expect_value_output("atr", output_id)?;
2391    let (high, low, close) = extract_ohlc_input("atr", req.data)?;
2392    let kernel = req.kernel.to_non_batch();
2393    collect_f64("atr", output_id, req.combos, close.len(), |params| {
2394        let length = get_usize_param("atr", params, "length", 14)?;
2395        let input = AtrInput::from_slices(
2396            high,
2397            low,
2398            close,
2399            AtrParams {
2400                length: Some(length),
2401            },
2402        );
2403        let out =
2404            atr_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2405                indicator: "atr".to_string(),
2406                details: e.to_string(),
2407            })?;
2408        Ok(out.values)
2409    })
2410}
2411
2412fn compute_macd_batch(
2413    req: IndicatorBatchRequest<'_>,
2414    output_id: &str,
2415) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2416    let data = extract_slice_input("macd", req.data, "close")?;
2417    let kernel = req.kernel.to_non_batch();
2418    collect_f64("macd", output_id, req.combos, data.len(), |params| {
2419        let fast_period = get_usize_param("macd", params, "fast_period", 12)?;
2420        let slow_period = get_usize_param("macd", params, "slow_period", 26)?;
2421        let signal_period = get_usize_param("macd", params, "signal_period", 9)?;
2422        let ma_type = get_enum_param("macd", params, "ma_type", "ema")?;
2423        let input = MacdInput::from_slice(
2424            data,
2425            MacdParams {
2426                fast_period: Some(fast_period),
2427                slow_period: Some(slow_period),
2428                signal_period: Some(signal_period),
2429                ma_type: Some(ma_type),
2430            },
2431        );
2432        let out = macd_with_kernel(&input, kernel).map_err(|e| {
2433            IndicatorDispatchError::ComputeFailed {
2434                indicator: "macd".to_string(),
2435                details: e.to_string(),
2436            }
2437        })?;
2438        if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
2439            return Ok(out.macd);
2440        }
2441        if output_id.eq_ignore_ascii_case("signal") {
2442            return Ok(out.signal);
2443        }
2444        if output_id.eq_ignore_ascii_case("hist") {
2445            return Ok(out.hist);
2446        }
2447        Err(IndicatorDispatchError::UnknownOutput {
2448            indicator: "macd".to_string(),
2449            output: output_id.to_string(),
2450        })
2451    })
2452}
2453
2454fn compute_bollinger_batch(
2455    req: IndicatorBatchRequest<'_>,
2456    output_id: &str,
2457) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2458    let data = extract_slice_input("bollinger_bands", req.data, "close")?;
2459    let kernel = req.kernel.to_non_batch();
2460    collect_f64(
2461        "bollinger_bands",
2462        output_id,
2463        req.combos,
2464        data.len(),
2465        |params| {
2466            let period = get_usize_param("bollinger_bands", params, "period", 20)?;
2467            let devup = get_f64_param("bollinger_bands", params, "devup", 2.0)?;
2468            let devdn = get_f64_param("bollinger_bands", params, "devdn", 2.0)?;
2469            let matype = get_enum_param("bollinger_bands", params, "matype", "sma")?;
2470            let devtype = get_usize_param("bollinger_bands", params, "devtype", 0)?;
2471            let input = BollingerBandsInput::from_slice(
2472                data,
2473                BollingerBandsParams {
2474                    period: Some(period),
2475                    devup: Some(devup),
2476                    devdn: Some(devdn),
2477                    matype: Some(matype),
2478                    devtype: Some(devtype),
2479                },
2480            );
2481            let out = bollinger_bands_with_kernel(&input, kernel).map_err(|e| {
2482                IndicatorDispatchError::ComputeFailed {
2483                    indicator: "bollinger_bands".to_string(),
2484                    details: e.to_string(),
2485                }
2486            })?;
2487            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
2488                return Ok(out.upper_band);
2489            }
2490            if output_id.eq_ignore_ascii_case("middle") {
2491                return Ok(out.middle_band);
2492            }
2493            if output_id.eq_ignore_ascii_case("lower") {
2494                return Ok(out.lower_band);
2495            }
2496            Err(IndicatorDispatchError::UnknownOutput {
2497                indicator: "bollinger_bands".to_string(),
2498                output: output_id.to_string(),
2499            })
2500        },
2501    )
2502}
2503
2504fn compute_stoch_batch(
2505    req: IndicatorBatchRequest<'_>,
2506    output_id: &str,
2507) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2508    let (high, low, close) = extract_ohlc_input("stoch", req.data)?;
2509    let kernel = req.kernel.to_non_batch();
2510    collect_f64("stoch", output_id, req.combos, close.len(), |params| {
2511        let fastk_period = get_usize_param("stoch", params, "fastk_period", 14)?;
2512        let slowk_period = get_usize_param("stoch", params, "slowk_period", 3)?;
2513        let slowd_period = get_usize_param("stoch", params, "slowd_period", 3)?;
2514        let slowk_ma_type = get_enum_param("stoch", params, "slowk_ma_type", "sma")?;
2515        let slowd_ma_type = get_enum_param("stoch", params, "slowd_ma_type", "sma")?;
2516        let input = StochInput::from_slices(
2517            high,
2518            low,
2519            close,
2520            StochParams {
2521                fastk_period: Some(fastk_period),
2522                slowk_period: Some(slowk_period),
2523                slowk_ma_type: Some(slowk_ma_type),
2524                slowd_period: Some(slowd_period),
2525                slowd_ma_type: Some(slowd_ma_type),
2526            },
2527        );
2528        let out = stoch_with_kernel(&input, kernel).map_err(|e| {
2529            IndicatorDispatchError::ComputeFailed {
2530                indicator: "stoch".to_string(),
2531                details: e.to_string(),
2532            }
2533        })?;
2534        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
2535            return Ok(out.k);
2536        }
2537        if output_id.eq_ignore_ascii_case("d") {
2538            return Ok(out.d);
2539        }
2540        Err(IndicatorDispatchError::UnknownOutput {
2541            indicator: "stoch".to_string(),
2542            output: output_id.to_string(),
2543        })
2544    })
2545}
2546
2547fn compute_stochf_batch(
2548    req: IndicatorBatchRequest<'_>,
2549    output_id: &str,
2550) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2551    let (high, low, close) = extract_ohlc_input("stochf", req.data)?;
2552    let kernel = req.kernel.to_non_batch();
2553    collect_f64("stochf", output_id, req.combos, close.len(), |params| {
2554        let fastk_period = get_usize_param("stochf", params, "fastk_period", 5)?;
2555        let fastd_period = get_usize_param("stochf", params, "fastd_period", 3)?;
2556        let fastd_matype = get_usize_param("stochf", params, "fastd_matype", 0)?;
2557        let input = StochfInput::from_slices(
2558            high,
2559            low,
2560            close,
2561            StochfParams {
2562                fastk_period: Some(fastk_period),
2563                fastd_period: Some(fastd_period),
2564                fastd_matype: Some(fastd_matype),
2565            },
2566        );
2567        let out = stochf_with_kernel(&input, kernel).map_err(|e| {
2568            IndicatorDispatchError::ComputeFailed {
2569                indicator: "stochf".to_string(),
2570                details: e.to_string(),
2571            }
2572        })?;
2573        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
2574            return Ok(out.k);
2575        }
2576        if output_id.eq_ignore_ascii_case("d") {
2577            return Ok(out.d);
2578        }
2579        Err(IndicatorDispatchError::UnknownOutput {
2580            indicator: "stochf".to_string(),
2581            output: output_id.to_string(),
2582        })
2583    })
2584}
2585
2586fn compute_vwmacd_batch(
2587    req: IndicatorBatchRequest<'_>,
2588    output_id: &str,
2589) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2590    let (close, volume) = extract_close_volume_input("vwmacd", req.data, "close")?;
2591    let kernel = req.kernel.to_non_batch();
2592    collect_f64("vwmacd", output_id, req.combos, close.len(), |params| {
2593        let fast_period =
2594            get_usize_param_with_aliases("vwmacd", params, &["fast", "fast_period"], 12)?;
2595        let slow_period =
2596            get_usize_param_with_aliases("vwmacd", params, &["slow", "slow_period"], 26)?;
2597        let signal_period =
2598            get_usize_param_with_aliases("vwmacd", params, &["signal", "signal_period"], 9)?;
2599        let fast_ma_type = get_enum_param("vwmacd", params, "fast_ma_type", "sma")?;
2600        let slow_ma_type = get_enum_param("vwmacd", params, "slow_ma_type", "sma")?;
2601        let signal_ma_type = get_enum_param("vwmacd", params, "signal_ma_type", "ema")?;
2602        let input = VwmacdInput::from_slices(
2603            close,
2604            volume,
2605            VwmacdParams {
2606                fast_period: Some(fast_period),
2607                slow_period: Some(slow_period),
2608                signal_period: Some(signal_period),
2609                fast_ma_type: Some(fast_ma_type),
2610                slow_ma_type: Some(slow_ma_type),
2611                signal_ma_type: Some(signal_ma_type),
2612            },
2613        );
2614        let out = vwmacd_with_kernel(&input, kernel).map_err(|e| {
2615            IndicatorDispatchError::ComputeFailed {
2616                indicator: "vwmacd".to_string(),
2617                details: e.to_string(),
2618            }
2619        })?;
2620        if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
2621            return Ok(out.macd);
2622        }
2623        if output_id.eq_ignore_ascii_case("signal") {
2624            return Ok(out.signal);
2625        }
2626        if output_id.eq_ignore_ascii_case("hist") {
2627            return Ok(out.hist);
2628        }
2629        Err(IndicatorDispatchError::UnknownOutput {
2630            indicator: "vwmacd".to_string(),
2631            output: output_id.to_string(),
2632        })
2633    })
2634}
2635
2636fn compute_vpci_batch(
2637    req: IndicatorBatchRequest<'_>,
2638    output_id: &str,
2639) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2640    let (close, volume) = extract_close_volume_input("vpci", req.data, "close")?;
2641    let kernel = req.kernel.to_non_batch();
2642    collect_f64("vpci", output_id, req.combos, close.len(), |params| {
2643        let short_range = get_usize_param("vpci", params, "short_range", 5)?;
2644        let long_range = get_usize_param("vpci", params, "long_range", 25)?;
2645        let input = VpciInput::from_slices(
2646            close,
2647            volume,
2648            VpciParams {
2649                short_range: Some(short_range),
2650                long_range: Some(long_range),
2651            },
2652        );
2653        let out = vpci_with_kernel(&input, kernel).map_err(|e| {
2654            IndicatorDispatchError::ComputeFailed {
2655                indicator: "vpci".to_string(),
2656                details: e.to_string(),
2657            }
2658        })?;
2659        if output_id.eq_ignore_ascii_case("vpci") || output_id.eq_ignore_ascii_case("value") {
2660            return Ok(out.vpci);
2661        }
2662        if output_id.eq_ignore_ascii_case("vpcis") {
2663            return Ok(out.vpcis);
2664        }
2665        Err(IndicatorDispatchError::UnknownOutput {
2666            indicator: "vpci".to_string(),
2667            output: output_id.to_string(),
2668        })
2669    })
2670}
2671
2672fn compute_ttm_trend_batch(
2673    req: IndicatorBatchRequest<'_>,
2674    output_id: &str,
2675) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2676    expect_value_output("ttm_trend", output_id)?;
2677    let mut derived_source: Option<Vec<f64>> = None;
2678    let (source, close): (&[f64], &[f64]) = match req.data {
2679        IndicatorDataRef::Candles { candles, source } => (
2680            source_type(candles, source.unwrap_or("hl2")),
2681            candles.close.as_slice(),
2682        ),
2683        IndicatorDataRef::Ohlc {
2684            high, low, close, ..
2685        } => {
2686            ensure_same_len_3("ttm_trend", high.len(), low.len(), close.len())?;
2687            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2688            (derived_source.as_deref().unwrap_or(close), close)
2689        }
2690        IndicatorDataRef::Ohlcv {
2691            high, low, close, ..
2692        } => {
2693            ensure_same_len_3("ttm_trend", high.len(), low.len(), close.len())?;
2694            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2695            (derived_source.as_deref().unwrap_or(close), close)
2696        }
2697        _ => {
2698            return Err(IndicatorDispatchError::MissingRequiredInput {
2699                indicator: "ttm_trend".to_string(),
2700                input: IndicatorInputKind::Ohlc,
2701            })
2702        }
2703    };
2704    let kernel = req.kernel.to_non_batch();
2705    collect_bool("ttm_trend", output_id, req.combos, close.len(), |params| {
2706        let period = get_usize_param("ttm_trend", params, "period", 5)?;
2707        let input = TtmTrendInput::from_slices(
2708            source,
2709            close,
2710            TtmTrendParams {
2711                period: Some(period),
2712            },
2713        );
2714        let out = ttm_trend_with_kernel(&input, kernel).map_err(|e| {
2715            IndicatorDispatchError::ComputeFailed {
2716                indicator: "ttm_trend".to_string(),
2717                details: e.to_string(),
2718            }
2719        })?;
2720        Ok(out.values)
2721    })
2722}
2723
2724fn compute_ttm_squeeze_batch(
2725    req: IndicatorBatchRequest<'_>,
2726    output_id: &str,
2727) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2728    let (high, low, close) = extract_ohlc_input("ttm_squeeze", req.data)?;
2729    let kernel = req.kernel.to_non_batch();
2730    collect_f64(
2731        "ttm_squeeze",
2732        output_id,
2733        req.combos,
2734        close.len(),
2735        |params| {
2736            let length = get_usize_param("ttm_squeeze", params, "length", 20)?;
2737            let bb_mult = get_f64_param("ttm_squeeze", params, "bb_mult", 2.0)?;
2738            let kc_mult_high = get_f64_param("ttm_squeeze", params, "kc_mult_high", 1.0)?;
2739            let kc_mult_mid = get_f64_param("ttm_squeeze", params, "kc_mult_mid", 1.5)?;
2740            let kc_mult_low = get_f64_param("ttm_squeeze", params, "kc_mult_low", 2.0)?;
2741            let input = TtmSqueezeInput::from_slices(
2742                high,
2743                low,
2744                close,
2745                TtmSqueezeParams {
2746                    length: Some(length),
2747                    bb_mult: Some(bb_mult),
2748                    kc_mult_high: Some(kc_mult_high),
2749                    kc_mult_mid: Some(kc_mult_mid),
2750                    kc_mult_low: Some(kc_mult_low),
2751                },
2752            );
2753            let out = ttm_squeeze_with_kernel(&input, kernel).map_err(|e| {
2754                IndicatorDispatchError::ComputeFailed {
2755                    indicator: "ttm_squeeze".to_string(),
2756                    details: e.to_string(),
2757                }
2758            })?;
2759            if output_id.eq_ignore_ascii_case("momentum") || output_id.eq_ignore_ascii_case("value")
2760            {
2761                return Ok(out.momentum);
2762            }
2763            if output_id.eq_ignore_ascii_case("squeeze") {
2764                return Ok(out.squeeze);
2765            }
2766            Err(IndicatorDispatchError::UnknownOutput {
2767                indicator: "ttm_squeeze".to_string(),
2768                output: output_id.to_string(),
2769            })
2770        },
2771    )
2772}
2773
2774fn compute_aroon_batch(
2775    req: IndicatorBatchRequest<'_>,
2776    output_id: &str,
2777) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2778    let (high, low) = extract_high_low_input("aroon", req.data)?;
2779    let kernel = req.kernel.to_non_batch();
2780    collect_f64("aroon", output_id, req.combos, high.len(), |params| {
2781        let length = get_usize_param("aroon", params, "length", 14)?;
2782        let input = AroonInput::from_slices_hl(
2783            high,
2784            low,
2785            AroonParams {
2786                length: Some(length),
2787            },
2788        );
2789        let out = aroon_with_kernel(&input, kernel).map_err(|e| {
2790            IndicatorDispatchError::ComputeFailed {
2791                indicator: "aroon".to_string(),
2792                details: e.to_string(),
2793            }
2794        })?;
2795        if output_id.eq_ignore_ascii_case("up")
2796            || output_id.eq_ignore_ascii_case("aroon_up")
2797            || output_id.eq_ignore_ascii_case("value")
2798        {
2799            return Ok(out.aroon_up);
2800        }
2801        if output_id.eq_ignore_ascii_case("down") || output_id.eq_ignore_ascii_case("aroon_down") {
2802            return Ok(out.aroon_down);
2803        }
2804        Err(IndicatorDispatchError::UnknownOutput {
2805            indicator: "aroon".to_string(),
2806            output: output_id.to_string(),
2807        })
2808    })
2809}
2810
2811fn compute_di_batch(
2812    req: IndicatorBatchRequest<'_>,
2813    output_id: &str,
2814) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2815    let (high, low, close) = extract_ohlc_input("di", req.data)?;
2816    let kernel = req.kernel.to_non_batch();
2817    collect_f64("di", output_id, req.combos, close.len(), |params| {
2818        let period = get_usize_param("di", params, "period", 14)?;
2819        let input = DiInput::from_slices(
2820            high,
2821            low,
2822            close,
2823            DiParams {
2824                period: Some(period),
2825            },
2826        );
2827        let out =
2828            di_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2829                indicator: "di".to_string(),
2830                details: e.to_string(),
2831            })?;
2832        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
2833            return Ok(out.plus);
2834        }
2835        if output_id.eq_ignore_ascii_case("minus") {
2836            return Ok(out.minus);
2837        }
2838        Err(IndicatorDispatchError::UnknownOutput {
2839            indicator: "di".to_string(),
2840            output: output_id.to_string(),
2841        })
2842    })
2843}
2844
2845fn compute_dm_batch(
2846    req: IndicatorBatchRequest<'_>,
2847    output_id: &str,
2848) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2849    let (high, low) = extract_high_low_input("dm", req.data)?;
2850    let kernel = req.kernel.to_non_batch();
2851    collect_f64("dm", output_id, req.combos, high.len(), |params| {
2852        let period = get_usize_param("dm", params, "period", 14)?;
2853        let input = DmInput::from_slices(
2854            high,
2855            low,
2856            DmParams {
2857                period: Some(period),
2858            },
2859        );
2860        let out =
2861            dm_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2862                indicator: "dm".to_string(),
2863                details: e.to_string(),
2864            })?;
2865        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
2866            return Ok(out.plus);
2867        }
2868        if output_id.eq_ignore_ascii_case("minus") {
2869            return Ok(out.minus);
2870        }
2871        Err(IndicatorDispatchError::UnknownOutput {
2872            indicator: "dm".to_string(),
2873            output: output_id.to_string(),
2874        })
2875    })
2876}
2877
2878fn compute_donchian_batch(
2879    req: IndicatorBatchRequest<'_>,
2880    output_id: &str,
2881) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2882    let (high, low) = extract_high_low_input("donchian", req.data)?;
2883    let kernel = req.kernel.to_non_batch();
2884    collect_f64("donchian", output_id, req.combos, high.len(), |params| {
2885        let period = get_usize_param("donchian", params, "period", 20)?;
2886        let input = DonchianInput::from_slices(
2887            high,
2888            low,
2889            DonchianParams {
2890                period: Some(period),
2891            },
2892        );
2893        let out = donchian_with_kernel(&input, kernel).map_err(|e| {
2894            IndicatorDispatchError::ComputeFailed {
2895                indicator: "donchian".to_string(),
2896                details: e.to_string(),
2897            }
2898        })?;
2899        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
2900            return Ok(out.upperband);
2901        }
2902        if output_id.eq_ignore_ascii_case("middle") {
2903            return Ok(out.middleband);
2904        }
2905        if output_id.eq_ignore_ascii_case("lower") {
2906            return Ok(out.lowerband);
2907        }
2908        Err(IndicatorDispatchError::UnknownOutput {
2909            indicator: "donchian".to_string(),
2910            output: output_id.to_string(),
2911        })
2912    })
2913}
2914
2915fn compute_kdj_batch(
2916    req: IndicatorBatchRequest<'_>,
2917    output_id: &str,
2918) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2919    let (high, low, close) = extract_ohlc_input("kdj", req.data)?;
2920    let kernel = req.kernel.to_non_batch();
2921    collect_f64("kdj", output_id, req.combos, close.len(), |params| {
2922        let fast_k_period = get_usize_param("kdj", params, "fast_k_period", 9)?;
2923        let slow_k_period = get_usize_param("kdj", params, "slow_k_period", 3)?;
2924        let slow_k_ma_type = get_enum_param("kdj", params, "slow_k_ma_type", "sma")?;
2925        let slow_d_period = get_usize_param("kdj", params, "slow_d_period", 3)?;
2926        let slow_d_ma_type = get_enum_param("kdj", params, "slow_d_ma_type", "sma")?;
2927        let input = KdjInput::from_slices(
2928            high,
2929            low,
2930            close,
2931            KdjParams {
2932                fast_k_period: Some(fast_k_period),
2933                slow_k_period: Some(slow_k_period),
2934                slow_k_ma_type: Some(slow_k_ma_type),
2935                slow_d_period: Some(slow_d_period),
2936                slow_d_ma_type: Some(slow_d_ma_type),
2937            },
2938        );
2939        let out =
2940            kdj_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2941                indicator: "kdj".to_string(),
2942                details: e.to_string(),
2943            })?;
2944        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
2945            return Ok(out.k);
2946        }
2947        if output_id.eq_ignore_ascii_case("d") {
2948            return Ok(out.d);
2949        }
2950        if output_id.eq_ignore_ascii_case("j") {
2951            return Ok(out.j);
2952        }
2953        Err(IndicatorDispatchError::UnknownOutput {
2954            indicator: "kdj".to_string(),
2955            output: output_id.to_string(),
2956        })
2957    })
2958}
2959
2960fn compute_keltner_batch(
2961    req: IndicatorBatchRequest<'_>,
2962    output_id: &str,
2963) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2964    let (high, low, close) = extract_ohlc_input("keltner", req.data)?;
2965    let kernel = req.kernel.to_non_batch();
2966    collect_f64("keltner", output_id, req.combos, close.len(), |params| {
2967        let period = get_usize_param("keltner", params, "period", 20)?;
2968        let multiplier = get_f64_param("keltner", params, "multiplier", 2.0)?;
2969        let ma_type = get_enum_param("keltner", params, "ma_type", "ema")?;
2970        let input = KeltnerInput::from_slice(
2971            high,
2972            low,
2973            close,
2974            close,
2975            KeltnerParams {
2976                period: Some(period),
2977                multiplier: Some(multiplier),
2978                ma_type: Some(ma_type),
2979            },
2980        );
2981        let out = keltner_with_kernel(&input, kernel).map_err(|e| {
2982            IndicatorDispatchError::ComputeFailed {
2983                indicator: "keltner".to_string(),
2984                details: e.to_string(),
2985            }
2986        })?;
2987        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
2988            return Ok(out.upper_band);
2989        }
2990        if output_id.eq_ignore_ascii_case("middle") {
2991            return Ok(out.middle_band);
2992        }
2993        if output_id.eq_ignore_ascii_case("lower") {
2994            return Ok(out.lower_band);
2995        }
2996        Err(IndicatorDispatchError::UnknownOutput {
2997            indicator: "keltner".to_string(),
2998            output: output_id.to_string(),
2999        })
3000    })
3001}
3002
3003fn compute_squeeze_momentum_batch(
3004    req: IndicatorBatchRequest<'_>,
3005    output_id: &str,
3006) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3007    let (high, low, close) = extract_ohlc_input("squeeze_momentum", req.data)?;
3008    let kernel = req.kernel.to_non_batch();
3009    collect_f64(
3010        "squeeze_momentum",
3011        output_id,
3012        req.combos,
3013        close.len(),
3014        |params| {
3015            let length_bb = get_usize_param("squeeze_momentum", params, "length_bb", 20)?;
3016            let mult_bb = get_f64_param("squeeze_momentum", params, "mult_bb", 2.0)?;
3017            let length_kc = get_usize_param("squeeze_momentum", params, "length_kc", 20)?;
3018            let mult_kc = get_f64_param("squeeze_momentum", params, "mult_kc", 1.5)?;
3019            let input = SqueezeMomentumInput::from_slices(
3020                high,
3021                low,
3022                close,
3023                SqueezeMomentumParams {
3024                    length_bb: Some(length_bb),
3025                    mult_bb: Some(mult_bb),
3026                    length_kc: Some(length_kc),
3027                    mult_kc: Some(mult_kc),
3028                },
3029            );
3030            let out = squeeze_momentum_with_kernel(&input, kernel).map_err(|e| {
3031                IndicatorDispatchError::ComputeFailed {
3032                    indicator: "squeeze_momentum".to_string(),
3033                    details: e.to_string(),
3034                }
3035            })?;
3036            if output_id.eq_ignore_ascii_case("momentum") || output_id.eq_ignore_ascii_case("value")
3037            {
3038                return Ok(out.momentum);
3039            }
3040            if output_id.eq_ignore_ascii_case("squeeze") {
3041                return Ok(out.squeeze);
3042            }
3043            if output_id.eq_ignore_ascii_case("signal")
3044                || output_id.eq_ignore_ascii_case("momentum_signal")
3045            {
3046                return Ok(out.momentum_signal);
3047            }
3048            Err(IndicatorDispatchError::UnknownOutput {
3049                indicator: "squeeze_momentum".to_string(),
3050                output: output_id.to_string(),
3051            })
3052        },
3053    )
3054}
3055
3056fn compute_srsi_batch(
3057    req: IndicatorBatchRequest<'_>,
3058    output_id: &str,
3059) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3060    let data = extract_slice_input("srsi", req.data, "close")?;
3061    let kernel = req.kernel.to_non_batch();
3062    collect_f64("srsi", output_id, req.combos, data.len(), |params| {
3063        let rsi_period = get_usize_param("srsi", params, "rsi_period", 14)?;
3064        let stoch_period = get_usize_param("srsi", params, "stoch_period", 14)?;
3065        let k = get_usize_param("srsi", params, "k", 3)?;
3066        let d = get_usize_param("srsi", params, "d", 3)?;
3067        let source = get_enum_param("srsi", params, "source", "close")?;
3068        let input = SrsiInput::from_slice(
3069            data,
3070            SrsiParams {
3071                rsi_period: Some(rsi_period),
3072                stoch_period: Some(stoch_period),
3073                k: Some(k),
3074                d: Some(d),
3075                source: Some(source),
3076            },
3077        );
3078        let out = srsi_with_kernel(&input, kernel).map_err(|e| {
3079            IndicatorDispatchError::ComputeFailed {
3080                indicator: "srsi".to_string(),
3081                details: e.to_string(),
3082            }
3083        })?;
3084        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
3085            return Ok(out.k);
3086        }
3087        if output_id.eq_ignore_ascii_case("d") {
3088            return Ok(out.d);
3089        }
3090        Err(IndicatorDispatchError::UnknownOutput {
3091            indicator: "srsi".to_string(),
3092            output: output_id.to_string(),
3093        })
3094    })
3095}
3096
3097fn compute_supertrend_batch(
3098    req: IndicatorBatchRequest<'_>,
3099    output_id: &str,
3100) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3101    let (high, low, close) = extract_ohlc_input("supertrend", req.data)?;
3102    let kernel = req.kernel.to_non_batch();
3103    collect_f64("supertrend", output_id, req.combos, close.len(), |params| {
3104        let period = get_usize_param("supertrend", params, "period", 10)?;
3105        let factor = get_f64_param("supertrend", params, "factor", 3.0)?;
3106        let input = SuperTrendInput::from_slices(
3107            high,
3108            low,
3109            close,
3110            SuperTrendParams {
3111                period: Some(period),
3112                factor: Some(factor),
3113            },
3114        );
3115        let out = supertrend_with_kernel(&input, kernel).map_err(|e| {
3116            IndicatorDispatchError::ComputeFailed {
3117                indicator: "supertrend".to_string(),
3118                details: e.to_string(),
3119            }
3120        })?;
3121        if output_id.eq_ignore_ascii_case("trend") || output_id.eq_ignore_ascii_case("value") {
3122            return Ok(out.trend);
3123        }
3124        if output_id.eq_ignore_ascii_case("changed") {
3125            return Ok(out.changed);
3126        }
3127        Err(IndicatorDispatchError::UnknownOutput {
3128            indicator: "supertrend".to_string(),
3129            output: output_id.to_string(),
3130        })
3131    })
3132}
3133
3134fn compute_vi_batch(
3135    req: IndicatorBatchRequest<'_>,
3136    output_id: &str,
3137) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3138    let (high, low, close) = extract_ohlc_input("vi", req.data)?;
3139    let kernel = req.kernel.to_non_batch();
3140    collect_f64("vi", output_id, req.combos, close.len(), |params| {
3141        let period = get_usize_param("vi", params, "period", 14)?;
3142        let input = ViInput::from_slices(
3143            high,
3144            low,
3145            close,
3146            ViParams {
3147                period: Some(period),
3148            },
3149        );
3150        let out =
3151            vi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3152                indicator: "vi".to_string(),
3153                details: e.to_string(),
3154            })?;
3155        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
3156            return Ok(out.plus);
3157        }
3158        if output_id.eq_ignore_ascii_case("minus") {
3159            return Ok(out.minus);
3160        }
3161        Err(IndicatorDispatchError::UnknownOutput {
3162            indicator: "vi".to_string(),
3163            output: output_id.to_string(),
3164        })
3165    })
3166}
3167
3168fn compute_wavetrend_batch(
3169    req: IndicatorBatchRequest<'_>,
3170    output_id: &str,
3171) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3172    let data = extract_slice_input("wavetrend", req.data, "hlc3")?;
3173    let kernel = req.kernel.to_non_batch();
3174    collect_f64("wavetrend", output_id, req.combos, data.len(), |params| {
3175        let channel_length = get_usize_param("wavetrend", params, "channel_length", 9)?;
3176        let average_length = get_usize_param("wavetrend", params, "average_length", 12)?;
3177        let ma_length = get_usize_param("wavetrend", params, "ma_length", 3)?;
3178        let factor = get_f64_param("wavetrend", params, "factor", 0.015)?;
3179        let input = WavetrendInput::from_slice(
3180            data,
3181            WavetrendParams {
3182                channel_length: Some(channel_length),
3183                average_length: Some(average_length),
3184                ma_length: Some(ma_length),
3185                factor: Some(factor),
3186            },
3187        );
3188        let out = wavetrend_with_kernel(&input, kernel).map_err(|e| {
3189            IndicatorDispatchError::ComputeFailed {
3190                indicator: "wavetrend".to_string(),
3191                details: e.to_string(),
3192            }
3193        })?;
3194        if output_id.eq_ignore_ascii_case("wt1") || output_id.eq_ignore_ascii_case("value") {
3195            return Ok(out.wt1);
3196        }
3197        if output_id.eq_ignore_ascii_case("wt2") {
3198            return Ok(out.wt2);
3199        }
3200        if output_id.eq_ignore_ascii_case("wt_diff") {
3201            return Ok(out.wt_diff);
3202        }
3203        Err(IndicatorDispatchError::UnknownOutput {
3204            indicator: "wavetrend".to_string(),
3205            output: output_id.to_string(),
3206        })
3207    })
3208}
3209
3210fn compute_wto_batch(
3211    req: IndicatorBatchRequest<'_>,
3212    output_id: &str,
3213) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3214    let data = extract_slice_input("wto", req.data, "close")?;
3215    let kernel = req.kernel.to_non_batch();
3216    collect_f64("wto", output_id, req.combos, data.len(), |params| {
3217        let channel_length = get_usize_param("wto", params, "channel_length", 10)?;
3218        let average_length = get_usize_param("wto", params, "average_length", 21)?;
3219        let input = WtoInput::from_slice(
3220            data,
3221            WtoParams {
3222                channel_length: Some(channel_length),
3223                average_length: Some(average_length),
3224            },
3225        );
3226        let out =
3227            wto_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3228                indicator: "wto".to_string(),
3229                details: e.to_string(),
3230            })?;
3231        if output_id.eq_ignore_ascii_case("wavetrend1")
3232            || output_id.eq_ignore_ascii_case("wt1")
3233            || output_id.eq_ignore_ascii_case("value")
3234        {
3235            return Ok(out.wavetrend1);
3236        }
3237        if output_id.eq_ignore_ascii_case("wavetrend2") || output_id.eq_ignore_ascii_case("wt2") {
3238            return Ok(out.wavetrend2);
3239        }
3240        if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist") {
3241            return Ok(out.histogram);
3242        }
3243        Err(IndicatorDispatchError::UnknownOutput {
3244            indicator: "wto".to_string(),
3245            output: output_id.to_string(),
3246        })
3247    })
3248}
3249
3250fn compute_acosc_batch(
3251    req: IndicatorBatchRequest<'_>,
3252    output_id: &str,
3253) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3254    let (high, low) = extract_high_low_input("acosc", req.data)?;
3255    let kernel = req.kernel.to_non_batch();
3256    collect_f64("acosc", output_id, req.combos, high.len(), |_params| {
3257        let input = AcoscInput::from_slices(high, low, AcoscParams::default());
3258        let out = acosc_with_kernel(&input, kernel).map_err(|e| {
3259            IndicatorDispatchError::ComputeFailed {
3260                indicator: "acosc".to_string(),
3261                details: e.to_string(),
3262            }
3263        })?;
3264        if output_id.eq_ignore_ascii_case("osc") || output_id.eq_ignore_ascii_case("value") {
3265            return Ok(out.osc);
3266        }
3267        if output_id.eq_ignore_ascii_case("change") {
3268            return Ok(out.change);
3269        }
3270        Err(IndicatorDispatchError::UnknownOutput {
3271            indicator: "acosc".to_string(),
3272            output: output_id.to_string(),
3273        })
3274    })
3275}
3276
3277fn compute_alligator_batch(
3278    req: IndicatorBatchRequest<'_>,
3279    output_id: &str,
3280) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3281    let data = extract_slice_input("alligator", req.data, "hl2")?;
3282    let kernel = req.kernel.to_non_batch();
3283    collect_f64("alligator", output_id, req.combos, data.len(), |params| {
3284        let jaw_period = get_usize_param("alligator", params, "jaw_period", 13)?;
3285        let jaw_offset = get_usize_param("alligator", params, "jaw_offset", 8)?;
3286        let teeth_period = get_usize_param("alligator", params, "teeth_period", 8)?;
3287        let teeth_offset = get_usize_param("alligator", params, "teeth_offset", 5)?;
3288        let lips_period = get_usize_param("alligator", params, "lips_period", 5)?;
3289        let lips_offset = get_usize_param("alligator", params, "lips_offset", 3)?;
3290        let input = AlligatorInput::from_slice(
3291            data,
3292            AlligatorParams {
3293                jaw_period: Some(jaw_period),
3294                jaw_offset: Some(jaw_offset),
3295                teeth_period: Some(teeth_period),
3296                teeth_offset: Some(teeth_offset),
3297                lips_period: Some(lips_period),
3298                lips_offset: Some(lips_offset),
3299            },
3300        );
3301        let out = alligator_with_kernel(&input, kernel).map_err(|e| {
3302            IndicatorDispatchError::ComputeFailed {
3303                indicator: "alligator".to_string(),
3304                details: e.to_string(),
3305            }
3306        })?;
3307        if output_id.eq_ignore_ascii_case("jaw") || output_id.eq_ignore_ascii_case("value") {
3308            return Ok(out.jaw);
3309        }
3310        if output_id.eq_ignore_ascii_case("teeth") {
3311            return Ok(out.teeth);
3312        }
3313        if output_id.eq_ignore_ascii_case("lips") {
3314            return Ok(out.lips);
3315        }
3316        Err(IndicatorDispatchError::UnknownOutput {
3317            indicator: "alligator".to_string(),
3318            output: output_id.to_string(),
3319        })
3320    })
3321}
3322
3323fn compute_alphatrend_batch(
3324    req: IndicatorBatchRequest<'_>,
3325    output_id: &str,
3326) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3327    let (open, high, low, close, volume) = extract_ohlcv_full_input("alphatrend", req.data)?;
3328    let kernel = req.kernel.to_non_batch();
3329    collect_f64("alphatrend", output_id, req.combos, close.len(), |params| {
3330        let coeff = get_f64_param("alphatrend", params, "coeff", 1.0)?;
3331        let period = get_usize_param("alphatrend", params, "period", 14)?;
3332        let no_volume = get_bool_param("alphatrend", params, "no_volume", false)?;
3333        let input = AlphaTrendInput::from_slices(
3334            open,
3335            high,
3336            low,
3337            close,
3338            volume,
3339            AlphaTrendParams {
3340                coeff: Some(coeff),
3341                period: Some(period),
3342                no_volume: Some(no_volume),
3343            },
3344        );
3345        let out = alphatrend_with_kernel(&input, kernel).map_err(|e| {
3346            IndicatorDispatchError::ComputeFailed {
3347                indicator: "alphatrend".to_string(),
3348                details: e.to_string(),
3349            }
3350        })?;
3351        if output_id.eq_ignore_ascii_case("k1") || output_id.eq_ignore_ascii_case("value") {
3352            return Ok(out.k1);
3353        }
3354        if output_id.eq_ignore_ascii_case("k2") {
3355            return Ok(out.k2);
3356        }
3357        Err(IndicatorDispatchError::UnknownOutput {
3358            indicator: "alphatrend".to_string(),
3359            output: output_id.to_string(),
3360        })
3361    })
3362}
3363
3364fn compute_aso_batch(
3365    req: IndicatorBatchRequest<'_>,
3366    output_id: &str,
3367) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3368    let (open, high, low, close) = match req.data {
3369        IndicatorDataRef::Candles { candles, source } => (
3370            candles.open.as_slice(),
3371            candles.high.as_slice(),
3372            candles.low.as_slice(),
3373            source_type(candles, source.unwrap_or("close")),
3374        ),
3375        IndicatorDataRef::Ohlc {
3376            open,
3377            high,
3378            low,
3379            close,
3380        } => {
3381            ensure_same_len_4("aso", open.len(), high.len(), low.len(), close.len())?;
3382            (open, high, low, close)
3383        }
3384        IndicatorDataRef::Ohlcv {
3385            open,
3386            high,
3387            low,
3388            close,
3389            volume,
3390        } => {
3391            ensure_same_len_5(
3392                "aso",
3393                open.len(),
3394                high.len(),
3395                low.len(),
3396                close.len(),
3397                volume.len(),
3398            )?;
3399            (open, high, low, close)
3400        }
3401        _ => {
3402            return Err(IndicatorDispatchError::MissingRequiredInput {
3403                indicator: "aso".to_string(),
3404                input: IndicatorInputKind::Ohlc,
3405            });
3406        }
3407    };
3408    let kernel = req.kernel.to_non_batch();
3409    collect_f64("aso", output_id, req.combos, close.len(), |params| {
3410        let period = get_usize_param("aso", params, "period", 10)?;
3411        let mode = get_usize_param("aso", params, "mode", 0)?;
3412        let input = AsoInput::from_slices(
3413            open,
3414            high,
3415            low,
3416            close,
3417            AsoParams {
3418                period: Some(period),
3419                mode: Some(mode),
3420            },
3421        );
3422        let out =
3423            aso_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3424                indicator: "aso".to_string(),
3425                details: e.to_string(),
3426            })?;
3427        if output_id.eq_ignore_ascii_case("bulls") || output_id.eq_ignore_ascii_case("value") {
3428            return Ok(out.bulls);
3429        }
3430        if output_id.eq_ignore_ascii_case("bears") {
3431            return Ok(out.bears);
3432        }
3433        Err(IndicatorDispatchError::UnknownOutput {
3434            indicator: "aso".to_string(),
3435            output: output_id.to_string(),
3436        })
3437    })
3438}
3439
3440fn compute_bandpass_batch(
3441    req: IndicatorBatchRequest<'_>,
3442    output_id: &str,
3443) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3444    let data = extract_slice_input("bandpass", req.data, "close")?;
3445    let kernel = req.kernel.to_non_batch();
3446    collect_f64("bandpass", output_id, req.combos, data.len(), |params| {
3447        let period = get_usize_param("bandpass", params, "period", 20)?;
3448        let bandwidth = get_f64_param("bandpass", params, "bandwidth", 0.3)?;
3449        let input = BandPassInput::from_slice(
3450            data,
3451            BandPassParams {
3452                period: Some(period),
3453                bandwidth: Some(bandwidth),
3454            },
3455        );
3456        let out = bandpass_with_kernel(&input, kernel).map_err(|e| {
3457            IndicatorDispatchError::ComputeFailed {
3458                indicator: "bandpass".to_string(),
3459                details: e.to_string(),
3460            }
3461        })?;
3462        if output_id.eq_ignore_ascii_case("bp") || output_id.eq_ignore_ascii_case("value") {
3463            return Ok(out.bp);
3464        }
3465        if output_id.eq_ignore_ascii_case("bp_normalized")
3466            || output_id.eq_ignore_ascii_case("normalized")
3467        {
3468            return Ok(out.bp_normalized);
3469        }
3470        if output_id.eq_ignore_ascii_case("signal") {
3471            return Ok(out.signal);
3472        }
3473        if output_id.eq_ignore_ascii_case("trigger") {
3474            return Ok(out.trigger);
3475        }
3476        Err(IndicatorDispatchError::UnknownOutput {
3477            indicator: "bandpass".to_string(),
3478            output: output_id.to_string(),
3479        })
3480    })
3481}
3482
3483fn compute_chandelier_exit_batch(
3484    req: IndicatorBatchRequest<'_>,
3485    output_id: &str,
3486) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3487    let (high, low, close) = extract_ohlc_input("chandelier_exit", req.data)?;
3488    let kernel = req.kernel.to_non_batch();
3489    collect_f64(
3490        "chandelier_exit",
3491        output_id,
3492        req.combos,
3493        close.len(),
3494        |params| {
3495            let period = get_usize_param("chandelier_exit", params, "period", 22)?;
3496            let mult = get_f64_param("chandelier_exit", params, "mult", 3.0)?;
3497            let use_close = get_bool_param("chandelier_exit", params, "use_close", true)?;
3498            let input = ChandelierExitInput::from_slices(
3499                high,
3500                low,
3501                close,
3502                ChandelierExitParams {
3503                    period: Some(period),
3504                    mult: Some(mult),
3505                    use_close: Some(use_close),
3506                },
3507            );
3508            let out = chandelier_exit_with_kernel(&input, kernel).map_err(|e| {
3509                IndicatorDispatchError::ComputeFailed {
3510                    indicator: "chandelier_exit".to_string(),
3511                    details: e.to_string(),
3512                }
3513            })?;
3514            if output_id.eq_ignore_ascii_case("long_stop")
3515                || output_id.eq_ignore_ascii_case("value")
3516            {
3517                return Ok(out.long_stop);
3518            }
3519            if output_id.eq_ignore_ascii_case("short_stop") {
3520                return Ok(out.short_stop);
3521            }
3522            Err(IndicatorDispatchError::UnknownOutput {
3523                indicator: "chandelier_exit".to_string(),
3524                output: output_id.to_string(),
3525            })
3526        },
3527    )
3528}
3529
3530fn compute_cksp_batch(
3531    req: IndicatorBatchRequest<'_>,
3532    output_id: &str,
3533) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3534    let (high, low, close) = extract_ohlc_input("cksp", req.data)?;
3535    let kernel = req.kernel.to_non_batch();
3536    collect_f64("cksp", output_id, req.combos, close.len(), |params| {
3537        let p = get_usize_param("cksp", params, "p", 10)?;
3538        let x = get_f64_param("cksp", params, "x", 1.0)?;
3539        let q = get_usize_param("cksp", params, "q", 9)?;
3540        let input = CkspInput::from_slices(
3541            high,
3542            low,
3543            close,
3544            CkspParams {
3545                p: Some(p),
3546                x: Some(x),
3547                q: Some(q),
3548            },
3549        );
3550        let out = cksp_with_kernel(&input, kernel).map_err(|e| {
3551            IndicatorDispatchError::ComputeFailed {
3552                indicator: "cksp".to_string(),
3553                details: e.to_string(),
3554            }
3555        })?;
3556        if output_id.eq_ignore_ascii_case("long_values")
3557            || output_id.eq_ignore_ascii_case("long")
3558            || output_id.eq_ignore_ascii_case("value")
3559        {
3560            return Ok(out.long_values);
3561        }
3562        if output_id.eq_ignore_ascii_case("short_values") || output_id.eq_ignore_ascii_case("short")
3563        {
3564            return Ok(out.short_values);
3565        }
3566        Err(IndicatorDispatchError::UnknownOutput {
3567            indicator: "cksp".to_string(),
3568            output: output_id.to_string(),
3569        })
3570    })
3571}
3572
3573fn compute_correlation_cycle_batch(
3574    req: IndicatorBatchRequest<'_>,
3575    output_id: &str,
3576) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3577    let data = extract_slice_input("correlation_cycle", req.data, "close")?;
3578    let kernel = req.kernel.to_non_batch();
3579    collect_f64(
3580        "correlation_cycle",
3581        output_id,
3582        req.combos,
3583        data.len(),
3584        |params| {
3585            let period = get_usize_param("correlation_cycle", params, "period", 20)?;
3586            let threshold = get_f64_param("correlation_cycle", params, "threshold", 9.0)?;
3587            let input = CorrelationCycleInput::from_slice(
3588                data,
3589                CorrelationCycleParams {
3590                    period: Some(period),
3591                    threshold: Some(threshold),
3592                },
3593            );
3594            let out = correlation_cycle_with_kernel(&input, kernel).map_err(|e| {
3595                IndicatorDispatchError::ComputeFailed {
3596                    indicator: "correlation_cycle".to_string(),
3597                    details: e.to_string(),
3598                }
3599            })?;
3600            if output_id.eq_ignore_ascii_case("real") || output_id.eq_ignore_ascii_case("value") {
3601                return Ok(out.real);
3602            }
3603            if output_id.eq_ignore_ascii_case("imag") {
3604                return Ok(out.imag);
3605            }
3606            if output_id.eq_ignore_ascii_case("angle") {
3607                return Ok(out.angle);
3608            }
3609            if output_id.eq_ignore_ascii_case("state") {
3610                return Ok(out.state);
3611            }
3612            Err(IndicatorDispatchError::UnknownOutput {
3613                indicator: "correlation_cycle".to_string(),
3614                output: output_id.to_string(),
3615            })
3616        },
3617    )
3618}
3619
3620fn compute_damiani_volatmeter_batch(
3621    req: IndicatorBatchRequest<'_>,
3622    output_id: &str,
3623) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3624    let data = extract_slice_input("damiani_volatmeter", req.data, "close")?;
3625    let kernel = req.kernel.to_non_batch();
3626    collect_f64(
3627        "damiani_volatmeter",
3628        output_id,
3629        req.combos,
3630        data.len(),
3631        |params| {
3632            let vis_atr = get_usize_param("damiani_volatmeter", params, "vis_atr", 13)?;
3633            let vis_std = get_usize_param("damiani_volatmeter", params, "vis_std", 20)?;
3634            let sed_atr = get_usize_param("damiani_volatmeter", params, "sed_atr", 40)?;
3635            let sed_std = get_usize_param("damiani_volatmeter", params, "sed_std", 100)?;
3636            let threshold = get_f64_param("damiani_volatmeter", params, "threshold", 1.4)?;
3637            let input = DamianiVolatmeterInput::from_slice(
3638                data,
3639                DamianiVolatmeterParams {
3640                    vis_atr: Some(vis_atr),
3641                    vis_std: Some(vis_std),
3642                    sed_atr: Some(sed_atr),
3643                    sed_std: Some(sed_std),
3644                    threshold: Some(threshold),
3645                },
3646            );
3647            let out = damiani_volatmeter_with_kernel(&input, kernel).map_err(|e| {
3648                IndicatorDispatchError::ComputeFailed {
3649                    indicator: "damiani_volatmeter".to_string(),
3650                    details: e.to_string(),
3651                }
3652            })?;
3653            if output_id.eq_ignore_ascii_case("vol") || output_id.eq_ignore_ascii_case("value") {
3654                return Ok(out.vol);
3655            }
3656            if output_id.eq_ignore_ascii_case("anti") {
3657                return Ok(out.anti);
3658            }
3659            Err(IndicatorDispatchError::UnknownOutput {
3660                indicator: "damiani_volatmeter".to_string(),
3661                output: output_id.to_string(),
3662            })
3663        },
3664    )
3665}
3666
3667fn compute_dvdiqqe_batch(
3668    req: IndicatorBatchRequest<'_>,
3669    output_id: &str,
3670) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3671    let (open, high, low, close, volume) = match req.data {
3672        IndicatorDataRef::Candles { candles, .. } => (
3673            candles.open.as_slice(),
3674            candles.high.as_slice(),
3675            candles.low.as_slice(),
3676            candles.close.as_slice(),
3677            Some(candles.volume.as_slice()),
3678        ),
3679        IndicatorDataRef::Ohlcv {
3680            open,
3681            high,
3682            low,
3683            close,
3684            volume,
3685        } => {
3686            ensure_same_len_5(
3687                "dvdiqqe",
3688                open.len(),
3689                high.len(),
3690                low.len(),
3691                close.len(),
3692                volume.len(),
3693            )?;
3694            (open, high, low, close, Some(volume))
3695        }
3696        IndicatorDataRef::Ohlc {
3697            open,
3698            high,
3699            low,
3700            close,
3701        } => {
3702            ensure_same_len_4("dvdiqqe", open.len(), high.len(), low.len(), close.len())?;
3703            (open, high, low, close, None)
3704        }
3705        _ => {
3706            return Err(IndicatorDispatchError::MissingRequiredInput {
3707                indicator: "dvdiqqe".to_string(),
3708                input: IndicatorInputKind::Ohlc,
3709            })
3710        }
3711    };
3712    let kernel = req.kernel.to_non_batch();
3713    collect_f64("dvdiqqe", output_id, req.combos, close.len(), |params| {
3714        let period = get_usize_param("dvdiqqe", params, "period", 13)?;
3715        let smoothing_period = get_usize_param("dvdiqqe", params, "smoothing_period", 6)?;
3716        let fast_multiplier = get_f64_param("dvdiqqe", params, "fast_multiplier", 2.618)?;
3717        let slow_multiplier = get_f64_param("dvdiqqe", params, "slow_multiplier", 4.236)?;
3718        let volume_type = get_enum_param("dvdiqqe", params, "volume_type", "default")?;
3719        let center_type = get_enum_param("dvdiqqe", params, "center_type", "dynamic")?;
3720        let tick_size = get_f64_param("dvdiqqe", params, "tick_size", 0.01)?;
3721        let input = DvdiqqeInput::from_slices(
3722            open,
3723            high,
3724            low,
3725            close,
3726            volume,
3727            DvdiqqeParams {
3728                period: Some(period),
3729                smoothing_period: Some(smoothing_period),
3730                fast_multiplier: Some(fast_multiplier),
3731                slow_multiplier: Some(slow_multiplier),
3732                volume_type: Some(volume_type),
3733                center_type: Some(center_type),
3734                tick_size: Some(tick_size),
3735            },
3736        );
3737        let out = dvdiqqe_with_kernel(&input, kernel).map_err(|e| {
3738            IndicatorDispatchError::ComputeFailed {
3739                indicator: "dvdiqqe".to_string(),
3740                details: e.to_string(),
3741            }
3742        })?;
3743        if output_id.eq_ignore_ascii_case("dvdi") || output_id.eq_ignore_ascii_case("value") {
3744            return Ok(out.dvdi);
3745        }
3746        if output_id.eq_ignore_ascii_case("fast_tl") || output_id.eq_ignore_ascii_case("fast") {
3747            return Ok(out.fast_tl);
3748        }
3749        if output_id.eq_ignore_ascii_case("slow_tl") || output_id.eq_ignore_ascii_case("slow") {
3750            return Ok(out.slow_tl);
3751        }
3752        if output_id.eq_ignore_ascii_case("center_line") || output_id.eq_ignore_ascii_case("center")
3753        {
3754            return Ok(out.center_line);
3755        }
3756        Err(IndicatorDispatchError::UnknownOutput {
3757            indicator: "dvdiqqe".to_string(),
3758            output: output_id.to_string(),
3759        })
3760    })
3761}
3762
3763fn compute_emd_batch(
3764    req: IndicatorBatchRequest<'_>,
3765    output_id: &str,
3766) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3767    let (high, low, close, volume) = extract_hlcv_input("emd", req.data)?;
3768    let kernel = req.kernel.to_non_batch();
3769    collect_f64("emd", output_id, req.combos, close.len(), |params| {
3770        let period = get_usize_param("emd", params, "period", 20)?;
3771        let delta = get_f64_param("emd", params, "delta", 0.5)?;
3772        let fraction = get_f64_param("emd", params, "fraction", 0.1)?;
3773        let input = EmdInput::from_slices(
3774            high,
3775            low,
3776            close,
3777            volume,
3778            EmdParams {
3779                period: Some(period),
3780                delta: Some(delta),
3781                fraction: Some(fraction),
3782            },
3783        );
3784        let out =
3785            emd_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3786                indicator: "emd".to_string(),
3787                details: e.to_string(),
3788            })?;
3789        if output_id.eq_ignore_ascii_case("upperband")
3790            || output_id.eq_ignore_ascii_case("upper")
3791            || output_id.eq_ignore_ascii_case("value")
3792        {
3793            return Ok(out.upperband);
3794        }
3795        if output_id.eq_ignore_ascii_case("middleband") || output_id.eq_ignore_ascii_case("middle")
3796        {
3797            return Ok(out.middleband);
3798        }
3799        if output_id.eq_ignore_ascii_case("lowerband") || output_id.eq_ignore_ascii_case("lower") {
3800            return Ok(out.lowerband);
3801        }
3802        Err(IndicatorDispatchError::UnknownOutput {
3803            indicator: "emd".to_string(),
3804            output: output_id.to_string(),
3805        })
3806    })
3807}
3808
3809fn compute_eri_batch(
3810    req: IndicatorBatchRequest<'_>,
3811    output_id: &str,
3812) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3813    let (high, low, source) = match req.data {
3814        IndicatorDataRef::Candles { candles, source } => (
3815            candles.high.as_slice(),
3816            candles.low.as_slice(),
3817            source_type(candles, source.unwrap_or("close")),
3818        ),
3819        IndicatorDataRef::Ohlc {
3820            open,
3821            high,
3822            low,
3823            close,
3824        } => {
3825            ensure_same_len_4("eri", open.len(), high.len(), low.len(), close.len())?;
3826            (high, low, close)
3827        }
3828        IndicatorDataRef::Ohlcv {
3829            open,
3830            high,
3831            low,
3832            close,
3833            volume,
3834        } => {
3835            ensure_same_len_5(
3836                "eri",
3837                open.len(),
3838                high.len(),
3839                low.len(),
3840                close.len(),
3841                volume.len(),
3842            )?;
3843            (high, low, close)
3844        }
3845        _ => {
3846            return Err(IndicatorDispatchError::MissingRequiredInput {
3847                indicator: "eri".to_string(),
3848                input: IndicatorInputKind::Ohlc,
3849            });
3850        }
3851    };
3852    let kernel = req.kernel.to_non_batch();
3853    collect_f64("eri", output_id, req.combos, source.len(), |params| {
3854        let period = get_usize_param("eri", params, "period", 13)?;
3855        let ma_type = get_enum_param("eri", params, "ma_type", "ema")?;
3856        let input = EriInput::from_slices(
3857            high,
3858            low,
3859            source,
3860            EriParams {
3861                period: Some(period),
3862                ma_type: Some(ma_type),
3863            },
3864        );
3865        let out =
3866            eri_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3867                indicator: "eri".to_string(),
3868                details: e.to_string(),
3869            })?;
3870        if output_id.eq_ignore_ascii_case("bull") || output_id.eq_ignore_ascii_case("value") {
3871            return Ok(out.bull);
3872        }
3873        if output_id.eq_ignore_ascii_case("bear") {
3874            return Ok(out.bear);
3875        }
3876        Err(IndicatorDispatchError::UnknownOutput {
3877            indicator: "eri".to_string(),
3878            output: output_id.to_string(),
3879        })
3880    })
3881}
3882
3883fn compute_fisher_batch(
3884    req: IndicatorBatchRequest<'_>,
3885    output_id: &str,
3886) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3887    let (high, low) = extract_high_low_input("fisher", req.data)?;
3888    let kernel = req.kernel.to_non_batch();
3889    collect_f64("fisher", output_id, req.combos, high.len(), |params| {
3890        let period = get_usize_param("fisher", params, "period", 9)?;
3891        let input = FisherInput::from_slices(
3892            high,
3893            low,
3894            FisherParams {
3895                period: Some(period),
3896            },
3897        );
3898        let out = fisher_with_kernel(&input, kernel).map_err(|e| {
3899            IndicatorDispatchError::ComputeFailed {
3900                indicator: "fisher".to_string(),
3901                details: e.to_string(),
3902            }
3903        })?;
3904        if output_id.eq_ignore_ascii_case("fisher") || output_id.eq_ignore_ascii_case("value") {
3905            return Ok(out.fisher);
3906        }
3907        if output_id.eq_ignore_ascii_case("signal") {
3908            return Ok(out.signal);
3909        }
3910        Err(IndicatorDispatchError::UnknownOutput {
3911            indicator: "fisher".to_string(),
3912            output: output_id.to_string(),
3913        })
3914    })
3915}
3916
3917fn compute_fvg_trailing_stop_batch(
3918    req: IndicatorBatchRequest<'_>,
3919    output_id: &str,
3920) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3921    let (high, low, close) = extract_ohlc_input("fvg_trailing_stop", req.data)?;
3922    let kernel = req.kernel.to_non_batch();
3923    collect_f64(
3924        "fvg_trailing_stop",
3925        output_id,
3926        req.combos,
3927        close.len(),
3928        |params| {
3929            let lookback =
3930                get_usize_param("fvg_trailing_stop", params, "unmitigated_fvg_lookback", 5)?;
3931            let smoothing_length =
3932                get_usize_param("fvg_trailing_stop", params, "smoothing_length", 9)?;
3933            let reset_on_cross =
3934                get_bool_param("fvg_trailing_stop", params, "reset_on_cross", false)?;
3935            let input = FvgTrailingStopInput::from_slices(
3936                high,
3937                low,
3938                close,
3939                FvgTrailingStopParams {
3940                    unmitigated_fvg_lookback: Some(lookback),
3941                    smoothing_length: Some(smoothing_length),
3942                    reset_on_cross: Some(reset_on_cross),
3943                },
3944            );
3945            let out = fvg_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
3946                IndicatorDispatchError::ComputeFailed {
3947                    indicator: "fvg_trailing_stop".to_string(),
3948                    details: e.to_string(),
3949                }
3950            })?;
3951            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
3952                return Ok(out.upper);
3953            }
3954            if output_id.eq_ignore_ascii_case("lower") {
3955                return Ok(out.lower);
3956            }
3957            if output_id.eq_ignore_ascii_case("upper_ts") {
3958                return Ok(out.upper_ts);
3959            }
3960            if output_id.eq_ignore_ascii_case("lower_ts") {
3961                return Ok(out.lower_ts);
3962            }
3963            Err(IndicatorDispatchError::UnknownOutput {
3964                indicator: "fvg_trailing_stop".to_string(),
3965                output: output_id.to_string(),
3966            })
3967        },
3968    )
3969}
3970
3971fn compute_gatorosc_batch(
3972    req: IndicatorBatchRequest<'_>,
3973    output_id: &str,
3974) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3975    let data = extract_slice_input("gatorosc", req.data, "close")?;
3976    let kernel = req.kernel.to_non_batch();
3977    collect_f64("gatorosc", output_id, req.combos, data.len(), |params| {
3978        let jaws_length = get_usize_param("gatorosc", params, "jaws_length", 13)?;
3979        let jaws_shift = get_usize_param("gatorosc", params, "jaws_shift", 8)?;
3980        let teeth_length = get_usize_param("gatorosc", params, "teeth_length", 8)?;
3981        let teeth_shift = get_usize_param("gatorosc", params, "teeth_shift", 5)?;
3982        let lips_length = get_usize_param("gatorosc", params, "lips_length", 5)?;
3983        let lips_shift = get_usize_param("gatorosc", params, "lips_shift", 3)?;
3984        let input = GatorOscInput::from_slice(
3985            data,
3986            GatorOscParams {
3987                jaws_length: Some(jaws_length),
3988                jaws_shift: Some(jaws_shift),
3989                teeth_length: Some(teeth_length),
3990                teeth_shift: Some(teeth_shift),
3991                lips_length: Some(lips_length),
3992                lips_shift: Some(lips_shift),
3993            },
3994        );
3995        let out = gatorosc_with_kernel(&input, kernel).map_err(|e| {
3996            IndicatorDispatchError::ComputeFailed {
3997                indicator: "gatorosc".to_string(),
3998                details: e.to_string(),
3999            }
4000        })?;
4001        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
4002            return Ok(out.upper);
4003        }
4004        if output_id.eq_ignore_ascii_case("lower") {
4005            return Ok(out.lower);
4006        }
4007        if output_id.eq_ignore_ascii_case("upper_change") {
4008            return Ok(out.upper_change);
4009        }
4010        if output_id.eq_ignore_ascii_case("lower_change") {
4011            return Ok(out.lower_change);
4012        }
4013        Err(IndicatorDispatchError::UnknownOutput {
4014            indicator: "gatorosc".to_string(),
4015            output: output_id.to_string(),
4016        })
4017    })
4018}
4019
4020fn compute_halftrend_batch(
4021    req: IndicatorBatchRequest<'_>,
4022    output_id: &str,
4023) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4024    let (high, low, close) = extract_ohlc_input("halftrend", req.data)?;
4025    let kernel = req.kernel.to_non_batch();
4026    collect_f64("halftrend", output_id, req.combos, close.len(), |params| {
4027        let amplitude = get_usize_param("halftrend", params, "amplitude", 2)?;
4028        let channel_deviation = get_f64_param("halftrend", params, "channel_deviation", 2.0)?;
4029        let atr_period = get_usize_param("halftrend", params, "atr_period", 100)?;
4030        let input = HalfTrendInput::from_slices(
4031            high,
4032            low,
4033            close,
4034            HalfTrendParams {
4035                amplitude: Some(amplitude),
4036                channel_deviation: Some(channel_deviation),
4037                atr_period: Some(atr_period),
4038            },
4039        );
4040        let out = halftrend_with_kernel(&input, kernel).map_err(|e| {
4041            IndicatorDispatchError::ComputeFailed {
4042                indicator: "halftrend".to_string(),
4043                details: e.to_string(),
4044            }
4045        })?;
4046        if output_id.eq_ignore_ascii_case("halftrend") || output_id.eq_ignore_ascii_case("value") {
4047            return Ok(out.halftrend);
4048        }
4049        if output_id.eq_ignore_ascii_case("trend") {
4050            return Ok(out.trend);
4051        }
4052        if output_id.eq_ignore_ascii_case("atr_high") {
4053            return Ok(out.atr_high);
4054        }
4055        if output_id.eq_ignore_ascii_case("atr_low") {
4056            return Ok(out.atr_low);
4057        }
4058        if output_id.eq_ignore_ascii_case("buy_signal") || output_id.eq_ignore_ascii_case("buy") {
4059            return Ok(out.buy_signal);
4060        }
4061        if output_id.eq_ignore_ascii_case("sell_signal") || output_id.eq_ignore_ascii_case("sell") {
4062            return Ok(out.sell_signal);
4063        }
4064        Err(IndicatorDispatchError::UnknownOutput {
4065            indicator: "halftrend".to_string(),
4066            output: output_id.to_string(),
4067        })
4068    })
4069}
4070
4071fn compute_kst_batch(
4072    req: IndicatorBatchRequest<'_>,
4073    output_id: &str,
4074) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4075    let data = extract_slice_input("kst", req.data, "close")?;
4076    let kernel = req.kernel.to_non_batch();
4077    collect_f64("kst", output_id, req.combos, data.len(), |params| {
4078        let sma_period1 = get_usize_param("kst", params, "sma_period1", 10)?;
4079        let sma_period2 = get_usize_param("kst", params, "sma_period2", 10)?;
4080        let sma_period3 = get_usize_param("kst", params, "sma_period3", 10)?;
4081        let sma_period4 = get_usize_param("kst", params, "sma_period4", 15)?;
4082        let roc_period1 = get_usize_param("kst", params, "roc_period1", 10)?;
4083        let roc_period2 = get_usize_param("kst", params, "roc_period2", 15)?;
4084        let roc_period3 = get_usize_param("kst", params, "roc_period3", 20)?;
4085        let roc_period4 = get_usize_param("kst", params, "roc_period4", 30)?;
4086        let signal_period = get_usize_param("kst", params, "signal_period", 9)?;
4087        let input = KstInput::from_slice(
4088            data,
4089            KstParams {
4090                sma_period1: Some(sma_period1),
4091                sma_period2: Some(sma_period2),
4092                sma_period3: Some(sma_period3),
4093                sma_period4: Some(sma_period4),
4094                roc_period1: Some(roc_period1),
4095                roc_period2: Some(roc_period2),
4096                roc_period3: Some(roc_period3),
4097                roc_period4: Some(roc_period4),
4098                signal_period: Some(signal_period),
4099            },
4100        );
4101        let out =
4102            kst_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4103                indicator: "kst".to_string(),
4104                details: e.to_string(),
4105            })?;
4106        if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
4107            return Ok(out.line);
4108        }
4109        if output_id.eq_ignore_ascii_case("signal") {
4110            return Ok(out.signal);
4111        }
4112        Err(IndicatorDispatchError::UnknownOutput {
4113            indicator: "kst".to_string(),
4114            output: output_id.to_string(),
4115        })
4116    })
4117}
4118
4119fn compute_lpc_batch(
4120    req: IndicatorBatchRequest<'_>,
4121    output_id: &str,
4122) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4123    let (high, low, close, src) = match req.data {
4124        IndicatorDataRef::Candles { candles, source } => (
4125            candles.high.as_slice(),
4126            candles.low.as_slice(),
4127            candles.close.as_slice(),
4128            source_type(candles, source.unwrap_or("close")),
4129        ),
4130        IndicatorDataRef::Ohlc {
4131            open,
4132            high,
4133            low,
4134            close,
4135        } => {
4136            ensure_same_len_4("lpc", open.len(), high.len(), low.len(), close.len())?;
4137            (high, low, close, close)
4138        }
4139        IndicatorDataRef::Ohlcv {
4140            open,
4141            high,
4142            low,
4143            close,
4144            volume,
4145        } => {
4146            ensure_same_len_5(
4147                "lpc",
4148                open.len(),
4149                high.len(),
4150                low.len(),
4151                close.len(),
4152                volume.len(),
4153            )?;
4154            (high, low, close, close)
4155        }
4156        _ => {
4157            return Err(IndicatorDispatchError::MissingRequiredInput {
4158                indicator: "lpc".to_string(),
4159                input: IndicatorInputKind::Ohlc,
4160            });
4161        }
4162    };
4163    let kernel = req.kernel.to_non_batch();
4164    collect_f64("lpc", output_id, req.combos, src.len(), |params| {
4165        let cutoff_type = get_enum_param("lpc", params, "cutoff_type", "adaptive")?;
4166        let fixed_period = get_usize_param("lpc", params, "fixed_period", 20)?;
4167        let max_cycle_limit = get_usize_param("lpc", params, "max_cycle_limit", 60)?;
4168        let cycle_mult = get_f64_param("lpc", params, "cycle_mult", 1.0)?;
4169        let tr_mult = get_f64_param("lpc", params, "tr_mult", 1.0)?;
4170        let input = LpcInput::from_slices(
4171            high,
4172            low,
4173            close,
4174            src,
4175            LpcParams {
4176                cutoff_type: Some(cutoff_type),
4177                fixed_period: Some(fixed_period),
4178                max_cycle_limit: Some(max_cycle_limit),
4179                cycle_mult: Some(cycle_mult),
4180                tr_mult: Some(tr_mult),
4181            },
4182        );
4183        let out =
4184            lpc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4185                indicator: "lpc".to_string(),
4186                details: e.to_string(),
4187            })?;
4188        if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
4189            return Ok(out.filter);
4190        }
4191        if output_id.eq_ignore_ascii_case("high_band") || output_id.eq_ignore_ascii_case("high") {
4192            return Ok(out.high_band);
4193        }
4194        if output_id.eq_ignore_ascii_case("low_band") || output_id.eq_ignore_ascii_case("low") {
4195            return Ok(out.low_band);
4196        }
4197        Err(IndicatorDispatchError::UnknownOutput {
4198            indicator: "lpc".to_string(),
4199            output: output_id.to_string(),
4200        })
4201    })
4202}
4203
4204fn compute_mab_batch(
4205    req: IndicatorBatchRequest<'_>,
4206    output_id: &str,
4207) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4208    let data = extract_slice_input("mab", req.data, "close")?;
4209    let kernel = req.kernel.to_non_batch();
4210    collect_f64("mab", output_id, req.combos, data.len(), |params| {
4211        let fast_period = get_usize_param("mab", params, "fast_period", 10)?;
4212        let slow_period = get_usize_param("mab", params, "slow_period", 50)?;
4213        let devup = get_f64_param("mab", params, "devup", 1.0)?;
4214        let devdn = get_f64_param("mab", params, "devdn", 1.0)?;
4215        let fast_ma_type = get_enum_param("mab", params, "fast_ma_type", "sma")?;
4216        let slow_ma_type = get_enum_param("mab", params, "slow_ma_type", "sma")?;
4217        let input = MabInput::from_slice(
4218            data,
4219            MabParams {
4220                fast_period: Some(fast_period),
4221                slow_period: Some(slow_period),
4222                devup: Some(devup),
4223                devdn: Some(devdn),
4224                fast_ma_type: Some(fast_ma_type),
4225                slow_ma_type: Some(slow_ma_type),
4226            },
4227        );
4228        let out =
4229            mab_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4230                indicator: "mab".to_string(),
4231                details: e.to_string(),
4232            })?;
4233        if output_id.eq_ignore_ascii_case("upperband")
4234            || output_id.eq_ignore_ascii_case("upper")
4235            || output_id.eq_ignore_ascii_case("value")
4236        {
4237            return Ok(out.upperband);
4238        }
4239        if output_id.eq_ignore_ascii_case("middleband") || output_id.eq_ignore_ascii_case("middle")
4240        {
4241            return Ok(out.middleband);
4242        }
4243        if output_id.eq_ignore_ascii_case("lowerband") || output_id.eq_ignore_ascii_case("lower") {
4244            return Ok(out.lowerband);
4245        }
4246        Err(IndicatorDispatchError::UnknownOutput {
4247            indicator: "mab".to_string(),
4248            output: output_id.to_string(),
4249        })
4250    })
4251}
4252
4253fn compute_macz_batch(
4254    req: IndicatorBatchRequest<'_>,
4255    output_id: &str,
4256) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4257    let (data, volume) = match req.data {
4258        IndicatorDataRef::Slice { values } => (values, None),
4259        IndicatorDataRef::Candles { candles, source } => (
4260            source_type(candles, source.unwrap_or("close")),
4261            Some(candles.volume.as_slice()),
4262        ),
4263        IndicatorDataRef::CloseVolume { close, volume } => {
4264            ensure_same_len_2("macz", close.len(), volume.len())?;
4265            (close, Some(volume))
4266        }
4267        IndicatorDataRef::Ohlc {
4268            open,
4269            high,
4270            low,
4271            close,
4272        } => {
4273            ensure_same_len_4("macz", open.len(), high.len(), low.len(), close.len())?;
4274            (close, None)
4275        }
4276        IndicatorDataRef::Ohlcv {
4277            open,
4278            high,
4279            low,
4280            close,
4281            volume,
4282        } => {
4283            ensure_same_len_5(
4284                "macz",
4285                open.len(),
4286                high.len(),
4287                low.len(),
4288                close.len(),
4289                volume.len(),
4290            )?;
4291            (close, Some(volume))
4292        }
4293        IndicatorDataRef::HighLow { .. } => {
4294            return Err(IndicatorDispatchError::MissingRequiredInput {
4295                indicator: "macz".to_string(),
4296                input: IndicatorInputKind::Slice,
4297            })
4298        }
4299    };
4300    let kernel = req.kernel.to_non_batch();
4301    collect_f64("macz", output_id, req.combos, data.len(), |params| {
4302        let fast_length = get_usize_param("macz", params, "fast_length", 12)?;
4303        let slow_length = get_usize_param("macz", params, "slow_length", 25)?;
4304        let signal_length = get_usize_param("macz", params, "signal_length", 9)?;
4305        let lengthz = get_usize_param("macz", params, "lengthz", 20)?;
4306        let length_stdev = get_usize_param("macz", params, "length_stdev", 25)?;
4307        let a = get_f64_param("macz", params, "a", 1.0)?;
4308        let b = get_f64_param("macz", params, "b", 1.0)?;
4309        let use_lag = get_bool_param("macz", params, "use_lag", false)?;
4310        let gamma = get_f64_param("macz", params, "gamma", 0.02)?;
4311        let macz_params = MaczParams {
4312            fast_length: Some(fast_length),
4313            slow_length: Some(slow_length),
4314            signal_length: Some(signal_length),
4315            lengthz: Some(lengthz),
4316            length_stdev: Some(length_stdev),
4317            a: Some(a),
4318            b: Some(b),
4319            use_lag: Some(use_lag),
4320            gamma: Some(gamma),
4321        };
4322        let input = if let Some(vol) = volume {
4323            MaczInput::from_slice_with_volume(data, vol, macz_params)
4324        } else {
4325            MaczInput::from_slice(data, macz_params)
4326        };
4327        let out = macz_with_kernel(&input, kernel).map_err(|e| {
4328            IndicatorDispatchError::ComputeFailed {
4329                indicator: "macz".to_string(),
4330                details: e.to_string(),
4331            }
4332        })?;
4333        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
4334            return Ok(out.values);
4335        }
4336        Err(IndicatorDispatchError::UnknownOutput {
4337            indicator: "macz".to_string(),
4338            output: output_id.to_string(),
4339        })
4340    })
4341}
4342
4343fn compute_minmax_batch(
4344    req: IndicatorBatchRequest<'_>,
4345    output_id: &str,
4346) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4347    let (high, low) = extract_high_low_input("minmax", req.data)?;
4348    let kernel = req.kernel.to_non_batch();
4349    collect_f64("minmax", output_id, req.combos, high.len(), |params| {
4350        let order = get_usize_param("minmax", params, "order", 3)?;
4351        let input = MinmaxInput::from_slices(high, low, MinmaxParams { order: Some(order) });
4352        let out = minmax_with_kernel(&input, kernel).map_err(|e| {
4353            IndicatorDispatchError::ComputeFailed {
4354                indicator: "minmax".to_string(),
4355                details: e.to_string(),
4356            }
4357        })?;
4358        if output_id.eq_ignore_ascii_case("is_min") || output_id.eq_ignore_ascii_case("value") {
4359            return Ok(out.is_min);
4360        }
4361        if output_id.eq_ignore_ascii_case("is_max") {
4362            return Ok(out.is_max);
4363        }
4364        if output_id.eq_ignore_ascii_case("last_min") {
4365            return Ok(out.last_min);
4366        }
4367        if output_id.eq_ignore_ascii_case("last_max") {
4368            return Ok(out.last_max);
4369        }
4370        Err(IndicatorDispatchError::UnknownOutput {
4371            indicator: "minmax".to_string(),
4372            output: output_id.to_string(),
4373        })
4374    })
4375}
4376
4377fn compute_msw_batch(
4378    req: IndicatorBatchRequest<'_>,
4379    output_id: &str,
4380) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4381    let data = extract_slice_input("msw", req.data, "close")?;
4382    let kernel = req.kernel.to_non_batch();
4383    collect_f64("msw", output_id, req.combos, data.len(), |params| {
4384        let period = get_usize_param("msw", params, "period", 5)?;
4385        let input = MswInput::from_slice(
4386            data,
4387            MswParams {
4388                period: Some(period),
4389            },
4390        );
4391        let out =
4392            msw_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4393                indicator: "msw".to_string(),
4394                details: e.to_string(),
4395            })?;
4396        if output_id.eq_ignore_ascii_case("sine") || output_id.eq_ignore_ascii_case("value") {
4397            return Ok(out.sine);
4398        }
4399        if output_id.eq_ignore_ascii_case("lead") {
4400            return Ok(out.lead);
4401        }
4402        Err(IndicatorDispatchError::UnknownOutput {
4403            indicator: "msw".to_string(),
4404            output: output_id.to_string(),
4405        })
4406    })
4407}
4408
4409fn compute_nadaraya_watson_envelope_batch(
4410    req: IndicatorBatchRequest<'_>,
4411    output_id: &str,
4412) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4413    let data = extract_slice_input("nadaraya_watson_envelope", req.data, "close")?;
4414    let kernel = req.kernel.to_non_batch();
4415    collect_f64(
4416        "nadaraya_watson_envelope",
4417        output_id,
4418        req.combos,
4419        data.len(),
4420        |params| {
4421            let bandwidth = get_f64_param("nadaraya_watson_envelope", params, "bandwidth", 8.0)?;
4422            let multiplier = get_f64_param("nadaraya_watson_envelope", params, "multiplier", 3.0)?;
4423            let lookback = get_usize_param("nadaraya_watson_envelope", params, "lookback", 500)?;
4424            let input = NweInput::from_slice(
4425                data,
4426                NweParams {
4427                    bandwidth: Some(bandwidth),
4428                    multiplier: Some(multiplier),
4429                    lookback: Some(lookback),
4430                },
4431            );
4432            let out = nadaraya_watson_envelope_with_kernel(&input, kernel).map_err(|e| {
4433                IndicatorDispatchError::ComputeFailed {
4434                    indicator: "nadaraya_watson_envelope".to_string(),
4435                    details: e.to_string(),
4436                }
4437            })?;
4438            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
4439                return Ok(out.upper);
4440            }
4441            if output_id.eq_ignore_ascii_case("lower") {
4442                return Ok(out.lower);
4443            }
4444            Err(IndicatorDispatchError::UnknownOutput {
4445                indicator: "nadaraya_watson_envelope".to_string(),
4446                output: output_id.to_string(),
4447            })
4448        },
4449    )
4450}
4451
4452fn compute_otto_batch(
4453    req: IndicatorBatchRequest<'_>,
4454    output_id: &str,
4455) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4456    let data = extract_slice_input("otto", req.data, "close")?;
4457    let kernel = req.kernel.to_non_batch();
4458    collect_f64("otto", output_id, req.combos, data.len(), |params| {
4459        let ott_period = get_usize_param("otto", params, "ott_period", 2)?;
4460        let ott_percent = get_f64_param("otto", params, "ott_percent", 0.6)?;
4461        let fast_vidya_length = get_usize_param("otto", params, "fast_vidya_length", 10)?;
4462        let slow_vidya_length = get_usize_param("otto", params, "slow_vidya_length", 25)?;
4463        let correcting_constant = get_f64_param("otto", params, "correcting_constant", 100000.0)?;
4464        let ma_type = get_enum_param("otto", params, "ma_type", "VAR")?;
4465        let input = OttoInput::from_slice(
4466            data,
4467            OttoParams {
4468                ott_period: Some(ott_period),
4469                ott_percent: Some(ott_percent),
4470                fast_vidya_length: Some(fast_vidya_length),
4471                slow_vidya_length: Some(slow_vidya_length),
4472                correcting_constant: Some(correcting_constant),
4473                ma_type: Some(ma_type),
4474            },
4475        );
4476        let out = otto_with_kernel(&input, kernel).map_err(|e| {
4477            IndicatorDispatchError::ComputeFailed {
4478                indicator: "otto".to_string(),
4479                details: e.to_string(),
4480            }
4481        })?;
4482        if output_id.eq_ignore_ascii_case("hott") || output_id.eq_ignore_ascii_case("value") {
4483            return Ok(out.hott);
4484        }
4485        if output_id.eq_ignore_ascii_case("lott") {
4486            return Ok(out.lott);
4487        }
4488        Err(IndicatorDispatchError::UnknownOutput {
4489            indicator: "otto".to_string(),
4490            output: output_id.to_string(),
4491        })
4492    })
4493}
4494
4495fn compute_pma_batch(
4496    req: IndicatorBatchRequest<'_>,
4497    output_id: &str,
4498) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4499    let data = extract_slice_input("pma", req.data, "close")?;
4500    let kernel = req.kernel.to_non_batch();
4501    collect_f64("pma", output_id, req.combos, data.len(), |_params| {
4502        let input = PmaInput::from_slice(data, PmaParams::default());
4503        let out =
4504            pma_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4505                indicator: "pma".to_string(),
4506                details: e.to_string(),
4507            })?;
4508        if output_id.eq_ignore_ascii_case("predict") || output_id.eq_ignore_ascii_case("value") {
4509            return Ok(out.predict);
4510        }
4511        if output_id.eq_ignore_ascii_case("trigger") {
4512            return Ok(out.trigger);
4513        }
4514        Err(IndicatorDispatchError::UnknownOutput {
4515            indicator: "pma".to_string(),
4516            output: output_id.to_string(),
4517        })
4518    })
4519}
4520
4521fn compute_prb_batch(
4522    req: IndicatorBatchRequest<'_>,
4523    output_id: &str,
4524) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4525    let data = extract_slice_input("prb", req.data, "close")?;
4526    let kernel = req.kernel.to_non_batch();
4527    collect_f64("prb", output_id, req.combos, data.len(), |params| {
4528        let smooth_data = get_bool_param("prb", params, "smooth_data", true)?;
4529        let smooth_period = get_usize_param("prb", params, "smooth_period", 10)?;
4530        let regression_period = get_usize_param("prb", params, "regression_period", 100)?;
4531        let polynomial_order = get_usize_param("prb", params, "polynomial_order", 2)?;
4532        let regression_offset = get_i32_param("prb", params, "regression_offset", 0)?;
4533        let ndev = get_f64_param("prb", params, "ndev", 2.0)?;
4534        let equ_from = get_usize_param("prb", params, "equ_from", 0)?;
4535        let input = PrbInput::from_slice(
4536            data,
4537            PrbParams {
4538                smooth_data: Some(smooth_data),
4539                smooth_period: Some(smooth_period),
4540                regression_period: Some(regression_period),
4541                polynomial_order: Some(polynomial_order),
4542                regression_offset: Some(regression_offset),
4543                ndev: Some(ndev),
4544                equ_from: Some(equ_from),
4545            },
4546        );
4547        let out =
4548            prb_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4549                indicator: "prb".to_string(),
4550                details: e.to_string(),
4551            })?;
4552        if output_id.eq_ignore_ascii_case("values") || output_id.eq_ignore_ascii_case("value") {
4553            return Ok(out.values);
4554        }
4555        if output_id.eq_ignore_ascii_case("upper_band") || output_id.eq_ignore_ascii_case("upper") {
4556            return Ok(out.upper_band);
4557        }
4558        if output_id.eq_ignore_ascii_case("lower_band") || output_id.eq_ignore_ascii_case("lower") {
4559            return Ok(out.lower_band);
4560        }
4561        Err(IndicatorDispatchError::UnknownOutput {
4562            indicator: "prb".to_string(),
4563            output: output_id.to_string(),
4564        })
4565    })
4566}
4567
4568fn compute_qqe_batch(
4569    req: IndicatorBatchRequest<'_>,
4570    output_id: &str,
4571) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4572    let data = extract_slice_input("qqe", req.data, "close")?;
4573    let kernel = req.kernel.to_non_batch();
4574    collect_f64("qqe", output_id, req.combos, data.len(), |params| {
4575        let rsi_period = get_usize_param("qqe", params, "rsi_period", 14)?;
4576        let smoothing_factor = get_usize_param("qqe", params, "smoothing_factor", 5)?;
4577        let fast_factor = get_f64_param("qqe", params, "fast_factor", 4.236)?;
4578        let input = QqeInput::from_slice(
4579            data,
4580            QqeParams {
4581                rsi_period: Some(rsi_period),
4582                smoothing_factor: Some(smoothing_factor),
4583                fast_factor: Some(fast_factor),
4584            },
4585        );
4586        let out =
4587            qqe_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4588                indicator: "qqe".to_string(),
4589                details: e.to_string(),
4590            })?;
4591        if output_id.eq_ignore_ascii_case("fast") || output_id.eq_ignore_ascii_case("value") {
4592            return Ok(out.fast);
4593        }
4594        if output_id.eq_ignore_ascii_case("slow") {
4595            return Ok(out.slow);
4596        }
4597        Err(IndicatorDispatchError::UnknownOutput {
4598            indicator: "qqe".to_string(),
4599            output: output_id.to_string(),
4600        })
4601    })
4602}
4603
4604fn compute_range_filter_batch(
4605    req: IndicatorBatchRequest<'_>,
4606    output_id: &str,
4607) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4608    let data = extract_slice_input("range_filter", req.data, "close")?;
4609    let kernel = req.kernel.to_non_batch();
4610    collect_f64(
4611        "range_filter",
4612        output_id,
4613        req.combos,
4614        data.len(),
4615        |params| {
4616            let range_size = get_f64_param("range_filter", params, "range_size", 2.618)?;
4617            let range_period = get_usize_param("range_filter", params, "range_period", 14)?;
4618            let smooth_range = get_bool_param("range_filter", params, "smooth_range", true)?;
4619            let smooth_period = get_usize_param("range_filter", params, "smooth_period", 27)?;
4620            let input = RangeFilterInput::from_slice(
4621                data,
4622                RangeFilterParams {
4623                    range_size: Some(range_size),
4624                    range_period: Some(range_period),
4625                    smooth_range: Some(smooth_range),
4626                    smooth_period: Some(smooth_period),
4627                },
4628            );
4629            let out = range_filter_with_kernel(&input, kernel).map_err(|e| {
4630                IndicatorDispatchError::ComputeFailed {
4631                    indicator: "range_filter".to_string(),
4632                    details: e.to_string(),
4633                }
4634            })?;
4635            if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
4636                return Ok(out.filter);
4637            }
4638            if output_id.eq_ignore_ascii_case("high_band") || output_id.eq_ignore_ascii_case("high")
4639            {
4640                return Ok(out.high_band);
4641            }
4642            if output_id.eq_ignore_ascii_case("low_band") || output_id.eq_ignore_ascii_case("low") {
4643                return Ok(out.low_band);
4644            }
4645            Err(IndicatorDispatchError::UnknownOutput {
4646                indicator: "range_filter".to_string(),
4647                output: output_id.to_string(),
4648            })
4649        },
4650    )
4651}
4652
4653fn compute_rsmk_batch(
4654    req: IndicatorBatchRequest<'_>,
4655    output_id: &str,
4656) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4657    let (main, compare) = match req.data {
4658        IndicatorDataRef::CloseVolume { close, volume } => {
4659            ensure_same_len_2("rsmk", close.len(), volume.len())?;
4660            (close, volume)
4661        }
4662        IndicatorDataRef::Ohlcv {
4663            open,
4664            high,
4665            low,
4666            close,
4667            volume,
4668        } => {
4669            ensure_same_len_5(
4670                "rsmk",
4671                open.len(),
4672                high.len(),
4673                low.len(),
4674                close.len(),
4675                volume.len(),
4676            )?;
4677            (close, volume)
4678        }
4679        IndicatorDataRef::Candles { candles, source } => (
4680            source_type(candles, source.unwrap_or("close")),
4681            candles.volume.as_slice(),
4682        ),
4683        _ => {
4684            return Err(IndicatorDispatchError::MissingRequiredInput {
4685                indicator: "rsmk".to_string(),
4686                input: IndicatorInputKind::CloseVolume,
4687            });
4688        }
4689    };
4690    let kernel = req.kernel.to_non_batch();
4691    collect_f64("rsmk", output_id, req.combos, main.len(), |params| {
4692        let lookback = get_usize_param("rsmk", params, "lookback", 90)?;
4693        let period = get_usize_param("rsmk", params, "period", 3)?;
4694        let signal_period = get_usize_param("rsmk", params, "signal_period", 20)?;
4695        let matype = get_enum_param("rsmk", params, "matype", "ema")?;
4696        let signal_matype = get_enum_param("rsmk", params, "signal_matype", "ema")?;
4697        let input = RsmkInput::from_slices(
4698            main,
4699            compare,
4700            RsmkParams {
4701                lookback: Some(lookback),
4702                period: Some(period),
4703                signal_period: Some(signal_period),
4704                matype: Some(matype),
4705                signal_matype: Some(signal_matype),
4706            },
4707        );
4708        let out = rsmk_with_kernel(&input, kernel).map_err(|e| {
4709            IndicatorDispatchError::ComputeFailed {
4710                indicator: "rsmk".to_string(),
4711                details: e.to_string(),
4712            }
4713        })?;
4714        if output_id.eq_ignore_ascii_case("indicator") || output_id.eq_ignore_ascii_case("value") {
4715            return Ok(out.indicator);
4716        }
4717        if output_id.eq_ignore_ascii_case("signal") {
4718            return Ok(out.signal);
4719        }
4720        Err(IndicatorDispatchError::UnknownOutput {
4721            indicator: "rsmk".to_string(),
4722            output: output_id.to_string(),
4723        })
4724    })
4725}
4726
4727fn compute_voss_batch(
4728    req: IndicatorBatchRequest<'_>,
4729    output_id: &str,
4730) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4731    let data = extract_slice_input("voss", req.data, "close")?;
4732    let kernel = req.kernel.to_non_batch();
4733    collect_f64("voss", output_id, req.combos, data.len(), |params| {
4734        let period = get_usize_param("voss", params, "period", 20)?;
4735        let predict = get_usize_param("voss", params, "predict", 3)?;
4736        let bandwidth = get_f64_param("voss", params, "bandwidth", 0.25)?;
4737        let input = VossInput::from_slice(
4738            data,
4739            VossParams {
4740                period: Some(period),
4741                predict: Some(predict),
4742                bandwidth: Some(bandwidth),
4743            },
4744        );
4745        let out = voss_with_kernel(&input, kernel).map_err(|e| {
4746            IndicatorDispatchError::ComputeFailed {
4747                indicator: "voss".to_string(),
4748                details: e.to_string(),
4749            }
4750        })?;
4751        if output_id.eq_ignore_ascii_case("voss") || output_id.eq_ignore_ascii_case("value") {
4752            return Ok(out.voss);
4753        }
4754        if output_id.eq_ignore_ascii_case("filt") || output_id.eq_ignore_ascii_case("filter") {
4755            return Ok(out.filt);
4756        }
4757        Err(IndicatorDispatchError::UnknownOutput {
4758            indicator: "voss".to_string(),
4759            output: output_id.to_string(),
4760        })
4761    })
4762}
4763
4764fn compute_pivot_batch(
4765    req: IndicatorBatchRequest<'_>,
4766    output_id: &str,
4767) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4768    let (open, high, low, close) = extract_ohlc_full_input("pivot", req.data)?;
4769    let kernel = req.kernel.to_non_batch();
4770    collect_f64("pivot", output_id, req.combos, close.len(), |params| {
4771        let mode = get_usize_param("pivot", params, "mode", 3)?;
4772        let input =
4773            PivotInput::from_slices(high, low, close, open, PivotParams { mode: Some(mode) });
4774        let out = pivot_with_kernel(&input, kernel).map_err(|e| {
4775            IndicatorDispatchError::ComputeFailed {
4776                indicator: "pivot".to_string(),
4777                details: e.to_string(),
4778            }
4779        })?;
4780        if output_id.eq_ignore_ascii_case("pp") || output_id.eq_ignore_ascii_case("value") {
4781            return Ok(out.pp);
4782        }
4783        if output_id.eq_ignore_ascii_case("r1") {
4784            return Ok(out.r1);
4785        }
4786        if output_id.eq_ignore_ascii_case("r2") {
4787            return Ok(out.r2);
4788        }
4789        if output_id.eq_ignore_ascii_case("r3") {
4790            return Ok(out.r3);
4791        }
4792        if output_id.eq_ignore_ascii_case("r4") {
4793            return Ok(out.r4);
4794        }
4795        if output_id.eq_ignore_ascii_case("s1") {
4796            return Ok(out.s1);
4797        }
4798        if output_id.eq_ignore_ascii_case("s2") {
4799            return Ok(out.s2);
4800        }
4801        if output_id.eq_ignore_ascii_case("s3") {
4802            return Ok(out.s3);
4803        }
4804        if output_id.eq_ignore_ascii_case("s4") {
4805            return Ok(out.s4);
4806        }
4807        Err(IndicatorDispatchError::UnknownOutput {
4808            indicator: "pivot".to_string(),
4809            output: output_id.to_string(),
4810        })
4811    })
4812}
4813
4814fn ma_data_from_req<'a>(
4815    indicator: &str,
4816    data: IndicatorDataRef<'a>,
4817) -> Result<MaData<'a>, IndicatorDispatchError> {
4818    match data {
4819        IndicatorDataRef::Slice { values } => Ok(MaData::Slice(values)),
4820        IndicatorDataRef::Candles { candles, source } => Ok(MaData::Candles {
4821            candles,
4822            source: source.unwrap_or("close"),
4823        }),
4824        IndicatorDataRef::Ohlc { close, .. } => Ok(MaData::Slice(close)),
4825        IndicatorDataRef::Ohlcv { close, .. } => Ok(MaData::Slice(close)),
4826        IndicatorDataRef::CloseVolume { close, .. } => Ok(MaData::Slice(close)),
4827        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
4828            indicator: indicator.to_string(),
4829            input: IndicatorInputKind::Slice,
4830        }),
4831    }
4832}
4833
4834fn ma_len_from_req(
4835    indicator: &str,
4836    data: IndicatorDataRef<'_>,
4837) -> Result<usize, IndicatorDispatchError> {
4838    match data {
4839        IndicatorDataRef::Slice { values } => Ok(values.len()),
4840        IndicatorDataRef::Candles { candles, source } => {
4841            Ok(source_type(candles, source.unwrap_or("close")).len())
4842        }
4843        IndicatorDataRef::Ohlc { close, .. } => Ok(close.len()),
4844        IndicatorDataRef::Ohlcv { close, .. } => Ok(close.len()),
4845        IndicatorDataRef::CloseVolume { close, .. } => Ok(close.len()),
4846        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
4847            indicator: indicator.to_string(),
4848            input: IndicatorInputKind::Slice,
4849        }),
4850    }
4851}
4852
4853fn ma_period_for_combo(
4854    info: &IndicatorInfo,
4855    params: &[ParamKV<'_>],
4856) -> Result<usize, IndicatorDispatchError> {
4857    if let Some(v) = find_param(params, "period") {
4858        return parse_usize_param_value(info.id, "period", v);
4859    }
4860    if let Some(default) = info
4861        .params
4862        .iter()
4863        .find(|p| p.key.eq_ignore_ascii_case("period"))
4864        .and_then(|p| p.default.as_ref())
4865    {
4866        if let ParamValueStatic::Int(v) = default {
4867            if *v >= 0 {
4868                return Ok(*v as usize);
4869            }
4870        }
4871    }
4872    Ok(14)
4873}
4874
4875fn convert_ma_params<'a>(
4876    params: &'a [ParamKV<'a>],
4877    indicator: &str,
4878    output_id: &str,
4879) -> Result<Vec<MaBatchParamKV<'a>>, IndicatorDispatchError> {
4880    let mut out = Vec::with_capacity(params.len());
4881    for p in params {
4882        if p.key.eq_ignore_ascii_case("period") {
4883            continue;
4884        }
4885        if p.key.eq_ignore_ascii_case("output") {
4886            let selected = match p.value {
4887                ParamValue::EnumString(v) => v,
4888                _ => {
4889                    return Err(IndicatorDispatchError::InvalidParam {
4890                        indicator: indicator.to_string(),
4891                        key: "output".to_string(),
4892                        reason: "expected EnumString".to_string(),
4893                    })
4894                }
4895            };
4896            if !selected.eq_ignore_ascii_case(output_id) {
4897                return Err(IndicatorDispatchError::InvalidParam {
4898                    indicator: indicator.to_string(),
4899                    key: "output".to_string(),
4900                    reason: format!(
4901                        "param output '{}' does not match requested output_id '{}'",
4902                        selected, output_id
4903                    ),
4904                });
4905            }
4906        }
4907        let value = match p.value {
4908            ParamValue::Int(v) => MaBatchParamValue::Int(v),
4909            ParamValue::Float(v) => {
4910                if !v.is_finite() {
4911                    return Err(IndicatorDispatchError::InvalidParam {
4912                        indicator: indicator.to_string(),
4913                        key: p.key.to_string(),
4914                        reason: "expected finite float".to_string(),
4915                    });
4916                }
4917                MaBatchParamValue::Float(v)
4918            }
4919            ParamValue::Bool(v) => MaBatchParamValue::Bool(v),
4920            ParamValue::EnumString(v) => MaBatchParamValue::EnumString(v),
4921        };
4922        out.push(MaBatchParamKV { key: p.key, value });
4923    }
4924    Ok(out)
4925}
4926
4927fn extract_slice_input<'a>(
4928    indicator: &str,
4929    data: IndicatorDataRef<'a>,
4930    default_source: &'a str,
4931) -> Result<&'a [f64], IndicatorDispatchError> {
4932    match data {
4933        IndicatorDataRef::Slice { values } => Ok(values),
4934        IndicatorDataRef::Candles { candles, source } => {
4935            Ok(source_type(candles, source.unwrap_or(default_source)))
4936        }
4937        IndicatorDataRef::Ohlc { close, .. } => Ok(close),
4938        IndicatorDataRef::Ohlcv { close, .. } => Ok(close),
4939        IndicatorDataRef::CloseVolume { close, .. } => Ok(close),
4940        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
4941            indicator: indicator.to_string(),
4942            input: IndicatorInputKind::Slice,
4943        }),
4944    }
4945}
4946
4947fn extract_ohlc_input<'a>(
4948    indicator: &str,
4949    data: IndicatorDataRef<'a>,
4950) -> Result<(&'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
4951    match data {
4952        IndicatorDataRef::Candles { candles, .. } => Ok((
4953            candles.high.as_slice(),
4954            candles.low.as_slice(),
4955            candles.close.as_slice(),
4956        )),
4957        IndicatorDataRef::Ohlc {
4958            high,
4959            low,
4960            close,
4961            open,
4962        } => {
4963            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
4964            Ok((high, low, close))
4965        }
4966        IndicatorDataRef::Ohlcv {
4967            high,
4968            low,
4969            close,
4970            open,
4971            volume,
4972        } => {
4973            ensure_same_len_5(
4974                indicator,
4975                open.len(),
4976                high.len(),
4977                low.len(),
4978                close.len(),
4979                volume.len(),
4980            )?;
4981            Ok((high, low, close))
4982        }
4983        _ => Err(IndicatorDispatchError::MissingRequiredInput {
4984            indicator: indicator.to_string(),
4985            input: IndicatorInputKind::Ohlc,
4986        }),
4987    }
4988}
4989
4990fn extract_ohlc_full_input<'a>(
4991    indicator: &str,
4992    data: IndicatorDataRef<'a>,
4993) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
4994    match data {
4995        IndicatorDataRef::Candles { candles, .. } => Ok((
4996            candles.open.as_slice(),
4997            candles.high.as_slice(),
4998            candles.low.as_slice(),
4999            candles.close.as_slice(),
5000        )),
5001        IndicatorDataRef::Ohlc {
5002            open,
5003            high,
5004            low,
5005            close,
5006        } => {
5007            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
5008            Ok((open, high, low, close))
5009        }
5010        IndicatorDataRef::Ohlcv {
5011            open,
5012            high,
5013            low,
5014            close,
5015            volume,
5016        } => {
5017            ensure_same_len_5(
5018                indicator,
5019                open.len(),
5020                high.len(),
5021                low.len(),
5022                close.len(),
5023                volume.len(),
5024            )?;
5025            Ok((open, high, low, close))
5026        }
5027        _ => Err(IndicatorDispatchError::MissingRequiredInput {
5028            indicator: indicator.to_string(),
5029            input: IndicatorInputKind::Ohlc,
5030        }),
5031    }
5032}
5033
5034fn extract_ohlcv_full_input<'a>(
5035    indicator: &str,
5036    data: IndicatorDataRef<'a>,
5037) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
5038    match data {
5039        IndicatorDataRef::Candles { candles, .. } => Ok((
5040            candles.open.as_slice(),
5041            candles.high.as_slice(),
5042            candles.low.as_slice(),
5043            candles.close.as_slice(),
5044            candles.volume.as_slice(),
5045        )),
5046        IndicatorDataRef::Ohlcv {
5047            open,
5048            high,
5049            low,
5050            close,
5051            volume,
5052        } => {
5053            ensure_same_len_5(
5054                indicator,
5055                open.len(),
5056                high.len(),
5057                low.len(),
5058                close.len(),
5059                volume.len(),
5060            )?;
5061            Ok((open, high, low, close, volume))
5062        }
5063        _ => Err(IndicatorDispatchError::MissingRequiredInput {
5064            indicator: indicator.to_string(),
5065            input: IndicatorInputKind::Ohlcv,
5066        }),
5067    }
5068}
5069
5070fn extract_high_low_input<'a>(
5071    indicator: &str,
5072    data: IndicatorDataRef<'a>,
5073) -> Result<(&'a [f64], &'a [f64]), IndicatorDispatchError> {
5074    match data {
5075        IndicatorDataRef::Candles { candles, .. } => {
5076            Ok((candles.high.as_slice(), candles.low.as_slice()))
5077        }
5078        IndicatorDataRef::Ohlc {
5079            high,
5080            low,
5081            open,
5082            close,
5083        } => {
5084            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
5085            Ok((high, low))
5086        }
5087        IndicatorDataRef::Ohlcv {
5088            high,
5089            low,
5090            open,
5091            close,
5092            volume,
5093        } => {
5094            ensure_same_len_5(
5095                indicator,
5096                open.len(),
5097                high.len(),
5098                low.len(),
5099                close.len(),
5100                volume.len(),
5101            )?;
5102            Ok((high, low))
5103        }
5104        IndicatorDataRef::HighLow { high, low } => {
5105            ensure_same_len_2(indicator, high.len(), low.len())?;
5106            Ok((high, low))
5107        }
5108        _ => Err(IndicatorDispatchError::MissingRequiredInput {
5109            indicator: indicator.to_string(),
5110            input: IndicatorInputKind::HighLow,
5111        }),
5112    }
5113}
5114
5115fn extract_hlcv_input<'a>(
5116    indicator: &str,
5117    data: IndicatorDataRef<'a>,
5118) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
5119    match data {
5120        IndicatorDataRef::Candles { candles, .. } => Ok((
5121            candles.high.as_slice(),
5122            candles.low.as_slice(),
5123            candles.close.as_slice(),
5124            candles.volume.as_slice(),
5125        )),
5126        IndicatorDataRef::Ohlcv {
5127            open,
5128            high,
5129            low,
5130            close,
5131            volume,
5132        } => {
5133            ensure_same_len_5(
5134                indicator,
5135                open.len(),
5136                high.len(),
5137                low.len(),
5138                close.len(),
5139                volume.len(),
5140            )?;
5141            Ok((high, low, close, volume))
5142        }
5143        _ => Err(IndicatorDispatchError::MissingRequiredInput {
5144            indicator: indicator.to_string(),
5145            input: IndicatorInputKind::Ohlcv,
5146        }),
5147    }
5148}
5149
5150fn extract_volume_input<'a>(
5151    indicator: &str,
5152    data: IndicatorDataRef<'a>,
5153) -> Result<&'a [f64], IndicatorDispatchError> {
5154    match data {
5155        IndicatorDataRef::Slice { values } => Ok(values),
5156        IndicatorDataRef::Candles { candles, source } => {
5157            Ok(source_type(candles, source.unwrap_or("volume")))
5158        }
5159        IndicatorDataRef::CloseVolume { close, volume } => {
5160            ensure_same_len_2(indicator, close.len(), volume.len())?;
5161            Ok(volume)
5162        }
5163        IndicatorDataRef::Ohlcv {
5164            open,
5165            high,
5166            low,
5167            close,
5168            volume,
5169        } => {
5170            ensure_same_len_5(
5171                indicator,
5172                open.len(),
5173                high.len(),
5174                low.len(),
5175                close.len(),
5176                volume.len(),
5177            )?;
5178            Ok(volume)
5179        }
5180        _ => Err(IndicatorDispatchError::MissingRequiredInput {
5181            indicator: indicator.to_string(),
5182            input: IndicatorInputKind::Slice,
5183        }),
5184    }
5185}
5186
5187fn extract_close_volume_input<'a>(
5188    indicator: &str,
5189    data: IndicatorDataRef<'a>,
5190    default_close_source: &'a str,
5191) -> Result<(&'a [f64], &'a [f64]), IndicatorDispatchError> {
5192    match data {
5193        IndicatorDataRef::CloseVolume { close, volume } => {
5194            ensure_same_len_2(indicator, close.len(), volume.len())?;
5195            Ok((close, volume))
5196        }
5197        IndicatorDataRef::Ohlcv {
5198            close,
5199            volume,
5200            open,
5201            high,
5202            low,
5203        } => {
5204            ensure_same_len_5(
5205                indicator,
5206                open.len(),
5207                high.len(),
5208                low.len(),
5209                close.len(),
5210                volume.len(),
5211            )?;
5212            Ok((close, volume))
5213        }
5214        IndicatorDataRef::Candles { candles, source } => {
5215            let close = source_type(candles, source.unwrap_or(default_close_source));
5216            let volume = candles.volume.as_slice();
5217            ensure_same_len_2(indicator, close.len(), volume.len())?;
5218            Ok((close, volume))
5219        }
5220        _ => Err(IndicatorDispatchError::MissingRequiredInput {
5221            indicator: indicator.to_string(),
5222            input: IndicatorInputKind::CloseVolume,
5223        }),
5224    }
5225}
5226
5227fn f64_output(output_id: &str, rows: usize, cols: usize, values: Vec<f64>) -> IndicatorBatchOutput {
5228    IndicatorBatchOutput {
5229        output_id: output_id.to_string(),
5230        rows,
5231        cols,
5232        values_f64: Some(values),
5233        values_i32: None,
5234        values_bool: None,
5235    }
5236}
5237
5238fn bool_output(
5239    output_id: &str,
5240    rows: usize,
5241    cols: usize,
5242    values: Vec<bool>,
5243) -> IndicatorBatchOutput {
5244    IndicatorBatchOutput {
5245        output_id: output_id.to_string(),
5246        rows,
5247        cols,
5248        values_f64: None,
5249        values_i32: None,
5250        values_bool: Some(values),
5251    }
5252}
5253
5254fn expect_value_output(indicator: &str, output_id: &str) -> Result<(), IndicatorDispatchError> {
5255    if output_id.eq_ignore_ascii_case("value") {
5256        return Ok(());
5257    }
5258    Err(IndicatorDispatchError::UnknownOutput {
5259        indicator: indicator.to_string(),
5260        output: output_id.to_string(),
5261    })
5262}
5263
5264fn ensure_len(indicator: &str, expected: usize, got: usize) -> Result<(), IndicatorDispatchError> {
5265    if expected == got {
5266        return Ok(());
5267    }
5268    Err(IndicatorDispatchError::DataLengthMismatch {
5269        details: format!("{indicator}: expected output length {expected}, got {got}"),
5270    })
5271}
5272
5273fn ensure_same_len_2(indicator: &str, a: usize, b: usize) -> Result<(), IndicatorDispatchError> {
5274    if a == b {
5275        return Ok(());
5276    }
5277    Err(IndicatorDispatchError::DataLengthMismatch {
5278        details: format!("{indicator}: expected equal lengths, got {a} and {b}"),
5279    })
5280}
5281
5282fn ensure_same_len_3(
5283    indicator: &str,
5284    a: usize,
5285    b: usize,
5286    c: usize,
5287) -> Result<(), IndicatorDispatchError> {
5288    if a == b && b == c {
5289        return Ok(());
5290    }
5291    Err(IndicatorDispatchError::DataLengthMismatch {
5292        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}"),
5293    })
5294}
5295
5296fn ensure_same_len_4(
5297    indicator: &str,
5298    a: usize,
5299    b: usize,
5300    c: usize,
5301    d: usize,
5302) -> Result<(), IndicatorDispatchError> {
5303    if a == b && b == c && c == d {
5304        return Ok(());
5305    }
5306    Err(IndicatorDispatchError::DataLengthMismatch {
5307        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}, {d}"),
5308    })
5309}
5310
5311fn ensure_same_len_5(
5312    indicator: &str,
5313    a: usize,
5314    b: usize,
5315    c: usize,
5316    d: usize,
5317    e: usize,
5318) -> Result<(), IndicatorDispatchError> {
5319    if a == b && b == c && c == d && d == e {
5320        return Ok(());
5321    }
5322    Err(IndicatorDispatchError::DataLengthMismatch {
5323        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}, {d}, {e}"),
5324    })
5325}
5326
5327fn has_key(params: &[ParamKV<'_>], key: &str) -> bool {
5328    params.iter().any(|kv| kv.key.eq_ignore_ascii_case(key))
5329}
5330
5331fn find_param<'a>(params: &'a [ParamKV<'a>], key: &str) -> Option<&'a ParamValue<'a>> {
5332    params
5333        .iter()
5334        .rev()
5335        .find(|kv| kv.key.eq_ignore_ascii_case(key))
5336        .map(|kv| &kv.value)
5337}
5338
5339fn get_usize_param(
5340    indicator: &str,
5341    params: &[ParamKV<'_>],
5342    key: &str,
5343    default: usize,
5344) -> Result<usize, IndicatorDispatchError> {
5345    match find_param(params, key) {
5346        Some(v) => parse_usize_param_value(indicator, key, v),
5347        None => Ok(default),
5348    }
5349}
5350
5351fn get_usize_param_with_aliases(
5352    indicator: &str,
5353    params: &[ParamKV<'_>],
5354    keys: &[&str],
5355    default: usize,
5356) -> Result<usize, IndicatorDispatchError> {
5357    for key in keys {
5358        if let Some(v) = find_param(params, key) {
5359            return parse_usize_param_value(indicator, key, v);
5360        }
5361    }
5362    Ok(default)
5363}
5364
5365fn parse_usize_param_value(
5366    indicator: &str,
5367    key: &str,
5368    value: &ParamValue<'_>,
5369) -> Result<usize, IndicatorDispatchError> {
5370    match value {
5371        ParamValue::Int(v) => {
5372            if *v < 0 {
5373                return Err(IndicatorDispatchError::InvalidParam {
5374                    indicator: indicator.to_string(),
5375                    key: key.to_string(),
5376                    reason: "expected integer >= 0".to_string(),
5377                });
5378            }
5379            Ok(*v as usize)
5380        }
5381        ParamValue::Float(v) => {
5382            if !v.is_finite() {
5383                return Err(IndicatorDispatchError::InvalidParam {
5384                    indicator: indicator.to_string(),
5385                    key: key.to_string(),
5386                    reason: "expected finite number".to_string(),
5387                });
5388            }
5389            if *v < 0.0 {
5390                return Err(IndicatorDispatchError::InvalidParam {
5391                    indicator: indicator.to_string(),
5392                    key: key.to_string(),
5393                    reason: "expected number >= 0".to_string(),
5394                });
5395            }
5396            let r = v.round();
5397            if (*v - r).abs() > 1e-9 {
5398                return Err(IndicatorDispatchError::InvalidParam {
5399                    indicator: indicator.to_string(),
5400                    key: key.to_string(),
5401                    reason: "expected integer value".to_string(),
5402                });
5403            }
5404            Ok(r as usize)
5405        }
5406        _ => Err(IndicatorDispatchError::InvalidParam {
5407            indicator: indicator.to_string(),
5408            key: key.to_string(),
5409            reason: "expected Int or Float".to_string(),
5410        }),
5411    }
5412}
5413
5414fn get_f64_param(
5415    indicator: &str,
5416    params: &[ParamKV<'_>],
5417    key: &str,
5418    default: f64,
5419) -> Result<f64, IndicatorDispatchError> {
5420    match find_param(params, key) {
5421        Some(ParamValue::Int(v)) => Ok(*v as f64),
5422        Some(ParamValue::Float(v)) => {
5423            if v.is_finite() {
5424                Ok(*v)
5425            } else {
5426                Err(IndicatorDispatchError::InvalidParam {
5427                    indicator: indicator.to_string(),
5428                    key: key.to_string(),
5429                    reason: "expected finite float".to_string(),
5430                })
5431            }
5432        }
5433        Some(_) => Err(IndicatorDispatchError::InvalidParam {
5434            indicator: indicator.to_string(),
5435            key: key.to_string(),
5436            reason: "expected Int or Float".to_string(),
5437        }),
5438        None => Ok(default),
5439    }
5440}
5441
5442fn get_bool_param(
5443    indicator: &str,
5444    params: &[ParamKV<'_>],
5445    key: &str,
5446    default: bool,
5447) -> Result<bool, IndicatorDispatchError> {
5448    match find_param(params, key) {
5449        Some(ParamValue::Bool(v)) => Ok(*v),
5450        Some(ParamValue::Int(v)) => match *v {
5451            0 => Ok(false),
5452            1 => Ok(true),
5453            _ => Err(IndicatorDispatchError::InvalidParam {
5454                indicator: indicator.to_string(),
5455                key: key.to_string(),
5456                reason: "expected Bool or Int(0/1)".to_string(),
5457            }),
5458        },
5459        Some(_) => Err(IndicatorDispatchError::InvalidParam {
5460            indicator: indicator.to_string(),
5461            key: key.to_string(),
5462            reason: "expected Bool".to_string(),
5463        }),
5464        None => Ok(default),
5465    }
5466}
5467
5468fn get_i32_param(
5469    indicator: &str,
5470    params: &[ParamKV<'_>],
5471    key: &str,
5472    default: i32,
5473) -> Result<i32, IndicatorDispatchError> {
5474    match find_param(params, key) {
5475        Some(ParamValue::Int(v)) => {
5476            if *v < i32::MIN as i64 || *v > i32::MAX as i64 {
5477                return Err(IndicatorDispatchError::InvalidParam {
5478                    indicator: indicator.to_string(),
5479                    key: key.to_string(),
5480                    reason: "integer out of i32 range".to_string(),
5481                });
5482            }
5483            Ok(*v as i32)
5484        }
5485        Some(ParamValue::Float(v)) => {
5486            if !v.is_finite() {
5487                return Err(IndicatorDispatchError::InvalidParam {
5488                    indicator: indicator.to_string(),
5489                    key: key.to_string(),
5490                    reason: "expected finite number".to_string(),
5491                });
5492            }
5493            let r = v.round();
5494            if (*v - r).abs() > 1e-9 || r < i32::MIN as f64 || r > i32::MAX as f64 {
5495                return Err(IndicatorDispatchError::InvalidParam {
5496                    indicator: indicator.to_string(),
5497                    key: key.to_string(),
5498                    reason: "expected i32-compatible whole number".to_string(),
5499                });
5500            }
5501            Ok(r as i32)
5502        }
5503        Some(_) => Err(IndicatorDispatchError::InvalidParam {
5504            indicator: indicator.to_string(),
5505            key: key.to_string(),
5506            reason: "expected Int or Float".to_string(),
5507        }),
5508        None => Ok(default),
5509    }
5510}
5511
5512fn get_enum_param(
5513    indicator: &str,
5514    params: &[ParamKV<'_>],
5515    key: &str,
5516    default: &str,
5517) -> Result<String, IndicatorDispatchError> {
5518    match find_param(params, key) {
5519        Some(ParamValue::EnumString(v)) => Ok((*v).to_string()),
5520        Some(_) => Err(IndicatorDispatchError::InvalidParam {
5521            indicator: indicator.to_string(),
5522            key: key.to_string(),
5523            reason: "expected EnumString".to_string(),
5524        }),
5525        None => Ok(default.to_string()),
5526    }
5527}
5528
5529#[cfg(test)]
5530mod tests {
5531    use super::*;
5532    use crate::indicators::ad::{ad_with_kernel, AdInput, AdParams};
5533    use crate::indicators::adx::{adx_with_kernel, AdxInput, AdxParams};
5534    use crate::indicators::ao::{ao_with_kernel, AoInput, AoParams};
5535    use crate::indicators::apo::{apo_with_kernel, ApoInput, ApoParams};
5536    use crate::indicators::cg::{cg_with_kernel, CgInput, CgParams};
5537    use crate::indicators::cmo::{cmo_with_kernel, CmoInput, CmoParams};
5538    use crate::indicators::deviation::{deviation_with_kernel, DeviationInput, DeviationParams};
5539    use crate::indicators::dx::{
5540        dx_batch_with_kernel, dx_with_kernel, DxBatchRange, DxInput, DxParams,
5541    };
5542    use crate::indicators::efi::{efi_with_kernel, EfiInput, EfiParams};
5543    use crate::indicators::fosc::{fosc_with_kernel, FoscInput, FoscParams};
5544    use crate::indicators::ift_rsi::{ift_rsi_with_kernel, IftRsiInput, IftRsiParams};
5545    use crate::indicators::kvo::{kvo_with_kernel, KvoInput, KvoParams};
5546    use crate::indicators::linearreg_angle::{
5547        linearreg_angle_with_kernel, Linearreg_angleInput, Linearreg_angleParams,
5548    };
5549    use crate::indicators::linearreg_intercept::{
5550        linearreg_intercept_with_kernel, LinearRegInterceptInput, LinearRegInterceptParams,
5551    };
5552    use crate::indicators::linearreg_slope::{
5553        linearreg_slope_with_kernel, LinearRegSlopeInput, LinearRegSlopeParams,
5554    };
5555    use crate::indicators::macd::{macd_with_kernel, MacdInput, MacdParams};
5556    use crate::indicators::mean_ad::{mean_ad_with_kernel, MeanAdInput, MeanAdParams};
5557    use crate::indicators::medprice::{medprice_with_kernel, MedpriceInput, MedpriceParams};
5558    use crate::indicators::mfi::{
5559        mfi_batch_with_kernel, mfi_with_kernel, MfiBatchRange, MfiInput, MfiParams,
5560    };
5561    use crate::indicators::moving_averages::ma::MaData;
5562    use crate::indicators::moving_averages::ma_batch::{
5563        ma_batch_with_kernel_and_typed_params, MaBatchParamKV, MaBatchParamValue,
5564    };
5565    use crate::indicators::natr::{natr_with_kernel, NatrInput, NatrParams};
5566    use crate::indicators::percentile_nearest_rank::{
5567        percentile_nearest_rank_with_kernel, PercentileNearestRankInput,
5568        PercentileNearestRankParams,
5569    };
5570    use crate::indicators::ppo::{ppo_with_kernel, PpoInput, PpoParams};
5571    use crate::indicators::pvi::{pvi_with_kernel, PviInput, PviParams};
5572    use crate::indicators::registry::{list_indicators, IndicatorParamKind};
5573    use crate::indicators::trix::{
5574        trix_batch_with_kernel, trix_with_kernel, TrixBatchRange, TrixInput, TrixParams,
5575    };
5576    use crate::indicators::ttm_trend::{ttm_trend_with_kernel, TtmTrendInput, TtmTrendParams};
5577    use crate::indicators::vpci::{vpci_with_kernel, VpciInput, VpciParams};
5578    use crate::indicators::zscore::{zscore_with_kernel, ZscoreInput, ZscoreParams};
5579    use crate::utilities::enums::Kernel;
5580    use std::time::Instant;
5581
5582    fn sample_series() -> Vec<f64> {
5583        (1..=64).map(|v| v as f64).collect()
5584    }
5585
5586    fn sample_ohlc() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
5587        let open: Vec<f64> = (0..128).map(|i| 100.0 + (i as f64 * 0.1)).collect();
5588        let high: Vec<f64> = open.iter().map(|v| v + 1.25).collect();
5589        let low: Vec<f64> = open.iter().map(|v| v - 1.1).collect();
5590        let close: Vec<f64> = open.iter().map(|v| v + 0.3).collect();
5591        (open, high, low, close)
5592    }
5593
5594    fn sample_candles() -> crate::utilities::data_loader::Candles {
5595        let (open, high, low, close) = sample_ohlc();
5596        let volume: Vec<f64> = (0..close.len()).map(|i| 1000.0 + (i as f64)).collect();
5597        let timestamp: Vec<i64> = (0..close.len()).map(|i| i as i64).collect();
5598        crate::utilities::data_loader::Candles::new(timestamp, open, high, low, close, volume)
5599    }
5600
5601    fn assert_series_eq(actual: &[f64], expected: &[f64], tol: f64) {
5602        assert_eq!(actual.len(), expected.len());
5603        for i in 0..actual.len() {
5604            let a = actual[i];
5605            let b = expected[i];
5606            if a.is_nan() && b.is_nan() {
5607                continue;
5608            }
5609            assert!(
5610                (a - b).abs() <= tol,
5611                "mismatch at index {i}: actual={a}, expected={b}, tol={tol}"
5612            );
5613        }
5614    }
5615
5616    #[test]
5617    fn unknown_indicator_is_rejected() {
5618        let data = sample_series();
5619        let req = IndicatorBatchRequest {
5620            indicator_id: "not_real",
5621            output_id: None,
5622            data: IndicatorDataRef::Slice { values: &data },
5623            combos: &[],
5624            kernel: Kernel::Auto,
5625        };
5626        let err = compute_cpu_batch(req).unwrap_err();
5627        assert!(matches!(
5628            err,
5629            IndicatorDispatchError::UnknownIndicator { .. }
5630        ));
5631    }
5632
5633    #[test]
5634    fn bucket_b_ma_indicator_is_supported() {
5635        let data = sample_series();
5636        let combos = [IndicatorParamSet { params: &[] }];
5637        let req = IndicatorBatchRequest {
5638            indicator_id: "mama",
5639            output_id: Some("mama"),
5640            data: IndicatorDataRef::Slice { values: &data },
5641            combos: &combos,
5642            kernel: Kernel::Auto,
5643        };
5644        let out = compute_cpu_batch(req).unwrap();
5645        assert_eq!(out.rows, 1);
5646        assert_eq!(out.cols, data.len());
5647        assert!(out.values_f64.is_some());
5648    }
5649
5650    #[test]
5651    fn strict_mode_rejects_convenience_mfi_ohlcv() {
5652        let (open, high, low, close) = sample_ohlc();
5653        let volume: Vec<f64> = (0..close.len()).map(|i| 1200.0 + (i as f64)).collect();
5654        let combo = [ParamKV {
5655            key: "period",
5656            value: ParamValue::Int(14),
5657        }];
5658        let combos = [IndicatorParamSet { params: &combo }];
5659        let req = IndicatorBatchRequest {
5660            indicator_id: "mfi",
5661            output_id: Some("value"),
5662            data: IndicatorDataRef::Ohlcv {
5663                open: &open,
5664                high: &high,
5665                low: &low,
5666                close: &close,
5667                volume: &volume,
5668            },
5669            combos: &combos,
5670            kernel: Kernel::Auto,
5671        };
5672        let err = compute_cpu_batch_strict(req).unwrap_err();
5673        match err {
5674            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
5675                assert_eq!(indicator, "mfi");
5676                assert_eq!(input, IndicatorInputKind::CloseVolume);
5677            }
5678            other => panic!("expected MissingRequiredInput, got {other:?}"),
5679        }
5680    }
5681
5682    #[test]
5683    fn strict_mode_accepts_precomputed_mfi_close_volume() {
5684        let (_open, high, low, close) = sample_ohlc();
5685        let volume: Vec<f64> = (0..close.len())
5686            .map(|i| 1000.0 + (i as f64 * 2.0))
5687            .collect();
5688        let typical: Vec<f64> = high
5689            .iter()
5690            .zip(&low)
5691            .zip(&close)
5692            .map(|((h, l), c)| (h + l + c) / 3.0)
5693            .collect();
5694        let combo = [ParamKV {
5695            key: "period",
5696            value: ParamValue::Int(14),
5697        }];
5698        let combos = [IndicatorParamSet { params: &combo }];
5699        let req = IndicatorBatchRequest {
5700            indicator_id: "mfi",
5701            output_id: Some("value"),
5702            data: IndicatorDataRef::CloseVolume {
5703                close: &typical,
5704                volume: &volume,
5705            },
5706            combos: &combos,
5707            kernel: Kernel::Auto,
5708        };
5709        let strict = compute_cpu_batch_strict(req).unwrap();
5710        let input = MfiInput::from_slices(&typical, &volume, MfiParams { period: Some(14) });
5711        let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
5712            .unwrap()
5713            .values;
5714        assert_series_eq(strict.values_f64.as_ref().unwrap(), &direct, 1e-12);
5715    }
5716
5717    #[test]
5718    fn strict_mode_rejects_ao_high_low_and_requires_slice() {
5719        let (_open, high, low, _close) = sample_ohlc();
5720        let combo = [
5721            ParamKV {
5722                key: "short_period",
5723                value: ParamValue::Int(5),
5724            },
5725            ParamKV {
5726                key: "long_period",
5727                value: ParamValue::Int(34),
5728            },
5729        ];
5730        let combos = [IndicatorParamSet { params: &combo }];
5731        let req = IndicatorBatchRequest {
5732            indicator_id: "ao",
5733            output_id: Some("value"),
5734            data: IndicatorDataRef::HighLow {
5735                high: &high,
5736                low: &low,
5737            },
5738            combos: &combos,
5739            kernel: Kernel::Auto,
5740        };
5741        let err = compute_cpu_batch_strict(req).unwrap_err();
5742        match err {
5743            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
5744                assert_eq!(indicator, "ao");
5745                assert_eq!(input, IndicatorInputKind::Slice);
5746            }
5747            other => panic!("expected MissingRequiredInput, got {other:?}"),
5748        }
5749    }
5750
5751    #[test]
5752    fn strict_mode_rejects_ttm_trend_ohlc_and_requires_candles() {
5753        let (open, high, low, close) = sample_ohlc();
5754        let combo = [ParamKV {
5755            key: "period",
5756            value: ParamValue::Int(5),
5757        }];
5758        let combos = [IndicatorParamSet { params: &combo }];
5759        let req = IndicatorBatchRequest {
5760            indicator_id: "ttm_trend",
5761            output_id: Some("value"),
5762            data: IndicatorDataRef::Ohlc {
5763                open: &open,
5764                high: &high,
5765                low: &low,
5766                close: &close,
5767            },
5768            combos: &combos,
5769            kernel: Kernel::Auto,
5770        };
5771        let err = compute_cpu_batch_strict(req).unwrap_err();
5772        match err {
5773            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
5774                assert_eq!(indicator, "ttm_trend");
5775                assert_eq!(input, IndicatorInputKind::Candles);
5776            }
5777            other => panic!("expected MissingRequiredInput, got {other:?}"),
5778        }
5779    }
5780
5781    #[test]
5782    fn strict_mode_accepts_ttm_trend_candles() {
5783        let candles = sample_candles();
5784        let combo = [ParamKV {
5785            key: "period",
5786            value: ParamValue::Int(5),
5787        }];
5788        let combos = [IndicatorParamSet { params: &combo }];
5789        let req = IndicatorBatchRequest {
5790            indicator_id: "ttm_trend",
5791            output_id: Some("value"),
5792            data: IndicatorDataRef::Candles {
5793                candles: &candles,
5794                source: Some("hl2"),
5795            },
5796            combos: &combos,
5797            kernel: Kernel::Auto,
5798        };
5799        let strict = compute_cpu_batch_strict(req).unwrap();
5800        let input = TtmTrendInput::from_slices(
5801            candles.hl2.as_slice(),
5802            candles.close.as_slice(),
5803            TtmTrendParams { period: Some(5) },
5804        );
5805        let direct = ttm_trend_with_kernel(&input, Kernel::Auto.to_non_batch())
5806            .unwrap()
5807            .values;
5808        let got = strict.values_bool.unwrap();
5809        assert_eq!(got, direct);
5810    }
5811
5812    #[test]
5813    fn rsi_cpu_batch_smoke() {
5814        let data = sample_series();
5815        let combo_1 = [ParamKV {
5816            key: "period",
5817            value: ParamValue::Int(7),
5818        }];
5819        let combo_2 = [ParamKV {
5820            key: "period",
5821            value: ParamValue::Int(14),
5822        }];
5823        let combos = [
5824            IndicatorParamSet { params: &combo_1 },
5825            IndicatorParamSet { params: &combo_2 },
5826        ];
5827        let req = IndicatorBatchRequest {
5828            indicator_id: "rsi",
5829            output_id: Some("value"),
5830            data: IndicatorDataRef::Slice { values: &data },
5831            combos: &combos,
5832            kernel: Kernel::Auto,
5833        };
5834        let out = compute_cpu_batch(req).unwrap();
5835        assert_eq!(out.output_id, "value");
5836        assert_eq!(out.rows, 2);
5837        assert_eq!(out.cols, data.len());
5838        assert_eq!(out.values_f64.as_ref().map(Vec::len), Some(2 * data.len()));
5839    }
5840
5841    #[test]
5842    fn ma_dispatch_regression_sma_matches_existing_ma_batch_api() {
5843        let data = sample_series();
5844        let combo = [ParamKV {
5845            key: "period",
5846            value: ParamValue::Int(14),
5847        }];
5848        let combos = [IndicatorParamSet { params: &combo }];
5849        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
5850            indicator_id: "sma",
5851            output_id: Some("value"),
5852            data: IndicatorDataRef::Slice { values: &data },
5853            combos: &combos,
5854            kernel: Kernel::Auto,
5855        })
5856        .unwrap();
5857
5858        let direct = ma_batch_with_kernel_and_typed_params(
5859            "sma",
5860            MaData::Slice(&data),
5861            (14, 14, 0),
5862            Kernel::Auto,
5863            &[],
5864        )
5865        .unwrap();
5866        assert_eq!(dispatch.rows, direct.rows);
5867        assert_eq!(dispatch.cols, direct.cols);
5868        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
5869    }
5870
5871    #[test]
5872    fn ma_dispatch_sma_period_sweep_matches_direct_batch() {
5873        let data = sample_series();
5874        let combo_1 = [ParamKV {
5875            key: "period",
5876            value: ParamValue::Int(5),
5877        }];
5878        let combo_2 = [ParamKV {
5879            key: "period",
5880            value: ParamValue::Int(7),
5881        }];
5882        let combo_3 = [ParamKV {
5883            key: "period",
5884            value: ParamValue::Int(9),
5885        }];
5886        let combos = [
5887            IndicatorParamSet { params: &combo_1 },
5888            IndicatorParamSet { params: &combo_2 },
5889            IndicatorParamSet { params: &combo_3 },
5890        ];
5891        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
5892            indicator_id: "sma",
5893            output_id: Some("value"),
5894            data: IndicatorDataRef::Slice { values: &data },
5895            combos: &combos,
5896            kernel: Kernel::Auto,
5897        })
5898        .unwrap();
5899
5900        let direct = ma_batch_with_kernel_and_typed_params(
5901            "sma",
5902            MaData::Slice(&data),
5903            (5, 9, 2),
5904            Kernel::Auto,
5905            &[],
5906        )
5907        .unwrap();
5908        assert_eq!(dispatch.rows, direct.rows);
5909        assert_eq!(dispatch.cols, direct.cols);
5910        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
5911    }
5912
5913    #[test]
5914    fn mfi_dispatch_period_sweep_matches_direct_batch() {
5915        let (_open, high, low, close) = sample_ohlc();
5916        let volume: Vec<f64> = (0..close.len())
5917            .map(|i| 1000.0 + (i as f64 * 2.0))
5918            .collect();
5919        let typical: Vec<f64> = high
5920            .iter()
5921            .zip(&low)
5922            .zip(&close)
5923            .map(|((h, l), c)| (h + l + c) / 3.0)
5924            .collect();
5925        let combo_1 = [ParamKV {
5926            key: "period",
5927            value: ParamValue::Int(5),
5928        }];
5929        let combo_2 = [ParamKV {
5930            key: "period",
5931            value: ParamValue::Int(7),
5932        }];
5933        let combo_3 = [ParamKV {
5934            key: "period",
5935            value: ParamValue::Int(9),
5936        }];
5937        let combos = [
5938            IndicatorParamSet { params: &combo_1 },
5939            IndicatorParamSet { params: &combo_2 },
5940            IndicatorParamSet { params: &combo_3 },
5941        ];
5942        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
5943            indicator_id: "mfi",
5944            output_id: Some("value"),
5945            data: IndicatorDataRef::CloseVolume {
5946                close: &typical,
5947                volume: &volume,
5948            },
5949            combos: &combos,
5950            kernel: Kernel::Auto,
5951        })
5952        .unwrap();
5953        let direct = mfi_batch_with_kernel(
5954            &typical,
5955            &volume,
5956            &MfiBatchRange { period: (5, 9, 2) },
5957            Kernel::Auto,
5958        )
5959        .unwrap();
5960        assert_eq!(dispatch.rows, direct.rows);
5961        assert_eq!(dispatch.cols, direct.cols);
5962        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
5963    }
5964
5965    #[test]
5966    fn dx_dispatch_period_sweep_keeps_requested_row_order() {
5967        let (open, high, low, close) = sample_ohlc();
5968        let combo_1 = [ParamKV {
5969            key: "period",
5970            value: ParamValue::Int(9),
5971        }];
5972        let combo_2 = [ParamKV {
5973            key: "period",
5974            value: ParamValue::Int(7),
5975        }];
5976        let combo_3 = [ParamKV {
5977            key: "period",
5978            value: ParamValue::Int(5),
5979        }];
5980        let combos = [
5981            IndicatorParamSet { params: &combo_1 },
5982            IndicatorParamSet { params: &combo_2 },
5983            IndicatorParamSet { params: &combo_3 },
5984        ];
5985        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
5986            indicator_id: "dx",
5987            output_id: Some("value"),
5988            data: IndicatorDataRef::Ohlc {
5989                open: &open,
5990                high: &high,
5991                low: &low,
5992                close: &close,
5993            },
5994            combos: &combos,
5995            kernel: Kernel::Auto,
5996        })
5997        .unwrap();
5998        let direct = dx_batch_with_kernel(
5999            &high,
6000            &low,
6001            &close,
6002            &DxBatchRange { period: (9, 5, 2) },
6003            Kernel::Auto,
6004        )
6005        .unwrap();
6006        let direct_periods: Vec<usize> = direct
6007            .combos
6008            .iter()
6009            .map(|combo| combo.period.unwrap_or(14))
6010            .collect();
6011        let period_to_row: std::collections::HashMap<usize, usize> = direct_periods
6012            .iter()
6013            .copied()
6014            .enumerate()
6015            .map(|(row, period)| (period, row))
6016            .collect();
6017        let requested = [9usize, 7usize, 5usize];
6018        let mut expected = Vec::with_capacity(requested.len() * direct.cols);
6019        for period in requested {
6020            let row = period_to_row[&period];
6021            let start = row * direct.cols;
6022            let end = start + direct.cols;
6023            expected.extend_from_slice(&direct.values[start..end]);
6024        }
6025        assert_eq!(dispatch.rows, requested.len());
6026        assert_eq!(dispatch.cols, direct.cols);
6027        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &expected, 1e-12);
6028    }
6029
6030    #[test]
6031    fn ma_dispatch_regression_alma_typed_params_match_existing_ma_batch_api() {
6032        let data = sample_series();
6033        let combo = [
6034            ParamKV {
6035                key: "period",
6036                value: ParamValue::Int(14),
6037            },
6038            ParamKV {
6039                key: "offset",
6040                value: ParamValue::Float(0.87),
6041            },
6042            ParamKV {
6043                key: "sigma",
6044                value: ParamValue::Float(5.5),
6045            },
6046        ];
6047        let combos = [IndicatorParamSet { params: &combo }];
6048        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
6049            indicator_id: "alma",
6050            output_id: Some("value"),
6051            data: IndicatorDataRef::Slice { values: &data },
6052            combos: &combos,
6053            kernel: Kernel::Auto,
6054        })
6055        .unwrap();
6056
6057        let typed = [
6058            MaBatchParamKV {
6059                key: "offset",
6060                value: MaBatchParamValue::Float(0.87),
6061            },
6062            MaBatchParamKV {
6063                key: "sigma",
6064                value: MaBatchParamValue::Float(5.5),
6065            },
6066        ];
6067        let direct = ma_batch_with_kernel_and_typed_params(
6068            "alma",
6069            MaData::Slice(&data),
6070            (14, 14, 0),
6071            Kernel::Auto,
6072            &typed,
6073        )
6074        .unwrap();
6075        assert_eq!(dispatch.rows, direct.rows);
6076        assert_eq!(dispatch.cols, direct.cols);
6077        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
6078    }
6079
6080    #[test]
6081    fn macd_signal_output_matches_direct() {
6082        let data = sample_series();
6083        let combo_1 = [
6084            ParamKV {
6085                key: "fast_period",
6086                value: ParamValue::Int(8),
6087            },
6088            ParamKV {
6089                key: "slow_period",
6090                value: ParamValue::Int(21),
6091            },
6092            ParamKV {
6093                key: "signal_period",
6094                value: ParamValue::Int(5),
6095            },
6096        ];
6097        let combo_2 = [
6098            ParamKV {
6099                key: "fast_period",
6100                value: ParamValue::Int(12),
6101            },
6102            ParamKV {
6103                key: "slow_period",
6104                value: ParamValue::Int(26),
6105            },
6106            ParamKV {
6107                key: "signal_period",
6108                value: ParamValue::Int(9),
6109            },
6110        ];
6111        let combos = [
6112            IndicatorParamSet { params: &combo_1 },
6113            IndicatorParamSet { params: &combo_2 },
6114        ];
6115        let req = IndicatorBatchRequest {
6116            indicator_id: "macd",
6117            output_id: Some("signal"),
6118            data: IndicatorDataRef::Slice { values: &data },
6119            combos: &combos,
6120            kernel: Kernel::Auto,
6121        };
6122        let out = compute_cpu_batch(req).unwrap();
6123        let matrix = out.values_f64.unwrap();
6124        for (row, combo) in combos.iter().enumerate() {
6125            let fast = match combo.params[0].value {
6126                ParamValue::Int(v) => v as usize,
6127                _ => unreachable!(),
6128            };
6129            let slow = match combo.params[1].value {
6130                ParamValue::Int(v) => v as usize,
6131                _ => unreachable!(),
6132            };
6133            let signal = match combo.params[2].value {
6134                ParamValue::Int(v) => v as usize,
6135                _ => unreachable!(),
6136            };
6137            let input = MacdInput::from_slice(
6138                &data,
6139                MacdParams {
6140                    fast_period: Some(fast),
6141                    slow_period: Some(slow),
6142                    signal_period: Some(signal),
6143                    ma_type: Some("ema".to_string()),
6144                },
6145            );
6146            let direct = macd_with_kernel(&input, Kernel::Auto.to_non_batch())
6147                .unwrap()
6148                .signal;
6149            let start = row * out.cols;
6150            let end = start + out.cols;
6151            assert_series_eq(&matrix[start..end], direct.as_slice(), 1e-12);
6152        }
6153    }
6154
6155    #[test]
6156    fn adx_output_matches_direct() {
6157        let (open, high, low, close) = sample_ohlc();
6158        let combo = [ParamKV {
6159            key: "period",
6160            value: ParamValue::Int(14),
6161        }];
6162        let combos = [IndicatorParamSet { params: &combo }];
6163        let req = IndicatorBatchRequest {
6164            indicator_id: "adx",
6165            output_id: Some("value"),
6166            data: IndicatorDataRef::Ohlc {
6167                open: &open,
6168                high: &high,
6169                low: &low,
6170                close: &close,
6171            },
6172            combos: &combos,
6173            kernel: Kernel::Auto,
6174        };
6175        let out = compute_cpu_batch(req).unwrap();
6176        let matrix = out.values_f64.unwrap();
6177        let input = AdxInput::from_slices(&high, &low, &close, AdxParams { period: Some(14) });
6178        let direct = adx_with_kernel(&input, Kernel::Auto.to_non_batch())
6179            .unwrap()
6180            .values;
6181        assert_series_eq(&matrix, &direct, 1e-12);
6182    }
6183
6184    #[test]
6185    fn cmo_output_matches_direct() {
6186        let data = sample_series();
6187        let combo = [ParamKV {
6188            key: "period",
6189            value: ParamValue::Int(14),
6190        }];
6191        let combos = [IndicatorParamSet { params: &combo }];
6192        let req = IndicatorBatchRequest {
6193            indicator_id: "cmo",
6194            output_id: Some("value"),
6195            data: IndicatorDataRef::Slice { values: &data },
6196            combos: &combos,
6197            kernel: Kernel::Auto,
6198        };
6199        let out = compute_cpu_batch(req).unwrap();
6200        let input = CmoInput::from_slice(&data, CmoParams { period: Some(14) });
6201        let direct = cmo_with_kernel(&input, Kernel::Auto.to_non_batch())
6202            .unwrap()
6203            .values;
6204        let got = out.values_f64.unwrap();
6205        assert_series_eq(&got, &direct, 1e-12);
6206    }
6207
6208    #[test]
6209    fn ppo_output_matches_direct() {
6210        let data = sample_series();
6211        let combo = [
6212            ParamKV {
6213                key: "fast_period",
6214                value: ParamValue::Int(12),
6215            },
6216            ParamKV {
6217                key: "slow_period",
6218                value: ParamValue::Int(26),
6219            },
6220            ParamKV {
6221                key: "ma_type",
6222                value: ParamValue::EnumString("sma"),
6223            },
6224        ];
6225        let combos = [IndicatorParamSet { params: &combo }];
6226        let req = IndicatorBatchRequest {
6227            indicator_id: "ppo",
6228            output_id: Some("value"),
6229            data: IndicatorDataRef::Slice { values: &data },
6230            combos: &combos,
6231            kernel: Kernel::Auto,
6232        };
6233        let out = compute_cpu_batch(req).unwrap();
6234        let input = PpoInput::from_slice(
6235            &data,
6236            PpoParams {
6237                fast_period: Some(12),
6238                slow_period: Some(26),
6239                ma_type: Some("sma".to_string()),
6240            },
6241        );
6242        let direct = ppo_with_kernel(&input, Kernel::Auto.to_non_batch())
6243            .unwrap()
6244            .values;
6245        let got = out.values_f64.unwrap();
6246        assert_series_eq(&got, &direct, 1e-12);
6247    }
6248
6249    #[test]
6250    fn apo_output_matches_direct() {
6251        let data = sample_series();
6252        let combo = [
6253            ParamKV {
6254                key: "short_period",
6255                value: ParamValue::Int(10),
6256            },
6257            ParamKV {
6258                key: "long_period",
6259                value: ParamValue::Int(20),
6260            },
6261        ];
6262        let combos = [IndicatorParamSet { params: &combo }];
6263        let req = IndicatorBatchRequest {
6264            indicator_id: "apo",
6265            output_id: Some("value"),
6266            data: IndicatorDataRef::Slice { values: &data },
6267            combos: &combos,
6268            kernel: Kernel::Auto,
6269        };
6270        let out = compute_cpu_batch(req).unwrap();
6271        let input = ApoInput::from_slice(
6272            &data,
6273            ApoParams {
6274                short_period: Some(10),
6275                long_period: Some(20),
6276            },
6277        );
6278        let direct = apo_with_kernel(&input, Kernel::Auto.to_non_batch())
6279            .unwrap()
6280            .values;
6281        let got = out.values_f64.unwrap();
6282        assert_series_eq(&got, &direct, 1e-12);
6283    }
6284
6285    #[test]
6286    fn natr_output_matches_direct() {
6287        let (open, high, low, close) = sample_ohlc();
6288        let combo = [ParamKV {
6289            key: "period",
6290            value: ParamValue::Int(14),
6291        }];
6292        let combos = [IndicatorParamSet { params: &combo }];
6293        let req = IndicatorBatchRequest {
6294            indicator_id: "natr",
6295            output_id: Some("value"),
6296            data: IndicatorDataRef::Ohlc {
6297                open: &open,
6298                high: &high,
6299                low: &low,
6300                close: &close,
6301            },
6302            combos: &combos,
6303            kernel: Kernel::Auto,
6304        };
6305        let out = compute_cpu_batch(req).unwrap();
6306        let input = NatrInput::from_slices(&high, &low, &close, NatrParams { period: Some(14) });
6307        let direct = natr_with_kernel(&input, Kernel::Auto.to_non_batch())
6308            .unwrap()
6309            .values;
6310        let got = out.values_f64.unwrap();
6311        assert_series_eq(&got, &direct, 1e-12);
6312    }
6313
6314    #[test]
6315    fn ad_output_matches_direct() {
6316        let (open, high, low, close) = sample_ohlc();
6317        let volume: Vec<f64> = (0..close.len())
6318            .map(|i| 1000.0 + (i as f64 * 3.0))
6319            .collect();
6320        let combos = [IndicatorParamSet { params: &[] }];
6321        let req = IndicatorBatchRequest {
6322            indicator_id: "ad",
6323            output_id: Some("value"),
6324            data: IndicatorDataRef::Ohlcv {
6325                open: &open,
6326                high: &high,
6327                low: &low,
6328                close: &close,
6329                volume: &volume,
6330            },
6331            combos: &combos,
6332            kernel: Kernel::Auto,
6333        };
6334        let out = compute_cpu_batch(req).unwrap();
6335        let input = AdInput::from_slices(&high, &low, &close, &volume, AdParams::default());
6336        let direct = ad_with_kernel(&input, Kernel::Auto.to_non_batch())
6337            .unwrap()
6338            .values;
6339        let got = out.values_f64.unwrap();
6340        assert_series_eq(&got, &direct, 1e-12);
6341    }
6342
6343    #[test]
6344    fn ao_output_matches_direct() {
6345        let (open, high, low, close) = sample_ohlc();
6346        let combo = [
6347            ParamKV {
6348                key: "short_period",
6349                value: ParamValue::Int(5),
6350            },
6351            ParamKV {
6352                key: "long_period",
6353                value: ParamValue::Int(34),
6354            },
6355        ];
6356        let combos = [IndicatorParamSet { params: &combo }];
6357        let req = IndicatorBatchRequest {
6358            indicator_id: "ao",
6359            output_id: Some("value"),
6360            data: IndicatorDataRef::Ohlc {
6361                open: &open,
6362                high: &high,
6363                low: &low,
6364                close: &close,
6365            },
6366            combos: &combos,
6367            kernel: Kernel::Auto,
6368        };
6369        let out = compute_cpu_batch(req).unwrap();
6370        let source: Vec<f64> = high.iter().zip(&low).map(|(h, l)| 0.5 * (h + l)).collect();
6371        let input = AoInput::from_slice(
6372            &source,
6373            AoParams {
6374                short_period: Some(5),
6375                long_period: Some(34),
6376            },
6377        );
6378        let direct = ao_with_kernel(&input, Kernel::Auto.to_non_batch())
6379            .unwrap()
6380            .values;
6381        let got = out.values_f64.unwrap();
6382        assert_series_eq(&got, &direct, 1e-12);
6383    }
6384
6385    #[test]
6386    fn pvi_output_matches_direct() {
6387        let data = sample_series();
6388        let volume: Vec<f64> = (0..data.len()).map(|i| 900.0 + (i as f64 * 5.0)).collect();
6389        let combo = [ParamKV {
6390            key: "initial_value",
6391            value: ParamValue::Float(1000.0),
6392        }];
6393        let combos = [IndicatorParamSet { params: &combo }];
6394        let req = IndicatorBatchRequest {
6395            indicator_id: "pvi",
6396            output_id: Some("value"),
6397            data: IndicatorDataRef::CloseVolume {
6398                close: &data,
6399                volume: &volume,
6400            },
6401            combos: &combos,
6402            kernel: Kernel::Auto,
6403        };
6404        let out = compute_cpu_batch(req).unwrap();
6405        let input = PviInput::from_slices(
6406            &data,
6407            &volume,
6408            PviParams {
6409                initial_value: Some(1000.0),
6410            },
6411        );
6412        let direct = pvi_with_kernel(&input, Kernel::Auto.to_non_batch())
6413            .unwrap()
6414            .values;
6415        let got = out.values_f64.unwrap();
6416        assert_series_eq(&got, &direct, 1e-12);
6417    }
6418
6419    #[test]
6420    fn efi_output_matches_direct() {
6421        let data = sample_series();
6422        let volume: Vec<f64> = (0..data.len()).map(|i| 1000.0 + (i as f64 * 4.0)).collect();
6423        let combo = [ParamKV {
6424            key: "period",
6425            value: ParamValue::Int(13),
6426        }];
6427        let combos = [IndicatorParamSet { params: &combo }];
6428        let req = IndicatorBatchRequest {
6429            indicator_id: "efi",
6430            output_id: Some("value"),
6431            data: IndicatorDataRef::CloseVolume {
6432                close: &data,
6433                volume: &volume,
6434            },
6435            combos: &combos,
6436            kernel: Kernel::Auto,
6437        };
6438        let out = compute_cpu_batch(req).unwrap();
6439        let input = EfiInput::from_slices(&data, &volume, EfiParams { period: Some(13) });
6440        let direct = efi_with_kernel(&input, Kernel::Auto.to_non_batch())
6441            .unwrap()
6442            .values;
6443        let got = out.values_f64.unwrap();
6444        assert_series_eq(&got, &direct, 1e-12);
6445    }
6446
6447    #[test]
6448    fn mfi_output_matches_direct() {
6449        let (open, high, low, close) = sample_ohlc();
6450        let volume: Vec<f64> = (0..close.len()).map(|i| 900.0 + (i as f64 * 6.0)).collect();
6451        let combo = [ParamKV {
6452            key: "period",
6453            value: ParamValue::Int(14),
6454        }];
6455        let combos = [IndicatorParamSet { params: &combo }];
6456        let req = IndicatorBatchRequest {
6457            indicator_id: "mfi",
6458            output_id: Some("value"),
6459            data: IndicatorDataRef::Ohlcv {
6460                open: &open,
6461                high: &high,
6462                low: &low,
6463                close: &close,
6464                volume: &volume,
6465            },
6466            combos: &combos,
6467            kernel: Kernel::Auto,
6468        };
6469        let out = compute_cpu_batch(req).unwrap();
6470        let typical_price: Vec<f64> = high
6471            .iter()
6472            .zip(&low)
6473            .zip(&close)
6474            .map(|((h, l), c)| (h + l + c) / 3.0)
6475            .collect();
6476        let input = MfiInput::from_slices(&typical_price, &volume, MfiParams { period: Some(14) });
6477        let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
6478            .unwrap()
6479            .values;
6480        let got = out.values_f64.unwrap();
6481        assert_series_eq(&got, &direct, 1e-12);
6482    }
6483
6484    #[test]
6485    fn mfi_non_sweep_fallback_rows_match_direct() {
6486        let (open, high, low, close) = sample_ohlc();
6487        let volume: Vec<f64> = (0..close.len()).map(|i| 950.0 + (i as f64 * 5.0)).collect();
6488        let combo_1 = [ParamKV {
6489            key: "period",
6490            value: ParamValue::Int(5),
6491        }];
6492        let combo_2 = [ParamKV {
6493            key: "period",
6494            value: ParamValue::Int(9),
6495        }];
6496        let combo_3 = [ParamKV {
6497            key: "period",
6498            value: ParamValue::Int(8),
6499        }];
6500        let combos = [
6501            IndicatorParamSet { params: &combo_1 },
6502            IndicatorParamSet { params: &combo_2 },
6503            IndicatorParamSet { params: &combo_3 },
6504        ];
6505        let req = IndicatorBatchRequest {
6506            indicator_id: "mfi",
6507            output_id: Some("value"),
6508            data: IndicatorDataRef::Ohlcv {
6509                open: &open,
6510                high: &high,
6511                low: &low,
6512                close: &close,
6513                volume: &volume,
6514            },
6515            combos: &combos,
6516            kernel: Kernel::Auto,
6517        };
6518        let out = compute_cpu_batch(req).unwrap();
6519        let matrix = out.values_f64.unwrap();
6520        let typical_price: Vec<f64> = high
6521            .iter()
6522            .zip(&low)
6523            .zip(&close)
6524            .map(|((h, l), c)| (h + l + c) / 3.0)
6525            .collect();
6526        for (row, period) in [5usize, 9usize, 8usize].iter().enumerate() {
6527            let input = MfiInput::from_slices(
6528                &typical_price,
6529                &volume,
6530                MfiParams {
6531                    period: Some(*period),
6532                },
6533            );
6534            let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
6535                .unwrap()
6536                .values;
6537            let start = row * close.len();
6538            let end = start + close.len();
6539            assert_series_eq(&matrix[start..end], &direct, 1e-12);
6540        }
6541    }
6542
6543    #[test]
6544    fn kvo_output_matches_direct() {
6545        let (open, high, low, close) = sample_ohlc();
6546        let volume: Vec<f64> = (0..close.len())
6547            .map(|i| 1200.0 + (i as f64 * 5.0))
6548            .collect();
6549        let combo = [
6550            ParamKV {
6551                key: "short_period",
6552                value: ParamValue::Int(2),
6553            },
6554            ParamKV {
6555                key: "long_period",
6556                value: ParamValue::Int(5),
6557            },
6558        ];
6559        let combos = [IndicatorParamSet { params: &combo }];
6560        let req = IndicatorBatchRequest {
6561            indicator_id: "kvo",
6562            output_id: Some("value"),
6563            data: IndicatorDataRef::Ohlcv {
6564                open: &open,
6565                high: &high,
6566                low: &low,
6567                close: &close,
6568                volume: &volume,
6569            },
6570            combos: &combos,
6571            kernel: Kernel::Auto,
6572        };
6573        let out = compute_cpu_batch(req).unwrap();
6574        let input = KvoInput::from_slices(
6575            &high,
6576            &low,
6577            &close,
6578            &volume,
6579            KvoParams {
6580                short_period: Some(2),
6581                long_period: Some(5),
6582            },
6583        );
6584        let direct = kvo_with_kernel(&input, Kernel::Auto.to_non_batch())
6585            .unwrap()
6586            .values;
6587        let got = out.values_f64.unwrap();
6588        assert_series_eq(&got, &direct, 1e-12);
6589    }
6590
6591    #[test]
6592    fn dx_output_matches_direct() {
6593        let (open, high, low, close) = sample_ohlc();
6594        let combo = [ParamKV {
6595            key: "period",
6596            value: ParamValue::Int(14),
6597        }];
6598        let combos = [IndicatorParamSet { params: &combo }];
6599        let req = IndicatorBatchRequest {
6600            indicator_id: "dx",
6601            output_id: Some("value"),
6602            data: IndicatorDataRef::Ohlc {
6603                open: &open,
6604                high: &high,
6605                low: &low,
6606                close: &close,
6607            },
6608            combos: &combos,
6609            kernel: Kernel::Auto,
6610        };
6611        let out = compute_cpu_batch(req).unwrap();
6612        let input = DxInput::from_hlc_slices(&high, &low, &close, DxParams { period: Some(14) });
6613        let direct = dx_with_kernel(&input, Kernel::Auto.to_non_batch())
6614            .unwrap()
6615            .values;
6616        let got = out.values_f64.unwrap();
6617        assert_series_eq(&got, &direct, 1e-12);
6618    }
6619
6620    #[test]
6621    fn dx_non_sweep_fallback_rows_match_direct() {
6622        let (open, high, low, close) = sample_ohlc();
6623        let combo_1 = [ParamKV {
6624            key: "period",
6625            value: ParamValue::Int(9),
6626        }];
6627        let combo_2 = [ParamKV {
6628            key: "period",
6629            value: ParamValue::Int(5),
6630        }];
6631        let combo_3 = [ParamKV {
6632            key: "period",
6633            value: ParamValue::Int(8),
6634        }];
6635        let combos = [
6636            IndicatorParamSet { params: &combo_1 },
6637            IndicatorParamSet { params: &combo_2 },
6638            IndicatorParamSet { params: &combo_3 },
6639        ];
6640        let req = IndicatorBatchRequest {
6641            indicator_id: "dx",
6642            output_id: Some("value"),
6643            data: IndicatorDataRef::Ohlc {
6644                open: &open,
6645                high: &high,
6646                low: &low,
6647                close: &close,
6648            },
6649            combos: &combos,
6650            kernel: Kernel::Auto,
6651        };
6652        let out = compute_cpu_batch(req).unwrap();
6653        let matrix = out.values_f64.unwrap();
6654        for (row, period) in [9usize, 5usize, 8usize].iter().enumerate() {
6655            let input = DxInput::from_hlc_slices(
6656                &high,
6657                &low,
6658                &close,
6659                DxParams {
6660                    period: Some(*period),
6661                },
6662            );
6663            let direct = dx_with_kernel(&input, Kernel::Auto.to_non_batch())
6664                .unwrap()
6665                .values;
6666            let start = row * close.len();
6667            let end = start + close.len();
6668            assert_series_eq(&matrix[start..end], &direct, 1e-12);
6669        }
6670    }
6671
6672    #[test]
6673    fn trix_dispatch_period_sweep_keeps_requested_row_order() {
6674        let data = sample_series();
6675        let combo_1 = [ParamKV {
6676            key: "period",
6677            value: ParamValue::Int(9),
6678        }];
6679        let combo_2 = [ParamKV {
6680            key: "period",
6681            value: ParamValue::Int(7),
6682        }];
6683        let combo_3 = [ParamKV {
6684            key: "period",
6685            value: ParamValue::Int(5),
6686        }];
6687        let combos = [
6688            IndicatorParamSet { params: &combo_1 },
6689            IndicatorParamSet { params: &combo_2 },
6690            IndicatorParamSet { params: &combo_3 },
6691        ];
6692        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
6693            indicator_id: "trix",
6694            output_id: Some("value"),
6695            data: IndicatorDataRef::Slice { values: &data },
6696            combos: &combos,
6697            kernel: Kernel::Auto,
6698        })
6699        .unwrap();
6700
6701        let direct =
6702            trix_batch_with_kernel(&data, &TrixBatchRange { period: (9, 5, 2) }, Kernel::Auto)
6703                .unwrap();
6704        let direct_periods: Vec<usize> = direct
6705            .combos
6706            .iter()
6707            .map(|combo| combo.period.unwrap_or(18))
6708            .collect();
6709        let period_to_row: std::collections::HashMap<usize, usize> = direct_periods
6710            .iter()
6711            .copied()
6712            .enumerate()
6713            .map(|(row, period)| (period, row))
6714            .collect();
6715        let requested = [9usize, 7usize, 5usize];
6716        let mut expected = Vec::with_capacity(requested.len() * direct.cols);
6717        for period in requested {
6718            let row = period_to_row[&period];
6719            let start = row * direct.cols;
6720            let end = start + direct.cols;
6721            expected.extend_from_slice(&direct.values[start..end]);
6722        }
6723        assert_eq!(dispatch.rows, requested.len());
6724        assert_eq!(dispatch.cols, direct.cols);
6725        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &expected, 1e-12);
6726    }
6727
6728    #[test]
6729    fn trix_non_sweep_fallback_rows_match_direct() {
6730        let data = sample_series();
6731        let combo_1 = [ParamKV {
6732            key: "period",
6733            value: ParamValue::Int(9),
6734        }];
6735        let combo_2 = [ParamKV {
6736            key: "period",
6737            value: ParamValue::Int(5),
6738        }];
6739        let combo_3 = [ParamKV {
6740            key: "period",
6741            value: ParamValue::Int(8),
6742        }];
6743        let combos = [
6744            IndicatorParamSet { params: &combo_1 },
6745            IndicatorParamSet { params: &combo_2 },
6746            IndicatorParamSet { params: &combo_3 },
6747        ];
6748        let out = compute_cpu_batch(IndicatorBatchRequest {
6749            indicator_id: "trix",
6750            output_id: Some("value"),
6751            data: IndicatorDataRef::Slice { values: &data },
6752            combos: &combos,
6753            kernel: Kernel::Auto,
6754        })
6755        .unwrap();
6756        let matrix = out.values_f64.unwrap();
6757        for (row, period) in [9usize, 5usize, 8usize].iter().enumerate() {
6758            let input = TrixInput::from_slice(
6759                &data,
6760                TrixParams {
6761                    period: Some(*period),
6762                },
6763            );
6764            let direct = trix_with_kernel(&input, Kernel::Auto.to_non_batch())
6765                .unwrap()
6766                .values;
6767            let start = row * data.len();
6768            let end = start + data.len();
6769            assert_series_eq(&matrix[start..end], &direct, 1e-12);
6770        }
6771    }
6772
6773    #[test]
6774    fn ift_rsi_output_matches_direct() {
6775        let data = sample_series();
6776        let combo = [
6777            ParamKV {
6778                key: "rsi_period",
6779                value: ParamValue::Int(6),
6780            },
6781            ParamKV {
6782                key: "wma_period",
6783                value: ParamValue::Int(10),
6784            },
6785        ];
6786        let combos = [IndicatorParamSet { params: &combo }];
6787        let req = IndicatorBatchRequest {
6788            indicator_id: "ift_rsi",
6789            output_id: Some("value"),
6790            data: IndicatorDataRef::Slice { values: &data },
6791            combos: &combos,
6792            kernel: Kernel::Auto,
6793        };
6794        let out = compute_cpu_batch(req).unwrap();
6795        let input = IftRsiInput::from_slice(
6796            &data,
6797            IftRsiParams {
6798                rsi_period: Some(6),
6799                wma_period: Some(10),
6800            },
6801        );
6802        let direct = ift_rsi_with_kernel(&input, Kernel::Auto.to_non_batch())
6803            .unwrap()
6804            .values;
6805        let got = out.values_f64.unwrap();
6806        assert_series_eq(&got, &direct, 1e-12);
6807    }
6808
6809    #[test]
6810    fn fosc_output_matches_direct() {
6811        let data = sample_series();
6812        let combo = [ParamKV {
6813            key: "period",
6814            value: ParamValue::Int(8),
6815        }];
6816        let combos = [IndicatorParamSet { params: &combo }];
6817        let req = IndicatorBatchRequest {
6818            indicator_id: "fosc",
6819            output_id: Some("value"),
6820            data: IndicatorDataRef::Slice { values: &data },
6821            combos: &combos,
6822            kernel: Kernel::Auto,
6823        };
6824        let out = compute_cpu_batch(req).unwrap();
6825        let input = FoscInput::from_slice(&data, FoscParams { period: Some(8) });
6826        let direct = fosc_with_kernel(&input, Kernel::Auto.to_non_batch())
6827            .unwrap()
6828            .values;
6829        let got = out.values_f64.unwrap();
6830        assert_series_eq(&got, &direct, 1e-12);
6831    }
6832
6833    #[test]
6834    fn linearreg_angle_output_matches_direct() {
6835        let data = sample_series();
6836        let combo = [ParamKV {
6837            key: "period",
6838            value: ParamValue::Int(14),
6839        }];
6840        let combos = [IndicatorParamSet { params: &combo }];
6841        let req = IndicatorBatchRequest {
6842            indicator_id: "linearreg_angle",
6843            output_id: Some("value"),
6844            data: IndicatorDataRef::Slice { values: &data },
6845            combos: &combos,
6846            kernel: Kernel::Auto,
6847        };
6848        let out = compute_cpu_batch(req).unwrap();
6849        let input =
6850            Linearreg_angleInput::from_slice(&data, Linearreg_angleParams { period: Some(14) });
6851        let direct = linearreg_angle_with_kernel(&input, Kernel::Auto.to_non_batch())
6852            .unwrap()
6853            .values;
6854        let got = out.values_f64.unwrap();
6855        assert_series_eq(&got, &direct, 1e-12);
6856    }
6857
6858    #[test]
6859    fn linearreg_intercept_output_matches_direct() {
6860        let data = sample_series();
6861        let combo = [ParamKV {
6862            key: "period",
6863            value: ParamValue::Int(14),
6864        }];
6865        let combos = [IndicatorParamSet { params: &combo }];
6866        let req = IndicatorBatchRequest {
6867            indicator_id: "linearreg_intercept",
6868            output_id: Some("value"),
6869            data: IndicatorDataRef::Slice { values: &data },
6870            combos: &combos,
6871            kernel: Kernel::Auto,
6872        };
6873        let out = compute_cpu_batch(req).unwrap();
6874        let input = LinearRegInterceptInput::from_slice(
6875            &data,
6876            LinearRegInterceptParams { period: Some(14) },
6877        );
6878        let direct = linearreg_intercept_with_kernel(&input, Kernel::Auto.to_non_batch())
6879            .unwrap()
6880            .values;
6881        let got = out.values_f64.unwrap();
6882        assert_series_eq(&got, &direct, 1e-12);
6883    }
6884
6885    #[test]
6886    fn cg_output_matches_direct() {
6887        let data = sample_series();
6888        let combo = [ParamKV {
6889            key: "period",
6890            value: ParamValue::Int(10),
6891        }];
6892        let combos = [IndicatorParamSet { params: &combo }];
6893        let req = IndicatorBatchRequest {
6894            indicator_id: "cg",
6895            output_id: Some("value"),
6896            data: IndicatorDataRef::Slice { values: &data },
6897            combos: &combos,
6898            kernel: Kernel::Auto,
6899        };
6900        let out = compute_cpu_batch(req).unwrap();
6901        let input = CgInput::from_slice(&data, CgParams { period: Some(10) });
6902        let direct = cg_with_kernel(&input, Kernel::Auto.to_non_batch())
6903            .unwrap()
6904            .values;
6905        let got = out.values_f64.unwrap();
6906        assert_series_eq(&got, &direct, 1e-12);
6907    }
6908
6909    #[test]
6910    fn linearreg_slope_output_matches_direct() {
6911        let data = sample_series();
6912        let combo = [ParamKV {
6913            key: "period",
6914            value: ParamValue::Int(14),
6915        }];
6916        let combos = [IndicatorParamSet { params: &combo }];
6917        let req = IndicatorBatchRequest {
6918            indicator_id: "linearreg_slope",
6919            output_id: Some("value"),
6920            data: IndicatorDataRef::Slice { values: &data },
6921            combos: &combos,
6922            kernel: Kernel::Auto,
6923        };
6924        let out = compute_cpu_batch(req).unwrap();
6925        let input =
6926            LinearRegSlopeInput::from_slice(&data, LinearRegSlopeParams { period: Some(14) });
6927        let direct = linearreg_slope_with_kernel(&input, Kernel::Auto.to_non_batch())
6928            .unwrap()
6929            .values;
6930        let got = out.values_f64.unwrap();
6931        assert_series_eq(&got, &direct, 1e-12);
6932    }
6933
6934    #[test]
6935    fn mean_ad_output_matches_direct() {
6936        let data = sample_series();
6937        let combo = [ParamKV {
6938            key: "period",
6939            value: ParamValue::Int(7),
6940        }];
6941        let combos = [IndicatorParamSet { params: &combo }];
6942        let req = IndicatorBatchRequest {
6943            indicator_id: "mean_ad",
6944            output_id: Some("value"),
6945            data: IndicatorDataRef::Slice { values: &data },
6946            combos: &combos,
6947            kernel: Kernel::Auto,
6948        };
6949        let out = compute_cpu_batch(req).unwrap();
6950        let input = MeanAdInput::from_slice(&data, MeanAdParams { period: Some(7) });
6951        let direct = mean_ad_with_kernel(&input, Kernel::Auto.to_non_batch())
6952            .unwrap()
6953            .values;
6954        let got = out.values_f64.unwrap();
6955        assert_series_eq(&got, &direct, 1e-12);
6956    }
6957
6958    #[test]
6959    fn deviation_output_matches_direct() {
6960        let data = sample_series();
6961        let combo = [
6962            ParamKV {
6963                key: "period",
6964                value: ParamValue::Int(9),
6965            },
6966            ParamKV {
6967                key: "devtype",
6968                value: ParamValue::Int(2),
6969            },
6970        ];
6971        let combos = [IndicatorParamSet { params: &combo }];
6972        let req = IndicatorBatchRequest {
6973            indicator_id: "deviation",
6974            output_id: Some("value"),
6975            data: IndicatorDataRef::Slice { values: &data },
6976            combos: &combos,
6977            kernel: Kernel::Auto,
6978        };
6979        let out = compute_cpu_batch(req).unwrap();
6980        let input = DeviationInput::from_slice(
6981            &data,
6982            DeviationParams {
6983                period: Some(9),
6984                devtype: Some(2),
6985            },
6986        );
6987        let direct = deviation_with_kernel(&input, Kernel::Auto.to_non_batch())
6988            .unwrap()
6989            .values;
6990        let got = out.values_f64.unwrap();
6991        assert_series_eq(&got, &direct, 1e-12);
6992    }
6993
6994    #[test]
6995    fn medprice_output_matches_direct() {
6996        let (_open, high, low, _close) = sample_ohlc();
6997        let combos = [IndicatorParamSet { params: &[] }];
6998        let req = IndicatorBatchRequest {
6999            indicator_id: "medprice",
7000            output_id: Some("value"),
7001            data: IndicatorDataRef::HighLow {
7002                high: &high,
7003                low: &low,
7004            },
7005            combos: &combos,
7006            kernel: Kernel::Auto,
7007        };
7008        let out = compute_cpu_batch(req).unwrap();
7009        let input = MedpriceInput::from_slices(&high, &low, MedpriceParams::default());
7010        let direct = medprice_with_kernel(&input, Kernel::Auto.to_non_batch())
7011            .unwrap()
7012            .values;
7013        let got = out.values_f64.unwrap();
7014        assert_series_eq(&got, &direct, 1e-12);
7015    }
7016
7017    #[test]
7018    fn percentile_nearest_rank_output_matches_direct() {
7019        let data = sample_series();
7020        let combo = [
7021            ParamKV {
7022                key: "length",
7023                value: ParamValue::Int(12),
7024            },
7025            ParamKV {
7026                key: "percentage",
7027                value: ParamValue::Float(70.0),
7028            },
7029        ];
7030        let combos = [IndicatorParamSet { params: &combo }];
7031        let req = IndicatorBatchRequest {
7032            indicator_id: "percentile_nearest_rank",
7033            output_id: Some("value"),
7034            data: IndicatorDataRef::Slice { values: &data },
7035            combos: &combos,
7036            kernel: Kernel::Auto,
7037        };
7038        let out = compute_cpu_batch(req).unwrap();
7039        let input = PercentileNearestRankInput::from_slice(
7040            &data,
7041            PercentileNearestRankParams {
7042                length: Some(12),
7043                percentage: Some(70.0),
7044            },
7045        );
7046        let direct = percentile_nearest_rank_with_kernel(&input, Kernel::Auto.to_non_batch())
7047            .unwrap()
7048            .values;
7049        let got = out.values_f64.unwrap();
7050        assert_series_eq(&got, &direct, 1e-12);
7051    }
7052
7053    #[test]
7054    fn zscore_output_matches_direct() {
7055        let data = sample_series();
7056        let combo = [
7057            ParamKV {
7058                key: "period",
7059                value: ParamValue::Int(14),
7060            },
7061            ParamKV {
7062                key: "ma_type",
7063                value: ParamValue::EnumString("ema"),
7064            },
7065            ParamKV {
7066                key: "nbdev",
7067                value: ParamValue::Float(1.25),
7068            },
7069            ParamKV {
7070                key: "devtype",
7071                value: ParamValue::Int(1),
7072            },
7073        ];
7074        let combos = [IndicatorParamSet { params: &combo }];
7075        let req = IndicatorBatchRequest {
7076            indicator_id: "zscore",
7077            output_id: Some("value"),
7078            data: IndicatorDataRef::Slice { values: &data },
7079            combos: &combos,
7080            kernel: Kernel::Auto,
7081        };
7082        let out = compute_cpu_batch(req).unwrap();
7083        let input = ZscoreInput::from_slice(
7084            &data,
7085            ZscoreParams {
7086                period: Some(14),
7087                ma_type: Some("ema".to_string()),
7088                nbdev: Some(1.25),
7089                devtype: Some(1),
7090            },
7091        );
7092        let direct = zscore_with_kernel(&input, Kernel::Auto.to_non_batch())
7093            .unwrap()
7094            .values;
7095        let got = out.values_f64.unwrap();
7096        assert_series_eq(&got, &direct, 1e-12);
7097    }
7098
7099    #[test]
7100    fn vpci_secondary_output_matches_direct() {
7101        let close = sample_series();
7102        let volume: Vec<f64> = (0..close.len())
7103            .map(|i| 1000.0 + (i as f64 * 7.0))
7104            .collect();
7105        let combo = [
7106            ParamKV {
7107                key: "short_range",
7108                value: ParamValue::Int(5),
7109            },
7110            ParamKV {
7111                key: "long_range",
7112                value: ParamValue::Int(25),
7113            },
7114        ];
7115        let combos = [IndicatorParamSet { params: &combo }];
7116        let req = IndicatorBatchRequest {
7117            indicator_id: "vpci",
7118            output_id: Some("vpcis"),
7119            data: IndicatorDataRef::CloseVolume {
7120                close: &close,
7121                volume: &volume,
7122            },
7123            combos: &combos,
7124            kernel: Kernel::Auto,
7125        };
7126        let out = compute_cpu_batch(req).unwrap();
7127        let input = VpciInput::from_slices(
7128            &close,
7129            &volume,
7130            VpciParams {
7131                short_range: Some(5),
7132                long_range: Some(25),
7133            },
7134        );
7135        let direct = vpci_with_kernel(&input, Kernel::Auto.to_non_batch())
7136            .unwrap()
7137            .vpcis;
7138        let got = out.values_f64.unwrap();
7139        assert_series_eq(&got, &direct, 1e-12);
7140    }
7141
7142    #[test]
7143    fn ttm_trend_bool_output_matches_direct() {
7144        let (open, high, low, close) = sample_ohlc();
7145        let combo = [ParamKV {
7146            key: "period",
7147            value: ParamValue::Int(5),
7148        }];
7149        let combos = [IndicatorParamSet { params: &combo }];
7150        let req = IndicatorBatchRequest {
7151            indicator_id: "ttm_trend",
7152            output_id: Some("value"),
7153            data: IndicatorDataRef::Ohlc {
7154                open: &open,
7155                high: &high,
7156                low: &low,
7157                close: &close,
7158            },
7159            combos: &combos,
7160            kernel: Kernel::Auto,
7161        };
7162        let out = compute_cpu_batch(req).unwrap();
7163        let source: Vec<f64> = high.iter().zip(&low).map(|(h, l)| 0.5 * (h + l)).collect();
7164        let input = TtmTrendInput::from_slices(&source, &close, TtmTrendParams { period: Some(5) });
7165        let direct = ttm_trend_with_kernel(&input, Kernel::Auto.to_non_batch())
7166            .unwrap()
7167            .values;
7168        assert_eq!(out.values_bool.unwrap(), direct);
7169    }
7170
7171    fn build_default_params_for_indicator(
7172        info: &crate::indicators::registry::IndicatorInfo,
7173    ) -> Option<Vec<ParamKV<'static>>> {
7174        let mut params: Vec<ParamKV<'static>> = Vec::new();
7175        for p in &info.params {
7176            if p.key.eq_ignore_ascii_case("output") {
7177                continue;
7178            }
7179            let value = if let Some(default) = p.default {
7180                match default {
7181                    crate::indicators::registry::ParamValueStatic::Int(v) => {
7182                        Some(ParamValue::Int(v))
7183                    }
7184                    crate::indicators::registry::ParamValueStatic::Float(v) => {
7185                        Some(ParamValue::Float(v))
7186                    }
7187                    crate::indicators::registry::ParamValueStatic::Bool(v) => {
7188                        Some(ParamValue::Bool(v))
7189                    }
7190                    crate::indicators::registry::ParamValueStatic::EnumString(v) => {
7191                        Some(ParamValue::EnumString(v))
7192                    }
7193                }
7194            } else {
7195                match p.kind {
7196                    IndicatorParamKind::Int => {
7197                        let mut v = p.min.unwrap_or(14.0).round() as i64;
7198                        if v < 0 {
7199                            v = 0;
7200                        }
7201                        if let Some(max) = p.max {
7202                            v = v.min(max.round() as i64);
7203                        }
7204                        Some(ParamValue::Int(v))
7205                    }
7206                    IndicatorParamKind::Float => {
7207                        let mut v = p.min.unwrap_or(1.0);
7208                        if !v.is_finite() {
7209                            v = 1.0;
7210                        }
7211                        if let Some(max) = p.max {
7212                            v = v.min(max);
7213                        }
7214                        Some(ParamValue::Float(v))
7215                    }
7216                    IndicatorParamKind::Bool => Some(ParamValue::Bool(false)),
7217                    IndicatorParamKind::EnumString => {
7218                        p.enum_values.first().copied().map(ParamValue::EnumString)
7219                    }
7220                }
7221            };
7222
7223            match value {
7224                Some(v) => params.push(ParamKV {
7225                    key: p.key,
7226                    value: v,
7227                }),
7228                None => {
7229                    if p.required {
7230                        return None;
7231                    }
7232                }
7233            }
7234        }
7235        Some(params)
7236    }
7237
7238    fn median_ns(mut samples: Vec<u128>) -> u128 {
7239        samples.sort_unstable();
7240        samples[samples.len() / 2]
7241    }
7242
7243    #[test]
7244    #[ignore]
7245    fn full_cpu_dispatch_perf_sweep_vs_direct_route() {
7246        const LEN: usize = 10_000;
7247        const REPS: usize = 5;
7248
7249        let open: Vec<f64> = (0..LEN).map(|i| 100.0 + (i as f64 * 0.01)).collect();
7250        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
7251        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
7252        let close: Vec<f64> = open.iter().map(|v| v + 0.25).collect();
7253        let volume: Vec<f64> = (0..LEN).map(|i| 1000.0 + (i as f64 * 0.5)).collect();
7254        let timestamp: Vec<i64> = (0..LEN).map(|i| i as i64).collect();
7255        let candles = crate::utilities::data_loader::Candles::new(
7256            timestamp,
7257            open.clone(),
7258            high.clone(),
7259            low.clone(),
7260            close.clone(),
7261            volume.clone(),
7262        );
7263
7264        let infos: Vec<_> = list_indicators()
7265            .iter()
7266            .filter(|i| i.capabilities.supports_cpu_batch)
7267            .collect();
7268        let mut rows: Vec<(String, f64, f64, f64)> = Vec::new();
7269        let mut failures: Vec<String> = Vec::new();
7270
7271        for info in infos {
7272            let Some(output) = info.outputs.first() else {
7273                failures.push(format!("{}: no outputs", info.id));
7274                continue;
7275            };
7276            let output_id = output.id;
7277            let Some(params_vec) = build_default_params_for_indicator(info) else {
7278                failures.push(format!("{}: missing required param defaults", info.id));
7279                continue;
7280            };
7281            let combos = [IndicatorParamSet {
7282                params: params_vec.as_slice(),
7283            }];
7284            let data = match info.input_kind {
7285                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
7286                    values: close.as_slice(),
7287                },
7288                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
7289                    candles: &candles,
7290                    source: None,
7291                },
7292                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
7293                    open: open.as_slice(),
7294                    high: high.as_slice(),
7295                    low: low.as_slice(),
7296                    close: close.as_slice(),
7297                },
7298                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
7299                    open: open.as_slice(),
7300                    high: high.as_slice(),
7301                    low: low.as_slice(),
7302                    close: close.as_slice(),
7303                    volume: volume.as_slice(),
7304                },
7305                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
7306                    high: high.as_slice(),
7307                    low: low.as_slice(),
7308                },
7309                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
7310                    close: close.as_slice(),
7311                    volume: volume.as_slice(),
7312                },
7313            };
7314
7315            let req = IndicatorBatchRequest {
7316                indicator_id: info.id,
7317                output_id: Some(output_id),
7318                data,
7319                combos: &combos,
7320                kernel: Kernel::Auto,
7321            };
7322
7323            let dispatch_once = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7324                compute_cpu_batch(req)
7325            })) {
7326                Ok(Ok(v)) => v,
7327                Ok(Err(e)) => {
7328                    failures.push(format!("{}: dispatch error: {}", info.id, e));
7329                    continue;
7330                }
7331                Err(_) => {
7332                    failures.push(format!("{}: dispatch panic", info.id));
7333                    continue;
7334                }
7335            };
7336            let direct_once = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7337                dispatch_cpu_batch_by_indicator(req, info, output_id)
7338            })) {
7339                Ok(Ok(v)) => v,
7340                Ok(Err(e)) => {
7341                    failures.push(format!("{}: direct-route error: {}", info.id, e));
7342                    continue;
7343                }
7344                Err(_) => {
7345                    failures.push(format!("{}: direct-route panic", info.id));
7346                    continue;
7347                }
7348            };
7349
7350            if dispatch_once.rows != direct_once.rows || dispatch_once.cols != direct_once.cols {
7351                failures.push(format!(
7352                    "{}: shape mismatch dispatch=({},{}) direct=({},{})",
7353                    info.id,
7354                    dispatch_once.rows,
7355                    dispatch_once.cols,
7356                    direct_once.rows,
7357                    direct_once.cols
7358                ));
7359                continue;
7360            }
7361
7362            let mut dispatch_samples = Vec::with_capacity(REPS);
7363            let mut direct_samples = Vec::with_capacity(REPS);
7364            let mut panicked = false;
7365            for _ in 0..REPS {
7366                let t0 = Instant::now();
7367                let dispatch_iter = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7368                    compute_cpu_batch(req)
7369                }));
7370                if !matches!(dispatch_iter, Ok(Ok(_))) {
7371                    failures.push(format!("{}: dispatch panic/error during sample", info.id));
7372                    panicked = true;
7373                    break;
7374                }
7375                dispatch_samples.push(t0.elapsed().as_nanos());
7376
7377                let t1 = Instant::now();
7378                let direct_iter = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
7379                    dispatch_cpu_batch_by_indicator(req, info, output_id)
7380                }));
7381                if !matches!(direct_iter, Ok(Ok(_))) {
7382                    failures.push(format!(
7383                        "{}: direct-route panic/error during sample",
7384                        info.id
7385                    ));
7386                    panicked = true;
7387                    break;
7388                }
7389                direct_samples.push(t1.elapsed().as_nanos());
7390            }
7391            if panicked {
7392                continue;
7393            }
7394
7395            let dispatch_median = median_ns(dispatch_samples) as f64 / 1_000_000.0;
7396            let direct_median = median_ns(direct_samples) as f64 / 1_000_000.0;
7397            let delta_pct = if direct_median > 0.0 {
7398                ((dispatch_median - direct_median) / direct_median) * 100.0
7399            } else {
7400                0.0
7401            };
7402            rows.push((
7403                info.id.to_string(),
7404                direct_median,
7405                dispatch_median,
7406                delta_pct,
7407            ));
7408        }
7409
7410        rows.sort_by(|a, b| b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal));
7411
7412        println!("id,direct_ms,dispatch_ms,delta_pct");
7413        for (id, direct_ms, dispatch_ms, delta_pct) in &rows {
7414            println!("{id},{direct_ms:.6},{dispatch_ms:.6},{delta_pct:.2}");
7415        }
7416        println!("total_indicators={}", rows.len());
7417
7418        assert!(
7419            failures.is_empty(),
7420            "perf sweep failures: {}",
7421            failures.join(" | ")
7422        );
7423        assert!(!rows.is_empty(), "no indicators were swept");
7424    }
7425
7426    #[test]
7427    fn multi_output_requires_output_id() {
7428        let data = sample_series();
7429        let combos: [IndicatorParamSet<'_>; 0] = [];
7430        let req = IndicatorBatchRequest {
7431            indicator_id: "macd",
7432            output_id: None,
7433            data: IndicatorDataRef::Slice { values: &data },
7434            combos: &combos,
7435            kernel: Kernel::Auto,
7436        };
7437        let err = compute_cpu_batch(req).unwrap_err();
7438        assert!(matches!(err, IndicatorDispatchError::InvalidParam { .. }));
7439    }
7440
7441    #[test]
7442    fn multi_output_unknown_output_is_rejected_globally() {
7443        let (open, high, low, close) = sample_ohlc();
7444        let volume: Vec<f64> = (0..close.len())
7445            .map(|i| 1000.0 + (i as f64 * 0.5))
7446            .collect();
7447        let timestamp: Vec<i64> = (0..close.len()).map(|i| i as i64).collect();
7448        let candles = crate::utilities::data_loader::Candles::new(
7449            timestamp,
7450            open.clone(),
7451            high.clone(),
7452            low.clone(),
7453            close.clone(),
7454            volume.clone(),
7455        );
7456
7457        for info in list_indicators()
7458            .iter()
7459            .filter(|i| i.capabilities.supports_cpu_batch && i.outputs.len() > 1)
7460        {
7461            let Some(params_vec) = build_default_params_for_indicator(info) else {
7462                continue;
7463            };
7464            let combos = [IndicatorParamSet {
7465                params: params_vec.as_slice(),
7466            }];
7467            let data = match info.input_kind {
7468                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
7469                    values: close.as_slice(),
7470                },
7471                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
7472                    candles: &candles,
7473                    source: None,
7474                },
7475                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
7476                    open: open.as_slice(),
7477                    high: high.as_slice(),
7478                    low: low.as_slice(),
7479                    close: close.as_slice(),
7480                },
7481                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
7482                    open: open.as_slice(),
7483                    high: high.as_slice(),
7484                    low: low.as_slice(),
7485                    close: close.as_slice(),
7486                    volume: volume.as_slice(),
7487                },
7488                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
7489                    high: high.as_slice(),
7490                    low: low.as_slice(),
7491                },
7492                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
7493                    close: close.as_slice(),
7494                    volume: volume.as_slice(),
7495                },
7496            };
7497            let req = IndicatorBatchRequest {
7498                indicator_id: info.id,
7499                output_id: Some("__unknown_output__"),
7500                data,
7501                combos: &combos,
7502                kernel: Kernel::Auto,
7503            };
7504            let err = compute_cpu_batch(req).unwrap_err();
7505            assert!(
7506                matches!(err, IndicatorDispatchError::UnknownOutput { .. }),
7507                "indicator {} returned unexpected error for unknown output: {:?}",
7508                info.id,
7509                err
7510            );
7511        }
7512    }
7513
7514    #[test]
7515    fn strict_mode_rejects_mismatched_input_kind_globally() {
7516        let data = sample_series();
7517        let candles = sample_candles();
7518
7519        for info in list_indicators()
7520            .iter()
7521            .filter(|i| i.capabilities.supports_cpu_batch)
7522        {
7523            let Some(output) = info.outputs.first() else {
7524                continue;
7525            };
7526            let Some(params_vec) = build_default_params_for_indicator(info) else {
7527                continue;
7528            };
7529            let combos = [IndicatorParamSet {
7530                params: params_vec.as_slice(),
7531            }];
7532            let expected = strict_expected_input_kind(info.id, info.input_kind);
7533            let mismatched = match expected {
7534                IndicatorInputKind::Slice => IndicatorDataRef::Candles {
7535                    candles: &candles,
7536                    source: None,
7537                },
7538                IndicatorInputKind::Candles => IndicatorDataRef::Slice { values: &data },
7539                IndicatorInputKind::Ohlc
7540                | IndicatorInputKind::Ohlcv
7541                | IndicatorInputKind::HighLow
7542                | IndicatorInputKind::CloseVolume => IndicatorDataRef::Slice { values: &data },
7543            };
7544            let req = IndicatorBatchRequest {
7545                indicator_id: info.id,
7546                output_id: Some(output.id),
7547                data: mismatched,
7548                combos: &combos,
7549                kernel: Kernel::Auto,
7550            };
7551            let err = compute_cpu_batch_strict(req).unwrap_err();
7552            assert!(
7553                matches!(err, IndicatorDispatchError::MissingRequiredInput { .. }),
7554                "indicator {} did not reject strict mismatched input: {:?}",
7555                info.id,
7556                err
7557            );
7558        }
7559    }
7560
7561    #[test]
7562    fn full_cpu_dispatch_parity_vs_direct_route_for_all_outputs() {
7563        const LEN: usize = 4096;
7564        let open: Vec<f64> = (0..LEN).map(|i| 100.0 + (i as f64 * 0.01)).collect();
7565        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
7566        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
7567        let close: Vec<f64> = open.iter().map(|v| v + 0.25).collect();
7568        let volume: Vec<f64> = (0..LEN).map(|i| 1000.0 + (i as f64 * 0.5)).collect();
7569        let timestamp: Vec<i64> = (0..LEN).map(|i| i as i64).collect();
7570        let candles = crate::utilities::data_loader::Candles::new(
7571            timestamp,
7572            open.clone(),
7573            high.clone(),
7574            low.clone(),
7575            close.clone(),
7576            volume.clone(),
7577        );
7578
7579        for info in list_indicators()
7580            .iter()
7581            .filter(|i| i.capabilities.supports_cpu_batch)
7582        {
7583            let Some(params_vec) = build_default_params_for_indicator(info) else {
7584                continue;
7585            };
7586            let combos = [IndicatorParamSet {
7587                params: params_vec.as_slice(),
7588            }];
7589            let data = match info.input_kind {
7590                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
7591                    values: close.as_slice(),
7592                },
7593                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
7594                    candles: &candles,
7595                    source: None,
7596                },
7597                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
7598                    open: open.as_slice(),
7599                    high: high.as_slice(),
7600                    low: low.as_slice(),
7601                    close: close.as_slice(),
7602                },
7603                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
7604                    open: open.as_slice(),
7605                    high: high.as_slice(),
7606                    low: low.as_slice(),
7607                    close: close.as_slice(),
7608                    volume: volume.as_slice(),
7609                },
7610                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
7611                    high: high.as_slice(),
7612                    low: low.as_slice(),
7613                },
7614                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
7615                    close: close.as_slice(),
7616                    volume: volume.as_slice(),
7617                },
7618            };
7619
7620            for output in info.outputs.iter() {
7621                let req = IndicatorBatchRequest {
7622                    indicator_id: info.id,
7623                    output_id: Some(output.id),
7624                    data,
7625                    combos: &combos,
7626                    kernel: Kernel::Auto,
7627                };
7628                let generic = compute_cpu_batch(req).unwrap_or_else(|e| {
7629                    panic!(
7630                        "generic dispatch failed for {}:{}: {}",
7631                        info.id, output.id, e
7632                    )
7633                });
7634                let direct =
7635                    dispatch_cpu_batch_by_indicator(req, info, output.id).unwrap_or_else(|e| {
7636                        panic!("direct route failed for {}:{}: {}", info.id, output.id, e)
7637                    });
7638
7639                assert_eq!(
7640                    generic.rows, direct.rows,
7641                    "rows mismatch for {}:{}",
7642                    info.id, output.id
7643                );
7644                assert_eq!(
7645                    generic.cols, direct.cols,
7646                    "cols mismatch for {}:{}",
7647                    info.id, output.id
7648                );
7649                assert_eq!(
7650                    generic.output_id, direct.output_id,
7651                    "output id mismatch for {}:{}",
7652                    info.id, output.id
7653                );
7654
7655                match (
7656                    generic.values_f64.as_ref(),
7657                    direct.values_f64.as_ref(),
7658                    generic.values_i32.as_ref(),
7659                    direct.values_i32.as_ref(),
7660                    generic.values_bool.as_ref(),
7661                    direct.values_bool.as_ref(),
7662                ) {
7663                    (Some(g), Some(d), None, None, None, None) => assert_series_eq(g, d, 1e-9),
7664                    (None, None, Some(g), Some(d), None, None) => assert_eq!(g, d),
7665                    (None, None, None, None, Some(g), Some(d)) => assert_eq!(g, d),
7666                    _ => panic!("value type mismatch for {}:{}", info.id, output.id),
7667                }
7668            }
7669        }
7670    }
7671}