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::absolute_strength_index_oscillator::{
6    absolute_strength_index_oscillator_with_kernel, AbsoluteStrengthIndexOscillatorInput,
7    AbsoluteStrengthIndexOscillatorParams,
8};
9use crate::indicators::accumulation_swing_index::{
10    accumulation_swing_index_with_kernel, AccumulationSwingIndexInput,
11    AccumulationSwingIndexParams,
12};
13use crate::indicators::acosc::{acosc_with_kernel, AcoscInput, AcoscParams};
14use crate::indicators::ad::{ad_with_kernel, AdInput, AdParams};
15use crate::indicators::adaptive_macd::{
16    adaptive_macd_with_kernel, AdaptiveMacdInput, AdaptiveMacdParams,
17};
18use crate::indicators::adaptive_schaff_trend_cycle::{
19    adaptive_schaff_trend_cycle_with_kernel, AdaptiveSchaffTrendCycleInput,
20    AdaptiveSchaffTrendCycleParams,
21};
22use crate::indicators::adaptive_momentum_oscillator::{
23    adaptive_momentum_oscillator_with_kernel, AdaptiveMomentumOscillatorInput,
24    AdaptiveMomentumOscillatorParams,
25};
26use crate::indicators::adjustable_ma_alternating_extremities::{
27    adjustable_ma_alternating_extremities_with_kernel, AdjustableMaAlternatingExtremitiesInput,
28    AdjustableMaAlternatingExtremitiesParams,
29};
30use crate::indicators::adaptive_bandpass_trigger_oscillator::{
31    adaptive_bandpass_trigger_oscillator_with_kernel, AdaptiveBandpassTriggerOscillatorInput,
32    AdaptiveBandpassTriggerOscillatorParams,
33};
34use crate::indicators::adosc::{adosc_with_kernel, AdoscInput, AdoscParams};
35use crate::indicators::advance_decline_line::{
36    advance_decline_line_with_kernel, AdvanceDeclineLineInput, AdvanceDeclineLineParams,
37};
38use crate::indicators::adx::{adx_with_kernel, AdxInput, AdxParams};
39use crate::indicators::adxr::{adxr_with_kernel, AdxrInput, AdxrParams};
40use crate::indicators::alligator::{alligator_with_kernel, AlligatorInput, AlligatorParams};
41use crate::indicators::alphatrend::{alphatrend_with_kernel, AlphaTrendInput, AlphaTrendParams};
42use crate::indicators::andean_oscillator::{
43    andean_oscillator_with_kernel, AndeanOscillatorInput, AndeanOscillatorParams,
44};
45use crate::indicators::ao::{ao_into_slice, AoInput, AoParams};
46use crate::indicators::apo::{apo_with_kernel, ApoInput, ApoParams};
47use crate::indicators::aroon::{aroon_with_kernel, AroonInput, AroonParams};
48use crate::indicators::aroonosc::{aroon_osc_with_kernel, AroonOscInput, AroonOscParams};
49use crate::indicators::aso::{aso_with_kernel, AsoInput, AsoParams};
50use crate::indicators::atr::{atr_with_kernel, AtrInput, AtrParams};
51use crate::indicators::atr_percentile::{
52    atr_percentile_with_kernel, AtrPercentileInput, AtrPercentileParams,
53};
54use crate::indicators::autocorrelation_indicator::{
55    autocorrelation_indicator_with_kernel, AutocorrelationIndicatorInput,
56    AutocorrelationIndicatorParams,
57};
58use crate::indicators::avsl::{avsl_with_kernel, AvslInput, AvslParams};
59use crate::indicators::bandpass::{bandpass_with_kernel, BandPassInput, BandPassParams};
60use crate::indicators::bollinger_bands::{
61    bollinger_bands_with_kernel, BollingerBandsInput, BollingerBandsParams,
62};
63use crate::indicators::bollinger_bands_width::{
64    bollinger_bands_width_with_kernel, BollingerBandsWidthInput, BollingerBandsWidthParams,
65};
66use crate::indicators::bop::{bop_with_kernel, BopInput, BopParams};
67use crate::indicators::bulls_v_bears::{
68    bulls_v_bears_with_kernel, BullsVBearsCalculationMethod, BullsVBearsInput, BullsVBearsMaType,
69    BullsVBearsParams,
70};
71use crate::indicators::bull_power_vs_bear_power::{
72    bull_power_vs_bear_power_with_kernel, BullPowerVsBearPowerInput, BullPowerVsBearPowerParams,
73};
74use crate::indicators::candle_strength_oscillator::{
75    candle_strength_oscillator_with_kernel, CandleStrengthOscillatorInput,
76    CandleStrengthOscillatorParams,
77};
78use crate::indicators::cci::{cci_with_kernel, CciInput, CciParams};
79use crate::indicators::cci_cycle::{cci_cycle_with_kernel, CciCycleInput, CciCycleParams};
80use crate::indicators::cfo::{cfo_with_kernel, CfoInput, CfoParams};
81use crate::indicators::chande::{chande_with_kernel, ChandeInput, ChandeParams};
82use crate::indicators::chandelier_exit::{
83    chandelier_exit_with_kernel, ChandelierExitInput, ChandelierExitParams,
84};
85use crate::indicators::chop::{chop_with_kernel, ChopInput, ChopParams};
86use crate::indicators::cksp::{cksp_with_kernel, CkspInput, CkspParams};
87use crate::indicators::cmo::{cmo_with_kernel, CmoInput, CmoParams};
88use crate::indicators::coppock::{coppock_with_kernel, CoppockInput, CoppockParams};
89use crate::indicators::correl_hl::{correl_hl_with_kernel, CorrelHlInput, CorrelHlParams};
90use crate::indicators::correlation_cycle::{
91    correlation_cycle_with_kernel, CorrelationCycleInput, CorrelationCycleParams,
92};
93use crate::indicators::cycle_channel_oscillator::{
94    cycle_channel_oscillator_with_kernel, CycleChannelOscillatorInput,
95    CycleChannelOscillatorParams,
96};
97use crate::indicators::cyberpunk_value_trend_analyzer::{
98    cyberpunk_value_trend_analyzer_with_kernel, CyberpunkValueTrendAnalyzerInput,
99    CyberpunkValueTrendAnalyzerParams,
100};
101use crate::indicators::daily_factor::{
102    daily_factor_with_kernel, DailyFactorInput, DailyFactorParams,
103};
104use crate::indicators::damiani_volatmeter::{
105    damiani_volatmeter_with_kernel, DamianiVolatmeterInput, DamianiVolatmeterParams,
106};
107use crate::indicators::decisionpoint_breadth_swenlin_trading_oscillator::{
108    decisionpoint_breadth_swenlin_trading_oscillator_with_kernel,
109    DecisionPointBreadthSwenlinTradingOscillatorInput,
110    DecisionPointBreadthSwenlinTradingOscillatorParams,
111};
112use crate::indicators::demand_index::{
113    demand_index_with_kernel, DemandIndexInput, DemandIndexParams,
114};
115use crate::indicators::deviation::{deviation_with_kernel, DeviationInput, DeviationParams};
116use crate::indicators::devstop::{devstop_with_kernel, DevStopInput, DevStopParams};
117use crate::indicators::di::{di_with_kernel, DiInput, DiParams};
118use crate::indicators::directional_imbalance_index::{
119    directional_imbalance_index_with_kernel, DirectionalImbalanceIndexInput,
120    DirectionalImbalanceIndexParams,
121};
122use crate::indicators::disparity_index::{
123    disparity_index_into_slice, DisparityIndexInput, DisparityIndexParams,
124};
125use crate::indicators::didi_index::{didi_index_with_kernel, DidiIndexInput, DidiIndexParams};
126use crate::indicators::dm::{dm_with_kernel, DmInput, DmParams};
127use crate::indicators::donchian::{donchian_with_kernel, DonchianInput, DonchianParams};
128use crate::indicators::donchian_channel_width::{
129    donchian_channel_width_into_slice, DonchianChannelWidthInput, DonchianChannelWidthParams,
130};
131use crate::indicators::dpo::{dpo_with_kernel, DpoInput, DpoParams};
132use crate::indicators::dti::{dti_into_slice, DtiInput, DtiParams};
133use crate::indicators::dual_ulcer_index::{
134    dual_ulcer_index_with_kernel, DualUlcerIndexInput, DualUlcerIndexParams,
135};
136use crate::indicators::dvdiqqe::{dvdiqqe_with_kernel, DvdiqqeInput, DvdiqqeParams};
137use crate::indicators::dx::{dx_batch_with_kernel, dx_into_slice, DxBatchRange, DxInput, DxParams};
138use crate::indicators::dynamic_momentum_index::{
139    dynamic_momentum_index_into_slice, dynamic_momentum_index_with_kernel,
140    DynamicMomentumIndexInput, DynamicMomentumIndexParams,
141};
142use crate::indicators::efi::{efi_with_kernel, EfiInput, EfiParams};
143use crate::indicators::ehlers_autocorrelation_periodogram::{
144    ehlers_autocorrelation_periodogram_with_kernel, EhlersAutocorrelationPeriodogramInput,
145    EhlersAutocorrelationPeriodogramParams,
146};
147use crate::indicators::ehlers_adaptive_cg::{
148    ehlers_adaptive_cg_with_kernel, EhlersAdaptiveCgInput, EhlersAdaptiveCgParams,
149};
150use crate::indicators::ehlers_adaptive_cyber_cycle::{
151    ehlers_adaptive_cyber_cycle_with_kernel, EhlersAdaptiveCyberCycleInput,
152    EhlersAdaptiveCyberCycleParams,
153};
154use crate::indicators::ehlers_data_sampling_relative_strength_indicator::{
155    ehlers_data_sampling_relative_strength_indicator_with_kernel,
156    EhlersDataSamplingRelativeStrengthIndicatorInput,
157    EhlersDataSamplingRelativeStrengthIndicatorParams,
158};
159use crate::indicators::ehlers_fm_demodulator::{
160    ehlers_fm_demodulator_with_kernel, EhlersFmDemodulatorInput, EhlersFmDemodulatorParams,
161};
162use crate::indicators::ehlers_detrending_filter::{
163    ehlers_detrending_filter_with_kernel, EhlersDetrendingFilterInput,
164    EhlersDetrendingFilterParams,
165};
166use crate::indicators::ehlers_linear_extrapolation_predictor::{
167    ehlers_linear_extrapolation_predictor_with_kernel, EhlersLinearExtrapolationPredictorInput,
168    EhlersLinearExtrapolationPredictorParams,
169};
170use crate::indicators::ehlers_simple_cycle_indicator::{
171    ehlers_simple_cycle_indicator_with_kernel, EhlersSimpleCycleIndicatorInput,
172    EhlersSimpleCycleIndicatorParams,
173};
174use crate::indicators::ehlers_smoothed_adaptive_momentum::{
175    ehlers_smoothed_adaptive_momentum_with_kernel, EhlersSmoothedAdaptiveMomentumInput,
176    EhlersSmoothedAdaptiveMomentumParams,
177};
178use crate::indicators::emd::{emd_with_kernel, EmdInput, EmdParams};
179use crate::indicators::emd_trend::{emd_trend_with_kernel, EmdTrendInput, EmdTrendParams};
180use crate::indicators::emv::{emv_with_kernel, EmvInput};
181use crate::indicators::er::{er_with_kernel, ErInput, ErParams};
182use crate::indicators::eri::{eri_with_kernel, EriInput, EriParams};
183use crate::indicators::ewma_volatility::{
184    ewma_volatility_with_kernel, EwmaVolatilityInput, EwmaVolatilityParams,
185};
186use crate::indicators::evasive_supertrend::{
187    evasive_supertrend_with_kernel, EvasiveSuperTrendInput, EvasiveSuperTrendParams,
188};
189use crate::indicators::exponential_trend::{
190    exponential_trend_with_kernel, ExponentialTrendInput, ExponentialTrendParams,
191};
192use crate::indicators::fibonacci_entry_bands::{
193    fibonacci_entry_bands_with_kernel, FibonacciEntryBandsInput, FibonacciEntryBandsParams,
194};
195use crate::indicators::fibonacci_trailing_stop::{
196    fibonacci_trailing_stop_with_kernel, FibonacciTrailingStopInput, FibonacciTrailingStopParams,
197};
198use crate::indicators::fisher::{fisher_with_kernel, FisherInput, FisherParams};
199use crate::indicators::forward_backward_exponential_oscillator::{
200    forward_backward_exponential_oscillator_with_kernel, ForwardBackwardExponentialOscillatorInput,
201    ForwardBackwardExponentialOscillatorParams,
202};
203use crate::indicators::fosc::{fosc_with_kernel, FoscInput, FoscParams};
204use crate::indicators::fractal_dimension_index::{
205    fractal_dimension_index_with_kernel, FractalDimensionIndexInput, FractalDimensionIndexParams,
206};
207use crate::indicators::fvg_positioning_average::{
208    fvg_positioning_average_with_kernel, FvgPositioningAverageInput, FvgPositioningAverageParams,
209};
210use crate::indicators::fvg_trailing_stop::{
211    fvg_trailing_stop_with_kernel, FvgTrailingStopInput, FvgTrailingStopParams,
212};
213use crate::indicators::garman_klass_volatility::{
214    garman_klass_volatility_with_kernel, GarmanKlassVolatilityInput, GarmanKlassVolatilityParams,
215};
216use crate::indicators::gatorosc::{gatorosc_with_kernel, GatorOscInput, GatorOscParams};
217use crate::indicators::geometric_bias_oscillator::{
218    geometric_bias_oscillator_with_kernel, GeometricBiasOscillatorInput,
219    GeometricBiasOscillatorParams,
220};
221use crate::indicators::gmma_oscillator::{
222    gmma_oscillator_with_kernel, GmmaOscillatorInput, GmmaOscillatorParams,
223};
224use crate::indicators::goertzel_cycle_composite_wave::{
225    goertzel_cycle_composite_wave_into_slice, GoertzelCycleCompositeWaveInput,
226    GoertzelCycleCompositeWaveParams, GoertzelDetrendMode,
227};
228use crate::indicators::gopalakrishnan_range_index::{
229    gopalakrishnan_range_index_with_kernel, GopalakrishnanRangeIndexInput,
230    GopalakrishnanRangeIndexParams,
231};
232use crate::indicators::grover_llorens_cycle_oscillator::{
233    grover_llorens_cycle_oscillator_with_kernel, GroverLlorensCycleOscillatorInput,
234    GroverLlorensCycleOscillatorParams,
235};
236use crate::indicators::half_causal_estimator::{
237    half_causal_estimator_with_kernel, HalfCausalEstimatorConfidenceAdjust,
238    HalfCausalEstimatorInput, HalfCausalEstimatorKernelType, HalfCausalEstimatorParams,
239};
240use crate::indicators::halftrend::{halftrend_with_kernel, HalfTrendInput, HalfTrendParams};
241use crate::indicators::hema_trend_levels::{
242    hema_trend_levels_with_kernel, HemaTrendLevelsInput, HemaTrendLevelsParams,
243};
244use crate::indicators::historical_volatility::{
245    historical_volatility_with_kernel, HistoricalVolatilityInput, HistoricalVolatilityParams,
246};
247use crate::indicators::historical_volatility_percentile::{
248    historical_volatility_percentile_with_kernel, HistoricalVolatilityPercentileInput,
249    HistoricalVolatilityPercentileParams,
250};
251use crate::indicators::historical_volatility_rank::{
252    historical_volatility_rank_with_kernel, HistoricalVolatilityRankInput,
253    HistoricalVolatilityRankParams,
254};
255use crate::indicators::hull_butterfly_oscillator::{
256    hull_butterfly_oscillator_with_kernel, HullButterflyOscillatorInput,
257    HullButterflyOscillatorParams,
258};
259use crate::indicators::hypertrend::{hypertrend_with_kernel, HyperTrendInput, HyperTrendParams};
260use crate::indicators::ift_rsi::{ift_rsi_with_kernel, IftRsiInput, IftRsiParams};
261use crate::indicators::ichimoku_oscillator::{
262    ichimoku_oscillator_with_kernel, IchimokuOscillatorInput, IchimokuOscillatorNormalizeMode,
263    IchimokuOscillatorParams,
264};
265use crate::indicators::ict_propulsion_block::{
266    ict_propulsion_block_with_kernel, IctPropulsionBlockInput,
267    IctPropulsionBlockMitigationPrice, IctPropulsionBlockParams,
268};
269use crate::indicators::impulse_macd::{
270    impulse_macd_with_kernel, ImpulseMacdInput, ImpulseMacdParams,
271};
272use crate::indicators::intraday_momentum_index::{
273    intraday_momentum_index_with_kernel, IntradayMomentumIndexInput, IntradayMomentumIndexParams,
274};
275use crate::indicators::kase_peak_oscillator_with_divergences::{
276    kase_peak_oscillator_with_divergences_with_kernel, KasePeakOscillatorWithDivergencesInput,
277    KasePeakOscillatorWithDivergencesParams,
278};
279use crate::indicators::kairi_relative_index::{
280    kairi_relative_index_into_slice, KairiRelativeIndexInput, KairiRelativeIndexParams,
281};
282use crate::indicators::kaufmanstop::{
283    kaufmanstop_with_kernel, KaufmanstopInput, KaufmanstopParams,
284};
285use crate::indicators::kdj::{kdj_with_kernel, KdjInput, KdjParams};
286use crate::indicators::keltner::{keltner_with_kernel, KeltnerInput, KeltnerParams};
287use crate::indicators::keltner_channel_width_oscillator::{
288    keltner_channel_width_oscillator_with_kernel, KeltnerChannelWidthOscillatorInput,
289    KeltnerChannelWidthOscillatorParams,
290};
291use crate::indicators::kst::{kst_with_kernel, KstInput, KstParams};
292use crate::indicators::kurtosis::{kurtosis_with_kernel, KurtosisInput, KurtosisParams};
293use crate::indicators::kvo::{kvo_with_kernel, KvoInput, KvoParams};
294use crate::indicators::leavitt_convolution_acceleration::{
295    leavitt_convolution_acceleration_with_kernel, LeavittConvolutionAccelerationInput,
296    LeavittConvolutionAccelerationParams,
297};
298use crate::indicators::linear_regression_intensity::{
299    linear_regression_intensity_with_kernel, LinearRegressionIntensityInput,
300    LinearRegressionIntensityParams,
301};
302use crate::indicators::l1_ehlers_phasor::{
303    l1_ehlers_phasor_with_kernel, L1EhlersPhasorInput, L1EhlersPhasorParams,
304};
305use crate::indicators::l2_ehlers_signal_to_noise::{
306    l2_ehlers_signal_to_noise_with_kernel, L2EhlersSignalToNoiseInput, L2EhlersSignalToNoiseParams,
307};
308use crate::indicators::linear_correlation_oscillator::{
309    linear_correlation_oscillator_with_kernel, LinearCorrelationOscillatorInput,
310    LinearCorrelationOscillatorParams,
311};
312use crate::indicators::linearreg_angle::{
313    linearreg_angle_with_kernel, Linearreg_angleInput, Linearreg_angleParams,
314};
315use crate::indicators::linearreg_intercept::{
316    linearreg_intercept_with_kernel, LinearRegInterceptInput, LinearRegInterceptParams,
317};
318use crate::indicators::linearreg_slope::{
319    linearreg_slope_with_kernel, LinearRegSlopeInput, LinearRegSlopeParams,
320};
321use crate::indicators::lpc::{lpc_with_kernel, LpcInput, LpcParams};
322use crate::indicators::lrsi::{lrsi_with_kernel, LrsiInput, LrsiParams};
323use crate::indicators::mab::{mab_with_kernel, MabInput, MabParams};
324use crate::indicators::macd::{macd_with_kernel, MacdInput, MacdParams};
325use crate::indicators::macd_wave_signal_pro::{
326    macd_wave_signal_pro_with_kernel, MacdWaveSignalProInput,
327};
328use crate::indicators::macz::{macz_with_kernel, MaczInput, MaczParams};
329use crate::indicators::market_meanness_index::{
330    market_meanness_index_with_kernel, MarketMeannessIndexInput, MarketMeannessIndexParams,
331};
332use crate::indicators::market_structure_trailing_stop::{
333    market_structure_trailing_stop_with_kernel, MarketStructureTrailingStopInput,
334    MarketStructureTrailingStopParams,
335};
336use crate::indicators::mass::{mass_with_kernel, MassInput, MassParams};
337use crate::indicators::mean_ad::{mean_ad_with_kernel, MeanAdInput, MeanAdParams};
338use crate::indicators::medium_ad::{medium_ad_with_kernel, MediumAdInput, MediumAdParams};
339use crate::indicators::medprice::{medprice_with_kernel, MedpriceInput, MedpriceParams};
340use crate::indicators::mesa_stochastic_multi_length::{
341    mesa_stochastic_multi_length_with_kernel, MesaStochasticMultiLengthInput,
342    MesaStochasticMultiLengthParams,
343};
344use crate::indicators::mfi::{
345    mfi_batch_with_kernel, mfi_into_slice, MfiBatchRange, MfiInput, MfiParams,
346};
347use crate::indicators::midpoint::{midpoint_with_kernel, MidpointInput, MidpointParams};
348use crate::indicators::midprice::{midprice_with_kernel, MidpriceInput, MidpriceParams};
349use crate::indicators::minmax::{minmax_with_kernel, MinmaxInput, MinmaxParams};
350use crate::indicators::mod_god_mode::{
351    mod_god_mode, ModGodModeData, ModGodModeInput, ModGodModeMode, ModGodModeParams,
352};
353use crate::indicators::mom::{mom_with_kernel, MomInput, MomParams};
354use crate::indicators::monotonicity_index::{
355    monotonicity_index_with_kernel, MonotonicityIndexInput, MonotonicityIndexMode,
356    MonotonicityIndexParams,
357};
358use crate::indicators::momentum_ratio_oscillator::{
359    momentum_ratio_oscillator_with_kernel, MomentumRatioOscillatorInput,
360    MomentumRatioOscillatorParams,
361};
362use crate::indicators::moving_average_cross_probability::{
363    moving_average_cross_probability_with_kernel, MovingAverageCrossProbabilityInput,
364    MovingAverageCrossProbabilityMaType, MovingAverageCrossProbabilityParams,
365};
366use crate::indicators::moving_averages::ma::MaData;
367use crate::indicators::moving_averages::ma_batch::{
368    ma_batch_with_kernel_and_typed_params, MaBatchParamKV, MaBatchParamValue,
369};
370use crate::indicators::moving_averages::logarithmic_moving_average::{
371    logarithmic_moving_average_with_kernel, LogarithmicMovingAverageInput,
372    LogarithmicMovingAverageParams,
373};
374use crate::indicators::moving_averages::registry::list_moving_averages;
375use crate::indicators::msw::{msw_with_kernel, MswInput, MswParams};
376use crate::indicators::multi_length_stochastic_average::{
377    multi_length_stochastic_average_with_kernel, MultiLengthStochasticAverageInput,
378    MultiLengthStochasticAverageParams,
379};
380use crate::indicators::nadaraya_watson_envelope::{
381    nadaraya_watson_envelope_with_kernel, NweInput, NweParams,
382};
383use crate::indicators::natr::{natr_with_kernel, NatrInput, NatrParams};
384use crate::indicators::neighboring_trailing_stop::{
385    neighboring_trailing_stop_with_kernel, NeighboringTrailingStopInput,
386    NeighboringTrailingStopParams,
387};
388use crate::indicators::net_myrsi::{net_myrsi_with_kernel, NetMyrsiInput, NetMyrsiParams};
389use crate::indicators::nonlinear_regression_zero_lag_moving_average::{
390    nonlinear_regression_zero_lag_moving_average_with_kernel,
391    NonlinearRegressionZeroLagMovingAverageInput,
392    NonlinearRegressionZeroLagMovingAverageParams,
393};
394use crate::indicators::normalized_volume_true_range::{
395    normalized_volume_true_range_with_kernel, NormalizedVolumeTrueRangeInput,
396    NormalizedVolumeTrueRangeParams, NormalizedVolumeTrueRangeStyle,
397};
398use crate::indicators::normalized_resonator::{
399    normalized_resonator_with_kernel, NormalizedResonatorInput, NormalizedResonatorParams,
400};
401use crate::indicators::nvi::{nvi_with_kernel, NviInput, NviParams};
402use crate::indicators::obv::{obv_with_kernel, ObvInput, ObvParams};
403use crate::indicators::on_balance_volume_oscillator::{
404    on_balance_volume_oscillator_with_kernel, OnBalanceVolumeOscillatorInput,
405    OnBalanceVolumeOscillatorParams,
406};
407use crate::indicators::otto::{otto_with_kernel, OttoInput, OttoParams};
408use crate::indicators::parkinson_volatility::{
409    parkinson_volatility_with_kernel, ParkinsonVolatilityInput, ParkinsonVolatilityParams,
410};
411use crate::indicators::price_moving_average_ratio_percentile::{
412    price_moving_average_ratio_percentile_with_kernel, PriceMovingAverageRatioPercentileInput,
413    PriceMovingAverageRatioPercentileLineMode, PriceMovingAverageRatioPercentileMaType,
414    PriceMovingAverageRatioPercentileParams,
415};
416use crate::indicators::percentile_nearest_rank::{
417    percentile_nearest_rank_with_kernel, PercentileNearestRankInput, PercentileNearestRankParams,
418};
419use crate::indicators::pfe::{pfe_with_kernel, PfeInput, PfeParams};
420use crate::indicators::pivot::{pivot_with_kernel, PivotInput, PivotParams};
421use crate::indicators::pma::{pma_with_kernel, PmaInput, PmaParams};
422use crate::indicators::possible_rsi::{
423    possible_rsi_with_kernel, PossibleRsiInput, PossibleRsiParams,
424};
425use crate::indicators::polynomial_regression_extrapolation::{
426    polynomial_regression_extrapolation_with_kernel, PolynomialRegressionExtrapolationInput,
427    PolynomialRegressionExtrapolationParams,
428};
429use crate::indicators::ppo::{ppo_with_kernel, PpoInput, PpoParams};
430use crate::indicators::prb::{prb_with_kernel, PrbInput, PrbParams};
431use crate::indicators::premier_rsi_oscillator::{
432    premier_rsi_oscillator_with_kernel, PremierRsiOscillatorInput, PremierRsiOscillatorParams,
433};
434use crate::indicators::pvi::{pvi_with_kernel, PviInput, PviParams};
435use crate::indicators::pretty_good_oscillator::{
436    pretty_good_oscillator_with_kernel, PrettyGoodOscillatorInput, PrettyGoodOscillatorParams,
437};
438use crate::indicators::price_density_market_noise::{
439    price_density_market_noise_with_kernel, PriceDensityMarketNoiseInput,
440    PriceDensityMarketNoiseParams,
441};
442use crate::indicators::projection_oscillator::{
443    projection_oscillator_with_kernel, ProjectionOscillatorInput, ProjectionOscillatorParams,
444};
445use crate::indicators::psychological_line::{
446    psychological_line_with_kernel, PsychologicalLineInput, PsychologicalLineParams,
447};
448use crate::indicators::qqe::{qqe_with_kernel, QqeInput, QqeParams};
449use crate::indicators::qqe_weighted_oscillator::{
450    qqe_weighted_oscillator_with_kernel, QqeWeightedOscillatorInput, QqeWeightedOscillatorParams,
451};
452use crate::indicators::qstick::{qstick_with_kernel, QstickInput, QstickParams};
453use crate::indicators::random_walk_index::{
454    random_walk_index_with_kernel, RandomWalkIndexInput, RandomWalkIndexParams,
455};
456use crate::indicators::range_breakout_signals::{
457    range_breakout_signals_with_kernel, RangeBreakoutSignalsInput, RangeBreakoutSignalsParams,
458};
459use crate::indicators::range_filter::{
460    range_filter_with_kernel, RangeFilterInput, RangeFilterParams,
461};
462use crate::indicators::rank_correlation_index::{
463    rank_correlation_index_with_kernel, RankCorrelationIndexInput, RankCorrelationIndexParams,
464};
465use crate::indicators::market_structure_confluence::{
466    market_structure_confluence_with_kernel, MarketStructureConfluenceInput,
467    MarketStructureConfluenceParams,
468};
469use crate::indicators::range_filtered_trend_signals::{
470    range_filtered_trend_signals_with_kernel, RangeFilteredTrendSignalsInput,
471    RangeFilteredTrendSignalsParams,
472};
473use crate::indicators::range_oscillator::{
474    range_oscillator_with_kernel, RangeOscillatorInput, RangeOscillatorParams,
475};
476use crate::indicators::registry::{
477    get_indicator, IndicatorInfo, IndicatorInputKind, ParamValueStatic,
478};
479use crate::indicators::regression_slope_oscillator::{
480    regression_slope_oscillator_with_kernel, RegressionSlopeOscillatorInput,
481    RegressionSlopeOscillatorParams,
482};
483use crate::indicators::relative_strength_index_wave_indicator::{
484    relative_strength_index_wave_indicator_with_kernel, RelativeStrengthIndexWaveIndicatorInput,
485    RelativeStrengthIndexWaveIndicatorParams,
486};
487use crate::indicators::reversal_signals::{
488    reversal_signals_with_kernel, ReversalSignalsInput, ReversalSignalsParams,
489};
490use crate::indicators::reverse_rsi::{reverse_rsi_with_kernel, ReverseRsiInput, ReverseRsiParams};
491use crate::indicators::roc::{roc_with_kernel, RocInput, RocParams};
492use crate::indicators::rocp::{rocp_with_kernel, RocpInput, RocpParams};
493use crate::indicators::rocr::{rocr_with_kernel, RocrInput, RocrParams};
494use crate::indicators::rogers_satchell_volatility::{
495    rogers_satchell_volatility_with_kernel, RogersSatchellVolatilityInput,
496    RogersSatchellVolatilityParams,
497};
498use crate::indicators::rolling_skewness_kurtosis::{
499    rolling_skewness_kurtosis_with_kernel, RollingSkewnessKurtosisInput,
500    RollingSkewnessKurtosisParams,
501};
502use crate::indicators::rolling_z_score_trend::{
503    rolling_z_score_trend_with_kernel, RollingZScoreTrendInput, RollingZScoreTrendParams,
504};
505use crate::indicators::rsi::{rsi_with_kernel, RsiInput, RsiParams};
506use crate::indicators::rsmk::{rsmk_with_kernel, RsmkInput, RsmkParams};
507use crate::indicators::rvi::{rvi_with_kernel, RviInput, RviParams};
508use crate::indicators::safezonestop::{
509    safezonestop_with_kernel, SafeZoneStopInput, SafeZoneStopParams,
510};
511use crate::indicators::smooth_theil_sen::{
512    smooth_theil_sen_with_kernel, SmoothTheilSenDeviationType, SmoothTheilSenInput,
513    SmoothTheilSenParams, SmoothTheilSenStatStyle,
514};
515use crate::indicators::spearman_correlation::{
516    spearman_correlation_with_kernel, SpearmanCorrelationInput, SpearmanCorrelationParams,
517};
518use crate::indicators::squeeze_index::{
519    squeeze_index_with_kernel, SqueezeIndexInput, SqueezeIndexParams,
520};
521use crate::indicators::squeeze_momentum::{
522    squeeze_momentum_with_kernel, SqueezeMomentumInput, SqueezeMomentumParams,
523};
524use crate::indicators::smoothed_gaussian_trend_filter::{
525    smoothed_gaussian_trend_filter_with_kernel, SmoothedGaussianTrendFilterInput,
526    SmoothedGaussianTrendFilterParams,
527};
528use crate::indicators::srsi::{srsi_with_kernel, SrsiInput, SrsiParams};
529use crate::indicators::standardized_psar_oscillator::{
530    standardized_psar_oscillator_with_kernel, StandardizedPsarOscillatorInput,
531    StandardizedPsarOscillatorParams,
532};
533use crate::indicators::statistical_trailing_stop::{
534    statistical_trailing_stop_with_kernel, StatisticalTrailingStopInput,
535    StatisticalTrailingStopParams,
536};
537use crate::indicators::stc::{stc_with_kernel, StcInput, StcParams};
538use crate::indicators::stddev::{stddev_with_kernel, StdDevInput, StdDevParams};
539use crate::indicators::stoch::{stoch_with_kernel, StochInput, StochParams};
540use crate::indicators::stochastic_distance::{
541    stochastic_distance_with_kernel, StochasticDistanceInput, StochasticDistanceParams,
542};
543use crate::indicators::stochastic_money_flow_index::{
544    stochastic_money_flow_index_with_kernel, StochasticMoneyFlowIndexInput,
545    StochasticMoneyFlowIndexParams,
546};
547use crate::indicators::stochastic_adaptive_d::{
548    stochastic_adaptive_d_with_kernel, StochasticAdaptiveDInput, StochasticAdaptiveDParams,
549};
550use crate::indicators::stochastic_connors_rsi::{
551    stochastic_connors_rsi_with_kernel, StochasticConnorsRsiInput, StochasticConnorsRsiParams,
552};
553use crate::indicators::stochf::{stochf_with_kernel, StochfInput, StochfParams};
554use crate::indicators::supertrend::{supertrend_with_kernel, SuperTrendInput, SuperTrendParams};
555use crate::indicators::supertrend_oscillator::{
556    supertrend_oscillator_with_kernel, SuperTrendOscillatorInput, SuperTrendOscillatorParams,
557};
558use crate::indicators::supertrend_recovery::{
559    supertrend_recovery_with_kernel, SuperTrendRecoveryInput, SuperTrendRecoveryParams,
560};
561use crate::indicators::trend_trigger_factor::{
562    trend_trigger_factor_with_kernel, TrendTriggerFactorInput, TrendTriggerFactorParams,
563};
564use crate::indicators::trend_flow_trail::{
565    trend_flow_trail_with_kernel, TrendFlowTrailInput, TrendFlowTrailParams,
566};
567use crate::indicators::trend_direction_force_index::{
568    trend_direction_force_index_into_slice, TrendDirectionForceIndexInput,
569    TrendDirectionForceIndexParams,
570};
571use crate::indicators::trix::{
572    trix_batch_with_kernel, trix_into_slice, trix_with_kernel, TrixBatchRange, TrixInput,
573    TrixParams,
574};
575use crate::indicators::tsf::{tsf_with_kernel, TsfInput, TsfParams};
576use crate::indicators::tsi::{tsi_with_kernel, TsiInput, TsiParams};
577use crate::indicators::ttm_squeeze::{ttm_squeeze_with_kernel, TtmSqueezeInput, TtmSqueezeParams};
578use crate::indicators::ttm_trend::{ttm_trend_with_kernel, TtmTrendInput, TtmTrendParams};
579use crate::indicators::trend_continuation_factor::{
580    trend_continuation_factor_with_kernel, TrendContinuationFactorInput,
581    TrendContinuationFactorParams,
582};
583use crate::indicators::twiggs_money_flow::{
584    twiggs_money_flow_with_kernel, TwiggsMoneyFlowInput, TwiggsMoneyFlowParams,
585};
586use crate::indicators::ui::{ui_with_kernel, UiInput, UiParams};
587use crate::indicators::ultosc::{ultosc_with_kernel, UltOscInput, UltOscParams};
588use crate::indicators::var::{var_with_kernel, VarInput, VarParams};
589use crate::indicators::vdubus_divergence_wave_pattern_generator::{
590    vdubus_divergence_wave_pattern_generator_with_kernel,
591    VdubusDivergenceWavePatternGeneratorInput,
592    VdubusDivergenceWavePatternGeneratorParams,
593};
594use crate::indicators::velocity::{velocity_with_kernel, VelocityInput, VelocityParams};
595use crate::indicators::velocity_acceleration_convergence_divergence_indicator::{
596    velocity_acceleration_convergence_divergence_indicator_with_kernel,
597    VelocityAccelerationConvergenceDivergenceIndicatorInput,
598    VelocityAccelerationConvergenceDivergenceIndicatorParams,
599};
600use crate::indicators::velocity_acceleration_indicator::{
601    velocity_acceleration_indicator_with_kernel, VelocityAccelerationIndicatorInput,
602    VelocityAccelerationIndicatorParams,
603};
604use crate::indicators::vertical_horizontal_filter::{
605    vertical_horizontal_filter_with_kernel, VerticalHorizontalFilterInput,
606    VerticalHorizontalFilterParams,
607};
608use crate::indicators::vi::{vi_with_kernel, ViInput, ViParams};
609use crate::indicators::vidya::{vidya_with_kernel, VidyaInput, VidyaParams};
610use crate::indicators::vlma::{vlma_with_kernel, VlmaInput, VlmaParams};
611use crate::indicators::volatility_quality_index::{
612    volatility_quality_index_with_kernel, VolatilityQualityIndexInput, VolatilityQualityIndexParams,
613};
614use crate::indicators::volatility_ratio_adaptive_rsx::{
615    volatility_ratio_adaptive_rsx_with_kernel, VolatilityRatioAdaptiveRsxInput,
616    VolatilityRatioAdaptiveRsxParams,
617};
618use crate::indicators::volume_energy_reservoirs::{
619    volume_energy_reservoirs_with_kernel, VolumeEnergyReservoirsInput, VolumeEnergyReservoirsParams,
620};
621use crate::indicators::volume_weighted_rsi::{
622    volume_weighted_rsi_batch_with_kernel, volume_weighted_rsi_into_slice,
623    VolumeWeightedRsiBatchRange, VolumeWeightedRsiInput, VolumeWeightedRsiParams,
624};
625use crate::indicators::volume_weighted_relative_strength_index::{
626    volume_weighted_relative_strength_index_with_kernel, VolumeWeightedRelativeStrengthIndexInput,
627    VolumeWeightedRelativeStrengthIndexParams,
628};
629use crate::indicators::volume_weighted_stochastic_rsi::{
630    volume_weighted_stochastic_rsi_with_kernel, VolumeWeightedStochasticRsiInput,
631    VolumeWeightedStochasticRsiParams,
632};
633use crate::indicators::volume_zone_oscillator::{
634    volume_zone_oscillator_with_kernel, VolumeZoneOscillatorInput, VolumeZoneOscillatorParams,
635};
636use crate::indicators::vosc::{vosc_with_kernel, VoscInput, VoscParams};
637use crate::indicators::voss::{voss_with_kernel, VossInput, VossParams};
638use crate::indicators::vpci::{vpci_with_kernel, VpciInput, VpciParams};
639use crate::indicators::vpt::{vpt_with_kernel, VptInput};
640use crate::indicators::vwap_deviation_oscillator::{
641    vwap_deviation_oscillator_with_kernel, VwapDeviationMode, VwapDeviationOscillatorInput,
642    VwapDeviationOscillatorParams, VwapDeviationSessionMode,
643};
644use crate::indicators::vwap_zscore_with_signals::{
645    vwap_zscore_with_signals_with_kernel, VwapZscoreWithSignalsInput, VwapZscoreWithSignalsParams,
646};
647use crate::indicators::vwmacd::{vwmacd_with_kernel, VwmacdInput, VwmacdParams};
648use crate::indicators::wad::{wad_with_kernel, WadInput};
649use crate::indicators::wavetrend::{wavetrend_with_kernel, WavetrendInput, WavetrendParams};
650use crate::indicators::wclprice::{wclprice_with_kernel, WclpriceInput};
651use crate::indicators::willr::{willr_with_kernel, WillrInput, WillrParams};
652use crate::indicators::wto::{wto_with_kernel, WtoInput, WtoParams};
653use crate::indicators::yang_zhang_volatility::{
654    yang_zhang_volatility_with_kernel, YangZhangVolatilityInput, YangZhangVolatilityParams,
655};
656use crate::indicators::zig_zag_channels::{
657    zig_zag_channels_with_kernel, ZigZagChannelsInput, ZigZagChannelsParams,
658};
659use crate::indicators::zscore::{zscore_with_kernel, ZscoreInput, ZscoreParams};
660use crate::indicators::{cg::cg_with_kernel, cg::CgInput, cg::CgParams};
661use crate::utilities::data_loader::source_type;
662use crate::utilities::enums::Kernel;
663use std::collections::HashMap;
664use std::str::FromStr;
665
666pub fn compute_cpu_batch(
667    req: IndicatorBatchRequest<'_>,
668) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
669    compute_cpu_batch_internal(req, false)
670}
671
672pub fn compute_cpu_batch_strict(
673    req: IndicatorBatchRequest<'_>,
674) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
675    compute_cpu_batch_internal(req, true)
676}
677
678fn compute_cpu_batch_internal(
679    req: IndicatorBatchRequest<'_>,
680    strict_inputs: bool,
681) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
682    if !strict_inputs {
683        if let Some(out) = try_fast_dispatch_non_strict(req) {
684            return out;
685        }
686    }
687
688    let info = get_indicator(req.indicator_id);
689
690    if let Some(info) = info {
691        if strict_inputs {
692            validate_input_kind_strict(info.id, info.input_kind, req.data)?;
693        }
694
695        let output_id = resolve_output_id(info, req.output_id)?;
696
697        if info.id.eq_ignore_ascii_case("logarithmic_moving_average") {
698            return compute_logarithmic_moving_average_batch(req, output_id);
699        }
700
701        if is_moving_average(info.id) {
702            return compute_ma_batch(req, info, output_id);
703        }
704
705        return dispatch_cpu_batch_by_indicator(req, info.id, output_id);
706    }
707
708    let output_id = req.output_id.unwrap_or("value");
709    match dispatch_cpu_batch_by_indicator(req, req.indicator_id, output_id) {
710        Err(IndicatorDispatchError::UnsupportedCapability { .. }) => {
711            Err(IndicatorDispatchError::UnknownIndicator {
712                id: req.indicator_id.to_string(),
713            })
714        }
715        other => other,
716    }
717}
718
719fn try_fast_dispatch_non_strict(
720    req: IndicatorBatchRequest<'_>,
721) -> Option<Result<IndicatorBatchOutput, IndicatorDispatchError>> {
722    let id = req.indicator_id;
723    let output_id = req.output_id;
724
725    if !id.as_bytes().iter().any(|b| b.is_ascii_uppercase()) {
726        return match id {
727            "bop" => Some(compute_bop_batch(req, output_id.unwrap_or("value"))),
728            "dpo" => Some(compute_dpo_batch(req, output_id.unwrap_or("value"))),
729            "cmo" => Some(compute_cmo_batch(req, output_id.unwrap_or("value"))),
730            "fosc" => Some(compute_fosc_batch(req, output_id.unwrap_or("value"))),
731            "emv" => Some(compute_emv_batch(req, output_id.unwrap_or("value"))),
732            "cci_cycle" => Some(compute_cci_cycle_batch(req, output_id.unwrap_or("value"))),
733            "cfo" => Some(compute_cfo_batch(req, output_id.unwrap_or("value"))),
734            "ehlers_adaptive_cg" => Some(compute_ehlers_adaptive_cg_batch(
735                req,
736                output_id.unwrap_or("cg"),
737            )),
738            "adaptive_momentum_oscillator" => Some(compute_adaptive_momentum_oscillator_batch(
739                req,
740                output_id.unwrap_or("amo"),
741            )),
742            "lrsi" => Some(compute_lrsi_batch(req, output_id.unwrap_or("value"))),
743            "nvi" => Some(compute_nvi_batch(req, output_id.unwrap_or("value"))),
744            "mom" => Some(compute_mom_batch(req, output_id.unwrap_or("value"))),
745            "velocity" => Some(compute_velocity_batch(req, output_id.unwrap_or("value"))),
746            "normalized_volume_true_range" => Some(compute_normalized_volume_true_range_batch(
747                req,
748                output_id.unwrap_or("normalized_volume"),
749            )),
750            "exponential_trend" => Some(compute_exponential_trend_batch(
751                req,
752                output_id.unwrap_or("uptrend_base"),
753            )),
754            "trend_flow_trail" => Some(compute_trend_flow_trail_batch(
755                req,
756                output_id.unwrap_or("alpha_trail"),
757            )),
758            "range_breakout_signals" => Some(compute_range_breakout_signals_batch(
759                req,
760                output_id.unwrap_or("range_top"),
761            )),
762            "vi" => {
763                if let Some(out) = output_id {
764                    Some(compute_vi_batch(req, out))
765                } else {
766                    None
767                }
768            }
769            "wto" => {
770                if let Some(out) = output_id {
771                    Some(compute_wto_batch(req, out))
772                } else {
773                    None
774                }
775            }
776            "rogers_satchell_volatility" => {
777                if let Some(out) = output_id {
778                    Some(compute_rogers_satchell_volatility_batch(req, out))
779                } else {
780                    None
781                }
782            }
783            "historical_volatility_rank" => {
784                if let Some(out) = output_id {
785                    Some(compute_historical_volatility_rank_batch(req, out))
786                } else {
787                    None
788                }
789            }
790            "dual_ulcer_index" => {
791                if let Some(out) = output_id {
792                    Some(compute_dual_ulcer_index_batch(req, out))
793                } else {
794                    None
795                }
796            }
797            "fractal_dimension_index" => {
798                if let Some(out) = output_id {
799                    Some(compute_fractal_dimension_index_batch(req, out))
800                } else {
801                    None
802                }
803            }
804            "volume_weighted_rsi" => {
805                if let Some(out) = output_id {
806                    Some(compute_volume_weighted_rsi_batch(req, out))
807                } else {
808                    None
809                }
810            }
811            "dynamic_momentum_index" => {
812                if let Some(out) = output_id {
813                    Some(compute_dynamic_momentum_index_batch(req, out))
814                } else {
815                    None
816                }
817            }
818            "disparity_index" => {
819                if let Some(out) = output_id {
820                    Some(compute_disparity_index_batch(req, out))
821                } else {
822                    None
823                }
824            }
825            "donchian_channel_width" => {
826                if let Some(out) = output_id {
827                    Some(compute_donchian_channel_width_batch(req, out))
828                } else {
829                    None
830                }
831            }
832            "kairi_relative_index" => {
833                if let Some(out) = output_id {
834                    Some(compute_kairi_relative_index_batch(req, out))
835                } else {
836                    None
837                }
838            }
839            "projection_oscillator" => {
840                if let Some(out) = output_id {
841                    Some(compute_projection_oscillator_batch(req, out))
842                } else {
843                    None
844                }
845            }
846            "market_structure_trailing_stop" => {
847                if let Some(out) = output_id {
848                    Some(compute_market_structure_trailing_stop_batch(req, out))
849                } else {
850                    None
851                }
852            }
853            "emd_trend" => {
854                if let Some(out) = output_id {
855                    Some(compute_emd_trend_batch(req, out))
856                } else {
857                    None
858                }
859            }
860            "cyberpunk_value_trend_analyzer" => {
861                if let Some(out) = output_id {
862                    Some(compute_cyberpunk_value_trend_analyzer_batch(req, out))
863                } else {
864                    None
865                }
866            }
867            "evasive_supertrend" => {
868                if let Some(out) = output_id {
869                    Some(compute_evasive_supertrend_batch(req, out))
870                } else {
871                    None
872                }
873            }
874            "reversal_signals" => {
875                if let Some(out) = output_id {
876                    Some(compute_reversal_signals_batch(req, out))
877                } else {
878                    None
879                }
880            }
881            "zig_zag_channels" => {
882                if let Some(out) = output_id {
883                    Some(compute_zig_zag_channels_batch(req, out))
884                } else {
885                    None
886                }
887            }
888            "directional_imbalance_index" => {
889                if let Some(out) = output_id {
890                    Some(compute_directional_imbalance_index_batch(req, out))
891                } else {
892                    None
893                }
894            }
895            "candle_strength_oscillator" => {
896                if let Some(out) = output_id {
897                    Some(compute_candle_strength_oscillator_batch(req, out))
898                } else {
899                    None
900                }
901            }
902            "gmma_oscillator" => {
903                if let Some(out) = output_id {
904                    Some(compute_gmma_oscillator_batch(req, out))
905                } else {
906                    None
907                }
908            }
909            "nonlinear_regression_zero_lag_moving_average" => {
910                if let Some(out) = output_id {
911                    Some(compute_nonlinear_regression_zero_lag_moving_average_batch(
912                        req, out,
913                    ))
914                } else {
915                    None
916                }
917            }
918            "possible_rsi" => {
919                if let Some(out) = output_id {
920                    Some(compute_possible_rsi_batch(req, out))
921                } else {
922                    None
923                }
924            }
925            "autocorrelation_indicator" => {
926                if let Some(out) = output_id {
927                    Some(compute_autocorrelation_indicator_batch(req, out))
928                } else {
929                    None
930                }
931            }
932            "goertzel_cycle_composite_wave" => {
933                if let Some(out) = output_id {
934                    Some(compute_goertzel_cycle_composite_wave_batch(req, out))
935                } else {
936                    None
937                }
938            }
939            "rolling_skewness_kurtosis" => {
940                if let Some(out) = output_id {
941                    Some(compute_rolling_skewness_kurtosis_batch(req, out))
942                } else {
943                    None
944                }
945            }
946            "rolling_z_score_trend" => {
947                if let Some(out) = output_id {
948                    Some(compute_rolling_z_score_trend_batch(req, out))
949                } else {
950                    None
951                }
952            }
953            "ehlers_data_sampling_relative_strength_indicator" => {
954                if let Some(out) = output_id {
955                    Some(compute_ehlers_data_sampling_relative_strength_indicator_batch(req, out))
956                } else {
957                    None
958                }
959            }
960            "velocity_acceleration_convergence_divergence_indicator" => {
961                if let Some(out) = output_id {
962                    Some(
963                        compute_velocity_acceleration_convergence_divergence_indicator_batch(
964                            req, out,
965                        ),
966                    )
967                } else {
968                    None
969                }
970            }
971            "trend_direction_force_index" => {
972                if let Some(out) = output_id {
973                    Some(compute_trend_direction_force_index_batch(req, out))
974                } else {
975                    None
976                }
977            }
978            "yang_zhang_volatility" => {
979                if let Some(out) = output_id {
980                    Some(compute_yang_zhang_volatility_batch(req, out))
981                } else {
982                    None
983                }
984            }
985            "garman_klass_volatility" => Some(compute_garman_klass_volatility_batch(
986                req,
987                output_id.unwrap_or("value"),
988            )),
989            "advance_decline_line" => Some(compute_advance_decline_line_batch(
990                req,
991                output_id.unwrap_or("value"),
992            )),
993            "decisionpoint_breadth_swenlin_trading_oscillator" => Some(
994                compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(
995                    req,
996                    output_id.unwrap_or("value"),
997                ),
998            ),
999            "velocity_acceleration_indicator" => Some(
1000                compute_velocity_acceleration_indicator_batch(req, output_id.unwrap_or("value")),
1001            ),
1002            "normalized_resonator" => Some(compute_normalized_resonator_batch(
1003                req,
1004                output_id.unwrap_or("oscillator"),
1005            )),
1006            "monotonicity_index" => Some(compute_monotonicity_index_batch(
1007                req,
1008                output_id.unwrap_or("index"),
1009            )),
1010            "half_causal_estimator" => Some(compute_half_causal_estimator_batch(
1011                req,
1012                output_id.unwrap_or("estimate"),
1013            )),
1014            "atr_percentile" => Some(compute_atr_percentile_batch(
1015                req,
1016                output_id.unwrap_or("value"),
1017            )),
1018            "bull_power_vs_bear_power" => Some(compute_bull_power_vs_bear_power_batch(
1019                req,
1020                output_id.unwrap_or("value"),
1021            )),
1022            "didi_index" => Some(compute_didi_index_batch(req, output_id.unwrap_or("short"))),
1023            "ehlers_autocorrelation_periodogram" => {
1024                Some(compute_ehlers_autocorrelation_periodogram_batch(
1025                    req,
1026                    output_id.unwrap_or("dominant_cycle"),
1027                ))
1028            }
1029            "ehlers_linear_extrapolation_predictor" => {
1030                Some(compute_ehlers_linear_extrapolation_predictor_batch(
1031                    req,
1032                    output_id.unwrap_or("prediction"),
1033                ))
1034            }
1035            "kase_peak_oscillator_with_divergences" => {
1036                Some(compute_kase_peak_oscillator_with_divergences_batch(
1037                    req,
1038                    output_id.unwrap_or("oscillator"),
1039                ))
1040            }
1041            "absolute_strength_index_oscillator" => {
1042                Some(compute_absolute_strength_index_oscillator_batch(
1043                    req,
1044                    output_id.unwrap_or("oscillator"),
1045                ))
1046            }
1047            "adaptive_bandpass_trigger_oscillator" => {
1048                Some(compute_adaptive_bandpass_trigger_oscillator_batch(
1049                    req,
1050                    output_id.unwrap_or("in_phase"),
1051                ))
1052            }
1053            "premier_rsi_oscillator" => Some(compute_premier_rsi_oscillator_batch(
1054                req,
1055                output_id.unwrap_or("value"),
1056            )),
1057            "multi_length_stochastic_average" => Some(
1058                compute_multi_length_stochastic_average_batch(req, output_id.unwrap_or("value")),
1059            ),
1060            "hull_butterfly_oscillator" => Some(compute_hull_butterfly_oscillator_batch(
1061                req,
1062                output_id.unwrap_or("oscillator"),
1063            )),
1064            "fibonacci_trailing_stop" => Some(compute_fibonacci_trailing_stop_batch(
1065                req,
1066                output_id.unwrap_or("trailing_stop"),
1067            )),
1068            "fibonacci_entry_bands" => Some(compute_fibonacci_entry_bands_batch(
1069                req,
1070                output_id.unwrap_or("middle"),
1071            )),
1072            "volume_energy_reservoirs" => Some(compute_volume_energy_reservoirs_batch(
1073                req,
1074                output_id.unwrap_or("momentum"),
1075            )),
1076            "neighboring_trailing_stop" => Some(compute_neighboring_trailing_stop_batch(
1077                req,
1078                output_id.unwrap_or("trailing_stop"),
1079            )),
1080            "grover_llorens_cycle_oscillator" => Some(
1081                compute_grover_llorens_cycle_oscillator_batch(req, output_id.unwrap_or("value")),
1082            ),
1083            "historical_volatility" => Some(compute_historical_volatility_batch(
1084                req,
1085                output_id.unwrap_or("value"),
1086            )),
1087            "squeeze_index" => Some(compute_squeeze_index_batch(
1088                req,
1089                output_id.unwrap_or("value"),
1090            )),
1091            "stochastic_distance" => Some(compute_stochastic_distance_batch(
1092                req,
1093                output_id.unwrap_or("oscillator"),
1094            )),
1095            "vertical_horizontal_filter" => Some(compute_vertical_horizontal_filter_batch(
1096                req,
1097                output_id.unwrap_or("value"),
1098            )),
1099            "intraday_momentum_index" => {
1100                if let Some(out) = output_id {
1101                    Some(compute_intraday_momentum_index_batch(req, out))
1102                } else {
1103                    None
1104                }
1105            }
1106            "vwap_zscore_with_signals" => {
1107                if let Some(out) = output_id {
1108                    Some(compute_vwap_zscore_with_signals_batch(req, out))
1109                } else {
1110                    None
1111                }
1112            }
1113            "macd_wave_signal_pro" => {
1114                if let Some(out) = output_id {
1115                    Some(compute_macd_wave_signal_pro_batch(req, out))
1116                } else {
1117                    None
1118                }
1119            }
1120            "hema_trend_levels" => {
1121                if let Some(out) = output_id {
1122                    Some(compute_hema_trend_levels_batch(req, out))
1123                } else {
1124                    None
1125                }
1126            }
1127            "demand_index" => {
1128                if let Some(out) = output_id {
1129                    Some(compute_demand_index_batch(req, out))
1130                } else {
1131                    None
1132                }
1133            }
1134            "gopalakrishnan_range_index" => Some(compute_gopalakrishnan_range_index_batch(
1135                req,
1136                output_id.unwrap_or("value"),
1137            )),
1138            "voss" => {
1139                if let Some(out) = output_id {
1140                    Some(compute_voss_batch(req, out))
1141                } else {
1142                    None
1143                }
1144            }
1145            "acosc" => {
1146                if let Some(out) = output_id {
1147                    Some(compute_acosc_batch(req, out))
1148                } else {
1149                    None
1150                }
1151            }
1152            _ => None,
1153        };
1154    }
1155
1156    if id.eq_ignore_ascii_case("bop") {
1157        return Some(compute_bop_batch(req, output_id.unwrap_or("value")));
1158    }
1159    if id.eq_ignore_ascii_case("dpo") {
1160        return Some(compute_dpo_batch(req, output_id.unwrap_or("value")));
1161    }
1162    if id.eq_ignore_ascii_case("cmo") {
1163        return Some(compute_cmo_batch(req, output_id.unwrap_or("value")));
1164    }
1165    if id.eq_ignore_ascii_case("fosc") {
1166        return Some(compute_fosc_batch(req, output_id.unwrap_or("value")));
1167    }
1168    if id.eq_ignore_ascii_case("emv") {
1169        return Some(compute_emv_batch(req, output_id.unwrap_or("value")));
1170    }
1171    if id.eq_ignore_ascii_case("cfo") {
1172        return Some(compute_cfo_batch(req, output_id.unwrap_or("value")));
1173    }
1174    if id.eq_ignore_ascii_case("ehlers_adaptive_cg") {
1175        return Some(compute_ehlers_adaptive_cg_batch(
1176            req,
1177            output_id.unwrap_or("cg"),
1178        ));
1179    }
1180    if id.eq_ignore_ascii_case("adaptive_momentum_oscillator") {
1181        return Some(compute_adaptive_momentum_oscillator_batch(
1182            req,
1183            output_id.unwrap_or("amo"),
1184        ));
1185    }
1186    if id.eq_ignore_ascii_case("adaptive_macd") {
1187        return Some(compute_adaptive_macd_batch(
1188            req,
1189            output_id.unwrap_or("macd"),
1190        ));
1191    }
1192    if id.eq_ignore_ascii_case("linear_correlation_oscillator") {
1193        return Some(compute_linear_correlation_oscillator_batch(
1194            req,
1195            output_id.unwrap_or("value"),
1196        ));
1197    }
1198    if id.eq_ignore_ascii_case("polynomial_regression_extrapolation") {
1199        return Some(compute_polynomial_regression_extrapolation_batch(
1200            req,
1201            output_id.unwrap_or("value"),
1202        ));
1203    }
1204    if id.eq_ignore_ascii_case("statistical_trailing_stop") {
1205        return Some(compute_statistical_trailing_stop_batch(
1206            req,
1207            output_id.unwrap_or("level"),
1208        ));
1209    }
1210    if id.eq_ignore_ascii_case("supertrend_recovery") {
1211        return Some(compute_supertrend_recovery_batch(
1212            req,
1213            output_id.unwrap_or("band"),
1214        ));
1215    }
1216    if id.eq_ignore_ascii_case("standardized_psar_oscillator") {
1217        return Some(compute_standardized_psar_oscillator_batch(
1218            req,
1219            output_id.unwrap_or("oscillator"),
1220        ));
1221    }
1222    if id.eq_ignore_ascii_case("geometric_bias_oscillator") {
1223        return Some(compute_geometric_bias_oscillator_batch(
1224            req,
1225            output_id.unwrap_or("value"),
1226        ));
1227    }
1228    if id.eq_ignore_ascii_case("lrsi") {
1229        return Some(compute_lrsi_batch(req, output_id.unwrap_or("value")));
1230    }
1231    if id.eq_ignore_ascii_case("nvi") {
1232        return Some(compute_nvi_batch(req, output_id.unwrap_or("value")));
1233    }
1234    if id.eq_ignore_ascii_case("mom") {
1235        return Some(compute_mom_batch(req, output_id.unwrap_or("value")));
1236    }
1237    if id.eq_ignore_ascii_case("velocity") {
1238        return Some(compute_velocity_batch(req, output_id.unwrap_or("value")));
1239    }
1240    if id.eq_ignore_ascii_case("normalized_volume_true_range") {
1241        return Some(compute_normalized_volume_true_range_batch(
1242            req,
1243            output_id.unwrap_or("normalized_volume"),
1244        ));
1245    }
1246    if id.eq_ignore_ascii_case("exponential_trend") {
1247        return Some(compute_exponential_trend_batch(
1248            req,
1249            output_id.unwrap_or("uptrend_base"),
1250        ));
1251    }
1252    if id.eq_ignore_ascii_case("trend_flow_trail") {
1253        return Some(compute_trend_flow_trail_batch(
1254            req,
1255            output_id.unwrap_or("alpha_trail"),
1256        ));
1257    }
1258    if id.eq_ignore_ascii_case("range_breakout_signals") {
1259        return Some(compute_range_breakout_signals_batch(
1260            req,
1261            output_id.unwrap_or("range_top"),
1262        ));
1263    }
1264    if id.eq_ignore_ascii_case("vi") {
1265        if let Some(out) = output_id {
1266            return Some(compute_vi_batch(req, out));
1267        }
1268        return None;
1269    }
1270    if id.eq_ignore_ascii_case("wto") {
1271        if let Some(out) = output_id {
1272            return Some(compute_wto_batch(req, out));
1273        }
1274        return None;
1275    }
1276    if id.eq_ignore_ascii_case("rogers_satchell_volatility") {
1277        if let Some(out) = output_id {
1278            return Some(compute_rogers_satchell_volatility_batch(req, out));
1279        }
1280        return None;
1281    }
1282    if id.eq_ignore_ascii_case("historical_volatility_rank") {
1283        if let Some(out) = output_id {
1284            return Some(compute_historical_volatility_rank_batch(req, out));
1285        }
1286        return None;
1287    }
1288    if id.eq_ignore_ascii_case("dual_ulcer_index") {
1289        if let Some(out) = output_id {
1290            return Some(compute_dual_ulcer_index_batch(req, out));
1291        }
1292        return None;
1293    }
1294    if id.eq_ignore_ascii_case("fractal_dimension_index") {
1295        if let Some(out) = output_id {
1296            return Some(compute_fractal_dimension_index_batch(req, out));
1297        }
1298        return None;
1299    }
1300    if id.eq_ignore_ascii_case("volume_weighted_rsi") {
1301        if let Some(out) = output_id {
1302            return Some(compute_volume_weighted_rsi_batch(req, out));
1303        }
1304        return None;
1305    }
1306    if id.eq_ignore_ascii_case("dynamic_momentum_index") {
1307        if let Some(out) = output_id {
1308            return Some(compute_dynamic_momentum_index_batch(req, out));
1309        }
1310        return None;
1311    }
1312    if id.eq_ignore_ascii_case("disparity_index") {
1313        if let Some(out) = output_id {
1314            return Some(compute_disparity_index_batch(req, out));
1315        }
1316        return None;
1317    }
1318    if id.eq_ignore_ascii_case("donchian_channel_width") {
1319        if let Some(out) = output_id {
1320            return Some(compute_donchian_channel_width_batch(req, out));
1321        }
1322        return None;
1323    }
1324    if id.eq_ignore_ascii_case("kairi_relative_index") {
1325        if let Some(out) = output_id {
1326            return Some(compute_kairi_relative_index_batch(req, out));
1327        }
1328        return None;
1329    }
1330    if id.eq_ignore_ascii_case("projection_oscillator") {
1331        if let Some(out) = output_id {
1332            return Some(compute_projection_oscillator_batch(req, out));
1333        }
1334        return None;
1335    }
1336    if id.eq_ignore_ascii_case("market_structure_trailing_stop") {
1337        if let Some(out) = output_id {
1338            return Some(compute_market_structure_trailing_stop_batch(req, out));
1339        }
1340        return None;
1341    }
1342    if id.eq_ignore_ascii_case("emd_trend") {
1343        if let Some(out) = output_id {
1344            return Some(compute_emd_trend_batch(req, out));
1345        }
1346        return None;
1347    }
1348    if id.eq_ignore_ascii_case("cyberpunk_value_trend_analyzer") {
1349        if let Some(out) = output_id {
1350            return Some(compute_cyberpunk_value_trend_analyzer_batch(req, out));
1351        }
1352        return None;
1353    }
1354    if id.eq_ignore_ascii_case("evasive_supertrend") {
1355        if let Some(out) = output_id {
1356            return Some(compute_evasive_supertrend_batch(req, out));
1357        }
1358        return None;
1359    }
1360    if id.eq_ignore_ascii_case("reversal_signals") {
1361        if let Some(out) = output_id {
1362            return Some(compute_reversal_signals_batch(req, out));
1363        }
1364        return None;
1365    }
1366    if id.eq_ignore_ascii_case("zig_zag_channels") {
1367        if let Some(out) = output_id {
1368            return Some(compute_zig_zag_channels_batch(req, out));
1369        }
1370        return None;
1371    }
1372    if id.eq_ignore_ascii_case("directional_imbalance_index") {
1373        if let Some(out) = output_id {
1374            return Some(compute_directional_imbalance_index_batch(req, out));
1375        }
1376        return None;
1377    }
1378    if id.eq_ignore_ascii_case("candle_strength_oscillator") {
1379        if let Some(out) = output_id {
1380            return Some(compute_candle_strength_oscillator_batch(req, out));
1381        }
1382        return None;
1383    }
1384    if id.eq_ignore_ascii_case("gmma_oscillator") {
1385        if let Some(out) = output_id {
1386            return Some(compute_gmma_oscillator_batch(req, out));
1387        }
1388        return None;
1389    }
1390    if id.eq_ignore_ascii_case("nonlinear_regression_zero_lag_moving_average") {
1391        if let Some(out) = output_id {
1392            return Some(compute_nonlinear_regression_zero_lag_moving_average_batch(
1393                req, out,
1394            ));
1395        }
1396        return None;
1397    }
1398    if id.eq_ignore_ascii_case("autocorrelation_indicator") {
1399        if let Some(out) = output_id {
1400            return Some(compute_autocorrelation_indicator_batch(req, out));
1401        }
1402        return None;
1403    }
1404    if id.eq_ignore_ascii_case("goertzel_cycle_composite_wave") {
1405        if let Some(out) = output_id {
1406            return Some(compute_goertzel_cycle_composite_wave_batch(req, out));
1407        }
1408        return None;
1409    }
1410    if id.eq_ignore_ascii_case("rolling_skewness_kurtosis") {
1411        if let Some(out) = output_id {
1412            return Some(compute_rolling_skewness_kurtosis_batch(req, out));
1413        }
1414        return None;
1415    }
1416    if id.eq_ignore_ascii_case("rolling_z_score_trend") {
1417        if let Some(out) = output_id {
1418            return Some(compute_rolling_z_score_trend_batch(req, out));
1419        }
1420        return None;
1421    }
1422    if id.eq_ignore_ascii_case("ehlers_data_sampling_relative_strength_indicator") {
1423        if let Some(out) = output_id {
1424            return Some(compute_ehlers_data_sampling_relative_strength_indicator_batch(req, out));
1425        }
1426        return None;
1427    }
1428    if id.eq_ignore_ascii_case("velocity_acceleration_convergence_divergence_indicator") {
1429        if let Some(out) = output_id {
1430            return Some(
1431                compute_velocity_acceleration_convergence_divergence_indicator_batch(req, out),
1432            );
1433        }
1434        return None;
1435    }
1436    if id.eq_ignore_ascii_case("trend_direction_force_index") {
1437        if let Some(out) = output_id {
1438            return Some(compute_trend_direction_force_index_batch(req, out));
1439        }
1440        return None;
1441    }
1442    if id.eq_ignore_ascii_case("yang_zhang_volatility") {
1443        if let Some(out) = output_id {
1444            return Some(compute_yang_zhang_volatility_batch(req, out));
1445        }
1446        return None;
1447    }
1448    if id.eq_ignore_ascii_case("garman_klass_volatility") {
1449        return Some(compute_garman_klass_volatility_batch(
1450            req,
1451            output_id.unwrap_or("value"),
1452        ));
1453    }
1454    if id.eq_ignore_ascii_case("advance_decline_line") {
1455        return Some(compute_advance_decline_line_batch(
1456            req,
1457            output_id.unwrap_or("value"),
1458        ));
1459    }
1460    if id.eq_ignore_ascii_case("decisionpoint_breadth_swenlin_trading_oscillator") {
1461        return Some(
1462            compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(
1463                req,
1464                output_id.unwrap_or("value"),
1465            ),
1466        );
1467    }
1468    if id.eq_ignore_ascii_case("velocity_acceleration_indicator") {
1469        return Some(compute_velocity_acceleration_indicator_batch(
1470            req,
1471            output_id.unwrap_or("value"),
1472        ));
1473    }
1474    if id.eq_ignore_ascii_case("normalized_resonator") {
1475        return Some(compute_normalized_resonator_batch(
1476            req,
1477            output_id.unwrap_or("oscillator"),
1478        ));
1479    }
1480    if id.eq_ignore_ascii_case("monotonicity_index") {
1481        return Some(compute_monotonicity_index_batch(
1482            req,
1483            output_id.unwrap_or("index"),
1484        ));
1485    }
1486    if id.eq_ignore_ascii_case("half_causal_estimator") {
1487        return Some(compute_half_causal_estimator_batch(
1488            req,
1489            output_id.unwrap_or("estimate"),
1490        ));
1491    }
1492    if id.eq_ignore_ascii_case("atr_percentile") {
1493        return Some(compute_atr_percentile_batch(
1494            req,
1495            output_id.unwrap_or("value"),
1496        ));
1497    }
1498    if id.eq_ignore_ascii_case("bull_power_vs_bear_power") {
1499        return Some(compute_bull_power_vs_bear_power_batch(
1500            req,
1501            output_id.unwrap_or("value"),
1502        ));
1503    }
1504    if id.eq_ignore_ascii_case("didi_index") {
1505        return Some(compute_didi_index_batch(req, output_id.unwrap_or("short")));
1506    }
1507    if id.eq_ignore_ascii_case("ehlers_autocorrelation_periodogram") {
1508        return Some(compute_ehlers_autocorrelation_periodogram_batch(
1509            req,
1510            output_id.unwrap_or("dominant_cycle"),
1511        ));
1512    }
1513    if id.eq_ignore_ascii_case("ehlers_linear_extrapolation_predictor") {
1514        return Some(compute_ehlers_linear_extrapolation_predictor_batch(
1515            req,
1516            output_id.unwrap_or("prediction"),
1517        ));
1518    }
1519    if id.eq_ignore_ascii_case("kase_peak_oscillator_with_divergences") {
1520        return Some(compute_kase_peak_oscillator_with_divergences_batch(
1521            req,
1522            output_id.unwrap_or("oscillator"),
1523        ));
1524    }
1525    if id.eq_ignore_ascii_case("absolute_strength_index_oscillator") {
1526        return Some(compute_absolute_strength_index_oscillator_batch(
1527            req,
1528            output_id.unwrap_or("oscillator"),
1529        ));
1530    }
1531    if id.eq_ignore_ascii_case("adaptive_bandpass_trigger_oscillator") {
1532        return Some(compute_adaptive_bandpass_trigger_oscillator_batch(
1533            req,
1534            output_id.unwrap_or("in_phase"),
1535        ));
1536    }
1537    if id.eq_ignore_ascii_case("premier_rsi_oscillator") {
1538        return Some(compute_premier_rsi_oscillator_batch(
1539            req,
1540            output_id.unwrap_or("value"),
1541        ));
1542    }
1543    if id.eq_ignore_ascii_case("multi_length_stochastic_average") {
1544        return Some(compute_multi_length_stochastic_average_batch(
1545            req,
1546            output_id.unwrap_or("value"),
1547        ));
1548    }
1549    if id.eq_ignore_ascii_case("hull_butterfly_oscillator") {
1550        return Some(compute_hull_butterfly_oscillator_batch(
1551            req,
1552            output_id.unwrap_or("oscillator"),
1553        ));
1554    }
1555    if id.eq_ignore_ascii_case("fibonacci_trailing_stop") {
1556        return Some(compute_fibonacci_trailing_stop_batch(
1557            req,
1558            output_id.unwrap_or("trailing_stop"),
1559        ));
1560    }
1561    if id.eq_ignore_ascii_case("fibonacci_entry_bands") {
1562        return Some(compute_fibonacci_entry_bands_batch(
1563            req,
1564            output_id.unwrap_or("middle"),
1565        ));
1566    }
1567    if id.eq_ignore_ascii_case("volume_energy_reservoirs") {
1568        return Some(compute_volume_energy_reservoirs_batch(
1569            req,
1570            output_id.unwrap_or("momentum"),
1571        ));
1572    }
1573    if id.eq_ignore_ascii_case("neighboring_trailing_stop") {
1574        return Some(compute_neighboring_trailing_stop_batch(
1575            req,
1576            output_id.unwrap_or("trailing_stop"),
1577        ));
1578    }
1579    if id.eq_ignore_ascii_case("grover_llorens_cycle_oscillator") {
1580        return Some(compute_grover_llorens_cycle_oscillator_batch(
1581            req,
1582            output_id.unwrap_or("value"),
1583        ));
1584    }
1585    if id.eq_ignore_ascii_case("historical_volatility") {
1586        return Some(compute_historical_volatility_batch(
1587            req,
1588            output_id.unwrap_or("value"),
1589        ));
1590    }
1591    if id.eq_ignore_ascii_case("squeeze_index") {
1592        return Some(compute_squeeze_index_batch(
1593            req,
1594            output_id.unwrap_or("value"),
1595        ));
1596    }
1597    if id.eq_ignore_ascii_case("stochastic_distance") {
1598        return Some(compute_stochastic_distance_batch(
1599            req,
1600            output_id.unwrap_or("oscillator"),
1601        ));
1602    }
1603    if id.eq_ignore_ascii_case("vertical_horizontal_filter") {
1604        return Some(compute_vertical_horizontal_filter_batch(
1605            req,
1606            output_id.unwrap_or("value"),
1607        ));
1608    }
1609    if id.eq_ignore_ascii_case("intraday_momentum_index") {
1610        if let Some(out) = output_id {
1611            return Some(compute_intraday_momentum_index_batch(req, out));
1612        }
1613    }
1614    if id.eq_ignore_ascii_case("vwap_zscore_with_signals") {
1615        if let Some(out) = output_id {
1616            return Some(compute_vwap_zscore_with_signals_batch(req, out));
1617        }
1618    }
1619    if id.eq_ignore_ascii_case("macd_wave_signal_pro") {
1620        if let Some(out) = output_id {
1621            return Some(compute_macd_wave_signal_pro_batch(req, out));
1622        }
1623    }
1624    if id.eq_ignore_ascii_case("hema_trend_levels") {
1625        if let Some(out) = output_id {
1626            return Some(compute_hema_trend_levels_batch(req, out));
1627        }
1628    }
1629    if id.eq_ignore_ascii_case("demand_index") {
1630        if let Some(out) = output_id {
1631            return Some(compute_demand_index_batch(req, out));
1632        }
1633    }
1634    if id.eq_ignore_ascii_case("gopalakrishnan_range_index") {
1635        return Some(compute_gopalakrishnan_range_index_batch(
1636            req,
1637            output_id.unwrap_or("value"),
1638        ));
1639    }
1640    if id.eq_ignore_ascii_case("voss") {
1641        if let Some(out) = output_id {
1642            return Some(compute_voss_batch(req, out));
1643        }
1644        return None;
1645    }
1646    if id.eq_ignore_ascii_case("acosc") {
1647        if let Some(out) = output_id {
1648            return Some(compute_acosc_batch(req, out));
1649        }
1650        return None;
1651    }
1652
1653    None
1654}
1655
1656fn dispatch_cpu_batch_by_indicator(
1657    req: IndicatorBatchRequest<'_>,
1658    indicator_id: &str,
1659    output_id: &str,
1660) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1661    if indicator_id.eq_ignore_ascii_case("logarithmic_moving_average") {
1662        return compute_logarithmic_moving_average_batch(req, output_id);
1663    }
1664    if is_moving_average(indicator_id) {
1665        if let Some(info) = get_indicator(indicator_id) {
1666            return compute_ma_batch(req, info, output_id);
1667        }
1668    }
1669    match indicator_id {
1670        "accumulation_swing_index" => compute_accumulation_swing_index_batch(req, output_id),
1671        "ad" => compute_ad_batch(req, output_id),
1672        "adosc" => compute_adosc_batch(req, output_id),
1673        "ao" => compute_ao_batch(req, output_id),
1674        "emv" => compute_emv_batch(req, output_id),
1675        "efi" => compute_efi_batch(req, output_id),
1676        "mfi" => compute_mfi_batch(req, output_id),
1677        "mass" => compute_mass_batch(req, output_id),
1678        "kvo" => compute_kvo_batch(req, output_id),
1679        "vosc" => compute_vosc_batch(req, output_id),
1680        "wad" => compute_wad_batch(req, output_id),
1681        "dx" => compute_dx_batch(req, output_id),
1682        "fosc" => compute_fosc_batch(req, output_id),
1683        "ift_rsi" => compute_ift_rsi_batch(req, output_id),
1684        "linearreg_angle" => compute_linearreg_angle_batch(req, output_id),
1685        "linearreg_intercept" => compute_linearreg_intercept_batch(req, output_id),
1686        "linearreg_slope" => compute_linearreg_slope_batch(req, output_id),
1687        "cg" => compute_cg_batch(req, output_id),
1688        "rsi" => compute_rsi_batch(req, output_id),
1689        "roc" => compute_roc_batch(req, output_id),
1690        "apo" => compute_apo_batch(req, output_id),
1691        "bop" => compute_bop_batch(req, output_id),
1692        "bulls_v_bears" => compute_bulls_v_bears_batch(req, output_id),
1693        "cci" => compute_cci_batch(req, output_id),
1694        "cci_cycle" => compute_cci_cycle_batch(req, output_id),
1695        "cfo" => compute_cfo_batch(req, output_id),
1696        "cycle_channel_oscillator" => compute_cycle_channel_oscillator_batch(req, output_id),
1697        "daily_factor" => compute_daily_factor_batch(req, output_id),
1698        "ehlers_adaptive_cg" => compute_ehlers_adaptive_cg_batch(req, output_id),
1699        "ehlers_adaptive_cyber_cycle" => compute_ehlers_adaptive_cyber_cycle_batch(req, output_id),
1700        "adaptive_schaff_trend_cycle" => compute_adaptive_schaff_trend_cycle_batch(req, output_id),
1701        "adaptive_momentum_oscillator" => {
1702            compute_adaptive_momentum_oscillator_batch(req, output_id)
1703        }
1704        "adaptive_macd" => compute_adaptive_macd_batch(req, output_id),
1705        "linear_correlation_oscillator" => {
1706            compute_linear_correlation_oscillator_batch(req, output_id)
1707        }
1708        "polynomial_regression_extrapolation" => {
1709            compute_polynomial_regression_extrapolation_batch(req, output_id)
1710        }
1711        "statistical_trailing_stop" => compute_statistical_trailing_stop_batch(req, output_id),
1712        "supertrend_recovery" => compute_supertrend_recovery_batch(req, output_id),
1713        "standardized_psar_oscillator" => {
1714            compute_standardized_psar_oscillator_batch(req, output_id)
1715        }
1716        "geometric_bias_oscillator" => compute_geometric_bias_oscillator_batch(req, output_id),
1717        "vdubus_divergence_wave_pattern_generator" => {
1718            compute_vdubus_divergence_wave_pattern_generator_batch(req, output_id)
1719        }
1720        "lrsi" => compute_lrsi_batch(req, output_id),
1721        "er" => compute_er_batch(req, output_id),
1722        "kurtosis" => compute_kurtosis_batch(req, output_id),
1723        "natr" => compute_natr_batch(req, output_id),
1724        "net_myrsi" => compute_net_myrsi_batch(req, output_id),
1725        "mean_ad" => compute_mean_ad_batch(req, output_id),
1726        "medium_ad" => compute_medium_ad_batch(req, output_id),
1727        "deviation" => compute_deviation_batch(req, output_id),
1728        "dpo" => compute_dpo_batch(req, output_id),
1729        "pfe" => compute_pfe_batch(req, output_id),
1730        "ehlers_detrending_filter" => compute_ehlers_detrending_filter_batch(req, output_id),
1731        "ehlers_fm_demodulator" => compute_ehlers_fm_demodulator_batch(req, output_id),
1732        "ehlers_simple_cycle_indicator" => compute_ehlers_simple_cycle_indicator_batch(req, output_id),
1733        "ehlers_smoothed_adaptive_momentum" => {
1734            compute_ehlers_smoothed_adaptive_momentum_batch(req, output_id)
1735        }
1736        "ewma_volatility" => compute_ewma_volatility_batch(req, output_id),
1737        "qstick" => compute_qstick_batch(req, output_id),
1738        "reverse_rsi" => compute_reverse_rsi_batch(req, output_id),
1739        "percentile_nearest_rank" => compute_percentile_nearest_rank_batch(req, output_id),
1740        "obv" => compute_obv_batch(req, output_id),
1741        "on_balance_volume_oscillator" => compute_on_balance_volume_oscillator_batch(req, output_id),
1742        "vpt" => compute_vpt_batch(req, output_id),
1743        "nvi" => compute_nvi_batch(req, output_id),
1744        "pvi" => compute_pvi_batch(req, output_id),
1745        "wclprice" => compute_wclprice_batch(req, output_id),
1746        "ui" => compute_ui_batch(req, output_id),
1747        "zscore" => compute_zscore_batch(req, output_id),
1748        "medprice" => compute_medprice_batch(req, output_id),
1749        "midpoint" => compute_midpoint_batch(req, output_id),
1750        "midprice" => compute_midprice_batch(req, output_id),
1751        "mom" => compute_mom_batch(req, output_id),
1752        "velocity" => compute_velocity_batch(req, output_id),
1753        "normalized_volume_true_range" => {
1754            compute_normalized_volume_true_range_batch(req, output_id)
1755        }
1756        "exponential_trend" => compute_exponential_trend_batch(req, output_id),
1757        "trend_flow_trail" => compute_trend_flow_trail_batch(req, output_id),
1758        "range_breakout_signals" => compute_range_breakout_signals_batch(req, output_id),
1759        "cmo" => compute_cmo_batch(req, output_id),
1760        "rocp" => compute_rocp_batch(req, output_id),
1761        "rocr" => compute_rocr_batch(req, output_id),
1762        "ppo" => compute_ppo_batch(req, output_id),
1763        "tsf" => compute_tsf_batch(req, output_id),
1764        "trix" => compute_trix_batch(req, output_id),
1765        "tsi" => compute_tsi_batch(req, output_id),
1766        "var" => compute_var_batch(req, output_id),
1767        "stddev" => compute_stddev_batch(req, output_id),
1768        "willr" => compute_willr_batch(req, output_id),
1769        "ultosc" => compute_ultosc_batch(req, output_id),
1770        "adx" => compute_adx_batch(req, output_id),
1771        "adxr" => compute_adxr_batch(req, output_id),
1772        "atr" => compute_atr_batch(req, output_id),
1773        "macd" => compute_macd_batch(req, output_id),
1774        "bollinger_bands" => compute_bollinger_batch(req, output_id),
1775        "bollinger_bands_width" => compute_bbw_batch(req, output_id),
1776        "stoch" => compute_stoch_batch(req, output_id),
1777        "stochf" => compute_stochf_batch(req, output_id),
1778        "stochastic_money_flow_index" => compute_stochastic_money_flow_index_batch(req, output_id),
1779        "vwmacd" => compute_vwmacd_batch(req, output_id),
1780        "vpci" => compute_vpci_batch(req, output_id),
1781        "ttm_trend" => compute_ttm_trend_batch(req, output_id),
1782        "ttm_squeeze" => compute_ttm_squeeze_batch(req, output_id),
1783        "aroon" => compute_aroon_batch(req, output_id),
1784        "aroonosc" => compute_aroonosc_batch(req, output_id),
1785        "di" => compute_di_batch(req, output_id),
1786        "dm" => compute_dm_batch(req, output_id),
1787        "dti" => compute_dti_batch(req, output_id),
1788        "donchian" => compute_donchian_batch(req, output_id),
1789        "kdj" => compute_kdj_batch(req, output_id),
1790        "keltner" => compute_keltner_batch(req, output_id),
1791        "squeeze_momentum" => compute_squeeze_momentum_batch(req, output_id),
1792        "srsi" => compute_srsi_batch(req, output_id),
1793        "supertrend" => compute_supertrend_batch(req, output_id),
1794        "adjustable_ma_alternating_extremities" => {
1795            compute_adjustable_ma_alternating_extremities_batch(req, output_id)
1796        }
1797        "vi" => compute_vi_batch(req, output_id),
1798        "wavetrend" => compute_wavetrend_batch(req, output_id),
1799        "wto" => compute_wto_batch(req, output_id),
1800        "rogers_satchell_volatility" => compute_rogers_satchell_volatility_batch(req, output_id),
1801        "historical_volatility_percentile" => {
1802            compute_historical_volatility_percentile_batch(req, output_id)
1803        }
1804        "historical_volatility_rank" => compute_historical_volatility_rank_batch(req, output_id),
1805        "dual_ulcer_index" => compute_dual_ulcer_index_batch(req, output_id),
1806        "fractal_dimension_index" => compute_fractal_dimension_index_batch(req, output_id),
1807        "ichimoku_oscillator" => compute_ichimoku_oscillator_batch(req, output_id),
1808        "volume_weighted_rsi" => compute_volume_weighted_rsi_batch(req, output_id),
1809        "dynamic_momentum_index" => compute_dynamic_momentum_index_batch(req, output_id),
1810        "disparity_index" => compute_disparity_index_batch(req, output_id),
1811        "donchian_channel_width" => compute_donchian_channel_width_batch(req, output_id),
1812        "kairi_relative_index" => compute_kairi_relative_index_batch(req, output_id),
1813        "projection_oscillator" => compute_projection_oscillator_batch(req, output_id),
1814        "market_structure_trailing_stop" => {
1815            compute_market_structure_trailing_stop_batch(req, output_id)
1816        }
1817        "emd_trend" => compute_emd_trend_batch(req, output_id),
1818        "cyberpunk_value_trend_analyzer" => {
1819            compute_cyberpunk_value_trend_analyzer_batch(req, output_id)
1820        }
1821        "evasive_supertrend" => compute_evasive_supertrend_batch(req, output_id),
1822        "reversal_signals" => compute_reversal_signals_batch(req, output_id),
1823        "zig_zag_channels" => compute_zig_zag_channels_batch(req, output_id),
1824        "directional_imbalance_index" => compute_directional_imbalance_index_batch(req, output_id),
1825        "candle_strength_oscillator" => compute_candle_strength_oscillator_batch(req, output_id),
1826        "gmma_oscillator" => compute_gmma_oscillator_batch(req, output_id),
1827        "nonlinear_regression_zero_lag_moving_average" => {
1828            compute_nonlinear_regression_zero_lag_moving_average_batch(req, output_id)
1829        }
1830        "possible_rsi" => compute_possible_rsi_batch(req, output_id),
1831        "autocorrelation_indicator" => compute_autocorrelation_indicator_batch(req, output_id),
1832        "goertzel_cycle_composite_wave" => {
1833            compute_goertzel_cycle_composite_wave_batch(req, output_id)
1834        }
1835        "rolling_skewness_kurtosis" => compute_rolling_skewness_kurtosis_batch(req, output_id),
1836        "rolling_z_score_trend" => compute_rolling_z_score_trend_batch(req, output_id),
1837        "ehlers_data_sampling_relative_strength_indicator" => {
1838            compute_ehlers_data_sampling_relative_strength_indicator_batch(req, output_id)
1839        }
1840        "velocity_acceleration_convergence_divergence_indicator" => {
1841            compute_velocity_acceleration_convergence_divergence_indicator_batch(req, output_id)
1842        }
1843        "trend_direction_force_index" => compute_trend_direction_force_index_batch(req, output_id),
1844        "yang_zhang_volatility" => compute_yang_zhang_volatility_batch(req, output_id),
1845        "garman_klass_volatility" => compute_garman_klass_volatility_batch(req, output_id),
1846        "advance_decline_line" => compute_advance_decline_line_batch(req, output_id),
1847        "decisionpoint_breadth_swenlin_trading_oscillator" => {
1848            compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(req, output_id)
1849        }
1850        "velocity_acceleration_indicator" => {
1851            compute_velocity_acceleration_indicator_batch(req, output_id)
1852        }
1853        "normalized_resonator" => compute_normalized_resonator_batch(req, output_id),
1854        "monotonicity_index" => compute_monotonicity_index_batch(req, output_id),
1855        "half_causal_estimator" => compute_half_causal_estimator_batch(req, output_id),
1856        "atr_percentile" => compute_atr_percentile_batch(req, output_id),
1857        "andean_oscillator" => compute_andean_oscillator_batch(req, output_id),
1858        "bull_power_vs_bear_power" => compute_bull_power_vs_bear_power_batch(req, output_id),
1859        "didi_index" => compute_didi_index_batch(req, output_id),
1860        "ehlers_autocorrelation_periodogram" => {
1861            compute_ehlers_autocorrelation_periodogram_batch(req, output_id)
1862        }
1863        "ehlers_linear_extrapolation_predictor" => {
1864            compute_ehlers_linear_extrapolation_predictor_batch(req, output_id)
1865        }
1866        "absolute_strength_index_oscillator" => {
1867            compute_absolute_strength_index_oscillator_batch(req, output_id)
1868        }
1869        "adaptive_bandpass_trigger_oscillator" => {
1870            compute_adaptive_bandpass_trigger_oscillator_batch(req, output_id)
1871        }
1872        "premier_rsi_oscillator" => compute_premier_rsi_oscillator_batch(req, output_id),
1873        "multi_length_stochastic_average" => {
1874            compute_multi_length_stochastic_average_batch(req, output_id)
1875        }
1876        "hull_butterfly_oscillator" => compute_hull_butterfly_oscillator_batch(req, output_id),
1877        "fibonacci_trailing_stop" => compute_fibonacci_trailing_stop_batch(req, output_id),
1878        "fibonacci_entry_bands" => compute_fibonacci_entry_bands_batch(req, output_id),
1879        "volume_energy_reservoirs" => compute_volume_energy_reservoirs_batch(req, output_id),
1880        "neighboring_trailing_stop" => compute_neighboring_trailing_stop_batch(req, output_id),
1881        "grover_llorens_cycle_oscillator" => {
1882            compute_grover_llorens_cycle_oscillator_batch(req, output_id)
1883        }
1884        "historical_volatility" => compute_historical_volatility_batch(req, output_id),
1885        "hypertrend" => compute_hypertrend_batch(req, output_id),
1886        "ict_propulsion_block" => compute_ict_propulsion_block_batch(req, output_id),
1887        "impulse_macd" => compute_impulse_macd_batch(req, output_id),
1888        "l1_ehlers_phasor" => compute_l1_ehlers_phasor_batch(req, output_id),
1889        "l2_ehlers_signal_to_noise" => compute_l2_ehlers_signal_to_noise_batch(req, output_id),
1890        "keltner_channel_width_oscillator" => {
1891            compute_keltner_channel_width_oscillator_batch(req, output_id)
1892        }
1893        "leavitt_convolution_acceleration" => {
1894            compute_leavitt_convolution_acceleration_batch(req, output_id)
1895        }
1896        "linear_regression_intensity" => compute_linear_regression_intensity_batch(req, output_id),
1897        "market_meanness_index" => compute_market_meanness_index_batch(req, output_id),
1898        "mesa_stochastic_multi_length" => compute_mesa_stochastic_multi_length_batch(req, output_id),
1899        "moving_average_cross_probability" => {
1900            compute_moving_average_cross_probability_batch(req, output_id)
1901        }
1902        "momentum_ratio_oscillator" => compute_momentum_ratio_oscillator_batch(req, output_id),
1903        "parkinson_volatility" => compute_parkinson_volatility_batch(req, output_id),
1904        "price_moving_average_ratio_percentile" => {
1905            compute_price_moving_average_ratio_percentile_batch(req, output_id)
1906        }
1907        "pretty_good_oscillator" => compute_pretty_good_oscillator_batch(req, output_id),
1908        "price_density_market_noise" => compute_price_density_market_noise_batch(req, output_id),
1909        "psychological_line" => compute_psychological_line_batch(req, output_id),
1910        "random_walk_index" => compute_random_walk_index_batch(req, output_id),
1911        "rank_correlation_index" => compute_rank_correlation_index_batch(req, output_id),
1912        "relative_strength_index_wave_indicator" => {
1913            compute_relative_strength_index_wave_indicator_batch(req, output_id)
1914        }
1915        "regression_slope_oscillator" => compute_regression_slope_oscillator_batch(req, output_id),
1916        "squeeze_index" => compute_squeeze_index_batch(req, output_id),
1917        "smoothed_gaussian_trend_filter" => {
1918            compute_smoothed_gaussian_trend_filter_batch(req, output_id)
1919        }
1920        "smooth_theil_sen" => compute_smooth_theil_sen_batch(req, output_id),
1921        "spearman_correlation" => compute_spearman_correlation_batch(req, output_id),
1922        "stochastic_adaptive_d" => compute_stochastic_adaptive_d_batch(req, output_id),
1923        "stochastic_connors_rsi" => compute_stochastic_connors_rsi_batch(req, output_id),
1924        "stochastic_distance" => compute_stochastic_distance_batch(req, output_id),
1925        "supertrend_oscillator" => compute_supertrend_oscillator_batch(req, output_id),
1926        "trend_trigger_factor" => compute_trend_trigger_factor_batch(req, output_id),
1927        "trend_continuation_factor" => compute_trend_continuation_factor_batch(req, output_id),
1928        "twiggs_money_flow" => compute_twiggs_money_flow_batch(req, output_id),
1929        "vertical_horizontal_filter" => compute_vertical_horizontal_filter_batch(req, output_id),
1930        "intraday_momentum_index" => compute_intraday_momentum_index_batch(req, output_id),
1931        "volatility_quality_index" => compute_volatility_quality_index_batch(req, output_id),
1932        "volatility_ratio_adaptive_rsx" => {
1933            compute_volatility_ratio_adaptive_rsx_batch(req, output_id)
1934        }
1935        "volume_weighted_stochastic_rsi" => {
1936            compute_volume_weighted_stochastic_rsi_batch(req, output_id)
1937        }
1938        "volume_zone_oscillator" => compute_volume_zone_oscillator_batch(req, output_id),
1939        "vwap_deviation_oscillator" => compute_vwap_deviation_oscillator_batch(req, output_id),
1940        "vwap_zscore_with_signals" => compute_vwap_zscore_with_signals_batch(req, output_id),
1941        "macd_wave_signal_pro" => compute_macd_wave_signal_pro_batch(req, output_id),
1942        "hema_trend_levels" => compute_hema_trend_levels_batch(req, output_id),
1943        "demand_index" => compute_demand_index_batch(req, output_id),
1944        "kase_peak_oscillator_with_divergences" => {
1945            compute_kase_peak_oscillator_with_divergences_batch(req, output_id)
1946        }
1947        "gopalakrishnan_range_index" => compute_gopalakrishnan_range_index_batch(req, output_id),
1948        "acosc" => compute_acosc_batch(req, output_id),
1949        "alligator" => compute_alligator_batch(req, output_id),
1950        "alphatrend" => compute_alphatrend_batch(req, output_id),
1951        "aso" => compute_aso_batch(req, output_id),
1952        "avsl" => compute_avsl_batch(req, output_id),
1953        "bandpass" => compute_bandpass_batch(req, output_id),
1954        "chande" => compute_chande_batch(req, output_id),
1955        "chandelier_exit" => compute_chandelier_exit_batch(req, output_id),
1956        "cksp" => compute_cksp_batch(req, output_id),
1957        "coppock" => compute_coppock_batch(req, output_id),
1958        "correl_hl" => compute_correl_hl_batch(req, output_id),
1959        "correlation_cycle" => compute_correlation_cycle_batch(req, output_id),
1960        "damiani_volatmeter" => compute_damiani_volatmeter_batch(req, output_id),
1961        "dvdiqqe" => compute_dvdiqqe_batch(req, output_id),
1962        "emd" => compute_emd_batch(req, output_id),
1963        "eri" => compute_eri_batch(req, output_id),
1964        "fisher" => compute_fisher_batch(req, output_id),
1965        "fvg_positioning_average" => compute_fvg_positioning_average_batch(req, output_id),
1966        "fvg_trailing_stop" => compute_fvg_trailing_stop_batch(req, output_id),
1967        "gatorosc" => compute_gatorosc_batch(req, output_id),
1968        "halftrend" => compute_halftrend_batch(req, output_id),
1969        "kaufmanstop" => compute_kaufmanstop_batch(req, output_id),
1970        "kst" => compute_kst_batch(req, output_id),
1971        "lpc" => compute_lpc_batch(req, output_id),
1972        "mab" => compute_mab_batch(req, output_id),
1973        "macz" => compute_macz_batch(req, output_id),
1974        "minmax" => compute_minmax_batch(req, output_id),
1975        "mod_god_mode" => compute_mod_god_mode_batch(req, output_id),
1976        "msw" => compute_msw_batch(req, output_id),
1977        "nadaraya_watson_envelope" => compute_nadaraya_watson_envelope_batch(req, output_id),
1978        "otto" => compute_otto_batch(req, output_id),
1979        "vidya" => compute_vidya_batch(req, output_id),
1980        "vlma" => compute_vlma_batch(req, output_id),
1981        "pma" => compute_pma_batch(req, output_id),
1982        "prb" => compute_prb_batch(req, output_id),
1983        "qqe" => compute_qqe_batch(req, output_id),
1984        "forward_backward_exponential_oscillator" => {
1985            compute_forward_backward_exponential_oscillator_batch(req, output_id)
1986        }
1987        "qqe_weighted_oscillator" => compute_qqe_weighted_oscillator_batch(req, output_id),
1988        "market_structure_confluence" => compute_market_structure_confluence_batch(req, output_id),
1989        "range_filtered_trend_signals" => {
1990            compute_range_filtered_trend_signals_batch(req, output_id)
1991        }
1992        "range_oscillator" => compute_range_oscillator_batch(req, output_id),
1993        "volume_weighted_relative_strength_index" => {
1994            compute_volume_weighted_relative_strength_index_batch(req, output_id)
1995        }
1996        "range_filter" => compute_range_filter_batch(req, output_id),
1997        "rsmk" => compute_rsmk_batch(req, output_id),
1998        "voss" => compute_voss_batch(req, output_id),
1999        "stc" => compute_stc_batch(req, output_id),
2000        "rvi" => compute_rvi_batch(req, output_id),
2001        "safezonestop" => compute_safezonestop_batch(req, output_id),
2002        "devstop" => compute_devstop_batch(req, output_id),
2003        "chop" => compute_chop_batch(req, output_id),
2004        "pivot" => compute_pivot_batch(req, output_id),
2005        _ => Err(IndicatorDispatchError::UnsupportedCapability {
2006            indicator: indicator_id.to_string(),
2007            capability: "cpu_batch",
2008        }),
2009    }
2010}
2011
2012fn validate_input_kind_strict(
2013    indicator: &str,
2014    expected: IndicatorInputKind,
2015    data: IndicatorDataRef<'_>,
2016) -> Result<(), IndicatorDispatchError> {
2017    let expected = strict_expected_input_kind(indicator, expected);
2018    if indicator.eq_ignore_ascii_case("mod_god_mode") {
2019        let matches = matches!(
2020            data,
2021            IndicatorDataRef::Candles { .. }
2022                | IndicatorDataRef::Ohlc { .. }
2023                | IndicatorDataRef::Ohlcv { .. }
2024        );
2025        if matches {
2026            return Ok(());
2027        }
2028    }
2029    let matches = matches!(
2030        (expected, data),
2031        (IndicatorInputKind::Slice, IndicatorDataRef::Slice { .. })
2032            | (
2033                IndicatorInputKind::Candles,
2034                IndicatorDataRef::Candles { .. }
2035            )
2036            | (IndicatorInputKind::Ohlc, IndicatorDataRef::Ohlc { .. })
2037            | (IndicatorInputKind::Ohlcv, IndicatorDataRef::Ohlcv { .. })
2038            | (
2039                IndicatorInputKind::HighLow,
2040                IndicatorDataRef::HighLow { .. }
2041            )
2042            | (
2043                IndicatorInputKind::CloseVolume,
2044                IndicatorDataRef::CloseVolume { .. }
2045            )
2046    );
2047
2048    if matches {
2049        Ok(())
2050    } else {
2051        Err(IndicatorDispatchError::MissingRequiredInput {
2052            indicator: indicator.to_string(),
2053            input: expected,
2054        })
2055    }
2056}
2057
2058fn strict_expected_input_kind(indicator: &str, fallback: IndicatorInputKind) -> IndicatorInputKind {
2059    if indicator.eq_ignore_ascii_case("ao") {
2060        return IndicatorInputKind::Slice;
2061    }
2062    if indicator.eq_ignore_ascii_case("ttm_trend") {
2063        return IndicatorInputKind::Candles;
2064    }
2065    fallback
2066}
2067
2068fn resolve_output_id<'a>(
2069    info: &'a IndicatorInfo,
2070    requested: Option<&str>,
2071) -> Result<&'a str, IndicatorDispatchError> {
2072    if info.outputs.is_empty() {
2073        return Err(IndicatorDispatchError::ComputeFailed {
2074            indicator: info.id.to_string(),
2075            details: "indicator has no registered outputs".to_string(),
2076        });
2077    }
2078
2079    if info.outputs.len() == 1 {
2080        let only = info.outputs[0].id;
2081        if let Some(req) = requested {
2082            if req == only {
2083                return Ok(only);
2084            }
2085            if !req.eq_ignore_ascii_case(only) {
2086                return Err(IndicatorDispatchError::UnknownOutput {
2087                    indicator: info.id.to_string(),
2088                    output: req.to_string(),
2089                });
2090            }
2091        }
2092        return Ok(only);
2093    }
2094
2095    let req = requested.ok_or_else(|| IndicatorDispatchError::InvalidParam {
2096        indicator: info.id.to_string(),
2097        key: "output_id".to_string(),
2098        reason: "output_id is required for multi-output indicators".to_string(),
2099    })?;
2100
2101    if let Some(out) = info.outputs.iter().find(|o| o.id == req) {
2102        return Ok(out.id);
2103    }
2104    info.outputs
2105        .iter()
2106        .find(|o| o.id.eq_ignore_ascii_case(req))
2107        .map(|o| o.id)
2108        .ok_or_else(|| IndicatorDispatchError::UnknownOutput {
2109            indicator: info.id.to_string(),
2110            output: req.to_string(),
2111        })
2112}
2113
2114fn is_moving_average(id: &str) -> bool {
2115    list_moving_averages()
2116        .iter()
2117        .any(|ma| ma.id.eq_ignore_ascii_case(id))
2118}
2119
2120fn ma_is_period_based(info: &IndicatorInfo) -> bool {
2121    info.params
2122        .iter()
2123        .any(|p| p.key.eq_ignore_ascii_case("period"))
2124}
2125
2126fn compute_ma_batch(
2127    req: IndicatorBatchRequest<'_>,
2128    info: &IndicatorInfo,
2129    output_id: &str,
2130) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2131    let data = ma_data_from_req(info.id, req.data)?;
2132    let cols = ma_len_from_req(info.id, req.data)?;
2133    let period_based = ma_is_period_based(info);
2134    if period_based {
2135        if let Some(out) = try_compute_ma_batch_fast(req, info, output_id, data.clone(), cols)? {
2136            return Ok(out);
2137        }
2138    }
2139    let rows = req.combos.len();
2140    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
2141
2142    for combo in req.combos {
2143        let period = ma_period_for_combo(info, combo.params)?;
2144        let mut params = convert_ma_params(combo.params, info.id, output_id)?;
2145        if info.outputs.len() > 1 && !has_key(combo.params, "output") {
2146            params.push(MaBatchParamKV {
2147                key: "output",
2148                value: MaBatchParamValue::EnumString(output_id),
2149            });
2150        }
2151        let out = ma_batch_with_kernel_and_typed_params(
2152            info.id,
2153            data.clone(),
2154            (period, period, 0),
2155            req.kernel,
2156            &params,
2157        )
2158        .map_err(|e| IndicatorDispatchError::ComputeFailed {
2159            indicator: info.id.to_string(),
2160            details: e.to_string(),
2161        })?;
2162        ensure_len(info.id, cols, out.cols)?;
2163        let row_values = if out.rows == 1 {
2164            out.values
2165        } else {
2166            reorder_or_take_f64_matrix_by_period(
2167                info.id,
2168                &[period],
2169                &out.periods,
2170                out.cols,
2171                out.values,
2172            )?
2173        };
2174        ensure_len(info.id, cols, row_values.len())?;
2175        matrix.extend_from_slice(&row_values);
2176    }
2177
2178    Ok(f64_output(output_id, rows, cols, matrix))
2179}
2180
2181fn try_compute_ma_batch_fast(
2182    req: IndicatorBatchRequest<'_>,
2183    info: &IndicatorInfo,
2184    output_id: &str,
2185    data: MaData<'_>,
2186    cols: usize,
2187) -> Result<Option<IndicatorBatchOutput>, IndicatorDispatchError> {
2188    if req.combos.is_empty() {
2189        return Ok(Some(f64_output(output_id, 0, cols, Vec::new())));
2190    }
2191    if !ma_is_period_based(info) {
2192        return Ok(None);
2193    }
2194
2195    let mut periods = Vec::with_capacity(req.combos.len());
2196    let mut shared_params: Option<Vec<MaBatchParamKV<'_>>> = None;
2197
2198    for combo in req.combos {
2199        periods.push(ma_period_for_combo(info, combo.params)?);
2200        let mut params = convert_ma_params(combo.params, info.id, output_id)?;
2201        if info.outputs.len() > 1 && !has_key(combo.params, "output") {
2202            params.push(MaBatchParamKV {
2203                key: "output",
2204                value: MaBatchParamValue::EnumString(output_id),
2205            });
2206        }
2207        match &shared_params {
2208            None => shared_params = Some(params),
2209            Some(existing) => {
2210                if !ma_params_equal(existing, &params) {
2211                    return Ok(None);
2212                }
2213            }
2214        }
2215    }
2216
2217    let Some((start, end, step)) = derive_period_sweep(&periods) else {
2218        return Ok(None);
2219    };
2220
2221    let out = ma_batch_with_kernel_and_typed_params(
2222        info.id,
2223        data,
2224        (start, end, step),
2225        req.kernel,
2226        shared_params.as_deref().unwrap_or(&[]),
2227    )
2228    .map_err(|e| IndicatorDispatchError::ComputeFailed {
2229        indicator: info.id.to_string(),
2230        details: e.to_string(),
2231    })?;
2232    ensure_len(info.id, cols, out.cols)?;
2233
2234    let values = reorder_or_take_f64_matrix_by_period(
2235        info.id,
2236        &periods,
2237        &out.periods,
2238        out.cols,
2239        out.values,
2240    )?;
2241    Ok(Some(f64_output(output_id, periods.len(), cols, values)))
2242}
2243
2244fn ma_params_equal(a: &[MaBatchParamKV<'_>], b: &[MaBatchParamKV<'_>]) -> bool {
2245    if a.len() != b.len() {
2246        return false;
2247    }
2248
2249    for (lhs, rhs) in a.iter().zip(b.iter()) {
2250        if !lhs.key.eq_ignore_ascii_case(rhs.key) {
2251            return false;
2252        }
2253        let same = match (&lhs.value, &rhs.value) {
2254            (MaBatchParamValue::Int(x), MaBatchParamValue::Int(y)) => x == y,
2255            (MaBatchParamValue::Float(x), MaBatchParamValue::Float(y)) => x == y,
2256            (MaBatchParamValue::Bool(x), MaBatchParamValue::Bool(y)) => x == y,
2257            (MaBatchParamValue::EnumString(x), MaBatchParamValue::EnumString(y)) => {
2258                x.eq_ignore_ascii_case(y)
2259            }
2260            _ => false,
2261        };
2262        if !same {
2263            return false;
2264        }
2265    }
2266    true
2267}
2268
2269fn collect_f64(
2270    indicator: &str,
2271    output_id: &str,
2272    combos: &[IndicatorParamSet<'_>],
2273    cols: usize,
2274    mut eval: impl FnMut(&[ParamKV<'_>]) -> Result<Vec<f64>, IndicatorDispatchError>,
2275) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2276    let rows = combos.len();
2277    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
2278    for combo in combos {
2279        let series = eval(combo.params)?;
2280        ensure_len(indicator, cols, series.len())?;
2281        matrix.extend_from_slice(&series);
2282    }
2283    Ok(f64_output(output_id, rows, cols, matrix))
2284}
2285
2286fn collect_bool(
2287    indicator: &str,
2288    output_id: &str,
2289    combos: &[IndicatorParamSet<'_>],
2290    cols: usize,
2291    mut eval: impl FnMut(&[ParamKV<'_>]) -> Result<Vec<bool>, IndicatorDispatchError>,
2292) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2293    let rows = combos.len();
2294    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
2295    for combo in combos {
2296        let series = eval(combo.params)?;
2297        ensure_len(indicator, cols, series.len())?;
2298        matrix.extend_from_slice(&series);
2299    }
2300    Ok(bool_output(output_id, rows, cols, matrix))
2301}
2302
2303fn collect_f64_into_rows(
2304    indicator: &str,
2305    output_id: &str,
2306    combos: &[IndicatorParamSet<'_>],
2307    cols: usize,
2308    mut eval_into: impl FnMut(&[ParamKV<'_>], &mut [f64]) -> Result<(), IndicatorDispatchError>,
2309) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2310    let rows = combos.len();
2311    let total = rows
2312        .checked_mul(cols)
2313        .ok_or_else(|| IndicatorDispatchError::ComputeFailed {
2314            indicator: indicator.to_string(),
2315            details: "rows*cols overflow".to_string(),
2316        })?;
2317    let mut matrix = vec![f64::NAN; total];
2318    for (row, combo) in combos.iter().enumerate() {
2319        let start = row * cols;
2320        let end = start + cols;
2321        eval_into(combo.params, &mut matrix[start..end])?;
2322    }
2323    Ok(f64_output(output_id, rows, cols, matrix))
2324}
2325
2326fn to_batch_kernel(kernel: Kernel) -> Kernel {
2327    match kernel {
2328        Kernel::Auto => Kernel::Auto,
2329        Kernel::Scalar => Kernel::ScalarBatch,
2330        Kernel::Avx2 => Kernel::Avx2Batch,
2331        Kernel::Avx512 => Kernel::Avx512Batch,
2332        other => other,
2333    }
2334}
2335
2336fn combo_periods(
2337    indicator: &str,
2338    combos: &[IndicatorParamSet<'_>],
2339    key: &str,
2340    default: usize,
2341) -> Result<Vec<usize>, IndicatorDispatchError> {
2342    let mut out = Vec::with_capacity(combos.len());
2343    for combo in combos {
2344        out.push(get_usize_param(indicator, combo.params, key, default)?);
2345    }
2346    Ok(out)
2347}
2348
2349fn derive_period_sweep(periods: &[usize]) -> Option<(usize, usize, usize)> {
2350    if periods.is_empty() {
2351        return None;
2352    }
2353    if periods.len() == 1 {
2354        return Some((periods[0], periods[0], 0));
2355    }
2356    if periods.windows(2).all(|w| w[0] == w[1]) {
2357        return Some((periods[0], periods[0], 0));
2358    }
2359
2360    let diff = periods[1] as isize - periods[0] as isize;
2361    if diff == 0 {
2362        return None;
2363    }
2364    if !periods
2365        .windows(2)
2366        .all(|w| (w[1] as isize - w[0] as isize) == diff)
2367    {
2368        return None;
2369    }
2370
2371    Some((
2372        periods[0],
2373        *periods.last().unwrap_or(&periods[0]),
2374        diff.unsigned_abs(),
2375    ))
2376}
2377
2378fn reorder_or_take_f64_matrix_by_period(
2379    indicator: &str,
2380    requested_periods: &[usize],
2381    produced_periods: &[usize],
2382    cols: usize,
2383    values: Vec<f64>,
2384) -> Result<Vec<f64>, IndicatorDispatchError> {
2385    ensure_len(
2386        indicator,
2387        produced_periods.len().saturating_mul(cols),
2388        values.len(),
2389    )?;
2390
2391    if requested_periods.len() == produced_periods.len() && requested_periods == produced_periods {
2392        return Ok(values);
2393    }
2394
2395    let period_to_row: HashMap<usize, usize> = produced_periods
2396        .iter()
2397        .copied()
2398        .enumerate()
2399        .map(|(row, period)| (period, row))
2400        .collect();
2401
2402    let mut out = Vec::with_capacity(requested_periods.len().saturating_mul(cols));
2403    for period in requested_periods {
2404        let row = period_to_row.get(period).copied().ok_or_else(|| {
2405            IndicatorDispatchError::ComputeFailed {
2406                indicator: indicator.to_string(),
2407                details: format!("batch output did not contain requested period {period}"),
2408            }
2409        })?;
2410        let start = row * cols;
2411        let end = start + cols;
2412        out.extend_from_slice(&values[start..end]);
2413    }
2414    Ok(out)
2415}
2416
2417fn compute_ad_batch(
2418    req: IndicatorBatchRequest<'_>,
2419    output_id: &str,
2420) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2421    expect_value_output("ad", output_id)?;
2422    let (high, low, close, volume) = extract_hlcv_input("ad", req.data)?;
2423    let kernel = req.kernel.to_non_batch();
2424    collect_f64("ad", output_id, req.combos, close.len(), |_params| {
2425        let input = AdInput::from_slices(high, low, close, volume, AdParams::default());
2426        let out =
2427            ad_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2428                indicator: "ad".to_string(),
2429                details: e.to_string(),
2430            })?;
2431        Ok(out.values)
2432    })
2433}
2434
2435fn compute_adosc_batch(
2436    req: IndicatorBatchRequest<'_>,
2437    output_id: &str,
2438) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2439    expect_value_output("adosc", output_id)?;
2440    let (high, low, close, volume) = extract_hlcv_input("adosc", req.data)?;
2441    let kernel = req.kernel.to_non_batch();
2442    collect_f64("adosc", output_id, req.combos, close.len(), |params| {
2443        let short_period = get_usize_param("adosc", params, "short_period", 3)?;
2444        let long_period = get_usize_param("adosc", params, "long_period", 10)?;
2445        let input = AdoscInput::from_slices(
2446            high,
2447            low,
2448            close,
2449            volume,
2450            AdoscParams {
2451                short_period: Some(short_period),
2452                long_period: Some(long_period),
2453            },
2454        );
2455        let out = adosc_with_kernel(&input, kernel).map_err(|e| {
2456            IndicatorDispatchError::ComputeFailed {
2457                indicator: "adosc".to_string(),
2458                details: e.to_string(),
2459            }
2460        })?;
2461        Ok(out.values)
2462    })
2463}
2464
2465fn compute_ao_batch(
2466    req: IndicatorBatchRequest<'_>,
2467    output_id: &str,
2468) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2469    expect_value_output("ao", output_id)?;
2470    let mut derived_source: Option<Vec<f64>> = None;
2471    let source: &[f64] = match req.data {
2472        IndicatorDataRef::Slice { values } => values,
2473        IndicatorDataRef::Candles { candles, source } => {
2474            source_type(candles, source.unwrap_or("hl2"))
2475        }
2476        IndicatorDataRef::HighLow { high, low } => {
2477            ensure_same_len_2("ao", high.len(), low.len())?;
2478            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2479            derived_source.as_deref().unwrap_or(high)
2480        }
2481        IndicatorDataRef::Ohlc {
2482            open,
2483            high,
2484            low,
2485            close,
2486        } => {
2487            ensure_same_len_4("ao", open.len(), high.len(), low.len(), close.len())?;
2488            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2489            derived_source.as_deref().unwrap_or(close)
2490        }
2491        IndicatorDataRef::Ohlcv {
2492            open,
2493            high,
2494            low,
2495            close,
2496            volume,
2497        } => {
2498            ensure_same_len_5(
2499                "ao",
2500                open.len(),
2501                high.len(),
2502                low.len(),
2503                close.len(),
2504                volume.len(),
2505            )?;
2506            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2507            derived_source.as_deref().unwrap_or(close)
2508        }
2509        IndicatorDataRef::CloseVolume { .. } => {
2510            return Err(IndicatorDispatchError::MissingRequiredInput {
2511                indicator: "ao".to_string(),
2512                input: IndicatorInputKind::HighLow,
2513            })
2514        }
2515    };
2516    let kernel = req.kernel.to_non_batch();
2517    collect_f64_into_rows("ao", output_id, req.combos, source.len(), |params, row| {
2518        let short_period = get_usize_param("ao", params, "short_period", 5)?;
2519        let long_period = get_usize_param("ao", params, "long_period", 34)?;
2520        let input = AoInput::from_slice(
2521            source,
2522            AoParams {
2523                short_period: Some(short_period),
2524                long_period: Some(long_period),
2525            },
2526        );
2527        ao_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2528            indicator: "ao".to_string(),
2529            details: e.to_string(),
2530        })
2531    })
2532}
2533
2534fn compute_bop_batch(
2535    req: IndicatorBatchRequest<'_>,
2536    output_id: &str,
2537) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2538    expect_value_output("bop", output_id)?;
2539    let (open, high, low, close): (&[f64], &[f64], &[f64], &[f64]) = match req.data {
2540        IndicatorDataRef::Candles { candles, .. } => (
2541            candles.open.as_slice(),
2542            candles.high.as_slice(),
2543            candles.low.as_slice(),
2544            candles.close.as_slice(),
2545        ),
2546        IndicatorDataRef::Ohlc {
2547            open,
2548            high,
2549            low,
2550            close,
2551        } => {
2552            ensure_same_len_4("bop", open.len(), high.len(), low.len(), close.len())?;
2553            (open, high, low, close)
2554        }
2555        IndicatorDataRef::Ohlcv {
2556            open,
2557            high,
2558            low,
2559            close,
2560            volume,
2561        } => {
2562            ensure_same_len_5(
2563                "bop",
2564                open.len(),
2565                high.len(),
2566                low.len(),
2567                close.len(),
2568                volume.len(),
2569            )?;
2570            (open, high, low, close)
2571        }
2572        _ => {
2573            return Err(IndicatorDispatchError::MissingRequiredInput {
2574                indicator: "bop".to_string(),
2575                input: IndicatorInputKind::Ohlc,
2576            })
2577        }
2578    };
2579    let kernel = req.kernel.to_non_batch();
2580    collect_f64("bop", output_id, req.combos, close.len(), |_params| {
2581        let input = BopInput::from_slices(open, high, low, close, BopParams::default());
2582        let out =
2583            bop_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2584                indicator: "bop".to_string(),
2585                details: e.to_string(),
2586            })?;
2587        Ok(out.values)
2588    })
2589}
2590
2591fn compute_emv_batch(
2592    req: IndicatorBatchRequest<'_>,
2593    output_id: &str,
2594) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2595    expect_value_output("emv", output_id)?;
2596    let (high, low, close, volume) = extract_hlcv_input("emv", req.data)?;
2597    let kernel = req.kernel.to_non_batch();
2598    collect_f64("emv", output_id, req.combos, close.len(), |_params| {
2599        let input = EmvInput::from_slices(high, low, close, volume);
2600        let out =
2601            emv_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2602                indicator: "emv".to_string(),
2603                details: e.to_string(),
2604            })?;
2605        Ok(out.values)
2606    })
2607}
2608
2609fn compute_efi_batch(
2610    req: IndicatorBatchRequest<'_>,
2611    output_id: &str,
2612) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2613    expect_value_output("efi", output_id)?;
2614    let (price, volume) = extract_close_volume_input("efi", req.data, "close")?;
2615    let kernel = req.kernel.to_non_batch();
2616    collect_f64("efi", output_id, req.combos, price.len(), |params| {
2617        let period = get_usize_param("efi", params, "period", 13)?;
2618        let input = EfiInput::from_slices(
2619            price,
2620            volume,
2621            EfiParams {
2622                period: Some(period),
2623            },
2624        );
2625        let out =
2626            efi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2627                indicator: "efi".to_string(),
2628                details: e.to_string(),
2629            })?;
2630        Ok(out.values)
2631    })
2632}
2633
2634fn compute_mfi_batch(
2635    req: IndicatorBatchRequest<'_>,
2636    output_id: &str,
2637) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2638    expect_value_output("mfi", output_id)?;
2639    let mut derived_typical_price: Option<Vec<f64>> = None;
2640    let (typical_price, volume): (&[f64], &[f64]) = match req.data {
2641        IndicatorDataRef::Candles { candles, source } => (
2642            source_type(candles, source.unwrap_or("hlc3")),
2643            candles.volume.as_slice(),
2644        ),
2645        IndicatorDataRef::Ohlcv {
2646            open,
2647            high,
2648            low,
2649            close,
2650            volume,
2651        } => {
2652            ensure_same_len_5(
2653                "mfi",
2654                open.len(),
2655                high.len(),
2656                low.len(),
2657                close.len(),
2658                volume.len(),
2659            )?;
2660            derived_typical_price = Some(
2661                high.iter()
2662                    .zip(low)
2663                    .zip(close)
2664                    .map(|((h, l), c)| (h + l + c) / 3.0)
2665                    .collect(),
2666            );
2667            (derived_typical_price.as_deref().unwrap_or(close), volume)
2668        }
2669        IndicatorDataRef::CloseVolume { close, volume } => {
2670            ensure_same_len_2("mfi", close.len(), volume.len())?;
2671            (close, volume)
2672        }
2673        _ => {
2674            return Err(IndicatorDispatchError::MissingRequiredInput {
2675                indicator: "mfi".to_string(),
2676                input: IndicatorInputKind::CloseVolume,
2677            })
2678        }
2679    };
2680
2681    let periods = combo_periods("mfi", req.combos, "period", 14)?;
2682    if let Some((start, end, step)) = derive_period_sweep(&periods) {
2683        let out = mfi_batch_with_kernel(
2684            typical_price,
2685            volume,
2686            &MfiBatchRange {
2687                period: (start, end, step),
2688            },
2689            to_batch_kernel(req.kernel),
2690        )
2691        .map_err(|e| IndicatorDispatchError::ComputeFailed {
2692            indicator: "mfi".to_string(),
2693            details: e.to_string(),
2694        })?;
2695        ensure_len("mfi", typical_price.len(), out.cols)?;
2696        let produced_periods: Vec<usize> = out
2697            .combos
2698            .iter()
2699            .map(|combo| combo.period.unwrap_or(14))
2700            .collect();
2701        let values = reorder_or_take_f64_matrix_by_period(
2702            "mfi",
2703            &periods,
2704            &produced_periods,
2705            out.cols,
2706            out.values,
2707        )?;
2708        return Ok(f64_output(output_id, periods.len(), out.cols, values));
2709    }
2710
2711    let kernel = req.kernel.to_non_batch();
2712    collect_f64_into_rows(
2713        "mfi",
2714        output_id,
2715        req.combos,
2716        typical_price.len(),
2717        |params, row| {
2718            let period = get_usize_param("mfi", params, "period", 14)?;
2719            let input = MfiInput::from_slices(
2720                typical_price,
2721                volume,
2722                MfiParams {
2723                    period: Some(period),
2724                },
2725            );
2726            mfi_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2727                indicator: "mfi".to_string(),
2728                details: e.to_string(),
2729            })
2730        },
2731    )
2732}
2733
2734fn compute_mass_batch(
2735    req: IndicatorBatchRequest<'_>,
2736    output_id: &str,
2737) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2738    expect_value_output("mass", output_id)?;
2739    let (high, low) = extract_high_low_input("mass", req.data)?;
2740    let kernel = req.kernel.to_non_batch();
2741    collect_f64("mass", output_id, req.combos, high.len(), |params| {
2742        let period = get_usize_param("mass", params, "period", 5)?;
2743        let input = MassInput::from_slices(
2744            high,
2745            low,
2746            MassParams {
2747                period: Some(period),
2748            },
2749        );
2750        let out = mass_with_kernel(&input, kernel).map_err(|e| {
2751            IndicatorDispatchError::ComputeFailed {
2752                indicator: "mass".to_string(),
2753                details: e.to_string(),
2754            }
2755        })?;
2756        Ok(out.values)
2757    })
2758}
2759
2760fn compute_kvo_batch(
2761    req: IndicatorBatchRequest<'_>,
2762    output_id: &str,
2763) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2764    expect_value_output("kvo", output_id)?;
2765    let (high, low, close, volume) = extract_hlcv_input("kvo", req.data)?;
2766    let kernel = req.kernel.to_non_batch();
2767    collect_f64("kvo", output_id, req.combos, close.len(), |params| {
2768        let short_period = get_usize_param("kvo", params, "short_period", 2)?;
2769        let long_period = get_usize_param("kvo", params, "long_period", 5)?;
2770        let input = KvoInput::from_slices(
2771            high,
2772            low,
2773            close,
2774            volume,
2775            KvoParams {
2776                short_period: Some(short_period),
2777                long_period: Some(long_period),
2778            },
2779        );
2780        let out =
2781            kvo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2782                indicator: "kvo".to_string(),
2783                details: e.to_string(),
2784            })?;
2785        Ok(out.values)
2786    })
2787}
2788
2789fn compute_vosc_batch(
2790    req: IndicatorBatchRequest<'_>,
2791    output_id: &str,
2792) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2793    expect_value_output("vosc", output_id)?;
2794    let volume = extract_volume_input("vosc", req.data)?;
2795    let kernel = req.kernel.to_non_batch();
2796    collect_f64("vosc", output_id, req.combos, volume.len(), |params| {
2797        let short_period = get_usize_param("vosc", params, "short_period", 2)?;
2798        let long_period = get_usize_param("vosc", params, "long_period", 5)?;
2799        let input = VoscInput::from_slice(
2800            volume,
2801            VoscParams {
2802                short_period: Some(short_period),
2803                long_period: Some(long_period),
2804            },
2805        );
2806        let out = vosc_with_kernel(&input, kernel).map_err(|e| {
2807            IndicatorDispatchError::ComputeFailed {
2808                indicator: "vosc".to_string(),
2809                details: e.to_string(),
2810            }
2811        })?;
2812        Ok(out.values)
2813    })
2814}
2815
2816fn compute_dx_batch(
2817    req: IndicatorBatchRequest<'_>,
2818    output_id: &str,
2819) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2820    expect_value_output("dx", output_id)?;
2821    let (high, low, close) = extract_ohlc_input("dx", req.data)?;
2822
2823    let periods = combo_periods("dx", req.combos, "period", 14)?;
2824    if let Some((start, end, step)) = derive_period_sweep(&periods) {
2825        let out = dx_batch_with_kernel(
2826            high,
2827            low,
2828            close,
2829            &DxBatchRange {
2830                period: (start, end, step),
2831            },
2832            to_batch_kernel(req.kernel),
2833        )
2834        .map_err(|e| IndicatorDispatchError::ComputeFailed {
2835            indicator: "dx".to_string(),
2836            details: e.to_string(),
2837        })?;
2838        ensure_len("dx", close.len(), out.cols)?;
2839        let produced_periods: Vec<usize> = out
2840            .combos
2841            .iter()
2842            .map(|combo| combo.period.unwrap_or(14))
2843            .collect();
2844        let values = reorder_or_take_f64_matrix_by_period(
2845            "dx",
2846            &periods,
2847            &produced_periods,
2848            out.cols,
2849            out.values,
2850        )?;
2851        return Ok(f64_output(output_id, periods.len(), out.cols, values));
2852    }
2853
2854    let kernel = req.kernel.to_non_batch();
2855    collect_f64_into_rows("dx", output_id, req.combos, close.len(), |params, row| {
2856        let period = get_usize_param("dx", params, "period", 14)?;
2857        let input = DxInput::from_hlc_slices(
2858            high,
2859            low,
2860            close,
2861            DxParams {
2862                period: Some(period),
2863            },
2864        );
2865        dx_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2866            indicator: "dx".to_string(),
2867            details: e.to_string(),
2868        })
2869    })
2870}
2871
2872fn compute_fosc_batch(
2873    req: IndicatorBatchRequest<'_>,
2874    output_id: &str,
2875) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2876    expect_value_output("fosc", output_id)?;
2877    let data = extract_slice_input("fosc", req.data, "close")?;
2878    let kernel = req.kernel.to_non_batch();
2879    collect_f64("fosc", output_id, req.combos, data.len(), |params| {
2880        let period = get_usize_param("fosc", params, "period", 5)?;
2881        let input = FoscInput::from_slice(
2882            data,
2883            FoscParams {
2884                period: Some(period),
2885            },
2886        );
2887        let out = fosc_with_kernel(&input, kernel).map_err(|e| {
2888            IndicatorDispatchError::ComputeFailed {
2889                indicator: "fosc".to_string(),
2890                details: e.to_string(),
2891            }
2892        })?;
2893        Ok(out.values)
2894    })
2895}
2896
2897fn compute_ift_rsi_batch(
2898    req: IndicatorBatchRequest<'_>,
2899    output_id: &str,
2900) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2901    expect_value_output("ift_rsi", output_id)?;
2902    let data = extract_slice_input("ift_rsi", req.data, "close")?;
2903    let kernel = req.kernel.to_non_batch();
2904    collect_f64("ift_rsi", output_id, req.combos, data.len(), |params| {
2905        let rsi_period = get_usize_param("ift_rsi", params, "rsi_period", 5)?;
2906        let wma_period = get_usize_param("ift_rsi", params, "wma_period", 9)?;
2907        let input = IftRsiInput::from_slice(
2908            data,
2909            IftRsiParams {
2910                rsi_period: Some(rsi_period),
2911                wma_period: Some(wma_period),
2912            },
2913        );
2914        let out = ift_rsi_with_kernel(&input, kernel).map_err(|e| {
2915            IndicatorDispatchError::ComputeFailed {
2916                indicator: "ift_rsi".to_string(),
2917                details: e.to_string(),
2918            }
2919        })?;
2920        Ok(out.values)
2921    })
2922}
2923
2924fn compute_linearreg_angle_batch(
2925    req: IndicatorBatchRequest<'_>,
2926    output_id: &str,
2927) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2928    expect_value_output("linearreg_angle", output_id)?;
2929    let data = extract_slice_input("linearreg_angle", req.data, "close")?;
2930    let kernel = req.kernel.to_non_batch();
2931    collect_f64(
2932        "linearreg_angle",
2933        output_id,
2934        req.combos,
2935        data.len(),
2936        |params| {
2937            let period = get_usize_param("linearreg_angle", params, "period", 14)?;
2938            let input = Linearreg_angleInput::from_slice(
2939                data,
2940                Linearreg_angleParams {
2941                    period: Some(period),
2942                },
2943            );
2944            let out = linearreg_angle_with_kernel(&input, kernel).map_err(|e| {
2945                IndicatorDispatchError::ComputeFailed {
2946                    indicator: "linearreg_angle".to_string(),
2947                    details: e.to_string(),
2948                }
2949            })?;
2950            Ok(out.values)
2951        },
2952    )
2953}
2954
2955fn compute_linearreg_intercept_batch(
2956    req: IndicatorBatchRequest<'_>,
2957    output_id: &str,
2958) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2959    expect_value_output("linearreg_intercept", output_id)?;
2960    let data = extract_slice_input("linearreg_intercept", req.data, "close")?;
2961    let kernel = req.kernel.to_non_batch();
2962    collect_f64(
2963        "linearreg_intercept",
2964        output_id,
2965        req.combos,
2966        data.len(),
2967        |params| {
2968            let period = get_usize_param("linearreg_intercept", params, "period", 14)?;
2969            let input = LinearRegInterceptInput::from_slice(
2970                data,
2971                LinearRegInterceptParams {
2972                    period: Some(period),
2973                },
2974            );
2975            let out = linearreg_intercept_with_kernel(&input, kernel).map_err(|e| {
2976                IndicatorDispatchError::ComputeFailed {
2977                    indicator: "linearreg_intercept".to_string(),
2978                    details: e.to_string(),
2979                }
2980            })?;
2981            Ok(out.values)
2982        },
2983    )
2984}
2985
2986fn compute_linearreg_slope_batch(
2987    req: IndicatorBatchRequest<'_>,
2988    output_id: &str,
2989) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2990    expect_value_output("linearreg_slope", output_id)?;
2991    let data = extract_slice_input("linearreg_slope", req.data, "close")?;
2992    let kernel = req.kernel.to_non_batch();
2993    collect_f64(
2994        "linearreg_slope",
2995        output_id,
2996        req.combos,
2997        data.len(),
2998        |params| {
2999            let period = get_usize_param("linearreg_slope", params, "period", 14)?;
3000            let input = LinearRegSlopeInput::from_slice(
3001                data,
3002                LinearRegSlopeParams {
3003                    period: Some(period),
3004                },
3005            );
3006            let out = linearreg_slope_with_kernel(&input, kernel).map_err(|e| {
3007                IndicatorDispatchError::ComputeFailed {
3008                    indicator: "linearreg_slope".to_string(),
3009                    details: e.to_string(),
3010                }
3011            })?;
3012            Ok(out.values)
3013        },
3014    )
3015}
3016
3017fn compute_cg_batch(
3018    req: IndicatorBatchRequest<'_>,
3019    output_id: &str,
3020) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3021    expect_value_output("cg", output_id)?;
3022    let data = extract_slice_input("cg", req.data, "close")?;
3023    let kernel = req.kernel.to_non_batch();
3024    collect_f64("cg", output_id, req.combos, data.len(), |params| {
3025        let period = get_usize_param("cg", params, "period", 10)?;
3026        let input = CgInput::from_slice(
3027            data,
3028            CgParams {
3029                period: Some(period),
3030            },
3031        );
3032        let out =
3033            cg_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3034                indicator: "cg".to_string(),
3035                details: e.to_string(),
3036            })?;
3037        Ok(out.values)
3038    })
3039}
3040
3041fn compute_rsi_batch(
3042    req: IndicatorBatchRequest<'_>,
3043    output_id: &str,
3044) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3045    expect_value_output("rsi", output_id)?;
3046    let data = extract_slice_input("rsi", req.data, "close")?;
3047    let kernel = req.kernel.to_non_batch();
3048    collect_f64("rsi", output_id, req.combos, data.len(), |params| {
3049        let period = get_usize_param("rsi", params, "period", 14)?;
3050        let input = RsiInput::from_slice(
3051            data,
3052            RsiParams {
3053                period: Some(period),
3054            },
3055        );
3056        let out =
3057            rsi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3058                indicator: "rsi".to_string(),
3059                details: e.to_string(),
3060            })?;
3061        Ok(out.values)
3062    })
3063}
3064
3065fn compute_roc_batch(
3066    req: IndicatorBatchRequest<'_>,
3067    output_id: &str,
3068) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3069    expect_value_output("roc", output_id)?;
3070    let data = extract_slice_input("roc", req.data, "close")?;
3071    let kernel = req.kernel.to_non_batch();
3072    collect_f64("roc", output_id, req.combos, data.len(), |params| {
3073        let period = get_usize_param("roc", params, "period", 9)?;
3074        let input = RocInput::from_slice(
3075            data,
3076            RocParams {
3077                period: Some(period),
3078            },
3079        );
3080        let out =
3081            roc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3082                indicator: "roc".to_string(),
3083                details: e.to_string(),
3084            })?;
3085        Ok(out.values)
3086    })
3087}
3088
3089fn compute_linear_correlation_oscillator_batch(
3090    req: IndicatorBatchRequest<'_>,
3091    output_id: &str,
3092) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3093    expect_value_output("linear_correlation_oscillator", output_id)?;
3094    let data = extract_slice_input("linear_correlation_oscillator", req.data, "close")?;
3095    let kernel = req.kernel.to_non_batch();
3096    collect_f64(
3097        "linear_correlation_oscillator",
3098        output_id,
3099        req.combos,
3100        data.len(),
3101        |params| {
3102            let period = get_usize_param("linear_correlation_oscillator", params, "period", 14)?;
3103            let input = LinearCorrelationOscillatorInput::from_slice(
3104                data,
3105                LinearCorrelationOscillatorParams {
3106                    period: Some(period),
3107                },
3108            );
3109            let out = linear_correlation_oscillator_with_kernel(&input, kernel).map_err(|e| {
3110                IndicatorDispatchError::ComputeFailed {
3111                    indicator: "linear_correlation_oscillator".to_string(),
3112                    details: e.to_string(),
3113                }
3114            })?;
3115            Ok(out.values)
3116        },
3117    )
3118}
3119
3120fn compute_apo_batch(
3121    req: IndicatorBatchRequest<'_>,
3122    output_id: &str,
3123) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3124    expect_value_output("apo", output_id)?;
3125    let data = extract_slice_input("apo", req.data, "close")?;
3126    let kernel = req.kernel.to_non_batch();
3127    collect_f64("apo", output_id, req.combos, data.len(), |params| {
3128        let short_period = get_usize_param("apo", params, "short_period", 10)?;
3129        let long_period = get_usize_param("apo", params, "long_period", 20)?;
3130        let input = ApoInput::from_slice(
3131            data,
3132            ApoParams {
3133                short_period: Some(short_period),
3134                long_period: Some(long_period),
3135            },
3136        );
3137        let out =
3138            apo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3139                indicator: "apo".to_string(),
3140                details: e.to_string(),
3141            })?;
3142        Ok(out.values)
3143    })
3144}
3145
3146fn compute_cci_batch(
3147    req: IndicatorBatchRequest<'_>,
3148    output_id: &str,
3149) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3150    expect_value_output("cci", output_id)?;
3151    let data = extract_slice_input("cci", req.data, "hlc3")?;
3152    let kernel = req.kernel.to_non_batch();
3153    collect_f64("cci", output_id, req.combos, data.len(), |params| {
3154        let period = get_usize_param("cci", params, "period", 14)?;
3155        let input = CciInput::from_slice(
3156            data,
3157            CciParams {
3158                period: Some(period),
3159            },
3160        );
3161        let out =
3162            cci_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3163                indicator: "cci".to_string(),
3164                details: e.to_string(),
3165            })?;
3166        Ok(out.values)
3167    })
3168}
3169
3170fn compute_cfo_batch(
3171    req: IndicatorBatchRequest<'_>,
3172    output_id: &str,
3173) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3174    expect_value_output("cfo", output_id)?;
3175    let data = extract_slice_input("cfo", req.data, "close")?;
3176    let kernel = req.kernel.to_non_batch();
3177    collect_f64("cfo", output_id, req.combos, data.len(), |params| {
3178        let period = get_usize_param("cfo", params, "period", 14)?;
3179        let scalar = get_f64_param("cfo", params, "scalar", 100.0)?;
3180        let input = CfoInput::from_slice(
3181            data,
3182            CfoParams {
3183                period: Some(period),
3184                scalar: Some(scalar),
3185            },
3186        );
3187        let out =
3188            cfo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3189                indicator: "cfo".to_string(),
3190                details: e.to_string(),
3191            })?;
3192        Ok(out.values)
3193    })
3194}
3195
3196fn compute_cci_cycle_batch(
3197    req: IndicatorBatchRequest<'_>,
3198    output_id: &str,
3199) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3200    expect_value_output("cci_cycle", output_id)?;
3201    let data = extract_slice_input("cci_cycle", req.data, "close")?;
3202    let kernel = req.kernel.to_non_batch();
3203    collect_f64("cci_cycle", output_id, req.combos, data.len(), |params| {
3204        let length = get_usize_param("cci_cycle", params, "length", 10)?;
3205        let factor = get_f64_param("cci_cycle", params, "factor", 0.5)?;
3206        let input = CciCycleInput::from_slice(
3207            data,
3208            CciCycleParams {
3209                length: Some(length),
3210                factor: Some(factor),
3211            },
3212        );
3213        let out = cci_cycle_with_kernel(&input, kernel).map_err(|e| {
3214            IndicatorDispatchError::ComputeFailed {
3215                indicator: "cci_cycle".to_string(),
3216                details: e.to_string(),
3217            }
3218        })?;
3219        Ok(out.values)
3220    })
3221}
3222
3223fn compute_lrsi_batch(
3224    req: IndicatorBatchRequest<'_>,
3225    output_id: &str,
3226) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3227    expect_value_output("lrsi", output_id)?;
3228    let (high, low) = extract_high_low_input("lrsi", req.data)?;
3229    let kernel = req.kernel.to_non_batch();
3230    collect_f64("lrsi", output_id, req.combos, high.len(), |params| {
3231        let alpha = get_f64_param("lrsi", params, "alpha", 0.2)?;
3232        let input = LrsiInput::from_slices(high, low, LrsiParams { alpha: Some(alpha) });
3233        let out = lrsi_with_kernel(&input, kernel).map_err(|e| {
3234            IndicatorDispatchError::ComputeFailed {
3235                indicator: "lrsi".to_string(),
3236                details: e.to_string(),
3237            }
3238        })?;
3239        Ok(out.values)
3240    })
3241}
3242
3243fn compute_er_batch(
3244    req: IndicatorBatchRequest<'_>,
3245    output_id: &str,
3246) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3247    expect_value_output("er", output_id)?;
3248    let data = extract_slice_input("er", req.data, "close")?;
3249    let kernel = req.kernel.to_non_batch();
3250    collect_f64("er", output_id, req.combos, data.len(), |params| {
3251        let period = get_usize_param("er", params, "period", 5)?;
3252        let input = ErInput::from_slice(
3253            data,
3254            ErParams {
3255                period: Some(period),
3256            },
3257        );
3258        let out =
3259            er_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3260                indicator: "er".to_string(),
3261                details: e.to_string(),
3262            })?;
3263        Ok(out.values)
3264    })
3265}
3266
3267fn compute_kurtosis_batch(
3268    req: IndicatorBatchRequest<'_>,
3269    output_id: &str,
3270) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3271    expect_value_output("kurtosis", output_id)?;
3272    let data = extract_slice_input("kurtosis", req.data, "hl2")?;
3273    let kernel = req.kernel.to_non_batch();
3274    collect_f64("kurtosis", output_id, req.combos, data.len(), |params| {
3275        let period = get_usize_param("kurtosis", params, "period", 5)?;
3276        let input = KurtosisInput::from_slice(
3277            data,
3278            KurtosisParams {
3279                period: Some(period),
3280            },
3281        );
3282        let out = kurtosis_with_kernel(&input, kernel).map_err(|e| {
3283            IndicatorDispatchError::ComputeFailed {
3284                indicator: "kurtosis".to_string(),
3285                details: e.to_string(),
3286            }
3287        })?;
3288        Ok(out.values)
3289    })
3290}
3291
3292fn compute_natr_batch(
3293    req: IndicatorBatchRequest<'_>,
3294    output_id: &str,
3295) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3296    expect_value_output("natr", output_id)?;
3297    let (high, low, close) = extract_ohlc_input("natr", req.data)?;
3298    let kernel = req.kernel.to_non_batch();
3299    collect_f64("natr", output_id, req.combos, close.len(), |params| {
3300        let period = get_usize_param("natr", params, "period", 14)?;
3301        let input = NatrInput::from_slices(
3302            high,
3303            low,
3304            close,
3305            NatrParams {
3306                period: Some(period),
3307            },
3308        );
3309        let out = natr_with_kernel(&input, kernel).map_err(|e| {
3310            IndicatorDispatchError::ComputeFailed {
3311                indicator: "natr".to_string(),
3312                details: e.to_string(),
3313            }
3314        })?;
3315        Ok(out.values)
3316    })
3317}
3318
3319fn compute_mean_ad_batch(
3320    req: IndicatorBatchRequest<'_>,
3321    output_id: &str,
3322) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3323    expect_value_output("mean_ad", output_id)?;
3324    let data = extract_slice_input("mean_ad", req.data, "close")?;
3325    let kernel = req.kernel.to_non_batch();
3326    collect_f64("mean_ad", output_id, req.combos, data.len(), |params| {
3327        let period = get_usize_param("mean_ad", params, "period", 5)?;
3328        let input = MeanAdInput::from_slice(
3329            data,
3330            MeanAdParams {
3331                period: Some(period),
3332            },
3333        );
3334        let out = mean_ad_with_kernel(&input, kernel).map_err(|e| {
3335            IndicatorDispatchError::ComputeFailed {
3336                indicator: "mean_ad".to_string(),
3337                details: e.to_string(),
3338            }
3339        })?;
3340        Ok(out.values)
3341    })
3342}
3343
3344fn compute_medium_ad_batch(
3345    req: IndicatorBatchRequest<'_>,
3346    output_id: &str,
3347) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3348    expect_value_output("medium_ad", output_id)?;
3349    let data = extract_slice_input("medium_ad", req.data, "close")?;
3350    let kernel = req.kernel.to_non_batch();
3351    collect_f64("medium_ad", output_id, req.combos, data.len(), |params| {
3352        let period = get_usize_param("medium_ad", params, "period", 5)?;
3353        let input = MediumAdInput::from_slice(
3354            data,
3355            MediumAdParams {
3356                period: Some(period),
3357            },
3358        );
3359        let out = medium_ad_with_kernel(&input, kernel).map_err(|e| {
3360            IndicatorDispatchError::ComputeFailed {
3361                indicator: "medium_ad".to_string(),
3362                details: e.to_string(),
3363            }
3364        })?;
3365        Ok(out.values)
3366    })
3367}
3368
3369fn compute_deviation_batch(
3370    req: IndicatorBatchRequest<'_>,
3371    output_id: &str,
3372) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3373    expect_value_output("deviation", output_id)?;
3374    let data = extract_slice_input("deviation", req.data, "close")?;
3375    let kernel = req.kernel.to_non_batch();
3376    collect_f64("deviation", output_id, req.combos, data.len(), |params| {
3377        let period = get_usize_param("deviation", params, "period", 9)?;
3378        let devtype = get_usize_param("deviation", params, "devtype", 0)?;
3379        let input = DeviationInput::from_slice(
3380            data,
3381            DeviationParams {
3382                period: Some(period),
3383                devtype: Some(devtype),
3384            },
3385        );
3386        let out = deviation_with_kernel(&input, kernel).map_err(|e| {
3387            IndicatorDispatchError::ComputeFailed {
3388                indicator: "deviation".to_string(),
3389                details: e.to_string(),
3390            }
3391        })?;
3392        Ok(out.values)
3393    })
3394}
3395
3396fn compute_dpo_batch(
3397    req: IndicatorBatchRequest<'_>,
3398    output_id: &str,
3399) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3400    expect_value_output("dpo", output_id)?;
3401    let data = extract_slice_input("dpo", req.data, "close")?;
3402    let kernel = req.kernel.to_non_batch();
3403    collect_f64("dpo", output_id, req.combos, data.len(), |params| {
3404        let period = get_usize_param("dpo", params, "period", 5)?;
3405        let input = DpoInput::from_slice(
3406            data,
3407            DpoParams {
3408                period: Some(period),
3409            },
3410        );
3411        let out =
3412            dpo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3413                indicator: "dpo".to_string(),
3414                details: e.to_string(),
3415            })?;
3416        Ok(out.values)
3417    })
3418}
3419
3420fn compute_pfe_batch(
3421    req: IndicatorBatchRequest<'_>,
3422    output_id: &str,
3423) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3424    expect_value_output("pfe", output_id)?;
3425    let data = extract_slice_input("pfe", req.data, "close")?;
3426    let kernel = req.kernel.to_non_batch();
3427    collect_f64("pfe", output_id, req.combos, data.len(), |params| {
3428        let period = get_usize_param("pfe", params, "period", 10)?;
3429        let smoothing = get_usize_param("pfe", params, "smoothing", 5)?;
3430        let input = PfeInput::from_slice(
3431            data,
3432            PfeParams {
3433                period: Some(period),
3434                smoothing: Some(smoothing),
3435            },
3436        );
3437        let out =
3438            pfe_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3439                indicator: "pfe".to_string(),
3440                details: e.to_string(),
3441            })?;
3442        Ok(out.values)
3443    })
3444}
3445
3446fn compute_qstick_batch(
3447    req: IndicatorBatchRequest<'_>,
3448    output_id: &str,
3449) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3450    expect_value_output("qstick", output_id)?;
3451    let (open, close) = match req.data {
3452        IndicatorDataRef::Candles { candles, .. } => {
3453            (candles.open.as_slice(), candles.close.as_slice())
3454        }
3455        IndicatorDataRef::Ohlc {
3456            open,
3457            high,
3458            low,
3459            close,
3460        } => {
3461            ensure_same_len_4("qstick", open.len(), high.len(), low.len(), close.len())?;
3462            (open, close)
3463        }
3464        IndicatorDataRef::Ohlcv {
3465            open,
3466            high,
3467            low,
3468            close,
3469            volume,
3470        } => {
3471            ensure_same_len_5(
3472                "qstick",
3473                open.len(),
3474                high.len(),
3475                low.len(),
3476                close.len(),
3477                volume.len(),
3478            )?;
3479            (open, close)
3480        }
3481        _ => {
3482            return Err(IndicatorDispatchError::MissingRequiredInput {
3483                indicator: "qstick".to_string(),
3484                input: IndicatorInputKind::Ohlc,
3485            })
3486        }
3487    };
3488    let kernel = req.kernel.to_non_batch();
3489    collect_f64("qstick", output_id, req.combos, close.len(), |params| {
3490        let period = get_usize_param("qstick", params, "period", 5)?;
3491        let input = QstickInput::from_slices(
3492            open,
3493            close,
3494            QstickParams {
3495                period: Some(period),
3496            },
3497        );
3498        let out = qstick_with_kernel(&input, kernel).map_err(|e| {
3499            IndicatorDispatchError::ComputeFailed {
3500                indicator: "qstick".to_string(),
3501                details: e.to_string(),
3502            }
3503        })?;
3504        Ok(out.values)
3505    })
3506}
3507
3508fn compute_ehlers_fm_demodulator_batch(
3509    req: IndicatorBatchRequest<'_>,
3510    output_id: &str,
3511) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3512    expect_value_output("ehlers_fm_demodulator", output_id)?;
3513    let (open, close) = match req.data {
3514        IndicatorDataRef::Candles { candles, .. } => {
3515            (candles.open.as_slice(), candles.close.as_slice())
3516        }
3517        IndicatorDataRef::Ohlc {
3518            open,
3519            high,
3520            low,
3521            close,
3522        } => {
3523            ensure_same_len_4(
3524                "ehlers_fm_demodulator",
3525                open.len(),
3526                high.len(),
3527                low.len(),
3528                close.len(),
3529            )?;
3530            (open, close)
3531        }
3532        IndicatorDataRef::Ohlcv {
3533            open,
3534            high,
3535            low,
3536            close,
3537            volume,
3538        } => {
3539            ensure_same_len_5(
3540                "ehlers_fm_demodulator",
3541                open.len(),
3542                high.len(),
3543                low.len(),
3544                close.len(),
3545                volume.len(),
3546            )?;
3547            (open, close)
3548        }
3549        _ => {
3550            return Err(IndicatorDispatchError::MissingRequiredInput {
3551                indicator: "ehlers_fm_demodulator".to_string(),
3552                input: IndicatorInputKind::Ohlc,
3553            })
3554        }
3555    };
3556    let kernel = req.kernel.to_non_batch();
3557    collect_f64(
3558        "ehlers_fm_demodulator",
3559        output_id,
3560        req.combos,
3561        close.len(),
3562        |params| {
3563            let period = get_usize_param("ehlers_fm_demodulator", params, "period", 30)?;
3564            let input = EhlersFmDemodulatorInput::from_slices(
3565                open,
3566                close,
3567                EhlersFmDemodulatorParams {
3568                    period: Some(period),
3569                },
3570            );
3571            let out = ehlers_fm_demodulator_with_kernel(&input, kernel).map_err(|e| {
3572                IndicatorDispatchError::ComputeFailed {
3573                    indicator: "ehlers_fm_demodulator".to_string(),
3574                    details: e.to_string(),
3575                }
3576            })?;
3577            Ok(out.values)
3578        },
3579    )
3580}
3581
3582fn compute_reverse_rsi_batch(
3583    req: IndicatorBatchRequest<'_>,
3584    output_id: &str,
3585) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3586    expect_value_output("reverse_rsi", output_id)?;
3587    let data = extract_slice_input("reverse_rsi", req.data, "close")?;
3588    let kernel = req.kernel.to_non_batch();
3589    collect_f64("reverse_rsi", output_id, req.combos, data.len(), |params| {
3590        let rsi_length = get_usize_param("reverse_rsi", params, "rsi_length", 14)?;
3591        let rsi_level = get_f64_param("reverse_rsi", params, "rsi_level", 50.0)?;
3592        let input = ReverseRsiInput::from_slice(
3593            data,
3594            ReverseRsiParams {
3595                rsi_length: Some(rsi_length),
3596                rsi_level: Some(rsi_level),
3597            },
3598        );
3599        let out = reverse_rsi_with_kernel(&input, kernel).map_err(|e| {
3600            IndicatorDispatchError::ComputeFailed {
3601                indicator: "reverse_rsi".to_string(),
3602                details: e.to_string(),
3603            }
3604        })?;
3605        Ok(out.values)
3606    })
3607}
3608
3609fn compute_percentile_nearest_rank_batch(
3610    req: IndicatorBatchRequest<'_>,
3611    output_id: &str,
3612) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3613    expect_value_output("percentile_nearest_rank", output_id)?;
3614    let data = extract_slice_input("percentile_nearest_rank", req.data, "close")?;
3615    let kernel = req.kernel.to_non_batch();
3616    collect_f64(
3617        "percentile_nearest_rank",
3618        output_id,
3619        req.combos,
3620        data.len(),
3621        |params| {
3622            let length = get_usize_param("percentile_nearest_rank", params, "length", 15)?;
3623            let percentage = get_f64_param("percentile_nearest_rank", params, "percentage", 50.0)?;
3624            let input = PercentileNearestRankInput::from_slice(
3625                data,
3626                PercentileNearestRankParams {
3627                    length: Some(length),
3628                    percentage: Some(percentage),
3629                },
3630            );
3631            let out = percentile_nearest_rank_with_kernel(&input, kernel).map_err(|e| {
3632                IndicatorDispatchError::ComputeFailed {
3633                    indicator: "percentile_nearest_rank".to_string(),
3634                    details: e.to_string(),
3635                }
3636            })?;
3637            Ok(out.values)
3638        },
3639    )
3640}
3641
3642fn compute_obv_batch(
3643    req: IndicatorBatchRequest<'_>,
3644    output_id: &str,
3645) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3646    expect_value_output("obv", output_id)?;
3647    let (close, volume) = extract_close_volume_input("obv", req.data, "close")?;
3648    let kernel = req.kernel.to_non_batch();
3649    collect_f64("obv", output_id, req.combos, close.len(), |_params| {
3650        let input = ObvInput::from_slices(close, volume, ObvParams::default());
3651        let out =
3652            obv_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3653                indicator: "obv".to_string(),
3654                details: e.to_string(),
3655            })?;
3656        Ok(out.values)
3657    })
3658}
3659
3660fn compute_vpt_batch(
3661    req: IndicatorBatchRequest<'_>,
3662    output_id: &str,
3663) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3664    expect_value_output("vpt", output_id)?;
3665    let (close, volume) = extract_close_volume_input("vpt", req.data, "close")?;
3666    let kernel = req.kernel.to_non_batch();
3667    collect_f64("vpt", output_id, req.combos, close.len(), |_params| {
3668        let input = VptInput::from_slices(close, volume);
3669        let out =
3670            vpt_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3671                indicator: "vpt".to_string(),
3672                details: e.to_string(),
3673            })?;
3674        Ok(out.values)
3675    })
3676}
3677
3678fn compute_nvi_batch(
3679    req: IndicatorBatchRequest<'_>,
3680    output_id: &str,
3681) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3682    expect_value_output("nvi", output_id)?;
3683    let (close, volume) = extract_close_volume_input("nvi", req.data, "close")?;
3684    let kernel = req.kernel.to_non_batch();
3685    collect_f64("nvi", output_id, req.combos, close.len(), |_params| {
3686        let input = NviInput::from_slices(close, volume, NviParams::default());
3687        let out =
3688            nvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3689                indicator: "nvi".to_string(),
3690                details: e.to_string(),
3691            })?;
3692        Ok(out.values)
3693    })
3694}
3695
3696fn compute_pvi_batch(
3697    req: IndicatorBatchRequest<'_>,
3698    output_id: &str,
3699) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3700    expect_value_output("pvi", output_id)?;
3701    let (close, volume) = extract_close_volume_input("pvi", req.data, "close")?;
3702    let kernel = req.kernel.to_non_batch();
3703    collect_f64("pvi", output_id, req.combos, close.len(), |params| {
3704        let initial_value = get_f64_param("pvi", params, "initial_value", 1000.0)?;
3705        let input = PviInput::from_slices(
3706            close,
3707            volume,
3708            PviParams {
3709                initial_value: Some(initial_value),
3710            },
3711        );
3712        let out =
3713            pvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3714                indicator: "pvi".to_string(),
3715                details: e.to_string(),
3716            })?;
3717        Ok(out.values)
3718    })
3719}
3720
3721fn compute_wclprice_batch(
3722    req: IndicatorBatchRequest<'_>,
3723    output_id: &str,
3724) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3725    expect_value_output("wclprice", output_id)?;
3726    let (high, low, close) = extract_ohlc_input("wclprice", req.data)?;
3727    let kernel = req.kernel.to_non_batch();
3728    collect_f64("wclprice", output_id, req.combos, close.len(), |_params| {
3729        let input = WclpriceInput::from_slices(high, low, close);
3730        let out = wclprice_with_kernel(&input, kernel).map_err(|e| {
3731            IndicatorDispatchError::ComputeFailed {
3732                indicator: "wclprice".to_string(),
3733                details: e.to_string(),
3734            }
3735        })?;
3736        Ok(out.values)
3737    })
3738}
3739
3740fn compute_ui_batch(
3741    req: IndicatorBatchRequest<'_>,
3742    output_id: &str,
3743) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3744    expect_value_output("ui", output_id)?;
3745    let data = extract_slice_input("ui", req.data, "close")?;
3746    let kernel = req.kernel.to_non_batch();
3747    collect_f64("ui", output_id, req.combos, data.len(), |params| {
3748        let period = get_usize_param("ui", params, "period", 14)?;
3749        let scalar = get_f64_param("ui", params, "scalar", 100.0)?;
3750        let input = UiInput::from_slice(
3751            data,
3752            UiParams {
3753                period: Some(period),
3754                scalar: Some(scalar),
3755            },
3756        );
3757        let out =
3758            ui_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3759                indicator: "ui".to_string(),
3760                details: e.to_string(),
3761            })?;
3762        Ok(out.values)
3763    })
3764}
3765
3766fn compute_zscore_batch(
3767    req: IndicatorBatchRequest<'_>,
3768    output_id: &str,
3769) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3770    expect_value_output("zscore", output_id)?;
3771    let data = extract_slice_input("zscore", req.data, "close")?;
3772    let kernel = req.kernel.to_non_batch();
3773    collect_f64("zscore", output_id, req.combos, data.len(), |params| {
3774        let period = get_usize_param("zscore", params, "period", 14)?;
3775        let ma_type = get_enum_param("zscore", params, "ma_type", "sma")?;
3776        let nbdev = get_f64_param("zscore", params, "nbdev", 1.0)?;
3777        let devtype = get_usize_param("zscore", params, "devtype", 0)?;
3778        let input = ZscoreInput::from_slice(
3779            data,
3780            ZscoreParams {
3781                period: Some(period),
3782                ma_type: Some(ma_type),
3783                nbdev: Some(nbdev),
3784                devtype: Some(devtype),
3785            },
3786        );
3787        let out = zscore_with_kernel(&input, kernel).map_err(|e| {
3788            IndicatorDispatchError::ComputeFailed {
3789                indicator: "zscore".to_string(),
3790                details: e.to_string(),
3791            }
3792        })?;
3793        Ok(out.values)
3794    })
3795}
3796
3797fn compute_medprice_batch(
3798    req: IndicatorBatchRequest<'_>,
3799    output_id: &str,
3800) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3801    expect_value_output("medprice", output_id)?;
3802    let (high, low) = extract_high_low_input("medprice", req.data)?;
3803    let kernel = req.kernel.to_non_batch();
3804    collect_f64("medprice", output_id, req.combos, high.len(), |_params| {
3805        let input = MedpriceInput::from_slices(high, low, MedpriceParams::default());
3806        let out = medprice_with_kernel(&input, kernel).map_err(|e| {
3807            IndicatorDispatchError::ComputeFailed {
3808                indicator: "medprice".to_string(),
3809                details: e.to_string(),
3810            }
3811        })?;
3812        Ok(out.values)
3813    })
3814}
3815
3816fn compute_midpoint_batch(
3817    req: IndicatorBatchRequest<'_>,
3818    output_id: &str,
3819) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3820    expect_value_output("midpoint", output_id)?;
3821    let data = extract_slice_input("midpoint", req.data, "close")?;
3822    let kernel = req.kernel.to_non_batch();
3823    collect_f64("midpoint", output_id, req.combos, data.len(), |params| {
3824        let period = get_usize_param("midpoint", params, "period", 14)?;
3825        let input = MidpointInput::from_slice(
3826            data,
3827            MidpointParams {
3828                period: Some(period),
3829            },
3830        );
3831        let out = midpoint_with_kernel(&input, kernel).map_err(|e| {
3832            IndicatorDispatchError::ComputeFailed {
3833                indicator: "midpoint".to_string(),
3834                details: e.to_string(),
3835            }
3836        })?;
3837        Ok(out.values)
3838    })
3839}
3840
3841fn compute_midprice_batch(
3842    req: IndicatorBatchRequest<'_>,
3843    output_id: &str,
3844) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3845    expect_value_output("midprice", output_id)?;
3846    let (high, low) = extract_high_low_input("midprice", req.data)?;
3847    let kernel = req.kernel.to_non_batch();
3848    collect_f64("midprice", output_id, req.combos, high.len(), |params| {
3849        let period = get_usize_param("midprice", params, "period", 14)?;
3850        let input = MidpriceInput::from_slices(
3851            high,
3852            low,
3853            MidpriceParams {
3854                period: Some(period),
3855            },
3856        );
3857        let out = midprice_with_kernel(&input, kernel).map_err(|e| {
3858            IndicatorDispatchError::ComputeFailed {
3859                indicator: "midprice".to_string(),
3860                details: e.to_string(),
3861            }
3862        })?;
3863        Ok(out.values)
3864    })
3865}
3866
3867fn compute_mom_batch(
3868    req: IndicatorBatchRequest<'_>,
3869    output_id: &str,
3870) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3871    expect_value_output("mom", output_id)?;
3872    let data = extract_slice_input("mom", req.data, "close")?;
3873    let kernel = req.kernel.to_non_batch();
3874    collect_f64("mom", output_id, req.combos, data.len(), |params| {
3875        let period = get_usize_param("mom", params, "period", 10)?;
3876        let input = MomInput::from_slice(
3877            data,
3878            MomParams {
3879                period: Some(period),
3880            },
3881        );
3882        let out =
3883            mom_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3884                indicator: "mom".to_string(),
3885                details: e.to_string(),
3886            })?;
3887        Ok(out.values)
3888    })
3889}
3890
3891fn compute_velocity_batch(
3892    req: IndicatorBatchRequest<'_>,
3893    output_id: &str,
3894) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3895    expect_value_output("velocity", output_id)?;
3896    let data = extract_slice_input("velocity", req.data, "hlcc4")?;
3897    let kernel = req.kernel.to_non_batch();
3898    collect_f64("velocity", output_id, req.combos, data.len(), |params| {
3899        let length = get_usize_param("velocity", params, "length", 21)?;
3900        let smooth_length = get_usize_param("velocity", params, "smooth_length", 5)?;
3901        let input = VelocityInput::from_slice(
3902            data,
3903            VelocityParams {
3904                length: Some(length),
3905                smooth_length: Some(smooth_length),
3906            },
3907        );
3908        let out = velocity_with_kernel(&input, kernel).map_err(|e| {
3909            IndicatorDispatchError::ComputeFailed {
3910                indicator: "velocity".to_string(),
3911                details: e.to_string(),
3912            }
3913        })?;
3914        Ok(out.values)
3915    })
3916}
3917
3918fn compute_adaptive_momentum_oscillator_batch(
3919    req: IndicatorBatchRequest<'_>,
3920    output_id: &str,
3921) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3922    let data = extract_slice_input("adaptive_momentum_oscillator", req.data, "close")?;
3923    let kernel = req.kernel.to_non_batch();
3924    collect_f64(
3925        "adaptive_momentum_oscillator",
3926        output_id,
3927        req.combos,
3928        data.len(),
3929        |params| {
3930            let length = get_usize_param("adaptive_momentum_oscillator", params, "length", 14)?;
3931            let smoothing_length = get_usize_param(
3932                "adaptive_momentum_oscillator",
3933                params,
3934                "smoothing_length",
3935                9,
3936            )?;
3937            let input = AdaptiveMomentumOscillatorInput::from_slice(
3938                data,
3939                AdaptiveMomentumOscillatorParams {
3940                    length: Some(length),
3941                    smoothing_length: Some(smoothing_length),
3942                },
3943            );
3944            let out = adaptive_momentum_oscillator_with_kernel(&input, kernel).map_err(|e| {
3945                IndicatorDispatchError::ComputeFailed {
3946                    indicator: "adaptive_momentum_oscillator".to_string(),
3947                    details: e.to_string(),
3948                }
3949            })?;
3950            match output_id {
3951                "amo" | "value" => Ok(out.amo),
3952                "ama" => Ok(out.ama),
3953                other => Err(IndicatorDispatchError::UnknownOutput {
3954                    indicator: "adaptive_momentum_oscillator".to_string(),
3955                    output: other.to_string(),
3956                }),
3957            }
3958        },
3959    )
3960}
3961
3962fn compute_normalized_volume_true_range_batch(
3963    req: IndicatorBatchRequest<'_>,
3964    output_id: &str,
3965) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3966    let (open, high, low, close, volume) =
3967        extract_ohlcv_full_input("normalized_volume_true_range", req.data)?;
3968    let kernel = req.kernel.to_non_batch();
3969    collect_f64(
3970        "normalized_volume_true_range",
3971        output_id,
3972        req.combos,
3973        close.len(),
3974        |params| {
3975            let true_range_style = match find_param(params, "true_range_style") {
3976                Some(ParamValue::EnumString(value)) => Some(
3977                    value
3978                        .parse::<NormalizedVolumeTrueRangeStyle>()
3979                        .map_err(|e| IndicatorDispatchError::InvalidParam {
3980                            indicator: "normalized_volume_true_range".to_string(),
3981                            key: "true_range_style".to_string(),
3982                            reason: e,
3983                        })?,
3984                ),
3985                Some(_) => {
3986                    return Err(IndicatorDispatchError::InvalidParam {
3987                        indicator: "normalized_volume_true_range".to_string(),
3988                        key: "true_range_style".to_string(),
3989                        reason: "expected enum string".to_string(),
3990                    });
3991                }
3992                None => Some(NormalizedVolumeTrueRangeStyle::Body),
3993            };
3994            let outlier_range =
3995                get_f64_param("normalized_volume_true_range", params, "outlier_range", 5.0)?;
3996            let atr_length =
3997                get_usize_param("normalized_volume_true_range", params, "atr_length", 14)?;
3998            let volume_length =
3999                get_usize_param("normalized_volume_true_range", params, "volume_length", 14)?;
4000
4001            let input = NormalizedVolumeTrueRangeInput::from_slices(
4002                open,
4003                high,
4004                low,
4005                close,
4006                volume,
4007                NormalizedVolumeTrueRangeParams {
4008                    true_range_style,
4009                    outlier_range: Some(outlier_range),
4010                    atr_length: Some(atr_length),
4011                    volume_length: Some(volume_length),
4012                },
4013            );
4014            let out = normalized_volume_true_range_with_kernel(&input, kernel).map_err(|e| {
4015                IndicatorDispatchError::ComputeFailed {
4016                    indicator: "normalized_volume_true_range".to_string(),
4017                    details: e.to_string(),
4018                }
4019            })?;
4020            if output_id.eq_ignore_ascii_case("normalized_volume")
4021                || output_id.eq_ignore_ascii_case("value")
4022            {
4023                return Ok(out.normalized_volume);
4024            }
4025            if output_id.eq_ignore_ascii_case("normalized_true_range") {
4026                return Ok(out.normalized_true_range);
4027            }
4028            if output_id.eq_ignore_ascii_case("baseline") {
4029                return Ok(out.baseline);
4030            }
4031            if output_id.eq_ignore_ascii_case("atr") {
4032                return Ok(out.atr);
4033            }
4034            if output_id.eq_ignore_ascii_case("average_volume") {
4035                return Ok(out.average_volume);
4036            }
4037            Err(IndicatorDispatchError::UnknownOutput {
4038                indicator: "normalized_volume_true_range".to_string(),
4039                output: output_id.to_string(),
4040            })
4041        },
4042    )
4043}
4044
4045fn compute_range_breakout_signals_batch(
4046    req: IndicatorBatchRequest<'_>,
4047    output_id: &str,
4048) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4049    let (open, high, low, close, volume) =
4050        extract_ohlcv_full_input("range_breakout_signals", req.data)?;
4051    let kernel = req.kernel.to_non_batch();
4052    collect_f64(
4053        "range_breakout_signals",
4054        output_id,
4055        req.combos,
4056        close.len(),
4057        |params| {
4058            let range_length =
4059                get_usize_param("range_breakout_signals", params, "range_length", 20)?;
4060            let confirmation_length =
4061                get_usize_param("range_breakout_signals", params, "confirmation_length", 5)?;
4062            let input = RangeBreakoutSignalsInput::from_slices(
4063                open,
4064                high,
4065                low,
4066                close,
4067                volume,
4068                RangeBreakoutSignalsParams {
4069                    range_length: Some(range_length),
4070                    confirmation_length: Some(confirmation_length),
4071                },
4072            );
4073            let out = range_breakout_signals_with_kernel(&input, kernel).map_err(|e| {
4074                IndicatorDispatchError::ComputeFailed {
4075                    indicator: "range_breakout_signals".to_string(),
4076                    details: e.to_string(),
4077                }
4078            })?;
4079            if output_id.eq_ignore_ascii_case("range_top")
4080                || output_id.eq_ignore_ascii_case("value")
4081            {
4082                return Ok(out.range_top);
4083            }
4084            if output_id.eq_ignore_ascii_case("range_bottom") {
4085                return Ok(out.range_bottom);
4086            }
4087            if output_id.eq_ignore_ascii_case("bullish") {
4088                return Ok(out.bullish);
4089            }
4090            if output_id.eq_ignore_ascii_case("extra_bullish") {
4091                return Ok(out.extra_bullish);
4092            }
4093            if output_id.eq_ignore_ascii_case("bearish") {
4094                return Ok(out.bearish);
4095            }
4096            if output_id.eq_ignore_ascii_case("extra_bearish") {
4097                return Ok(out.extra_bearish);
4098            }
4099            Err(IndicatorDispatchError::UnknownOutput {
4100                indicator: "range_breakout_signals".to_string(),
4101                output: output_id.to_string(),
4102            })
4103        },
4104    )
4105}
4106
4107fn compute_exponential_trend_batch(
4108    req: IndicatorBatchRequest<'_>,
4109    output_id: &str,
4110) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4111    let (high, low, close) = extract_ohlc_input("exponential_trend", req.data)?;
4112    let kernel = req.kernel.to_non_batch();
4113    collect_f64(
4114        "exponential_trend",
4115        output_id,
4116        req.combos,
4117        close.len(),
4118        |params| {
4119            let exp_rate = get_f64_param("exponential_trend", params, "exp_rate", 0.00003)?;
4120            let initial_distance =
4121                get_f64_param("exponential_trend", params, "initial_distance", 4.0)?;
4122            let width_multiplier =
4123                get_f64_param("exponential_trend", params, "width_multiplier", 1.0)?;
4124            let input = ExponentialTrendInput::from_slices(
4125                high,
4126                low,
4127                close,
4128                ExponentialTrendParams {
4129                    exp_rate: Some(exp_rate),
4130                    initial_distance: Some(initial_distance),
4131                    width_multiplier: Some(width_multiplier),
4132                },
4133            );
4134            let out = exponential_trend_with_kernel(&input, kernel).map_err(|e| {
4135                IndicatorDispatchError::ComputeFailed {
4136                    indicator: "exponential_trend".to_string(),
4137                    details: e.to_string(),
4138                }
4139            })?;
4140            if output_id.eq_ignore_ascii_case("uptrend_base")
4141                || output_id.eq_ignore_ascii_case("value")
4142            {
4143                return Ok(out.uptrend_base);
4144            }
4145            if output_id.eq_ignore_ascii_case("downtrend_base") {
4146                return Ok(out.downtrend_base);
4147            }
4148            if output_id.eq_ignore_ascii_case("uptrend_extension") {
4149                return Ok(out.uptrend_extension);
4150            }
4151            if output_id.eq_ignore_ascii_case("downtrend_extension") {
4152                return Ok(out.downtrend_extension);
4153            }
4154            if output_id.eq_ignore_ascii_case("bullish_change") {
4155                return Ok(out.bullish_change);
4156            }
4157            if output_id.eq_ignore_ascii_case("bearish_change") {
4158                return Ok(out.bearish_change);
4159            }
4160            Err(IndicatorDispatchError::UnknownOutput {
4161                indicator: "exponential_trend".to_string(),
4162                output: output_id.to_string(),
4163            })
4164        },
4165    )
4166}
4167
4168fn compute_trend_flow_trail_batch(
4169    req: IndicatorBatchRequest<'_>,
4170    output_id: &str,
4171) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4172    let (open, high, low, close, volume) = extract_ohlcv_full_input("trend_flow_trail", req.data)?;
4173    let kernel = req.kernel.to_non_batch();
4174    collect_f64(
4175        "trend_flow_trail",
4176        output_id,
4177        req.combos,
4178        close.len(),
4179        |params| {
4180            let alpha_length = get_usize_param("trend_flow_trail", params, "alpha_length", 33)?;
4181            let alpha_multiplier =
4182                get_f64_param("trend_flow_trail", params, "alpha_multiplier", 3.3)?;
4183            let mfi_length = get_usize_param("trend_flow_trail", params, "mfi_length", 14)?;
4184            let input = TrendFlowTrailInput::from_slices(
4185                open,
4186                high,
4187                low,
4188                close,
4189                volume,
4190                TrendFlowTrailParams {
4191                    alpha_length: Some(alpha_length),
4192                    alpha_multiplier: Some(alpha_multiplier),
4193                    mfi_length: Some(mfi_length),
4194                },
4195            );
4196            let out = trend_flow_trail_with_kernel(&input, kernel).map_err(|e| {
4197                IndicatorDispatchError::ComputeFailed {
4198                    indicator: "trend_flow_trail".to_string(),
4199                    details: e.to_string(),
4200                }
4201            })?;
4202            if output_id.eq_ignore_ascii_case("alpha_trail")
4203                || output_id.eq_ignore_ascii_case("value")
4204            {
4205                return Ok(out.alpha_trail);
4206            }
4207            if output_id.eq_ignore_ascii_case("alpha_trail_bullish") {
4208                return Ok(out.alpha_trail_bullish);
4209            }
4210            if output_id.eq_ignore_ascii_case("alpha_trail_bearish") {
4211                return Ok(out.alpha_trail_bearish);
4212            }
4213            if output_id.eq_ignore_ascii_case("alpha_dir") {
4214                return Ok(out.alpha_dir);
4215            }
4216            if output_id.eq_ignore_ascii_case("mfi") {
4217                return Ok(out.mfi);
4218            }
4219            if output_id.eq_ignore_ascii_case("tp_upper") {
4220                return Ok(out.tp_upper);
4221            }
4222            if output_id.eq_ignore_ascii_case("tp_lower") {
4223                return Ok(out.tp_lower);
4224            }
4225            if output_id.eq_ignore_ascii_case("alpha_trail_bullish_switch") {
4226                return Ok(out.alpha_trail_bullish_switch);
4227            }
4228            if output_id.eq_ignore_ascii_case("alpha_trail_bearish_switch") {
4229                return Ok(out.alpha_trail_bearish_switch);
4230            }
4231            if output_id.eq_ignore_ascii_case("mfi_overbought") {
4232                return Ok(out.mfi_overbought);
4233            }
4234            if output_id.eq_ignore_ascii_case("mfi_oversold") {
4235                return Ok(out.mfi_oversold);
4236            }
4237            if output_id.eq_ignore_ascii_case("mfi_cross_up_mid") {
4238                return Ok(out.mfi_cross_up_mid);
4239            }
4240            if output_id.eq_ignore_ascii_case("mfi_cross_down_mid") {
4241                return Ok(out.mfi_cross_down_mid);
4242            }
4243            if output_id.eq_ignore_ascii_case("price_cross_alpha_trail_up") {
4244                return Ok(out.price_cross_alpha_trail_up);
4245            }
4246            if output_id.eq_ignore_ascii_case("price_cross_alpha_trail_down") {
4247                return Ok(out.price_cross_alpha_trail_down);
4248            }
4249            if output_id.eq_ignore_ascii_case("mfi_above_90") {
4250                return Ok(out.mfi_above_90);
4251            }
4252            if output_id.eq_ignore_ascii_case("mfi_below_10") {
4253                return Ok(out.mfi_below_10);
4254            }
4255            Err(IndicatorDispatchError::UnknownOutput {
4256                indicator: "trend_flow_trail".to_string(),
4257                output: output_id.to_string(),
4258            })
4259        },
4260    )
4261}
4262
4263fn compute_cmo_batch(
4264    req: IndicatorBatchRequest<'_>,
4265    output_id: &str,
4266) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4267    expect_value_output("cmo", output_id)?;
4268    let data = extract_slice_input("cmo", req.data, "close")?;
4269    let kernel = req.kernel.to_non_batch();
4270    collect_f64("cmo", output_id, req.combos, data.len(), |params| {
4271        let period = get_usize_param("cmo", params, "period", 14)?;
4272        let input = CmoInput::from_slice(
4273            data,
4274            CmoParams {
4275                period: Some(period),
4276            },
4277        );
4278        let out =
4279            cmo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4280                indicator: "cmo".to_string(),
4281                details: e.to_string(),
4282            })?;
4283        Ok(out.values)
4284    })
4285}
4286
4287fn compute_rocp_batch(
4288    req: IndicatorBatchRequest<'_>,
4289    output_id: &str,
4290) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4291    expect_value_output("rocp", output_id)?;
4292    let data = extract_slice_input("rocp", req.data, "close")?;
4293    let kernel = req.kernel.to_non_batch();
4294    collect_f64("rocp", output_id, req.combos, data.len(), |params| {
4295        let period = get_usize_param("rocp", params, "period", 10)?;
4296        let input = RocpInput::from_slice(
4297            data,
4298            RocpParams {
4299                period: Some(period),
4300            },
4301        );
4302        let out = rocp_with_kernel(&input, kernel).map_err(|e| {
4303            IndicatorDispatchError::ComputeFailed {
4304                indicator: "rocp".to_string(),
4305                details: e.to_string(),
4306            }
4307        })?;
4308        Ok(out.values)
4309    })
4310}
4311
4312fn compute_rocr_batch(
4313    req: IndicatorBatchRequest<'_>,
4314    output_id: &str,
4315) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4316    expect_value_output("rocr", output_id)?;
4317    let data = extract_slice_input("rocr", req.data, "close")?;
4318    let kernel = req.kernel.to_non_batch();
4319    collect_f64("rocr", output_id, req.combos, data.len(), |params| {
4320        let period = get_usize_param("rocr", params, "period", 10)?;
4321        let input = RocrInput::from_slice(
4322            data,
4323            RocrParams {
4324                period: Some(period),
4325            },
4326        );
4327        let out = rocr_with_kernel(&input, kernel).map_err(|e| {
4328            IndicatorDispatchError::ComputeFailed {
4329                indicator: "rocr".to_string(),
4330                details: e.to_string(),
4331            }
4332        })?;
4333        Ok(out.values)
4334    })
4335}
4336
4337fn compute_ppo_batch(
4338    req: IndicatorBatchRequest<'_>,
4339    output_id: &str,
4340) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4341    expect_value_output("ppo", output_id)?;
4342    let data = extract_slice_input("ppo", req.data, "close")?;
4343    let kernel = req.kernel.to_non_batch();
4344    collect_f64("ppo", output_id, req.combos, data.len(), |params| {
4345        let fast_period = get_usize_param("ppo", params, "fast_period", 12)?;
4346        let slow_period = get_usize_param("ppo", params, "slow_period", 26)?;
4347        let ma_type = get_enum_param("ppo", params, "ma_type", "sma")?;
4348        let input = PpoInput::from_slice(
4349            data,
4350            PpoParams {
4351                fast_period: Some(fast_period),
4352                slow_period: Some(slow_period),
4353                ma_type: Some(ma_type),
4354            },
4355        );
4356        let out =
4357            ppo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4358                indicator: "ppo".to_string(),
4359                details: e.to_string(),
4360            })?;
4361        Ok(out.values)
4362    })
4363}
4364
4365fn compute_trix_batch(
4366    req: IndicatorBatchRequest<'_>,
4367    output_id: &str,
4368) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4369    expect_value_output("trix", output_id)?;
4370    let data = extract_slice_input("trix", req.data, "close")?;
4371    let periods = combo_periods("trix", req.combos, "period", 18)?;
4372    if let Some((start, end, step)) = derive_period_sweep(&periods) {
4373        let out = trix_batch_with_kernel(
4374            data,
4375            &TrixBatchRange {
4376                period: (start, end, step),
4377            },
4378            to_batch_kernel(req.kernel),
4379        )
4380        .map_err(|e| IndicatorDispatchError::ComputeFailed {
4381            indicator: "trix".to_string(),
4382            details: e.to_string(),
4383        })?;
4384        ensure_len("trix", data.len(), out.cols)?;
4385        let produced_periods: Vec<usize> = out
4386            .combos
4387            .iter()
4388            .map(|combo| combo.period.unwrap_or(18))
4389            .collect();
4390        let values = reorder_or_take_f64_matrix_by_period(
4391            "trix",
4392            &periods,
4393            &produced_periods,
4394            out.cols,
4395            out.values,
4396        )?;
4397        return Ok(f64_output(output_id, periods.len(), out.cols, values));
4398    }
4399
4400    let kernel = req.kernel.to_non_batch();
4401    collect_f64_into_rows("trix", output_id, req.combos, data.len(), |params, row| {
4402        let period = get_usize_param("trix", params, "period", 18)?;
4403        let input = TrixInput::from_slice(
4404            data,
4405            TrixParams {
4406                period: Some(period),
4407            },
4408        );
4409        trix_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4410            indicator: "trix".to_string(),
4411            details: e.to_string(),
4412        })
4413    })
4414}
4415
4416fn compute_tsi_batch(
4417    req: IndicatorBatchRequest<'_>,
4418    output_id: &str,
4419) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4420    expect_value_output("tsi", output_id)?;
4421    let data = extract_slice_input("tsi", req.data, "close")?;
4422    let kernel = req.kernel.to_non_batch();
4423    collect_f64("tsi", output_id, req.combos, data.len(), |params| {
4424        let long_period = get_usize_param("tsi", params, "long_period", 25)?;
4425        let short_period = get_usize_param("tsi", params, "short_period", 13)?;
4426        let input = TsiInput::from_slice(
4427            data,
4428            TsiParams {
4429                long_period: Some(long_period),
4430                short_period: Some(short_period),
4431            },
4432        );
4433        let out =
4434            tsi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4435                indicator: "tsi".to_string(),
4436                details: e.to_string(),
4437            })?;
4438        Ok(out.values)
4439    })
4440}
4441
4442fn compute_tsf_batch(
4443    req: IndicatorBatchRequest<'_>,
4444    output_id: &str,
4445) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4446    expect_value_output("tsf", output_id)?;
4447    let data = extract_slice_input("tsf", req.data, "close")?;
4448    let kernel = req.kernel.to_non_batch();
4449    collect_f64("tsf", output_id, req.combos, data.len(), |params| {
4450        let period = get_usize_param("tsf", params, "period", 14)?;
4451        let input = TsfInput::from_slice(
4452            data,
4453            TsfParams {
4454                period: Some(period),
4455            },
4456        );
4457        let out =
4458            tsf_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4459                indicator: "tsf".to_string(),
4460                details: e.to_string(),
4461            })?;
4462        Ok(out.values)
4463    })
4464}
4465
4466fn compute_polynomial_regression_extrapolation_batch(
4467    req: IndicatorBatchRequest<'_>,
4468    output_id: &str,
4469) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4470    expect_value_output("polynomial_regression_extrapolation", output_id)?;
4471    let data = extract_slice_input("polynomial_regression_extrapolation", req.data, "close")?;
4472    let kernel = req.kernel.to_non_batch();
4473    collect_f64(
4474        "polynomial_regression_extrapolation",
4475        output_id,
4476        req.combos,
4477        data.len(),
4478        |params| {
4479            let length =
4480                get_usize_param("polynomial_regression_extrapolation", params, "length", 100)?;
4481            let extrapolate = get_usize_param(
4482                "polynomial_regression_extrapolation",
4483                params,
4484                "extrapolate",
4485                10,
4486            )?;
4487            let degree =
4488                get_usize_param("polynomial_regression_extrapolation", params, "degree", 3)?;
4489            let input = PolynomialRegressionExtrapolationInput::from_slice(
4490                data,
4491                PolynomialRegressionExtrapolationParams {
4492                    length: Some(length),
4493                    extrapolate: Some(extrapolate),
4494                    degree: Some(degree),
4495                },
4496            );
4497            let out =
4498                polynomial_regression_extrapolation_with_kernel(&input, kernel).map_err(|e| {
4499                    IndicatorDispatchError::ComputeFailed {
4500                        indicator: "polynomial_regression_extrapolation".to_string(),
4501                        details: e.to_string(),
4502                    }
4503                })?;
4504            Ok(out.values)
4505        },
4506    )
4507}
4508
4509fn compute_adaptive_macd_batch(
4510    req: IndicatorBatchRequest<'_>,
4511    output_id: &str,
4512) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4513    let data = extract_slice_input("adaptive_macd", req.data, "close")?;
4514    let kernel = req.kernel.to_non_batch();
4515    collect_f64(
4516        "adaptive_macd",
4517        output_id,
4518        req.combos,
4519        data.len(),
4520        |params| {
4521            let length = get_usize_param("adaptive_macd", params, "length", 20)?;
4522            let fast_period = get_usize_param("adaptive_macd", params, "fast_period", 10)?;
4523            let slow_period = get_usize_param("adaptive_macd", params, "slow_period", 20)?;
4524            let signal_period = get_usize_param("adaptive_macd", params, "signal_period", 9)?;
4525            let input = AdaptiveMacdInput::from_slice(
4526                data,
4527                AdaptiveMacdParams {
4528                    length: Some(length),
4529                    fast_period: Some(fast_period),
4530                    slow_period: Some(slow_period),
4531                    signal_period: Some(signal_period),
4532                },
4533            );
4534            let out = adaptive_macd_with_kernel(&input, kernel).map_err(|e| {
4535                IndicatorDispatchError::ComputeFailed {
4536                    indicator: "adaptive_macd".to_string(),
4537                    details: e.to_string(),
4538                }
4539            })?;
4540            if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
4541                return Ok(out.macd);
4542            }
4543            if output_id.eq_ignore_ascii_case("signal") {
4544                return Ok(out.signal);
4545            }
4546            if output_id.eq_ignore_ascii_case("hist") {
4547                return Ok(out.hist);
4548            }
4549            Err(IndicatorDispatchError::UnknownOutput {
4550                indicator: "adaptive_macd".to_string(),
4551                output: output_id.to_string(),
4552            })
4553        },
4554    )
4555}
4556
4557fn compute_statistical_trailing_stop_batch(
4558    req: IndicatorBatchRequest<'_>,
4559    output_id: &str,
4560) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4561    let (high, low, close) = extract_ohlc_input("statistical_trailing_stop", req.data)?;
4562    let kernel = req.kernel.to_non_batch();
4563    collect_f64(
4564        "statistical_trailing_stop",
4565        output_id,
4566        req.combos,
4567        close.len(),
4568        |params| {
4569            let data_length =
4570                get_usize_param("statistical_trailing_stop", params, "data_length", 10)?;
4571            let normalization_length = get_usize_param(
4572                "statistical_trailing_stop",
4573                params,
4574                "normalization_length",
4575                100,
4576            )?;
4577            let base_level =
4578                get_enum_param("statistical_trailing_stop", params, "base_level", "level2")?;
4579            let input = StatisticalTrailingStopInput::from_slices(
4580                high,
4581                low,
4582                close,
4583                StatisticalTrailingStopParams {
4584                    data_length: Some(data_length),
4585                    normalization_length: Some(normalization_length),
4586                    base_level: Some(base_level),
4587                },
4588            );
4589            let out = statistical_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
4590                IndicatorDispatchError::ComputeFailed {
4591                    indicator: "statistical_trailing_stop".to_string(),
4592                    details: e.to_string(),
4593                }
4594            })?;
4595            if output_id.eq_ignore_ascii_case("level") || output_id.eq_ignore_ascii_case("value") {
4596                return Ok(out.level);
4597            }
4598            if output_id.eq_ignore_ascii_case("anchor") {
4599                return Ok(out.anchor);
4600            }
4601            if output_id.eq_ignore_ascii_case("bias") {
4602                return Ok(out.bias);
4603            }
4604            if output_id.eq_ignore_ascii_case("changed") {
4605                return Ok(out.changed);
4606            }
4607            Err(IndicatorDispatchError::UnknownOutput {
4608                indicator: "statistical_trailing_stop".to_string(),
4609                output: output_id.to_string(),
4610            })
4611        },
4612    )
4613}
4614
4615fn compute_supertrend_recovery_batch(
4616    req: IndicatorBatchRequest<'_>,
4617    output_id: &str,
4618) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4619    let (high, low, close) = extract_ohlc_input("supertrend_recovery", req.data)?;
4620    let kernel = req.kernel.to_non_batch();
4621    collect_f64(
4622        "supertrend_recovery",
4623        output_id,
4624        req.combos,
4625        close.len(),
4626        |params| {
4627            let atr_length = get_usize_param("supertrend_recovery", params, "atr_length", 10)?;
4628            let multiplier = get_f64_param("supertrend_recovery", params, "multiplier", 3.0)?;
4629            let alpha_percent = get_f64_param("supertrend_recovery", params, "alpha_percent", 5.0)?;
4630            let threshold_atr = get_f64_param("supertrend_recovery", params, "threshold_atr", 1.0)?;
4631            let input = SuperTrendRecoveryInput::from_slices(
4632                high,
4633                low,
4634                close,
4635                SuperTrendRecoveryParams {
4636                    atr_length: Some(atr_length),
4637                    multiplier: Some(multiplier),
4638                    alpha_percent: Some(alpha_percent),
4639                    threshold_atr: Some(threshold_atr),
4640                },
4641            );
4642            let out = supertrend_recovery_with_kernel(&input, kernel).map_err(|e| {
4643                IndicatorDispatchError::ComputeFailed {
4644                    indicator: "supertrend_recovery".to_string(),
4645                    details: e.to_string(),
4646                }
4647            })?;
4648            if output_id.eq_ignore_ascii_case("band") || output_id.eq_ignore_ascii_case("value") {
4649                return Ok(out.band);
4650            }
4651            if output_id.eq_ignore_ascii_case("switch_price") {
4652                return Ok(out.switch_price);
4653            }
4654            if output_id.eq_ignore_ascii_case("trend") {
4655                return Ok(out.trend);
4656            }
4657            if output_id.eq_ignore_ascii_case("changed") {
4658                return Ok(out.changed);
4659            }
4660            Err(IndicatorDispatchError::UnknownOutput {
4661                indicator: "supertrend_recovery".to_string(),
4662                output: output_id.to_string(),
4663            })
4664        },
4665    )
4666}
4667
4668fn compute_standardized_psar_oscillator_batch(
4669    req: IndicatorBatchRequest<'_>,
4670    output_id: &str,
4671) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4672    let (high, low, close) = extract_ohlc_input("standardized_psar_oscillator", req.data)?;
4673    let kernel = req.kernel.to_non_batch();
4674    collect_f64(
4675        "standardized_psar_oscillator",
4676        output_id,
4677        req.combos,
4678        close.len(),
4679        |params| {
4680            let start = get_f64_param("standardized_psar_oscillator", params, "start", 0.02)?;
4681            let increment =
4682                get_f64_param("standardized_psar_oscillator", params, "increment", 0.0005)?;
4683            let maximum = get_f64_param("standardized_psar_oscillator", params, "maximum", 0.2)?;
4684            let standardization_length = get_usize_param(
4685                "standardized_psar_oscillator",
4686                params,
4687                "standardization_length",
4688                21,
4689            )?;
4690            let wma_length =
4691                get_usize_param("standardized_psar_oscillator", params, "wma_length", 40)?;
4692            let wma_lag = get_usize_param("standardized_psar_oscillator", params, "wma_lag", 3)?;
4693            let pivot_left =
4694                get_usize_param("standardized_psar_oscillator", params, "pivot_left", 15)?;
4695            let pivot_right =
4696                get_usize_param("standardized_psar_oscillator", params, "pivot_right", 1)?;
4697            let plot_bullish =
4698                get_bool_param("standardized_psar_oscillator", params, "plot_bullish", true)?;
4699            let plot_bearish =
4700                get_bool_param("standardized_psar_oscillator", params, "plot_bearish", true)?;
4701            let input = StandardizedPsarOscillatorInput::from_slices(
4702                high,
4703                low,
4704                close,
4705                StandardizedPsarOscillatorParams {
4706                    start: Some(start),
4707                    increment: Some(increment),
4708                    maximum: Some(maximum),
4709                    standardization_length: Some(standardization_length),
4710                    wma_length: Some(wma_length),
4711                    wma_lag: Some(wma_lag),
4712                    pivot_left: Some(pivot_left),
4713                    pivot_right: Some(pivot_right),
4714                    plot_bullish: Some(plot_bullish),
4715                    plot_bearish: Some(plot_bearish),
4716                },
4717            );
4718            let out = standardized_psar_oscillator_with_kernel(&input, kernel).map_err(|e| {
4719                IndicatorDispatchError::ComputeFailed {
4720                    indicator: "standardized_psar_oscillator".to_string(),
4721                    details: e.to_string(),
4722                }
4723            })?;
4724            match output_id {
4725                "oscillator" | "value" => Ok(out.oscillator),
4726                "ma" => Ok(out.ma),
4727                "bullish_reversal" => Ok(out.bullish_reversal),
4728                "bearish_reversal" => Ok(out.bearish_reversal),
4729                "regular_bullish" => Ok(out.regular_bullish),
4730                "regular_bearish" => Ok(out.regular_bearish),
4731                "bullish_weakening" => Ok(out.bullish_weakening),
4732                "bearish_weakening" => Ok(out.bearish_weakening),
4733                _ => Err(IndicatorDispatchError::UnknownOutput {
4734                    indicator: "standardized_psar_oscillator".to_string(),
4735                    output: output_id.to_string(),
4736                }),
4737            }
4738        },
4739    )
4740}
4741
4742fn compute_geometric_bias_oscillator_batch(
4743    req: IndicatorBatchRequest<'_>,
4744    output_id: &str,
4745) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4746    expect_value_output("geometric_bias_oscillator", output_id)?;
4747    let (high, low, close) = extract_ohlc_input("geometric_bias_oscillator", req.data)?;
4748    let kernel = req.kernel.to_non_batch();
4749    collect_f64(
4750        "geometric_bias_oscillator",
4751        output_id,
4752        req.combos,
4753        close.len(),
4754        |params| {
4755            let length = get_usize_param("geometric_bias_oscillator", params, "length", 100)?;
4756            let multiplier = get_f64_param("geometric_bias_oscillator", params, "multiplier", 2.0)?;
4757            let atr_length =
4758                get_usize_param("geometric_bias_oscillator", params, "atr_length", 14)?;
4759            let smooth = get_usize_param("geometric_bias_oscillator", params, "smooth", 1)?;
4760            let input = GeometricBiasOscillatorInput::from_slices(
4761                high,
4762                low,
4763                close,
4764                GeometricBiasOscillatorParams {
4765                    length: Some(length),
4766                    multiplier: Some(multiplier),
4767                    atr_length: Some(atr_length),
4768                    smooth: Some(smooth),
4769                },
4770            );
4771            let out = geometric_bias_oscillator_with_kernel(&input, kernel).map_err(|e| {
4772                IndicatorDispatchError::ComputeFailed {
4773                    indicator: "geometric_bias_oscillator".to_string(),
4774                    details: e.to_string(),
4775                }
4776            })?;
4777            Ok(out.values)
4778        },
4779    )
4780}
4781
4782fn compute_stddev_batch(
4783    req: IndicatorBatchRequest<'_>,
4784    output_id: &str,
4785) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4786    expect_value_output("stddev", output_id)?;
4787    let data = extract_slice_input("stddev", req.data, "close")?;
4788    let kernel = req.kernel.to_non_batch();
4789    collect_f64("stddev", output_id, req.combos, data.len(), |params| {
4790        let period = get_usize_param("stddev", params, "period", 5)?;
4791        let nbdev = get_f64_param("stddev", params, "nbdev", 1.0)?;
4792        let input = StdDevInput::from_slice(
4793            data,
4794            StdDevParams {
4795                period: Some(period),
4796                nbdev: Some(nbdev),
4797            },
4798        );
4799        let out = stddev_with_kernel(&input, kernel).map_err(|e| {
4800            IndicatorDispatchError::ComputeFailed {
4801                indicator: "stddev".to_string(),
4802                details: e.to_string(),
4803            }
4804        })?;
4805        Ok(out.values)
4806    })
4807}
4808
4809fn compute_vdubus_divergence_wave_pattern_generator_batch(
4810    req: IndicatorBatchRequest<'_>,
4811    output_id: &str,
4812) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4813    expect_value_output("vdubus_divergence_wave_pattern_generator", output_id)?;
4814    let (high, low, close) =
4815        extract_ohlc_input("vdubus_divergence_wave_pattern_generator", req.data)?;
4816    let kernel = req.kernel.to_non_batch();
4817    collect_f64(
4818        "vdubus_divergence_wave_pattern_generator",
4819        output_id,
4820        req.combos,
4821        close.len(),
4822        |params| {
4823            let fast_depth = get_usize_param(
4824                "vdubus_divergence_wave_pattern_generator",
4825                params,
4826                "fast_depth",
4827                9,
4828            )?;
4829            let slow_depth = get_usize_param(
4830                "vdubus_divergence_wave_pattern_generator",
4831                params,
4832                "slow_depth",
4833                24,
4834            )?;
4835            let fast_length = get_usize_param(
4836                "vdubus_divergence_wave_pattern_generator",
4837                params,
4838                "fast_length",
4839                21,
4840            )?;
4841            let slow_length = get_usize_param(
4842                "vdubus_divergence_wave_pattern_generator",
4843                params,
4844                "slow_length",
4845                34,
4846            )?;
4847            let signal_length = get_usize_param(
4848                "vdubus_divergence_wave_pattern_generator",
4849                params,
4850                "signal_length",
4851                5,
4852            )?;
4853            let lookback = get_usize_param(
4854                "vdubus_divergence_wave_pattern_generator",
4855                params,
4856                "lookback",
4857                3,
4858            )?;
4859            let err_tol = get_f64_param(
4860                "vdubus_divergence_wave_pattern_generator",
4861                params,
4862                "err_tol",
4863                0.15,
4864            )?;
4865            let show_standard = get_bool_param(
4866                "vdubus_divergence_wave_pattern_generator",
4867                params,
4868                "show_standard",
4869                true,
4870            )?;
4871            let show_climax = get_bool_param(
4872                "vdubus_divergence_wave_pattern_generator",
4873                params,
4874                "show_climax",
4875                true,
4876            )?;
4877            let show_rounded = get_bool_param(
4878                "vdubus_divergence_wave_pattern_generator",
4879                params,
4880                "show_rounded",
4881                true,
4882            )?;
4883            let show_predator = get_bool_param(
4884                "vdubus_divergence_wave_pattern_generator",
4885                params,
4886                "show_predator",
4887                true,
4888            )?;
4889            let show_gartley = get_bool_param(
4890                "vdubus_divergence_wave_pattern_generator",
4891                params,
4892                "show_gartley",
4893                false,
4894            )?;
4895            let show_bat = get_bool_param(
4896                "vdubus_divergence_wave_pattern_generator",
4897                params,
4898                "show_bat",
4899                false,
4900            )?;
4901            let show_butterfly = get_bool_param(
4902                "vdubus_divergence_wave_pattern_generator",
4903                params,
4904                "show_butterfly",
4905                false,
4906            )?;
4907            let show_crab = get_bool_param(
4908                "vdubus_divergence_wave_pattern_generator",
4909                params,
4910                "show_crab",
4911                false,
4912            )?;
4913            let show_deep = get_bool_param(
4914                "vdubus_divergence_wave_pattern_generator",
4915                params,
4916                "show_deep",
4917                false,
4918            )?;
4919            let show_hs = get_bool_param(
4920                "vdubus_divergence_wave_pattern_generator",
4921                params,
4922                "show_hs",
4923                true,
4924            )?;
4925            let input = VdubusDivergenceWavePatternGeneratorInput::from_slices(
4926                high,
4927                low,
4928                close,
4929                VdubusDivergenceWavePatternGeneratorParams {
4930                    fast_depth: Some(fast_depth),
4931                    slow_depth: Some(slow_depth),
4932                    fast_length: Some(fast_length),
4933                    slow_length: Some(slow_length),
4934                    signal_length: Some(signal_length),
4935                    lookback: Some(lookback),
4936                    err_tol: Some(err_tol),
4937                    show_standard: Some(show_standard),
4938                    show_climax: Some(show_climax),
4939                    show_rounded: Some(show_rounded),
4940                    show_predator: Some(show_predator),
4941                    show_gartley: Some(show_gartley),
4942                    show_bat: Some(show_bat),
4943                    show_butterfly: Some(show_butterfly),
4944                    show_crab: Some(show_crab),
4945                    show_deep: Some(show_deep),
4946                    show_hs: Some(show_hs),
4947                },
4948            );
4949            let out = vdubus_divergence_wave_pattern_generator_with_kernel(&input, kernel)
4950                .map_err(|e| IndicatorDispatchError::ComputeFailed {
4951                    indicator: "vdubus_divergence_wave_pattern_generator".to_string(),
4952                    details: e.to_string(),
4953                })?;
4954            match output_id {
4955                "fast_standard" => Ok(out.fast_standard),
4956                "fast_climax" => Ok(out.fast_climax),
4957                "fast_rounded" => Ok(out.fast_rounded),
4958                "fast_predator" => Ok(out.fast_predator),
4959                "slow_standard" => Ok(out.slow_standard),
4960                "slow_climax" => Ok(out.slow_climax),
4961                "slow_rounded" => Ok(out.slow_rounded),
4962                "slow_predator" => Ok(out.slow_predator),
4963                "opposing_force" => Ok(out.opposing_force),
4964                "macd" => Ok(out.macd),
4965                "signal" => Ok(out.signal),
4966                "hist" => Ok(out.hist),
4967                _ => Err(IndicatorDispatchError::UnknownOutput {
4968                    indicator: "vdubus_divergence_wave_pattern_generator".to_string(),
4969                    output: output_id.to_string(),
4970                }),
4971            }
4972        },
4973    )
4974}
4975
4976fn compute_var_batch(
4977    req: IndicatorBatchRequest<'_>,
4978    output_id: &str,
4979) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4980    expect_value_output("var", output_id)?;
4981    let data = extract_slice_input("var", req.data, "close")?;
4982    let kernel = req.kernel.to_non_batch();
4983    collect_f64("var", output_id, req.combos, data.len(), |params| {
4984        let period = get_usize_param("var", params, "period", 14)?;
4985        let nbdev = get_f64_param("var", params, "nbdev", 1.0)?;
4986        let input = VarInput::from_slice(
4987            data,
4988            VarParams {
4989                period: Some(period),
4990                nbdev: Some(nbdev),
4991            },
4992        );
4993        let out =
4994            var_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4995                indicator: "var".to_string(),
4996                details: e.to_string(),
4997            })?;
4998        Ok(out.values)
4999    })
5000}
5001
5002fn compute_willr_batch(
5003    req: IndicatorBatchRequest<'_>,
5004    output_id: &str,
5005) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5006    expect_value_output("willr", output_id)?;
5007    let (high, low, close) = extract_ohlc_input("willr", req.data)?;
5008    let kernel = req.kernel.to_non_batch();
5009    collect_f64("willr", output_id, req.combos, close.len(), |params| {
5010        let period = get_usize_param("willr", params, "period", 14)?;
5011        let input = WillrInput::from_slices(
5012            high,
5013            low,
5014            close,
5015            WillrParams {
5016                period: Some(period),
5017            },
5018        );
5019        let out = willr_with_kernel(&input, kernel).map_err(|e| {
5020            IndicatorDispatchError::ComputeFailed {
5021                indicator: "willr".to_string(),
5022                details: e.to_string(),
5023            }
5024        })?;
5025        Ok(out.values)
5026    })
5027}
5028
5029fn compute_ultosc_batch(
5030    req: IndicatorBatchRequest<'_>,
5031    output_id: &str,
5032) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5033    expect_value_output("ultosc", output_id)?;
5034    let (high, low, close) = extract_ohlc_input("ultosc", req.data)?;
5035    let kernel = req.kernel.to_non_batch();
5036    collect_f64("ultosc", output_id, req.combos, close.len(), |params| {
5037        let timeperiod1 = get_usize_param("ultosc", params, "timeperiod1", 7)?;
5038        let timeperiod2 = get_usize_param("ultosc", params, "timeperiod2", 14)?;
5039        let timeperiod3 = get_usize_param("ultosc", params, "timeperiod3", 28)?;
5040        let input = UltOscInput::from_slices(
5041            high,
5042            low,
5043            close,
5044            UltOscParams {
5045                timeperiod1: Some(timeperiod1),
5046                timeperiod2: Some(timeperiod2),
5047                timeperiod3: Some(timeperiod3),
5048            },
5049        );
5050        let out = ultosc_with_kernel(&input, kernel).map_err(|e| {
5051            IndicatorDispatchError::ComputeFailed {
5052                indicator: "ultosc".to_string(),
5053                details: e.to_string(),
5054            }
5055        })?;
5056        Ok(out.values)
5057    })
5058}
5059
5060fn compute_adx_batch(
5061    req: IndicatorBatchRequest<'_>,
5062    output_id: &str,
5063) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5064    expect_value_output("adx", output_id)?;
5065    let (high, low, close) = extract_ohlc_input("adx", req.data)?;
5066    let kernel = req.kernel.to_non_batch();
5067    collect_f64("adx", output_id, req.combos, close.len(), |params| {
5068        let period = get_usize_param("adx", params, "period", 14)?;
5069        let input = AdxInput::from_slices(
5070            high,
5071            low,
5072            close,
5073            AdxParams {
5074                period: Some(period),
5075            },
5076        );
5077        let out =
5078            adx_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5079                indicator: "adx".to_string(),
5080                details: e.to_string(),
5081            })?;
5082        Ok(out.values)
5083    })
5084}
5085
5086fn compute_adxr_batch(
5087    req: IndicatorBatchRequest<'_>,
5088    output_id: &str,
5089) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5090    expect_value_output("adxr", output_id)?;
5091    let (high, low, close) = extract_ohlc_input("adxr", req.data)?;
5092    let kernel = req.kernel.to_non_batch();
5093    collect_f64("adxr", output_id, req.combos, close.len(), |params| {
5094        let period = get_usize_param("adxr", params, "period", 14)?;
5095        let input = AdxrInput::from_slices(
5096            high,
5097            low,
5098            close,
5099            AdxrParams {
5100                period: Some(period),
5101            },
5102        );
5103        let out = adxr_with_kernel(&input, kernel).map_err(|e| {
5104            IndicatorDispatchError::ComputeFailed {
5105                indicator: "adxr".to_string(),
5106                details: e.to_string(),
5107            }
5108        })?;
5109        Ok(out.values)
5110    })
5111}
5112
5113fn compute_atr_batch(
5114    req: IndicatorBatchRequest<'_>,
5115    output_id: &str,
5116) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5117    expect_value_output("atr", output_id)?;
5118    let (high, low, close) = extract_ohlc_input("atr", req.data)?;
5119    let kernel = req.kernel.to_non_batch();
5120    collect_f64("atr", output_id, req.combos, close.len(), |params| {
5121        let length = get_usize_param("atr", params, "length", 14)?;
5122        let input = AtrInput::from_slices(
5123            high,
5124            low,
5125            close,
5126            AtrParams {
5127                length: Some(length),
5128            },
5129        );
5130        let out =
5131            atr_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5132                indicator: "atr".to_string(),
5133                details: e.to_string(),
5134            })?;
5135        Ok(out.values)
5136    })
5137}
5138
5139fn compute_macd_batch(
5140    req: IndicatorBatchRequest<'_>,
5141    output_id: &str,
5142) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5143    let data = extract_slice_input("macd", req.data, "close")?;
5144    let kernel = req.kernel.to_non_batch();
5145    collect_f64("macd", output_id, req.combos, data.len(), |params| {
5146        let fast_period = get_usize_param("macd", params, "fast_period", 12)?;
5147        let slow_period = get_usize_param("macd", params, "slow_period", 26)?;
5148        let signal_period = get_usize_param("macd", params, "signal_period", 9)?;
5149        let ma_type = get_enum_param("macd", params, "ma_type", "ema")?;
5150        let input = MacdInput::from_slice(
5151            data,
5152            MacdParams {
5153                fast_period: Some(fast_period),
5154                slow_period: Some(slow_period),
5155                signal_period: Some(signal_period),
5156                ma_type: Some(ma_type),
5157            },
5158        );
5159        let out = macd_with_kernel(&input, kernel).map_err(|e| {
5160            IndicatorDispatchError::ComputeFailed {
5161                indicator: "macd".to_string(),
5162                details: e.to_string(),
5163            }
5164        })?;
5165        if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
5166            return Ok(out.macd);
5167        }
5168        if output_id.eq_ignore_ascii_case("signal") {
5169            return Ok(out.signal);
5170        }
5171        if output_id.eq_ignore_ascii_case("hist") {
5172            return Ok(out.hist);
5173        }
5174        Err(IndicatorDispatchError::UnknownOutput {
5175            indicator: "macd".to_string(),
5176            output: output_id.to_string(),
5177        })
5178    })
5179}
5180
5181fn compute_bollinger_batch(
5182    req: IndicatorBatchRequest<'_>,
5183    output_id: &str,
5184) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5185    let data = extract_slice_input("bollinger_bands", req.data, "close")?;
5186    let kernel = req.kernel.to_non_batch();
5187    collect_f64(
5188        "bollinger_bands",
5189        output_id,
5190        req.combos,
5191        data.len(),
5192        |params| {
5193            let period = get_usize_param("bollinger_bands", params, "period", 20)?;
5194            let devup = get_f64_param("bollinger_bands", params, "devup", 2.0)?;
5195            let devdn = get_f64_param("bollinger_bands", params, "devdn", 2.0)?;
5196            let matype = get_enum_param("bollinger_bands", params, "matype", "sma")?;
5197            let devtype = get_usize_param("bollinger_bands", params, "devtype", 0)?;
5198            let input = BollingerBandsInput::from_slice(
5199                data,
5200                BollingerBandsParams {
5201                    period: Some(period),
5202                    devup: Some(devup),
5203                    devdn: Some(devdn),
5204                    matype: Some(matype),
5205                    devtype: Some(devtype),
5206                },
5207            );
5208            let out = bollinger_bands_with_kernel(&input, kernel).map_err(|e| {
5209                IndicatorDispatchError::ComputeFailed {
5210                    indicator: "bollinger_bands".to_string(),
5211                    details: e.to_string(),
5212                }
5213            })?;
5214            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
5215                return Ok(out.upper_band);
5216            }
5217            if output_id.eq_ignore_ascii_case("middle") {
5218                return Ok(out.middle_band);
5219            }
5220            if output_id.eq_ignore_ascii_case("lower") {
5221                return Ok(out.lower_band);
5222            }
5223            Err(IndicatorDispatchError::UnknownOutput {
5224                indicator: "bollinger_bands".to_string(),
5225                output: output_id.to_string(),
5226            })
5227        },
5228    )
5229}
5230
5231fn compute_bbw_batch(
5232    req: IndicatorBatchRequest<'_>,
5233    output_id: &str,
5234) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5235    let data = extract_slice_input("bollinger_bands_width", req.data, "close")?;
5236    let kernel = req.kernel.to_non_batch();
5237    collect_f64(
5238        "bollinger_bands_width",
5239        output_id,
5240        req.combos,
5241        data.len(),
5242        |params| {
5243            let period = get_usize_param("bollinger_bands_width", params, "period", 20)?;
5244            let devup = get_f64_param("bollinger_bands_width", params, "devup", 2.0)?;
5245            let devdn = get_f64_param("bollinger_bands_width", params, "devdn", 2.0)?;
5246            let matype = get_enum_param("bollinger_bands_width", params, "matype", "sma")?;
5247            let devtype = get_usize_param("bollinger_bands_width", params, "devtype", 0)?;
5248            let input = BollingerBandsWidthInput::from_slice(
5249                data,
5250                BollingerBandsWidthParams {
5251                    period: Some(period),
5252                    devup: Some(devup),
5253                    devdn: Some(devdn),
5254                    matype: Some(matype),
5255                    devtype: Some(devtype),
5256                },
5257            );
5258            let out = bollinger_bands_width_with_kernel(&input, kernel).map_err(|e| {
5259                IndicatorDispatchError::ComputeFailed {
5260                    indicator: "bollinger_bands_width".to_string(),
5261                    details: e.to_string(),
5262                }
5263            })?;
5264            if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
5265                return Ok(out.values);
5266            }
5267            Err(IndicatorDispatchError::UnknownOutput {
5268                indicator: "bollinger_bands_width".to_string(),
5269                output: output_id.to_string(),
5270            })
5271        },
5272    )
5273}
5274
5275fn compute_stoch_batch(
5276    req: IndicatorBatchRequest<'_>,
5277    output_id: &str,
5278) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5279    let (high, low, close) = extract_ohlc_input("stoch", req.data)?;
5280    let kernel = req.kernel.to_non_batch();
5281    collect_f64("stoch", output_id, req.combos, close.len(), |params| {
5282        let fastk_period = get_usize_param("stoch", params, "fastk_period", 14)?;
5283        let slowk_period = get_usize_param("stoch", params, "slowk_period", 3)?;
5284        let slowd_period = get_usize_param("stoch", params, "slowd_period", 3)?;
5285        let slowk_ma_type = get_enum_param("stoch", params, "slowk_ma_type", "sma")?;
5286        let slowd_ma_type = get_enum_param("stoch", params, "slowd_ma_type", "sma")?;
5287        let input = StochInput::from_slices(
5288            high,
5289            low,
5290            close,
5291            StochParams {
5292                fastk_period: Some(fastk_period),
5293                slowk_period: Some(slowk_period),
5294                slowk_ma_type: Some(slowk_ma_type),
5295                slowd_period: Some(slowd_period),
5296                slowd_ma_type: Some(slowd_ma_type),
5297            },
5298        );
5299        let out = stoch_with_kernel(&input, kernel).map_err(|e| {
5300            IndicatorDispatchError::ComputeFailed {
5301                indicator: "stoch".to_string(),
5302                details: e.to_string(),
5303            }
5304        })?;
5305        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5306            return Ok(out.k);
5307        }
5308        if output_id.eq_ignore_ascii_case("d") {
5309            return Ok(out.d);
5310        }
5311        Err(IndicatorDispatchError::UnknownOutput {
5312            indicator: "stoch".to_string(),
5313            output: output_id.to_string(),
5314        })
5315    })
5316}
5317
5318fn compute_stochf_batch(
5319    req: IndicatorBatchRequest<'_>,
5320    output_id: &str,
5321) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5322    let (high, low, close) = extract_ohlc_input("stochf", req.data)?;
5323    let kernel = req.kernel.to_non_batch();
5324    collect_f64("stochf", output_id, req.combos, close.len(), |params| {
5325        let fastk_period = get_usize_param("stochf", params, "fastk_period", 5)?;
5326        let fastd_period = get_usize_param("stochf", params, "fastd_period", 3)?;
5327        let fastd_matype = get_usize_param("stochf", params, "fastd_matype", 0)?;
5328        let input = StochfInput::from_slices(
5329            high,
5330            low,
5331            close,
5332            StochfParams {
5333                fastk_period: Some(fastk_period),
5334                fastd_period: Some(fastd_period),
5335                fastd_matype: Some(fastd_matype),
5336            },
5337        );
5338        let out = stochf_with_kernel(&input, kernel).map_err(|e| {
5339            IndicatorDispatchError::ComputeFailed {
5340                indicator: "stochf".to_string(),
5341                details: e.to_string(),
5342            }
5343        })?;
5344        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5345            return Ok(out.k);
5346        }
5347        if output_id.eq_ignore_ascii_case("d") {
5348            return Ok(out.d);
5349        }
5350        Err(IndicatorDispatchError::UnknownOutput {
5351            indicator: "stochf".to_string(),
5352            output: output_id.to_string(),
5353        })
5354    })
5355}
5356
5357fn compute_stochastic_money_flow_index_batch(
5358    req: IndicatorBatchRequest<'_>,
5359    output_id: &str,
5360) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5361    let (source, volume) =
5362        extract_close_volume_input("stochastic_money_flow_index", req.data, "close")?;
5363    let kernel = req.kernel.to_non_batch();
5364    collect_f64(
5365        "stochastic_money_flow_index",
5366        output_id,
5367        req.combos,
5368        source.len(),
5369        |params| {
5370            let stoch_k_length =
5371                get_usize_param("stochastic_money_flow_index", params, "stoch_k_length", 14)?;
5372            let stoch_k_smooth =
5373                get_usize_param("stochastic_money_flow_index", params, "stoch_k_smooth", 3)?;
5374            let stoch_d_smooth =
5375                get_usize_param("stochastic_money_flow_index", params, "stoch_d_smooth", 3)?;
5376            let mfi_length =
5377                get_usize_param("stochastic_money_flow_index", params, "mfi_length", 14)?;
5378            let input = StochasticMoneyFlowIndexInput::from_slices(
5379                source,
5380                volume,
5381                StochasticMoneyFlowIndexParams {
5382                    stoch_k_length: Some(stoch_k_length),
5383                    stoch_k_smooth: Some(stoch_k_smooth),
5384                    stoch_d_smooth: Some(stoch_d_smooth),
5385                    mfi_length: Some(mfi_length),
5386                },
5387            );
5388            let out = stochastic_money_flow_index_with_kernel(&input, kernel).map_err(|e| {
5389                IndicatorDispatchError::ComputeFailed {
5390                    indicator: "stochastic_money_flow_index".to_string(),
5391                    details: e.to_string(),
5392                }
5393            })?;
5394            if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5395                return Ok(out.k);
5396            }
5397            if output_id.eq_ignore_ascii_case("d") {
5398                return Ok(out.d);
5399            }
5400            Err(IndicatorDispatchError::UnknownOutput {
5401                indicator: "stochastic_money_flow_index".to_string(),
5402                output: output_id.to_string(),
5403            })
5404        },
5405    )
5406}
5407
5408fn compute_vwmacd_batch(
5409    req: IndicatorBatchRequest<'_>,
5410    output_id: &str,
5411) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5412    let (close, volume) = extract_close_volume_input("vwmacd", req.data, "close")?;
5413    let kernel = req.kernel.to_non_batch();
5414    collect_f64("vwmacd", output_id, req.combos, close.len(), |params| {
5415        let fast_period =
5416            get_usize_param_with_aliases("vwmacd", params, &["fast", "fast_period"], 12)?;
5417        let slow_period =
5418            get_usize_param_with_aliases("vwmacd", params, &["slow", "slow_period"], 26)?;
5419        let signal_period =
5420            get_usize_param_with_aliases("vwmacd", params, &["signal", "signal_period"], 9)?;
5421        let fast_ma_type = get_enum_param("vwmacd", params, "fast_ma_type", "sma")?;
5422        let slow_ma_type = get_enum_param("vwmacd", params, "slow_ma_type", "sma")?;
5423        let signal_ma_type = get_enum_param("vwmacd", params, "signal_ma_type", "ema")?;
5424        let input = VwmacdInput::from_slices(
5425            close,
5426            volume,
5427            VwmacdParams {
5428                fast_period: Some(fast_period),
5429                slow_period: Some(slow_period),
5430                signal_period: Some(signal_period),
5431                fast_ma_type: Some(fast_ma_type),
5432                slow_ma_type: Some(slow_ma_type),
5433                signal_ma_type: Some(signal_ma_type),
5434            },
5435        );
5436        let out = vwmacd_with_kernel(&input, kernel).map_err(|e| {
5437            IndicatorDispatchError::ComputeFailed {
5438                indicator: "vwmacd".to_string(),
5439                details: e.to_string(),
5440            }
5441        })?;
5442        if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
5443            return Ok(out.macd);
5444        }
5445        if output_id.eq_ignore_ascii_case("signal") {
5446            return Ok(out.signal);
5447        }
5448        if output_id.eq_ignore_ascii_case("hist") {
5449            return Ok(out.hist);
5450        }
5451        Err(IndicatorDispatchError::UnknownOutput {
5452            indicator: "vwmacd".to_string(),
5453            output: output_id.to_string(),
5454        })
5455    })
5456}
5457
5458fn compute_vpci_batch(
5459    req: IndicatorBatchRequest<'_>,
5460    output_id: &str,
5461) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5462    let (close, volume) = extract_close_volume_input("vpci", req.data, "close")?;
5463    let kernel = req.kernel.to_non_batch();
5464    collect_f64("vpci", output_id, req.combos, close.len(), |params| {
5465        let short_range = get_usize_param("vpci", params, "short_range", 5)?;
5466        let long_range = get_usize_param("vpci", params, "long_range", 25)?;
5467        let input = VpciInput::from_slices(
5468            close,
5469            volume,
5470            VpciParams {
5471                short_range: Some(short_range),
5472                long_range: Some(long_range),
5473            },
5474        );
5475        let out = vpci_with_kernel(&input, kernel).map_err(|e| {
5476            IndicatorDispatchError::ComputeFailed {
5477                indicator: "vpci".to_string(),
5478                details: e.to_string(),
5479            }
5480        })?;
5481        if output_id.eq_ignore_ascii_case("vpci") || output_id.eq_ignore_ascii_case("value") {
5482            return Ok(out.vpci);
5483        }
5484        if output_id.eq_ignore_ascii_case("vpcis") {
5485            return Ok(out.vpcis);
5486        }
5487        Err(IndicatorDispatchError::UnknownOutput {
5488            indicator: "vpci".to_string(),
5489            output: output_id.to_string(),
5490        })
5491    })
5492}
5493
5494fn compute_ttm_trend_batch(
5495    req: IndicatorBatchRequest<'_>,
5496    output_id: &str,
5497) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5498    expect_value_output("ttm_trend", output_id)?;
5499    let mut derived_source: Option<Vec<f64>> = None;
5500    let (source, close): (&[f64], &[f64]) = match req.data {
5501        IndicatorDataRef::Candles { candles, source } => (
5502            source_type(candles, source.unwrap_or("hl2")),
5503            candles.close.as_slice(),
5504        ),
5505        IndicatorDataRef::Ohlc {
5506            high, low, close, ..
5507        } => {
5508            ensure_same_len_3("ttm_trend", high.len(), low.len(), close.len())?;
5509            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
5510            (derived_source.as_deref().unwrap_or(close), close)
5511        }
5512        IndicatorDataRef::Ohlcv {
5513            high, low, close, ..
5514        } => {
5515            ensure_same_len_3("ttm_trend", high.len(), low.len(), close.len())?;
5516            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
5517            (derived_source.as_deref().unwrap_or(close), close)
5518        }
5519        _ => {
5520            return Err(IndicatorDispatchError::MissingRequiredInput {
5521                indicator: "ttm_trend".to_string(),
5522                input: IndicatorInputKind::Ohlc,
5523            })
5524        }
5525    };
5526    let kernel = req.kernel.to_non_batch();
5527    collect_bool("ttm_trend", output_id, req.combos, close.len(), |params| {
5528        let period = get_usize_param("ttm_trend", params, "period", 5)?;
5529        let input = TtmTrendInput::from_slices(
5530            source,
5531            close,
5532            TtmTrendParams {
5533                period: Some(period),
5534            },
5535        );
5536        let out = ttm_trend_with_kernel(&input, kernel).map_err(|e| {
5537            IndicatorDispatchError::ComputeFailed {
5538                indicator: "ttm_trend".to_string(),
5539                details: e.to_string(),
5540            }
5541        })?;
5542        Ok(out.values)
5543    })
5544}
5545
5546fn compute_ttm_squeeze_batch(
5547    req: IndicatorBatchRequest<'_>,
5548    output_id: &str,
5549) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5550    let (high, low, close) = extract_ohlc_input("ttm_squeeze", req.data)?;
5551    let kernel = req.kernel.to_non_batch();
5552    collect_f64(
5553        "ttm_squeeze",
5554        output_id,
5555        req.combos,
5556        close.len(),
5557        |params| {
5558            let length = get_usize_param("ttm_squeeze", params, "length", 20)?;
5559            let bb_mult = get_f64_param("ttm_squeeze", params, "bb_mult", 2.0)?;
5560            let kc_mult_high = get_f64_param_with_aliases(
5561                "ttm_squeeze",
5562                params,
5563                &["kc_high", "kc_mult_high"],
5564                1.0,
5565            )?;
5566            let kc_mult_mid =
5567                get_f64_param_with_aliases("ttm_squeeze", params, &["kc_mid", "kc_mult_mid"], 1.5)?;
5568            let kc_mult_low =
5569                get_f64_param_with_aliases("ttm_squeeze", params, &["kc_low", "kc_mult_low"], 2.0)?;
5570            let input = TtmSqueezeInput::from_slices(
5571                high,
5572                low,
5573                close,
5574                TtmSqueezeParams {
5575                    length: Some(length),
5576                    bb_mult: Some(bb_mult),
5577                    kc_mult_high: Some(kc_mult_high),
5578                    kc_mult_mid: Some(kc_mult_mid),
5579                    kc_mult_low: Some(kc_mult_low),
5580                },
5581            );
5582            let out = ttm_squeeze_with_kernel(&input, kernel).map_err(|e| {
5583                IndicatorDispatchError::ComputeFailed {
5584                    indicator: "ttm_squeeze".to_string(),
5585                    details: e.to_string(),
5586                }
5587            })?;
5588            if output_id.eq_ignore_ascii_case("momentum") || output_id.eq_ignore_ascii_case("value")
5589            {
5590                return Ok(out.momentum);
5591            }
5592            if output_id.eq_ignore_ascii_case("squeeze") {
5593                return Ok(out.squeeze);
5594            }
5595            Err(IndicatorDispatchError::UnknownOutput {
5596                indicator: "ttm_squeeze".to_string(),
5597                output: output_id.to_string(),
5598            })
5599        },
5600    )
5601}
5602
5603fn compute_aroon_batch(
5604    req: IndicatorBatchRequest<'_>,
5605    output_id: &str,
5606) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5607    let (high, low) = extract_high_low_input("aroon", req.data)?;
5608    let kernel = req.kernel.to_non_batch();
5609    collect_f64("aroon", output_id, req.combos, high.len(), |params| {
5610        let length = get_usize_param("aroon", params, "length", 14)?;
5611        let input = AroonInput::from_slices_hl(
5612            high,
5613            low,
5614            AroonParams {
5615                length: Some(length),
5616            },
5617        );
5618        let out = aroon_with_kernel(&input, kernel).map_err(|e| {
5619            IndicatorDispatchError::ComputeFailed {
5620                indicator: "aroon".to_string(),
5621                details: e.to_string(),
5622            }
5623        })?;
5624        if output_id.eq_ignore_ascii_case("up")
5625            || output_id.eq_ignore_ascii_case("aroon_up")
5626            || output_id.eq_ignore_ascii_case("value")
5627        {
5628            return Ok(out.aroon_up);
5629        }
5630        if output_id.eq_ignore_ascii_case("down") || output_id.eq_ignore_ascii_case("aroon_down") {
5631            return Ok(out.aroon_down);
5632        }
5633        Err(IndicatorDispatchError::UnknownOutput {
5634            indicator: "aroon".to_string(),
5635            output: output_id.to_string(),
5636        })
5637    })
5638}
5639
5640fn compute_aroonosc_batch(
5641    req: IndicatorBatchRequest<'_>,
5642    output_id: &str,
5643) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5644    let (high, low) = extract_high_low_input("aroonosc", req.data)?;
5645    let kernel = req.kernel.to_non_batch();
5646    collect_f64("aroonosc", output_id, req.combos, high.len(), |params| {
5647        let length = get_usize_param("aroonosc", params, "length", 14)?;
5648        let input = AroonOscInput::from_slices_hl(
5649            high,
5650            low,
5651            AroonOscParams {
5652                length: Some(length),
5653            },
5654        );
5655        let out = aroon_osc_with_kernel(&input, kernel).map_err(|e| {
5656            IndicatorDispatchError::ComputeFailed {
5657                indicator: "aroonosc".to_string(),
5658                details: e.to_string(),
5659            }
5660        })?;
5661        if output_id.eq_ignore_ascii_case("value") {
5662            return Ok(out.values);
5663        }
5664        Err(IndicatorDispatchError::UnknownOutput {
5665            indicator: "aroonosc".to_string(),
5666            output: output_id.to_string(),
5667        })
5668    })
5669}
5670
5671fn compute_di_batch(
5672    req: IndicatorBatchRequest<'_>,
5673    output_id: &str,
5674) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5675    let (high, low, close) = extract_ohlc_input("di", req.data)?;
5676    let kernel = req.kernel.to_non_batch();
5677    collect_f64("di", output_id, req.combos, close.len(), |params| {
5678        let period = get_usize_param("di", params, "period", 14)?;
5679        let input = DiInput::from_slices(
5680            high,
5681            low,
5682            close,
5683            DiParams {
5684                period: Some(period),
5685            },
5686        );
5687        let out =
5688            di_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5689                indicator: "di".to_string(),
5690                details: e.to_string(),
5691            })?;
5692        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
5693            return Ok(out.plus);
5694        }
5695        if output_id.eq_ignore_ascii_case("minus") {
5696            return Ok(out.minus);
5697        }
5698        Err(IndicatorDispatchError::UnknownOutput {
5699            indicator: "di".to_string(),
5700            output: output_id.to_string(),
5701        })
5702    })
5703}
5704
5705fn compute_dm_batch(
5706    req: IndicatorBatchRequest<'_>,
5707    output_id: &str,
5708) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5709    let (high, low) = extract_high_low_input("dm", req.data)?;
5710    let kernel = req.kernel.to_non_batch();
5711    collect_f64("dm", output_id, req.combos, high.len(), |params| {
5712        let period = get_usize_param("dm", params, "period", 14)?;
5713        let input = DmInput::from_slices(
5714            high,
5715            low,
5716            DmParams {
5717                period: Some(period),
5718            },
5719        );
5720        let out =
5721            dm_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5722                indicator: "dm".to_string(),
5723                details: e.to_string(),
5724            })?;
5725        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
5726            return Ok(out.plus);
5727        }
5728        if output_id.eq_ignore_ascii_case("minus") {
5729            return Ok(out.minus);
5730        }
5731        Err(IndicatorDispatchError::UnknownOutput {
5732            indicator: "dm".to_string(),
5733            output: output_id.to_string(),
5734        })
5735    })
5736}
5737
5738fn compute_dti_batch(
5739    req: IndicatorBatchRequest<'_>,
5740    output_id: &str,
5741) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5742    expect_value_output("dti", output_id)?;
5743    let (high, low) = extract_high_low_input("dti", req.data)?;
5744    let kernel = req.kernel.to_non_batch();
5745    collect_f64_into_rows("dti", output_id, req.combos, high.len(), |params, row| {
5746        let r = get_usize_param("dti", params, "r", 14)?;
5747        let s = get_usize_param("dti", params, "s", 10)?;
5748        let u = get_usize_param("dti", params, "u", 5)?;
5749        let input = DtiInput::from_slices(
5750            high,
5751            low,
5752            DtiParams {
5753                r: Some(r),
5754                s: Some(s),
5755                u: Some(u),
5756            },
5757        );
5758        dti_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5759            indicator: "dti".to_string(),
5760            details: e.to_string(),
5761        })
5762    })
5763}
5764
5765fn compute_donchian_batch(
5766    req: IndicatorBatchRequest<'_>,
5767    output_id: &str,
5768) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5769    let (high, low) = extract_high_low_input("donchian", req.data)?;
5770    let kernel = req.kernel.to_non_batch();
5771    collect_f64("donchian", output_id, req.combos, high.len(), |params| {
5772        let period = get_usize_param("donchian", params, "period", 20)?;
5773        let input = DonchianInput::from_slices(
5774            high,
5775            low,
5776            DonchianParams {
5777                period: Some(period),
5778            },
5779        );
5780        let out = donchian_with_kernel(&input, kernel).map_err(|e| {
5781            IndicatorDispatchError::ComputeFailed {
5782                indicator: "donchian".to_string(),
5783                details: e.to_string(),
5784            }
5785        })?;
5786        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
5787            return Ok(out.upperband);
5788        }
5789        if output_id.eq_ignore_ascii_case("middle") {
5790            return Ok(out.middleband);
5791        }
5792        if output_id.eq_ignore_ascii_case("lower") {
5793            return Ok(out.lowerband);
5794        }
5795        Err(IndicatorDispatchError::UnknownOutput {
5796            indicator: "donchian".to_string(),
5797            output: output_id.to_string(),
5798        })
5799    })
5800}
5801
5802fn compute_kdj_batch(
5803    req: IndicatorBatchRequest<'_>,
5804    output_id: &str,
5805) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5806    let (high, low, close) = extract_ohlc_input("kdj", req.data)?;
5807    let kernel = req.kernel.to_non_batch();
5808    collect_f64("kdj", output_id, req.combos, close.len(), |params| {
5809        let fast_k_period = get_usize_param("kdj", params, "fast_k_period", 9)?;
5810        let slow_k_period = get_usize_param("kdj", params, "slow_k_period", 3)?;
5811        let slow_k_ma_type = get_enum_param("kdj", params, "slow_k_ma_type", "sma")?;
5812        let slow_d_period = get_usize_param("kdj", params, "slow_d_period", 3)?;
5813        let slow_d_ma_type = get_enum_param("kdj", params, "slow_d_ma_type", "sma")?;
5814        let input = KdjInput::from_slices(
5815            high,
5816            low,
5817            close,
5818            KdjParams {
5819                fast_k_period: Some(fast_k_period),
5820                slow_k_period: Some(slow_k_period),
5821                slow_k_ma_type: Some(slow_k_ma_type),
5822                slow_d_period: Some(slow_d_period),
5823                slow_d_ma_type: Some(slow_d_ma_type),
5824            },
5825        );
5826        let out =
5827            kdj_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5828                indicator: "kdj".to_string(),
5829                details: e.to_string(),
5830            })?;
5831        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5832            return Ok(out.k);
5833        }
5834        if output_id.eq_ignore_ascii_case("d") {
5835            return Ok(out.d);
5836        }
5837        if output_id.eq_ignore_ascii_case("j") {
5838            return Ok(out.j);
5839        }
5840        Err(IndicatorDispatchError::UnknownOutput {
5841            indicator: "kdj".to_string(),
5842            output: output_id.to_string(),
5843        })
5844    })
5845}
5846
5847fn compute_keltner_batch(
5848    req: IndicatorBatchRequest<'_>,
5849    output_id: &str,
5850) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5851    let (high, low, close) = extract_ohlc_input("keltner", req.data)?;
5852    let kernel = req.kernel.to_non_batch();
5853    collect_f64("keltner", output_id, req.combos, close.len(), |params| {
5854        let period = get_usize_param("keltner", params, "period", 20)?;
5855        let multiplier = get_f64_param("keltner", params, "multiplier", 2.0)?;
5856        let ma_type = get_enum_param("keltner", params, "ma_type", "ema")?;
5857        let input = KeltnerInput::from_slice(
5858            high,
5859            low,
5860            close,
5861            close,
5862            KeltnerParams {
5863                period: Some(period),
5864                multiplier: Some(multiplier),
5865                ma_type: Some(ma_type),
5866            },
5867        );
5868        let out = keltner_with_kernel(&input, kernel).map_err(|e| {
5869            IndicatorDispatchError::ComputeFailed {
5870                indicator: "keltner".to_string(),
5871                details: e.to_string(),
5872            }
5873        })?;
5874        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
5875            return Ok(out.upper_band);
5876        }
5877        if output_id.eq_ignore_ascii_case("middle") {
5878            return Ok(out.middle_band);
5879        }
5880        if output_id.eq_ignore_ascii_case("lower") {
5881            return Ok(out.lower_band);
5882        }
5883        Err(IndicatorDispatchError::UnknownOutput {
5884            indicator: "keltner".to_string(),
5885            output: output_id.to_string(),
5886        })
5887    })
5888}
5889
5890fn compute_squeeze_momentum_batch(
5891    req: IndicatorBatchRequest<'_>,
5892    output_id: &str,
5893) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5894    let (high, low, close) = extract_ohlc_input("squeeze_momentum", req.data)?;
5895    let kernel = req.kernel.to_non_batch();
5896    collect_f64(
5897        "squeeze_momentum",
5898        output_id,
5899        req.combos,
5900        close.len(),
5901        |params| {
5902            let length_bb = get_usize_param("squeeze_momentum", params, "length_bb", 20)?;
5903            let mult_bb = get_f64_param("squeeze_momentum", params, "mult_bb", 2.0)?;
5904            let length_kc = get_usize_param("squeeze_momentum", params, "length_kc", 20)?;
5905            let mult_kc = get_f64_param("squeeze_momentum", params, "mult_kc", 1.5)?;
5906            let input = SqueezeMomentumInput::from_slices(
5907                high,
5908                low,
5909                close,
5910                SqueezeMomentumParams {
5911                    length_bb: Some(length_bb),
5912                    mult_bb: Some(mult_bb),
5913                    length_kc: Some(length_kc),
5914                    mult_kc: Some(mult_kc),
5915                },
5916            );
5917            let out = squeeze_momentum_with_kernel(&input, kernel).map_err(|e| {
5918                IndicatorDispatchError::ComputeFailed {
5919                    indicator: "squeeze_momentum".to_string(),
5920                    details: e.to_string(),
5921                }
5922            })?;
5923            if output_id.eq_ignore_ascii_case("momentum") || output_id.eq_ignore_ascii_case("value")
5924            {
5925                return Ok(out.momentum);
5926            }
5927            if output_id.eq_ignore_ascii_case("squeeze") {
5928                return Ok(out.squeeze);
5929            }
5930            if output_id.eq_ignore_ascii_case("signal")
5931                || output_id.eq_ignore_ascii_case("momentum_signal")
5932            {
5933                return Ok(out.momentum_signal);
5934            }
5935            Err(IndicatorDispatchError::UnknownOutput {
5936                indicator: "squeeze_momentum".to_string(),
5937                output: output_id.to_string(),
5938            })
5939        },
5940    )
5941}
5942
5943fn compute_srsi_batch(
5944    req: IndicatorBatchRequest<'_>,
5945    output_id: &str,
5946) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5947    let data = extract_slice_input("srsi", req.data, "close")?;
5948    let kernel = req.kernel.to_non_batch();
5949    collect_f64("srsi", output_id, req.combos, data.len(), |params| {
5950        let rsi_period = get_usize_param("srsi", params, "rsi_period", 14)?;
5951        let stoch_period = get_usize_param("srsi", params, "stoch_period", 14)?;
5952        let k = get_usize_param("srsi", params, "k", 3)?;
5953        let d = get_usize_param("srsi", params, "d", 3)?;
5954        let source = get_enum_param("srsi", params, "source", "close")?;
5955        let input = SrsiInput::from_slice(
5956            data,
5957            SrsiParams {
5958                rsi_period: Some(rsi_period),
5959                stoch_period: Some(stoch_period),
5960                k: Some(k),
5961                d: Some(d),
5962                source: Some(source),
5963            },
5964        );
5965        let out = srsi_with_kernel(&input, kernel).map_err(|e| {
5966            IndicatorDispatchError::ComputeFailed {
5967                indicator: "srsi".to_string(),
5968                details: e.to_string(),
5969            }
5970        })?;
5971        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5972            return Ok(out.k);
5973        }
5974        if output_id.eq_ignore_ascii_case("d") {
5975            return Ok(out.d);
5976        }
5977        Err(IndicatorDispatchError::UnknownOutput {
5978            indicator: "srsi".to_string(),
5979            output: output_id.to_string(),
5980        })
5981    })
5982}
5983
5984fn compute_supertrend_batch(
5985    req: IndicatorBatchRequest<'_>,
5986    output_id: &str,
5987) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5988    let (high, low, close) = extract_ohlc_input("supertrend", req.data)?;
5989    let kernel = req.kernel.to_non_batch();
5990    collect_f64("supertrend", output_id, req.combos, close.len(), |params| {
5991        let period = get_usize_param("supertrend", params, "period", 10)?;
5992        let factor = get_f64_param("supertrend", params, "factor", 3.0)?;
5993        let input = SuperTrendInput::from_slices(
5994            high,
5995            low,
5996            close,
5997            SuperTrendParams {
5998                period: Some(period),
5999                factor: Some(factor),
6000            },
6001        );
6002        let out = supertrend_with_kernel(&input, kernel).map_err(|e| {
6003            IndicatorDispatchError::ComputeFailed {
6004                indicator: "supertrend".to_string(),
6005                details: e.to_string(),
6006            }
6007        })?;
6008        if output_id.eq_ignore_ascii_case("trend") || output_id.eq_ignore_ascii_case("value") {
6009            return Ok(out.trend);
6010        }
6011        if output_id.eq_ignore_ascii_case("changed") {
6012            return Ok(out.changed);
6013        }
6014        Err(IndicatorDispatchError::UnknownOutput {
6015            indicator: "supertrend".to_string(),
6016            output: output_id.to_string(),
6017        })
6018    })
6019}
6020
6021fn compute_adjustable_ma_alternating_extremities_batch(
6022    req: IndicatorBatchRequest<'_>,
6023    output_id: &str,
6024) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6025    let (high, low, close) = extract_ohlc_input("adjustable_ma_alternating_extremities", req.data)?;
6026    let kernel = req.kernel.to_non_batch();
6027    collect_f64(
6028        "adjustable_ma_alternating_extremities",
6029        output_id,
6030        req.combos,
6031        close.len(),
6032        |params| {
6033            let length = get_usize_param(
6034                "adjustable_ma_alternating_extremities",
6035                params,
6036                "length",
6037                50,
6038            )?;
6039            let mult = get_f64_param("adjustable_ma_alternating_extremities", params, "mult", 2.0)?;
6040            let alpha = get_f64_param(
6041                "adjustable_ma_alternating_extremities",
6042                params,
6043                "alpha",
6044                1.0,
6045            )?;
6046            let beta = get_f64_param("adjustable_ma_alternating_extremities", params, "beta", 0.5)?;
6047            let input = AdjustableMaAlternatingExtremitiesInput::from_slices(
6048                high,
6049                low,
6050                close,
6051                AdjustableMaAlternatingExtremitiesParams {
6052                    length: Some(length),
6053                    mult: Some(mult),
6054                    alpha: Some(alpha),
6055                    beta: Some(beta),
6056                },
6057            );
6058            let out =
6059                adjustable_ma_alternating_extremities_with_kernel(&input, kernel).map_err(|e| {
6060                    IndicatorDispatchError::ComputeFailed {
6061                        indicator: "adjustable_ma_alternating_extremities".to_string(),
6062                        details: e.to_string(),
6063                    }
6064                })?;
6065            if output_id.eq_ignore_ascii_case("ma") || output_id.eq_ignore_ascii_case("value") {
6066                return Ok(out.ma);
6067            }
6068            if output_id.eq_ignore_ascii_case("upper") {
6069                return Ok(out.upper);
6070            }
6071            if output_id.eq_ignore_ascii_case("lower") {
6072                return Ok(out.lower);
6073            }
6074            if output_id.eq_ignore_ascii_case("extremity") {
6075                return Ok(out.extremity);
6076            }
6077            if output_id.eq_ignore_ascii_case("state") {
6078                return Ok(out.state);
6079            }
6080            if output_id.eq_ignore_ascii_case("changed") {
6081                return Ok(out.changed);
6082            }
6083            if output_id.eq_ignore_ascii_case("smoothed_open") {
6084                return Ok(out.smoothed_open);
6085            }
6086            if output_id.eq_ignore_ascii_case("smoothed_high") {
6087                return Ok(out.smoothed_high);
6088            }
6089            if output_id.eq_ignore_ascii_case("smoothed_low") {
6090                return Ok(out.smoothed_low);
6091            }
6092            if output_id.eq_ignore_ascii_case("smoothed_close") {
6093                return Ok(out.smoothed_close);
6094            }
6095            Err(IndicatorDispatchError::UnknownOutput {
6096                indicator: "adjustable_ma_alternating_extremities".to_string(),
6097                output: output_id.to_string(),
6098            })
6099        },
6100    )
6101}
6102
6103fn compute_vi_batch(
6104    req: IndicatorBatchRequest<'_>,
6105    output_id: &str,
6106) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6107    let (high, low, close) = extract_ohlc_input("vi", req.data)?;
6108    let kernel = req.kernel.to_non_batch();
6109    collect_f64("vi", output_id, req.combos, close.len(), |params| {
6110        let period = get_usize_param("vi", params, "period", 14)?;
6111        let input = ViInput::from_slices(
6112            high,
6113            low,
6114            close,
6115            ViParams {
6116                period: Some(period),
6117            },
6118        );
6119        let out =
6120            vi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
6121                indicator: "vi".to_string(),
6122                details: e.to_string(),
6123            })?;
6124        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
6125            return Ok(out.plus);
6126        }
6127        if output_id.eq_ignore_ascii_case("minus") {
6128            return Ok(out.minus);
6129        }
6130        Err(IndicatorDispatchError::UnknownOutput {
6131            indicator: "vi".to_string(),
6132            output: output_id.to_string(),
6133        })
6134    })
6135}
6136
6137fn compute_wavetrend_batch(
6138    req: IndicatorBatchRequest<'_>,
6139    output_id: &str,
6140) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6141    let data = extract_slice_input("wavetrend", req.data, "hlc3")?;
6142    let kernel = req.kernel.to_non_batch();
6143    collect_f64("wavetrend", output_id, req.combos, data.len(), |params| {
6144        let channel_length = get_usize_param("wavetrend", params, "channel_length", 9)?;
6145        let average_length = get_usize_param("wavetrend", params, "average_length", 12)?;
6146        let ma_length = get_usize_param("wavetrend", params, "ma_length", 3)?;
6147        let factor = get_f64_param("wavetrend", params, "factor", 0.015)?;
6148        let input = WavetrendInput::from_slice(
6149            data,
6150            WavetrendParams {
6151                channel_length: Some(channel_length),
6152                average_length: Some(average_length),
6153                ma_length: Some(ma_length),
6154                factor: Some(factor),
6155            },
6156        );
6157        let out = wavetrend_with_kernel(&input, kernel).map_err(|e| {
6158            IndicatorDispatchError::ComputeFailed {
6159                indicator: "wavetrend".to_string(),
6160                details: e.to_string(),
6161            }
6162        })?;
6163        if output_id.eq_ignore_ascii_case("wt1") || output_id.eq_ignore_ascii_case("value") {
6164            return Ok(out.wt1);
6165        }
6166        if output_id.eq_ignore_ascii_case("wt2") {
6167            return Ok(out.wt2);
6168        }
6169        if output_id.eq_ignore_ascii_case("wt_diff") {
6170            return Ok(out.wt_diff);
6171        }
6172        Err(IndicatorDispatchError::UnknownOutput {
6173            indicator: "wavetrend".to_string(),
6174            output: output_id.to_string(),
6175        })
6176    })
6177}
6178
6179fn compute_wto_batch(
6180    req: IndicatorBatchRequest<'_>,
6181    output_id: &str,
6182) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6183    let data = extract_slice_input("wto", req.data, "close")?;
6184    let kernel = req.kernel.to_non_batch();
6185    collect_f64("wto", output_id, req.combos, data.len(), |params| {
6186        let channel_length = get_usize_param("wto", params, "channel_length", 10)?;
6187        let average_length = get_usize_param("wto", params, "average_length", 21)?;
6188        let input = WtoInput::from_slice(
6189            data,
6190            WtoParams {
6191                channel_length: Some(channel_length),
6192                average_length: Some(average_length),
6193            },
6194        );
6195        let out =
6196            wto_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
6197                indicator: "wto".to_string(),
6198                details: e.to_string(),
6199            })?;
6200        if output_id.eq_ignore_ascii_case("wavetrend1")
6201            || output_id.eq_ignore_ascii_case("wt1")
6202            || output_id.eq_ignore_ascii_case("value")
6203        {
6204            return Ok(out.wavetrend1);
6205        }
6206        if output_id.eq_ignore_ascii_case("wavetrend2") || output_id.eq_ignore_ascii_case("wt2") {
6207            return Ok(out.wavetrend2);
6208        }
6209        if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist") {
6210            return Ok(out.histogram);
6211        }
6212        Err(IndicatorDispatchError::UnknownOutput {
6213            indicator: "wto".to_string(),
6214            output: output_id.to_string(),
6215        })
6216    })
6217}
6218
6219fn compute_rogers_satchell_volatility_batch(
6220    req: IndicatorBatchRequest<'_>,
6221    output_id: &str,
6222) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6223    let (open, high, low, close) = extract_ohlc_full_input("rogers_satchell_volatility", req.data)?;
6224    let kernel = req.kernel.to_non_batch();
6225    collect_f64(
6226        "rogers_satchell_volatility",
6227        output_id,
6228        req.combos,
6229        close.len(),
6230        |params| {
6231            let lookback = get_usize_param("rogers_satchell_volatility", params, "lookback", 8)?;
6232            let signal_length =
6233                get_usize_param("rogers_satchell_volatility", params, "signal_length", 8)?;
6234            let input = RogersSatchellVolatilityInput::from_slices(
6235                open,
6236                high,
6237                low,
6238                close,
6239                RogersSatchellVolatilityParams {
6240                    lookback: Some(lookback),
6241                    signal_length: Some(signal_length),
6242                },
6243            );
6244            let out = rogers_satchell_volatility_with_kernel(&input, kernel).map_err(|e| {
6245                IndicatorDispatchError::ComputeFailed {
6246                    indicator: "rogers_satchell_volatility".to_string(),
6247                    details: e.to_string(),
6248                }
6249            })?;
6250            if output_id.eq_ignore_ascii_case("rs") || output_id.eq_ignore_ascii_case("value") {
6251                return Ok(out.rs);
6252            }
6253            if output_id.eq_ignore_ascii_case("signal") {
6254                return Ok(out.signal);
6255            }
6256            Err(IndicatorDispatchError::UnknownOutput {
6257                indicator: "rogers_satchell_volatility".to_string(),
6258                output: output_id.to_string(),
6259            })
6260        },
6261    )
6262}
6263
6264fn compute_historical_volatility_rank_batch(
6265    req: IndicatorBatchRequest<'_>,
6266    output_id: &str,
6267) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6268    let data = extract_slice_input("historical_volatility_rank", req.data, "close")?;
6269    let kernel = req.kernel.to_non_batch();
6270    collect_f64(
6271        "historical_volatility_rank",
6272        output_id,
6273        req.combos,
6274        data.len(),
6275        |params| {
6276            let hv_length = get_usize_param("historical_volatility_rank", params, "hv_length", 10)?;
6277            let rank_length =
6278                get_usize_param("historical_volatility_rank", params, "rank_length", 52 * 7)?;
6279            let annualization_days = get_f64_param(
6280                "historical_volatility_rank",
6281                params,
6282                "annualization_days",
6283                365.0,
6284            )?;
6285            let bar_days = get_f64_param("historical_volatility_rank", params, "bar_days", 1.0)?;
6286            let input = HistoricalVolatilityRankInput::from_slice(
6287                data,
6288                HistoricalVolatilityRankParams {
6289                    hv_length: Some(hv_length),
6290                    rank_length: Some(rank_length),
6291                    annualization_days: Some(annualization_days),
6292                    bar_days: Some(bar_days),
6293                },
6294            );
6295            let out = historical_volatility_rank_with_kernel(&input, kernel).map_err(|e| {
6296                IndicatorDispatchError::ComputeFailed {
6297                    indicator: "historical_volatility_rank".to_string(),
6298                    details: e.to_string(),
6299                }
6300            })?;
6301            if output_id.eq_ignore_ascii_case("hvr") || output_id.eq_ignore_ascii_case("value") {
6302                return Ok(out.hvr);
6303            }
6304            if output_id.eq_ignore_ascii_case("hv") {
6305                return Ok(out.hv);
6306            }
6307            Err(IndicatorDispatchError::UnknownOutput {
6308                indicator: "historical_volatility_rank".to_string(),
6309                output: output_id.to_string(),
6310            })
6311        },
6312    )
6313}
6314
6315fn compute_dual_ulcer_index_batch(
6316    req: IndicatorBatchRequest<'_>,
6317    output_id: &str,
6318) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6319    let data = extract_slice_input("dual_ulcer_index", req.data, "close")?;
6320    let kernel = req.kernel.to_non_batch();
6321    collect_f64(
6322        "dual_ulcer_index",
6323        output_id,
6324        req.combos,
6325        data.len(),
6326        |params| {
6327            let period = get_usize_param("dual_ulcer_index", params, "period", 5)?;
6328            let auto_threshold =
6329                get_bool_param("dual_ulcer_index", params, "auto_threshold", true)?;
6330            let threshold = get_f64_param("dual_ulcer_index", params, "threshold", 0.1)?;
6331            let input = DualUlcerIndexInput::from_slice(
6332                data,
6333                DualUlcerIndexParams {
6334                    period: Some(period),
6335                    auto_threshold: Some(auto_threshold),
6336                    threshold: Some(threshold),
6337                },
6338            );
6339            let out = dual_ulcer_index_with_kernel(&input, kernel).map_err(|e| {
6340                IndicatorDispatchError::ComputeFailed {
6341                    indicator: "dual_ulcer_index".to_string(),
6342                    details: e.to_string(),
6343                }
6344            })?;
6345            if output_id.eq_ignore_ascii_case("long_ulcer")
6346                || output_id.eq_ignore_ascii_case("uulcer")
6347                || output_id.eq_ignore_ascii_case("value")
6348            {
6349                return Ok(out.long_ulcer);
6350            }
6351            if output_id.eq_ignore_ascii_case("short_ulcer")
6352                || output_id.eq_ignore_ascii_case("dulcer")
6353            {
6354                return Ok(out.short_ulcer);
6355            }
6356            if output_id.eq_ignore_ascii_case("threshold") {
6357                return Ok(out.threshold);
6358            }
6359            Err(IndicatorDispatchError::UnknownOutput {
6360                indicator: "dual_ulcer_index".to_string(),
6361                output: output_id.to_string(),
6362            })
6363        },
6364    )
6365}
6366
6367fn compute_fractal_dimension_index_batch(
6368    req: IndicatorBatchRequest<'_>,
6369    output_id: &str,
6370) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6371    let data = extract_slice_input("fractal_dimension_index", req.data, "close")?;
6372    let kernel = req.kernel.to_non_batch();
6373    collect_f64(
6374        "fractal_dimension_index",
6375        output_id,
6376        req.combos,
6377        data.len(),
6378        |params| {
6379            let length = get_usize_param("fractal_dimension_index", params, "length", 30)?;
6380            let input = FractalDimensionIndexInput::from_slice(
6381                data,
6382                FractalDimensionIndexParams {
6383                    length: Some(length),
6384                },
6385            );
6386            let out = fractal_dimension_index_with_kernel(&input, kernel).map_err(|e| {
6387                IndicatorDispatchError::ComputeFailed {
6388                    indicator: "fractal_dimension_index".to_string(),
6389                    details: e.to_string(),
6390                }
6391            })?;
6392            if output_id.eq_ignore_ascii_case("value") {
6393                return Ok(out.values);
6394            }
6395            Err(IndicatorDispatchError::UnknownOutput {
6396                indicator: "fractal_dimension_index".to_string(),
6397                output: output_id.to_string(),
6398            })
6399        },
6400    )
6401}
6402
6403fn compute_volume_weighted_rsi_batch(
6404    req: IndicatorBatchRequest<'_>,
6405    output_id: &str,
6406) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6407    expect_value_output("volume_weighted_rsi", output_id)?;
6408    let (close, volume) = extract_close_volume_input("volume_weighted_rsi", req.data, "close")?;
6409    let periods = combo_periods("volume_weighted_rsi", req.combos, "period", 14)?;
6410    if let Some((start, end, step)) = derive_period_sweep(&periods) {
6411        let out = volume_weighted_rsi_batch_with_kernel(
6412            close,
6413            volume,
6414            &VolumeWeightedRsiBatchRange {
6415                period: (start, end, step),
6416            },
6417            to_batch_kernel(req.kernel),
6418        )
6419        .map_err(|e| IndicatorDispatchError::ComputeFailed {
6420            indicator: "volume_weighted_rsi".to_string(),
6421            details: e.to_string(),
6422        })?;
6423        ensure_len("volume_weighted_rsi", close.len(), out.cols)?;
6424        let produced_periods: Vec<usize> = out
6425            .combos
6426            .iter()
6427            .map(|combo| combo.period.unwrap_or(14))
6428            .collect();
6429        let values = reorder_or_take_f64_matrix_by_period(
6430            "volume_weighted_rsi",
6431            &periods,
6432            &produced_periods,
6433            out.cols,
6434            out.values,
6435        )?;
6436        return Ok(f64_output(output_id, periods.len(), out.cols, values));
6437    }
6438
6439    let kernel = req.kernel.to_non_batch();
6440    collect_f64_into_rows(
6441        "volume_weighted_rsi",
6442        output_id,
6443        req.combos,
6444        close.len(),
6445        |params, row| {
6446            let period = get_usize_param("volume_weighted_rsi", params, "period", 14)?;
6447            let input = VolumeWeightedRsiInput::from_slices(
6448                close,
6449                volume,
6450                VolumeWeightedRsiParams {
6451                    period: Some(period),
6452                },
6453            );
6454            volume_weighted_rsi_into_slice(row, &input, kernel).map_err(|e| {
6455                IndicatorDispatchError::ComputeFailed {
6456                    indicator: "volume_weighted_rsi".to_string(),
6457                    details: e.to_string(),
6458                }
6459            })
6460        },
6461    )
6462}
6463
6464fn compute_dynamic_momentum_index_batch(
6465    req: IndicatorBatchRequest<'_>,
6466    output_id: &str,
6467) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6468    expect_value_output("dynamic_momentum_index", output_id)?;
6469    let data = extract_slice_input("dynamic_momentum_index", req.data, "close")?;
6470    let kernel = req.kernel.to_non_batch();
6471    collect_f64_into_rows(
6472        "dynamic_momentum_index",
6473        output_id,
6474        req.combos,
6475        data.len(),
6476        |params, row| {
6477            let rsi_period = get_usize_param("dynamic_momentum_index", params, "rsi_period", 14)?;
6478            let volatility_period =
6479                get_usize_param("dynamic_momentum_index", params, "volatility_period", 5)?;
6480            let volatility_sma_period = get_usize_param(
6481                "dynamic_momentum_index",
6482                params,
6483                "volatility_sma_period",
6484                10,
6485            )?;
6486            let upper_limit = get_usize_param("dynamic_momentum_index", params, "upper_limit", 30)?;
6487            let lower_limit = get_usize_param("dynamic_momentum_index", params, "lower_limit", 5)?;
6488            let input = DynamicMomentumIndexInput::from_slice(
6489                data,
6490                DynamicMomentumIndexParams {
6491                    rsi_period: Some(rsi_period),
6492                    volatility_period: Some(volatility_period),
6493                    volatility_sma_period: Some(volatility_sma_period),
6494                    upper_limit: Some(upper_limit),
6495                    lower_limit: Some(lower_limit),
6496                },
6497            );
6498            dynamic_momentum_index_into_slice(row, &input, kernel).map_err(|e| {
6499                IndicatorDispatchError::ComputeFailed {
6500                    indicator: "dynamic_momentum_index".to_string(),
6501                    details: e.to_string(),
6502                }
6503            })
6504        },
6505    )
6506}
6507
6508fn compute_disparity_index_batch(
6509    req: IndicatorBatchRequest<'_>,
6510    output_id: &str,
6511) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6512    expect_value_output("disparity_index", output_id)?;
6513    let data = extract_slice_input("disparity_index", req.data, "close")?;
6514    let kernel = req.kernel.to_non_batch();
6515    collect_f64_into_rows(
6516        "disparity_index",
6517        output_id,
6518        req.combos,
6519        data.len(),
6520        |params, row| {
6521            let ema_period = get_usize_param("disparity_index", params, "ema_period", 14)?;
6522            let lookback_period =
6523                get_usize_param("disparity_index", params, "lookback_period", 14)?;
6524            let smoothing_period =
6525                get_usize_param("disparity_index", params, "smoothing_period", 9)?;
6526            let smoothing_type =
6527                get_enum_param("disparity_index", params, "smoothing_type", "ema")?;
6528            let input = DisparityIndexInput::from_slice(
6529                data,
6530                DisparityIndexParams {
6531                    ema_period: Some(ema_period),
6532                    lookback_period: Some(lookback_period),
6533                    smoothing_period: Some(smoothing_period),
6534                    smoothing_type: Some(smoothing_type),
6535                },
6536            );
6537            disparity_index_into_slice(row, &input, kernel).map_err(|e| {
6538                IndicatorDispatchError::ComputeFailed {
6539                    indicator: "disparity_index".to_string(),
6540                    details: e.to_string(),
6541                }
6542            })
6543        },
6544    )
6545}
6546
6547fn compute_donchian_channel_width_batch(
6548    req: IndicatorBatchRequest<'_>,
6549    output_id: &str,
6550) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6551    expect_value_output("donchian_channel_width", output_id)?;
6552    let (high, low) = extract_high_low_input("donchian_channel_width", req.data)?;
6553
6554    collect_f64_into_rows(
6555        "donchian_channel_width",
6556        output_id,
6557        req.combos,
6558        high.len(),
6559        |params, row| {
6560            let period = get_usize_param("donchian_channel_width", params, "period", 20)?;
6561            let kernel = req.kernel;
6562            let input = DonchianChannelWidthInput::from_slices(
6563                high,
6564                low,
6565                DonchianChannelWidthParams {
6566                    period: Some(period),
6567                },
6568            );
6569            donchian_channel_width_into_slice(row, &input, kernel).map_err(|e| {
6570                IndicatorDispatchError::ComputeFailed {
6571                    indicator: "donchian_channel_width".to_string(),
6572                    details: e.to_string(),
6573                }
6574            })
6575        },
6576    )
6577}
6578
6579fn compute_kairi_relative_index_batch(
6580    req: IndicatorBatchRequest<'_>,
6581    output_id: &str,
6582) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6583    expect_value_output("kairi_relative_index", output_id)?;
6584    let kernel = req.kernel.to_non_batch();
6585    let len = match req.data {
6586        IndicatorDataRef::Slice { values } => values.len(),
6587        IndicatorDataRef::Candles { candles, source } => {
6588            source_type(candles, source.unwrap_or("close")).len()
6589        }
6590        IndicatorDataRef::CloseVolume { close, volume } => {
6591            ensure_same_len_2("kairi_relative_index", close.len(), volume.len())?;
6592            close.len()
6593        }
6594        IndicatorDataRef::Ohlc {
6595            open,
6596            high,
6597            low,
6598            close,
6599        } => {
6600            ensure_same_len_4(
6601                "kairi_relative_index",
6602                open.len(),
6603                high.len(),
6604                low.len(),
6605                close.len(),
6606            )?;
6607            close.len()
6608        }
6609        IndicatorDataRef::Ohlcv {
6610            open,
6611            high,
6612            low,
6613            close,
6614            volume,
6615        } => {
6616            ensure_same_len_5(
6617                "kairi_relative_index",
6618                open.len(),
6619                high.len(),
6620                low.len(),
6621                close.len(),
6622                volume.len(),
6623            )?;
6624            close.len()
6625        }
6626        IndicatorDataRef::HighLow { .. } => {
6627            return Err(IndicatorDispatchError::MissingRequiredInput {
6628                indicator: "kairi_relative_index".to_string(),
6629                input: IndicatorInputKind::Candles,
6630            });
6631        }
6632    };
6633
6634    collect_f64_into_rows(
6635        "kairi_relative_index",
6636        output_id,
6637        req.combos,
6638        len,
6639        |params, row| {
6640            let length = get_usize_param("kairi_relative_index", params, "length", 50)?;
6641            let ma_type = get_enum_param("kairi_relative_index", params, "ma_type", "SMA")?;
6642            if ma_type.eq_ignore_ascii_case("VWMA") {
6643                match req.data {
6644                    IndicatorDataRef::Slice { .. } | IndicatorDataRef::Ohlc { .. } => {
6645                        return Err(IndicatorDispatchError::MissingRequiredInput {
6646                            indicator: "kairi_relative_index".to_string(),
6647                            input: IndicatorInputKind::CloseVolume,
6648                        });
6649                    }
6650                    _ => {}
6651                }
6652            }
6653
6654            let input = match req.data {
6655                IndicatorDataRef::Slice { values } => KairiRelativeIndexInput::from_slices(
6656                    values,
6657                    values,
6658                    KairiRelativeIndexParams {
6659                        length: Some(length),
6660                        ma_type: Some(ma_type.to_string()),
6661                    },
6662                ),
6663                IndicatorDataRef::Candles { candles, source } => {
6664                    KairiRelativeIndexInput::from_candles(
6665                        candles,
6666                        source.unwrap_or("close"),
6667                        KairiRelativeIndexParams {
6668                            length: Some(length),
6669                            ma_type: Some(ma_type.to_string()),
6670                        },
6671                    )
6672                }
6673                IndicatorDataRef::CloseVolume { close, volume } => {
6674                    KairiRelativeIndexInput::from_slices(
6675                        close,
6676                        volume,
6677                        KairiRelativeIndexParams {
6678                            length: Some(length),
6679                            ma_type: Some(ma_type.to_string()),
6680                        },
6681                    )
6682                }
6683                IndicatorDataRef::Ohlc { close, .. } => KairiRelativeIndexInput::from_slices(
6684                    close,
6685                    close,
6686                    KairiRelativeIndexParams {
6687                        length: Some(length),
6688                        ma_type: Some(ma_type.to_string()),
6689                    },
6690                ),
6691                IndicatorDataRef::Ohlcv { close, volume, .. } => {
6692                    KairiRelativeIndexInput::from_slices(
6693                        close,
6694                        volume,
6695                        KairiRelativeIndexParams {
6696                            length: Some(length),
6697                            ma_type: Some(ma_type.to_string()),
6698                        },
6699                    )
6700                }
6701                IndicatorDataRef::HighLow { .. } => unreachable!(),
6702            };
6703
6704            kairi_relative_index_into_slice(row, &input, kernel).map_err(|e| {
6705                IndicatorDispatchError::ComputeFailed {
6706                    indicator: "kairi_relative_index".to_string(),
6707                    details: e.to_string(),
6708                }
6709            })
6710        },
6711    )
6712}
6713
6714fn compute_projection_oscillator_batch(
6715    req: IndicatorBatchRequest<'_>,
6716    output_id: &str,
6717) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6718    let (high, low, close) = extract_ohlc_input("projection_oscillator", req.data)?;
6719    let kernel = req.kernel.to_non_batch();
6720    collect_f64(
6721        "projection_oscillator",
6722        output_id,
6723        req.combos,
6724        close.len(),
6725        |params| {
6726            let length = get_usize_param("projection_oscillator", params, "length", 14)?;
6727            let smooth_length =
6728                get_usize_param("projection_oscillator", params, "smooth_length", 4)?;
6729            let input = ProjectionOscillatorInput::from_slices(
6730                high,
6731                low,
6732                close,
6733                ProjectionOscillatorParams {
6734                    length: Some(length),
6735                    smooth_length: Some(smooth_length),
6736                },
6737            );
6738            let out = projection_oscillator_with_kernel(&input, kernel).map_err(|e| {
6739                IndicatorDispatchError::ComputeFailed {
6740                    indicator: "projection_oscillator".to_string(),
6741                    details: e.to_string(),
6742                }
6743            })?;
6744            if output_id.eq_ignore_ascii_case("pbo") || output_id.eq_ignore_ascii_case("value") {
6745                return Ok(out.pbo);
6746            }
6747            if output_id.eq_ignore_ascii_case("signal") {
6748                return Ok(out.signal);
6749            }
6750            Err(IndicatorDispatchError::UnknownOutput {
6751                indicator: "projection_oscillator".to_string(),
6752                output: output_id.to_string(),
6753            })
6754        },
6755    )
6756}
6757
6758fn compute_market_structure_trailing_stop_batch(
6759    req: IndicatorBatchRequest<'_>,
6760    output_id: &str,
6761) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6762    let (open, high, low, close) =
6763        extract_ohlc_full_input("market_structure_trailing_stop", req.data)?;
6764    let kernel = req.kernel.to_non_batch();
6765    collect_f64(
6766        "market_structure_trailing_stop",
6767        output_id,
6768        req.combos,
6769        close.len(),
6770        |params| {
6771            let length = get_usize_param("market_structure_trailing_stop", params, "length", 14)?;
6772            let increment_factor = get_f64_param(
6773                "market_structure_trailing_stop",
6774                params,
6775                "increment_factor",
6776                100.0,
6777            )?;
6778            let reset_on = get_enum_param(
6779                "market_structure_trailing_stop",
6780                params,
6781                "reset_on",
6782                "CHoCH",
6783            )?;
6784            let input = MarketStructureTrailingStopInput::from_slices(
6785                open,
6786                high,
6787                low,
6788                close,
6789                MarketStructureTrailingStopParams {
6790                    length: Some(length),
6791                    increment_factor: Some(increment_factor),
6792                    reset_on: Some(reset_on),
6793                },
6794            );
6795            let out = market_structure_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
6796                IndicatorDispatchError::ComputeFailed {
6797                    indicator: "market_structure_trailing_stop".to_string(),
6798                    details: e.to_string(),
6799                }
6800            })?;
6801            if output_id.eq_ignore_ascii_case("trailing_stop")
6802                || output_id.eq_ignore_ascii_case("value")
6803            {
6804                return Ok(out.trailing_stop);
6805            }
6806            if output_id.eq_ignore_ascii_case("state") {
6807                return Ok(out.state);
6808            }
6809            if output_id.eq_ignore_ascii_case("structure") {
6810                return Ok(out.structure);
6811            }
6812            Err(IndicatorDispatchError::UnknownOutput {
6813                indicator: "market_structure_trailing_stop".to_string(),
6814                output: output_id.to_string(),
6815            })
6816        },
6817    )
6818}
6819
6820fn compute_evasive_supertrend_batch(
6821    req: IndicatorBatchRequest<'_>,
6822    output_id: &str,
6823) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6824    let (open, high, low, close) = extract_ohlc_full_input("evasive_supertrend", req.data)?;
6825    let kernel = req.kernel.to_non_batch();
6826    collect_f64(
6827        "evasive_supertrend",
6828        output_id,
6829        req.combos,
6830        close.len(),
6831        |params| {
6832            let atr_length = get_usize_param("evasive_supertrend", params, "atr_length", 10)?;
6833            let base_multiplier =
6834                get_f64_param("evasive_supertrend", params, "base_multiplier", 3.0)?;
6835            let noise_threshold =
6836                get_f64_param("evasive_supertrend", params, "noise_threshold", 1.0)?;
6837            let expansion_alpha =
6838                get_f64_param("evasive_supertrend", params, "expansion_alpha", 0.5)?;
6839            let input = EvasiveSuperTrendInput::from_slices(
6840                open,
6841                high,
6842                low,
6843                close,
6844                EvasiveSuperTrendParams {
6845                    atr_length: Some(atr_length),
6846                    base_multiplier: Some(base_multiplier),
6847                    noise_threshold: Some(noise_threshold),
6848                    expansion_alpha: Some(expansion_alpha),
6849                },
6850            );
6851            let out = evasive_supertrend_with_kernel(&input, kernel).map_err(|e| {
6852                IndicatorDispatchError::ComputeFailed {
6853                    indicator: "evasive_supertrend".to_string(),
6854                    details: e.to_string(),
6855                }
6856            })?;
6857            if output_id.eq_ignore_ascii_case("band") || output_id.eq_ignore_ascii_case("value") {
6858                return Ok(out.band);
6859            }
6860            if output_id.eq_ignore_ascii_case("state") {
6861                return Ok(out.state);
6862            }
6863            if output_id.eq_ignore_ascii_case("noisy") {
6864                return Ok(out.noisy);
6865            }
6866            if output_id.eq_ignore_ascii_case("changed") {
6867                return Ok(out.changed);
6868            }
6869            Err(IndicatorDispatchError::UnknownOutput {
6870                indicator: "evasive_supertrend".to_string(),
6871                output: output_id.to_string(),
6872            })
6873        },
6874    )
6875}
6876
6877fn compute_reversal_signals_batch(
6878    req: IndicatorBatchRequest<'_>,
6879    output_id: &str,
6880) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6881    let (open, high, low, close, volume) =
6882        extract_ohlcv_full_input("reversal_signals", req.data)?;
6883    let kernel = req.kernel.to_non_batch();
6884    collect_f64("reversal_signals", output_id, req.combos, close.len(), |params| {
6885        let lookback_period =
6886            get_usize_param("reversal_signals", params, "lookback_period", 12)?;
6887        let confirmation_period =
6888            get_usize_param("reversal_signals", params, "confirmation_period", 3)?;
6889        let use_volume_confirmation = get_bool_param(
6890            "reversal_signals",
6891            params,
6892            "use_volume_confirmation",
6893            true,
6894        )?;
6895        let trend_ma_period =
6896            get_usize_param("reversal_signals", params, "trend_ma_period", 50)?;
6897        let trend_ma_type =
6898            get_enum_param("reversal_signals", params, "trend_ma_type", "EMA")?;
6899        let ma_step_period =
6900            get_usize_param("reversal_signals", params, "ma_step_period", 33)?;
6901        let input = ReversalSignalsInput::from_slices(
6902            open,
6903            high,
6904            low,
6905            close,
6906            volume,
6907            ReversalSignalsParams {
6908                lookback_period: Some(lookback_period),
6909                confirmation_period: Some(confirmation_period),
6910                use_volume_confirmation: Some(use_volume_confirmation),
6911                trend_ma_period: Some(trend_ma_period),
6912                trend_ma_type: Some(trend_ma_type.to_string()),
6913                ma_step_period: Some(ma_step_period),
6914            },
6915        );
6916        let out = reversal_signals_with_kernel(&input, kernel).map_err(|e| {
6917            IndicatorDispatchError::ComputeFailed {
6918                indicator: "reversal_signals".to_string(),
6919                details: e.to_string(),
6920            }
6921        })?;
6922        if output_id.eq_ignore_ascii_case("buy_signal") {
6923            return Ok(out.buy_signal);
6924        }
6925        if output_id.eq_ignore_ascii_case("sell_signal") {
6926            return Ok(out.sell_signal);
6927        }
6928        if output_id.eq_ignore_ascii_case("stepped_ma") || output_id.eq_ignore_ascii_case("value")
6929        {
6930            return Ok(out.stepped_ma);
6931        }
6932        if output_id.eq_ignore_ascii_case("state") {
6933            return Ok(out.state);
6934        }
6935        Err(IndicatorDispatchError::UnknownOutput {
6936            indicator: "reversal_signals".to_string(),
6937            output: output_id.to_string(),
6938        })
6939    })
6940}
6941
6942fn compute_zig_zag_channels_batch(
6943    req: IndicatorBatchRequest<'_>,
6944    output_id: &str,
6945) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6946    let (open, high, low, close) = extract_ohlc_full_input("zig_zag_channels", req.data)?;
6947    let kernel = req.kernel.to_non_batch();
6948    collect_f64(
6949        "zig_zag_channels",
6950        output_id,
6951        req.combos,
6952        close.len(),
6953        |params| {
6954            let length = get_usize_param("zig_zag_channels", params, "length", 100)?;
6955            let extend = get_bool_param("zig_zag_channels", params, "extend", true)?;
6956            let input = ZigZagChannelsInput::from_slices(
6957                open,
6958                high,
6959                low,
6960                close,
6961                ZigZagChannelsParams {
6962                    length: Some(length),
6963                    extend: Some(extend),
6964                },
6965            );
6966            let out = zig_zag_channels_with_kernel(&input, kernel).map_err(|e| {
6967                IndicatorDispatchError::ComputeFailed {
6968                    indicator: "zig_zag_channels".to_string(),
6969                    details: e.to_string(),
6970                }
6971            })?;
6972            if output_id.eq_ignore_ascii_case("middle") || output_id.eq_ignore_ascii_case("value") {
6973                return Ok(out.middle);
6974            }
6975            if output_id.eq_ignore_ascii_case("upper") {
6976                return Ok(out.upper);
6977            }
6978            if output_id.eq_ignore_ascii_case("lower") {
6979                return Ok(out.lower);
6980            }
6981            Err(IndicatorDispatchError::UnknownOutput {
6982                indicator: "zig_zag_channels".to_string(),
6983                output: output_id.to_string(),
6984            })
6985        },
6986    )
6987}
6988
6989fn compute_directional_imbalance_index_batch(
6990    req: IndicatorBatchRequest<'_>,
6991    output_id: &str,
6992) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6993    let (high, low) = match req.data {
6994        IndicatorDataRef::Candles { candles, .. } => {
6995            (candles.high.as_slice(), candles.low.as_slice())
6996        }
6997        IndicatorDataRef::HighLow { high, low } => (high, low),
6998        IndicatorDataRef::Ohlc { high, low, .. } => (high, low),
6999        IndicatorDataRef::Ohlcv { high, low, .. } => (high, low),
7000        _ => {
7001            return Err(IndicatorDispatchError::MissingRequiredInput {
7002                indicator: "directional_imbalance_index".to_string(),
7003                input: IndicatorInputKind::HighLow,
7004            });
7005        }
7006    };
7007    let kernel = req.kernel.to_non_batch();
7008    collect_f64(
7009        "directional_imbalance_index",
7010        output_id,
7011        req.combos,
7012        high.len(),
7013        |params| {
7014            let length = get_usize_param("directional_imbalance_index", params, "length", 10)?;
7015            let period = get_usize_param("directional_imbalance_index", params, "period", 70)?;
7016            let input = DirectionalImbalanceIndexInput::from_slices(
7017                high,
7018                low,
7019                DirectionalImbalanceIndexParams {
7020                    length: Some(length),
7021                    period: Some(period),
7022                },
7023            );
7024            let out = directional_imbalance_index_with_kernel(&input, kernel).map_err(|e| {
7025                IndicatorDispatchError::ComputeFailed {
7026                    indicator: "directional_imbalance_index".to_string(),
7027                    details: e.to_string(),
7028                }
7029            })?;
7030            if output_id.eq_ignore_ascii_case("up") || output_id.eq_ignore_ascii_case("value") {
7031                return Ok(out.up);
7032            }
7033            if output_id.eq_ignore_ascii_case("down") {
7034                return Ok(out.down);
7035            }
7036            if output_id.eq_ignore_ascii_case("bulls") {
7037                return Ok(out.bulls);
7038            }
7039            if output_id.eq_ignore_ascii_case("bears") {
7040                return Ok(out.bears);
7041            }
7042            if output_id.eq_ignore_ascii_case("upper") {
7043                return Ok(out.upper);
7044            }
7045            if output_id.eq_ignore_ascii_case("lower") {
7046                return Ok(out.lower);
7047            }
7048            Err(IndicatorDispatchError::UnknownOutput {
7049                indicator: "directional_imbalance_index".to_string(),
7050                output: output_id.to_string(),
7051            })
7052        },
7053    )
7054}
7055
7056fn compute_candle_strength_oscillator_batch(
7057    req: IndicatorBatchRequest<'_>,
7058    output_id: &str,
7059) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7060    let (open, high, low, close) = match req.data {
7061        IndicatorDataRef::Candles { candles, .. } => (
7062            candles.open.as_slice(),
7063            candles.high.as_slice(),
7064            candles.low.as_slice(),
7065            candles.close.as_slice(),
7066        ),
7067        IndicatorDataRef::Ohlc {
7068            open,
7069            high,
7070            low,
7071            close,
7072        } => (open, high, low, close),
7073        IndicatorDataRef::Ohlcv {
7074            open,
7075            high,
7076            low,
7077            close,
7078            ..
7079        } => (open, high, low, close),
7080        _ => {
7081            return Err(IndicatorDispatchError::MissingRequiredInput {
7082                indicator: "candle_strength_oscillator".to_string(),
7083                input: IndicatorInputKind::Ohlc,
7084            });
7085        }
7086    };
7087    let kernel = req.kernel.to_non_batch();
7088    collect_f64(
7089        "candle_strength_oscillator",
7090        output_id,
7091        req.combos,
7092        close.len(),
7093        |params| {
7094            let period = get_usize_param("candle_strength_oscillator", params, "period", 50)?;
7095            let atr_enabled =
7096                get_bool_param("candle_strength_oscillator", params, "atr_enabled", false)?;
7097            let atr_length =
7098                get_usize_param("candle_strength_oscillator", params, "atr_length", 50)?;
7099            let mode = get_enum_param("candle_strength_oscillator", params, "mode", "bollinger")?;
7100            let input = CandleStrengthOscillatorInput::from_slices(
7101                open,
7102                high,
7103                low,
7104                close,
7105                CandleStrengthOscillatorParams {
7106                    period: Some(period),
7107                    atr_enabled: Some(atr_enabled),
7108                    atr_length: Some(atr_length),
7109                    mode: Some(mode.to_string()),
7110                },
7111            );
7112            let out = candle_strength_oscillator_with_kernel(&input, kernel).map_err(|e| {
7113                IndicatorDispatchError::ComputeFailed {
7114                    indicator: "candle_strength_oscillator".to_string(),
7115                    details: e.to_string(),
7116                }
7117            })?;
7118            if output_id.eq_ignore_ascii_case("strength") || output_id.eq_ignore_ascii_case("value")
7119            {
7120                return Ok(out.strength);
7121            }
7122            if output_id.eq_ignore_ascii_case("highs") {
7123                return Ok(out.highs);
7124            }
7125            if output_id.eq_ignore_ascii_case("lows") {
7126                return Ok(out.lows);
7127            }
7128            if output_id.eq_ignore_ascii_case("mid") {
7129                return Ok(out.mid);
7130            }
7131            if output_id.eq_ignore_ascii_case("long_signal") {
7132                return Ok(out.long_signal);
7133            }
7134            if output_id.eq_ignore_ascii_case("short_signal") {
7135                return Ok(out.short_signal);
7136            }
7137            Err(IndicatorDispatchError::UnknownOutput {
7138                indicator: "candle_strength_oscillator".to_string(),
7139                output: output_id.to_string(),
7140            })
7141        },
7142    )
7143}
7144
7145fn compute_gmma_oscillator_batch(
7146    req: IndicatorBatchRequest<'_>,
7147    output_id: &str,
7148) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7149    let kernel = req.kernel.to_non_batch();
7150    let owned_source;
7151    let data = match req.data {
7152        IndicatorDataRef::Slice { values } => values,
7153        IndicatorDataRef::Candles { candles, source } => {
7154            source_type(candles, source.unwrap_or("close"))
7155        }
7156        IndicatorDataRef::Ohlc { close, .. } => close,
7157        IndicatorDataRef::Ohlcv { close, .. } => close,
7158        IndicatorDataRef::CloseVolume { close, volume } => {
7159            ensure_same_len_2("gmma_oscillator", close.len(), volume.len())?;
7160            close
7161        }
7162        IndicatorDataRef::HighLow { high, low } => {
7163            ensure_same_len_2("gmma_oscillator", high.len(), low.len())?;
7164            owned_source = high
7165                .iter()
7166                .zip(low.iter())
7167                .map(|(&h, &l)| (h + l) * 0.5)
7168                .collect::<Vec<_>>();
7169            owned_source.as_slice()
7170        }
7171    };
7172
7173    collect_f64(
7174        "gmma_oscillator",
7175        output_id,
7176        req.combos,
7177        data.len(),
7178        |params| {
7179            let gmma_type = get_enum_param("gmma_oscillator", params, "gmma_type", "guppy")?;
7180            let smooth_length = get_usize_param("gmma_oscillator", params, "smooth_length", 1)?;
7181            let signal_length = get_usize_param("gmma_oscillator", params, "signal_length", 13)?;
7182            let anchor_minutes = get_usize_param("gmma_oscillator", params, "anchor_minutes", 0)?;
7183            let interval_minutes = if params
7184                .iter()
7185                .any(|param| param.key.eq_ignore_ascii_case("interval_minutes"))
7186            {
7187                Some(get_usize_param(
7188                    "gmma_oscillator",
7189                    params,
7190                    "interval_minutes",
7191                    1,
7192                )?)
7193            } else {
7194                None
7195            };
7196            let input = GmmaOscillatorInput::from_slice(
7197                data,
7198                GmmaOscillatorParams {
7199                    gmma_type: Some(gmma_type.to_string()),
7200                    smooth_length: Some(smooth_length),
7201                    signal_length: Some(signal_length),
7202                    anchor_minutes: Some(anchor_minutes),
7203                    interval_minutes,
7204                },
7205            );
7206            let out = gmma_oscillator_with_kernel(&input, kernel).map_err(|e| {
7207                IndicatorDispatchError::ComputeFailed {
7208                    indicator: "gmma_oscillator".to_string(),
7209                    details: e.to_string(),
7210                }
7211            })?;
7212            if output_id.eq_ignore_ascii_case("oscillator")
7213                || output_id.eq_ignore_ascii_case("value")
7214            {
7215                return Ok(out.oscillator);
7216            }
7217            if output_id.eq_ignore_ascii_case("signal") {
7218                return Ok(out.signal);
7219            }
7220            Err(IndicatorDispatchError::UnknownOutput {
7221                indicator: "gmma_oscillator".to_string(),
7222                output: output_id.to_string(),
7223            })
7224        },
7225    )
7226}
7227
7228fn compute_nonlinear_regression_zero_lag_moving_average_batch(
7229    req: IndicatorBatchRequest<'_>,
7230    output_id: &str,
7231) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7232    let data = extract_slice_input(
7233        "nonlinear_regression_zero_lag_moving_average",
7234        req.data,
7235        "close",
7236    )?;
7237    let kernel = req.kernel.to_non_batch();
7238    collect_f64(
7239        "nonlinear_regression_zero_lag_moving_average",
7240        output_id,
7241        req.combos,
7242        data.len(),
7243        |params| {
7244            let zlma_period = get_usize_param(
7245                "nonlinear_regression_zero_lag_moving_average",
7246                params,
7247                "zlma_period",
7248                15,
7249            )?;
7250            let regression_period = get_usize_param(
7251                "nonlinear_regression_zero_lag_moving_average",
7252                params,
7253                "regression_period",
7254                15,
7255            )?;
7256            let input = NonlinearRegressionZeroLagMovingAverageInput::from_slice(
7257                data,
7258                NonlinearRegressionZeroLagMovingAverageParams {
7259                    zlma_period: Some(zlma_period),
7260                    regression_period: Some(regression_period),
7261                },
7262            );
7263            let out = nonlinear_regression_zero_lag_moving_average_with_kernel(&input, kernel)
7264                .map_err(|e| IndicatorDispatchError::ComputeFailed {
7265                    indicator: "nonlinear_regression_zero_lag_moving_average".to_string(),
7266                    details: e.to_string(),
7267                })?;
7268            if output_id.eq_ignore_ascii_case("value") {
7269                return Ok(out.value);
7270            }
7271            if output_id.eq_ignore_ascii_case("signal") {
7272                return Ok(out.signal);
7273            }
7274            if output_id.eq_ignore_ascii_case("long_signal") {
7275                return Ok(out.long_signal);
7276            }
7277            if output_id.eq_ignore_ascii_case("short_signal") {
7278                return Ok(out.short_signal);
7279            }
7280            Err(IndicatorDispatchError::UnknownOutput {
7281                indicator: "nonlinear_regression_zero_lag_moving_average".to_string(),
7282                output: output_id.to_string(),
7283            })
7284        },
7285    )
7286}
7287
7288fn compute_possible_rsi_batch(
7289    req: IndicatorBatchRequest<'_>,
7290    output_id: &str,
7291) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7292    let data = extract_slice_input("possible_rsi", req.data, "close")?;
7293    let kernel = req.kernel.to_non_batch();
7294    collect_f64(
7295        "possible_rsi",
7296        output_id,
7297        req.combos,
7298        data.len(),
7299        |params| {
7300            let period = get_usize_param("possible_rsi", params, "period", 32)?;
7301            let rsi_mode = get_enum_param("possible_rsi", params, "rsi_mode", "regular")?;
7302            let norm_period = get_usize_param("possible_rsi", params, "norm_period", 100)?;
7303            let normalization_mode = get_enum_param(
7304                "possible_rsi",
7305                params,
7306                "normalization_mode",
7307                "gaussian_fisher",
7308            )?;
7309            let normalization_length =
7310                get_usize_param("possible_rsi", params, "normalization_length", 15)?;
7311            let nonlag_period = get_usize_param("possible_rsi", params, "nonlag_period", 15)?;
7312            let dynamic_zone_period =
7313                get_usize_param("possible_rsi", params, "dynamic_zone_period", 20)?;
7314            let buy_probability = get_f64_param("possible_rsi", params, "buy_probability", 0.2)?;
7315            let sell_probability = get_f64_param("possible_rsi", params, "sell_probability", 0.2)?;
7316            let signal_type =
7317                get_enum_param("possible_rsi", params, "signal_type", "zeroline_crossover")?;
7318            let run_highpass = get_bool_param("possible_rsi", params, "run_highpass", false)?;
7319            let highpass_period = get_usize_param("possible_rsi", params, "highpass_period", 15)?;
7320            let input = PossibleRsiInput::from_slice(
7321                data,
7322                PossibleRsiParams {
7323                    period: Some(period),
7324                    rsi_mode: Some(rsi_mode.to_string()),
7325                    norm_period: Some(norm_period),
7326                    normalization_mode: Some(normalization_mode.to_string()),
7327                    normalization_length: Some(normalization_length),
7328                    nonlag_period: Some(nonlag_period),
7329                    dynamic_zone_period: Some(dynamic_zone_period),
7330                    buy_probability: Some(buy_probability),
7331                    sell_probability: Some(sell_probability),
7332                    signal_type: Some(signal_type.to_string()),
7333                    run_highpass: Some(run_highpass),
7334                    highpass_period: Some(highpass_period),
7335                },
7336            );
7337            let out = possible_rsi_with_kernel(&input, kernel).map_err(|e| {
7338                IndicatorDispatchError::ComputeFailed {
7339                    indicator: "possible_rsi".to_string(),
7340                    details: e.to_string(),
7341                }
7342            })?;
7343            if output_id.eq_ignore_ascii_case("value") {
7344                return Ok(out.value);
7345            }
7346            if output_id.eq_ignore_ascii_case("buy_level") {
7347                return Ok(out.buy_level);
7348            }
7349            if output_id.eq_ignore_ascii_case("sell_level") {
7350                return Ok(out.sell_level);
7351            }
7352            if output_id.eq_ignore_ascii_case("middle")
7353                || output_id.eq_ignore_ascii_case("middle_level")
7354            {
7355                return Ok(out.middle_level);
7356            }
7357            if output_id.eq_ignore_ascii_case("trend") || output_id.eq_ignore_ascii_case("state") {
7358                return Ok(out.state);
7359            }
7360            if output_id.eq_ignore_ascii_case("long_signal") {
7361                return Ok(out.long_signal);
7362            }
7363            if output_id.eq_ignore_ascii_case("short_signal") {
7364                return Ok(out.short_signal);
7365            }
7366            Err(IndicatorDispatchError::UnknownOutput {
7367                indicator: "possible_rsi".to_string(),
7368                output: output_id.to_string(),
7369            })
7370        },
7371    )
7372}
7373
7374fn compute_autocorrelation_indicator_batch(
7375    req: IndicatorBatchRequest<'_>,
7376    output_id: &str,
7377) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7378    let data = extract_slice_input("autocorrelation_indicator", req.data, "close")?;
7379    let kernel = req.kernel.to_non_batch();
7380    collect_f64(
7381        "autocorrelation_indicator",
7382        output_id,
7383        req.combos,
7384        data.len(),
7385        |params| {
7386            let length = get_usize_param("autocorrelation_indicator", params, "length", 20)?;
7387            let lag = get_usize_param("autocorrelation_indicator", params, "lag", 1)?;
7388            let use_test_signal = get_bool_param(
7389                "autocorrelation_indicator",
7390                params,
7391                "use_test_signal",
7392                false,
7393            )?;
7394            let max_lag = if output_id.eq_ignore_ascii_case("correlation") {
7395                lag
7396            } else {
7397                1
7398            };
7399            let input = AutocorrelationIndicatorInput::from_slice(
7400                data,
7401                AutocorrelationIndicatorParams {
7402                    length: Some(length),
7403                    max_lag: Some(max_lag),
7404                    use_test_signal: Some(use_test_signal),
7405                },
7406            );
7407            let out = autocorrelation_indicator_with_kernel(&input, kernel).map_err(|e| {
7408                IndicatorDispatchError::ComputeFailed {
7409                    indicator: "autocorrelation_indicator".to_string(),
7410                    details: e.to_string(),
7411                }
7412            })?;
7413            if output_id.eq_ignore_ascii_case("filtered") || output_id.eq_ignore_ascii_case("value")
7414            {
7415                return Ok(out.filtered);
7416            }
7417            if output_id.eq_ignore_ascii_case("correlation") {
7418                let start = (lag - 1).checked_mul(data.len()).ok_or_else(|| {
7419                    IndicatorDispatchError::ComputeFailed {
7420                        indicator: "autocorrelation_indicator".to_string(),
7421                        details: "lag * cols overflow".to_string(),
7422                    }
7423                })?;
7424                let end = start + data.len();
7425                return Ok(out.correlations[start..end].to_vec());
7426            }
7427            Err(IndicatorDispatchError::UnknownOutput {
7428                indicator: "autocorrelation_indicator".to_string(),
7429                output: output_id.to_string(),
7430            })
7431        },
7432    )
7433}
7434
7435fn compute_goertzel_cycle_composite_wave_batch(
7436    req: IndicatorBatchRequest<'_>,
7437    output_id: &str,
7438) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7439    if !output_id.eq_ignore_ascii_case("value") && !output_id.eq_ignore_ascii_case("wave") {
7440        return Err(IndicatorDispatchError::UnknownOutput {
7441            indicator: "goertzel_cycle_composite_wave".to_string(),
7442            output: output_id.to_string(),
7443        });
7444    }
7445    let data = extract_slice_input("goertzel_cycle_composite_wave", req.data, "close")?;
7446    let kernel = req.kernel.to_non_batch();
7447    collect_f64_into_rows(
7448        "goertzel_cycle_composite_wave",
7449        output_id,
7450        req.combos,
7451        data.len(),
7452        |params, row| {
7453            let max_period =
7454                get_usize_param("goertzel_cycle_composite_wave", params, "max_period", 120)?;
7455            let start_at_cycle =
7456                get_usize_param("goertzel_cycle_composite_wave", params, "start_at_cycle", 1)?;
7457            let use_top_cycles =
7458                get_usize_param("goertzel_cycle_composite_wave", params, "use_top_cycles", 2)?;
7459            let bar_to_calculate = get_usize_param(
7460                "goertzel_cycle_composite_wave",
7461                params,
7462                "bar_to_calculate",
7463                1,
7464            )?;
7465            let detrend_mode = get_enum_string_param(
7466                "goertzel_cycle_composite_wave",
7467                params,
7468                "detrend_mode",
7469                "hodrick_prescott_detrending",
7470            )?;
7471            let detrend_mode = GoertzelDetrendMode::parse(detrend_mode).ok_or_else(|| {
7472                IndicatorDispatchError::InvalidParam {
7473                    indicator: "goertzel_cycle_composite_wave".to_string(),
7474                    key: "detrend_mode".to_string(),
7475                    reason: format!("unknown mode: {detrend_mode}"),
7476                }
7477            })?;
7478            let dt_zl_per1 =
7479                get_usize_param("goertzel_cycle_composite_wave", params, "dt_zl_per1", 10)?;
7480            let dt_zl_per2 =
7481                get_usize_param("goertzel_cycle_composite_wave", params, "dt_zl_per2", 40)?;
7482            let dt_hp_per1 =
7483                get_usize_param("goertzel_cycle_composite_wave", params, "dt_hp_per1", 20)?;
7484            let dt_hp_per2 =
7485                get_usize_param("goertzel_cycle_composite_wave", params, "dt_hp_per2", 80)?;
7486            let dt_reg_zl_smooth_per = get_usize_param(
7487                "goertzel_cycle_composite_wave",
7488                params,
7489                "dt_reg_zl_smooth_per",
7490                5,
7491            )?;
7492            let hp_smooth_per =
7493                get_usize_param("goertzel_cycle_composite_wave", params, "hp_smooth_per", 20)?;
7494            let zlma_smooth_per = get_usize_param(
7495                "goertzel_cycle_composite_wave",
7496                params,
7497                "zlma_smooth_per",
7498                10,
7499            )?;
7500            let filter_bartels = get_bool_param(
7501                "goertzel_cycle_composite_wave",
7502                params,
7503                "filter_bartels",
7504                false,
7505            )?;
7506            let bart_no_cycles =
7507                get_usize_param("goertzel_cycle_composite_wave", params, "bart_no_cycles", 5)?;
7508            let bart_smooth_per = get_usize_param(
7509                "goertzel_cycle_composite_wave",
7510                params,
7511                "bart_smooth_per",
7512                2,
7513            )?;
7514            let bart_sig_limit = get_usize_param(
7515                "goertzel_cycle_composite_wave",
7516                params,
7517                "bart_sig_limit",
7518                50,
7519            )?;
7520            let sort_bartels = get_bool_param(
7521                "goertzel_cycle_composite_wave",
7522                params,
7523                "sort_bartels",
7524                false,
7525            )?;
7526            let squared_amp =
7527                get_bool_param("goertzel_cycle_composite_wave", params, "squared_amp", true)?;
7528            let use_cosine =
7529                get_bool_param("goertzel_cycle_composite_wave", params, "use_cosine", true)?;
7530            let subtract_noise = get_bool_param(
7531                "goertzel_cycle_composite_wave",
7532                params,
7533                "subtract_noise",
7534                false,
7535            )?;
7536            let use_cycle_strength = get_bool_param(
7537                "goertzel_cycle_composite_wave",
7538                params,
7539                "use_cycle_strength",
7540                true,
7541            )?;
7542
7543            let input = GoertzelCycleCompositeWaveInput::from_slice(
7544                data,
7545                GoertzelCycleCompositeWaveParams {
7546                    max_period: Some(max_period),
7547                    start_at_cycle: Some(start_at_cycle),
7548                    use_top_cycles: Some(use_top_cycles),
7549                    bar_to_calculate: Some(bar_to_calculate),
7550                    detrend_mode: Some(detrend_mode),
7551                    dt_zl_per1: Some(dt_zl_per1),
7552                    dt_zl_per2: Some(dt_zl_per2),
7553                    dt_hp_per1: Some(dt_hp_per1),
7554                    dt_hp_per2: Some(dt_hp_per2),
7555                    dt_reg_zl_smooth_per: Some(dt_reg_zl_smooth_per),
7556                    hp_smooth_per: Some(hp_smooth_per),
7557                    zlma_smooth_per: Some(zlma_smooth_per),
7558                    filter_bartels: Some(filter_bartels),
7559                    bart_no_cycles: Some(bart_no_cycles),
7560                    bart_smooth_per: Some(bart_smooth_per),
7561                    bart_sig_limit: Some(bart_sig_limit),
7562                    sort_bartels: Some(sort_bartels),
7563                    squared_amp: Some(squared_amp),
7564                    use_cosine: Some(use_cosine),
7565                    subtract_noise: Some(subtract_noise),
7566                    use_cycle_strength: Some(use_cycle_strength),
7567                },
7568            );
7569            goertzel_cycle_composite_wave_into_slice(row, &input, kernel).map_err(|e| {
7570                IndicatorDispatchError::ComputeFailed {
7571                    indicator: "goertzel_cycle_composite_wave".to_string(),
7572                    details: e.to_string(),
7573                }
7574            })
7575        },
7576    )
7577}
7578
7579fn compute_rolling_skewness_kurtosis_batch(
7580    req: IndicatorBatchRequest<'_>,
7581    output_id: &str,
7582) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7583    let data = extract_slice_input("rolling_skewness_kurtosis", req.data, "close")?;
7584    let kernel = req.kernel.to_non_batch();
7585    collect_f64(
7586        "rolling_skewness_kurtosis",
7587        output_id,
7588        req.combos,
7589        data.len(),
7590        |params| {
7591            let length = get_usize_param("rolling_skewness_kurtosis", params, "length", 50)?;
7592            let smooth_length =
7593                get_usize_param("rolling_skewness_kurtosis", params, "smooth_length", 3)?;
7594            let input = RollingSkewnessKurtosisInput::from_slice(
7595                data,
7596                RollingSkewnessKurtosisParams {
7597                    length: Some(length),
7598                    smooth_length: Some(smooth_length),
7599                },
7600            );
7601            let out = rolling_skewness_kurtosis_with_kernel(&input, kernel).map_err(|e| {
7602                IndicatorDispatchError::ComputeFailed {
7603                    indicator: "rolling_skewness_kurtosis".to_string(),
7604                    details: e.to_string(),
7605                }
7606            })?;
7607            if output_id.eq_ignore_ascii_case("skewness") {
7608                return Ok(out.skewness);
7609            }
7610            if output_id.eq_ignore_ascii_case("kurtosis") {
7611                return Ok(out.kurtosis);
7612            }
7613            Err(IndicatorDispatchError::UnknownOutput {
7614                indicator: "rolling_skewness_kurtosis".to_string(),
7615                output: output_id.to_string(),
7616            })
7617        },
7618    )
7619}
7620
7621fn compute_rolling_z_score_trend_batch(
7622    req: IndicatorBatchRequest<'_>,
7623    output_id: &str,
7624) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7625    let data = extract_slice_input("rolling_z_score_trend", req.data, "close")?;
7626    let kernel = req.kernel.to_non_batch();
7627    collect_f64(
7628        "rolling_z_score_trend",
7629        output_id,
7630        req.combos,
7631        data.len(),
7632        |params| {
7633            let lookback_period =
7634                get_usize_param("rolling_z_score_trend", params, "lookback_period", 20)?;
7635            let input = RollingZScoreTrendInput::from_slice(
7636                data,
7637                RollingZScoreTrendParams {
7638                    lookback_period: Some(lookback_period),
7639                },
7640            );
7641            let out = rolling_z_score_trend_with_kernel(&input, kernel).map_err(|e| {
7642                IndicatorDispatchError::ComputeFailed {
7643                    indicator: "rolling_z_score_trend".to_string(),
7644                    details: e.to_string(),
7645                }
7646            })?;
7647            if output_id.eq_ignore_ascii_case("zscore") {
7648                return Ok(out.zscore);
7649            }
7650            if output_id.eq_ignore_ascii_case("momentum") {
7651                return Ok(out.momentum);
7652            }
7653            Err(IndicatorDispatchError::UnknownOutput {
7654                indicator: "rolling_z_score_trend".to_string(),
7655                output: output_id.to_string(),
7656            })
7657        },
7658    )
7659}
7660
7661fn compute_ehlers_data_sampling_relative_strength_indicator_batch(
7662    req: IndicatorBatchRequest<'_>,
7663    output_id: &str,
7664) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7665    let (open, close) = match req.data {
7666        IndicatorDataRef::Candles { candles, .. } => {
7667            (candles.open.as_slice(), candles.close.as_slice())
7668        }
7669        IndicatorDataRef::Ohlc {
7670            open,
7671            high,
7672            low,
7673            close,
7674        } => {
7675            ensure_same_len_4(
7676                "ehlers_data_sampling_relative_strength_indicator",
7677                open.len(),
7678                high.len(),
7679                low.len(),
7680                close.len(),
7681            )?;
7682            (open, close)
7683        }
7684        IndicatorDataRef::Ohlcv {
7685            open,
7686            high,
7687            low,
7688            close,
7689            volume,
7690        } => {
7691            ensure_same_len_5(
7692                "ehlers_data_sampling_relative_strength_indicator",
7693                open.len(),
7694                high.len(),
7695                low.len(),
7696                close.len(),
7697                volume.len(),
7698            )?;
7699            (open, close)
7700        }
7701        _ => {
7702            return Err(IndicatorDispatchError::MissingRequiredInput {
7703                indicator: "ehlers_data_sampling_relative_strength_indicator".to_string(),
7704                input: IndicatorInputKind::Ohlc,
7705            })
7706        }
7707    };
7708    let kernel = req.kernel.to_non_batch();
7709    collect_f64(
7710        "ehlers_data_sampling_relative_strength_indicator",
7711        output_id,
7712        req.combos,
7713        close.len(),
7714        |params| {
7715            let length = get_usize_param(
7716                "ehlers_data_sampling_relative_strength_indicator",
7717                params,
7718                "length",
7719                14,
7720            )?;
7721            let input = EhlersDataSamplingRelativeStrengthIndicatorInput::from_slices(
7722                open,
7723                close,
7724                EhlersDataSamplingRelativeStrengthIndicatorParams {
7725                    length: Some(length),
7726                },
7727            );
7728            let out = ehlers_data_sampling_relative_strength_indicator_with_kernel(&input, kernel)
7729                .map_err(|e| IndicatorDispatchError::ComputeFailed {
7730                    indicator: "ehlers_data_sampling_relative_strength_indicator".to_string(),
7731                    details: e.to_string(),
7732                })?;
7733            if output_id.eq_ignore_ascii_case("ds_rsi")
7734                || output_id.eq_ignore_ascii_case("data_sampling_rsi")
7735            {
7736                return Ok(out.ds_rsi);
7737            }
7738            if output_id.eq_ignore_ascii_case("original_rsi")
7739                || output_id.eq_ignore_ascii_case("orig_rsi")
7740            {
7741                return Ok(out.original_rsi);
7742            }
7743            if output_id.eq_ignore_ascii_case("signal") {
7744                return Ok(out.signal);
7745            }
7746            Err(IndicatorDispatchError::UnknownOutput {
7747                indicator: "ehlers_data_sampling_relative_strength_indicator".to_string(),
7748                output: output_id.to_string(),
7749            })
7750        },
7751    )
7752}
7753
7754fn compute_velocity_acceleration_convergence_divergence_indicator_batch(
7755    req: IndicatorBatchRequest<'_>,
7756    output_id: &str,
7757) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7758    let owned_source;
7759    let data = match req.data {
7760        IndicatorDataRef::Slice { values } => values,
7761        IndicatorDataRef::Candles { candles, source } => {
7762            source_type(candles, source.unwrap_or("hlcc4"))
7763        }
7764        IndicatorDataRef::Ohlc {
7765            open,
7766            high,
7767            low,
7768            close,
7769        } => {
7770            ensure_same_len_4(
7771                "velocity_acceleration_convergence_divergence_indicator",
7772                open.len(),
7773                high.len(),
7774                low.len(),
7775                close.len(),
7776            )?;
7777            owned_source = high
7778                .iter()
7779                .zip(low.iter())
7780                .zip(close.iter())
7781                .map(|((&h, &l), &c)| (h + l + 2.0 * c) * 0.25)
7782                .collect::<Vec<_>>();
7783            owned_source.as_slice()
7784        }
7785        IndicatorDataRef::Ohlcv {
7786            open,
7787            high,
7788            low,
7789            close,
7790            volume,
7791        } => {
7792            ensure_same_len_5(
7793                "velocity_acceleration_convergence_divergence_indicator",
7794                open.len(),
7795                high.len(),
7796                low.len(),
7797                close.len(),
7798                volume.len(),
7799            )?;
7800            owned_source = high
7801                .iter()
7802                .zip(low.iter())
7803                .zip(close.iter())
7804                .map(|((&h, &l), &c)| (h + l + 2.0 * c) * 0.25)
7805                .collect::<Vec<_>>();
7806            owned_source.as_slice()
7807        }
7808        IndicatorDataRef::CloseVolume { close, volume } => {
7809            ensure_same_len_2(
7810                "velocity_acceleration_convergence_divergence_indicator",
7811                close.len(),
7812                volume.len(),
7813            )?;
7814            close
7815        }
7816        IndicatorDataRef::HighLow { .. } => {
7817            return Err(IndicatorDispatchError::MissingRequiredInput {
7818                indicator: "velocity_acceleration_convergence_divergence_indicator".to_string(),
7819                input: IndicatorInputKind::Candles,
7820            });
7821        }
7822    };
7823    let kernel = req.kernel.to_non_batch();
7824    collect_f64(
7825        "velocity_acceleration_convergence_divergence_indicator",
7826        output_id,
7827        req.combos,
7828        data.len(),
7829        |params| {
7830            let length = get_usize_param(
7831                "velocity_acceleration_convergence_divergence_indicator",
7832                params,
7833                "length",
7834                21,
7835            )?;
7836            let smooth_length = get_usize_param(
7837                "velocity_acceleration_convergence_divergence_indicator",
7838                params,
7839                "smooth_length",
7840                5,
7841            )?;
7842            let input = VelocityAccelerationConvergenceDivergenceIndicatorInput::from_slice(
7843                data,
7844                VelocityAccelerationConvergenceDivergenceIndicatorParams {
7845                    length: Some(length),
7846                    smooth_length: Some(smooth_length),
7847                },
7848            );
7849            let out =
7850                velocity_acceleration_convergence_divergence_indicator_with_kernel(&input, kernel)
7851                    .map_err(|e| IndicatorDispatchError::ComputeFailed {
7852                        indicator: "velocity_acceleration_convergence_divergence_indicator"
7853                            .to_string(),
7854                        details: e.to_string(),
7855                    })?;
7856            if output_id.eq_ignore_ascii_case("vacd") || output_id.eq_ignore_ascii_case("value") {
7857                return Ok(out.vacd);
7858            }
7859            if output_id.eq_ignore_ascii_case("signal") {
7860                return Ok(out.signal);
7861            }
7862            Err(IndicatorDispatchError::UnknownOutput {
7863                indicator: "velocity_acceleration_convergence_divergence_indicator".to_string(),
7864                output: output_id.to_string(),
7865            })
7866        },
7867    )
7868}
7869
7870fn compute_trend_direction_force_index_batch(
7871    req: IndicatorBatchRequest<'_>,
7872    output_id: &str,
7873) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7874    expect_value_output("trend_direction_force_index", output_id)?;
7875    let data = extract_slice_input("trend_direction_force_index", req.data, "close")?;
7876    let kernel = req.kernel.to_non_batch();
7877    collect_f64_into_rows(
7878        "trend_direction_force_index",
7879        output_id,
7880        req.combos,
7881        data.len(),
7882        |params, row| {
7883            let length = get_usize_param("trend_direction_force_index", params, "length", 10)?;
7884            let input = TrendDirectionForceIndexInput::from_slice(
7885                data,
7886                TrendDirectionForceIndexParams {
7887                    length: Some(length),
7888                },
7889            );
7890            trend_direction_force_index_into_slice(row, &input, kernel).map_err(|e| {
7891                IndicatorDispatchError::ComputeFailed {
7892                    indicator: "trend_direction_force_index".to_string(),
7893                    details: e.to_string(),
7894                }
7895            })
7896        },
7897    )
7898}
7899
7900fn compute_yang_zhang_volatility_batch(
7901    req: IndicatorBatchRequest<'_>,
7902    output_id: &str,
7903) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7904    let (open, high, low, close) = extract_ohlc_full_input("yang_zhang_volatility", req.data)?;
7905    let kernel = req.kernel.to_non_batch();
7906    collect_f64(
7907        "yang_zhang_volatility",
7908        output_id,
7909        req.combos,
7910        close.len(),
7911        |params| {
7912            let lookback = get_usize_param("yang_zhang_volatility", params, "lookback", 14)?;
7913            let k_override = get_bool_param("yang_zhang_volatility", params, "k_override", false)?;
7914            let k = get_f64_param("yang_zhang_volatility", params, "k", 0.34)?;
7915            let input = YangZhangVolatilityInput::from_slices(
7916                open,
7917                high,
7918                low,
7919                close,
7920                YangZhangVolatilityParams {
7921                    lookback: Some(lookback),
7922                    k_override: Some(k_override),
7923                    k: Some(k),
7924                },
7925            );
7926            let out = yang_zhang_volatility_with_kernel(&input, kernel).map_err(|e| {
7927                IndicatorDispatchError::ComputeFailed {
7928                    indicator: "yang_zhang_volatility".to_string(),
7929                    details: e.to_string(),
7930                }
7931            })?;
7932            if output_id.eq_ignore_ascii_case("yz") || output_id.eq_ignore_ascii_case("value") {
7933                return Ok(out.yz);
7934            }
7935            if output_id.eq_ignore_ascii_case("rs") {
7936                return Ok(out.rs);
7937            }
7938            Err(IndicatorDispatchError::UnknownOutput {
7939                indicator: "yang_zhang_volatility".to_string(),
7940                output: output_id.to_string(),
7941            })
7942        },
7943    )
7944}
7945
7946fn compute_garman_klass_volatility_batch(
7947    req: IndicatorBatchRequest<'_>,
7948    output_id: &str,
7949) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7950    let (open, high, low, close) = extract_ohlc_full_input("garman_klass_volatility", req.data)?;
7951    let kernel = req.kernel.to_non_batch();
7952    collect_f64(
7953        "garman_klass_volatility",
7954        output_id,
7955        req.combos,
7956        close.len(),
7957        |params| {
7958            let lookback = get_usize_param("garman_klass_volatility", params, "lookback", 14)?;
7959            let input = GarmanKlassVolatilityInput::from_slices(
7960                open,
7961                high,
7962                low,
7963                close,
7964                GarmanKlassVolatilityParams {
7965                    lookback: Some(lookback),
7966                },
7967            );
7968            let out = garman_klass_volatility_with_kernel(&input, kernel).map_err(|e| {
7969                IndicatorDispatchError::ComputeFailed {
7970                    indicator: "garman_klass_volatility".to_string(),
7971                    details: e.to_string(),
7972                }
7973            })?;
7974            if output_id.eq_ignore_ascii_case("value") {
7975                return Ok(out.values);
7976            }
7977            Err(IndicatorDispatchError::UnknownOutput {
7978                indicator: "garman_klass_volatility".to_string(),
7979                output: output_id.to_string(),
7980            })
7981        },
7982    )
7983}
7984
7985fn compute_atr_percentile_batch(
7986    req: IndicatorBatchRequest<'_>,
7987    output_id: &str,
7988) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7989    expect_value_output("atr_percentile", output_id)?;
7990    let (high, low, close) = extract_ohlc_input("atr_percentile", req.data)?;
7991    let kernel = req.kernel.to_non_batch();
7992    collect_f64(
7993        "atr_percentile",
7994        output_id,
7995        req.combos,
7996        close.len(),
7997        |params| {
7998            let atr_length = get_usize_param("atr_percentile", params, "atr_length", 10)?;
7999            let percentile_length =
8000                get_usize_param("atr_percentile", params, "percentile_length", 50)?;
8001            let input = AtrPercentileInput::from_slices(
8002                high,
8003                low,
8004                close,
8005                AtrPercentileParams {
8006                    atr_length: Some(atr_length),
8007                    percentile_length: Some(percentile_length),
8008                },
8009            );
8010            let out = atr_percentile_with_kernel(&input, kernel).map_err(|e| {
8011                IndicatorDispatchError::ComputeFailed {
8012                    indicator: "atr_percentile".to_string(),
8013                    details: e.to_string(),
8014                }
8015            })?;
8016            Ok(out.values)
8017        },
8018    )
8019}
8020
8021fn compute_bull_power_vs_bear_power_batch(
8022    req: IndicatorBatchRequest<'_>,
8023    output_id: &str,
8024) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8025    expect_value_output("bull_power_vs_bear_power", output_id)?;
8026    let (open, high, low, close) = extract_ohlc_full_input("bull_power_vs_bear_power", req.data)?;
8027    let kernel = req.kernel.to_non_batch();
8028    collect_f64(
8029        "bull_power_vs_bear_power",
8030        output_id,
8031        req.combos,
8032        close.len(),
8033        |params| {
8034            let period = get_usize_param("bull_power_vs_bear_power", params, "period", 5)?;
8035            let input = BullPowerVsBearPowerInput::from_slices(
8036                open,
8037                high,
8038                low,
8039                close,
8040                BullPowerVsBearPowerParams {
8041                    period: Some(period),
8042                },
8043            );
8044            let out = bull_power_vs_bear_power_with_kernel(&input, kernel).map_err(|e| {
8045                IndicatorDispatchError::ComputeFailed {
8046                    indicator: "bull_power_vs_bear_power".to_string(),
8047                    details: e.to_string(),
8048                }
8049            })?;
8050            Ok(out.values)
8051        },
8052    )
8053}
8054
8055fn compute_advance_decline_line_batch(
8056    req: IndicatorBatchRequest<'_>,
8057    output_id: &str,
8058) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8059    expect_value_output("advance_decline_line", output_id)?;
8060    let data = extract_slice_input("advance_decline_line", req.data, "close")?;
8061    let kernel = req.kernel.to_non_batch();
8062    collect_f64(
8063        "advance_decline_line",
8064        output_id,
8065        req.combos,
8066        data.len(),
8067        |_params| {
8068            let input = AdvanceDeclineLineInput::from_slice(data, AdvanceDeclineLineParams);
8069            let out = advance_decline_line_with_kernel(&input, kernel).map_err(|e| {
8070                IndicatorDispatchError::ComputeFailed {
8071                    indicator: "advance_decline_line".to_string(),
8072                    details: e.to_string(),
8073                }
8074            })?;
8075            Ok(out.values)
8076        },
8077    )
8078}
8079
8080fn compute_didi_index_batch(
8081    req: IndicatorBatchRequest<'_>,
8082    output_id: &str,
8083) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8084    let data = extract_slice_input("didi_index", req.data, "close")?;
8085    let kernel = req.kernel.to_non_batch();
8086    collect_f64("didi_index", output_id, req.combos, data.len(), |params| {
8087        let short_length = get_usize_param("didi_index", params, "short_length", 3)?;
8088        let medium_length = get_usize_param("didi_index", params, "medium_length", 8)?;
8089        let long_length = get_usize_param("didi_index", params, "long_length", 20)?;
8090        let input = DidiIndexInput::from_slice(
8091            data,
8092            DidiIndexParams {
8093                short_length: Some(short_length),
8094                medium_length: Some(medium_length),
8095                long_length: Some(long_length),
8096            },
8097        );
8098        let out = didi_index_with_kernel(&input, kernel).map_err(|e| {
8099            IndicatorDispatchError::ComputeFailed {
8100                indicator: "didi_index".to_string(),
8101                details: e.to_string(),
8102            }
8103        })?;
8104        if output_id.eq_ignore_ascii_case("short") || output_id.eq_ignore_ascii_case("value") {
8105            return Ok(out.short);
8106        }
8107        if output_id.eq_ignore_ascii_case("long") {
8108            return Ok(out.long);
8109        }
8110        if output_id.eq_ignore_ascii_case("crossover") {
8111            return Ok(out.crossover);
8112        }
8113        if output_id.eq_ignore_ascii_case("crossunder") {
8114            return Ok(out.crossunder);
8115        }
8116        Err(IndicatorDispatchError::UnknownOutput {
8117            indicator: "didi_index".to_string(),
8118            output: output_id.to_string(),
8119        })
8120    })
8121}
8122
8123fn compute_absolute_strength_index_oscillator_batch(
8124    req: IndicatorBatchRequest<'_>,
8125    output_id: &str,
8126) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8127    let data = extract_slice_input("absolute_strength_index_oscillator", req.data, "close")?;
8128    let kernel = req.kernel.to_non_batch();
8129    collect_f64(
8130        "absolute_strength_index_oscillator",
8131        output_id,
8132        req.combos,
8133        data.len(),
8134        |params| {
8135            let ema_length = get_usize_param(
8136                "absolute_strength_index_oscillator",
8137                params,
8138                "ema_length",
8139                21,
8140            )?;
8141            let signal_length = get_usize_param(
8142                "absolute_strength_index_oscillator",
8143                params,
8144                "signal_length",
8145                34,
8146            )?;
8147            let input = AbsoluteStrengthIndexOscillatorInput::from_slice(
8148                data,
8149                AbsoluteStrengthIndexOscillatorParams {
8150                    ema_length: Some(ema_length),
8151                    signal_length: Some(signal_length),
8152                },
8153            );
8154            let out =
8155                absolute_strength_index_oscillator_with_kernel(&input, kernel).map_err(|e| {
8156                    IndicatorDispatchError::ComputeFailed {
8157                        indicator: "absolute_strength_index_oscillator".to_string(),
8158                        details: e.to_string(),
8159                    }
8160                })?;
8161            if output_id.eq_ignore_ascii_case("oscillator")
8162                || output_id.eq_ignore_ascii_case("indicator")
8163                || output_id.eq_ignore_ascii_case("value")
8164            {
8165                return Ok(out.oscillator);
8166            }
8167            if output_id.eq_ignore_ascii_case("signal") {
8168                return Ok(out.signal);
8169            }
8170            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
8171            {
8172                return Ok(out.histogram);
8173            }
8174            Err(IndicatorDispatchError::UnknownOutput {
8175                indicator: "absolute_strength_index_oscillator".to_string(),
8176                output: output_id.to_string(),
8177            })
8178        },
8179    )
8180}
8181
8182fn compute_adaptive_bandpass_trigger_oscillator_batch(
8183    req: IndicatorBatchRequest<'_>,
8184    output_id: &str,
8185) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8186    let data = extract_slice_input("adaptive_bandpass_trigger_oscillator", req.data, "close")?;
8187    let kernel = req.kernel.to_non_batch();
8188    collect_f64(
8189        "adaptive_bandpass_trigger_oscillator",
8190        output_id,
8191        req.combos,
8192        data.len(),
8193        |params| {
8194            let delta =
8195                get_f64_param("adaptive_bandpass_trigger_oscillator", params, "delta", 0.1)?;
8196            let alpha = get_f64_param(
8197                "adaptive_bandpass_trigger_oscillator",
8198                params,
8199                "alpha",
8200                0.07,
8201            )?;
8202            let input = AdaptiveBandpassTriggerOscillatorInput::from_slice(
8203                data,
8204                AdaptiveBandpassTriggerOscillatorParams {
8205                    delta: Some(delta),
8206                    alpha: Some(alpha),
8207                },
8208            );
8209            let out =
8210                adaptive_bandpass_trigger_oscillator_with_kernel(&input, kernel).map_err(|e| {
8211                    IndicatorDispatchError::ComputeFailed {
8212                        indicator: "adaptive_bandpass_trigger_oscillator".to_string(),
8213                        details: e.to_string(),
8214                    }
8215                })?;
8216            match output_id {
8217                "in_phase" => Ok(out.in_phase),
8218                "lead" => Ok(out.lead),
8219                _ => Err(IndicatorDispatchError::UnknownOutput {
8220                    indicator: "adaptive_bandpass_trigger_oscillator".to_string(),
8221                    output: output_id.to_string(),
8222                }),
8223            }
8224        },
8225    )
8226}
8227
8228fn compute_premier_rsi_oscillator_batch(
8229    req: IndicatorBatchRequest<'_>,
8230    output_id: &str,
8231) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8232    expect_value_output("premier_rsi_oscillator", output_id)?;
8233    let data = extract_slice_input("premier_rsi_oscillator", req.data, "close")?;
8234    let kernel = req.kernel.to_non_batch();
8235    collect_f64(
8236        "premier_rsi_oscillator",
8237        output_id,
8238        req.combos,
8239        data.len(),
8240        |params| {
8241            let rsi_length = get_usize_param("premier_rsi_oscillator", params, "rsi_length", 14)?;
8242            let stoch_length =
8243                get_usize_param("premier_rsi_oscillator", params, "stoch_length", 8)?;
8244            let smooth_length =
8245                get_usize_param("premier_rsi_oscillator", params, "smooth_length", 25)?;
8246            let input = PremierRsiOscillatorInput::from_slice(
8247                data,
8248                PremierRsiOscillatorParams {
8249                    rsi_length: Some(rsi_length),
8250                    stoch_length: Some(stoch_length),
8251                    smooth_length: Some(smooth_length),
8252                },
8253            );
8254            let out = premier_rsi_oscillator_with_kernel(&input, kernel).map_err(|e| {
8255                IndicatorDispatchError::ComputeFailed {
8256                    indicator: "premier_rsi_oscillator".to_string(),
8257                    details: e.to_string(),
8258                }
8259            })?;
8260            Ok(out.values)
8261        },
8262    )
8263}
8264
8265fn compute_multi_length_stochastic_average_batch(
8266    req: IndicatorBatchRequest<'_>,
8267    output_id: &str,
8268) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8269    expect_value_output("multi_length_stochastic_average", output_id)?;
8270    let data_len = match req.data {
8271        IndicatorDataRef::Slice { values } => values.len(),
8272        IndicatorDataRef::Candles { candles, source } => {
8273            source_type(candles, source.unwrap_or("close")).len()
8274        }
8275        _ => {
8276            return Err(IndicatorDispatchError::MissingRequiredInput {
8277                indicator: "multi_length_stochastic_average".to_string(),
8278                input: IndicatorInputKind::Candles,
8279            });
8280        }
8281    };
8282    let kernel = req.kernel.to_non_batch();
8283    collect_f64(
8284        "multi_length_stochastic_average",
8285        output_id,
8286        req.combos,
8287        data_len,
8288        |params| {
8289            let source =
8290                get_enum_param("multi_length_stochastic_average", params, "source", "close")?;
8291            let length = get_usize_param("multi_length_stochastic_average", params, "length", 14)?;
8292            let presmooth =
8293                get_usize_param("multi_length_stochastic_average", params, "presmooth", 10)?;
8294            let premethod = get_enum_param(
8295                "multi_length_stochastic_average",
8296                params,
8297                "premethod",
8298                "sma",
8299            )?;
8300            let postsmooth =
8301                get_usize_param("multi_length_stochastic_average", params, "postsmooth", 10)?;
8302            let postmethod = get_enum_param(
8303                "multi_length_stochastic_average",
8304                params,
8305                "postmethod",
8306                "sma",
8307            )?;
8308            let data = match req.data {
8309                IndicatorDataRef::Slice { values } => values,
8310                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8311                _ => unreachable!(),
8312            };
8313            let input = MultiLengthStochasticAverageInput::from_slice(
8314                data,
8315                MultiLengthStochasticAverageParams {
8316                    length: Some(length),
8317                    presmooth: Some(presmooth),
8318                    premethod: Some(premethod),
8319                    postsmooth: Some(postsmooth),
8320                    postmethod: Some(postmethod),
8321                },
8322            );
8323            let out = multi_length_stochastic_average_with_kernel(&input, kernel).map_err(|e| {
8324                IndicatorDispatchError::ComputeFailed {
8325                    indicator: "multi_length_stochastic_average".to_string(),
8326                    details: e.to_string(),
8327                }
8328            })?;
8329            Ok(out.values)
8330        },
8331    )
8332}
8333
8334fn compute_hull_butterfly_oscillator_batch(
8335    req: IndicatorBatchRequest<'_>,
8336    output_id: &str,
8337) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8338    let data_len = match req.data {
8339        IndicatorDataRef::Slice { values } => values.len(),
8340        IndicatorDataRef::Candles { candles, source } => {
8341            source_type(candles, source.unwrap_or("close")).len()
8342        }
8343        _ => {
8344            return Err(IndicatorDispatchError::MissingRequiredInput {
8345                indicator: "hull_butterfly_oscillator".to_string(),
8346                input: IndicatorInputKind::Candles,
8347            });
8348        }
8349    };
8350    let kernel = req.kernel.to_non_batch();
8351    collect_f64(
8352        "hull_butterfly_oscillator",
8353        output_id,
8354        req.combos,
8355        data_len,
8356        |params| {
8357            let source = get_enum_param("hull_butterfly_oscillator", params, "source", "close")?;
8358            let length = get_usize_param("hull_butterfly_oscillator", params, "length", 14)?;
8359            let mult = get_f64_param("hull_butterfly_oscillator", params, "mult", 2.0)?;
8360            let data = match req.data {
8361                IndicatorDataRef::Slice { values } => values,
8362                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8363                _ => unreachable!(),
8364            };
8365            let input = HullButterflyOscillatorInput::from_slice(
8366                data,
8367                HullButterflyOscillatorParams {
8368                    length: Some(length),
8369                    mult: Some(mult),
8370                },
8371            );
8372            let out = hull_butterfly_oscillator_with_kernel(&input, kernel).map_err(|e| {
8373                IndicatorDispatchError::ComputeFailed {
8374                    indicator: "hull_butterfly_oscillator".to_string(),
8375                    details: e.to_string(),
8376                }
8377            })?;
8378            match output_id {
8379                "oscillator" => Ok(out.oscillator),
8380                "cumulative_mean" => Ok(out.cumulative_mean),
8381                "signal" => Ok(out.signal),
8382                _ => Err(IndicatorDispatchError::UnknownOutput {
8383                    indicator: "hull_butterfly_oscillator".to_string(),
8384                    output: output_id.to_string(),
8385                }),
8386            }
8387        },
8388    )
8389}
8390
8391fn compute_fibonacci_trailing_stop_batch(
8392    req: IndicatorBatchRequest<'_>,
8393    output_id: &str,
8394) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8395    let (high, low, close) = extract_ohlc_input("fibonacci_trailing_stop", req.data)?;
8396    let kernel = req.kernel.to_non_batch();
8397    collect_f64(
8398        "fibonacci_trailing_stop",
8399        output_id,
8400        req.combos,
8401        close.len(),
8402        |params| {
8403            let left_bars = get_usize_param("fibonacci_trailing_stop", params, "left_bars", 20)?;
8404            let right_bars = get_usize_param("fibonacci_trailing_stop", params, "right_bars", 1)?;
8405            let level = get_f64_param("fibonacci_trailing_stop", params, "level", -0.382)?;
8406            let trigger = get_enum_param("fibonacci_trailing_stop", params, "trigger", "close")?;
8407            let input = FibonacciTrailingStopInput::from_slices(
8408                high,
8409                low,
8410                close,
8411                FibonacciTrailingStopParams {
8412                    left_bars: Some(left_bars),
8413                    right_bars: Some(right_bars),
8414                    level: Some(level),
8415                    trigger: Some(trigger),
8416                },
8417            );
8418            let out = fibonacci_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
8419                IndicatorDispatchError::ComputeFailed {
8420                    indicator: "fibonacci_trailing_stop".to_string(),
8421                    details: e.to_string(),
8422                }
8423            })?;
8424            if output_id.eq_ignore_ascii_case("trailing_stop")
8425                || output_id.eq_ignore_ascii_case("value")
8426            {
8427                return Ok(out.trailing_stop);
8428            }
8429            if output_id.eq_ignore_ascii_case("long_stop") {
8430                return Ok(out.long_stop);
8431            }
8432            if output_id.eq_ignore_ascii_case("short_stop") {
8433                return Ok(out.short_stop);
8434            }
8435            if output_id.eq_ignore_ascii_case("direction") {
8436                return Ok(out.direction);
8437            }
8438            Err(IndicatorDispatchError::UnknownOutput {
8439                indicator: "fibonacci_trailing_stop".to_string(),
8440                output: output_id.to_string(),
8441            })
8442        },
8443    )
8444}
8445
8446fn compute_fibonacci_entry_bands_batch(
8447    req: IndicatorBatchRequest<'_>,
8448    output_id: &str,
8449) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8450    let (open, high, low, close) = extract_ohlc_full_input("fibonacci_entry_bands", req.data)?;
8451    let kernel = req.kernel.to_non_batch();
8452    collect_f64(
8453        "fibonacci_entry_bands",
8454        output_id,
8455        req.combos,
8456        close.len(),
8457        |params| {
8458            let source = get_enum_param("fibonacci_entry_bands", params, "source", "hlc3")?;
8459            let length = get_usize_param("fibonacci_entry_bands", params, "length", 21)?;
8460            let atr_length = get_usize_param("fibonacci_entry_bands", params, "atr_length", 14)?;
8461            let use_atr = get_bool_param("fibonacci_entry_bands", params, "use_atr", true)?;
8462            let tp_aggressiveness =
8463                get_enum_param("fibonacci_entry_bands", params, "tp_aggressiveness", "low")?;
8464            let input = FibonacciEntryBandsInput::from_slices(
8465                open,
8466                high,
8467                low,
8468                close,
8469                FibonacciEntryBandsParams {
8470                    source: Some(source),
8471                    length: Some(length),
8472                    atr_length: Some(atr_length),
8473                    use_atr: Some(use_atr),
8474                    tp_aggressiveness: Some(tp_aggressiveness),
8475                },
8476            );
8477            let out = fibonacci_entry_bands_with_kernel(&input, kernel).map_err(|e| {
8478                IndicatorDispatchError::ComputeFailed {
8479                    indicator: "fibonacci_entry_bands".to_string(),
8480                    details: e.to_string(),
8481                }
8482            })?;
8483            if output_id.eq_ignore_ascii_case("middle") || output_id.eq_ignore_ascii_case("basis") {
8484                return Ok(out.basis);
8485            }
8486            if output_id.eq_ignore_ascii_case("trend") {
8487                return Ok(out.trend);
8488            }
8489            if output_id.eq_ignore_ascii_case("upper_0618") {
8490                return Ok(out.upper_0618);
8491            }
8492            if output_id.eq_ignore_ascii_case("upper_1000") {
8493                return Ok(out.upper_1000);
8494            }
8495            if output_id.eq_ignore_ascii_case("upper_1618") {
8496                return Ok(out.upper_1618);
8497            }
8498            if output_id.eq_ignore_ascii_case("upper_2618") {
8499                return Ok(out.upper_2618);
8500            }
8501            if output_id.eq_ignore_ascii_case("lower_0618") {
8502                return Ok(out.lower_0618);
8503            }
8504            if output_id.eq_ignore_ascii_case("lower_1000") {
8505                return Ok(out.lower_1000);
8506            }
8507            if output_id.eq_ignore_ascii_case("lower_1618") {
8508                return Ok(out.lower_1618);
8509            }
8510            if output_id.eq_ignore_ascii_case("lower_2618") {
8511                return Ok(out.lower_2618);
8512            }
8513            if output_id.eq_ignore_ascii_case("tp_long_band") {
8514                return Ok(out.tp_long_band);
8515            }
8516            if output_id.eq_ignore_ascii_case("tp_short_band") {
8517                return Ok(out.tp_short_band);
8518            }
8519            if output_id.eq_ignore_ascii_case("go_long")
8520                || output_id.eq_ignore_ascii_case("long_entry")
8521            {
8522                return Ok(out.long_entry);
8523            }
8524            if output_id.eq_ignore_ascii_case("go_short")
8525                || output_id.eq_ignore_ascii_case("short_entry")
8526            {
8527                return Ok(out.short_entry);
8528            }
8529            if output_id.eq_ignore_ascii_case("rejection_long") {
8530                return Ok(out.rejection_long);
8531            }
8532            if output_id.eq_ignore_ascii_case("rejection_short") {
8533                return Ok(out.rejection_short);
8534            }
8535            if output_id.eq_ignore_ascii_case("long_bounce") {
8536                return Ok(out.long_bounce);
8537            }
8538            if output_id.eq_ignore_ascii_case("short_bounce") {
8539                return Ok(out.short_bounce);
8540            }
8541            Err(IndicatorDispatchError::UnknownOutput {
8542                indicator: "fibonacci_entry_bands".to_string(),
8543                output: output_id.to_string(),
8544            })
8545        },
8546    )
8547}
8548
8549fn compute_volume_energy_reservoirs_batch(
8550    req: IndicatorBatchRequest<'_>,
8551    output_id: &str,
8552) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8553    let (_, high, low, close, volume) =
8554        extract_ohlcv_full_input("volume_energy_reservoirs", req.data)?;
8555    let kernel = req.kernel.to_non_batch();
8556    collect_f64(
8557        "volume_energy_reservoirs",
8558        output_id,
8559        req.combos,
8560        close.len(),
8561        |params| {
8562            let length = get_usize_param("volume_energy_reservoirs", params, "length", 20)?;
8563            let sensitivity =
8564                get_f64_param("volume_energy_reservoirs", params, "sensitivity", 1.5)?;
8565            let input = VolumeEnergyReservoirsInput::from_slices(
8566                high,
8567                low,
8568                close,
8569                volume,
8570                VolumeEnergyReservoirsParams {
8571                    length: Some(length),
8572                    sensitivity: Some(sensitivity),
8573                },
8574            );
8575            let out = volume_energy_reservoirs_with_kernel(&input, kernel).map_err(|e| {
8576                IndicatorDispatchError::ComputeFailed {
8577                    indicator: "volume_energy_reservoirs".to_string(),
8578                    details: e.to_string(),
8579                }
8580            })?;
8581            match output_id {
8582                "momentum" | "value" => Ok(out.momentum),
8583                "reservoir" => Ok(out.reservoir),
8584                "squeeze_active" => Ok(out.squeeze_active),
8585                "squeeze_start" => Ok(out.squeeze_start),
8586                "range_high" => Ok(out.range_high),
8587                "range_low" => Ok(out.range_low),
8588                _ => Err(IndicatorDispatchError::UnknownOutput {
8589                    indicator: "volume_energy_reservoirs".to_string(),
8590                    output: output_id.to_string(),
8591                }),
8592            }
8593        },
8594    )
8595}
8596
8597fn compute_neighboring_trailing_stop_batch(
8598    req: IndicatorBatchRequest<'_>,
8599    output_id: &str,
8600) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8601    let (high, low, close) = extract_ohlc_input("neighboring_trailing_stop", req.data)?;
8602    let kernel = req.kernel.to_non_batch();
8603    collect_f64(
8604        "neighboring_trailing_stop",
8605        output_id,
8606        req.combos,
8607        close.len(),
8608        |params| {
8609            let buffer_size =
8610                get_usize_param("neighboring_trailing_stop", params, "buffer_size", 200)?;
8611            let k = get_usize_param("neighboring_trailing_stop", params, "k", 50)?;
8612            let percentile =
8613                get_f64_param("neighboring_trailing_stop", params, "percentile", 90.0)?;
8614            let smooth = get_usize_param("neighboring_trailing_stop", params, "smooth", 5)?;
8615            let input = NeighboringTrailingStopInput::from_slices(
8616                high,
8617                low,
8618                close,
8619                NeighboringTrailingStopParams {
8620                    buffer_size: Some(buffer_size),
8621                    k: Some(k),
8622                    percentile: Some(percentile),
8623                    smooth: Some(smooth),
8624                },
8625            );
8626            let out = neighboring_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
8627                IndicatorDispatchError::ComputeFailed {
8628                    indicator: "neighboring_trailing_stop".to_string(),
8629                    details: e.to_string(),
8630                }
8631            })?;
8632            match output_id {
8633                "trailing_stop" | "value" => Ok(out.trailing_stop),
8634                "bullish_band" => Ok(out.bullish_band),
8635                "bearish_band" => Ok(out.bearish_band),
8636                "direction" => Ok(out.direction),
8637                "discovery_bull" => Ok(out.discovery_bull),
8638                "discovery_bear" => Ok(out.discovery_bear),
8639                _ => Err(IndicatorDispatchError::UnknownOutput {
8640                    indicator: "neighboring_trailing_stop".to_string(),
8641                    output: output_id.to_string(),
8642                }),
8643            }
8644        },
8645    )
8646}
8647
8648fn compute_grover_llorens_cycle_oscillator_batch(
8649    req: IndicatorBatchRequest<'_>,
8650    output_id: &str,
8651) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8652    expect_value_output("grover_llorens_cycle_oscillator", output_id)?;
8653    let (open, high, low, close) =
8654        extract_ohlc_full_input("grover_llorens_cycle_oscillator", req.data)?;
8655    let kernel = req.kernel.to_non_batch();
8656    collect_f64(
8657        "grover_llorens_cycle_oscillator",
8658        output_id,
8659        req.combos,
8660        close.len(),
8661        |params| {
8662            let length = get_usize_param("grover_llorens_cycle_oscillator", params, "length", 100)?;
8663            let mult = get_f64_param("grover_llorens_cycle_oscillator", params, "mult", 10.0)?;
8664            let source = match find_param(params, "source") {
8665                Some(ParamValue::EnumString(v)) => (*v).to_string(),
8666                Some(_) => {
8667                    return Err(IndicatorDispatchError::InvalidParam {
8668                        indicator: "grover_llorens_cycle_oscillator".to_string(),
8669                        key: "source".to_string(),
8670                        reason: "expected string".to_string(),
8671                    });
8672                }
8673                None => "close".to_string(),
8674            };
8675            let smooth = get_bool_param("grover_llorens_cycle_oscillator", params, "smooth", true)?;
8676            let rsi_period =
8677                get_usize_param("grover_llorens_cycle_oscillator", params, "rsi_period", 20)?;
8678            let input = GroverLlorensCycleOscillatorInput::from_slices(
8679                open,
8680                high,
8681                low,
8682                close,
8683                GroverLlorensCycleOscillatorParams {
8684                    length: Some(length),
8685                    mult: Some(mult),
8686                    source: Some(source),
8687                    smooth: Some(smooth),
8688                    rsi_period: Some(rsi_period),
8689                },
8690            );
8691            let out = grover_llorens_cycle_oscillator_with_kernel(&input, kernel).map_err(|e| {
8692                IndicatorDispatchError::ComputeFailed {
8693                    indicator: "grover_llorens_cycle_oscillator".to_string(),
8694                    details: e.to_string(),
8695                }
8696            })?;
8697            Ok(out.values)
8698        },
8699    )
8700}
8701
8702fn compute_ehlers_autocorrelation_periodogram_batch(
8703    req: IndicatorBatchRequest<'_>,
8704    output_id: &str,
8705) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8706    let data = extract_slice_input("ehlers_autocorrelation_periodogram", req.data, "close")?;
8707    let kernel = req.kernel.to_non_batch();
8708    collect_f64(
8709        "ehlers_autocorrelation_periodogram",
8710        output_id,
8711        req.combos,
8712        data.len(),
8713        |params| {
8714            let min_period = get_usize_param(
8715                "ehlers_autocorrelation_periodogram",
8716                params,
8717                "min_period",
8718                8,
8719            )?;
8720            let max_period = get_usize_param(
8721                "ehlers_autocorrelation_periodogram",
8722                params,
8723                "max_period",
8724                48,
8725            )?;
8726            let avg_length = get_usize_param(
8727                "ehlers_autocorrelation_periodogram",
8728                params,
8729                "avg_length",
8730                3,
8731            )?;
8732            let enhance = get_bool_param(
8733                "ehlers_autocorrelation_periodogram",
8734                params,
8735                "enhance",
8736                true,
8737            )?;
8738            let input = EhlersAutocorrelationPeriodogramInput::from_slice(
8739                data,
8740                EhlersAutocorrelationPeriodogramParams {
8741                    min_period: Some(min_period),
8742                    max_period: Some(max_period),
8743                    avg_length: Some(avg_length),
8744                    enhance: Some(enhance),
8745                },
8746            );
8747            let out =
8748                ehlers_autocorrelation_periodogram_with_kernel(&input, kernel).map_err(|e| {
8749                    IndicatorDispatchError::ComputeFailed {
8750                        indicator: "ehlers_autocorrelation_periodogram".to_string(),
8751                        details: e.to_string(),
8752                    }
8753                })?;
8754            if output_id.eq_ignore_ascii_case("dominant_cycle")
8755                || output_id.eq_ignore_ascii_case("value")
8756            {
8757                return Ok(out.dominant_cycle);
8758            }
8759            if output_id.eq_ignore_ascii_case("normalized_power") {
8760                return Ok(out.normalized_power);
8761            }
8762            Err(IndicatorDispatchError::UnknownOutput {
8763                indicator: "ehlers_autocorrelation_periodogram".to_string(),
8764                output: output_id.to_string(),
8765            })
8766        },
8767    )
8768}
8769
8770fn compute_ehlers_linear_extrapolation_predictor_batch(
8771    req: IndicatorBatchRequest<'_>,
8772    output_id: &str,
8773) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8774    let data = extract_slice_input("ehlers_linear_extrapolation_predictor", req.data, "close")?;
8775    let kernel = req.kernel.to_non_batch();
8776    collect_f64(
8777        "ehlers_linear_extrapolation_predictor",
8778        output_id,
8779        req.combos,
8780        data.len(),
8781        |params| {
8782            let high_pass_length = get_usize_param(
8783                "ehlers_linear_extrapolation_predictor",
8784                params,
8785                "high_pass_length",
8786                125,
8787            )?;
8788            let low_pass_length = get_usize_param(
8789                "ehlers_linear_extrapolation_predictor",
8790                params,
8791                "low_pass_length",
8792                12,
8793            )?;
8794            let gain = get_f64_param("ehlers_linear_extrapolation_predictor", params, "gain", 0.7)?;
8795            let bars_forward = get_usize_param(
8796                "ehlers_linear_extrapolation_predictor",
8797                params,
8798                "bars_forward",
8799                5,
8800            )?;
8801            let signal_mode = get_enum_param(
8802                "ehlers_linear_extrapolation_predictor",
8803                params,
8804                "signal_mode",
8805                "predict_filter_crosses",
8806            )?;
8807            let input = EhlersLinearExtrapolationPredictorInput::from_slice(
8808                data,
8809                EhlersLinearExtrapolationPredictorParams {
8810                    high_pass_length: Some(high_pass_length),
8811                    low_pass_length: Some(low_pass_length),
8812                    gain: Some(gain),
8813                    bars_forward: Some(bars_forward),
8814                    signal_mode: Some(signal_mode),
8815                },
8816            );
8817            let out =
8818                ehlers_linear_extrapolation_predictor_with_kernel(&input, kernel).map_err(|e| {
8819                    IndicatorDispatchError::ComputeFailed {
8820                        indicator: "ehlers_linear_extrapolation_predictor".to_string(),
8821                        details: e.to_string(),
8822                    }
8823                })?;
8824            if output_id.eq_ignore_ascii_case("prediction")
8825                || output_id.eq_ignore_ascii_case("value")
8826            {
8827                return Ok(out.prediction);
8828            }
8829            if output_id.eq_ignore_ascii_case("filter") {
8830                return Ok(out.filter);
8831            }
8832            if output_id.eq_ignore_ascii_case("state") {
8833                return Ok(out.state);
8834            }
8835            if output_id.eq_ignore_ascii_case("go_long") {
8836                return Ok(out.go_long);
8837            }
8838            if output_id.eq_ignore_ascii_case("go_short") {
8839                return Ok(out.go_short);
8840            }
8841            Err(IndicatorDispatchError::UnknownOutput {
8842                indicator: "ehlers_linear_extrapolation_predictor".to_string(),
8843                output: output_id.to_string(),
8844            })
8845        },
8846    )
8847}
8848
8849fn compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(
8850    req: IndicatorBatchRequest<'_>,
8851    output_id: &str,
8852) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8853    expect_value_output(
8854        "decisionpoint_breadth_swenlin_trading_oscillator",
8855        output_id,
8856    )?;
8857    let (advancing, declining) =
8858        extract_high_low_input("decisionpoint_breadth_swenlin_trading_oscillator", req.data)?;
8859    let kernel = req.kernel.to_non_batch();
8860    collect_f64(
8861        "decisionpoint_breadth_swenlin_trading_oscillator",
8862        output_id,
8863        req.combos,
8864        advancing.len(),
8865        |_params| {
8866            let input = DecisionPointBreadthSwenlinTradingOscillatorInput::from_slices(
8867                advancing,
8868                declining,
8869                DecisionPointBreadthSwenlinTradingOscillatorParams,
8870            );
8871            let out = decisionpoint_breadth_swenlin_trading_oscillator_with_kernel(&input, kernel)
8872                .map_err(|e| IndicatorDispatchError::ComputeFailed {
8873                    indicator: "decisionpoint_breadth_swenlin_trading_oscillator".to_string(),
8874                    details: e.to_string(),
8875                })?;
8876            Ok(out.values)
8877        },
8878    )
8879}
8880
8881fn compute_velocity_acceleration_indicator_batch(
8882    req: IndicatorBatchRequest<'_>,
8883    output_id: &str,
8884) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8885    expect_value_output("velocity_acceleration_indicator", output_id)?;
8886    let data_len = match req.data {
8887        IndicatorDataRef::Slice { values } => values.len(),
8888        IndicatorDataRef::Candles { candles, source } => {
8889            source_type(candles, source.unwrap_or("hlcc4")).len()
8890        }
8891        _ => {
8892            return Err(IndicatorDispatchError::MissingRequiredInput {
8893                indicator: "velocity_acceleration_indicator".to_string(),
8894                input: IndicatorInputKind::Candles,
8895            });
8896        }
8897    };
8898    let kernel = req.kernel.to_non_batch();
8899    collect_f64(
8900        "velocity_acceleration_indicator",
8901        output_id,
8902        req.combos,
8903        data_len,
8904        |params| {
8905            let source =
8906                get_enum_param("velocity_acceleration_indicator", params, "source", "hlcc4")?;
8907            let length = get_usize_param("velocity_acceleration_indicator", params, "length", 21)?;
8908            let smooth_length = get_usize_param(
8909                "velocity_acceleration_indicator",
8910                params,
8911                "smooth_length",
8912                5,
8913            )?;
8914            let data = match req.data {
8915                IndicatorDataRef::Slice { values } => values,
8916                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8917                _ => unreachable!(),
8918            };
8919            let input = VelocityAccelerationIndicatorInput::from_slice(
8920                data,
8921                VelocityAccelerationIndicatorParams {
8922                    length: Some(length),
8923                    smooth_length: Some(smooth_length),
8924                },
8925            );
8926            let out = velocity_acceleration_indicator_with_kernel(&input, kernel).map_err(|e| {
8927                IndicatorDispatchError::ComputeFailed {
8928                    indicator: "velocity_acceleration_indicator".to_string(),
8929                    details: e.to_string(),
8930                }
8931            })?;
8932            Ok(out.values)
8933        },
8934    )
8935}
8936
8937fn compute_normalized_resonator_batch(
8938    req: IndicatorBatchRequest<'_>,
8939    output_id: &str,
8940) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8941    let data_len = match req.data {
8942        IndicatorDataRef::Slice { values } => values.len(),
8943        IndicatorDataRef::Candles { candles, source } => {
8944            source_type(candles, source.unwrap_or("hl2")).len()
8945        }
8946        _ => {
8947            return Err(IndicatorDispatchError::MissingRequiredInput {
8948                indicator: "normalized_resonator".to_string(),
8949                input: IndicatorInputKind::Candles,
8950            });
8951        }
8952    };
8953    let kernel = req.kernel.to_non_batch();
8954    collect_f64(
8955        "normalized_resonator",
8956        output_id,
8957        req.combos,
8958        data_len,
8959        |params| {
8960            let source = get_enum_param("normalized_resonator", params, "source", "hl2")?;
8961            let period = get_usize_param("normalized_resonator", params, "period", 100)?;
8962            let delta = get_f64_param("normalized_resonator", params, "delta", 0.5)?;
8963            let lookback_mult =
8964                get_f64_param("normalized_resonator", params, "lookback_mult", 1.0)?;
8965            let signal_length =
8966                get_usize_param("normalized_resonator", params, "signal_length", 9)?;
8967            let data = match req.data {
8968                IndicatorDataRef::Slice { values } => values,
8969                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8970                _ => unreachable!(),
8971            };
8972            let input = NormalizedResonatorInput::from_slice(
8973                data,
8974                NormalizedResonatorParams {
8975                    period: Some(period),
8976                    delta: Some(delta),
8977                    lookback_mult: Some(lookback_mult),
8978                    signal_length: Some(signal_length),
8979                },
8980            );
8981            let out = normalized_resonator_with_kernel(&input, kernel).map_err(|e| {
8982                IndicatorDispatchError::ComputeFailed {
8983                    indicator: "normalized_resonator".to_string(),
8984                    details: e.to_string(),
8985                }
8986            })?;
8987            match output_id {
8988                "oscillator" => Ok(out.oscillator),
8989                "signal" => Ok(out.signal),
8990                _ => Err(IndicatorDispatchError::UnknownOutput {
8991                    indicator: "normalized_resonator".to_string(),
8992                    output: output_id.to_string(),
8993                }),
8994            }
8995        },
8996    )
8997}
8998
8999fn compute_monotonicity_index_batch(
9000    req: IndicatorBatchRequest<'_>,
9001    output_id: &str,
9002) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9003    let data_len = match req.data {
9004        IndicatorDataRef::Slice { values } => values.len(),
9005        IndicatorDataRef::Candles { candles, source } => {
9006            source_type(candles, source.unwrap_or("close")).len()
9007        }
9008        _ => {
9009            return Err(IndicatorDispatchError::MissingRequiredInput {
9010                indicator: "monotonicity_index".to_string(),
9011                input: IndicatorInputKind::Candles,
9012            });
9013        }
9014    };
9015    let kernel = req.kernel.to_non_batch();
9016    collect_f64(
9017        "monotonicity_index",
9018        output_id,
9019        req.combos,
9020        data_len,
9021        |params| {
9022            let source = get_enum_param("monotonicity_index", params, "source", "close")?;
9023            let length = get_usize_param("monotonicity_index", params, "length", 20)?;
9024            let mode = get_enum_param("monotonicity_index", params, "mode", "efficiency")?;
9025            let index_smooth = get_usize_param("monotonicity_index", params, "index_smooth", 5)?;
9026            let mode = MonotonicityIndexMode::parse(&mode).ok_or_else(|| {
9027                IndicatorDispatchError::InvalidParam {
9028                    indicator: "monotonicity_index".to_string(),
9029                    key: "mode".to_string(),
9030                    reason: format!("invalid mode: {mode}"),
9031                }
9032            })?;
9033            let data = match req.data {
9034                IndicatorDataRef::Slice { values } => values,
9035                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9036                _ => unreachable!(),
9037            };
9038            let input = MonotonicityIndexInput::from_slice(
9039                data,
9040                MonotonicityIndexParams {
9041                    length: Some(length),
9042                    mode: Some(mode),
9043                    index_smooth: Some(index_smooth),
9044                },
9045            );
9046            let out = monotonicity_index_with_kernel(&input, kernel).map_err(|e| {
9047                IndicatorDispatchError::ComputeFailed {
9048                    indicator: "monotonicity_index".to_string(),
9049                    details: e.to_string(),
9050                }
9051            })?;
9052            match output_id {
9053                "index" => Ok(out.index),
9054                "cumulative_mean" => Ok(out.cumulative_mean),
9055                "upper_bound" => Ok(out.upper_bound),
9056                _ => Err(IndicatorDispatchError::UnknownOutput {
9057                    indicator: "monotonicity_index".to_string(),
9058                    output: output_id.to_string(),
9059                }),
9060            }
9061        },
9062    )
9063}
9064
9065fn compute_half_causal_estimator_batch(
9066    req: IndicatorBatchRequest<'_>,
9067    output_id: &str,
9068) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9069    if output_id != "estimate" && output_id != "expected_value" {
9070        return Err(IndicatorDispatchError::UnknownOutput {
9071            indicator: "half_causal_estimator".to_string(),
9072            output: output_id.to_string(),
9073        });
9074    }
9075
9076    let data_len = match req.data {
9077        IndicatorDataRef::Slice { values } => values.len(),
9078        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
9079        _ => {
9080            return Err(IndicatorDispatchError::MissingRequiredInput {
9081                indicator: "half_causal_estimator".to_string(),
9082                input: IndicatorInputKind::Candles,
9083            });
9084        }
9085    };
9086    let kernel = req.kernel.to_non_batch();
9087    collect_f64(
9088        "half_causal_estimator",
9089        output_id,
9090        req.combos,
9091        data_len,
9092        |params| {
9093            let slots_per_day = get_usize_param_with_aliases(
9094                "half_causal_estimator",
9095                params,
9096                &["slots_per_day"],
9097                0,
9098            )?;
9099            let data_period = get_usize_param("half_causal_estimator", params, "data_period", 5)?;
9100            let filter_length =
9101                get_usize_param("half_causal_estimator", params, "filter_length", 20)?;
9102            let kernel_width =
9103                get_f64_param("half_causal_estimator", params, "kernel_width", 20.0)?;
9104            let maximum_confidence_adjust = get_f64_param(
9105                "half_causal_estimator",
9106                params,
9107                "maximum_confidence_adjust",
9108                100.0,
9109            )?;
9110            let extra_smoothing =
9111                get_usize_param("half_causal_estimator", params, "extra_smoothing", 0)?;
9112            let enable_expected_value = get_bool_param(
9113                "half_causal_estimator",
9114                params,
9115                "enable_expected_value",
9116                false,
9117            )?;
9118            let source = get_enum_param("half_causal_estimator", params, "source", "volume")?;
9119            let kernel_type = match get_enum_param(
9120                "half_causal_estimator",
9121                params,
9122                "kernel_type",
9123                "epanechnikov",
9124            )?
9125            .to_ascii_lowercase()
9126            .as_str()
9127            {
9128                "gaussian" => HalfCausalEstimatorKernelType::Gaussian,
9129                "epanechnikov" => HalfCausalEstimatorKernelType::Epanechnikov,
9130                "triangular" => HalfCausalEstimatorKernelType::Triangular,
9131                "sinc" => HalfCausalEstimatorKernelType::Sinc,
9132                other => {
9133                    return Err(IndicatorDispatchError::InvalidParam {
9134                        indicator: "half_causal_estimator".to_string(),
9135                        key: "kernel_type".to_string(),
9136                        reason: format!("unsupported value '{other}'"),
9137                    })
9138                }
9139            };
9140            let confidence_adjust = match get_enum_param(
9141                "half_causal_estimator",
9142                params,
9143                "confidence_adjust",
9144                "symmetric",
9145            )?
9146            .to_ascii_lowercase()
9147            .as_str()
9148            {
9149                "symmetric" => HalfCausalEstimatorConfidenceAdjust::Symmetric,
9150                "linear" => HalfCausalEstimatorConfidenceAdjust::Linear,
9151                "none" => HalfCausalEstimatorConfidenceAdjust::None,
9152                other => {
9153                    return Err(IndicatorDispatchError::InvalidParam {
9154                        indicator: "half_causal_estimator".to_string(),
9155                        key: "confidence_adjust".to_string(),
9156                        reason: format!("unsupported value '{other}'"),
9157                    })
9158                }
9159            };
9160
9161            let indicator_params = HalfCausalEstimatorParams {
9162                slots_per_day: if slots_per_day == 0 {
9163                    None
9164                } else {
9165                    Some(slots_per_day)
9166                },
9167                data_period: Some(data_period),
9168                filter_length: Some(filter_length),
9169                kernel_width: Some(kernel_width),
9170                kernel_type: Some(kernel_type),
9171                confidence_adjust: Some(confidence_adjust),
9172                maximum_confidence_adjust: Some(maximum_confidence_adjust),
9173                enable_expected_value: Some(enable_expected_value),
9174                extra_smoothing: Some(extra_smoothing),
9175            };
9176
9177            let out = match req.data {
9178                IndicatorDataRef::Slice { values } => {
9179                    let input = HalfCausalEstimatorInput::from_slice(values, indicator_params);
9180                    half_causal_estimator_with_kernel(&input, kernel)
9181                }
9182                IndicatorDataRef::Candles { candles, .. } => {
9183                    let input =
9184                        HalfCausalEstimatorInput::from_candles(candles, &source, indicator_params);
9185                    half_causal_estimator_with_kernel(&input, kernel)
9186                }
9187                _ => unreachable!(),
9188            }
9189            .map_err(|e| IndicatorDispatchError::ComputeFailed {
9190                indicator: "half_causal_estimator".to_string(),
9191                details: e.to_string(),
9192            })?;
9193
9194            Ok(match output_id {
9195                "estimate" => out.estimate,
9196                "expected_value" => out.expected_value,
9197                _ => unreachable!(),
9198            })
9199        },
9200    )
9201}
9202
9203fn compute_historical_volatility_batch(
9204    req: IndicatorBatchRequest<'_>,
9205    output_id: &str,
9206) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9207    expect_value_output("historical_volatility", output_id)?;
9208    let data = extract_slice_input("historical_volatility", req.data, "close")?;
9209    let kernel = req.kernel.to_non_batch();
9210    collect_f64(
9211        "historical_volatility",
9212        output_id,
9213        req.combos,
9214        data.len(),
9215        |params| {
9216            let lookback = get_usize_param("historical_volatility", params, "lookback", 20)?;
9217            let annualization_days =
9218                get_f64_param("historical_volatility", params, "annualization_days", 250.0)?;
9219            let input = HistoricalVolatilityInput::from_slice(
9220                data,
9221                HistoricalVolatilityParams {
9222                    lookback: Some(lookback),
9223                    annualization_days: Some(annualization_days),
9224                },
9225            );
9226            let out = historical_volatility_with_kernel(&input, kernel).map_err(|e| {
9227                IndicatorDispatchError::ComputeFailed {
9228                    indicator: "historical_volatility".to_string(),
9229                    details: e.to_string(),
9230                }
9231            })?;
9232            Ok(out.values)
9233        },
9234    )
9235}
9236
9237fn compute_historical_volatility_percentile_batch(
9238    req: IndicatorBatchRequest<'_>,
9239    output_id: &str,
9240) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9241    let data = extract_slice_input("historical_volatility_percentile", req.data, "close")?;
9242    let kernel = req.kernel.to_non_batch();
9243    collect_f64(
9244        "historical_volatility_percentile",
9245        output_id,
9246        req.combos,
9247        data.len(),
9248        |params| {
9249            let length =
9250                get_usize_param("historical_volatility_percentile", params, "length", 20)?;
9251            let annual_length = get_usize_param(
9252                "historical_volatility_percentile",
9253                params,
9254                "annual_length",
9255                252,
9256            )?;
9257            let input = HistoricalVolatilityPercentileInput::from_slice(
9258                data,
9259                HistoricalVolatilityPercentileParams {
9260                    length: Some(length),
9261                    annual_length: Some(annual_length),
9262                },
9263            );
9264            let out = historical_volatility_percentile_with_kernel(&input, kernel).map_err(|e| {
9265                IndicatorDispatchError::ComputeFailed {
9266                    indicator: "historical_volatility_percentile".to_string(),
9267                    details: e.to_string(),
9268                }
9269            })?;
9270            if output_id.eq_ignore_ascii_case("hvp") {
9271                return Ok(out.hvp);
9272            }
9273            if output_id.eq_ignore_ascii_case("hvp_sma") {
9274                return Ok(out.hvp_sma);
9275            }
9276            Err(IndicatorDispatchError::UnknownOutput {
9277                indicator: "historical_volatility_percentile".to_string(),
9278                output: output_id.to_string(),
9279            })
9280        },
9281    )
9282}
9283
9284fn compute_volatility_ratio_adaptive_rsx_batch(
9285    req: IndicatorBatchRequest<'_>,
9286    output_id: &str,
9287) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9288    let data = extract_slice_input("volatility_ratio_adaptive_rsx", req.data, "close")?;
9289    let kernel = req.kernel.to_non_batch();
9290    collect_f64(
9291        "volatility_ratio_adaptive_rsx",
9292        output_id,
9293        req.combos,
9294        data.len(),
9295        |params| {
9296            let period =
9297                get_usize_param("volatility_ratio_adaptive_rsx", params, "period", 14)?;
9298            let speed = get_f64_param("volatility_ratio_adaptive_rsx", params, "speed", 0.5)?;
9299            let input = VolatilityRatioAdaptiveRsxInput::from_slice(
9300                data,
9301                VolatilityRatioAdaptiveRsxParams {
9302                    period: Some(period),
9303                    speed: Some(speed),
9304                },
9305            );
9306            let out = volatility_ratio_adaptive_rsx_with_kernel(&input, kernel).map_err(|e| {
9307                IndicatorDispatchError::ComputeFailed {
9308                    indicator: "volatility_ratio_adaptive_rsx".to_string(),
9309                    details: e.to_string(),
9310                }
9311            })?;
9312            if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
9313                return Ok(out.line);
9314            }
9315            if output_id.eq_ignore_ascii_case("signal") {
9316                return Ok(out.signal);
9317            }
9318            Err(IndicatorDispatchError::UnknownOutput {
9319                indicator: "volatility_ratio_adaptive_rsx".to_string(),
9320                output: output_id.to_string(),
9321            })
9322        },
9323    )
9324}
9325
9326fn compute_on_balance_volume_oscillator_batch(
9327    req: IndicatorBatchRequest<'_>,
9328    output_id: &str,
9329) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9330    let (close, volume) =
9331        extract_close_volume_input("on_balance_volume_oscillator", req.data, "close")?;
9332    let kernel = req.kernel.to_non_batch();
9333    collect_f64(
9334        "on_balance_volume_oscillator",
9335        output_id,
9336        req.combos,
9337        close.len(),
9338        |params| {
9339            let obv_length =
9340                get_usize_param("on_balance_volume_oscillator", params, "obv_length", 20)?;
9341            let ema_length =
9342                get_usize_param("on_balance_volume_oscillator", params, "ema_length", 9)?;
9343            let input = OnBalanceVolumeOscillatorInput::from_slices(
9344                close,
9345                volume,
9346                OnBalanceVolumeOscillatorParams {
9347                    obv_length: Some(obv_length),
9348                    ema_length: Some(ema_length),
9349                },
9350            );
9351            let out = on_balance_volume_oscillator_with_kernel(&input, kernel).map_err(|e| {
9352                IndicatorDispatchError::ComputeFailed {
9353                    indicator: "on_balance_volume_oscillator".to_string(),
9354                    details: e.to_string(),
9355                }
9356            })?;
9357            if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
9358                return Ok(out.line);
9359            }
9360            if output_id.eq_ignore_ascii_case("signal") {
9361                return Ok(out.signal);
9362            }
9363            Err(IndicatorDispatchError::UnknownOutput {
9364                indicator: "on_balance_volume_oscillator".to_string(),
9365                output: output_id.to_string(),
9366            })
9367        },
9368    )
9369}
9370
9371fn compute_twiggs_money_flow_batch(
9372    req: IndicatorBatchRequest<'_>,
9373    output_id: &str,
9374) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9375    let (high, low, close, volume) = extract_hlcv_input("twiggs_money_flow", req.data)?;
9376    let kernel = req.kernel.to_non_batch();
9377    collect_f64("twiggs_money_flow", output_id, req.combos, close.len(), |params| {
9378        let length = get_usize_param("twiggs_money_flow", params, "length", 21)?;
9379        let smoothing_length =
9380            get_usize_param("twiggs_money_flow", params, "smoothing_length", 4)?;
9381        let ma_type = get_enum_param("twiggs_money_flow", params, "ma_type", "ema")?;
9382        let input = TwiggsMoneyFlowInput::from_slices(
9383            high,
9384            low,
9385            close,
9386            volume,
9387            TwiggsMoneyFlowParams {
9388                length: Some(length),
9389                smoothing_length: Some(smoothing_length),
9390                ma_type: Some(ma_type),
9391            },
9392        );
9393        let out = twiggs_money_flow_with_kernel(&input, kernel).map_err(|e| {
9394            IndicatorDispatchError::ComputeFailed {
9395                indicator: "twiggs_money_flow".to_string(),
9396                details: e.to_string(),
9397            }
9398        })?;
9399        if output_id.eq_ignore_ascii_case("tmf") || output_id.eq_ignore_ascii_case("value") {
9400            return Ok(out.tmf);
9401        }
9402        if output_id.eq_ignore_ascii_case("smoothed") {
9403            return Ok(out.smoothed);
9404        }
9405        Err(IndicatorDispatchError::UnknownOutput {
9406            indicator: "twiggs_money_flow".to_string(),
9407            output: output_id.to_string(),
9408        })
9409    })
9410}
9411
9412fn compute_parkinson_volatility_batch(
9413    req: IndicatorBatchRequest<'_>,
9414    output_id: &str,
9415) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9416    let (high, low) = extract_high_low_input("parkinson_volatility", req.data)?;
9417    let kernel = req.kernel.to_non_batch();
9418    collect_f64(
9419        "parkinson_volatility",
9420        output_id,
9421        req.combos,
9422        high.len(),
9423        |params| {
9424            let period = get_usize_param("parkinson_volatility", params, "period", 10)?;
9425            let input = ParkinsonVolatilityInput::from_slices(
9426                high,
9427                low,
9428                ParkinsonVolatilityParams {
9429                    period: Some(period),
9430                },
9431            );
9432            let out = parkinson_volatility_with_kernel(&input, kernel).map_err(|e| {
9433                IndicatorDispatchError::ComputeFailed {
9434                    indicator: "parkinson_volatility".to_string(),
9435                    details: e.to_string(),
9436                }
9437            })?;
9438            if output_id.eq_ignore_ascii_case("volatility") || output_id.eq_ignore_ascii_case("value")
9439            {
9440                return Ok(out.volatility);
9441            }
9442            if output_id.eq_ignore_ascii_case("variance") {
9443                return Ok(out.variance);
9444            }
9445            Err(IndicatorDispatchError::UnknownOutput {
9446                indicator: "parkinson_volatility".to_string(),
9447                output: output_id.to_string(),
9448            })
9449        },
9450    )
9451}
9452
9453fn compute_l2_ehlers_signal_to_noise_batch(
9454    req: IndicatorBatchRequest<'_>,
9455    output_id: &str,
9456) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9457    expect_value_output("l2_ehlers_signal_to_noise", output_id)?;
9458    let data_len = match req.data {
9459        IndicatorDataRef::Slice { values } => values.len(),
9460        IndicatorDataRef::Candles { candles, source } => {
9461            source_type(candles, source.unwrap_or("hl2")).len()
9462        }
9463        _ => {
9464            return Err(IndicatorDispatchError::MissingRequiredInput {
9465                indicator: "l2_ehlers_signal_to_noise".to_string(),
9466                input: IndicatorInputKind::Candles,
9467            })
9468        }
9469    };
9470    let kernel = req.kernel.to_non_batch();
9471    collect_f64(
9472        "l2_ehlers_signal_to_noise",
9473        output_id,
9474        req.combos,
9475        data_len,
9476        |params| {
9477            let source = get_enum_param("l2_ehlers_signal_to_noise", params, "source", "hl2")?;
9478            let smooth_period =
9479                get_usize_param("l2_ehlers_signal_to_noise", params, "smooth_period", 10)?;
9480            let src = match req.data {
9481                IndicatorDataRef::Slice { values } => values,
9482                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9483                _ => unreachable!(),
9484            };
9485            let (high, low) = match req.data {
9486                IndicatorDataRef::Candles { candles, .. } => {
9487                    (candles.high.as_slice(), candles.low.as_slice())
9488                }
9489                IndicatorDataRef::Ohlc { high, low, .. } => (high, low),
9490                IndicatorDataRef::Ohlcv { high, low, .. } => (high, low),
9491                _ => {
9492                    return Err(IndicatorDispatchError::MissingRequiredInput {
9493                        indicator: "l2_ehlers_signal_to_noise".to_string(),
9494                        input: IndicatorInputKind::Candles,
9495                    })
9496                }
9497            };
9498            let input = L2EhlersSignalToNoiseInput::from_slices(
9499                src,
9500                high,
9501                low,
9502                L2EhlersSignalToNoiseParams {
9503                    smooth_period: Some(smooth_period),
9504                },
9505            );
9506            let out = l2_ehlers_signal_to_noise_with_kernel(&input, kernel).map_err(|e| {
9507                IndicatorDispatchError::ComputeFailed {
9508                    indicator: "l2_ehlers_signal_to_noise".to_string(),
9509                    details: e.to_string(),
9510                }
9511            })?;
9512            Ok(out.values)
9513        },
9514    )
9515}
9516
9517fn compute_cycle_channel_oscillator_batch(
9518    req: IndicatorBatchRequest<'_>,
9519    output_id: &str,
9520) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9521    let data_len = match req.data {
9522        IndicatorDataRef::Candles { candles, source } => {
9523            source_type(candles, source.unwrap_or("close")).len()
9524        }
9525        _ => {
9526            return Err(IndicatorDispatchError::MissingRequiredInput {
9527                indicator: "cycle_channel_oscillator".to_string(),
9528                input: IndicatorInputKind::Candles,
9529            })
9530        }
9531    };
9532    let kernel = req.kernel.to_non_batch();
9533    collect_f64(
9534        "cycle_channel_oscillator",
9535        output_id,
9536        req.combos,
9537        data_len,
9538        |params| {
9539            let source = get_enum_param("cycle_channel_oscillator", params, "source", "close")?;
9540            let short_cycle_length =
9541                get_usize_param("cycle_channel_oscillator", params, "short_cycle_length", 10)?;
9542            let medium_cycle_length =
9543                get_usize_param("cycle_channel_oscillator", params, "medium_cycle_length", 30)?;
9544            let short_multiplier =
9545                get_f64_param("cycle_channel_oscillator", params, "short_multiplier", 1.0)?;
9546            let medium_multiplier =
9547                get_f64_param("cycle_channel_oscillator", params, "medium_multiplier", 3.0)?;
9548            let (src, high, low, close) = match req.data {
9549                IndicatorDataRef::Candles { candles, .. } => (
9550                    source_type(candles, &source),
9551                    candles.high.as_slice(),
9552                    candles.low.as_slice(),
9553                    candles.close.as_slice(),
9554                ),
9555                _ => unreachable!(),
9556            };
9557            let input = CycleChannelOscillatorInput::from_slices(
9558                src,
9559                high,
9560                low,
9561                close,
9562                CycleChannelOscillatorParams {
9563                    short_cycle_length: Some(short_cycle_length),
9564                    medium_cycle_length: Some(medium_cycle_length),
9565                    short_multiplier: Some(short_multiplier),
9566                    medium_multiplier: Some(medium_multiplier),
9567                },
9568            );
9569            let out = cycle_channel_oscillator_with_kernel(&input, kernel).map_err(|e| {
9570                IndicatorDispatchError::ComputeFailed {
9571                    indicator: "cycle_channel_oscillator".to_string(),
9572                    details: e.to_string(),
9573                }
9574            })?;
9575            if output_id.eq_ignore_ascii_case("fast") || output_id.eq_ignore_ascii_case("value") {
9576                return Ok(out.fast);
9577            }
9578            if output_id.eq_ignore_ascii_case("slow") {
9579                return Ok(out.slow);
9580            }
9581            Err(IndicatorDispatchError::UnknownOutput {
9582                indicator: "cycle_channel_oscillator".to_string(),
9583                output: output_id.to_string(),
9584            })
9585        },
9586    )
9587}
9588
9589fn compute_andean_oscillator_batch(
9590    req: IndicatorBatchRequest<'_>,
9591    output_id: &str,
9592) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9593    let (open, _high, _low, close) = extract_ohlc_full_input("andean_oscillator", req.data)?;
9594    let kernel = req.kernel.to_non_batch();
9595    collect_f64("andean_oscillator", output_id, req.combos, close.len(), |params| {
9596        let length = get_usize_param("andean_oscillator", params, "length", 50)?;
9597        let signal_length =
9598            get_usize_param("andean_oscillator", params, "signal_length", 9)?;
9599        let input = AndeanOscillatorInput::from_slices(
9600            open,
9601            close,
9602            AndeanOscillatorParams {
9603                length: Some(length),
9604                signal_length: Some(signal_length),
9605            },
9606        );
9607        let out = andean_oscillator_with_kernel(&input, kernel).map_err(|e| {
9608            IndicatorDispatchError::ComputeFailed {
9609                indicator: "andean_oscillator".to_string(),
9610                details: e.to_string(),
9611            }
9612        })?;
9613        if output_id.eq_ignore_ascii_case("bull") {
9614            return Ok(out.bull);
9615        }
9616        if output_id.eq_ignore_ascii_case("bear") {
9617            return Ok(out.bear);
9618        }
9619        if output_id.eq_ignore_ascii_case("signal") {
9620            return Ok(out.signal);
9621        }
9622        Err(IndicatorDispatchError::UnknownOutput {
9623            indicator: "andean_oscillator".to_string(),
9624            output: output_id.to_string(),
9625        })
9626    })
9627}
9628
9629fn compute_daily_factor_batch(
9630    req: IndicatorBatchRequest<'_>,
9631    output_id: &str,
9632) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9633    let (open, high, low, close) = extract_ohlc_full_input("daily_factor", req.data)?;
9634    let kernel = req.kernel.to_non_batch();
9635    collect_f64("daily_factor", output_id, req.combos, close.len(), |params| {
9636        let threshold_level =
9637            get_f64_param("daily_factor", params, "threshold_level", 0.35)?;
9638        let input = DailyFactorInput::from_slices(
9639            open,
9640            high,
9641            low,
9642            close,
9643            DailyFactorParams {
9644                threshold_level: Some(threshold_level),
9645            },
9646        );
9647        let out = daily_factor_with_kernel(&input, kernel).map_err(|e| {
9648            IndicatorDispatchError::ComputeFailed {
9649                indicator: "daily_factor".to_string(),
9650                details: e.to_string(),
9651            }
9652        })?;
9653        if output_id.eq_ignore_ascii_case("value") {
9654            return Ok(out.value);
9655        }
9656        if output_id.eq_ignore_ascii_case("ema") {
9657            return Ok(out.ema);
9658        }
9659        if output_id.eq_ignore_ascii_case("signal") {
9660            return Ok(out.signal);
9661        }
9662        Err(IndicatorDispatchError::UnknownOutput {
9663            indicator: "daily_factor".to_string(),
9664            output: output_id.to_string(),
9665        })
9666    })
9667}
9668
9669fn compute_ehlers_adaptive_cyber_cycle_batch(
9670    req: IndicatorBatchRequest<'_>,
9671    output_id: &str,
9672) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9673    let data_len = match req.data {
9674        IndicatorDataRef::Slice { values } => values.len(),
9675        IndicatorDataRef::Candles { candles, source } => {
9676            source_type(candles, source.unwrap_or("hl2")).len()
9677        }
9678        _ => {
9679            return Err(IndicatorDispatchError::MissingRequiredInput {
9680                indicator: "ehlers_adaptive_cyber_cycle".to_string(),
9681                input: IndicatorInputKind::Candles,
9682            })
9683        }
9684    };
9685    let kernel = req.kernel.to_non_batch();
9686    collect_f64(
9687        "ehlers_adaptive_cyber_cycle",
9688        output_id,
9689        req.combos,
9690        data_len,
9691        |params| {
9692            let source = get_enum_param("ehlers_adaptive_cyber_cycle", params, "source", "hl2")?;
9693            let alpha = get_f64_param("ehlers_adaptive_cyber_cycle", params, "alpha", 0.07)?;
9694            let data = match req.data {
9695                IndicatorDataRef::Slice { values } => values,
9696                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9697                _ => unreachable!(),
9698            };
9699            let input = EhlersAdaptiveCyberCycleInput::from_slice(
9700                data,
9701                EhlersAdaptiveCyberCycleParams { alpha: Some(alpha) },
9702            );
9703            let out = ehlers_adaptive_cyber_cycle_with_kernel(&input, kernel).map_err(|e| {
9704                IndicatorDispatchError::ComputeFailed {
9705                    indicator: "ehlers_adaptive_cyber_cycle".to_string(),
9706                    details: e.to_string(),
9707                }
9708            })?;
9709            if output_id.eq_ignore_ascii_case("cycle") || output_id.eq_ignore_ascii_case("value") {
9710                return Ok(out.cycle);
9711            }
9712            if output_id.eq_ignore_ascii_case("trigger") {
9713                return Ok(out.trigger);
9714            }
9715            Err(IndicatorDispatchError::UnknownOutput {
9716                indicator: "ehlers_adaptive_cyber_cycle".to_string(),
9717                output: output_id.to_string(),
9718            })
9719        },
9720    )
9721}
9722
9723fn compute_ehlers_simple_cycle_indicator_batch(
9724    req: IndicatorBatchRequest<'_>,
9725    output_id: &str,
9726) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9727    let data_len = match req.data {
9728        IndicatorDataRef::Slice { values } => values.len(),
9729        IndicatorDataRef::Candles { candles, source } => {
9730            source_type(candles, source.unwrap_or("hl2")).len()
9731        }
9732        _ => {
9733            return Err(IndicatorDispatchError::MissingRequiredInput {
9734                indicator: "ehlers_simple_cycle_indicator".to_string(),
9735                input: IndicatorInputKind::Candles,
9736            })
9737        }
9738    };
9739    let kernel = req.kernel.to_non_batch();
9740    collect_f64(
9741        "ehlers_simple_cycle_indicator",
9742        output_id,
9743        req.combos,
9744        data_len,
9745        |params| {
9746            let source = get_enum_param("ehlers_simple_cycle_indicator", params, "source", "hl2")?;
9747            let alpha = get_f64_param("ehlers_simple_cycle_indicator", params, "alpha", 0.07)?;
9748            let data = match req.data {
9749                IndicatorDataRef::Slice { values } => values,
9750                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9751                _ => unreachable!(),
9752            };
9753            let input = EhlersSimpleCycleIndicatorInput::from_slice(
9754                data,
9755                EhlersSimpleCycleIndicatorParams { alpha: Some(alpha) },
9756            );
9757            let out = ehlers_simple_cycle_indicator_with_kernel(&input, kernel).map_err(|e| {
9758                IndicatorDispatchError::ComputeFailed {
9759                    indicator: "ehlers_simple_cycle_indicator".to_string(),
9760                    details: e.to_string(),
9761                }
9762            })?;
9763            if output_id.eq_ignore_ascii_case("cycle") || output_id.eq_ignore_ascii_case("value") {
9764                return Ok(out.cycle);
9765            }
9766            if output_id.eq_ignore_ascii_case("trigger") {
9767                return Ok(out.trigger);
9768            }
9769            Err(IndicatorDispatchError::UnknownOutput {
9770                indicator: "ehlers_simple_cycle_indicator".to_string(),
9771                output: output_id.to_string(),
9772            })
9773        },
9774    )
9775}
9776
9777fn compute_l1_ehlers_phasor_batch(
9778    req: IndicatorBatchRequest<'_>,
9779    output_id: &str,
9780) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9781    expect_value_output("l1_ehlers_phasor", output_id)?;
9782    let data = extract_slice_input("l1_ehlers_phasor", req.data, "close")?;
9783    let kernel = req.kernel.to_non_batch();
9784    collect_f64("l1_ehlers_phasor", output_id, req.combos, data.len(), |params| {
9785        let domestic_cycle_length =
9786            get_usize_param("l1_ehlers_phasor", params, "domestic_cycle_length", 15)?;
9787        let input = L1EhlersPhasorInput::from_slice(
9788            data,
9789            L1EhlersPhasorParams {
9790                domestic_cycle_length: Some(domestic_cycle_length),
9791            },
9792        );
9793        let out = l1_ehlers_phasor_with_kernel(&input, kernel).map_err(|e| {
9794            IndicatorDispatchError::ComputeFailed {
9795                indicator: "l1_ehlers_phasor".to_string(),
9796                details: e.to_string(),
9797            }
9798        })?;
9799        Ok(out.values)
9800    })
9801}
9802
9803fn compute_ehlers_smoothed_adaptive_momentum_batch(
9804    req: IndicatorBatchRequest<'_>,
9805    output_id: &str,
9806) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9807    expect_value_output("ehlers_smoothed_adaptive_momentum", output_id)?;
9808    let data_len = match req.data {
9809        IndicatorDataRef::Slice { values } => values.len(),
9810        IndicatorDataRef::Candles { candles, source } => {
9811            source_type(candles, source.unwrap_or("hl2")).len()
9812        }
9813        _ => {
9814            return Err(IndicatorDispatchError::MissingRequiredInput {
9815                indicator: "ehlers_smoothed_adaptive_momentum".to_string(),
9816                input: IndicatorInputKind::Candles,
9817            })
9818        }
9819    };
9820    let kernel = req.kernel.to_non_batch();
9821    collect_f64(
9822        "ehlers_smoothed_adaptive_momentum",
9823        output_id,
9824        req.combos,
9825        data_len,
9826        |params| {
9827            let source =
9828                get_enum_param("ehlers_smoothed_adaptive_momentum", params, "source", "hl2")?;
9829            let alpha =
9830                get_f64_param("ehlers_smoothed_adaptive_momentum", params, "alpha", 0.07)?;
9831            let cutoff =
9832                get_f64_param("ehlers_smoothed_adaptive_momentum", params, "cutoff", 8.0)?;
9833            let data = match req.data {
9834                IndicatorDataRef::Slice { values } => values,
9835                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9836                _ => unreachable!(),
9837            };
9838            let input = EhlersSmoothedAdaptiveMomentumInput::from_slice(
9839                data,
9840                EhlersSmoothedAdaptiveMomentumParams {
9841                    alpha: Some(alpha),
9842                    cutoff: Some(cutoff),
9843                },
9844            );
9845            let out =
9846                ehlers_smoothed_adaptive_momentum_with_kernel(&input, kernel).map_err(|e| {
9847                    IndicatorDispatchError::ComputeFailed {
9848                        indicator: "ehlers_smoothed_adaptive_momentum".to_string(),
9849                        details: e.to_string(),
9850                    }
9851                })?;
9852            Ok(out.values)
9853        },
9854    )
9855}
9856
9857fn compute_ewma_volatility_batch(
9858    req: IndicatorBatchRequest<'_>,
9859    output_id: &str,
9860) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9861    expect_value_output("ewma_volatility", output_id)?;
9862    let data = extract_slice_input("ewma_volatility", req.data, "close")?;
9863    let kernel = req.kernel.to_non_batch();
9864    collect_f64("ewma_volatility", output_id, req.combos, data.len(), |params| {
9865        let lambda = get_f64_param("ewma_volatility", params, "lambda", 0.94)?;
9866        let input = EwmaVolatilityInput::from_slice(
9867            data,
9868            EwmaVolatilityParams {
9869                lambda: Some(lambda),
9870            },
9871        );
9872        let out = ewma_volatility_with_kernel(&input, kernel).map_err(|e| {
9873            IndicatorDispatchError::ComputeFailed {
9874                indicator: "ewma_volatility".to_string(),
9875                details: e.to_string(),
9876            }
9877        })?;
9878        Ok(out.values)
9879    })
9880}
9881
9882fn compute_random_walk_index_batch(
9883    req: IndicatorBatchRequest<'_>,
9884    output_id: &str,
9885) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9886    let (high, low, close) = extract_ohlc_input("random_walk_index", req.data)?;
9887    let kernel = req.kernel.to_non_batch();
9888    collect_f64("random_walk_index", output_id, req.combos, close.len(), |params| {
9889        let length = get_usize_param("random_walk_index", params, "length", 14)?;
9890        let input = RandomWalkIndexInput::from_slices(
9891            high,
9892            low,
9893            close,
9894            RandomWalkIndexParams {
9895                length: Some(length),
9896            },
9897        );
9898        let out = random_walk_index_with_kernel(&input, kernel).map_err(|e| {
9899            IndicatorDispatchError::ComputeFailed {
9900                indicator: "random_walk_index".to_string(),
9901                details: e.to_string(),
9902            }
9903        })?;
9904        if output_id.eq_ignore_ascii_case("high") {
9905            return Ok(out.high);
9906        }
9907        if output_id.eq_ignore_ascii_case("low") {
9908            return Ok(out.low);
9909        }
9910        Err(IndicatorDispatchError::UnknownOutput {
9911            indicator: "random_walk_index".to_string(),
9912            output: output_id.to_string(),
9913        })
9914    })
9915}
9916
9917fn compute_price_moving_average_ratio_percentile_batch(
9918    req: IndicatorBatchRequest<'_>,
9919    output_id: &str,
9920) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9921    let data_len = match req.data {
9922        IndicatorDataRef::Candles { candles, source } => {
9923            source_type(candles, source.unwrap_or("close")).len()
9924        }
9925        IndicatorDataRef::CloseVolume { close, .. } => close.len(),
9926        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
9927        _ => {
9928            return Err(IndicatorDispatchError::MissingRequiredInput {
9929                indicator: "price_moving_average_ratio_percentile".to_string(),
9930                input: IndicatorInputKind::CloseVolume,
9931            })
9932        }
9933    };
9934    let kernel = req.kernel.to_non_batch();
9935    collect_f64(
9936        "price_moving_average_ratio_percentile",
9937        output_id,
9938        req.combos,
9939        data_len,
9940        |params| {
9941            let source =
9942                get_enum_param("price_moving_average_ratio_percentile", params, "source", "close")?;
9943            let ma_length =
9944                get_usize_param("price_moving_average_ratio_percentile", params, "ma_length", 20)?;
9945            let ma_type = get_enum_param(
9946                "price_moving_average_ratio_percentile",
9947                params,
9948                "ma_type",
9949                "sma",
9950            )?
9951            .parse::<PriceMovingAverageRatioPercentileMaType>()
9952            .map_err(|e| IndicatorDispatchError::InvalidParam {
9953                indicator: "price_moving_average_ratio_percentile".to_string(),
9954                key: "ma_type".to_string(),
9955                reason: e,
9956            })?;
9957            let pmarp_lookback = get_usize_param(
9958                "price_moving_average_ratio_percentile",
9959                params,
9960                "pmarp_lookback",
9961                350,
9962            )?;
9963            let signal_ma_length = get_usize_param(
9964                "price_moving_average_ratio_percentile",
9965                params,
9966                "signal_ma_length",
9967                20,
9968            )?;
9969            let signal_ma_type = get_enum_param(
9970                "price_moving_average_ratio_percentile",
9971                params,
9972                "signal_ma_type",
9973                "sma",
9974            )?
9975            .parse::<PriceMovingAverageRatioPercentileMaType>()
9976            .map_err(|e| IndicatorDispatchError::InvalidParam {
9977                indicator: "price_moving_average_ratio_percentile".to_string(),
9978                key: "signal_ma_type".to_string(),
9979                reason: e,
9980            })?;
9981            let line_mode = get_enum_param(
9982                "price_moving_average_ratio_percentile",
9983                params,
9984                "line_mode",
9985                "pmar",
9986            )?
9987            .parse::<PriceMovingAverageRatioPercentileLineMode>()
9988            .map_err(|e| IndicatorDispatchError::InvalidParam {
9989                indicator: "price_moving_average_ratio_percentile".to_string(),
9990                key: "line_mode".to_string(),
9991                reason: e,
9992            })?;
9993            let (price, volume) = match req.data {
9994                IndicatorDataRef::Candles { candles, .. } => {
9995                    (source_type(candles, &source), candles.volume.as_slice())
9996                }
9997                IndicatorDataRef::CloseVolume { close, volume } => (close, volume),
9998                IndicatorDataRef::Ohlcv {
9999                    open,
10000                    high,
10001                    low,
10002                    close,
10003                    volume,
10004                } => {
10005                    let price = match source.to_ascii_lowercase().as_str() {
10006                        "open" => open,
10007                        "high" => high,
10008                        "low" => low,
10009                        _ => close,
10010                    };
10011                    (price, volume)
10012                }
10013                _ => unreachable!(),
10014            };
10015            let input = PriceMovingAverageRatioPercentileInput::from_slices(
10016                price,
10017                volume,
10018                PriceMovingAverageRatioPercentileParams {
10019                    ma_length: Some(ma_length),
10020                    ma_type: Some(ma_type),
10021                    pmarp_lookback: Some(pmarp_lookback),
10022                    signal_ma_length: Some(signal_ma_length),
10023                    signal_ma_type: Some(signal_ma_type),
10024                    line_mode: Some(line_mode),
10025                },
10026            );
10027            let out = price_moving_average_ratio_percentile_with_kernel(&input, kernel).map_err(
10028                |e| IndicatorDispatchError::ComputeFailed {
10029                    indicator: "price_moving_average_ratio_percentile".to_string(),
10030                    details: e.to_string(),
10031                },
10032            )?;
10033            if output_id.eq_ignore_ascii_case("pmar") {
10034                return Ok(out.pmar);
10035            }
10036            if output_id.eq_ignore_ascii_case("pmarp") {
10037                return Ok(out.pmarp);
10038            }
10039            if output_id.eq_ignore_ascii_case("plotline") || output_id.eq_ignore_ascii_case("value")
10040            {
10041                return Ok(out.plotline);
10042            }
10043            if output_id.eq_ignore_ascii_case("signal") {
10044                return Ok(out.signal);
10045            }
10046            if output_id.eq_ignore_ascii_case("pmar_high") {
10047                return Ok(out.pmar_high);
10048            }
10049            if output_id.eq_ignore_ascii_case("pmar_low") {
10050                return Ok(out.pmar_low);
10051            }
10052            if output_id.eq_ignore_ascii_case("scaled_pmar") {
10053                return Ok(out.scaled_pmar);
10054            }
10055            Err(IndicatorDispatchError::UnknownOutput {
10056                indicator: "price_moving_average_ratio_percentile".to_string(),
10057                output: output_id.to_string(),
10058            })
10059        },
10060    )
10061}
10062
10063fn compute_trend_trigger_factor_batch(
10064    req: IndicatorBatchRequest<'_>,
10065    output_id: &str,
10066) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10067    expect_value_output("trend_trigger_factor", output_id)?;
10068    let (high, low) = extract_high_low_input("trend_trigger_factor", req.data)?;
10069    let kernel = req.kernel.to_non_batch();
10070    collect_f64("trend_trigger_factor", output_id, req.combos, high.len(), |params| {
10071        let length = get_usize_param("trend_trigger_factor", params, "length", 15)?;
10072        let input = TrendTriggerFactorInput::from_slices(
10073            high,
10074            low,
10075            TrendTriggerFactorParams {
10076                length: Some(length),
10077            },
10078        );
10079        let out = trend_trigger_factor_with_kernel(&input, kernel).map_err(|e| {
10080            IndicatorDispatchError::ComputeFailed {
10081                indicator: "trend_trigger_factor".to_string(),
10082                details: e.to_string(),
10083            }
10084        })?;
10085        Ok(out.values)
10086    })
10087}
10088
10089fn compute_mesa_stochastic_multi_length_batch(
10090    req: IndicatorBatchRequest<'_>,
10091    output_id: &str,
10092) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10093    let data_len = match req.data {
10094        IndicatorDataRef::Slice { values } => values.len(),
10095        IndicatorDataRef::Candles { candles, source } => {
10096            source_type(candles, source.unwrap_or("close")).len()
10097        }
10098        _ => {
10099            return Err(IndicatorDispatchError::MissingRequiredInput {
10100                indicator: "mesa_stochastic_multi_length".to_string(),
10101                input: IndicatorInputKind::Candles,
10102            })
10103        }
10104    };
10105    let kernel = req.kernel.to_non_batch();
10106    collect_f64(
10107        "mesa_stochastic_multi_length",
10108        output_id,
10109        req.combos,
10110        data_len,
10111        |params| {
10112            let source =
10113                get_enum_param("mesa_stochastic_multi_length", params, "source", "close")?;
10114            let length_1 =
10115                get_usize_param("mesa_stochastic_multi_length", params, "length_1", 48)?;
10116            let length_2 =
10117                get_usize_param("mesa_stochastic_multi_length", params, "length_2", 21)?;
10118            let length_3 =
10119                get_usize_param("mesa_stochastic_multi_length", params, "length_3", 9)?;
10120            let length_4 =
10121                get_usize_param("mesa_stochastic_multi_length", params, "length_4", 6)?;
10122            let trigger_length =
10123                get_usize_param("mesa_stochastic_multi_length", params, "trigger_length", 2)?;
10124            let data = match req.data {
10125                IndicatorDataRef::Slice { values } => values,
10126                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
10127                _ => unreachable!(),
10128            };
10129            let input = MesaStochasticMultiLengthInput::from_slices(
10130                data,
10131                MesaStochasticMultiLengthParams {
10132                    length_1: Some(length_1),
10133                    length_2: Some(length_2),
10134                    length_3: Some(length_3),
10135                    length_4: Some(length_4),
10136                    trigger_length: Some(trigger_length),
10137                },
10138            );
10139            let out = mesa_stochastic_multi_length_with_kernel(&input, kernel).map_err(|e| {
10140                IndicatorDispatchError::ComputeFailed {
10141                    indicator: "mesa_stochastic_multi_length".to_string(),
10142                    details: e.to_string(),
10143                }
10144            })?;
10145            if output_id.eq_ignore_ascii_case("mesa_1") {
10146                return Ok(out.mesa_1);
10147            }
10148            if output_id.eq_ignore_ascii_case("mesa_2") {
10149                return Ok(out.mesa_2);
10150            }
10151            if output_id.eq_ignore_ascii_case("mesa_3") {
10152                return Ok(out.mesa_3);
10153            }
10154            if output_id.eq_ignore_ascii_case("mesa_4") {
10155                return Ok(out.mesa_4);
10156            }
10157            if output_id.eq_ignore_ascii_case("trigger_1") {
10158                return Ok(out.trigger_1);
10159            }
10160            if output_id.eq_ignore_ascii_case("trigger_2") {
10161                return Ok(out.trigger_2);
10162            }
10163            if output_id.eq_ignore_ascii_case("trigger_3") {
10164                return Ok(out.trigger_3);
10165            }
10166            if output_id.eq_ignore_ascii_case("trigger_4") {
10167                return Ok(out.trigger_4);
10168            }
10169            Err(IndicatorDispatchError::UnknownOutput {
10170                indicator: "mesa_stochastic_multi_length".to_string(),
10171                output: output_id.to_string(),
10172            })
10173        },
10174    )
10175}
10176
10177fn compute_spearman_correlation_batch(
10178    req: IndicatorBatchRequest<'_>,
10179    output_id: &str,
10180) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10181    let data_len = match req.data {
10182        IndicatorDataRef::Candles { candles, source } => {
10183            source_type(candles, source.unwrap_or("close")).len()
10184        }
10185        _ => {
10186            return Err(IndicatorDispatchError::MissingRequiredInput {
10187                indicator: "spearman_correlation".to_string(),
10188                input: IndicatorInputKind::Candles,
10189            })
10190        }
10191    };
10192    let kernel = req.kernel.to_non_batch();
10193    collect_f64("spearman_correlation", output_id, req.combos, data_len, |params| {
10194        let source = get_enum_param("spearman_correlation", params, "source", "close")?;
10195        let comparison_source =
10196            get_enum_param("spearman_correlation", params, "comparison_source", "open")?;
10197        let lookback = get_usize_param("spearman_correlation", params, "lookback", 30)?;
10198        let smoothing_length =
10199            get_usize_param("spearman_correlation", params, "smoothing_length", 3)?;
10200        let (main, compare) = match req.data {
10201            IndicatorDataRef::Candles { candles, .. } => (
10202                source_type(candles, &source),
10203                source_type(candles, &comparison_source),
10204            ),
10205            _ => unreachable!(),
10206        };
10207        let input = SpearmanCorrelationInput::from_slices(
10208            main,
10209            compare,
10210            SpearmanCorrelationParams {
10211                lookback: Some(lookback),
10212                smoothing_length: Some(smoothing_length),
10213            },
10214        );
10215        let out = spearman_correlation_with_kernel(&input, kernel).map_err(|e| {
10216            IndicatorDispatchError::ComputeFailed {
10217                indicator: "spearman_correlation".to_string(),
10218                details: e.to_string(),
10219            }
10220        })?;
10221        if output_id.eq_ignore_ascii_case("raw") || output_id.eq_ignore_ascii_case("value") {
10222            return Ok(out.raw);
10223        }
10224        if output_id.eq_ignore_ascii_case("smoothed") {
10225            return Ok(out.smoothed);
10226        }
10227        Err(IndicatorDispatchError::UnknownOutput {
10228            indicator: "spearman_correlation".to_string(),
10229            output: output_id.to_string(),
10230        })
10231    })
10232}
10233
10234fn compute_relative_strength_index_wave_indicator_batch(
10235    req: IndicatorBatchRequest<'_>,
10236    output_id: &str,
10237) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10238    let data_len = match req.data {
10239        IndicatorDataRef::Candles { candles, source } => {
10240            source_type(candles, source.unwrap_or("close")).len()
10241        }
10242        _ => {
10243            return Err(IndicatorDispatchError::MissingRequiredInput {
10244                indicator: "relative_strength_index_wave_indicator".to_string(),
10245                input: IndicatorInputKind::Candles,
10246            })
10247        }
10248    };
10249    let kernel = req.kernel.to_non_batch();
10250    collect_f64(
10251        "relative_strength_index_wave_indicator",
10252        output_id,
10253        req.combos,
10254        data_len,
10255        |params| {
10256            let source = get_enum_param(
10257                "relative_strength_index_wave_indicator",
10258                params,
10259                "source",
10260                "close",
10261            )?;
10262            let rsi_length = get_usize_param(
10263                "relative_strength_index_wave_indicator",
10264                params,
10265                "rsi_length",
10266                14,
10267            )?;
10268            let length1 = get_usize_param(
10269                "relative_strength_index_wave_indicator",
10270                params,
10271                "length1",
10272                2,
10273            )?;
10274            let length2 = get_usize_param(
10275                "relative_strength_index_wave_indicator",
10276                params,
10277                "length2",
10278                5,
10279            )?;
10280            let length3 = get_usize_param(
10281                "relative_strength_index_wave_indicator",
10282                params,
10283                "length3",
10284                9,
10285            )?;
10286            let length4 = get_usize_param(
10287                "relative_strength_index_wave_indicator",
10288                params,
10289                "length4",
10290                13,
10291            )?;
10292            let (src, high, low) = match req.data {
10293                IndicatorDataRef::Candles { candles, .. } => (
10294                    source_type(candles, &source),
10295                    candles.high.as_slice(),
10296                    candles.low.as_slice(),
10297                ),
10298                _ => unreachable!(),
10299            };
10300            let input = RelativeStrengthIndexWaveIndicatorInput::from_slices(
10301                src,
10302                high,
10303                low,
10304                RelativeStrengthIndexWaveIndicatorParams {
10305                    rsi_length: Some(rsi_length),
10306                    length1: Some(length1),
10307                    length2: Some(length2),
10308                    length3: Some(length3),
10309                    length4: Some(length4),
10310                },
10311            );
10312            let out =
10313                relative_strength_index_wave_indicator_with_kernel(&input, kernel).map_err(|e| {
10314                    IndicatorDispatchError::ComputeFailed {
10315                        indicator: "relative_strength_index_wave_indicator".to_string(),
10316                        details: e.to_string(),
10317                    }
10318                })?;
10319            if output_id.eq_ignore_ascii_case("rsi_ma1") || output_id.eq_ignore_ascii_case("value")
10320            {
10321                return Ok(out.rsi_ma1);
10322            }
10323            if output_id.eq_ignore_ascii_case("rsi_ma2") {
10324                return Ok(out.rsi_ma2);
10325            }
10326            if output_id.eq_ignore_ascii_case("rsi_ma3") {
10327                return Ok(out.rsi_ma3);
10328            }
10329            if output_id.eq_ignore_ascii_case("rsi_ma4") {
10330                return Ok(out.rsi_ma4);
10331            }
10332            if output_id.eq_ignore_ascii_case("state") {
10333                return Ok(out.state);
10334            }
10335            Err(IndicatorDispatchError::UnknownOutput {
10336                indicator: "relative_strength_index_wave_indicator".to_string(),
10337                output: output_id.to_string(),
10338            })
10339        },
10340    )
10341}
10342
10343fn compute_accumulation_swing_index_batch(
10344    req: IndicatorBatchRequest<'_>,
10345    output_id: &str,
10346) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10347    expect_value_output("accumulation_swing_index", output_id)?;
10348    let (open, high, low, close) = extract_ohlc_full_input("accumulation_swing_index", req.data)?;
10349    let kernel = req.kernel.to_non_batch();
10350    collect_f64(
10351        "accumulation_swing_index",
10352        output_id,
10353        req.combos,
10354        close.len(),
10355        |params| {
10356            let daily_limit =
10357                get_f64_param("accumulation_swing_index", params, "daily_limit", 10_000.0)?;
10358            let input = AccumulationSwingIndexInput::from_slices(
10359                open,
10360                high,
10361                low,
10362                close,
10363                AccumulationSwingIndexParams {
10364                    daily_limit: Some(daily_limit),
10365                },
10366            );
10367            let out = accumulation_swing_index_with_kernel(&input, kernel).map_err(|e| {
10368                IndicatorDispatchError::ComputeFailed {
10369                    indicator: "accumulation_swing_index".to_string(),
10370                    details: e.to_string(),
10371                }
10372            })?;
10373            Ok(out.values)
10374        },
10375    )
10376}
10377
10378fn compute_ichimoku_oscillator_batch(
10379    req: IndicatorBatchRequest<'_>,
10380    output_id: &str,
10381) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10382    let (high, low, close) = extract_ohlc_input("ichimoku_oscillator", req.data)?;
10383    let kernel = req.kernel.to_non_batch();
10384    collect_f64("ichimoku_oscillator", output_id, req.combos, close.len(), |params| {
10385        let source_name = get_enum_param("ichimoku_oscillator", params, "source", "close")?;
10386        let conversion_periods =
10387            get_usize_param("ichimoku_oscillator", params, "conversion_periods", 9)?;
10388        let base_periods = get_usize_param("ichimoku_oscillator", params, "base_periods", 26)?;
10389        let lagging_span_periods =
10390            get_usize_param("ichimoku_oscillator", params, "lagging_span_periods", 52)?;
10391        let displacement = get_usize_param("ichimoku_oscillator", params, "displacement", 26)?;
10392        let ma_length = get_usize_param("ichimoku_oscillator", params, "ma_length", 12)?;
10393        let smoothing_length =
10394            get_usize_param("ichimoku_oscillator", params, "smoothing_length", 3)?;
10395        let extra_smoothing =
10396            get_bool_param("ichimoku_oscillator", params, "extra_smoothing", true)?;
10397        let normalize = get_enum_param("ichimoku_oscillator", params, "normalize", "window")?
10398            .parse::<IchimokuOscillatorNormalizeMode>()
10399            .map_err(|e| IndicatorDispatchError::InvalidParam {
10400                indicator: "ichimoku_oscillator".to_string(),
10401                key: "normalize".to_string(),
10402                reason: e,
10403            })?;
10404        let window_size = get_usize_param("ichimoku_oscillator", params, "window_size", 20)?;
10405        let clamp = get_bool_param("ichimoku_oscillator", params, "clamp", true)?;
10406        let top_band = get_f64_param("ichimoku_oscillator", params, "top_band", 2.0)?;
10407        let mid_band = get_f64_param("ichimoku_oscillator", params, "mid_band", 1.5)?;
10408        let source = match req.data {
10409            IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source_name),
10410            _ => close,
10411        };
10412        let input = IchimokuOscillatorInput::from_slices(
10413            high,
10414            low,
10415            close,
10416            source,
10417            IchimokuOscillatorParams {
10418                conversion_periods: Some(conversion_periods),
10419                base_periods: Some(base_periods),
10420                lagging_span_periods: Some(lagging_span_periods),
10421                displacement: Some(displacement),
10422                ma_length: Some(ma_length),
10423                smoothing_length: Some(smoothing_length),
10424                extra_smoothing: Some(extra_smoothing),
10425                normalize: Some(normalize),
10426                window_size: Some(window_size),
10427                clamp: Some(clamp),
10428                top_band: Some(top_band),
10429                mid_band: Some(mid_band),
10430            },
10431        );
10432        let out = ichimoku_oscillator_with_kernel(&input, kernel).map_err(|e| {
10433            IndicatorDispatchError::ComputeFailed {
10434                indicator: "ichimoku_oscillator".to_string(),
10435                details: e.to_string(),
10436            }
10437        })?;
10438        if output_id.eq_ignore_ascii_case("signal") || output_id.eq_ignore_ascii_case("value") {
10439            return Ok(out.signal);
10440        }
10441        if output_id.eq_ignore_ascii_case("ma") {
10442            return Ok(out.ma);
10443        }
10444        if output_id.eq_ignore_ascii_case("conversion") {
10445            return Ok(out.conversion);
10446        }
10447        if output_id.eq_ignore_ascii_case("base") {
10448            return Ok(out.base);
10449        }
10450        if output_id.eq_ignore_ascii_case("chikou") {
10451            return Ok(out.chikou);
10452        }
10453        if output_id.eq_ignore_ascii_case("current_kumo_a") {
10454            return Ok(out.current_kumo_a);
10455        }
10456        if output_id.eq_ignore_ascii_case("current_kumo_b") {
10457            return Ok(out.current_kumo_b);
10458        }
10459        if output_id.eq_ignore_ascii_case("future_kumo_a") {
10460            return Ok(out.future_kumo_a);
10461        }
10462        if output_id.eq_ignore_ascii_case("future_kumo_b") {
10463            return Ok(out.future_kumo_b);
10464        }
10465        if output_id.eq_ignore_ascii_case("max_level") {
10466            return Ok(out.max_level);
10467        }
10468        if output_id.eq_ignore_ascii_case("high_level") {
10469            return Ok(out.high_level);
10470        }
10471        if output_id.eq_ignore_ascii_case("low_level") {
10472            return Ok(out.low_level);
10473        }
10474        if output_id.eq_ignore_ascii_case("min_level") {
10475            return Ok(out.min_level);
10476        }
10477        Err(IndicatorDispatchError::UnknownOutput {
10478            indicator: "ichimoku_oscillator".to_string(),
10479            output: output_id.to_string(),
10480        })
10481    })
10482}
10483
10484fn compute_volatility_quality_index_batch(
10485    req: IndicatorBatchRequest<'_>,
10486    output_id: &str,
10487) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10488    let (open, high, low, close) = extract_ohlc_full_input("volatility_quality_index", req.data)?;
10489    let kernel = req.kernel.to_non_batch();
10490    collect_f64("volatility_quality_index", output_id, req.combos, close.len(), |params| {
10491        let fast_length = get_usize_param("volatility_quality_index", params, "fast_length", 9)?;
10492        let slow_length =
10493            get_usize_param("volatility_quality_index", params, "slow_length", 200)?;
10494        let input = VolatilityQualityIndexInput::from_slices(
10495            open,
10496            high,
10497            low,
10498            close,
10499            VolatilityQualityIndexParams {
10500                fast_length: Some(fast_length),
10501                slow_length: Some(slow_length),
10502            },
10503        );
10504        let out = volatility_quality_index_with_kernel(&input, kernel).map_err(|e| {
10505            IndicatorDispatchError::ComputeFailed {
10506                indicator: "volatility_quality_index".to_string(),
10507                details: e.to_string(),
10508            }
10509        })?;
10510        if output_id.eq_ignore_ascii_case("vqi_sum") || output_id.eq_ignore_ascii_case("value") {
10511            return Ok(out.vqi_sum);
10512        }
10513        if output_id.eq_ignore_ascii_case("fast_sma") {
10514            return Ok(out.fast_sma);
10515        }
10516        if output_id.eq_ignore_ascii_case("slow_sma") {
10517            return Ok(out.slow_sma);
10518        }
10519        Err(IndicatorDispatchError::UnknownOutput {
10520            indicator: "volatility_quality_index".to_string(),
10521            output: output_id.to_string(),
10522        })
10523    })
10524}
10525
10526fn compute_vwap_deviation_oscillator_batch(
10527    req: IndicatorBatchRequest<'_>,
10528    output_id: &str,
10529) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10530    let (timestamps, high, low, close, volume): (&[i64], &[f64], &[f64], &[f64], &[f64]) =
10531        match req.data {
10532            IndicatorDataRef::Candles { candles, .. } => (
10533                candles.timestamp.as_slice(),
10534                candles.high.as_slice(),
10535                candles.low.as_slice(),
10536                candles.close.as_slice(),
10537                candles.volume.as_slice(),
10538            ),
10539            _ => {
10540                return Err(IndicatorDispatchError::MissingRequiredInput {
10541                    indicator: "vwap_deviation_oscillator".to_string(),
10542                    input: IndicatorInputKind::Candles,
10543                })
10544            }
10545        };
10546    let kernel = req.kernel.to_non_batch();
10547    collect_f64(
10548        "vwap_deviation_oscillator",
10549        output_id,
10550        req.combos,
10551        close.len(),
10552        |params| {
10553            let session_mode =
10554                get_enum_param("vwap_deviation_oscillator", params, "session_mode", "rolling_bars")?
10555                    .parse::<VwapDeviationSessionMode>()
10556                    .map_err(|e| IndicatorDispatchError::InvalidParam {
10557                        indicator: "vwap_deviation_oscillator".to_string(),
10558                        key: "session_mode".to_string(),
10559                        reason: e,
10560                    })?;
10561            let rolling_period =
10562                get_usize_param("vwap_deviation_oscillator", params, "rolling_period", 20)?;
10563            let rolling_days =
10564                get_usize_param("vwap_deviation_oscillator", params, "rolling_days", 30)?;
10565            let use_close =
10566                get_bool_param("vwap_deviation_oscillator", params, "use_close", false)?;
10567            let deviation_mode =
10568                get_enum_param("vwap_deviation_oscillator", params, "deviation_mode", "absolute")?
10569                    .parse::<VwapDeviationMode>()
10570                    .map_err(|e| IndicatorDispatchError::InvalidParam {
10571                        indicator: "vwap_deviation_oscillator".to_string(),
10572                        key: "deviation_mode".to_string(),
10573                        reason: e,
10574                    })?;
10575            let z_window = get_usize_param("vwap_deviation_oscillator", params, "z_window", 50)?;
10576            let pct_vol_lookback =
10577                get_usize_param("vwap_deviation_oscillator", params, "pct_vol_lookback", 100)?;
10578            let pct_min_sigma =
10579                get_f64_param("vwap_deviation_oscillator", params, "pct_min_sigma", 0.1)?;
10580            let abs_vol_lookback =
10581                get_usize_param("vwap_deviation_oscillator", params, "abs_vol_lookback", 100)?;
10582            let input = VwapDeviationOscillatorInput::from_slices(
10583                timestamps,
10584                high,
10585                low,
10586                close,
10587                volume,
10588                VwapDeviationOscillatorParams {
10589                    session_mode: Some(session_mode),
10590                    rolling_period: Some(rolling_period),
10591                    rolling_days: Some(rolling_days),
10592                    use_close: Some(use_close),
10593                    deviation_mode: Some(deviation_mode),
10594                    z_window: Some(z_window),
10595                    pct_vol_lookback: Some(pct_vol_lookback),
10596                    pct_min_sigma: Some(pct_min_sigma),
10597                    abs_vol_lookback: Some(abs_vol_lookback),
10598                },
10599            );
10600            let out = vwap_deviation_oscillator_with_kernel(&input, kernel).map_err(|e| {
10601                IndicatorDispatchError::ComputeFailed {
10602                    indicator: "vwap_deviation_oscillator".to_string(),
10603                    details: e.to_string(),
10604                }
10605            })?;
10606            if output_id.eq_ignore_ascii_case("osc") || output_id.eq_ignore_ascii_case("value") {
10607                return Ok(out.osc);
10608            }
10609            if output_id.eq_ignore_ascii_case("std1") {
10610                return Ok(out.std1);
10611            }
10612            if output_id.eq_ignore_ascii_case("std2") {
10613                return Ok(out.std2);
10614            }
10615            if output_id.eq_ignore_ascii_case("std3") {
10616                return Ok(out.std3);
10617            }
10618            Err(IndicatorDispatchError::UnknownOutput {
10619                indicator: "vwap_deviation_oscillator".to_string(),
10620                output: output_id.to_string(),
10621            })
10622        },
10623    )
10624}
10625
10626fn compute_bulls_v_bears_batch(
10627    req: IndicatorBatchRequest<'_>,
10628    output_id: &str,
10629) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10630    let (high, low, close) = extract_ohlc_input("bulls_v_bears", req.data)?;
10631    let kernel = req.kernel.to_non_batch();
10632    collect_f64("bulls_v_bears", output_id, req.combos, close.len(), |params| {
10633        let period = get_usize_param("bulls_v_bears", params, "period", 14)?;
10634        let ma_type = get_enum_param("bulls_v_bears", params, "ma_type", "ema")?
10635            .parse::<BullsVBearsMaType>()
10636            .map_err(|e| IndicatorDispatchError::InvalidParam {
10637                indicator: "bulls_v_bears".to_string(),
10638                key: "ma_type".to_string(),
10639                reason: e,
10640            })?;
10641        let calculation_method =
10642            get_enum_param("bulls_v_bears", params, "calculation_method", "normalized")?
10643                .parse::<BullsVBearsCalculationMethod>()
10644                .map_err(|e| IndicatorDispatchError::InvalidParam {
10645                    indicator: "bulls_v_bears".to_string(),
10646                    key: "calculation_method".to_string(),
10647                    reason: e,
10648                })?;
10649        let normalized_bars_back =
10650            get_usize_param("bulls_v_bears", params, "normalized_bars_back", 120)?;
10651        let raw_rolling_period =
10652            get_usize_param("bulls_v_bears", params, "raw_rolling_period", 50)?;
10653        let raw_threshold_percentile =
10654            get_f64_param("bulls_v_bears", params, "raw_threshold_percentile", 95.0)?;
10655        let threshold_level =
10656            get_f64_param("bulls_v_bears", params, "threshold_level", 80.0)?;
10657        let input = BullsVBearsInput::from_slices(
10658            high,
10659            low,
10660            close,
10661            BullsVBearsParams {
10662                period: Some(period),
10663                ma_type: Some(ma_type),
10664                calculation_method: Some(calculation_method),
10665                normalized_bars_back: Some(normalized_bars_back),
10666                raw_rolling_period: Some(raw_rolling_period),
10667                raw_threshold_percentile: Some(raw_threshold_percentile),
10668                threshold_level: Some(threshold_level),
10669            },
10670        );
10671        let out = bulls_v_bears_with_kernel(&input, kernel).map_err(|e| {
10672            IndicatorDispatchError::ComputeFailed {
10673                indicator: "bulls_v_bears".to_string(),
10674                details: e.to_string(),
10675            }
10676        })?;
10677        if output_id.eq_ignore_ascii_case("value") {
10678            return Ok(out.value);
10679        }
10680        if output_id.eq_ignore_ascii_case("bull") {
10681            return Ok(out.bull);
10682        }
10683        if output_id.eq_ignore_ascii_case("bear") {
10684            return Ok(out.bear);
10685        }
10686        if output_id.eq_ignore_ascii_case("ma") {
10687            return Ok(out.ma);
10688        }
10689        if output_id.eq_ignore_ascii_case("upper") {
10690            return Ok(out.upper);
10691        }
10692        if output_id.eq_ignore_ascii_case("lower") {
10693            return Ok(out.lower);
10694        }
10695        if output_id.eq_ignore_ascii_case("bullish_signal") {
10696            return Ok(out.bullish_signal);
10697        }
10698        if output_id.eq_ignore_ascii_case("bearish_signal") {
10699            return Ok(out.bearish_signal);
10700        }
10701        if output_id.eq_ignore_ascii_case("zero_cross_up") {
10702            return Ok(out.zero_cross_up);
10703        }
10704        if output_id.eq_ignore_ascii_case("zero_cross_down") {
10705            return Ok(out.zero_cross_down);
10706        }
10707        Err(IndicatorDispatchError::UnknownOutput {
10708            indicator: "bulls_v_bears".to_string(),
10709            output: output_id.to_string(),
10710        })
10711    })
10712}
10713
10714fn compute_smooth_theil_sen_batch(
10715    req: IndicatorBatchRequest<'_>,
10716    output_id: &str,
10717) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10718    let data = extract_slice_input("smooth_theil_sen", req.data, "close")?;
10719    let kernel = req.kernel.to_non_batch();
10720    collect_f64("smooth_theil_sen", output_id, req.combos, data.len(), |params| {
10721        let length = get_usize_param("smooth_theil_sen", params, "length", 25)?;
10722        let offset = get_usize_param("smooth_theil_sen", params, "offset", 0)?;
10723        let multiplier = get_f64_param("smooth_theil_sen", params, "multiplier", 2.0)?;
10724        let slope_style = get_enum_param("smooth_theil_sen", params, "slope_style", "smooth_median")?
10725            .parse::<SmoothTheilSenStatStyle>()
10726            .map_err(|e| IndicatorDispatchError::InvalidParam {
10727                indicator: "smooth_theil_sen".to_string(),
10728                key: "slope_style".to_string(),
10729                reason: e,
10730            })?;
10731        let residual_style =
10732            get_enum_param("smooth_theil_sen", params, "residual_style", "smooth_median")?
10733                .parse::<SmoothTheilSenStatStyle>()
10734                .map_err(|e| IndicatorDispatchError::InvalidParam {
10735                    indicator: "smooth_theil_sen".to_string(),
10736                    key: "residual_style".to_string(),
10737                    reason: e,
10738                })?;
10739        let deviation_style =
10740            get_enum_param("smooth_theil_sen", params, "deviation_style", "mad")?
10741                .parse::<SmoothTheilSenDeviationType>()
10742                .map_err(|e| IndicatorDispatchError::InvalidParam {
10743                    indicator: "smooth_theil_sen".to_string(),
10744                    key: "deviation_style".to_string(),
10745                    reason: e,
10746                })?;
10747        let mad_style = get_enum_param("smooth_theil_sen", params, "mad_style", "smooth_median")?
10748            .parse::<SmoothTheilSenStatStyle>()
10749            .map_err(|e| IndicatorDispatchError::InvalidParam {
10750                indicator: "smooth_theil_sen".to_string(),
10751                key: "mad_style".to_string(),
10752                reason: e,
10753            })?;
10754        let include_prediction_in_deviation = get_bool_param(
10755            "smooth_theil_sen",
10756            params,
10757            "include_prediction_in_deviation",
10758            false,
10759        )?;
10760        let input = SmoothTheilSenInput::from_slice(
10761            data,
10762            SmoothTheilSenParams {
10763                length: Some(length),
10764                offset: Some(offset),
10765                multiplier: Some(multiplier),
10766                slope_style: Some(slope_style),
10767                residual_style: Some(residual_style),
10768                deviation_style: Some(deviation_style),
10769                mad_style: Some(mad_style),
10770                include_prediction_in_deviation: Some(include_prediction_in_deviation),
10771            },
10772        );
10773        let out = smooth_theil_sen_with_kernel(&input, kernel).map_err(|e| {
10774            IndicatorDispatchError::ComputeFailed {
10775                indicator: "smooth_theil_sen".to_string(),
10776                details: e.to_string(),
10777            }
10778        })?;
10779        if output_id.eq_ignore_ascii_case("value") {
10780            return Ok(out.value);
10781        }
10782        if output_id.eq_ignore_ascii_case("upper") {
10783            return Ok(out.upper);
10784        }
10785        if output_id.eq_ignore_ascii_case("lower") {
10786            return Ok(out.lower);
10787        }
10788        if output_id.eq_ignore_ascii_case("slope") {
10789            return Ok(out.slope);
10790        }
10791        if output_id.eq_ignore_ascii_case("intercept") {
10792            return Ok(out.intercept);
10793        }
10794        if output_id.eq_ignore_ascii_case("deviation") {
10795            return Ok(out.deviation);
10796        }
10797        Err(IndicatorDispatchError::UnknownOutput {
10798            indicator: "smooth_theil_sen".to_string(),
10799            output: output_id.to_string(),
10800        })
10801    })
10802}
10803
10804fn compute_regression_slope_oscillator_batch(
10805    req: IndicatorBatchRequest<'_>,
10806    output_id: &str,
10807) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10808    let data_len = match req.data {
10809        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
10810        IndicatorDataRef::Slice { values } => values.len(),
10811        _ => {
10812            return Err(IndicatorDispatchError::MissingRequiredInput {
10813                indicator: "regression_slope_oscillator".to_string(),
10814                input: IndicatorInputKind::Slice,
10815            })
10816        }
10817    };
10818    let kernel = req.kernel.to_non_batch();
10819    collect_f64(
10820        "regression_slope_oscillator",
10821        output_id,
10822        req.combos,
10823        data_len,
10824        |params| {
10825            let min_range =
10826                get_usize_param("regression_slope_oscillator", params, "min_range", 10)?;
10827            let max_range =
10828                get_usize_param("regression_slope_oscillator", params, "max_range", 100)?;
10829            let step = get_usize_param("regression_slope_oscillator", params, "step", 5)?;
10830            let signal_line =
10831                get_usize_param("regression_slope_oscillator", params, "signal_line", 7)?;
10832            let input = match req.data {
10833                IndicatorDataRef::Candles { candles, .. } => {
10834                    RegressionSlopeOscillatorInput::from_candles(
10835                        candles,
10836                        RegressionSlopeOscillatorParams {
10837                            min_range: Some(min_range),
10838                            max_range: Some(max_range),
10839                            step: Some(step),
10840                            signal_line: Some(signal_line),
10841                        },
10842                    )
10843                }
10844                IndicatorDataRef::Slice { values } => RegressionSlopeOscillatorInput::from_slice(
10845                    values,
10846                    RegressionSlopeOscillatorParams {
10847                        min_range: Some(min_range),
10848                        max_range: Some(max_range),
10849                        step: Some(step),
10850                        signal_line: Some(signal_line),
10851                    },
10852                ),
10853                _ => unreachable!(),
10854            };
10855            let out = regression_slope_oscillator_with_kernel(&input, kernel).map_err(|e| {
10856                IndicatorDispatchError::ComputeFailed {
10857                    indicator: "regression_slope_oscillator".to_string(),
10858                    details: e.to_string(),
10859                }
10860            })?;
10861            if output_id.eq_ignore_ascii_case("value") {
10862                return Ok(out.value);
10863            }
10864            if output_id.eq_ignore_ascii_case("signal") {
10865                return Ok(out.signal);
10866            }
10867            if output_id.eq_ignore_ascii_case("bullish_reversal") {
10868                return Ok(out.bullish_reversal);
10869            }
10870            if output_id.eq_ignore_ascii_case("bearish_reversal") {
10871                return Ok(out.bearish_reversal);
10872            }
10873            Err(IndicatorDispatchError::UnknownOutput {
10874                indicator: "regression_slope_oscillator".to_string(),
10875                output: output_id.to_string(),
10876            })
10877        },
10878    )
10879}
10880
10881fn compute_linear_regression_intensity_batch(
10882    req: IndicatorBatchRequest<'_>,
10883    output_id: &str,
10884) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10885    expect_value_output("linear_regression_intensity", output_id)?;
10886    let data_len = match req.data {
10887        IndicatorDataRef::Candles { candles, source } => {
10888            source_type(candles, source.unwrap_or("close")).len()
10889        }
10890        IndicatorDataRef::Slice { values } => values.len(),
10891        _ => {
10892            return Err(IndicatorDispatchError::MissingRequiredInput {
10893                indicator: "linear_regression_intensity".to_string(),
10894                input: IndicatorInputKind::Slice,
10895            })
10896        }
10897    };
10898    let kernel = req.kernel.to_non_batch();
10899    collect_f64(
10900        "linear_regression_intensity",
10901        output_id,
10902        req.combos,
10903        data_len,
10904        |params| {
10905            let source =
10906                get_enum_param("linear_regression_intensity", params, "source", "close")?;
10907            let lookback_period =
10908                get_usize_param("linear_regression_intensity", params, "lookback_period", 12)?;
10909            let range_tolerance = get_f64_param(
10910                "linear_regression_intensity",
10911                params,
10912                "range_tolerance",
10913                90.0,
10914            )?;
10915            let linreg_length =
10916                get_usize_param("linear_regression_intensity", params, "linreg_length", 90)?;
10917            let input = match req.data {
10918                IndicatorDataRef::Candles { candles, .. } => {
10919                    LinearRegressionIntensityInput::from_candles(
10920                        candles,
10921                        &source,
10922                        LinearRegressionIntensityParams {
10923                            lookback_period: Some(lookback_period),
10924                            range_tolerance: Some(range_tolerance),
10925                            linreg_length: Some(linreg_length),
10926                        },
10927                    )
10928                }
10929                IndicatorDataRef::Slice { values } => LinearRegressionIntensityInput::from_slice(
10930                    values,
10931                    LinearRegressionIntensityParams {
10932                        lookback_period: Some(lookback_period),
10933                        range_tolerance: Some(range_tolerance),
10934                        linreg_length: Some(linreg_length),
10935                    },
10936                ),
10937                _ => unreachable!(),
10938            };
10939            let out = linear_regression_intensity_with_kernel(&input, kernel).map_err(|e| {
10940                IndicatorDispatchError::ComputeFailed {
10941                    indicator: "linear_regression_intensity".to_string(),
10942                    details: e.to_string(),
10943                }
10944            })?;
10945            Ok(out.values)
10946        },
10947    )
10948}
10949
10950fn compute_moving_average_cross_probability_batch(
10951    req: IndicatorBatchRequest<'_>,
10952    output_id: &str,
10953) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10954    let data_len = match req.data {
10955        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
10956        IndicatorDataRef::Slice { values } => values.len(),
10957        _ => {
10958            return Err(IndicatorDispatchError::MissingRequiredInput {
10959                indicator: "moving_average_cross_probability".to_string(),
10960                input: IndicatorInputKind::Slice,
10961            })
10962        }
10963    };
10964    let kernel = req.kernel.to_non_batch();
10965    collect_f64(
10966        "moving_average_cross_probability",
10967        output_id,
10968        req.combos,
10969        data_len,
10970        |params| {
10971            let ma_type = get_enum_param(
10972                "moving_average_cross_probability",
10973                params,
10974                "ma_type",
10975                "ema",
10976            )?
10977            .parse::<MovingAverageCrossProbabilityMaType>()
10978            .map_err(|e| IndicatorDispatchError::InvalidParam {
10979                indicator: "moving_average_cross_probability".to_string(),
10980                key: "ma_type".to_string(),
10981                reason: e,
10982            })?;
10983            let smoothing_window = get_usize_param(
10984                "moving_average_cross_probability",
10985                params,
10986                "smoothing_window",
10987                7,
10988            )?;
10989            let slow_length =
10990                get_usize_param("moving_average_cross_probability", params, "slow_length", 30)?;
10991            let fast_length =
10992                get_usize_param("moving_average_cross_probability", params, "fast_length", 14)?;
10993            let resolution =
10994                get_usize_param("moving_average_cross_probability", params, "resolution", 50)?;
10995            let params = MovingAverageCrossProbabilityParams {
10996                ma_type: Some(ma_type),
10997                smoothing_window: Some(smoothing_window),
10998                slow_length: Some(slow_length),
10999                fast_length: Some(fast_length),
11000                resolution: Some(resolution),
11001            };
11002            let input = match req.data {
11003                IndicatorDataRef::Candles { candles, .. } => {
11004                    MovingAverageCrossProbabilityInput::from_candles(candles, params)
11005                }
11006                IndicatorDataRef::Slice { values } => {
11007                    MovingAverageCrossProbabilityInput::from_slice(values, params)
11008                }
11009                _ => unreachable!(),
11010            };
11011            let out = moving_average_cross_probability_with_kernel(&input, kernel).map_err(
11012                |e| IndicatorDispatchError::ComputeFailed {
11013                    indicator: "moving_average_cross_probability".to_string(),
11014                    details: e.to_string(),
11015                },
11016            )?;
11017            if output_id.eq_ignore_ascii_case("value") {
11018                return Ok(out.value);
11019            }
11020            if output_id.eq_ignore_ascii_case("slow_ma") {
11021                return Ok(out.slow_ma);
11022            }
11023            if output_id.eq_ignore_ascii_case("fast_ma") {
11024                return Ok(out.fast_ma);
11025            }
11026            if output_id.eq_ignore_ascii_case("forecast") {
11027                return Ok(out.forecast);
11028            }
11029            if output_id.eq_ignore_ascii_case("upper") {
11030                return Ok(out.upper);
11031            }
11032            if output_id.eq_ignore_ascii_case("lower") {
11033                return Ok(out.lower);
11034            }
11035            if output_id.eq_ignore_ascii_case("direction") {
11036                return Ok(out.direction);
11037            }
11038            Err(IndicatorDispatchError::UnknownOutput {
11039                indicator: "moving_average_cross_probability".to_string(),
11040                output: output_id.to_string(),
11041            })
11042        },
11043    )
11044}
11045
11046fn compute_volume_zone_oscillator_batch(
11047    req: IndicatorBatchRequest<'_>,
11048    output_id: &str,
11049) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11050    expect_value_output("volume_zone_oscillator", output_id)?;
11051    let (close, volume) = extract_close_volume_input("volume_zone_oscillator", req.data, "close")?;
11052    let kernel = req.kernel.to_non_batch();
11053    collect_f64(
11054        "volume_zone_oscillator",
11055        output_id,
11056        req.combos,
11057        close.len(),
11058        |params| {
11059            let length = get_usize_param("volume_zone_oscillator", params, "length", 14)?;
11060            let intraday_smoothing = get_bool_param(
11061                "volume_zone_oscillator",
11062                params,
11063                "intraday_smoothing",
11064                true,
11065            )?;
11066            let noise_filter =
11067                get_usize_param("volume_zone_oscillator", params, "noise_filter", 4)?;
11068            let input = VolumeZoneOscillatorInput::from_slices(
11069                close,
11070                volume,
11071                VolumeZoneOscillatorParams {
11072                    length: Some(length),
11073                    intraday_smoothing: Some(intraday_smoothing),
11074                    noise_filter: Some(noise_filter),
11075                },
11076            );
11077            let out = volume_zone_oscillator_with_kernel(&input, kernel).map_err(|e| {
11078                IndicatorDispatchError::ComputeFailed {
11079                    indicator: "volume_zone_oscillator".to_string(),
11080                    details: e.to_string(),
11081                }
11082            })?;
11083            Ok(out.values)
11084        },
11085    )
11086}
11087
11088fn compute_market_meanness_index_batch(
11089    req: IndicatorBatchRequest<'_>,
11090    output_id: &str,
11091) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11092    let data_len = match req.data {
11093        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
11094        IndicatorDataRef::Ohlc { close, .. } => close.len(),
11095        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
11096        _ => {
11097            return Err(IndicatorDispatchError::MissingRequiredInput {
11098                indicator: "market_meanness_index".to_string(),
11099                input: IndicatorInputKind::Ohlc,
11100            })
11101        }
11102    };
11103    let kernel = req.kernel.to_non_batch();
11104    collect_f64("market_meanness_index", output_id, req.combos, data_len, |params| {
11105        let length = get_usize_param("market_meanness_index", params, "length", 300)?;
11106        let source_mode =
11107            get_enum_param("market_meanness_index", params, "source_mode", "Price")?;
11108        let input = match req.data {
11109            IndicatorDataRef::Candles { candles, .. } => MarketMeannessIndexInput::from_candles(
11110                candles,
11111                MarketMeannessIndexParams {
11112                    length: Some(length),
11113                    source_mode: Some(source_mode),
11114                },
11115            ),
11116            IndicatorDataRef::Ohlc { open, close, .. } => MarketMeannessIndexInput::from_slices(
11117                open,
11118                close,
11119                MarketMeannessIndexParams {
11120                    length: Some(length),
11121                    source_mode: Some(source_mode),
11122                },
11123            ),
11124            IndicatorDataRef::Ohlcv { open, close, .. } => MarketMeannessIndexInput::from_slices(
11125                open,
11126                close,
11127                MarketMeannessIndexParams {
11128                    length: Some(length),
11129                    source_mode: Some(source_mode),
11130                },
11131            ),
11132            _ => unreachable!(),
11133        };
11134        let out = market_meanness_index_with_kernel(&input, kernel).map_err(|e| {
11135            IndicatorDispatchError::ComputeFailed {
11136                indicator: "market_meanness_index".to_string(),
11137                details: e.to_string(),
11138            }
11139        })?;
11140        if output_id.eq_ignore_ascii_case("mmi") || output_id.eq_ignore_ascii_case("value") {
11141            return Ok(out.mmi);
11142        }
11143        if output_id.eq_ignore_ascii_case("mmi_smoothed") {
11144            return Ok(out.mmi_smoothed);
11145        }
11146        Err(IndicatorDispatchError::UnknownOutput {
11147            indicator: "market_meanness_index".to_string(),
11148            output: output_id.to_string(),
11149        })
11150    })
11151}
11152
11153fn compute_momentum_ratio_oscillator_batch(
11154    req: IndicatorBatchRequest<'_>,
11155    output_id: &str,
11156) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11157    let data_len = match req.data {
11158        IndicatorDataRef::Candles { candles, source } => {
11159            source_type(candles, source.unwrap_or("close")).len()
11160        }
11161        IndicatorDataRef::Slice { values } => values.len(),
11162        _ => {
11163            return Err(IndicatorDispatchError::MissingRequiredInput {
11164                indicator: "momentum_ratio_oscillator".to_string(),
11165                input: IndicatorInputKind::Slice,
11166            })
11167        }
11168    };
11169    let kernel = req.kernel.to_non_batch();
11170    collect_f64(
11171        "momentum_ratio_oscillator",
11172        output_id,
11173        req.combos,
11174        data_len,
11175        |params| {
11176            let source = get_enum_param("momentum_ratio_oscillator", params, "source", "close")?;
11177            let period = get_usize_param("momentum_ratio_oscillator", params, "period", 50)?;
11178            let input = match req.data {
11179                IndicatorDataRef::Candles { candles, .. } => {
11180                    MomentumRatioOscillatorInput::from_candles(
11181                        candles,
11182                        &source,
11183                        MomentumRatioOscillatorParams {
11184                            period: Some(period),
11185                        },
11186                    )
11187                }
11188                IndicatorDataRef::Slice { values } => MomentumRatioOscillatorInput::from_slice(
11189                    values,
11190                    MomentumRatioOscillatorParams {
11191                        period: Some(period),
11192                    },
11193                ),
11194                _ => unreachable!(),
11195            };
11196            let out = momentum_ratio_oscillator_with_kernel(&input, kernel).map_err(|e| {
11197                IndicatorDispatchError::ComputeFailed {
11198                    indicator: "momentum_ratio_oscillator".to_string(),
11199                    details: e.to_string(),
11200                }
11201            })?;
11202            if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
11203                return Ok(out.line);
11204            }
11205            if output_id.eq_ignore_ascii_case("signal") {
11206                return Ok(out.signal);
11207            }
11208            Err(IndicatorDispatchError::UnknownOutput {
11209                indicator: "momentum_ratio_oscillator".to_string(),
11210                output: output_id.to_string(),
11211            })
11212        },
11213    )
11214}
11215
11216fn compute_pretty_good_oscillator_batch(
11217    req: IndicatorBatchRequest<'_>,
11218    output_id: &str,
11219) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11220    expect_value_output("pretty_good_oscillator", output_id)?;
11221    let data_len = match req.data {
11222        IndicatorDataRef::Candles { candles, source } => {
11223            source_type(candles, source.unwrap_or("close")).len()
11224        }
11225        IndicatorDataRef::Ohlc { close, .. } => close.len(),
11226        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
11227        _ => {
11228            return Err(IndicatorDispatchError::MissingRequiredInput {
11229                indicator: "pretty_good_oscillator".to_string(),
11230                input: IndicatorInputKind::Ohlc,
11231            })
11232        }
11233    };
11234    let kernel = req.kernel.to_non_batch();
11235    collect_f64("pretty_good_oscillator", output_id, req.combos, data_len, |params| {
11236        let source = get_enum_param("pretty_good_oscillator", params, "source", "close")?;
11237        let length = get_usize_param("pretty_good_oscillator", params, "length", 14)?;
11238        let input = match req.data {
11239            IndicatorDataRef::Candles { candles, .. } => PrettyGoodOscillatorInput::from_candles(
11240                candles,
11241                &source,
11242                PrettyGoodOscillatorParams {
11243                    length: Some(length),
11244                },
11245            ),
11246            IndicatorDataRef::Ohlc { high, low, close, open } => {
11247                ensure_same_len_4("pretty_good_oscillator", open.len(), high.len(), low.len(), close.len())?;
11248                let src = match source.to_ascii_lowercase().as_str() {
11249                    "open" => open,
11250                    "high" => high,
11251                    "low" => low,
11252                    _ => close,
11253                };
11254                PrettyGoodOscillatorInput::from_slices(
11255                    high,
11256                    low,
11257                    close,
11258                    src,
11259                    PrettyGoodOscillatorParams {
11260                        length: Some(length),
11261                    },
11262                )
11263            }
11264            IndicatorDataRef::Ohlcv { high, low, close, open, volume } => {
11265                ensure_same_len_5(
11266                    "pretty_good_oscillator",
11267                    open.len(),
11268                    high.len(),
11269                    low.len(),
11270                    close.len(),
11271                    volume.len(),
11272                )?;
11273                let src = match source.to_ascii_lowercase().as_str() {
11274                    "open" => open,
11275                    "high" => high,
11276                    "low" => low,
11277                    _ => close,
11278                };
11279                PrettyGoodOscillatorInput::from_slices(
11280                    high,
11281                    low,
11282                    close,
11283                    src,
11284                    PrettyGoodOscillatorParams {
11285                        length: Some(length),
11286                    },
11287                )
11288            }
11289            _ => unreachable!(),
11290        };
11291        let out = pretty_good_oscillator_with_kernel(&input, kernel).map_err(|e| {
11292            IndicatorDispatchError::ComputeFailed {
11293                indicator: "pretty_good_oscillator".to_string(),
11294                details: e.to_string(),
11295            }
11296        })?;
11297        Ok(out.values)
11298    })
11299}
11300
11301fn compute_price_density_market_noise_batch(
11302    req: IndicatorBatchRequest<'_>,
11303    output_id: &str,
11304) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11305    let (high, low, close) = extract_ohlc_input("price_density_market_noise", req.data)?;
11306    let kernel = req.kernel.to_non_batch();
11307    collect_f64(
11308        "price_density_market_noise",
11309        output_id,
11310        req.combos,
11311        close.len(),
11312        |params| {
11313            let length = get_usize_param("price_density_market_noise", params, "length", 14)?;
11314            let eval_period =
11315                get_usize_param("price_density_market_noise", params, "eval_period", 200)?;
11316            let input = PriceDensityMarketNoiseInput::from_slices(
11317                high,
11318                low,
11319                close,
11320                PriceDensityMarketNoiseParams {
11321                    length: Some(length),
11322                    eval_period: Some(eval_period),
11323                },
11324            );
11325            let out = price_density_market_noise_with_kernel(&input, kernel).map_err(|e| {
11326                IndicatorDispatchError::ComputeFailed {
11327                    indicator: "price_density_market_noise".to_string(),
11328                    details: e.to_string(),
11329                }
11330            })?;
11331            if output_id.eq_ignore_ascii_case("price_density") || output_id.eq_ignore_ascii_case("value")
11332            {
11333                return Ok(out.price_density);
11334            }
11335            if output_id.eq_ignore_ascii_case("price_density_percent") {
11336                return Ok(out.price_density_percent);
11337            }
11338            Err(IndicatorDispatchError::UnknownOutput {
11339                indicator: "price_density_market_noise".to_string(),
11340                output: output_id.to_string(),
11341            })
11342        },
11343    )
11344}
11345
11346fn compute_psychological_line_batch(
11347    req: IndicatorBatchRequest<'_>,
11348    output_id: &str,
11349) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11350    expect_value_output("psychological_line", output_id)?;
11351    let data_len = match req.data {
11352        IndicatorDataRef::Candles { candles, source } => {
11353            source_type(candles, source.unwrap_or("close")).len()
11354        }
11355        IndicatorDataRef::Slice { values } => values.len(),
11356        _ => {
11357            return Err(IndicatorDispatchError::MissingRequiredInput {
11358                indicator: "psychological_line".to_string(),
11359                input: IndicatorInputKind::Slice,
11360            })
11361        }
11362    };
11363    let kernel = req.kernel.to_non_batch();
11364    collect_f64("psychological_line", output_id, req.combos, data_len, |params| {
11365        let source = get_enum_param("psychological_line", params, "source", "close")?;
11366        let length = get_usize_param("psychological_line", params, "length", 20)?;
11367        let input = match req.data {
11368            IndicatorDataRef::Candles { candles, .. } => PsychologicalLineInput::from_candles(
11369                candles,
11370                &source,
11371                PsychologicalLineParams {
11372                    length: Some(length),
11373                },
11374            ),
11375            IndicatorDataRef::Slice { values } => PsychologicalLineInput::from_slice(
11376                values,
11377                PsychologicalLineParams {
11378                    length: Some(length),
11379                },
11380            ),
11381            _ => unreachable!(),
11382        };
11383        let out = psychological_line_with_kernel(&input, kernel).map_err(|e| {
11384            IndicatorDispatchError::ComputeFailed {
11385                indicator: "psychological_line".to_string(),
11386                details: e.to_string(),
11387            }
11388        })?;
11389        Ok(out.values)
11390    })
11391}
11392
11393fn compute_rank_correlation_index_batch(
11394    req: IndicatorBatchRequest<'_>,
11395    output_id: &str,
11396) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11397    expect_value_output("rank_correlation_index", output_id)?;
11398    let data_len = match req.data {
11399        IndicatorDataRef::Candles { candles, source } => {
11400            source_type(candles, source.unwrap_or("close")).len()
11401        }
11402        IndicatorDataRef::Slice { values } => values.len(),
11403        _ => {
11404            return Err(IndicatorDispatchError::MissingRequiredInput {
11405                indicator: "rank_correlation_index".to_string(),
11406                input: IndicatorInputKind::Slice,
11407            })
11408        }
11409    };
11410    let kernel = req.kernel.to_non_batch();
11411    collect_f64("rank_correlation_index", output_id, req.combos, data_len, |params| {
11412        let source = get_enum_param("rank_correlation_index", params, "source", "close")?;
11413        let length = get_usize_param("rank_correlation_index", params, "length", 12)?;
11414        let input = match req.data {
11415            IndicatorDataRef::Candles { candles, .. } => RankCorrelationIndexInput::from_candles(
11416                candles,
11417                &source,
11418                RankCorrelationIndexParams {
11419                    length: Some(length),
11420                },
11421            ),
11422            IndicatorDataRef::Slice { values } => RankCorrelationIndexInput::from_slice(
11423                values,
11424                RankCorrelationIndexParams {
11425                    length: Some(length),
11426                },
11427            ),
11428            _ => unreachable!(),
11429        };
11430        let out = rank_correlation_index_with_kernel(&input, kernel).map_err(|e| {
11431            IndicatorDispatchError::ComputeFailed {
11432                indicator: "rank_correlation_index".to_string(),
11433                details: e.to_string(),
11434            }
11435        })?;
11436        Ok(out.values)
11437    })
11438}
11439
11440fn compute_smoothed_gaussian_trend_filter_batch(
11441    req: IndicatorBatchRequest<'_>,
11442    output_id: &str,
11443) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11444    let (high, low, close) = extract_ohlc_input("smoothed_gaussian_trend_filter", req.data)?;
11445    let kernel = req.kernel.to_non_batch();
11446    collect_f64(
11447        "smoothed_gaussian_trend_filter",
11448        output_id,
11449        req.combos,
11450        close.len(),
11451        |params| {
11452            let gaussian_length = get_usize_param(
11453                "smoothed_gaussian_trend_filter",
11454                params,
11455                "gaussian_length",
11456                15,
11457            )?;
11458            let poles =
11459                get_usize_param("smoothed_gaussian_trend_filter", params, "poles", 3)?;
11460            let smoothing_length = get_usize_param(
11461                "smoothed_gaussian_trend_filter",
11462                params,
11463                "smoothing_length",
11464                22,
11465            )?;
11466            let linreg_offset = get_usize_param(
11467                "smoothed_gaussian_trend_filter",
11468                params,
11469                "linreg_offset",
11470                7,
11471            )?;
11472            let input = SmoothedGaussianTrendFilterInput::from_slices(
11473                high,
11474                low,
11475                close,
11476                SmoothedGaussianTrendFilterParams {
11477                    gaussian_length: Some(gaussian_length),
11478                    poles: Some(poles),
11479                    smoothing_length: Some(smoothing_length),
11480                    linreg_offset: Some(linreg_offset),
11481                },
11482            );
11483            let out = smoothed_gaussian_trend_filter_with_kernel(&input, kernel).map_err(|e| {
11484                IndicatorDispatchError::ComputeFailed {
11485                    indicator: "smoothed_gaussian_trend_filter".to_string(),
11486                    details: e.to_string(),
11487                }
11488            })?;
11489            if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
11490                return Ok(out.filter);
11491            }
11492            if output_id.eq_ignore_ascii_case("supertrend") {
11493                return Ok(out.supertrend);
11494            }
11495            if output_id.eq_ignore_ascii_case("trend") {
11496                return Ok(out.trend);
11497            }
11498            if output_id.eq_ignore_ascii_case("ranging") {
11499                return Ok(out.ranging);
11500            }
11501            Err(IndicatorDispatchError::UnknownOutput {
11502                indicator: "smoothed_gaussian_trend_filter".to_string(),
11503                output: output_id.to_string(),
11504            })
11505        },
11506    )
11507}
11508
11509fn compute_stochastic_adaptive_d_batch(
11510    req: IndicatorBatchRequest<'_>,
11511    output_id: &str,
11512) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11513    let (high, low, close) = extract_ohlc_input("stochastic_adaptive_d", req.data)?;
11514    let kernel = req.kernel.to_non_batch();
11515    collect_f64("stochastic_adaptive_d", output_id, req.combos, close.len(), |params| {
11516        let k_length = get_usize_param("stochastic_adaptive_d", params, "k_length", 20)?;
11517        let d_smoothing =
11518            get_usize_param("stochastic_adaptive_d", params, "d_smoothing", 9)?;
11519        let pre_smooth =
11520            get_usize_param("stochastic_adaptive_d", params, "pre_smooth", 20)?;
11521        let attenuation =
11522            get_f64_param("stochastic_adaptive_d", params, "attenuation", 2.0)?;
11523        let input = StochasticAdaptiveDInput::from_slices(
11524            high,
11525            low,
11526            close,
11527            StochasticAdaptiveDParams {
11528                k_length: Some(k_length),
11529                d_smoothing: Some(d_smoothing),
11530                pre_smooth: Some(pre_smooth),
11531                attenuation: Some(attenuation),
11532            },
11533        );
11534        let out = stochastic_adaptive_d_with_kernel(&input, kernel).map_err(|e| {
11535            IndicatorDispatchError::ComputeFailed {
11536                indicator: "stochastic_adaptive_d".to_string(),
11537                details: e.to_string(),
11538            }
11539        })?;
11540        if output_id.eq_ignore_ascii_case("standard_d") || output_id.eq_ignore_ascii_case("value") {
11541            return Ok(out.standard_d);
11542        }
11543        if output_id.eq_ignore_ascii_case("adaptive_d") {
11544            return Ok(out.adaptive_d);
11545        }
11546        if output_id.eq_ignore_ascii_case("difference") {
11547            return Ok(out.difference);
11548        }
11549        Err(IndicatorDispatchError::UnknownOutput {
11550            indicator: "stochastic_adaptive_d".to_string(),
11551            output: output_id.to_string(),
11552        })
11553    })
11554}
11555
11556fn compute_stochastic_connors_rsi_batch(
11557    req: IndicatorBatchRequest<'_>,
11558    output_id: &str,
11559) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11560    let data_len = match req.data {
11561        IndicatorDataRef::Candles { candles, source } => {
11562            source_type(candles, source.unwrap_or("close")).len()
11563        }
11564        IndicatorDataRef::Slice { values } => values.len(),
11565        _ => {
11566            return Err(IndicatorDispatchError::MissingRequiredInput {
11567                indicator: "stochastic_connors_rsi".to_string(),
11568                input: IndicatorInputKind::Slice,
11569            })
11570        }
11571    };
11572    let kernel = req.kernel.to_non_batch();
11573    collect_f64("stochastic_connors_rsi", output_id, req.combos, data_len, |params| {
11574        let source = get_enum_param("stochastic_connors_rsi", params, "source", "close")?;
11575        let stoch_length =
11576            get_usize_param("stochastic_connors_rsi", params, "stoch_length", 3)?;
11577        let smooth_k = get_usize_param("stochastic_connors_rsi", params, "smooth_k", 3)?;
11578        let smooth_d = get_usize_param("stochastic_connors_rsi", params, "smooth_d", 3)?;
11579        let rsi_length = get_usize_param("stochastic_connors_rsi", params, "rsi_length", 3)?;
11580        let updown_length =
11581            get_usize_param("stochastic_connors_rsi", params, "updown_length", 2)?;
11582        let roc_length = get_usize_param("stochastic_connors_rsi", params, "roc_length", 100)?;
11583        let input = match req.data {
11584            IndicatorDataRef::Candles { candles, .. } => StochasticConnorsRsiInput::from_candles(
11585                candles,
11586                &source,
11587                StochasticConnorsRsiParams {
11588                    stoch_length: Some(stoch_length),
11589                    smooth_k: Some(smooth_k),
11590                    smooth_d: Some(smooth_d),
11591                    rsi_length: Some(rsi_length),
11592                    updown_length: Some(updown_length),
11593                    roc_length: Some(roc_length),
11594                },
11595            ),
11596            IndicatorDataRef::Slice { values } => StochasticConnorsRsiInput::from_slice(
11597                values,
11598                StochasticConnorsRsiParams {
11599                    stoch_length: Some(stoch_length),
11600                    smooth_k: Some(smooth_k),
11601                    smooth_d: Some(smooth_d),
11602                    rsi_length: Some(rsi_length),
11603                    updown_length: Some(updown_length),
11604                    roc_length: Some(roc_length),
11605                },
11606            ),
11607            _ => unreachable!(),
11608        };
11609        let out = stochastic_connors_rsi_with_kernel(&input, kernel).map_err(|e| {
11610            IndicatorDispatchError::ComputeFailed {
11611                indicator: "stochastic_connors_rsi".to_string(),
11612                details: e.to_string(),
11613            }
11614        })?;
11615        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
11616            return Ok(out.k);
11617        }
11618        if output_id.eq_ignore_ascii_case("d") {
11619            return Ok(out.d);
11620        }
11621        Err(IndicatorDispatchError::UnknownOutput {
11622            indicator: "stochastic_connors_rsi".to_string(),
11623            output: output_id.to_string(),
11624        })
11625    })
11626}
11627
11628fn compute_supertrend_oscillator_batch(
11629    req: IndicatorBatchRequest<'_>,
11630    output_id: &str,
11631) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11632    let data_len = match req.data {
11633        IndicatorDataRef::Candles { candles, source } => {
11634            source_type(candles, source.unwrap_or("close")).len()
11635        }
11636        IndicatorDataRef::Ohlc { close, .. } => close.len(),
11637        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
11638        _ => {
11639            return Err(IndicatorDispatchError::MissingRequiredInput {
11640                indicator: "supertrend_oscillator".to_string(),
11641                input: IndicatorInputKind::Ohlc,
11642            })
11643        }
11644    };
11645    let kernel = req.kernel.to_non_batch();
11646    collect_f64("supertrend_oscillator", output_id, req.combos, data_len, |params| {
11647        let source = get_enum_param("supertrend_oscillator", params, "source", "close")?;
11648        let length = get_usize_param("supertrend_oscillator", params, "length", 10)?;
11649        let mult = get_f64_param("supertrend_oscillator", params, "mult", 2.0)?;
11650        let smooth = get_usize_param("supertrend_oscillator", params, "smooth", 72)?;
11651        let input = match req.data {
11652            IndicatorDataRef::Candles { candles, .. } => SuperTrendOscillatorInput::from_candles(
11653                candles,
11654                &source,
11655                SuperTrendOscillatorParams {
11656                    length: Some(length),
11657                    mult: Some(mult),
11658                    smooth: Some(smooth),
11659                },
11660            ),
11661            IndicatorDataRef::Ohlc { high, low, close, open } => {
11662                ensure_same_len_4("supertrend_oscillator", open.len(), high.len(), low.len(), close.len())?;
11663                let src = match source.to_ascii_lowercase().as_str() {
11664                    "open" => open,
11665                    "high" => high,
11666                    "low" => low,
11667                    _ => close,
11668                };
11669                SuperTrendOscillatorInput::from_slices(
11670                    high,
11671                    low,
11672                    src,
11673                    SuperTrendOscillatorParams {
11674                        length: Some(length),
11675                        mult: Some(mult),
11676                        smooth: Some(smooth),
11677                    },
11678                )
11679            }
11680            IndicatorDataRef::Ohlcv { high, low, close, open, volume } => {
11681                ensure_same_len_5(
11682                    "supertrend_oscillator",
11683                    open.len(),
11684                    high.len(),
11685                    low.len(),
11686                    close.len(),
11687                    volume.len(),
11688                )?;
11689                let src = match source.to_ascii_lowercase().as_str() {
11690                    "open" => open,
11691                    "high" => high,
11692                    "low" => low,
11693                    _ => close,
11694                };
11695                SuperTrendOscillatorInput::from_slices(
11696                    high,
11697                    low,
11698                    src,
11699                    SuperTrendOscillatorParams {
11700                        length: Some(length),
11701                        mult: Some(mult),
11702                        smooth: Some(smooth),
11703                    },
11704                )
11705            }
11706            _ => unreachable!(),
11707        };
11708        let out = supertrend_oscillator_with_kernel(&input, kernel).map_err(|e| {
11709            IndicatorDispatchError::ComputeFailed {
11710                indicator: "supertrend_oscillator".to_string(),
11711                details: e.to_string(),
11712            }
11713        })?;
11714        if output_id.eq_ignore_ascii_case("oscillator") || output_id.eq_ignore_ascii_case("value") {
11715            return Ok(out.oscillator);
11716        }
11717        if output_id.eq_ignore_ascii_case("signal") {
11718            return Ok(out.signal);
11719        }
11720        if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist") {
11721            return Ok(out.histogram);
11722        }
11723        Err(IndicatorDispatchError::UnknownOutput {
11724            indicator: "supertrend_oscillator".to_string(),
11725            output: output_id.to_string(),
11726        })
11727    })
11728}
11729
11730fn compute_trend_continuation_factor_batch(
11731    req: IndicatorBatchRequest<'_>,
11732    output_id: &str,
11733) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11734    let data_len = match req.data {
11735        IndicatorDataRef::Candles { candles, source } => {
11736            source_type(candles, source.unwrap_or("close")).len()
11737        }
11738        IndicatorDataRef::Slice { values } => values.len(),
11739        _ => {
11740            return Err(IndicatorDispatchError::MissingRequiredInput {
11741                indicator: "trend_continuation_factor".to_string(),
11742                input: IndicatorInputKind::Slice,
11743            })
11744        }
11745    };
11746    let kernel = req.kernel.to_non_batch();
11747    collect_f64(
11748        "trend_continuation_factor",
11749        output_id,
11750        req.combos,
11751        data_len,
11752        |params| {
11753            let source =
11754                get_enum_param("trend_continuation_factor", params, "source", "close")?;
11755            let length = get_usize_param("trend_continuation_factor", params, "length", 35)?;
11756            let input = match req.data {
11757                IndicatorDataRef::Candles { candles, .. } => {
11758                    TrendContinuationFactorInput::from_candles(
11759                        candles,
11760                        &source,
11761                        TrendContinuationFactorParams {
11762                            length: Some(length),
11763                        },
11764                    )
11765                }
11766                IndicatorDataRef::Slice { values } => TrendContinuationFactorInput::from_slice(
11767                    values,
11768                    TrendContinuationFactorParams {
11769                        length: Some(length),
11770                    },
11771                ),
11772                _ => unreachable!(),
11773            };
11774            let out = trend_continuation_factor_with_kernel(&input, kernel).map_err(|e| {
11775                IndicatorDispatchError::ComputeFailed {
11776                    indicator: "trend_continuation_factor".to_string(),
11777                    details: e.to_string(),
11778                }
11779            })?;
11780            if output_id.eq_ignore_ascii_case("plus_tcf") || output_id.eq_ignore_ascii_case("value") {
11781                return Ok(out.plus_tcf);
11782            }
11783            if output_id.eq_ignore_ascii_case("minus_tcf") {
11784                return Ok(out.minus_tcf);
11785            }
11786            Err(IndicatorDispatchError::UnknownOutput {
11787                indicator: "trend_continuation_factor".to_string(),
11788                output: output_id.to_string(),
11789            })
11790        },
11791    )
11792}
11793
11794fn compute_volume_weighted_stochastic_rsi_batch(
11795    req: IndicatorBatchRequest<'_>,
11796    output_id: &str,
11797) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11798    let (source, volume) =
11799        extract_close_volume_input("volume_weighted_stochastic_rsi", req.data, "close")?;
11800    let kernel = req.kernel.to_non_batch();
11801    collect_f64(
11802        "volume_weighted_stochastic_rsi",
11803        output_id,
11804        req.combos,
11805        source.len(),
11806        |params| {
11807            let rsi_length =
11808                get_usize_param("volume_weighted_stochastic_rsi", params, "rsi_length", 14)?;
11809            let stoch_length =
11810                get_usize_param("volume_weighted_stochastic_rsi", params, "stoch_length", 14)?;
11811            let k_length =
11812                get_usize_param("volume_weighted_stochastic_rsi", params, "k_length", 3)?;
11813            let d_length =
11814                get_usize_param("volume_weighted_stochastic_rsi", params, "d_length", 3)?;
11815            let ma_type =
11816                get_enum_param("volume_weighted_stochastic_rsi", params, "ma_type", "WSMA")?;
11817            let input = VolumeWeightedStochasticRsiInput::from_slices(
11818                source,
11819                volume,
11820                VolumeWeightedStochasticRsiParams {
11821                    rsi_length: Some(rsi_length),
11822                    stoch_length: Some(stoch_length),
11823                    k_length: Some(k_length),
11824                    d_length: Some(d_length),
11825                    ma_type: Some(ma_type),
11826                },
11827            );
11828            let out = volume_weighted_stochastic_rsi_with_kernel(&input, kernel).map_err(|e| {
11829                IndicatorDispatchError::ComputeFailed {
11830                    indicator: "volume_weighted_stochastic_rsi".to_string(),
11831                    details: e.to_string(),
11832                }
11833            })?;
11834            if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
11835                return Ok(out.k);
11836            }
11837            if output_id.eq_ignore_ascii_case("d") {
11838                return Ok(out.d);
11839            }
11840            Err(IndicatorDispatchError::UnknownOutput {
11841                indicator: "volume_weighted_stochastic_rsi".to_string(),
11842                output: output_id.to_string(),
11843            })
11844        },
11845    )
11846}
11847
11848fn compute_logarithmic_moving_average_batch(
11849    req: IndicatorBatchRequest<'_>,
11850    output_id: &str,
11851) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11852    let data_len = match req.data {
11853        IndicatorDataRef::Candles { candles, source } => {
11854            source_type(candles, source.unwrap_or("close")).len()
11855        }
11856        IndicatorDataRef::Slice { values } => values.len(),
11857        IndicatorDataRef::CloseVolume { close, .. } => close.len(),
11858        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
11859        _ => {
11860            return Err(IndicatorDispatchError::MissingRequiredInput {
11861                indicator: "logarithmic_moving_average".to_string(),
11862                input: IndicatorInputKind::CloseVolume,
11863            })
11864        }
11865    };
11866    let kernel = req.kernel.to_non_batch();
11867    collect_f64(
11868        "logarithmic_moving_average",
11869        output_id,
11870        req.combos,
11871        data_len,
11872        |params| {
11873            let source =
11874                get_enum_param("logarithmic_moving_average", params, "source", "close")?;
11875            let period = get_usize_param("logarithmic_moving_average", params, "period", 100)?;
11876            let steepness =
11877                get_f64_param("logarithmic_moving_average", params, "steepness", 2.5)?;
11878            let ma_type =
11879                get_enum_param("logarithmic_moving_average", params, "ma_type", "ema")?;
11880            let smooth = get_usize_param("logarithmic_moving_average", params, "smooth", 10)?;
11881            let momentum_weight = get_f64_param(
11882                "logarithmic_moving_average",
11883                params,
11884                "momentum_weight",
11885                1.2,
11886            )?;
11887            let long_threshold = get_f64_param(
11888                "logarithmic_moving_average",
11889                params,
11890                "long_threshold",
11891                0.5,
11892            )?;
11893            let short_threshold = get_f64_param(
11894                "logarithmic_moving_average",
11895                params,
11896                "short_threshold",
11897                -0.5,
11898            )?;
11899            let params = LogarithmicMovingAverageParams {
11900                period: Some(period),
11901                steepness: Some(steepness),
11902                ma_type: Some(ma_type),
11903                smooth: Some(smooth),
11904                momentum_weight: Some(momentum_weight),
11905                long_threshold: Some(long_threshold),
11906                short_threshold: Some(short_threshold),
11907            };
11908            let input = match req.data {
11909                IndicatorDataRef::Candles { candles, .. } => {
11910                    LogarithmicMovingAverageInput::from_candles(candles, &source, params)
11911                }
11912                IndicatorDataRef::Slice { values } => {
11913                    LogarithmicMovingAverageInput::from_slice(values, params)
11914                }
11915                IndicatorDataRef::CloseVolume { close, volume } => {
11916                    LogarithmicMovingAverageInput::from_slice_with_volume(close, volume, params)
11917                }
11918                IndicatorDataRef::Ohlcv {
11919                    open,
11920                    high,
11921                    low,
11922                    close,
11923                    volume,
11924                } => {
11925                    let price = match source.to_ascii_lowercase().as_str() {
11926                        "open" => open,
11927                        "high" => high,
11928                        "low" => low,
11929                        _ => close,
11930                    };
11931                    LogarithmicMovingAverageInput::from_slice_with_volume(price, volume, params)
11932                }
11933                _ => unreachable!(),
11934            };
11935            let out = logarithmic_moving_average_with_kernel(&input, kernel).map_err(|e| {
11936                IndicatorDispatchError::ComputeFailed {
11937                    indicator: "logarithmic_moving_average".to_string(),
11938                    details: e.to_string(),
11939                }
11940            })?;
11941            if output_id.eq_ignore_ascii_case("lma") || output_id.eq_ignore_ascii_case("value") {
11942                return Ok(out.lma);
11943            }
11944            if output_id.eq_ignore_ascii_case("signal") {
11945                return Ok(out.signal);
11946            }
11947            if output_id.eq_ignore_ascii_case("position") {
11948                return Ok(out.position);
11949            }
11950            if output_id.eq_ignore_ascii_case("momentum_confirmed") {
11951                return Ok(out.momentum_confirmed);
11952            }
11953            Err(IndicatorDispatchError::UnknownOutput {
11954                indicator: "logarithmic_moving_average".to_string(),
11955                output: output_id.to_string(),
11956            })
11957        },
11958    )
11959}
11960
11961fn compute_adaptive_schaff_trend_cycle_batch(
11962    req: IndicatorBatchRequest<'_>,
11963    output_id: &str,
11964) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11965    let (high, low, close) = extract_ohlc_input("adaptive_schaff_trend_cycle", req.data)?;
11966    let kernel = req.kernel.to_non_batch();
11967    collect_f64(
11968        "adaptive_schaff_trend_cycle",
11969        output_id,
11970        req.combos,
11971        close.len(),
11972        |params| {
11973            let adaptive_length =
11974                get_usize_param("adaptive_schaff_trend_cycle", params, "adaptive_length", 55)?;
11975            let stc_length =
11976                get_usize_param("adaptive_schaff_trend_cycle", params, "stc_length", 12)?;
11977            let smoothing_factor =
11978                get_f64_param("adaptive_schaff_trend_cycle", params, "smoothing_factor", 0.45)?;
11979            let fast_length =
11980                get_usize_param("adaptive_schaff_trend_cycle", params, "fast_length", 26)?;
11981            let slow_length =
11982                get_usize_param("adaptive_schaff_trend_cycle", params, "slow_length", 50)?;
11983            let input = AdaptiveSchaffTrendCycleInput::from_slices(
11984                high,
11985                low,
11986                close,
11987                AdaptiveSchaffTrendCycleParams {
11988                    adaptive_length: Some(adaptive_length),
11989                    stc_length: Some(stc_length),
11990                    smoothing_factor: Some(smoothing_factor),
11991                    fast_length: Some(fast_length),
11992                    slow_length: Some(slow_length),
11993                },
11994            );
11995            let out = adaptive_schaff_trend_cycle_with_kernel(&input, kernel).map_err(|e| {
11996                IndicatorDispatchError::ComputeFailed {
11997                    indicator: "adaptive_schaff_trend_cycle".to_string(),
11998                    details: e.to_string(),
11999                }
12000            })?;
12001            if output_id.eq_ignore_ascii_case("stc") || output_id.eq_ignore_ascii_case("value") {
12002                return Ok(out.stc);
12003            }
12004            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
12005            {
12006                return Ok(out.histogram);
12007            }
12008            Err(IndicatorDispatchError::UnknownOutput {
12009                indicator: "adaptive_schaff_trend_cycle".to_string(),
12010                output: output_id.to_string(),
12011            })
12012        },
12013    )
12014}
12015
12016fn compute_ehlers_detrending_filter_batch(
12017    req: IndicatorBatchRequest<'_>,
12018    output_id: &str,
12019) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12020    let data_len = match req.data {
12021        IndicatorDataRef::Candles { candles, source } => {
12022            source_type(candles, source.unwrap_or("hlcc4")).len()
12023        }
12024        IndicatorDataRef::Slice { values } => values.len(),
12025        _ => {
12026            return Err(IndicatorDispatchError::MissingRequiredInput {
12027                indicator: "ehlers_detrending_filter".to_string(),
12028                input: IndicatorInputKind::Slice,
12029            })
12030        }
12031    };
12032    let kernel = req.kernel.to_non_batch();
12033    collect_f64(
12034        "ehlers_detrending_filter",
12035        output_id,
12036        req.combos,
12037        data_len,
12038        |params| {
12039            let source = get_enum_param("ehlers_detrending_filter", params, "source", "hlcc4")?;
12040            let length = get_usize_param("ehlers_detrending_filter", params, "length", 10)?;
12041            let input = match req.data {
12042                IndicatorDataRef::Candles { candles, .. } => EhlersDetrendingFilterInput::from_candles(
12043                    candles,
12044                    &source,
12045                    EhlersDetrendingFilterParams {
12046                        length: Some(length),
12047                    },
12048                ),
12049                IndicatorDataRef::Slice { values } => EhlersDetrendingFilterInput::from_slice(
12050                    values,
12051                    EhlersDetrendingFilterParams {
12052                        length: Some(length),
12053                    },
12054                ),
12055                _ => unreachable!(),
12056            };
12057            let out = ehlers_detrending_filter_with_kernel(&input, kernel).map_err(|e| {
12058                IndicatorDispatchError::ComputeFailed {
12059                    indicator: "ehlers_detrending_filter".to_string(),
12060                    details: e.to_string(),
12061                }
12062            })?;
12063            if output_id.eq_ignore_ascii_case("edf") || output_id.eq_ignore_ascii_case("value") {
12064                return Ok(out.edf);
12065            }
12066            if output_id.eq_ignore_ascii_case("signal") {
12067                return Ok(out.signal);
12068            }
12069            Err(IndicatorDispatchError::UnknownOutput {
12070                indicator: "ehlers_detrending_filter".to_string(),
12071                output: output_id.to_string(),
12072            })
12073        },
12074    )
12075}
12076
12077fn compute_hypertrend_batch(
12078    req: IndicatorBatchRequest<'_>,
12079    output_id: &str,
12080) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12081    let data_len = match req.data {
12082        IndicatorDataRef::Candles { candles, source } => {
12083            source_type(candles, source.unwrap_or("close")).len()
12084        }
12085        IndicatorDataRef::Ohlc { close, .. } => close.len(),
12086        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
12087        _ => {
12088            return Err(IndicatorDispatchError::MissingRequiredInput {
12089                indicator: "hypertrend".to_string(),
12090                input: IndicatorInputKind::Ohlc,
12091            })
12092        }
12093    };
12094    let kernel = req.kernel.to_non_batch();
12095    collect_f64("hypertrend", output_id, req.combos, data_len, |params| {
12096        let source = get_enum_param("hypertrend", params, "source", "close")?;
12097        let factor = get_f64_param("hypertrend", params, "factor", 5.0)?;
12098        let slope = get_f64_param("hypertrend", params, "slope", 14.0)?;
12099        let width_percent = get_f64_param("hypertrend", params, "width_percent", 80.0)?;
12100        let input = match req.data {
12101            IndicatorDataRef::Candles { candles, .. } => HyperTrendInput::from_candles(
12102                candles,
12103                &source,
12104                HyperTrendParams {
12105                    factor: Some(factor),
12106                    slope: Some(slope),
12107                    width_percent: Some(width_percent),
12108                },
12109            ),
12110            IndicatorDataRef::Ohlc { high, low, close, open } => {
12111                ensure_same_len_4("hypertrend", open.len(), high.len(), low.len(), close.len())?;
12112                let src = match source.to_ascii_lowercase().as_str() {
12113                    "open" => open,
12114                    "high" => high,
12115                    "low" => low,
12116                    _ => close,
12117                };
12118                HyperTrendInput::from_slices(
12119                    high,
12120                    low,
12121                    src,
12122                    HyperTrendParams {
12123                        factor: Some(factor),
12124                        slope: Some(slope),
12125                        width_percent: Some(width_percent),
12126                    },
12127                )
12128            }
12129            IndicatorDataRef::Ohlcv { high, low, close, open, volume } => {
12130                ensure_same_len_5("hypertrend", open.len(), high.len(), low.len(), close.len(), volume.len())?;
12131                let src = match source.to_ascii_lowercase().as_str() {
12132                    "open" => open,
12133                    "high" => high,
12134                    "low" => low,
12135                    _ => close,
12136                };
12137                HyperTrendInput::from_slices(
12138                    high,
12139                    low,
12140                    src,
12141                    HyperTrendParams {
12142                        factor: Some(factor),
12143                        slope: Some(slope),
12144                        width_percent: Some(width_percent),
12145                    },
12146                )
12147            }
12148            _ => unreachable!(),
12149        };
12150        let out = hypertrend_with_kernel(&input, kernel).map_err(|e| {
12151            IndicatorDispatchError::ComputeFailed {
12152                indicator: "hypertrend".to_string(),
12153                details: e.to_string(),
12154            }
12155        })?;
12156        if output_id.eq_ignore_ascii_case("upper") {
12157            return Ok(out.upper);
12158        }
12159        if output_id.eq_ignore_ascii_case("average") || output_id.eq_ignore_ascii_case("value") {
12160            return Ok(out.average);
12161        }
12162        if output_id.eq_ignore_ascii_case("lower") {
12163            return Ok(out.lower);
12164        }
12165        if output_id.eq_ignore_ascii_case("trend") {
12166            return Ok(out.trend);
12167        }
12168        if output_id.eq_ignore_ascii_case("changed") {
12169            return Ok(out.changed);
12170        }
12171        Err(IndicatorDispatchError::UnknownOutput {
12172            indicator: "hypertrend".to_string(),
12173            output: output_id.to_string(),
12174        })
12175    })
12176}
12177
12178fn compute_ict_propulsion_block_batch(
12179    req: IndicatorBatchRequest<'_>,
12180    output_id: &str,
12181) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12182    let (open, high, low, close) = extract_ohlc_full_input("ict_propulsion_block", req.data)?;
12183    let kernel = req.kernel.to_non_batch();
12184    collect_f64("ict_propulsion_block", output_id, req.combos, close.len(), |params| {
12185        let swing_length = get_usize_param("ict_propulsion_block", params, "swing_length", 3)?;
12186        let mitigation_price = match get_enum_param(
12187            "ict_propulsion_block",
12188            params,
12189            "mitigation_price",
12190            "close",
12191        )?
12192        .to_ascii_lowercase()
12193        .as_str()
12194        {
12195            "close" => IctPropulsionBlockMitigationPrice::Close,
12196            "wick" => IctPropulsionBlockMitigationPrice::Wick,
12197            other => {
12198                return Err(IndicatorDispatchError::InvalidParam {
12199                    indicator: "ict_propulsion_block".to_string(),
12200                    key: "mitigation_price".to_string(),
12201                    reason: format!("unsupported value '{other}'"),
12202                })
12203            }
12204        };
12205        let input = IctPropulsionBlockInput::from_slices(
12206            open,
12207            high,
12208            low,
12209            close,
12210            IctPropulsionBlockParams {
12211                swing_length: Some(swing_length),
12212                mitigation_price: Some(mitigation_price),
12213            },
12214        );
12215        let out = ict_propulsion_block_with_kernel(&input, kernel).map_err(|e| {
12216            IndicatorDispatchError::ComputeFailed {
12217                indicator: "ict_propulsion_block".to_string(),
12218                details: e.to_string(),
12219            }
12220        })?;
12221        if output_id.eq_ignore_ascii_case("bullish_high") {
12222            return Ok(out.bullish_high);
12223        }
12224        if output_id.eq_ignore_ascii_case("bullish_low") {
12225            return Ok(out.bullish_low);
12226        }
12227        if output_id.eq_ignore_ascii_case("bullish_kind") {
12228            return Ok(out.bullish_kind);
12229        }
12230        if output_id.eq_ignore_ascii_case("bullish_active") {
12231            return Ok(out.bullish_active);
12232        }
12233        if output_id.eq_ignore_ascii_case("bullish_mitigated") {
12234            return Ok(out.bullish_mitigated);
12235        }
12236        if output_id.eq_ignore_ascii_case("bullish_new") {
12237            return Ok(out.bullish_new);
12238        }
12239        if output_id.eq_ignore_ascii_case("bearish_high") {
12240            return Ok(out.bearish_high);
12241        }
12242        if output_id.eq_ignore_ascii_case("bearish_low") {
12243            return Ok(out.bearish_low);
12244        }
12245        if output_id.eq_ignore_ascii_case("bearish_kind") {
12246            return Ok(out.bearish_kind);
12247        }
12248        if output_id.eq_ignore_ascii_case("bearish_active") {
12249            return Ok(out.bearish_active);
12250        }
12251        if output_id.eq_ignore_ascii_case("bearish_mitigated") {
12252            return Ok(out.bearish_mitigated);
12253        }
12254        if output_id.eq_ignore_ascii_case("bearish_new") {
12255            return Ok(out.bearish_new);
12256        }
12257        Err(IndicatorDispatchError::UnknownOutput {
12258            indicator: "ict_propulsion_block".to_string(),
12259            output: output_id.to_string(),
12260        })
12261    })
12262}
12263
12264fn compute_impulse_macd_batch(
12265    req: IndicatorBatchRequest<'_>,
12266    output_id: &str,
12267) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12268    let (high, low, close) = extract_ohlc_input("impulse_macd", req.data)?;
12269    let kernel = req.kernel.to_non_batch();
12270    collect_f64("impulse_macd", output_id, req.combos, close.len(), |params| {
12271        let length_ma = get_usize_param("impulse_macd", params, "length_ma", 34)?;
12272        let length_signal = get_usize_param("impulse_macd", params, "length_signal", 9)?;
12273        let input = ImpulseMacdInput::from_slices(
12274            high,
12275            low,
12276            close,
12277            ImpulseMacdParams {
12278                length_ma: Some(length_ma),
12279                length_signal: Some(length_signal),
12280            },
12281        );
12282        let out = impulse_macd_with_kernel(&input, kernel).map_err(|e| {
12283            IndicatorDispatchError::ComputeFailed {
12284                indicator: "impulse_macd".to_string(),
12285                details: e.to_string(),
12286            }
12287        })?;
12288        if output_id.eq_ignore_ascii_case("impulse_macd") || output_id.eq_ignore_ascii_case("value")
12289        {
12290            return Ok(out.impulse_macd);
12291        }
12292        if output_id.eq_ignore_ascii_case("impulse_histo")
12293            || output_id.eq_ignore_ascii_case("histogram")
12294            || output_id.eq_ignore_ascii_case("hist")
12295        {
12296            return Ok(out.impulse_histo);
12297        }
12298        if output_id.eq_ignore_ascii_case("signal") {
12299            return Ok(out.signal);
12300        }
12301        Err(IndicatorDispatchError::UnknownOutput {
12302            indicator: "impulse_macd".to_string(),
12303            output: output_id.to_string(),
12304        })
12305    })
12306}
12307
12308fn compute_keltner_channel_width_oscillator_batch(
12309    req: IndicatorBatchRequest<'_>,
12310    output_id: &str,
12311) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12312    let (high, low, close) = extract_ohlc_input("keltner_channel_width_oscillator", req.data)?;
12313    let kernel = req.kernel.to_non_batch();
12314    collect_f64(
12315        "keltner_channel_width_oscillator",
12316        output_id,
12317        req.combos,
12318        close.len(),
12319        |params| {
12320            let source =
12321                get_enum_param("keltner_channel_width_oscillator", params, "source", "close")?;
12322            let length =
12323                get_usize_param("keltner_channel_width_oscillator", params, "length", 20)?;
12324            let multiplier =
12325                get_f64_param("keltner_channel_width_oscillator", params, "multiplier", 2.0)?;
12326            let use_exponential = get_bool_param(
12327                "keltner_channel_width_oscillator",
12328                params,
12329                "use_exponential",
12330                true,
12331            )?;
12332            let bands_style = get_enum_param(
12333                "keltner_channel_width_oscillator",
12334                params,
12335                "bands_style",
12336                "Average True Range",
12337            )?;
12338            let atr_length =
12339                get_usize_param("keltner_channel_width_oscillator", params, "atr_length", 10)?;
12340            let src = match req.data {
12341                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
12342                IndicatorDataRef::Ohlc { open, high, low, close } => {
12343                    ensure_same_len_4(
12344                        "keltner_channel_width_oscillator",
12345                        open.len(),
12346                        high.len(),
12347                        low.len(),
12348                        close.len(),
12349                    )?;
12350                    match source.to_ascii_lowercase().as_str() {
12351                        "open" => open,
12352                        "high" => high,
12353                        "low" => low,
12354                        _ => close,
12355                    }
12356                }
12357                IndicatorDataRef::Ohlcv {
12358                    open,
12359                    high,
12360                    low,
12361                    close,
12362                    volume,
12363                } => {
12364                    ensure_same_len_5(
12365                        "keltner_channel_width_oscillator",
12366                        open.len(),
12367                        high.len(),
12368                        low.len(),
12369                        close.len(),
12370                        volume.len(),
12371                    )?;
12372                    match source.to_ascii_lowercase().as_str() {
12373                        "open" => open,
12374                        "high" => high,
12375                        "low" => low,
12376                        _ => close,
12377                    }
12378                }
12379                _ => close,
12380            };
12381            let input = KeltnerChannelWidthOscillatorInput::from_slices(
12382                high,
12383                low,
12384                close,
12385                src,
12386                KeltnerChannelWidthOscillatorParams {
12387                    length: Some(length),
12388                    multiplier: Some(multiplier),
12389                    use_exponential: Some(use_exponential),
12390                    bands_style: Some(bands_style),
12391                    atr_length: Some(atr_length),
12392                },
12393            );
12394            let out = keltner_channel_width_oscillator_with_kernel(&input, kernel).map_err(|e| {
12395                IndicatorDispatchError::ComputeFailed {
12396                    indicator: "keltner_channel_width_oscillator".to_string(),
12397                    details: e.to_string(),
12398                }
12399            })?;
12400            if output_id.eq_ignore_ascii_case("kbw") || output_id.eq_ignore_ascii_case("value") {
12401                return Ok(out.kbw);
12402            }
12403            if output_id.eq_ignore_ascii_case("kbw_sma") {
12404                return Ok(out.kbw_sma);
12405            }
12406            Err(IndicatorDispatchError::UnknownOutput {
12407                indicator: "keltner_channel_width_oscillator".to_string(),
12408                output: output_id.to_string(),
12409            })
12410        },
12411    )
12412}
12413
12414fn compute_leavitt_convolution_acceleration_batch(
12415    req: IndicatorBatchRequest<'_>,
12416    output_id: &str,
12417) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12418    let data_len = match req.data {
12419        IndicatorDataRef::Candles { candles, source } => {
12420            source_type(candles, source.unwrap_or("close")).len()
12421        }
12422        IndicatorDataRef::Slice { values } => values.len(),
12423        _ => {
12424            return Err(IndicatorDispatchError::MissingRequiredInput {
12425                indicator: "leavitt_convolution_acceleration".to_string(),
12426                input: IndicatorInputKind::Slice,
12427            })
12428        }
12429    };
12430    let kernel = req.kernel.to_non_batch();
12431    collect_f64(
12432        "leavitt_convolution_acceleration",
12433        output_id,
12434        req.combos,
12435        data_len,
12436        |params| {
12437            let source =
12438                get_enum_param("leavitt_convolution_acceleration", params, "source", "close")?;
12439            let length =
12440                get_usize_param("leavitt_convolution_acceleration", params, "length", 70)?;
12441            let norm_length =
12442                get_usize_param("leavitt_convolution_acceleration", params, "norm_length", 150)?;
12443            let use_norm_hyperbolic = get_bool_param(
12444                "leavitt_convolution_acceleration",
12445                params,
12446                "use_norm_hyperbolic",
12447                true,
12448            )?;
12449            let input = match req.data {
12450                IndicatorDataRef::Candles { candles, .. } => {
12451                    LeavittConvolutionAccelerationInput::from_candles(
12452                        candles,
12453                        &source,
12454                        LeavittConvolutionAccelerationParams {
12455                            length: Some(length),
12456                            norm_length: Some(norm_length),
12457                            use_norm_hyperbolic: Some(use_norm_hyperbolic),
12458                        },
12459                    )
12460                }
12461                IndicatorDataRef::Slice { values } => LeavittConvolutionAccelerationInput::from_slice(
12462                    values,
12463                    LeavittConvolutionAccelerationParams {
12464                        length: Some(length),
12465                        norm_length: Some(norm_length),
12466                        use_norm_hyperbolic: Some(use_norm_hyperbolic),
12467                    },
12468                ),
12469                _ => unreachable!(),
12470            };
12471            let out = leavitt_convolution_acceleration_with_kernel(&input, kernel).map_err(|e| {
12472                IndicatorDispatchError::ComputeFailed {
12473                    indicator: "leavitt_convolution_acceleration".to_string(),
12474                    details: e.to_string(),
12475                }
12476            })?;
12477            if output_id.eq_ignore_ascii_case("conv_acceleration")
12478                || output_id.eq_ignore_ascii_case("value")
12479            {
12480                return Ok(out.conv_acceleration);
12481            }
12482            if output_id.eq_ignore_ascii_case("signal") {
12483                return Ok(out.signal);
12484            }
12485            Err(IndicatorDispatchError::UnknownOutput {
12486                indicator: "leavitt_convolution_acceleration".to_string(),
12487                output: output_id.to_string(),
12488            })
12489        },
12490    )
12491}
12492
12493fn compute_squeeze_index_batch(
12494    req: IndicatorBatchRequest<'_>,
12495    output_id: &str,
12496) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12497    expect_value_output("squeeze_index", output_id)?;
12498    let data = extract_slice_input("squeeze_index", req.data, "close")?;
12499    let kernel = req.kernel.to_non_batch();
12500    collect_f64(
12501        "squeeze_index",
12502        output_id,
12503        req.combos,
12504        data.len(),
12505        |params| {
12506            let conv = get_f64_param("squeeze_index", params, "conv", 50.0)?;
12507            let length = get_usize_param("squeeze_index", params, "length", 20)?;
12508            let input = SqueezeIndexInput::from_slice(
12509                data,
12510                SqueezeIndexParams {
12511                    conv: Some(conv),
12512                    length: Some(length),
12513                },
12514            );
12515            let out = squeeze_index_with_kernel(&input, kernel).map_err(|e| {
12516                IndicatorDispatchError::ComputeFailed {
12517                    indicator: "squeeze_index".to_string(),
12518                    details: e.to_string(),
12519                }
12520            })?;
12521            Ok(out.values)
12522        },
12523    )
12524}
12525
12526fn compute_stochastic_distance_batch(
12527    req: IndicatorBatchRequest<'_>,
12528    output_id: &str,
12529) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12530    if !output_id.eq_ignore_ascii_case("oscillator") && !output_id.eq_ignore_ascii_case("signal") {
12531        return Err(IndicatorDispatchError::UnknownOutput {
12532            indicator: "stochastic_distance".to_string(),
12533            output: output_id.to_string(),
12534        });
12535    }
12536    let data = extract_slice_input("stochastic_distance", req.data, "close")?;
12537    let kernel = req.kernel.to_non_batch();
12538    collect_f64(
12539        "stochastic_distance",
12540        output_id,
12541        req.combos,
12542        data.len(),
12543        |params| {
12544            let lookback_length =
12545                get_usize_param("stochastic_distance", params, "lookback_length", 200)?;
12546            let length1 = get_usize_param("stochastic_distance", params, "length1", 12)?;
12547            let length2 = get_usize_param("stochastic_distance", params, "length2", 3)?;
12548            let ob_level = get_i32_param("stochastic_distance", params, "ob_level", 40)?;
12549            let os_level = get_i32_param("stochastic_distance", params, "os_level", -40)?;
12550            let input = StochasticDistanceInput::from_slice(
12551                data,
12552                StochasticDistanceParams {
12553                    lookback_length: Some(lookback_length),
12554                    length1: Some(length1),
12555                    length2: Some(length2),
12556                    ob_level: Some(ob_level),
12557                    os_level: Some(os_level),
12558                },
12559            );
12560            let out = stochastic_distance_with_kernel(&input, kernel).map_err(|e| {
12561                IndicatorDispatchError::ComputeFailed {
12562                    indicator: "stochastic_distance".to_string(),
12563                    details: e.to_string(),
12564                }
12565            })?;
12566            match output_id {
12567                "oscillator" => Ok(out.oscillator),
12568                "signal" => Ok(out.signal),
12569                _ => Err(IndicatorDispatchError::UnknownOutput {
12570                    indicator: "stochastic_distance".to_string(),
12571                    output: output_id.to_string(),
12572                }),
12573            }
12574        },
12575    )
12576}
12577
12578fn compute_vertical_horizontal_filter_batch(
12579    req: IndicatorBatchRequest<'_>,
12580    output_id: &str,
12581) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12582    expect_value_output("vertical_horizontal_filter", output_id)?;
12583    let data = extract_slice_input("vertical_horizontal_filter", req.data, "close")?;
12584    let kernel = req.kernel.to_non_batch();
12585    collect_f64(
12586        "vertical_horizontal_filter",
12587        output_id,
12588        req.combos,
12589        data.len(),
12590        |params| {
12591            let length = get_usize_param("vertical_horizontal_filter", params, "length", 28)?;
12592            let input = VerticalHorizontalFilterInput::from_slice(
12593                data,
12594                VerticalHorizontalFilterParams {
12595                    length: Some(length),
12596                },
12597            );
12598            let out = vertical_horizontal_filter_with_kernel(&input, kernel).map_err(|e| {
12599                IndicatorDispatchError::ComputeFailed {
12600                    indicator: "vertical_horizontal_filter".to_string(),
12601                    details: e.to_string(),
12602                }
12603            })?;
12604            Ok(out.values)
12605        },
12606    )
12607}
12608
12609fn compute_intraday_momentum_index_batch(
12610    req: IndicatorBatchRequest<'_>,
12611    output_id: &str,
12612) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12613    let (open, _high, _low, close) = extract_ohlc_full_input("intraday_momentum_index", req.data)?;
12614    let kernel = req.kernel.to_non_batch();
12615    collect_f64(
12616        "intraday_momentum_index",
12617        output_id,
12618        req.combos,
12619        open.len(),
12620        |params| {
12621            let length = get_usize_param("intraday_momentum_index", params, "length", 14)?;
12622            let length_ma = get_usize_param("intraday_momentum_index", params, "length_ma", 6)?;
12623            let mult = get_f64_param("intraday_momentum_index", params, "mult", 2.0)?;
12624            let length_bb = get_usize_param("intraday_momentum_index", params, "length_bb", 20)?;
12625            let apply_smoothing =
12626                get_bool_param("intraday_momentum_index", params, "apply_smoothing", false)?;
12627            let low_band = get_usize_param("intraday_momentum_index", params, "low_band", 10)?;
12628            let input = IntradayMomentumIndexInput::from_slices(
12629                open,
12630                close,
12631                IntradayMomentumIndexParams {
12632                    length: Some(length),
12633                    length_ma: Some(length_ma),
12634                    mult: Some(mult),
12635                    length_bb: Some(length_bb),
12636                    apply_smoothing: Some(apply_smoothing),
12637                    low_band: Some(low_band),
12638                },
12639            );
12640            let out = intraday_momentum_index_with_kernel(&input, kernel).map_err(|e| {
12641                IndicatorDispatchError::ComputeFailed {
12642                    indicator: "intraday_momentum_index".to_string(),
12643                    details: e.to_string(),
12644                }
12645            })?;
12646            if output_id.eq_ignore_ascii_case("imi") || output_id.eq_ignore_ascii_case("value") {
12647                return Ok(out.imi);
12648            }
12649            if output_id.eq_ignore_ascii_case("upper_hit") {
12650                return Ok(out.upper_hit);
12651            }
12652            if output_id.eq_ignore_ascii_case("lower_hit") {
12653                return Ok(out.lower_hit);
12654            }
12655            if output_id.eq_ignore_ascii_case("signal") {
12656                return Ok(out.signal);
12657            }
12658            Err(IndicatorDispatchError::UnknownOutput {
12659                indicator: "intraday_momentum_index".to_string(),
12660                output: output_id.to_string(),
12661            })
12662        },
12663    )
12664}
12665
12666fn compute_vwap_zscore_with_signals_batch(
12667    req: IndicatorBatchRequest<'_>,
12668    output_id: &str,
12669) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12670    let (close, volume) =
12671        extract_close_volume_input("vwap_zscore_with_signals", req.data, "close")?;
12672    let kernel = req.kernel.to_non_batch();
12673    collect_f64(
12674        "vwap_zscore_with_signals",
12675        output_id,
12676        req.combos,
12677        close.len(),
12678        |params| {
12679            let length = get_usize_param("vwap_zscore_with_signals", params, "length", 20)?;
12680            let upper_bottom =
12681                get_f64_param("vwap_zscore_with_signals", params, "upper_bottom", 2.5)?;
12682            let lower_bottom =
12683                get_f64_param("vwap_zscore_with_signals", params, "lower_bottom", -2.5)?;
12684            let input = VwapZscoreWithSignalsInput::from_slices(
12685                close,
12686                volume,
12687                VwapZscoreWithSignalsParams {
12688                    length: Some(length),
12689                    upper_bottom: Some(upper_bottom),
12690                    lower_bottom: Some(lower_bottom),
12691                },
12692            );
12693            let out = vwap_zscore_with_signals_with_kernel(&input, kernel).map_err(|e| {
12694                IndicatorDispatchError::ComputeFailed {
12695                    indicator: "vwap_zscore_with_signals".to_string(),
12696                    details: e.to_string(),
12697                }
12698            })?;
12699            if output_id.eq_ignore_ascii_case("zvwap") || output_id.eq_ignore_ascii_case("value") {
12700                return Ok(out.zvwap);
12701            }
12702            if output_id.eq_ignore_ascii_case("support_signal") {
12703                return Ok(out.support_signal);
12704            }
12705            if output_id.eq_ignore_ascii_case("resistance_signal") {
12706                return Ok(out.resistance_signal);
12707            }
12708            Err(IndicatorDispatchError::UnknownOutput {
12709                indicator: "vwap_zscore_with_signals".to_string(),
12710                output: output_id.to_string(),
12711            })
12712        },
12713    )
12714}
12715
12716fn compute_hema_trend_levels_batch(
12717    req: IndicatorBatchRequest<'_>,
12718    output_id: &str,
12719) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12720    let (open, high, low, close) = extract_ohlc_full_input("hema_trend_levels", req.data)?;
12721    let kernel = req.kernel.to_non_batch();
12722    collect_f64(
12723        "hema_trend_levels",
12724        output_id,
12725        req.combos,
12726        close.len(),
12727        |params| {
12728            let fast_length = get_usize_param("hema_trend_levels", params, "fast_length", 20)?;
12729            let slow_length = get_usize_param("hema_trend_levels", params, "slow_length", 40)?;
12730            let input = HemaTrendLevelsInput::from_slices(
12731                open,
12732                high,
12733                low,
12734                close,
12735                HemaTrendLevelsParams {
12736                    fast_length: Some(fast_length),
12737                    slow_length: Some(slow_length),
12738                },
12739            );
12740            let out = hema_trend_levels_with_kernel(&input, kernel).map_err(|e| {
12741                IndicatorDispatchError::ComputeFailed {
12742                    indicator: "hema_trend_levels".to_string(),
12743                    details: e.to_string(),
12744                }
12745            })?;
12746            if output_id.eq_ignore_ascii_case("fast_hema")
12747                || output_id.eq_ignore_ascii_case("value")
12748            {
12749                return Ok(out.fast_hema);
12750            }
12751            if output_id.eq_ignore_ascii_case("slow_hema") {
12752                return Ok(out.slow_hema);
12753            }
12754            if output_id.eq_ignore_ascii_case("trend_direction")
12755                || output_id.eq_ignore_ascii_case("trend")
12756            {
12757                return Ok(out.trend_direction);
12758            }
12759            if output_id.eq_ignore_ascii_case("bar_state") {
12760                return Ok(out.bar_state);
12761            }
12762            if output_id.eq_ignore_ascii_case("bullish_crossover")
12763                || output_id.eq_ignore_ascii_case("buy_signal")
12764                || output_id.eq_ignore_ascii_case("buy")
12765            {
12766                return Ok(out.bullish_crossover);
12767            }
12768            if output_id.eq_ignore_ascii_case("bearish_crossunder")
12769                || output_id.eq_ignore_ascii_case("sell_signal")
12770                || output_id.eq_ignore_ascii_case("sell")
12771            {
12772                return Ok(out.bearish_crossunder);
12773            }
12774            if output_id.eq_ignore_ascii_case("box_offset") {
12775                return Ok(out.box_offset);
12776            }
12777            if output_id.eq_ignore_ascii_case("bull_box_top") {
12778                return Ok(out.bull_box_top);
12779            }
12780            if output_id.eq_ignore_ascii_case("bull_box_bottom") {
12781                return Ok(out.bull_box_bottom);
12782            }
12783            if output_id.eq_ignore_ascii_case("bear_box_top") {
12784                return Ok(out.bear_box_top);
12785            }
12786            if output_id.eq_ignore_ascii_case("bear_box_bottom") {
12787                return Ok(out.bear_box_bottom);
12788            }
12789            if output_id.eq_ignore_ascii_case("bullish_test") {
12790                return Ok(out.bullish_test);
12791            }
12792            if output_id.eq_ignore_ascii_case("bearish_test") {
12793                return Ok(out.bearish_test);
12794            }
12795            if output_id.eq_ignore_ascii_case("bullish_test_level") {
12796                return Ok(out.bullish_test_level);
12797            }
12798            if output_id.eq_ignore_ascii_case("bearish_test_level") {
12799                return Ok(out.bearish_test_level);
12800            }
12801            Err(IndicatorDispatchError::UnknownOutput {
12802                indicator: "hema_trend_levels".to_string(),
12803                output: output_id.to_string(),
12804            })
12805        },
12806    )
12807}
12808
12809fn compute_macd_wave_signal_pro_batch(
12810    req: IndicatorBatchRequest<'_>,
12811    output_id: &str,
12812) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12813    let (open, high, low, close) = extract_ohlc_full_input("macd_wave_signal_pro", req.data)?;
12814    let kernel = req.kernel.to_non_batch();
12815    collect_f64(
12816        "macd_wave_signal_pro",
12817        output_id,
12818        req.combos,
12819        close.len(),
12820        |_params| {
12821            let input =
12822                MacdWaveSignalProInput::from_slices(open, high, low, close, Default::default());
12823            let out = macd_wave_signal_pro_with_kernel(&input, kernel).map_err(|e| {
12824                IndicatorDispatchError::ComputeFailed {
12825                    indicator: "macd_wave_signal_pro".to_string(),
12826                    details: e.to_string(),
12827                }
12828            })?;
12829            if output_id.eq_ignore_ascii_case("diff") || output_id.eq_ignore_ascii_case("value") {
12830                return Ok(out.diff);
12831            }
12832            if output_id.eq_ignore_ascii_case("dea") {
12833                return Ok(out.dea);
12834            }
12835            if output_id.eq_ignore_ascii_case("macd_histogram")
12836                || output_id.eq_ignore_ascii_case("macd")
12837                || output_id.eq_ignore_ascii_case("histogram")
12838                || output_id.eq_ignore_ascii_case("hist")
12839            {
12840                return Ok(out.macd_histogram);
12841            }
12842            if output_id.eq_ignore_ascii_case("line_convergence")
12843                || output_id.eq_ignore_ascii_case("line_conv")
12844            {
12845                return Ok(out.line_convergence);
12846            }
12847            if output_id.eq_ignore_ascii_case("buy_signal") || output_id.eq_ignore_ascii_case("buy")
12848            {
12849                return Ok(out.buy_signal);
12850            }
12851            if output_id.eq_ignore_ascii_case("sell_signal")
12852                || output_id.eq_ignore_ascii_case("sell")
12853            {
12854                return Ok(out.sell_signal);
12855            }
12856            Err(IndicatorDispatchError::UnknownOutput {
12857                indicator: "macd_wave_signal_pro".to_string(),
12858                output: output_id.to_string(),
12859            })
12860        },
12861    )
12862}
12863
12864fn compute_demand_index_batch(
12865    req: IndicatorBatchRequest<'_>,
12866    output_id: &str,
12867) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12868    let (high, low, close, volume) = extract_hlcv_input("demand_index", req.data)?;
12869    let kernel = req.kernel.to_non_batch();
12870    collect_f64(
12871        "demand_index",
12872        output_id,
12873        req.combos,
12874        high.len(),
12875        |params| {
12876            let len_bs = get_usize_param("demand_index", params, "len_bs", 19)?;
12877            let len_bs_ma = get_usize_param("demand_index", params, "len_bs_ma", 19)?;
12878            let len_di_ma = get_usize_param("demand_index", params, "len_di_ma", 19)?;
12879            let ma_type = get_enum_param("demand_index", params, "ma_type", "ema")?;
12880            let input = DemandIndexInput::from_slices(
12881                high,
12882                low,
12883                close,
12884                volume,
12885                DemandIndexParams {
12886                    len_bs: Some(len_bs),
12887                    len_bs_ma: Some(len_bs_ma),
12888                    len_di_ma: Some(len_di_ma),
12889                    ma_type: Some(ma_type),
12890                },
12891            );
12892            let out = demand_index_with_kernel(&input, kernel).map_err(|e| {
12893                IndicatorDispatchError::ComputeFailed {
12894                    indicator: "demand_index".to_string(),
12895                    details: e.to_string(),
12896                }
12897            })?;
12898            if output_id.eq_ignore_ascii_case("demand_index")
12899                || output_id.eq_ignore_ascii_case("value")
12900            {
12901                return Ok(out.demand_index);
12902            }
12903            if output_id.eq_ignore_ascii_case("signal") {
12904                return Ok(out.signal);
12905            }
12906            Err(IndicatorDispatchError::UnknownOutput {
12907                indicator: "demand_index".to_string(),
12908                output: output_id.to_string(),
12909            })
12910        },
12911    )
12912}
12913
12914fn compute_kase_peak_oscillator_with_divergences_batch(
12915    req: IndicatorBatchRequest<'_>,
12916    output_id: &str,
12917) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12918    let (high, low, close) = extract_ohlc_input("kase_peak_oscillator_with_divergences", req.data)?;
12919    let kernel = req.kernel.to_non_batch();
12920    collect_f64(
12921        "kase_peak_oscillator_with_divergences",
12922        output_id,
12923        req.combos,
12924        close.len(),
12925        |params| {
12926            let deviations = get_f64_param(
12927                "kase_peak_oscillator_with_divergences",
12928                params,
12929                "deviations",
12930                2.0,
12931            )?;
12932            let short_cycle = get_usize_param(
12933                "kase_peak_oscillator_with_divergences",
12934                params,
12935                "short_cycle",
12936                8,
12937            )?;
12938            let long_cycle = get_usize_param(
12939                "kase_peak_oscillator_with_divergences",
12940                params,
12941                "long_cycle",
12942                65,
12943            )?;
12944            let sensitivity = get_f64_param(
12945                "kase_peak_oscillator_with_divergences",
12946                params,
12947                "sensitivity",
12948                40.0,
12949            )?;
12950            let all_peaks_mode = get_bool_param(
12951                "kase_peak_oscillator_with_divergences",
12952                params,
12953                "all_peaks_mode",
12954                true,
12955            )?;
12956            let lb_r = get_usize_param("kase_peak_oscillator_with_divergences", params, "lb_r", 5)?;
12957            let lb_l = get_usize_param("kase_peak_oscillator_with_divergences", params, "lb_l", 5)?;
12958            let range_upper = get_usize_param(
12959                "kase_peak_oscillator_with_divergences",
12960                params,
12961                "range_upper",
12962                60,
12963            )?;
12964            let range_lower = get_usize_param(
12965                "kase_peak_oscillator_with_divergences",
12966                params,
12967                "range_lower",
12968                5,
12969            )?;
12970            let plot_bull = get_bool_param(
12971                "kase_peak_oscillator_with_divergences",
12972                params,
12973                "plot_bull",
12974                true,
12975            )?;
12976            let plot_hidden_bull = get_bool_param(
12977                "kase_peak_oscillator_with_divergences",
12978                params,
12979                "plot_hidden_bull",
12980                false,
12981            )?;
12982            let plot_bear = get_bool_param(
12983                "kase_peak_oscillator_with_divergences",
12984                params,
12985                "plot_bear",
12986                true,
12987            )?;
12988            let plot_hidden_bear = get_bool_param(
12989                "kase_peak_oscillator_with_divergences",
12990                params,
12991                "plot_hidden_bear",
12992                false,
12993            )?;
12994            let input = KasePeakOscillatorWithDivergencesInput::from_slices(
12995                high,
12996                low,
12997                close,
12998                KasePeakOscillatorWithDivergencesParams {
12999                    deviations: Some(deviations),
13000                    short_cycle: Some(short_cycle),
13001                    long_cycle: Some(long_cycle),
13002                    sensitivity: Some(sensitivity),
13003                    all_peaks_mode: Some(all_peaks_mode),
13004                    lb_r: Some(lb_r),
13005                    lb_l: Some(lb_l),
13006                    range_upper: Some(range_upper),
13007                    range_lower: Some(range_lower),
13008                    plot_bull: Some(plot_bull),
13009                    plot_hidden_bull: Some(plot_hidden_bull),
13010                    plot_bear: Some(plot_bear),
13011                    plot_hidden_bear: Some(plot_hidden_bear),
13012                },
13013            );
13014            let out =
13015                kase_peak_oscillator_with_divergences_with_kernel(&input, kernel).map_err(|e| {
13016                    IndicatorDispatchError::ComputeFailed {
13017                        indicator: "kase_peak_oscillator_with_divergences".to_string(),
13018                        details: e.to_string(),
13019                    }
13020                })?;
13021            if output_id.eq_ignore_ascii_case("oscillator")
13022                || output_id.eq_ignore_ascii_case("value")
13023            {
13024                return Ok(out.oscillator);
13025            }
13026            if output_id.eq_ignore_ascii_case("hist") || output_id.eq_ignore_ascii_case("histogram")
13027            {
13028                return Ok(out.histogram);
13029            }
13030            if output_id.eq_ignore_ascii_case("max_peak_value") {
13031                return Ok(out.max_peak_value);
13032            }
13033            if output_id.eq_ignore_ascii_case("min_peak_value") {
13034                return Ok(out.min_peak_value);
13035            }
13036            if output_id.eq_ignore_ascii_case("market_extreme") {
13037                return Ok(out.market_extreme);
13038            }
13039            if output_id.eq_ignore_ascii_case("regular_bullish") {
13040                return Ok(out.regular_bullish);
13041            }
13042            if output_id.eq_ignore_ascii_case("hidden_bullish") {
13043                return Ok(out.hidden_bullish);
13044            }
13045            if output_id.eq_ignore_ascii_case("regular_bearish") {
13046                return Ok(out.regular_bearish);
13047            }
13048            if output_id.eq_ignore_ascii_case("hidden_bearish") {
13049                return Ok(out.hidden_bearish);
13050            }
13051            if output_id.eq_ignore_ascii_case("go_long") {
13052                return Ok(out.go_long);
13053            }
13054            if output_id.eq_ignore_ascii_case("go_short") {
13055                return Ok(out.go_short);
13056            }
13057            Err(IndicatorDispatchError::UnknownOutput {
13058                indicator: "kase_peak_oscillator_with_divergences".to_string(),
13059                output: output_id.to_string(),
13060            })
13061        },
13062    )
13063}
13064
13065fn compute_gopalakrishnan_range_index_batch(
13066    req: IndicatorBatchRequest<'_>,
13067    output_id: &str,
13068) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13069    expect_value_output("gopalakrishnan_range_index", output_id)?;
13070    let (high, low) = extract_high_low_input("gopalakrishnan_range_index", req.data)?;
13071    let kernel = req.kernel.to_non_batch();
13072    collect_f64(
13073        "gopalakrishnan_range_index",
13074        output_id,
13075        req.combos,
13076        high.len(),
13077        |params| {
13078            let length = get_usize_param("gopalakrishnan_range_index", params, "length", 5)?;
13079            let input = GopalakrishnanRangeIndexInput::from_slices(
13080                high,
13081                low,
13082                GopalakrishnanRangeIndexParams {
13083                    length: Some(length),
13084                },
13085            );
13086            let out = gopalakrishnan_range_index_with_kernel(&input, kernel).map_err(|e| {
13087                IndicatorDispatchError::ComputeFailed {
13088                    indicator: "gopalakrishnan_range_index".to_string(),
13089                    details: e.to_string(),
13090                }
13091            })?;
13092            Ok(out.values)
13093        },
13094    )
13095}
13096
13097fn compute_acosc_batch(
13098    req: IndicatorBatchRequest<'_>,
13099    output_id: &str,
13100) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13101    let (high, low) = extract_high_low_input("acosc", req.data)?;
13102    let kernel = req.kernel.to_non_batch();
13103    collect_f64("acosc", output_id, req.combos, high.len(), |_params| {
13104        let input = AcoscInput::from_slices(high, low, AcoscParams::default());
13105        let out = acosc_with_kernel(&input, kernel).map_err(|e| {
13106            IndicatorDispatchError::ComputeFailed {
13107                indicator: "acosc".to_string(),
13108                details: e.to_string(),
13109            }
13110        })?;
13111        if output_id.eq_ignore_ascii_case("osc") || output_id.eq_ignore_ascii_case("value") {
13112            return Ok(out.osc);
13113        }
13114        if output_id.eq_ignore_ascii_case("change") {
13115            return Ok(out.change);
13116        }
13117        Err(IndicatorDispatchError::UnknownOutput {
13118            indicator: "acosc".to_string(),
13119            output: output_id.to_string(),
13120        })
13121    })
13122}
13123
13124fn compute_alligator_batch(
13125    req: IndicatorBatchRequest<'_>,
13126    output_id: &str,
13127) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13128    let data = extract_slice_input("alligator", req.data, "hl2")?;
13129    let kernel = req.kernel.to_non_batch();
13130    collect_f64("alligator", output_id, req.combos, data.len(), |params| {
13131        let jaw_period = get_usize_param("alligator", params, "jaw_period", 13)?;
13132        let jaw_offset = get_usize_param("alligator", params, "jaw_offset", 8)?;
13133        let teeth_period = get_usize_param("alligator", params, "teeth_period", 8)?;
13134        let teeth_offset = get_usize_param("alligator", params, "teeth_offset", 5)?;
13135        let lips_period = get_usize_param("alligator", params, "lips_period", 5)?;
13136        let lips_offset = get_usize_param("alligator", params, "lips_offset", 3)?;
13137        let input = AlligatorInput::from_slice(
13138            data,
13139            AlligatorParams {
13140                jaw_period: Some(jaw_period),
13141                jaw_offset: Some(jaw_offset),
13142                teeth_period: Some(teeth_period),
13143                teeth_offset: Some(teeth_offset),
13144                lips_period: Some(lips_period),
13145                lips_offset: Some(lips_offset),
13146            },
13147        );
13148        let out = alligator_with_kernel(&input, kernel).map_err(|e| {
13149            IndicatorDispatchError::ComputeFailed {
13150                indicator: "alligator".to_string(),
13151                details: e.to_string(),
13152            }
13153        })?;
13154        if output_id.eq_ignore_ascii_case("jaw") || output_id.eq_ignore_ascii_case("value") {
13155            return Ok(out.jaw);
13156        }
13157        if output_id.eq_ignore_ascii_case("teeth") {
13158            return Ok(out.teeth);
13159        }
13160        if output_id.eq_ignore_ascii_case("lips") {
13161            return Ok(out.lips);
13162        }
13163        Err(IndicatorDispatchError::UnknownOutput {
13164            indicator: "alligator".to_string(),
13165            output: output_id.to_string(),
13166        })
13167    })
13168}
13169
13170fn compute_alphatrend_batch(
13171    req: IndicatorBatchRequest<'_>,
13172    output_id: &str,
13173) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13174    let (open, high, low, close, volume) = extract_ohlcv_full_input("alphatrend", req.data)?;
13175    let kernel = req.kernel.to_non_batch();
13176    collect_f64("alphatrend", output_id, req.combos, close.len(), |params| {
13177        let coeff = get_f64_param("alphatrend", params, "coeff", 1.0)?;
13178        let period = get_usize_param("alphatrend", params, "period", 14)?;
13179        let no_volume = get_bool_param("alphatrend", params, "no_volume", false)?;
13180        let input = AlphaTrendInput::from_slices(
13181            open,
13182            high,
13183            low,
13184            close,
13185            volume,
13186            AlphaTrendParams {
13187                coeff: Some(coeff),
13188                period: Some(period),
13189                no_volume: Some(no_volume),
13190            },
13191        );
13192        let out = alphatrend_with_kernel(&input, kernel).map_err(|e| {
13193            IndicatorDispatchError::ComputeFailed {
13194                indicator: "alphatrend".to_string(),
13195                details: e.to_string(),
13196            }
13197        })?;
13198        if output_id.eq_ignore_ascii_case("k1") || output_id.eq_ignore_ascii_case("value") {
13199            return Ok(out.k1);
13200        }
13201        if output_id.eq_ignore_ascii_case("k2") {
13202            return Ok(out.k2);
13203        }
13204        Err(IndicatorDispatchError::UnknownOutput {
13205            indicator: "alphatrend".to_string(),
13206            output: output_id.to_string(),
13207        })
13208    })
13209}
13210
13211fn compute_aso_batch(
13212    req: IndicatorBatchRequest<'_>,
13213    output_id: &str,
13214) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13215    let (open, high, low, close) = match req.data {
13216        IndicatorDataRef::Candles { candles, source } => (
13217            candles.open.as_slice(),
13218            candles.high.as_slice(),
13219            candles.low.as_slice(),
13220            source_type(candles, source.unwrap_or("close")),
13221        ),
13222        IndicatorDataRef::Ohlc {
13223            open,
13224            high,
13225            low,
13226            close,
13227        } => {
13228            ensure_same_len_4("aso", open.len(), high.len(), low.len(), close.len())?;
13229            (open, high, low, close)
13230        }
13231        IndicatorDataRef::Ohlcv {
13232            open,
13233            high,
13234            low,
13235            close,
13236            volume,
13237        } => {
13238            ensure_same_len_5(
13239                "aso",
13240                open.len(),
13241                high.len(),
13242                low.len(),
13243                close.len(),
13244                volume.len(),
13245            )?;
13246            (open, high, low, close)
13247        }
13248        _ => {
13249            return Err(IndicatorDispatchError::MissingRequiredInput {
13250                indicator: "aso".to_string(),
13251                input: IndicatorInputKind::Ohlc,
13252            });
13253        }
13254    };
13255    let kernel = req.kernel.to_non_batch();
13256    collect_f64("aso", output_id, req.combos, close.len(), |params| {
13257        let period = get_usize_param("aso", params, "period", 10)?;
13258        let mode = get_usize_param("aso", params, "mode", 0)?;
13259        let input = AsoInput::from_slices(
13260            open,
13261            high,
13262            low,
13263            close,
13264            AsoParams {
13265                period: Some(period),
13266                mode: Some(mode),
13267            },
13268        );
13269        let out =
13270            aso_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
13271                indicator: "aso".to_string(),
13272                details: e.to_string(),
13273            })?;
13274        if output_id.eq_ignore_ascii_case("bulls") || output_id.eq_ignore_ascii_case("value") {
13275            return Ok(out.bulls);
13276        }
13277        if output_id.eq_ignore_ascii_case("bears") {
13278            return Ok(out.bears);
13279        }
13280        Err(IndicatorDispatchError::UnknownOutput {
13281            indicator: "aso".to_string(),
13282            output: output_id.to_string(),
13283        })
13284    })
13285}
13286
13287fn compute_avsl_batch(
13288    req: IndicatorBatchRequest<'_>,
13289    output_id: &str,
13290) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13291    expect_value_output("avsl", output_id)?;
13292    let (_high, low, close, volume) = extract_hlcv_input("avsl", req.data)?;
13293    let kernel = req.kernel.to_non_batch();
13294    collect_f64("avsl", output_id, req.combos, close.len(), |params| {
13295        let fast_period = get_usize_param("avsl", params, "fast_period", 12)?;
13296        let slow_period = get_usize_param("avsl", params, "slow_period", 26)?;
13297        let multiplier = get_f64_param("avsl", params, "multiplier", 2.0)?;
13298        let input = AvslInput::from_slices(
13299            close,
13300            low,
13301            volume,
13302            AvslParams {
13303                fast_period: Some(fast_period),
13304                slow_period: Some(slow_period),
13305                multiplier: Some(multiplier),
13306            },
13307        );
13308        let out = avsl_with_kernel(&input, kernel).map_err(|e| {
13309            IndicatorDispatchError::ComputeFailed {
13310                indicator: "avsl".to_string(),
13311                details: e.to_string(),
13312            }
13313        })?;
13314        Ok(out.values)
13315    })
13316}
13317
13318fn compute_bandpass_batch(
13319    req: IndicatorBatchRequest<'_>,
13320    output_id: &str,
13321) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13322    let data = extract_slice_input("bandpass", req.data, "close")?;
13323    let kernel = req.kernel.to_non_batch();
13324    collect_f64("bandpass", output_id, req.combos, data.len(), |params| {
13325        let period = get_usize_param("bandpass", params, "period", 20)?;
13326        let bandwidth = get_f64_param("bandpass", params, "bandwidth", 0.3)?;
13327        let input = BandPassInput::from_slice(
13328            data,
13329            BandPassParams {
13330                period: Some(period),
13331                bandwidth: Some(bandwidth),
13332            },
13333        );
13334        let out = bandpass_with_kernel(&input, kernel).map_err(|e| {
13335            IndicatorDispatchError::ComputeFailed {
13336                indicator: "bandpass".to_string(),
13337                details: e.to_string(),
13338            }
13339        })?;
13340        if output_id.eq_ignore_ascii_case("bp") || output_id.eq_ignore_ascii_case("value") {
13341            return Ok(out.bp);
13342        }
13343        if output_id.eq_ignore_ascii_case("bp_normalized")
13344            || output_id.eq_ignore_ascii_case("normalized")
13345        {
13346            return Ok(out.bp_normalized);
13347        }
13348        if output_id.eq_ignore_ascii_case("signal") {
13349            return Ok(out.signal);
13350        }
13351        if output_id.eq_ignore_ascii_case("trigger") {
13352            return Ok(out.trigger);
13353        }
13354        Err(IndicatorDispatchError::UnknownOutput {
13355            indicator: "bandpass".to_string(),
13356            output: output_id.to_string(),
13357        })
13358    })
13359}
13360
13361fn compute_chande_batch(
13362    req: IndicatorBatchRequest<'_>,
13363    output_id: &str,
13364) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13365    expect_value_output("chande", output_id)?;
13366    let (high, low, close) = extract_ohlc_input("chande", req.data)?;
13367    let kernel = req.kernel.to_non_batch();
13368    collect_f64("chande", output_id, req.combos, close.len(), |params| {
13369        let period = get_usize_param("chande", params, "period", 22)?;
13370        let mult = get_f64_param("chande", params, "mult", 3.0)?;
13371        let direction = get_enum_param("chande", params, "direction", "long")?;
13372        let input = ChandeInput::from_slices(
13373            high,
13374            low,
13375            close,
13376            ChandeParams {
13377                period: Some(period),
13378                mult: Some(mult),
13379                direction: Some(direction.to_string()),
13380            },
13381        );
13382        let out = chande_with_kernel(&input, kernel).map_err(|e| {
13383            IndicatorDispatchError::ComputeFailed {
13384                indicator: "chande".to_string(),
13385                details: e.to_string(),
13386            }
13387        })?;
13388        Ok(out.values)
13389    })
13390}
13391
13392fn compute_chandelier_exit_batch(
13393    req: IndicatorBatchRequest<'_>,
13394    output_id: &str,
13395) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13396    let (high, low, close) = extract_ohlc_input("chandelier_exit", req.data)?;
13397    let kernel = req.kernel.to_non_batch();
13398    collect_f64(
13399        "chandelier_exit",
13400        output_id,
13401        req.combos,
13402        close.len(),
13403        |params| {
13404            let period = get_usize_param("chandelier_exit", params, "period", 22)?;
13405            let mult = get_f64_param("chandelier_exit", params, "mult", 3.0)?;
13406            let use_close = get_bool_param("chandelier_exit", params, "use_close", true)?;
13407            let input = ChandelierExitInput::from_slices(
13408                high,
13409                low,
13410                close,
13411                ChandelierExitParams {
13412                    period: Some(period),
13413                    mult: Some(mult),
13414                    use_close: Some(use_close),
13415                },
13416            );
13417            let out = chandelier_exit_with_kernel(&input, kernel).map_err(|e| {
13418                IndicatorDispatchError::ComputeFailed {
13419                    indicator: "chandelier_exit".to_string(),
13420                    details: e.to_string(),
13421                }
13422            })?;
13423            if output_id.eq_ignore_ascii_case("long_stop")
13424                || output_id.eq_ignore_ascii_case("value")
13425            {
13426                return Ok(out.long_stop);
13427            }
13428            if output_id.eq_ignore_ascii_case("short_stop") {
13429                return Ok(out.short_stop);
13430            }
13431            Err(IndicatorDispatchError::UnknownOutput {
13432                indicator: "chandelier_exit".to_string(),
13433                output: output_id.to_string(),
13434            })
13435        },
13436    )
13437}
13438
13439fn compute_cksp_batch(
13440    req: IndicatorBatchRequest<'_>,
13441    output_id: &str,
13442) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13443    let (high, low, close) = extract_ohlc_input("cksp", req.data)?;
13444    let kernel = req.kernel.to_non_batch();
13445    collect_f64("cksp", output_id, req.combos, close.len(), |params| {
13446        let p = get_usize_param("cksp", params, "p", 10)?;
13447        let x = get_f64_param("cksp", params, "x", 1.0)?;
13448        let q = get_usize_param("cksp", params, "q", 9)?;
13449        let input = CkspInput::from_slices(
13450            high,
13451            low,
13452            close,
13453            CkspParams {
13454                p: Some(p),
13455                x: Some(x),
13456                q: Some(q),
13457            },
13458        );
13459        let out = cksp_with_kernel(&input, kernel).map_err(|e| {
13460            IndicatorDispatchError::ComputeFailed {
13461                indicator: "cksp".to_string(),
13462                details: e.to_string(),
13463            }
13464        })?;
13465        if output_id.eq_ignore_ascii_case("long_values")
13466            || output_id.eq_ignore_ascii_case("long")
13467            || output_id.eq_ignore_ascii_case("value")
13468        {
13469            return Ok(out.long_values);
13470        }
13471        if output_id.eq_ignore_ascii_case("short_values") || output_id.eq_ignore_ascii_case("short")
13472        {
13473            return Ok(out.short_values);
13474        }
13475        Err(IndicatorDispatchError::UnknownOutput {
13476            indicator: "cksp".to_string(),
13477            output: output_id.to_string(),
13478        })
13479    })
13480}
13481
13482fn compute_correlation_cycle_batch(
13483    req: IndicatorBatchRequest<'_>,
13484    output_id: &str,
13485) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13486    let data = extract_slice_input("correlation_cycle", req.data, "close")?;
13487    let kernel = req.kernel.to_non_batch();
13488    collect_f64(
13489        "correlation_cycle",
13490        output_id,
13491        req.combos,
13492        data.len(),
13493        |params| {
13494            let period = get_usize_param("correlation_cycle", params, "period", 20)?;
13495            let threshold = get_f64_param("correlation_cycle", params, "threshold", 9.0)?;
13496            let input = CorrelationCycleInput::from_slice(
13497                data,
13498                CorrelationCycleParams {
13499                    period: Some(period),
13500                    threshold: Some(threshold),
13501                },
13502            );
13503            let out = correlation_cycle_with_kernel(&input, kernel).map_err(|e| {
13504                IndicatorDispatchError::ComputeFailed {
13505                    indicator: "correlation_cycle".to_string(),
13506                    details: e.to_string(),
13507                }
13508            })?;
13509            if output_id.eq_ignore_ascii_case("real") || output_id.eq_ignore_ascii_case("value") {
13510                return Ok(out.real);
13511            }
13512            if output_id.eq_ignore_ascii_case("imag") {
13513                return Ok(out.imag);
13514            }
13515            if output_id.eq_ignore_ascii_case("angle") {
13516                return Ok(out.angle);
13517            }
13518            if output_id.eq_ignore_ascii_case("state") {
13519                return Ok(out.state);
13520            }
13521            Err(IndicatorDispatchError::UnknownOutput {
13522                indicator: "correlation_cycle".to_string(),
13523                output: output_id.to_string(),
13524            })
13525        },
13526    )
13527}
13528
13529fn compute_damiani_volatmeter_batch(
13530    req: IndicatorBatchRequest<'_>,
13531    output_id: &str,
13532) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13533    let data = extract_slice_input("damiani_volatmeter", req.data, "close")?;
13534    let kernel = req.kernel.to_non_batch();
13535    collect_f64(
13536        "damiani_volatmeter",
13537        output_id,
13538        req.combos,
13539        data.len(),
13540        |params| {
13541            let vis_atr = get_usize_param("damiani_volatmeter", params, "vis_atr", 13)?;
13542            let vis_std = get_usize_param("damiani_volatmeter", params, "vis_std", 20)?;
13543            let sed_atr = get_usize_param("damiani_volatmeter", params, "sed_atr", 40)?;
13544            let sed_std = get_usize_param("damiani_volatmeter", params, "sed_std", 100)?;
13545            let threshold = get_f64_param("damiani_volatmeter", params, "threshold", 1.4)?;
13546            let input = DamianiVolatmeterInput::from_slice(
13547                data,
13548                DamianiVolatmeterParams {
13549                    vis_atr: Some(vis_atr),
13550                    vis_std: Some(vis_std),
13551                    sed_atr: Some(sed_atr),
13552                    sed_std: Some(sed_std),
13553                    threshold: Some(threshold),
13554                },
13555            );
13556            let out = damiani_volatmeter_with_kernel(&input, kernel).map_err(|e| {
13557                IndicatorDispatchError::ComputeFailed {
13558                    indicator: "damiani_volatmeter".to_string(),
13559                    details: e.to_string(),
13560                }
13561            })?;
13562            if output_id.eq_ignore_ascii_case("vol") || output_id.eq_ignore_ascii_case("value") {
13563                return Ok(out.vol);
13564            }
13565            if output_id.eq_ignore_ascii_case("anti") {
13566                return Ok(out.anti);
13567            }
13568            Err(IndicatorDispatchError::UnknownOutput {
13569                indicator: "damiani_volatmeter".to_string(),
13570                output: output_id.to_string(),
13571            })
13572        },
13573    )
13574}
13575
13576fn compute_dvdiqqe_batch(
13577    req: IndicatorBatchRequest<'_>,
13578    output_id: &str,
13579) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13580    let (open, high, low, close, volume) = match req.data {
13581        IndicatorDataRef::Candles { candles, .. } => (
13582            candles.open.as_slice(),
13583            candles.high.as_slice(),
13584            candles.low.as_slice(),
13585            candles.close.as_slice(),
13586            Some(candles.volume.as_slice()),
13587        ),
13588        IndicatorDataRef::Ohlcv {
13589            open,
13590            high,
13591            low,
13592            close,
13593            volume,
13594        } => {
13595            ensure_same_len_5(
13596                "dvdiqqe",
13597                open.len(),
13598                high.len(),
13599                low.len(),
13600                close.len(),
13601                volume.len(),
13602            )?;
13603            (open, high, low, close, Some(volume))
13604        }
13605        IndicatorDataRef::Ohlc {
13606            open,
13607            high,
13608            low,
13609            close,
13610        } => {
13611            ensure_same_len_4("dvdiqqe", open.len(), high.len(), low.len(), close.len())?;
13612            (open, high, low, close, None)
13613        }
13614        _ => {
13615            return Err(IndicatorDispatchError::MissingRequiredInput {
13616                indicator: "dvdiqqe".to_string(),
13617                input: IndicatorInputKind::Ohlc,
13618            })
13619        }
13620    };
13621    let kernel = req.kernel.to_non_batch();
13622    collect_f64("dvdiqqe", output_id, req.combos, close.len(), |params| {
13623        let period = get_usize_param("dvdiqqe", params, "period", 13)?;
13624        let smoothing_period = get_usize_param("dvdiqqe", params, "smoothing_period", 6)?;
13625        let fast_multiplier = get_f64_param("dvdiqqe", params, "fast_multiplier", 2.618)?;
13626        let slow_multiplier = get_f64_param("dvdiqqe", params, "slow_multiplier", 4.236)?;
13627        let volume_type = get_enum_param("dvdiqqe", params, "volume_type", "default")?;
13628        let center_type = get_enum_param("dvdiqqe", params, "center_type", "dynamic")?;
13629        let tick_size = get_f64_param("dvdiqqe", params, "tick_size", 0.01)?;
13630        let input = DvdiqqeInput::from_slices(
13631            open,
13632            high,
13633            low,
13634            close,
13635            volume,
13636            DvdiqqeParams {
13637                period: Some(period),
13638                smoothing_period: Some(smoothing_period),
13639                fast_multiplier: Some(fast_multiplier),
13640                slow_multiplier: Some(slow_multiplier),
13641                volume_type: Some(volume_type),
13642                center_type: Some(center_type),
13643                tick_size: Some(tick_size),
13644            },
13645        );
13646        let out = dvdiqqe_with_kernel(&input, kernel).map_err(|e| {
13647            IndicatorDispatchError::ComputeFailed {
13648                indicator: "dvdiqqe".to_string(),
13649                details: e.to_string(),
13650            }
13651        })?;
13652        if output_id.eq_ignore_ascii_case("dvdi") || output_id.eq_ignore_ascii_case("value") {
13653            return Ok(out.dvdi);
13654        }
13655        if output_id.eq_ignore_ascii_case("fast_tl") || output_id.eq_ignore_ascii_case("fast") {
13656            return Ok(out.fast_tl);
13657        }
13658        if output_id.eq_ignore_ascii_case("slow_tl") || output_id.eq_ignore_ascii_case("slow") {
13659            return Ok(out.slow_tl);
13660        }
13661        if output_id.eq_ignore_ascii_case("center_line") || output_id.eq_ignore_ascii_case("center")
13662        {
13663            return Ok(out.center_line);
13664        }
13665        Err(IndicatorDispatchError::UnknownOutput {
13666            indicator: "dvdiqqe".to_string(),
13667            output: output_id.to_string(),
13668        })
13669    })
13670}
13671
13672fn compute_emd_batch(
13673    req: IndicatorBatchRequest<'_>,
13674    output_id: &str,
13675) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13676    let (high, low, close, volume) = extract_hlcv_input("emd", req.data)?;
13677    let kernel = req.kernel.to_non_batch();
13678    collect_f64("emd", output_id, req.combos, close.len(), |params| {
13679        let period = get_usize_param("emd", params, "period", 20)?;
13680        let delta = get_f64_param("emd", params, "delta", 0.5)?;
13681        let fraction = get_f64_param("emd", params, "fraction", 0.1)?;
13682        let input = EmdInput::from_slices(
13683            high,
13684            low,
13685            close,
13686            volume,
13687            EmdParams {
13688                period: Some(period),
13689                delta: Some(delta),
13690                fraction: Some(fraction),
13691            },
13692        );
13693        let out =
13694            emd_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
13695                indicator: "emd".to_string(),
13696                details: e.to_string(),
13697            })?;
13698        if output_id.eq_ignore_ascii_case("upperband")
13699            || output_id.eq_ignore_ascii_case("upper")
13700            || output_id.eq_ignore_ascii_case("value")
13701        {
13702            return Ok(out.upperband);
13703        }
13704        if output_id.eq_ignore_ascii_case("middleband") || output_id.eq_ignore_ascii_case("middle")
13705        {
13706            return Ok(out.middleband);
13707        }
13708        if output_id.eq_ignore_ascii_case("lowerband") || output_id.eq_ignore_ascii_case("lower") {
13709            return Ok(out.lowerband);
13710        }
13711        Err(IndicatorDispatchError::UnknownOutput {
13712            indicator: "emd".to_string(),
13713            output: output_id.to_string(),
13714        })
13715    })
13716}
13717
13718fn compute_emd_trend_batch(
13719    req: IndicatorBatchRequest<'_>,
13720    output_id: &str,
13721) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13722    let (open, high, low, close) = extract_ohlc_full_input("emd_trend", req.data)?;
13723    let kernel = req.kernel.to_non_batch();
13724    collect_f64("emd_trend", output_id, req.combos, close.len(), |params| {
13725        let source = get_enum_param("emd_trend", params, "source", "close")?;
13726        let avg_type = get_enum_param("emd_trend", params, "avg_type", "SMA")?;
13727        let length = get_usize_param("emd_trend", params, "length", 28)?;
13728        let mult = get_f64_param("emd_trend", params, "mult", 1.0)?;
13729        let input = EmdTrendInput::from_slices(
13730            open,
13731            high,
13732            low,
13733            close,
13734            EmdTrendParams {
13735                source: Some(source),
13736                avg_type: Some(avg_type),
13737                length: Some(length),
13738                mult: Some(mult),
13739            },
13740        );
13741        let out = emd_trend_with_kernel(&input, kernel).map_err(|e| {
13742            IndicatorDispatchError::ComputeFailed {
13743                indicator: "emd_trend".to_string(),
13744                details: e.to_string(),
13745            }
13746        })?;
13747        if output_id.eq_ignore_ascii_case("direction") {
13748            return Ok(out.direction);
13749        }
13750        if output_id.eq_ignore_ascii_case("average") || output_id.eq_ignore_ascii_case("value") {
13751            return Ok(out.average);
13752        }
13753        if output_id.eq_ignore_ascii_case("upper") {
13754            return Ok(out.upper);
13755        }
13756        if output_id.eq_ignore_ascii_case("lower") {
13757            return Ok(out.lower);
13758        }
13759        Err(IndicatorDispatchError::UnknownOutput {
13760            indicator: "emd_trend".to_string(),
13761            output: output_id.to_string(),
13762        })
13763    })
13764}
13765
13766fn compute_cyberpunk_value_trend_analyzer_batch(
13767    req: IndicatorBatchRequest<'_>,
13768    output_id: &str,
13769) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13770    let (open, high, low, close) =
13771        extract_ohlc_full_input("cyberpunk_value_trend_analyzer", req.data)?;
13772    let kernel = req.kernel.to_non_batch();
13773    collect_f64(
13774        "cyberpunk_value_trend_analyzer",
13775        output_id,
13776        req.combos,
13777        close.len(),
13778        |params| {
13779            let entry_level =
13780                get_usize_param("cyberpunk_value_trend_analyzer", params, "entry_level", 30)?;
13781            let exit_level =
13782                get_usize_param("cyberpunk_value_trend_analyzer", params, "exit_level", 75)?;
13783            let input = CyberpunkValueTrendAnalyzerInput::from_slices(
13784                open,
13785                high,
13786                low,
13787                close,
13788                CyberpunkValueTrendAnalyzerParams {
13789                    entry_level: Some(entry_level),
13790                    exit_level: Some(exit_level),
13791                },
13792            );
13793            let out = cyberpunk_value_trend_analyzer_with_kernel(&input, kernel).map_err(|e| {
13794                IndicatorDispatchError::ComputeFailed {
13795                    indicator: "cyberpunk_value_trend_analyzer".to_string(),
13796                    details: e.to_string(),
13797                }
13798            })?;
13799            if output_id.eq_ignore_ascii_case("value_trend")
13800                || output_id.eq_ignore_ascii_case("value")
13801            {
13802                return Ok(out.value_trend);
13803            }
13804            if output_id.eq_ignore_ascii_case("value_trend_lag")
13805                || output_id.eq_ignore_ascii_case("lag")
13806            {
13807                return Ok(out.value_trend_lag);
13808            }
13809            if output_id.eq_ignore_ascii_case("deviation_index") {
13810                return Ok(out.deviation_index);
13811            }
13812            if output_id.eq_ignore_ascii_case("overbought_signal")
13813                || output_id.eq_ignore_ascii_case("overbought")
13814            {
13815                return Ok(out.overbought_signal);
13816            }
13817            if output_id.eq_ignore_ascii_case("buy_signal") {
13818                return Ok(out.buy_signal);
13819            }
13820            if output_id.eq_ignore_ascii_case("sell_signal") {
13821                return Ok(out.sell_signal);
13822            }
13823            Err(IndicatorDispatchError::UnknownOutput {
13824                indicator: "cyberpunk_value_trend_analyzer".to_string(),
13825                output: output_id.to_string(),
13826            })
13827        },
13828    )
13829}
13830
13831fn compute_eri_batch(
13832    req: IndicatorBatchRequest<'_>,
13833    output_id: &str,
13834) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13835    let (high, low, source) = match req.data {
13836        IndicatorDataRef::Candles { candles, source } => (
13837            candles.high.as_slice(),
13838            candles.low.as_slice(),
13839            source_type(candles, source.unwrap_or("close")),
13840        ),
13841        IndicatorDataRef::Ohlc {
13842            open,
13843            high,
13844            low,
13845            close,
13846        } => {
13847            ensure_same_len_4("eri", open.len(), high.len(), low.len(), close.len())?;
13848            (high, low, close)
13849        }
13850        IndicatorDataRef::Ohlcv {
13851            open,
13852            high,
13853            low,
13854            close,
13855            volume,
13856        } => {
13857            ensure_same_len_5(
13858                "eri",
13859                open.len(),
13860                high.len(),
13861                low.len(),
13862                close.len(),
13863                volume.len(),
13864            )?;
13865            (high, low, close)
13866        }
13867        _ => {
13868            return Err(IndicatorDispatchError::MissingRequiredInput {
13869                indicator: "eri".to_string(),
13870                input: IndicatorInputKind::Ohlc,
13871            });
13872        }
13873    };
13874    let kernel = req.kernel.to_non_batch();
13875    collect_f64("eri", output_id, req.combos, source.len(), |params| {
13876        let period = get_usize_param("eri", params, "period", 13)?;
13877        let ma_type = get_enum_param("eri", params, "ma_type", "ema")?;
13878        let input = EriInput::from_slices(
13879            high,
13880            low,
13881            source,
13882            EriParams {
13883                period: Some(period),
13884                ma_type: Some(ma_type),
13885            },
13886        );
13887        let out =
13888            eri_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
13889                indicator: "eri".to_string(),
13890                details: e.to_string(),
13891            })?;
13892        if output_id.eq_ignore_ascii_case("bull") || output_id.eq_ignore_ascii_case("value") {
13893            return Ok(out.bull);
13894        }
13895        if output_id.eq_ignore_ascii_case("bear") {
13896            return Ok(out.bear);
13897        }
13898        Err(IndicatorDispatchError::UnknownOutput {
13899            indicator: "eri".to_string(),
13900            output: output_id.to_string(),
13901        })
13902    })
13903}
13904
13905fn compute_fisher_batch(
13906    req: IndicatorBatchRequest<'_>,
13907    output_id: &str,
13908) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13909    let (high, low) = extract_high_low_input("fisher", req.data)?;
13910    let kernel = req.kernel.to_non_batch();
13911    collect_f64("fisher", output_id, req.combos, high.len(), |params| {
13912        let period = get_usize_param("fisher", params, "period", 9)?;
13913        let input = FisherInput::from_slices(
13914            high,
13915            low,
13916            FisherParams {
13917                period: Some(period),
13918            },
13919        );
13920        let out = fisher_with_kernel(&input, kernel).map_err(|e| {
13921            IndicatorDispatchError::ComputeFailed {
13922                indicator: "fisher".to_string(),
13923                details: e.to_string(),
13924            }
13925        })?;
13926        if output_id.eq_ignore_ascii_case("fisher") || output_id.eq_ignore_ascii_case("value") {
13927            return Ok(out.fisher);
13928        }
13929        if output_id.eq_ignore_ascii_case("signal") {
13930            return Ok(out.signal);
13931        }
13932        Err(IndicatorDispatchError::UnknownOutput {
13933            indicator: "fisher".to_string(),
13934            output: output_id.to_string(),
13935        })
13936    })
13937}
13938
13939fn compute_fvg_positioning_average_batch(
13940    req: IndicatorBatchRequest<'_>,
13941    output_id: &str,
13942) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13943    let (open, high, low, close) = extract_ohlc_full_input("fvg_positioning_average", req.data)?;
13944    let kernel = req.kernel.to_non_batch();
13945    collect_f64(
13946        "fvg_positioning_average",
13947        output_id,
13948        req.combos,
13949        close.len(),
13950        |params| {
13951            let lookback = get_usize_param("fvg_positioning_average", params, "lookback", 30)?;
13952            let lookback_type = get_enum_param(
13953                "fvg_positioning_average",
13954                params,
13955                "lookback_type",
13956                "Bar Count",
13957            )?;
13958            let atr_multiplier =
13959                get_f64_param("fvg_positioning_average", params, "atr_multiplier", 0.25)?;
13960            let input = FvgPositioningAverageInput::from_slices(
13961                open,
13962                high,
13963                low,
13964                close,
13965                FvgPositioningAverageParams {
13966                    lookback: Some(lookback),
13967                    lookback_type: Some(lookback_type),
13968                    atr_multiplier: Some(atr_multiplier),
13969                },
13970            );
13971            let out = fvg_positioning_average_with_kernel(&input, kernel).map_err(|e| {
13972                IndicatorDispatchError::ComputeFailed {
13973                    indicator: "fvg_positioning_average".to_string(),
13974                    details: e.to_string(),
13975                }
13976            })?;
13977            if output_id.eq_ignore_ascii_case("bull_average")
13978                || output_id.eq_ignore_ascii_case("value")
13979            {
13980                return Ok(out.bull_average);
13981            }
13982            if output_id.eq_ignore_ascii_case("bear_average") {
13983                return Ok(out.bear_average);
13984            }
13985            if output_id.eq_ignore_ascii_case("bull_mid") {
13986                return Ok(out.bull_mid);
13987            }
13988            if output_id.eq_ignore_ascii_case("bear_mid") {
13989                return Ok(out.bear_mid);
13990            }
13991            Err(IndicatorDispatchError::UnknownOutput {
13992                indicator: "fvg_positioning_average".to_string(),
13993                output: output_id.to_string(),
13994            })
13995        },
13996    )
13997}
13998
13999fn compute_fvg_trailing_stop_batch(
14000    req: IndicatorBatchRequest<'_>,
14001    output_id: &str,
14002) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14003    let (high, low, close) = extract_ohlc_input("fvg_trailing_stop", req.data)?;
14004    let kernel = req.kernel.to_non_batch();
14005    collect_f64(
14006        "fvg_trailing_stop",
14007        output_id,
14008        req.combos,
14009        close.len(),
14010        |params| {
14011            let lookback =
14012                get_usize_param("fvg_trailing_stop", params, "unmitigated_fvg_lookback", 5)?;
14013            let smoothing_length =
14014                get_usize_param("fvg_trailing_stop", params, "smoothing_length", 9)?;
14015            let reset_on_cross =
14016                get_bool_param("fvg_trailing_stop", params, "reset_on_cross", false)?;
14017            let input = FvgTrailingStopInput::from_slices(
14018                high,
14019                low,
14020                close,
14021                FvgTrailingStopParams {
14022                    unmitigated_fvg_lookback: Some(lookback),
14023                    smoothing_length: Some(smoothing_length),
14024                    reset_on_cross: Some(reset_on_cross),
14025                },
14026            );
14027            let out = fvg_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
14028                IndicatorDispatchError::ComputeFailed {
14029                    indicator: "fvg_trailing_stop".to_string(),
14030                    details: e.to_string(),
14031                }
14032            })?;
14033            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
14034                return Ok(out.upper);
14035            }
14036            if output_id.eq_ignore_ascii_case("lower") {
14037                return Ok(out.lower);
14038            }
14039            if output_id.eq_ignore_ascii_case("upper_ts") {
14040                return Ok(out.upper_ts);
14041            }
14042            if output_id.eq_ignore_ascii_case("lower_ts") {
14043                return Ok(out.lower_ts);
14044            }
14045            Err(IndicatorDispatchError::UnknownOutput {
14046                indicator: "fvg_trailing_stop".to_string(),
14047                output: output_id.to_string(),
14048            })
14049        },
14050    )
14051}
14052
14053fn compute_gatorosc_batch(
14054    req: IndicatorBatchRequest<'_>,
14055    output_id: &str,
14056) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14057    let data = extract_slice_input("gatorosc", req.data, "close")?;
14058    let kernel = req.kernel.to_non_batch();
14059    collect_f64("gatorosc", output_id, req.combos, data.len(), |params| {
14060        let jaws_length = get_usize_param("gatorosc", params, "jaws_length", 13)?;
14061        let jaws_shift = get_usize_param("gatorosc", params, "jaws_shift", 8)?;
14062        let teeth_length = get_usize_param("gatorosc", params, "teeth_length", 8)?;
14063        let teeth_shift = get_usize_param("gatorosc", params, "teeth_shift", 5)?;
14064        let lips_length = get_usize_param("gatorosc", params, "lips_length", 5)?;
14065        let lips_shift = get_usize_param("gatorosc", params, "lips_shift", 3)?;
14066        let input = GatorOscInput::from_slice(
14067            data,
14068            GatorOscParams {
14069                jaws_length: Some(jaws_length),
14070                jaws_shift: Some(jaws_shift),
14071                teeth_length: Some(teeth_length),
14072                teeth_shift: Some(teeth_shift),
14073                lips_length: Some(lips_length),
14074                lips_shift: Some(lips_shift),
14075            },
14076        );
14077        let out = gatorosc_with_kernel(&input, kernel).map_err(|e| {
14078            IndicatorDispatchError::ComputeFailed {
14079                indicator: "gatorosc".to_string(),
14080                details: e.to_string(),
14081            }
14082        })?;
14083        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
14084            return Ok(out.upper);
14085        }
14086        if output_id.eq_ignore_ascii_case("lower") {
14087            return Ok(out.lower);
14088        }
14089        if output_id.eq_ignore_ascii_case("upper_change") {
14090            return Ok(out.upper_change);
14091        }
14092        if output_id.eq_ignore_ascii_case("lower_change") {
14093            return Ok(out.lower_change);
14094        }
14095        Err(IndicatorDispatchError::UnknownOutput {
14096            indicator: "gatorosc".to_string(),
14097            output: output_id.to_string(),
14098        })
14099    })
14100}
14101
14102fn compute_halftrend_batch(
14103    req: IndicatorBatchRequest<'_>,
14104    output_id: &str,
14105) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14106    let (high, low, close) = extract_ohlc_input("halftrend", req.data)?;
14107    let kernel = req.kernel.to_non_batch();
14108    collect_f64("halftrend", output_id, req.combos, close.len(), |params| {
14109        let amplitude = get_usize_param("halftrend", params, "amplitude", 2)?;
14110        let channel_deviation = get_f64_param("halftrend", params, "channel_deviation", 2.0)?;
14111        let atr_period = get_usize_param("halftrend", params, "atr_period", 100)?;
14112        let input = HalfTrendInput::from_slices(
14113            high,
14114            low,
14115            close,
14116            HalfTrendParams {
14117                amplitude: Some(amplitude),
14118                channel_deviation: Some(channel_deviation),
14119                atr_period: Some(atr_period),
14120            },
14121        );
14122        let out = halftrend_with_kernel(&input, kernel).map_err(|e| {
14123            IndicatorDispatchError::ComputeFailed {
14124                indicator: "halftrend".to_string(),
14125                details: e.to_string(),
14126            }
14127        })?;
14128        if output_id.eq_ignore_ascii_case("halftrend") || output_id.eq_ignore_ascii_case("value") {
14129            return Ok(out.halftrend);
14130        }
14131        if output_id.eq_ignore_ascii_case("trend") {
14132            return Ok(out.trend);
14133        }
14134        if output_id.eq_ignore_ascii_case("atr_high") {
14135            return Ok(out.atr_high);
14136        }
14137        if output_id.eq_ignore_ascii_case("atr_low") {
14138            return Ok(out.atr_low);
14139        }
14140        if output_id.eq_ignore_ascii_case("buy_signal") || output_id.eq_ignore_ascii_case("buy") {
14141            return Ok(out.buy_signal);
14142        }
14143        if output_id.eq_ignore_ascii_case("sell_signal") || output_id.eq_ignore_ascii_case("sell") {
14144            return Ok(out.sell_signal);
14145        }
14146        Err(IndicatorDispatchError::UnknownOutput {
14147            indicator: "halftrend".to_string(),
14148            output: output_id.to_string(),
14149        })
14150    })
14151}
14152
14153fn compute_safezonestop_batch(
14154    req: IndicatorBatchRequest<'_>,
14155    output_id: &str,
14156) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14157    let (high, low) = extract_high_low_input("safezonestop", req.data)?;
14158    let kernel = req.kernel.to_non_batch();
14159    collect_f64(
14160        "safezonestop",
14161        output_id,
14162        req.combos,
14163        high.len(),
14164        |params| {
14165            let period = get_usize_param("safezonestop", params, "period", 22)?;
14166            let mult = get_f64_param("safezonestop", params, "mult", 2.5)?;
14167            let max_lookback = get_usize_param("safezonestop", params, "max_lookback", 3)?;
14168            let direction = get_enum_param("safezonestop", params, "direction", "long")?;
14169            let input = SafeZoneStopInput::from_slices(
14170                high,
14171                low,
14172                direction.as_str(),
14173                SafeZoneStopParams {
14174                    period: Some(period),
14175                    mult: Some(mult),
14176                    max_lookback: Some(max_lookback),
14177                },
14178            );
14179            let out = safezonestop_with_kernel(&input, kernel).map_err(|e| {
14180                IndicatorDispatchError::ComputeFailed {
14181                    indicator: "safezonestop".to_string(),
14182                    details: e.to_string(),
14183                }
14184            })?;
14185            if output_id.eq_ignore_ascii_case("value") {
14186                return Ok(out.values);
14187            }
14188            Err(IndicatorDispatchError::UnknownOutput {
14189                indicator: "safezonestop".to_string(),
14190                output: output_id.to_string(),
14191            })
14192        },
14193    )
14194}
14195
14196fn compute_devstop_batch(
14197    req: IndicatorBatchRequest<'_>,
14198    output_id: &str,
14199) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14200    let (high, low) = extract_high_low_input("devstop", req.data)?;
14201    let kernel = req.kernel.to_non_batch();
14202    collect_f64("devstop", output_id, req.combos, high.len(), |params| {
14203        let period = get_usize_param("devstop", params, "period", 20)?;
14204        let mult = get_f64_param("devstop", params, "mult", 0.0)?;
14205        let devtype = get_usize_param("devstop", params, "devtype", 0)?;
14206        let direction = get_enum_param("devstop", params, "direction", "long")?;
14207        let ma_type = get_enum_param("devstop", params, "ma_type", "sma")?;
14208        let input = DevStopInput::from_slices(
14209            high,
14210            low,
14211            DevStopParams {
14212                period: Some(period),
14213                mult: Some(mult),
14214                devtype: Some(devtype),
14215                direction: Some(direction),
14216                ma_type: Some(ma_type),
14217            },
14218        );
14219        let out = devstop_with_kernel(&input, kernel).map_err(|e| {
14220            IndicatorDispatchError::ComputeFailed {
14221                indicator: "devstop".to_string(),
14222                details: e.to_string(),
14223            }
14224        })?;
14225        if output_id.eq_ignore_ascii_case("value") {
14226            return Ok(out.values);
14227        }
14228        Err(IndicatorDispatchError::UnknownOutput {
14229            indicator: "devstop".to_string(),
14230            output: output_id.to_string(),
14231        })
14232    })
14233}
14234
14235fn compute_chop_batch(
14236    req: IndicatorBatchRequest<'_>,
14237    output_id: &str,
14238) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14239    let (high, low, close) = extract_ohlc_input("chop", req.data)?;
14240    let kernel = req.kernel.to_non_batch();
14241    collect_f64("chop", output_id, req.combos, close.len(), |params| {
14242        let period = get_usize_param("chop", params, "period", 14)?;
14243        let scalar = get_f64_param("chop", params, "scalar", 100.0)?;
14244        let drift = get_usize_param("chop", params, "drift", 1)?;
14245        let input = ChopInput::from_slices(
14246            high,
14247            low,
14248            close,
14249            ChopParams {
14250                period: Some(period),
14251                scalar: Some(scalar),
14252                drift: Some(drift),
14253            },
14254        );
14255        let out = chop_with_kernel(&input, kernel).map_err(|e| {
14256            IndicatorDispatchError::ComputeFailed {
14257                indicator: "chop".to_string(),
14258                details: e.to_string(),
14259            }
14260        })?;
14261        if output_id.eq_ignore_ascii_case("value") {
14262            return Ok(out.values);
14263        }
14264        Err(IndicatorDispatchError::UnknownOutput {
14265            indicator: "chop".to_string(),
14266            output: output_id.to_string(),
14267        })
14268    })
14269}
14270
14271fn compute_kst_batch(
14272    req: IndicatorBatchRequest<'_>,
14273    output_id: &str,
14274) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14275    let data = extract_slice_input("kst", req.data, "close")?;
14276    let kernel = req.kernel.to_non_batch();
14277    collect_f64("kst", output_id, req.combos, data.len(), |params| {
14278        let sma_period1 = get_usize_param("kst", params, "sma_period1", 10)?;
14279        let sma_period2 = get_usize_param("kst", params, "sma_period2", 10)?;
14280        let sma_period3 = get_usize_param("kst", params, "sma_period3", 10)?;
14281        let sma_period4 = get_usize_param("kst", params, "sma_period4", 15)?;
14282        let roc_period1 = get_usize_param("kst", params, "roc_period1", 10)?;
14283        let roc_period2 = get_usize_param("kst", params, "roc_period2", 15)?;
14284        let roc_period3 = get_usize_param("kst", params, "roc_period3", 20)?;
14285        let roc_period4 = get_usize_param("kst", params, "roc_period4", 30)?;
14286        let signal_period = get_usize_param("kst", params, "signal_period", 9)?;
14287        let input = KstInput::from_slice(
14288            data,
14289            KstParams {
14290                sma_period1: Some(sma_period1),
14291                sma_period2: Some(sma_period2),
14292                sma_period3: Some(sma_period3),
14293                sma_period4: Some(sma_period4),
14294                roc_period1: Some(roc_period1),
14295                roc_period2: Some(roc_period2),
14296                roc_period3: Some(roc_period3),
14297                roc_period4: Some(roc_period4),
14298                signal_period: Some(signal_period),
14299            },
14300        );
14301        let out =
14302            kst_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14303                indicator: "kst".to_string(),
14304                details: e.to_string(),
14305            })?;
14306        if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
14307            return Ok(out.line);
14308        }
14309        if output_id.eq_ignore_ascii_case("signal") {
14310            return Ok(out.signal);
14311        }
14312        Err(IndicatorDispatchError::UnknownOutput {
14313            indicator: "kst".to_string(),
14314            output: output_id.to_string(),
14315        })
14316    })
14317}
14318
14319fn compute_kaufmanstop_batch(
14320    req: IndicatorBatchRequest<'_>,
14321    output_id: &str,
14322) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14323    expect_value_output("kaufmanstop", output_id)?;
14324    let (high, low) = extract_high_low_input("kaufmanstop", req.data)?;
14325    let kernel = req.kernel.to_non_batch();
14326    collect_f64("kaufmanstop", output_id, req.combos, high.len(), |params| {
14327        let period = get_usize_param("kaufmanstop", params, "period", 22)?;
14328        let mult = get_f64_param("kaufmanstop", params, "mult", 2.0)?;
14329        let direction = get_enum_param("kaufmanstop", params, "direction", "long")?;
14330        let ma_type = get_enum_param("kaufmanstop", params, "ma_type", "sma")?;
14331        let input = KaufmanstopInput::from_slices(
14332            high,
14333            low,
14334            KaufmanstopParams {
14335                period: Some(period),
14336                mult: Some(mult),
14337                direction: Some(direction),
14338                ma_type: Some(ma_type),
14339            },
14340        );
14341        let out = kaufmanstop_with_kernel(&input, kernel).map_err(|e| {
14342            IndicatorDispatchError::ComputeFailed {
14343                indicator: "kaufmanstop".to_string(),
14344                details: e.to_string(),
14345            }
14346        })?;
14347        Ok(out.values)
14348    })
14349}
14350
14351fn compute_lpc_batch(
14352    req: IndicatorBatchRequest<'_>,
14353    output_id: &str,
14354) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14355    let (high, low, close, src) = match req.data {
14356        IndicatorDataRef::Candles { candles, source } => (
14357            candles.high.as_slice(),
14358            candles.low.as_slice(),
14359            candles.close.as_slice(),
14360            source_type(candles, source.unwrap_or("close")),
14361        ),
14362        IndicatorDataRef::Ohlc {
14363            open,
14364            high,
14365            low,
14366            close,
14367        } => {
14368            ensure_same_len_4("lpc", open.len(), high.len(), low.len(), close.len())?;
14369            (high, low, close, close)
14370        }
14371        IndicatorDataRef::Ohlcv {
14372            open,
14373            high,
14374            low,
14375            close,
14376            volume,
14377        } => {
14378            ensure_same_len_5(
14379                "lpc",
14380                open.len(),
14381                high.len(),
14382                low.len(),
14383                close.len(),
14384                volume.len(),
14385            )?;
14386            (high, low, close, close)
14387        }
14388        _ => {
14389            return Err(IndicatorDispatchError::MissingRequiredInput {
14390                indicator: "lpc".to_string(),
14391                input: IndicatorInputKind::Ohlc,
14392            });
14393        }
14394    };
14395    let kernel = req.kernel.to_non_batch();
14396    collect_f64("lpc", output_id, req.combos, src.len(), |params| {
14397        let cutoff_type = get_enum_param("lpc", params, "cutoff_type", "adaptive")?;
14398        let fixed_period = get_usize_param("lpc", params, "fixed_period", 20)?;
14399        let max_cycle_limit = get_usize_param("lpc", params, "max_cycle_limit", 60)?;
14400        let cycle_mult = get_f64_param("lpc", params, "cycle_mult", 1.0)?;
14401        let tr_mult = get_f64_param("lpc", params, "tr_mult", 1.0)?;
14402        let input = LpcInput::from_slices(
14403            high,
14404            low,
14405            close,
14406            src,
14407            LpcParams {
14408                cutoff_type: Some(cutoff_type),
14409                fixed_period: Some(fixed_period),
14410                max_cycle_limit: Some(max_cycle_limit),
14411                cycle_mult: Some(cycle_mult),
14412                tr_mult: Some(tr_mult),
14413            },
14414        );
14415        let out =
14416            lpc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14417                indicator: "lpc".to_string(),
14418                details: e.to_string(),
14419            })?;
14420        if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
14421            return Ok(out.filter);
14422        }
14423        if output_id.eq_ignore_ascii_case("high_band") || output_id.eq_ignore_ascii_case("high") {
14424            return Ok(out.high_band);
14425        }
14426        if output_id.eq_ignore_ascii_case("low_band") || output_id.eq_ignore_ascii_case("low") {
14427            return Ok(out.low_band);
14428        }
14429        Err(IndicatorDispatchError::UnknownOutput {
14430            indicator: "lpc".to_string(),
14431            output: output_id.to_string(),
14432        })
14433    })
14434}
14435
14436fn compute_mab_batch(
14437    req: IndicatorBatchRequest<'_>,
14438    output_id: &str,
14439) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14440    let data = extract_slice_input("mab", req.data, "close")?;
14441    let kernel = req.kernel.to_non_batch();
14442    collect_f64("mab", output_id, req.combos, data.len(), |params| {
14443        let fast_period = get_usize_param("mab", params, "fast_period", 10)?;
14444        let slow_period = get_usize_param("mab", params, "slow_period", 50)?;
14445        let devup = get_f64_param("mab", params, "devup", 1.0)?;
14446        let devdn = get_f64_param("mab", params, "devdn", 1.0)?;
14447        let fast_ma_type = get_enum_param("mab", params, "fast_ma_type", "sma")?;
14448        let slow_ma_type = get_enum_param("mab", params, "slow_ma_type", "sma")?;
14449        let input = MabInput::from_slice(
14450            data,
14451            MabParams {
14452                fast_period: Some(fast_period),
14453                slow_period: Some(slow_period),
14454                devup: Some(devup),
14455                devdn: Some(devdn),
14456                fast_ma_type: Some(fast_ma_type),
14457                slow_ma_type: Some(slow_ma_type),
14458            },
14459        );
14460        let out =
14461            mab_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14462                indicator: "mab".to_string(),
14463                details: e.to_string(),
14464            })?;
14465        if output_id.eq_ignore_ascii_case("upperband")
14466            || output_id.eq_ignore_ascii_case("upper")
14467            || output_id.eq_ignore_ascii_case("value")
14468        {
14469            return Ok(out.upperband);
14470        }
14471        if output_id.eq_ignore_ascii_case("middleband") || output_id.eq_ignore_ascii_case("middle")
14472        {
14473            return Ok(out.middleband);
14474        }
14475        if output_id.eq_ignore_ascii_case("lowerband") || output_id.eq_ignore_ascii_case("lower") {
14476            return Ok(out.lowerband);
14477        }
14478        Err(IndicatorDispatchError::UnknownOutput {
14479            indicator: "mab".to_string(),
14480            output: output_id.to_string(),
14481        })
14482    })
14483}
14484
14485fn compute_macz_batch(
14486    req: IndicatorBatchRequest<'_>,
14487    output_id: &str,
14488) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14489    let (data, volume) = match req.data {
14490        IndicatorDataRef::Slice { values } => (values, None),
14491        IndicatorDataRef::Candles { candles, source } => (
14492            source_type(candles, source.unwrap_or("close")),
14493            Some(candles.volume.as_slice()),
14494        ),
14495        IndicatorDataRef::CloseVolume { close, volume } => {
14496            ensure_same_len_2("macz", close.len(), volume.len())?;
14497            (close, Some(volume))
14498        }
14499        IndicatorDataRef::Ohlc {
14500            open,
14501            high,
14502            low,
14503            close,
14504        } => {
14505            ensure_same_len_4("macz", open.len(), high.len(), low.len(), close.len())?;
14506            (close, None)
14507        }
14508        IndicatorDataRef::Ohlcv {
14509            open,
14510            high,
14511            low,
14512            close,
14513            volume,
14514        } => {
14515            ensure_same_len_5(
14516                "macz",
14517                open.len(),
14518                high.len(),
14519                low.len(),
14520                close.len(),
14521                volume.len(),
14522            )?;
14523            (close, Some(volume))
14524        }
14525        IndicatorDataRef::HighLow { .. } => {
14526            return Err(IndicatorDispatchError::MissingRequiredInput {
14527                indicator: "macz".to_string(),
14528                input: IndicatorInputKind::Slice,
14529            })
14530        }
14531    };
14532    let kernel = req.kernel.to_non_batch();
14533    collect_f64("macz", output_id, req.combos, data.len(), |params| {
14534        let fast_length = get_usize_param("macz", params, "fast_length", 12)?;
14535        let slow_length = get_usize_param("macz", params, "slow_length", 25)?;
14536        let signal_length = get_usize_param("macz", params, "signal_length", 9)?;
14537        let lengthz = get_usize_param("macz", params, "lengthz", 20)?;
14538        let length_stdev = get_usize_param("macz", params, "length_stdev", 25)?;
14539        let a = get_f64_param("macz", params, "a", 1.0)?;
14540        let b = get_f64_param("macz", params, "b", 1.0)?;
14541        let use_lag = get_bool_param("macz", params, "use_lag", false)?;
14542        let gamma = get_f64_param("macz", params, "gamma", 0.02)?;
14543        let macz_params = MaczParams {
14544            fast_length: Some(fast_length),
14545            slow_length: Some(slow_length),
14546            signal_length: Some(signal_length),
14547            lengthz: Some(lengthz),
14548            length_stdev: Some(length_stdev),
14549            a: Some(a),
14550            b: Some(b),
14551            use_lag: Some(use_lag),
14552            gamma: Some(gamma),
14553        };
14554        let input = if let Some(vol) = volume {
14555            MaczInput::from_slice_with_volume(data, vol, macz_params)
14556        } else {
14557            MaczInput::from_slice(data, macz_params)
14558        };
14559        let out = macz_with_kernel(&input, kernel).map_err(|e| {
14560            IndicatorDispatchError::ComputeFailed {
14561                indicator: "macz".to_string(),
14562                details: e.to_string(),
14563            }
14564        })?;
14565        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
14566            return Ok(out.values);
14567        }
14568        Err(IndicatorDispatchError::UnknownOutput {
14569            indicator: "macz".to_string(),
14570            output: output_id.to_string(),
14571        })
14572    })
14573}
14574
14575fn compute_minmax_batch(
14576    req: IndicatorBatchRequest<'_>,
14577    output_id: &str,
14578) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14579    let (high, low) = extract_high_low_input("minmax", req.data)?;
14580    let kernel = req.kernel.to_non_batch();
14581    collect_f64("minmax", output_id, req.combos, high.len(), |params| {
14582        let order = get_usize_param("minmax", params, "order", 3)?;
14583        let input = MinmaxInput::from_slices(high, low, MinmaxParams { order: Some(order) });
14584        let out = minmax_with_kernel(&input, kernel).map_err(|e| {
14585            IndicatorDispatchError::ComputeFailed {
14586                indicator: "minmax".to_string(),
14587                details: e.to_string(),
14588            }
14589        })?;
14590        if output_id.eq_ignore_ascii_case("is_min") || output_id.eq_ignore_ascii_case("value") {
14591            return Ok(out.is_min);
14592        }
14593        if output_id.eq_ignore_ascii_case("is_max") {
14594            return Ok(out.is_max);
14595        }
14596        if output_id.eq_ignore_ascii_case("last_min") {
14597            return Ok(out.last_min);
14598        }
14599        if output_id.eq_ignore_ascii_case("last_max") {
14600            return Ok(out.last_max);
14601        }
14602        Err(IndicatorDispatchError::UnknownOutput {
14603            indicator: "minmax".to_string(),
14604            output: output_id.to_string(),
14605        })
14606    })
14607}
14608
14609fn compute_mod_god_mode_batch(
14610    req: IndicatorBatchRequest<'_>,
14611    output_id: &str,
14612) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14613    let (high, low, close, volume) = match req.data {
14614        IndicatorDataRef::Candles { candles, .. } => (
14615            candles.high.as_slice(),
14616            candles.low.as_slice(),
14617            candles.close.as_slice(),
14618            Some(candles.volume.as_slice()),
14619        ),
14620        IndicatorDataRef::Ohlc {
14621            open,
14622            high,
14623            low,
14624            close,
14625        } => {
14626            ensure_same_len_4(
14627                "mod_god_mode",
14628                open.len(),
14629                high.len(),
14630                low.len(),
14631                close.len(),
14632            )?;
14633            (high, low, close, None)
14634        }
14635        IndicatorDataRef::Ohlcv {
14636            open,
14637            high,
14638            low,
14639            close,
14640            volume,
14641        } => {
14642            ensure_same_len_5(
14643                "mod_god_mode",
14644                open.len(),
14645                high.len(),
14646                low.len(),
14647                close.len(),
14648                volume.len(),
14649            )?;
14650            (high, low, close, Some(volume))
14651        }
14652        _ => {
14653            return Err(IndicatorDispatchError::MissingRequiredInput {
14654                indicator: "mod_god_mode".to_string(),
14655                input: IndicatorInputKind::Ohlc,
14656            });
14657        }
14658    };
14659
14660    collect_f64(
14661        "mod_god_mode",
14662        output_id,
14663        req.combos,
14664        close.len(),
14665        |params| {
14666            let n1 = get_usize_param("mod_god_mode", params, "n1", 17)?;
14667            let n2 = get_usize_param("mod_god_mode", params, "n2", 6)?;
14668            let n3 = get_usize_param("mod_god_mode", params, "n3", 4)?;
14669            let mode = get_enum_param("mod_god_mode", params, "mode", "tradition_mg")?;
14670            let use_volume = get_bool_param("mod_god_mode", params, "use_volume", true)?;
14671            let mode = match mode.as_str() {
14672                "godmode" => ModGodModeMode::Godmode,
14673                "tradition" => ModGodModeMode::Tradition,
14674                "godmode_mg" => ModGodModeMode::GodmodeMg,
14675                "tradition_mg" => ModGodModeMode::TraditionMg,
14676                other => {
14677                    return Err(IndicatorDispatchError::InvalidParam {
14678                        indicator: "mod_god_mode".to_string(),
14679                        key: "mode".to_string(),
14680                        reason: format!("unknown mode: {other}"),
14681                    });
14682                }
14683            };
14684            let input = ModGodModeInput {
14685                data: ModGodModeData::Slices {
14686                    high,
14687                    low,
14688                    close,
14689                    volume: if use_volume { volume } else { None },
14690                },
14691                params: ModGodModeParams {
14692                    n1: Some(n1),
14693                    n2: Some(n2),
14694                    n3: Some(n3),
14695                    mode: Some(mode),
14696                    use_volume: Some(use_volume),
14697                },
14698            };
14699            let out = mod_god_mode(&input).map_err(|e| IndicatorDispatchError::ComputeFailed {
14700                indicator: "mod_god_mode".to_string(),
14701                details: e.to_string(),
14702            })?;
14703            if output_id.eq_ignore_ascii_case("wavetrend")
14704                || output_id.eq_ignore_ascii_case("wt1")
14705                || output_id.eq_ignore_ascii_case("value")
14706            {
14707                return Ok(out.wavetrend);
14708            }
14709            if output_id.eq_ignore_ascii_case("signal") || output_id.eq_ignore_ascii_case("wt2") {
14710                return Ok(out.signal);
14711            }
14712            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
14713            {
14714                return Ok(out.histogram);
14715            }
14716            Err(IndicatorDispatchError::UnknownOutput {
14717                indicator: "mod_god_mode".to_string(),
14718                output: output_id.to_string(),
14719            })
14720        },
14721    )
14722}
14723
14724fn compute_msw_batch(
14725    req: IndicatorBatchRequest<'_>,
14726    output_id: &str,
14727) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14728    let data = extract_slice_input("msw", req.data, "close")?;
14729    let kernel = req.kernel.to_non_batch();
14730    collect_f64("msw", output_id, req.combos, data.len(), |params| {
14731        let period = get_usize_param("msw", params, "period", 5)?;
14732        let input = MswInput::from_slice(
14733            data,
14734            MswParams {
14735                period: Some(period),
14736            },
14737        );
14738        let out =
14739            msw_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14740                indicator: "msw".to_string(),
14741                details: e.to_string(),
14742            })?;
14743        if output_id.eq_ignore_ascii_case("sine") || output_id.eq_ignore_ascii_case("value") {
14744            return Ok(out.sine);
14745        }
14746        if output_id.eq_ignore_ascii_case("lead") {
14747            return Ok(out.lead);
14748        }
14749        Err(IndicatorDispatchError::UnknownOutput {
14750            indicator: "msw".to_string(),
14751            output: output_id.to_string(),
14752        })
14753    })
14754}
14755
14756fn compute_nadaraya_watson_envelope_batch(
14757    req: IndicatorBatchRequest<'_>,
14758    output_id: &str,
14759) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14760    let data = extract_slice_input("nadaraya_watson_envelope", req.data, "close")?;
14761    let kernel = req.kernel.to_non_batch();
14762    collect_f64(
14763        "nadaraya_watson_envelope",
14764        output_id,
14765        req.combos,
14766        data.len(),
14767        |params| {
14768            let bandwidth = get_f64_param("nadaraya_watson_envelope", params, "bandwidth", 8.0)?;
14769            let multiplier = get_f64_param("nadaraya_watson_envelope", params, "multiplier", 3.0)?;
14770            let lookback = get_usize_param("nadaraya_watson_envelope", params, "lookback", 500)?;
14771            let input = NweInput::from_slice(
14772                data,
14773                NweParams {
14774                    bandwidth: Some(bandwidth),
14775                    multiplier: Some(multiplier),
14776                    lookback: Some(lookback),
14777                },
14778            );
14779            let out = nadaraya_watson_envelope_with_kernel(&input, kernel).map_err(|e| {
14780                IndicatorDispatchError::ComputeFailed {
14781                    indicator: "nadaraya_watson_envelope".to_string(),
14782                    details: e.to_string(),
14783                }
14784            })?;
14785            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
14786                return Ok(out.upper);
14787            }
14788            if output_id.eq_ignore_ascii_case("lower") {
14789                return Ok(out.lower);
14790            }
14791            Err(IndicatorDispatchError::UnknownOutput {
14792                indicator: "nadaraya_watson_envelope".to_string(),
14793                output: output_id.to_string(),
14794            })
14795        },
14796    )
14797}
14798
14799fn compute_otto_batch(
14800    req: IndicatorBatchRequest<'_>,
14801    output_id: &str,
14802) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14803    let data = extract_slice_input("otto", req.data, "close")?;
14804    let kernel = req.kernel.to_non_batch();
14805    collect_f64("otto", output_id, req.combos, data.len(), |params| {
14806        let ott_period = get_usize_param("otto", params, "ott_period", 2)?;
14807        let ott_percent = get_f64_param("otto", params, "ott_percent", 0.6)?;
14808        let fast_vidya_length = get_usize_param("otto", params, "fast_vidya_length", 10)?;
14809        let slow_vidya_length = get_usize_param("otto", params, "slow_vidya_length", 25)?;
14810        let correcting_constant = get_f64_param("otto", params, "correcting_constant", 100000.0)?;
14811        let ma_type = get_enum_param("otto", params, "ma_type", "VAR")?;
14812        let input = OttoInput::from_slice(
14813            data,
14814            OttoParams {
14815                ott_period: Some(ott_period),
14816                ott_percent: Some(ott_percent),
14817                fast_vidya_length: Some(fast_vidya_length),
14818                slow_vidya_length: Some(slow_vidya_length),
14819                correcting_constant: Some(correcting_constant),
14820                ma_type: Some(ma_type),
14821            },
14822        );
14823        let out = otto_with_kernel(&input, kernel).map_err(|e| {
14824            IndicatorDispatchError::ComputeFailed {
14825                indicator: "otto".to_string(),
14826                details: e.to_string(),
14827            }
14828        })?;
14829        if output_id.eq_ignore_ascii_case("hott") || output_id.eq_ignore_ascii_case("value") {
14830            return Ok(out.hott);
14831        }
14832        if output_id.eq_ignore_ascii_case("lott") {
14833            return Ok(out.lott);
14834        }
14835        Err(IndicatorDispatchError::UnknownOutput {
14836            indicator: "otto".to_string(),
14837            output: output_id.to_string(),
14838        })
14839    })
14840}
14841
14842fn compute_vidya_batch(
14843    req: IndicatorBatchRequest<'_>,
14844    output_id: &str,
14845) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14846    let data = extract_slice_input("vidya", req.data, "close")?;
14847    let kernel = req.kernel.to_non_batch();
14848    collect_f64("vidya", output_id, req.combos, data.len(), |params| {
14849        let short_period = get_usize_param("vidya", params, "short_period", 2)?;
14850        let long_period = get_usize_param("vidya", params, "long_period", 5)?;
14851        let alpha = get_f64_param("vidya", params, "alpha", 0.2)?;
14852        let input = VidyaInput::from_slice(
14853            data,
14854            VidyaParams {
14855                short_period: Some(short_period),
14856                long_period: Some(long_period),
14857                alpha: Some(alpha),
14858            },
14859        );
14860        let out = vidya_with_kernel(&input, kernel).map_err(|e| {
14861            IndicatorDispatchError::ComputeFailed {
14862                indicator: "vidya".to_string(),
14863                details: e.to_string(),
14864            }
14865        })?;
14866        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
14867            return Ok(out.values);
14868        }
14869        Err(IndicatorDispatchError::UnknownOutput {
14870            indicator: "vidya".to_string(),
14871            output: output_id.to_string(),
14872        })
14873    })
14874}
14875
14876fn compute_vlma_batch(
14877    req: IndicatorBatchRequest<'_>,
14878    output_id: &str,
14879) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14880    let data = extract_slice_input("vlma", req.data, "close")?;
14881    let kernel = req.kernel.to_non_batch();
14882    collect_f64("vlma", output_id, req.combos, data.len(), |params| {
14883        let min_period = get_usize_param("vlma", params, "min_period", 5)?;
14884        let max_period = get_usize_param("vlma", params, "max_period", 50)?;
14885        let matype = get_enum_param("vlma", params, "matype", "sma")?;
14886        let devtype = get_usize_param("vlma", params, "devtype", 0)?;
14887        let input = VlmaInput::from_slice(
14888            data,
14889            VlmaParams {
14890                min_period: Some(min_period),
14891                max_period: Some(max_period),
14892                matype: Some(matype),
14893                devtype: Some(devtype),
14894            },
14895        );
14896        let out = vlma_with_kernel(&input, kernel).map_err(|e| {
14897            IndicatorDispatchError::ComputeFailed {
14898                indicator: "vlma".to_string(),
14899                details: e.to_string(),
14900            }
14901        })?;
14902        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
14903            return Ok(out.values);
14904        }
14905        Err(IndicatorDispatchError::UnknownOutput {
14906            indicator: "vlma".to_string(),
14907            output: output_id.to_string(),
14908        })
14909    })
14910}
14911
14912fn compute_pma_batch(
14913    req: IndicatorBatchRequest<'_>,
14914    output_id: &str,
14915) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14916    let data = extract_slice_input("pma", req.data, "close")?;
14917    let kernel = req.kernel.to_non_batch();
14918    collect_f64("pma", output_id, req.combos, data.len(), |_params| {
14919        let input = PmaInput::from_slice(data, PmaParams::default());
14920        let out =
14921            pma_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14922                indicator: "pma".to_string(),
14923                details: e.to_string(),
14924            })?;
14925        if output_id.eq_ignore_ascii_case("predict") || output_id.eq_ignore_ascii_case("value") {
14926            return Ok(out.predict);
14927        }
14928        if output_id.eq_ignore_ascii_case("trigger") {
14929            return Ok(out.trigger);
14930        }
14931        Err(IndicatorDispatchError::UnknownOutput {
14932            indicator: "pma".to_string(),
14933            output: output_id.to_string(),
14934        })
14935    })
14936}
14937
14938fn compute_ehlers_adaptive_cg_batch(
14939    req: IndicatorBatchRequest<'_>,
14940    output_id: &str,
14941) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14942    let data = extract_slice_input("ehlers_adaptive_cg", req.data, "hl2")?;
14943    let kernel = req.kernel.to_non_batch();
14944    collect_f64(
14945        "ehlers_adaptive_cg",
14946        output_id,
14947        req.combos,
14948        data.len(),
14949        |params| {
14950            let alpha = get_f64_param("ehlers_adaptive_cg", params, "alpha", 0.07)?;
14951            let input = EhlersAdaptiveCgInput::from_slice(
14952                data,
14953                EhlersAdaptiveCgParams { alpha: Some(alpha) },
14954            );
14955            let out = ehlers_adaptive_cg_with_kernel(&input, kernel).map_err(|e| {
14956                IndicatorDispatchError::ComputeFailed {
14957                    indicator: "ehlers_adaptive_cg".to_string(),
14958                    details: e.to_string(),
14959                }
14960            })?;
14961            if output_id.eq_ignore_ascii_case("cg") || output_id.eq_ignore_ascii_case("value") {
14962                return Ok(out.cg);
14963            }
14964            if output_id.eq_ignore_ascii_case("trigger") {
14965                return Ok(out.trigger);
14966            }
14967            Err(IndicatorDispatchError::UnknownOutput {
14968                indicator: "ehlers_adaptive_cg".to_string(),
14969                output: output_id.to_string(),
14970            })
14971        },
14972    )
14973}
14974
14975fn compute_prb_batch(
14976    req: IndicatorBatchRequest<'_>,
14977    output_id: &str,
14978) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14979    let data = extract_slice_input("prb", req.data, "close")?;
14980    let kernel = req.kernel.to_non_batch();
14981    collect_f64("prb", output_id, req.combos, data.len(), |params| {
14982        let smooth_data = get_bool_param("prb", params, "smooth_data", true)?;
14983        let smooth_period = get_usize_param("prb", params, "smooth_period", 10)?;
14984        let regression_period = get_usize_param("prb", params, "regression_period", 100)?;
14985        let polynomial_order = get_usize_param("prb", params, "polynomial_order", 2)?;
14986        let regression_offset = get_i32_param("prb", params, "regression_offset", 0)?;
14987        let ndev = get_f64_param("prb", params, "ndev", 2.0)?;
14988        let equ_from = get_usize_param("prb", params, "equ_from", 0)?;
14989        let input = PrbInput::from_slice(
14990            data,
14991            PrbParams {
14992                smooth_data: Some(smooth_data),
14993                smooth_period: Some(smooth_period),
14994                regression_period: Some(regression_period),
14995                polynomial_order: Some(polynomial_order),
14996                regression_offset: Some(regression_offset),
14997                ndev: Some(ndev),
14998                equ_from: Some(equ_from),
14999            },
15000        );
15001        let out =
15002            prb_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15003                indicator: "prb".to_string(),
15004                details: e.to_string(),
15005            })?;
15006        if output_id.eq_ignore_ascii_case("values") || output_id.eq_ignore_ascii_case("value") {
15007            return Ok(out.values);
15008        }
15009        if output_id.eq_ignore_ascii_case("upper_band") || output_id.eq_ignore_ascii_case("upper") {
15010            return Ok(out.upper_band);
15011        }
15012        if output_id.eq_ignore_ascii_case("lower_band") || output_id.eq_ignore_ascii_case("lower") {
15013            return Ok(out.lower_band);
15014        }
15015        Err(IndicatorDispatchError::UnknownOutput {
15016            indicator: "prb".to_string(),
15017            output: output_id.to_string(),
15018        })
15019    })
15020}
15021
15022fn compute_qqe_batch(
15023    req: IndicatorBatchRequest<'_>,
15024    output_id: &str,
15025) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15026    let data = extract_slice_input("qqe", req.data, "close")?;
15027    let kernel = req.kernel.to_non_batch();
15028    collect_f64("qqe", output_id, req.combos, data.len(), |params| {
15029        let rsi_period = get_usize_param("qqe", params, "rsi_period", 14)?;
15030        let smoothing_factor = get_usize_param("qqe", params, "smoothing_factor", 5)?;
15031        let fast_factor = get_f64_param("qqe", params, "fast_factor", 4.236)?;
15032        let input = QqeInput::from_slice(
15033            data,
15034            QqeParams {
15035                rsi_period: Some(rsi_period),
15036                smoothing_factor: Some(smoothing_factor),
15037                fast_factor: Some(fast_factor),
15038            },
15039        );
15040        let out =
15041            qqe_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15042                indicator: "qqe".to_string(),
15043                details: e.to_string(),
15044            })?;
15045        if output_id.eq_ignore_ascii_case("fast") || output_id.eq_ignore_ascii_case("value") {
15046            return Ok(out.fast);
15047        }
15048        if output_id.eq_ignore_ascii_case("slow") {
15049            return Ok(out.slow);
15050        }
15051        Err(IndicatorDispatchError::UnknownOutput {
15052            indicator: "qqe".to_string(),
15053            output: output_id.to_string(),
15054        })
15055    })
15056}
15057
15058fn compute_qqe_weighted_oscillator_batch(
15059    req: IndicatorBatchRequest<'_>,
15060    output_id: &str,
15061) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15062    let data = extract_slice_input("qqe_weighted_oscillator", req.data, "close")?;
15063    let kernel = req.kernel.to_non_batch();
15064    collect_f64(
15065        "qqe_weighted_oscillator",
15066        output_id,
15067        req.combos,
15068        data.len(),
15069        |params| {
15070            let length = get_usize_param("qqe_weighted_oscillator", params, "length", 14)?;
15071            let factor = get_f64_param("qqe_weighted_oscillator", params, "factor", 4.236)?;
15072            let smooth = get_usize_param("qqe_weighted_oscillator", params, "smooth", 5)?;
15073            let weight = get_f64_param("qqe_weighted_oscillator", params, "weight", 2.0)?;
15074            let input = QqeWeightedOscillatorInput::from_slice(
15075                data,
15076                QqeWeightedOscillatorParams {
15077                    length: Some(length),
15078                    factor: Some(factor),
15079                    smooth: Some(smooth),
15080                    weight: Some(weight),
15081                },
15082            );
15083            let out = qqe_weighted_oscillator_with_kernel(&input, kernel).map_err(|e| {
15084                IndicatorDispatchError::ComputeFailed {
15085                    indicator: "qqe_weighted_oscillator".to_string(),
15086                    details: e.to_string(),
15087                }
15088            })?;
15089            if output_id.eq_ignore_ascii_case("rsi") || output_id.eq_ignore_ascii_case("value") {
15090                return Ok(out.rsi);
15091            }
15092            if output_id.eq_ignore_ascii_case("trailing_stop")
15093                || output_id.eq_ignore_ascii_case("ts")
15094            {
15095                return Ok(out.trailing_stop);
15096            }
15097            Err(IndicatorDispatchError::UnknownOutput {
15098                indicator: "qqe_weighted_oscillator".to_string(),
15099                output: output_id.to_string(),
15100            })
15101        },
15102    )
15103}
15104
15105fn compute_forward_backward_exponential_oscillator_batch(
15106    req: IndicatorBatchRequest<'_>,
15107    output_id: &str,
15108) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15109    let data = extract_slice_input("forward_backward_exponential_oscillator", req.data, "close")?;
15110    let kernel = req.kernel.to_non_batch();
15111    collect_f64(
15112        "forward_backward_exponential_oscillator",
15113        output_id,
15114        req.combos,
15115        data.len(),
15116        |params| {
15117            let length = get_usize_param(
15118                "forward_backward_exponential_oscillator",
15119                params,
15120                "length",
15121                20,
15122            )?;
15123            let smooth = get_usize_param(
15124                "forward_backward_exponential_oscillator",
15125                params,
15126                "smooth",
15127                10,
15128            )?;
15129            let input = ForwardBackwardExponentialOscillatorInput::from_slice(
15130                data,
15131                ForwardBackwardExponentialOscillatorParams {
15132                    length: Some(length),
15133                    smooth: Some(smooth),
15134                },
15135            );
15136            let out = forward_backward_exponential_oscillator_with_kernel(&input, kernel).map_err(
15137                |e| IndicatorDispatchError::ComputeFailed {
15138                    indicator: "forward_backward_exponential_oscillator".to_string(),
15139                    details: e.to_string(),
15140                },
15141            )?;
15142            if output_id.eq_ignore_ascii_case("forward_backward")
15143                || output_id.eq_ignore_ascii_case("value")
15144                || output_id.eq_ignore_ascii_case("fb")
15145            {
15146                return Ok(out.forward_backward);
15147            }
15148            if output_id.eq_ignore_ascii_case("backward")
15149                || output_id.eq_ignore_ascii_case("bwrd")
15150                || output_id.eq_ignore_ascii_case("bw")
15151            {
15152                return Ok(out.backward);
15153            }
15154            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
15155            {
15156                return Ok(out.histogram);
15157            }
15158            Err(IndicatorDispatchError::UnknownOutput {
15159                indicator: "forward_backward_exponential_oscillator".to_string(),
15160                output: output_id.to_string(),
15161            })
15162        },
15163    )
15164}
15165
15166fn compute_range_oscillator_batch(
15167    req: IndicatorBatchRequest<'_>,
15168    output_id: &str,
15169) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15170    let (high, low, close) = extract_ohlc_input("range_oscillator", req.data)?;
15171    let kernel = req.kernel.to_non_batch();
15172    collect_f64(
15173        "range_oscillator",
15174        output_id,
15175        req.combos,
15176        close.len(),
15177        |params| {
15178            let length = get_usize_param("range_oscillator", params, "length", 50)?;
15179            let mult = get_f64_param("range_oscillator", params, "mult", 2.0)?;
15180            let input = RangeOscillatorInput::from_slices(
15181                high,
15182                low,
15183                close,
15184                RangeOscillatorParams {
15185                    length: Some(length),
15186                    mult: Some(mult),
15187                },
15188            );
15189            let out = range_oscillator_with_kernel(&input, kernel).map_err(|e| {
15190                IndicatorDispatchError::ComputeFailed {
15191                    indicator: "range_oscillator".to_string(),
15192                    details: e.to_string(),
15193                }
15194            })?;
15195            if output_id.eq_ignore_ascii_case("oscillator")
15196                || output_id.eq_ignore_ascii_case("osc")
15197                || output_id.eq_ignore_ascii_case("value")
15198            {
15199                return Ok(out.oscillator);
15200            }
15201            if output_id.eq_ignore_ascii_case("ma") {
15202                return Ok(out.ma);
15203            }
15204            if output_id.eq_ignore_ascii_case("upper_band")
15205                || output_id.eq_ignore_ascii_case("upper")
15206            {
15207                return Ok(out.upper_band);
15208            }
15209            if output_id.eq_ignore_ascii_case("lower_band")
15210                || output_id.eq_ignore_ascii_case("lower")
15211            {
15212                return Ok(out.lower_band);
15213            }
15214            if output_id.eq_ignore_ascii_case("range_width")
15215                || output_id.eq_ignore_ascii_case("width")
15216            {
15217                return Ok(out.range_width);
15218            }
15219            if output_id.eq_ignore_ascii_case("in_range") {
15220                return Ok(out.in_range);
15221            }
15222            if output_id.eq_ignore_ascii_case("trend") {
15223                return Ok(out.trend);
15224            }
15225            if output_id.eq_ignore_ascii_case("break_up") {
15226                return Ok(out.break_up);
15227            }
15228            if output_id.eq_ignore_ascii_case("break_down") {
15229                return Ok(out.break_down);
15230            }
15231            Err(IndicatorDispatchError::UnknownOutput {
15232                indicator: "range_oscillator".to_string(),
15233                output: output_id.to_string(),
15234            })
15235        },
15236    )
15237}
15238
15239fn compute_market_structure_confluence_batch(
15240    req: IndicatorBatchRequest<'_>,
15241    output_id: &str,
15242) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15243    let (high, low, close) = extract_ohlc_input("market_structure_confluence", req.data)?;
15244    let kernel = req.kernel.to_non_batch();
15245    collect_f64(
15246        "market_structure_confluence",
15247        output_id,
15248        req.combos,
15249        close.len(),
15250        |params| {
15251            let swing_size =
15252                get_usize_param("market_structure_confluence", params, "swing_size", 10)?;
15253            let bos_confirmation = get_enum_param(
15254                "market_structure_confluence",
15255                params,
15256                "bos_confirmation",
15257                "Candle Close",
15258            )?;
15259            let basis_length =
15260                get_usize_param("market_structure_confluence", params, "basis_length", 100)?;
15261            let atr_length =
15262                get_usize_param("market_structure_confluence", params, "atr_length", 14)?;
15263            let atr_smooth =
15264                get_usize_param("market_structure_confluence", params, "atr_smooth", 21)?;
15265            let vol_mult =
15266                get_f64_param("market_structure_confluence", params, "vol_mult", 2.0)?;
15267            let input = MarketStructureConfluenceInput::from_slices(
15268                high,
15269                low,
15270                close,
15271                MarketStructureConfluenceParams {
15272                    swing_size: Some(swing_size),
15273                    bos_confirmation: Some(bos_confirmation),
15274                    basis_length: Some(basis_length),
15275                    atr_length: Some(atr_length),
15276                    atr_smooth: Some(atr_smooth),
15277                    vol_mult: Some(vol_mult),
15278                },
15279            );
15280            let out = market_structure_confluence_with_kernel(&input, kernel).map_err(|e| {
15281                IndicatorDispatchError::ComputeFailed {
15282                    indicator: "market_structure_confluence".to_string(),
15283                    details: e.to_string(),
15284                }
15285            })?;
15286            if output_id.eq_ignore_ascii_case("basis") {
15287                return Ok(out.basis);
15288            }
15289            if output_id.eq_ignore_ascii_case("upper_band")
15290                || output_id.eq_ignore_ascii_case("upper")
15291            {
15292                return Ok(out.upper_band);
15293            }
15294            if output_id.eq_ignore_ascii_case("lower_band")
15295                || output_id.eq_ignore_ascii_case("lower")
15296            {
15297                return Ok(out.lower_band);
15298            }
15299            if output_id.eq_ignore_ascii_case("structure_direction")
15300                || output_id.eq_ignore_ascii_case("direction")
15301                || output_id.eq_ignore_ascii_case("trend")
15302            {
15303                return Ok(out.structure_direction);
15304            }
15305            if output_id.eq_ignore_ascii_case("bullish_arrow") {
15306                return Ok(out.bullish_arrow);
15307            }
15308            if output_id.eq_ignore_ascii_case("bearish_arrow") {
15309                return Ok(out.bearish_arrow);
15310            }
15311            if output_id.eq_ignore_ascii_case("bullish_change") {
15312                return Ok(out.bullish_change);
15313            }
15314            if output_id.eq_ignore_ascii_case("bearish_change") {
15315                return Ok(out.bearish_change);
15316            }
15317            if output_id.eq_ignore_ascii_case("hh") {
15318                return Ok(out.hh);
15319            }
15320            if output_id.eq_ignore_ascii_case("lh") {
15321                return Ok(out.lh);
15322            }
15323            if output_id.eq_ignore_ascii_case("hl") {
15324                return Ok(out.hl);
15325            }
15326            if output_id.eq_ignore_ascii_case("ll") {
15327                return Ok(out.ll);
15328            }
15329            if output_id.eq_ignore_ascii_case("bullish_bos") {
15330                return Ok(out.bullish_bos);
15331            }
15332            if output_id.eq_ignore_ascii_case("bullish_choch") {
15333                return Ok(out.bullish_choch);
15334            }
15335            if output_id.eq_ignore_ascii_case("bearish_bos") {
15336                return Ok(out.bearish_bos);
15337            }
15338            if output_id.eq_ignore_ascii_case("bearish_choch") {
15339                return Ok(out.bearish_choch);
15340            }
15341            Err(IndicatorDispatchError::UnknownOutput {
15342                indicator: "market_structure_confluence".to_string(),
15343                output: output_id.to_string(),
15344            })
15345        },
15346    )
15347}
15348
15349fn compute_range_filtered_trend_signals_batch(
15350    req: IndicatorBatchRequest<'_>,
15351    output_id: &str,
15352) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15353    let (high, low, close) = extract_ohlc_input("range_filtered_trend_signals", req.data)?;
15354    let kernel = req.kernel.to_non_batch();
15355    collect_f64(
15356        "range_filtered_trend_signals",
15357        output_id,
15358        req.combos,
15359        close.len(),
15360        |params| {
15361            let kalman_alpha =
15362                get_f64_param("range_filtered_trend_signals", params, "kalman_alpha", 0.01)?;
15363            let kalman_beta =
15364                get_f64_param("range_filtered_trend_signals", params, "kalman_beta", 0.1)?;
15365            let kalman_period =
15366                get_usize_param("range_filtered_trend_signals", params, "kalman_period", 77)?;
15367            let dev = get_f64_param("range_filtered_trend_signals", params, "dev", 1.2)?;
15368            let supertrend_factor = get_f64_param(
15369                "range_filtered_trend_signals",
15370                params,
15371                "supertrend_factor",
15372                0.7,
15373            )?;
15374            let supertrend_atr_period = get_usize_param(
15375                "range_filtered_trend_signals",
15376                params,
15377                "supertrend_atr_period",
15378                7,
15379            )?;
15380            let input = RangeFilteredTrendSignalsInput::from_slices(
15381                high,
15382                low,
15383                close,
15384                RangeFilteredTrendSignalsParams {
15385                    kalman_alpha: Some(kalman_alpha),
15386                    kalman_beta: Some(kalman_beta),
15387                    kalman_period: Some(kalman_period),
15388                    dev: Some(dev),
15389                    supertrend_factor: Some(supertrend_factor),
15390                    supertrend_atr_period: Some(supertrend_atr_period),
15391                },
15392            );
15393            let out = range_filtered_trend_signals_with_kernel(&input, kernel).map_err(|e| {
15394                IndicatorDispatchError::ComputeFailed {
15395                    indicator: "range_filtered_trend_signals".to_string(),
15396                    details: e.to_string(),
15397                }
15398            })?;
15399            if output_id.eq_ignore_ascii_case("kalman") {
15400                return Ok(out.kalman);
15401            }
15402            if output_id.eq_ignore_ascii_case("supertrend") {
15403                return Ok(out.supertrend);
15404            }
15405            if output_id.eq_ignore_ascii_case("upper_band")
15406                || output_id.eq_ignore_ascii_case("upper")
15407            {
15408                return Ok(out.upper_band);
15409            }
15410            if output_id.eq_ignore_ascii_case("lower_band")
15411                || output_id.eq_ignore_ascii_case("lower")
15412            {
15413                return Ok(out.lower_band);
15414            }
15415            if output_id.eq_ignore_ascii_case("trend") {
15416                return Ok(out.trend);
15417            }
15418            if output_id.eq_ignore_ascii_case("kalman_trend")
15419                || output_id.eq_ignore_ascii_case("long_trend")
15420            {
15421                return Ok(out.kalman_trend);
15422            }
15423            if output_id.eq_ignore_ascii_case("state") {
15424                return Ok(out.state);
15425            }
15426            if output_id.eq_ignore_ascii_case("market_trending") {
15427                return Ok(out.market_trending);
15428            }
15429            if output_id.eq_ignore_ascii_case("market_ranging") {
15430                return Ok(out.market_ranging);
15431            }
15432            if output_id.eq_ignore_ascii_case("short_term_bullish") {
15433                return Ok(out.short_term_bullish);
15434            }
15435            if output_id.eq_ignore_ascii_case("short_term_bearish") {
15436                return Ok(out.short_term_bearish);
15437            }
15438            if output_id.eq_ignore_ascii_case("long_term_bullish") {
15439                return Ok(out.long_term_bullish);
15440            }
15441            if output_id.eq_ignore_ascii_case("long_term_bearish") {
15442                return Ok(out.long_term_bearish);
15443            }
15444            Err(IndicatorDispatchError::UnknownOutput {
15445                indicator: "range_filtered_trend_signals".to_string(),
15446                output: output_id.to_string(),
15447            })
15448        },
15449    )
15450}
15451
15452fn compute_volume_weighted_relative_strength_index_batch(
15453    req: IndicatorBatchRequest<'_>,
15454    output_id: &str,
15455) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15456    let (source, volume) =
15457        extract_close_volume_input("volume_weighted_relative_strength_index", req.data, "close")?;
15458    let kernel = req.kernel.to_non_batch();
15459    collect_f64(
15460        "volume_weighted_relative_strength_index",
15461        output_id,
15462        req.combos,
15463        source.len(),
15464        |params| {
15465            let rsi_length = get_usize_param(
15466                "volume_weighted_relative_strength_index",
15467                params,
15468                "rsi_length",
15469                14,
15470            )?;
15471            let range_length = get_usize_param(
15472                "volume_weighted_relative_strength_index",
15473                params,
15474                "range_length",
15475                10,
15476            )?;
15477            let ma_length = get_usize_param(
15478                "volume_weighted_relative_strength_index",
15479                params,
15480                "ma_length",
15481                14,
15482            )?;
15483            let ma_type = get_enum_param(
15484                "volume_weighted_relative_strength_index",
15485                params,
15486                "ma_type",
15487                "EMA",
15488            )?;
15489            let input = VolumeWeightedRelativeStrengthIndexInput::from_slices(
15490                source,
15491                volume,
15492                VolumeWeightedRelativeStrengthIndexParams {
15493                    rsi_length: Some(rsi_length),
15494                    range_length: Some(range_length),
15495                    ma_length: Some(ma_length),
15496                    ma_type: Some(ma_type),
15497                },
15498            );
15499            let out = volume_weighted_relative_strength_index_with_kernel(&input, kernel).map_err(
15500                |e| IndicatorDispatchError::ComputeFailed {
15501                    indicator: "volume_weighted_relative_strength_index".to_string(),
15502                    details: e.to_string(),
15503                },
15504            )?;
15505            if output_id.eq_ignore_ascii_case("rsi") || output_id.eq_ignore_ascii_case("value") {
15506                return Ok(out.rsi);
15507            }
15508            if output_id.eq_ignore_ascii_case("consolidation_strength")
15509                || output_id.eq_ignore_ascii_case("consolidation")
15510            {
15511                return Ok(out.consolidation_strength);
15512            }
15513            if output_id.eq_ignore_ascii_case("rsi_ma") || output_id.eq_ignore_ascii_case("ma") {
15514                return Ok(out.rsi_ma);
15515            }
15516            if output_id.eq_ignore_ascii_case("bearish_tp") {
15517                return Ok(out.bearish_tp);
15518            }
15519            if output_id.eq_ignore_ascii_case("bullish_tp") {
15520                return Ok(out.bullish_tp);
15521            }
15522            Err(IndicatorDispatchError::UnknownOutput {
15523                indicator: "volume_weighted_relative_strength_index".to_string(),
15524                output: output_id.to_string(),
15525            })
15526        },
15527    )
15528}
15529
15530fn compute_range_filter_batch(
15531    req: IndicatorBatchRequest<'_>,
15532    output_id: &str,
15533) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15534    let data = extract_slice_input("range_filter", req.data, "close")?;
15535    let kernel = req.kernel.to_non_batch();
15536    collect_f64(
15537        "range_filter",
15538        output_id,
15539        req.combos,
15540        data.len(),
15541        |params| {
15542            let range_size = get_f64_param("range_filter", params, "range_size", 2.618)?;
15543            let range_period = get_usize_param("range_filter", params, "range_period", 14)?;
15544            let smooth_range = get_bool_param("range_filter", params, "smooth_range", true)?;
15545            let smooth_period = get_usize_param("range_filter", params, "smooth_period", 27)?;
15546            let input = RangeFilterInput::from_slice(
15547                data,
15548                RangeFilterParams {
15549                    range_size: Some(range_size),
15550                    range_period: Some(range_period),
15551                    smooth_range: Some(smooth_range),
15552                    smooth_period: Some(smooth_period),
15553                },
15554            );
15555            let out = range_filter_with_kernel(&input, kernel).map_err(|e| {
15556                IndicatorDispatchError::ComputeFailed {
15557                    indicator: "range_filter".to_string(),
15558                    details: e.to_string(),
15559                }
15560            })?;
15561            if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
15562                return Ok(out.filter);
15563            }
15564            if output_id.eq_ignore_ascii_case("high_band") || output_id.eq_ignore_ascii_case("high")
15565            {
15566                return Ok(out.high_band);
15567            }
15568            if output_id.eq_ignore_ascii_case("low_band") || output_id.eq_ignore_ascii_case("low") {
15569                return Ok(out.low_band);
15570            }
15571            Err(IndicatorDispatchError::UnknownOutput {
15572                indicator: "range_filter".to_string(),
15573                output: output_id.to_string(),
15574            })
15575        },
15576    )
15577}
15578
15579fn compute_rsmk_batch(
15580    req: IndicatorBatchRequest<'_>,
15581    output_id: &str,
15582) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15583    let (main, compare) = match req.data {
15584        IndicatorDataRef::CloseVolume { close, volume } => {
15585            ensure_same_len_2("rsmk", close.len(), volume.len())?;
15586            (close, volume)
15587        }
15588        IndicatorDataRef::Ohlcv {
15589            open,
15590            high,
15591            low,
15592            close,
15593            volume,
15594        } => {
15595            ensure_same_len_5(
15596                "rsmk",
15597                open.len(),
15598                high.len(),
15599                low.len(),
15600                close.len(),
15601                volume.len(),
15602            )?;
15603            (close, volume)
15604        }
15605        IndicatorDataRef::Candles { candles, source } => (
15606            source_type(candles, source.unwrap_or("close")),
15607            candles.volume.as_slice(),
15608        ),
15609        _ => {
15610            return Err(IndicatorDispatchError::MissingRequiredInput {
15611                indicator: "rsmk".to_string(),
15612                input: IndicatorInputKind::CloseVolume,
15613            });
15614        }
15615    };
15616    let kernel = req.kernel.to_non_batch();
15617    collect_f64("rsmk", output_id, req.combos, main.len(), |params| {
15618        let lookback = get_usize_param("rsmk", params, "lookback", 90)?;
15619        let period = get_usize_param("rsmk", params, "period", 3)?;
15620        let signal_period = get_usize_param("rsmk", params, "signal_period", 20)?;
15621        let matype = get_enum_param("rsmk", params, "matype", "ema")?;
15622        let signal_matype = get_enum_param("rsmk", params, "signal_matype", "ema")?;
15623        let input = RsmkInput::from_slices(
15624            main,
15625            compare,
15626            RsmkParams {
15627                lookback: Some(lookback),
15628                period: Some(period),
15629                signal_period: Some(signal_period),
15630                matype: Some(matype),
15631                signal_matype: Some(signal_matype),
15632            },
15633        );
15634        let out = rsmk_with_kernel(&input, kernel).map_err(|e| {
15635            IndicatorDispatchError::ComputeFailed {
15636                indicator: "rsmk".to_string(),
15637                details: e.to_string(),
15638            }
15639        })?;
15640        if output_id.eq_ignore_ascii_case("indicator") || output_id.eq_ignore_ascii_case("value") {
15641            return Ok(out.indicator);
15642        }
15643        if output_id.eq_ignore_ascii_case("signal") {
15644            return Ok(out.signal);
15645        }
15646        Err(IndicatorDispatchError::UnknownOutput {
15647            indicator: "rsmk".to_string(),
15648            output: output_id.to_string(),
15649        })
15650    })
15651}
15652
15653fn compute_voss_batch(
15654    req: IndicatorBatchRequest<'_>,
15655    output_id: &str,
15656) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15657    let data = extract_slice_input("voss", req.data, "close")?;
15658    let kernel = req.kernel.to_non_batch();
15659    collect_f64("voss", output_id, req.combos, data.len(), |params| {
15660        let period = get_usize_param("voss", params, "period", 20)?;
15661        let predict = get_usize_param("voss", params, "predict", 3)?;
15662        let bandwidth = get_f64_param("voss", params, "bandwidth", 0.25)?;
15663        let input = VossInput::from_slice(
15664            data,
15665            VossParams {
15666                period: Some(period),
15667                predict: Some(predict),
15668                bandwidth: Some(bandwidth),
15669            },
15670        );
15671        let out = voss_with_kernel(&input, kernel).map_err(|e| {
15672            IndicatorDispatchError::ComputeFailed {
15673                indicator: "voss".to_string(),
15674                details: e.to_string(),
15675            }
15676        })?;
15677        if output_id.eq_ignore_ascii_case("voss") || output_id.eq_ignore_ascii_case("value") {
15678            return Ok(out.voss);
15679        }
15680        if output_id.eq_ignore_ascii_case("filt") || output_id.eq_ignore_ascii_case("filter") {
15681            return Ok(out.filt);
15682        }
15683        Err(IndicatorDispatchError::UnknownOutput {
15684            indicator: "voss".to_string(),
15685            output: output_id.to_string(),
15686        })
15687    })
15688}
15689
15690fn compute_stc_batch(
15691    req: IndicatorBatchRequest<'_>,
15692    output_id: &str,
15693) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15694    let data = extract_slice_input("stc", req.data, "close")?;
15695    let kernel = req.kernel.to_non_batch();
15696    collect_f64("stc", output_id, req.combos, data.len(), |params| {
15697        let fast_period = get_usize_param("stc", params, "fast_period", 23)?;
15698        let slow_period = get_usize_param("stc", params, "slow_period", 50)?;
15699        let k_period = get_usize_param("stc", params, "k_period", 10)?;
15700        let d_period = get_usize_param("stc", params, "d_period", 3)?;
15701        let input = StcInput::from_slice(
15702            data,
15703            StcParams {
15704                fast_period: Some(fast_period),
15705                slow_period: Some(slow_period),
15706                k_period: Some(k_period),
15707                d_period: Some(d_period),
15708                fast_ma_type: Some("ema".to_string()),
15709                slow_ma_type: Some("ema".to_string()),
15710            },
15711        );
15712        let out =
15713            stc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15714                indicator: "stc".to_string(),
15715                details: e.to_string(),
15716            })?;
15717        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15718            return Ok(out.values);
15719        }
15720        Err(IndicatorDispatchError::UnknownOutput {
15721            indicator: "stc".to_string(),
15722            output: output_id.to_string(),
15723        })
15724    })
15725}
15726
15727fn compute_rvi_batch(
15728    req: IndicatorBatchRequest<'_>,
15729    output_id: &str,
15730) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15731    let data = extract_slice_input("rvi", req.data, "close")?;
15732    let kernel = req.kernel.to_non_batch();
15733    collect_f64("rvi", output_id, req.combos, data.len(), |params| {
15734        let period = get_usize_param("rvi", params, "period", 10)?;
15735        let ma_len = get_usize_param("rvi", params, "ma_len", 14)?;
15736        let matype = get_usize_param("rvi", params, "matype", 1)?;
15737        let devtype = get_usize_param("rvi", params, "devtype", 0)?;
15738        let input = RviInput::from_slice(
15739            data,
15740            RviParams {
15741                period: Some(period),
15742                ma_len: Some(ma_len),
15743                matype: Some(matype),
15744                devtype: Some(devtype),
15745            },
15746        );
15747        let out =
15748            rvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15749                indicator: "rvi".to_string(),
15750                details: e.to_string(),
15751            })?;
15752        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15753            return Ok(out.values);
15754        }
15755        Err(IndicatorDispatchError::UnknownOutput {
15756            indicator: "rvi".to_string(),
15757            output: output_id.to_string(),
15758        })
15759    })
15760}
15761
15762fn compute_coppock_batch(
15763    req: IndicatorBatchRequest<'_>,
15764    output_id: &str,
15765) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15766    let data = extract_slice_input("coppock", req.data, "close")?;
15767    let kernel = req.kernel.to_non_batch();
15768    collect_f64("coppock", output_id, req.combos, data.len(), |params| {
15769        let short_roc_period = get_usize_param("coppock", params, "short_roc_period", 11)?;
15770        let long_roc_period = get_usize_param("coppock", params, "long_roc_period", 14)?;
15771        let ma_period = get_usize_param("coppock", params, "ma_period", 10)?;
15772        let input = CoppockInput::from_slice(
15773            data,
15774            CoppockParams {
15775                short_roc_period: Some(short_roc_period),
15776                long_roc_period: Some(long_roc_period),
15777                ma_period: Some(ma_period),
15778                ma_type: Some("wma".to_string()),
15779            },
15780        );
15781        let out = coppock_with_kernel(&input, kernel).map_err(|e| {
15782            IndicatorDispatchError::ComputeFailed {
15783                indicator: "coppock".to_string(),
15784                details: e.to_string(),
15785            }
15786        })?;
15787        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15788            return Ok(out.values);
15789        }
15790        Err(IndicatorDispatchError::UnknownOutput {
15791            indicator: "coppock".to_string(),
15792            output: output_id.to_string(),
15793        })
15794    })
15795}
15796
15797fn compute_correl_hl_batch(
15798    req: IndicatorBatchRequest<'_>,
15799    output_id: &str,
15800) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15801    expect_value_output("correl_hl", output_id)?;
15802    let (high, low) = extract_high_low_input("correl_hl", req.data)?;
15803    let kernel = req.kernel.to_non_batch();
15804    collect_f64("correl_hl", output_id, req.combos, high.len(), |params| {
15805        let period = get_usize_param("correl_hl", params, "period", 9)?;
15806        let input = CorrelHlInput::from_slices(
15807            high,
15808            low,
15809            CorrelHlParams {
15810                period: Some(period),
15811            },
15812        );
15813        let out = correl_hl_with_kernel(&input, kernel).map_err(|e| {
15814            IndicatorDispatchError::ComputeFailed {
15815                indicator: "correl_hl".to_string(),
15816                details: e.to_string(),
15817            }
15818        })?;
15819        Ok(out.values)
15820    })
15821}
15822
15823fn compute_net_myrsi_batch(
15824    req: IndicatorBatchRequest<'_>,
15825    output_id: &str,
15826) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15827    let data = extract_slice_input("net_myrsi", req.data, "close")?;
15828    let kernel = req.kernel.to_non_batch();
15829    collect_f64("net_myrsi", output_id, req.combos, data.len(), |params| {
15830        let period = get_usize_param("net_myrsi", params, "period", 14)?;
15831        let input = NetMyrsiInput::from_slice(
15832            data,
15833            NetMyrsiParams {
15834                period: Some(period),
15835            },
15836        );
15837        let out = net_myrsi_with_kernel(&input, kernel).map_err(|e| {
15838            IndicatorDispatchError::ComputeFailed {
15839                indicator: "net_myrsi".to_string(),
15840                details: e.to_string(),
15841            }
15842        })?;
15843        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15844            return Ok(out.values);
15845        }
15846        Err(IndicatorDispatchError::UnknownOutput {
15847            indicator: "net_myrsi".to_string(),
15848            output: output_id.to_string(),
15849        })
15850    })
15851}
15852
15853fn compute_pivot_batch(
15854    req: IndicatorBatchRequest<'_>,
15855    output_id: &str,
15856) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15857    let (open, high, low, close) = extract_ohlc_full_input("pivot", req.data)?;
15858    let kernel = req.kernel.to_non_batch();
15859    collect_f64("pivot", output_id, req.combos, close.len(), |params| {
15860        let mode = get_usize_param("pivot", params, "mode", 3)?;
15861        let input =
15862            PivotInput::from_slices(high, low, close, open, PivotParams { mode: Some(mode) });
15863        let out = pivot_with_kernel(&input, kernel).map_err(|e| {
15864            IndicatorDispatchError::ComputeFailed {
15865                indicator: "pivot".to_string(),
15866                details: e.to_string(),
15867            }
15868        })?;
15869        if output_id.eq_ignore_ascii_case("pp") || output_id.eq_ignore_ascii_case("value") {
15870            return Ok(out.pp);
15871        }
15872        if output_id.eq_ignore_ascii_case("r1") {
15873            return Ok(out.r1);
15874        }
15875        if output_id.eq_ignore_ascii_case("r2") {
15876            return Ok(out.r2);
15877        }
15878        if output_id.eq_ignore_ascii_case("r3") {
15879            return Ok(out.r3);
15880        }
15881        if output_id.eq_ignore_ascii_case("r4") {
15882            return Ok(out.r4);
15883        }
15884        if output_id.eq_ignore_ascii_case("s1") {
15885            return Ok(out.s1);
15886        }
15887        if output_id.eq_ignore_ascii_case("s2") {
15888            return Ok(out.s2);
15889        }
15890        if output_id.eq_ignore_ascii_case("s3") {
15891            return Ok(out.s3);
15892        }
15893        if output_id.eq_ignore_ascii_case("s4") {
15894            return Ok(out.s4);
15895        }
15896        Err(IndicatorDispatchError::UnknownOutput {
15897            indicator: "pivot".to_string(),
15898            output: output_id.to_string(),
15899        })
15900    })
15901}
15902
15903fn compute_wad_batch(
15904    req: IndicatorBatchRequest<'_>,
15905    output_id: &str,
15906) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15907    expect_value_output("wad", output_id)?;
15908    let (_open, high, low, close) = extract_ohlc_full_input("wad", req.data)?;
15909    let kernel = req.kernel.to_non_batch();
15910    collect_f64("wad", output_id, req.combos, close.len(), |_params| {
15911        let input = WadInput::from_slices(high, low, close);
15912        let out =
15913            wad_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15914                indicator: "wad".to_string(),
15915                details: e.to_string(),
15916            })?;
15917        Ok(out.values)
15918    })
15919}
15920
15921fn ma_data_from_req<'a>(
15922    indicator: &str,
15923    data: IndicatorDataRef<'a>,
15924) -> Result<MaData<'a>, IndicatorDispatchError> {
15925    match data {
15926        IndicatorDataRef::Slice { values } => Ok(MaData::Slice(values)),
15927        IndicatorDataRef::Candles { candles, source } => Ok(MaData::Candles {
15928            candles,
15929            source: source.unwrap_or("close"),
15930        }),
15931        IndicatorDataRef::Ohlc { close, .. } => Ok(MaData::Slice(close)),
15932        IndicatorDataRef::Ohlcv { close, .. } => Ok(MaData::Slice(close)),
15933        IndicatorDataRef::CloseVolume { close, .. } => Ok(MaData::Slice(close)),
15934        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
15935            indicator: indicator.to_string(),
15936            input: IndicatorInputKind::Slice,
15937        }),
15938    }
15939}
15940
15941fn ma_len_from_req(
15942    indicator: &str,
15943    data: IndicatorDataRef<'_>,
15944) -> Result<usize, IndicatorDispatchError> {
15945    match data {
15946        IndicatorDataRef::Slice { values } => Ok(values.len()),
15947        IndicatorDataRef::Candles { candles, source } => {
15948            Ok(source_type(candles, source.unwrap_or("close")).len())
15949        }
15950        IndicatorDataRef::Ohlc { close, .. } => Ok(close.len()),
15951        IndicatorDataRef::Ohlcv { close, .. } => Ok(close.len()),
15952        IndicatorDataRef::CloseVolume { close, .. } => Ok(close.len()),
15953        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
15954            indicator: indicator.to_string(),
15955            input: IndicatorInputKind::Slice,
15956        }),
15957    }
15958}
15959
15960fn ma_period_for_combo(
15961    info: &IndicatorInfo,
15962    params: &[ParamKV<'_>],
15963) -> Result<usize, IndicatorDispatchError> {
15964    if let Some(v) = find_param(params, "period") {
15965        return parse_usize_param_value(info.id, "period", v);
15966    }
15967    if let Some(default) = info
15968        .params
15969        .iter()
15970        .find(|p| p.key.eq_ignore_ascii_case("period"))
15971        .and_then(|p| p.default.as_ref())
15972    {
15973        if let ParamValueStatic::Int(v) = default {
15974            if *v >= 0 {
15975                return Ok(*v as usize);
15976            }
15977        }
15978    }
15979    Ok(14)
15980}
15981
15982fn convert_ma_params<'a>(
15983    params: &'a [ParamKV<'a>],
15984    indicator: &str,
15985    output_id: &str,
15986) -> Result<Vec<MaBatchParamKV<'a>>, IndicatorDispatchError> {
15987    let mut out = Vec::with_capacity(params.len());
15988    for p in params {
15989        if p.key.eq_ignore_ascii_case("period") {
15990            continue;
15991        }
15992        if p.key.eq_ignore_ascii_case("output") {
15993            let selected = match p.value {
15994                ParamValue::EnumString(v) => v,
15995                _ => {
15996                    return Err(IndicatorDispatchError::InvalidParam {
15997                        indicator: indicator.to_string(),
15998                        key: "output".to_string(),
15999                        reason: "expected EnumString".to_string(),
16000                    })
16001                }
16002            };
16003            if !selected.eq_ignore_ascii_case(output_id) {
16004                return Err(IndicatorDispatchError::InvalidParam {
16005                    indicator: indicator.to_string(),
16006                    key: "output".to_string(),
16007                    reason: format!(
16008                        "param output '{}' does not match requested output_id '{}'",
16009                        selected, output_id
16010                    ),
16011                });
16012            }
16013        }
16014        let value = match p.value {
16015            ParamValue::Int(v) => MaBatchParamValue::Int(v),
16016            ParamValue::Float(v) => {
16017                if !v.is_finite() {
16018                    return Err(IndicatorDispatchError::InvalidParam {
16019                        indicator: indicator.to_string(),
16020                        key: p.key.to_string(),
16021                        reason: "expected finite float".to_string(),
16022                    });
16023                }
16024                MaBatchParamValue::Float(v)
16025            }
16026            ParamValue::Bool(v) => MaBatchParamValue::Bool(v),
16027            ParamValue::EnumString(v) => MaBatchParamValue::EnumString(v),
16028        };
16029        out.push(MaBatchParamKV { key: p.key, value });
16030    }
16031    Ok(out)
16032}
16033
16034fn extract_slice_input<'a>(
16035    indicator: &str,
16036    data: IndicatorDataRef<'a>,
16037    default_source: &'a str,
16038) -> Result<&'a [f64], IndicatorDispatchError> {
16039    match data {
16040        IndicatorDataRef::Slice { values } => Ok(values),
16041        IndicatorDataRef::Candles { candles, source } => {
16042            Ok(source_type(candles, source.unwrap_or(default_source)))
16043        }
16044        IndicatorDataRef::Ohlc { close, .. } => Ok(close),
16045        IndicatorDataRef::Ohlcv { close, .. } => Ok(close),
16046        IndicatorDataRef::CloseVolume { close, .. } => Ok(close),
16047        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
16048            indicator: indicator.to_string(),
16049            input: IndicatorInputKind::Slice,
16050        }),
16051    }
16052}
16053
16054fn extract_ohlc_input<'a>(
16055    indicator: &str,
16056    data: IndicatorDataRef<'a>,
16057) -> Result<(&'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16058    match data {
16059        IndicatorDataRef::Candles { candles, .. } => Ok((
16060            candles.high.as_slice(),
16061            candles.low.as_slice(),
16062            candles.close.as_slice(),
16063        )),
16064        IndicatorDataRef::Ohlc {
16065            high,
16066            low,
16067            close,
16068            open,
16069        } => {
16070            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
16071            Ok((high, low, close))
16072        }
16073        IndicatorDataRef::Ohlcv {
16074            high,
16075            low,
16076            close,
16077            open,
16078            volume,
16079        } => {
16080            ensure_same_len_5(
16081                indicator,
16082                open.len(),
16083                high.len(),
16084                low.len(),
16085                close.len(),
16086                volume.len(),
16087            )?;
16088            Ok((high, low, close))
16089        }
16090        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16091            indicator: indicator.to_string(),
16092            input: IndicatorInputKind::Ohlc,
16093        }),
16094    }
16095}
16096
16097fn extract_ohlc_full_input<'a>(
16098    indicator: &str,
16099    data: IndicatorDataRef<'a>,
16100) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16101    match data {
16102        IndicatorDataRef::Candles { candles, .. } => Ok((
16103            candles.open.as_slice(),
16104            candles.high.as_slice(),
16105            candles.low.as_slice(),
16106            candles.close.as_slice(),
16107        )),
16108        IndicatorDataRef::Ohlc {
16109            open,
16110            high,
16111            low,
16112            close,
16113        } => {
16114            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
16115            Ok((open, high, low, close))
16116        }
16117        IndicatorDataRef::Ohlcv {
16118            open,
16119            high,
16120            low,
16121            close,
16122            volume,
16123        } => {
16124            ensure_same_len_5(
16125                indicator,
16126                open.len(),
16127                high.len(),
16128                low.len(),
16129                close.len(),
16130                volume.len(),
16131            )?;
16132            Ok((open, high, low, close))
16133        }
16134        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16135            indicator: indicator.to_string(),
16136            input: IndicatorInputKind::Ohlc,
16137        }),
16138    }
16139}
16140
16141fn extract_ohlcv_full_input<'a>(
16142    indicator: &str,
16143    data: IndicatorDataRef<'a>,
16144) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16145    match data {
16146        IndicatorDataRef::Candles { candles, .. } => Ok((
16147            candles.open.as_slice(),
16148            candles.high.as_slice(),
16149            candles.low.as_slice(),
16150            candles.close.as_slice(),
16151            candles.volume.as_slice(),
16152        )),
16153        IndicatorDataRef::Ohlcv {
16154            open,
16155            high,
16156            low,
16157            close,
16158            volume,
16159        } => {
16160            ensure_same_len_5(
16161                indicator,
16162                open.len(),
16163                high.len(),
16164                low.len(),
16165                close.len(),
16166                volume.len(),
16167            )?;
16168            Ok((open, high, low, close, volume))
16169        }
16170        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16171            indicator: indicator.to_string(),
16172            input: IndicatorInputKind::Ohlcv,
16173        }),
16174    }
16175}
16176
16177fn extract_high_low_input<'a>(
16178    indicator: &str,
16179    data: IndicatorDataRef<'a>,
16180) -> Result<(&'a [f64], &'a [f64]), IndicatorDispatchError> {
16181    match data {
16182        IndicatorDataRef::Candles { candles, .. } => {
16183            Ok((candles.high.as_slice(), candles.low.as_slice()))
16184        }
16185        IndicatorDataRef::Ohlc {
16186            high,
16187            low,
16188            open,
16189            close,
16190        } => {
16191            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
16192            Ok((high, low))
16193        }
16194        IndicatorDataRef::Ohlcv {
16195            high,
16196            low,
16197            open,
16198            close,
16199            volume,
16200        } => {
16201            ensure_same_len_5(
16202                indicator,
16203                open.len(),
16204                high.len(),
16205                low.len(),
16206                close.len(),
16207                volume.len(),
16208            )?;
16209            Ok((high, low))
16210        }
16211        IndicatorDataRef::HighLow { high, low } => {
16212            ensure_same_len_2(indicator, high.len(), low.len())?;
16213            Ok((high, low))
16214        }
16215        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16216            indicator: indicator.to_string(),
16217            input: IndicatorInputKind::HighLow,
16218        }),
16219    }
16220}
16221
16222fn extract_hlcv_input<'a>(
16223    indicator: &str,
16224    data: IndicatorDataRef<'a>,
16225) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16226    match data {
16227        IndicatorDataRef::Candles { candles, .. } => Ok((
16228            candles.high.as_slice(),
16229            candles.low.as_slice(),
16230            candles.close.as_slice(),
16231            candles.volume.as_slice(),
16232        )),
16233        IndicatorDataRef::Ohlcv {
16234            open,
16235            high,
16236            low,
16237            close,
16238            volume,
16239        } => {
16240            ensure_same_len_5(
16241                indicator,
16242                open.len(),
16243                high.len(),
16244                low.len(),
16245                close.len(),
16246                volume.len(),
16247            )?;
16248            Ok((high, low, close, volume))
16249        }
16250        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16251            indicator: indicator.to_string(),
16252            input: IndicatorInputKind::Ohlcv,
16253        }),
16254    }
16255}
16256
16257fn extract_volume_input<'a>(
16258    indicator: &str,
16259    data: IndicatorDataRef<'a>,
16260) -> Result<&'a [f64], IndicatorDispatchError> {
16261    match data {
16262        IndicatorDataRef::Slice { values } => Ok(values),
16263        IndicatorDataRef::Candles { candles, source } => {
16264            Ok(source_type(candles, source.unwrap_or("volume")))
16265        }
16266        IndicatorDataRef::CloseVolume { close, volume } => {
16267            ensure_same_len_2(indicator, close.len(), volume.len())?;
16268            Ok(volume)
16269        }
16270        IndicatorDataRef::Ohlcv {
16271            open,
16272            high,
16273            low,
16274            close,
16275            volume,
16276        } => {
16277            ensure_same_len_5(
16278                indicator,
16279                open.len(),
16280                high.len(),
16281                low.len(),
16282                close.len(),
16283                volume.len(),
16284            )?;
16285            Ok(volume)
16286        }
16287        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16288            indicator: indicator.to_string(),
16289            input: IndicatorInputKind::Slice,
16290        }),
16291    }
16292}
16293
16294fn extract_close_volume_input<'a>(
16295    indicator: &str,
16296    data: IndicatorDataRef<'a>,
16297    default_close_source: &'a str,
16298) -> Result<(&'a [f64], &'a [f64]), IndicatorDispatchError> {
16299    match data {
16300        IndicatorDataRef::CloseVolume { close, volume } => {
16301            ensure_same_len_2(indicator, close.len(), volume.len())?;
16302            Ok((close, volume))
16303        }
16304        IndicatorDataRef::Ohlcv {
16305            close,
16306            volume,
16307            open,
16308            high,
16309            low,
16310        } => {
16311            ensure_same_len_5(
16312                indicator,
16313                open.len(),
16314                high.len(),
16315                low.len(),
16316                close.len(),
16317                volume.len(),
16318            )?;
16319            Ok((close, volume))
16320        }
16321        IndicatorDataRef::Candles { candles, source } => {
16322            let close = source_type(candles, source.unwrap_or(default_close_source));
16323            let volume = candles.volume.as_slice();
16324            ensure_same_len_2(indicator, close.len(), volume.len())?;
16325            Ok((close, volume))
16326        }
16327        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16328            indicator: indicator.to_string(),
16329            input: IndicatorInputKind::CloseVolume,
16330        }),
16331    }
16332}
16333
16334fn f64_output(output_id: &str, rows: usize, cols: usize, values: Vec<f64>) -> IndicatorBatchOutput {
16335    IndicatorBatchOutput {
16336        output_id: output_id.to_string(),
16337        rows,
16338        cols,
16339        values_f64: Some(values),
16340        values_i32: None,
16341        values_bool: None,
16342    }
16343}
16344
16345fn bool_output(
16346    output_id: &str,
16347    rows: usize,
16348    cols: usize,
16349    values: Vec<bool>,
16350) -> IndicatorBatchOutput {
16351    IndicatorBatchOutput {
16352        output_id: output_id.to_string(),
16353        rows,
16354        cols,
16355        values_f64: None,
16356        values_i32: None,
16357        values_bool: Some(values),
16358    }
16359}
16360
16361fn expect_value_output(indicator: &str, output_id: &str) -> Result<(), IndicatorDispatchError> {
16362    if output_id.eq_ignore_ascii_case("value") {
16363        return Ok(());
16364    }
16365    Err(IndicatorDispatchError::UnknownOutput {
16366        indicator: indicator.to_string(),
16367        output: output_id.to_string(),
16368    })
16369}
16370
16371fn ensure_len(indicator: &str, expected: usize, got: usize) -> Result<(), IndicatorDispatchError> {
16372    if expected == got {
16373        return Ok(());
16374    }
16375    Err(IndicatorDispatchError::DataLengthMismatch {
16376        details: format!("{indicator}: expected output length {expected}, got {got}"),
16377    })
16378}
16379
16380fn ensure_same_len_2(indicator: &str, a: usize, b: usize) -> Result<(), IndicatorDispatchError> {
16381    if a == b {
16382        return Ok(());
16383    }
16384    Err(IndicatorDispatchError::DataLengthMismatch {
16385        details: format!("{indicator}: expected equal lengths, got {a} and {b}"),
16386    })
16387}
16388
16389fn ensure_same_len_3(
16390    indicator: &str,
16391    a: usize,
16392    b: usize,
16393    c: usize,
16394) -> Result<(), IndicatorDispatchError> {
16395    if a == b && b == c {
16396        return Ok(());
16397    }
16398    Err(IndicatorDispatchError::DataLengthMismatch {
16399        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}"),
16400    })
16401}
16402
16403fn ensure_same_len_4(
16404    indicator: &str,
16405    a: usize,
16406    b: usize,
16407    c: usize,
16408    d: usize,
16409) -> Result<(), IndicatorDispatchError> {
16410    if a == b && b == c && c == d {
16411        return Ok(());
16412    }
16413    Err(IndicatorDispatchError::DataLengthMismatch {
16414        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}, {d}"),
16415    })
16416}
16417
16418fn ensure_same_len_5(
16419    indicator: &str,
16420    a: usize,
16421    b: usize,
16422    c: usize,
16423    d: usize,
16424    e: usize,
16425) -> Result<(), IndicatorDispatchError> {
16426    if a == b && b == c && c == d && d == e {
16427        return Ok(());
16428    }
16429    Err(IndicatorDispatchError::DataLengthMismatch {
16430        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}, {d}, {e}"),
16431    })
16432}
16433
16434fn has_key(params: &[ParamKV<'_>], key: &str) -> bool {
16435    params.iter().any(|kv| kv.key.eq_ignore_ascii_case(key))
16436}
16437
16438fn find_param<'a>(params: &'a [ParamKV<'a>], key: &str) -> Option<&'a ParamValue<'a>> {
16439    params
16440        .iter()
16441        .rev()
16442        .find(|kv| kv.key.eq_ignore_ascii_case(key))
16443        .map(|kv| &kv.value)
16444}
16445
16446fn get_usize_param(
16447    indicator: &str,
16448    params: &[ParamKV<'_>],
16449    key: &str,
16450    default: usize,
16451) -> Result<usize, IndicatorDispatchError> {
16452    match find_param(params, key) {
16453        Some(v) => parse_usize_param_value(indicator, key, v),
16454        None => Ok(default),
16455    }
16456}
16457
16458fn get_usize_param_with_aliases(
16459    indicator: &str,
16460    params: &[ParamKV<'_>],
16461    keys: &[&str],
16462    default: usize,
16463) -> Result<usize, IndicatorDispatchError> {
16464    for key in keys {
16465        if let Some(v) = find_param(params, key) {
16466            return parse_usize_param_value(indicator, key, v);
16467        }
16468    }
16469    Ok(default)
16470}
16471
16472fn get_f64_param_with_aliases(
16473    indicator: &str,
16474    params: &[ParamKV<'_>],
16475    keys: &[&str],
16476    default: f64,
16477) -> Result<f64, IndicatorDispatchError> {
16478    for key in keys {
16479        match find_param(params, key) {
16480            Some(ParamValue::Int(v)) => return Ok(*v as f64),
16481            Some(ParamValue::Float(v)) => {
16482                if v.is_finite() {
16483                    return Ok(*v);
16484                }
16485                return Err(IndicatorDispatchError::InvalidParam {
16486                    indicator: indicator.to_string(),
16487                    key: key.to_string(),
16488                    reason: "expected finite float".to_string(),
16489                });
16490            }
16491            Some(_) => {
16492                return Err(IndicatorDispatchError::InvalidParam {
16493                    indicator: indicator.to_string(),
16494                    key: key.to_string(),
16495                    reason: "expected Int or Float".to_string(),
16496                });
16497            }
16498            None => continue,
16499        }
16500    }
16501    Ok(default)
16502}
16503
16504fn parse_usize_param_value(
16505    indicator: &str,
16506    key: &str,
16507    value: &ParamValue<'_>,
16508) -> Result<usize, IndicatorDispatchError> {
16509    match value {
16510        ParamValue::Int(v) => {
16511            if *v < 0 {
16512                return Err(IndicatorDispatchError::InvalidParam {
16513                    indicator: indicator.to_string(),
16514                    key: key.to_string(),
16515                    reason: "expected integer >= 0".to_string(),
16516                });
16517            }
16518            Ok(*v as usize)
16519        }
16520        ParamValue::Float(v) => {
16521            if !v.is_finite() {
16522                return Err(IndicatorDispatchError::InvalidParam {
16523                    indicator: indicator.to_string(),
16524                    key: key.to_string(),
16525                    reason: "expected finite number".to_string(),
16526                });
16527            }
16528            if *v < 0.0 {
16529                return Err(IndicatorDispatchError::InvalidParam {
16530                    indicator: indicator.to_string(),
16531                    key: key.to_string(),
16532                    reason: "expected number >= 0".to_string(),
16533                });
16534            }
16535            let r = v.round();
16536            if (*v - r).abs() > 1e-9 {
16537                return Err(IndicatorDispatchError::InvalidParam {
16538                    indicator: indicator.to_string(),
16539                    key: key.to_string(),
16540                    reason: "expected integer value".to_string(),
16541                });
16542            }
16543            Ok(r as usize)
16544        }
16545        _ => Err(IndicatorDispatchError::InvalidParam {
16546            indicator: indicator.to_string(),
16547            key: key.to_string(),
16548            reason: "expected Int or Float".to_string(),
16549        }),
16550    }
16551}
16552
16553fn get_f64_param(
16554    indicator: &str,
16555    params: &[ParamKV<'_>],
16556    key: &str,
16557    default: f64,
16558) -> Result<f64, IndicatorDispatchError> {
16559    match find_param(params, key) {
16560        Some(ParamValue::Int(v)) => Ok(*v as f64),
16561        Some(ParamValue::Float(v)) => {
16562            if v.is_finite() {
16563                Ok(*v)
16564            } else {
16565                Err(IndicatorDispatchError::InvalidParam {
16566                    indicator: indicator.to_string(),
16567                    key: key.to_string(),
16568                    reason: "expected finite float".to_string(),
16569                })
16570            }
16571        }
16572        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16573            indicator: indicator.to_string(),
16574            key: key.to_string(),
16575            reason: "expected Int or Float".to_string(),
16576        }),
16577        None => Ok(default),
16578    }
16579}
16580
16581fn get_bool_param(
16582    indicator: &str,
16583    params: &[ParamKV<'_>],
16584    key: &str,
16585    default: bool,
16586) -> Result<bool, IndicatorDispatchError> {
16587    match find_param(params, key) {
16588        Some(ParamValue::Bool(v)) => Ok(*v),
16589        Some(ParamValue::Int(v)) => match *v {
16590            0 => Ok(false),
16591            1 => Ok(true),
16592            _ => Err(IndicatorDispatchError::InvalidParam {
16593                indicator: indicator.to_string(),
16594                key: key.to_string(),
16595                reason: "expected Bool or Int(0/1)".to_string(),
16596            }),
16597        },
16598        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16599            indicator: indicator.to_string(),
16600            key: key.to_string(),
16601            reason: "expected Bool".to_string(),
16602        }),
16603        None => Ok(default),
16604    }
16605}
16606
16607fn get_enum_string_param<'a>(
16608    indicator: &str,
16609    params: &'a [ParamKV<'a>],
16610    key: &str,
16611    default: &'a str,
16612) -> Result<&'a str, IndicatorDispatchError> {
16613    match find_param(params, key) {
16614        Some(ParamValue::EnumString(v)) => Ok(v),
16615        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16616            indicator: indicator.to_string(),
16617            key: key.to_string(),
16618            reason: "expected EnumString".to_string(),
16619        }),
16620        None => Ok(default),
16621    }
16622}
16623
16624fn get_i32_param(
16625    indicator: &str,
16626    params: &[ParamKV<'_>],
16627    key: &str,
16628    default: i32,
16629) -> Result<i32, IndicatorDispatchError> {
16630    match find_param(params, key) {
16631        Some(ParamValue::Int(v)) => {
16632            if *v < i32::MIN as i64 || *v > i32::MAX as i64 {
16633                return Err(IndicatorDispatchError::InvalidParam {
16634                    indicator: indicator.to_string(),
16635                    key: key.to_string(),
16636                    reason: "integer out of i32 range".to_string(),
16637                });
16638            }
16639            Ok(*v as i32)
16640        }
16641        Some(ParamValue::Float(v)) => {
16642            if !v.is_finite() {
16643                return Err(IndicatorDispatchError::InvalidParam {
16644                    indicator: indicator.to_string(),
16645                    key: key.to_string(),
16646                    reason: "expected finite number".to_string(),
16647                });
16648            }
16649            let r = v.round();
16650            if (*v - r).abs() > 1e-9 || r < i32::MIN as f64 || r > i32::MAX as f64 {
16651                return Err(IndicatorDispatchError::InvalidParam {
16652                    indicator: indicator.to_string(),
16653                    key: key.to_string(),
16654                    reason: "expected i32-compatible whole number".to_string(),
16655                });
16656            }
16657            Ok(r as i32)
16658        }
16659        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16660            indicator: indicator.to_string(),
16661            key: key.to_string(),
16662            reason: "expected Int or Float".to_string(),
16663        }),
16664        None => Ok(default),
16665    }
16666}
16667
16668fn get_enum_param(
16669    indicator: &str,
16670    params: &[ParamKV<'_>],
16671    key: &str,
16672    default: &str,
16673) -> Result<String, IndicatorDispatchError> {
16674    match find_param(params, key) {
16675        Some(ParamValue::EnumString(v)) => Ok((*v).to_string()),
16676        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16677            indicator: indicator.to_string(),
16678            key: key.to_string(),
16679            reason: "expected EnumString".to_string(),
16680        }),
16681        None => Ok(default.to_string()),
16682    }
16683}
16684
16685#[cfg(test)]
16686mod tests {
16687    use super::*;
16688    use crate::indicators::absolute_strength_index_oscillator::{
16689        absolute_strength_index_oscillator_with_kernel, AbsoluteStrengthIndexOscillatorInput,
16690        AbsoluteStrengthIndexOscillatorParams,
16691    };
16692    use crate::indicators::ad::{ad_with_kernel, AdInput, AdParams};
16693    use crate::indicators::adaptive_bandpass_trigger_oscillator::{
16694        adaptive_bandpass_trigger_oscillator_with_kernel, AdaptiveBandpassTriggerOscillatorInput,
16695        AdaptiveBandpassTriggerOscillatorParams,
16696    };
16697    use crate::indicators::advance_decline_line::{
16698        advance_decline_line_with_kernel, AdvanceDeclineLineInput, AdvanceDeclineLineParams,
16699    };
16700    use crate::indicators::accumulation_swing_index::{
16701        accumulation_swing_index_with_kernel, AccumulationSwingIndexInput,
16702        AccumulationSwingIndexParams,
16703    };
16704    use crate::indicators::adx::{adx_with_kernel, AdxInput, AdxParams};
16705    use crate::indicators::ao::{ao_with_kernel, AoInput, AoParams};
16706    use crate::indicators::apo::{apo_with_kernel, ApoInput, ApoParams};
16707    use crate::indicators::atr_percentile::{
16708        atr_percentile_with_kernel, AtrPercentileInput, AtrPercentileParams,
16709    };
16710    use crate::indicators::bull_power_vs_bear_power::{
16711        bull_power_vs_bear_power_with_kernel, BullPowerVsBearPowerInput, BullPowerVsBearPowerParams,
16712    };
16713    use crate::indicators::cg::{cg_with_kernel, CgInput, CgParams};
16714    use crate::indicators::cmo::{cmo_with_kernel, CmoInput, CmoParams};
16715    use crate::indicators::decisionpoint_breadth_swenlin_trading_oscillator::{
16716        decisionpoint_breadth_swenlin_trading_oscillator_with_kernel,
16717        DecisionPointBreadthSwenlinTradingOscillatorInput,
16718        DecisionPointBreadthSwenlinTradingOscillatorParams,
16719    };
16720    use crate::indicators::demand_index::{
16721        demand_index_with_kernel, DemandIndexInput, DemandIndexParams,
16722    };
16723    use crate::indicators::deviation::{deviation_with_kernel, DeviationInput, DeviationParams};
16724    use crate::indicators::dx::{
16725        dx_batch_with_kernel, dx_with_kernel, DxBatchRange, DxInput, DxParams,
16726    };
16727    use crate::indicators::efi::{efi_with_kernel, EfiInput, EfiParams};
16728    use crate::indicators::cycle_channel_oscillator::{
16729        cycle_channel_oscillator_with_kernel, CycleChannelOscillatorInput,
16730        CycleChannelOscillatorParams,
16731    };
16732    use crate::indicators::daily_factor::{
16733        daily_factor_with_kernel, DailyFactorInput, DailyFactorParams,
16734    };
16735    use crate::indicators::ehlers_adaptive_cyber_cycle::{
16736        ehlers_adaptive_cyber_cycle_with_kernel, EhlersAdaptiveCyberCycleInput,
16737        EhlersAdaptiveCyberCycleParams,
16738    };
16739    use crate::indicators::ehlers_linear_extrapolation_predictor::{
16740        ehlers_linear_extrapolation_predictor_with_kernel, EhlersLinearExtrapolationPredictorInput,
16741        EhlersLinearExtrapolationPredictorParams,
16742    };
16743    use crate::indicators::ehlers_simple_cycle_indicator::{
16744        ehlers_simple_cycle_indicator_with_kernel, EhlersSimpleCycleIndicatorInput,
16745        EhlersSimpleCycleIndicatorParams,
16746    };
16747    use crate::indicators::ehlers_smoothed_adaptive_momentum::{
16748        ehlers_smoothed_adaptive_momentum_with_kernel, EhlersSmoothedAdaptiveMomentumInput,
16749        EhlersSmoothedAdaptiveMomentumParams,
16750    };
16751    use crate::indicators::ewma_volatility::{
16752        ewma_volatility_with_kernel, EwmaVolatilityInput, EwmaVolatilityParams,
16753    };
16754    use crate::indicators::fibonacci_entry_bands::{
16755        fibonacci_entry_bands_with_kernel, FibonacciEntryBandsInput, FibonacciEntryBandsParams,
16756    };
16757    use crate::indicators::fibonacci_trailing_stop::{
16758        fibonacci_trailing_stop_with_kernel, FibonacciTrailingStopInput,
16759        FibonacciTrailingStopParams,
16760    };
16761    use crate::indicators::fosc::{fosc_with_kernel, FoscInput, FoscParams};
16762    use crate::indicators::garman_klass_volatility::{
16763        garman_klass_volatility_with_kernel, GarmanKlassVolatilityInput,
16764        GarmanKlassVolatilityParams,
16765    };
16766    use crate::indicators::gopalakrishnan_range_index::{
16767        gopalakrishnan_range_index_with_kernel, GopalakrishnanRangeIndexInput,
16768        GopalakrishnanRangeIndexParams,
16769    };
16770    use crate::indicators::grover_llorens_cycle_oscillator::{
16771        grover_llorens_cycle_oscillator_with_kernel, GroverLlorensCycleOscillatorInput,
16772        GroverLlorensCycleOscillatorParams,
16773    };
16774    use crate::indicators::hema_trend_levels::{
16775        hema_trend_levels_with_kernel, HemaTrendLevelsInput, HemaTrendLevelsParams,
16776    };
16777    use crate::indicators::historical_volatility::{
16778        historical_volatility_with_kernel, HistoricalVolatilityInput, HistoricalVolatilityParams,
16779    };
16780    use crate::indicators::historical_volatility_percentile::{
16781        historical_volatility_percentile_with_kernel, HistoricalVolatilityPercentileInput,
16782        HistoricalVolatilityPercentileParams,
16783    };
16784    use crate::indicators::hull_butterfly_oscillator::{
16785        hull_butterfly_oscillator_with_kernel, HullButterflyOscillatorInput,
16786        HullButterflyOscillatorParams,
16787    };
16788    use crate::indicators::ichimoku_oscillator::{
16789        ichimoku_oscillator_with_kernel, IchimokuOscillatorInput,
16790        IchimokuOscillatorNormalizeMode, IchimokuOscillatorParams,
16791    };
16792    use crate::indicators::ift_rsi::{ift_rsi_with_kernel, IftRsiInput, IftRsiParams};
16793    use crate::indicators::intraday_momentum_index::{
16794        intraday_momentum_index_with_kernel, IntradayMomentumIndexInput,
16795        IntradayMomentumIndexParams,
16796    };
16797    use crate::indicators::kvo::{kvo_with_kernel, KvoInput, KvoParams};
16798    use crate::indicators::l2_ehlers_signal_to_noise::{
16799        l2_ehlers_signal_to_noise_with_kernel, L2EhlersSignalToNoiseInput,
16800        L2EhlersSignalToNoiseParams,
16801    };
16802    use crate::indicators::linearreg_angle::{
16803        linearreg_angle_with_kernel, Linearreg_angleInput, Linearreg_angleParams,
16804    };
16805    use crate::indicators::linearreg_intercept::{
16806        linearreg_intercept_with_kernel, LinearRegInterceptInput, LinearRegInterceptParams,
16807    };
16808    use crate::indicators::linearreg_slope::{
16809        linearreg_slope_with_kernel, LinearRegSlopeInput, LinearRegSlopeParams,
16810    };
16811    use crate::indicators::macd::{macd_with_kernel, MacdInput, MacdParams};
16812    use crate::indicators::macd_wave_signal_pro::{
16813        macd_wave_signal_pro_with_kernel, MacdWaveSignalProInput,
16814    };
16815    use crate::indicators::mean_ad::{mean_ad_with_kernel, MeanAdInput, MeanAdParams};
16816    use crate::indicators::medprice::{medprice_with_kernel, MedpriceInput, MedpriceParams};
16817    use crate::indicators::mesa_stochastic_multi_length::{
16818        mesa_stochastic_multi_length_with_kernel, MesaStochasticMultiLengthInput,
16819        MesaStochasticMultiLengthParams,
16820    };
16821    use crate::indicators::mfi::{
16822        mfi_batch_with_kernel, mfi_with_kernel, MfiBatchRange, MfiInput, MfiParams,
16823    };
16824    use crate::indicators::monotonicity_index::{
16825        monotonicity_index_with_kernel, MonotonicityIndexInput, MonotonicityIndexMode,
16826        MonotonicityIndexParams,
16827    };
16828    use crate::indicators::moving_averages::ma::MaData;
16829    use crate::indicators::moving_averages::ma_batch::{
16830        ma_batch_with_kernel_and_typed_params, MaBatchParamKV, MaBatchParamValue,
16831    };
16832    use crate::indicators::multi_length_stochastic_average::{
16833        multi_length_stochastic_average_with_kernel, MultiLengthStochasticAverageInput,
16834        MultiLengthStochasticAverageParams,
16835    };
16836    use crate::indicators::natr::{natr_with_kernel, NatrInput, NatrParams};
16837    use crate::indicators::neighboring_trailing_stop::{
16838        neighboring_trailing_stop_with_kernel, NeighboringTrailingStopInput,
16839        NeighboringTrailingStopParams,
16840    };
16841    use crate::indicators::percentile_nearest_rank::{
16842        percentile_nearest_rank_with_kernel, PercentileNearestRankInput,
16843        PercentileNearestRankParams,
16844    };
16845    use crate::indicators::ppo::{ppo_with_kernel, PpoInput, PpoParams};
16846    use crate::indicators::price_moving_average_ratio_percentile::{
16847        price_moving_average_ratio_percentile_with_kernel, PriceMovingAverageRatioPercentileInput,
16848        PriceMovingAverageRatioPercentileLineMode, PriceMovingAverageRatioPercentileMaType,
16849        PriceMovingAverageRatioPercentileParams,
16850    };
16851    use crate::indicators::premier_rsi_oscillator::{
16852        premier_rsi_oscillator_with_kernel, PremierRsiOscillatorInput, PremierRsiOscillatorParams,
16853    };
16854    use crate::indicators::pvi::{pvi_with_kernel, PviInput, PviParams};
16855    use crate::indicators::random_walk_index::{
16856        random_walk_index_with_kernel, RandomWalkIndexInput, RandomWalkIndexParams,
16857    };
16858    use crate::indicators::registry::{list_indicators, IndicatorParamKind};
16859    use crate::indicators::spearman_correlation::{
16860        spearman_correlation_with_kernel, SpearmanCorrelationInput, SpearmanCorrelationParams,
16861    };
16862    use crate::indicators::squeeze_index::{
16863        squeeze_index_with_kernel, SqueezeIndexInput, SqueezeIndexParams,
16864    };
16865    use crate::indicators::stochastic_distance::{
16866        stochastic_distance_with_kernel, StochasticDistanceInput, StochasticDistanceParams,
16867    };
16868    use crate::indicators::trix::{
16869        trix_batch_with_kernel, trix_with_kernel, TrixBatchRange, TrixInput, TrixParams,
16870    };
16871    use crate::indicators::trend_trigger_factor::{
16872        trend_trigger_factor_with_kernel, TrendTriggerFactorInput, TrendTriggerFactorParams,
16873    };
16874    use crate::indicators::ttm_trend::{ttm_trend_with_kernel, TtmTrendInput, TtmTrendParams};
16875    use crate::indicators::velocity_acceleration_indicator::{
16876        velocity_acceleration_indicator_with_kernel, VelocityAccelerationIndicatorInput,
16877        VelocityAccelerationIndicatorParams,
16878    };
16879    use crate::indicators::velocity_acceleration_convergence_divergence_indicator::{
16880        velocity_acceleration_convergence_divergence_indicator_with_kernel,
16881        VelocityAccelerationConvergenceDivergenceIndicatorInput,
16882        VelocityAccelerationConvergenceDivergenceIndicatorParams,
16883    };
16884    use crate::indicators::volatility_quality_index::{
16885        volatility_quality_index_with_kernel, VolatilityQualityIndexInput,
16886        VolatilityQualityIndexParams,
16887    };
16888    use crate::indicators::volatility_ratio_adaptive_rsx::{
16889        volatility_ratio_adaptive_rsx_with_kernel, VolatilityRatioAdaptiveRsxInput,
16890        VolatilityRatioAdaptiveRsxParams,
16891    };
16892    use crate::indicators::volume_energy_reservoirs::{
16893        volume_energy_reservoirs_with_kernel, VolumeEnergyReservoirsInput,
16894        VolumeEnergyReservoirsParams,
16895    };
16896    use crate::indicators::volume_zone_oscillator::{
16897        volume_zone_oscillator_with_kernel, VolumeZoneOscillatorInput, VolumeZoneOscillatorParams,
16898    };
16899    use crate::indicators::vpci::{vpci_with_kernel, VpciInput, VpciParams};
16900    use crate::indicators::vwap_deviation_oscillator::{
16901        vwap_deviation_oscillator_with_kernel, VwapDeviationMode, VwapDeviationOscillatorInput,
16902        VwapDeviationOscillatorParams, VwapDeviationSessionMode,
16903    };
16904    use crate::indicators::vwap_zscore_with_signals::{
16905        vwap_zscore_with_signals_with_kernel, VwapZscoreWithSignalsInput,
16906        VwapZscoreWithSignalsParams,
16907    };
16908    use crate::indicators::yang_zhang_volatility::{
16909        yang_zhang_volatility_with_kernel, YangZhangVolatilityInput, YangZhangVolatilityParams,
16910    };
16911    use crate::indicators::zscore::{zscore_with_kernel, ZscoreInput, ZscoreParams};
16912    use crate::utilities::data_loader::Candles;
16913    use crate::utilities::enums::Kernel;
16914    use std::time::Instant;
16915
16916    fn sample_series() -> Vec<f64> {
16917        (1..=64).map(|v| v as f64).collect()
16918    }
16919
16920    fn sample_ohlc() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
16921        let open: Vec<f64> = (0..128).map(|i| 100.0 + (i as f64 * 0.1)).collect();
16922        let high: Vec<f64> = open.iter().map(|v| v + 1.25).collect();
16923        let low: Vec<f64> = open.iter().map(|v| v - 1.1).collect();
16924        let close: Vec<f64> = open.iter().map(|v| v + 0.3).collect();
16925        (open, high, low, close)
16926    }
16927
16928    fn sample_candles() -> crate::utilities::data_loader::Candles {
16929        let (open, high, low, close) = sample_ohlc();
16930        let volume: Vec<f64> = (0..close.len()).map(|i| 1000.0 + (i as f64)).collect();
16931        let timestamp: Vec<i64> = (0..close.len()).map(|i| i as i64).collect();
16932        crate::utilities::data_loader::Candles::new(timestamp, open, high, low, close, volume)
16933    }
16934
16935    fn assert_series_eq(actual: &[f64], expected: &[f64], tol: f64) {
16936        assert_eq!(actual.len(), expected.len());
16937        for i in 0..actual.len() {
16938            let a = actual[i];
16939            let b = expected[i];
16940            if a.is_nan() && b.is_nan() {
16941                continue;
16942            }
16943            assert!(
16944                (a - b).abs() <= tol,
16945                "mismatch at index {i}: actual={a}, expected={b}, tol={tol}"
16946            );
16947        }
16948    }
16949
16950    #[test]
16951    fn unknown_indicator_is_rejected() {
16952        let data = sample_series();
16953        let req = IndicatorBatchRequest {
16954            indicator_id: "not_real",
16955            output_id: None,
16956            data: IndicatorDataRef::Slice { values: &data },
16957            combos: &[],
16958            kernel: Kernel::Auto,
16959        };
16960        let err = compute_cpu_batch(req).unwrap_err();
16961        assert!(matches!(
16962            err,
16963            IndicatorDispatchError::UnknownIndicator { .. }
16964        ));
16965    }
16966
16967    #[test]
16968    fn bucket_b_ma_indicator_is_supported() {
16969        let data = sample_series();
16970        let combos = [IndicatorParamSet { params: &[] }];
16971        let req = IndicatorBatchRequest {
16972            indicator_id: "mama",
16973            output_id: Some("mama"),
16974            data: IndicatorDataRef::Slice { values: &data },
16975            combos: &combos,
16976            kernel: Kernel::Auto,
16977        };
16978        let out = compute_cpu_batch(req).unwrap();
16979        assert_eq!(out.rows, 1);
16980        assert_eq!(out.cols, data.len());
16981        assert!(out.values_f64.is_some());
16982    }
16983
16984    #[test]
16985    fn strict_mode_rejects_convenience_mfi_ohlcv() {
16986        let (open, high, low, close) = sample_ohlc();
16987        let volume: Vec<f64> = (0..close.len()).map(|i| 1200.0 + (i as f64)).collect();
16988        let combo = [ParamKV {
16989            key: "period",
16990            value: ParamValue::Int(14),
16991        }];
16992        let combos = [IndicatorParamSet { params: &combo }];
16993        let req = IndicatorBatchRequest {
16994            indicator_id: "mfi",
16995            output_id: Some("value"),
16996            data: IndicatorDataRef::Ohlcv {
16997                open: &open,
16998                high: &high,
16999                low: &low,
17000                close: &close,
17001                volume: &volume,
17002            },
17003            combos: &combos,
17004            kernel: Kernel::Auto,
17005        };
17006        let err = compute_cpu_batch_strict(req).unwrap_err();
17007        match err {
17008            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
17009                assert_eq!(indicator, "mfi");
17010                assert_eq!(input, IndicatorInputKind::CloseVolume);
17011            }
17012            other => panic!("expected MissingRequiredInput, got {other:?}"),
17013        }
17014    }
17015
17016    #[test]
17017    fn strict_mode_accepts_precomputed_mfi_close_volume() {
17018        let (_open, high, low, close) = sample_ohlc();
17019        let volume: Vec<f64> = (0..close.len())
17020            .map(|i| 1000.0 + (i as f64 * 2.0))
17021            .collect();
17022        let typical: Vec<f64> = high
17023            .iter()
17024            .zip(&low)
17025            .zip(&close)
17026            .map(|((h, l), c)| (h + l + c) / 3.0)
17027            .collect();
17028        let combo = [ParamKV {
17029            key: "period",
17030            value: ParamValue::Int(14),
17031        }];
17032        let combos = [IndicatorParamSet { params: &combo }];
17033        let req = IndicatorBatchRequest {
17034            indicator_id: "mfi",
17035            output_id: Some("value"),
17036            data: IndicatorDataRef::CloseVolume {
17037                close: &typical,
17038                volume: &volume,
17039            },
17040            combos: &combos,
17041            kernel: Kernel::Auto,
17042        };
17043        let strict = compute_cpu_batch_strict(req).unwrap();
17044        let input = MfiInput::from_slices(&typical, &volume, MfiParams { period: Some(14) });
17045        let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
17046            .unwrap()
17047            .values;
17048        assert_series_eq(strict.values_f64.as_ref().unwrap(), &direct, 1e-12);
17049    }
17050
17051    #[test]
17052    fn strict_mode_rejects_ao_high_low_and_requires_slice() {
17053        let (_open, high, low, _close) = sample_ohlc();
17054        let combo = [
17055            ParamKV {
17056                key: "short_period",
17057                value: ParamValue::Int(5),
17058            },
17059            ParamKV {
17060                key: "long_period",
17061                value: ParamValue::Int(34),
17062            },
17063        ];
17064        let combos = [IndicatorParamSet { params: &combo }];
17065        let req = IndicatorBatchRequest {
17066            indicator_id: "ao",
17067            output_id: Some("value"),
17068            data: IndicatorDataRef::HighLow {
17069                high: &high,
17070                low: &low,
17071            },
17072            combos: &combos,
17073            kernel: Kernel::Auto,
17074        };
17075        let err = compute_cpu_batch_strict(req).unwrap_err();
17076        match err {
17077            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
17078                assert_eq!(indicator, "ao");
17079                assert_eq!(input, IndicatorInputKind::Slice);
17080            }
17081            other => panic!("expected MissingRequiredInput, got {other:?}"),
17082        }
17083    }
17084
17085    #[test]
17086    fn strict_mode_rejects_ttm_trend_ohlc_and_requires_candles() {
17087        let (open, high, low, close) = sample_ohlc();
17088        let combo = [ParamKV {
17089            key: "period",
17090            value: ParamValue::Int(5),
17091        }];
17092        let combos = [IndicatorParamSet { params: &combo }];
17093        let req = IndicatorBatchRequest {
17094            indicator_id: "ttm_trend",
17095            output_id: Some("value"),
17096            data: IndicatorDataRef::Ohlc {
17097                open: &open,
17098                high: &high,
17099                low: &low,
17100                close: &close,
17101            },
17102            combos: &combos,
17103            kernel: Kernel::Auto,
17104        };
17105        let err = compute_cpu_batch_strict(req).unwrap_err();
17106        match err {
17107            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
17108                assert_eq!(indicator, "ttm_trend");
17109                assert_eq!(input, IndicatorInputKind::Candles);
17110            }
17111            other => panic!("expected MissingRequiredInput, got {other:?}"),
17112        }
17113    }
17114
17115    #[test]
17116    fn strict_mode_accepts_ttm_trend_candles() {
17117        let candles = sample_candles();
17118        let combo = [ParamKV {
17119            key: "period",
17120            value: ParamValue::Int(5),
17121        }];
17122        let combos = [IndicatorParamSet { params: &combo }];
17123        let req = IndicatorBatchRequest {
17124            indicator_id: "ttm_trend",
17125            output_id: Some("value"),
17126            data: IndicatorDataRef::Candles {
17127                candles: &candles,
17128                source: Some("hl2"),
17129            },
17130            combos: &combos,
17131            kernel: Kernel::Auto,
17132        };
17133        let strict = compute_cpu_batch_strict(req).unwrap();
17134        let input = TtmTrendInput::from_slices(
17135            candles.hl2.as_slice(),
17136            candles.close.as_slice(),
17137            TtmTrendParams { period: Some(5) },
17138        );
17139        let direct = ttm_trend_with_kernel(&input, Kernel::Auto.to_non_batch())
17140            .unwrap()
17141            .values;
17142        let got = strict.values_bool.unwrap();
17143        assert_eq!(got, direct);
17144    }
17145
17146    #[test]
17147    fn rsi_cpu_batch_smoke() {
17148        let data = sample_series();
17149        let combo_1 = [ParamKV {
17150            key: "period",
17151            value: ParamValue::Int(7),
17152        }];
17153        let combo_2 = [ParamKV {
17154            key: "period",
17155            value: ParamValue::Int(14),
17156        }];
17157        let combos = [
17158            IndicatorParamSet { params: &combo_1 },
17159            IndicatorParamSet { params: &combo_2 },
17160        ];
17161        let req = IndicatorBatchRequest {
17162            indicator_id: "rsi",
17163            output_id: Some("value"),
17164            data: IndicatorDataRef::Slice { values: &data },
17165            combos: &combos,
17166            kernel: Kernel::Auto,
17167        };
17168        let out = compute_cpu_batch(req).unwrap();
17169        assert_eq!(out.output_id, "value");
17170        assert_eq!(out.rows, 2);
17171        assert_eq!(out.cols, data.len());
17172        assert_eq!(out.values_f64.as_ref().map(Vec::len), Some(2 * data.len()));
17173    }
17174
17175    #[test]
17176    fn ma_dispatch_regression_sma_matches_existing_ma_batch_api() {
17177        let data = sample_series();
17178        let combo = [ParamKV {
17179            key: "period",
17180            value: ParamValue::Int(14),
17181        }];
17182        let combos = [IndicatorParamSet { params: &combo }];
17183        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17184            indicator_id: "sma",
17185            output_id: Some("value"),
17186            data: IndicatorDataRef::Slice { values: &data },
17187            combos: &combos,
17188            kernel: Kernel::Auto,
17189        })
17190        .unwrap();
17191
17192        let direct = ma_batch_with_kernel_and_typed_params(
17193            "sma",
17194            MaData::Slice(&data),
17195            (14, 14, 0),
17196            Kernel::Auto,
17197            &[],
17198        )
17199        .unwrap();
17200        assert_eq!(dispatch.rows, direct.rows);
17201        assert_eq!(dispatch.cols, direct.cols);
17202        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17203    }
17204
17205    #[test]
17206    fn ma_dispatch_sma_period_sweep_matches_direct_batch() {
17207        let data = sample_series();
17208        let combo_1 = [ParamKV {
17209            key: "period",
17210            value: ParamValue::Int(5),
17211        }];
17212        let combo_2 = [ParamKV {
17213            key: "period",
17214            value: ParamValue::Int(7),
17215        }];
17216        let combo_3 = [ParamKV {
17217            key: "period",
17218            value: ParamValue::Int(9),
17219        }];
17220        let combos = [
17221            IndicatorParamSet { params: &combo_1 },
17222            IndicatorParamSet { params: &combo_2 },
17223            IndicatorParamSet { params: &combo_3 },
17224        ];
17225        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17226            indicator_id: "sma",
17227            output_id: Some("value"),
17228            data: IndicatorDataRef::Slice { values: &data },
17229            combos: &combos,
17230            kernel: Kernel::Auto,
17231        })
17232        .unwrap();
17233
17234        let direct = ma_batch_with_kernel_and_typed_params(
17235            "sma",
17236            MaData::Slice(&data),
17237            (5, 9, 2),
17238            Kernel::Auto,
17239            &[],
17240        )
17241        .unwrap();
17242        assert_eq!(dispatch.rows, direct.rows);
17243        assert_eq!(dispatch.cols, direct.cols);
17244        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17245    }
17246
17247    #[test]
17248    fn mfi_dispatch_period_sweep_matches_direct_batch() {
17249        let (_open, high, low, close) = sample_ohlc();
17250        let volume: Vec<f64> = (0..close.len())
17251            .map(|i| 1000.0 + (i as f64 * 2.0))
17252            .collect();
17253        let typical: Vec<f64> = high
17254            .iter()
17255            .zip(&low)
17256            .zip(&close)
17257            .map(|((h, l), c)| (h + l + c) / 3.0)
17258            .collect();
17259        let combo_1 = [ParamKV {
17260            key: "period",
17261            value: ParamValue::Int(5),
17262        }];
17263        let combo_2 = [ParamKV {
17264            key: "period",
17265            value: ParamValue::Int(7),
17266        }];
17267        let combo_3 = [ParamKV {
17268            key: "period",
17269            value: ParamValue::Int(9),
17270        }];
17271        let combos = [
17272            IndicatorParamSet { params: &combo_1 },
17273            IndicatorParamSet { params: &combo_2 },
17274            IndicatorParamSet { params: &combo_3 },
17275        ];
17276        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17277            indicator_id: "mfi",
17278            output_id: Some("value"),
17279            data: IndicatorDataRef::CloseVolume {
17280                close: &typical,
17281                volume: &volume,
17282            },
17283            combos: &combos,
17284            kernel: Kernel::Auto,
17285        })
17286        .unwrap();
17287        let direct = mfi_batch_with_kernel(
17288            &typical,
17289            &volume,
17290            &MfiBatchRange { period: (5, 9, 2) },
17291            Kernel::Auto,
17292        )
17293        .unwrap();
17294        assert_eq!(dispatch.rows, direct.rows);
17295        assert_eq!(dispatch.cols, direct.cols);
17296        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17297    }
17298
17299    #[test]
17300    fn dx_dispatch_period_sweep_keeps_requested_row_order() {
17301        let (open, high, low, close) = sample_ohlc();
17302        let combo_1 = [ParamKV {
17303            key: "period",
17304            value: ParamValue::Int(9),
17305        }];
17306        let combo_2 = [ParamKV {
17307            key: "period",
17308            value: ParamValue::Int(7),
17309        }];
17310        let combo_3 = [ParamKV {
17311            key: "period",
17312            value: ParamValue::Int(5),
17313        }];
17314        let combos = [
17315            IndicatorParamSet { params: &combo_1 },
17316            IndicatorParamSet { params: &combo_2 },
17317            IndicatorParamSet { params: &combo_3 },
17318        ];
17319        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17320            indicator_id: "dx",
17321            output_id: Some("value"),
17322            data: IndicatorDataRef::Ohlc {
17323                open: &open,
17324                high: &high,
17325                low: &low,
17326                close: &close,
17327            },
17328            combos: &combos,
17329            kernel: Kernel::Auto,
17330        })
17331        .unwrap();
17332        let direct = dx_batch_with_kernel(
17333            &high,
17334            &low,
17335            &close,
17336            &DxBatchRange { period: (9, 5, 2) },
17337            Kernel::Auto,
17338        )
17339        .unwrap();
17340        let direct_periods: Vec<usize> = direct
17341            .combos
17342            .iter()
17343            .map(|combo| combo.period.unwrap_or(14))
17344            .collect();
17345        let period_to_row: std::collections::HashMap<usize, usize> = direct_periods
17346            .iter()
17347            .copied()
17348            .enumerate()
17349            .map(|(row, period)| (period, row))
17350            .collect();
17351        let requested = [9usize, 7usize, 5usize];
17352        let mut expected = Vec::with_capacity(requested.len() * direct.cols);
17353        for period in requested {
17354            let row = period_to_row[&period];
17355            let start = row * direct.cols;
17356            let end = start + direct.cols;
17357            expected.extend_from_slice(&direct.values[start..end]);
17358        }
17359        assert_eq!(dispatch.rows, requested.len());
17360        assert_eq!(dispatch.cols, direct.cols);
17361        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &expected, 1e-12);
17362    }
17363
17364    #[test]
17365    fn ma_dispatch_regression_alma_typed_params_match_existing_ma_batch_api() {
17366        let data = sample_series();
17367        let combo = [
17368            ParamKV {
17369                key: "period",
17370                value: ParamValue::Int(14),
17371            },
17372            ParamKV {
17373                key: "offset",
17374                value: ParamValue::Float(0.87),
17375            },
17376            ParamKV {
17377                key: "sigma",
17378                value: ParamValue::Float(5.5),
17379            },
17380        ];
17381        let combos = [IndicatorParamSet { params: &combo }];
17382        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17383            indicator_id: "alma",
17384            output_id: Some("value"),
17385            data: IndicatorDataRef::Slice { values: &data },
17386            combos: &combos,
17387            kernel: Kernel::Auto,
17388        })
17389        .unwrap();
17390
17391        let typed = [
17392            MaBatchParamKV {
17393                key: "offset",
17394                value: MaBatchParamValue::Float(0.87),
17395            },
17396            MaBatchParamKV {
17397                key: "sigma",
17398                value: MaBatchParamValue::Float(5.5),
17399            },
17400        ];
17401        let direct = ma_batch_with_kernel_and_typed_params(
17402            "alma",
17403            MaData::Slice(&data),
17404            (14, 14, 0),
17405            Kernel::Auto,
17406            &typed,
17407        )
17408        .unwrap();
17409        assert_eq!(dispatch.rows, direct.rows);
17410        assert_eq!(dispatch.cols, direct.cols);
17411        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17412    }
17413
17414    #[test]
17415    fn macd_signal_output_matches_direct() {
17416        let data = sample_series();
17417        let combo_1 = [
17418            ParamKV {
17419                key: "fast_period",
17420                value: ParamValue::Int(8),
17421            },
17422            ParamKV {
17423                key: "slow_period",
17424                value: ParamValue::Int(21),
17425            },
17426            ParamKV {
17427                key: "signal_period",
17428                value: ParamValue::Int(5),
17429            },
17430        ];
17431        let combo_2 = [
17432            ParamKV {
17433                key: "fast_period",
17434                value: ParamValue::Int(12),
17435            },
17436            ParamKV {
17437                key: "slow_period",
17438                value: ParamValue::Int(26),
17439            },
17440            ParamKV {
17441                key: "signal_period",
17442                value: ParamValue::Int(9),
17443            },
17444        ];
17445        let combos = [
17446            IndicatorParamSet { params: &combo_1 },
17447            IndicatorParamSet { params: &combo_2 },
17448        ];
17449        let req = IndicatorBatchRequest {
17450            indicator_id: "macd",
17451            output_id: Some("signal"),
17452            data: IndicatorDataRef::Slice { values: &data },
17453            combos: &combos,
17454            kernel: Kernel::Auto,
17455        };
17456        let out = compute_cpu_batch(req).unwrap();
17457        let matrix = out.values_f64.unwrap();
17458        for (row, combo) in combos.iter().enumerate() {
17459            let fast = match combo.params[0].value {
17460                ParamValue::Int(v) => v as usize,
17461                _ => unreachable!(),
17462            };
17463            let slow = match combo.params[1].value {
17464                ParamValue::Int(v) => v as usize,
17465                _ => unreachable!(),
17466            };
17467            let signal = match combo.params[2].value {
17468                ParamValue::Int(v) => v as usize,
17469                _ => unreachable!(),
17470            };
17471            let input = MacdInput::from_slice(
17472                &data,
17473                MacdParams {
17474                    fast_period: Some(fast),
17475                    slow_period: Some(slow),
17476                    signal_period: Some(signal),
17477                    ma_type: Some("ema".to_string()),
17478                },
17479            );
17480            let direct = macd_with_kernel(&input, Kernel::Auto.to_non_batch())
17481                .unwrap()
17482                .signal;
17483            let start = row * out.cols;
17484            let end = start + out.cols;
17485            assert_series_eq(&matrix[start..end], direct.as_slice(), 1e-12);
17486        }
17487    }
17488
17489    #[test]
17490    fn adx_output_matches_direct() {
17491        let (open, high, low, close) = sample_ohlc();
17492        let combo = [ParamKV {
17493            key: "period",
17494            value: ParamValue::Int(14),
17495        }];
17496        let combos = [IndicatorParamSet { params: &combo }];
17497        let req = IndicatorBatchRequest {
17498            indicator_id: "adx",
17499            output_id: Some("value"),
17500            data: IndicatorDataRef::Ohlc {
17501                open: &open,
17502                high: &high,
17503                low: &low,
17504                close: &close,
17505            },
17506            combos: &combos,
17507            kernel: Kernel::Auto,
17508        };
17509        let out = compute_cpu_batch(req).unwrap();
17510        let matrix = out.values_f64.unwrap();
17511        let input = AdxInput::from_slices(&high, &low, &close, AdxParams { period: Some(14) });
17512        let direct = adx_with_kernel(&input, Kernel::Auto.to_non_batch())
17513            .unwrap()
17514            .values;
17515        assert_series_eq(&matrix, &direct, 1e-12);
17516    }
17517
17518    #[test]
17519    fn garman_klass_output_matches_direct() {
17520        let (open, high, low, close) = sample_ohlc();
17521        let combo = [ParamKV {
17522            key: "lookback",
17523            value: ParamValue::Int(17),
17524        }];
17525        let combos = [IndicatorParamSet { params: &combo }];
17526        let req = IndicatorBatchRequest {
17527            indicator_id: "garman_klass_volatility",
17528            output_id: Some("value"),
17529            data: IndicatorDataRef::Ohlc {
17530                open: &open,
17531                high: &high,
17532                low: &low,
17533                close: &close,
17534            },
17535            combos: &combos,
17536            kernel: Kernel::Auto,
17537        };
17538        let out = compute_cpu_batch(req).unwrap();
17539        let got = out.values_f64.unwrap();
17540        let input = GarmanKlassVolatilityInput::from_slices(
17541            &open,
17542            &high,
17543            &low,
17544            &close,
17545            GarmanKlassVolatilityParams { lookback: Some(17) },
17546        );
17547        let direct = garman_klass_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
17548            .unwrap()
17549            .values;
17550        assert_series_eq(&got, &direct, 1e-12);
17551    }
17552
17553    #[test]
17554    fn cmo_output_matches_direct() {
17555        let data = sample_series();
17556        let combo = [ParamKV {
17557            key: "period",
17558            value: ParamValue::Int(14),
17559        }];
17560        let combos = [IndicatorParamSet { params: &combo }];
17561        let req = IndicatorBatchRequest {
17562            indicator_id: "cmo",
17563            output_id: Some("value"),
17564            data: IndicatorDataRef::Slice { values: &data },
17565            combos: &combos,
17566            kernel: Kernel::Auto,
17567        };
17568        let out = compute_cpu_batch(req).unwrap();
17569        let input = CmoInput::from_slice(&data, CmoParams { period: Some(14) });
17570        let direct = cmo_with_kernel(&input, Kernel::Auto.to_non_batch())
17571            .unwrap()
17572            .values;
17573        let got = out.values_f64.unwrap();
17574        assert_series_eq(&got, &direct, 1e-12);
17575    }
17576
17577    #[test]
17578    fn ppo_output_matches_direct() {
17579        let data = sample_series();
17580        let combo = [
17581            ParamKV {
17582                key: "fast_period",
17583                value: ParamValue::Int(12),
17584            },
17585            ParamKV {
17586                key: "slow_period",
17587                value: ParamValue::Int(26),
17588            },
17589            ParamKV {
17590                key: "ma_type",
17591                value: ParamValue::EnumString("sma"),
17592            },
17593        ];
17594        let combos = [IndicatorParamSet { params: &combo }];
17595        let req = IndicatorBatchRequest {
17596            indicator_id: "ppo",
17597            output_id: Some("value"),
17598            data: IndicatorDataRef::Slice { values: &data },
17599            combos: &combos,
17600            kernel: Kernel::Auto,
17601        };
17602        let out = compute_cpu_batch(req).unwrap();
17603        let input = PpoInput::from_slice(
17604            &data,
17605            PpoParams {
17606                fast_period: Some(12),
17607                slow_period: Some(26),
17608                ma_type: Some("sma".to_string()),
17609            },
17610        );
17611        let direct = ppo_with_kernel(&input, Kernel::Auto.to_non_batch())
17612            .unwrap()
17613            .values;
17614        let got = out.values_f64.unwrap();
17615        assert_series_eq(&got, &direct, 1e-12);
17616    }
17617
17618    #[test]
17619    fn apo_output_matches_direct() {
17620        let data = sample_series();
17621        let combo = [
17622            ParamKV {
17623                key: "short_period",
17624                value: ParamValue::Int(10),
17625            },
17626            ParamKV {
17627                key: "long_period",
17628                value: ParamValue::Int(20),
17629            },
17630        ];
17631        let combos = [IndicatorParamSet { params: &combo }];
17632        let req = IndicatorBatchRequest {
17633            indicator_id: "apo",
17634            output_id: Some("value"),
17635            data: IndicatorDataRef::Slice { values: &data },
17636            combos: &combos,
17637            kernel: Kernel::Auto,
17638        };
17639        let out = compute_cpu_batch(req).unwrap();
17640        let input = ApoInput::from_slice(
17641            &data,
17642            ApoParams {
17643                short_period: Some(10),
17644                long_period: Some(20),
17645            },
17646        );
17647        let direct = apo_with_kernel(&input, Kernel::Auto.to_non_batch())
17648            .unwrap()
17649            .values;
17650        let got = out.values_f64.unwrap();
17651        assert_series_eq(&got, &direct, 1e-12);
17652    }
17653
17654    #[test]
17655    fn natr_output_matches_direct() {
17656        let (open, high, low, close) = sample_ohlc();
17657        let combo = [ParamKV {
17658            key: "period",
17659            value: ParamValue::Int(14),
17660        }];
17661        let combos = [IndicatorParamSet { params: &combo }];
17662        let req = IndicatorBatchRequest {
17663            indicator_id: "natr",
17664            output_id: Some("value"),
17665            data: IndicatorDataRef::Ohlc {
17666                open: &open,
17667                high: &high,
17668                low: &low,
17669                close: &close,
17670            },
17671            combos: &combos,
17672            kernel: Kernel::Auto,
17673        };
17674        let out = compute_cpu_batch(req).unwrap();
17675        let input = NatrInput::from_slices(&high, &low, &close, NatrParams { period: Some(14) });
17676        let direct = natr_with_kernel(&input, Kernel::Auto.to_non_batch())
17677            .unwrap()
17678            .values;
17679        let got = out.values_f64.unwrap();
17680        assert_series_eq(&got, &direct, 1e-12);
17681    }
17682
17683    #[test]
17684    fn ad_output_matches_direct() {
17685        let (open, high, low, close) = sample_ohlc();
17686        let volume: Vec<f64> = (0..close.len())
17687            .map(|i| 1000.0 + (i as f64 * 3.0))
17688            .collect();
17689        let combos = [IndicatorParamSet { params: &[] }];
17690        let req = IndicatorBatchRequest {
17691            indicator_id: "ad",
17692            output_id: Some("value"),
17693            data: IndicatorDataRef::Ohlcv {
17694                open: &open,
17695                high: &high,
17696                low: &low,
17697                close: &close,
17698                volume: &volume,
17699            },
17700            combos: &combos,
17701            kernel: Kernel::Auto,
17702        };
17703        let out = compute_cpu_batch(req).unwrap();
17704        let input = AdInput::from_slices(&high, &low, &close, &volume, AdParams::default());
17705        let direct = ad_with_kernel(&input, Kernel::Auto.to_non_batch())
17706            .unwrap()
17707            .values;
17708        let got = out.values_f64.unwrap();
17709        assert_series_eq(&got, &direct, 1e-12);
17710    }
17711
17712    #[test]
17713    fn ao_output_matches_direct() {
17714        let (open, high, low, close) = sample_ohlc();
17715        let combo = [
17716            ParamKV {
17717                key: "short_period",
17718                value: ParamValue::Int(5),
17719            },
17720            ParamKV {
17721                key: "long_period",
17722                value: ParamValue::Int(34),
17723            },
17724        ];
17725        let combos = [IndicatorParamSet { params: &combo }];
17726        let req = IndicatorBatchRequest {
17727            indicator_id: "ao",
17728            output_id: Some("value"),
17729            data: IndicatorDataRef::Ohlc {
17730                open: &open,
17731                high: &high,
17732                low: &low,
17733                close: &close,
17734            },
17735            combos: &combos,
17736            kernel: Kernel::Auto,
17737        };
17738        let out = compute_cpu_batch(req).unwrap();
17739        let source: Vec<f64> = high.iter().zip(&low).map(|(h, l)| 0.5 * (h + l)).collect();
17740        let input = AoInput::from_slice(
17741            &source,
17742            AoParams {
17743                short_period: Some(5),
17744                long_period: Some(34),
17745            },
17746        );
17747        let direct = ao_with_kernel(&input, Kernel::Auto.to_non_batch())
17748            .unwrap()
17749            .values;
17750        let got = out.values_f64.unwrap();
17751        assert_series_eq(&got, &direct, 1e-12);
17752    }
17753
17754    #[test]
17755    fn pvi_output_matches_direct() {
17756        let data = sample_series();
17757        let volume: Vec<f64> = (0..data.len()).map(|i| 900.0 + (i as f64 * 5.0)).collect();
17758        let combo = [ParamKV {
17759            key: "initial_value",
17760            value: ParamValue::Float(1000.0),
17761        }];
17762        let combos = [IndicatorParamSet { params: &combo }];
17763        let req = IndicatorBatchRequest {
17764            indicator_id: "pvi",
17765            output_id: Some("value"),
17766            data: IndicatorDataRef::CloseVolume {
17767                close: &data,
17768                volume: &volume,
17769            },
17770            combos: &combos,
17771            kernel: Kernel::Auto,
17772        };
17773        let out = compute_cpu_batch(req).unwrap();
17774        let input = PviInput::from_slices(
17775            &data,
17776            &volume,
17777            PviParams {
17778                initial_value: Some(1000.0),
17779            },
17780        );
17781        let direct = pvi_with_kernel(&input, Kernel::Auto.to_non_batch())
17782            .unwrap()
17783            .values;
17784        let got = out.values_f64.unwrap();
17785        assert_series_eq(&got, &direct, 1e-12);
17786    }
17787
17788    #[test]
17789    fn efi_output_matches_direct() {
17790        let data = sample_series();
17791        let volume: Vec<f64> = (0..data.len()).map(|i| 1000.0 + (i as f64 * 4.0)).collect();
17792        let combo = [ParamKV {
17793            key: "period",
17794            value: ParamValue::Int(13),
17795        }];
17796        let combos = [IndicatorParamSet { params: &combo }];
17797        let req = IndicatorBatchRequest {
17798            indicator_id: "efi",
17799            output_id: Some("value"),
17800            data: IndicatorDataRef::CloseVolume {
17801                close: &data,
17802                volume: &volume,
17803            },
17804            combos: &combos,
17805            kernel: Kernel::Auto,
17806        };
17807        let out = compute_cpu_batch(req).unwrap();
17808        let input = EfiInput::from_slices(&data, &volume, EfiParams { period: Some(13) });
17809        let direct = efi_with_kernel(&input, Kernel::Auto.to_non_batch())
17810            .unwrap()
17811            .values;
17812        let got = out.values_f64.unwrap();
17813        assert_series_eq(&got, &direct, 1e-12);
17814    }
17815
17816    #[test]
17817    fn mfi_output_matches_direct() {
17818        let (open, high, low, close) = sample_ohlc();
17819        let volume: Vec<f64> = (0..close.len()).map(|i| 900.0 + (i as f64 * 6.0)).collect();
17820        let combo = [ParamKV {
17821            key: "period",
17822            value: ParamValue::Int(14),
17823        }];
17824        let combos = [IndicatorParamSet { params: &combo }];
17825        let req = IndicatorBatchRequest {
17826            indicator_id: "mfi",
17827            output_id: Some("value"),
17828            data: IndicatorDataRef::Ohlcv {
17829                open: &open,
17830                high: &high,
17831                low: &low,
17832                close: &close,
17833                volume: &volume,
17834            },
17835            combos: &combos,
17836            kernel: Kernel::Auto,
17837        };
17838        let out = compute_cpu_batch(req).unwrap();
17839        let typical_price: Vec<f64> = high
17840            .iter()
17841            .zip(&low)
17842            .zip(&close)
17843            .map(|((h, l), c)| (h + l + c) / 3.0)
17844            .collect();
17845        let input = MfiInput::from_slices(&typical_price, &volume, MfiParams { period: Some(14) });
17846        let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
17847            .unwrap()
17848            .values;
17849        let got = out.values_f64.unwrap();
17850        assert_series_eq(&got, &direct, 1e-12);
17851    }
17852
17853    #[test]
17854    fn mfi_non_sweep_fallback_rows_match_direct() {
17855        let (open, high, low, close) = sample_ohlc();
17856        let volume: Vec<f64> = (0..close.len()).map(|i| 950.0 + (i as f64 * 5.0)).collect();
17857        let combo_1 = [ParamKV {
17858            key: "period",
17859            value: ParamValue::Int(5),
17860        }];
17861        let combo_2 = [ParamKV {
17862            key: "period",
17863            value: ParamValue::Int(9),
17864        }];
17865        let combo_3 = [ParamKV {
17866            key: "period",
17867            value: ParamValue::Int(8),
17868        }];
17869        let combos = [
17870            IndicatorParamSet { params: &combo_1 },
17871            IndicatorParamSet { params: &combo_2 },
17872            IndicatorParamSet { params: &combo_3 },
17873        ];
17874        let req = IndicatorBatchRequest {
17875            indicator_id: "mfi",
17876            output_id: Some("value"),
17877            data: IndicatorDataRef::Ohlcv {
17878                open: &open,
17879                high: &high,
17880                low: &low,
17881                close: &close,
17882                volume: &volume,
17883            },
17884            combos: &combos,
17885            kernel: Kernel::Auto,
17886        };
17887        let out = compute_cpu_batch(req).unwrap();
17888        let matrix = out.values_f64.unwrap();
17889        let typical_price: Vec<f64> = high
17890            .iter()
17891            .zip(&low)
17892            .zip(&close)
17893            .map(|((h, l), c)| (h + l + c) / 3.0)
17894            .collect();
17895        for (row, period) in [5usize, 9usize, 8usize].iter().enumerate() {
17896            let input = MfiInput::from_slices(
17897                &typical_price,
17898                &volume,
17899                MfiParams {
17900                    period: Some(*period),
17901                },
17902            );
17903            let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
17904                .unwrap()
17905                .values;
17906            let start = row * close.len();
17907            let end = start + close.len();
17908            assert_series_eq(&matrix[start..end], &direct, 1e-12);
17909        }
17910    }
17911
17912    #[test]
17913    fn kvo_output_matches_direct() {
17914        let (open, high, low, close) = sample_ohlc();
17915        let volume: Vec<f64> = (0..close.len())
17916            .map(|i| 1200.0 + (i as f64 * 5.0))
17917            .collect();
17918        let combo = [
17919            ParamKV {
17920                key: "short_period",
17921                value: ParamValue::Int(2),
17922            },
17923            ParamKV {
17924                key: "long_period",
17925                value: ParamValue::Int(5),
17926            },
17927        ];
17928        let combos = [IndicatorParamSet { params: &combo }];
17929        let req = IndicatorBatchRequest {
17930            indicator_id: "kvo",
17931            output_id: Some("value"),
17932            data: IndicatorDataRef::Ohlcv {
17933                open: &open,
17934                high: &high,
17935                low: &low,
17936                close: &close,
17937                volume: &volume,
17938            },
17939            combos: &combos,
17940            kernel: Kernel::Auto,
17941        };
17942        let out = compute_cpu_batch(req).unwrap();
17943        let input = KvoInput::from_slices(
17944            &high,
17945            &low,
17946            &close,
17947            &volume,
17948            KvoParams {
17949                short_period: Some(2),
17950                long_period: Some(5),
17951            },
17952        );
17953        let direct = kvo_with_kernel(&input, Kernel::Auto.to_non_batch())
17954            .unwrap()
17955            .values;
17956        let got = out.values_f64.unwrap();
17957        assert_series_eq(&got, &direct, 1e-12);
17958    }
17959
17960    #[test]
17961    fn dx_output_matches_direct() {
17962        let (open, high, low, close) = sample_ohlc();
17963        let combo = [ParamKV {
17964            key: "period",
17965            value: ParamValue::Int(14),
17966        }];
17967        let combos = [IndicatorParamSet { params: &combo }];
17968        let req = IndicatorBatchRequest {
17969            indicator_id: "dx",
17970            output_id: Some("value"),
17971            data: IndicatorDataRef::Ohlc {
17972                open: &open,
17973                high: &high,
17974                low: &low,
17975                close: &close,
17976            },
17977            combos: &combos,
17978            kernel: Kernel::Auto,
17979        };
17980        let out = compute_cpu_batch(req).unwrap();
17981        let input = DxInput::from_hlc_slices(&high, &low, &close, DxParams { period: Some(14) });
17982        let direct = dx_with_kernel(&input, Kernel::Auto.to_non_batch())
17983            .unwrap()
17984            .values;
17985        let got = out.values_f64.unwrap();
17986        assert_series_eq(&got, &direct, 1e-12);
17987    }
17988
17989    #[test]
17990    fn dx_non_sweep_fallback_rows_match_direct() {
17991        let (open, high, low, close) = sample_ohlc();
17992        let combo_1 = [ParamKV {
17993            key: "period",
17994            value: ParamValue::Int(9),
17995        }];
17996        let combo_2 = [ParamKV {
17997            key: "period",
17998            value: ParamValue::Int(5),
17999        }];
18000        let combo_3 = [ParamKV {
18001            key: "period",
18002            value: ParamValue::Int(8),
18003        }];
18004        let combos = [
18005            IndicatorParamSet { params: &combo_1 },
18006            IndicatorParamSet { params: &combo_2 },
18007            IndicatorParamSet { params: &combo_3 },
18008        ];
18009        let req = IndicatorBatchRequest {
18010            indicator_id: "dx",
18011            output_id: Some("value"),
18012            data: IndicatorDataRef::Ohlc {
18013                open: &open,
18014                high: &high,
18015                low: &low,
18016                close: &close,
18017            },
18018            combos: &combos,
18019            kernel: Kernel::Auto,
18020        };
18021        let out = compute_cpu_batch(req).unwrap();
18022        let matrix = out.values_f64.unwrap();
18023        for (row, period) in [9usize, 5usize, 8usize].iter().enumerate() {
18024            let input = DxInput::from_hlc_slices(
18025                &high,
18026                &low,
18027                &close,
18028                DxParams {
18029                    period: Some(*period),
18030                },
18031            );
18032            let direct = dx_with_kernel(&input, Kernel::Auto.to_non_batch())
18033                .unwrap()
18034                .values;
18035            let start = row * close.len();
18036            let end = start + close.len();
18037            assert_series_eq(&matrix[start..end], &direct, 1e-12);
18038        }
18039    }
18040
18041    #[test]
18042    fn trix_dispatch_period_sweep_keeps_requested_row_order() {
18043        let data = sample_series();
18044        let combo_1 = [ParamKV {
18045            key: "period",
18046            value: ParamValue::Int(9),
18047        }];
18048        let combo_2 = [ParamKV {
18049            key: "period",
18050            value: ParamValue::Int(7),
18051        }];
18052        let combo_3 = [ParamKV {
18053            key: "period",
18054            value: ParamValue::Int(5),
18055        }];
18056        let combos = [
18057            IndicatorParamSet { params: &combo_1 },
18058            IndicatorParamSet { params: &combo_2 },
18059            IndicatorParamSet { params: &combo_3 },
18060        ];
18061        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
18062            indicator_id: "trix",
18063            output_id: Some("value"),
18064            data: IndicatorDataRef::Slice { values: &data },
18065            combos: &combos,
18066            kernel: Kernel::Auto,
18067        })
18068        .unwrap();
18069
18070        let direct =
18071            trix_batch_with_kernel(&data, &TrixBatchRange { period: (9, 5, 2) }, Kernel::Auto)
18072                .unwrap();
18073        let direct_periods: Vec<usize> = direct
18074            .combos
18075            .iter()
18076            .map(|combo| combo.period.unwrap_or(18))
18077            .collect();
18078        let period_to_row: std::collections::HashMap<usize, usize> = direct_periods
18079            .iter()
18080            .copied()
18081            .enumerate()
18082            .map(|(row, period)| (period, row))
18083            .collect();
18084        let requested = [9usize, 7usize, 5usize];
18085        let mut expected = Vec::with_capacity(requested.len() * direct.cols);
18086        for period in requested {
18087            let row = period_to_row[&period];
18088            let start = row * direct.cols;
18089            let end = start + direct.cols;
18090            expected.extend_from_slice(&direct.values[start..end]);
18091        }
18092        assert_eq!(dispatch.rows, requested.len());
18093        assert_eq!(dispatch.cols, direct.cols);
18094        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &expected, 1e-12);
18095    }
18096
18097    #[test]
18098    fn trix_non_sweep_fallback_rows_match_direct() {
18099        let data = sample_series();
18100        let combo_1 = [ParamKV {
18101            key: "period",
18102            value: ParamValue::Int(9),
18103        }];
18104        let combo_2 = [ParamKV {
18105            key: "period",
18106            value: ParamValue::Int(5),
18107        }];
18108        let combo_3 = [ParamKV {
18109            key: "period",
18110            value: ParamValue::Int(8),
18111        }];
18112        let combos = [
18113            IndicatorParamSet { params: &combo_1 },
18114            IndicatorParamSet { params: &combo_2 },
18115            IndicatorParamSet { params: &combo_3 },
18116        ];
18117        let out = compute_cpu_batch(IndicatorBatchRequest {
18118            indicator_id: "trix",
18119            output_id: Some("value"),
18120            data: IndicatorDataRef::Slice { values: &data },
18121            combos: &combos,
18122            kernel: Kernel::Auto,
18123        })
18124        .unwrap();
18125        let matrix = out.values_f64.unwrap();
18126        for (row, period) in [9usize, 5usize, 8usize].iter().enumerate() {
18127            let input = TrixInput::from_slice(
18128                &data,
18129                TrixParams {
18130                    period: Some(*period),
18131                },
18132            );
18133            let direct = trix_with_kernel(&input, Kernel::Auto.to_non_batch())
18134                .unwrap()
18135                .values;
18136            let start = row * data.len();
18137            let end = start + data.len();
18138            assert_series_eq(&matrix[start..end], &direct, 1e-12);
18139        }
18140    }
18141
18142    #[test]
18143    fn ift_rsi_output_matches_direct() {
18144        let data = sample_series();
18145        let combo = [
18146            ParamKV {
18147                key: "rsi_period",
18148                value: ParamValue::Int(6),
18149            },
18150            ParamKV {
18151                key: "wma_period",
18152                value: ParamValue::Int(10),
18153            },
18154        ];
18155        let combos = [IndicatorParamSet { params: &combo }];
18156        let req = IndicatorBatchRequest {
18157            indicator_id: "ift_rsi",
18158            output_id: Some("value"),
18159            data: IndicatorDataRef::Slice { values: &data },
18160            combos: &combos,
18161            kernel: Kernel::Auto,
18162        };
18163        let out = compute_cpu_batch(req).unwrap();
18164        let input = IftRsiInput::from_slice(
18165            &data,
18166            IftRsiParams {
18167                rsi_period: Some(6),
18168                wma_period: Some(10),
18169            },
18170        );
18171        let direct = ift_rsi_with_kernel(&input, Kernel::Auto.to_non_batch())
18172            .unwrap()
18173            .values;
18174        let got = out.values_f64.unwrap();
18175        assert_series_eq(&got, &direct, 1e-12);
18176    }
18177
18178    #[test]
18179    fn fosc_output_matches_direct() {
18180        let data = sample_series();
18181        let combo = [ParamKV {
18182            key: "period",
18183            value: ParamValue::Int(8),
18184        }];
18185        let combos = [IndicatorParamSet { params: &combo }];
18186        let req = IndicatorBatchRequest {
18187            indicator_id: "fosc",
18188            output_id: Some("value"),
18189            data: IndicatorDataRef::Slice { values: &data },
18190            combos: &combos,
18191            kernel: Kernel::Auto,
18192        };
18193        let out = compute_cpu_batch(req).unwrap();
18194        let input = FoscInput::from_slice(&data, FoscParams { period: Some(8) });
18195        let direct = fosc_with_kernel(&input, Kernel::Auto.to_non_batch())
18196            .unwrap()
18197            .values;
18198        let got = out.values_f64.unwrap();
18199        assert_series_eq(&got, &direct, 1e-12);
18200    }
18201
18202    #[test]
18203    fn linearreg_angle_output_matches_direct() {
18204        let data = sample_series();
18205        let combo = [ParamKV {
18206            key: "period",
18207            value: ParamValue::Int(14),
18208        }];
18209        let combos = [IndicatorParamSet { params: &combo }];
18210        let req = IndicatorBatchRequest {
18211            indicator_id: "linearreg_angle",
18212            output_id: Some("value"),
18213            data: IndicatorDataRef::Slice { values: &data },
18214            combos: &combos,
18215            kernel: Kernel::Auto,
18216        };
18217        let out = compute_cpu_batch(req).unwrap();
18218        let input =
18219            Linearreg_angleInput::from_slice(&data, Linearreg_angleParams { period: Some(14) });
18220        let direct = linearreg_angle_with_kernel(&input, Kernel::Auto.to_non_batch())
18221            .unwrap()
18222            .values;
18223        let got = out.values_f64.unwrap();
18224        assert_series_eq(&got, &direct, 1e-12);
18225    }
18226
18227    #[test]
18228    fn linearreg_intercept_output_matches_direct() {
18229        let data = sample_series();
18230        let combo = [ParamKV {
18231            key: "period",
18232            value: ParamValue::Int(14),
18233        }];
18234        let combos = [IndicatorParamSet { params: &combo }];
18235        let req = IndicatorBatchRequest {
18236            indicator_id: "linearreg_intercept",
18237            output_id: Some("value"),
18238            data: IndicatorDataRef::Slice { values: &data },
18239            combos: &combos,
18240            kernel: Kernel::Auto,
18241        };
18242        let out = compute_cpu_batch(req).unwrap();
18243        let input = LinearRegInterceptInput::from_slice(
18244            &data,
18245            LinearRegInterceptParams { period: Some(14) },
18246        );
18247        let direct = linearreg_intercept_with_kernel(&input, Kernel::Auto.to_non_batch())
18248            .unwrap()
18249            .values;
18250        let got = out.values_f64.unwrap();
18251        assert_series_eq(&got, &direct, 1e-12);
18252    }
18253
18254    #[test]
18255    fn cg_output_matches_direct() {
18256        let data = sample_series();
18257        let combo = [ParamKV {
18258            key: "period",
18259            value: ParamValue::Int(10),
18260        }];
18261        let combos = [IndicatorParamSet { params: &combo }];
18262        let req = IndicatorBatchRequest {
18263            indicator_id: "cg",
18264            output_id: Some("value"),
18265            data: IndicatorDataRef::Slice { values: &data },
18266            combos: &combos,
18267            kernel: Kernel::Auto,
18268        };
18269        let out = compute_cpu_batch(req).unwrap();
18270        let input = CgInput::from_slice(&data, CgParams { period: Some(10) });
18271        let direct = cg_with_kernel(&input, Kernel::Auto.to_non_batch())
18272            .unwrap()
18273            .values;
18274        let got = out.values_f64.unwrap();
18275        assert_series_eq(&got, &direct, 1e-12);
18276    }
18277
18278    #[test]
18279    fn linearreg_slope_output_matches_direct() {
18280        let data = sample_series();
18281        let combo = [ParamKV {
18282            key: "period",
18283            value: ParamValue::Int(14),
18284        }];
18285        let combos = [IndicatorParamSet { params: &combo }];
18286        let req = IndicatorBatchRequest {
18287            indicator_id: "linearreg_slope",
18288            output_id: Some("value"),
18289            data: IndicatorDataRef::Slice { values: &data },
18290            combos: &combos,
18291            kernel: Kernel::Auto,
18292        };
18293        let out = compute_cpu_batch(req).unwrap();
18294        let input =
18295            LinearRegSlopeInput::from_slice(&data, LinearRegSlopeParams { period: Some(14) });
18296        let direct = linearreg_slope_with_kernel(&input, Kernel::Auto.to_non_batch())
18297            .unwrap()
18298            .values;
18299        let got = out.values_f64.unwrap();
18300        assert_series_eq(&got, &direct, 1e-12);
18301    }
18302
18303    #[test]
18304    fn mean_ad_output_matches_direct() {
18305        let data = sample_series();
18306        let combo = [ParamKV {
18307            key: "period",
18308            value: ParamValue::Int(7),
18309        }];
18310        let combos = [IndicatorParamSet { params: &combo }];
18311        let req = IndicatorBatchRequest {
18312            indicator_id: "mean_ad",
18313            output_id: Some("value"),
18314            data: IndicatorDataRef::Slice { values: &data },
18315            combos: &combos,
18316            kernel: Kernel::Auto,
18317        };
18318        let out = compute_cpu_batch(req).unwrap();
18319        let input = MeanAdInput::from_slice(&data, MeanAdParams { period: Some(7) });
18320        let direct = mean_ad_with_kernel(&input, Kernel::Auto.to_non_batch())
18321            .unwrap()
18322            .values;
18323        let got = out.values_f64.unwrap();
18324        assert_series_eq(&got, &direct, 1e-12);
18325    }
18326
18327    #[test]
18328    fn deviation_output_matches_direct() {
18329        let data = sample_series();
18330        let combo = [
18331            ParamKV {
18332                key: "period",
18333                value: ParamValue::Int(9),
18334            },
18335            ParamKV {
18336                key: "devtype",
18337                value: ParamValue::Int(2),
18338            },
18339        ];
18340        let combos = [IndicatorParamSet { params: &combo }];
18341        let req = IndicatorBatchRequest {
18342            indicator_id: "deviation",
18343            output_id: Some("value"),
18344            data: IndicatorDataRef::Slice { values: &data },
18345            combos: &combos,
18346            kernel: Kernel::Auto,
18347        };
18348        let out = compute_cpu_batch(req).unwrap();
18349        let input = DeviationInput::from_slice(
18350            &data,
18351            DeviationParams {
18352                period: Some(9),
18353                devtype: Some(2),
18354            },
18355        );
18356        let direct = deviation_with_kernel(&input, Kernel::Auto.to_non_batch())
18357            .unwrap()
18358            .values;
18359        let got = out.values_f64.unwrap();
18360        assert_series_eq(&got, &direct, 1e-12);
18361    }
18362
18363    #[test]
18364    fn medprice_output_matches_direct() {
18365        let (_open, high, low, _close) = sample_ohlc();
18366        let combos = [IndicatorParamSet { params: &[] }];
18367        let req = IndicatorBatchRequest {
18368            indicator_id: "medprice",
18369            output_id: Some("value"),
18370            data: IndicatorDataRef::HighLow {
18371                high: &high,
18372                low: &low,
18373            },
18374            combos: &combos,
18375            kernel: Kernel::Auto,
18376        };
18377        let out = compute_cpu_batch(req).unwrap();
18378        let input = MedpriceInput::from_slices(&high, &low, MedpriceParams::default());
18379        let direct = medprice_with_kernel(&input, Kernel::Auto.to_non_batch())
18380            .unwrap()
18381            .values;
18382        let got = out.values_f64.unwrap();
18383        assert_series_eq(&got, &direct, 1e-12);
18384    }
18385
18386    #[test]
18387    fn percentile_nearest_rank_output_matches_direct() {
18388        let data = sample_series();
18389        let combo = [
18390            ParamKV {
18391                key: "length",
18392                value: ParamValue::Int(12),
18393            },
18394            ParamKV {
18395                key: "percentage",
18396                value: ParamValue::Float(70.0),
18397            },
18398        ];
18399        let combos = [IndicatorParamSet { params: &combo }];
18400        let req = IndicatorBatchRequest {
18401            indicator_id: "percentile_nearest_rank",
18402            output_id: Some("value"),
18403            data: IndicatorDataRef::Slice { values: &data },
18404            combos: &combos,
18405            kernel: Kernel::Auto,
18406        };
18407        let out = compute_cpu_batch(req).unwrap();
18408        let input = PercentileNearestRankInput::from_slice(
18409            &data,
18410            PercentileNearestRankParams {
18411                length: Some(12),
18412                percentage: Some(70.0),
18413            },
18414        );
18415        let direct = percentile_nearest_rank_with_kernel(&input, Kernel::Auto.to_non_batch())
18416            .unwrap()
18417            .values;
18418        let got = out.values_f64.unwrap();
18419        assert_series_eq(&got, &direct, 1e-12);
18420    }
18421
18422    #[test]
18423    fn zscore_output_matches_direct() {
18424        let data = sample_series();
18425        let combo = [
18426            ParamKV {
18427                key: "period",
18428                value: ParamValue::Int(14),
18429            },
18430            ParamKV {
18431                key: "ma_type",
18432                value: ParamValue::EnumString("ema"),
18433            },
18434            ParamKV {
18435                key: "nbdev",
18436                value: ParamValue::Float(1.25),
18437            },
18438            ParamKV {
18439                key: "devtype",
18440                value: ParamValue::Int(1),
18441            },
18442        ];
18443        let combos = [IndicatorParamSet { params: &combo }];
18444        let req = IndicatorBatchRequest {
18445            indicator_id: "zscore",
18446            output_id: Some("value"),
18447            data: IndicatorDataRef::Slice { values: &data },
18448            combos: &combos,
18449            kernel: Kernel::Auto,
18450        };
18451        let out = compute_cpu_batch(req).unwrap();
18452        let input = ZscoreInput::from_slice(
18453            &data,
18454            ZscoreParams {
18455                period: Some(14),
18456                ma_type: Some("ema".to_string()),
18457                nbdev: Some(1.25),
18458                devtype: Some(1),
18459            },
18460        );
18461        let direct = zscore_with_kernel(&input, Kernel::Auto.to_non_batch())
18462            .unwrap()
18463            .values;
18464        let got = out.values_f64.unwrap();
18465        assert_series_eq(&got, &direct, 1e-12);
18466    }
18467
18468    #[test]
18469    fn vpci_secondary_output_matches_direct() {
18470        let close = sample_series();
18471        let volume: Vec<f64> = (0..close.len())
18472            .map(|i| 1000.0 + (i as f64 * 7.0))
18473            .collect();
18474        let combo = [
18475            ParamKV {
18476                key: "short_range",
18477                value: ParamValue::Int(5),
18478            },
18479            ParamKV {
18480                key: "long_range",
18481                value: ParamValue::Int(25),
18482            },
18483        ];
18484        let combos = [IndicatorParamSet { params: &combo }];
18485        let req = IndicatorBatchRequest {
18486            indicator_id: "vpci",
18487            output_id: Some("vpcis"),
18488            data: IndicatorDataRef::CloseVolume {
18489                close: &close,
18490                volume: &volume,
18491            },
18492            combos: &combos,
18493            kernel: Kernel::Auto,
18494        };
18495        let out = compute_cpu_batch(req).unwrap();
18496        let input = VpciInput::from_slices(
18497            &close,
18498            &volume,
18499            VpciParams {
18500                short_range: Some(5),
18501                long_range: Some(25),
18502            },
18503        );
18504        let direct = vpci_with_kernel(&input, Kernel::Auto.to_non_batch())
18505            .unwrap()
18506            .vpcis;
18507        let got = out.values_f64.unwrap();
18508        assert_series_eq(&got, &direct, 1e-12);
18509    }
18510
18511    #[test]
18512    fn yang_zhang_secondary_output_matches_direct() {
18513        let (open, high, low, close) = sample_ohlc();
18514        let combo = [
18515            ParamKV {
18516                key: "lookback",
18517                value: ParamValue::Int(21),
18518            },
18519            ParamKV {
18520                key: "k_override",
18521                value: ParamValue::Bool(true),
18522            },
18523            ParamKV {
18524                key: "k",
18525                value: ParamValue::Float(0.28),
18526            },
18527        ];
18528        let combos = [IndicatorParamSet { params: &combo }];
18529        let req = IndicatorBatchRequest {
18530            indicator_id: "yang_zhang_volatility",
18531            output_id: Some("rs"),
18532            data: IndicatorDataRef::Ohlc {
18533                open: &open,
18534                high: &high,
18535                low: &low,
18536                close: &close,
18537            },
18538            combos: &combos,
18539            kernel: Kernel::Auto,
18540        };
18541        let out = compute_cpu_batch(req).unwrap();
18542        let input = YangZhangVolatilityInput::from_slices(
18543            &open,
18544            &high,
18545            &low,
18546            &close,
18547            YangZhangVolatilityParams {
18548                lookback: Some(21),
18549                k_override: Some(true),
18550                k: Some(0.28),
18551            },
18552        );
18553        let direct = yang_zhang_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
18554            .unwrap()
18555            .rs;
18556        let got = out.values_f64.unwrap();
18557        assert_series_eq(&got, &direct, 1e-12);
18558    }
18559
18560    #[test]
18561    fn historical_volatility_percentile_signal_output_matches_direct() {
18562        let data = sample_series();
18563        let combo = [
18564            ParamKV {
18565                key: "length",
18566                value: ParamValue::Int(5),
18567            },
18568            ParamKV {
18569                key: "annual_length",
18570                value: ParamValue::Int(10),
18571            },
18572        ];
18573        let combos = [IndicatorParamSet { params: &combo }];
18574        let req = IndicatorBatchRequest {
18575            indicator_id: "historical_volatility_percentile",
18576            output_id: Some("hvp_sma"),
18577            data: IndicatorDataRef::Slice { values: &data },
18578            combos: &combos,
18579            kernel: Kernel::Auto,
18580        };
18581        let out = compute_cpu_batch(req).unwrap();
18582        let input = HistoricalVolatilityPercentileInput::from_slice(
18583            &data,
18584            HistoricalVolatilityPercentileParams {
18585                length: Some(5),
18586                annual_length: Some(10),
18587            },
18588        );
18589        let direct =
18590            historical_volatility_percentile_with_kernel(&input, Kernel::Auto.to_non_batch())
18591                .unwrap()
18592                .hvp_sma;
18593        let got = out.values_f64.unwrap();
18594        assert_series_eq(&got, &direct, 1e-12);
18595    }
18596
18597    #[test]
18598    fn volatility_ratio_adaptive_rsx_signal_output_matches_direct() {
18599        let data = sample_series();
18600        let combo = [
18601            ParamKV {
18602                key: "period",
18603                value: ParamValue::Int(6),
18604            },
18605            ParamKV {
18606                key: "speed",
18607                value: ParamValue::Float(0.5),
18608            },
18609        ];
18610        let combos = [IndicatorParamSet { params: &combo }];
18611        let req = IndicatorBatchRequest {
18612            indicator_id: "volatility_ratio_adaptive_rsx",
18613            output_id: Some("signal"),
18614            data: IndicatorDataRef::Slice { values: &data },
18615            combos: &combos,
18616            kernel: Kernel::Auto,
18617        };
18618        let out = compute_cpu_batch(req).unwrap();
18619        let input = VolatilityRatioAdaptiveRsxInput::from_slice(
18620            &data,
18621            VolatilityRatioAdaptiveRsxParams {
18622                period: Some(6),
18623                speed: Some(0.5),
18624            },
18625        );
18626        let direct = volatility_ratio_adaptive_rsx_with_kernel(&input, Kernel::Auto.to_non_batch())
18627            .unwrap()
18628            .signal;
18629        let got = out.values_f64.unwrap();
18630        assert_series_eq(&got, &direct, 1e-12);
18631    }
18632
18633    #[test]
18634    fn on_balance_volume_oscillator_signal_output_matches_direct() {
18635        let close = sample_series();
18636        let volume: Vec<f64> = (0..close.len()).map(|i| 1000.0 + i as f64 * 3.0).collect();
18637        let combo = [
18638            ParamKV {
18639                key: "obv_length",
18640                value: ParamValue::Int(20),
18641            },
18642            ParamKV {
18643                key: "ema_length",
18644                value: ParamValue::Int(9),
18645            },
18646        ];
18647        let combos = [IndicatorParamSet { params: &combo }];
18648        let req = IndicatorBatchRequest {
18649            indicator_id: "on_balance_volume_oscillator",
18650            output_id: Some("signal"),
18651            data: IndicatorDataRef::CloseVolume {
18652                close: &close,
18653                volume: &volume,
18654            },
18655            combos: &combos,
18656            kernel: Kernel::Auto,
18657        };
18658        let out = compute_cpu_batch(req).unwrap();
18659        let input = OnBalanceVolumeOscillatorInput::from_slices(
18660            &close,
18661            &volume,
18662            OnBalanceVolumeOscillatorParams {
18663                obv_length: Some(20),
18664                ema_length: Some(9),
18665            },
18666        );
18667        let direct = on_balance_volume_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
18668            .unwrap()
18669            .signal;
18670        let got = out.values_f64.unwrap();
18671        assert_series_eq(&got, &direct, 1e-12);
18672    }
18673
18674    #[test]
18675    fn twiggs_money_flow_smoothed_output_matches_direct() {
18676        let open = vec![10.0, 10.2, 10.4, 10.7, 10.9, 11.1, 11.3, 11.5, 11.7, 11.9];
18677        let high = vec![10.4, 10.7, 10.9, 11.1, 11.4, 11.6, 11.8, 12.0, 12.2, 12.4];
18678        let low = vec![9.8, 10.0, 10.2, 10.5, 10.7, 10.9, 11.1, 11.3, 11.5, 11.7];
18679        let close = vec![10.1, 10.5, 10.7, 10.9, 11.2, 11.4, 11.6, 11.8, 12.0, 12.2];
18680        let volume = vec![
18681            1000.0, 1015.0, 1030.0, 1045.0, 1060.0, 1075.0, 1090.0, 1105.0, 1120.0, 1135.0,
18682        ];
18683        let combo = [
18684            ParamKV {
18685                key: "length",
18686                value: ParamValue::Int(5),
18687            },
18688            ParamKV {
18689                key: "smoothing_length",
18690                value: ParamValue::Int(4),
18691            },
18692            ParamKV {
18693                key: "ma_type",
18694                value: ParamValue::EnumString("WMA"),
18695            },
18696        ];
18697        let combos = [IndicatorParamSet { params: &combo }];
18698        let req = IndicatorBatchRequest {
18699            indicator_id: "twiggs_money_flow",
18700            output_id: Some("smoothed"),
18701            data: IndicatorDataRef::Ohlcv {
18702                open: &open,
18703                high: &high,
18704                low: &low,
18705                close: &close,
18706                volume: &volume,
18707            },
18708            combos: &combos,
18709            kernel: Kernel::Auto,
18710        };
18711        let out = compute_cpu_batch(req).unwrap();
18712        let input = TwiggsMoneyFlowInput::from_slices(
18713            &high,
18714            &low,
18715            &close,
18716            &volume,
18717            TwiggsMoneyFlowParams {
18718                length: Some(5),
18719                smoothing_length: Some(4),
18720                ma_type: Some("WMA".to_string()),
18721            },
18722        );
18723        let direct = twiggs_money_flow_with_kernel(&input, Kernel::Auto.to_non_batch())
18724            .unwrap()
18725            .smoothed;
18726        let got = out.values_f64.unwrap();
18727        assert_series_eq(&got, &direct, 1e-12);
18728    }
18729
18730    #[test]
18731    fn parkinson_variance_output_matches_direct() {
18732        let (_open, high, low, _close) = sample_ohlc();
18733        let combo = [ParamKV {
18734            key: "period",
18735            value: ParamValue::Int(9),
18736        }];
18737        let combos = [IndicatorParamSet { params: &combo }];
18738        let req = IndicatorBatchRequest {
18739            indicator_id: "parkinson_volatility",
18740            output_id: Some("variance"),
18741            data: IndicatorDataRef::HighLow {
18742                high: &high,
18743                low: &low,
18744            },
18745            combos: &combos,
18746            kernel: Kernel::Auto,
18747        };
18748        let out = compute_cpu_batch(req).unwrap();
18749        let input = ParkinsonVolatilityInput::from_slices(
18750            &high,
18751            &low,
18752            ParkinsonVolatilityParams { period: Some(9) },
18753        );
18754        let direct = parkinson_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
18755            .unwrap()
18756            .variance;
18757        let got = out.values_f64.unwrap();
18758        assert_series_eq(&got, &direct, 1e-12);
18759    }
18760
18761    #[test]
18762    fn l2_ehlers_signal_to_noise_output_matches_direct() {
18763        let candles = sample_candles();
18764        let combo = [
18765            ParamKV {
18766                key: "source",
18767                value: ParamValue::EnumString("hl2"),
18768            },
18769            ParamKV {
18770                key: "smooth_period",
18771                value: ParamValue::Int(10),
18772            },
18773        ];
18774        let combos = [IndicatorParamSet { params: &combo }];
18775        let req = IndicatorBatchRequest {
18776            indicator_id: "l2_ehlers_signal_to_noise",
18777            output_id: Some("value"),
18778            data: IndicatorDataRef::Candles {
18779                candles: &candles,
18780                source: Some("hl2"),
18781            },
18782            combos: &combos,
18783            kernel: Kernel::Auto,
18784        };
18785        let out = compute_cpu_batch(req).unwrap();
18786        let input = L2EhlersSignalToNoiseInput::from_slices(
18787            crate::utilities::data_loader::source_type(&candles, "hl2"),
18788            candles.high.as_slice(),
18789            candles.low.as_slice(),
18790            L2EhlersSignalToNoiseParams {
18791                smooth_period: Some(10),
18792            },
18793        );
18794        let direct = l2_ehlers_signal_to_noise_with_kernel(&input, Kernel::Auto.to_non_batch())
18795            .unwrap()
18796            .values;
18797        let got = out.values_f64.unwrap();
18798        assert_series_eq(&got, &direct, 1e-12);
18799    }
18800
18801    #[test]
18802    fn cycle_channel_oscillator_output_matches_direct() {
18803        let candles = sample_candles();
18804        let combo = [
18805            ParamKV {
18806                key: "source",
18807                value: ParamValue::EnumString("close"),
18808            },
18809            ParamKV {
18810                key: "short_cycle_length",
18811                value: ParamValue::Int(10),
18812            },
18813            ParamKV {
18814                key: "medium_cycle_length",
18815                value: ParamValue::Int(30),
18816            },
18817            ParamKV {
18818                key: "short_multiplier",
18819                value: ParamValue::Float(1.0),
18820            },
18821            ParamKV {
18822                key: "medium_multiplier",
18823                value: ParamValue::Float(3.0),
18824            },
18825        ];
18826        let combos = [IndicatorParamSet { params: &combo }];
18827        let req = IndicatorBatchRequest {
18828            indicator_id: "cycle_channel_oscillator",
18829            output_id: Some("fast"),
18830            data: IndicatorDataRef::Candles {
18831                candles: &candles,
18832                source: Some("close"),
18833            },
18834            combos: &combos,
18835            kernel: Kernel::Auto,
18836        };
18837        let out = compute_cpu_batch(req).unwrap();
18838        let input = CycleChannelOscillatorInput::from_slices(
18839            crate::utilities::data_loader::source_type(&candles, "close"),
18840            candles.high.as_slice(),
18841            candles.low.as_slice(),
18842            candles.close.as_slice(),
18843            CycleChannelOscillatorParams {
18844                short_cycle_length: Some(10),
18845                medium_cycle_length: Some(30),
18846                short_multiplier: Some(1.0),
18847                medium_multiplier: Some(3.0),
18848            },
18849        );
18850        let direct = cycle_channel_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
18851            .unwrap()
18852            .fast;
18853        let got = out.values_f64.unwrap();
18854        assert_series_eq(&got, &direct, 1e-12);
18855    }
18856
18857    #[test]
18858    fn andean_oscillator_output_matches_direct() {
18859        let candles = sample_candles();
18860        let combo = [
18861            ParamKV {
18862                key: "length",
18863                value: ParamValue::Int(50),
18864            },
18865            ParamKV {
18866                key: "signal_length",
18867                value: ParamValue::Int(9),
18868            },
18869        ];
18870        let combos = [IndicatorParamSet { params: &combo }];
18871        let req = IndicatorBatchRequest {
18872            indicator_id: "andean_oscillator",
18873            output_id: Some("bull"),
18874            data: IndicatorDataRef::Candles {
18875                candles: &candles,
18876                source: None,
18877            },
18878            combos: &combos,
18879            kernel: Kernel::Auto,
18880        };
18881        let out = compute_cpu_batch(req).unwrap();
18882        let input = AndeanOscillatorInput::from_slices(
18883            candles.open.as_slice(),
18884            candles.close.as_slice(),
18885            AndeanOscillatorParams {
18886                length: Some(50),
18887                signal_length: Some(9),
18888            },
18889        );
18890        let direct = andean_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
18891            .unwrap()
18892            .bull;
18893        let got = out.values_f64.unwrap();
18894        assert_series_eq(&got, &direct, 1e-12);
18895    }
18896
18897    #[test]
18898    fn daily_factor_output_matches_direct() {
18899        let (open, high, low, close) = sample_ohlc();
18900        let combo = [ParamKV {
18901            key: "threshold_level",
18902            value: ParamValue::Float(0.35),
18903        }];
18904        let combos = [IndicatorParamSet { params: &combo }];
18905        let req = IndicatorBatchRequest {
18906            indicator_id: "daily_factor",
18907            output_id: Some("signal"),
18908            data: IndicatorDataRef::Ohlc {
18909                open: &open,
18910                high: &high,
18911                low: &low,
18912                close: &close,
18913            },
18914            combos: &combos,
18915            kernel: Kernel::Auto,
18916        };
18917        let out = compute_cpu_batch(req).unwrap();
18918        let input = DailyFactorInput::from_slices(
18919            &open,
18920            &high,
18921            &low,
18922            &close,
18923            DailyFactorParams {
18924                threshold_level: Some(0.35),
18925            },
18926        );
18927        let direct = daily_factor_with_kernel(&input, Kernel::Auto.to_non_batch())
18928            .unwrap()
18929            .signal;
18930        let got = out.values_f64.unwrap();
18931        assert_series_eq(&got, &direct, 1e-12);
18932    }
18933
18934    #[test]
18935    fn ehlers_adaptive_cyber_cycle_output_matches_direct() {
18936        let candles = sample_candles();
18937        let combo = [
18938            ParamKV {
18939                key: "source",
18940                value: ParamValue::EnumString("hl2"),
18941            },
18942            ParamKV {
18943                key: "alpha",
18944                value: ParamValue::Float(0.07),
18945            },
18946        ];
18947        let combos = [IndicatorParamSet { params: &combo }];
18948        let req = IndicatorBatchRequest {
18949            indicator_id: "ehlers_adaptive_cyber_cycle",
18950            output_id: Some("cycle"),
18951            data: IndicatorDataRef::Candles {
18952                candles: &candles,
18953                source: Some("hl2"),
18954            },
18955            combos: &combos,
18956            kernel: Kernel::Auto,
18957        };
18958        let out = compute_cpu_batch(req).unwrap();
18959        let input = EhlersAdaptiveCyberCycleInput::from_slice(
18960            crate::utilities::data_loader::source_type(&candles, "hl2"),
18961            EhlersAdaptiveCyberCycleParams { alpha: Some(0.07) },
18962        );
18963        let direct = ehlers_adaptive_cyber_cycle_with_kernel(&input, Kernel::Auto.to_non_batch())
18964            .unwrap()
18965            .cycle;
18966        let got = out.values_f64.unwrap();
18967        assert_series_eq(&got, &direct, 1e-12);
18968    }
18969
18970    #[test]
18971    fn ehlers_simple_cycle_indicator_output_matches_direct() {
18972        let candles = sample_candles();
18973        let combo = [
18974            ParamKV {
18975                key: "source",
18976                value: ParamValue::EnumString("hl2"),
18977            },
18978            ParamKV {
18979                key: "alpha",
18980                value: ParamValue::Float(0.07),
18981            },
18982        ];
18983        let combos = [IndicatorParamSet { params: &combo }];
18984        let req = IndicatorBatchRequest {
18985            indicator_id: "ehlers_simple_cycle_indicator",
18986            output_id: Some("cycle"),
18987            data: IndicatorDataRef::Candles {
18988                candles: &candles,
18989                source: Some("hl2"),
18990            },
18991            combos: &combos,
18992            kernel: Kernel::Auto,
18993        };
18994        let out = compute_cpu_batch(req).unwrap();
18995        let input = EhlersSimpleCycleIndicatorInput::from_slice(
18996            crate::utilities::data_loader::source_type(&candles, "hl2"),
18997            EhlersSimpleCycleIndicatorParams { alpha: Some(0.07) },
18998        );
18999        let direct = ehlers_simple_cycle_indicator_with_kernel(&input, Kernel::Auto.to_non_batch())
19000            .unwrap()
19001            .cycle;
19002        let got = out.values_f64.unwrap();
19003        assert_series_eq(&got, &direct, 1e-12);
19004    }
19005
19006    #[test]
19007    fn l1_ehlers_phasor_output_matches_direct() {
19008        let candles = sample_candles();
19009        let combo = [ParamKV {
19010            key: "domestic_cycle_length",
19011            value: ParamValue::Int(15),
19012        }];
19013        let combos = [IndicatorParamSet { params: &combo }];
19014        let req = IndicatorBatchRequest {
19015            indicator_id: "l1_ehlers_phasor",
19016            output_id: Some("value"),
19017            data: IndicatorDataRef::Candles {
19018                candles: &candles,
19019                source: Some("close"),
19020            },
19021            combos: &combos,
19022            kernel: Kernel::Auto,
19023        };
19024        let out = compute_cpu_batch(req).unwrap();
19025        let input = L1EhlersPhasorInput::from_slice(
19026            candles.close.as_slice(),
19027            L1EhlersPhasorParams {
19028                domestic_cycle_length: Some(15),
19029            },
19030        );
19031        let direct = l1_ehlers_phasor_with_kernel(&input, Kernel::Auto.to_non_batch())
19032            .unwrap()
19033            .values;
19034        let got = out.values_f64.unwrap();
19035        assert_series_eq(&got, &direct, 1e-12);
19036    }
19037
19038    #[test]
19039    fn ehlers_smoothed_adaptive_momentum_output_matches_direct() {
19040        let candles = sample_candles();
19041        let combo = [
19042            ParamKV {
19043                key: "source",
19044                value: ParamValue::EnumString("hl2"),
19045            },
19046            ParamKV {
19047                key: "alpha",
19048                value: ParamValue::Float(0.07),
19049            },
19050            ParamKV {
19051                key: "cutoff",
19052                value: ParamValue::Float(8.0),
19053            },
19054        ];
19055        let combos = [IndicatorParamSet { params: &combo }];
19056        let req = IndicatorBatchRequest {
19057            indicator_id: "ehlers_smoothed_adaptive_momentum",
19058            output_id: Some("value"),
19059            data: IndicatorDataRef::Candles {
19060                candles: &candles,
19061                source: Some("hl2"),
19062            },
19063            combos: &combos,
19064            kernel: Kernel::Auto,
19065        };
19066        let out = compute_cpu_batch(req).unwrap();
19067        let input = EhlersSmoothedAdaptiveMomentumInput::from_slice(
19068            crate::utilities::data_loader::source_type(&candles, "hl2"),
19069            EhlersSmoothedAdaptiveMomentumParams {
19070                alpha: Some(0.07),
19071                cutoff: Some(8.0),
19072            },
19073        );
19074        let direct =
19075            ehlers_smoothed_adaptive_momentum_with_kernel(&input, Kernel::Auto.to_non_batch())
19076                .unwrap()
19077                .values;
19078        let got = out.values_f64.unwrap();
19079        assert_series_eq(&got, &direct, 1e-12);
19080    }
19081
19082    #[test]
19083    fn ewma_volatility_output_matches_direct() {
19084        let close = sample_series();
19085        let combo = [ParamKV {
19086            key: "lambda",
19087            value: ParamValue::Float(0.94),
19088        }];
19089        let combos = [IndicatorParamSet { params: &combo }];
19090        let req = IndicatorBatchRequest {
19091            indicator_id: "ewma_volatility",
19092            output_id: Some("value"),
19093            data: IndicatorDataRef::Slice { values: &close },
19094            combos: &combos,
19095            kernel: Kernel::Auto,
19096        };
19097        let out = compute_cpu_batch(req).unwrap();
19098        let input =
19099            EwmaVolatilityInput::from_slice(&close, EwmaVolatilityParams { lambda: Some(0.94) });
19100        let direct = ewma_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
19101            .unwrap()
19102            .values;
19103        let got = out.values_f64.unwrap();
19104        assert_series_eq(&got, &direct, 1e-12);
19105    }
19106
19107    #[test]
19108    fn random_walk_index_output_matches_direct() {
19109        let open = sample_series();
19110        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
19111        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
19112        let close: Vec<f64> = open
19113            .iter()
19114            .enumerate()
19115            .map(|(i, v)| v + 0.1 * (i as f64 + 1.0))
19116            .collect();
19117        let combo = [ParamKV {
19118            key: "length",
19119            value: ParamValue::Int(14),
19120        }];
19121        let combos = [IndicatorParamSet { params: &combo }];
19122        let req = IndicatorBatchRequest {
19123            indicator_id: "random_walk_index",
19124            output_id: Some("high"),
19125            data: IndicatorDataRef::Ohlc {
19126                open: &open,
19127                high: &high,
19128                low: &low,
19129                close: &close,
19130            },
19131            combos: &combos,
19132            kernel: Kernel::Auto,
19133        };
19134        let out = compute_cpu_batch(req).unwrap();
19135        let input = RandomWalkIndexInput::from_slices(
19136            &high,
19137            &low,
19138            &close,
19139            RandomWalkIndexParams { length: Some(14) },
19140        );
19141        let direct = random_walk_index_with_kernel(&input, Kernel::Auto.to_non_batch())
19142            .unwrap()
19143            .high;
19144        let got = out.values_f64.unwrap();
19145        assert_series_eq(&got, &direct, 1e-12);
19146    }
19147
19148    #[test]
19149    fn price_moving_average_ratio_percentile_output_matches_direct() {
19150        let open = sample_series();
19151        let high: Vec<f64> = open
19152            .iter()
19153            .enumerate()
19154            .map(|(i, v)| v + 1.0 + (i as f64 * 0.03).sin() * 0.15)
19155            .collect();
19156        let low: Vec<f64> = open
19157            .iter()
19158            .enumerate()
19159            .map(|(i, v)| v - 1.0 - (i as f64 * 0.05).cos() * 0.12)
19160            .collect();
19161        let close: Vec<f64> = open
19162            .iter()
19163            .enumerate()
19164            .map(|(i, v)| v + 0.12 * (i as f64 + 1.0))
19165            .collect();
19166        let volume: Vec<f64> = (0..open.len())
19167            .map(|i| 1_000.0 + i as f64 * 2.0 + (i as f64 * 0.09).sin() * 40.0)
19168            .collect();
19169        let combo = [
19170            ParamKV {
19171                key: "source",
19172                value: ParamValue::EnumString("close"),
19173            },
19174            ParamKV {
19175                key: "ma_length",
19176                value: ParamValue::Int(20),
19177            },
19178            ParamKV {
19179                key: "ma_type",
19180                value: ParamValue::EnumString("vwma"),
19181            },
19182            ParamKV {
19183                key: "pmarp_lookback",
19184                value: ParamValue::Int(30),
19185            },
19186            ParamKV {
19187                key: "signal_ma_length",
19188                value: ParamValue::Int(10),
19189            },
19190            ParamKV {
19191                key: "signal_ma_type",
19192                value: ParamValue::EnumString("sma"),
19193            },
19194            ParamKV {
19195                key: "line_mode",
19196                value: ParamValue::EnumString("pmarp"),
19197            },
19198        ];
19199        let combos = [IndicatorParamSet { params: &combo }];
19200        let req = IndicatorBatchRequest {
19201            indicator_id: "price_moving_average_ratio_percentile",
19202            output_id: Some("plotline"),
19203            data: IndicatorDataRef::Candles {
19204                candles: &crate::utilities::data_loader::Candles {
19205                    timestamp: vec![0; open.len()],
19206                    open: open.clone(),
19207                    high: high.clone(),
19208                    low: low.clone(),
19209                    close: close.clone(),
19210                    volume: volume.clone(),
19211                    fields: crate::utilities::data_loader::CandleFieldFlags {
19212                        open: true,
19213                        high: true,
19214                        low: true,
19215                        close: true,
19216                        volume: true,
19217                    },
19218                    hl2: high
19219                        .iter()
19220                        .zip(low.iter())
19221                        .map(|(h, l)| (h + l) * 0.5)
19222                        .collect(),
19223                    hlc3: high
19224                        .iter()
19225                        .zip(low.iter())
19226                        .zip(close.iter())
19227                        .map(|((h, l), c)| (h + l + c) / 3.0)
19228                        .collect(),
19229                    ohlc4: open
19230                        .iter()
19231                        .zip(high.iter())
19232                        .zip(low.iter())
19233                        .zip(close.iter())
19234                        .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19235                        .collect(),
19236                    hlcc4: high
19237                        .iter()
19238                        .zip(low.iter())
19239                        .zip(close.iter())
19240                        .map(|((h, l), c)| (h + l + c + c) * 0.25)
19241                        .collect(),
19242                },
19243                source: Some("close"),
19244            },
19245            combos: &combos,
19246            kernel: Kernel::Auto,
19247        };
19248        let out = compute_cpu_batch(req).unwrap();
19249        let input = PriceMovingAverageRatioPercentileInput::from_slices(
19250            &close,
19251            &volume,
19252            PriceMovingAverageRatioPercentileParams {
19253                ma_length: Some(20),
19254                ma_type: Some(PriceMovingAverageRatioPercentileMaType::Vwma),
19255                pmarp_lookback: Some(30),
19256                signal_ma_length: Some(10),
19257                signal_ma_type: Some(PriceMovingAverageRatioPercentileMaType::Sma),
19258                line_mode: Some(PriceMovingAverageRatioPercentileLineMode::Pmarp),
19259            },
19260        );
19261        let direct =
19262            price_moving_average_ratio_percentile_with_kernel(&input, Kernel::Auto.to_non_batch())
19263                .unwrap()
19264                .plotline;
19265        let got = out.values_f64.unwrap();
19266        assert_series_eq(&got, &direct, 1e-12);
19267    }
19268
19269    #[test]
19270    fn trend_trigger_factor_output_matches_direct() {
19271        let base = sample_series();
19272        let high: Vec<f64> = base
19273            .iter()
19274            .enumerate()
19275            .map(|(i, v)| v + 1.0 + (i as f64 * 0.03).sin() * 0.15)
19276            .collect();
19277        let low: Vec<f64> = base
19278            .iter()
19279            .enumerate()
19280            .map(|(i, v)| v - 1.0 - (i as f64 * 0.05).cos() * 0.12)
19281            .collect();
19282        let combo = [ParamKV {
19283            key: "length",
19284            value: ParamValue::Int(15),
19285        }];
19286        let combos = [IndicatorParamSet { params: &combo }];
19287        let req = IndicatorBatchRequest {
19288            indicator_id: "trend_trigger_factor",
19289            output_id: Some("value"),
19290            data: IndicatorDataRef::HighLow {
19291                high: &high,
19292                low: &low,
19293            },
19294            combos: &combos,
19295            kernel: Kernel::Auto,
19296        };
19297        let out = compute_cpu_batch(req).unwrap();
19298        let input = TrendTriggerFactorInput::from_slices(
19299            &high,
19300            &low,
19301            TrendTriggerFactorParams { length: Some(15) },
19302        );
19303        let direct = trend_trigger_factor_with_kernel(&input, Kernel::Auto.to_non_batch())
19304            .unwrap()
19305            .values;
19306        let got = out.values_f64.unwrap();
19307        assert_series_eq(&got, &direct, 1e-12);
19308    }
19309
19310    #[test]
19311    fn mesa_stochastic_multi_length_output_matches_direct() {
19312        let source: Vec<f64> = (0..180)
19313            .map(|i| 100.0 + (i as f64 * 0.09).sin() * 2.0 + i as f64 * 0.015)
19314            .collect();
19315        let high: Vec<f64> = source.iter().map(|v| v + 1.0).collect();
19316        let low: Vec<f64> = source.iter().map(|v| v - 1.0).collect();
19317        let open = source.clone();
19318        let volume: Vec<f64> = (0..180).map(|i| 1000.0 + i as f64).collect();
19319        let combo = [
19320            ParamKV {
19321                key: "source",
19322                value: ParamValue::EnumString("close"),
19323            },
19324            ParamKV {
19325                key: "length_1",
19326                value: ParamValue::Int(48),
19327            },
19328            ParamKV {
19329                key: "length_2",
19330                value: ParamValue::Int(21),
19331            },
19332            ParamKV {
19333                key: "length_3",
19334                value: ParamValue::Int(9),
19335            },
19336            ParamKV {
19337                key: "length_4",
19338                value: ParamValue::Int(6),
19339            },
19340            ParamKV {
19341                key: "trigger_length",
19342                value: ParamValue::Int(2),
19343            },
19344        ];
19345        let combos = [IndicatorParamSet { params: &combo }];
19346        let req = IndicatorBatchRequest {
19347            indicator_id: "mesa_stochastic_multi_length",
19348            output_id: Some("mesa_1"),
19349            data: IndicatorDataRef::Candles {
19350                candles: &crate::utilities::data_loader::Candles {
19351                    timestamp: vec![0; source.len()],
19352                    open: open.clone(),
19353                    high: high.clone(),
19354                    low: low.clone(),
19355                    close: source.clone(),
19356                    volume,
19357                    fields: crate::utilities::data_loader::CandleFieldFlags {
19358                        open: true,
19359                        high: true,
19360                        low: true,
19361                        close: true,
19362                        volume: true,
19363                    },
19364                    hl2: high
19365                        .iter()
19366                        .zip(low.iter())
19367                        .map(|(h, l)| (h + l) * 0.5)
19368                        .collect(),
19369                    hlc3: high
19370                        .iter()
19371                        .zip(low.iter())
19372                        .zip(source.iter())
19373                        .map(|((h, l), c)| (h + l + c) / 3.0)
19374                        .collect(),
19375                    ohlc4: open
19376                        .iter()
19377                        .zip(high.iter())
19378                        .zip(low.iter())
19379                        .zip(source.iter())
19380                        .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19381                        .collect(),
19382                    hlcc4: high
19383                        .iter()
19384                        .zip(low.iter())
19385                        .zip(source.iter())
19386                        .map(|((h, l), c)| (h + l + c + c) * 0.25)
19387                        .collect(),
19388                },
19389                source: Some("close"),
19390            },
19391            combos: &combos,
19392            kernel: Kernel::Auto,
19393        };
19394        let out = compute_cpu_batch(req).unwrap();
19395        let input = MesaStochasticMultiLengthInput::from_slices(
19396            &source,
19397            MesaStochasticMultiLengthParams::default(),
19398        );
19399        let direct = mesa_stochastic_multi_length_with_kernel(&input, Kernel::Auto.to_non_batch())
19400            .unwrap()
19401            .mesa_1;
19402        let got = out.values_f64.unwrap();
19403        assert_series_eq(&got, &direct, 1e-12);
19404    }
19405
19406    #[test]
19407    fn spearman_correlation_output_matches_direct() {
19408        let close: Vec<f64> = (0..180)
19409            .map(|i| 100.0 + (i as f64 * 0.13).sin() * 2.0 + i as f64 * 0.02)
19410            .collect();
19411        let open: Vec<f64> = (0..180)
19412            .map(|i| 98.0 + (i as f64 * 0.07).cos() * 1.6 + i as f64 * 0.015)
19413            .collect();
19414        let high: Vec<f64> = close.iter().map(|v| v + 1.0).collect();
19415        let low: Vec<f64> = close.iter().map(|v| v - 1.0).collect();
19416        let volume: Vec<f64> = (0..180).map(|i| 1000.0 + i as f64).collect();
19417        let combo = [
19418            ParamKV {
19419                key: "source",
19420                value: ParamValue::EnumString("close"),
19421            },
19422            ParamKV {
19423                key: "comparison_source",
19424                value: ParamValue::EnumString("open"),
19425            },
19426            ParamKV {
19427                key: "lookback",
19428                value: ParamValue::Int(30),
19429            },
19430            ParamKV {
19431                key: "smoothing_length",
19432                value: ParamValue::Int(3),
19433            },
19434        ];
19435        let combos = [IndicatorParamSet { params: &combo }];
19436        let req = IndicatorBatchRequest {
19437            indicator_id: "spearman_correlation",
19438            output_id: Some("smoothed"),
19439            data: IndicatorDataRef::Candles {
19440                candles: &crate::utilities::data_loader::Candles {
19441                    timestamp: vec![0; close.len()],
19442                    open: open.clone(),
19443                    high: high.clone(),
19444                    low: low.clone(),
19445                    close: close.clone(),
19446                    volume,
19447                    fields: crate::utilities::data_loader::CandleFieldFlags {
19448                        open: true,
19449                        high: true,
19450                        low: true,
19451                        close: true,
19452                        volume: true,
19453                    },
19454                    hl2: high
19455                        .iter()
19456                        .zip(low.iter())
19457                        .map(|(h, l)| (h + l) * 0.5)
19458                        .collect(),
19459                    hlc3: high
19460                        .iter()
19461                        .zip(low.iter())
19462                        .zip(close.iter())
19463                        .map(|((h, l), c)| (h + l + c) / 3.0)
19464                        .collect(),
19465                    ohlc4: open
19466                        .iter()
19467                        .zip(high.iter())
19468                        .zip(low.iter())
19469                        .zip(close.iter())
19470                        .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19471                        .collect(),
19472                    hlcc4: high
19473                        .iter()
19474                        .zip(low.iter())
19475                        .zip(close.iter())
19476                        .map(|((h, l), c)| (h + l + c + c) * 0.25)
19477                        .collect(),
19478                },
19479                source: Some("close"),
19480            },
19481            combos: &combos,
19482            kernel: Kernel::Auto,
19483        };
19484        let out = compute_cpu_batch(req).unwrap();
19485        let input = SpearmanCorrelationInput::from_slices(
19486            &close,
19487            &open,
19488            SpearmanCorrelationParams {
19489                lookback: Some(30),
19490                smoothing_length: Some(3),
19491            },
19492        );
19493        let direct = spearman_correlation_with_kernel(&input, Kernel::Auto.to_non_batch())
19494            .unwrap()
19495            .smoothed;
19496        let got = out.values_f64.unwrap();
19497        assert_series_eq(&got, &direct, 1e-12);
19498    }
19499
19500    #[test]
19501    fn relative_strength_index_wave_indicator_output_matches_direct() {
19502        let open = sample_series();
19503        let close: Vec<f64> = open
19504            .iter()
19505            .enumerate()
19506            .map(|(i, v)| v + 0.2 * (i as f64 * 0.1).sin())
19507            .collect();
19508        let high: Vec<f64> = close.iter().map(|v| v + 0.9).collect();
19509        let low: Vec<f64> = close.iter().map(|v| v - 0.8).collect();
19510        let volume: Vec<f64> = (0..close.len()).map(|i| 1_000.0 + i as f64).collect();
19511        let candles = crate::utilities::data_loader::Candles {
19512            timestamp: vec![0; close.len()],
19513            open: open.clone(),
19514            high: high.clone(),
19515            low: low.clone(),
19516            close: close.clone(),
19517            volume,
19518            fields: crate::utilities::data_loader::CandleFieldFlags {
19519                open: true,
19520                high: true,
19521                low: true,
19522                close: true,
19523                volume: true,
19524            },
19525            hl2: high
19526                .iter()
19527                .zip(low.iter())
19528                .map(|(h, l)| (h + l) * 0.5)
19529                .collect(),
19530            hlc3: high
19531                .iter()
19532                .zip(low.iter())
19533                .zip(close.iter())
19534                .map(|((h, l), c)| (h + l + c) / 3.0)
19535                .collect(),
19536            ohlc4: open
19537                .iter()
19538                .zip(high.iter())
19539                .zip(low.iter())
19540                .zip(close.iter())
19541                .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19542                .collect(),
19543            hlcc4: high
19544                .iter()
19545                .zip(low.iter())
19546                .zip(close.iter())
19547                .map(|((h, l), c)| (h + l + 2.0 * c) * 0.25)
19548                .collect(),
19549        };
19550        let combo = [
19551            ParamKV {
19552                key: "source",
19553                value: ParamValue::EnumString("hlcc4"),
19554            },
19555            ParamKV {
19556                key: "rsi_length",
19557                value: ParamValue::Int(14),
19558            },
19559            ParamKV {
19560                key: "length1",
19561                value: ParamValue::Int(2),
19562            },
19563            ParamKV {
19564                key: "length2",
19565                value: ParamValue::Int(5),
19566            },
19567            ParamKV {
19568                key: "length3",
19569                value: ParamValue::Int(9),
19570            },
19571            ParamKV {
19572                key: "length4",
19573                value: ParamValue::Int(13),
19574            },
19575        ];
19576        let combos = [IndicatorParamSet { params: &combo }];
19577        let req = IndicatorBatchRequest {
19578            indicator_id: "relative_strength_index_wave_indicator",
19579            output_id: Some("rsi_ma1"),
19580            data: IndicatorDataRef::Candles {
19581                candles: &candles,
19582                source: Some("hlcc4"),
19583            },
19584            combos: &combos,
19585            kernel: Kernel::Auto,
19586        };
19587        let out = compute_cpu_batch(req).unwrap();
19588        let input = RelativeStrengthIndexWaveIndicatorInput::from_slices(
19589            &candles.hlcc4,
19590            &high,
19591            &low,
19592            RelativeStrengthIndexWaveIndicatorParams {
19593                rsi_length: Some(14),
19594                length1: Some(2),
19595                length2: Some(5),
19596                length3: Some(9),
19597                length4: Some(13),
19598            },
19599        );
19600        let direct =
19601            relative_strength_index_wave_indicator_with_kernel(&input, Kernel::Auto.to_non_batch())
19602                .unwrap()
19603                .rsi_ma1;
19604        let got = out.values_f64.unwrap();
19605        assert_series_eq(&got, &direct, 1e-12);
19606    }
19607
19608    #[test]
19609    fn accumulation_swing_index_output_matches_direct() {
19610        let open = sample_series();
19611        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
19612        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
19613        let close: Vec<f64> = open
19614            .iter()
19615            .enumerate()
19616            .map(|(i, v)| v + 0.1 * (i as f64 + 1.0))
19617            .collect();
19618        let combo = [ParamKV {
19619            key: "daily_limit",
19620            value: ParamValue::Float(10_000.0),
19621        }];
19622        let combos = [IndicatorParamSet { params: &combo }];
19623        let req = IndicatorBatchRequest {
19624            indicator_id: "accumulation_swing_index",
19625            output_id: Some("value"),
19626            data: IndicatorDataRef::Ohlc {
19627                open: &open,
19628                high: &high,
19629                low: &low,
19630                close: &close,
19631            },
19632            combos: &combos,
19633            kernel: Kernel::Auto,
19634        };
19635        let out = compute_cpu_batch(req).unwrap();
19636        let input = AccumulationSwingIndexInput::from_slices(
19637            &open,
19638            &high,
19639            &low,
19640            &close,
19641            AccumulationSwingIndexParams {
19642                daily_limit: Some(10_000.0),
19643            },
19644        );
19645        let direct = accumulation_swing_index_with_kernel(&input, Kernel::Auto.to_non_batch())
19646            .unwrap()
19647            .values;
19648        let got = out.values_f64.unwrap();
19649        assert_series_eq(&got, &direct, 1e-12);
19650    }
19651
19652    #[test]
19653    fn ichimoku_oscillator_output_matches_direct() {
19654        let open: Vec<f64> = (0..160)
19655            .map(|i| 100.0 + (i as f64 * 0.07).sin() * 3.0 + i as f64 * 0.02)
19656            .collect();
19657        let high: Vec<f64> = open
19658            .iter()
19659            .enumerate()
19660            .map(|(i, v)| v + 1.2 + (i as f64 * 0.03).sin() * 0.25)
19661            .collect();
19662        let low: Vec<f64> = open
19663            .iter()
19664            .enumerate()
19665            .map(|(i, v)| v - 1.1 - (i as f64 * 0.05).cos() * 0.2)
19666            .collect();
19667        let close: Vec<f64> = open
19668            .iter()
19669            .enumerate()
19670            .map(|(i, v)| v + 0.12 * (i as f64 + 1.0))
19671            .collect();
19672        let combo = [
19673            ParamKV {
19674                key: "conversion_periods",
19675                value: ParamValue::Int(9),
19676            },
19677            ParamKV {
19678                key: "base_periods",
19679                value: ParamValue::Int(26),
19680            },
19681            ParamKV {
19682                key: "lagging_span_periods",
19683                value: ParamValue::Int(52),
19684            },
19685            ParamKV {
19686                key: "displacement",
19687                value: ParamValue::Int(26),
19688            },
19689            ParamKV {
19690                key: "ma_length",
19691                value: ParamValue::Int(12),
19692            },
19693            ParamKV {
19694                key: "smoothing_length",
19695                value: ParamValue::Int(3),
19696            },
19697            ParamKV {
19698                key: "extra_smoothing",
19699                value: ParamValue::Bool(true),
19700            },
19701            ParamKV {
19702                key: "normalize",
19703                value: ParamValue::EnumString("window"),
19704            },
19705            ParamKV {
19706                key: "window_size",
19707                value: ParamValue::Int(20),
19708            },
19709            ParamKV {
19710                key: "clamp",
19711                value: ParamValue::Bool(true),
19712            },
19713            ParamKV {
19714                key: "top_band",
19715                value: ParamValue::Float(2.0),
19716            },
19717            ParamKV {
19718                key: "mid_band",
19719                value: ParamValue::Float(1.5),
19720            },
19721        ];
19722        let combos = [IndicatorParamSet { params: &combo }];
19723        let req = IndicatorBatchRequest {
19724            indicator_id: "ichimoku_oscillator",
19725            output_id: Some("signal"),
19726            data: IndicatorDataRef::Ohlc {
19727                open: &open,
19728                high: &high,
19729                low: &low,
19730                close: &close,
19731            },
19732            combos: &combos,
19733            kernel: Kernel::Auto,
19734        };
19735        let out = compute_cpu_batch(req).unwrap();
19736        let input = IchimokuOscillatorInput::from_slices(
19737            &high,
19738            &low,
19739            &close,
19740            &close,
19741            IchimokuOscillatorParams {
19742                conversion_periods: Some(9),
19743                base_periods: Some(26),
19744                lagging_span_periods: Some(52),
19745                displacement: Some(26),
19746                ma_length: Some(12),
19747                smoothing_length: Some(3),
19748                extra_smoothing: Some(true),
19749                normalize: Some(IchimokuOscillatorNormalizeMode::Window),
19750                window_size: Some(20),
19751                clamp: Some(true),
19752                top_band: Some(2.0),
19753                mid_band: Some(1.5),
19754            },
19755        );
19756        let direct = ichimoku_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
19757            .unwrap()
19758            .signal;
19759        let got = out.values_f64.unwrap();
19760        assert_series_eq(&got, &direct, 1e-12);
19761    }
19762
19763    #[test]
19764    fn volatility_quality_index_output_matches_direct() {
19765        let open = sample_series();
19766        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
19767        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
19768        let close: Vec<f64> = open
19769            .iter()
19770            .enumerate()
19771            .map(|(i, v)| v + 0.2 * (i as f64 + 1.0))
19772            .collect();
19773        let combo = [
19774            ParamKV {
19775                key: "fast_length",
19776                value: ParamValue::Int(9),
19777            },
19778            ParamKV {
19779                key: "slow_length",
19780                value: ParamValue::Int(21),
19781            },
19782        ];
19783        let combos = [IndicatorParamSet { params: &combo }];
19784        let req = IndicatorBatchRequest {
19785            indicator_id: "volatility_quality_index",
19786            output_id: Some("fast_sma"),
19787            data: IndicatorDataRef::Ohlc {
19788                open: &open,
19789                high: &high,
19790                low: &low,
19791                close: &close,
19792            },
19793            combos: &combos,
19794            kernel: Kernel::Auto,
19795        };
19796        let out = compute_cpu_batch(req).unwrap();
19797        let input = VolatilityQualityIndexInput::from_slices(
19798            &open,
19799            &high,
19800            &low,
19801            &close,
19802            VolatilityQualityIndexParams {
19803                fast_length: Some(9),
19804                slow_length: Some(21),
19805            },
19806        );
19807        let direct = volatility_quality_index_with_kernel(&input, Kernel::Auto.to_non_batch())
19808            .unwrap()
19809            .fast_sma;
19810        let got = out.values_f64.unwrap();
19811        assert_series_eq(&got, &direct, 1e-12);
19812    }
19813
19814    #[test]
19815    fn vwap_deviation_oscillator_output_matches_direct() {
19816        let open = sample_series();
19817        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
19818        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
19819        let close: Vec<f64> = open
19820            .iter()
19821            .enumerate()
19822            .map(|(i, v)| v + 0.15 * (i as f64 + 1.0))
19823            .collect();
19824        let volume: Vec<f64> = (0..close.len())
19825            .map(|i| 1000.0 + (i as f64 * 11.0))
19826            .collect();
19827        let timestamps: Vec<i64> = (0..close.len())
19828            .map(|i| 1_700_000_000_000i64 + (i as i64) * 14_400_000)
19829            .collect();
19830        let candles = Candles::new(
19831            timestamps.clone(),
19832            open.clone(),
19833            high.clone(),
19834            low.clone(),
19835            close.clone(),
19836            volume.clone(),
19837        );
19838        let combo = [
19839            ParamKV {
19840                key: "session_mode",
19841                value: ParamValue::EnumString("rolling_bars"),
19842            },
19843            ParamKV {
19844                key: "rolling_period",
19845                value: ParamValue::Int(20),
19846            },
19847            ParamKV {
19848                key: "rolling_days",
19849                value: ParamValue::Int(30),
19850            },
19851            ParamKV {
19852                key: "use_close",
19853                value: ParamValue::Bool(false),
19854            },
19855            ParamKV {
19856                key: "deviation_mode",
19857                value: ParamValue::EnumString("zscore"),
19858            },
19859            ParamKV {
19860                key: "z_window",
19861                value: ParamValue::Int(25),
19862            },
19863        ];
19864        let combos = [IndicatorParamSet { params: &combo }];
19865        let req = IndicatorBatchRequest {
19866            indicator_id: "vwap_deviation_oscillator",
19867            output_id: Some("osc"),
19868            data: IndicatorDataRef::Candles {
19869                candles: &candles,
19870                source: None,
19871            },
19872            combos: &combos,
19873            kernel: Kernel::Auto,
19874        };
19875        let out = compute_cpu_batch(req).unwrap();
19876        let input = VwapDeviationOscillatorInput::from_slices(
19877            &timestamps,
19878            &high,
19879            &low,
19880            &close,
19881            &volume,
19882            VwapDeviationOscillatorParams {
19883                session_mode: Some(VwapDeviationSessionMode::RollingBars),
19884                rolling_period: Some(20),
19885                rolling_days: Some(30),
19886                use_close: Some(false),
19887                deviation_mode: Some(VwapDeviationMode::ZScore),
19888                z_window: Some(25),
19889                pct_vol_lookback: Some(100),
19890                pct_min_sigma: Some(0.1),
19891                abs_vol_lookback: Some(100),
19892            },
19893        );
19894        let direct = vwap_deviation_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
19895            .unwrap()
19896            .osc;
19897        let got = out.values_f64.unwrap();
19898        assert_series_eq(&got, &direct, 1e-12);
19899    }
19900
19901    #[test]
19902    fn volume_zone_oscillator_output_matches_direct() {
19903        let close = sample_series();
19904        let volume: Vec<f64> = close
19905            .iter()
19906            .enumerate()
19907            .map(|(i, _)| 1000.0 + (i as f64 * 17.0))
19908            .collect();
19909        let combo = [
19910            ParamKV {
19911                key: "length",
19912                value: ParamValue::Int(14),
19913            },
19914            ParamKV {
19915                key: "intraday_smoothing",
19916                value: ParamValue::Bool(true),
19917            },
19918            ParamKV {
19919                key: "noise_filter",
19920                value: ParamValue::Int(4),
19921            },
19922        ];
19923        let combos = [IndicatorParamSet { params: &combo }];
19924        let req = IndicatorBatchRequest {
19925            indicator_id: "volume_zone_oscillator",
19926            output_id: Some("value"),
19927            data: IndicatorDataRef::CloseVolume {
19928                close: &close,
19929                volume: &volume,
19930            },
19931            combos: &combos,
19932            kernel: Kernel::Auto,
19933        };
19934        let out = compute_cpu_batch(req).unwrap();
19935        let input = VolumeZoneOscillatorInput::from_slices(
19936            &close,
19937            &volume,
19938            VolumeZoneOscillatorParams {
19939                length: Some(14),
19940                intraday_smoothing: Some(true),
19941                noise_filter: Some(4),
19942            },
19943        );
19944        let direct = volume_zone_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
19945            .unwrap()
19946            .values;
19947        let got = out.values_f64.unwrap();
19948        assert_series_eq(&got, &direct, 1e-12);
19949    }
19950
19951    #[test]
19952    fn ttm_trend_bool_output_matches_direct() {
19953        let (open, high, low, close) = sample_ohlc();
19954        let combo = [ParamKV {
19955            key: "period",
19956            value: ParamValue::Int(5),
19957        }];
19958        let combos = [IndicatorParamSet { params: &combo }];
19959        let req = IndicatorBatchRequest {
19960            indicator_id: "ttm_trend",
19961            output_id: Some("value"),
19962            data: IndicatorDataRef::Ohlc {
19963                open: &open,
19964                high: &high,
19965                low: &low,
19966                close: &close,
19967            },
19968            combos: &combos,
19969            kernel: Kernel::Auto,
19970        };
19971        let out = compute_cpu_batch(req).unwrap();
19972        let source: Vec<f64> = high.iter().zip(&low).map(|(h, l)| 0.5 * (h + l)).collect();
19973        let input = TtmTrendInput::from_slices(&source, &close, TtmTrendParams { period: Some(5) });
19974        let direct = ttm_trend_with_kernel(&input, Kernel::Auto.to_non_batch())
19975            .unwrap()
19976            .values;
19977        assert_eq!(out.values_bool.unwrap(), direct);
19978    }
19979
19980    fn build_default_params_for_indicator(
19981        info: &crate::indicators::registry::IndicatorInfo,
19982    ) -> Option<Vec<ParamKV<'static>>> {
19983        let mut params: Vec<ParamKV<'static>> = Vec::new();
19984        for p in &info.params {
19985            if p.key.eq_ignore_ascii_case("output") {
19986                continue;
19987            }
19988            let value = if let Some(default) = p.default {
19989                match default {
19990                    crate::indicators::registry::ParamValueStatic::Int(v) => {
19991                        Some(ParamValue::Int(v))
19992                    }
19993                    crate::indicators::registry::ParamValueStatic::Float(v) => {
19994                        Some(ParamValue::Float(v))
19995                    }
19996                    crate::indicators::registry::ParamValueStatic::Bool(v) => {
19997                        Some(ParamValue::Bool(v))
19998                    }
19999                    crate::indicators::registry::ParamValueStatic::EnumString(v) => {
20000                        Some(ParamValue::EnumString(v))
20001                    }
20002                }
20003            } else {
20004                match p.kind {
20005                    IndicatorParamKind::Int => {
20006                        let mut v = p.min.unwrap_or(14.0).round() as i64;
20007                        if v < 0 {
20008                            v = 0;
20009                        }
20010                        if let Some(max) = p.max {
20011                            v = v.min(max.round() as i64);
20012                        }
20013                        Some(ParamValue::Int(v))
20014                    }
20015                    IndicatorParamKind::Float => {
20016                        let mut v = p.min.unwrap_or(1.0);
20017                        if !v.is_finite() {
20018                            v = 1.0;
20019                        }
20020                        if let Some(max) = p.max {
20021                            v = v.min(max);
20022                        }
20023                        Some(ParamValue::Float(v))
20024                    }
20025                    IndicatorParamKind::Bool => Some(ParamValue::Bool(false)),
20026                    IndicatorParamKind::EnumString => {
20027                        p.enum_values.first().copied().map(ParamValue::EnumString)
20028                    }
20029                }
20030            };
20031
20032            match value {
20033                Some(v) => params.push(ParamKV {
20034                    key: p.key,
20035                    value: v,
20036                }),
20037                None => {
20038                    if p.required {
20039                        return None;
20040                    }
20041                }
20042            }
20043        }
20044        Some(params)
20045    }
20046
20047    fn median_ns(mut samples: Vec<u128>) -> u128 {
20048        samples.sort_unstable();
20049        samples[samples.len() / 2]
20050    }
20051
20052    #[test]
20053    #[ignore]
20054    fn full_cpu_dispatch_perf_sweep_vs_direct_route() {
20055        const LEN: usize = 10_000;
20056        const REPS: usize = 5;
20057
20058        let open: Vec<f64> = (0..LEN).map(|i| 100.0 + (i as f64 * 0.01)).collect();
20059        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
20060        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
20061        let close: Vec<f64> = open.iter().map(|v| v + 0.25).collect();
20062        let volume: Vec<f64> = (0..LEN).map(|i| 1000.0 + (i as f64 * 0.5)).collect();
20063        let timestamp: Vec<i64> = (0..LEN).map(|i| i as i64).collect();
20064        let candles = crate::utilities::data_loader::Candles::new(
20065            timestamp,
20066            open.clone(),
20067            high.clone(),
20068            low.clone(),
20069            close.clone(),
20070            volume.clone(),
20071        );
20072
20073        let infos: Vec<_> = list_indicators()
20074            .iter()
20075            .filter(|i| i.capabilities.supports_cpu_batch)
20076            .collect();
20077        let mut rows: Vec<(String, f64, f64, f64)> = Vec::new();
20078        let mut failures: Vec<String> = Vec::new();
20079
20080        for info in infos {
20081            let Some(output) = info.outputs.first() else {
20082                failures.push(format!("{}: no outputs", info.id));
20083                continue;
20084            };
20085            let output_id = output.id;
20086            let Some(params_vec) = build_default_params_for_indicator(info) else {
20087                failures.push(format!("{}: missing required param defaults", info.id));
20088                continue;
20089            };
20090            let combos = [IndicatorParamSet {
20091                params: params_vec.as_slice(),
20092            }];
20093            let data = match info.input_kind {
20094                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
20095                    values: close.as_slice(),
20096                },
20097                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
20098                    candles: &candles,
20099                    source: None,
20100                },
20101                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
20102                    open: open.as_slice(),
20103                    high: high.as_slice(),
20104                    low: low.as_slice(),
20105                    close: close.as_slice(),
20106                },
20107                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
20108                    open: open.as_slice(),
20109                    high: high.as_slice(),
20110                    low: low.as_slice(),
20111                    close: close.as_slice(),
20112                    volume: volume.as_slice(),
20113                },
20114                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
20115                    high: high.as_slice(),
20116                    low: low.as_slice(),
20117                },
20118                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
20119                    close: close.as_slice(),
20120                    volume: volume.as_slice(),
20121                },
20122            };
20123
20124            let req = IndicatorBatchRequest {
20125                indicator_id: info.id,
20126                output_id: Some(output_id),
20127                data,
20128                combos: &combos,
20129                kernel: Kernel::Auto,
20130            };
20131
20132            let dispatch_once = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20133                compute_cpu_batch(req)
20134            })) {
20135                Ok(Ok(v)) => v,
20136                Ok(Err(e)) => {
20137                    failures.push(format!("{}: dispatch error: {}", info.id, e));
20138                    continue;
20139                }
20140                Err(_) => {
20141                    failures.push(format!("{}: dispatch panic", info.id));
20142                    continue;
20143                }
20144            };
20145            let direct_once = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20146                dispatch_cpu_batch_by_indicator(req, info.id, output_id)
20147            })) {
20148                Ok(Ok(v)) => v,
20149                Ok(Err(e)) => {
20150                    failures.push(format!("{}: direct-route error: {}", info.id, e));
20151                    continue;
20152                }
20153                Err(_) => {
20154                    failures.push(format!("{}: direct-route panic", info.id));
20155                    continue;
20156                }
20157            };
20158
20159            if dispatch_once.rows != direct_once.rows || dispatch_once.cols != direct_once.cols {
20160                failures.push(format!(
20161                    "{}: shape mismatch dispatch=({},{}) direct=({},{})",
20162                    info.id,
20163                    dispatch_once.rows,
20164                    dispatch_once.cols,
20165                    direct_once.rows,
20166                    direct_once.cols
20167                ));
20168                continue;
20169            }
20170
20171            let mut dispatch_samples = Vec::with_capacity(REPS);
20172            let mut direct_samples = Vec::with_capacity(REPS);
20173            let mut panicked = false;
20174            for _ in 0..REPS {
20175                let t0 = Instant::now();
20176                let dispatch_iter = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20177                    compute_cpu_batch(req)
20178                }));
20179                if !matches!(dispatch_iter, Ok(Ok(_))) {
20180                    failures.push(format!("{}: dispatch panic/error during sample", info.id));
20181                    panicked = true;
20182                    break;
20183                }
20184                dispatch_samples.push(t0.elapsed().as_nanos());
20185
20186                let t1 = Instant::now();
20187                let direct_iter = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20188                    dispatch_cpu_batch_by_indicator(req, info.id, output_id)
20189                }));
20190                if !matches!(direct_iter, Ok(Ok(_))) {
20191                    failures.push(format!(
20192                        "{}: direct-route panic/error during sample",
20193                        info.id
20194                    ));
20195                    panicked = true;
20196                    break;
20197                }
20198                direct_samples.push(t1.elapsed().as_nanos());
20199            }
20200            if panicked {
20201                continue;
20202            }
20203
20204            let dispatch_median = median_ns(dispatch_samples) as f64 / 1_000_000.0;
20205            let direct_median = median_ns(direct_samples) as f64 / 1_000_000.0;
20206            let delta_pct = if direct_median > 0.0 {
20207                ((dispatch_median - direct_median) / direct_median) * 100.0
20208            } else {
20209                0.0
20210            };
20211            rows.push((
20212                info.id.to_string(),
20213                direct_median,
20214                dispatch_median,
20215                delta_pct,
20216            ));
20217        }
20218
20219        rows.sort_by(|a, b| b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal));
20220
20221        println!("id,direct_ms,dispatch_ms,delta_pct");
20222        for (id, direct_ms, dispatch_ms, delta_pct) in &rows {
20223            println!("{id},{direct_ms:.6},{dispatch_ms:.6},{delta_pct:.2}");
20224        }
20225        println!("total_indicators={}", rows.len());
20226
20227        assert!(
20228            failures.is_empty(),
20229            "perf sweep failures: {}",
20230            failures.join(" | ")
20231        );
20232        assert!(!rows.is_empty(), "no indicators were swept");
20233    }
20234
20235    #[test]
20236    fn multi_output_requires_output_id() {
20237        let data = sample_series();
20238        let combos: [IndicatorParamSet<'_>; 0] = [];
20239        let req = IndicatorBatchRequest {
20240            indicator_id: "macd",
20241            output_id: None,
20242            data: IndicatorDataRef::Slice { values: &data },
20243            combos: &combos,
20244            kernel: Kernel::Auto,
20245        };
20246        let err = compute_cpu_batch(req).unwrap_err();
20247        assert!(matches!(err, IndicatorDispatchError::InvalidParam { .. }));
20248    }
20249
20250    #[test]
20251    fn multi_output_unknown_output_is_rejected_globally() {
20252        let (open, high, low, close) = sample_ohlc();
20253        let volume: Vec<f64> = (0..close.len())
20254            .map(|i| 1000.0 + (i as f64 * 0.5))
20255            .collect();
20256        let timestamp: Vec<i64> = (0..close.len()).map(|i| i as i64).collect();
20257        let candles = crate::utilities::data_loader::Candles::new(
20258            timestamp,
20259            open.clone(),
20260            high.clone(),
20261            low.clone(),
20262            close.clone(),
20263            volume.clone(),
20264        );
20265
20266        for info in list_indicators()
20267            .iter()
20268            .filter(|i| i.capabilities.supports_cpu_batch && i.outputs.len() > 1)
20269        {
20270            let Some(params_vec) = build_default_params_for_indicator(info) else {
20271                continue;
20272            };
20273            let combos = [IndicatorParamSet {
20274                params: params_vec.as_slice(),
20275            }];
20276            let data = match info.input_kind {
20277                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
20278                    values: close.as_slice(),
20279                },
20280                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
20281                    candles: &candles,
20282                    source: None,
20283                },
20284                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
20285                    open: open.as_slice(),
20286                    high: high.as_slice(),
20287                    low: low.as_slice(),
20288                    close: close.as_slice(),
20289                },
20290                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
20291                    open: open.as_slice(),
20292                    high: high.as_slice(),
20293                    low: low.as_slice(),
20294                    close: close.as_slice(),
20295                    volume: volume.as_slice(),
20296                },
20297                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
20298                    high: high.as_slice(),
20299                    low: low.as_slice(),
20300                },
20301                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
20302                    close: close.as_slice(),
20303                    volume: volume.as_slice(),
20304                },
20305            };
20306            let req = IndicatorBatchRequest {
20307                indicator_id: info.id,
20308                output_id: Some("__unknown_output__"),
20309                data,
20310                combos: &combos,
20311                kernel: Kernel::Auto,
20312            };
20313            let err = compute_cpu_batch(req).unwrap_err();
20314            assert!(
20315                matches!(err, IndicatorDispatchError::UnknownOutput { .. }),
20316                "indicator {} returned unexpected error for unknown output: {:?}",
20317                info.id,
20318                err
20319            );
20320        }
20321    }
20322
20323    #[test]
20324    fn strict_mode_rejects_mismatched_input_kind_globally() {
20325        let data = sample_series();
20326        let candles = sample_candles();
20327
20328        for info in list_indicators()
20329            .iter()
20330            .filter(|i| i.capabilities.supports_cpu_batch)
20331        {
20332            let Some(output) = info.outputs.first() else {
20333                continue;
20334            };
20335            let Some(params_vec) = build_default_params_for_indicator(info) else {
20336                continue;
20337            };
20338            let combos = [IndicatorParamSet {
20339                params: params_vec.as_slice(),
20340            }];
20341            let expected = strict_expected_input_kind(info.id, info.input_kind);
20342            let mismatched = match expected {
20343                IndicatorInputKind::Slice => IndicatorDataRef::Candles {
20344                    candles: &candles,
20345                    source: None,
20346                },
20347                IndicatorInputKind::Candles => IndicatorDataRef::Slice { values: &data },
20348                IndicatorInputKind::Ohlc
20349                | IndicatorInputKind::Ohlcv
20350                | IndicatorInputKind::HighLow
20351                | IndicatorInputKind::CloseVolume => IndicatorDataRef::Slice { values: &data },
20352            };
20353            let req = IndicatorBatchRequest {
20354                indicator_id: info.id,
20355                output_id: Some(output.id),
20356                data: mismatched,
20357                combos: &combos,
20358                kernel: Kernel::Auto,
20359            };
20360            let err = compute_cpu_batch_strict(req).unwrap_err();
20361            assert!(
20362                matches!(err, IndicatorDispatchError::MissingRequiredInput { .. }),
20363                "indicator {} did not reject strict mismatched input: {:?}",
20364                info.id,
20365                err
20366            );
20367        }
20368    }
20369
20370    #[test]
20371    fn full_cpu_dispatch_parity_vs_direct_route_for_all_outputs() {
20372        const LEN: usize = 4096;
20373        let open: Vec<f64> = (0..LEN).map(|i| 100.0 + (i as f64 * 0.01)).collect();
20374        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
20375        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
20376        let close: Vec<f64> = open.iter().map(|v| v + 0.25).collect();
20377        let volume: Vec<f64> = (0..LEN).map(|i| 1000.0 + (i as f64 * 0.5)).collect();
20378        let timestamp: Vec<i64> = (0..LEN).map(|i| i as i64).collect();
20379        let candles = crate::utilities::data_loader::Candles::new(
20380            timestamp,
20381            open.clone(),
20382            high.clone(),
20383            low.clone(),
20384            close.clone(),
20385            volume.clone(),
20386        );
20387
20388        for info in list_indicators()
20389            .iter()
20390            .filter(|i| i.capabilities.supports_cpu_batch)
20391        {
20392            let Some(params_vec) = build_default_params_for_indicator(info) else {
20393                continue;
20394            };
20395            let combos = [IndicatorParamSet {
20396                params: params_vec.as_slice(),
20397            }];
20398            let data = match info.input_kind {
20399                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
20400                    values: close.as_slice(),
20401                },
20402                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
20403                    candles: &candles,
20404                    source: None,
20405                },
20406                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
20407                    open: open.as_slice(),
20408                    high: high.as_slice(),
20409                    low: low.as_slice(),
20410                    close: close.as_slice(),
20411                },
20412                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
20413                    open: open.as_slice(),
20414                    high: high.as_slice(),
20415                    low: low.as_slice(),
20416                    close: close.as_slice(),
20417                    volume: volume.as_slice(),
20418                },
20419                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
20420                    high: high.as_slice(),
20421                    low: low.as_slice(),
20422                },
20423                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
20424                    close: close.as_slice(),
20425                    volume: volume.as_slice(),
20426                },
20427            };
20428
20429            for output in info.outputs.iter() {
20430                let req = IndicatorBatchRequest {
20431                    indicator_id: info.id,
20432                    output_id: Some(output.id),
20433                    data,
20434                    combos: &combos,
20435                    kernel: Kernel::Auto,
20436                };
20437                let generic = compute_cpu_batch(req).unwrap_or_else(|e| {
20438                    panic!(
20439                        "generic dispatch failed for {}:{}: {}",
20440                        info.id, output.id, e
20441                    )
20442                });
20443                let direct =
20444                    dispatch_cpu_batch_by_indicator(req, info.id, output.id).unwrap_or_else(|e| {
20445                        panic!("direct route failed for {}:{}: {}", info.id, output.id, e)
20446                    });
20447
20448                assert_eq!(
20449                    generic.rows, direct.rows,
20450                    "rows mismatch for {}:{}",
20451                    info.id, output.id
20452                );
20453                assert_eq!(
20454                    generic.cols, direct.cols,
20455                    "cols mismatch for {}:{}",
20456                    info.id, output.id
20457                );
20458                assert_eq!(
20459                    generic.output_id, direct.output_id,
20460                    "output id mismatch for {}:{}",
20461                    info.id, output.id
20462                );
20463
20464                match (
20465                    generic.values_f64.as_ref(),
20466                    direct.values_f64.as_ref(),
20467                    generic.values_i32.as_ref(),
20468                    direct.values_i32.as_ref(),
20469                    generic.values_bool.as_ref(),
20470                    direct.values_bool.as_ref(),
20471                ) {
20472                    (Some(g), Some(d), None, None, None, None) => assert_series_eq(g, d, 1e-9),
20473                    (None, None, Some(g), Some(d), None, None) => assert_eq!(g, d),
20474                    (None, None, None, None, Some(g), Some(d)) => assert_eq!(g, d),
20475                    _ => panic!("value type mismatch for {}:{}", info.id, output.id),
20476                }
20477            }
20478        }
20479    }
20480
20481    #[test]
20482    fn compute_cpu_batch_bull_power_vs_bear_power_matches_direct() {
20483        let open: Vec<f64> = (0..256)
20484            .map(|i| 100.0 + (i as f64 * 0.03).sin() + i as f64 * 0.02)
20485            .collect();
20486        let close: Vec<f64> = open
20487            .iter()
20488            .enumerate()
20489            .map(|(i, &o)| o + (i as f64 * 0.025).cos() * 0.8)
20490            .collect();
20491        let high: Vec<f64> = open
20492            .iter()
20493            .zip(close.iter())
20494            .enumerate()
20495            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.013).sin().abs() * 0.2)
20496            .collect();
20497        let low: Vec<f64> = open
20498            .iter()
20499            .zip(close.iter())
20500            .enumerate()
20501            .map(|(i, (&o, &c))| o.min(c) - 0.4 - (i as f64 * 0.017).cos().abs() * 0.15)
20502            .collect();
20503        let params = [ParamKV {
20504            key: "period",
20505            value: ParamValue::Int(5),
20506        }];
20507        let combos = [IndicatorParamSet { params: &params }];
20508        let candles = crate::utilities::data_loader::Candles::new(
20509            vec![0; close.len()],
20510            open.clone(),
20511            high.clone(),
20512            low.clone(),
20513            close.clone(),
20514            vec![0.0; close.len()],
20515        );
20516
20517        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20518            indicator_id: "bull_power_vs_bear_power",
20519            output_id: Some("value"),
20520            data: IndicatorDataRef::Candles {
20521                candles: &candles,
20522                source: None,
20523            },
20524            combos: &combos,
20525            kernel: Kernel::Auto,
20526        })
20527        .unwrap();
20528
20529        let direct = bull_power_vs_bear_power_with_kernel(
20530            &BullPowerVsBearPowerInput::from_slices(
20531                &open,
20532                &high,
20533                &low,
20534                &close,
20535                BullPowerVsBearPowerParams { period: Some(5) },
20536            ),
20537            Kernel::Auto,
20538        )
20539        .unwrap();
20540
20541        let values = dispatched.values_f64.as_ref().unwrap();
20542        assert_eq!(values.len(), close.len());
20543        assert_series_eq(values, &direct.values, 1e-9);
20544    }
20545
20546    #[test]
20547    fn compute_cpu_batch_advance_decline_line_matches_direct() {
20548        let close: Vec<f64> = (0..256)
20549            .map(|i| ((i as f64) * 0.05).sin() * 100.0 + ((i as f64) * 0.02).cos() * 25.0)
20550            .collect();
20551        let combos = [IndicatorParamSet { params: &[] }];
20552
20553        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20554            indicator_id: "advance_decline_line",
20555            output_id: Some("value"),
20556            data: IndicatorDataRef::Slice { values: &close },
20557            combos: &combos,
20558            kernel: Kernel::Auto,
20559        })
20560        .unwrap();
20561
20562        let direct = advance_decline_line_with_kernel(
20563            &AdvanceDeclineLineInput::from_slice(&close, AdvanceDeclineLineParams),
20564            Kernel::Auto,
20565        )
20566        .unwrap();
20567
20568        let values = dispatched.values_f64.as_ref().unwrap();
20569        assert_eq!(values.len(), close.len());
20570        assert_series_eq(values, &direct.values, 1e-9);
20571    }
20572
20573    #[test]
20574    fn compute_cpu_batch_decisionpoint_breadth_swenlin_trading_oscillator_matches_direct() {
20575        let advancing: Vec<f64> = (0..256)
20576            .map(|i| 1500.0 + i as f64 * 0.8 + (i as f64 * 0.07).sin() * 120.0 + 40.0)
20577            .collect();
20578        let declining: Vec<f64> = (0..256)
20579            .map(|i| 1300.0 + i as f64 * 0.5 + (i as f64 * 0.05).cos() * 95.0 + 30.0)
20580            .collect();
20581        let combos = [IndicatorParamSet { params: &[] }];
20582
20583        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20584            indicator_id: "decisionpoint_breadth_swenlin_trading_oscillator",
20585            output_id: Some("value"),
20586            data: IndicatorDataRef::HighLow {
20587                high: &advancing,
20588                low: &declining,
20589            },
20590            combos: &combos,
20591            kernel: Kernel::Auto,
20592        })
20593        .unwrap();
20594
20595        let direct = decisionpoint_breadth_swenlin_trading_oscillator_with_kernel(
20596            &DecisionPointBreadthSwenlinTradingOscillatorInput::from_slices(
20597                &advancing,
20598                &declining,
20599                DecisionPointBreadthSwenlinTradingOscillatorParams,
20600            ),
20601            Kernel::Auto,
20602        )
20603        .unwrap();
20604
20605        let values = dispatched.values_f64.as_ref().unwrap();
20606        assert_eq!(values.len(), advancing.len());
20607        assert_series_eq(values, &direct.values, 1e-9);
20608    }
20609
20610    #[test]
20611    fn compute_cpu_batch_velocity_acceleration_indicator_matches_direct() {
20612        let open: Vec<f64> = (0..256)
20613            .map(|i| 100.0 + i as f64 * 0.04 + (i as f64 * 0.09).sin())
20614            .collect();
20615        let close: Vec<f64> = open
20616            .iter()
20617            .enumerate()
20618            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.9)
20619            .collect();
20620        let high: Vec<f64> = open
20621            .iter()
20622            .zip(close.iter())
20623            .enumerate()
20624            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.03).sin().abs() * 0.2)
20625            .collect();
20626        let low: Vec<f64> = open
20627            .iter()
20628            .zip(close.iter())
20629            .enumerate()
20630            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.05).cos().abs() * 0.2)
20631            .collect();
20632        let candles = crate::utilities::data_loader::Candles::new(
20633            (0..256_i64).collect(),
20634            open,
20635            high,
20636            low,
20637            close,
20638            vec![1_000.0; 256],
20639        );
20640        let params = [
20641            ParamKV {
20642                key: "length",
20643                value: ParamValue::Int(21),
20644            },
20645            ParamKV {
20646                key: "smooth_length",
20647                value: ParamValue::Int(5),
20648            },
20649            ParamKV {
20650                key: "source",
20651                value: ParamValue::EnumString("hlcc4"),
20652            },
20653        ];
20654        let combos = [IndicatorParamSet { params: &params }];
20655
20656        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20657            indicator_id: "velocity_acceleration_indicator",
20658            output_id: Some("value"),
20659            data: IndicatorDataRef::Candles {
20660                candles: &candles,
20661                source: Some("hlcc4"),
20662            },
20663            combos: &combos,
20664            kernel: Kernel::Auto,
20665        })
20666        .unwrap();
20667
20668        let direct = velocity_acceleration_indicator_with_kernel(
20669            &VelocityAccelerationIndicatorInput::from_candles(
20670                &candles,
20671                "hlcc4",
20672                VelocityAccelerationIndicatorParams {
20673                    length: Some(21),
20674                    smooth_length: Some(5),
20675                },
20676            ),
20677            Kernel::Auto,
20678        )
20679        .unwrap();
20680
20681        let values = dispatched.values_f64.as_ref().unwrap();
20682        assert_eq!(values.len(), candles.close.len());
20683        assert_series_eq(values, &direct.values, 1e-9);
20684    }
20685
20686    #[test]
20687    fn compute_cpu_batch_normalized_resonator_matches_direct() {
20688        let open: Vec<f64> = (0..256)
20689            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.07).sin())
20690            .collect();
20691        let close: Vec<f64> = open
20692            .iter()
20693            .enumerate()
20694            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.8)
20695            .collect();
20696        let high: Vec<f64> = open
20697            .iter()
20698            .zip(close.iter())
20699            .enumerate()
20700            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.05).sin().abs() * 0.2)
20701            .collect();
20702        let low: Vec<f64> = open
20703            .iter()
20704            .zip(close.iter())
20705            .enumerate()
20706            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.03).cos().abs() * 0.2)
20707            .collect();
20708        let candles = crate::utilities::data_loader::Candles::new(
20709            (0..256_i64).collect(),
20710            open,
20711            high,
20712            low,
20713            close,
20714            vec![1_000.0; 256],
20715        );
20716        let params = [
20717            ParamKV {
20718                key: "period",
20719                value: ParamValue::Int(48),
20720            },
20721            ParamKV {
20722                key: "delta",
20723                value: ParamValue::Float(0.4),
20724            },
20725            ParamKV {
20726                key: "lookback_mult",
20727                value: ParamValue::Float(1.2),
20728            },
20729            ParamKV {
20730                key: "signal_length",
20731                value: ParamValue::Int(7),
20732            },
20733            ParamKV {
20734                key: "source",
20735                value: ParamValue::EnumString("hl2"),
20736            },
20737        ];
20738        let combos = [IndicatorParamSet { params: &params }];
20739
20740        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20741            indicator_id: "normalized_resonator",
20742            output_id: Some("oscillator"),
20743            data: IndicatorDataRef::Candles {
20744                candles: &candles,
20745                source: Some("hl2"),
20746            },
20747            combos: &combos,
20748            kernel: Kernel::Auto,
20749        })
20750        .unwrap();
20751
20752        let direct = normalized_resonator_with_kernel(
20753            &NormalizedResonatorInput::from_candles(
20754                &candles,
20755                "hl2",
20756                NormalizedResonatorParams {
20757                    period: Some(48),
20758                    delta: Some(0.4),
20759                    lookback_mult: Some(1.2),
20760                    signal_length: Some(7),
20761                },
20762            ),
20763            Kernel::Auto,
20764        )
20765        .unwrap();
20766
20767        let values = dispatched.values_f64.as_ref().unwrap();
20768        assert_eq!(values.len(), candles.close.len());
20769        assert_series_eq(values, &direct.oscillator, 1e-9);
20770    }
20771
20772    #[test]
20773    fn compute_cpu_batch_monotonicity_index_matches_direct() {
20774        let open: Vec<f64> = (0..256)
20775            .map(|i| 100.0 + i as f64 * 0.04 + (i as f64 * 0.08).sin())
20776            .collect();
20777        let close: Vec<f64> = open
20778            .iter()
20779            .enumerate()
20780            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.9)
20781            .collect();
20782        let high: Vec<f64> = open
20783            .iter()
20784            .zip(close.iter())
20785            .enumerate()
20786            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.05).sin().abs() * 0.2)
20787            .collect();
20788        let low: Vec<f64> = open
20789            .iter()
20790            .zip(close.iter())
20791            .enumerate()
20792            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.03).cos().abs() * 0.2)
20793            .collect();
20794        let candles = crate::utilities::data_loader::Candles::new(
20795            (0..256_i64).collect(),
20796            open,
20797            high,
20798            low,
20799            close,
20800            vec![1_000.0; 256],
20801        );
20802        let params = [
20803            ParamKV {
20804                key: "length",
20805                value: ParamValue::Int(20),
20806            },
20807            ParamKV {
20808                key: "mode",
20809                value: ParamValue::EnumString("efficiency"),
20810            },
20811            ParamKV {
20812                key: "index_smooth",
20813                value: ParamValue::Int(5),
20814            },
20815            ParamKV {
20816                key: "source",
20817                value: ParamValue::EnumString("close"),
20818            },
20819        ];
20820        let combos = [IndicatorParamSet { params: &params }];
20821
20822        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20823            indicator_id: "monotonicity_index",
20824            output_id: Some("index"),
20825            data: IndicatorDataRef::Candles {
20826                candles: &candles,
20827                source: Some("close"),
20828            },
20829            combos: &combos,
20830            kernel: Kernel::Auto,
20831        })
20832        .unwrap();
20833
20834        let direct = monotonicity_index_with_kernel(
20835            &MonotonicityIndexInput::from_candles(
20836                &candles,
20837                "close",
20838                MonotonicityIndexParams {
20839                    length: Some(20),
20840                    mode: Some(MonotonicityIndexMode::Efficiency),
20841                    index_smooth: Some(5),
20842                },
20843            ),
20844            Kernel::Auto,
20845        )
20846        .unwrap();
20847
20848        let values = dispatched.values_f64.as_ref().unwrap();
20849        assert_eq!(values.len(), candles.close.len());
20850        assert_series_eq(values, &direct.index, 1e-9);
20851    }
20852
20853    #[test]
20854    fn compute_cpu_batch_half_causal_estimator_matches_direct() {
20855        let len = 240usize;
20856        let slots_per_day = 60usize;
20857        let close: Vec<f64> = (0..len)
20858            .map(|i| {
20859                let slot = (i % slots_per_day) as f64;
20860                let day = (i / slots_per_day) as f64;
20861                1000.0
20862                    + day * 4.0
20863                    + (slot * 0.13).sin() * 25.0
20864                    + (slot * 0.04).cos() * 9.0
20865                    + slot * 0.2
20866            })
20867            .collect();
20868        let params = [
20869            ParamKV {
20870                key: "slots_per_day",
20871                value: ParamValue::Int(slots_per_day as i64),
20872            },
20873            ParamKV {
20874                key: "data_period",
20875                value: ParamValue::Int(5),
20876            },
20877            ParamKV {
20878                key: "filter_length",
20879                value: ParamValue::Int(20),
20880            },
20881            ParamKV {
20882                key: "kernel_width",
20883                value: ParamValue::Float(20.0),
20884            },
20885            ParamKV {
20886                key: "kernel_type",
20887                value: ParamValue::EnumString("epanechnikov"),
20888            },
20889            ParamKV {
20890                key: "confidence_adjust",
20891                value: ParamValue::EnumString("symmetric"),
20892            },
20893            ParamKV {
20894                key: "maximum_confidence_adjust",
20895                value: ParamValue::Float(100.0),
20896            },
20897            ParamKV {
20898                key: "enable_expected_value",
20899                value: ParamValue::Bool(true),
20900            },
20901            ParamKV {
20902                key: "extra_smoothing",
20903                value: ParamValue::Int(0),
20904            },
20905        ];
20906        let combos = [IndicatorParamSet { params: &params }];
20907
20908        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20909            indicator_id: "half_causal_estimator",
20910            output_id: Some("estimate"),
20911            data: IndicatorDataRef::Slice { values: &close },
20912            combos: &combos,
20913            kernel: Kernel::Auto,
20914        })
20915        .unwrap();
20916
20917        let direct = crate::indicators::half_causal_estimator::half_causal_estimator_with_kernel(
20918            &crate::indicators::half_causal_estimator::HalfCausalEstimatorInput::from_slice(
20919                &close,
20920                crate::indicators::half_causal_estimator::HalfCausalEstimatorParams {
20921                    slots_per_day: Some(slots_per_day),
20922                    data_period: Some(5),
20923                    filter_length: Some(20),
20924                    kernel_width: Some(20.0),
20925                    kernel_type: Some(
20926                        crate::indicators::half_causal_estimator::HalfCausalEstimatorKernelType::Epanechnikov,
20927                    ),
20928                    confidence_adjust: Some(
20929                        crate::indicators::half_causal_estimator::HalfCausalEstimatorConfidenceAdjust::Symmetric,
20930                    ),
20931                    maximum_confidence_adjust: Some(100.0),
20932                    enable_expected_value: Some(true),
20933                    extra_smoothing: Some(0),
20934                },
20935            ),
20936            Kernel::Auto,
20937        )
20938        .unwrap();
20939
20940        let values = dispatched.values_f64.as_ref().unwrap();
20941        assert_eq!(values.len(), close.len());
20942        assert_series_eq(values, &direct.estimate, 1e-9);
20943    }
20944
20945    #[test]
20946    fn compute_cpu_batch_didi_index_matches_direct() {
20947        let close: Vec<f64> = (0..256)
20948            .map(|i| 100.0 + ((i as f64) * 0.09).sin() * 7.0 + (i as f64) * 0.03)
20949            .collect();
20950        let params = [
20951            ParamKV {
20952                key: "short_length",
20953                value: ParamValue::Int(3),
20954            },
20955            ParamKV {
20956                key: "medium_length",
20957                value: ParamValue::Int(8),
20958            },
20959            ParamKV {
20960                key: "long_length",
20961                value: ParamValue::Int(20),
20962            },
20963        ];
20964        let combos = [IndicatorParamSet { params: &params }];
20965
20966        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20967            indicator_id: "didi_index",
20968            output_id: Some("short"),
20969            data: IndicatorDataRef::Slice { values: &close },
20970            combos: &combos,
20971            kernel: Kernel::Auto,
20972        })
20973        .unwrap();
20974
20975        let direct = didi_index_with_kernel(
20976            &DidiIndexInput::from_slice(
20977                &close,
20978                DidiIndexParams {
20979                    short_length: Some(3),
20980                    medium_length: Some(8),
20981                    long_length: Some(20),
20982                },
20983            ),
20984            Kernel::Auto,
20985        )
20986        .unwrap();
20987
20988        let values = dispatched.values_f64.as_ref().unwrap();
20989        assert_eq!(values.len(), close.len());
20990        assert_series_eq(values, &direct.short, 1e-9);
20991    }
20992
20993    #[test]
20994    fn compute_cpu_batch_ehlers_autocorrelation_periodogram_matches_direct() {
20995        let close: Vec<f64> = (0..256)
20996            .map(|i| {
20997                let phase = 2.0 * std::f64::consts::PI * i as f64 / 20.0;
20998                phase.sin() + 0.15 * (phase * 0.5).cos()
20999            })
21000            .collect();
21001        let params = [
21002            ParamKV {
21003                key: "min_period",
21004                value: ParamValue::Int(8),
21005            },
21006            ParamKV {
21007                key: "max_period",
21008                value: ParamValue::Int(48),
21009            },
21010            ParamKV {
21011                key: "avg_length",
21012                value: ParamValue::Int(3),
21013            },
21014            ParamKV {
21015                key: "enhance",
21016                value: ParamValue::Bool(true),
21017            },
21018        ];
21019        let combos = [IndicatorParamSet { params: &params }];
21020
21021        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21022            indicator_id: "ehlers_autocorrelation_periodogram",
21023            output_id: Some("dominant_cycle"),
21024            data: IndicatorDataRef::Slice { values: &close },
21025            combos: &combos,
21026            kernel: Kernel::Auto,
21027        })
21028        .unwrap();
21029
21030        let direct = ehlers_autocorrelation_periodogram_with_kernel(
21031            &EhlersAutocorrelationPeriodogramInput::from_slice(
21032                &close,
21033                EhlersAutocorrelationPeriodogramParams {
21034                    min_period: Some(8),
21035                    max_period: Some(48),
21036                    avg_length: Some(3),
21037                    enhance: Some(true),
21038                },
21039            ),
21040            Kernel::Auto,
21041        )
21042        .unwrap();
21043
21044        let values = dispatched.values_f64.as_ref().unwrap();
21045        assert_eq!(values.len(), close.len());
21046        assert_series_eq(values, &direct.dominant_cycle, 1e-9);
21047    }
21048
21049    #[test]
21050    fn compute_cpu_batch_ehlers_linear_extrapolation_predictor_matches_direct() {
21051        let close: Vec<f64> = (0..256)
21052            .map(|i| 100.0 + ((i as f64) * 0.09).sin() * 2.0 + (i as f64 * 0.03))
21053            .collect();
21054        let params = [
21055            ParamKV {
21056                key: "high_pass_length",
21057                value: ParamValue::Int(125),
21058            },
21059            ParamKV {
21060                key: "low_pass_length",
21061                value: ParamValue::Int(12),
21062            },
21063            ParamKV {
21064                key: "gain",
21065                value: ParamValue::Float(0.7),
21066            },
21067            ParamKV {
21068                key: "bars_forward",
21069                value: ParamValue::Int(5),
21070            },
21071            ParamKV {
21072                key: "signal_mode",
21073                value: ParamValue::EnumString("predict_filter_crosses"),
21074            },
21075        ];
21076        let combos = [IndicatorParamSet { params: &params }];
21077
21078        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21079            indicator_id: "ehlers_linear_extrapolation_predictor",
21080            output_id: Some("prediction"),
21081            data: IndicatorDataRef::Slice { values: &close },
21082            combos: &combos,
21083            kernel: Kernel::Auto,
21084        })
21085        .unwrap();
21086
21087        let direct = ehlers_linear_extrapolation_predictor_with_kernel(
21088            &EhlersLinearExtrapolationPredictorInput::from_slice(
21089                &close,
21090                EhlersLinearExtrapolationPredictorParams {
21091                    high_pass_length: Some(125),
21092                    low_pass_length: Some(12),
21093                    gain: Some(0.7),
21094                    bars_forward: Some(5),
21095                    signal_mode: Some("predict_filter_crosses".to_string()),
21096                },
21097            ),
21098            Kernel::Auto,
21099        )
21100        .unwrap();
21101
21102        let values = dispatched.values_f64.as_ref().unwrap();
21103        assert_eq!(values.len(), close.len());
21104        assert_series_eq(values, &direct.prediction, 1e-9);
21105    }
21106
21107    #[test]
21108    fn compute_cpu_batch_grover_llorens_cycle_oscillator_matches_direct() {
21109        let mut open = Vec::with_capacity(256);
21110        let mut high = Vec::with_capacity(256);
21111        let mut low = Vec::with_capacity(256);
21112        let mut close = Vec::with_capacity(256);
21113        let mut prev = 100.0;
21114        for i in 0..256 {
21115            let x = i as f64;
21116            let wave = (x * 0.11).sin() * 2.4 + (x * 0.037).cos() * 1.3;
21117            let o = prev + wave * 0.35;
21118            let c = o + (x * 0.19).sin() * 1.1 - (x * 0.07).cos() * 0.4;
21119            let h = o.max(c) + 0.6 + (x * 0.03).sin().abs() * 0.25;
21120            let l = o.min(c) - 0.6 - (x * 0.02).cos().abs() * 0.25;
21121            open.push(o);
21122            high.push(h);
21123            low.push(l);
21124            close.push(c);
21125            prev = c;
21126        }
21127
21128        let params = [
21129            ParamKV {
21130                key: "length",
21131                value: ParamValue::Int(60),
21132            },
21133            ParamKV {
21134                key: "mult",
21135                value: ParamValue::Float(8.0),
21136            },
21137            ParamKV {
21138                key: "source",
21139                value: ParamValue::EnumString("hlc3"),
21140            },
21141            ParamKV {
21142                key: "smooth",
21143                value: ParamValue::Bool(true),
21144            },
21145            ParamKV {
21146                key: "rsi_period",
21147                value: ParamValue::Int(14),
21148            },
21149        ];
21150        let combos = [IndicatorParamSet { params: &params }];
21151
21152        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21153            indicator_id: "grover_llorens_cycle_oscillator",
21154            output_id: Some("value"),
21155            data: IndicatorDataRef::Ohlc {
21156                open: &open,
21157                high: &high,
21158                low: &low,
21159                close: &close,
21160            },
21161            combos: &combos,
21162            kernel: Kernel::Auto,
21163        })
21164        .unwrap();
21165
21166        let direct = grover_llorens_cycle_oscillator_with_kernel(
21167            &GroverLlorensCycleOscillatorInput::from_slices(
21168                &open,
21169                &high,
21170                &low,
21171                &close,
21172                GroverLlorensCycleOscillatorParams {
21173                    length: Some(60),
21174                    mult: Some(8.0),
21175                    source: Some("hlc3".to_string()),
21176                    smooth: Some(true),
21177                    rsi_period: Some(14),
21178                },
21179            ),
21180            Kernel::Auto,
21181        )
21182        .unwrap();
21183
21184        let values = dispatched.values_f64.as_ref().unwrap();
21185        assert_eq!(values.len(), close.len());
21186        assert_series_eq(values, &direct.values, 1e-9);
21187    }
21188
21189    #[test]
21190    fn compute_cpu_batch_historical_volatility_matches_direct() {
21191        let close: Vec<f64> = (0..256)
21192            .map(|i| 100.0 + ((i as f64) * 0.02).sin() + (i as f64 * 0.1))
21193            .collect();
21194        let params = [
21195            ParamKV {
21196                key: "lookback",
21197                value: ParamValue::Int(20),
21198            },
21199            ParamKV {
21200                key: "annualization_days",
21201                value: ParamValue::Float(252.0),
21202            },
21203        ];
21204        let combos = [IndicatorParamSet { params: &params }];
21205
21206        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21207            indicator_id: "historical_volatility",
21208            output_id: Some("value"),
21209            data: IndicatorDataRef::Slice { values: &close },
21210            combos: &combos,
21211            kernel: Kernel::Auto,
21212        })
21213        .unwrap();
21214
21215        let direct = historical_volatility_with_kernel(
21216            &HistoricalVolatilityInput::from_slice(
21217                &close,
21218                HistoricalVolatilityParams {
21219                    lookback: Some(20),
21220                    annualization_days: Some(252.0),
21221                },
21222            ),
21223            Kernel::Auto,
21224        )
21225        .unwrap();
21226
21227        let values = dispatched.values_f64.as_ref().unwrap();
21228        assert_eq!(values.len(), close.len());
21229        assert_series_eq(values, &direct.values, 1e-9);
21230    }
21231
21232    #[test]
21233    fn compute_cpu_batch_stochastic_distance_matches_direct() {
21234        let close: Vec<f64> = (0..256)
21235            .map(|i| 100.0 + (i as f64 * 0.07).sin() * 1.3 + i as f64 * 0.03)
21236            .collect();
21237        let params = [
21238            ParamKV {
21239                key: "lookback_length",
21240                value: ParamValue::Int(50),
21241            },
21242            ParamKV {
21243                key: "length1",
21244                value: ParamValue::Int(8),
21245            },
21246            ParamKV {
21247                key: "length2",
21248                value: ParamValue::Int(4),
21249            },
21250            ParamKV {
21251                key: "ob_level",
21252                value: ParamValue::Int(40),
21253            },
21254            ParamKV {
21255                key: "os_level",
21256                value: ParamValue::Int(-40),
21257            },
21258        ];
21259        let combos = [IndicatorParamSet { params: &params }];
21260
21261        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21262            indicator_id: "stochastic_distance",
21263            output_id: Some("oscillator"),
21264            data: IndicatorDataRef::Slice { values: &close },
21265            combos: &combos,
21266            kernel: Kernel::Auto,
21267        })
21268        .unwrap();
21269
21270        let direct = stochastic_distance_with_kernel(
21271            &StochasticDistanceInput::from_slice(
21272                &close,
21273                StochasticDistanceParams {
21274                    lookback_length: Some(50),
21275                    length1: Some(8),
21276                    length2: Some(4),
21277                    ob_level: Some(40),
21278                    os_level: Some(-40),
21279                },
21280            ),
21281            Kernel::Auto,
21282        )
21283        .unwrap();
21284
21285        let values = dispatched.values_f64.as_ref().unwrap();
21286        assert_eq!(values.len(), close.len());
21287        assert_series_eq(values, &direct.oscillator, 1e-9);
21288    }
21289
21290    #[test]
21291    fn compute_cpu_batch_adaptive_bandpass_trigger_oscillator_matches_direct() {
21292        let close: Vec<f64> = (0..256)
21293            .map(|i| 100.0 + (i as f64 * 0.07).sin() * 1.3 + (i as f64 * 0.03).cos() * 0.6)
21294            .collect();
21295        let params = [
21296            ParamKV {
21297                key: "delta",
21298                value: ParamValue::Float(0.1),
21299            },
21300            ParamKV {
21301                key: "alpha",
21302                value: ParamValue::Float(0.07),
21303            },
21304        ];
21305        let combos = [IndicatorParamSet { params: &params }];
21306
21307        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21308            indicator_id: "adaptive_bandpass_trigger_oscillator",
21309            output_id: Some("in_phase"),
21310            data: IndicatorDataRef::Slice { values: &close },
21311            combos: &combos,
21312            kernel: Kernel::Auto,
21313        })
21314        .unwrap();
21315
21316        let direct = adaptive_bandpass_trigger_oscillator_with_kernel(
21317            &AdaptiveBandpassTriggerOscillatorInput::from_slice(
21318                &close,
21319                AdaptiveBandpassTriggerOscillatorParams {
21320                    delta: Some(0.1),
21321                    alpha: Some(0.07),
21322                },
21323            ),
21324            Kernel::Auto,
21325        )
21326        .unwrap();
21327
21328        let values = dispatched.values_f64.as_ref().unwrap();
21329        assert_eq!(values.len(), close.len());
21330        assert_series_eq(values, &direct.in_phase, 1e-9);
21331    }
21332
21333    #[test]
21334    fn compute_cpu_batch_squeeze_index_matches_direct() {
21335        let close: Vec<f64> = (0..256)
21336            .map(|i| 100.0 + ((i as f64) * 0.11).sin() * 1.2 + (i as f64 * 0.02))
21337            .collect();
21338        let params = [
21339            ParamKV {
21340                key: "conv",
21341                value: ParamValue::Float(50.0),
21342            },
21343            ParamKV {
21344                key: "length",
21345                value: ParamValue::Int(20),
21346            },
21347        ];
21348        let combos = [IndicatorParamSet { params: &params }];
21349
21350        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21351            indicator_id: "squeeze_index",
21352            output_id: Some("value"),
21353            data: IndicatorDataRef::Slice { values: &close },
21354            combos: &combos,
21355            kernel: Kernel::Auto,
21356        })
21357        .unwrap();
21358
21359        let direct = squeeze_index_with_kernel(
21360            &SqueezeIndexInput::from_slice(
21361                &close,
21362                SqueezeIndexParams {
21363                    conv: Some(50.0),
21364                    length: Some(20),
21365                },
21366            ),
21367            Kernel::Auto,
21368        )
21369        .unwrap();
21370
21371        let values = dispatched.values_f64.as_ref().unwrap();
21372        assert_eq!(values.len(), close.len());
21373        assert_series_eq(values, &direct.values, 1e-9);
21374    }
21375
21376    #[test]
21377    fn compute_cpu_batch_absolute_strength_index_oscillator_matches_direct() {
21378        let close: Vec<f64> = (0..256)
21379            .map(|i| 100.0 + ((i as f64) * 0.17).sin() * 1.8 + ((i % 7) as f64 - 3.0) * 0.04)
21380            .collect();
21381        let params = [
21382            ParamKV {
21383                key: "ema_length",
21384                value: ParamValue::Int(21),
21385            },
21386            ParamKV {
21387                key: "signal_length",
21388                value: ParamValue::Int(34),
21389            },
21390        ];
21391        let combos = [IndicatorParamSet { params: &params }];
21392
21393        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21394            indicator_id: "absolute_strength_index_oscillator",
21395            output_id: Some("oscillator"),
21396            data: IndicatorDataRef::Slice { values: &close },
21397            combos: &combos,
21398            kernel: Kernel::Auto,
21399        })
21400        .unwrap();
21401
21402        let direct = absolute_strength_index_oscillator_with_kernel(
21403            &AbsoluteStrengthIndexOscillatorInput::from_slice(
21404                &close,
21405                AbsoluteStrengthIndexOscillatorParams {
21406                    ema_length: Some(21),
21407                    signal_length: Some(34),
21408                },
21409            ),
21410            Kernel::Auto,
21411        )
21412        .unwrap();
21413
21414        let values = dispatched.values_f64.as_ref().unwrap();
21415        assert_eq!(values.len(), close.len());
21416        assert_series_eq(values, &direct.oscillator, 1e-9);
21417    }
21418
21419    #[test]
21420    fn compute_cpu_batch_premier_rsi_oscillator_matches_direct() {
21421        let close: Vec<f64> = (0..256)
21422            .map(|i| 100.0 + ((i as f64) * 0.13).sin() * 1.4 + ((i % 11) as f64 - 5.0) * 0.03)
21423            .collect();
21424        let params = [
21425            ParamKV {
21426                key: "rsi_length",
21427                value: ParamValue::Int(14),
21428            },
21429            ParamKV {
21430                key: "stoch_length",
21431                value: ParamValue::Int(8),
21432            },
21433            ParamKV {
21434                key: "smooth_length",
21435                value: ParamValue::Int(25),
21436            },
21437        ];
21438        let combos = [IndicatorParamSet { params: &params }];
21439
21440        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21441            indicator_id: "premier_rsi_oscillator",
21442            output_id: Some("value"),
21443            data: IndicatorDataRef::Slice { values: &close },
21444            combos: &combos,
21445            kernel: Kernel::Auto,
21446        })
21447        .unwrap();
21448
21449        let direct = premier_rsi_oscillator_with_kernel(
21450            &PremierRsiOscillatorInput::from_slice(
21451                &close,
21452                PremierRsiOscillatorParams {
21453                    rsi_length: Some(14),
21454                    stoch_length: Some(8),
21455                    smooth_length: Some(25),
21456                },
21457            ),
21458            Kernel::Auto,
21459        )
21460        .unwrap();
21461
21462        let values = dispatched.values_f64.as_ref().unwrap();
21463        assert_eq!(values.len(), close.len());
21464        assert_series_eq(values, &direct.values, 1e-9);
21465    }
21466
21467    #[test]
21468    fn compute_cpu_batch_multi_length_stochastic_average_matches_direct() {
21469        let open: Vec<f64> = (0..256)
21470            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.09).sin())
21471            .collect();
21472        let close: Vec<f64> = open
21473            .iter()
21474            .enumerate()
21475            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.8)
21476            .collect();
21477        let high: Vec<f64> = open
21478            .iter()
21479            .zip(close.iter())
21480            .enumerate()
21481            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.05).sin().abs() * 0.2)
21482            .collect();
21483        let low: Vec<f64> = open
21484            .iter()
21485            .zip(close.iter())
21486            .enumerate()
21487            .map(|(i, (&o, &c))| o.min(c) - 0.5 - (i as f64 * 0.07).cos().abs() * 0.2)
21488            .collect();
21489        let candles = crate::utilities::data_loader::Candles::new(
21490            (0..256_i64).collect(),
21491            open,
21492            high,
21493            low,
21494            close,
21495            vec![1_000.0; 256],
21496        );
21497        let params = [
21498            ParamKV {
21499                key: "length",
21500                value: ParamValue::Int(14),
21501            },
21502            ParamKV {
21503                key: "presmooth",
21504                value: ParamValue::Int(10),
21505            },
21506            ParamKV {
21507                key: "premethod",
21508                value: ParamValue::EnumString("sma"),
21509            },
21510            ParamKV {
21511                key: "postsmooth",
21512                value: ParamValue::Int(10),
21513            },
21514            ParamKV {
21515                key: "postmethod",
21516                value: ParamValue::EnumString("lsma"),
21517            },
21518            ParamKV {
21519                key: "source",
21520                value: ParamValue::EnumString("hlc3"),
21521            },
21522        ];
21523        let combos = [IndicatorParamSet { params: &params }];
21524
21525        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21526            indicator_id: "multi_length_stochastic_average",
21527            output_id: Some("value"),
21528            data: IndicatorDataRef::Candles {
21529                candles: &candles,
21530                source: Some("hlc3"),
21531            },
21532            combos: &combos,
21533            kernel: Kernel::Auto,
21534        })
21535        .unwrap();
21536
21537        let direct = multi_length_stochastic_average_with_kernel(
21538            &MultiLengthStochasticAverageInput::from_candles(
21539                &candles,
21540                "hlc3",
21541                MultiLengthStochasticAverageParams {
21542                    length: Some(14),
21543                    presmooth: Some(10),
21544                    premethod: Some("sma".to_string()),
21545                    postsmooth: Some(10),
21546                    postmethod: Some("lsma".to_string()),
21547                },
21548            ),
21549            Kernel::Auto,
21550        )
21551        .unwrap();
21552
21553        let values = dispatched.values_f64.as_ref().unwrap();
21554        assert_eq!(values.len(), candles.close.len());
21555        assert_series_eq(values, &direct.values, 1e-9);
21556    }
21557
21558    #[test]
21559    fn compute_cpu_batch_hull_butterfly_oscillator_matches_direct() {
21560        let open: Vec<f64> = (0..256)
21561            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.09).sin())
21562            .collect();
21563        let close: Vec<f64> = open
21564            .iter()
21565            .enumerate()
21566            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.8)
21567            .collect();
21568        let high: Vec<f64> = open
21569            .iter()
21570            .zip(close.iter())
21571            .enumerate()
21572            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.05).sin().abs() * 0.2)
21573            .collect();
21574        let low: Vec<f64> = open
21575            .iter()
21576            .zip(close.iter())
21577            .enumerate()
21578            .map(|(i, (&o, &c))| o.min(c) - 0.5 - (i as f64 * 0.07).cos().abs() * 0.2)
21579            .collect();
21580        let candles = crate::utilities::data_loader::Candles::new(
21581            (0..256_i64).collect(),
21582            open,
21583            high,
21584            low,
21585            close,
21586            vec![1_000.0; 256],
21587        );
21588        let params = [
21589            ParamKV {
21590                key: "length",
21591                value: ParamValue::Int(14),
21592            },
21593            ParamKV {
21594                key: "mult",
21595                value: ParamValue::Float(1.75),
21596            },
21597            ParamKV {
21598                key: "source",
21599                value: ParamValue::EnumString("hlc3"),
21600            },
21601        ];
21602        let combos = [IndicatorParamSet { params: &params }];
21603
21604        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21605            indicator_id: "hull_butterfly_oscillator",
21606            output_id: Some("oscillator"),
21607            data: IndicatorDataRef::Candles {
21608                candles: &candles,
21609                source: Some("hlc3"),
21610            },
21611            combos: &combos,
21612            kernel: Kernel::Auto,
21613        })
21614        .unwrap();
21615
21616        let direct = hull_butterfly_oscillator_with_kernel(
21617            &HullButterflyOscillatorInput::from_candles(
21618                &candles,
21619                "hlc3",
21620                HullButterflyOscillatorParams {
21621                    length: Some(14),
21622                    mult: Some(1.75),
21623                },
21624            ),
21625            Kernel::Auto,
21626        )
21627        .unwrap();
21628
21629        let values = dispatched.values_f64.as_ref().unwrap();
21630        assert_eq!(values.len(), candles.close.len());
21631        assert_series_eq(values, &direct.oscillator, 1e-9);
21632    }
21633
21634    #[test]
21635    fn compute_cpu_batch_fibonacci_trailing_stop_matches_direct() {
21636        let open: Vec<f64> = (0..256)
21637            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.09).sin())
21638            .collect();
21639        let close: Vec<f64> = open
21640            .iter()
21641            .enumerate()
21642            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.8)
21643            .collect();
21644        let high: Vec<f64> = open
21645            .iter()
21646            .zip(close.iter())
21647            .enumerate()
21648            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.05).sin().abs() * 0.2)
21649            .collect();
21650        let low: Vec<f64> = open
21651            .iter()
21652            .zip(close.iter())
21653            .enumerate()
21654            .map(|(i, (&o, &c))| o.min(c) - 0.5 - (i as f64 * 0.07).cos().abs() * 0.2)
21655            .collect();
21656
21657        let params = [
21658            ParamKV {
21659                key: "left_bars",
21660                value: ParamValue::Int(12),
21661            },
21662            ParamKV {
21663                key: "right_bars",
21664                value: ParamValue::Int(2),
21665            },
21666            ParamKV {
21667                key: "level",
21668                value: ParamValue::Float(-0.236),
21669            },
21670            ParamKV {
21671                key: "trigger",
21672                value: ParamValue::EnumString("wick"),
21673            },
21674        ];
21675        let combos = [IndicatorParamSet { params: &params }];
21676
21677        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21678            indicator_id: "fibonacci_trailing_stop",
21679            output_id: Some("trailing_stop"),
21680            data: IndicatorDataRef::Ohlc {
21681                open: &open,
21682                high: &high,
21683                low: &low,
21684                close: &close,
21685            },
21686            combos: &combos,
21687            kernel: Kernel::Auto,
21688        })
21689        .unwrap();
21690
21691        let direct = fibonacci_trailing_stop_with_kernel(
21692            &FibonacciTrailingStopInput::from_slices(
21693                &high,
21694                &low,
21695                &close,
21696                FibonacciTrailingStopParams {
21697                    left_bars: Some(12),
21698                    right_bars: Some(2),
21699                    level: Some(-0.236),
21700                    trigger: Some("wick".to_string()),
21701                },
21702            ),
21703            Kernel::Auto,
21704        )
21705        .unwrap();
21706
21707        let values = dispatched.values_f64.as_ref().unwrap();
21708        assert_eq!(values.len(), close.len());
21709        assert_series_eq(values, &direct.trailing_stop, 1e-9);
21710    }
21711
21712    #[test]
21713    fn compute_cpu_batch_volume_energy_reservoirs_matches_direct() {
21714        let open: Vec<f64> = (0..256)
21715            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.08).sin())
21716            .collect();
21717        let close: Vec<f64> = open
21718            .iter()
21719            .enumerate()
21720            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.9)
21721            .collect();
21722        let high: Vec<f64> = open
21723            .iter()
21724            .zip(close.iter())
21725            .enumerate()
21726            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.03).sin().abs() * 0.25)
21727            .collect();
21728        let low: Vec<f64> = open
21729            .iter()
21730            .zip(close.iter())
21731            .enumerate()
21732            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.05).cos().abs() * 0.2)
21733            .collect();
21734        let volume: Vec<f64> = (0..256)
21735            .map(|i| 1_000.0 + i as f64 * 4.0 + (i as f64 * 0.09).sin() * 180.0)
21736            .collect();
21737
21738        let params = [
21739            ParamKV {
21740                key: "length",
21741                value: ParamValue::Int(18),
21742            },
21743            ParamKV {
21744                key: "sensitivity",
21745                value: ParamValue::Float(1.7),
21746            },
21747        ];
21748        let combos = [IndicatorParamSet { params: &params }];
21749
21750        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21751            indicator_id: "volume_energy_reservoirs",
21752            output_id: Some("momentum"),
21753            data: IndicatorDataRef::Ohlcv {
21754                open: &open,
21755                high: &high,
21756                low: &low,
21757                close: &close,
21758                volume: &volume,
21759            },
21760            combos: &combos,
21761            kernel: Kernel::Auto,
21762        })
21763        .unwrap();
21764
21765        let direct = volume_energy_reservoirs_with_kernel(
21766            &VolumeEnergyReservoirsInput::from_slices(
21767                &high,
21768                &low,
21769                &close,
21770                &volume,
21771                VolumeEnergyReservoirsParams {
21772                    length: Some(18),
21773                    sensitivity: Some(1.7),
21774                },
21775            ),
21776            Kernel::Auto,
21777        )
21778        .unwrap();
21779
21780        let values = dispatched.values_f64.as_ref().unwrap();
21781        assert_eq!(values.len(), close.len());
21782        assert_series_eq(values, &direct.momentum, 1e-9);
21783    }
21784
21785    #[test]
21786    fn compute_cpu_batch_neighboring_trailing_stop_matches_direct() {
21787        let open: Vec<f64> = (0..256)
21788            .map(|i| 100.0 + i as f64 * 0.04 + (i as f64 * 0.07).sin())
21789            .collect();
21790        let close: Vec<f64> = open
21791            .iter()
21792            .enumerate()
21793            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.85)
21794            .collect();
21795        let high: Vec<f64> = open
21796            .iter()
21797            .zip(close.iter())
21798            .enumerate()
21799            .map(|(i, (&o, &c))| o.max(c) + 0.55 + (i as f64 * 0.03).sin().abs() * 0.2)
21800            .collect();
21801        let low: Vec<f64> = open
21802            .iter()
21803            .zip(close.iter())
21804            .enumerate()
21805            .map(|(i, (&o, &c))| o.min(c) - 0.55 - (i as f64 * 0.05).cos().abs() * 0.2)
21806            .collect();
21807
21808        let params = [
21809            ParamKV {
21810                key: "buffer_size",
21811                value: ParamValue::Int(180),
21812            },
21813            ParamKV {
21814                key: "k",
21815                value: ParamValue::Int(30),
21816            },
21817            ParamKV {
21818                key: "percentile",
21819                value: ParamValue::Float(87.5),
21820            },
21821            ParamKV {
21822                key: "smooth",
21823                value: ParamValue::Int(4),
21824            },
21825        ];
21826        let combos = [IndicatorParamSet { params: &params }];
21827
21828        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21829            indicator_id: "neighboring_trailing_stop",
21830            output_id: Some("trailing_stop"),
21831            data: IndicatorDataRef::Ohlc {
21832                open: &open,
21833                high: &high,
21834                low: &low,
21835                close: &close,
21836            },
21837            combos: &combos,
21838            kernel: Kernel::Auto,
21839        })
21840        .unwrap();
21841
21842        let direct = neighboring_trailing_stop_with_kernel(
21843            &NeighboringTrailingStopInput::from_slices(
21844                &high,
21845                &low,
21846                &close,
21847                NeighboringTrailingStopParams {
21848                    buffer_size: Some(180),
21849                    k: Some(30),
21850                    percentile: Some(87.5),
21851                    smooth: Some(4),
21852                },
21853            ),
21854            Kernel::Auto,
21855        )
21856        .unwrap();
21857
21858        let values = dispatched.values_f64.as_ref().unwrap();
21859        assert_eq!(values.len(), close.len());
21860        assert_series_eq(values, &direct.trailing_stop, 1e-9);
21861    }
21862
21863    #[test]
21864    fn compute_cpu_batch_macd_wave_signal_pro_matches_direct() {
21865        let open: Vec<f64> = (0..256)
21866            .map(|i| 100.0 + i as f64 * 0.08 + ((i as f64) * 0.05).sin() * 0.7)
21867            .collect();
21868        let close: Vec<f64> = open
21869            .iter()
21870            .enumerate()
21871            .map(|(i, o)| o + ((i as f64) * 0.09).cos() * 0.9)
21872            .collect();
21873        let high: Vec<f64> = open
21874            .iter()
21875            .zip(close.iter())
21876            .enumerate()
21877            .map(|(i, (&o, &c))| o.max(c) + 0.55 + (i as f64 * 0.03).sin().abs() * 0.2)
21878            .collect();
21879        let low: Vec<f64> = open
21880            .iter()
21881            .zip(close.iter())
21882            .enumerate()
21883            .map(|(i, (&o, &c))| o.min(c) - 0.55 - (i as f64 * 0.05).cos().abs() * 0.2)
21884            .collect();
21885        let combos = [IndicatorParamSet { params: &[] }];
21886
21887        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21888            indicator_id: "macd_wave_signal_pro",
21889            output_id: Some("line_convergence"),
21890            data: IndicatorDataRef::Ohlc {
21891                open: &open,
21892                high: &high,
21893                low: &low,
21894                close: &close,
21895            },
21896            combos: &combos,
21897            kernel: Kernel::Auto,
21898        })
21899        .unwrap();
21900
21901        let direct = macd_wave_signal_pro_with_kernel(
21902            &MacdWaveSignalProInput::from_slices(&open, &high, &low, &close, Default::default()),
21903            Kernel::Auto,
21904        )
21905        .unwrap();
21906
21907        let values = dispatched.values_f64.as_ref().unwrap();
21908        assert_eq!(values.len(), close.len());
21909        assert_series_eq(values, &direct.line_convergence, 1e-9);
21910    }
21911
21912    #[test]
21913    fn compute_cpu_batch_hema_trend_levels_matches_direct() {
21914        let open: Vec<f64> = (0..256)
21915            .map(|i| 100.0 + i as f64 * 0.05 + ((i as f64) * 0.09).sin() * 1.3)
21916            .collect();
21917        let close: Vec<f64> = open
21918            .iter()
21919            .enumerate()
21920            .map(|(i, o)| o + ((i as f64) * 0.07).cos() * 1.1)
21921            .collect();
21922        let high: Vec<f64> = open
21923            .iter()
21924            .zip(close.iter())
21925            .enumerate()
21926            .map(|(i, (&o, &c))| o.max(c) + 0.65 + (i as f64 * 0.03).sin().abs() * 0.25)
21927            .collect();
21928        let low: Vec<f64> = open
21929            .iter()
21930            .zip(close.iter())
21931            .enumerate()
21932            .map(|(i, (&o, &c))| o.min(c) - 0.65 - (i as f64 * 0.05).cos().abs() * 0.25)
21933            .collect();
21934        let params = [
21935            ParamKV {
21936                key: "fast_length",
21937                value: ParamValue::Int(20),
21938            },
21939            ParamKV {
21940                key: "slow_length",
21941                value: ParamValue::Int(40),
21942            },
21943        ];
21944        let combos = [IndicatorParamSet { params: &params }];
21945
21946        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21947            indicator_id: "hema_trend_levels",
21948            output_id: Some("bullish_test_level"),
21949            data: IndicatorDataRef::Ohlc {
21950                open: &open,
21951                high: &high,
21952                low: &low,
21953                close: &close,
21954            },
21955            combos: &combos,
21956            kernel: Kernel::Auto,
21957        })
21958        .unwrap();
21959
21960        let direct = hema_trend_levels_with_kernel(
21961            &HemaTrendLevelsInput::from_slices(
21962                &open,
21963                &high,
21964                &low,
21965                &close,
21966                HemaTrendLevelsParams {
21967                    fast_length: Some(20),
21968                    slow_length: Some(40),
21969                },
21970            ),
21971            Kernel::Auto,
21972        )
21973        .unwrap();
21974
21975        let values = dispatched.values_f64.as_ref().unwrap();
21976        assert_eq!(values.len(), close.len());
21977        assert_series_eq(values, &direct.bullish_test_level, 1e-9);
21978    }
21979
21980    #[test]
21981    fn compute_cpu_batch_fibonacci_entry_bands_matches_direct() {
21982        let open: Vec<f64> = (0..256)
21983            .map(|i| 100.0 + i as f64 * 0.05 + ((i as f64) * 0.09).sin() * 1.3)
21984            .collect();
21985        let close: Vec<f64> = open
21986            .iter()
21987            .enumerate()
21988            .map(|(i, o)| o + ((i as f64) * 0.07).cos() * 1.1)
21989            .collect();
21990        let high: Vec<f64> = open
21991            .iter()
21992            .zip(close.iter())
21993            .enumerate()
21994            .map(|(i, (&o, &c))| o.max(c) + 0.65 + (i as f64 * 0.03).sin().abs() * 0.25)
21995            .collect();
21996        let low: Vec<f64> = open
21997            .iter()
21998            .zip(close.iter())
21999            .enumerate()
22000            .map(|(i, (&o, &c))| o.min(c) - 0.65 - (i as f64 * 0.05).cos().abs() * 0.25)
22001            .collect();
22002        let params = [
22003            ParamKV {
22004                key: "source",
22005                value: ParamValue::EnumString("hlc3"),
22006            },
22007            ParamKV {
22008                key: "length",
22009                value: ParamValue::Int(20),
22010            },
22011            ParamKV {
22012                key: "atr_length",
22013                value: ParamValue::Int(11),
22014            },
22015            ParamKV {
22016                key: "use_atr",
22017                value: ParamValue::Bool(true),
22018            },
22019            ParamKV {
22020                key: "tp_aggressiveness",
22021                value: ParamValue::EnumString("medium"),
22022            },
22023        ];
22024        let combos = [IndicatorParamSet { params: &params }];
22025
22026        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22027            indicator_id: "fibonacci_entry_bands",
22028            output_id: Some("tp_long_band"),
22029            data: IndicatorDataRef::Ohlc {
22030                open: &open,
22031                high: &high,
22032                low: &low,
22033                close: &close,
22034            },
22035            combos: &combos,
22036            kernel: Kernel::Auto,
22037        })
22038        .unwrap();
22039
22040        let direct = fibonacci_entry_bands_with_kernel(
22041            &FibonacciEntryBandsInput::from_slices(
22042                &open,
22043                &high,
22044                &low,
22045                &close,
22046                FibonacciEntryBandsParams {
22047                    source: Some("hlc3".to_string()),
22048                    length: Some(20),
22049                    atr_length: Some(11),
22050                    use_atr: Some(true),
22051                    tp_aggressiveness: Some("medium".to_string()),
22052                },
22053            ),
22054            Kernel::Auto,
22055        )
22056        .unwrap();
22057
22058        let values = dispatched.values_f64.as_ref().unwrap();
22059        assert_eq!(values.len(), close.len());
22060        assert_series_eq(values, &direct.tp_long_band, 1e-9);
22061    }
22062
22063    #[test]
22064    fn compute_cpu_batch_vertical_horizontal_filter_matches_direct() {
22065        let close: Vec<f64> = (0..256)
22066            .map(|i| 100.0 + ((i as f64) * 0.02).sin() + (i as f64 * 0.1))
22067            .collect();
22068        let params = [ParamKV {
22069            key: "length",
22070            value: ParamValue::Int(28),
22071        }];
22072        let combos = [IndicatorParamSet { params: &params }];
22073
22074        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22075            indicator_id: "vertical_horizontal_filter",
22076            output_id: Some("value"),
22077            data: IndicatorDataRef::Slice { values: &close },
22078            combos: &combos,
22079            kernel: Kernel::Auto,
22080        })
22081        .unwrap();
22082
22083        let direct = vertical_horizontal_filter_with_kernel(
22084            &VerticalHorizontalFilterInput::from_slice(
22085                &close,
22086                VerticalHorizontalFilterParams { length: Some(28) },
22087            ),
22088            Kernel::Auto,
22089        )
22090        .unwrap();
22091
22092        let values = dispatched.values_f64.as_ref().unwrap();
22093        assert_eq!(values.len(), close.len());
22094        assert_series_eq(values, &direct.values, 1e-9);
22095    }
22096
22097    #[test]
22098    fn compute_cpu_batch_intraday_momentum_index_matches_direct() {
22099        let open: Vec<f64> = (0..256)
22100            .map(|i| 100.0 + i as f64 * 0.1 + ((i as f64) * 0.05).cos() * 0.2)
22101            .collect();
22102        let high: Vec<f64> = open.iter().map(|v| v + 0.9).collect();
22103        let low: Vec<f64> = open.iter().map(|v| v - 0.8).collect();
22104        let close: Vec<f64> = open
22105            .iter()
22106            .enumerate()
22107            .map(|(i, o)| o + ((i as f64) * 0.09).sin() * 0.6)
22108            .collect();
22109        let params = [
22110            ParamKV {
22111                key: "length",
22112                value: ParamValue::Int(14),
22113            },
22114            ParamKV {
22115                key: "length_ma",
22116                value: ParamValue::Int(6),
22117            },
22118            ParamKV {
22119                key: "mult",
22120                value: ParamValue::Float(2.0),
22121            },
22122            ParamKV {
22123                key: "length_bb",
22124                value: ParamValue::Int(20),
22125            },
22126            ParamKV {
22127                key: "apply_smoothing",
22128                value: ParamValue::Bool(true),
22129            },
22130            ParamKV {
22131                key: "low_band",
22132                value: ParamValue::Int(10),
22133            },
22134        ];
22135        let combos = [IndicatorParamSet { params: &params }];
22136
22137        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22138            indicator_id: "intraday_momentum_index",
22139            output_id: Some("imi"),
22140            data: IndicatorDataRef::Ohlc {
22141                open: &open,
22142                high: &high,
22143                low: &low,
22144                close: &close,
22145            },
22146            combos: &combos,
22147            kernel: Kernel::Auto,
22148        })
22149        .unwrap();
22150
22151        let direct = intraday_momentum_index_with_kernel(
22152            &IntradayMomentumIndexInput::from_slices(
22153                &open,
22154                &close,
22155                IntradayMomentumIndexParams {
22156                    length: Some(14),
22157                    length_ma: Some(6),
22158                    mult: Some(2.0),
22159                    length_bb: Some(20),
22160                    apply_smoothing: Some(true),
22161                    low_band: Some(10),
22162                },
22163            ),
22164            Kernel::Auto,
22165        )
22166        .unwrap();
22167
22168        let values = dispatched.values_f64.as_ref().unwrap();
22169        assert_eq!(values.len(), close.len());
22170        assert_series_eq(values, &direct.imi, 1e-9);
22171    }
22172
22173    #[test]
22174    fn compute_cpu_batch_atr_percentile_matches_direct() {
22175        let high: Vec<f64> = (0..256)
22176            .map(|i| 100.0 + i as f64 * 0.1 + ((i as f64) * 0.03).sin().abs())
22177            .collect();
22178        let low: Vec<f64> = high
22179            .iter()
22180            .enumerate()
22181            .map(|(i, h)| h - 0.75 - ((i as f64) * 0.02).cos().abs() * 0.2)
22182            .collect();
22183        let close: Vec<f64> = low
22184            .iter()
22185            .zip(high.iter())
22186            .enumerate()
22187            .map(|(i, (l, h))| l + (h - l) * (0.35 + 0.2 * ((i as f64) * 0.05).sin().abs()))
22188            .collect();
22189        let params = [
22190            ParamKV {
22191                key: "atr_length",
22192                value: ParamValue::Int(10),
22193            },
22194            ParamKV {
22195                key: "percentile_length",
22196                value: ParamValue::Int(20),
22197            },
22198        ];
22199        let combos = [IndicatorParamSet { params: &params }];
22200
22201        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22202            indicator_id: "atr_percentile",
22203            output_id: Some("value"),
22204            data: IndicatorDataRef::Ohlc {
22205                open: &close,
22206                high: &high,
22207                low: &low,
22208                close: &close,
22209            },
22210            combos: &combos,
22211            kernel: Kernel::Auto,
22212        })
22213        .unwrap();
22214
22215        let direct = atr_percentile_with_kernel(
22216            &AtrPercentileInput::from_slices(
22217                &high,
22218                &low,
22219                &close,
22220                AtrPercentileParams {
22221                    atr_length: Some(10),
22222                    percentile_length: Some(20),
22223                },
22224            ),
22225            Kernel::Auto,
22226        )
22227        .unwrap();
22228
22229        let values = dispatched.values_f64.as_ref().unwrap();
22230        assert_eq!(values.len(), close.len());
22231        assert_series_eq(values, &direct.values, 1e-9);
22232    }
22233
22234    #[test]
22235    fn compute_cpu_batch_demand_index_matches_direct() {
22236        let high: Vec<f64> = (0..256)
22237            .map(|i| 100.0 + i as f64 * 0.15 + ((i as f64) * 0.03).sin().abs())
22238            .collect();
22239        let low: Vec<f64> = high
22240            .iter()
22241            .enumerate()
22242            .map(|(i, h)| h - 0.9 - ((i as f64) * 0.04).cos().abs() * 0.3)
22243            .collect();
22244        let close: Vec<f64> = low
22245            .iter()
22246            .zip(high.iter())
22247            .enumerate()
22248            .map(|(i, (l, h))| l + (h - l) * (0.25 + 0.5 * ((i as f64) * 0.07).sin().abs()))
22249            .collect();
22250        let open: Vec<f64> = close
22251            .iter()
22252            .enumerate()
22253            .map(|(i, c)| c - 0.2 + ((i as f64) * 0.05).cos() * 0.1)
22254            .collect();
22255        let volume: Vec<f64> = (0..256)
22256            .map(|i| 1000.0 + (i as f64) * 3.0 + ((i as f64) * 0.11).sin().abs() * 40.0)
22257            .collect();
22258        let params = [
22259            ParamKV {
22260                key: "len_bs",
22261                value: ParamValue::Int(19),
22262            },
22263            ParamKV {
22264                key: "len_bs_ma",
22265                value: ParamValue::Int(19),
22266            },
22267            ParamKV {
22268                key: "len_di_ma",
22269                value: ParamValue::Int(19),
22270            },
22271            ParamKV {
22272                key: "ma_type",
22273                value: ParamValue::EnumString("ema"),
22274            },
22275        ];
22276        let combos = [IndicatorParamSet { params: &params }];
22277
22278        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22279            indicator_id: "demand_index",
22280            output_id: Some("demand_index"),
22281            data: IndicatorDataRef::Ohlcv {
22282                open: &open,
22283                high: &high,
22284                low: &low,
22285                close: &close,
22286                volume: &volume,
22287            },
22288            combos: &combos,
22289            kernel: Kernel::Auto,
22290        })
22291        .unwrap();
22292
22293        let direct = demand_index_with_kernel(
22294            &DemandIndexInput::from_slices(
22295                &high,
22296                &low,
22297                &close,
22298                &volume,
22299                DemandIndexParams {
22300                    len_bs: Some(19),
22301                    len_bs_ma: Some(19),
22302                    len_di_ma: Some(19),
22303                    ma_type: Some("ema".to_string()),
22304                },
22305            ),
22306            Kernel::Auto,
22307        )
22308        .unwrap();
22309
22310        let values = dispatched.values_f64.as_ref().unwrap();
22311        assert_eq!(values.len(), close.len());
22312        assert_series_eq(values, &direct.demand_index, 1e-9);
22313    }
22314
22315    #[test]
22316    fn compute_cpu_batch_vwap_zscore_with_signals_matches_direct() {
22317        let close: Vec<f64> = (0..192).map(|i| 100.0 + (i as f64 * 0.15)).collect();
22318        let volume: Vec<f64> = (0..192).map(|i| 1_000.0 + (i as f64 * 2.0)).collect();
22319        let req = IndicatorBatchRequest {
22320            indicator_id: "vwap_zscore_with_signals",
22321            output_id: Some("zvwap"),
22322            data: IndicatorDataRef::CloseVolume {
22323                close: &close,
22324                volume: &volume,
22325            },
22326            combos: &[IndicatorParamSet {
22327                params: &[
22328                    ParamKV {
22329                        key: "length",
22330                        value: ParamValue::Int(20),
22331                    },
22332                    ParamKV {
22333                        key: "upper_bottom",
22334                        value: ParamValue::Float(2.5),
22335                    },
22336                    ParamKV {
22337                        key: "lower_bottom",
22338                        value: ParamValue::Float(-2.5),
22339                    },
22340                ],
22341            }],
22342            kernel: Kernel::Auto,
22343        };
22344
22345        let out = compute_cpu_batch(req).unwrap();
22346        let values = out.values_f64.as_ref().unwrap();
22347        let direct = vwap_zscore_with_signals_with_kernel(
22348            &VwapZscoreWithSignalsInput::from_slices(
22349                &close,
22350                &volume,
22351                VwapZscoreWithSignalsParams {
22352                    length: Some(20),
22353                    upper_bottom: Some(2.5),
22354                    lower_bottom: Some(-2.5),
22355                },
22356            ),
22357            Kernel::Auto,
22358        )
22359        .unwrap();
22360        assert_eq!(out.rows, 1);
22361        assert_eq!(out.cols, close.len());
22362        assert_series_eq(values, &direct.zvwap, 1e-9);
22363    }
22364
22365    #[test]
22366    fn compute_cpu_batch_gopalakrishnan_range_index_matches_direct() {
22367        let high: Vec<f64> = (0..256)
22368            .map(|i| 100.0 + i as f64 * 0.1 + ((i as f64) * 0.03).sin().abs())
22369            .collect();
22370        let low: Vec<f64> = high
22371            .iter()
22372            .enumerate()
22373            .map(|(i, h)| h - 0.75 - ((i as f64) * 0.02).cos().abs() * 0.2)
22374            .collect();
22375        let params = [ParamKV {
22376            key: "length",
22377            value: ParamValue::Int(5),
22378        }];
22379        let combos = [IndicatorParamSet { params: &params }];
22380
22381        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22382            indicator_id: "gopalakrishnan_range_index",
22383            output_id: Some("value"),
22384            data: IndicatorDataRef::HighLow {
22385                high: &high,
22386                low: &low,
22387            },
22388            combos: &combos,
22389            kernel: Kernel::Auto,
22390        })
22391        .unwrap();
22392
22393        let direct = gopalakrishnan_range_index_with_kernel(
22394            &GopalakrishnanRangeIndexInput::from_slices(
22395                &high,
22396                &low,
22397                GopalakrishnanRangeIndexParams { length: Some(5) },
22398            ),
22399            Kernel::Auto,
22400        )
22401        .unwrap();
22402
22403        let values = dispatched.values_f64.as_ref().unwrap();
22404        assert_eq!(values.len(), high.len());
22405        assert_series_eq(values, &direct.values, 1e-9);
22406    }
22407}