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