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, AccumulationSwingIndexParams,
11};
12use crate::indicators::acosc::{acosc_with_kernel, AcoscInput, AcoscParams};
13use crate::indicators::ad::{ad_with_kernel, AdInput, AdParams};
14use crate::indicators::adaptive_bandpass_trigger_oscillator::{
15    adaptive_bandpass_trigger_oscillator_with_kernel, AdaptiveBandpassTriggerOscillatorInput,
16    AdaptiveBandpassTriggerOscillatorParams,
17};
18use crate::indicators::adaptive_macd::{
19    adaptive_macd_with_kernel, AdaptiveMacdInput, AdaptiveMacdParams,
20};
21use crate::indicators::adaptive_momentum_oscillator::{
22    adaptive_momentum_oscillator_with_kernel, AdaptiveMomentumOscillatorInput,
23    AdaptiveMomentumOscillatorParams,
24};
25use crate::indicators::adaptive_schaff_trend_cycle::{
26    adaptive_schaff_trend_cycle_with_kernel, AdaptiveSchaffTrendCycleInput,
27    AdaptiveSchaffTrendCycleParams,
28};
29use crate::indicators::adjustable_ma_alternating_extremities::{
30    adjustable_ma_alternating_extremities_with_kernel, AdjustableMaAlternatingExtremitiesInput,
31    AdjustableMaAlternatingExtremitiesParams,
32};
33use crate::indicators::adosc::{adosc_with_kernel, AdoscInput, AdoscParams};
34use crate::indicators::advance_decline_line::{
35    advance_decline_line_with_kernel, AdvanceDeclineLineInput, AdvanceDeclineLineParams,
36};
37use crate::indicators::adx::{adx_with_kernel, AdxInput, AdxParams};
38use crate::indicators::adxr::{adxr_with_kernel, AdxrInput, AdxrParams};
39use crate::indicators::alligator::{alligator_with_kernel, AlligatorInput, AlligatorParams};
40use crate::indicators::alphatrend::{alphatrend_with_kernel, AlphaTrendInput, AlphaTrendParams};
41use crate::indicators::andean_oscillator::{
42    andean_oscillator_with_kernel, AndeanOscillatorInput, AndeanOscillatorParams,
43};
44use crate::indicators::ao::{ao_into_slice, AoInput, AoParams};
45use crate::indicators::apo::{apo_with_kernel, ApoInput, ApoParams};
46use crate::indicators::aroon::{aroon_with_kernel, AroonInput, AroonParams};
47use crate::indicators::aroonosc::{aroon_osc_with_kernel, AroonOscInput, AroonOscParams};
48use crate::indicators::aso::{aso_with_kernel, AsoInput, AsoParams};
49use crate::indicators::atr::{atr_with_kernel, AtrInput, AtrParams};
50use crate::indicators::atr_percentile::{
51    atr_percentile_with_kernel, AtrPercentileInput, AtrPercentileParams,
52};
53use crate::indicators::autocorrelation_indicator::{
54    autocorrelation_indicator_with_kernel, AutocorrelationIndicatorInput,
55    AutocorrelationIndicatorParams,
56};
57use crate::indicators::avsl::{avsl_with_kernel, AvslInput, AvslParams};
58use crate::indicators::bandpass::{bandpass_with_kernel, BandPassInput, BandPassParams};
59use crate::indicators::bollinger_bands::{
60    bollinger_bands_with_kernel, BollingerBandsInput, BollingerBandsParams,
61};
62use crate::indicators::bollinger_bands_width::{
63    bollinger_bands_width_with_kernel, BollingerBandsWidthInput, BollingerBandsWidthParams,
64};
65use crate::indicators::bop::{bop_with_kernel, BopInput, BopParams};
66use crate::indicators::bull_power_vs_bear_power::{
67    bull_power_vs_bear_power_with_kernel, BullPowerVsBearPowerInput, BullPowerVsBearPowerParams,
68};
69use crate::indicators::bulls_v_bears::{
70    bulls_v_bears_with_kernel, BullsVBearsCalculationMethod, BullsVBearsInput, BullsVBearsMaType,
71    BullsVBearsParams,
72};
73use crate::indicators::candle_strength_oscillator::{
74    candle_strength_oscillator_with_kernel, CandleStrengthOscillatorInput,
75    CandleStrengthOscillatorParams,
76};
77use crate::indicators::cci::{cci_with_kernel, CciInput, CciParams};
78use crate::indicators::cci_cycle::{cci_cycle_with_kernel, CciCycleInput, CciCycleParams};
79use crate::indicators::cfo::{cfo_with_kernel, CfoInput, CfoParams};
80use crate::indicators::chande::{chande_with_kernel, ChandeInput, ChandeParams};
81use crate::indicators::chandelier_exit::{
82    chandelier_exit_with_kernel, ChandelierExitInput, ChandelierExitParams,
83};
84use crate::indicators::chop::{chop_with_kernel, ChopInput, ChopParams};
85use crate::indicators::cksp::{cksp_with_kernel, CkspInput, CkspParams};
86use crate::indicators::cmo::{cmo_with_kernel, CmoInput, CmoParams};
87use crate::indicators::coppock::{coppock_with_kernel, CoppockInput, CoppockParams};
88use crate::indicators::correl_hl::{correl_hl_with_kernel, CorrelHlInput, CorrelHlParams};
89use crate::indicators::correlation_cycle::{
90    correlation_cycle_with_kernel, CorrelationCycleInput, CorrelationCycleParams,
91};
92use crate::indicators::cyberpunk_value_trend_analyzer::{
93    cyberpunk_value_trend_analyzer_with_kernel, CyberpunkValueTrendAnalyzerInput,
94    CyberpunkValueTrendAnalyzerParams,
95};
96use crate::indicators::cycle_channel_oscillator::{
97    cycle_channel_oscillator_with_kernel, CycleChannelOscillatorInput, CycleChannelOscillatorParams,
98};
99use crate::indicators::daily_factor::{
100    daily_factor_with_kernel, DailyFactorInput, DailyFactorParams,
101};
102use crate::indicators::damiani_volatmeter::{
103    damiani_volatmeter_with_kernel, DamianiVolatmeterInput, DamianiVolatmeterParams,
104};
105use crate::indicators::decisionpoint_breadth_swenlin_trading_oscillator::{
106    decisionpoint_breadth_swenlin_trading_oscillator_with_kernel,
107    DecisionPointBreadthSwenlinTradingOscillatorInput,
108    DecisionPointBreadthSwenlinTradingOscillatorParams,
109};
110use crate::indicators::demand_index::{
111    demand_index_with_kernel, DemandIndexInput, DemandIndexParams,
112};
113use crate::indicators::deviation::{deviation_with_kernel, DeviationInput, DeviationParams};
114use crate::indicators::devstop::{devstop_with_kernel, DevStopInput, DevStopParams};
115use crate::indicators::di::{di_with_kernel, DiInput, DiParams};
116use crate::indicators::didi_index::{didi_index_with_kernel, DidiIndexInput, DidiIndexParams};
117use crate::indicators::directional_imbalance_index::{
118    directional_imbalance_index_with_kernel, DirectionalImbalanceIndexInput,
119    DirectionalImbalanceIndexParams,
120};
121use crate::indicators::disparity_index::{
122    disparity_index_into_slice, DisparityIndexInput, DisparityIndexParams,
123};
124use crate::indicators::dm::{dm_with_kernel, DmInput, DmParams};
125use crate::indicators::donchian::{donchian_with_kernel, DonchianInput, DonchianParams};
126use crate::indicators::donchian_channel_width::{
127    donchian_channel_width_into_slice, DonchianChannelWidthInput, DonchianChannelWidthParams,
128};
129use crate::indicators::dpo::{dpo_with_kernel, DpoInput, DpoParams};
130use crate::indicators::dti::{dti_into_slice, DtiInput, DtiParams};
131use crate::indicators::dual_ulcer_index::{
132    dual_ulcer_index_with_kernel, DualUlcerIndexInput, DualUlcerIndexParams,
133};
134use crate::indicators::dvdiqqe::{dvdiqqe_with_kernel, DvdiqqeInput, DvdiqqeParams};
135use crate::indicators::dx::{dx_batch_with_kernel, dx_into_slice, DxBatchRange, DxInput, DxParams};
136use crate::indicators::dynamic_momentum_index::{
137    dynamic_momentum_index_into_slice, dynamic_momentum_index_with_kernel,
138    DynamicMomentumIndexInput, DynamicMomentumIndexParams,
139};
140use crate::indicators::efi::{efi_with_kernel, EfiInput, EfiParams};
141use crate::indicators::ehlers_adaptive_cg::{
142    ehlers_adaptive_cg_with_kernel, EhlersAdaptiveCgInput, EhlersAdaptiveCgParams,
143};
144use crate::indicators::ehlers_adaptive_cyber_cycle::{
145    ehlers_adaptive_cyber_cycle_with_kernel, EhlersAdaptiveCyberCycleInput,
146    EhlersAdaptiveCyberCycleParams,
147};
148use crate::indicators::ehlers_autocorrelation_periodogram::{
149    ehlers_autocorrelation_periodogram_with_kernel, EhlersAutocorrelationPeriodogramInput,
150    EhlersAutocorrelationPeriodogramParams,
151};
152use crate::indicators::ehlers_data_sampling_relative_strength_indicator::{
153    ehlers_data_sampling_relative_strength_indicator_with_kernel,
154    EhlersDataSamplingRelativeStrengthIndicatorInput,
155    EhlersDataSamplingRelativeStrengthIndicatorParams,
156};
157use crate::indicators::ehlers_detrending_filter::{
158    ehlers_detrending_filter_with_kernel, EhlersDetrendingFilterInput, EhlersDetrendingFilterParams,
159};
160use crate::indicators::ehlers_fm_demodulator::{
161    ehlers_fm_demodulator_with_kernel, EhlersFmDemodulatorInput, EhlersFmDemodulatorParams,
162};
163use crate::indicators::ehlers_linear_extrapolation_predictor::{
164    ehlers_linear_extrapolation_predictor_with_kernel, EhlersLinearExtrapolationPredictorInput,
165    EhlersLinearExtrapolationPredictorParams,
166};
167use crate::indicators::ehlers_simple_cycle_indicator::{
168    ehlers_simple_cycle_indicator_with_kernel, EhlersSimpleCycleIndicatorInput,
169    EhlersSimpleCycleIndicatorParams,
170};
171use crate::indicators::ehlers_smoothed_adaptive_momentum::{
172    ehlers_smoothed_adaptive_momentum_with_kernel, EhlersSmoothedAdaptiveMomentumInput,
173    EhlersSmoothedAdaptiveMomentumParams,
174};
175use crate::indicators::emd::{emd_with_kernel, EmdInput, EmdParams};
176use crate::indicators::emd_trend::{emd_trend_with_kernel, EmdTrendInput, EmdTrendParams};
177use crate::indicators::emv::{emv_with_kernel, EmvInput};
178use crate::indicators::er::{er_with_kernel, ErInput, ErParams};
179use crate::indicators::eri::{eri_with_kernel, EriInput, EriParams};
180use crate::indicators::evasive_supertrend::{
181    evasive_supertrend_with_kernel, EvasiveSuperTrendInput, EvasiveSuperTrendParams,
182};
183use crate::indicators::ewma_volatility::{
184    ewma_volatility_with_kernel, EwmaVolatilityInput, EwmaVolatilityParams,
185};
186use crate::indicators::exponential_trend::{
187    exponential_trend_with_kernel, ExponentialTrendInput, ExponentialTrendParams,
188};
189use crate::indicators::fibonacci_entry_bands::{
190    fibonacci_entry_bands_with_kernel, FibonacciEntryBandsInput, FibonacciEntryBandsParams,
191};
192use crate::indicators::fibonacci_trailing_stop::{
193    fibonacci_trailing_stop_with_kernel, FibonacciTrailingStopInput, FibonacciTrailingStopParams,
194};
195use crate::indicators::fisher::{fisher_with_kernel, FisherInput, FisherParams};
196use crate::indicators::forward_backward_exponential_oscillator::{
197    forward_backward_exponential_oscillator_with_kernel, ForwardBackwardExponentialOscillatorInput,
198    ForwardBackwardExponentialOscillatorParams,
199};
200use crate::indicators::fosc::{fosc_with_kernel, FoscInput, FoscParams};
201use crate::indicators::fractal_dimension_index::{
202    fractal_dimension_index_with_kernel, FractalDimensionIndexInput, FractalDimensionIndexParams,
203};
204use crate::indicators::fvg_positioning_average::{
205    fvg_positioning_average_with_kernel, FvgPositioningAverageInput, FvgPositioningAverageParams,
206};
207use crate::indicators::fvg_trailing_stop::{
208    fvg_trailing_stop_with_kernel, FvgTrailingStopInput, FvgTrailingStopParams,
209};
210use crate::indicators::garman_klass_volatility::{
211    garman_klass_volatility_with_kernel, GarmanKlassVolatilityInput, GarmanKlassVolatilityParams,
212};
213use crate::indicators::gatorosc::{gatorosc_with_kernel, GatorOscInput, GatorOscParams};
214use crate::indicators::geometric_bias_oscillator::{
215    geometric_bias_oscillator_with_kernel, GeometricBiasOscillatorInput,
216    GeometricBiasOscillatorParams,
217};
218use crate::indicators::gmma_oscillator::{
219    gmma_oscillator_with_kernel, GmmaOscillatorInput, GmmaOscillatorParams,
220};
221use crate::indicators::goertzel_cycle_composite_wave::{
222    goertzel_cycle_composite_wave_into_slice, GoertzelCycleCompositeWaveInput,
223    GoertzelCycleCompositeWaveParams, GoertzelDetrendMode,
224};
225use crate::indicators::gopalakrishnan_range_index::{
226    gopalakrishnan_range_index_with_kernel, GopalakrishnanRangeIndexInput,
227    GopalakrishnanRangeIndexParams,
228};
229use crate::indicators::grover_llorens_cycle_oscillator::{
230    grover_llorens_cycle_oscillator_with_kernel, GroverLlorensCycleOscillatorInput,
231    GroverLlorensCycleOscillatorParams,
232};
233use crate::indicators::half_causal_estimator::{
234    half_causal_estimator_with_kernel, HalfCausalEstimatorConfidenceAdjust,
235    HalfCausalEstimatorInput, HalfCausalEstimatorKernelType, HalfCausalEstimatorParams,
236};
237use crate::indicators::halftrend::{halftrend_with_kernel, HalfTrendInput, HalfTrendParams};
238use crate::indicators::hema_trend_levels::{
239    hema_trend_levels_with_kernel, HemaTrendLevelsInput, HemaTrendLevelsParams,
240};
241use crate::indicators::historical_volatility::{
242    historical_volatility_with_kernel, HistoricalVolatilityInput, HistoricalVolatilityParams,
243};
244use crate::indicators::historical_volatility_percentile::{
245    historical_volatility_percentile_with_kernel, HistoricalVolatilityPercentileInput,
246    HistoricalVolatilityPercentileParams,
247};
248use crate::indicators::historical_volatility_rank::{
249    historical_volatility_rank_with_kernel, HistoricalVolatilityRankInput,
250    HistoricalVolatilityRankParams,
251};
252use crate::indicators::hull_butterfly_oscillator::{
253    hull_butterfly_oscillator_with_kernel, HullButterflyOscillatorInput,
254    HullButterflyOscillatorParams,
255};
256use crate::indicators::hypertrend::{hypertrend_with_kernel, HyperTrendInput, HyperTrendParams};
257use crate::indicators::ichimoku_oscillator::{
258    ichimoku_oscillator_with_kernel, IchimokuOscillatorInput, IchimokuOscillatorNormalizeMode,
259    IchimokuOscillatorParams,
260};
261use crate::indicators::ict_propulsion_block::{
262    ict_propulsion_block_with_kernel, IctPropulsionBlockInput, IctPropulsionBlockMitigationPrice,
263    IctPropulsionBlockParams,
264};
265use crate::indicators::ift_rsi::{ift_rsi_with_kernel, IftRsiInput, IftRsiParams};
266use crate::indicators::impulse_macd::{
267    impulse_macd_with_kernel, ImpulseMacdInput, ImpulseMacdParams,
268};
269use crate::indicators::intraday_momentum_index::{
270    intraday_momentum_index_with_kernel, IntradayMomentumIndexInput, IntradayMomentumIndexParams,
271};
272use crate::indicators::kairi_relative_index::{
273    kairi_relative_index_into_slice, KairiRelativeIndexInput, KairiRelativeIndexParams,
274};
275use crate::indicators::kase_peak_oscillator_with_divergences::{
276    kase_peak_oscillator_with_divergences_with_kernel, KasePeakOscillatorWithDivergencesInput,
277    KasePeakOscillatorWithDivergencesParams,
278};
279use crate::indicators::kaufmanstop::{
280    kaufmanstop_with_kernel, KaufmanstopInput, KaufmanstopParams,
281};
282use crate::indicators::kdj::{kdj_with_kernel, KdjInput, KdjParams};
283use crate::indicators::keltner::{keltner_with_kernel, KeltnerInput, KeltnerParams};
284use crate::indicators::keltner_channel_width_oscillator::{
285    keltner_channel_width_oscillator_with_kernel, KeltnerChannelWidthOscillatorInput,
286    KeltnerChannelWidthOscillatorParams,
287};
288use crate::indicators::kst::{kst_with_kernel, KstInput, KstParams};
289use crate::indicators::kurtosis::{kurtosis_with_kernel, KurtosisInput, KurtosisParams};
290use crate::indicators::kvo::{kvo_with_kernel, KvoInput, KvoParams};
291use crate::indicators::l1_ehlers_phasor::{
292    l1_ehlers_phasor_with_kernel, L1EhlersPhasorInput, L1EhlersPhasorParams,
293};
294use crate::indicators::l2_ehlers_signal_to_noise::{
295    l2_ehlers_signal_to_noise_with_kernel, L2EhlersSignalToNoiseInput, L2EhlersSignalToNoiseParams,
296};
297use crate::indicators::leavitt_convolution_acceleration::{
298    leavitt_convolution_acceleration_with_kernel, LeavittConvolutionAccelerationInput,
299    LeavittConvolutionAccelerationParams,
300};
301use crate::indicators::linear_correlation_oscillator::{
302    linear_correlation_oscillator_with_kernel, LinearCorrelationOscillatorInput,
303    LinearCorrelationOscillatorParams,
304};
305use crate::indicators::linear_regression_intensity::{
306    linear_regression_intensity_with_kernel, LinearRegressionIntensityInput,
307    LinearRegressionIntensityParams,
308};
309use crate::indicators::linearreg_angle::{
310    linearreg_angle_with_kernel, Linearreg_angleInput, Linearreg_angleParams,
311};
312use crate::indicators::linearreg_intercept::{
313    linearreg_intercept_with_kernel, LinearRegInterceptInput, LinearRegInterceptParams,
314};
315use crate::indicators::linearreg_slope::{
316    linearreg_slope_with_kernel, LinearRegSlopeInput, LinearRegSlopeParams,
317};
318use crate::indicators::lpc::{lpc_with_kernel, LpcInput, LpcParams};
319use crate::indicators::lrsi::{lrsi_with_kernel, LrsiInput, LrsiParams};
320use crate::indicators::mab::{mab_with_kernel, MabInput, MabParams};
321use crate::indicators::macd::{macd_with_kernel, MacdInput, MacdParams};
322use crate::indicators::macd_wave_signal_pro::{
323    macd_wave_signal_pro_with_kernel, MacdWaveSignalProInput,
324};
325use crate::indicators::macz::{macz_with_kernel, MaczInput, MaczParams};
326use crate::indicators::market_meanness_index::{
327    market_meanness_index_with_kernel, MarketMeannessIndexInput, MarketMeannessIndexParams,
328};
329use crate::indicators::market_structure_confluence::{
330    market_structure_confluence_with_kernel, MarketStructureConfluenceInput,
331    MarketStructureConfluenceParams,
332};
333use crate::indicators::market_structure_trailing_stop::{
334    market_structure_trailing_stop_with_kernel, MarketStructureTrailingStopInput,
335    MarketStructureTrailingStopParams,
336};
337use crate::indicators::mass::{mass_with_kernel, MassInput, MassParams};
338use crate::indicators::mean_ad::{mean_ad_with_kernel, MeanAdInput, MeanAdParams};
339use crate::indicators::medium_ad::{medium_ad_with_kernel, MediumAdInput, MediumAdParams};
340use crate::indicators::medprice::{medprice_with_kernel, MedpriceInput, MedpriceParams};
341use crate::indicators::mesa_stochastic_multi_length::{
342    mesa_stochastic_multi_length_with_kernel, MesaStochasticMultiLengthInput,
343    MesaStochasticMultiLengthParams,
344};
345use crate::indicators::mfi::{
346    mfi_batch_with_kernel, mfi_into_slice, MfiBatchRange, MfiInput, MfiParams,
347};
348use crate::indicators::midpoint::{midpoint_with_kernel, MidpointInput, MidpointParams};
349use crate::indicators::midprice::{midprice_with_kernel, MidpriceInput, MidpriceParams};
350use crate::indicators::minmax::{minmax_with_kernel, MinmaxInput, MinmaxParams};
351use crate::indicators::mod_god_mode::{
352    mod_god_mode, ModGodModeData, ModGodModeInput, ModGodModeMode, ModGodModeParams,
353};
354use crate::indicators::mom::{mom_with_kernel, MomInput, MomParams};
355use crate::indicators::momentum_ratio_oscillator::{
356    momentum_ratio_oscillator_with_kernel, MomentumRatioOscillatorInput,
357    MomentumRatioOscillatorParams,
358};
359use crate::indicators::monotonicity_index::{
360    monotonicity_index_with_kernel, MonotonicityIndexInput, MonotonicityIndexMode,
361    MonotonicityIndexParams,
362};
363use crate::indicators::moving_average_cross_probability::{
364    moving_average_cross_probability_with_kernel, MovingAverageCrossProbabilityInput,
365    MovingAverageCrossProbabilityMaType, MovingAverageCrossProbabilityParams,
366};
367use crate::indicators::moving_averages::logarithmic_moving_average::{
368    logarithmic_moving_average_with_kernel, LogarithmicMovingAverageInput,
369    LogarithmicMovingAverageParams,
370};
371use crate::indicators::moving_averages::ma::MaData;
372use crate::indicators::moving_averages::ma_batch::{
373    ma_batch_with_kernel_and_typed_params, MaBatchParamKV, MaBatchParamValue,
374};
375use crate::indicators::moving_averages::registry::list_moving_averages;
376use crate::indicators::msw::{msw_with_kernel, MswInput, MswParams};
377use crate::indicators::multi_length_stochastic_average::{
378    multi_length_stochastic_average_with_kernel, MultiLengthStochasticAverageInput,
379    MultiLengthStochasticAverageParams,
380};
381use crate::indicators::nadaraya_watson_envelope::{
382    nadaraya_watson_envelope_with_kernel, NweInput, NweParams,
383};
384use crate::indicators::natr::{natr_with_kernel, NatrInput, NatrParams};
385use crate::indicators::neighboring_trailing_stop::{
386    neighboring_trailing_stop_with_kernel, NeighboringTrailingStopInput,
387    NeighboringTrailingStopParams,
388};
389use crate::indicators::net_myrsi::{net_myrsi_with_kernel, NetMyrsiInput, NetMyrsiParams};
390use crate::indicators::nonlinear_regression_zero_lag_moving_average::{
391    nonlinear_regression_zero_lag_moving_average_with_kernel,
392    NonlinearRegressionZeroLagMovingAverageInput, NonlinearRegressionZeroLagMovingAverageParams,
393};
394use crate::indicators::normalized_resonator::{
395    normalized_resonator_with_kernel, NormalizedResonatorInput, NormalizedResonatorParams,
396};
397use crate::indicators::normalized_volume_true_range::{
398    normalized_volume_true_range_with_kernel, NormalizedVolumeTrueRangeInput,
399    NormalizedVolumeTrueRangeParams, NormalizedVolumeTrueRangeStyle,
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::percentile_nearest_rank::{
412    percentile_nearest_rank_with_kernel, PercentileNearestRankInput, PercentileNearestRankParams,
413};
414use crate::indicators::pfe::{pfe_with_kernel, PfeInput, PfeParams};
415use crate::indicators::pivot::{pivot_with_kernel, PivotInput, PivotParams};
416use crate::indicators::pma::{pma_with_kernel, PmaInput, PmaParams};
417use crate::indicators::polynomial_regression_extrapolation::{
418    polynomial_regression_extrapolation_with_kernel, PolynomialRegressionExtrapolationInput,
419    PolynomialRegressionExtrapolationParams,
420};
421use crate::indicators::possible_rsi::{
422    possible_rsi_with_kernel, PossibleRsiInput, PossibleRsiParams,
423};
424use crate::indicators::ppo::{ppo_with_kernel, PpoInput, PpoParams};
425use crate::indicators::prb::{prb_with_kernel, PrbInput, PrbParams};
426use crate::indicators::premier_rsi_oscillator::{
427    premier_rsi_oscillator_with_kernel, PremierRsiOscillatorInput, PremierRsiOscillatorParams,
428};
429use crate::indicators::pretty_good_oscillator::{
430    pretty_good_oscillator_with_kernel, PrettyGoodOscillatorInput, PrettyGoodOscillatorParams,
431};
432use crate::indicators::price_density_market_noise::{
433    price_density_market_noise_with_kernel, PriceDensityMarketNoiseInput,
434    PriceDensityMarketNoiseParams,
435};
436use crate::indicators::price_moving_average_ratio_percentile::{
437    price_moving_average_ratio_percentile_with_kernel, PriceMovingAverageRatioPercentileInput,
438    PriceMovingAverageRatioPercentileLineMode, PriceMovingAverageRatioPercentileMaType,
439    PriceMovingAverageRatioPercentileParams,
440};
441use crate::indicators::projection_oscillator::{
442    projection_oscillator_with_kernel, ProjectionOscillatorInput, ProjectionOscillatorParams,
443};
444use crate::indicators::psychological_line::{
445    psychological_line_with_kernel, PsychologicalLineInput, PsychologicalLineParams,
446};
447use crate::indicators::pvi::{pvi_with_kernel, PviInput, PviParams};
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::range_filtered_trend_signals::{
463    range_filtered_trend_signals_with_kernel, RangeFilteredTrendSignalsInput,
464    RangeFilteredTrendSignalsParams,
465};
466use crate::indicators::range_oscillator::{
467    range_oscillator_with_kernel, RangeOscillatorInput, RangeOscillatorParams,
468};
469use crate::indicators::rank_correlation_index::{
470    rank_correlation_index_with_kernel, RankCorrelationIndexInput, RankCorrelationIndexParams,
471};
472use crate::indicators::registry::{
473    get_indicator, IndicatorInfo, IndicatorInputKind, ParamValueStatic,
474};
475use crate::indicators::regression_slope_oscillator::{
476    regression_slope_oscillator_with_kernel, RegressionSlopeOscillatorInput,
477    RegressionSlopeOscillatorParams,
478};
479use crate::indicators::relative_strength_index_wave_indicator::{
480    relative_strength_index_wave_indicator_with_kernel, RelativeStrengthIndexWaveIndicatorInput,
481    RelativeStrengthIndexWaveIndicatorParams,
482};
483use crate::indicators::reversal_signals::{
484    reversal_signals_with_kernel, ReversalSignalsInput, ReversalSignalsParams,
485};
486use crate::indicators::reverse_rsi::{reverse_rsi_with_kernel, ReverseRsiInput, ReverseRsiParams};
487use crate::indicators::roc::{roc_with_kernel, RocInput, RocParams};
488use crate::indicators::rocp::{rocp_with_kernel, RocpInput, RocpParams};
489use crate::indicators::rocr::{rocr_with_kernel, RocrInput, RocrParams};
490use crate::indicators::rogers_satchell_volatility::{
491    rogers_satchell_volatility_with_kernel, RogersSatchellVolatilityInput,
492    RogersSatchellVolatilityParams,
493};
494use crate::indicators::rolling_skewness_kurtosis::{
495    rolling_skewness_kurtosis_with_kernel, RollingSkewnessKurtosisInput,
496    RollingSkewnessKurtosisParams,
497};
498use crate::indicators::rolling_z_score_trend::{
499    rolling_z_score_trend_with_kernel, RollingZScoreTrendInput, RollingZScoreTrendParams,
500};
501use crate::indicators::rsi::{rsi_with_kernel, RsiInput, RsiParams};
502use crate::indicators::rsmk::{rsmk_with_kernel, RsmkInput, RsmkParams};
503use crate::indicators::rvi::{rvi_with_kernel, RviInput, RviParams};
504use crate::indicators::safezonestop::{
505    safezonestop_with_kernel, SafeZoneStopInput, SafeZoneStopParams,
506};
507use crate::indicators::smooth_theil_sen::{
508    smooth_theil_sen_with_kernel, SmoothTheilSenDeviationType, SmoothTheilSenInput,
509    SmoothTheilSenParams, SmoothTheilSenStatStyle,
510};
511use crate::indicators::smoothed_gaussian_trend_filter::{
512    smoothed_gaussian_trend_filter_with_kernel, SmoothedGaussianTrendFilterInput,
513    SmoothedGaussianTrendFilterParams,
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::srsi::{srsi_with_kernel, SrsiInput, SrsiParams};
525use crate::indicators::standardized_psar_oscillator::{
526    standardized_psar_oscillator_with_kernel, StandardizedPsarOscillatorInput,
527    StandardizedPsarOscillatorParams,
528};
529use crate::indicators::statistical_trailing_stop::{
530    statistical_trailing_stop_with_kernel, StatisticalTrailingStopInput,
531    StatisticalTrailingStopParams,
532};
533use crate::indicators::stc::{stc_with_kernel, StcInput, StcParams};
534use crate::indicators::stddev::{stddev_with_kernel, StdDevInput, StdDevParams};
535use crate::indicators::stoch::{stoch_with_kernel, StochInput, StochParams};
536use crate::indicators::stochastic_adaptive_d::{
537    stochastic_adaptive_d_with_kernel, StochasticAdaptiveDInput, StochasticAdaptiveDParams,
538};
539use crate::indicators::stochastic_connors_rsi::{
540    stochastic_connors_rsi_with_kernel, StochasticConnorsRsiInput, StochasticConnorsRsiParams,
541};
542use crate::indicators::stochastic_distance::{
543    stochastic_distance_with_kernel, StochasticDistanceInput, StochasticDistanceParams,
544};
545use crate::indicators::stochastic_money_flow_index::{
546    stochastic_money_flow_index_with_kernel, StochasticMoneyFlowIndexInput,
547    StochasticMoneyFlowIndexParams,
548};
549use crate::indicators::stochf::{stochf_with_kernel, StochfInput, StochfParams};
550use crate::indicators::supertrend::{supertrend_with_kernel, SuperTrendInput, SuperTrendParams};
551use crate::indicators::supertrend_oscillator::{
552    supertrend_oscillator_with_kernel, SuperTrendOscillatorInput, SuperTrendOscillatorParams,
553};
554use crate::indicators::supertrend_recovery::{
555    supertrend_recovery_with_kernel, SuperTrendRecoveryInput, SuperTrendRecoveryParams,
556};
557use crate::indicators::trend_continuation_factor::{
558    trend_continuation_factor_with_kernel, TrendContinuationFactorInput,
559    TrendContinuationFactorParams,
560};
561use crate::indicators::trend_direction_force_index::{
562    trend_direction_force_index_into_slice, TrendDirectionForceIndexInput,
563    TrendDirectionForceIndexParams,
564};
565use crate::indicators::trend_flow_trail::{
566    trend_flow_trail_with_kernel, TrendFlowTrailInput, TrendFlowTrailParams,
567};
568use crate::indicators::trend_trigger_factor::{
569    trend_trigger_factor_with_kernel, TrendTriggerFactorInput, TrendTriggerFactorParams,
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::twiggs_money_flow::{
580    twiggs_money_flow_with_kernel, TwiggsMoneyFlowInput, TwiggsMoneyFlowParams,
581};
582use crate::indicators::ui::{ui_with_kernel, UiInput, UiParams};
583use crate::indicators::ultosc::{ultosc_with_kernel, UltOscInput, UltOscParams};
584use crate::indicators::var::{var_with_kernel, VarInput, VarParams};
585use crate::indicators::vdubus_divergence_wave_pattern_generator::{
586    vdubus_divergence_wave_pattern_generator_with_kernel,
587    VdubusDivergenceWavePatternGeneratorInput, VdubusDivergenceWavePatternGeneratorParams,
588};
589use crate::indicators::velocity::{velocity_with_kernel, VelocityInput, VelocityParams};
590use crate::indicators::velocity_acceleration_convergence_divergence_indicator::{
591    velocity_acceleration_convergence_divergence_indicator_with_kernel,
592    VelocityAccelerationConvergenceDivergenceIndicatorInput,
593    VelocityAccelerationConvergenceDivergenceIndicatorParams,
594};
595use crate::indicators::velocity_acceleration_indicator::{
596    velocity_acceleration_indicator_with_kernel, VelocityAccelerationIndicatorInput,
597    VelocityAccelerationIndicatorParams,
598};
599use crate::indicators::vertical_horizontal_filter::{
600    vertical_horizontal_filter_with_kernel, VerticalHorizontalFilterInput,
601    VerticalHorizontalFilterParams,
602};
603use crate::indicators::vi::{vi_with_kernel, ViInput, ViParams};
604use crate::indicators::vidya::{vidya_with_kernel, VidyaInput, VidyaParams};
605use crate::indicators::vlma::{vlma_with_kernel, VlmaInput, VlmaParams};
606use crate::indicators::volatility_quality_index::{
607    volatility_quality_index_with_kernel, VolatilityQualityIndexInput, VolatilityQualityIndexParams,
608};
609use crate::indicators::volatility_ratio_adaptive_rsx::{
610    volatility_ratio_adaptive_rsx_with_kernel, VolatilityRatioAdaptiveRsxInput,
611    VolatilityRatioAdaptiveRsxParams,
612};
613use crate::indicators::volume_energy_reservoirs::{
614    volume_energy_reservoirs_with_kernel, VolumeEnergyReservoirsInput, VolumeEnergyReservoirsParams,
615};
616use crate::indicators::volume_weighted_relative_strength_index::{
617    volume_weighted_relative_strength_index_with_kernel, VolumeWeightedRelativeStrengthIndexInput,
618    VolumeWeightedRelativeStrengthIndexParams,
619};
620use crate::indicators::volume_weighted_rsi::{
621    volume_weighted_rsi_batch_with_kernel, volume_weighted_rsi_into_slice,
622    VolumeWeightedRsiBatchRange, VolumeWeightedRsiInput, VolumeWeightedRsiParams,
623};
624use crate::indicators::volume_weighted_stochastic_rsi::{
625    volume_weighted_stochastic_rsi_with_kernel, VolumeWeightedStochasticRsiInput,
626    VolumeWeightedStochasticRsiParams,
627};
628use crate::indicators::volume_zone_oscillator::{
629    volume_zone_oscillator_with_kernel, VolumeZoneOscillatorInput, VolumeZoneOscillatorParams,
630};
631use crate::indicators::vosc::{vosc_with_kernel, VoscInput, VoscParams};
632use crate::indicators::voss::{voss_with_kernel, VossInput, VossParams};
633use crate::indicators::vpci::{vpci_with_kernel, VpciInput, VpciParams};
634use crate::indicators::vpt::{vpt_with_kernel, VptInput};
635use crate::indicators::vwap_deviation_oscillator::{
636    vwap_deviation_oscillator_with_kernel, VwapDeviationMode, VwapDeviationOscillatorInput,
637    VwapDeviationOscillatorParams, VwapDeviationSessionMode,
638};
639use crate::indicators::vwap_zscore_with_signals::{
640    vwap_zscore_with_signals_with_kernel, VwapZscoreWithSignalsInput, VwapZscoreWithSignalsParams,
641};
642use crate::indicators::vwmacd::{vwmacd_with_kernel, VwmacdInput, VwmacdParams};
643use crate::indicators::wad::{wad_with_kernel, WadInput};
644use crate::indicators::wavetrend::{wavetrend_with_kernel, WavetrendInput, WavetrendParams};
645use crate::indicators::wclprice::{wclprice_with_kernel, WclpriceInput};
646use crate::indicators::willr::{willr_with_kernel, WillrInput, WillrParams};
647use crate::indicators::wto::{wto_with_kernel, WtoInput, WtoParams};
648use crate::indicators::yang_zhang_volatility::{
649    yang_zhang_volatility_with_kernel, YangZhangVolatilityInput, YangZhangVolatilityParams,
650};
651use crate::indicators::zig_zag_channels::{
652    zig_zag_channels_with_kernel, ZigZagChannelsInput, ZigZagChannelsParams,
653};
654use crate::indicators::zscore::{zscore_with_kernel, ZscoreInput, ZscoreParams};
655use crate::indicators::{cg::cg_with_kernel, cg::CgInput, cg::CgParams};
656use crate::utilities::data_loader::source_type;
657use crate::utilities::enums::Kernel;
658use std::collections::HashMap;
659use std::str::FromStr;
660
661pub fn compute_cpu_batch(
662    req: IndicatorBatchRequest<'_>,
663) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
664    compute_cpu_batch_internal(req, false)
665}
666
667pub fn compute_cpu_batch_strict(
668    req: IndicatorBatchRequest<'_>,
669) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
670    compute_cpu_batch_internal(req, true)
671}
672
673fn compute_cpu_batch_internal(
674    req: IndicatorBatchRequest<'_>,
675    strict_inputs: bool,
676) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
677    if !strict_inputs {
678        if let Some(out) = try_fast_dispatch_non_strict(req) {
679            return out;
680        }
681    }
682
683    let info = get_indicator(req.indicator_id);
684
685    if let Some(info) = info {
686        if strict_inputs {
687            validate_input_kind_strict(info.id, info.input_kind, req.data)?;
688        }
689
690        let output_id = resolve_output_id(info, req.output_id)?;
691
692        if info.id.eq_ignore_ascii_case("logarithmic_moving_average") {
693            return compute_logarithmic_moving_average_batch(req, output_id);
694        }
695
696        if is_moving_average(info.id) {
697            return compute_ma_batch(req, info, output_id);
698        }
699
700        return dispatch_cpu_batch_by_indicator(req, info.id, output_id);
701    }
702
703    let output_id = req.output_id.unwrap_or("value");
704    match dispatch_cpu_batch_by_indicator(req, req.indicator_id, output_id) {
705        Err(IndicatorDispatchError::UnsupportedCapability { .. }) => {
706            Err(IndicatorDispatchError::UnknownIndicator {
707                id: req.indicator_id.to_string(),
708            })
709        }
710        other => other,
711    }
712}
713
714fn try_fast_dispatch_non_strict(
715    req: IndicatorBatchRequest<'_>,
716) -> Option<Result<IndicatorBatchOutput, IndicatorDispatchError>> {
717    let id = req.indicator_id;
718    let output_id = req.output_id;
719
720    if !id.as_bytes().iter().any(|b| b.is_ascii_uppercase()) {
721        return match id {
722            "bop" => Some(compute_bop_batch(req, output_id.unwrap_or("value"))),
723            "dpo" => Some(compute_dpo_batch(req, output_id.unwrap_or("value"))),
724            "cmo" => Some(compute_cmo_batch(req, output_id.unwrap_or("value"))),
725            "fosc" => Some(compute_fosc_batch(req, output_id.unwrap_or("value"))),
726            "emv" => Some(compute_emv_batch(req, output_id.unwrap_or("value"))),
727            "cci_cycle" => Some(compute_cci_cycle_batch(req, output_id.unwrap_or("value"))),
728            "cfo" => Some(compute_cfo_batch(req, output_id.unwrap_or("value"))),
729            "ehlers_adaptive_cg" => Some(compute_ehlers_adaptive_cg_batch(
730                req,
731                output_id.unwrap_or("cg"),
732            )),
733            "adaptive_momentum_oscillator" => Some(compute_adaptive_momentum_oscillator_batch(
734                req,
735                output_id.unwrap_or("amo"),
736            )),
737            "lrsi" => Some(compute_lrsi_batch(req, output_id.unwrap_or("value"))),
738            "nvi" => Some(compute_nvi_batch(req, output_id.unwrap_or("value"))),
739            "mom" => Some(compute_mom_batch(req, output_id.unwrap_or("value"))),
740            "velocity" => Some(compute_velocity_batch(req, output_id.unwrap_or("value"))),
741            "normalized_volume_true_range" => Some(compute_normalized_volume_true_range_batch(
742                req,
743                output_id.unwrap_or("normalized_volume"),
744            )),
745            "exponential_trend" => Some(compute_exponential_trend_batch(
746                req,
747                output_id.unwrap_or("uptrend_base"),
748            )),
749            "trend_flow_trail" => Some(compute_trend_flow_trail_batch(
750                req,
751                output_id.unwrap_or("alpha_trail"),
752            )),
753            "range_breakout_signals" => Some(compute_range_breakout_signals_batch(
754                req,
755                output_id.unwrap_or("range_top"),
756            )),
757            "vi" => {
758                if let Some(out) = output_id {
759                    Some(compute_vi_batch(req, out))
760                } else {
761                    None
762                }
763            }
764            "wto" => {
765                if let Some(out) = output_id {
766                    Some(compute_wto_batch(req, out))
767                } else {
768                    None
769                }
770            }
771            "rogers_satchell_volatility" => {
772                if let Some(out) = output_id {
773                    Some(compute_rogers_satchell_volatility_batch(req, out))
774                } else {
775                    None
776                }
777            }
778            "historical_volatility_rank" => {
779                if let Some(out) = output_id {
780                    Some(compute_historical_volatility_rank_batch(req, out))
781                } else {
782                    None
783                }
784            }
785            "dual_ulcer_index" => {
786                if let Some(out) = output_id {
787                    Some(compute_dual_ulcer_index_batch(req, out))
788                } else {
789                    None
790                }
791            }
792            "fractal_dimension_index" => {
793                if let Some(out) = output_id {
794                    Some(compute_fractal_dimension_index_batch(req, out))
795                } else {
796                    None
797                }
798            }
799            "volume_weighted_rsi" => {
800                if let Some(out) = output_id {
801                    Some(compute_volume_weighted_rsi_batch(req, out))
802                } else {
803                    None
804                }
805            }
806            "dynamic_momentum_index" => {
807                if let Some(out) = output_id {
808                    Some(compute_dynamic_momentum_index_batch(req, out))
809                } else {
810                    None
811                }
812            }
813            "disparity_index" => {
814                if let Some(out) = output_id {
815                    Some(compute_disparity_index_batch(req, out))
816                } else {
817                    None
818                }
819            }
820            "donchian_channel_width" => {
821                if let Some(out) = output_id {
822                    Some(compute_donchian_channel_width_batch(req, out))
823                } else {
824                    None
825                }
826            }
827            "kairi_relative_index" => {
828                if let Some(out) = output_id {
829                    Some(compute_kairi_relative_index_batch(req, out))
830                } else {
831                    None
832                }
833            }
834            "projection_oscillator" => {
835                if let Some(out) = output_id {
836                    Some(compute_projection_oscillator_batch(req, out))
837                } else {
838                    None
839                }
840            }
841            "market_structure_trailing_stop" => {
842                if let Some(out) = output_id {
843                    Some(compute_market_structure_trailing_stop_batch(req, out))
844                } else {
845                    None
846                }
847            }
848            "emd_trend" => {
849                if let Some(out) = output_id {
850                    Some(compute_emd_trend_batch(req, out))
851                } else {
852                    None
853                }
854            }
855            "cyberpunk_value_trend_analyzer" => {
856                if let Some(out) = output_id {
857                    Some(compute_cyberpunk_value_trend_analyzer_batch(req, out))
858                } else {
859                    None
860                }
861            }
862            "evasive_supertrend" => {
863                if let Some(out) = output_id {
864                    Some(compute_evasive_supertrend_batch(req, out))
865                } else {
866                    None
867                }
868            }
869            "reversal_signals" => {
870                if let Some(out) = output_id {
871                    Some(compute_reversal_signals_batch(req, out))
872                } else {
873                    None
874                }
875            }
876            "zig_zag_channels" => {
877                if let Some(out) = output_id {
878                    Some(compute_zig_zag_channels_batch(req, out))
879                } else {
880                    None
881                }
882            }
883            "directional_imbalance_index" => {
884                if let Some(out) = output_id {
885                    Some(compute_directional_imbalance_index_batch(req, out))
886                } else {
887                    None
888                }
889            }
890            "candle_strength_oscillator" => {
891                if let Some(out) = output_id {
892                    Some(compute_candle_strength_oscillator_batch(req, out))
893                } else {
894                    None
895                }
896            }
897            "gmma_oscillator" => {
898                if let Some(out) = output_id {
899                    Some(compute_gmma_oscillator_batch(req, out))
900                } else {
901                    None
902                }
903            }
904            "nonlinear_regression_zero_lag_moving_average" => {
905                if let Some(out) = output_id {
906                    Some(compute_nonlinear_regression_zero_lag_moving_average_batch(
907                        req, out,
908                    ))
909                } else {
910                    None
911                }
912            }
913            "possible_rsi" => {
914                if let Some(out) = output_id {
915                    Some(compute_possible_rsi_batch(req, out))
916                } else {
917                    None
918                }
919            }
920            "autocorrelation_indicator" => {
921                if let Some(out) = output_id {
922                    Some(compute_autocorrelation_indicator_batch(req, out))
923                } else {
924                    None
925                }
926            }
927            "goertzel_cycle_composite_wave" => {
928                if let Some(out) = output_id {
929                    Some(compute_goertzel_cycle_composite_wave_batch(req, out))
930                } else {
931                    None
932                }
933            }
934            "rolling_skewness_kurtosis" => {
935                if let Some(out) = output_id {
936                    Some(compute_rolling_skewness_kurtosis_batch(req, out))
937                } else {
938                    None
939                }
940            }
941            "rolling_z_score_trend" => {
942                if let Some(out) = output_id {
943                    Some(compute_rolling_z_score_trend_batch(req, out))
944                } else {
945                    None
946                }
947            }
948            "ehlers_data_sampling_relative_strength_indicator" => {
949                if let Some(out) = output_id {
950                    Some(compute_ehlers_data_sampling_relative_strength_indicator_batch(req, out))
951                } else {
952                    None
953                }
954            }
955            "velocity_acceleration_convergence_divergence_indicator" => {
956                if let Some(out) = output_id {
957                    Some(
958                        compute_velocity_acceleration_convergence_divergence_indicator_batch(
959                            req, out,
960                        ),
961                    )
962                } else {
963                    None
964                }
965            }
966            "trend_direction_force_index" => {
967                if let Some(out) = output_id {
968                    Some(compute_trend_direction_force_index_batch(req, out))
969                } else {
970                    None
971                }
972            }
973            "yang_zhang_volatility" => {
974                if let Some(out) = output_id {
975                    Some(compute_yang_zhang_volatility_batch(req, out))
976                } else {
977                    None
978                }
979            }
980            "garman_klass_volatility" => Some(compute_garman_klass_volatility_batch(
981                req,
982                output_id.unwrap_or("value"),
983            )),
984            "advance_decline_line" => Some(compute_advance_decline_line_batch(
985                req,
986                output_id.unwrap_or("value"),
987            )),
988            "decisionpoint_breadth_swenlin_trading_oscillator" => Some(
989                compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(
990                    req,
991                    output_id.unwrap_or("value"),
992                ),
993            ),
994            "velocity_acceleration_indicator" => Some(
995                compute_velocity_acceleration_indicator_batch(req, output_id.unwrap_or("value")),
996            ),
997            "normalized_resonator" => Some(compute_normalized_resonator_batch(
998                req,
999                output_id.unwrap_or("oscillator"),
1000            )),
1001            "monotonicity_index" => Some(compute_monotonicity_index_batch(
1002                req,
1003                output_id.unwrap_or("index"),
1004            )),
1005            "half_causal_estimator" => Some(compute_half_causal_estimator_batch(
1006                req,
1007                output_id.unwrap_or("estimate"),
1008            )),
1009            "atr_percentile" => Some(compute_atr_percentile_batch(
1010                req,
1011                output_id.unwrap_or("value"),
1012            )),
1013            "bull_power_vs_bear_power" => Some(compute_bull_power_vs_bear_power_batch(
1014                req,
1015                output_id.unwrap_or("value"),
1016            )),
1017            "didi_index" => Some(compute_didi_index_batch(req, output_id.unwrap_or("short"))),
1018            "ehlers_autocorrelation_periodogram" => {
1019                Some(compute_ehlers_autocorrelation_periodogram_batch(
1020                    req,
1021                    output_id.unwrap_or("dominant_cycle"),
1022                ))
1023            }
1024            "ehlers_linear_extrapolation_predictor" => {
1025                Some(compute_ehlers_linear_extrapolation_predictor_batch(
1026                    req,
1027                    output_id.unwrap_or("prediction"),
1028                ))
1029            }
1030            "kase_peak_oscillator_with_divergences" => {
1031                Some(compute_kase_peak_oscillator_with_divergences_batch(
1032                    req,
1033                    output_id.unwrap_or("oscillator"),
1034                ))
1035            }
1036            "absolute_strength_index_oscillator" => {
1037                Some(compute_absolute_strength_index_oscillator_batch(
1038                    req,
1039                    output_id.unwrap_or("oscillator"),
1040                ))
1041            }
1042            "adaptive_bandpass_trigger_oscillator" => {
1043                Some(compute_adaptive_bandpass_trigger_oscillator_batch(
1044                    req,
1045                    output_id.unwrap_or("in_phase"),
1046                ))
1047            }
1048            "premier_rsi_oscillator" => Some(compute_premier_rsi_oscillator_batch(
1049                req,
1050                output_id.unwrap_or("value"),
1051            )),
1052            "multi_length_stochastic_average" => Some(
1053                compute_multi_length_stochastic_average_batch(req, output_id.unwrap_or("value")),
1054            ),
1055            "hull_butterfly_oscillator" => Some(compute_hull_butterfly_oscillator_batch(
1056                req,
1057                output_id.unwrap_or("oscillator"),
1058            )),
1059            "fibonacci_trailing_stop" => Some(compute_fibonacci_trailing_stop_batch(
1060                req,
1061                output_id.unwrap_or("trailing_stop"),
1062            )),
1063            "fibonacci_entry_bands" => Some(compute_fibonacci_entry_bands_batch(
1064                req,
1065                output_id.unwrap_or("middle"),
1066            )),
1067            "volume_energy_reservoirs" => Some(compute_volume_energy_reservoirs_batch(
1068                req,
1069                output_id.unwrap_or("momentum"),
1070            )),
1071            "neighboring_trailing_stop" => Some(compute_neighboring_trailing_stop_batch(
1072                req,
1073                output_id.unwrap_or("trailing_stop"),
1074            )),
1075            "grover_llorens_cycle_oscillator" => Some(
1076                compute_grover_llorens_cycle_oscillator_batch(req, output_id.unwrap_or("value")),
1077            ),
1078            "historical_volatility" => Some(compute_historical_volatility_batch(
1079                req,
1080                output_id.unwrap_or("value"),
1081            )),
1082            "squeeze_index" => Some(compute_squeeze_index_batch(
1083                req,
1084                output_id.unwrap_or("value"),
1085            )),
1086            "stochastic_distance" => Some(compute_stochastic_distance_batch(
1087                req,
1088                output_id.unwrap_or("oscillator"),
1089            )),
1090            "vertical_horizontal_filter" => Some(compute_vertical_horizontal_filter_batch(
1091                req,
1092                output_id.unwrap_or("value"),
1093            )),
1094            "intraday_momentum_index" => {
1095                if let Some(out) = output_id {
1096                    Some(compute_intraday_momentum_index_batch(req, out))
1097                } else {
1098                    None
1099                }
1100            }
1101            "vwap_zscore_with_signals" => {
1102                if let Some(out) = output_id {
1103                    Some(compute_vwap_zscore_with_signals_batch(req, out))
1104                } else {
1105                    None
1106                }
1107            }
1108            "macd_wave_signal_pro" => {
1109                if let Some(out) = output_id {
1110                    Some(compute_macd_wave_signal_pro_batch(req, out))
1111                } else {
1112                    None
1113                }
1114            }
1115            "hema_trend_levels" => {
1116                if let Some(out) = output_id {
1117                    Some(compute_hema_trend_levels_batch(req, out))
1118                } else {
1119                    None
1120                }
1121            }
1122            "demand_index" => {
1123                if let Some(out) = output_id {
1124                    Some(compute_demand_index_batch(req, out))
1125                } else {
1126                    None
1127                }
1128            }
1129            "gopalakrishnan_range_index" => Some(compute_gopalakrishnan_range_index_batch(
1130                req,
1131                output_id.unwrap_or("value"),
1132            )),
1133            "voss" => {
1134                if let Some(out) = output_id {
1135                    Some(compute_voss_batch(req, out))
1136                } else {
1137                    None
1138                }
1139            }
1140            "acosc" => {
1141                if let Some(out) = output_id {
1142                    Some(compute_acosc_batch(req, out))
1143                } else {
1144                    None
1145                }
1146            }
1147            _ => None,
1148        };
1149    }
1150
1151    if id.eq_ignore_ascii_case("bop") {
1152        return Some(compute_bop_batch(req, output_id.unwrap_or("value")));
1153    }
1154    if id.eq_ignore_ascii_case("dpo") {
1155        return Some(compute_dpo_batch(req, output_id.unwrap_or("value")));
1156    }
1157    if id.eq_ignore_ascii_case("cmo") {
1158        return Some(compute_cmo_batch(req, output_id.unwrap_or("value")));
1159    }
1160    if id.eq_ignore_ascii_case("fosc") {
1161        return Some(compute_fosc_batch(req, output_id.unwrap_or("value")));
1162    }
1163    if id.eq_ignore_ascii_case("emv") {
1164        return Some(compute_emv_batch(req, output_id.unwrap_or("value")));
1165    }
1166    if id.eq_ignore_ascii_case("cfo") {
1167        return Some(compute_cfo_batch(req, output_id.unwrap_or("value")));
1168    }
1169    if id.eq_ignore_ascii_case("ehlers_adaptive_cg") {
1170        return Some(compute_ehlers_adaptive_cg_batch(
1171            req,
1172            output_id.unwrap_or("cg"),
1173        ));
1174    }
1175    if id.eq_ignore_ascii_case("adaptive_momentum_oscillator") {
1176        return Some(compute_adaptive_momentum_oscillator_batch(
1177            req,
1178            output_id.unwrap_or("amo"),
1179        ));
1180    }
1181    if id.eq_ignore_ascii_case("adaptive_macd") {
1182        return Some(compute_adaptive_macd_batch(
1183            req,
1184            output_id.unwrap_or("macd"),
1185        ));
1186    }
1187    if id.eq_ignore_ascii_case("linear_correlation_oscillator") {
1188        return Some(compute_linear_correlation_oscillator_batch(
1189            req,
1190            output_id.unwrap_or("value"),
1191        ));
1192    }
1193    if id.eq_ignore_ascii_case("polynomial_regression_extrapolation") {
1194        return Some(compute_polynomial_regression_extrapolation_batch(
1195            req,
1196            output_id.unwrap_or("value"),
1197        ));
1198    }
1199    if id.eq_ignore_ascii_case("statistical_trailing_stop") {
1200        return Some(compute_statistical_trailing_stop_batch(
1201            req,
1202            output_id.unwrap_or("level"),
1203        ));
1204    }
1205    if id.eq_ignore_ascii_case("supertrend_recovery") {
1206        return Some(compute_supertrend_recovery_batch(
1207            req,
1208            output_id.unwrap_or("band"),
1209        ));
1210    }
1211    if id.eq_ignore_ascii_case("standardized_psar_oscillator") {
1212        return Some(compute_standardized_psar_oscillator_batch(
1213            req,
1214            output_id.unwrap_or("oscillator"),
1215        ));
1216    }
1217    if id.eq_ignore_ascii_case("geometric_bias_oscillator") {
1218        return Some(compute_geometric_bias_oscillator_batch(
1219            req,
1220            output_id.unwrap_or("value"),
1221        ));
1222    }
1223    if id.eq_ignore_ascii_case("lrsi") {
1224        return Some(compute_lrsi_batch(req, output_id.unwrap_or("value")));
1225    }
1226    if id.eq_ignore_ascii_case("nvi") {
1227        return Some(compute_nvi_batch(req, output_id.unwrap_or("value")));
1228    }
1229    if id.eq_ignore_ascii_case("mom") {
1230        return Some(compute_mom_batch(req, output_id.unwrap_or("value")));
1231    }
1232    if id.eq_ignore_ascii_case("velocity") {
1233        return Some(compute_velocity_batch(req, output_id.unwrap_or("value")));
1234    }
1235    if id.eq_ignore_ascii_case("normalized_volume_true_range") {
1236        return Some(compute_normalized_volume_true_range_batch(
1237            req,
1238            output_id.unwrap_or("normalized_volume"),
1239        ));
1240    }
1241    if id.eq_ignore_ascii_case("exponential_trend") {
1242        return Some(compute_exponential_trend_batch(
1243            req,
1244            output_id.unwrap_or("uptrend_base"),
1245        ));
1246    }
1247    if id.eq_ignore_ascii_case("trend_flow_trail") {
1248        return Some(compute_trend_flow_trail_batch(
1249            req,
1250            output_id.unwrap_or("alpha_trail"),
1251        ));
1252    }
1253    if id.eq_ignore_ascii_case("range_breakout_signals") {
1254        return Some(compute_range_breakout_signals_batch(
1255            req,
1256            output_id.unwrap_or("range_top"),
1257        ));
1258    }
1259    if id.eq_ignore_ascii_case("vi") {
1260        if let Some(out) = output_id {
1261            return Some(compute_vi_batch(req, out));
1262        }
1263        return None;
1264    }
1265    if id.eq_ignore_ascii_case("wto") {
1266        if let Some(out) = output_id {
1267            return Some(compute_wto_batch(req, out));
1268        }
1269        return None;
1270    }
1271    if id.eq_ignore_ascii_case("rogers_satchell_volatility") {
1272        if let Some(out) = output_id {
1273            return Some(compute_rogers_satchell_volatility_batch(req, out));
1274        }
1275        return None;
1276    }
1277    if id.eq_ignore_ascii_case("historical_volatility_rank") {
1278        if let Some(out) = output_id {
1279            return Some(compute_historical_volatility_rank_batch(req, out));
1280        }
1281        return None;
1282    }
1283    if id.eq_ignore_ascii_case("dual_ulcer_index") {
1284        if let Some(out) = output_id {
1285            return Some(compute_dual_ulcer_index_batch(req, out));
1286        }
1287        return None;
1288    }
1289    if id.eq_ignore_ascii_case("fractal_dimension_index") {
1290        if let Some(out) = output_id {
1291            return Some(compute_fractal_dimension_index_batch(req, out));
1292        }
1293        return None;
1294    }
1295    if id.eq_ignore_ascii_case("volume_weighted_rsi") {
1296        if let Some(out) = output_id {
1297            return Some(compute_volume_weighted_rsi_batch(req, out));
1298        }
1299        return None;
1300    }
1301    if id.eq_ignore_ascii_case("dynamic_momentum_index") {
1302        if let Some(out) = output_id {
1303            return Some(compute_dynamic_momentum_index_batch(req, out));
1304        }
1305        return None;
1306    }
1307    if id.eq_ignore_ascii_case("disparity_index") {
1308        if let Some(out) = output_id {
1309            return Some(compute_disparity_index_batch(req, out));
1310        }
1311        return None;
1312    }
1313    if id.eq_ignore_ascii_case("donchian_channel_width") {
1314        if let Some(out) = output_id {
1315            return Some(compute_donchian_channel_width_batch(req, out));
1316        }
1317        return None;
1318    }
1319    if id.eq_ignore_ascii_case("kairi_relative_index") {
1320        if let Some(out) = output_id {
1321            return Some(compute_kairi_relative_index_batch(req, out));
1322        }
1323        return None;
1324    }
1325    if id.eq_ignore_ascii_case("projection_oscillator") {
1326        if let Some(out) = output_id {
1327            return Some(compute_projection_oscillator_batch(req, out));
1328        }
1329        return None;
1330    }
1331    if id.eq_ignore_ascii_case("market_structure_trailing_stop") {
1332        if let Some(out) = output_id {
1333            return Some(compute_market_structure_trailing_stop_batch(req, out));
1334        }
1335        return None;
1336    }
1337    if id.eq_ignore_ascii_case("emd_trend") {
1338        if let Some(out) = output_id {
1339            return Some(compute_emd_trend_batch(req, out));
1340        }
1341        return None;
1342    }
1343    if id.eq_ignore_ascii_case("cyberpunk_value_trend_analyzer") {
1344        if let Some(out) = output_id {
1345            return Some(compute_cyberpunk_value_trend_analyzer_batch(req, out));
1346        }
1347        return None;
1348    }
1349    if id.eq_ignore_ascii_case("evasive_supertrend") {
1350        if let Some(out) = output_id {
1351            return Some(compute_evasive_supertrend_batch(req, out));
1352        }
1353        return None;
1354    }
1355    if id.eq_ignore_ascii_case("reversal_signals") {
1356        if let Some(out) = output_id {
1357            return Some(compute_reversal_signals_batch(req, out));
1358        }
1359        return None;
1360    }
1361    if id.eq_ignore_ascii_case("zig_zag_channels") {
1362        if let Some(out) = output_id {
1363            return Some(compute_zig_zag_channels_batch(req, out));
1364        }
1365        return None;
1366    }
1367    if id.eq_ignore_ascii_case("directional_imbalance_index") {
1368        if let Some(out) = output_id {
1369            return Some(compute_directional_imbalance_index_batch(req, out));
1370        }
1371        return None;
1372    }
1373    if id.eq_ignore_ascii_case("candle_strength_oscillator") {
1374        if let Some(out) = output_id {
1375            return Some(compute_candle_strength_oscillator_batch(req, out));
1376        }
1377        return None;
1378    }
1379    if id.eq_ignore_ascii_case("gmma_oscillator") {
1380        if let Some(out) = output_id {
1381            return Some(compute_gmma_oscillator_batch(req, out));
1382        }
1383        return None;
1384    }
1385    if id.eq_ignore_ascii_case("nonlinear_regression_zero_lag_moving_average") {
1386        if let Some(out) = output_id {
1387            return Some(compute_nonlinear_regression_zero_lag_moving_average_batch(
1388                req, out,
1389            ));
1390        }
1391        return None;
1392    }
1393    if id.eq_ignore_ascii_case("autocorrelation_indicator") {
1394        if let Some(out) = output_id {
1395            return Some(compute_autocorrelation_indicator_batch(req, out));
1396        }
1397        return None;
1398    }
1399    if id.eq_ignore_ascii_case("goertzel_cycle_composite_wave") {
1400        if let Some(out) = output_id {
1401            return Some(compute_goertzel_cycle_composite_wave_batch(req, out));
1402        }
1403        return None;
1404    }
1405    if id.eq_ignore_ascii_case("rolling_skewness_kurtosis") {
1406        if let Some(out) = output_id {
1407            return Some(compute_rolling_skewness_kurtosis_batch(req, out));
1408        }
1409        return None;
1410    }
1411    if id.eq_ignore_ascii_case("rolling_z_score_trend") {
1412        if let Some(out) = output_id {
1413            return Some(compute_rolling_z_score_trend_batch(req, out));
1414        }
1415        return None;
1416    }
1417    if id.eq_ignore_ascii_case("ehlers_data_sampling_relative_strength_indicator") {
1418        if let Some(out) = output_id {
1419            return Some(compute_ehlers_data_sampling_relative_strength_indicator_batch(req, out));
1420        }
1421        return None;
1422    }
1423    if id.eq_ignore_ascii_case("velocity_acceleration_convergence_divergence_indicator") {
1424        if let Some(out) = output_id {
1425            return Some(
1426                compute_velocity_acceleration_convergence_divergence_indicator_batch(req, out),
1427            );
1428        }
1429        return None;
1430    }
1431    if id.eq_ignore_ascii_case("trend_direction_force_index") {
1432        if let Some(out) = output_id {
1433            return Some(compute_trend_direction_force_index_batch(req, out));
1434        }
1435        return None;
1436    }
1437    if id.eq_ignore_ascii_case("yang_zhang_volatility") {
1438        if let Some(out) = output_id {
1439            return Some(compute_yang_zhang_volatility_batch(req, out));
1440        }
1441        return None;
1442    }
1443    if id.eq_ignore_ascii_case("garman_klass_volatility") {
1444        return Some(compute_garman_klass_volatility_batch(
1445            req,
1446            output_id.unwrap_or("value"),
1447        ));
1448    }
1449    if id.eq_ignore_ascii_case("advance_decline_line") {
1450        return Some(compute_advance_decline_line_batch(
1451            req,
1452            output_id.unwrap_or("value"),
1453        ));
1454    }
1455    if id.eq_ignore_ascii_case("decisionpoint_breadth_swenlin_trading_oscillator") {
1456        return Some(
1457            compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(
1458                req,
1459                output_id.unwrap_or("value"),
1460            ),
1461        );
1462    }
1463    if id.eq_ignore_ascii_case("velocity_acceleration_indicator") {
1464        return Some(compute_velocity_acceleration_indicator_batch(
1465            req,
1466            output_id.unwrap_or("value"),
1467        ));
1468    }
1469    if id.eq_ignore_ascii_case("normalized_resonator") {
1470        return Some(compute_normalized_resonator_batch(
1471            req,
1472            output_id.unwrap_or("oscillator"),
1473        ));
1474    }
1475    if id.eq_ignore_ascii_case("monotonicity_index") {
1476        return Some(compute_monotonicity_index_batch(
1477            req,
1478            output_id.unwrap_or("index"),
1479        ));
1480    }
1481    if id.eq_ignore_ascii_case("half_causal_estimator") {
1482        return Some(compute_half_causal_estimator_batch(
1483            req,
1484            output_id.unwrap_or("estimate"),
1485        ));
1486    }
1487    if id.eq_ignore_ascii_case("atr_percentile") {
1488        return Some(compute_atr_percentile_batch(
1489            req,
1490            output_id.unwrap_or("value"),
1491        ));
1492    }
1493    if id.eq_ignore_ascii_case("bull_power_vs_bear_power") {
1494        return Some(compute_bull_power_vs_bear_power_batch(
1495            req,
1496            output_id.unwrap_or("value"),
1497        ));
1498    }
1499    if id.eq_ignore_ascii_case("didi_index") {
1500        return Some(compute_didi_index_batch(req, output_id.unwrap_or("short")));
1501    }
1502    if id.eq_ignore_ascii_case("ehlers_autocorrelation_periodogram") {
1503        return Some(compute_ehlers_autocorrelation_periodogram_batch(
1504            req,
1505            output_id.unwrap_or("dominant_cycle"),
1506        ));
1507    }
1508    if id.eq_ignore_ascii_case("ehlers_linear_extrapolation_predictor") {
1509        return Some(compute_ehlers_linear_extrapolation_predictor_batch(
1510            req,
1511            output_id.unwrap_or("prediction"),
1512        ));
1513    }
1514    if id.eq_ignore_ascii_case("kase_peak_oscillator_with_divergences") {
1515        return Some(compute_kase_peak_oscillator_with_divergences_batch(
1516            req,
1517            output_id.unwrap_or("oscillator"),
1518        ));
1519    }
1520    if id.eq_ignore_ascii_case("absolute_strength_index_oscillator") {
1521        return Some(compute_absolute_strength_index_oscillator_batch(
1522            req,
1523            output_id.unwrap_or("oscillator"),
1524        ));
1525    }
1526    if id.eq_ignore_ascii_case("adaptive_bandpass_trigger_oscillator") {
1527        return Some(compute_adaptive_bandpass_trigger_oscillator_batch(
1528            req,
1529            output_id.unwrap_or("in_phase"),
1530        ));
1531    }
1532    if id.eq_ignore_ascii_case("premier_rsi_oscillator") {
1533        return Some(compute_premier_rsi_oscillator_batch(
1534            req,
1535            output_id.unwrap_or("value"),
1536        ));
1537    }
1538    if id.eq_ignore_ascii_case("multi_length_stochastic_average") {
1539        return Some(compute_multi_length_stochastic_average_batch(
1540            req,
1541            output_id.unwrap_or("value"),
1542        ));
1543    }
1544    if id.eq_ignore_ascii_case("hull_butterfly_oscillator") {
1545        return Some(compute_hull_butterfly_oscillator_batch(
1546            req,
1547            output_id.unwrap_or("oscillator"),
1548        ));
1549    }
1550    if id.eq_ignore_ascii_case("fibonacci_trailing_stop") {
1551        return Some(compute_fibonacci_trailing_stop_batch(
1552            req,
1553            output_id.unwrap_or("trailing_stop"),
1554        ));
1555    }
1556    if id.eq_ignore_ascii_case("fibonacci_entry_bands") {
1557        return Some(compute_fibonacci_entry_bands_batch(
1558            req,
1559            output_id.unwrap_or("middle"),
1560        ));
1561    }
1562    if id.eq_ignore_ascii_case("volume_energy_reservoirs") {
1563        return Some(compute_volume_energy_reservoirs_batch(
1564            req,
1565            output_id.unwrap_or("momentum"),
1566        ));
1567    }
1568    if id.eq_ignore_ascii_case("neighboring_trailing_stop") {
1569        return Some(compute_neighboring_trailing_stop_batch(
1570            req,
1571            output_id.unwrap_or("trailing_stop"),
1572        ));
1573    }
1574    if id.eq_ignore_ascii_case("grover_llorens_cycle_oscillator") {
1575        return Some(compute_grover_llorens_cycle_oscillator_batch(
1576            req,
1577            output_id.unwrap_or("value"),
1578        ));
1579    }
1580    if id.eq_ignore_ascii_case("historical_volatility") {
1581        return Some(compute_historical_volatility_batch(
1582            req,
1583            output_id.unwrap_or("value"),
1584        ));
1585    }
1586    if id.eq_ignore_ascii_case("squeeze_index") {
1587        return Some(compute_squeeze_index_batch(
1588            req,
1589            output_id.unwrap_or("value"),
1590        ));
1591    }
1592    if id.eq_ignore_ascii_case("stochastic_distance") {
1593        return Some(compute_stochastic_distance_batch(
1594            req,
1595            output_id.unwrap_or("oscillator"),
1596        ));
1597    }
1598    if id.eq_ignore_ascii_case("vertical_horizontal_filter") {
1599        return Some(compute_vertical_horizontal_filter_batch(
1600            req,
1601            output_id.unwrap_or("value"),
1602        ));
1603    }
1604    if id.eq_ignore_ascii_case("intraday_momentum_index") {
1605        if let Some(out) = output_id {
1606            return Some(compute_intraday_momentum_index_batch(req, out));
1607        }
1608    }
1609    if id.eq_ignore_ascii_case("vwap_zscore_with_signals") {
1610        if let Some(out) = output_id {
1611            return Some(compute_vwap_zscore_with_signals_batch(req, out));
1612        }
1613    }
1614    if id.eq_ignore_ascii_case("macd_wave_signal_pro") {
1615        if let Some(out) = output_id {
1616            return Some(compute_macd_wave_signal_pro_batch(req, out));
1617        }
1618    }
1619    if id.eq_ignore_ascii_case("hema_trend_levels") {
1620        if let Some(out) = output_id {
1621            return Some(compute_hema_trend_levels_batch(req, out));
1622        }
1623    }
1624    if id.eq_ignore_ascii_case("demand_index") {
1625        if let Some(out) = output_id {
1626            return Some(compute_demand_index_batch(req, out));
1627        }
1628    }
1629    if id.eq_ignore_ascii_case("gopalakrishnan_range_index") {
1630        return Some(compute_gopalakrishnan_range_index_batch(
1631            req,
1632            output_id.unwrap_or("value"),
1633        ));
1634    }
1635    if id.eq_ignore_ascii_case("voss") {
1636        if let Some(out) = output_id {
1637            return Some(compute_voss_batch(req, out));
1638        }
1639        return None;
1640    }
1641    if id.eq_ignore_ascii_case("acosc") {
1642        if let Some(out) = output_id {
1643            return Some(compute_acosc_batch(req, out));
1644        }
1645        return None;
1646    }
1647
1648    None
1649}
1650
1651fn dispatch_cpu_batch_by_indicator(
1652    req: IndicatorBatchRequest<'_>,
1653    indicator_id: &str,
1654    output_id: &str,
1655) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
1656    if indicator_id.eq_ignore_ascii_case("logarithmic_moving_average") {
1657        return compute_logarithmic_moving_average_batch(req, output_id);
1658    }
1659    if is_moving_average(indicator_id) {
1660        if let Some(info) = get_indicator(indicator_id) {
1661            return compute_ma_batch(req, info, output_id);
1662        }
1663    }
1664    match indicator_id {
1665        "accumulation_swing_index" => compute_accumulation_swing_index_batch(req, output_id),
1666        "ad" => compute_ad_batch(req, output_id),
1667        "adosc" => compute_adosc_batch(req, output_id),
1668        "ao" => compute_ao_batch(req, output_id),
1669        "emv" => compute_emv_batch(req, output_id),
1670        "efi" => compute_efi_batch(req, output_id),
1671        "mfi" => compute_mfi_batch(req, output_id),
1672        "mass" => compute_mass_batch(req, output_id),
1673        "kvo" => compute_kvo_batch(req, output_id),
1674        "vosc" => compute_vosc_batch(req, output_id),
1675        "wad" => compute_wad_batch(req, output_id),
1676        "dx" => compute_dx_batch(req, output_id),
1677        "fosc" => compute_fosc_batch(req, output_id),
1678        "ift_rsi" => compute_ift_rsi_batch(req, output_id),
1679        "linearreg_angle" => compute_linearreg_angle_batch(req, output_id),
1680        "linearreg_intercept" => compute_linearreg_intercept_batch(req, output_id),
1681        "linearreg_slope" => compute_linearreg_slope_batch(req, output_id),
1682        "cg" => compute_cg_batch(req, output_id),
1683        "rsi" => compute_rsi_batch(req, output_id),
1684        "roc" => compute_roc_batch(req, output_id),
1685        "apo" => compute_apo_batch(req, output_id),
1686        "bop" => compute_bop_batch(req, output_id),
1687        "bulls_v_bears" => compute_bulls_v_bears_batch(req, output_id),
1688        "cci" => compute_cci_batch(req, output_id),
1689        "cci_cycle" => compute_cci_cycle_batch(req, output_id),
1690        "cfo" => compute_cfo_batch(req, output_id),
1691        "cycle_channel_oscillator" => compute_cycle_channel_oscillator_batch(req, output_id),
1692        "daily_factor" => compute_daily_factor_batch(req, output_id),
1693        "ehlers_adaptive_cg" => compute_ehlers_adaptive_cg_batch(req, output_id),
1694        "ehlers_adaptive_cyber_cycle" => compute_ehlers_adaptive_cyber_cycle_batch(req, output_id),
1695        "adaptive_schaff_trend_cycle" => compute_adaptive_schaff_trend_cycle_batch(req, output_id),
1696        "adaptive_momentum_oscillator" => {
1697            compute_adaptive_momentum_oscillator_batch(req, output_id)
1698        }
1699        "adaptive_macd" => compute_adaptive_macd_batch(req, output_id),
1700        "linear_correlation_oscillator" => {
1701            compute_linear_correlation_oscillator_batch(req, output_id)
1702        }
1703        "polynomial_regression_extrapolation" => {
1704            compute_polynomial_regression_extrapolation_batch(req, output_id)
1705        }
1706        "statistical_trailing_stop" => compute_statistical_trailing_stop_batch(req, output_id),
1707        "supertrend_recovery" => compute_supertrend_recovery_batch(req, output_id),
1708        "standardized_psar_oscillator" => {
1709            compute_standardized_psar_oscillator_batch(req, output_id)
1710        }
1711        "geometric_bias_oscillator" => compute_geometric_bias_oscillator_batch(req, output_id),
1712        "vdubus_divergence_wave_pattern_generator" => {
1713            compute_vdubus_divergence_wave_pattern_generator_batch(req, output_id)
1714        }
1715        "lrsi" => compute_lrsi_batch(req, output_id),
1716        "er" => compute_er_batch(req, output_id),
1717        "kurtosis" => compute_kurtosis_batch(req, output_id),
1718        "natr" => compute_natr_batch(req, output_id),
1719        "net_myrsi" => compute_net_myrsi_batch(req, output_id),
1720        "mean_ad" => compute_mean_ad_batch(req, output_id),
1721        "medium_ad" => compute_medium_ad_batch(req, output_id),
1722        "deviation" => compute_deviation_batch(req, output_id),
1723        "dpo" => compute_dpo_batch(req, output_id),
1724        "pfe" => compute_pfe_batch(req, output_id),
1725        "ehlers_detrending_filter" => compute_ehlers_detrending_filter_batch(req, output_id),
1726        "ehlers_fm_demodulator" => compute_ehlers_fm_demodulator_batch(req, output_id),
1727        "ehlers_simple_cycle_indicator" => {
1728            compute_ehlers_simple_cycle_indicator_batch(req, output_id)
1729        }
1730        "ehlers_smoothed_adaptive_momentum" => {
1731            compute_ehlers_smoothed_adaptive_momentum_batch(req, output_id)
1732        }
1733        "ewma_volatility" => compute_ewma_volatility_batch(req, output_id),
1734        "qstick" => compute_qstick_batch(req, output_id),
1735        "reverse_rsi" => compute_reverse_rsi_batch(req, output_id),
1736        "percentile_nearest_rank" => compute_percentile_nearest_rank_batch(req, output_id),
1737        "obv" => compute_obv_batch(req, output_id),
1738        "on_balance_volume_oscillator" => {
1739            compute_on_balance_volume_oscillator_batch(req, output_id)
1740        }
1741        "vpt" => compute_vpt_batch(req, output_id),
1742        "nvi" => compute_nvi_batch(req, output_id),
1743        "pvi" => compute_pvi_batch(req, output_id),
1744        "wclprice" => compute_wclprice_batch(req, output_id),
1745        "ui" => compute_ui_batch(req, output_id),
1746        "zscore" => compute_zscore_batch(req, output_id),
1747        "medprice" => compute_medprice_batch(req, output_id),
1748        "midpoint" => compute_midpoint_batch(req, output_id),
1749        "midprice" => compute_midprice_batch(req, output_id),
1750        "mom" => compute_mom_batch(req, output_id),
1751        "velocity" => compute_velocity_batch(req, output_id),
1752        "normalized_volume_true_range" => {
1753            compute_normalized_volume_true_range_batch(req, output_id)
1754        }
1755        "exponential_trend" => compute_exponential_trend_batch(req, output_id),
1756        "trend_flow_trail" => compute_trend_flow_trail_batch(req, output_id),
1757        "range_breakout_signals" => compute_range_breakout_signals_batch(req, output_id),
1758        "cmo" => compute_cmo_batch(req, output_id),
1759        "rocp" => compute_rocp_batch(req, output_id),
1760        "rocr" => compute_rocr_batch(req, output_id),
1761        "ppo" => compute_ppo_batch(req, output_id),
1762        "tsf" => compute_tsf_batch(req, output_id),
1763        "trix" => compute_trix_batch(req, output_id),
1764        "tsi" => compute_tsi_batch(req, output_id),
1765        "var" => compute_var_batch(req, output_id),
1766        "stddev" => compute_stddev_batch(req, output_id),
1767        "willr" => compute_willr_batch(req, output_id),
1768        "ultosc" => compute_ultosc_batch(req, output_id),
1769        "adx" => compute_adx_batch(req, output_id),
1770        "adxr" => compute_adxr_batch(req, output_id),
1771        "atr" => compute_atr_batch(req, output_id),
1772        "macd" => compute_macd_batch(req, output_id),
1773        "bollinger_bands" => compute_bollinger_batch(req, output_id),
1774        "bollinger_bands_width" => compute_bbw_batch(req, output_id),
1775        "stoch" => compute_stoch_batch(req, output_id),
1776        "stochf" => compute_stochf_batch(req, output_id),
1777        "stochastic_money_flow_index" => compute_stochastic_money_flow_index_batch(req, output_id),
1778        "vwmacd" => compute_vwmacd_batch(req, output_id),
1779        "vpci" => compute_vpci_batch(req, output_id),
1780        "ttm_trend" => compute_ttm_trend_batch(req, output_id),
1781        "ttm_squeeze" => compute_ttm_squeeze_batch(req, output_id),
1782        "aroon" => compute_aroon_batch(req, output_id),
1783        "aroonosc" => compute_aroonosc_batch(req, output_id),
1784        "di" => compute_di_batch(req, output_id),
1785        "dm" => compute_dm_batch(req, output_id),
1786        "dti" => compute_dti_batch(req, output_id),
1787        "donchian" => compute_donchian_batch(req, output_id),
1788        "kdj" => compute_kdj_batch(req, output_id),
1789        "keltner" => compute_keltner_batch(req, output_id),
1790        "squeeze_momentum" => compute_squeeze_momentum_batch(req, output_id),
1791        "srsi" => compute_srsi_batch(req, output_id),
1792        "supertrend" => compute_supertrend_batch(req, output_id),
1793        "adjustable_ma_alternating_extremities" => {
1794            compute_adjustable_ma_alternating_extremities_batch(req, output_id)
1795        }
1796        "vi" => compute_vi_batch(req, output_id),
1797        "wavetrend" => compute_wavetrend_batch(req, output_id),
1798        "wto" => compute_wto_batch(req, output_id),
1799        "rogers_satchell_volatility" => compute_rogers_satchell_volatility_batch(req, output_id),
1800        "historical_volatility_percentile" => {
1801            compute_historical_volatility_percentile_batch(req, output_id)
1802        }
1803        "historical_volatility_rank" => compute_historical_volatility_rank_batch(req, output_id),
1804        "dual_ulcer_index" => compute_dual_ulcer_index_batch(req, output_id),
1805        "fractal_dimension_index" => compute_fractal_dimension_index_batch(req, output_id),
1806        "ichimoku_oscillator" => compute_ichimoku_oscillator_batch(req, output_id),
1807        "volume_weighted_rsi" => compute_volume_weighted_rsi_batch(req, output_id),
1808        "dynamic_momentum_index" => compute_dynamic_momentum_index_batch(req, output_id),
1809        "disparity_index" => compute_disparity_index_batch(req, output_id),
1810        "donchian_channel_width" => compute_donchian_channel_width_batch(req, output_id),
1811        "kairi_relative_index" => compute_kairi_relative_index_batch(req, output_id),
1812        "projection_oscillator" => compute_projection_oscillator_batch(req, output_id),
1813        "market_structure_trailing_stop" => {
1814            compute_market_structure_trailing_stop_batch(req, output_id)
1815        }
1816        "emd_trend" => compute_emd_trend_batch(req, output_id),
1817        "cyberpunk_value_trend_analyzer" => {
1818            compute_cyberpunk_value_trend_analyzer_batch(req, output_id)
1819        }
1820        "evasive_supertrend" => compute_evasive_supertrend_batch(req, output_id),
1821        "reversal_signals" => compute_reversal_signals_batch(req, output_id),
1822        "zig_zag_channels" => compute_zig_zag_channels_batch(req, output_id),
1823        "directional_imbalance_index" => compute_directional_imbalance_index_batch(req, output_id),
1824        "candle_strength_oscillator" => compute_candle_strength_oscillator_batch(req, output_id),
1825        "gmma_oscillator" => compute_gmma_oscillator_batch(req, output_id),
1826        "nonlinear_regression_zero_lag_moving_average" => {
1827            compute_nonlinear_regression_zero_lag_moving_average_batch(req, output_id)
1828        }
1829        "possible_rsi" => compute_possible_rsi_batch(req, output_id),
1830        "autocorrelation_indicator" => compute_autocorrelation_indicator_batch(req, output_id),
1831        "goertzel_cycle_composite_wave" => {
1832            compute_goertzel_cycle_composite_wave_batch(req, output_id)
1833        }
1834        "rolling_skewness_kurtosis" => compute_rolling_skewness_kurtosis_batch(req, output_id),
1835        "rolling_z_score_trend" => compute_rolling_z_score_trend_batch(req, output_id),
1836        "ehlers_data_sampling_relative_strength_indicator" => {
1837            compute_ehlers_data_sampling_relative_strength_indicator_batch(req, output_id)
1838        }
1839        "velocity_acceleration_convergence_divergence_indicator" => {
1840            compute_velocity_acceleration_convergence_divergence_indicator_batch(req, output_id)
1841        }
1842        "trend_direction_force_index" => compute_trend_direction_force_index_batch(req, output_id),
1843        "yang_zhang_volatility" => compute_yang_zhang_volatility_batch(req, output_id),
1844        "garman_klass_volatility" => compute_garman_klass_volatility_batch(req, output_id),
1845        "advance_decline_line" => compute_advance_decline_line_batch(req, output_id),
1846        "decisionpoint_breadth_swenlin_trading_oscillator" => {
1847            compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(req, output_id)
1848        }
1849        "velocity_acceleration_indicator" => {
1850            compute_velocity_acceleration_indicator_batch(req, output_id)
1851        }
1852        "normalized_resonator" => compute_normalized_resonator_batch(req, output_id),
1853        "monotonicity_index" => compute_monotonicity_index_batch(req, output_id),
1854        "half_causal_estimator" => compute_half_causal_estimator_batch(req, output_id),
1855        "atr_percentile" => compute_atr_percentile_batch(req, output_id),
1856        "andean_oscillator" => compute_andean_oscillator_batch(req, output_id),
1857        "bull_power_vs_bear_power" => compute_bull_power_vs_bear_power_batch(req, output_id),
1858        "didi_index" => compute_didi_index_batch(req, output_id),
1859        "ehlers_autocorrelation_periodogram" => {
1860            compute_ehlers_autocorrelation_periodogram_batch(req, output_id)
1861        }
1862        "ehlers_linear_extrapolation_predictor" => {
1863            compute_ehlers_linear_extrapolation_predictor_batch(req, output_id)
1864        }
1865        "absolute_strength_index_oscillator" => {
1866            compute_absolute_strength_index_oscillator_batch(req, output_id)
1867        }
1868        "adaptive_bandpass_trigger_oscillator" => {
1869            compute_adaptive_bandpass_trigger_oscillator_batch(req, output_id)
1870        }
1871        "premier_rsi_oscillator" => compute_premier_rsi_oscillator_batch(req, output_id),
1872        "multi_length_stochastic_average" => {
1873            compute_multi_length_stochastic_average_batch(req, output_id)
1874        }
1875        "hull_butterfly_oscillator" => compute_hull_butterfly_oscillator_batch(req, output_id),
1876        "fibonacci_trailing_stop" => compute_fibonacci_trailing_stop_batch(req, output_id),
1877        "fibonacci_entry_bands" => compute_fibonacci_entry_bands_batch(req, output_id),
1878        "volume_energy_reservoirs" => compute_volume_energy_reservoirs_batch(req, output_id),
1879        "neighboring_trailing_stop" => compute_neighboring_trailing_stop_batch(req, output_id),
1880        "grover_llorens_cycle_oscillator" => {
1881            compute_grover_llorens_cycle_oscillator_batch(req, output_id)
1882        }
1883        "historical_volatility" => compute_historical_volatility_batch(req, output_id),
1884        "hypertrend" => compute_hypertrend_batch(req, output_id),
1885        "ict_propulsion_block" => compute_ict_propulsion_block_batch(req, output_id),
1886        "impulse_macd" => compute_impulse_macd_batch(req, output_id),
1887        "l1_ehlers_phasor" => compute_l1_ehlers_phasor_batch(req, output_id),
1888        "l2_ehlers_signal_to_noise" => compute_l2_ehlers_signal_to_noise_batch(req, output_id),
1889        "keltner_channel_width_oscillator" => {
1890            compute_keltner_channel_width_oscillator_batch(req, output_id)
1891        }
1892        "leavitt_convolution_acceleration" => {
1893            compute_leavitt_convolution_acceleration_batch(req, output_id)
1894        }
1895        "linear_regression_intensity" => compute_linear_regression_intensity_batch(req, output_id),
1896        "market_meanness_index" => compute_market_meanness_index_batch(req, output_id),
1897        "mesa_stochastic_multi_length" => {
1898            compute_mesa_stochastic_multi_length_batch(req, output_id)
1899        }
1900        "moving_average_cross_probability" => {
1901            compute_moving_average_cross_probability_batch(req, output_id)
1902        }
1903        "momentum_ratio_oscillator" => compute_momentum_ratio_oscillator_batch(req, output_id),
1904        "parkinson_volatility" => compute_parkinson_volatility_batch(req, output_id),
1905        "price_moving_average_ratio_percentile" => {
1906            compute_price_moving_average_ratio_percentile_batch(req, output_id)
1907        }
1908        "pretty_good_oscillator" => compute_pretty_good_oscillator_batch(req, output_id),
1909        "price_density_market_noise" => compute_price_density_market_noise_batch(req, output_id),
1910        "psychological_line" => compute_psychological_line_batch(req, output_id),
1911        "random_walk_index" => compute_random_walk_index_batch(req, output_id),
1912        "rank_correlation_index" => compute_rank_correlation_index_batch(req, output_id),
1913        "relative_strength_index_wave_indicator" => {
1914            compute_relative_strength_index_wave_indicator_batch(req, output_id)
1915        }
1916        "regression_slope_oscillator" => compute_regression_slope_oscillator_batch(req, output_id),
1917        "squeeze_index" => compute_squeeze_index_batch(req, output_id),
1918        "smoothed_gaussian_trend_filter" => {
1919            compute_smoothed_gaussian_trend_filter_batch(req, output_id)
1920        }
1921        "smooth_theil_sen" => compute_smooth_theil_sen_batch(req, output_id),
1922        "spearman_correlation" => compute_spearman_correlation_batch(req, output_id),
1923        "stochastic_adaptive_d" => compute_stochastic_adaptive_d_batch(req, output_id),
1924        "stochastic_connors_rsi" => compute_stochastic_connors_rsi_batch(req, output_id),
1925        "stochastic_distance" => compute_stochastic_distance_batch(req, output_id),
1926        "supertrend_oscillator" => compute_supertrend_oscillator_batch(req, output_id),
1927        "trend_trigger_factor" => compute_trend_trigger_factor_batch(req, output_id),
1928        "trend_continuation_factor" => compute_trend_continuation_factor_batch(req, output_id),
1929        "twiggs_money_flow" => compute_twiggs_money_flow_batch(req, output_id),
1930        "vertical_horizontal_filter" => compute_vertical_horizontal_filter_batch(req, output_id),
1931        "intraday_momentum_index" => compute_intraday_momentum_index_batch(req, output_id),
1932        "volatility_quality_index" => compute_volatility_quality_index_batch(req, output_id),
1933        "volatility_ratio_adaptive_rsx" => {
1934            compute_volatility_ratio_adaptive_rsx_batch(req, output_id)
1935        }
1936        "volume_weighted_stochastic_rsi" => {
1937            compute_volume_weighted_stochastic_rsi_batch(req, output_id)
1938        }
1939        "volume_zone_oscillator" => compute_volume_zone_oscillator_batch(req, output_id),
1940        "vwap_deviation_oscillator" => compute_vwap_deviation_oscillator_batch(req, output_id),
1941        "vwap_zscore_with_signals" => compute_vwap_zscore_with_signals_batch(req, output_id),
1942        "macd_wave_signal_pro" => compute_macd_wave_signal_pro_batch(req, output_id),
1943        "hema_trend_levels" => compute_hema_trend_levels_batch(req, output_id),
1944        "demand_index" => compute_demand_index_batch(req, output_id),
1945        "kase_peak_oscillator_with_divergences" => {
1946            compute_kase_peak_oscillator_with_divergences_batch(req, output_id)
1947        }
1948        "gopalakrishnan_range_index" => compute_gopalakrishnan_range_index_batch(req, output_id),
1949        "acosc" => compute_acosc_batch(req, output_id),
1950        "alligator" => compute_alligator_batch(req, output_id),
1951        "alphatrend" => compute_alphatrend_batch(req, output_id),
1952        "aso" => compute_aso_batch(req, output_id),
1953        "avsl" => compute_avsl_batch(req, output_id),
1954        "bandpass" => compute_bandpass_batch(req, output_id),
1955        "chande" => compute_chande_batch(req, output_id),
1956        "chandelier_exit" => compute_chandelier_exit_batch(req, output_id),
1957        "cksp" => compute_cksp_batch(req, output_id),
1958        "coppock" => compute_coppock_batch(req, output_id),
1959        "correl_hl" => compute_correl_hl_batch(req, output_id),
1960        "correlation_cycle" => compute_correlation_cycle_batch(req, output_id),
1961        "damiani_volatmeter" => compute_damiani_volatmeter_batch(req, output_id),
1962        "dvdiqqe" => compute_dvdiqqe_batch(req, output_id),
1963        "emd" => compute_emd_batch(req, output_id),
1964        "eri" => compute_eri_batch(req, output_id),
1965        "fisher" => compute_fisher_batch(req, output_id),
1966        "fvg_positioning_average" => compute_fvg_positioning_average_batch(req, output_id),
1967        "fvg_trailing_stop" => compute_fvg_trailing_stop_batch(req, output_id),
1968        "gatorosc" => compute_gatorosc_batch(req, output_id),
1969        "halftrend" => compute_halftrend_batch(req, output_id),
1970        "kaufmanstop" => compute_kaufmanstop_batch(req, output_id),
1971        "kst" => compute_kst_batch(req, output_id),
1972        "lpc" => compute_lpc_batch(req, output_id),
1973        "mab" => compute_mab_batch(req, output_id),
1974        "macz" => compute_macz_batch(req, output_id),
1975        "minmax" => compute_minmax_batch(req, output_id),
1976        "mod_god_mode" => compute_mod_god_mode_batch(req, output_id),
1977        "msw" => compute_msw_batch(req, output_id),
1978        "nadaraya_watson_envelope" => compute_nadaraya_watson_envelope_batch(req, output_id),
1979        "otto" => compute_otto_batch(req, output_id),
1980        "vidya" => compute_vidya_batch(req, output_id),
1981        "vlma" => compute_vlma_batch(req, output_id),
1982        "pma" => compute_pma_batch(req, output_id),
1983        "prb" => compute_prb_batch(req, output_id),
1984        "qqe" => compute_qqe_batch(req, output_id),
1985        "forward_backward_exponential_oscillator" => {
1986            compute_forward_backward_exponential_oscillator_batch(req, output_id)
1987        }
1988        "qqe_weighted_oscillator" => compute_qqe_weighted_oscillator_batch(req, output_id),
1989        "market_structure_confluence" => compute_market_structure_confluence_batch(req, output_id),
1990        "range_filtered_trend_signals" => {
1991            compute_range_filtered_trend_signals_batch(req, output_id)
1992        }
1993        "range_oscillator" => compute_range_oscillator_batch(req, output_id),
1994        "volume_weighted_relative_strength_index" => {
1995            compute_volume_weighted_relative_strength_index_batch(req, output_id)
1996        }
1997        "range_filter" => compute_range_filter_batch(req, output_id),
1998        "rsmk" => compute_rsmk_batch(req, output_id),
1999        "voss" => compute_voss_batch(req, output_id),
2000        "stc" => compute_stc_batch(req, output_id),
2001        "rvi" => compute_rvi_batch(req, output_id),
2002        "safezonestop" => compute_safezonestop_batch(req, output_id),
2003        "devstop" => compute_devstop_batch(req, output_id),
2004        "chop" => compute_chop_batch(req, output_id),
2005        "pivot" => compute_pivot_batch(req, output_id),
2006        _ => Err(IndicatorDispatchError::UnsupportedCapability {
2007            indicator: indicator_id.to_string(),
2008            capability: "cpu_batch",
2009        }),
2010    }
2011}
2012
2013fn validate_input_kind_strict(
2014    indicator: &str,
2015    expected: IndicatorInputKind,
2016    data: IndicatorDataRef<'_>,
2017) -> Result<(), IndicatorDispatchError> {
2018    let expected = strict_expected_input_kind(indicator, expected);
2019    if indicator.eq_ignore_ascii_case("mod_god_mode") {
2020        let matches = matches!(
2021            data,
2022            IndicatorDataRef::Candles { .. }
2023                | IndicatorDataRef::Ohlc { .. }
2024                | IndicatorDataRef::Ohlcv { .. }
2025        );
2026        if matches {
2027            return Ok(());
2028        }
2029    }
2030    let matches = matches!(
2031        (expected, data),
2032        (IndicatorInputKind::Slice, IndicatorDataRef::Slice { .. })
2033            | (
2034                IndicatorInputKind::Candles,
2035                IndicatorDataRef::Candles { .. }
2036            )
2037            | (IndicatorInputKind::Ohlc, IndicatorDataRef::Ohlc { .. })
2038            | (IndicatorInputKind::Ohlcv, IndicatorDataRef::Ohlcv { .. })
2039            | (
2040                IndicatorInputKind::HighLow,
2041                IndicatorDataRef::HighLow { .. }
2042            )
2043            | (
2044                IndicatorInputKind::CloseVolume,
2045                IndicatorDataRef::CloseVolume { .. }
2046            )
2047    );
2048
2049    if matches {
2050        Ok(())
2051    } else {
2052        Err(IndicatorDispatchError::MissingRequiredInput {
2053            indicator: indicator.to_string(),
2054            input: expected,
2055        })
2056    }
2057}
2058
2059fn strict_expected_input_kind(indicator: &str, fallback: IndicatorInputKind) -> IndicatorInputKind {
2060    if indicator.eq_ignore_ascii_case("ao") {
2061        return IndicatorInputKind::Slice;
2062    }
2063    if indicator.eq_ignore_ascii_case("ttm_trend") {
2064        return IndicatorInputKind::Candles;
2065    }
2066    fallback
2067}
2068
2069fn normalize_output_token(value: &str) -> String {
2070    let mut normalized = String::with_capacity(value.len());
2071    for ch in value.chars() {
2072        if ch.is_ascii_alphanumeric() {
2073            normalized.push(ch.to_ascii_lowercase());
2074        }
2075    }
2076    if normalized == "values" {
2077        "value".to_string()
2078    } else {
2079        normalized
2080    }
2081}
2082
2083fn output_id_matches(candidate: &str, requested: &str) -> bool {
2084    candidate.eq_ignore_ascii_case(requested)
2085        || normalize_output_token(candidate) == normalize_output_token(requested)
2086}
2087
2088fn resolve_output_id<'a>(
2089    info: &'a IndicatorInfo,
2090    requested: Option<&str>,
2091) -> Result<&'a str, IndicatorDispatchError> {
2092    if info.outputs.is_empty() {
2093        return Err(IndicatorDispatchError::ComputeFailed {
2094            indicator: info.id.to_string(),
2095            details: "indicator has no registered outputs".to_string(),
2096        });
2097    }
2098
2099    if info.outputs.len() == 1 {
2100        let only = info.outputs[0].id;
2101        if let Some(req) = requested {
2102            if req == only {
2103                return Ok(only);
2104            }
2105            if !output_id_matches(only, req) {
2106                return Err(IndicatorDispatchError::UnknownOutput {
2107                    indicator: info.id.to_string(),
2108                    output: req.to_string(),
2109                });
2110            }
2111        }
2112        return Ok(only);
2113    }
2114
2115    let req = requested.ok_or_else(|| IndicatorDispatchError::InvalidParam {
2116        indicator: info.id.to_string(),
2117        key: "output_id".to_string(),
2118        reason: "output_id is required for multi-output indicators".to_string(),
2119    })?;
2120
2121    if let Some(out) = info.outputs.iter().find(|o| o.id == req) {
2122        return Ok(out.id);
2123    }
2124    info.outputs
2125        .iter()
2126        .find(|o| output_id_matches(o.id, req))
2127        .map(|o| o.id)
2128        .ok_or_else(|| IndicatorDispatchError::UnknownOutput {
2129            indicator: info.id.to_string(),
2130            output: req.to_string(),
2131        })
2132}
2133
2134fn is_moving_average(id: &str) -> bool {
2135    list_moving_averages()
2136        .iter()
2137        .any(|ma| ma.id.eq_ignore_ascii_case(id))
2138}
2139
2140fn ma_is_period_based(info: &IndicatorInfo) -> bool {
2141    info.params
2142        .iter()
2143        .any(|p| p.key.eq_ignore_ascii_case("period"))
2144}
2145
2146fn compute_ma_batch(
2147    req: IndicatorBatchRequest<'_>,
2148    info: &IndicatorInfo,
2149    output_id: &str,
2150) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2151    let data = ma_data_from_req(info.id, req.data)?;
2152    let cols = ma_len_from_req(info.id, req.data)?;
2153    let period_based = ma_is_period_based(info);
2154    if period_based {
2155        if let Some(out) = try_compute_ma_batch_fast(req, info, output_id, data.clone(), cols)? {
2156            return Ok(out);
2157        }
2158    }
2159    let rows = req.combos.len();
2160    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
2161
2162    for combo in req.combos {
2163        let period = ma_period_for_combo(info, combo.params)?;
2164        let mut params = convert_ma_params(combo.params, info.id, output_id)?;
2165        if info.outputs.len() > 1 && !has_key(combo.params, "output") {
2166            params.push(MaBatchParamKV {
2167                key: "output",
2168                value: MaBatchParamValue::EnumString(output_id),
2169            });
2170        }
2171        let out = ma_batch_with_kernel_and_typed_params(
2172            info.id,
2173            data.clone(),
2174            (period, period, 0),
2175            req.kernel,
2176            &params,
2177        )
2178        .map_err(|e| IndicatorDispatchError::ComputeFailed {
2179            indicator: info.id.to_string(),
2180            details: e.to_string(),
2181        })?;
2182        ensure_len(info.id, cols, out.cols)?;
2183        let row_values = if out.rows == 1 {
2184            out.values
2185        } else {
2186            reorder_or_take_f64_matrix_by_period(
2187                info.id,
2188                &[period],
2189                &out.periods,
2190                out.cols,
2191                out.values,
2192            )?
2193        };
2194        ensure_len(info.id, cols, row_values.len())?;
2195        matrix.extend_from_slice(&row_values);
2196    }
2197
2198    Ok(f64_output(output_id, rows, cols, matrix))
2199}
2200
2201fn try_compute_ma_batch_fast(
2202    req: IndicatorBatchRequest<'_>,
2203    info: &IndicatorInfo,
2204    output_id: &str,
2205    data: MaData<'_>,
2206    cols: usize,
2207) -> Result<Option<IndicatorBatchOutput>, IndicatorDispatchError> {
2208    if req.combos.is_empty() {
2209        return Ok(Some(f64_output(output_id, 0, cols, Vec::new())));
2210    }
2211    if !ma_is_period_based(info) {
2212        return Ok(None);
2213    }
2214
2215    let mut periods = Vec::with_capacity(req.combos.len());
2216    let mut shared_params: Option<Vec<MaBatchParamKV<'_>>> = None;
2217
2218    for combo in req.combos {
2219        periods.push(ma_period_for_combo(info, combo.params)?);
2220        let mut params = convert_ma_params(combo.params, info.id, output_id)?;
2221        if info.outputs.len() > 1 && !has_key(combo.params, "output") {
2222            params.push(MaBatchParamKV {
2223                key: "output",
2224                value: MaBatchParamValue::EnumString(output_id),
2225            });
2226        }
2227        match &shared_params {
2228            None => shared_params = Some(params),
2229            Some(existing) => {
2230                if !ma_params_equal(existing, &params) {
2231                    return Ok(None);
2232                }
2233            }
2234        }
2235    }
2236
2237    let Some((start, end, step)) = derive_period_sweep(&periods) else {
2238        return Ok(None);
2239    };
2240
2241    let out = ma_batch_with_kernel_and_typed_params(
2242        info.id,
2243        data,
2244        (start, end, step),
2245        req.kernel,
2246        shared_params.as_deref().unwrap_or(&[]),
2247    )
2248    .map_err(|e| IndicatorDispatchError::ComputeFailed {
2249        indicator: info.id.to_string(),
2250        details: e.to_string(),
2251    })?;
2252    ensure_len(info.id, cols, out.cols)?;
2253
2254    let values = reorder_or_take_f64_matrix_by_period(
2255        info.id,
2256        &periods,
2257        &out.periods,
2258        out.cols,
2259        out.values,
2260    )?;
2261    Ok(Some(f64_output(output_id, periods.len(), cols, values)))
2262}
2263
2264fn ma_params_equal(a: &[MaBatchParamKV<'_>], b: &[MaBatchParamKV<'_>]) -> bool {
2265    if a.len() != b.len() {
2266        return false;
2267    }
2268
2269    for (lhs, rhs) in a.iter().zip(b.iter()) {
2270        if !lhs.key.eq_ignore_ascii_case(rhs.key) {
2271            return false;
2272        }
2273        let same = match (&lhs.value, &rhs.value) {
2274            (MaBatchParamValue::Int(x), MaBatchParamValue::Int(y)) => x == y,
2275            (MaBatchParamValue::Float(x), MaBatchParamValue::Float(y)) => x == y,
2276            (MaBatchParamValue::Bool(x), MaBatchParamValue::Bool(y)) => x == y,
2277            (MaBatchParamValue::EnumString(x), MaBatchParamValue::EnumString(y)) => {
2278                x.eq_ignore_ascii_case(y)
2279            }
2280            _ => false,
2281        };
2282        if !same {
2283            return false;
2284        }
2285    }
2286    true
2287}
2288
2289fn collect_f64(
2290    indicator: &str,
2291    output_id: &str,
2292    combos: &[IndicatorParamSet<'_>],
2293    cols: usize,
2294    mut eval: impl FnMut(&[ParamKV<'_>]) -> Result<Vec<f64>, IndicatorDispatchError>,
2295) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2296    let rows = combos.len();
2297    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
2298    for combo in combos {
2299        let series = eval(combo.params)?;
2300        ensure_len(indicator, cols, series.len())?;
2301        matrix.extend_from_slice(&series);
2302    }
2303    Ok(f64_output(output_id, rows, cols, matrix))
2304}
2305
2306fn collect_bool(
2307    indicator: &str,
2308    output_id: &str,
2309    combos: &[IndicatorParamSet<'_>],
2310    cols: usize,
2311    mut eval: impl FnMut(&[ParamKV<'_>]) -> Result<Vec<bool>, IndicatorDispatchError>,
2312) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2313    let rows = combos.len();
2314    let mut matrix = Vec::with_capacity(rows.saturating_mul(cols));
2315    for combo in combos {
2316        let series = eval(combo.params)?;
2317        ensure_len(indicator, cols, series.len())?;
2318        matrix.extend_from_slice(&series);
2319    }
2320    Ok(bool_output(output_id, rows, cols, matrix))
2321}
2322
2323fn collect_f64_into_rows(
2324    indicator: &str,
2325    output_id: &str,
2326    combos: &[IndicatorParamSet<'_>],
2327    cols: usize,
2328    mut eval_into: impl FnMut(&[ParamKV<'_>], &mut [f64]) -> Result<(), IndicatorDispatchError>,
2329) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2330    let rows = combos.len();
2331    let total = rows
2332        .checked_mul(cols)
2333        .ok_or_else(|| IndicatorDispatchError::ComputeFailed {
2334            indicator: indicator.to_string(),
2335            details: "rows*cols overflow".to_string(),
2336        })?;
2337    let mut matrix = vec![f64::NAN; total];
2338    for (row, combo) in combos.iter().enumerate() {
2339        let start = row * cols;
2340        let end = start + cols;
2341        eval_into(combo.params, &mut matrix[start..end])?;
2342    }
2343    Ok(f64_output(output_id, rows, cols, matrix))
2344}
2345
2346fn to_batch_kernel(kernel: Kernel) -> Kernel {
2347    match kernel {
2348        Kernel::Auto => Kernel::Auto,
2349        Kernel::Scalar => Kernel::ScalarBatch,
2350        Kernel::Avx2 => Kernel::Avx2Batch,
2351        Kernel::Avx512 => Kernel::Avx512Batch,
2352        other => other,
2353    }
2354}
2355
2356fn combo_periods(
2357    indicator: &str,
2358    combos: &[IndicatorParamSet<'_>],
2359    key: &str,
2360    default: usize,
2361) -> Result<Vec<usize>, IndicatorDispatchError> {
2362    let mut out = Vec::with_capacity(combos.len());
2363    for combo in combos {
2364        out.push(get_usize_param(indicator, combo.params, key, default)?);
2365    }
2366    Ok(out)
2367}
2368
2369fn derive_period_sweep(periods: &[usize]) -> Option<(usize, usize, usize)> {
2370    if periods.is_empty() {
2371        return None;
2372    }
2373    if periods.len() == 1 {
2374        return Some((periods[0], periods[0], 0));
2375    }
2376    if periods.windows(2).all(|w| w[0] == w[1]) {
2377        return Some((periods[0], periods[0], 0));
2378    }
2379
2380    let diff = periods[1] as isize - periods[0] as isize;
2381    if diff == 0 {
2382        return None;
2383    }
2384    if !periods
2385        .windows(2)
2386        .all(|w| (w[1] as isize - w[0] as isize) == diff)
2387    {
2388        return None;
2389    }
2390
2391    Some((
2392        periods[0],
2393        *periods.last().unwrap_or(&periods[0]),
2394        diff.unsigned_abs(),
2395    ))
2396}
2397
2398fn reorder_or_take_f64_matrix_by_period(
2399    indicator: &str,
2400    requested_periods: &[usize],
2401    produced_periods: &[usize],
2402    cols: usize,
2403    values: Vec<f64>,
2404) -> Result<Vec<f64>, IndicatorDispatchError> {
2405    ensure_len(
2406        indicator,
2407        produced_periods.len().saturating_mul(cols),
2408        values.len(),
2409    )?;
2410
2411    if requested_periods.len() == produced_periods.len() && requested_periods == produced_periods {
2412        return Ok(values);
2413    }
2414
2415    let period_to_row: HashMap<usize, usize> = produced_periods
2416        .iter()
2417        .copied()
2418        .enumerate()
2419        .map(|(row, period)| (period, row))
2420        .collect();
2421
2422    let mut out = Vec::with_capacity(requested_periods.len().saturating_mul(cols));
2423    for period in requested_periods {
2424        let row = period_to_row.get(period).copied().ok_or_else(|| {
2425            IndicatorDispatchError::ComputeFailed {
2426                indicator: indicator.to_string(),
2427                details: format!("batch output did not contain requested period {period}"),
2428            }
2429        })?;
2430        let start = row * cols;
2431        let end = start + cols;
2432        out.extend_from_slice(&values[start..end]);
2433    }
2434    Ok(out)
2435}
2436
2437fn compute_ad_batch(
2438    req: IndicatorBatchRequest<'_>,
2439    output_id: &str,
2440) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2441    expect_value_output("ad", output_id)?;
2442    let (high, low, close, volume) = extract_hlcv_input("ad", req.data)?;
2443    let kernel = req.kernel.to_non_batch();
2444    collect_f64("ad", output_id, req.combos, close.len(), |_params| {
2445        let input = AdInput::from_slices(high, low, close, volume, AdParams::default());
2446        let out =
2447            ad_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2448                indicator: "ad".to_string(),
2449                details: e.to_string(),
2450            })?;
2451        Ok(out.values)
2452    })
2453}
2454
2455fn compute_adosc_batch(
2456    req: IndicatorBatchRequest<'_>,
2457    output_id: &str,
2458) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2459    expect_value_output("adosc", output_id)?;
2460    let (high, low, close, volume) = extract_hlcv_input("adosc", req.data)?;
2461    let kernel = req.kernel.to_non_batch();
2462    collect_f64("adosc", output_id, req.combos, close.len(), |params| {
2463        let short_period = get_usize_param("adosc", params, "short_period", 3)?;
2464        let long_period = get_usize_param("adosc", params, "long_period", 10)?;
2465        let input = AdoscInput::from_slices(
2466            high,
2467            low,
2468            close,
2469            volume,
2470            AdoscParams {
2471                short_period: Some(short_period),
2472                long_period: Some(long_period),
2473            },
2474        );
2475        let out = adosc_with_kernel(&input, kernel).map_err(|e| {
2476            IndicatorDispatchError::ComputeFailed {
2477                indicator: "adosc".to_string(),
2478                details: e.to_string(),
2479            }
2480        })?;
2481        Ok(out.values)
2482    })
2483}
2484
2485fn compute_ao_batch(
2486    req: IndicatorBatchRequest<'_>,
2487    output_id: &str,
2488) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2489    expect_value_output("ao", output_id)?;
2490    let mut derived_source: Option<Vec<f64>> = None;
2491    let source: &[f64] = match req.data {
2492        IndicatorDataRef::Slice { values } => values,
2493        IndicatorDataRef::Candles { candles, source } => {
2494            source_type(candles, source.unwrap_or("hl2"))
2495        }
2496        IndicatorDataRef::HighLow { high, low } => {
2497            ensure_same_len_2("ao", high.len(), low.len())?;
2498            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2499            derived_source.as_deref().unwrap_or(high)
2500        }
2501        IndicatorDataRef::Ohlc {
2502            open,
2503            high,
2504            low,
2505            close,
2506        } => {
2507            ensure_same_len_4("ao", open.len(), high.len(), low.len(), close.len())?;
2508            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2509            derived_source.as_deref().unwrap_or(close)
2510        }
2511        IndicatorDataRef::Ohlcv {
2512            open,
2513            high,
2514            low,
2515            close,
2516            volume,
2517        } => {
2518            ensure_same_len_5(
2519                "ao",
2520                open.len(),
2521                high.len(),
2522                low.len(),
2523                close.len(),
2524                volume.len(),
2525            )?;
2526            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
2527            derived_source.as_deref().unwrap_or(close)
2528        }
2529        IndicatorDataRef::CloseVolume { .. } => {
2530            return Err(IndicatorDispatchError::MissingRequiredInput {
2531                indicator: "ao".to_string(),
2532                input: IndicatorInputKind::HighLow,
2533            })
2534        }
2535    };
2536    let kernel = req.kernel.to_non_batch();
2537    collect_f64_into_rows("ao", output_id, req.combos, source.len(), |params, row| {
2538        let short_period = get_usize_param("ao", params, "short_period", 5)?;
2539        let long_period = get_usize_param("ao", params, "long_period", 34)?;
2540        let input = AoInput::from_slice(
2541            source,
2542            AoParams {
2543                short_period: Some(short_period),
2544                long_period: Some(long_period),
2545            },
2546        );
2547        ao_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2548            indicator: "ao".to_string(),
2549            details: e.to_string(),
2550        })
2551    })
2552}
2553
2554fn compute_bop_batch(
2555    req: IndicatorBatchRequest<'_>,
2556    output_id: &str,
2557) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2558    expect_value_output("bop", output_id)?;
2559    let (open, high, low, close): (&[f64], &[f64], &[f64], &[f64]) = match req.data {
2560        IndicatorDataRef::Candles { candles, .. } => (
2561            candles.open.as_slice(),
2562            candles.high.as_slice(),
2563            candles.low.as_slice(),
2564            candles.close.as_slice(),
2565        ),
2566        IndicatorDataRef::Ohlc {
2567            open,
2568            high,
2569            low,
2570            close,
2571        } => {
2572            ensure_same_len_4("bop", open.len(), high.len(), low.len(), close.len())?;
2573            (open, high, low, close)
2574        }
2575        IndicatorDataRef::Ohlcv {
2576            open,
2577            high,
2578            low,
2579            close,
2580            volume,
2581        } => {
2582            ensure_same_len_5(
2583                "bop",
2584                open.len(),
2585                high.len(),
2586                low.len(),
2587                close.len(),
2588                volume.len(),
2589            )?;
2590            (open, high, low, close)
2591        }
2592        _ => {
2593            return Err(IndicatorDispatchError::MissingRequiredInput {
2594                indicator: "bop".to_string(),
2595                input: IndicatorInputKind::Ohlc,
2596            })
2597        }
2598    };
2599    let kernel = req.kernel.to_non_batch();
2600    collect_f64("bop", output_id, req.combos, close.len(), |_params| {
2601        let input = BopInput::from_slices(open, high, low, close, BopParams::default());
2602        let out =
2603            bop_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2604                indicator: "bop".to_string(),
2605                details: e.to_string(),
2606            })?;
2607        Ok(out.values)
2608    })
2609}
2610
2611fn compute_emv_batch(
2612    req: IndicatorBatchRequest<'_>,
2613    output_id: &str,
2614) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2615    expect_value_output("emv", output_id)?;
2616    let (high, low, close, volume) = extract_hlcv_input("emv", req.data)?;
2617    let kernel = req.kernel.to_non_batch();
2618    collect_f64("emv", output_id, req.combos, close.len(), |_params| {
2619        let input = EmvInput::from_slices(high, low, close, volume);
2620        let out =
2621            emv_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2622                indicator: "emv".to_string(),
2623                details: e.to_string(),
2624            })?;
2625        Ok(out.values)
2626    })
2627}
2628
2629fn compute_efi_batch(
2630    req: IndicatorBatchRequest<'_>,
2631    output_id: &str,
2632) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2633    expect_value_output("efi", output_id)?;
2634    let (price, volume) = extract_close_volume_input("efi", req.data, "close")?;
2635    let kernel = req.kernel.to_non_batch();
2636    collect_f64("efi", output_id, req.combos, price.len(), |params| {
2637        let period = get_usize_param("efi", params, "period", 13)?;
2638        let input = EfiInput::from_slices(
2639            price,
2640            volume,
2641            EfiParams {
2642                period: Some(period),
2643            },
2644        );
2645        let out =
2646            efi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2647                indicator: "efi".to_string(),
2648                details: e.to_string(),
2649            })?;
2650        Ok(out.values)
2651    })
2652}
2653
2654fn compute_mfi_batch(
2655    req: IndicatorBatchRequest<'_>,
2656    output_id: &str,
2657) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2658    expect_value_output("mfi", output_id)?;
2659    let mut derived_typical_price: Option<Vec<f64>> = None;
2660    let (typical_price, volume): (&[f64], &[f64]) = match req.data {
2661        IndicatorDataRef::Candles { candles, source } => (
2662            source_type(candles, source.unwrap_or("hlc3")),
2663            candles.volume.as_slice(),
2664        ),
2665        IndicatorDataRef::Ohlcv {
2666            open,
2667            high,
2668            low,
2669            close,
2670            volume,
2671        } => {
2672            ensure_same_len_5(
2673                "mfi",
2674                open.len(),
2675                high.len(),
2676                low.len(),
2677                close.len(),
2678                volume.len(),
2679            )?;
2680            derived_typical_price = Some(
2681                high.iter()
2682                    .zip(low)
2683                    .zip(close)
2684                    .map(|((h, l), c)| (h + l + c) / 3.0)
2685                    .collect(),
2686            );
2687            (derived_typical_price.as_deref().unwrap_or(close), volume)
2688        }
2689        IndicatorDataRef::CloseVolume { close, volume } => {
2690            ensure_same_len_2("mfi", close.len(), volume.len())?;
2691            (close, volume)
2692        }
2693        _ => {
2694            return Err(IndicatorDispatchError::MissingRequiredInput {
2695                indicator: "mfi".to_string(),
2696                input: IndicatorInputKind::CloseVolume,
2697            })
2698        }
2699    };
2700
2701    let periods = combo_periods("mfi", req.combos, "period", 14)?;
2702    if let Some((start, end, step)) = derive_period_sweep(&periods) {
2703        let out = mfi_batch_with_kernel(
2704            typical_price,
2705            volume,
2706            &MfiBatchRange {
2707                period: (start, end, step),
2708            },
2709            to_batch_kernel(req.kernel),
2710        )
2711        .map_err(|e| IndicatorDispatchError::ComputeFailed {
2712            indicator: "mfi".to_string(),
2713            details: e.to_string(),
2714        })?;
2715        ensure_len("mfi", typical_price.len(), out.cols)?;
2716        let produced_periods: Vec<usize> = out
2717            .combos
2718            .iter()
2719            .map(|combo| combo.period.unwrap_or(14))
2720            .collect();
2721        let values = reorder_or_take_f64_matrix_by_period(
2722            "mfi",
2723            &periods,
2724            &produced_periods,
2725            out.cols,
2726            out.values,
2727        )?;
2728        return Ok(f64_output(output_id, periods.len(), out.cols, values));
2729    }
2730
2731    let kernel = req.kernel.to_non_batch();
2732    collect_f64_into_rows(
2733        "mfi",
2734        output_id,
2735        req.combos,
2736        typical_price.len(),
2737        |params, row| {
2738            let period = get_usize_param("mfi", params, "period", 14)?;
2739            let input = MfiInput::from_slices(
2740                typical_price,
2741                volume,
2742                MfiParams {
2743                    period: Some(period),
2744                },
2745            );
2746            mfi_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2747                indicator: "mfi".to_string(),
2748                details: e.to_string(),
2749            })
2750        },
2751    )
2752}
2753
2754fn compute_mass_batch(
2755    req: IndicatorBatchRequest<'_>,
2756    output_id: &str,
2757) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2758    expect_value_output("mass", output_id)?;
2759    let (high, low) = extract_high_low_input("mass", req.data)?;
2760    let kernel = req.kernel.to_non_batch();
2761    collect_f64("mass", output_id, req.combos, high.len(), |params| {
2762        let period = get_usize_param("mass", params, "period", 5)?;
2763        let input = MassInput::from_slices(
2764            high,
2765            low,
2766            MassParams {
2767                period: Some(period),
2768            },
2769        );
2770        let out = mass_with_kernel(&input, kernel).map_err(|e| {
2771            IndicatorDispatchError::ComputeFailed {
2772                indicator: "mass".to_string(),
2773                details: e.to_string(),
2774            }
2775        })?;
2776        Ok(out.values)
2777    })
2778}
2779
2780fn compute_kvo_batch(
2781    req: IndicatorBatchRequest<'_>,
2782    output_id: &str,
2783) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2784    expect_value_output("kvo", output_id)?;
2785    let (high, low, close, volume) = extract_hlcv_input("kvo", req.data)?;
2786    let kernel = req.kernel.to_non_batch();
2787    collect_f64("kvo", output_id, req.combos, close.len(), |params| {
2788        let short_period = get_usize_param("kvo", params, "short_period", 2)?;
2789        let long_period = get_usize_param("kvo", params, "long_period", 5)?;
2790        let input = KvoInput::from_slices(
2791            high,
2792            low,
2793            close,
2794            volume,
2795            KvoParams {
2796                short_period: Some(short_period),
2797                long_period: Some(long_period),
2798            },
2799        );
2800        let out =
2801            kvo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2802                indicator: "kvo".to_string(),
2803                details: e.to_string(),
2804            })?;
2805        Ok(out.values)
2806    })
2807}
2808
2809fn compute_vosc_batch(
2810    req: IndicatorBatchRequest<'_>,
2811    output_id: &str,
2812) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2813    expect_value_output("vosc", output_id)?;
2814    let volume = extract_volume_input("vosc", req.data)?;
2815    let kernel = req.kernel.to_non_batch();
2816    collect_f64("vosc", output_id, req.combos, volume.len(), |params| {
2817        let short_period = get_usize_param("vosc", params, "short_period", 2)?;
2818        let long_period = get_usize_param("vosc", params, "long_period", 5)?;
2819        let input = VoscInput::from_slice(
2820            volume,
2821            VoscParams {
2822                short_period: Some(short_period),
2823                long_period: Some(long_period),
2824            },
2825        );
2826        let out = vosc_with_kernel(&input, kernel).map_err(|e| {
2827            IndicatorDispatchError::ComputeFailed {
2828                indicator: "vosc".to_string(),
2829                details: e.to_string(),
2830            }
2831        })?;
2832        Ok(out.values)
2833    })
2834}
2835
2836fn compute_dx_batch(
2837    req: IndicatorBatchRequest<'_>,
2838    output_id: &str,
2839) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2840    expect_value_output("dx", output_id)?;
2841    let (high, low, close) = extract_ohlc_input("dx", req.data)?;
2842
2843    let periods = combo_periods("dx", req.combos, "period", 14)?;
2844    if let Some((start, end, step)) = derive_period_sweep(&periods) {
2845        let out = dx_batch_with_kernel(
2846            high,
2847            low,
2848            close,
2849            &DxBatchRange {
2850                period: (start, end, step),
2851            },
2852            to_batch_kernel(req.kernel),
2853        )
2854        .map_err(|e| IndicatorDispatchError::ComputeFailed {
2855            indicator: "dx".to_string(),
2856            details: e.to_string(),
2857        })?;
2858        ensure_len("dx", close.len(), out.cols)?;
2859        let produced_periods: Vec<usize> = out
2860            .combos
2861            .iter()
2862            .map(|combo| combo.period.unwrap_or(14))
2863            .collect();
2864        let values = reorder_or_take_f64_matrix_by_period(
2865            "dx",
2866            &periods,
2867            &produced_periods,
2868            out.cols,
2869            out.values,
2870        )?;
2871        return Ok(f64_output(output_id, periods.len(), out.cols, values));
2872    }
2873
2874    let kernel = req.kernel.to_non_batch();
2875    collect_f64_into_rows("dx", output_id, req.combos, close.len(), |params, row| {
2876        let period = get_usize_param("dx", params, "period", 14)?;
2877        let input = DxInput::from_hlc_slices(
2878            high,
2879            low,
2880            close,
2881            DxParams {
2882                period: Some(period),
2883            },
2884        );
2885        dx_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
2886            indicator: "dx".to_string(),
2887            details: e.to_string(),
2888        })
2889    })
2890}
2891
2892fn compute_fosc_batch(
2893    req: IndicatorBatchRequest<'_>,
2894    output_id: &str,
2895) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2896    expect_value_output("fosc", output_id)?;
2897    let data = extract_slice_input("fosc", req.data, "close")?;
2898    let kernel = req.kernel.to_non_batch();
2899    collect_f64("fosc", output_id, req.combos, data.len(), |params| {
2900        let period = get_usize_param("fosc", params, "period", 5)?;
2901        let input = FoscInput::from_slice(
2902            data,
2903            FoscParams {
2904                period: Some(period),
2905            },
2906        );
2907        let out = fosc_with_kernel(&input, kernel).map_err(|e| {
2908            IndicatorDispatchError::ComputeFailed {
2909                indicator: "fosc".to_string(),
2910                details: e.to_string(),
2911            }
2912        })?;
2913        Ok(out.values)
2914    })
2915}
2916
2917fn compute_ift_rsi_batch(
2918    req: IndicatorBatchRequest<'_>,
2919    output_id: &str,
2920) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2921    expect_value_output("ift_rsi", output_id)?;
2922    let data = extract_slice_input("ift_rsi", req.data, "close")?;
2923    let kernel = req.kernel.to_non_batch();
2924    collect_f64("ift_rsi", output_id, req.combos, data.len(), |params| {
2925        let rsi_period = get_usize_param("ift_rsi", params, "rsi_period", 5)?;
2926        let wma_period = get_usize_param("ift_rsi", params, "wma_period", 9)?;
2927        let input = IftRsiInput::from_slice(
2928            data,
2929            IftRsiParams {
2930                rsi_period: Some(rsi_period),
2931                wma_period: Some(wma_period),
2932            },
2933        );
2934        let out = ift_rsi_with_kernel(&input, kernel).map_err(|e| {
2935            IndicatorDispatchError::ComputeFailed {
2936                indicator: "ift_rsi".to_string(),
2937                details: e.to_string(),
2938            }
2939        })?;
2940        Ok(out.values)
2941    })
2942}
2943
2944fn compute_linearreg_angle_batch(
2945    req: IndicatorBatchRequest<'_>,
2946    output_id: &str,
2947) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2948    expect_value_output("linearreg_angle", output_id)?;
2949    let data = extract_slice_input("linearreg_angle", req.data, "close")?;
2950    let kernel = req.kernel.to_non_batch();
2951    collect_f64(
2952        "linearreg_angle",
2953        output_id,
2954        req.combos,
2955        data.len(),
2956        |params| {
2957            let period = get_usize_param("linearreg_angle", params, "period", 14)?;
2958            let input = Linearreg_angleInput::from_slice(
2959                data,
2960                Linearreg_angleParams {
2961                    period: Some(period),
2962                },
2963            );
2964            let out = linearreg_angle_with_kernel(&input, kernel).map_err(|e| {
2965                IndicatorDispatchError::ComputeFailed {
2966                    indicator: "linearreg_angle".to_string(),
2967                    details: e.to_string(),
2968                }
2969            })?;
2970            Ok(out.values)
2971        },
2972    )
2973}
2974
2975fn compute_linearreg_intercept_batch(
2976    req: IndicatorBatchRequest<'_>,
2977    output_id: &str,
2978) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
2979    expect_value_output("linearreg_intercept", output_id)?;
2980    let data = extract_slice_input("linearreg_intercept", req.data, "close")?;
2981    let kernel = req.kernel.to_non_batch();
2982    collect_f64(
2983        "linearreg_intercept",
2984        output_id,
2985        req.combos,
2986        data.len(),
2987        |params| {
2988            let period = get_usize_param("linearreg_intercept", params, "period", 14)?;
2989            let input = LinearRegInterceptInput::from_slice(
2990                data,
2991                LinearRegInterceptParams {
2992                    period: Some(period),
2993                },
2994            );
2995            let out = linearreg_intercept_with_kernel(&input, kernel).map_err(|e| {
2996                IndicatorDispatchError::ComputeFailed {
2997                    indicator: "linearreg_intercept".to_string(),
2998                    details: e.to_string(),
2999                }
3000            })?;
3001            Ok(out.values)
3002        },
3003    )
3004}
3005
3006fn compute_linearreg_slope_batch(
3007    req: IndicatorBatchRequest<'_>,
3008    output_id: &str,
3009) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3010    expect_value_output("linearreg_slope", output_id)?;
3011    let data = extract_slice_input("linearreg_slope", req.data, "close")?;
3012    let kernel = req.kernel.to_non_batch();
3013    collect_f64(
3014        "linearreg_slope",
3015        output_id,
3016        req.combos,
3017        data.len(),
3018        |params| {
3019            let period = get_usize_param("linearreg_slope", params, "period", 14)?;
3020            let input = LinearRegSlopeInput::from_slice(
3021                data,
3022                LinearRegSlopeParams {
3023                    period: Some(period),
3024                },
3025            );
3026            let out = linearreg_slope_with_kernel(&input, kernel).map_err(|e| {
3027                IndicatorDispatchError::ComputeFailed {
3028                    indicator: "linearreg_slope".to_string(),
3029                    details: e.to_string(),
3030                }
3031            })?;
3032            Ok(out.values)
3033        },
3034    )
3035}
3036
3037fn compute_cg_batch(
3038    req: IndicatorBatchRequest<'_>,
3039    output_id: &str,
3040) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3041    expect_value_output("cg", output_id)?;
3042    let data = extract_slice_input("cg", req.data, "close")?;
3043    let kernel = req.kernel.to_non_batch();
3044    collect_f64("cg", output_id, req.combos, data.len(), |params| {
3045        let period = get_usize_param("cg", params, "period", 10)?;
3046        let input = CgInput::from_slice(
3047            data,
3048            CgParams {
3049                period: Some(period),
3050            },
3051        );
3052        let out =
3053            cg_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3054                indicator: "cg".to_string(),
3055                details: e.to_string(),
3056            })?;
3057        Ok(out.values)
3058    })
3059}
3060
3061fn compute_rsi_batch(
3062    req: IndicatorBatchRequest<'_>,
3063    output_id: &str,
3064) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3065    expect_value_output("rsi", output_id)?;
3066    let data = extract_slice_input("rsi", req.data, "close")?;
3067    let kernel = req.kernel.to_non_batch();
3068    collect_f64("rsi", output_id, req.combos, data.len(), |params| {
3069        let period = get_usize_param("rsi", params, "period", 14)?;
3070        let input = RsiInput::from_slice(
3071            data,
3072            RsiParams {
3073                period: Some(period),
3074            },
3075        );
3076        let out =
3077            rsi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3078                indicator: "rsi".to_string(),
3079                details: e.to_string(),
3080            })?;
3081        Ok(out.values)
3082    })
3083}
3084
3085fn compute_roc_batch(
3086    req: IndicatorBatchRequest<'_>,
3087    output_id: &str,
3088) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3089    expect_value_output("roc", output_id)?;
3090    let data = extract_slice_input("roc", req.data, "close")?;
3091    let kernel = req.kernel.to_non_batch();
3092    collect_f64("roc", output_id, req.combos, data.len(), |params| {
3093        let period = get_usize_param("roc", params, "period", 9)?;
3094        let input = RocInput::from_slice(
3095            data,
3096            RocParams {
3097                period: Some(period),
3098            },
3099        );
3100        let out =
3101            roc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3102                indicator: "roc".to_string(),
3103                details: e.to_string(),
3104            })?;
3105        Ok(out.values)
3106    })
3107}
3108
3109fn compute_linear_correlation_oscillator_batch(
3110    req: IndicatorBatchRequest<'_>,
3111    output_id: &str,
3112) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3113    expect_value_output("linear_correlation_oscillator", output_id)?;
3114    let data = extract_slice_input("linear_correlation_oscillator", req.data, "close")?;
3115    let kernel = req.kernel.to_non_batch();
3116    collect_f64(
3117        "linear_correlation_oscillator",
3118        output_id,
3119        req.combos,
3120        data.len(),
3121        |params| {
3122            let period = get_usize_param("linear_correlation_oscillator", params, "period", 14)?;
3123            let input = LinearCorrelationOscillatorInput::from_slice(
3124                data,
3125                LinearCorrelationOscillatorParams {
3126                    period: Some(period),
3127                },
3128            );
3129            let out = linear_correlation_oscillator_with_kernel(&input, kernel).map_err(|e| {
3130                IndicatorDispatchError::ComputeFailed {
3131                    indicator: "linear_correlation_oscillator".to_string(),
3132                    details: e.to_string(),
3133                }
3134            })?;
3135            Ok(out.values)
3136        },
3137    )
3138}
3139
3140fn compute_apo_batch(
3141    req: IndicatorBatchRequest<'_>,
3142    output_id: &str,
3143) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3144    expect_value_output("apo", output_id)?;
3145    let data = extract_slice_input("apo", req.data, "close")?;
3146    let kernel = req.kernel.to_non_batch();
3147    collect_f64("apo", output_id, req.combos, data.len(), |params| {
3148        let short_period = get_usize_param("apo", params, "short_period", 10)?;
3149        let long_period = get_usize_param("apo", params, "long_period", 20)?;
3150        let input = ApoInput::from_slice(
3151            data,
3152            ApoParams {
3153                short_period: Some(short_period),
3154                long_period: Some(long_period),
3155            },
3156        );
3157        let out =
3158            apo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3159                indicator: "apo".to_string(),
3160                details: e.to_string(),
3161            })?;
3162        Ok(out.values)
3163    })
3164}
3165
3166fn compute_cci_batch(
3167    req: IndicatorBatchRequest<'_>,
3168    output_id: &str,
3169) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3170    expect_value_output("cci", output_id)?;
3171    let data = extract_slice_input("cci", req.data, "hlc3")?;
3172    let kernel = req.kernel.to_non_batch();
3173    collect_f64("cci", output_id, req.combos, data.len(), |params| {
3174        let period = get_usize_param("cci", params, "period", 14)?;
3175        let input = CciInput::from_slice(
3176            data,
3177            CciParams {
3178                period: Some(period),
3179            },
3180        );
3181        let out =
3182            cci_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3183                indicator: "cci".to_string(),
3184                details: e.to_string(),
3185            })?;
3186        Ok(out.values)
3187    })
3188}
3189
3190fn compute_cfo_batch(
3191    req: IndicatorBatchRequest<'_>,
3192    output_id: &str,
3193) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3194    expect_value_output("cfo", output_id)?;
3195    let data = extract_slice_input("cfo", req.data, "close")?;
3196    let kernel = req.kernel.to_non_batch();
3197    collect_f64("cfo", output_id, req.combos, data.len(), |params| {
3198        let period = get_usize_param("cfo", params, "period", 14)?;
3199        let scalar = get_f64_param("cfo", params, "scalar", 100.0)?;
3200        let input = CfoInput::from_slice(
3201            data,
3202            CfoParams {
3203                period: Some(period),
3204                scalar: Some(scalar),
3205            },
3206        );
3207        let out =
3208            cfo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3209                indicator: "cfo".to_string(),
3210                details: e.to_string(),
3211            })?;
3212        Ok(out.values)
3213    })
3214}
3215
3216fn compute_cci_cycle_batch(
3217    req: IndicatorBatchRequest<'_>,
3218    output_id: &str,
3219) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3220    expect_value_output("cci_cycle", output_id)?;
3221    let data = extract_slice_input("cci_cycle", req.data, "close")?;
3222    let kernel = req.kernel.to_non_batch();
3223    collect_f64("cci_cycle", output_id, req.combos, data.len(), |params| {
3224        let length = get_usize_param("cci_cycle", params, "length", 10)?;
3225        let factor = get_f64_param("cci_cycle", params, "factor", 0.5)?;
3226        let input = CciCycleInput::from_slice(
3227            data,
3228            CciCycleParams {
3229                length: Some(length),
3230                factor: Some(factor),
3231            },
3232        );
3233        let out = cci_cycle_with_kernel(&input, kernel).map_err(|e| {
3234            IndicatorDispatchError::ComputeFailed {
3235                indicator: "cci_cycle".to_string(),
3236                details: e.to_string(),
3237            }
3238        })?;
3239        Ok(out.values)
3240    })
3241}
3242
3243fn compute_lrsi_batch(
3244    req: IndicatorBatchRequest<'_>,
3245    output_id: &str,
3246) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3247    expect_value_output("lrsi", output_id)?;
3248    let (high, low) = extract_high_low_input("lrsi", req.data)?;
3249    let kernel = req.kernel.to_non_batch();
3250    collect_f64("lrsi", output_id, req.combos, high.len(), |params| {
3251        let alpha = get_f64_param("lrsi", params, "alpha", 0.2)?;
3252        let input = LrsiInput::from_slices(high, low, LrsiParams { alpha: Some(alpha) });
3253        let out = lrsi_with_kernel(&input, kernel).map_err(|e| {
3254            IndicatorDispatchError::ComputeFailed {
3255                indicator: "lrsi".to_string(),
3256                details: e.to_string(),
3257            }
3258        })?;
3259        Ok(out.values)
3260    })
3261}
3262
3263fn compute_er_batch(
3264    req: IndicatorBatchRequest<'_>,
3265    output_id: &str,
3266) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3267    expect_value_output("er", output_id)?;
3268    let data = extract_slice_input("er", req.data, "close")?;
3269    let kernel = req.kernel.to_non_batch();
3270    collect_f64("er", output_id, req.combos, data.len(), |params| {
3271        let period = get_usize_param("er", params, "period", 5)?;
3272        let input = ErInput::from_slice(
3273            data,
3274            ErParams {
3275                period: Some(period),
3276            },
3277        );
3278        let out =
3279            er_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3280                indicator: "er".to_string(),
3281                details: e.to_string(),
3282            })?;
3283        Ok(out.values)
3284    })
3285}
3286
3287fn compute_kurtosis_batch(
3288    req: IndicatorBatchRequest<'_>,
3289    output_id: &str,
3290) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3291    expect_value_output("kurtosis", output_id)?;
3292    let data = extract_slice_input("kurtosis", req.data, "hl2")?;
3293    let kernel = req.kernel.to_non_batch();
3294    collect_f64("kurtosis", output_id, req.combos, data.len(), |params| {
3295        let period = get_usize_param("kurtosis", params, "period", 5)?;
3296        let input = KurtosisInput::from_slice(
3297            data,
3298            KurtosisParams {
3299                period: Some(period),
3300            },
3301        );
3302        let out = kurtosis_with_kernel(&input, kernel).map_err(|e| {
3303            IndicatorDispatchError::ComputeFailed {
3304                indicator: "kurtosis".to_string(),
3305                details: e.to_string(),
3306            }
3307        })?;
3308        Ok(out.values)
3309    })
3310}
3311
3312fn compute_natr_batch(
3313    req: IndicatorBatchRequest<'_>,
3314    output_id: &str,
3315) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3316    expect_value_output("natr", output_id)?;
3317    let (high, low, close) = extract_ohlc_input("natr", req.data)?;
3318    let kernel = req.kernel.to_non_batch();
3319    collect_f64("natr", output_id, req.combos, close.len(), |params| {
3320        let period = get_usize_param("natr", params, "period", 14)?;
3321        let input = NatrInput::from_slices(
3322            high,
3323            low,
3324            close,
3325            NatrParams {
3326                period: Some(period),
3327            },
3328        );
3329        let out = natr_with_kernel(&input, kernel).map_err(|e| {
3330            IndicatorDispatchError::ComputeFailed {
3331                indicator: "natr".to_string(),
3332                details: e.to_string(),
3333            }
3334        })?;
3335        Ok(out.values)
3336    })
3337}
3338
3339fn compute_mean_ad_batch(
3340    req: IndicatorBatchRequest<'_>,
3341    output_id: &str,
3342) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3343    expect_value_output("mean_ad", output_id)?;
3344    let data = extract_slice_input("mean_ad", req.data, "close")?;
3345    let kernel = req.kernel.to_non_batch();
3346    collect_f64("mean_ad", output_id, req.combos, data.len(), |params| {
3347        let period = get_usize_param("mean_ad", params, "period", 5)?;
3348        let input = MeanAdInput::from_slice(
3349            data,
3350            MeanAdParams {
3351                period: Some(period),
3352            },
3353        );
3354        let out = mean_ad_with_kernel(&input, kernel).map_err(|e| {
3355            IndicatorDispatchError::ComputeFailed {
3356                indicator: "mean_ad".to_string(),
3357                details: e.to_string(),
3358            }
3359        })?;
3360        Ok(out.values)
3361    })
3362}
3363
3364fn compute_medium_ad_batch(
3365    req: IndicatorBatchRequest<'_>,
3366    output_id: &str,
3367) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3368    expect_value_output("medium_ad", output_id)?;
3369    let data = extract_slice_input("medium_ad", req.data, "close")?;
3370    let kernel = req.kernel.to_non_batch();
3371    collect_f64("medium_ad", output_id, req.combos, data.len(), |params| {
3372        let period = get_usize_param("medium_ad", params, "period", 5)?;
3373        let input = MediumAdInput::from_slice(
3374            data,
3375            MediumAdParams {
3376                period: Some(period),
3377            },
3378        );
3379        let out = medium_ad_with_kernel(&input, kernel).map_err(|e| {
3380            IndicatorDispatchError::ComputeFailed {
3381                indicator: "medium_ad".to_string(),
3382                details: e.to_string(),
3383            }
3384        })?;
3385        Ok(out.values)
3386    })
3387}
3388
3389fn compute_deviation_batch(
3390    req: IndicatorBatchRequest<'_>,
3391    output_id: &str,
3392) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3393    expect_value_output("deviation", output_id)?;
3394    let data = extract_slice_input("deviation", req.data, "close")?;
3395    let kernel = req.kernel.to_non_batch();
3396    collect_f64("deviation", output_id, req.combos, data.len(), |params| {
3397        let period = get_usize_param("deviation", params, "period", 9)?;
3398        let devtype = get_usize_param("deviation", params, "devtype", 0)?;
3399        let input = DeviationInput::from_slice(
3400            data,
3401            DeviationParams {
3402                period: Some(period),
3403                devtype: Some(devtype),
3404            },
3405        );
3406        let out = deviation_with_kernel(&input, kernel).map_err(|e| {
3407            IndicatorDispatchError::ComputeFailed {
3408                indicator: "deviation".to_string(),
3409                details: e.to_string(),
3410            }
3411        })?;
3412        Ok(out.values)
3413    })
3414}
3415
3416fn compute_dpo_batch(
3417    req: IndicatorBatchRequest<'_>,
3418    output_id: &str,
3419) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3420    expect_value_output("dpo", output_id)?;
3421    let data = extract_slice_input("dpo", req.data, "close")?;
3422    let kernel = req.kernel.to_non_batch();
3423    collect_f64("dpo", output_id, req.combos, data.len(), |params| {
3424        let period = get_usize_param("dpo", params, "period", 5)?;
3425        let input = DpoInput::from_slice(
3426            data,
3427            DpoParams {
3428                period: Some(period),
3429            },
3430        );
3431        let out =
3432            dpo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3433                indicator: "dpo".to_string(),
3434                details: e.to_string(),
3435            })?;
3436        Ok(out.values)
3437    })
3438}
3439
3440fn compute_pfe_batch(
3441    req: IndicatorBatchRequest<'_>,
3442    output_id: &str,
3443) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3444    expect_value_output("pfe", output_id)?;
3445    let data = extract_slice_input("pfe", req.data, "close")?;
3446    let kernel = req.kernel.to_non_batch();
3447    collect_f64("pfe", output_id, req.combos, data.len(), |params| {
3448        let period = get_usize_param("pfe", params, "period", 10)?;
3449        let smoothing = get_usize_param("pfe", params, "smoothing", 5)?;
3450        let input = PfeInput::from_slice(
3451            data,
3452            PfeParams {
3453                period: Some(period),
3454                smoothing: Some(smoothing),
3455            },
3456        );
3457        let out =
3458            pfe_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3459                indicator: "pfe".to_string(),
3460                details: e.to_string(),
3461            })?;
3462        Ok(out.values)
3463    })
3464}
3465
3466fn compute_qstick_batch(
3467    req: IndicatorBatchRequest<'_>,
3468    output_id: &str,
3469) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3470    expect_value_output("qstick", output_id)?;
3471    let (open, close) = match req.data {
3472        IndicatorDataRef::Candles { candles, .. } => {
3473            (candles.open.as_slice(), candles.close.as_slice())
3474        }
3475        IndicatorDataRef::Ohlc {
3476            open,
3477            high,
3478            low,
3479            close,
3480        } => {
3481            ensure_same_len_4("qstick", open.len(), high.len(), low.len(), close.len())?;
3482            (open, close)
3483        }
3484        IndicatorDataRef::Ohlcv {
3485            open,
3486            high,
3487            low,
3488            close,
3489            volume,
3490        } => {
3491            ensure_same_len_5(
3492                "qstick",
3493                open.len(),
3494                high.len(),
3495                low.len(),
3496                close.len(),
3497                volume.len(),
3498            )?;
3499            (open, close)
3500        }
3501        _ => {
3502            return Err(IndicatorDispatchError::MissingRequiredInput {
3503                indicator: "qstick".to_string(),
3504                input: IndicatorInputKind::Ohlc,
3505            })
3506        }
3507    };
3508    let kernel = req.kernel.to_non_batch();
3509    collect_f64("qstick", output_id, req.combos, close.len(), |params| {
3510        let period = get_usize_param("qstick", params, "period", 5)?;
3511        let input = QstickInput::from_slices(
3512            open,
3513            close,
3514            QstickParams {
3515                period: Some(period),
3516            },
3517        );
3518        let out = qstick_with_kernel(&input, kernel).map_err(|e| {
3519            IndicatorDispatchError::ComputeFailed {
3520                indicator: "qstick".to_string(),
3521                details: e.to_string(),
3522            }
3523        })?;
3524        Ok(out.values)
3525    })
3526}
3527
3528fn compute_ehlers_fm_demodulator_batch(
3529    req: IndicatorBatchRequest<'_>,
3530    output_id: &str,
3531) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3532    expect_value_output("ehlers_fm_demodulator", output_id)?;
3533    let (open, close) = match req.data {
3534        IndicatorDataRef::Candles { candles, .. } => {
3535            (candles.open.as_slice(), candles.close.as_slice())
3536        }
3537        IndicatorDataRef::Ohlc {
3538            open,
3539            high,
3540            low,
3541            close,
3542        } => {
3543            ensure_same_len_4(
3544                "ehlers_fm_demodulator",
3545                open.len(),
3546                high.len(),
3547                low.len(),
3548                close.len(),
3549            )?;
3550            (open, close)
3551        }
3552        IndicatorDataRef::Ohlcv {
3553            open,
3554            high,
3555            low,
3556            close,
3557            volume,
3558        } => {
3559            ensure_same_len_5(
3560                "ehlers_fm_demodulator",
3561                open.len(),
3562                high.len(),
3563                low.len(),
3564                close.len(),
3565                volume.len(),
3566            )?;
3567            (open, close)
3568        }
3569        _ => {
3570            return Err(IndicatorDispatchError::MissingRequiredInput {
3571                indicator: "ehlers_fm_demodulator".to_string(),
3572                input: IndicatorInputKind::Ohlc,
3573            })
3574        }
3575    };
3576    let kernel = req.kernel.to_non_batch();
3577    collect_f64(
3578        "ehlers_fm_demodulator",
3579        output_id,
3580        req.combos,
3581        close.len(),
3582        |params| {
3583            let period = get_usize_param("ehlers_fm_demodulator", params, "period", 30)?;
3584            let input = EhlersFmDemodulatorInput::from_slices(
3585                open,
3586                close,
3587                EhlersFmDemodulatorParams {
3588                    period: Some(period),
3589                },
3590            );
3591            let out = ehlers_fm_demodulator_with_kernel(&input, kernel).map_err(|e| {
3592                IndicatorDispatchError::ComputeFailed {
3593                    indicator: "ehlers_fm_demodulator".to_string(),
3594                    details: e.to_string(),
3595                }
3596            })?;
3597            Ok(out.values)
3598        },
3599    )
3600}
3601
3602fn compute_reverse_rsi_batch(
3603    req: IndicatorBatchRequest<'_>,
3604    output_id: &str,
3605) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3606    expect_value_output("reverse_rsi", output_id)?;
3607    let data = extract_slice_input("reverse_rsi", req.data, "close")?;
3608    let kernel = req.kernel.to_non_batch();
3609    collect_f64("reverse_rsi", output_id, req.combos, data.len(), |params| {
3610        let rsi_length = get_usize_param("reverse_rsi", params, "rsi_length", 14)?;
3611        let rsi_level = get_f64_param("reverse_rsi", params, "rsi_level", 50.0)?;
3612        let input = ReverseRsiInput::from_slice(
3613            data,
3614            ReverseRsiParams {
3615                rsi_length: Some(rsi_length),
3616                rsi_level: Some(rsi_level),
3617            },
3618        );
3619        let out = reverse_rsi_with_kernel(&input, kernel).map_err(|e| {
3620            IndicatorDispatchError::ComputeFailed {
3621                indicator: "reverse_rsi".to_string(),
3622                details: e.to_string(),
3623            }
3624        })?;
3625        Ok(out.values)
3626    })
3627}
3628
3629fn compute_percentile_nearest_rank_batch(
3630    req: IndicatorBatchRequest<'_>,
3631    output_id: &str,
3632) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3633    expect_value_output("percentile_nearest_rank", output_id)?;
3634    let data = extract_slice_input("percentile_nearest_rank", req.data, "close")?;
3635    let kernel = req.kernel.to_non_batch();
3636    collect_f64(
3637        "percentile_nearest_rank",
3638        output_id,
3639        req.combos,
3640        data.len(),
3641        |params| {
3642            let length = get_usize_param("percentile_nearest_rank", params, "length", 15)?;
3643            let percentage = get_f64_param("percentile_nearest_rank", params, "percentage", 50.0)?;
3644            let input = PercentileNearestRankInput::from_slice(
3645                data,
3646                PercentileNearestRankParams {
3647                    length: Some(length),
3648                    percentage: Some(percentage),
3649                },
3650            );
3651            let out = percentile_nearest_rank_with_kernel(&input, kernel).map_err(|e| {
3652                IndicatorDispatchError::ComputeFailed {
3653                    indicator: "percentile_nearest_rank".to_string(),
3654                    details: e.to_string(),
3655                }
3656            })?;
3657            Ok(out.values)
3658        },
3659    )
3660}
3661
3662fn compute_obv_batch(
3663    req: IndicatorBatchRequest<'_>,
3664    output_id: &str,
3665) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3666    expect_value_output("obv", output_id)?;
3667    let (close, volume) = extract_close_volume_input("obv", req.data, "close")?;
3668    let kernel = req.kernel.to_non_batch();
3669    collect_f64("obv", output_id, req.combos, close.len(), |_params| {
3670        let input = ObvInput::from_slices(close, volume, ObvParams::default());
3671        let out =
3672            obv_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3673                indicator: "obv".to_string(),
3674                details: e.to_string(),
3675            })?;
3676        Ok(out.values)
3677    })
3678}
3679
3680fn compute_vpt_batch(
3681    req: IndicatorBatchRequest<'_>,
3682    output_id: &str,
3683) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3684    expect_value_output("vpt", output_id)?;
3685    let (close, volume) = extract_close_volume_input("vpt", req.data, "close")?;
3686    let kernel = req.kernel.to_non_batch();
3687    collect_f64("vpt", output_id, req.combos, close.len(), |_params| {
3688        let input = VptInput::from_slices(close, volume);
3689        let out =
3690            vpt_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3691                indicator: "vpt".to_string(),
3692                details: e.to_string(),
3693            })?;
3694        Ok(out.values)
3695    })
3696}
3697
3698fn compute_nvi_batch(
3699    req: IndicatorBatchRequest<'_>,
3700    output_id: &str,
3701) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3702    expect_value_output("nvi", output_id)?;
3703    let (close, volume) = extract_close_volume_input("nvi", req.data, "close")?;
3704    let kernel = req.kernel.to_non_batch();
3705    collect_f64("nvi", output_id, req.combos, close.len(), |_params| {
3706        let input = NviInput::from_slices(close, volume, NviParams::default());
3707        let out =
3708            nvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3709                indicator: "nvi".to_string(),
3710                details: e.to_string(),
3711            })?;
3712        Ok(out.values)
3713    })
3714}
3715
3716fn compute_pvi_batch(
3717    req: IndicatorBatchRequest<'_>,
3718    output_id: &str,
3719) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3720    expect_value_output("pvi", output_id)?;
3721    let (close, volume) = extract_close_volume_input("pvi", req.data, "close")?;
3722    let kernel = req.kernel.to_non_batch();
3723    collect_f64("pvi", output_id, req.combos, close.len(), |params| {
3724        let initial_value = get_f64_param("pvi", params, "initial_value", 1000.0)?;
3725        let input = PviInput::from_slices(
3726            close,
3727            volume,
3728            PviParams {
3729                initial_value: Some(initial_value),
3730            },
3731        );
3732        let out =
3733            pvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3734                indicator: "pvi".to_string(),
3735                details: e.to_string(),
3736            })?;
3737        Ok(out.values)
3738    })
3739}
3740
3741fn compute_wclprice_batch(
3742    req: IndicatorBatchRequest<'_>,
3743    output_id: &str,
3744) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3745    expect_value_output("wclprice", output_id)?;
3746    let (high, low, close) = extract_ohlc_input("wclprice", req.data)?;
3747    let kernel = req.kernel.to_non_batch();
3748    collect_f64("wclprice", output_id, req.combos, close.len(), |_params| {
3749        let input = WclpriceInput::from_slices(high, low, close);
3750        let out = wclprice_with_kernel(&input, kernel).map_err(|e| {
3751            IndicatorDispatchError::ComputeFailed {
3752                indicator: "wclprice".to_string(),
3753                details: e.to_string(),
3754            }
3755        })?;
3756        Ok(out.values)
3757    })
3758}
3759
3760fn compute_ui_batch(
3761    req: IndicatorBatchRequest<'_>,
3762    output_id: &str,
3763) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3764    expect_value_output("ui", output_id)?;
3765    let data = extract_slice_input("ui", req.data, "close")?;
3766    let kernel = req.kernel.to_non_batch();
3767    collect_f64("ui", output_id, req.combos, data.len(), |params| {
3768        let period = get_usize_param("ui", params, "period", 14)?;
3769        let scalar = get_f64_param("ui", params, "scalar", 100.0)?;
3770        let input = UiInput::from_slice(
3771            data,
3772            UiParams {
3773                period: Some(period),
3774                scalar: Some(scalar),
3775            },
3776        );
3777        let out =
3778            ui_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3779                indicator: "ui".to_string(),
3780                details: e.to_string(),
3781            })?;
3782        Ok(out.values)
3783    })
3784}
3785
3786fn compute_zscore_batch(
3787    req: IndicatorBatchRequest<'_>,
3788    output_id: &str,
3789) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3790    expect_value_output("zscore", output_id)?;
3791    let data = extract_slice_input("zscore", req.data, "close")?;
3792    let kernel = req.kernel.to_non_batch();
3793    collect_f64("zscore", output_id, req.combos, data.len(), |params| {
3794        let period = get_usize_param("zscore", params, "period", 14)?;
3795        let ma_type = get_enum_param("zscore", params, "ma_type", "sma")?;
3796        let nbdev = get_f64_param("zscore", params, "nbdev", 1.0)?;
3797        let devtype = get_usize_param("zscore", params, "devtype", 0)?;
3798        let input = ZscoreInput::from_slice(
3799            data,
3800            ZscoreParams {
3801                period: Some(period),
3802                ma_type: Some(ma_type),
3803                nbdev: Some(nbdev),
3804                devtype: Some(devtype),
3805            },
3806        );
3807        let out = zscore_with_kernel(&input, kernel).map_err(|e| {
3808            IndicatorDispatchError::ComputeFailed {
3809                indicator: "zscore".to_string(),
3810                details: e.to_string(),
3811            }
3812        })?;
3813        Ok(out.values)
3814    })
3815}
3816
3817fn compute_medprice_batch(
3818    req: IndicatorBatchRequest<'_>,
3819    output_id: &str,
3820) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3821    expect_value_output("medprice", output_id)?;
3822    let (high, low) = extract_high_low_input("medprice", req.data)?;
3823    let kernel = req.kernel.to_non_batch();
3824    collect_f64("medprice", output_id, req.combos, high.len(), |_params| {
3825        let input = MedpriceInput::from_slices(high, low, MedpriceParams::default());
3826        let out = medprice_with_kernel(&input, kernel).map_err(|e| {
3827            IndicatorDispatchError::ComputeFailed {
3828                indicator: "medprice".to_string(),
3829                details: e.to_string(),
3830            }
3831        })?;
3832        Ok(out.values)
3833    })
3834}
3835
3836fn compute_midpoint_batch(
3837    req: IndicatorBatchRequest<'_>,
3838    output_id: &str,
3839) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3840    expect_value_output("midpoint", output_id)?;
3841    let data = extract_slice_input("midpoint", req.data, "close")?;
3842    let kernel = req.kernel.to_non_batch();
3843    collect_f64("midpoint", output_id, req.combos, data.len(), |params| {
3844        let period = get_usize_param("midpoint", params, "period", 14)?;
3845        let input = MidpointInput::from_slice(
3846            data,
3847            MidpointParams {
3848                period: Some(period),
3849            },
3850        );
3851        let out = midpoint_with_kernel(&input, kernel).map_err(|e| {
3852            IndicatorDispatchError::ComputeFailed {
3853                indicator: "midpoint".to_string(),
3854                details: e.to_string(),
3855            }
3856        })?;
3857        Ok(out.values)
3858    })
3859}
3860
3861fn compute_midprice_batch(
3862    req: IndicatorBatchRequest<'_>,
3863    output_id: &str,
3864) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3865    expect_value_output("midprice", output_id)?;
3866    let (high, low) = extract_high_low_input("midprice", req.data)?;
3867    let kernel = req.kernel.to_non_batch();
3868    collect_f64("midprice", output_id, req.combos, high.len(), |params| {
3869        let period = get_usize_param("midprice", params, "period", 14)?;
3870        let input = MidpriceInput::from_slices(
3871            high,
3872            low,
3873            MidpriceParams {
3874                period: Some(period),
3875            },
3876        );
3877        let out = midprice_with_kernel(&input, kernel).map_err(|e| {
3878            IndicatorDispatchError::ComputeFailed {
3879                indicator: "midprice".to_string(),
3880                details: e.to_string(),
3881            }
3882        })?;
3883        Ok(out.values)
3884    })
3885}
3886
3887fn compute_mom_batch(
3888    req: IndicatorBatchRequest<'_>,
3889    output_id: &str,
3890) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3891    expect_value_output("mom", output_id)?;
3892    let data = extract_slice_input("mom", req.data, "close")?;
3893    let kernel = req.kernel.to_non_batch();
3894    collect_f64("mom", output_id, req.combos, data.len(), |params| {
3895        let period = get_usize_param("mom", params, "period", 10)?;
3896        let input = MomInput::from_slice(
3897            data,
3898            MomParams {
3899                period: Some(period),
3900            },
3901        );
3902        let out =
3903            mom_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
3904                indicator: "mom".to_string(),
3905                details: e.to_string(),
3906            })?;
3907        Ok(out.values)
3908    })
3909}
3910
3911fn compute_velocity_batch(
3912    req: IndicatorBatchRequest<'_>,
3913    output_id: &str,
3914) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3915    expect_value_output("velocity", output_id)?;
3916    let data = extract_slice_input("velocity", req.data, "hlcc4")?;
3917    let kernel = req.kernel.to_non_batch();
3918    collect_f64("velocity", output_id, req.combos, data.len(), |params| {
3919        let length = get_usize_param("velocity", params, "length", 21)?;
3920        let smooth_length = get_usize_param("velocity", params, "smooth_length", 5)?;
3921        let input = VelocityInput::from_slice(
3922            data,
3923            VelocityParams {
3924                length: Some(length),
3925                smooth_length: Some(smooth_length),
3926            },
3927        );
3928        let out = velocity_with_kernel(&input, kernel).map_err(|e| {
3929            IndicatorDispatchError::ComputeFailed {
3930                indicator: "velocity".to_string(),
3931                details: e.to_string(),
3932            }
3933        })?;
3934        Ok(out.values)
3935    })
3936}
3937
3938fn compute_adaptive_momentum_oscillator_batch(
3939    req: IndicatorBatchRequest<'_>,
3940    output_id: &str,
3941) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3942    let data = extract_slice_input("adaptive_momentum_oscillator", req.data, "close")?;
3943    let kernel = req.kernel.to_non_batch();
3944    collect_f64(
3945        "adaptive_momentum_oscillator",
3946        output_id,
3947        req.combos,
3948        data.len(),
3949        |params| {
3950            let length = get_usize_param("adaptive_momentum_oscillator", params, "length", 14)?;
3951            let smoothing_length = get_usize_param(
3952                "adaptive_momentum_oscillator",
3953                params,
3954                "smoothing_length",
3955                9,
3956            )?;
3957            let input = AdaptiveMomentumOscillatorInput::from_slice(
3958                data,
3959                AdaptiveMomentumOscillatorParams {
3960                    length: Some(length),
3961                    smoothing_length: Some(smoothing_length),
3962                },
3963            );
3964            let out = adaptive_momentum_oscillator_with_kernel(&input, kernel).map_err(|e| {
3965                IndicatorDispatchError::ComputeFailed {
3966                    indicator: "adaptive_momentum_oscillator".to_string(),
3967                    details: e.to_string(),
3968                }
3969            })?;
3970            match output_id {
3971                "amo" | "value" => Ok(out.amo),
3972                "ama" => Ok(out.ama),
3973                other => Err(IndicatorDispatchError::UnknownOutput {
3974                    indicator: "adaptive_momentum_oscillator".to_string(),
3975                    output: other.to_string(),
3976                }),
3977            }
3978        },
3979    )
3980}
3981
3982fn compute_normalized_volume_true_range_batch(
3983    req: IndicatorBatchRequest<'_>,
3984    output_id: &str,
3985) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
3986    let (open, high, low, close, volume) =
3987        extract_ohlcv_full_input("normalized_volume_true_range", req.data)?;
3988    let kernel = req.kernel.to_non_batch();
3989    collect_f64(
3990        "normalized_volume_true_range",
3991        output_id,
3992        req.combos,
3993        close.len(),
3994        |params| {
3995            let true_range_style = match find_param(params, "true_range_style") {
3996                Some(ParamValue::EnumString(value)) => Some(
3997                    value
3998                        .parse::<NormalizedVolumeTrueRangeStyle>()
3999                        .map_err(|e| IndicatorDispatchError::InvalidParam {
4000                            indicator: "normalized_volume_true_range".to_string(),
4001                            key: "true_range_style".to_string(),
4002                            reason: e,
4003                        })?,
4004                ),
4005                Some(_) => {
4006                    return Err(IndicatorDispatchError::InvalidParam {
4007                        indicator: "normalized_volume_true_range".to_string(),
4008                        key: "true_range_style".to_string(),
4009                        reason: "expected enum string".to_string(),
4010                    });
4011                }
4012                None => Some(NormalizedVolumeTrueRangeStyle::Body),
4013            };
4014            let outlier_range =
4015                get_f64_param("normalized_volume_true_range", params, "outlier_range", 5.0)?;
4016            let atr_length =
4017                get_usize_param("normalized_volume_true_range", params, "atr_length", 14)?;
4018            let volume_length =
4019                get_usize_param("normalized_volume_true_range", params, "volume_length", 14)?;
4020
4021            let input = NormalizedVolumeTrueRangeInput::from_slices(
4022                open,
4023                high,
4024                low,
4025                close,
4026                volume,
4027                NormalizedVolumeTrueRangeParams {
4028                    true_range_style,
4029                    outlier_range: Some(outlier_range),
4030                    atr_length: Some(atr_length),
4031                    volume_length: Some(volume_length),
4032                },
4033            );
4034            let out = normalized_volume_true_range_with_kernel(&input, kernel).map_err(|e| {
4035                IndicatorDispatchError::ComputeFailed {
4036                    indicator: "normalized_volume_true_range".to_string(),
4037                    details: e.to_string(),
4038                }
4039            })?;
4040            if output_id.eq_ignore_ascii_case("normalized_volume")
4041                || output_id.eq_ignore_ascii_case("value")
4042            {
4043                return Ok(out.normalized_volume);
4044            }
4045            if output_id.eq_ignore_ascii_case("normalized_true_range") {
4046                return Ok(out.normalized_true_range);
4047            }
4048            if output_id.eq_ignore_ascii_case("baseline") {
4049                return Ok(out.baseline);
4050            }
4051            if output_id.eq_ignore_ascii_case("atr") {
4052                return Ok(out.atr);
4053            }
4054            if output_id.eq_ignore_ascii_case("average_volume") {
4055                return Ok(out.average_volume);
4056            }
4057            Err(IndicatorDispatchError::UnknownOutput {
4058                indicator: "normalized_volume_true_range".to_string(),
4059                output: output_id.to_string(),
4060            })
4061        },
4062    )
4063}
4064
4065fn compute_range_breakout_signals_batch(
4066    req: IndicatorBatchRequest<'_>,
4067    output_id: &str,
4068) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4069    let (open, high, low, close, volume) =
4070        extract_ohlcv_full_input("range_breakout_signals", req.data)?;
4071    let kernel = req.kernel.to_non_batch();
4072    collect_f64(
4073        "range_breakout_signals",
4074        output_id,
4075        req.combos,
4076        close.len(),
4077        |params| {
4078            let range_length =
4079                get_usize_param("range_breakout_signals", params, "range_length", 20)?;
4080            let confirmation_length =
4081                get_usize_param("range_breakout_signals", params, "confirmation_length", 5)?;
4082            let input = RangeBreakoutSignalsInput::from_slices(
4083                open,
4084                high,
4085                low,
4086                close,
4087                volume,
4088                RangeBreakoutSignalsParams {
4089                    range_length: Some(range_length),
4090                    confirmation_length: Some(confirmation_length),
4091                },
4092            );
4093            let out = range_breakout_signals_with_kernel(&input, kernel).map_err(|e| {
4094                IndicatorDispatchError::ComputeFailed {
4095                    indicator: "range_breakout_signals".to_string(),
4096                    details: e.to_string(),
4097                }
4098            })?;
4099            if output_id.eq_ignore_ascii_case("range_top")
4100                || output_id.eq_ignore_ascii_case("value")
4101            {
4102                return Ok(out.range_top);
4103            }
4104            if output_id.eq_ignore_ascii_case("range_bottom") {
4105                return Ok(out.range_bottom);
4106            }
4107            if output_id.eq_ignore_ascii_case("bullish") {
4108                return Ok(out.bullish);
4109            }
4110            if output_id.eq_ignore_ascii_case("extra_bullish") {
4111                return Ok(out.extra_bullish);
4112            }
4113            if output_id.eq_ignore_ascii_case("bearish") {
4114                return Ok(out.bearish);
4115            }
4116            if output_id.eq_ignore_ascii_case("extra_bearish") {
4117                return Ok(out.extra_bearish);
4118            }
4119            Err(IndicatorDispatchError::UnknownOutput {
4120                indicator: "range_breakout_signals".to_string(),
4121                output: output_id.to_string(),
4122            })
4123        },
4124    )
4125}
4126
4127fn compute_exponential_trend_batch(
4128    req: IndicatorBatchRequest<'_>,
4129    output_id: &str,
4130) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4131    let (high, low, close) = extract_ohlc_input("exponential_trend", req.data)?;
4132    let kernel = req.kernel.to_non_batch();
4133    collect_f64(
4134        "exponential_trend",
4135        output_id,
4136        req.combos,
4137        close.len(),
4138        |params| {
4139            let exp_rate = get_f64_param("exponential_trend", params, "exp_rate", 0.00003)?;
4140            let initial_distance =
4141                get_f64_param("exponential_trend", params, "initial_distance", 4.0)?;
4142            let width_multiplier =
4143                get_f64_param("exponential_trend", params, "width_multiplier", 1.0)?;
4144            let input = ExponentialTrendInput::from_slices(
4145                high,
4146                low,
4147                close,
4148                ExponentialTrendParams {
4149                    exp_rate: Some(exp_rate),
4150                    initial_distance: Some(initial_distance),
4151                    width_multiplier: Some(width_multiplier),
4152                },
4153            );
4154            let out = exponential_trend_with_kernel(&input, kernel).map_err(|e| {
4155                IndicatorDispatchError::ComputeFailed {
4156                    indicator: "exponential_trend".to_string(),
4157                    details: e.to_string(),
4158                }
4159            })?;
4160            if output_id.eq_ignore_ascii_case("uptrend_base")
4161                || output_id.eq_ignore_ascii_case("value")
4162            {
4163                return Ok(out.uptrend_base);
4164            }
4165            if output_id.eq_ignore_ascii_case("downtrend_base") {
4166                return Ok(out.downtrend_base);
4167            }
4168            if output_id.eq_ignore_ascii_case("uptrend_extension") {
4169                return Ok(out.uptrend_extension);
4170            }
4171            if output_id.eq_ignore_ascii_case("downtrend_extension") {
4172                return Ok(out.downtrend_extension);
4173            }
4174            if output_id.eq_ignore_ascii_case("bullish_change") {
4175                return Ok(out.bullish_change);
4176            }
4177            if output_id.eq_ignore_ascii_case("bearish_change") {
4178                return Ok(out.bearish_change);
4179            }
4180            Err(IndicatorDispatchError::UnknownOutput {
4181                indicator: "exponential_trend".to_string(),
4182                output: output_id.to_string(),
4183            })
4184        },
4185    )
4186}
4187
4188fn compute_trend_flow_trail_batch(
4189    req: IndicatorBatchRequest<'_>,
4190    output_id: &str,
4191) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4192    let (open, high, low, close, volume) = extract_ohlcv_full_input("trend_flow_trail", req.data)?;
4193    let kernel = req.kernel.to_non_batch();
4194    collect_f64(
4195        "trend_flow_trail",
4196        output_id,
4197        req.combos,
4198        close.len(),
4199        |params| {
4200            let alpha_length = get_usize_param("trend_flow_trail", params, "alpha_length", 33)?;
4201            let alpha_multiplier =
4202                get_f64_param("trend_flow_trail", params, "alpha_multiplier", 3.3)?;
4203            let mfi_length = get_usize_param("trend_flow_trail", params, "mfi_length", 14)?;
4204            let input = TrendFlowTrailInput::from_slices(
4205                open,
4206                high,
4207                low,
4208                close,
4209                volume,
4210                TrendFlowTrailParams {
4211                    alpha_length: Some(alpha_length),
4212                    alpha_multiplier: Some(alpha_multiplier),
4213                    mfi_length: Some(mfi_length),
4214                },
4215            );
4216            let out = trend_flow_trail_with_kernel(&input, kernel).map_err(|e| {
4217                IndicatorDispatchError::ComputeFailed {
4218                    indicator: "trend_flow_trail".to_string(),
4219                    details: e.to_string(),
4220                }
4221            })?;
4222            if output_id.eq_ignore_ascii_case("alpha_trail")
4223                || output_id.eq_ignore_ascii_case("value")
4224            {
4225                return Ok(out.alpha_trail);
4226            }
4227            if output_id.eq_ignore_ascii_case("alpha_trail_bullish") {
4228                return Ok(out.alpha_trail_bullish);
4229            }
4230            if output_id.eq_ignore_ascii_case("alpha_trail_bearish") {
4231                return Ok(out.alpha_trail_bearish);
4232            }
4233            if output_id.eq_ignore_ascii_case("alpha_dir") {
4234                return Ok(out.alpha_dir);
4235            }
4236            if output_id.eq_ignore_ascii_case("mfi") {
4237                return Ok(out.mfi);
4238            }
4239            if output_id.eq_ignore_ascii_case("tp_upper") {
4240                return Ok(out.tp_upper);
4241            }
4242            if output_id.eq_ignore_ascii_case("tp_lower") {
4243                return Ok(out.tp_lower);
4244            }
4245            if output_id.eq_ignore_ascii_case("alpha_trail_bullish_switch") {
4246                return Ok(out.alpha_trail_bullish_switch);
4247            }
4248            if output_id.eq_ignore_ascii_case("alpha_trail_bearish_switch") {
4249                return Ok(out.alpha_trail_bearish_switch);
4250            }
4251            if output_id.eq_ignore_ascii_case("mfi_overbought") {
4252                return Ok(out.mfi_overbought);
4253            }
4254            if output_id.eq_ignore_ascii_case("mfi_oversold") {
4255                return Ok(out.mfi_oversold);
4256            }
4257            if output_id.eq_ignore_ascii_case("mfi_cross_up_mid") {
4258                return Ok(out.mfi_cross_up_mid);
4259            }
4260            if output_id.eq_ignore_ascii_case("mfi_cross_down_mid") {
4261                return Ok(out.mfi_cross_down_mid);
4262            }
4263            if output_id.eq_ignore_ascii_case("price_cross_alpha_trail_up") {
4264                return Ok(out.price_cross_alpha_trail_up);
4265            }
4266            if output_id.eq_ignore_ascii_case("price_cross_alpha_trail_down") {
4267                return Ok(out.price_cross_alpha_trail_down);
4268            }
4269            if output_id.eq_ignore_ascii_case("mfi_above_90") {
4270                return Ok(out.mfi_above_90);
4271            }
4272            if output_id.eq_ignore_ascii_case("mfi_below_10") {
4273                return Ok(out.mfi_below_10);
4274            }
4275            Err(IndicatorDispatchError::UnknownOutput {
4276                indicator: "trend_flow_trail".to_string(),
4277                output: output_id.to_string(),
4278            })
4279        },
4280    )
4281}
4282
4283fn compute_cmo_batch(
4284    req: IndicatorBatchRequest<'_>,
4285    output_id: &str,
4286) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4287    expect_value_output("cmo", output_id)?;
4288    let data = extract_slice_input("cmo", req.data, "close")?;
4289    let kernel = req.kernel.to_non_batch();
4290    collect_f64("cmo", output_id, req.combos, data.len(), |params| {
4291        let period = get_usize_param("cmo", params, "period", 14)?;
4292        let input = CmoInput::from_slice(
4293            data,
4294            CmoParams {
4295                period: Some(period),
4296            },
4297        );
4298        let out =
4299            cmo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4300                indicator: "cmo".to_string(),
4301                details: e.to_string(),
4302            })?;
4303        Ok(out.values)
4304    })
4305}
4306
4307fn compute_rocp_batch(
4308    req: IndicatorBatchRequest<'_>,
4309    output_id: &str,
4310) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4311    expect_value_output("rocp", output_id)?;
4312    let data = extract_slice_input("rocp", req.data, "close")?;
4313    let kernel = req.kernel.to_non_batch();
4314    collect_f64("rocp", output_id, req.combos, data.len(), |params| {
4315        let period = get_usize_param("rocp", params, "period", 10)?;
4316        let input = RocpInput::from_slice(
4317            data,
4318            RocpParams {
4319                period: Some(period),
4320            },
4321        );
4322        let out = rocp_with_kernel(&input, kernel).map_err(|e| {
4323            IndicatorDispatchError::ComputeFailed {
4324                indicator: "rocp".to_string(),
4325                details: e.to_string(),
4326            }
4327        })?;
4328        Ok(out.values)
4329    })
4330}
4331
4332fn compute_rocr_batch(
4333    req: IndicatorBatchRequest<'_>,
4334    output_id: &str,
4335) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4336    expect_value_output("rocr", output_id)?;
4337    let data = extract_slice_input("rocr", req.data, "close")?;
4338    let kernel = req.kernel.to_non_batch();
4339    collect_f64("rocr", output_id, req.combos, data.len(), |params| {
4340        let period = get_usize_param("rocr", params, "period", 10)?;
4341        let input = RocrInput::from_slice(
4342            data,
4343            RocrParams {
4344                period: Some(period),
4345            },
4346        );
4347        let out = rocr_with_kernel(&input, kernel).map_err(|e| {
4348            IndicatorDispatchError::ComputeFailed {
4349                indicator: "rocr".to_string(),
4350                details: e.to_string(),
4351            }
4352        })?;
4353        Ok(out.values)
4354    })
4355}
4356
4357fn compute_ppo_batch(
4358    req: IndicatorBatchRequest<'_>,
4359    output_id: &str,
4360) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4361    expect_value_output("ppo", output_id)?;
4362    let data = extract_slice_input("ppo", req.data, "close")?;
4363    let kernel = req.kernel.to_non_batch();
4364    collect_f64("ppo", output_id, req.combos, data.len(), |params| {
4365        let fast_period = get_usize_param("ppo", params, "fast_period", 12)?;
4366        let slow_period = get_usize_param("ppo", params, "slow_period", 26)?;
4367        let ma_type = get_enum_param("ppo", params, "ma_type", "sma")?;
4368        let input = PpoInput::from_slice(
4369            data,
4370            PpoParams {
4371                fast_period: Some(fast_period),
4372                slow_period: Some(slow_period),
4373                ma_type: Some(ma_type),
4374            },
4375        );
4376        let out =
4377            ppo_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4378                indicator: "ppo".to_string(),
4379                details: e.to_string(),
4380            })?;
4381        Ok(out.values)
4382    })
4383}
4384
4385fn compute_trix_batch(
4386    req: IndicatorBatchRequest<'_>,
4387    output_id: &str,
4388) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4389    expect_value_output("trix", output_id)?;
4390    let data = extract_slice_input("trix", req.data, "close")?;
4391    let periods = combo_periods("trix", req.combos, "period", 18)?;
4392    if let Some((start, end, step)) = derive_period_sweep(&periods) {
4393        let out = trix_batch_with_kernel(
4394            data,
4395            &TrixBatchRange {
4396                period: (start, end, step),
4397            },
4398            to_batch_kernel(req.kernel),
4399        )
4400        .map_err(|e| IndicatorDispatchError::ComputeFailed {
4401            indicator: "trix".to_string(),
4402            details: e.to_string(),
4403        })?;
4404        ensure_len("trix", data.len(), out.cols)?;
4405        let produced_periods: Vec<usize> = out
4406            .combos
4407            .iter()
4408            .map(|combo| combo.period.unwrap_or(18))
4409            .collect();
4410        let values = reorder_or_take_f64_matrix_by_period(
4411            "trix",
4412            &periods,
4413            &produced_periods,
4414            out.cols,
4415            out.values,
4416        )?;
4417        return Ok(f64_output(output_id, periods.len(), out.cols, values));
4418    }
4419
4420    let kernel = req.kernel.to_non_batch();
4421    collect_f64_into_rows("trix", output_id, req.combos, data.len(), |params, row| {
4422        let period = get_usize_param("trix", params, "period", 18)?;
4423        let input = TrixInput::from_slice(
4424            data,
4425            TrixParams {
4426                period: Some(period),
4427            },
4428        );
4429        trix_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4430            indicator: "trix".to_string(),
4431            details: e.to_string(),
4432        })
4433    })
4434}
4435
4436fn compute_tsi_batch(
4437    req: IndicatorBatchRequest<'_>,
4438    output_id: &str,
4439) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4440    expect_value_output("tsi", output_id)?;
4441    let data = extract_slice_input("tsi", req.data, "close")?;
4442    let kernel = req.kernel.to_non_batch();
4443    collect_f64("tsi", output_id, req.combos, data.len(), |params| {
4444        let long_period = get_usize_param("tsi", params, "long_period", 25)?;
4445        let short_period = get_usize_param("tsi", params, "short_period", 13)?;
4446        let input = TsiInput::from_slice(
4447            data,
4448            TsiParams {
4449                long_period: Some(long_period),
4450                short_period: Some(short_period),
4451            },
4452        );
4453        let out =
4454            tsi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4455                indicator: "tsi".to_string(),
4456                details: e.to_string(),
4457            })?;
4458        Ok(out.values)
4459    })
4460}
4461
4462fn compute_tsf_batch(
4463    req: IndicatorBatchRequest<'_>,
4464    output_id: &str,
4465) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4466    expect_value_output("tsf", output_id)?;
4467    let data = extract_slice_input("tsf", req.data, "close")?;
4468    let kernel = req.kernel.to_non_batch();
4469    collect_f64("tsf", output_id, req.combos, data.len(), |params| {
4470        let period = get_usize_param("tsf", params, "period", 14)?;
4471        let input = TsfInput::from_slice(
4472            data,
4473            TsfParams {
4474                period: Some(period),
4475            },
4476        );
4477        let out =
4478            tsf_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
4479                indicator: "tsf".to_string(),
4480                details: e.to_string(),
4481            })?;
4482        Ok(out.values)
4483    })
4484}
4485
4486fn compute_polynomial_regression_extrapolation_batch(
4487    req: IndicatorBatchRequest<'_>,
4488    output_id: &str,
4489) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4490    expect_value_output("polynomial_regression_extrapolation", output_id)?;
4491    let data = extract_slice_input("polynomial_regression_extrapolation", req.data, "close")?;
4492    let kernel = req.kernel.to_non_batch();
4493    collect_f64(
4494        "polynomial_regression_extrapolation",
4495        output_id,
4496        req.combos,
4497        data.len(),
4498        |params| {
4499            let length =
4500                get_usize_param("polynomial_regression_extrapolation", params, "length", 100)?;
4501            let extrapolate = get_usize_param(
4502                "polynomial_regression_extrapolation",
4503                params,
4504                "extrapolate",
4505                10,
4506            )?;
4507            let degree =
4508                get_usize_param("polynomial_regression_extrapolation", params, "degree", 3)?;
4509            let input = PolynomialRegressionExtrapolationInput::from_slice(
4510                data,
4511                PolynomialRegressionExtrapolationParams {
4512                    length: Some(length),
4513                    extrapolate: Some(extrapolate),
4514                    degree: Some(degree),
4515                },
4516            );
4517            let out =
4518                polynomial_regression_extrapolation_with_kernel(&input, kernel).map_err(|e| {
4519                    IndicatorDispatchError::ComputeFailed {
4520                        indicator: "polynomial_regression_extrapolation".to_string(),
4521                        details: e.to_string(),
4522                    }
4523                })?;
4524            Ok(out.values)
4525        },
4526    )
4527}
4528
4529fn compute_adaptive_macd_batch(
4530    req: IndicatorBatchRequest<'_>,
4531    output_id: &str,
4532) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4533    let data = extract_slice_input("adaptive_macd", req.data, "close")?;
4534    let kernel = req.kernel.to_non_batch();
4535    collect_f64(
4536        "adaptive_macd",
4537        output_id,
4538        req.combos,
4539        data.len(),
4540        |params| {
4541            let length = get_usize_param("adaptive_macd", params, "length", 20)?;
4542            let fast_period = get_usize_param("adaptive_macd", params, "fast_period", 10)?;
4543            let slow_period = get_usize_param("adaptive_macd", params, "slow_period", 20)?;
4544            let signal_period = get_usize_param("adaptive_macd", params, "signal_period", 9)?;
4545            let input = AdaptiveMacdInput::from_slice(
4546                data,
4547                AdaptiveMacdParams {
4548                    length: Some(length),
4549                    fast_period: Some(fast_period),
4550                    slow_period: Some(slow_period),
4551                    signal_period: Some(signal_period),
4552                },
4553            );
4554            let out = adaptive_macd_with_kernel(&input, kernel).map_err(|e| {
4555                IndicatorDispatchError::ComputeFailed {
4556                    indicator: "adaptive_macd".to_string(),
4557                    details: e.to_string(),
4558                }
4559            })?;
4560            if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
4561                return Ok(out.macd);
4562            }
4563            if output_id.eq_ignore_ascii_case("signal") {
4564                return Ok(out.signal);
4565            }
4566            if output_id.eq_ignore_ascii_case("hist") {
4567                return Ok(out.hist);
4568            }
4569            Err(IndicatorDispatchError::UnknownOutput {
4570                indicator: "adaptive_macd".to_string(),
4571                output: output_id.to_string(),
4572            })
4573        },
4574    )
4575}
4576
4577fn compute_statistical_trailing_stop_batch(
4578    req: IndicatorBatchRequest<'_>,
4579    output_id: &str,
4580) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4581    let (high, low, close) = extract_ohlc_input("statistical_trailing_stop", req.data)?;
4582    let kernel = req.kernel.to_non_batch();
4583    collect_f64(
4584        "statistical_trailing_stop",
4585        output_id,
4586        req.combos,
4587        close.len(),
4588        |params| {
4589            let data_length =
4590                get_usize_param("statistical_trailing_stop", params, "data_length", 10)?;
4591            let normalization_length = get_usize_param(
4592                "statistical_trailing_stop",
4593                params,
4594                "normalization_length",
4595                100,
4596            )?;
4597            let base_level =
4598                get_enum_param("statistical_trailing_stop", params, "base_level", "level2")?;
4599            let input = StatisticalTrailingStopInput::from_slices(
4600                high,
4601                low,
4602                close,
4603                StatisticalTrailingStopParams {
4604                    data_length: Some(data_length),
4605                    normalization_length: Some(normalization_length),
4606                    base_level: Some(base_level),
4607                },
4608            );
4609            let out = statistical_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
4610                IndicatorDispatchError::ComputeFailed {
4611                    indicator: "statistical_trailing_stop".to_string(),
4612                    details: e.to_string(),
4613                }
4614            })?;
4615            if output_id.eq_ignore_ascii_case("level") || output_id.eq_ignore_ascii_case("value") {
4616                return Ok(out.level);
4617            }
4618            if output_id.eq_ignore_ascii_case("anchor") {
4619                return Ok(out.anchor);
4620            }
4621            if output_id.eq_ignore_ascii_case("bias") {
4622                return Ok(out.bias);
4623            }
4624            if output_id.eq_ignore_ascii_case("changed") {
4625                return Ok(out.changed);
4626            }
4627            Err(IndicatorDispatchError::UnknownOutput {
4628                indicator: "statistical_trailing_stop".to_string(),
4629                output: output_id.to_string(),
4630            })
4631        },
4632    )
4633}
4634
4635fn compute_supertrend_recovery_batch(
4636    req: IndicatorBatchRequest<'_>,
4637    output_id: &str,
4638) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4639    let (high, low, close) = extract_ohlc_input("supertrend_recovery", req.data)?;
4640    let kernel = req.kernel.to_non_batch();
4641    collect_f64(
4642        "supertrend_recovery",
4643        output_id,
4644        req.combos,
4645        close.len(),
4646        |params| {
4647            let atr_length = get_usize_param("supertrend_recovery", params, "atr_length", 10)?;
4648            let multiplier = get_f64_param("supertrend_recovery", params, "multiplier", 3.0)?;
4649            let alpha_percent = get_f64_param("supertrend_recovery", params, "alpha_percent", 5.0)?;
4650            let threshold_atr = get_f64_param("supertrend_recovery", params, "threshold_atr", 1.0)?;
4651            let input = SuperTrendRecoveryInput::from_slices(
4652                high,
4653                low,
4654                close,
4655                SuperTrendRecoveryParams {
4656                    atr_length: Some(atr_length),
4657                    multiplier: Some(multiplier),
4658                    alpha_percent: Some(alpha_percent),
4659                    threshold_atr: Some(threshold_atr),
4660                },
4661            );
4662            let out = supertrend_recovery_with_kernel(&input, kernel).map_err(|e| {
4663                IndicatorDispatchError::ComputeFailed {
4664                    indicator: "supertrend_recovery".to_string(),
4665                    details: e.to_string(),
4666                }
4667            })?;
4668            if output_id.eq_ignore_ascii_case("band") || output_id.eq_ignore_ascii_case("value") {
4669                return Ok(out.band);
4670            }
4671            if output_id.eq_ignore_ascii_case("switch_price") {
4672                return Ok(out.switch_price);
4673            }
4674            if output_id.eq_ignore_ascii_case("trend") {
4675                return Ok(out.trend);
4676            }
4677            if output_id.eq_ignore_ascii_case("changed") {
4678                return Ok(out.changed);
4679            }
4680            Err(IndicatorDispatchError::UnknownOutput {
4681                indicator: "supertrend_recovery".to_string(),
4682                output: output_id.to_string(),
4683            })
4684        },
4685    )
4686}
4687
4688fn compute_standardized_psar_oscillator_batch(
4689    req: IndicatorBatchRequest<'_>,
4690    output_id: &str,
4691) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4692    let (high, low, close) = extract_ohlc_input("standardized_psar_oscillator", req.data)?;
4693    let kernel = req.kernel.to_non_batch();
4694    collect_f64(
4695        "standardized_psar_oscillator",
4696        output_id,
4697        req.combos,
4698        close.len(),
4699        |params| {
4700            let start = get_f64_param("standardized_psar_oscillator", params, "start", 0.02)?;
4701            let increment =
4702                get_f64_param("standardized_psar_oscillator", params, "increment", 0.0005)?;
4703            let maximum = get_f64_param("standardized_psar_oscillator", params, "maximum", 0.2)?;
4704            let standardization_length = get_usize_param(
4705                "standardized_psar_oscillator",
4706                params,
4707                "standardization_length",
4708                21,
4709            )?;
4710            let wma_length =
4711                get_usize_param("standardized_psar_oscillator", params, "wma_length", 40)?;
4712            let wma_lag = get_usize_param("standardized_psar_oscillator", params, "wma_lag", 3)?;
4713            let pivot_left =
4714                get_usize_param("standardized_psar_oscillator", params, "pivot_left", 15)?;
4715            let pivot_right =
4716                get_usize_param("standardized_psar_oscillator", params, "pivot_right", 1)?;
4717            let plot_bullish =
4718                get_bool_param("standardized_psar_oscillator", params, "plot_bullish", true)?;
4719            let plot_bearish =
4720                get_bool_param("standardized_psar_oscillator", params, "plot_bearish", true)?;
4721            let input = StandardizedPsarOscillatorInput::from_slices(
4722                high,
4723                low,
4724                close,
4725                StandardizedPsarOscillatorParams {
4726                    start: Some(start),
4727                    increment: Some(increment),
4728                    maximum: Some(maximum),
4729                    standardization_length: Some(standardization_length),
4730                    wma_length: Some(wma_length),
4731                    wma_lag: Some(wma_lag),
4732                    pivot_left: Some(pivot_left),
4733                    pivot_right: Some(pivot_right),
4734                    plot_bullish: Some(plot_bullish),
4735                    plot_bearish: Some(plot_bearish),
4736                },
4737            );
4738            let out = standardized_psar_oscillator_with_kernel(&input, kernel).map_err(|e| {
4739                IndicatorDispatchError::ComputeFailed {
4740                    indicator: "standardized_psar_oscillator".to_string(),
4741                    details: e.to_string(),
4742                }
4743            })?;
4744            match output_id {
4745                "oscillator" | "value" => Ok(out.oscillator),
4746                "ma" => Ok(out.ma),
4747                "bullish_reversal" => Ok(out.bullish_reversal),
4748                "bearish_reversal" => Ok(out.bearish_reversal),
4749                "regular_bullish" => Ok(out.regular_bullish),
4750                "regular_bearish" => Ok(out.regular_bearish),
4751                "bullish_weakening" => Ok(out.bullish_weakening),
4752                "bearish_weakening" => Ok(out.bearish_weakening),
4753                _ => Err(IndicatorDispatchError::UnknownOutput {
4754                    indicator: "standardized_psar_oscillator".to_string(),
4755                    output: output_id.to_string(),
4756                }),
4757            }
4758        },
4759    )
4760}
4761
4762fn compute_geometric_bias_oscillator_batch(
4763    req: IndicatorBatchRequest<'_>,
4764    output_id: &str,
4765) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4766    expect_value_output("geometric_bias_oscillator", output_id)?;
4767    let (high, low, close) = extract_ohlc_input("geometric_bias_oscillator", req.data)?;
4768    let kernel = req.kernel.to_non_batch();
4769    collect_f64(
4770        "geometric_bias_oscillator",
4771        output_id,
4772        req.combos,
4773        close.len(),
4774        |params| {
4775            let length = get_usize_param("geometric_bias_oscillator", params, "length", 100)?;
4776            let multiplier = get_f64_param("geometric_bias_oscillator", params, "multiplier", 2.0)?;
4777            let atr_length =
4778                get_usize_param("geometric_bias_oscillator", params, "atr_length", 14)?;
4779            let smooth = get_usize_param("geometric_bias_oscillator", params, "smooth", 1)?;
4780            let input = GeometricBiasOscillatorInput::from_slices(
4781                high,
4782                low,
4783                close,
4784                GeometricBiasOscillatorParams {
4785                    length: Some(length),
4786                    multiplier: Some(multiplier),
4787                    atr_length: Some(atr_length),
4788                    smooth: Some(smooth),
4789                },
4790            );
4791            let out = geometric_bias_oscillator_with_kernel(&input, kernel).map_err(|e| {
4792                IndicatorDispatchError::ComputeFailed {
4793                    indicator: "geometric_bias_oscillator".to_string(),
4794                    details: e.to_string(),
4795                }
4796            })?;
4797            Ok(out.values)
4798        },
4799    )
4800}
4801
4802fn compute_stddev_batch(
4803    req: IndicatorBatchRequest<'_>,
4804    output_id: &str,
4805) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4806    expect_value_output("stddev", output_id)?;
4807    let data = extract_slice_input("stddev", req.data, "close")?;
4808    let kernel = req.kernel.to_non_batch();
4809    collect_f64("stddev", output_id, req.combos, data.len(), |params| {
4810        let period = get_usize_param("stddev", params, "period", 5)?;
4811        let nbdev = get_f64_param("stddev", params, "nbdev", 1.0)?;
4812        let input = StdDevInput::from_slice(
4813            data,
4814            StdDevParams {
4815                period: Some(period),
4816                nbdev: Some(nbdev),
4817            },
4818        );
4819        let out = stddev_with_kernel(&input, kernel).map_err(|e| {
4820            IndicatorDispatchError::ComputeFailed {
4821                indicator: "stddev".to_string(),
4822                details: e.to_string(),
4823            }
4824        })?;
4825        Ok(out.values)
4826    })
4827}
4828
4829fn compute_vdubus_divergence_wave_pattern_generator_batch(
4830    req: IndicatorBatchRequest<'_>,
4831    output_id: &str,
4832) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
4833    expect_value_output("vdubus_divergence_wave_pattern_generator", output_id)?;
4834    let (high, low, close) =
4835        extract_ohlc_input("vdubus_divergence_wave_pattern_generator", req.data)?;
4836    let kernel = req.kernel.to_non_batch();
4837    collect_f64(
4838        "vdubus_divergence_wave_pattern_generator",
4839        output_id,
4840        req.combos,
4841        close.len(),
4842        |params| {
4843            let fast_depth = get_usize_param(
4844                "vdubus_divergence_wave_pattern_generator",
4845                params,
4846                "fast_depth",
4847                9,
4848            )?;
4849            let slow_depth = get_usize_param(
4850                "vdubus_divergence_wave_pattern_generator",
4851                params,
4852                "slow_depth",
4853                24,
4854            )?;
4855            let fast_length = get_usize_param(
4856                "vdubus_divergence_wave_pattern_generator",
4857                params,
4858                "fast_length",
4859                21,
4860            )?;
4861            let slow_length = get_usize_param(
4862                "vdubus_divergence_wave_pattern_generator",
4863                params,
4864                "slow_length",
4865                34,
4866            )?;
4867            let signal_length = get_usize_param(
4868                "vdubus_divergence_wave_pattern_generator",
4869                params,
4870                "signal_length",
4871                5,
4872            )?;
4873            let lookback = get_usize_param(
4874                "vdubus_divergence_wave_pattern_generator",
4875                params,
4876                "lookback",
4877                3,
4878            )?;
4879            let err_tol = get_f64_param(
4880                "vdubus_divergence_wave_pattern_generator",
4881                params,
4882                "err_tol",
4883                0.15,
4884            )?;
4885            let show_standard = get_bool_param(
4886                "vdubus_divergence_wave_pattern_generator",
4887                params,
4888                "show_standard",
4889                true,
4890            )?;
4891            let show_climax = get_bool_param(
4892                "vdubus_divergence_wave_pattern_generator",
4893                params,
4894                "show_climax",
4895                true,
4896            )?;
4897            let show_rounded = get_bool_param(
4898                "vdubus_divergence_wave_pattern_generator",
4899                params,
4900                "show_rounded",
4901                true,
4902            )?;
4903            let show_predator = get_bool_param(
4904                "vdubus_divergence_wave_pattern_generator",
4905                params,
4906                "show_predator",
4907                true,
4908            )?;
4909            let show_gartley = get_bool_param(
4910                "vdubus_divergence_wave_pattern_generator",
4911                params,
4912                "show_gartley",
4913                false,
4914            )?;
4915            let show_bat = get_bool_param(
4916                "vdubus_divergence_wave_pattern_generator",
4917                params,
4918                "show_bat",
4919                false,
4920            )?;
4921            let show_butterfly = get_bool_param(
4922                "vdubus_divergence_wave_pattern_generator",
4923                params,
4924                "show_butterfly",
4925                false,
4926            )?;
4927            let show_crab = get_bool_param(
4928                "vdubus_divergence_wave_pattern_generator",
4929                params,
4930                "show_crab",
4931                false,
4932            )?;
4933            let show_deep = get_bool_param(
4934                "vdubus_divergence_wave_pattern_generator",
4935                params,
4936                "show_deep",
4937                false,
4938            )?;
4939            let show_hs = get_bool_param(
4940                "vdubus_divergence_wave_pattern_generator",
4941                params,
4942                "show_hs",
4943                true,
4944            )?;
4945            let input = VdubusDivergenceWavePatternGeneratorInput::from_slices(
4946                high,
4947                low,
4948                close,
4949                VdubusDivergenceWavePatternGeneratorParams {
4950                    fast_depth: Some(fast_depth),
4951                    slow_depth: Some(slow_depth),
4952                    fast_length: Some(fast_length),
4953                    slow_length: Some(slow_length),
4954                    signal_length: Some(signal_length),
4955                    lookback: Some(lookback),
4956                    err_tol: Some(err_tol),
4957                    show_standard: Some(show_standard),
4958                    show_climax: Some(show_climax),
4959                    show_rounded: Some(show_rounded),
4960                    show_predator: Some(show_predator),
4961                    show_gartley: Some(show_gartley),
4962                    show_bat: Some(show_bat),
4963                    show_butterfly: Some(show_butterfly),
4964                    show_crab: Some(show_crab),
4965                    show_deep: Some(show_deep),
4966                    show_hs: Some(show_hs),
4967                },
4968            );
4969            let out = vdubus_divergence_wave_pattern_generator_with_kernel(&input, kernel)
4970                .map_err(|e| IndicatorDispatchError::ComputeFailed {
4971                    indicator: "vdubus_divergence_wave_pattern_generator".to_string(),
4972                    details: e.to_string(),
4973                })?;
4974            match output_id {
4975                "fast_standard" => Ok(out.fast_standard),
4976                "fast_climax" => Ok(out.fast_climax),
4977                "fast_rounded" => Ok(out.fast_rounded),
4978                "fast_predator" => Ok(out.fast_predator),
4979                "slow_standard" => Ok(out.slow_standard),
4980                "slow_climax" => Ok(out.slow_climax),
4981                "slow_rounded" => Ok(out.slow_rounded),
4982                "slow_predator" => Ok(out.slow_predator),
4983                "opposing_force" => Ok(out.opposing_force),
4984                "macd" => Ok(out.macd),
4985                "signal" => Ok(out.signal),
4986                "hist" => Ok(out.hist),
4987                _ => Err(IndicatorDispatchError::UnknownOutput {
4988                    indicator: "vdubus_divergence_wave_pattern_generator".to_string(),
4989                    output: output_id.to_string(),
4990                }),
4991            }
4992        },
4993    )
4994}
4995
4996fn compute_var_batch(
4997    req: IndicatorBatchRequest<'_>,
4998    output_id: &str,
4999) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5000    expect_value_output("var", output_id)?;
5001    let data = extract_slice_input("var", req.data, "close")?;
5002    let kernel = req.kernel.to_non_batch();
5003    collect_f64("var", output_id, req.combos, data.len(), |params| {
5004        let period = get_usize_param("var", params, "period", 14)?;
5005        let nbdev = get_f64_param("var", params, "nbdev", 1.0)?;
5006        let input = VarInput::from_slice(
5007            data,
5008            VarParams {
5009                period: Some(period),
5010                nbdev: Some(nbdev),
5011            },
5012        );
5013        let out =
5014            var_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5015                indicator: "var".to_string(),
5016                details: e.to_string(),
5017            })?;
5018        Ok(out.values)
5019    })
5020}
5021
5022fn compute_willr_batch(
5023    req: IndicatorBatchRequest<'_>,
5024    output_id: &str,
5025) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5026    expect_value_output("willr", output_id)?;
5027    let (high, low, close) = extract_ohlc_input("willr", req.data)?;
5028    let kernel = req.kernel.to_non_batch();
5029    collect_f64("willr", output_id, req.combos, close.len(), |params| {
5030        let period = get_usize_param("willr", params, "period", 14)?;
5031        let input = WillrInput::from_slices(
5032            high,
5033            low,
5034            close,
5035            WillrParams {
5036                period: Some(period),
5037            },
5038        );
5039        let out = willr_with_kernel(&input, kernel).map_err(|e| {
5040            IndicatorDispatchError::ComputeFailed {
5041                indicator: "willr".to_string(),
5042                details: e.to_string(),
5043            }
5044        })?;
5045        Ok(out.values)
5046    })
5047}
5048
5049fn compute_ultosc_batch(
5050    req: IndicatorBatchRequest<'_>,
5051    output_id: &str,
5052) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5053    expect_value_output("ultosc", output_id)?;
5054    let (high, low, close) = extract_ohlc_input("ultosc", req.data)?;
5055    let kernel = req.kernel.to_non_batch();
5056    collect_f64("ultosc", output_id, req.combos, close.len(), |params| {
5057        let timeperiod1 = get_usize_param("ultosc", params, "timeperiod1", 7)?;
5058        let timeperiod2 = get_usize_param("ultosc", params, "timeperiod2", 14)?;
5059        let timeperiod3 = get_usize_param("ultosc", params, "timeperiod3", 28)?;
5060        let input = UltOscInput::from_slices(
5061            high,
5062            low,
5063            close,
5064            UltOscParams {
5065                timeperiod1: Some(timeperiod1),
5066                timeperiod2: Some(timeperiod2),
5067                timeperiod3: Some(timeperiod3),
5068            },
5069        );
5070        let out = ultosc_with_kernel(&input, kernel).map_err(|e| {
5071            IndicatorDispatchError::ComputeFailed {
5072                indicator: "ultosc".to_string(),
5073                details: e.to_string(),
5074            }
5075        })?;
5076        Ok(out.values)
5077    })
5078}
5079
5080fn compute_adx_batch(
5081    req: IndicatorBatchRequest<'_>,
5082    output_id: &str,
5083) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5084    expect_value_output("adx", output_id)?;
5085    let (high, low, close) = extract_ohlc_input("adx", req.data)?;
5086    let kernel = req.kernel.to_non_batch();
5087    collect_f64("adx", output_id, req.combos, close.len(), |params| {
5088        let period = get_usize_param("adx", params, "period", 14)?;
5089        let input = AdxInput::from_slices(
5090            high,
5091            low,
5092            close,
5093            AdxParams {
5094                period: Some(period),
5095            },
5096        );
5097        let out =
5098            adx_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5099                indicator: "adx".to_string(),
5100                details: e.to_string(),
5101            })?;
5102        Ok(out.values)
5103    })
5104}
5105
5106fn compute_adxr_batch(
5107    req: IndicatorBatchRequest<'_>,
5108    output_id: &str,
5109) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5110    expect_value_output("adxr", output_id)?;
5111    let (high, low, close) = extract_ohlc_input("adxr", req.data)?;
5112    let kernel = req.kernel.to_non_batch();
5113    collect_f64("adxr", output_id, req.combos, close.len(), |params| {
5114        let period = get_usize_param("adxr", params, "period", 14)?;
5115        let input = AdxrInput::from_slices(
5116            high,
5117            low,
5118            close,
5119            AdxrParams {
5120                period: Some(period),
5121            },
5122        );
5123        let out = adxr_with_kernel(&input, kernel).map_err(|e| {
5124            IndicatorDispatchError::ComputeFailed {
5125                indicator: "adxr".to_string(),
5126                details: e.to_string(),
5127            }
5128        })?;
5129        Ok(out.values)
5130    })
5131}
5132
5133fn compute_atr_batch(
5134    req: IndicatorBatchRequest<'_>,
5135    output_id: &str,
5136) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5137    expect_value_output("atr", output_id)?;
5138    let (high, low, close) = extract_ohlc_input("atr", req.data)?;
5139    let kernel = req.kernel.to_non_batch();
5140    collect_f64("atr", output_id, req.combos, close.len(), |params| {
5141        let length = get_usize_param("atr", params, "length", 14)?;
5142        let input = AtrInput::from_slices(
5143            high,
5144            low,
5145            close,
5146            AtrParams {
5147                length: Some(length),
5148            },
5149        );
5150        let out =
5151            atr_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5152                indicator: "atr".to_string(),
5153                details: e.to_string(),
5154            })?;
5155        Ok(out.values)
5156    })
5157}
5158
5159fn compute_macd_batch(
5160    req: IndicatorBatchRequest<'_>,
5161    output_id: &str,
5162) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5163    let data = extract_slice_input("macd", req.data, "close")?;
5164    let kernel = req.kernel.to_non_batch();
5165    collect_f64("macd", output_id, req.combos, data.len(), |params| {
5166        let fast_period = get_usize_param("macd", params, "fast_period", 12)?;
5167        let slow_period = get_usize_param("macd", params, "slow_period", 26)?;
5168        let signal_period = get_usize_param("macd", params, "signal_period", 9)?;
5169        let ma_type = get_enum_param("macd", params, "ma_type", "ema")?;
5170        let input = MacdInput::from_slice(
5171            data,
5172            MacdParams {
5173                fast_period: Some(fast_period),
5174                slow_period: Some(slow_period),
5175                signal_period: Some(signal_period),
5176                ma_type: Some(ma_type),
5177            },
5178        );
5179        let out = macd_with_kernel(&input, kernel).map_err(|e| {
5180            IndicatorDispatchError::ComputeFailed {
5181                indicator: "macd".to_string(),
5182                details: e.to_string(),
5183            }
5184        })?;
5185        if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
5186            return Ok(out.macd);
5187        }
5188        if output_id.eq_ignore_ascii_case("signal") {
5189            return Ok(out.signal);
5190        }
5191        if output_id.eq_ignore_ascii_case("hist") {
5192            return Ok(out.hist);
5193        }
5194        Err(IndicatorDispatchError::UnknownOutput {
5195            indicator: "macd".to_string(),
5196            output: output_id.to_string(),
5197        })
5198    })
5199}
5200
5201fn compute_bollinger_batch(
5202    req: IndicatorBatchRequest<'_>,
5203    output_id: &str,
5204) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5205    let data = extract_slice_input("bollinger_bands", req.data, "close")?;
5206    let kernel = req.kernel.to_non_batch();
5207    collect_f64(
5208        "bollinger_bands",
5209        output_id,
5210        req.combos,
5211        data.len(),
5212        |params| {
5213            let period = get_usize_param("bollinger_bands", params, "period", 20)?;
5214            let devup = get_f64_param("bollinger_bands", params, "devup", 2.0)?;
5215            let devdn = get_f64_param("bollinger_bands", params, "devdn", 2.0)?;
5216            let matype = get_enum_param("bollinger_bands", params, "matype", "sma")?;
5217            let devtype = get_usize_param("bollinger_bands", params, "devtype", 0)?;
5218            let input = BollingerBandsInput::from_slice(
5219                data,
5220                BollingerBandsParams {
5221                    period: Some(period),
5222                    devup: Some(devup),
5223                    devdn: Some(devdn),
5224                    matype: Some(matype),
5225                    devtype: Some(devtype),
5226                },
5227            );
5228            let out = bollinger_bands_with_kernel(&input, kernel).map_err(|e| {
5229                IndicatorDispatchError::ComputeFailed {
5230                    indicator: "bollinger_bands".to_string(),
5231                    details: e.to_string(),
5232                }
5233            })?;
5234            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
5235                return Ok(out.upper_band);
5236            }
5237            if output_id.eq_ignore_ascii_case("middle") {
5238                return Ok(out.middle_band);
5239            }
5240            if output_id.eq_ignore_ascii_case("lower") {
5241                return Ok(out.lower_band);
5242            }
5243            Err(IndicatorDispatchError::UnknownOutput {
5244                indicator: "bollinger_bands".to_string(),
5245                output: output_id.to_string(),
5246            })
5247        },
5248    )
5249}
5250
5251fn compute_bbw_batch(
5252    req: IndicatorBatchRequest<'_>,
5253    output_id: &str,
5254) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5255    let data = extract_slice_input("bollinger_bands_width", req.data, "close")?;
5256    let kernel = req.kernel.to_non_batch();
5257    collect_f64(
5258        "bollinger_bands_width",
5259        output_id,
5260        req.combos,
5261        data.len(),
5262        |params| {
5263            let period = get_usize_param("bollinger_bands_width", params, "period", 20)?;
5264            let devup = get_f64_param("bollinger_bands_width", params, "devup", 2.0)?;
5265            let devdn = get_f64_param("bollinger_bands_width", params, "devdn", 2.0)?;
5266            let matype = get_enum_param("bollinger_bands_width", params, "matype", "sma")?;
5267            let devtype = get_usize_param("bollinger_bands_width", params, "devtype", 0)?;
5268            let input = BollingerBandsWidthInput::from_slice(
5269                data,
5270                BollingerBandsWidthParams {
5271                    period: Some(period),
5272                    devup: Some(devup),
5273                    devdn: Some(devdn),
5274                    matype: Some(matype),
5275                    devtype: Some(devtype),
5276                },
5277            );
5278            let out = bollinger_bands_width_with_kernel(&input, kernel).map_err(|e| {
5279                IndicatorDispatchError::ComputeFailed {
5280                    indicator: "bollinger_bands_width".to_string(),
5281                    details: e.to_string(),
5282                }
5283            })?;
5284            if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
5285                return Ok(out.values);
5286            }
5287            Err(IndicatorDispatchError::UnknownOutput {
5288                indicator: "bollinger_bands_width".to_string(),
5289                output: output_id.to_string(),
5290            })
5291        },
5292    )
5293}
5294
5295fn compute_stoch_batch(
5296    req: IndicatorBatchRequest<'_>,
5297    output_id: &str,
5298) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5299    let (high, low, close) = extract_ohlc_input("stoch", req.data)?;
5300    let kernel = req.kernel.to_non_batch();
5301    collect_f64("stoch", output_id, req.combos, close.len(), |params| {
5302        let fastk_period = get_usize_param("stoch", params, "fastk_period", 14)?;
5303        let slowk_period = get_usize_param("stoch", params, "slowk_period", 3)?;
5304        let slowd_period = get_usize_param("stoch", params, "slowd_period", 3)?;
5305        let slowk_ma_type = get_enum_param("stoch", params, "slowk_ma_type", "sma")?;
5306        let slowd_ma_type = get_enum_param("stoch", params, "slowd_ma_type", "sma")?;
5307        let input = StochInput::from_slices(
5308            high,
5309            low,
5310            close,
5311            StochParams {
5312                fastk_period: Some(fastk_period),
5313                slowk_period: Some(slowk_period),
5314                slowk_ma_type: Some(slowk_ma_type),
5315                slowd_period: Some(slowd_period),
5316                slowd_ma_type: Some(slowd_ma_type),
5317            },
5318        );
5319        let out = stoch_with_kernel(&input, kernel).map_err(|e| {
5320            IndicatorDispatchError::ComputeFailed {
5321                indicator: "stoch".to_string(),
5322                details: e.to_string(),
5323            }
5324        })?;
5325        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5326            return Ok(out.k);
5327        }
5328        if output_id.eq_ignore_ascii_case("d") {
5329            return Ok(out.d);
5330        }
5331        Err(IndicatorDispatchError::UnknownOutput {
5332            indicator: "stoch".to_string(),
5333            output: output_id.to_string(),
5334        })
5335    })
5336}
5337
5338fn compute_stochf_batch(
5339    req: IndicatorBatchRequest<'_>,
5340    output_id: &str,
5341) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5342    let (high, low, close) = extract_ohlc_input("stochf", req.data)?;
5343    let kernel = req.kernel.to_non_batch();
5344    collect_f64("stochf", output_id, req.combos, close.len(), |params| {
5345        let fastk_period = get_usize_param("stochf", params, "fastk_period", 5)?;
5346        let fastd_period = get_usize_param("stochf", params, "fastd_period", 3)?;
5347        let fastd_matype = get_usize_param("stochf", params, "fastd_matype", 0)?;
5348        let input = StochfInput::from_slices(
5349            high,
5350            low,
5351            close,
5352            StochfParams {
5353                fastk_period: Some(fastk_period),
5354                fastd_period: Some(fastd_period),
5355                fastd_matype: Some(fastd_matype),
5356            },
5357        );
5358        let out = stochf_with_kernel(&input, kernel).map_err(|e| {
5359            IndicatorDispatchError::ComputeFailed {
5360                indicator: "stochf".to_string(),
5361                details: e.to_string(),
5362            }
5363        })?;
5364        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5365            return Ok(out.k);
5366        }
5367        if output_id.eq_ignore_ascii_case("d") {
5368            return Ok(out.d);
5369        }
5370        Err(IndicatorDispatchError::UnknownOutput {
5371            indicator: "stochf".to_string(),
5372            output: output_id.to_string(),
5373        })
5374    })
5375}
5376
5377fn compute_stochastic_money_flow_index_batch(
5378    req: IndicatorBatchRequest<'_>,
5379    output_id: &str,
5380) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5381    let (source, volume) =
5382        extract_close_volume_input("stochastic_money_flow_index", req.data, "close")?;
5383    let kernel = req.kernel.to_non_batch();
5384    collect_f64(
5385        "stochastic_money_flow_index",
5386        output_id,
5387        req.combos,
5388        source.len(),
5389        |params| {
5390            let stoch_k_length =
5391                get_usize_param("stochastic_money_flow_index", params, "stoch_k_length", 14)?;
5392            let stoch_k_smooth =
5393                get_usize_param("stochastic_money_flow_index", params, "stoch_k_smooth", 3)?;
5394            let stoch_d_smooth =
5395                get_usize_param("stochastic_money_flow_index", params, "stoch_d_smooth", 3)?;
5396            let mfi_length =
5397                get_usize_param("stochastic_money_flow_index", params, "mfi_length", 14)?;
5398            let input = StochasticMoneyFlowIndexInput::from_slices(
5399                source,
5400                volume,
5401                StochasticMoneyFlowIndexParams {
5402                    stoch_k_length: Some(stoch_k_length),
5403                    stoch_k_smooth: Some(stoch_k_smooth),
5404                    stoch_d_smooth: Some(stoch_d_smooth),
5405                    mfi_length: Some(mfi_length),
5406                },
5407            );
5408            let out = stochastic_money_flow_index_with_kernel(&input, kernel).map_err(|e| {
5409                IndicatorDispatchError::ComputeFailed {
5410                    indicator: "stochastic_money_flow_index".to_string(),
5411                    details: e.to_string(),
5412                }
5413            })?;
5414            if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5415                return Ok(out.k);
5416            }
5417            if output_id.eq_ignore_ascii_case("d") {
5418                return Ok(out.d);
5419            }
5420            Err(IndicatorDispatchError::UnknownOutput {
5421                indicator: "stochastic_money_flow_index".to_string(),
5422                output: output_id.to_string(),
5423            })
5424        },
5425    )
5426}
5427
5428fn compute_vwmacd_batch(
5429    req: IndicatorBatchRequest<'_>,
5430    output_id: &str,
5431) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5432    let (close, volume) = extract_close_volume_input("vwmacd", req.data, "close")?;
5433    let kernel = req.kernel.to_non_batch();
5434    collect_f64("vwmacd", output_id, req.combos, close.len(), |params| {
5435        let fast_period =
5436            get_usize_param_with_aliases("vwmacd", params, &["fast", "fast_period"], 12)?;
5437        let slow_period =
5438            get_usize_param_with_aliases("vwmacd", params, &["slow", "slow_period"], 26)?;
5439        let signal_period =
5440            get_usize_param_with_aliases("vwmacd", params, &["signal", "signal_period"], 9)?;
5441        let fast_ma_type = get_enum_param("vwmacd", params, "fast_ma_type", "sma")?;
5442        let slow_ma_type = get_enum_param("vwmacd", params, "slow_ma_type", "sma")?;
5443        let signal_ma_type = get_enum_param("vwmacd", params, "signal_ma_type", "ema")?;
5444        let input = VwmacdInput::from_slices(
5445            close,
5446            volume,
5447            VwmacdParams {
5448                fast_period: Some(fast_period),
5449                slow_period: Some(slow_period),
5450                signal_period: Some(signal_period),
5451                fast_ma_type: Some(fast_ma_type),
5452                slow_ma_type: Some(slow_ma_type),
5453                signal_ma_type: Some(signal_ma_type),
5454            },
5455        );
5456        let out = vwmacd_with_kernel(&input, kernel).map_err(|e| {
5457            IndicatorDispatchError::ComputeFailed {
5458                indicator: "vwmacd".to_string(),
5459                details: e.to_string(),
5460            }
5461        })?;
5462        if output_id.eq_ignore_ascii_case("macd") || output_id.eq_ignore_ascii_case("value") {
5463            return Ok(out.macd);
5464        }
5465        if output_id.eq_ignore_ascii_case("signal") {
5466            return Ok(out.signal);
5467        }
5468        if output_id.eq_ignore_ascii_case("hist") {
5469            return Ok(out.hist);
5470        }
5471        Err(IndicatorDispatchError::UnknownOutput {
5472            indicator: "vwmacd".to_string(),
5473            output: output_id.to_string(),
5474        })
5475    })
5476}
5477
5478fn compute_vpci_batch(
5479    req: IndicatorBatchRequest<'_>,
5480    output_id: &str,
5481) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5482    let (close, volume) = extract_close_volume_input("vpci", req.data, "close")?;
5483    let kernel = req.kernel.to_non_batch();
5484    collect_f64("vpci", output_id, req.combos, close.len(), |params| {
5485        let short_range = get_usize_param("vpci", params, "short_range", 5)?;
5486        let long_range = get_usize_param("vpci", params, "long_range", 25)?;
5487        let input = VpciInput::from_slices(
5488            close,
5489            volume,
5490            VpciParams {
5491                short_range: Some(short_range),
5492                long_range: Some(long_range),
5493            },
5494        );
5495        let out = vpci_with_kernel(&input, kernel).map_err(|e| {
5496            IndicatorDispatchError::ComputeFailed {
5497                indicator: "vpci".to_string(),
5498                details: e.to_string(),
5499            }
5500        })?;
5501        if output_id.eq_ignore_ascii_case("vpci") || output_id.eq_ignore_ascii_case("value") {
5502            return Ok(out.vpci);
5503        }
5504        if output_id.eq_ignore_ascii_case("vpcis") {
5505            return Ok(out.vpcis);
5506        }
5507        Err(IndicatorDispatchError::UnknownOutput {
5508            indicator: "vpci".to_string(),
5509            output: output_id.to_string(),
5510        })
5511    })
5512}
5513
5514fn compute_ttm_trend_batch(
5515    req: IndicatorBatchRequest<'_>,
5516    output_id: &str,
5517) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5518    expect_value_output("ttm_trend", output_id)?;
5519    let mut derived_source: Option<Vec<f64>> = None;
5520    let (source, close): (&[f64], &[f64]) = match req.data {
5521        IndicatorDataRef::Candles { candles, source } => (
5522            source_type(candles, source.unwrap_or("hl2")),
5523            candles.close.as_slice(),
5524        ),
5525        IndicatorDataRef::Ohlc {
5526            high, low, close, ..
5527        } => {
5528            ensure_same_len_3("ttm_trend", high.len(), low.len(), close.len())?;
5529            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
5530            (derived_source.as_deref().unwrap_or(close), close)
5531        }
5532        IndicatorDataRef::Ohlcv {
5533            high, low, close, ..
5534        } => {
5535            ensure_same_len_3("ttm_trend", high.len(), low.len(), close.len())?;
5536            derived_source = Some(high.iter().zip(low).map(|(h, l)| 0.5 * (h + l)).collect());
5537            (derived_source.as_deref().unwrap_or(close), close)
5538        }
5539        _ => {
5540            return Err(IndicatorDispatchError::MissingRequiredInput {
5541                indicator: "ttm_trend".to_string(),
5542                input: IndicatorInputKind::Ohlc,
5543            })
5544        }
5545    };
5546    let kernel = req.kernel.to_non_batch();
5547    collect_bool("ttm_trend", output_id, req.combos, close.len(), |params| {
5548        let period = get_usize_param("ttm_trend", params, "period", 5)?;
5549        let input = TtmTrendInput::from_slices(
5550            source,
5551            close,
5552            TtmTrendParams {
5553                period: Some(period),
5554            },
5555        );
5556        let out = ttm_trend_with_kernel(&input, kernel).map_err(|e| {
5557            IndicatorDispatchError::ComputeFailed {
5558                indicator: "ttm_trend".to_string(),
5559                details: e.to_string(),
5560            }
5561        })?;
5562        Ok(out.values)
5563    })
5564}
5565
5566fn compute_ttm_squeeze_batch(
5567    req: IndicatorBatchRequest<'_>,
5568    output_id: &str,
5569) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5570    let (high, low, close) = extract_ohlc_input("ttm_squeeze", req.data)?;
5571    let kernel = req.kernel.to_non_batch();
5572    collect_f64(
5573        "ttm_squeeze",
5574        output_id,
5575        req.combos,
5576        close.len(),
5577        |params| {
5578            let length = get_usize_param("ttm_squeeze", params, "length", 20)?;
5579            let bb_mult = get_f64_param("ttm_squeeze", params, "bb_mult", 2.0)?;
5580            let kc_mult_high = get_f64_param_with_aliases(
5581                "ttm_squeeze",
5582                params,
5583                &["kc_high", "kc_mult_high"],
5584                1.0,
5585            )?;
5586            let kc_mult_mid =
5587                get_f64_param_with_aliases("ttm_squeeze", params, &["kc_mid", "kc_mult_mid"], 1.5)?;
5588            let kc_mult_low =
5589                get_f64_param_with_aliases("ttm_squeeze", params, &["kc_low", "kc_mult_low"], 2.0)?;
5590            let input = TtmSqueezeInput::from_slices(
5591                high,
5592                low,
5593                close,
5594                TtmSqueezeParams {
5595                    length: Some(length),
5596                    bb_mult: Some(bb_mult),
5597                    kc_mult_high: Some(kc_mult_high),
5598                    kc_mult_mid: Some(kc_mult_mid),
5599                    kc_mult_low: Some(kc_mult_low),
5600                },
5601            );
5602            let out = ttm_squeeze_with_kernel(&input, kernel).map_err(|e| {
5603                IndicatorDispatchError::ComputeFailed {
5604                    indicator: "ttm_squeeze".to_string(),
5605                    details: e.to_string(),
5606                }
5607            })?;
5608            if output_id.eq_ignore_ascii_case("momentum") || output_id.eq_ignore_ascii_case("value")
5609            {
5610                return Ok(out.momentum);
5611            }
5612            if output_id.eq_ignore_ascii_case("squeeze") {
5613                return Ok(out.squeeze);
5614            }
5615            Err(IndicatorDispatchError::UnknownOutput {
5616                indicator: "ttm_squeeze".to_string(),
5617                output: output_id.to_string(),
5618            })
5619        },
5620    )
5621}
5622
5623fn compute_aroon_batch(
5624    req: IndicatorBatchRequest<'_>,
5625    output_id: &str,
5626) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5627    let (high, low) = extract_high_low_input("aroon", req.data)?;
5628    let kernel = req.kernel.to_non_batch();
5629    collect_f64("aroon", output_id, req.combos, high.len(), |params| {
5630        let length = get_usize_param("aroon", params, "length", 14)?;
5631        let input = AroonInput::from_slices_hl(
5632            high,
5633            low,
5634            AroonParams {
5635                length: Some(length),
5636            },
5637        );
5638        let out = aroon_with_kernel(&input, kernel).map_err(|e| {
5639            IndicatorDispatchError::ComputeFailed {
5640                indicator: "aroon".to_string(),
5641                details: e.to_string(),
5642            }
5643        })?;
5644        if output_id.eq_ignore_ascii_case("up")
5645            || output_id.eq_ignore_ascii_case("aroon_up")
5646            || output_id.eq_ignore_ascii_case("value")
5647        {
5648            return Ok(out.aroon_up);
5649        }
5650        if output_id.eq_ignore_ascii_case("down") || output_id.eq_ignore_ascii_case("aroon_down") {
5651            return Ok(out.aroon_down);
5652        }
5653        Err(IndicatorDispatchError::UnknownOutput {
5654            indicator: "aroon".to_string(),
5655            output: output_id.to_string(),
5656        })
5657    })
5658}
5659
5660fn compute_aroonosc_batch(
5661    req: IndicatorBatchRequest<'_>,
5662    output_id: &str,
5663) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5664    let (high, low) = extract_high_low_input("aroonosc", req.data)?;
5665    let kernel = req.kernel.to_non_batch();
5666    collect_f64("aroonosc", output_id, req.combos, high.len(), |params| {
5667        let length = get_usize_param("aroonosc", params, "length", 14)?;
5668        let input = AroonOscInput::from_slices_hl(
5669            high,
5670            low,
5671            AroonOscParams {
5672                length: Some(length),
5673            },
5674        );
5675        let out = aroon_osc_with_kernel(&input, kernel).map_err(|e| {
5676            IndicatorDispatchError::ComputeFailed {
5677                indicator: "aroonosc".to_string(),
5678                details: e.to_string(),
5679            }
5680        })?;
5681        if output_id.eq_ignore_ascii_case("value") {
5682            return Ok(out.values);
5683        }
5684        Err(IndicatorDispatchError::UnknownOutput {
5685            indicator: "aroonosc".to_string(),
5686            output: output_id.to_string(),
5687        })
5688    })
5689}
5690
5691fn compute_di_batch(
5692    req: IndicatorBatchRequest<'_>,
5693    output_id: &str,
5694) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5695    let (high, low, close) = extract_ohlc_input("di", req.data)?;
5696    let kernel = req.kernel.to_non_batch();
5697    collect_f64("di", output_id, req.combos, close.len(), |params| {
5698        let period = get_usize_param("di", params, "period", 14)?;
5699        let input = DiInput::from_slices(
5700            high,
5701            low,
5702            close,
5703            DiParams {
5704                period: Some(period),
5705            },
5706        );
5707        let out =
5708            di_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5709                indicator: "di".to_string(),
5710                details: e.to_string(),
5711            })?;
5712        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
5713            return Ok(out.plus);
5714        }
5715        if output_id.eq_ignore_ascii_case("minus") {
5716            return Ok(out.minus);
5717        }
5718        Err(IndicatorDispatchError::UnknownOutput {
5719            indicator: "di".to_string(),
5720            output: output_id.to_string(),
5721        })
5722    })
5723}
5724
5725fn compute_dm_batch(
5726    req: IndicatorBatchRequest<'_>,
5727    output_id: &str,
5728) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5729    let (high, low) = extract_high_low_input("dm", req.data)?;
5730    let kernel = req.kernel.to_non_batch();
5731    collect_f64("dm", output_id, req.combos, high.len(), |params| {
5732        let period = get_usize_param("dm", params, "period", 14)?;
5733        let input = DmInput::from_slices(
5734            high,
5735            low,
5736            DmParams {
5737                period: Some(period),
5738            },
5739        );
5740        let out =
5741            dm_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5742                indicator: "dm".to_string(),
5743                details: e.to_string(),
5744            })?;
5745        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
5746            return Ok(out.plus);
5747        }
5748        if output_id.eq_ignore_ascii_case("minus") {
5749            return Ok(out.minus);
5750        }
5751        Err(IndicatorDispatchError::UnknownOutput {
5752            indicator: "dm".to_string(),
5753            output: output_id.to_string(),
5754        })
5755    })
5756}
5757
5758fn compute_dti_batch(
5759    req: IndicatorBatchRequest<'_>,
5760    output_id: &str,
5761) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5762    expect_value_output("dti", output_id)?;
5763    let (high, low) = extract_high_low_input("dti", req.data)?;
5764    let kernel = req.kernel.to_non_batch();
5765    collect_f64_into_rows("dti", output_id, req.combos, high.len(), |params, row| {
5766        let r = get_usize_param("dti", params, "r", 14)?;
5767        let s = get_usize_param("dti", params, "s", 10)?;
5768        let u = get_usize_param("dti", params, "u", 5)?;
5769        let input = DtiInput::from_slices(
5770            high,
5771            low,
5772            DtiParams {
5773                r: Some(r),
5774                s: Some(s),
5775                u: Some(u),
5776            },
5777        );
5778        dti_into_slice(row, &input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5779            indicator: "dti".to_string(),
5780            details: e.to_string(),
5781        })
5782    })
5783}
5784
5785fn compute_donchian_batch(
5786    req: IndicatorBatchRequest<'_>,
5787    output_id: &str,
5788) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5789    let (high, low) = extract_high_low_input("donchian", req.data)?;
5790    let kernel = req.kernel.to_non_batch();
5791    collect_f64("donchian", output_id, req.combos, high.len(), |params| {
5792        let period = get_usize_param("donchian", params, "period", 20)?;
5793        let input = DonchianInput::from_slices(
5794            high,
5795            low,
5796            DonchianParams {
5797                period: Some(period),
5798            },
5799        );
5800        let out = donchian_with_kernel(&input, kernel).map_err(|e| {
5801            IndicatorDispatchError::ComputeFailed {
5802                indicator: "donchian".to_string(),
5803                details: e.to_string(),
5804            }
5805        })?;
5806        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
5807            return Ok(out.upperband);
5808        }
5809        if output_id.eq_ignore_ascii_case("middle") {
5810            return Ok(out.middleband);
5811        }
5812        if output_id.eq_ignore_ascii_case("lower") {
5813            return Ok(out.lowerband);
5814        }
5815        Err(IndicatorDispatchError::UnknownOutput {
5816            indicator: "donchian".to_string(),
5817            output: output_id.to_string(),
5818        })
5819    })
5820}
5821
5822fn compute_kdj_batch(
5823    req: IndicatorBatchRequest<'_>,
5824    output_id: &str,
5825) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5826    let (high, low, close) = extract_ohlc_input("kdj", req.data)?;
5827    let kernel = req.kernel.to_non_batch();
5828    collect_f64("kdj", output_id, req.combos, close.len(), |params| {
5829        let fast_k_period = get_usize_param("kdj", params, "fast_k_period", 9)?;
5830        let slow_k_period = get_usize_param("kdj", params, "slow_k_period", 3)?;
5831        let slow_k_ma_type = get_enum_param("kdj", params, "slow_k_ma_type", "sma")?;
5832        let slow_d_period = get_usize_param("kdj", params, "slow_d_period", 3)?;
5833        let slow_d_ma_type = get_enum_param("kdj", params, "slow_d_ma_type", "sma")?;
5834        let input = KdjInput::from_slices(
5835            high,
5836            low,
5837            close,
5838            KdjParams {
5839                fast_k_period: Some(fast_k_period),
5840                slow_k_period: Some(slow_k_period),
5841                slow_k_ma_type: Some(slow_k_ma_type),
5842                slow_d_period: Some(slow_d_period),
5843                slow_d_ma_type: Some(slow_d_ma_type),
5844            },
5845        );
5846        let out =
5847            kdj_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
5848                indicator: "kdj".to_string(),
5849                details: e.to_string(),
5850            })?;
5851        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5852            return Ok(out.k);
5853        }
5854        if output_id.eq_ignore_ascii_case("d") {
5855            return Ok(out.d);
5856        }
5857        if output_id.eq_ignore_ascii_case("j") {
5858            return Ok(out.j);
5859        }
5860        Err(IndicatorDispatchError::UnknownOutput {
5861            indicator: "kdj".to_string(),
5862            output: output_id.to_string(),
5863        })
5864    })
5865}
5866
5867fn compute_keltner_batch(
5868    req: IndicatorBatchRequest<'_>,
5869    output_id: &str,
5870) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5871    let (high, low, close) = extract_ohlc_input("keltner", req.data)?;
5872    let kernel = req.kernel.to_non_batch();
5873    collect_f64("keltner", output_id, req.combos, close.len(), |params| {
5874        let period = get_usize_param("keltner", params, "period", 20)?;
5875        let multiplier = get_f64_param("keltner", params, "multiplier", 2.0)?;
5876        let ma_type = get_enum_param("keltner", params, "ma_type", "ema")?;
5877        let input = KeltnerInput::from_slice(
5878            high,
5879            low,
5880            close,
5881            close,
5882            KeltnerParams {
5883                period: Some(period),
5884                multiplier: Some(multiplier),
5885                ma_type: Some(ma_type),
5886            },
5887        );
5888        let out = keltner_with_kernel(&input, kernel).map_err(|e| {
5889            IndicatorDispatchError::ComputeFailed {
5890                indicator: "keltner".to_string(),
5891                details: e.to_string(),
5892            }
5893        })?;
5894        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
5895            return Ok(out.upper_band);
5896        }
5897        if output_id.eq_ignore_ascii_case("middle") {
5898            return Ok(out.middle_band);
5899        }
5900        if output_id.eq_ignore_ascii_case("lower") {
5901            return Ok(out.lower_band);
5902        }
5903        Err(IndicatorDispatchError::UnknownOutput {
5904            indicator: "keltner".to_string(),
5905            output: output_id.to_string(),
5906        })
5907    })
5908}
5909
5910fn compute_squeeze_momentum_batch(
5911    req: IndicatorBatchRequest<'_>,
5912    output_id: &str,
5913) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5914    let (high, low, close) = extract_ohlc_input("squeeze_momentum", req.data)?;
5915    let kernel = req.kernel.to_non_batch();
5916    collect_f64(
5917        "squeeze_momentum",
5918        output_id,
5919        req.combos,
5920        close.len(),
5921        |params| {
5922            let length_bb = get_usize_param("squeeze_momentum", params, "length_bb", 20)?;
5923            let mult_bb = get_f64_param("squeeze_momentum", params, "mult_bb", 2.0)?;
5924            let length_kc = get_usize_param("squeeze_momentum", params, "length_kc", 20)?;
5925            let mult_kc = get_f64_param("squeeze_momentum", params, "mult_kc", 1.5)?;
5926            let input = SqueezeMomentumInput::from_slices(
5927                high,
5928                low,
5929                close,
5930                SqueezeMomentumParams {
5931                    length_bb: Some(length_bb),
5932                    mult_bb: Some(mult_bb),
5933                    length_kc: Some(length_kc),
5934                    mult_kc: Some(mult_kc),
5935                },
5936            );
5937            let out = squeeze_momentum_with_kernel(&input, kernel).map_err(|e| {
5938                IndicatorDispatchError::ComputeFailed {
5939                    indicator: "squeeze_momentum".to_string(),
5940                    details: e.to_string(),
5941                }
5942            })?;
5943            if output_id.eq_ignore_ascii_case("momentum") || output_id.eq_ignore_ascii_case("value")
5944            {
5945                return Ok(out.momentum);
5946            }
5947            if output_id.eq_ignore_ascii_case("squeeze") {
5948                return Ok(out.squeeze);
5949            }
5950            if output_id.eq_ignore_ascii_case("signal")
5951                || output_id.eq_ignore_ascii_case("momentum_signal")
5952            {
5953                return Ok(out.momentum_signal);
5954            }
5955            Err(IndicatorDispatchError::UnknownOutput {
5956                indicator: "squeeze_momentum".to_string(),
5957                output: output_id.to_string(),
5958            })
5959        },
5960    )
5961}
5962
5963fn compute_srsi_batch(
5964    req: IndicatorBatchRequest<'_>,
5965    output_id: &str,
5966) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
5967    let data = extract_slice_input("srsi", req.data, "close")?;
5968    let kernel = req.kernel.to_non_batch();
5969    collect_f64("srsi", output_id, req.combos, data.len(), |params| {
5970        let rsi_period = get_usize_param("srsi", params, "rsi_period", 14)?;
5971        let stoch_period = get_usize_param("srsi", params, "stoch_period", 14)?;
5972        let k = get_usize_param("srsi", params, "k", 3)?;
5973        let d = get_usize_param("srsi", params, "d", 3)?;
5974        let source = get_enum_param("srsi", params, "source", "close")?;
5975        let input = SrsiInput::from_slice(
5976            data,
5977            SrsiParams {
5978                rsi_period: Some(rsi_period),
5979                stoch_period: Some(stoch_period),
5980                k: Some(k),
5981                d: Some(d),
5982                source: Some(source),
5983            },
5984        );
5985        let out = srsi_with_kernel(&input, kernel).map_err(|e| {
5986            IndicatorDispatchError::ComputeFailed {
5987                indicator: "srsi".to_string(),
5988                details: e.to_string(),
5989            }
5990        })?;
5991        if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
5992            return Ok(out.k);
5993        }
5994        if output_id.eq_ignore_ascii_case("d") {
5995            return Ok(out.d);
5996        }
5997        Err(IndicatorDispatchError::UnknownOutput {
5998            indicator: "srsi".to_string(),
5999            output: output_id.to_string(),
6000        })
6001    })
6002}
6003
6004fn compute_supertrend_batch(
6005    req: IndicatorBatchRequest<'_>,
6006    output_id: &str,
6007) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6008    let (high, low, close) = extract_ohlc_input("supertrend", req.data)?;
6009    let kernel = req.kernel.to_non_batch();
6010    collect_f64("supertrend", output_id, req.combos, close.len(), |params| {
6011        let period = get_usize_param("supertrend", params, "period", 10)?;
6012        let factor = get_f64_param("supertrend", params, "factor", 3.0)?;
6013        let input = SuperTrendInput::from_slices(
6014            high,
6015            low,
6016            close,
6017            SuperTrendParams {
6018                period: Some(period),
6019                factor: Some(factor),
6020            },
6021        );
6022        let out = supertrend_with_kernel(&input, kernel).map_err(|e| {
6023            IndicatorDispatchError::ComputeFailed {
6024                indicator: "supertrend".to_string(),
6025                details: e.to_string(),
6026            }
6027        })?;
6028        if output_id.eq_ignore_ascii_case("trend") || output_id.eq_ignore_ascii_case("value") {
6029            return Ok(out.trend);
6030        }
6031        if output_id.eq_ignore_ascii_case("changed") {
6032            return Ok(out.changed);
6033        }
6034        Err(IndicatorDispatchError::UnknownOutput {
6035            indicator: "supertrend".to_string(),
6036            output: output_id.to_string(),
6037        })
6038    })
6039}
6040
6041fn compute_adjustable_ma_alternating_extremities_batch(
6042    req: IndicatorBatchRequest<'_>,
6043    output_id: &str,
6044) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6045    let (high, low, close) = extract_ohlc_input("adjustable_ma_alternating_extremities", req.data)?;
6046    let kernel = req.kernel.to_non_batch();
6047    collect_f64(
6048        "adjustable_ma_alternating_extremities",
6049        output_id,
6050        req.combos,
6051        close.len(),
6052        |params| {
6053            let length = get_usize_param(
6054                "adjustable_ma_alternating_extremities",
6055                params,
6056                "length",
6057                50,
6058            )?;
6059            let mult = get_f64_param("adjustable_ma_alternating_extremities", params, "mult", 2.0)?;
6060            let alpha = get_f64_param(
6061                "adjustable_ma_alternating_extremities",
6062                params,
6063                "alpha",
6064                1.0,
6065            )?;
6066            let beta = get_f64_param("adjustable_ma_alternating_extremities", params, "beta", 0.5)?;
6067            let input = AdjustableMaAlternatingExtremitiesInput::from_slices(
6068                high,
6069                low,
6070                close,
6071                AdjustableMaAlternatingExtremitiesParams {
6072                    length: Some(length),
6073                    mult: Some(mult),
6074                    alpha: Some(alpha),
6075                    beta: Some(beta),
6076                },
6077            );
6078            let out =
6079                adjustable_ma_alternating_extremities_with_kernel(&input, kernel).map_err(|e| {
6080                    IndicatorDispatchError::ComputeFailed {
6081                        indicator: "adjustable_ma_alternating_extremities".to_string(),
6082                        details: e.to_string(),
6083                    }
6084                })?;
6085            if output_id.eq_ignore_ascii_case("ma") || output_id.eq_ignore_ascii_case("value") {
6086                return Ok(out.ma);
6087            }
6088            if output_id.eq_ignore_ascii_case("upper") {
6089                return Ok(out.upper);
6090            }
6091            if output_id.eq_ignore_ascii_case("lower") {
6092                return Ok(out.lower);
6093            }
6094            if output_id.eq_ignore_ascii_case("extremity") {
6095                return Ok(out.extremity);
6096            }
6097            if output_id.eq_ignore_ascii_case("state") {
6098                return Ok(out.state);
6099            }
6100            if output_id.eq_ignore_ascii_case("changed") {
6101                return Ok(out.changed);
6102            }
6103            if output_id.eq_ignore_ascii_case("smoothed_open") {
6104                return Ok(out.smoothed_open);
6105            }
6106            if output_id.eq_ignore_ascii_case("smoothed_high") {
6107                return Ok(out.smoothed_high);
6108            }
6109            if output_id.eq_ignore_ascii_case("smoothed_low") {
6110                return Ok(out.smoothed_low);
6111            }
6112            if output_id.eq_ignore_ascii_case("smoothed_close") {
6113                return Ok(out.smoothed_close);
6114            }
6115            Err(IndicatorDispatchError::UnknownOutput {
6116                indicator: "adjustable_ma_alternating_extremities".to_string(),
6117                output: output_id.to_string(),
6118            })
6119        },
6120    )
6121}
6122
6123fn compute_vi_batch(
6124    req: IndicatorBatchRequest<'_>,
6125    output_id: &str,
6126) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6127    let (high, low, close) = extract_ohlc_input("vi", req.data)?;
6128    let kernel = req.kernel.to_non_batch();
6129    collect_f64("vi", output_id, req.combos, close.len(), |params| {
6130        let period = get_usize_param("vi", params, "period", 14)?;
6131        let input = ViInput::from_slices(
6132            high,
6133            low,
6134            close,
6135            ViParams {
6136                period: Some(period),
6137            },
6138        );
6139        let out =
6140            vi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
6141                indicator: "vi".to_string(),
6142                details: e.to_string(),
6143            })?;
6144        if output_id.eq_ignore_ascii_case("plus") || output_id.eq_ignore_ascii_case("value") {
6145            return Ok(out.plus);
6146        }
6147        if output_id.eq_ignore_ascii_case("minus") {
6148            return Ok(out.minus);
6149        }
6150        Err(IndicatorDispatchError::UnknownOutput {
6151            indicator: "vi".to_string(),
6152            output: output_id.to_string(),
6153        })
6154    })
6155}
6156
6157fn compute_wavetrend_batch(
6158    req: IndicatorBatchRequest<'_>,
6159    output_id: &str,
6160) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6161    let data = extract_slice_input("wavetrend", req.data, "hlc3")?;
6162    let kernel = req.kernel.to_non_batch();
6163    collect_f64("wavetrend", output_id, req.combos, data.len(), |params| {
6164        let channel_length = get_usize_param("wavetrend", params, "channel_length", 9)?;
6165        let average_length = get_usize_param("wavetrend", params, "average_length", 12)?;
6166        let ma_length = get_usize_param("wavetrend", params, "ma_length", 3)?;
6167        let factor = get_f64_param("wavetrend", params, "factor", 0.015)?;
6168        let input = WavetrendInput::from_slice(
6169            data,
6170            WavetrendParams {
6171                channel_length: Some(channel_length),
6172                average_length: Some(average_length),
6173                ma_length: Some(ma_length),
6174                factor: Some(factor),
6175            },
6176        );
6177        let out = wavetrend_with_kernel(&input, kernel).map_err(|e| {
6178            IndicatorDispatchError::ComputeFailed {
6179                indicator: "wavetrend".to_string(),
6180                details: e.to_string(),
6181            }
6182        })?;
6183        if output_id.eq_ignore_ascii_case("wt1") || output_id.eq_ignore_ascii_case("value") {
6184            return Ok(out.wt1);
6185        }
6186        if output_id.eq_ignore_ascii_case("wt2") {
6187            return Ok(out.wt2);
6188        }
6189        if output_id.eq_ignore_ascii_case("wt_diff") {
6190            return Ok(out.wt_diff);
6191        }
6192        Err(IndicatorDispatchError::UnknownOutput {
6193            indicator: "wavetrend".to_string(),
6194            output: output_id.to_string(),
6195        })
6196    })
6197}
6198
6199fn compute_wto_batch(
6200    req: IndicatorBatchRequest<'_>,
6201    output_id: &str,
6202) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6203    let data = extract_slice_input("wto", req.data, "close")?;
6204    let kernel = req.kernel.to_non_batch();
6205    collect_f64("wto", output_id, req.combos, data.len(), |params| {
6206        let channel_length = get_usize_param("wto", params, "channel_length", 10)?;
6207        let average_length = get_usize_param("wto", params, "average_length", 21)?;
6208        let input = WtoInput::from_slice(
6209            data,
6210            WtoParams {
6211                channel_length: Some(channel_length),
6212                average_length: Some(average_length),
6213            },
6214        );
6215        let out =
6216            wto_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
6217                indicator: "wto".to_string(),
6218                details: e.to_string(),
6219            })?;
6220        if output_id.eq_ignore_ascii_case("wavetrend1")
6221            || output_id.eq_ignore_ascii_case("wt1")
6222            || output_id.eq_ignore_ascii_case("value")
6223        {
6224            return Ok(out.wavetrend1);
6225        }
6226        if output_id.eq_ignore_ascii_case("wavetrend2") || output_id.eq_ignore_ascii_case("wt2") {
6227            return Ok(out.wavetrend2);
6228        }
6229        if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist") {
6230            return Ok(out.histogram);
6231        }
6232        Err(IndicatorDispatchError::UnknownOutput {
6233            indicator: "wto".to_string(),
6234            output: output_id.to_string(),
6235        })
6236    })
6237}
6238
6239fn compute_rogers_satchell_volatility_batch(
6240    req: IndicatorBatchRequest<'_>,
6241    output_id: &str,
6242) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6243    let (open, high, low, close) = extract_ohlc_full_input("rogers_satchell_volatility", req.data)?;
6244    let kernel = req.kernel.to_non_batch();
6245    collect_f64(
6246        "rogers_satchell_volatility",
6247        output_id,
6248        req.combos,
6249        close.len(),
6250        |params| {
6251            let lookback = get_usize_param("rogers_satchell_volatility", params, "lookback", 8)?;
6252            let signal_length =
6253                get_usize_param("rogers_satchell_volatility", params, "signal_length", 8)?;
6254            let input = RogersSatchellVolatilityInput::from_slices(
6255                open,
6256                high,
6257                low,
6258                close,
6259                RogersSatchellVolatilityParams {
6260                    lookback: Some(lookback),
6261                    signal_length: Some(signal_length),
6262                },
6263            );
6264            let out = rogers_satchell_volatility_with_kernel(&input, kernel).map_err(|e| {
6265                IndicatorDispatchError::ComputeFailed {
6266                    indicator: "rogers_satchell_volatility".to_string(),
6267                    details: e.to_string(),
6268                }
6269            })?;
6270            if output_id.eq_ignore_ascii_case("rs") || output_id.eq_ignore_ascii_case("value") {
6271                return Ok(out.rs);
6272            }
6273            if output_id.eq_ignore_ascii_case("signal") {
6274                return Ok(out.signal);
6275            }
6276            Err(IndicatorDispatchError::UnknownOutput {
6277                indicator: "rogers_satchell_volatility".to_string(),
6278                output: output_id.to_string(),
6279            })
6280        },
6281    )
6282}
6283
6284fn compute_historical_volatility_rank_batch(
6285    req: IndicatorBatchRequest<'_>,
6286    output_id: &str,
6287) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6288    let data = extract_slice_input("historical_volatility_rank", req.data, "close")?;
6289    let kernel = req.kernel.to_non_batch();
6290    collect_f64(
6291        "historical_volatility_rank",
6292        output_id,
6293        req.combos,
6294        data.len(),
6295        |params| {
6296            let hv_length = get_usize_param("historical_volatility_rank", params, "hv_length", 10)?;
6297            let rank_length =
6298                get_usize_param("historical_volatility_rank", params, "rank_length", 52 * 7)?;
6299            let annualization_days = get_f64_param(
6300                "historical_volatility_rank",
6301                params,
6302                "annualization_days",
6303                365.0,
6304            )?;
6305            let bar_days = get_f64_param("historical_volatility_rank", params, "bar_days", 1.0)?;
6306            let input = HistoricalVolatilityRankInput::from_slice(
6307                data,
6308                HistoricalVolatilityRankParams {
6309                    hv_length: Some(hv_length),
6310                    rank_length: Some(rank_length),
6311                    annualization_days: Some(annualization_days),
6312                    bar_days: Some(bar_days),
6313                },
6314            );
6315            let out = historical_volatility_rank_with_kernel(&input, kernel).map_err(|e| {
6316                IndicatorDispatchError::ComputeFailed {
6317                    indicator: "historical_volatility_rank".to_string(),
6318                    details: e.to_string(),
6319                }
6320            })?;
6321            if output_id.eq_ignore_ascii_case("hvr") || output_id.eq_ignore_ascii_case("value") {
6322                return Ok(out.hvr);
6323            }
6324            if output_id.eq_ignore_ascii_case("hv") {
6325                return Ok(out.hv);
6326            }
6327            Err(IndicatorDispatchError::UnknownOutput {
6328                indicator: "historical_volatility_rank".to_string(),
6329                output: output_id.to_string(),
6330            })
6331        },
6332    )
6333}
6334
6335fn compute_dual_ulcer_index_batch(
6336    req: IndicatorBatchRequest<'_>,
6337    output_id: &str,
6338) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6339    let data = extract_slice_input("dual_ulcer_index", req.data, "close")?;
6340    let kernel = req.kernel.to_non_batch();
6341    collect_f64(
6342        "dual_ulcer_index",
6343        output_id,
6344        req.combos,
6345        data.len(),
6346        |params| {
6347            let period = get_usize_param("dual_ulcer_index", params, "period", 5)?;
6348            let auto_threshold =
6349                get_bool_param("dual_ulcer_index", params, "auto_threshold", true)?;
6350            let threshold = get_f64_param("dual_ulcer_index", params, "threshold", 0.1)?;
6351            let input = DualUlcerIndexInput::from_slice(
6352                data,
6353                DualUlcerIndexParams {
6354                    period: Some(period),
6355                    auto_threshold: Some(auto_threshold),
6356                    threshold: Some(threshold),
6357                },
6358            );
6359            let out = dual_ulcer_index_with_kernel(&input, kernel).map_err(|e| {
6360                IndicatorDispatchError::ComputeFailed {
6361                    indicator: "dual_ulcer_index".to_string(),
6362                    details: e.to_string(),
6363                }
6364            })?;
6365            if output_id.eq_ignore_ascii_case("long_ulcer")
6366                || output_id.eq_ignore_ascii_case("uulcer")
6367                || output_id.eq_ignore_ascii_case("value")
6368            {
6369                return Ok(out.long_ulcer);
6370            }
6371            if output_id.eq_ignore_ascii_case("short_ulcer")
6372                || output_id.eq_ignore_ascii_case("dulcer")
6373            {
6374                return Ok(out.short_ulcer);
6375            }
6376            if output_id.eq_ignore_ascii_case("threshold") {
6377                return Ok(out.threshold);
6378            }
6379            Err(IndicatorDispatchError::UnknownOutput {
6380                indicator: "dual_ulcer_index".to_string(),
6381                output: output_id.to_string(),
6382            })
6383        },
6384    )
6385}
6386
6387fn compute_fractal_dimension_index_batch(
6388    req: IndicatorBatchRequest<'_>,
6389    output_id: &str,
6390) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6391    let data = extract_slice_input("fractal_dimension_index", req.data, "close")?;
6392    let kernel = req.kernel.to_non_batch();
6393    collect_f64(
6394        "fractal_dimension_index",
6395        output_id,
6396        req.combos,
6397        data.len(),
6398        |params| {
6399            let length = get_usize_param("fractal_dimension_index", params, "length", 30)?;
6400            let input = FractalDimensionIndexInput::from_slice(
6401                data,
6402                FractalDimensionIndexParams {
6403                    length: Some(length),
6404                },
6405            );
6406            let out = fractal_dimension_index_with_kernel(&input, kernel).map_err(|e| {
6407                IndicatorDispatchError::ComputeFailed {
6408                    indicator: "fractal_dimension_index".to_string(),
6409                    details: e.to_string(),
6410                }
6411            })?;
6412            if output_id.eq_ignore_ascii_case("value") {
6413                return Ok(out.values);
6414            }
6415            Err(IndicatorDispatchError::UnknownOutput {
6416                indicator: "fractal_dimension_index".to_string(),
6417                output: output_id.to_string(),
6418            })
6419        },
6420    )
6421}
6422
6423fn compute_volume_weighted_rsi_batch(
6424    req: IndicatorBatchRequest<'_>,
6425    output_id: &str,
6426) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6427    expect_value_output("volume_weighted_rsi", output_id)?;
6428    let (close, volume) = extract_close_volume_input("volume_weighted_rsi", req.data, "close")?;
6429    let periods = combo_periods("volume_weighted_rsi", req.combos, "period", 14)?;
6430    if let Some((start, end, step)) = derive_period_sweep(&periods) {
6431        let out = volume_weighted_rsi_batch_with_kernel(
6432            close,
6433            volume,
6434            &VolumeWeightedRsiBatchRange {
6435                period: (start, end, step),
6436            },
6437            to_batch_kernel(req.kernel),
6438        )
6439        .map_err(|e| IndicatorDispatchError::ComputeFailed {
6440            indicator: "volume_weighted_rsi".to_string(),
6441            details: e.to_string(),
6442        })?;
6443        ensure_len("volume_weighted_rsi", close.len(), out.cols)?;
6444        let produced_periods: Vec<usize> = out
6445            .combos
6446            .iter()
6447            .map(|combo| combo.period.unwrap_or(14))
6448            .collect();
6449        let values = reorder_or_take_f64_matrix_by_period(
6450            "volume_weighted_rsi",
6451            &periods,
6452            &produced_periods,
6453            out.cols,
6454            out.values,
6455        )?;
6456        return Ok(f64_output(output_id, periods.len(), out.cols, values));
6457    }
6458
6459    let kernel = req.kernel.to_non_batch();
6460    collect_f64_into_rows(
6461        "volume_weighted_rsi",
6462        output_id,
6463        req.combos,
6464        close.len(),
6465        |params, row| {
6466            let period = get_usize_param("volume_weighted_rsi", params, "period", 14)?;
6467            let input = VolumeWeightedRsiInput::from_slices(
6468                close,
6469                volume,
6470                VolumeWeightedRsiParams {
6471                    period: Some(period),
6472                },
6473            );
6474            volume_weighted_rsi_into_slice(row, &input, kernel).map_err(|e| {
6475                IndicatorDispatchError::ComputeFailed {
6476                    indicator: "volume_weighted_rsi".to_string(),
6477                    details: e.to_string(),
6478                }
6479            })
6480        },
6481    )
6482}
6483
6484fn compute_dynamic_momentum_index_batch(
6485    req: IndicatorBatchRequest<'_>,
6486    output_id: &str,
6487) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6488    expect_value_output("dynamic_momentum_index", output_id)?;
6489    let data = extract_slice_input("dynamic_momentum_index", req.data, "close")?;
6490    let kernel = req.kernel.to_non_batch();
6491    collect_f64_into_rows(
6492        "dynamic_momentum_index",
6493        output_id,
6494        req.combos,
6495        data.len(),
6496        |params, row| {
6497            let rsi_period = get_usize_param("dynamic_momentum_index", params, "rsi_period", 14)?;
6498            let volatility_period =
6499                get_usize_param("dynamic_momentum_index", params, "volatility_period", 5)?;
6500            let volatility_sma_period = get_usize_param(
6501                "dynamic_momentum_index",
6502                params,
6503                "volatility_sma_period",
6504                10,
6505            )?;
6506            let upper_limit = get_usize_param("dynamic_momentum_index", params, "upper_limit", 30)?;
6507            let lower_limit = get_usize_param("dynamic_momentum_index", params, "lower_limit", 5)?;
6508            let input = DynamicMomentumIndexInput::from_slice(
6509                data,
6510                DynamicMomentumIndexParams {
6511                    rsi_period: Some(rsi_period),
6512                    volatility_period: Some(volatility_period),
6513                    volatility_sma_period: Some(volatility_sma_period),
6514                    upper_limit: Some(upper_limit),
6515                    lower_limit: Some(lower_limit),
6516                },
6517            );
6518            dynamic_momentum_index_into_slice(row, &input, kernel).map_err(|e| {
6519                IndicatorDispatchError::ComputeFailed {
6520                    indicator: "dynamic_momentum_index".to_string(),
6521                    details: e.to_string(),
6522                }
6523            })
6524        },
6525    )
6526}
6527
6528fn compute_disparity_index_batch(
6529    req: IndicatorBatchRequest<'_>,
6530    output_id: &str,
6531) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6532    expect_value_output("disparity_index", output_id)?;
6533    let data = extract_slice_input("disparity_index", req.data, "close")?;
6534    let kernel = req.kernel.to_non_batch();
6535    collect_f64_into_rows(
6536        "disparity_index",
6537        output_id,
6538        req.combos,
6539        data.len(),
6540        |params, row| {
6541            let ema_period = get_usize_param("disparity_index", params, "ema_period", 14)?;
6542            let lookback_period =
6543                get_usize_param("disparity_index", params, "lookback_period", 14)?;
6544            let smoothing_period =
6545                get_usize_param("disparity_index", params, "smoothing_period", 9)?;
6546            let smoothing_type =
6547                get_enum_param("disparity_index", params, "smoothing_type", "ema")?;
6548            let input = DisparityIndexInput::from_slice(
6549                data,
6550                DisparityIndexParams {
6551                    ema_period: Some(ema_period),
6552                    lookback_period: Some(lookback_period),
6553                    smoothing_period: Some(smoothing_period),
6554                    smoothing_type: Some(smoothing_type),
6555                },
6556            );
6557            disparity_index_into_slice(row, &input, kernel).map_err(|e| {
6558                IndicatorDispatchError::ComputeFailed {
6559                    indicator: "disparity_index".to_string(),
6560                    details: e.to_string(),
6561                }
6562            })
6563        },
6564    )
6565}
6566
6567fn compute_donchian_channel_width_batch(
6568    req: IndicatorBatchRequest<'_>,
6569    output_id: &str,
6570) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6571    expect_value_output("donchian_channel_width", output_id)?;
6572    let (high, low) = extract_high_low_input("donchian_channel_width", req.data)?;
6573
6574    collect_f64_into_rows(
6575        "donchian_channel_width",
6576        output_id,
6577        req.combos,
6578        high.len(),
6579        |params, row| {
6580            let period = get_usize_param("donchian_channel_width", params, "period", 20)?;
6581            let kernel = req.kernel;
6582            let input = DonchianChannelWidthInput::from_slices(
6583                high,
6584                low,
6585                DonchianChannelWidthParams {
6586                    period: Some(period),
6587                },
6588            );
6589            donchian_channel_width_into_slice(row, &input, kernel).map_err(|e| {
6590                IndicatorDispatchError::ComputeFailed {
6591                    indicator: "donchian_channel_width".to_string(),
6592                    details: e.to_string(),
6593                }
6594            })
6595        },
6596    )
6597}
6598
6599fn compute_kairi_relative_index_batch(
6600    req: IndicatorBatchRequest<'_>,
6601    output_id: &str,
6602) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6603    expect_value_output("kairi_relative_index", output_id)?;
6604    let kernel = req.kernel.to_non_batch();
6605    let len = match req.data {
6606        IndicatorDataRef::Slice { values } => values.len(),
6607        IndicatorDataRef::Candles { candles, source } => {
6608            source_type(candles, source.unwrap_or("close")).len()
6609        }
6610        IndicatorDataRef::CloseVolume { close, volume } => {
6611            ensure_same_len_2("kairi_relative_index", close.len(), volume.len())?;
6612            close.len()
6613        }
6614        IndicatorDataRef::Ohlc {
6615            open,
6616            high,
6617            low,
6618            close,
6619        } => {
6620            ensure_same_len_4(
6621                "kairi_relative_index",
6622                open.len(),
6623                high.len(),
6624                low.len(),
6625                close.len(),
6626            )?;
6627            close.len()
6628        }
6629        IndicatorDataRef::Ohlcv {
6630            open,
6631            high,
6632            low,
6633            close,
6634            volume,
6635        } => {
6636            ensure_same_len_5(
6637                "kairi_relative_index",
6638                open.len(),
6639                high.len(),
6640                low.len(),
6641                close.len(),
6642                volume.len(),
6643            )?;
6644            close.len()
6645        }
6646        IndicatorDataRef::HighLow { .. } => {
6647            return Err(IndicatorDispatchError::MissingRequiredInput {
6648                indicator: "kairi_relative_index".to_string(),
6649                input: IndicatorInputKind::Candles,
6650            });
6651        }
6652    };
6653
6654    collect_f64_into_rows(
6655        "kairi_relative_index",
6656        output_id,
6657        req.combos,
6658        len,
6659        |params, row| {
6660            let length = get_usize_param("kairi_relative_index", params, "length", 50)?;
6661            let ma_type = get_enum_param("kairi_relative_index", params, "ma_type", "SMA")?;
6662            if ma_type.eq_ignore_ascii_case("VWMA") {
6663                match req.data {
6664                    IndicatorDataRef::Slice { .. } | IndicatorDataRef::Ohlc { .. } => {
6665                        return Err(IndicatorDispatchError::MissingRequiredInput {
6666                            indicator: "kairi_relative_index".to_string(),
6667                            input: IndicatorInputKind::CloseVolume,
6668                        });
6669                    }
6670                    _ => {}
6671                }
6672            }
6673
6674            let input = match req.data {
6675                IndicatorDataRef::Slice { values } => KairiRelativeIndexInput::from_slices(
6676                    values,
6677                    values,
6678                    KairiRelativeIndexParams {
6679                        length: Some(length),
6680                        ma_type: Some(ma_type.to_string()),
6681                    },
6682                ),
6683                IndicatorDataRef::Candles { candles, source } => {
6684                    KairiRelativeIndexInput::from_candles(
6685                        candles,
6686                        source.unwrap_or("close"),
6687                        KairiRelativeIndexParams {
6688                            length: Some(length),
6689                            ma_type: Some(ma_type.to_string()),
6690                        },
6691                    )
6692                }
6693                IndicatorDataRef::CloseVolume { close, volume } => {
6694                    KairiRelativeIndexInput::from_slices(
6695                        close,
6696                        volume,
6697                        KairiRelativeIndexParams {
6698                            length: Some(length),
6699                            ma_type: Some(ma_type.to_string()),
6700                        },
6701                    )
6702                }
6703                IndicatorDataRef::Ohlc { close, .. } => KairiRelativeIndexInput::from_slices(
6704                    close,
6705                    close,
6706                    KairiRelativeIndexParams {
6707                        length: Some(length),
6708                        ma_type: Some(ma_type.to_string()),
6709                    },
6710                ),
6711                IndicatorDataRef::Ohlcv { close, volume, .. } => {
6712                    KairiRelativeIndexInput::from_slices(
6713                        close,
6714                        volume,
6715                        KairiRelativeIndexParams {
6716                            length: Some(length),
6717                            ma_type: Some(ma_type.to_string()),
6718                        },
6719                    )
6720                }
6721                IndicatorDataRef::HighLow { .. } => unreachable!(),
6722            };
6723
6724            kairi_relative_index_into_slice(row, &input, kernel).map_err(|e| {
6725                IndicatorDispatchError::ComputeFailed {
6726                    indicator: "kairi_relative_index".to_string(),
6727                    details: e.to_string(),
6728                }
6729            })
6730        },
6731    )
6732}
6733
6734fn compute_projection_oscillator_batch(
6735    req: IndicatorBatchRequest<'_>,
6736    output_id: &str,
6737) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6738    let (high, low, close) = extract_ohlc_input("projection_oscillator", req.data)?;
6739    let kernel = req.kernel.to_non_batch();
6740    collect_f64(
6741        "projection_oscillator",
6742        output_id,
6743        req.combos,
6744        close.len(),
6745        |params| {
6746            let length = get_usize_param("projection_oscillator", params, "length", 14)?;
6747            let smooth_length =
6748                get_usize_param("projection_oscillator", params, "smooth_length", 4)?;
6749            let input = ProjectionOscillatorInput::from_slices(
6750                high,
6751                low,
6752                close,
6753                ProjectionOscillatorParams {
6754                    length: Some(length),
6755                    smooth_length: Some(smooth_length),
6756                },
6757            );
6758            let out = projection_oscillator_with_kernel(&input, kernel).map_err(|e| {
6759                IndicatorDispatchError::ComputeFailed {
6760                    indicator: "projection_oscillator".to_string(),
6761                    details: e.to_string(),
6762                }
6763            })?;
6764            if output_id.eq_ignore_ascii_case("pbo") || output_id.eq_ignore_ascii_case("value") {
6765                return Ok(out.pbo);
6766            }
6767            if output_id.eq_ignore_ascii_case("signal") {
6768                return Ok(out.signal);
6769            }
6770            Err(IndicatorDispatchError::UnknownOutput {
6771                indicator: "projection_oscillator".to_string(),
6772                output: output_id.to_string(),
6773            })
6774        },
6775    )
6776}
6777
6778fn compute_market_structure_trailing_stop_batch(
6779    req: IndicatorBatchRequest<'_>,
6780    output_id: &str,
6781) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6782    let (open, high, low, close) =
6783        extract_ohlc_full_input("market_structure_trailing_stop", req.data)?;
6784    let kernel = req.kernel.to_non_batch();
6785    collect_f64(
6786        "market_structure_trailing_stop",
6787        output_id,
6788        req.combos,
6789        close.len(),
6790        |params| {
6791            let length = get_usize_param("market_structure_trailing_stop", params, "length", 14)?;
6792            let increment_factor = get_f64_param(
6793                "market_structure_trailing_stop",
6794                params,
6795                "increment_factor",
6796                100.0,
6797            )?;
6798            let reset_on = get_enum_param(
6799                "market_structure_trailing_stop",
6800                params,
6801                "reset_on",
6802                "CHoCH",
6803            )?;
6804            let input = MarketStructureTrailingStopInput::from_slices(
6805                open,
6806                high,
6807                low,
6808                close,
6809                MarketStructureTrailingStopParams {
6810                    length: Some(length),
6811                    increment_factor: Some(increment_factor),
6812                    reset_on: Some(reset_on),
6813                },
6814            );
6815            let out = market_structure_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
6816                IndicatorDispatchError::ComputeFailed {
6817                    indicator: "market_structure_trailing_stop".to_string(),
6818                    details: e.to_string(),
6819                }
6820            })?;
6821            if output_id.eq_ignore_ascii_case("trailing_stop")
6822                || output_id.eq_ignore_ascii_case("value")
6823            {
6824                return Ok(out.trailing_stop);
6825            }
6826            if output_id.eq_ignore_ascii_case("state") {
6827                return Ok(out.state);
6828            }
6829            if output_id.eq_ignore_ascii_case("structure") {
6830                return Ok(out.structure);
6831            }
6832            Err(IndicatorDispatchError::UnknownOutput {
6833                indicator: "market_structure_trailing_stop".to_string(),
6834                output: output_id.to_string(),
6835            })
6836        },
6837    )
6838}
6839
6840fn compute_evasive_supertrend_batch(
6841    req: IndicatorBatchRequest<'_>,
6842    output_id: &str,
6843) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6844    let (open, high, low, close) = extract_ohlc_full_input("evasive_supertrend", req.data)?;
6845    let kernel = req.kernel.to_non_batch();
6846    collect_f64(
6847        "evasive_supertrend",
6848        output_id,
6849        req.combos,
6850        close.len(),
6851        |params| {
6852            let atr_length = get_usize_param("evasive_supertrend", params, "atr_length", 10)?;
6853            let base_multiplier =
6854                get_f64_param("evasive_supertrend", params, "base_multiplier", 3.0)?;
6855            let noise_threshold =
6856                get_f64_param("evasive_supertrend", params, "noise_threshold", 1.0)?;
6857            let expansion_alpha =
6858                get_f64_param("evasive_supertrend", params, "expansion_alpha", 0.5)?;
6859            let input = EvasiveSuperTrendInput::from_slices(
6860                open,
6861                high,
6862                low,
6863                close,
6864                EvasiveSuperTrendParams {
6865                    atr_length: Some(atr_length),
6866                    base_multiplier: Some(base_multiplier),
6867                    noise_threshold: Some(noise_threshold),
6868                    expansion_alpha: Some(expansion_alpha),
6869                },
6870            );
6871            let out = evasive_supertrend_with_kernel(&input, kernel).map_err(|e| {
6872                IndicatorDispatchError::ComputeFailed {
6873                    indicator: "evasive_supertrend".to_string(),
6874                    details: e.to_string(),
6875                }
6876            })?;
6877            if output_id.eq_ignore_ascii_case("band") || output_id.eq_ignore_ascii_case("value") {
6878                return Ok(out.band);
6879            }
6880            if output_id.eq_ignore_ascii_case("state") {
6881                return Ok(out.state);
6882            }
6883            if output_id.eq_ignore_ascii_case("noisy") {
6884                return Ok(out.noisy);
6885            }
6886            if output_id.eq_ignore_ascii_case("changed") {
6887                return Ok(out.changed);
6888            }
6889            Err(IndicatorDispatchError::UnknownOutput {
6890                indicator: "evasive_supertrend".to_string(),
6891                output: output_id.to_string(),
6892            })
6893        },
6894    )
6895}
6896
6897fn compute_reversal_signals_batch(
6898    req: IndicatorBatchRequest<'_>,
6899    output_id: &str,
6900) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6901    let (open, high, low, close, volume) = extract_ohlcv_full_input("reversal_signals", req.data)?;
6902    let kernel = req.kernel.to_non_batch();
6903    collect_f64(
6904        "reversal_signals",
6905        output_id,
6906        req.combos,
6907        close.len(),
6908        |params| {
6909            let lookback_period =
6910                get_usize_param("reversal_signals", params, "lookback_period", 12)?;
6911            let confirmation_period =
6912                get_usize_param("reversal_signals", params, "confirmation_period", 3)?;
6913            let use_volume_confirmation =
6914                get_bool_param("reversal_signals", params, "use_volume_confirmation", true)?;
6915            let trend_ma_period =
6916                get_usize_param("reversal_signals", params, "trend_ma_period", 50)?;
6917            let trend_ma_type = get_enum_param("reversal_signals", params, "trend_ma_type", "EMA")?;
6918            let ma_step_period = get_usize_param("reversal_signals", params, "ma_step_period", 33)?;
6919            let input = ReversalSignalsInput::from_slices(
6920                open,
6921                high,
6922                low,
6923                close,
6924                volume,
6925                ReversalSignalsParams {
6926                    lookback_period: Some(lookback_period),
6927                    confirmation_period: Some(confirmation_period),
6928                    use_volume_confirmation: Some(use_volume_confirmation),
6929                    trend_ma_period: Some(trend_ma_period),
6930                    trend_ma_type: Some(trend_ma_type.to_string()),
6931                    ma_step_period: Some(ma_step_period),
6932                },
6933            );
6934            let out = reversal_signals_with_kernel(&input, kernel).map_err(|e| {
6935                IndicatorDispatchError::ComputeFailed {
6936                    indicator: "reversal_signals".to_string(),
6937                    details: e.to_string(),
6938                }
6939            })?;
6940            if output_id.eq_ignore_ascii_case("buy_signal") {
6941                return Ok(out.buy_signal);
6942            }
6943            if output_id.eq_ignore_ascii_case("sell_signal") {
6944                return Ok(out.sell_signal);
6945            }
6946            if output_id.eq_ignore_ascii_case("stepped_ma")
6947                || output_id.eq_ignore_ascii_case("value")
6948            {
6949                return Ok(out.stepped_ma);
6950            }
6951            if output_id.eq_ignore_ascii_case("state") {
6952                return Ok(out.state);
6953            }
6954            Err(IndicatorDispatchError::UnknownOutput {
6955                indicator: "reversal_signals".to_string(),
6956                output: output_id.to_string(),
6957            })
6958        },
6959    )
6960}
6961
6962fn compute_zig_zag_channels_batch(
6963    req: IndicatorBatchRequest<'_>,
6964    output_id: &str,
6965) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
6966    let (open, high, low, close) = extract_ohlc_full_input("zig_zag_channels", req.data)?;
6967    let kernel = req.kernel.to_non_batch();
6968    collect_f64(
6969        "zig_zag_channels",
6970        output_id,
6971        req.combos,
6972        close.len(),
6973        |params| {
6974            let length = get_usize_param("zig_zag_channels", params, "length", 100)?;
6975            let extend = get_bool_param("zig_zag_channels", params, "extend", true)?;
6976            let input = ZigZagChannelsInput::from_slices(
6977                open,
6978                high,
6979                low,
6980                close,
6981                ZigZagChannelsParams {
6982                    length: Some(length),
6983                    extend: Some(extend),
6984                },
6985            );
6986            let out = zig_zag_channels_with_kernel(&input, kernel).map_err(|e| {
6987                IndicatorDispatchError::ComputeFailed {
6988                    indicator: "zig_zag_channels".to_string(),
6989                    details: e.to_string(),
6990                }
6991            })?;
6992            if output_id.eq_ignore_ascii_case("middle") || output_id.eq_ignore_ascii_case("value") {
6993                return Ok(out.middle);
6994            }
6995            if output_id.eq_ignore_ascii_case("upper") {
6996                return Ok(out.upper);
6997            }
6998            if output_id.eq_ignore_ascii_case("lower") {
6999                return Ok(out.lower);
7000            }
7001            Err(IndicatorDispatchError::UnknownOutput {
7002                indicator: "zig_zag_channels".to_string(),
7003                output: output_id.to_string(),
7004            })
7005        },
7006    )
7007}
7008
7009fn compute_directional_imbalance_index_batch(
7010    req: IndicatorBatchRequest<'_>,
7011    output_id: &str,
7012) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7013    let (high, low) = match req.data {
7014        IndicatorDataRef::Candles { candles, .. } => {
7015            (candles.high.as_slice(), candles.low.as_slice())
7016        }
7017        IndicatorDataRef::HighLow { high, low } => (high, low),
7018        IndicatorDataRef::Ohlc { high, low, .. } => (high, low),
7019        IndicatorDataRef::Ohlcv { high, low, .. } => (high, low),
7020        _ => {
7021            return Err(IndicatorDispatchError::MissingRequiredInput {
7022                indicator: "directional_imbalance_index".to_string(),
7023                input: IndicatorInputKind::HighLow,
7024            });
7025        }
7026    };
7027    let kernel = req.kernel.to_non_batch();
7028    collect_f64(
7029        "directional_imbalance_index",
7030        output_id,
7031        req.combos,
7032        high.len(),
7033        |params| {
7034            let length = get_usize_param("directional_imbalance_index", params, "length", 10)?;
7035            let period = get_usize_param("directional_imbalance_index", params, "period", 70)?;
7036            let input = DirectionalImbalanceIndexInput::from_slices(
7037                high,
7038                low,
7039                DirectionalImbalanceIndexParams {
7040                    length: Some(length),
7041                    period: Some(period),
7042                },
7043            );
7044            let out = directional_imbalance_index_with_kernel(&input, kernel).map_err(|e| {
7045                IndicatorDispatchError::ComputeFailed {
7046                    indicator: "directional_imbalance_index".to_string(),
7047                    details: e.to_string(),
7048                }
7049            })?;
7050            if output_id.eq_ignore_ascii_case("up") || output_id.eq_ignore_ascii_case("value") {
7051                return Ok(out.up);
7052            }
7053            if output_id.eq_ignore_ascii_case("down") {
7054                return Ok(out.down);
7055            }
7056            if output_id.eq_ignore_ascii_case("bulls") {
7057                return Ok(out.bulls);
7058            }
7059            if output_id.eq_ignore_ascii_case("bears") {
7060                return Ok(out.bears);
7061            }
7062            if output_id.eq_ignore_ascii_case("upper") {
7063                return Ok(out.upper);
7064            }
7065            if output_id.eq_ignore_ascii_case("lower") {
7066                return Ok(out.lower);
7067            }
7068            Err(IndicatorDispatchError::UnknownOutput {
7069                indicator: "directional_imbalance_index".to_string(),
7070                output: output_id.to_string(),
7071            })
7072        },
7073    )
7074}
7075
7076fn compute_candle_strength_oscillator_batch(
7077    req: IndicatorBatchRequest<'_>,
7078    output_id: &str,
7079) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7080    let (open, high, low, close) = match req.data {
7081        IndicatorDataRef::Candles { candles, .. } => (
7082            candles.open.as_slice(),
7083            candles.high.as_slice(),
7084            candles.low.as_slice(),
7085            candles.close.as_slice(),
7086        ),
7087        IndicatorDataRef::Ohlc {
7088            open,
7089            high,
7090            low,
7091            close,
7092        } => (open, high, low, close),
7093        IndicatorDataRef::Ohlcv {
7094            open,
7095            high,
7096            low,
7097            close,
7098            ..
7099        } => (open, high, low, close),
7100        _ => {
7101            return Err(IndicatorDispatchError::MissingRequiredInput {
7102                indicator: "candle_strength_oscillator".to_string(),
7103                input: IndicatorInputKind::Ohlc,
7104            });
7105        }
7106    };
7107    let kernel = req.kernel.to_non_batch();
7108    collect_f64(
7109        "candle_strength_oscillator",
7110        output_id,
7111        req.combos,
7112        close.len(),
7113        |params| {
7114            let period = get_usize_param("candle_strength_oscillator", params, "period", 50)?;
7115            let atr_enabled =
7116                get_bool_param("candle_strength_oscillator", params, "atr_enabled", false)?;
7117            let atr_length =
7118                get_usize_param("candle_strength_oscillator", params, "atr_length", 50)?;
7119            let mode = get_enum_param("candle_strength_oscillator", params, "mode", "bollinger")?;
7120            let input = CandleStrengthOscillatorInput::from_slices(
7121                open,
7122                high,
7123                low,
7124                close,
7125                CandleStrengthOscillatorParams {
7126                    period: Some(period),
7127                    atr_enabled: Some(atr_enabled),
7128                    atr_length: Some(atr_length),
7129                    mode: Some(mode.to_string()),
7130                },
7131            );
7132            let out = candle_strength_oscillator_with_kernel(&input, kernel).map_err(|e| {
7133                IndicatorDispatchError::ComputeFailed {
7134                    indicator: "candle_strength_oscillator".to_string(),
7135                    details: e.to_string(),
7136                }
7137            })?;
7138            if output_id.eq_ignore_ascii_case("strength") || output_id.eq_ignore_ascii_case("value")
7139            {
7140                return Ok(out.strength);
7141            }
7142            if output_id.eq_ignore_ascii_case("highs") {
7143                return Ok(out.highs);
7144            }
7145            if output_id.eq_ignore_ascii_case("lows") {
7146                return Ok(out.lows);
7147            }
7148            if output_id.eq_ignore_ascii_case("mid") {
7149                return Ok(out.mid);
7150            }
7151            if output_id.eq_ignore_ascii_case("long_signal") {
7152                return Ok(out.long_signal);
7153            }
7154            if output_id.eq_ignore_ascii_case("short_signal") {
7155                return Ok(out.short_signal);
7156            }
7157            Err(IndicatorDispatchError::UnknownOutput {
7158                indicator: "candle_strength_oscillator".to_string(),
7159                output: output_id.to_string(),
7160            })
7161        },
7162    )
7163}
7164
7165fn compute_gmma_oscillator_batch(
7166    req: IndicatorBatchRequest<'_>,
7167    output_id: &str,
7168) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7169    let kernel = req.kernel.to_non_batch();
7170    let owned_source;
7171    let data = match req.data {
7172        IndicatorDataRef::Slice { values } => values,
7173        IndicatorDataRef::Candles { candles, source } => {
7174            source_type(candles, source.unwrap_or("close"))
7175        }
7176        IndicatorDataRef::Ohlc { close, .. } => close,
7177        IndicatorDataRef::Ohlcv { close, .. } => close,
7178        IndicatorDataRef::CloseVolume { close, volume } => {
7179            ensure_same_len_2("gmma_oscillator", close.len(), volume.len())?;
7180            close
7181        }
7182        IndicatorDataRef::HighLow { high, low } => {
7183            ensure_same_len_2("gmma_oscillator", high.len(), low.len())?;
7184            owned_source = high
7185                .iter()
7186                .zip(low.iter())
7187                .map(|(&h, &l)| (h + l) * 0.5)
7188                .collect::<Vec<_>>();
7189            owned_source.as_slice()
7190        }
7191    };
7192
7193    collect_f64(
7194        "gmma_oscillator",
7195        output_id,
7196        req.combos,
7197        data.len(),
7198        |params| {
7199            let gmma_type = get_enum_param("gmma_oscillator", params, "gmma_type", "guppy")?;
7200            let smooth_length = get_usize_param("gmma_oscillator", params, "smooth_length", 1)?;
7201            let signal_length = get_usize_param("gmma_oscillator", params, "signal_length", 13)?;
7202            let anchor_minutes = get_usize_param("gmma_oscillator", params, "anchor_minutes", 0)?;
7203            let interval_minutes = if params
7204                .iter()
7205                .any(|param| param.key.eq_ignore_ascii_case("interval_minutes"))
7206            {
7207                Some(get_usize_param(
7208                    "gmma_oscillator",
7209                    params,
7210                    "interval_minutes",
7211                    1,
7212                )?)
7213            } else {
7214                None
7215            };
7216            let input = GmmaOscillatorInput::from_slice(
7217                data,
7218                GmmaOscillatorParams {
7219                    gmma_type: Some(gmma_type.to_string()),
7220                    smooth_length: Some(smooth_length),
7221                    signal_length: Some(signal_length),
7222                    anchor_minutes: Some(anchor_minutes),
7223                    interval_minutes,
7224                },
7225            );
7226            let out = gmma_oscillator_with_kernel(&input, kernel).map_err(|e| {
7227                IndicatorDispatchError::ComputeFailed {
7228                    indicator: "gmma_oscillator".to_string(),
7229                    details: e.to_string(),
7230                }
7231            })?;
7232            if output_id.eq_ignore_ascii_case("oscillator")
7233                || output_id.eq_ignore_ascii_case("value")
7234            {
7235                return Ok(out.oscillator);
7236            }
7237            if output_id.eq_ignore_ascii_case("signal") {
7238                return Ok(out.signal);
7239            }
7240            Err(IndicatorDispatchError::UnknownOutput {
7241                indicator: "gmma_oscillator".to_string(),
7242                output: output_id.to_string(),
7243            })
7244        },
7245    )
7246}
7247
7248fn compute_nonlinear_regression_zero_lag_moving_average_batch(
7249    req: IndicatorBatchRequest<'_>,
7250    output_id: &str,
7251) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7252    let data = extract_slice_input(
7253        "nonlinear_regression_zero_lag_moving_average",
7254        req.data,
7255        "close",
7256    )?;
7257    let kernel = req.kernel.to_non_batch();
7258    collect_f64(
7259        "nonlinear_regression_zero_lag_moving_average",
7260        output_id,
7261        req.combos,
7262        data.len(),
7263        |params| {
7264            let zlma_period = get_usize_param(
7265                "nonlinear_regression_zero_lag_moving_average",
7266                params,
7267                "zlma_period",
7268                15,
7269            )?;
7270            let regression_period = get_usize_param(
7271                "nonlinear_regression_zero_lag_moving_average",
7272                params,
7273                "regression_period",
7274                15,
7275            )?;
7276            let input = NonlinearRegressionZeroLagMovingAverageInput::from_slice(
7277                data,
7278                NonlinearRegressionZeroLagMovingAverageParams {
7279                    zlma_period: Some(zlma_period),
7280                    regression_period: Some(regression_period),
7281                },
7282            );
7283            let out = nonlinear_regression_zero_lag_moving_average_with_kernel(&input, kernel)
7284                .map_err(|e| IndicatorDispatchError::ComputeFailed {
7285                    indicator: "nonlinear_regression_zero_lag_moving_average".to_string(),
7286                    details: e.to_string(),
7287                })?;
7288            if output_id.eq_ignore_ascii_case("value") {
7289                return Ok(out.value);
7290            }
7291            if output_id.eq_ignore_ascii_case("signal") {
7292                return Ok(out.signal);
7293            }
7294            if output_id.eq_ignore_ascii_case("long_signal") {
7295                return Ok(out.long_signal);
7296            }
7297            if output_id.eq_ignore_ascii_case("short_signal") {
7298                return Ok(out.short_signal);
7299            }
7300            Err(IndicatorDispatchError::UnknownOutput {
7301                indicator: "nonlinear_regression_zero_lag_moving_average".to_string(),
7302                output: output_id.to_string(),
7303            })
7304        },
7305    )
7306}
7307
7308fn compute_possible_rsi_batch(
7309    req: IndicatorBatchRequest<'_>,
7310    output_id: &str,
7311) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7312    let data = extract_slice_input("possible_rsi", req.data, "close")?;
7313    let kernel = req.kernel.to_non_batch();
7314    collect_f64(
7315        "possible_rsi",
7316        output_id,
7317        req.combos,
7318        data.len(),
7319        |params| {
7320            let period = get_usize_param("possible_rsi", params, "period", 32)?;
7321            let rsi_mode = get_enum_param("possible_rsi", params, "rsi_mode", "regular")?;
7322            let norm_period = get_usize_param("possible_rsi", params, "norm_period", 100)?;
7323            let normalization_mode = get_enum_param(
7324                "possible_rsi",
7325                params,
7326                "normalization_mode",
7327                "gaussian_fisher",
7328            )?;
7329            let normalization_length =
7330                get_usize_param("possible_rsi", params, "normalization_length", 15)?;
7331            let nonlag_period = get_usize_param("possible_rsi", params, "nonlag_period", 15)?;
7332            let dynamic_zone_period =
7333                get_usize_param("possible_rsi", params, "dynamic_zone_period", 20)?;
7334            let buy_probability = get_f64_param("possible_rsi", params, "buy_probability", 0.2)?;
7335            let sell_probability = get_f64_param("possible_rsi", params, "sell_probability", 0.2)?;
7336            let signal_type =
7337                get_enum_param("possible_rsi", params, "signal_type", "zeroline_crossover")?;
7338            let run_highpass = get_bool_param("possible_rsi", params, "run_highpass", false)?;
7339            let highpass_period = get_usize_param("possible_rsi", params, "highpass_period", 15)?;
7340            let input = PossibleRsiInput::from_slice(
7341                data,
7342                PossibleRsiParams {
7343                    period: Some(period),
7344                    rsi_mode: Some(rsi_mode.to_string()),
7345                    norm_period: Some(norm_period),
7346                    normalization_mode: Some(normalization_mode.to_string()),
7347                    normalization_length: Some(normalization_length),
7348                    nonlag_period: Some(nonlag_period),
7349                    dynamic_zone_period: Some(dynamic_zone_period),
7350                    buy_probability: Some(buy_probability),
7351                    sell_probability: Some(sell_probability),
7352                    signal_type: Some(signal_type.to_string()),
7353                    run_highpass: Some(run_highpass),
7354                    highpass_period: Some(highpass_period),
7355                },
7356            );
7357            let out = possible_rsi_with_kernel(&input, kernel).map_err(|e| {
7358                IndicatorDispatchError::ComputeFailed {
7359                    indicator: "possible_rsi".to_string(),
7360                    details: e.to_string(),
7361                }
7362            })?;
7363            if output_id.eq_ignore_ascii_case("value") {
7364                return Ok(out.value);
7365            }
7366            if output_id.eq_ignore_ascii_case("buy_level") {
7367                return Ok(out.buy_level);
7368            }
7369            if output_id.eq_ignore_ascii_case("sell_level") {
7370                return Ok(out.sell_level);
7371            }
7372            if output_id.eq_ignore_ascii_case("middle")
7373                || output_id.eq_ignore_ascii_case("middle_level")
7374            {
7375                return Ok(out.middle_level);
7376            }
7377            if output_id.eq_ignore_ascii_case("trend") || output_id.eq_ignore_ascii_case("state") {
7378                return Ok(out.state);
7379            }
7380            if output_id.eq_ignore_ascii_case("long_signal") {
7381                return Ok(out.long_signal);
7382            }
7383            if output_id.eq_ignore_ascii_case("short_signal") {
7384                return Ok(out.short_signal);
7385            }
7386            Err(IndicatorDispatchError::UnknownOutput {
7387                indicator: "possible_rsi".to_string(),
7388                output: output_id.to_string(),
7389            })
7390        },
7391    )
7392}
7393
7394fn compute_autocorrelation_indicator_batch(
7395    req: IndicatorBatchRequest<'_>,
7396    output_id: &str,
7397) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7398    let data = extract_slice_input("autocorrelation_indicator", req.data, "close")?;
7399    let kernel = req.kernel.to_non_batch();
7400    collect_f64(
7401        "autocorrelation_indicator",
7402        output_id,
7403        req.combos,
7404        data.len(),
7405        |params| {
7406            let length = get_usize_param("autocorrelation_indicator", params, "length", 20)?;
7407            let lag = get_usize_param("autocorrelation_indicator", params, "lag", 1)?;
7408            let use_test_signal = get_bool_param(
7409                "autocorrelation_indicator",
7410                params,
7411                "use_test_signal",
7412                false,
7413            )?;
7414            let max_lag = if output_id.eq_ignore_ascii_case("correlation") {
7415                lag
7416            } else {
7417                1
7418            };
7419            let input = AutocorrelationIndicatorInput::from_slice(
7420                data,
7421                AutocorrelationIndicatorParams {
7422                    length: Some(length),
7423                    max_lag: Some(max_lag),
7424                    use_test_signal: Some(use_test_signal),
7425                },
7426            );
7427            let out = autocorrelation_indicator_with_kernel(&input, kernel).map_err(|e| {
7428                IndicatorDispatchError::ComputeFailed {
7429                    indicator: "autocorrelation_indicator".to_string(),
7430                    details: e.to_string(),
7431                }
7432            })?;
7433            if output_id.eq_ignore_ascii_case("filtered") || output_id.eq_ignore_ascii_case("value")
7434            {
7435                return Ok(out.filtered);
7436            }
7437            if output_id.eq_ignore_ascii_case("correlation") {
7438                let start = (lag - 1).checked_mul(data.len()).ok_or_else(|| {
7439                    IndicatorDispatchError::ComputeFailed {
7440                        indicator: "autocorrelation_indicator".to_string(),
7441                        details: "lag * cols overflow".to_string(),
7442                    }
7443                })?;
7444                let end = start + data.len();
7445                return Ok(out.correlations[start..end].to_vec());
7446            }
7447            Err(IndicatorDispatchError::UnknownOutput {
7448                indicator: "autocorrelation_indicator".to_string(),
7449                output: output_id.to_string(),
7450            })
7451        },
7452    )
7453}
7454
7455fn compute_goertzel_cycle_composite_wave_batch(
7456    req: IndicatorBatchRequest<'_>,
7457    output_id: &str,
7458) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7459    if !output_id.eq_ignore_ascii_case("value") && !output_id.eq_ignore_ascii_case("wave") {
7460        return Err(IndicatorDispatchError::UnknownOutput {
7461            indicator: "goertzel_cycle_composite_wave".to_string(),
7462            output: output_id.to_string(),
7463        });
7464    }
7465    let data = extract_slice_input("goertzel_cycle_composite_wave", req.data, "close")?;
7466    let kernel = req.kernel.to_non_batch();
7467    collect_f64_into_rows(
7468        "goertzel_cycle_composite_wave",
7469        output_id,
7470        req.combos,
7471        data.len(),
7472        |params, row| {
7473            let max_period =
7474                get_usize_param("goertzel_cycle_composite_wave", params, "max_period", 120)?;
7475            let start_at_cycle =
7476                get_usize_param("goertzel_cycle_composite_wave", params, "start_at_cycle", 1)?;
7477            let use_top_cycles =
7478                get_usize_param("goertzel_cycle_composite_wave", params, "use_top_cycles", 2)?;
7479            let bar_to_calculate = get_usize_param(
7480                "goertzel_cycle_composite_wave",
7481                params,
7482                "bar_to_calculate",
7483                1,
7484            )?;
7485            let detrend_mode = get_enum_string_param(
7486                "goertzel_cycle_composite_wave",
7487                params,
7488                "detrend_mode",
7489                "hodrick_prescott_detrending",
7490            )?;
7491            let detrend_mode = GoertzelDetrendMode::parse(detrend_mode).ok_or_else(|| {
7492                IndicatorDispatchError::InvalidParam {
7493                    indicator: "goertzel_cycle_composite_wave".to_string(),
7494                    key: "detrend_mode".to_string(),
7495                    reason: format!("unknown mode: {detrend_mode}"),
7496                }
7497            })?;
7498            let dt_zl_per1 =
7499                get_usize_param("goertzel_cycle_composite_wave", params, "dt_zl_per1", 10)?;
7500            let dt_zl_per2 =
7501                get_usize_param("goertzel_cycle_composite_wave", params, "dt_zl_per2", 40)?;
7502            let dt_hp_per1 =
7503                get_usize_param("goertzel_cycle_composite_wave", params, "dt_hp_per1", 20)?;
7504            let dt_hp_per2 =
7505                get_usize_param("goertzel_cycle_composite_wave", params, "dt_hp_per2", 80)?;
7506            let dt_reg_zl_smooth_per = get_usize_param(
7507                "goertzel_cycle_composite_wave",
7508                params,
7509                "dt_reg_zl_smooth_per",
7510                5,
7511            )?;
7512            let hp_smooth_per =
7513                get_usize_param("goertzel_cycle_composite_wave", params, "hp_smooth_per", 20)?;
7514            let zlma_smooth_per = get_usize_param(
7515                "goertzel_cycle_composite_wave",
7516                params,
7517                "zlma_smooth_per",
7518                10,
7519            )?;
7520            let filter_bartels = get_bool_param(
7521                "goertzel_cycle_composite_wave",
7522                params,
7523                "filter_bartels",
7524                false,
7525            )?;
7526            let bart_no_cycles =
7527                get_usize_param("goertzel_cycle_composite_wave", params, "bart_no_cycles", 5)?;
7528            let bart_smooth_per = get_usize_param(
7529                "goertzel_cycle_composite_wave",
7530                params,
7531                "bart_smooth_per",
7532                2,
7533            )?;
7534            let bart_sig_limit = get_usize_param(
7535                "goertzel_cycle_composite_wave",
7536                params,
7537                "bart_sig_limit",
7538                50,
7539            )?;
7540            let sort_bartels = get_bool_param(
7541                "goertzel_cycle_composite_wave",
7542                params,
7543                "sort_bartels",
7544                false,
7545            )?;
7546            let squared_amp =
7547                get_bool_param("goertzel_cycle_composite_wave", params, "squared_amp", true)?;
7548            let use_cosine =
7549                get_bool_param("goertzel_cycle_composite_wave", params, "use_cosine", true)?;
7550            let subtract_noise = get_bool_param(
7551                "goertzel_cycle_composite_wave",
7552                params,
7553                "subtract_noise",
7554                false,
7555            )?;
7556            let use_cycle_strength = get_bool_param(
7557                "goertzel_cycle_composite_wave",
7558                params,
7559                "use_cycle_strength",
7560                true,
7561            )?;
7562
7563            let input = GoertzelCycleCompositeWaveInput::from_slice(
7564                data,
7565                GoertzelCycleCompositeWaveParams {
7566                    max_period: Some(max_period),
7567                    start_at_cycle: Some(start_at_cycle),
7568                    use_top_cycles: Some(use_top_cycles),
7569                    bar_to_calculate: Some(bar_to_calculate),
7570                    detrend_mode: Some(detrend_mode),
7571                    dt_zl_per1: Some(dt_zl_per1),
7572                    dt_zl_per2: Some(dt_zl_per2),
7573                    dt_hp_per1: Some(dt_hp_per1),
7574                    dt_hp_per2: Some(dt_hp_per2),
7575                    dt_reg_zl_smooth_per: Some(dt_reg_zl_smooth_per),
7576                    hp_smooth_per: Some(hp_smooth_per),
7577                    zlma_smooth_per: Some(zlma_smooth_per),
7578                    filter_bartels: Some(filter_bartels),
7579                    bart_no_cycles: Some(bart_no_cycles),
7580                    bart_smooth_per: Some(bart_smooth_per),
7581                    bart_sig_limit: Some(bart_sig_limit),
7582                    sort_bartels: Some(sort_bartels),
7583                    squared_amp: Some(squared_amp),
7584                    use_cosine: Some(use_cosine),
7585                    subtract_noise: Some(subtract_noise),
7586                    use_cycle_strength: Some(use_cycle_strength),
7587                },
7588            );
7589            goertzel_cycle_composite_wave_into_slice(row, &input, kernel).map_err(|e| {
7590                IndicatorDispatchError::ComputeFailed {
7591                    indicator: "goertzel_cycle_composite_wave".to_string(),
7592                    details: e.to_string(),
7593                }
7594            })
7595        },
7596    )
7597}
7598
7599fn compute_rolling_skewness_kurtosis_batch(
7600    req: IndicatorBatchRequest<'_>,
7601    output_id: &str,
7602) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7603    let data = extract_slice_input("rolling_skewness_kurtosis", req.data, "close")?;
7604    let kernel = req.kernel.to_non_batch();
7605    collect_f64(
7606        "rolling_skewness_kurtosis",
7607        output_id,
7608        req.combos,
7609        data.len(),
7610        |params| {
7611            let length = get_usize_param("rolling_skewness_kurtosis", params, "length", 50)?;
7612            let smooth_length =
7613                get_usize_param("rolling_skewness_kurtosis", params, "smooth_length", 3)?;
7614            let input = RollingSkewnessKurtosisInput::from_slice(
7615                data,
7616                RollingSkewnessKurtosisParams {
7617                    length: Some(length),
7618                    smooth_length: Some(smooth_length),
7619                },
7620            );
7621            let out = rolling_skewness_kurtosis_with_kernel(&input, kernel).map_err(|e| {
7622                IndicatorDispatchError::ComputeFailed {
7623                    indicator: "rolling_skewness_kurtosis".to_string(),
7624                    details: e.to_string(),
7625                }
7626            })?;
7627            if output_id.eq_ignore_ascii_case("skewness") {
7628                return Ok(out.skewness);
7629            }
7630            if output_id.eq_ignore_ascii_case("kurtosis") {
7631                return Ok(out.kurtosis);
7632            }
7633            Err(IndicatorDispatchError::UnknownOutput {
7634                indicator: "rolling_skewness_kurtosis".to_string(),
7635                output: output_id.to_string(),
7636            })
7637        },
7638    )
7639}
7640
7641fn compute_rolling_z_score_trend_batch(
7642    req: IndicatorBatchRequest<'_>,
7643    output_id: &str,
7644) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7645    let data = extract_slice_input("rolling_z_score_trend", req.data, "close")?;
7646    let kernel = req.kernel.to_non_batch();
7647    collect_f64(
7648        "rolling_z_score_trend",
7649        output_id,
7650        req.combos,
7651        data.len(),
7652        |params| {
7653            let lookback_period =
7654                get_usize_param("rolling_z_score_trend", params, "lookback_period", 20)?;
7655            let input = RollingZScoreTrendInput::from_slice(
7656                data,
7657                RollingZScoreTrendParams {
7658                    lookback_period: Some(lookback_period),
7659                },
7660            );
7661            let out = rolling_z_score_trend_with_kernel(&input, kernel).map_err(|e| {
7662                IndicatorDispatchError::ComputeFailed {
7663                    indicator: "rolling_z_score_trend".to_string(),
7664                    details: e.to_string(),
7665                }
7666            })?;
7667            if output_id.eq_ignore_ascii_case("zscore") {
7668                return Ok(out.zscore);
7669            }
7670            if output_id.eq_ignore_ascii_case("momentum") {
7671                return Ok(out.momentum);
7672            }
7673            Err(IndicatorDispatchError::UnknownOutput {
7674                indicator: "rolling_z_score_trend".to_string(),
7675                output: output_id.to_string(),
7676            })
7677        },
7678    )
7679}
7680
7681fn compute_ehlers_data_sampling_relative_strength_indicator_batch(
7682    req: IndicatorBatchRequest<'_>,
7683    output_id: &str,
7684) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7685    let (open, close) = match req.data {
7686        IndicatorDataRef::Candles { candles, .. } => {
7687            (candles.open.as_slice(), candles.close.as_slice())
7688        }
7689        IndicatorDataRef::Ohlc {
7690            open,
7691            high,
7692            low,
7693            close,
7694        } => {
7695            ensure_same_len_4(
7696                "ehlers_data_sampling_relative_strength_indicator",
7697                open.len(),
7698                high.len(),
7699                low.len(),
7700                close.len(),
7701            )?;
7702            (open, close)
7703        }
7704        IndicatorDataRef::Ohlcv {
7705            open,
7706            high,
7707            low,
7708            close,
7709            volume,
7710        } => {
7711            ensure_same_len_5(
7712                "ehlers_data_sampling_relative_strength_indicator",
7713                open.len(),
7714                high.len(),
7715                low.len(),
7716                close.len(),
7717                volume.len(),
7718            )?;
7719            (open, close)
7720        }
7721        _ => {
7722            return Err(IndicatorDispatchError::MissingRequiredInput {
7723                indicator: "ehlers_data_sampling_relative_strength_indicator".to_string(),
7724                input: IndicatorInputKind::Ohlc,
7725            })
7726        }
7727    };
7728    let kernel = req.kernel.to_non_batch();
7729    collect_f64(
7730        "ehlers_data_sampling_relative_strength_indicator",
7731        output_id,
7732        req.combos,
7733        close.len(),
7734        |params| {
7735            let length = get_usize_param(
7736                "ehlers_data_sampling_relative_strength_indicator",
7737                params,
7738                "length",
7739                14,
7740            )?;
7741            let input = EhlersDataSamplingRelativeStrengthIndicatorInput::from_slices(
7742                open,
7743                close,
7744                EhlersDataSamplingRelativeStrengthIndicatorParams {
7745                    length: Some(length),
7746                },
7747            );
7748            let out = ehlers_data_sampling_relative_strength_indicator_with_kernel(&input, kernel)
7749                .map_err(|e| IndicatorDispatchError::ComputeFailed {
7750                    indicator: "ehlers_data_sampling_relative_strength_indicator".to_string(),
7751                    details: e.to_string(),
7752                })?;
7753            if output_id.eq_ignore_ascii_case("ds_rsi")
7754                || output_id.eq_ignore_ascii_case("data_sampling_rsi")
7755            {
7756                return Ok(out.ds_rsi);
7757            }
7758            if output_id.eq_ignore_ascii_case("original_rsi")
7759                || output_id.eq_ignore_ascii_case("orig_rsi")
7760            {
7761                return Ok(out.original_rsi);
7762            }
7763            if output_id.eq_ignore_ascii_case("signal") {
7764                return Ok(out.signal);
7765            }
7766            Err(IndicatorDispatchError::UnknownOutput {
7767                indicator: "ehlers_data_sampling_relative_strength_indicator".to_string(),
7768                output: output_id.to_string(),
7769            })
7770        },
7771    )
7772}
7773
7774fn compute_velocity_acceleration_convergence_divergence_indicator_batch(
7775    req: IndicatorBatchRequest<'_>,
7776    output_id: &str,
7777) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7778    let owned_source;
7779    let data = match req.data {
7780        IndicatorDataRef::Slice { values } => values,
7781        IndicatorDataRef::Candles { candles, source } => {
7782            source_type(candles, source.unwrap_or("hlcc4"))
7783        }
7784        IndicatorDataRef::Ohlc {
7785            open,
7786            high,
7787            low,
7788            close,
7789        } => {
7790            ensure_same_len_4(
7791                "velocity_acceleration_convergence_divergence_indicator",
7792                open.len(),
7793                high.len(),
7794                low.len(),
7795                close.len(),
7796            )?;
7797            owned_source = high
7798                .iter()
7799                .zip(low.iter())
7800                .zip(close.iter())
7801                .map(|((&h, &l), &c)| (h + l + 2.0 * c) * 0.25)
7802                .collect::<Vec<_>>();
7803            owned_source.as_slice()
7804        }
7805        IndicatorDataRef::Ohlcv {
7806            open,
7807            high,
7808            low,
7809            close,
7810            volume,
7811        } => {
7812            ensure_same_len_5(
7813                "velocity_acceleration_convergence_divergence_indicator",
7814                open.len(),
7815                high.len(),
7816                low.len(),
7817                close.len(),
7818                volume.len(),
7819            )?;
7820            owned_source = high
7821                .iter()
7822                .zip(low.iter())
7823                .zip(close.iter())
7824                .map(|((&h, &l), &c)| (h + l + 2.0 * c) * 0.25)
7825                .collect::<Vec<_>>();
7826            owned_source.as_slice()
7827        }
7828        IndicatorDataRef::CloseVolume { close, volume } => {
7829            ensure_same_len_2(
7830                "velocity_acceleration_convergence_divergence_indicator",
7831                close.len(),
7832                volume.len(),
7833            )?;
7834            close
7835        }
7836        IndicatorDataRef::HighLow { .. } => {
7837            return Err(IndicatorDispatchError::MissingRequiredInput {
7838                indicator: "velocity_acceleration_convergence_divergence_indicator".to_string(),
7839                input: IndicatorInputKind::Candles,
7840            });
7841        }
7842    };
7843    let kernel = req.kernel.to_non_batch();
7844    collect_f64(
7845        "velocity_acceleration_convergence_divergence_indicator",
7846        output_id,
7847        req.combos,
7848        data.len(),
7849        |params| {
7850            let length = get_usize_param(
7851                "velocity_acceleration_convergence_divergence_indicator",
7852                params,
7853                "length",
7854                21,
7855            )?;
7856            let smooth_length = get_usize_param(
7857                "velocity_acceleration_convergence_divergence_indicator",
7858                params,
7859                "smooth_length",
7860                5,
7861            )?;
7862            let input = VelocityAccelerationConvergenceDivergenceIndicatorInput::from_slice(
7863                data,
7864                VelocityAccelerationConvergenceDivergenceIndicatorParams {
7865                    length: Some(length),
7866                    smooth_length: Some(smooth_length),
7867                },
7868            );
7869            let out =
7870                velocity_acceleration_convergence_divergence_indicator_with_kernel(&input, kernel)
7871                    .map_err(|e| IndicatorDispatchError::ComputeFailed {
7872                        indicator: "velocity_acceleration_convergence_divergence_indicator"
7873                            .to_string(),
7874                        details: e.to_string(),
7875                    })?;
7876            if output_id.eq_ignore_ascii_case("vacd") || output_id.eq_ignore_ascii_case("value") {
7877                return Ok(out.vacd);
7878            }
7879            if output_id.eq_ignore_ascii_case("signal") {
7880                return Ok(out.signal);
7881            }
7882            Err(IndicatorDispatchError::UnknownOutput {
7883                indicator: "velocity_acceleration_convergence_divergence_indicator".to_string(),
7884                output: output_id.to_string(),
7885            })
7886        },
7887    )
7888}
7889
7890fn compute_trend_direction_force_index_batch(
7891    req: IndicatorBatchRequest<'_>,
7892    output_id: &str,
7893) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7894    expect_value_output("trend_direction_force_index", output_id)?;
7895    let data = extract_slice_input("trend_direction_force_index", req.data, "close")?;
7896    let kernel = req.kernel.to_non_batch();
7897    collect_f64_into_rows(
7898        "trend_direction_force_index",
7899        output_id,
7900        req.combos,
7901        data.len(),
7902        |params, row| {
7903            let length = get_usize_param("trend_direction_force_index", params, "length", 10)?;
7904            let input = TrendDirectionForceIndexInput::from_slice(
7905                data,
7906                TrendDirectionForceIndexParams {
7907                    length: Some(length),
7908                },
7909            );
7910            trend_direction_force_index_into_slice(row, &input, kernel).map_err(|e| {
7911                IndicatorDispatchError::ComputeFailed {
7912                    indicator: "trend_direction_force_index".to_string(),
7913                    details: e.to_string(),
7914                }
7915            })
7916        },
7917    )
7918}
7919
7920fn compute_yang_zhang_volatility_batch(
7921    req: IndicatorBatchRequest<'_>,
7922    output_id: &str,
7923) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7924    let (open, high, low, close) = extract_ohlc_full_input("yang_zhang_volatility", req.data)?;
7925    let kernel = req.kernel.to_non_batch();
7926    collect_f64(
7927        "yang_zhang_volatility",
7928        output_id,
7929        req.combos,
7930        close.len(),
7931        |params| {
7932            let lookback = get_usize_param("yang_zhang_volatility", params, "lookback", 14)?;
7933            let k_override = get_bool_param("yang_zhang_volatility", params, "k_override", false)?;
7934            let k = get_f64_param("yang_zhang_volatility", params, "k", 0.34)?;
7935            let input = YangZhangVolatilityInput::from_slices(
7936                open,
7937                high,
7938                low,
7939                close,
7940                YangZhangVolatilityParams {
7941                    lookback: Some(lookback),
7942                    k_override: Some(k_override),
7943                    k: Some(k),
7944                },
7945            );
7946            let out = yang_zhang_volatility_with_kernel(&input, kernel).map_err(|e| {
7947                IndicatorDispatchError::ComputeFailed {
7948                    indicator: "yang_zhang_volatility".to_string(),
7949                    details: e.to_string(),
7950                }
7951            })?;
7952            if output_id.eq_ignore_ascii_case("yz") || output_id.eq_ignore_ascii_case("value") {
7953                return Ok(out.yz);
7954            }
7955            if output_id.eq_ignore_ascii_case("rs") {
7956                return Ok(out.rs);
7957            }
7958            Err(IndicatorDispatchError::UnknownOutput {
7959                indicator: "yang_zhang_volatility".to_string(),
7960                output: output_id.to_string(),
7961            })
7962        },
7963    )
7964}
7965
7966fn compute_garman_klass_volatility_batch(
7967    req: IndicatorBatchRequest<'_>,
7968    output_id: &str,
7969) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
7970    let (open, high, low, close) = extract_ohlc_full_input("garman_klass_volatility", req.data)?;
7971    let kernel = req.kernel.to_non_batch();
7972    collect_f64(
7973        "garman_klass_volatility",
7974        output_id,
7975        req.combos,
7976        close.len(),
7977        |params| {
7978            let lookback = get_usize_param("garman_klass_volatility", params, "lookback", 14)?;
7979            let input = GarmanKlassVolatilityInput::from_slices(
7980                open,
7981                high,
7982                low,
7983                close,
7984                GarmanKlassVolatilityParams {
7985                    lookback: Some(lookback),
7986                },
7987            );
7988            let out = garman_klass_volatility_with_kernel(&input, kernel).map_err(|e| {
7989                IndicatorDispatchError::ComputeFailed {
7990                    indicator: "garman_klass_volatility".to_string(),
7991                    details: e.to_string(),
7992                }
7993            })?;
7994            if output_id.eq_ignore_ascii_case("value") {
7995                return Ok(out.values);
7996            }
7997            Err(IndicatorDispatchError::UnknownOutput {
7998                indicator: "garman_klass_volatility".to_string(),
7999                output: output_id.to_string(),
8000            })
8001        },
8002    )
8003}
8004
8005fn compute_atr_percentile_batch(
8006    req: IndicatorBatchRequest<'_>,
8007    output_id: &str,
8008) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8009    expect_value_output("atr_percentile", output_id)?;
8010    let (high, low, close) = extract_ohlc_input("atr_percentile", req.data)?;
8011    let kernel = req.kernel.to_non_batch();
8012    collect_f64(
8013        "atr_percentile",
8014        output_id,
8015        req.combos,
8016        close.len(),
8017        |params| {
8018            let atr_length = get_usize_param("atr_percentile", params, "atr_length", 10)?;
8019            let percentile_length =
8020                get_usize_param("atr_percentile", params, "percentile_length", 50)?;
8021            let input = AtrPercentileInput::from_slices(
8022                high,
8023                low,
8024                close,
8025                AtrPercentileParams {
8026                    atr_length: Some(atr_length),
8027                    percentile_length: Some(percentile_length),
8028                },
8029            );
8030            let out = atr_percentile_with_kernel(&input, kernel).map_err(|e| {
8031                IndicatorDispatchError::ComputeFailed {
8032                    indicator: "atr_percentile".to_string(),
8033                    details: e.to_string(),
8034                }
8035            })?;
8036            Ok(out.values)
8037        },
8038    )
8039}
8040
8041fn compute_bull_power_vs_bear_power_batch(
8042    req: IndicatorBatchRequest<'_>,
8043    output_id: &str,
8044) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8045    expect_value_output("bull_power_vs_bear_power", output_id)?;
8046    let (open, high, low, close) = extract_ohlc_full_input("bull_power_vs_bear_power", req.data)?;
8047    let kernel = req.kernel.to_non_batch();
8048    collect_f64(
8049        "bull_power_vs_bear_power",
8050        output_id,
8051        req.combos,
8052        close.len(),
8053        |params| {
8054            let period = get_usize_param("bull_power_vs_bear_power", params, "period", 5)?;
8055            let input = BullPowerVsBearPowerInput::from_slices(
8056                open,
8057                high,
8058                low,
8059                close,
8060                BullPowerVsBearPowerParams {
8061                    period: Some(period),
8062                },
8063            );
8064            let out = bull_power_vs_bear_power_with_kernel(&input, kernel).map_err(|e| {
8065                IndicatorDispatchError::ComputeFailed {
8066                    indicator: "bull_power_vs_bear_power".to_string(),
8067                    details: e.to_string(),
8068                }
8069            })?;
8070            Ok(out.values)
8071        },
8072    )
8073}
8074
8075fn compute_advance_decline_line_batch(
8076    req: IndicatorBatchRequest<'_>,
8077    output_id: &str,
8078) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8079    expect_value_output("advance_decline_line", output_id)?;
8080    let data = extract_slice_input("advance_decline_line", req.data, "close")?;
8081    let kernel = req.kernel.to_non_batch();
8082    collect_f64(
8083        "advance_decline_line",
8084        output_id,
8085        req.combos,
8086        data.len(),
8087        |_params| {
8088            let input = AdvanceDeclineLineInput::from_slice(data, AdvanceDeclineLineParams);
8089            let out = advance_decline_line_with_kernel(&input, kernel).map_err(|e| {
8090                IndicatorDispatchError::ComputeFailed {
8091                    indicator: "advance_decline_line".to_string(),
8092                    details: e.to_string(),
8093                }
8094            })?;
8095            Ok(out.values)
8096        },
8097    )
8098}
8099
8100fn compute_didi_index_batch(
8101    req: IndicatorBatchRequest<'_>,
8102    output_id: &str,
8103) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8104    let data = extract_slice_input("didi_index", req.data, "close")?;
8105    let kernel = req.kernel.to_non_batch();
8106    collect_f64("didi_index", output_id, req.combos, data.len(), |params| {
8107        let short_length = get_usize_param("didi_index", params, "short_length", 3)?;
8108        let medium_length = get_usize_param("didi_index", params, "medium_length", 8)?;
8109        let long_length = get_usize_param("didi_index", params, "long_length", 20)?;
8110        let input = DidiIndexInput::from_slice(
8111            data,
8112            DidiIndexParams {
8113                short_length: Some(short_length),
8114                medium_length: Some(medium_length),
8115                long_length: Some(long_length),
8116            },
8117        );
8118        let out = didi_index_with_kernel(&input, kernel).map_err(|e| {
8119            IndicatorDispatchError::ComputeFailed {
8120                indicator: "didi_index".to_string(),
8121                details: e.to_string(),
8122            }
8123        })?;
8124        if output_id.eq_ignore_ascii_case("short") || output_id.eq_ignore_ascii_case("value") {
8125            return Ok(out.short);
8126        }
8127        if output_id.eq_ignore_ascii_case("long") {
8128            return Ok(out.long);
8129        }
8130        if output_id.eq_ignore_ascii_case("crossover") {
8131            return Ok(out.crossover);
8132        }
8133        if output_id.eq_ignore_ascii_case("crossunder") {
8134            return Ok(out.crossunder);
8135        }
8136        Err(IndicatorDispatchError::UnknownOutput {
8137            indicator: "didi_index".to_string(),
8138            output: output_id.to_string(),
8139        })
8140    })
8141}
8142
8143fn compute_absolute_strength_index_oscillator_batch(
8144    req: IndicatorBatchRequest<'_>,
8145    output_id: &str,
8146) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8147    let data = extract_slice_input("absolute_strength_index_oscillator", req.data, "close")?;
8148    let kernel = req.kernel.to_non_batch();
8149    collect_f64(
8150        "absolute_strength_index_oscillator",
8151        output_id,
8152        req.combos,
8153        data.len(),
8154        |params| {
8155            let ema_length = get_usize_param(
8156                "absolute_strength_index_oscillator",
8157                params,
8158                "ema_length",
8159                21,
8160            )?;
8161            let signal_length = get_usize_param(
8162                "absolute_strength_index_oscillator",
8163                params,
8164                "signal_length",
8165                34,
8166            )?;
8167            let input = AbsoluteStrengthIndexOscillatorInput::from_slice(
8168                data,
8169                AbsoluteStrengthIndexOscillatorParams {
8170                    ema_length: Some(ema_length),
8171                    signal_length: Some(signal_length),
8172                },
8173            );
8174            let out =
8175                absolute_strength_index_oscillator_with_kernel(&input, kernel).map_err(|e| {
8176                    IndicatorDispatchError::ComputeFailed {
8177                        indicator: "absolute_strength_index_oscillator".to_string(),
8178                        details: e.to_string(),
8179                    }
8180                })?;
8181            if output_id.eq_ignore_ascii_case("oscillator")
8182                || output_id.eq_ignore_ascii_case("indicator")
8183                || output_id.eq_ignore_ascii_case("value")
8184            {
8185                return Ok(out.oscillator);
8186            }
8187            if output_id.eq_ignore_ascii_case("signal") {
8188                return Ok(out.signal);
8189            }
8190            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
8191            {
8192                return Ok(out.histogram);
8193            }
8194            Err(IndicatorDispatchError::UnknownOutput {
8195                indicator: "absolute_strength_index_oscillator".to_string(),
8196                output: output_id.to_string(),
8197            })
8198        },
8199    )
8200}
8201
8202fn compute_adaptive_bandpass_trigger_oscillator_batch(
8203    req: IndicatorBatchRequest<'_>,
8204    output_id: &str,
8205) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8206    let data = extract_slice_input("adaptive_bandpass_trigger_oscillator", req.data, "close")?;
8207    let kernel = req.kernel.to_non_batch();
8208    collect_f64(
8209        "adaptive_bandpass_trigger_oscillator",
8210        output_id,
8211        req.combos,
8212        data.len(),
8213        |params| {
8214            let delta =
8215                get_f64_param("adaptive_bandpass_trigger_oscillator", params, "delta", 0.1)?;
8216            let alpha = get_f64_param(
8217                "adaptive_bandpass_trigger_oscillator",
8218                params,
8219                "alpha",
8220                0.07,
8221            )?;
8222            let input = AdaptiveBandpassTriggerOscillatorInput::from_slice(
8223                data,
8224                AdaptiveBandpassTriggerOscillatorParams {
8225                    delta: Some(delta),
8226                    alpha: Some(alpha),
8227                },
8228            );
8229            let out =
8230                adaptive_bandpass_trigger_oscillator_with_kernel(&input, kernel).map_err(|e| {
8231                    IndicatorDispatchError::ComputeFailed {
8232                        indicator: "adaptive_bandpass_trigger_oscillator".to_string(),
8233                        details: e.to_string(),
8234                    }
8235                })?;
8236            match output_id {
8237                "in_phase" => Ok(out.in_phase),
8238                "lead" => Ok(out.lead),
8239                _ => Err(IndicatorDispatchError::UnknownOutput {
8240                    indicator: "adaptive_bandpass_trigger_oscillator".to_string(),
8241                    output: output_id.to_string(),
8242                }),
8243            }
8244        },
8245    )
8246}
8247
8248fn compute_premier_rsi_oscillator_batch(
8249    req: IndicatorBatchRequest<'_>,
8250    output_id: &str,
8251) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8252    expect_value_output("premier_rsi_oscillator", output_id)?;
8253    let data = extract_slice_input("premier_rsi_oscillator", req.data, "close")?;
8254    let kernel = req.kernel.to_non_batch();
8255    collect_f64(
8256        "premier_rsi_oscillator",
8257        output_id,
8258        req.combos,
8259        data.len(),
8260        |params| {
8261            let rsi_length = get_usize_param("premier_rsi_oscillator", params, "rsi_length", 14)?;
8262            let stoch_length =
8263                get_usize_param("premier_rsi_oscillator", params, "stoch_length", 8)?;
8264            let smooth_length =
8265                get_usize_param("premier_rsi_oscillator", params, "smooth_length", 25)?;
8266            let input = PremierRsiOscillatorInput::from_slice(
8267                data,
8268                PremierRsiOscillatorParams {
8269                    rsi_length: Some(rsi_length),
8270                    stoch_length: Some(stoch_length),
8271                    smooth_length: Some(smooth_length),
8272                },
8273            );
8274            let out = premier_rsi_oscillator_with_kernel(&input, kernel).map_err(|e| {
8275                IndicatorDispatchError::ComputeFailed {
8276                    indicator: "premier_rsi_oscillator".to_string(),
8277                    details: e.to_string(),
8278                }
8279            })?;
8280            Ok(out.values)
8281        },
8282    )
8283}
8284
8285fn compute_multi_length_stochastic_average_batch(
8286    req: IndicatorBatchRequest<'_>,
8287    output_id: &str,
8288) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8289    expect_value_output("multi_length_stochastic_average", output_id)?;
8290    let data_len = match req.data {
8291        IndicatorDataRef::Slice { values } => values.len(),
8292        IndicatorDataRef::Candles { candles, source } => {
8293            source_type(candles, source.unwrap_or("close")).len()
8294        }
8295        _ => {
8296            return Err(IndicatorDispatchError::MissingRequiredInput {
8297                indicator: "multi_length_stochastic_average".to_string(),
8298                input: IndicatorInputKind::Candles,
8299            });
8300        }
8301    };
8302    let kernel = req.kernel.to_non_batch();
8303    collect_f64(
8304        "multi_length_stochastic_average",
8305        output_id,
8306        req.combos,
8307        data_len,
8308        |params| {
8309            let source =
8310                get_enum_param("multi_length_stochastic_average", params, "source", "close")?;
8311            let length = get_usize_param("multi_length_stochastic_average", params, "length", 14)?;
8312            let presmooth =
8313                get_usize_param("multi_length_stochastic_average", params, "presmooth", 10)?;
8314            let premethod = get_enum_param(
8315                "multi_length_stochastic_average",
8316                params,
8317                "premethod",
8318                "sma",
8319            )?;
8320            let postsmooth =
8321                get_usize_param("multi_length_stochastic_average", params, "postsmooth", 10)?;
8322            let postmethod = get_enum_param(
8323                "multi_length_stochastic_average",
8324                params,
8325                "postmethod",
8326                "sma",
8327            )?;
8328            let data = match req.data {
8329                IndicatorDataRef::Slice { values } => values,
8330                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8331                _ => unreachable!(),
8332            };
8333            let input = MultiLengthStochasticAverageInput::from_slice(
8334                data,
8335                MultiLengthStochasticAverageParams {
8336                    length: Some(length),
8337                    presmooth: Some(presmooth),
8338                    premethod: Some(premethod),
8339                    postsmooth: Some(postsmooth),
8340                    postmethod: Some(postmethod),
8341                },
8342            );
8343            let out = multi_length_stochastic_average_with_kernel(&input, kernel).map_err(|e| {
8344                IndicatorDispatchError::ComputeFailed {
8345                    indicator: "multi_length_stochastic_average".to_string(),
8346                    details: e.to_string(),
8347                }
8348            })?;
8349            Ok(out.values)
8350        },
8351    )
8352}
8353
8354fn compute_hull_butterfly_oscillator_batch(
8355    req: IndicatorBatchRequest<'_>,
8356    output_id: &str,
8357) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8358    let data_len = match req.data {
8359        IndicatorDataRef::Slice { values } => values.len(),
8360        IndicatorDataRef::Candles { candles, source } => {
8361            source_type(candles, source.unwrap_or("close")).len()
8362        }
8363        _ => {
8364            return Err(IndicatorDispatchError::MissingRequiredInput {
8365                indicator: "hull_butterfly_oscillator".to_string(),
8366                input: IndicatorInputKind::Candles,
8367            });
8368        }
8369    };
8370    let kernel = req.kernel.to_non_batch();
8371    collect_f64(
8372        "hull_butterfly_oscillator",
8373        output_id,
8374        req.combos,
8375        data_len,
8376        |params| {
8377            let source = get_enum_param("hull_butterfly_oscillator", params, "source", "close")?;
8378            let length = get_usize_param("hull_butterfly_oscillator", params, "length", 14)?;
8379            let mult = get_f64_param("hull_butterfly_oscillator", params, "mult", 2.0)?;
8380            let data = match req.data {
8381                IndicatorDataRef::Slice { values } => values,
8382                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8383                _ => unreachable!(),
8384            };
8385            let input = HullButterflyOscillatorInput::from_slice(
8386                data,
8387                HullButterflyOscillatorParams {
8388                    length: Some(length),
8389                    mult: Some(mult),
8390                },
8391            );
8392            let out = hull_butterfly_oscillator_with_kernel(&input, kernel).map_err(|e| {
8393                IndicatorDispatchError::ComputeFailed {
8394                    indicator: "hull_butterfly_oscillator".to_string(),
8395                    details: e.to_string(),
8396                }
8397            })?;
8398            match output_id {
8399                "oscillator" => Ok(out.oscillator),
8400                "cumulative_mean" => Ok(out.cumulative_mean),
8401                "signal" => Ok(out.signal),
8402                _ => Err(IndicatorDispatchError::UnknownOutput {
8403                    indicator: "hull_butterfly_oscillator".to_string(),
8404                    output: output_id.to_string(),
8405                }),
8406            }
8407        },
8408    )
8409}
8410
8411fn compute_fibonacci_trailing_stop_batch(
8412    req: IndicatorBatchRequest<'_>,
8413    output_id: &str,
8414) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8415    let (high, low, close) = extract_ohlc_input("fibonacci_trailing_stop", req.data)?;
8416    let kernel = req.kernel.to_non_batch();
8417    collect_f64(
8418        "fibonacci_trailing_stop",
8419        output_id,
8420        req.combos,
8421        close.len(),
8422        |params| {
8423            let left_bars = get_usize_param("fibonacci_trailing_stop", params, "left_bars", 20)?;
8424            let right_bars = get_usize_param("fibonacci_trailing_stop", params, "right_bars", 1)?;
8425            let level = get_f64_param("fibonacci_trailing_stop", params, "level", -0.382)?;
8426            let trigger = get_enum_param("fibonacci_trailing_stop", params, "trigger", "close")?;
8427            let input = FibonacciTrailingStopInput::from_slices(
8428                high,
8429                low,
8430                close,
8431                FibonacciTrailingStopParams {
8432                    left_bars: Some(left_bars),
8433                    right_bars: Some(right_bars),
8434                    level: Some(level),
8435                    trigger: Some(trigger),
8436                },
8437            );
8438            let out = fibonacci_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
8439                IndicatorDispatchError::ComputeFailed {
8440                    indicator: "fibonacci_trailing_stop".to_string(),
8441                    details: e.to_string(),
8442                }
8443            })?;
8444            if output_id.eq_ignore_ascii_case("trailing_stop")
8445                || output_id.eq_ignore_ascii_case("value")
8446            {
8447                return Ok(out.trailing_stop);
8448            }
8449            if output_id.eq_ignore_ascii_case("long_stop") {
8450                return Ok(out.long_stop);
8451            }
8452            if output_id.eq_ignore_ascii_case("short_stop") {
8453                return Ok(out.short_stop);
8454            }
8455            if output_id.eq_ignore_ascii_case("direction") {
8456                return Ok(out.direction);
8457            }
8458            Err(IndicatorDispatchError::UnknownOutput {
8459                indicator: "fibonacci_trailing_stop".to_string(),
8460                output: output_id.to_string(),
8461            })
8462        },
8463    )
8464}
8465
8466fn compute_fibonacci_entry_bands_batch(
8467    req: IndicatorBatchRequest<'_>,
8468    output_id: &str,
8469) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8470    let (open, high, low, close) = extract_ohlc_full_input("fibonacci_entry_bands", req.data)?;
8471    let kernel = req.kernel.to_non_batch();
8472    collect_f64(
8473        "fibonacci_entry_bands",
8474        output_id,
8475        req.combos,
8476        close.len(),
8477        |params| {
8478            let source = get_enum_param("fibonacci_entry_bands", params, "source", "hlc3")?;
8479            let length = get_usize_param("fibonacci_entry_bands", params, "length", 21)?;
8480            let atr_length = get_usize_param("fibonacci_entry_bands", params, "atr_length", 14)?;
8481            let use_atr = get_bool_param("fibonacci_entry_bands", params, "use_atr", true)?;
8482            let tp_aggressiveness =
8483                get_enum_param("fibonacci_entry_bands", params, "tp_aggressiveness", "low")?;
8484            let input = FibonacciEntryBandsInput::from_slices(
8485                open,
8486                high,
8487                low,
8488                close,
8489                FibonacciEntryBandsParams {
8490                    source: Some(source),
8491                    length: Some(length),
8492                    atr_length: Some(atr_length),
8493                    use_atr: Some(use_atr),
8494                    tp_aggressiveness: Some(tp_aggressiveness),
8495                },
8496            );
8497            let out = fibonacci_entry_bands_with_kernel(&input, kernel).map_err(|e| {
8498                IndicatorDispatchError::ComputeFailed {
8499                    indicator: "fibonacci_entry_bands".to_string(),
8500                    details: e.to_string(),
8501                }
8502            })?;
8503            if output_id.eq_ignore_ascii_case("middle") || output_id.eq_ignore_ascii_case("basis") {
8504                return Ok(out.basis);
8505            }
8506            if output_id.eq_ignore_ascii_case("trend") {
8507                return Ok(out.trend);
8508            }
8509            if output_id.eq_ignore_ascii_case("upper_0618") {
8510                return Ok(out.upper_0618);
8511            }
8512            if output_id.eq_ignore_ascii_case("upper_1000") {
8513                return Ok(out.upper_1000);
8514            }
8515            if output_id.eq_ignore_ascii_case("upper_1618") {
8516                return Ok(out.upper_1618);
8517            }
8518            if output_id.eq_ignore_ascii_case("upper_2618") {
8519                return Ok(out.upper_2618);
8520            }
8521            if output_id.eq_ignore_ascii_case("lower_0618") {
8522                return Ok(out.lower_0618);
8523            }
8524            if output_id.eq_ignore_ascii_case("lower_1000") {
8525                return Ok(out.lower_1000);
8526            }
8527            if output_id.eq_ignore_ascii_case("lower_1618") {
8528                return Ok(out.lower_1618);
8529            }
8530            if output_id.eq_ignore_ascii_case("lower_2618") {
8531                return Ok(out.lower_2618);
8532            }
8533            if output_id.eq_ignore_ascii_case("tp_long_band") {
8534                return Ok(out.tp_long_band);
8535            }
8536            if output_id.eq_ignore_ascii_case("tp_short_band") {
8537                return Ok(out.tp_short_band);
8538            }
8539            if output_id.eq_ignore_ascii_case("go_long")
8540                || output_id.eq_ignore_ascii_case("long_entry")
8541            {
8542                return Ok(out.long_entry);
8543            }
8544            if output_id.eq_ignore_ascii_case("go_short")
8545                || output_id.eq_ignore_ascii_case("short_entry")
8546            {
8547                return Ok(out.short_entry);
8548            }
8549            if output_id.eq_ignore_ascii_case("rejection_long") {
8550                return Ok(out.rejection_long);
8551            }
8552            if output_id.eq_ignore_ascii_case("rejection_short") {
8553                return Ok(out.rejection_short);
8554            }
8555            if output_id.eq_ignore_ascii_case("long_bounce") {
8556                return Ok(out.long_bounce);
8557            }
8558            if output_id.eq_ignore_ascii_case("short_bounce") {
8559                return Ok(out.short_bounce);
8560            }
8561            Err(IndicatorDispatchError::UnknownOutput {
8562                indicator: "fibonacci_entry_bands".to_string(),
8563                output: output_id.to_string(),
8564            })
8565        },
8566    )
8567}
8568
8569fn compute_volume_energy_reservoirs_batch(
8570    req: IndicatorBatchRequest<'_>,
8571    output_id: &str,
8572) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8573    let (_, high, low, close, volume) =
8574        extract_ohlcv_full_input("volume_energy_reservoirs", req.data)?;
8575    let kernel = req.kernel.to_non_batch();
8576    collect_f64(
8577        "volume_energy_reservoirs",
8578        output_id,
8579        req.combos,
8580        close.len(),
8581        |params| {
8582            let length = get_usize_param("volume_energy_reservoirs", params, "length", 20)?;
8583            let sensitivity =
8584                get_f64_param("volume_energy_reservoirs", params, "sensitivity", 1.5)?;
8585            let input = VolumeEnergyReservoirsInput::from_slices(
8586                high,
8587                low,
8588                close,
8589                volume,
8590                VolumeEnergyReservoirsParams {
8591                    length: Some(length),
8592                    sensitivity: Some(sensitivity),
8593                },
8594            );
8595            let out = volume_energy_reservoirs_with_kernel(&input, kernel).map_err(|e| {
8596                IndicatorDispatchError::ComputeFailed {
8597                    indicator: "volume_energy_reservoirs".to_string(),
8598                    details: e.to_string(),
8599                }
8600            })?;
8601            match output_id {
8602                "momentum" | "value" => Ok(out.momentum),
8603                "reservoir" => Ok(out.reservoir),
8604                "squeeze_active" => Ok(out.squeeze_active),
8605                "squeeze_start" => Ok(out.squeeze_start),
8606                "range_high" => Ok(out.range_high),
8607                "range_low" => Ok(out.range_low),
8608                _ => Err(IndicatorDispatchError::UnknownOutput {
8609                    indicator: "volume_energy_reservoirs".to_string(),
8610                    output: output_id.to_string(),
8611                }),
8612            }
8613        },
8614    )
8615}
8616
8617fn compute_neighboring_trailing_stop_batch(
8618    req: IndicatorBatchRequest<'_>,
8619    output_id: &str,
8620) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8621    let (high, low, close) = extract_ohlc_input("neighboring_trailing_stop", req.data)?;
8622    let kernel = req.kernel.to_non_batch();
8623    collect_f64(
8624        "neighboring_trailing_stop",
8625        output_id,
8626        req.combos,
8627        close.len(),
8628        |params| {
8629            let buffer_size =
8630                get_usize_param("neighboring_trailing_stop", params, "buffer_size", 200)?;
8631            let k = get_usize_param("neighboring_trailing_stop", params, "k", 50)?;
8632            let percentile =
8633                get_f64_param("neighboring_trailing_stop", params, "percentile", 90.0)?;
8634            let smooth = get_usize_param("neighboring_trailing_stop", params, "smooth", 5)?;
8635            let input = NeighboringTrailingStopInput::from_slices(
8636                high,
8637                low,
8638                close,
8639                NeighboringTrailingStopParams {
8640                    buffer_size: Some(buffer_size),
8641                    k: Some(k),
8642                    percentile: Some(percentile),
8643                    smooth: Some(smooth),
8644                },
8645            );
8646            let out = neighboring_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
8647                IndicatorDispatchError::ComputeFailed {
8648                    indicator: "neighboring_trailing_stop".to_string(),
8649                    details: e.to_string(),
8650                }
8651            })?;
8652            match output_id {
8653                "trailing_stop" | "value" => Ok(out.trailing_stop),
8654                "bullish_band" => Ok(out.bullish_band),
8655                "bearish_band" => Ok(out.bearish_band),
8656                "direction" => Ok(out.direction),
8657                "discovery_bull" => Ok(out.discovery_bull),
8658                "discovery_bear" => Ok(out.discovery_bear),
8659                _ => Err(IndicatorDispatchError::UnknownOutput {
8660                    indicator: "neighboring_trailing_stop".to_string(),
8661                    output: output_id.to_string(),
8662                }),
8663            }
8664        },
8665    )
8666}
8667
8668fn compute_grover_llorens_cycle_oscillator_batch(
8669    req: IndicatorBatchRequest<'_>,
8670    output_id: &str,
8671) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8672    expect_value_output("grover_llorens_cycle_oscillator", output_id)?;
8673    let (open, high, low, close) =
8674        extract_ohlc_full_input("grover_llorens_cycle_oscillator", req.data)?;
8675    let kernel = req.kernel.to_non_batch();
8676    collect_f64(
8677        "grover_llorens_cycle_oscillator",
8678        output_id,
8679        req.combos,
8680        close.len(),
8681        |params| {
8682            let length = get_usize_param("grover_llorens_cycle_oscillator", params, "length", 100)?;
8683            let mult = get_f64_param("grover_llorens_cycle_oscillator", params, "mult", 10.0)?;
8684            let source = match find_param(params, "source") {
8685                Some(ParamValue::EnumString(v)) => (*v).to_string(),
8686                Some(_) => {
8687                    return Err(IndicatorDispatchError::InvalidParam {
8688                        indicator: "grover_llorens_cycle_oscillator".to_string(),
8689                        key: "source".to_string(),
8690                        reason: "expected string".to_string(),
8691                    });
8692                }
8693                None => "close".to_string(),
8694            };
8695            let smooth = get_bool_param("grover_llorens_cycle_oscillator", params, "smooth", true)?;
8696            let rsi_period =
8697                get_usize_param("grover_llorens_cycle_oscillator", params, "rsi_period", 20)?;
8698            let input = GroverLlorensCycleOscillatorInput::from_slices(
8699                open,
8700                high,
8701                low,
8702                close,
8703                GroverLlorensCycleOscillatorParams {
8704                    length: Some(length),
8705                    mult: Some(mult),
8706                    source: Some(source),
8707                    smooth: Some(smooth),
8708                    rsi_period: Some(rsi_period),
8709                },
8710            );
8711            let out = grover_llorens_cycle_oscillator_with_kernel(&input, kernel).map_err(|e| {
8712                IndicatorDispatchError::ComputeFailed {
8713                    indicator: "grover_llorens_cycle_oscillator".to_string(),
8714                    details: e.to_string(),
8715                }
8716            })?;
8717            Ok(out.values)
8718        },
8719    )
8720}
8721
8722fn compute_ehlers_autocorrelation_periodogram_batch(
8723    req: IndicatorBatchRequest<'_>,
8724    output_id: &str,
8725) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8726    let data = extract_slice_input("ehlers_autocorrelation_periodogram", req.data, "close")?;
8727    let kernel = req.kernel.to_non_batch();
8728    collect_f64(
8729        "ehlers_autocorrelation_periodogram",
8730        output_id,
8731        req.combos,
8732        data.len(),
8733        |params| {
8734            let min_period = get_usize_param(
8735                "ehlers_autocorrelation_periodogram",
8736                params,
8737                "min_period",
8738                8,
8739            )?;
8740            let max_period = get_usize_param(
8741                "ehlers_autocorrelation_periodogram",
8742                params,
8743                "max_period",
8744                48,
8745            )?;
8746            let avg_length = get_usize_param(
8747                "ehlers_autocorrelation_periodogram",
8748                params,
8749                "avg_length",
8750                3,
8751            )?;
8752            let enhance = get_bool_param(
8753                "ehlers_autocorrelation_periodogram",
8754                params,
8755                "enhance",
8756                true,
8757            )?;
8758            let input = EhlersAutocorrelationPeriodogramInput::from_slice(
8759                data,
8760                EhlersAutocorrelationPeriodogramParams {
8761                    min_period: Some(min_period),
8762                    max_period: Some(max_period),
8763                    avg_length: Some(avg_length),
8764                    enhance: Some(enhance),
8765                },
8766            );
8767            let out =
8768                ehlers_autocorrelation_periodogram_with_kernel(&input, kernel).map_err(|e| {
8769                    IndicatorDispatchError::ComputeFailed {
8770                        indicator: "ehlers_autocorrelation_periodogram".to_string(),
8771                        details: e.to_string(),
8772                    }
8773                })?;
8774            if output_id.eq_ignore_ascii_case("dominant_cycle")
8775                || output_id.eq_ignore_ascii_case("value")
8776            {
8777                return Ok(out.dominant_cycle);
8778            }
8779            if output_id.eq_ignore_ascii_case("normalized_power") {
8780                return Ok(out.normalized_power);
8781            }
8782            Err(IndicatorDispatchError::UnknownOutput {
8783                indicator: "ehlers_autocorrelation_periodogram".to_string(),
8784                output: output_id.to_string(),
8785            })
8786        },
8787    )
8788}
8789
8790fn compute_ehlers_linear_extrapolation_predictor_batch(
8791    req: IndicatorBatchRequest<'_>,
8792    output_id: &str,
8793) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8794    let data = extract_slice_input("ehlers_linear_extrapolation_predictor", req.data, "close")?;
8795    let kernel = req.kernel.to_non_batch();
8796    collect_f64(
8797        "ehlers_linear_extrapolation_predictor",
8798        output_id,
8799        req.combos,
8800        data.len(),
8801        |params| {
8802            let high_pass_length = get_usize_param(
8803                "ehlers_linear_extrapolation_predictor",
8804                params,
8805                "high_pass_length",
8806                125,
8807            )?;
8808            let low_pass_length = get_usize_param(
8809                "ehlers_linear_extrapolation_predictor",
8810                params,
8811                "low_pass_length",
8812                12,
8813            )?;
8814            let gain = get_f64_param("ehlers_linear_extrapolation_predictor", params, "gain", 0.7)?;
8815            let bars_forward = get_usize_param(
8816                "ehlers_linear_extrapolation_predictor",
8817                params,
8818                "bars_forward",
8819                5,
8820            )?;
8821            let signal_mode = get_enum_param(
8822                "ehlers_linear_extrapolation_predictor",
8823                params,
8824                "signal_mode",
8825                "predict_filter_crosses",
8826            )?;
8827            let input = EhlersLinearExtrapolationPredictorInput::from_slice(
8828                data,
8829                EhlersLinearExtrapolationPredictorParams {
8830                    high_pass_length: Some(high_pass_length),
8831                    low_pass_length: Some(low_pass_length),
8832                    gain: Some(gain),
8833                    bars_forward: Some(bars_forward),
8834                    signal_mode: Some(signal_mode),
8835                },
8836            );
8837            let out =
8838                ehlers_linear_extrapolation_predictor_with_kernel(&input, kernel).map_err(|e| {
8839                    IndicatorDispatchError::ComputeFailed {
8840                        indicator: "ehlers_linear_extrapolation_predictor".to_string(),
8841                        details: e.to_string(),
8842                    }
8843                })?;
8844            if output_id.eq_ignore_ascii_case("prediction")
8845                || output_id.eq_ignore_ascii_case("value")
8846            {
8847                return Ok(out.prediction);
8848            }
8849            if output_id.eq_ignore_ascii_case("filter") {
8850                return Ok(out.filter);
8851            }
8852            if output_id.eq_ignore_ascii_case("state") {
8853                return Ok(out.state);
8854            }
8855            if output_id.eq_ignore_ascii_case("go_long") {
8856                return Ok(out.go_long);
8857            }
8858            if output_id.eq_ignore_ascii_case("go_short") {
8859                return Ok(out.go_short);
8860            }
8861            Err(IndicatorDispatchError::UnknownOutput {
8862                indicator: "ehlers_linear_extrapolation_predictor".to_string(),
8863                output: output_id.to_string(),
8864            })
8865        },
8866    )
8867}
8868
8869fn compute_decisionpoint_breadth_swenlin_trading_oscillator_batch(
8870    req: IndicatorBatchRequest<'_>,
8871    output_id: &str,
8872) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8873    expect_value_output(
8874        "decisionpoint_breadth_swenlin_trading_oscillator",
8875        output_id,
8876    )?;
8877    let (advancing, declining) =
8878        extract_high_low_input("decisionpoint_breadth_swenlin_trading_oscillator", req.data)?;
8879    let kernel = req.kernel.to_non_batch();
8880    collect_f64(
8881        "decisionpoint_breadth_swenlin_trading_oscillator",
8882        output_id,
8883        req.combos,
8884        advancing.len(),
8885        |_params| {
8886            let input = DecisionPointBreadthSwenlinTradingOscillatorInput::from_slices(
8887                advancing,
8888                declining,
8889                DecisionPointBreadthSwenlinTradingOscillatorParams,
8890            );
8891            let out = decisionpoint_breadth_swenlin_trading_oscillator_with_kernel(&input, kernel)
8892                .map_err(|e| IndicatorDispatchError::ComputeFailed {
8893                    indicator: "decisionpoint_breadth_swenlin_trading_oscillator".to_string(),
8894                    details: e.to_string(),
8895                })?;
8896            Ok(out.values)
8897        },
8898    )
8899}
8900
8901fn compute_velocity_acceleration_indicator_batch(
8902    req: IndicatorBatchRequest<'_>,
8903    output_id: &str,
8904) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8905    expect_value_output("velocity_acceleration_indicator", output_id)?;
8906    let data_len = match req.data {
8907        IndicatorDataRef::Slice { values } => values.len(),
8908        IndicatorDataRef::Candles { candles, source } => {
8909            source_type(candles, source.unwrap_or("hlcc4")).len()
8910        }
8911        _ => {
8912            return Err(IndicatorDispatchError::MissingRequiredInput {
8913                indicator: "velocity_acceleration_indicator".to_string(),
8914                input: IndicatorInputKind::Candles,
8915            });
8916        }
8917    };
8918    let kernel = req.kernel.to_non_batch();
8919    collect_f64(
8920        "velocity_acceleration_indicator",
8921        output_id,
8922        req.combos,
8923        data_len,
8924        |params| {
8925            let source =
8926                get_enum_param("velocity_acceleration_indicator", params, "source", "hlcc4")?;
8927            let length = get_usize_param("velocity_acceleration_indicator", params, "length", 21)?;
8928            let smooth_length = get_usize_param(
8929                "velocity_acceleration_indicator",
8930                params,
8931                "smooth_length",
8932                5,
8933            )?;
8934            let data = match req.data {
8935                IndicatorDataRef::Slice { values } => values,
8936                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8937                _ => unreachable!(),
8938            };
8939            let input = VelocityAccelerationIndicatorInput::from_slice(
8940                data,
8941                VelocityAccelerationIndicatorParams {
8942                    length: Some(length),
8943                    smooth_length: Some(smooth_length),
8944                },
8945            );
8946            let out = velocity_acceleration_indicator_with_kernel(&input, kernel).map_err(|e| {
8947                IndicatorDispatchError::ComputeFailed {
8948                    indicator: "velocity_acceleration_indicator".to_string(),
8949                    details: e.to_string(),
8950                }
8951            })?;
8952            Ok(out.values)
8953        },
8954    )
8955}
8956
8957fn compute_normalized_resonator_batch(
8958    req: IndicatorBatchRequest<'_>,
8959    output_id: &str,
8960) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
8961    let data_len = match req.data {
8962        IndicatorDataRef::Slice { values } => values.len(),
8963        IndicatorDataRef::Candles { candles, source } => {
8964            source_type(candles, source.unwrap_or("hl2")).len()
8965        }
8966        _ => {
8967            return Err(IndicatorDispatchError::MissingRequiredInput {
8968                indicator: "normalized_resonator".to_string(),
8969                input: IndicatorInputKind::Candles,
8970            });
8971        }
8972    };
8973    let kernel = req.kernel.to_non_batch();
8974    collect_f64(
8975        "normalized_resonator",
8976        output_id,
8977        req.combos,
8978        data_len,
8979        |params| {
8980            let source = get_enum_param("normalized_resonator", params, "source", "hl2")?;
8981            let period = get_usize_param("normalized_resonator", params, "period", 100)?;
8982            let delta = get_f64_param("normalized_resonator", params, "delta", 0.5)?;
8983            let lookback_mult =
8984                get_f64_param("normalized_resonator", params, "lookback_mult", 1.0)?;
8985            let signal_length =
8986                get_usize_param("normalized_resonator", params, "signal_length", 9)?;
8987            let data = match req.data {
8988                IndicatorDataRef::Slice { values } => values,
8989                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
8990                _ => unreachable!(),
8991            };
8992            let input = NormalizedResonatorInput::from_slice(
8993                data,
8994                NormalizedResonatorParams {
8995                    period: Some(period),
8996                    delta: Some(delta),
8997                    lookback_mult: Some(lookback_mult),
8998                    signal_length: Some(signal_length),
8999                },
9000            );
9001            let out = normalized_resonator_with_kernel(&input, kernel).map_err(|e| {
9002                IndicatorDispatchError::ComputeFailed {
9003                    indicator: "normalized_resonator".to_string(),
9004                    details: e.to_string(),
9005                }
9006            })?;
9007            match output_id {
9008                "oscillator" => Ok(out.oscillator),
9009                "signal" => Ok(out.signal),
9010                _ => Err(IndicatorDispatchError::UnknownOutput {
9011                    indicator: "normalized_resonator".to_string(),
9012                    output: output_id.to_string(),
9013                }),
9014            }
9015        },
9016    )
9017}
9018
9019fn compute_monotonicity_index_batch(
9020    req: IndicatorBatchRequest<'_>,
9021    output_id: &str,
9022) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9023    let data_len = match req.data {
9024        IndicatorDataRef::Slice { values } => values.len(),
9025        IndicatorDataRef::Candles { candles, source } => {
9026            source_type(candles, source.unwrap_or("close")).len()
9027        }
9028        _ => {
9029            return Err(IndicatorDispatchError::MissingRequiredInput {
9030                indicator: "monotonicity_index".to_string(),
9031                input: IndicatorInputKind::Candles,
9032            });
9033        }
9034    };
9035    let kernel = req.kernel.to_non_batch();
9036    collect_f64(
9037        "monotonicity_index",
9038        output_id,
9039        req.combos,
9040        data_len,
9041        |params| {
9042            let source = get_enum_param("monotonicity_index", params, "source", "close")?;
9043            let length = get_usize_param("monotonicity_index", params, "length", 20)?;
9044            let mode = get_enum_param("monotonicity_index", params, "mode", "efficiency")?;
9045            let index_smooth = get_usize_param("monotonicity_index", params, "index_smooth", 5)?;
9046            let mode = MonotonicityIndexMode::parse(&mode).ok_or_else(|| {
9047                IndicatorDispatchError::InvalidParam {
9048                    indicator: "monotonicity_index".to_string(),
9049                    key: "mode".to_string(),
9050                    reason: format!("invalid mode: {mode}"),
9051                }
9052            })?;
9053            let data = match req.data {
9054                IndicatorDataRef::Slice { values } => values,
9055                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9056                _ => unreachable!(),
9057            };
9058            let input = MonotonicityIndexInput::from_slice(
9059                data,
9060                MonotonicityIndexParams {
9061                    length: Some(length),
9062                    mode: Some(mode),
9063                    index_smooth: Some(index_smooth),
9064                },
9065            );
9066            let out = monotonicity_index_with_kernel(&input, kernel).map_err(|e| {
9067                IndicatorDispatchError::ComputeFailed {
9068                    indicator: "monotonicity_index".to_string(),
9069                    details: e.to_string(),
9070                }
9071            })?;
9072            match output_id {
9073                "index" => Ok(out.index),
9074                "cumulative_mean" => Ok(out.cumulative_mean),
9075                "upper_bound" => Ok(out.upper_bound),
9076                _ => Err(IndicatorDispatchError::UnknownOutput {
9077                    indicator: "monotonicity_index".to_string(),
9078                    output: output_id.to_string(),
9079                }),
9080            }
9081        },
9082    )
9083}
9084
9085fn compute_half_causal_estimator_batch(
9086    req: IndicatorBatchRequest<'_>,
9087    output_id: &str,
9088) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9089    if output_id != "estimate" && output_id != "expected_value" {
9090        return Err(IndicatorDispatchError::UnknownOutput {
9091            indicator: "half_causal_estimator".to_string(),
9092            output: output_id.to_string(),
9093        });
9094    }
9095
9096    let data_len = match req.data {
9097        IndicatorDataRef::Slice { values } => values.len(),
9098        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
9099        _ => {
9100            return Err(IndicatorDispatchError::MissingRequiredInput {
9101                indicator: "half_causal_estimator".to_string(),
9102                input: IndicatorInputKind::Candles,
9103            });
9104        }
9105    };
9106    let kernel = req.kernel.to_non_batch();
9107    collect_f64(
9108        "half_causal_estimator",
9109        output_id,
9110        req.combos,
9111        data_len,
9112        |params| {
9113            let slots_per_day = get_usize_param_with_aliases(
9114                "half_causal_estimator",
9115                params,
9116                &["slots_per_day"],
9117                0,
9118            )?;
9119            let data_period = get_usize_param("half_causal_estimator", params, "data_period", 5)?;
9120            let filter_length =
9121                get_usize_param("half_causal_estimator", params, "filter_length", 20)?;
9122            let kernel_width =
9123                get_f64_param("half_causal_estimator", params, "kernel_width", 20.0)?;
9124            let maximum_confidence_adjust = get_f64_param(
9125                "half_causal_estimator",
9126                params,
9127                "maximum_confidence_adjust",
9128                100.0,
9129            )?;
9130            let extra_smoothing =
9131                get_usize_param("half_causal_estimator", params, "extra_smoothing", 0)?;
9132            let enable_expected_value = get_bool_param(
9133                "half_causal_estimator",
9134                params,
9135                "enable_expected_value",
9136                false,
9137            )?;
9138            let source = get_enum_param("half_causal_estimator", params, "source", "volume")?;
9139            let kernel_type = match get_enum_param(
9140                "half_causal_estimator",
9141                params,
9142                "kernel_type",
9143                "epanechnikov",
9144            )?
9145            .to_ascii_lowercase()
9146            .as_str()
9147            {
9148                "gaussian" => HalfCausalEstimatorKernelType::Gaussian,
9149                "epanechnikov" => HalfCausalEstimatorKernelType::Epanechnikov,
9150                "triangular" => HalfCausalEstimatorKernelType::Triangular,
9151                "sinc" => HalfCausalEstimatorKernelType::Sinc,
9152                other => {
9153                    return Err(IndicatorDispatchError::InvalidParam {
9154                        indicator: "half_causal_estimator".to_string(),
9155                        key: "kernel_type".to_string(),
9156                        reason: format!("unsupported value '{other}'"),
9157                    })
9158                }
9159            };
9160            let confidence_adjust = match get_enum_param(
9161                "half_causal_estimator",
9162                params,
9163                "confidence_adjust",
9164                "symmetric",
9165            )?
9166            .to_ascii_lowercase()
9167            .as_str()
9168            {
9169                "symmetric" => HalfCausalEstimatorConfidenceAdjust::Symmetric,
9170                "linear" => HalfCausalEstimatorConfidenceAdjust::Linear,
9171                "none" => HalfCausalEstimatorConfidenceAdjust::None,
9172                other => {
9173                    return Err(IndicatorDispatchError::InvalidParam {
9174                        indicator: "half_causal_estimator".to_string(),
9175                        key: "confidence_adjust".to_string(),
9176                        reason: format!("unsupported value '{other}'"),
9177                    })
9178                }
9179            };
9180
9181            let indicator_params = HalfCausalEstimatorParams {
9182                slots_per_day: if slots_per_day == 0 {
9183                    None
9184                } else {
9185                    Some(slots_per_day)
9186                },
9187                data_period: Some(data_period),
9188                filter_length: Some(filter_length),
9189                kernel_width: Some(kernel_width),
9190                kernel_type: Some(kernel_type),
9191                confidence_adjust: Some(confidence_adjust),
9192                maximum_confidence_adjust: Some(maximum_confidence_adjust),
9193                enable_expected_value: Some(enable_expected_value),
9194                extra_smoothing: Some(extra_smoothing),
9195            };
9196
9197            let out = match req.data {
9198                IndicatorDataRef::Slice { values } => {
9199                    let input = HalfCausalEstimatorInput::from_slice(values, indicator_params);
9200                    half_causal_estimator_with_kernel(&input, kernel)
9201                }
9202                IndicatorDataRef::Candles { candles, .. } => {
9203                    let input =
9204                        HalfCausalEstimatorInput::from_candles(candles, &source, indicator_params);
9205                    half_causal_estimator_with_kernel(&input, kernel)
9206                }
9207                _ => unreachable!(),
9208            }
9209            .map_err(|e| IndicatorDispatchError::ComputeFailed {
9210                indicator: "half_causal_estimator".to_string(),
9211                details: e.to_string(),
9212            })?;
9213
9214            Ok(match output_id {
9215                "estimate" => out.estimate,
9216                "expected_value" => out.expected_value,
9217                _ => unreachable!(),
9218            })
9219        },
9220    )
9221}
9222
9223fn compute_historical_volatility_batch(
9224    req: IndicatorBatchRequest<'_>,
9225    output_id: &str,
9226) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9227    expect_value_output("historical_volatility", output_id)?;
9228    let data = extract_slice_input("historical_volatility", req.data, "close")?;
9229    let kernel = req.kernel.to_non_batch();
9230    collect_f64(
9231        "historical_volatility",
9232        output_id,
9233        req.combos,
9234        data.len(),
9235        |params| {
9236            let lookback = get_usize_param("historical_volatility", params, "lookback", 20)?;
9237            let annualization_days =
9238                get_f64_param("historical_volatility", params, "annualization_days", 250.0)?;
9239            let input = HistoricalVolatilityInput::from_slice(
9240                data,
9241                HistoricalVolatilityParams {
9242                    lookback: Some(lookback),
9243                    annualization_days: Some(annualization_days),
9244                },
9245            );
9246            let out = historical_volatility_with_kernel(&input, kernel).map_err(|e| {
9247                IndicatorDispatchError::ComputeFailed {
9248                    indicator: "historical_volatility".to_string(),
9249                    details: e.to_string(),
9250                }
9251            })?;
9252            Ok(out.values)
9253        },
9254    )
9255}
9256
9257fn compute_historical_volatility_percentile_batch(
9258    req: IndicatorBatchRequest<'_>,
9259    output_id: &str,
9260) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9261    let data = extract_slice_input("historical_volatility_percentile", req.data, "close")?;
9262    let kernel = req.kernel.to_non_batch();
9263    collect_f64(
9264        "historical_volatility_percentile",
9265        output_id,
9266        req.combos,
9267        data.len(),
9268        |params| {
9269            let length = get_usize_param("historical_volatility_percentile", params, "length", 20)?;
9270            let annual_length = get_usize_param(
9271                "historical_volatility_percentile",
9272                params,
9273                "annual_length",
9274                252,
9275            )?;
9276            let input = HistoricalVolatilityPercentileInput::from_slice(
9277                data,
9278                HistoricalVolatilityPercentileParams {
9279                    length: Some(length),
9280                    annual_length: Some(annual_length),
9281                },
9282            );
9283            let out =
9284                historical_volatility_percentile_with_kernel(&input, kernel).map_err(|e| {
9285                    IndicatorDispatchError::ComputeFailed {
9286                        indicator: "historical_volatility_percentile".to_string(),
9287                        details: e.to_string(),
9288                    }
9289                })?;
9290            if output_id.eq_ignore_ascii_case("hvp") {
9291                return Ok(out.hvp);
9292            }
9293            if output_id.eq_ignore_ascii_case("hvp_sma") {
9294                return Ok(out.hvp_sma);
9295            }
9296            Err(IndicatorDispatchError::UnknownOutput {
9297                indicator: "historical_volatility_percentile".to_string(),
9298                output: output_id.to_string(),
9299            })
9300        },
9301    )
9302}
9303
9304fn compute_volatility_ratio_adaptive_rsx_batch(
9305    req: IndicatorBatchRequest<'_>,
9306    output_id: &str,
9307) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9308    let data = extract_slice_input("volatility_ratio_adaptive_rsx", req.data, "close")?;
9309    let kernel = req.kernel.to_non_batch();
9310    collect_f64(
9311        "volatility_ratio_adaptive_rsx",
9312        output_id,
9313        req.combos,
9314        data.len(),
9315        |params| {
9316            let period = get_usize_param("volatility_ratio_adaptive_rsx", params, "period", 14)?;
9317            let speed = get_f64_param("volatility_ratio_adaptive_rsx", params, "speed", 0.5)?;
9318            let input = VolatilityRatioAdaptiveRsxInput::from_slice(
9319                data,
9320                VolatilityRatioAdaptiveRsxParams {
9321                    period: Some(period),
9322                    speed: Some(speed),
9323                },
9324            );
9325            let out = volatility_ratio_adaptive_rsx_with_kernel(&input, kernel).map_err(|e| {
9326                IndicatorDispatchError::ComputeFailed {
9327                    indicator: "volatility_ratio_adaptive_rsx".to_string(),
9328                    details: e.to_string(),
9329                }
9330            })?;
9331            if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
9332                return Ok(out.line);
9333            }
9334            if output_id.eq_ignore_ascii_case("signal") {
9335                return Ok(out.signal);
9336            }
9337            Err(IndicatorDispatchError::UnknownOutput {
9338                indicator: "volatility_ratio_adaptive_rsx".to_string(),
9339                output: output_id.to_string(),
9340            })
9341        },
9342    )
9343}
9344
9345fn compute_on_balance_volume_oscillator_batch(
9346    req: IndicatorBatchRequest<'_>,
9347    output_id: &str,
9348) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9349    let (close, volume) =
9350        extract_close_volume_input("on_balance_volume_oscillator", req.data, "close")?;
9351    let kernel = req.kernel.to_non_batch();
9352    collect_f64(
9353        "on_balance_volume_oscillator",
9354        output_id,
9355        req.combos,
9356        close.len(),
9357        |params| {
9358            let obv_length =
9359                get_usize_param("on_balance_volume_oscillator", params, "obv_length", 20)?;
9360            let ema_length =
9361                get_usize_param("on_balance_volume_oscillator", params, "ema_length", 9)?;
9362            let input = OnBalanceVolumeOscillatorInput::from_slices(
9363                close,
9364                volume,
9365                OnBalanceVolumeOscillatorParams {
9366                    obv_length: Some(obv_length),
9367                    ema_length: Some(ema_length),
9368                },
9369            );
9370            let out = on_balance_volume_oscillator_with_kernel(&input, kernel).map_err(|e| {
9371                IndicatorDispatchError::ComputeFailed {
9372                    indicator: "on_balance_volume_oscillator".to_string(),
9373                    details: e.to_string(),
9374                }
9375            })?;
9376            if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
9377                return Ok(out.line);
9378            }
9379            if output_id.eq_ignore_ascii_case("signal") {
9380                return Ok(out.signal);
9381            }
9382            Err(IndicatorDispatchError::UnknownOutput {
9383                indicator: "on_balance_volume_oscillator".to_string(),
9384                output: output_id.to_string(),
9385            })
9386        },
9387    )
9388}
9389
9390fn compute_twiggs_money_flow_batch(
9391    req: IndicatorBatchRequest<'_>,
9392    output_id: &str,
9393) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9394    let (high, low, close, volume) = extract_hlcv_input("twiggs_money_flow", req.data)?;
9395    let kernel = req.kernel.to_non_batch();
9396    collect_f64(
9397        "twiggs_money_flow",
9398        output_id,
9399        req.combos,
9400        close.len(),
9401        |params| {
9402            let length = get_usize_param("twiggs_money_flow", params, "length", 21)?;
9403            let smoothing_length =
9404                get_usize_param("twiggs_money_flow", params, "smoothing_length", 4)?;
9405            let ma_type = get_enum_param("twiggs_money_flow", params, "ma_type", "ema")?;
9406            let input = TwiggsMoneyFlowInput::from_slices(
9407                high,
9408                low,
9409                close,
9410                volume,
9411                TwiggsMoneyFlowParams {
9412                    length: Some(length),
9413                    smoothing_length: Some(smoothing_length),
9414                    ma_type: Some(ma_type),
9415                },
9416            );
9417            let out = twiggs_money_flow_with_kernel(&input, kernel).map_err(|e| {
9418                IndicatorDispatchError::ComputeFailed {
9419                    indicator: "twiggs_money_flow".to_string(),
9420                    details: e.to_string(),
9421                }
9422            })?;
9423            if output_id.eq_ignore_ascii_case("tmf") || output_id.eq_ignore_ascii_case("value") {
9424                return Ok(out.tmf);
9425            }
9426            if output_id.eq_ignore_ascii_case("smoothed") {
9427                return Ok(out.smoothed);
9428            }
9429            Err(IndicatorDispatchError::UnknownOutput {
9430                indicator: "twiggs_money_flow".to_string(),
9431                output: output_id.to_string(),
9432            })
9433        },
9434    )
9435}
9436
9437fn compute_parkinson_volatility_batch(
9438    req: IndicatorBatchRequest<'_>,
9439    output_id: &str,
9440) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9441    let (high, low) = extract_high_low_input("parkinson_volatility", req.data)?;
9442    let kernel = req.kernel.to_non_batch();
9443    collect_f64(
9444        "parkinson_volatility",
9445        output_id,
9446        req.combos,
9447        high.len(),
9448        |params| {
9449            let period = get_usize_param("parkinson_volatility", params, "period", 10)?;
9450            let input = ParkinsonVolatilityInput::from_slices(
9451                high,
9452                low,
9453                ParkinsonVolatilityParams {
9454                    period: Some(period),
9455                },
9456            );
9457            let out = parkinson_volatility_with_kernel(&input, kernel).map_err(|e| {
9458                IndicatorDispatchError::ComputeFailed {
9459                    indicator: "parkinson_volatility".to_string(),
9460                    details: e.to_string(),
9461                }
9462            })?;
9463            if output_id.eq_ignore_ascii_case("volatility")
9464                || output_id.eq_ignore_ascii_case("value")
9465            {
9466                return Ok(out.volatility);
9467            }
9468            if output_id.eq_ignore_ascii_case("variance") {
9469                return Ok(out.variance);
9470            }
9471            Err(IndicatorDispatchError::UnknownOutput {
9472                indicator: "parkinson_volatility".to_string(),
9473                output: output_id.to_string(),
9474            })
9475        },
9476    )
9477}
9478
9479fn compute_l2_ehlers_signal_to_noise_batch(
9480    req: IndicatorBatchRequest<'_>,
9481    output_id: &str,
9482) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9483    expect_value_output("l2_ehlers_signal_to_noise", output_id)?;
9484    let data_len = match req.data {
9485        IndicatorDataRef::Slice { values } => values.len(),
9486        IndicatorDataRef::Candles { candles, source } => {
9487            source_type(candles, source.unwrap_or("hl2")).len()
9488        }
9489        _ => {
9490            return Err(IndicatorDispatchError::MissingRequiredInput {
9491                indicator: "l2_ehlers_signal_to_noise".to_string(),
9492                input: IndicatorInputKind::Candles,
9493            })
9494        }
9495    };
9496    let kernel = req.kernel.to_non_batch();
9497    collect_f64(
9498        "l2_ehlers_signal_to_noise",
9499        output_id,
9500        req.combos,
9501        data_len,
9502        |params| {
9503            let source = get_enum_param("l2_ehlers_signal_to_noise", params, "source", "hl2")?;
9504            let smooth_period =
9505                get_usize_param("l2_ehlers_signal_to_noise", params, "smooth_period", 10)?;
9506            let src = match req.data {
9507                IndicatorDataRef::Slice { values } => values,
9508                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9509                _ => unreachable!(),
9510            };
9511            let (high, low) = match req.data {
9512                IndicatorDataRef::Candles { candles, .. } => {
9513                    (candles.high.as_slice(), candles.low.as_slice())
9514                }
9515                IndicatorDataRef::Ohlc { high, low, .. } => (high, low),
9516                IndicatorDataRef::Ohlcv { high, low, .. } => (high, low),
9517                _ => {
9518                    return Err(IndicatorDispatchError::MissingRequiredInput {
9519                        indicator: "l2_ehlers_signal_to_noise".to_string(),
9520                        input: IndicatorInputKind::Candles,
9521                    })
9522                }
9523            };
9524            let input = L2EhlersSignalToNoiseInput::from_slices(
9525                src,
9526                high,
9527                low,
9528                L2EhlersSignalToNoiseParams {
9529                    smooth_period: Some(smooth_period),
9530                },
9531            );
9532            let out = l2_ehlers_signal_to_noise_with_kernel(&input, kernel).map_err(|e| {
9533                IndicatorDispatchError::ComputeFailed {
9534                    indicator: "l2_ehlers_signal_to_noise".to_string(),
9535                    details: e.to_string(),
9536                }
9537            })?;
9538            Ok(out.values)
9539        },
9540    )
9541}
9542
9543fn compute_cycle_channel_oscillator_batch(
9544    req: IndicatorBatchRequest<'_>,
9545    output_id: &str,
9546) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9547    let data_len = match req.data {
9548        IndicatorDataRef::Candles { candles, source } => {
9549            source_type(candles, source.unwrap_or("close")).len()
9550        }
9551        _ => {
9552            return Err(IndicatorDispatchError::MissingRequiredInput {
9553                indicator: "cycle_channel_oscillator".to_string(),
9554                input: IndicatorInputKind::Candles,
9555            })
9556        }
9557    };
9558    let kernel = req.kernel.to_non_batch();
9559    collect_f64(
9560        "cycle_channel_oscillator",
9561        output_id,
9562        req.combos,
9563        data_len,
9564        |params| {
9565            let source = get_enum_param("cycle_channel_oscillator", params, "source", "close")?;
9566            let short_cycle_length =
9567                get_usize_param("cycle_channel_oscillator", params, "short_cycle_length", 10)?;
9568            let medium_cycle_length = get_usize_param(
9569                "cycle_channel_oscillator",
9570                params,
9571                "medium_cycle_length",
9572                30,
9573            )?;
9574            let short_multiplier =
9575                get_f64_param("cycle_channel_oscillator", params, "short_multiplier", 1.0)?;
9576            let medium_multiplier =
9577                get_f64_param("cycle_channel_oscillator", params, "medium_multiplier", 3.0)?;
9578            let (src, high, low, close) = match req.data {
9579                IndicatorDataRef::Candles { candles, .. } => (
9580                    source_type(candles, &source),
9581                    candles.high.as_slice(),
9582                    candles.low.as_slice(),
9583                    candles.close.as_slice(),
9584                ),
9585                _ => unreachable!(),
9586            };
9587            let input = CycleChannelOscillatorInput::from_slices(
9588                src,
9589                high,
9590                low,
9591                close,
9592                CycleChannelOscillatorParams {
9593                    short_cycle_length: Some(short_cycle_length),
9594                    medium_cycle_length: Some(medium_cycle_length),
9595                    short_multiplier: Some(short_multiplier),
9596                    medium_multiplier: Some(medium_multiplier),
9597                },
9598            );
9599            let out = cycle_channel_oscillator_with_kernel(&input, kernel).map_err(|e| {
9600                IndicatorDispatchError::ComputeFailed {
9601                    indicator: "cycle_channel_oscillator".to_string(),
9602                    details: e.to_string(),
9603                }
9604            })?;
9605            if output_id.eq_ignore_ascii_case("fast") || output_id.eq_ignore_ascii_case("value") {
9606                return Ok(out.fast);
9607            }
9608            if output_id.eq_ignore_ascii_case("slow") {
9609                return Ok(out.slow);
9610            }
9611            Err(IndicatorDispatchError::UnknownOutput {
9612                indicator: "cycle_channel_oscillator".to_string(),
9613                output: output_id.to_string(),
9614            })
9615        },
9616    )
9617}
9618
9619fn compute_andean_oscillator_batch(
9620    req: IndicatorBatchRequest<'_>,
9621    output_id: &str,
9622) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9623    let (open, _high, _low, close) = extract_ohlc_full_input("andean_oscillator", req.data)?;
9624    let kernel = req.kernel.to_non_batch();
9625    collect_f64(
9626        "andean_oscillator",
9627        output_id,
9628        req.combos,
9629        close.len(),
9630        |params| {
9631            let length = get_usize_param("andean_oscillator", params, "length", 50)?;
9632            let signal_length = get_usize_param("andean_oscillator", params, "signal_length", 9)?;
9633            let input = AndeanOscillatorInput::from_slices(
9634                open,
9635                close,
9636                AndeanOscillatorParams {
9637                    length: Some(length),
9638                    signal_length: Some(signal_length),
9639                },
9640            );
9641            let out = andean_oscillator_with_kernel(&input, kernel).map_err(|e| {
9642                IndicatorDispatchError::ComputeFailed {
9643                    indicator: "andean_oscillator".to_string(),
9644                    details: e.to_string(),
9645                }
9646            })?;
9647            if output_id.eq_ignore_ascii_case("bull") {
9648                return Ok(out.bull);
9649            }
9650            if output_id.eq_ignore_ascii_case("bear") {
9651                return Ok(out.bear);
9652            }
9653            if output_id.eq_ignore_ascii_case("signal") {
9654                return Ok(out.signal);
9655            }
9656            Err(IndicatorDispatchError::UnknownOutput {
9657                indicator: "andean_oscillator".to_string(),
9658                output: output_id.to_string(),
9659            })
9660        },
9661    )
9662}
9663
9664fn compute_daily_factor_batch(
9665    req: IndicatorBatchRequest<'_>,
9666    output_id: &str,
9667) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9668    let (open, high, low, close) = extract_ohlc_full_input("daily_factor", req.data)?;
9669    let kernel = req.kernel.to_non_batch();
9670    collect_f64(
9671        "daily_factor",
9672        output_id,
9673        req.combos,
9674        close.len(),
9675        |params| {
9676            let threshold_level = get_f64_param("daily_factor", params, "threshold_level", 0.35)?;
9677            let input = DailyFactorInput::from_slices(
9678                open,
9679                high,
9680                low,
9681                close,
9682                DailyFactorParams {
9683                    threshold_level: Some(threshold_level),
9684                },
9685            );
9686            let out = daily_factor_with_kernel(&input, kernel).map_err(|e| {
9687                IndicatorDispatchError::ComputeFailed {
9688                    indicator: "daily_factor".to_string(),
9689                    details: e.to_string(),
9690                }
9691            })?;
9692            if output_id.eq_ignore_ascii_case("value") {
9693                return Ok(out.value);
9694            }
9695            if output_id.eq_ignore_ascii_case("ema") {
9696                return Ok(out.ema);
9697            }
9698            if output_id.eq_ignore_ascii_case("signal") {
9699                return Ok(out.signal);
9700            }
9701            Err(IndicatorDispatchError::UnknownOutput {
9702                indicator: "daily_factor".to_string(),
9703                output: output_id.to_string(),
9704            })
9705        },
9706    )
9707}
9708
9709fn compute_ehlers_adaptive_cyber_cycle_batch(
9710    req: IndicatorBatchRequest<'_>,
9711    output_id: &str,
9712) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9713    let data_len = match req.data {
9714        IndicatorDataRef::Slice { values } => values.len(),
9715        IndicatorDataRef::Candles { candles, source } => {
9716            source_type(candles, source.unwrap_or("hl2")).len()
9717        }
9718        _ => {
9719            return Err(IndicatorDispatchError::MissingRequiredInput {
9720                indicator: "ehlers_adaptive_cyber_cycle".to_string(),
9721                input: IndicatorInputKind::Candles,
9722            })
9723        }
9724    };
9725    let kernel = req.kernel.to_non_batch();
9726    collect_f64(
9727        "ehlers_adaptive_cyber_cycle",
9728        output_id,
9729        req.combos,
9730        data_len,
9731        |params| {
9732            let source = get_enum_param("ehlers_adaptive_cyber_cycle", params, "source", "hl2")?;
9733            let alpha = get_f64_param("ehlers_adaptive_cyber_cycle", params, "alpha", 0.07)?;
9734            let data = match req.data {
9735                IndicatorDataRef::Slice { values } => values,
9736                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9737                _ => unreachable!(),
9738            };
9739            let input = EhlersAdaptiveCyberCycleInput::from_slice(
9740                data,
9741                EhlersAdaptiveCyberCycleParams { alpha: Some(alpha) },
9742            );
9743            let out = ehlers_adaptive_cyber_cycle_with_kernel(&input, kernel).map_err(|e| {
9744                IndicatorDispatchError::ComputeFailed {
9745                    indicator: "ehlers_adaptive_cyber_cycle".to_string(),
9746                    details: e.to_string(),
9747                }
9748            })?;
9749            if output_id.eq_ignore_ascii_case("cycle") || output_id.eq_ignore_ascii_case("value") {
9750                return Ok(out.cycle);
9751            }
9752            if output_id.eq_ignore_ascii_case("trigger") {
9753                return Ok(out.trigger);
9754            }
9755            Err(IndicatorDispatchError::UnknownOutput {
9756                indicator: "ehlers_adaptive_cyber_cycle".to_string(),
9757                output: output_id.to_string(),
9758            })
9759        },
9760    )
9761}
9762
9763fn compute_ehlers_simple_cycle_indicator_batch(
9764    req: IndicatorBatchRequest<'_>,
9765    output_id: &str,
9766) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9767    let data_len = match req.data {
9768        IndicatorDataRef::Slice { values } => values.len(),
9769        IndicatorDataRef::Candles { candles, source } => {
9770            source_type(candles, source.unwrap_or("hl2")).len()
9771        }
9772        _ => {
9773            return Err(IndicatorDispatchError::MissingRequiredInput {
9774                indicator: "ehlers_simple_cycle_indicator".to_string(),
9775                input: IndicatorInputKind::Candles,
9776            })
9777        }
9778    };
9779    let kernel = req.kernel.to_non_batch();
9780    collect_f64(
9781        "ehlers_simple_cycle_indicator",
9782        output_id,
9783        req.combos,
9784        data_len,
9785        |params| {
9786            let source = get_enum_param("ehlers_simple_cycle_indicator", params, "source", "hl2")?;
9787            let alpha = get_f64_param("ehlers_simple_cycle_indicator", params, "alpha", 0.07)?;
9788            let data = match req.data {
9789                IndicatorDataRef::Slice { values } => values,
9790                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9791                _ => unreachable!(),
9792            };
9793            let input = EhlersSimpleCycleIndicatorInput::from_slice(
9794                data,
9795                EhlersSimpleCycleIndicatorParams { alpha: Some(alpha) },
9796            );
9797            let out = ehlers_simple_cycle_indicator_with_kernel(&input, kernel).map_err(|e| {
9798                IndicatorDispatchError::ComputeFailed {
9799                    indicator: "ehlers_simple_cycle_indicator".to_string(),
9800                    details: e.to_string(),
9801                }
9802            })?;
9803            if output_id.eq_ignore_ascii_case("cycle") || output_id.eq_ignore_ascii_case("value") {
9804                return Ok(out.cycle);
9805            }
9806            if output_id.eq_ignore_ascii_case("trigger") {
9807                return Ok(out.trigger);
9808            }
9809            Err(IndicatorDispatchError::UnknownOutput {
9810                indicator: "ehlers_simple_cycle_indicator".to_string(),
9811                output: output_id.to_string(),
9812            })
9813        },
9814    )
9815}
9816
9817fn compute_l1_ehlers_phasor_batch(
9818    req: IndicatorBatchRequest<'_>,
9819    output_id: &str,
9820) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9821    expect_value_output("l1_ehlers_phasor", output_id)?;
9822    let data = extract_slice_input("l1_ehlers_phasor", req.data, "close")?;
9823    let kernel = req.kernel.to_non_batch();
9824    collect_f64(
9825        "l1_ehlers_phasor",
9826        output_id,
9827        req.combos,
9828        data.len(),
9829        |params| {
9830            let domestic_cycle_length =
9831                get_usize_param("l1_ehlers_phasor", params, "domestic_cycle_length", 15)?;
9832            let input = L1EhlersPhasorInput::from_slice(
9833                data,
9834                L1EhlersPhasorParams {
9835                    domestic_cycle_length: Some(domestic_cycle_length),
9836                },
9837            );
9838            let out = l1_ehlers_phasor_with_kernel(&input, kernel).map_err(|e| {
9839                IndicatorDispatchError::ComputeFailed {
9840                    indicator: "l1_ehlers_phasor".to_string(),
9841                    details: e.to_string(),
9842                }
9843            })?;
9844            Ok(out.values)
9845        },
9846    )
9847}
9848
9849fn compute_ehlers_smoothed_adaptive_momentum_batch(
9850    req: IndicatorBatchRequest<'_>,
9851    output_id: &str,
9852) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9853    expect_value_output("ehlers_smoothed_adaptive_momentum", output_id)?;
9854    let data_len = match req.data {
9855        IndicatorDataRef::Slice { values } => values.len(),
9856        IndicatorDataRef::Candles { candles, source } => {
9857            source_type(candles, source.unwrap_or("hl2")).len()
9858        }
9859        _ => {
9860            return Err(IndicatorDispatchError::MissingRequiredInput {
9861                indicator: "ehlers_smoothed_adaptive_momentum".to_string(),
9862                input: IndicatorInputKind::Candles,
9863            })
9864        }
9865    };
9866    let kernel = req.kernel.to_non_batch();
9867    collect_f64(
9868        "ehlers_smoothed_adaptive_momentum",
9869        output_id,
9870        req.combos,
9871        data_len,
9872        |params| {
9873            let source =
9874                get_enum_param("ehlers_smoothed_adaptive_momentum", params, "source", "hl2")?;
9875            let alpha = get_f64_param("ehlers_smoothed_adaptive_momentum", params, "alpha", 0.07)?;
9876            let cutoff = get_f64_param("ehlers_smoothed_adaptive_momentum", params, "cutoff", 8.0)?;
9877            let data = match req.data {
9878                IndicatorDataRef::Slice { values } => values,
9879                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
9880                _ => unreachable!(),
9881            };
9882            let input = EhlersSmoothedAdaptiveMomentumInput::from_slice(
9883                data,
9884                EhlersSmoothedAdaptiveMomentumParams {
9885                    alpha: Some(alpha),
9886                    cutoff: Some(cutoff),
9887                },
9888            );
9889            let out =
9890                ehlers_smoothed_adaptive_momentum_with_kernel(&input, kernel).map_err(|e| {
9891                    IndicatorDispatchError::ComputeFailed {
9892                        indicator: "ehlers_smoothed_adaptive_momentum".to_string(),
9893                        details: e.to_string(),
9894                    }
9895                })?;
9896            Ok(out.values)
9897        },
9898    )
9899}
9900
9901fn compute_ewma_volatility_batch(
9902    req: IndicatorBatchRequest<'_>,
9903    output_id: &str,
9904) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9905    expect_value_output("ewma_volatility", output_id)?;
9906    let data = extract_slice_input("ewma_volatility", req.data, "close")?;
9907    let kernel = req.kernel.to_non_batch();
9908    collect_f64(
9909        "ewma_volatility",
9910        output_id,
9911        req.combos,
9912        data.len(),
9913        |params| {
9914            let lambda = get_f64_param("ewma_volatility", params, "lambda", 0.94)?;
9915            let input = EwmaVolatilityInput::from_slice(
9916                data,
9917                EwmaVolatilityParams {
9918                    lambda: Some(lambda),
9919                },
9920            );
9921            let out = ewma_volatility_with_kernel(&input, kernel).map_err(|e| {
9922                IndicatorDispatchError::ComputeFailed {
9923                    indicator: "ewma_volatility".to_string(),
9924                    details: e.to_string(),
9925                }
9926            })?;
9927            Ok(out.values)
9928        },
9929    )
9930}
9931
9932fn compute_random_walk_index_batch(
9933    req: IndicatorBatchRequest<'_>,
9934    output_id: &str,
9935) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9936    let (high, low, close) = extract_ohlc_input("random_walk_index", req.data)?;
9937    let kernel = req.kernel.to_non_batch();
9938    collect_f64(
9939        "random_walk_index",
9940        output_id,
9941        req.combos,
9942        close.len(),
9943        |params| {
9944            let length = get_usize_param("random_walk_index", params, "length", 14)?;
9945            let input = RandomWalkIndexInput::from_slices(
9946                high,
9947                low,
9948                close,
9949                RandomWalkIndexParams {
9950                    length: Some(length),
9951                },
9952            );
9953            let out = random_walk_index_with_kernel(&input, kernel).map_err(|e| {
9954                IndicatorDispatchError::ComputeFailed {
9955                    indicator: "random_walk_index".to_string(),
9956                    details: e.to_string(),
9957                }
9958            })?;
9959            if output_id.eq_ignore_ascii_case("high") {
9960                return Ok(out.high);
9961            }
9962            if output_id.eq_ignore_ascii_case("low") {
9963                return Ok(out.low);
9964            }
9965            Err(IndicatorDispatchError::UnknownOutput {
9966                indicator: "random_walk_index".to_string(),
9967                output: output_id.to_string(),
9968            })
9969        },
9970    )
9971}
9972
9973fn compute_price_moving_average_ratio_percentile_batch(
9974    req: IndicatorBatchRequest<'_>,
9975    output_id: &str,
9976) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
9977    let data_len = match req.data {
9978        IndicatorDataRef::Candles { candles, source } => {
9979            source_type(candles, source.unwrap_or("close")).len()
9980        }
9981        IndicatorDataRef::CloseVolume { close, .. } => close.len(),
9982        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
9983        _ => {
9984            return Err(IndicatorDispatchError::MissingRequiredInput {
9985                indicator: "price_moving_average_ratio_percentile".to_string(),
9986                input: IndicatorInputKind::CloseVolume,
9987            })
9988        }
9989    };
9990    let kernel = req.kernel.to_non_batch();
9991    collect_f64(
9992        "price_moving_average_ratio_percentile",
9993        output_id,
9994        req.combos,
9995        data_len,
9996        |params| {
9997            let source = get_enum_param(
9998                "price_moving_average_ratio_percentile",
9999                params,
10000                "source",
10001                "close",
10002            )?;
10003            let ma_length = get_usize_param(
10004                "price_moving_average_ratio_percentile",
10005                params,
10006                "ma_length",
10007                20,
10008            )?;
10009            let ma_type = get_enum_param(
10010                "price_moving_average_ratio_percentile",
10011                params,
10012                "ma_type",
10013                "sma",
10014            )?
10015            .parse::<PriceMovingAverageRatioPercentileMaType>()
10016            .map_err(|e| IndicatorDispatchError::InvalidParam {
10017                indicator: "price_moving_average_ratio_percentile".to_string(),
10018                key: "ma_type".to_string(),
10019                reason: e,
10020            })?;
10021            let pmarp_lookback = get_usize_param(
10022                "price_moving_average_ratio_percentile",
10023                params,
10024                "pmarp_lookback",
10025                350,
10026            )?;
10027            let signal_ma_length = get_usize_param(
10028                "price_moving_average_ratio_percentile",
10029                params,
10030                "signal_ma_length",
10031                20,
10032            )?;
10033            let signal_ma_type = get_enum_param(
10034                "price_moving_average_ratio_percentile",
10035                params,
10036                "signal_ma_type",
10037                "sma",
10038            )?
10039            .parse::<PriceMovingAverageRatioPercentileMaType>()
10040            .map_err(|e| IndicatorDispatchError::InvalidParam {
10041                indicator: "price_moving_average_ratio_percentile".to_string(),
10042                key: "signal_ma_type".to_string(),
10043                reason: e,
10044            })?;
10045            let line_mode = get_enum_param(
10046                "price_moving_average_ratio_percentile",
10047                params,
10048                "line_mode",
10049                "pmar",
10050            )?
10051            .parse::<PriceMovingAverageRatioPercentileLineMode>()
10052            .map_err(|e| IndicatorDispatchError::InvalidParam {
10053                indicator: "price_moving_average_ratio_percentile".to_string(),
10054                key: "line_mode".to_string(),
10055                reason: e,
10056            })?;
10057            let (price, volume) = match req.data {
10058                IndicatorDataRef::Candles { candles, .. } => {
10059                    (source_type(candles, &source), candles.volume.as_slice())
10060                }
10061                IndicatorDataRef::CloseVolume { close, volume } => (close, volume),
10062                IndicatorDataRef::Ohlcv {
10063                    open,
10064                    high,
10065                    low,
10066                    close,
10067                    volume,
10068                } => {
10069                    let price = match source.to_ascii_lowercase().as_str() {
10070                        "open" => open,
10071                        "high" => high,
10072                        "low" => low,
10073                        _ => close,
10074                    };
10075                    (price, volume)
10076                }
10077                _ => unreachable!(),
10078            };
10079            let input = PriceMovingAverageRatioPercentileInput::from_slices(
10080                price,
10081                volume,
10082                PriceMovingAverageRatioPercentileParams {
10083                    ma_length: Some(ma_length),
10084                    ma_type: Some(ma_type),
10085                    pmarp_lookback: Some(pmarp_lookback),
10086                    signal_ma_length: Some(signal_ma_length),
10087                    signal_ma_type: Some(signal_ma_type),
10088                    line_mode: Some(line_mode),
10089                },
10090            );
10091            let out =
10092                price_moving_average_ratio_percentile_with_kernel(&input, kernel).map_err(|e| {
10093                    IndicatorDispatchError::ComputeFailed {
10094                        indicator: "price_moving_average_ratio_percentile".to_string(),
10095                        details: e.to_string(),
10096                    }
10097                })?;
10098            if output_id.eq_ignore_ascii_case("pmar") {
10099                return Ok(out.pmar);
10100            }
10101            if output_id.eq_ignore_ascii_case("pmarp") {
10102                return Ok(out.pmarp);
10103            }
10104            if output_id.eq_ignore_ascii_case("plotline") || output_id.eq_ignore_ascii_case("value")
10105            {
10106                return Ok(out.plotline);
10107            }
10108            if output_id.eq_ignore_ascii_case("signal") {
10109                return Ok(out.signal);
10110            }
10111            if output_id.eq_ignore_ascii_case("pmar_high") {
10112                return Ok(out.pmar_high);
10113            }
10114            if output_id.eq_ignore_ascii_case("pmar_low") {
10115                return Ok(out.pmar_low);
10116            }
10117            if output_id.eq_ignore_ascii_case("scaled_pmar") {
10118                return Ok(out.scaled_pmar);
10119            }
10120            Err(IndicatorDispatchError::UnknownOutput {
10121                indicator: "price_moving_average_ratio_percentile".to_string(),
10122                output: output_id.to_string(),
10123            })
10124        },
10125    )
10126}
10127
10128fn compute_trend_trigger_factor_batch(
10129    req: IndicatorBatchRequest<'_>,
10130    output_id: &str,
10131) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10132    expect_value_output("trend_trigger_factor", output_id)?;
10133    let (high, low) = extract_high_low_input("trend_trigger_factor", req.data)?;
10134    let kernel = req.kernel.to_non_batch();
10135    collect_f64(
10136        "trend_trigger_factor",
10137        output_id,
10138        req.combos,
10139        high.len(),
10140        |params| {
10141            let length = get_usize_param("trend_trigger_factor", params, "length", 15)?;
10142            let input = TrendTriggerFactorInput::from_slices(
10143                high,
10144                low,
10145                TrendTriggerFactorParams {
10146                    length: Some(length),
10147                },
10148            );
10149            let out = trend_trigger_factor_with_kernel(&input, kernel).map_err(|e| {
10150                IndicatorDispatchError::ComputeFailed {
10151                    indicator: "trend_trigger_factor".to_string(),
10152                    details: e.to_string(),
10153                }
10154            })?;
10155            Ok(out.values)
10156        },
10157    )
10158}
10159
10160fn compute_mesa_stochastic_multi_length_batch(
10161    req: IndicatorBatchRequest<'_>,
10162    output_id: &str,
10163) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10164    let data_len = match req.data {
10165        IndicatorDataRef::Slice { values } => values.len(),
10166        IndicatorDataRef::Candles { candles, source } => {
10167            source_type(candles, source.unwrap_or("close")).len()
10168        }
10169        _ => {
10170            return Err(IndicatorDispatchError::MissingRequiredInput {
10171                indicator: "mesa_stochastic_multi_length".to_string(),
10172                input: IndicatorInputKind::Candles,
10173            })
10174        }
10175    };
10176    let kernel = req.kernel.to_non_batch();
10177    collect_f64(
10178        "mesa_stochastic_multi_length",
10179        output_id,
10180        req.combos,
10181        data_len,
10182        |params| {
10183            let source = get_enum_param("mesa_stochastic_multi_length", params, "source", "close")?;
10184            let length_1 = get_usize_param("mesa_stochastic_multi_length", params, "length_1", 48)?;
10185            let length_2 = get_usize_param("mesa_stochastic_multi_length", params, "length_2", 21)?;
10186            let length_3 = get_usize_param("mesa_stochastic_multi_length", params, "length_3", 9)?;
10187            let length_4 = get_usize_param("mesa_stochastic_multi_length", params, "length_4", 6)?;
10188            let trigger_length =
10189                get_usize_param("mesa_stochastic_multi_length", params, "trigger_length", 2)?;
10190            let data = match req.data {
10191                IndicatorDataRef::Slice { values } => values,
10192                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
10193                _ => unreachable!(),
10194            };
10195            let input = MesaStochasticMultiLengthInput::from_slices(
10196                data,
10197                MesaStochasticMultiLengthParams {
10198                    length_1: Some(length_1),
10199                    length_2: Some(length_2),
10200                    length_3: Some(length_3),
10201                    length_4: Some(length_4),
10202                    trigger_length: Some(trigger_length),
10203                },
10204            );
10205            let out = mesa_stochastic_multi_length_with_kernel(&input, kernel).map_err(|e| {
10206                IndicatorDispatchError::ComputeFailed {
10207                    indicator: "mesa_stochastic_multi_length".to_string(),
10208                    details: e.to_string(),
10209                }
10210            })?;
10211            if output_id.eq_ignore_ascii_case("mesa_1") {
10212                return Ok(out.mesa_1);
10213            }
10214            if output_id.eq_ignore_ascii_case("mesa_2") {
10215                return Ok(out.mesa_2);
10216            }
10217            if output_id.eq_ignore_ascii_case("mesa_3") {
10218                return Ok(out.mesa_3);
10219            }
10220            if output_id.eq_ignore_ascii_case("mesa_4") {
10221                return Ok(out.mesa_4);
10222            }
10223            if output_id.eq_ignore_ascii_case("trigger_1") {
10224                return Ok(out.trigger_1);
10225            }
10226            if output_id.eq_ignore_ascii_case("trigger_2") {
10227                return Ok(out.trigger_2);
10228            }
10229            if output_id.eq_ignore_ascii_case("trigger_3") {
10230                return Ok(out.trigger_3);
10231            }
10232            if output_id.eq_ignore_ascii_case("trigger_4") {
10233                return Ok(out.trigger_4);
10234            }
10235            Err(IndicatorDispatchError::UnknownOutput {
10236                indicator: "mesa_stochastic_multi_length".to_string(),
10237                output: output_id.to_string(),
10238            })
10239        },
10240    )
10241}
10242
10243fn compute_spearman_correlation_batch(
10244    req: IndicatorBatchRequest<'_>,
10245    output_id: &str,
10246) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10247    let data_len = match req.data {
10248        IndicatorDataRef::Candles { candles, source } => {
10249            source_type(candles, source.unwrap_or("close")).len()
10250        }
10251        _ => {
10252            return Err(IndicatorDispatchError::MissingRequiredInput {
10253                indicator: "spearman_correlation".to_string(),
10254                input: IndicatorInputKind::Candles,
10255            })
10256        }
10257    };
10258    let kernel = req.kernel.to_non_batch();
10259    collect_f64(
10260        "spearman_correlation",
10261        output_id,
10262        req.combos,
10263        data_len,
10264        |params| {
10265            let source = get_enum_param("spearman_correlation", params, "source", "close")?;
10266            let comparison_source =
10267                get_enum_param("spearman_correlation", params, "comparison_source", "open")?;
10268            let lookback = get_usize_param("spearman_correlation", params, "lookback", 30)?;
10269            let smoothing_length =
10270                get_usize_param("spearman_correlation", params, "smoothing_length", 3)?;
10271            let (main, compare) = match req.data {
10272                IndicatorDataRef::Candles { candles, .. } => (
10273                    source_type(candles, &source),
10274                    source_type(candles, &comparison_source),
10275                ),
10276                _ => unreachable!(),
10277            };
10278            let input = SpearmanCorrelationInput::from_slices(
10279                main,
10280                compare,
10281                SpearmanCorrelationParams {
10282                    lookback: Some(lookback),
10283                    smoothing_length: Some(smoothing_length),
10284                },
10285            );
10286            let out = spearman_correlation_with_kernel(&input, kernel).map_err(|e| {
10287                IndicatorDispatchError::ComputeFailed {
10288                    indicator: "spearman_correlation".to_string(),
10289                    details: e.to_string(),
10290                }
10291            })?;
10292            if output_id.eq_ignore_ascii_case("raw") || output_id.eq_ignore_ascii_case("value") {
10293                return Ok(out.raw);
10294            }
10295            if output_id.eq_ignore_ascii_case("smoothed") {
10296                return Ok(out.smoothed);
10297            }
10298            Err(IndicatorDispatchError::UnknownOutput {
10299                indicator: "spearman_correlation".to_string(),
10300                output: output_id.to_string(),
10301            })
10302        },
10303    )
10304}
10305
10306fn compute_relative_strength_index_wave_indicator_batch(
10307    req: IndicatorBatchRequest<'_>,
10308    output_id: &str,
10309) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10310    let data_len = match req.data {
10311        IndicatorDataRef::Candles { candles, source } => {
10312            source_type(candles, source.unwrap_or("close")).len()
10313        }
10314        _ => {
10315            return Err(IndicatorDispatchError::MissingRequiredInput {
10316                indicator: "relative_strength_index_wave_indicator".to_string(),
10317                input: IndicatorInputKind::Candles,
10318            })
10319        }
10320    };
10321    let kernel = req.kernel.to_non_batch();
10322    collect_f64(
10323        "relative_strength_index_wave_indicator",
10324        output_id,
10325        req.combos,
10326        data_len,
10327        |params| {
10328            let source = get_enum_param(
10329                "relative_strength_index_wave_indicator",
10330                params,
10331                "source",
10332                "close",
10333            )?;
10334            let rsi_length = get_usize_param(
10335                "relative_strength_index_wave_indicator",
10336                params,
10337                "rsi_length",
10338                14,
10339            )?;
10340            let length1 = get_usize_param(
10341                "relative_strength_index_wave_indicator",
10342                params,
10343                "length1",
10344                2,
10345            )?;
10346            let length2 = get_usize_param(
10347                "relative_strength_index_wave_indicator",
10348                params,
10349                "length2",
10350                5,
10351            )?;
10352            let length3 = get_usize_param(
10353                "relative_strength_index_wave_indicator",
10354                params,
10355                "length3",
10356                9,
10357            )?;
10358            let length4 = get_usize_param(
10359                "relative_strength_index_wave_indicator",
10360                params,
10361                "length4",
10362                13,
10363            )?;
10364            let (src, high, low) = match req.data {
10365                IndicatorDataRef::Candles { candles, .. } => (
10366                    source_type(candles, &source),
10367                    candles.high.as_slice(),
10368                    candles.low.as_slice(),
10369                ),
10370                _ => unreachable!(),
10371            };
10372            let input = RelativeStrengthIndexWaveIndicatorInput::from_slices(
10373                src,
10374                high,
10375                low,
10376                RelativeStrengthIndexWaveIndicatorParams {
10377                    rsi_length: Some(rsi_length),
10378                    length1: Some(length1),
10379                    length2: Some(length2),
10380                    length3: Some(length3),
10381                    length4: Some(length4),
10382                },
10383            );
10384            let out = relative_strength_index_wave_indicator_with_kernel(&input, kernel).map_err(
10385                |e| IndicatorDispatchError::ComputeFailed {
10386                    indicator: "relative_strength_index_wave_indicator".to_string(),
10387                    details: e.to_string(),
10388                },
10389            )?;
10390            if output_id.eq_ignore_ascii_case("rsi_ma1") || output_id.eq_ignore_ascii_case("value")
10391            {
10392                return Ok(out.rsi_ma1);
10393            }
10394            if output_id.eq_ignore_ascii_case("rsi_ma2") {
10395                return Ok(out.rsi_ma2);
10396            }
10397            if output_id.eq_ignore_ascii_case("rsi_ma3") {
10398                return Ok(out.rsi_ma3);
10399            }
10400            if output_id.eq_ignore_ascii_case("rsi_ma4") {
10401                return Ok(out.rsi_ma4);
10402            }
10403            if output_id.eq_ignore_ascii_case("state") {
10404                return Ok(out.state);
10405            }
10406            Err(IndicatorDispatchError::UnknownOutput {
10407                indicator: "relative_strength_index_wave_indicator".to_string(),
10408                output: output_id.to_string(),
10409            })
10410        },
10411    )
10412}
10413
10414fn compute_accumulation_swing_index_batch(
10415    req: IndicatorBatchRequest<'_>,
10416    output_id: &str,
10417) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10418    expect_value_output("accumulation_swing_index", output_id)?;
10419    let (open, high, low, close) = extract_ohlc_full_input("accumulation_swing_index", req.data)?;
10420    let kernel = req.kernel.to_non_batch();
10421    collect_f64(
10422        "accumulation_swing_index",
10423        output_id,
10424        req.combos,
10425        close.len(),
10426        |params| {
10427            let daily_limit =
10428                get_f64_param("accumulation_swing_index", params, "daily_limit", 10_000.0)?;
10429            let input = AccumulationSwingIndexInput::from_slices(
10430                open,
10431                high,
10432                low,
10433                close,
10434                AccumulationSwingIndexParams {
10435                    daily_limit: Some(daily_limit),
10436                },
10437            );
10438            let out = accumulation_swing_index_with_kernel(&input, kernel).map_err(|e| {
10439                IndicatorDispatchError::ComputeFailed {
10440                    indicator: "accumulation_swing_index".to_string(),
10441                    details: e.to_string(),
10442                }
10443            })?;
10444            Ok(out.values)
10445        },
10446    )
10447}
10448
10449fn compute_ichimoku_oscillator_batch(
10450    req: IndicatorBatchRequest<'_>,
10451    output_id: &str,
10452) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10453    let (high, low, close) = extract_ohlc_input("ichimoku_oscillator", req.data)?;
10454    let kernel = req.kernel.to_non_batch();
10455    collect_f64(
10456        "ichimoku_oscillator",
10457        output_id,
10458        req.combos,
10459        close.len(),
10460        |params| {
10461            let source_name = get_enum_param("ichimoku_oscillator", params, "source", "close")?;
10462            let conversion_periods =
10463                get_usize_param("ichimoku_oscillator", params, "conversion_periods", 9)?;
10464            let base_periods = get_usize_param("ichimoku_oscillator", params, "base_periods", 26)?;
10465            let lagging_span_periods =
10466                get_usize_param("ichimoku_oscillator", params, "lagging_span_periods", 52)?;
10467            let displacement = get_usize_param("ichimoku_oscillator", params, "displacement", 26)?;
10468            let ma_length = get_usize_param("ichimoku_oscillator", params, "ma_length", 12)?;
10469            let smoothing_length =
10470                get_usize_param("ichimoku_oscillator", params, "smoothing_length", 3)?;
10471            let extra_smoothing =
10472                get_bool_param("ichimoku_oscillator", params, "extra_smoothing", true)?;
10473            let normalize = get_enum_param("ichimoku_oscillator", params, "normalize", "window")?
10474                .parse::<IchimokuOscillatorNormalizeMode>()
10475                .map_err(|e| IndicatorDispatchError::InvalidParam {
10476                    indicator: "ichimoku_oscillator".to_string(),
10477                    key: "normalize".to_string(),
10478                    reason: e,
10479                })?;
10480            let window_size = get_usize_param("ichimoku_oscillator", params, "window_size", 20)?;
10481            let clamp = get_bool_param("ichimoku_oscillator", params, "clamp", true)?;
10482            let top_band = get_f64_param("ichimoku_oscillator", params, "top_band", 2.0)?;
10483            let mid_band = get_f64_param("ichimoku_oscillator", params, "mid_band", 1.5)?;
10484            let source = match req.data {
10485                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source_name),
10486                _ => close,
10487            };
10488            let input = IchimokuOscillatorInput::from_slices(
10489                high,
10490                low,
10491                close,
10492                source,
10493                IchimokuOscillatorParams {
10494                    conversion_periods: Some(conversion_periods),
10495                    base_periods: Some(base_periods),
10496                    lagging_span_periods: Some(lagging_span_periods),
10497                    displacement: Some(displacement),
10498                    ma_length: Some(ma_length),
10499                    smoothing_length: Some(smoothing_length),
10500                    extra_smoothing: Some(extra_smoothing),
10501                    normalize: Some(normalize),
10502                    window_size: Some(window_size),
10503                    clamp: Some(clamp),
10504                    top_band: Some(top_band),
10505                    mid_band: Some(mid_band),
10506                },
10507            );
10508            let out = ichimoku_oscillator_with_kernel(&input, kernel).map_err(|e| {
10509                IndicatorDispatchError::ComputeFailed {
10510                    indicator: "ichimoku_oscillator".to_string(),
10511                    details: e.to_string(),
10512                }
10513            })?;
10514            if output_id.eq_ignore_ascii_case("signal") || output_id.eq_ignore_ascii_case("value") {
10515                return Ok(out.signal);
10516            }
10517            if output_id.eq_ignore_ascii_case("ma") {
10518                return Ok(out.ma);
10519            }
10520            if output_id.eq_ignore_ascii_case("conversion") {
10521                return Ok(out.conversion);
10522            }
10523            if output_id.eq_ignore_ascii_case("base") {
10524                return Ok(out.base);
10525            }
10526            if output_id.eq_ignore_ascii_case("chikou") {
10527                return Ok(out.chikou);
10528            }
10529            if output_id.eq_ignore_ascii_case("current_kumo_a") {
10530                return Ok(out.current_kumo_a);
10531            }
10532            if output_id.eq_ignore_ascii_case("current_kumo_b") {
10533                return Ok(out.current_kumo_b);
10534            }
10535            if output_id.eq_ignore_ascii_case("future_kumo_a") {
10536                return Ok(out.future_kumo_a);
10537            }
10538            if output_id.eq_ignore_ascii_case("future_kumo_b") {
10539                return Ok(out.future_kumo_b);
10540            }
10541            if output_id.eq_ignore_ascii_case("max_level") {
10542                return Ok(out.max_level);
10543            }
10544            if output_id.eq_ignore_ascii_case("high_level") {
10545                return Ok(out.high_level);
10546            }
10547            if output_id.eq_ignore_ascii_case("low_level") {
10548                return Ok(out.low_level);
10549            }
10550            if output_id.eq_ignore_ascii_case("min_level") {
10551                return Ok(out.min_level);
10552            }
10553            Err(IndicatorDispatchError::UnknownOutput {
10554                indicator: "ichimoku_oscillator".to_string(),
10555                output: output_id.to_string(),
10556            })
10557        },
10558    )
10559}
10560
10561fn compute_volatility_quality_index_batch(
10562    req: IndicatorBatchRequest<'_>,
10563    output_id: &str,
10564) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10565    let (open, high, low, close) = extract_ohlc_full_input("volatility_quality_index", req.data)?;
10566    let kernel = req.kernel.to_non_batch();
10567    collect_f64(
10568        "volatility_quality_index",
10569        output_id,
10570        req.combos,
10571        close.len(),
10572        |params| {
10573            let fast_length =
10574                get_usize_param("volatility_quality_index", params, "fast_length", 9)?;
10575            let slow_length =
10576                get_usize_param("volatility_quality_index", params, "slow_length", 200)?;
10577            let input = VolatilityQualityIndexInput::from_slices(
10578                open,
10579                high,
10580                low,
10581                close,
10582                VolatilityQualityIndexParams {
10583                    fast_length: Some(fast_length),
10584                    slow_length: Some(slow_length),
10585                },
10586            );
10587            let out = volatility_quality_index_with_kernel(&input, kernel).map_err(|e| {
10588                IndicatorDispatchError::ComputeFailed {
10589                    indicator: "volatility_quality_index".to_string(),
10590                    details: e.to_string(),
10591                }
10592            })?;
10593            if output_id.eq_ignore_ascii_case("vqi_sum") || output_id.eq_ignore_ascii_case("value")
10594            {
10595                return Ok(out.vqi_sum);
10596            }
10597            if output_id.eq_ignore_ascii_case("fast_sma") {
10598                return Ok(out.fast_sma);
10599            }
10600            if output_id.eq_ignore_ascii_case("slow_sma") {
10601                return Ok(out.slow_sma);
10602            }
10603            Err(IndicatorDispatchError::UnknownOutput {
10604                indicator: "volatility_quality_index".to_string(),
10605                output: output_id.to_string(),
10606            })
10607        },
10608    )
10609}
10610
10611fn compute_vwap_deviation_oscillator_batch(
10612    req: IndicatorBatchRequest<'_>,
10613    output_id: &str,
10614) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10615    let (timestamps, high, low, close, volume): (&[i64], &[f64], &[f64], &[f64], &[f64]) =
10616        match req.data {
10617            IndicatorDataRef::Candles { candles, .. } => (
10618                candles.timestamp.as_slice(),
10619                candles.high.as_slice(),
10620                candles.low.as_slice(),
10621                candles.close.as_slice(),
10622                candles.volume.as_slice(),
10623            ),
10624            _ => {
10625                return Err(IndicatorDispatchError::MissingRequiredInput {
10626                    indicator: "vwap_deviation_oscillator".to_string(),
10627                    input: IndicatorInputKind::Candles,
10628                })
10629            }
10630        };
10631    let kernel = req.kernel.to_non_batch();
10632    collect_f64(
10633        "vwap_deviation_oscillator",
10634        output_id,
10635        req.combos,
10636        close.len(),
10637        |params| {
10638            let session_mode = get_enum_param(
10639                "vwap_deviation_oscillator",
10640                params,
10641                "session_mode",
10642                "rolling_bars",
10643            )?
10644            .parse::<VwapDeviationSessionMode>()
10645            .map_err(|e| IndicatorDispatchError::InvalidParam {
10646                indicator: "vwap_deviation_oscillator".to_string(),
10647                key: "session_mode".to_string(),
10648                reason: e,
10649            })?;
10650            let rolling_period =
10651                get_usize_param("vwap_deviation_oscillator", params, "rolling_period", 20)?;
10652            let rolling_days =
10653                get_usize_param("vwap_deviation_oscillator", params, "rolling_days", 30)?;
10654            let use_close =
10655                get_bool_param("vwap_deviation_oscillator", params, "use_close", false)?;
10656            let deviation_mode = get_enum_param(
10657                "vwap_deviation_oscillator",
10658                params,
10659                "deviation_mode",
10660                "absolute",
10661            )?
10662            .parse::<VwapDeviationMode>()
10663            .map_err(|e| IndicatorDispatchError::InvalidParam {
10664                indicator: "vwap_deviation_oscillator".to_string(),
10665                key: "deviation_mode".to_string(),
10666                reason: e,
10667            })?;
10668            let z_window = get_usize_param("vwap_deviation_oscillator", params, "z_window", 50)?;
10669            let pct_vol_lookback =
10670                get_usize_param("vwap_deviation_oscillator", params, "pct_vol_lookback", 100)?;
10671            let pct_min_sigma =
10672                get_f64_param("vwap_deviation_oscillator", params, "pct_min_sigma", 0.1)?;
10673            let abs_vol_lookback =
10674                get_usize_param("vwap_deviation_oscillator", params, "abs_vol_lookback", 100)?;
10675            let input = VwapDeviationOscillatorInput::from_slices(
10676                timestamps,
10677                high,
10678                low,
10679                close,
10680                volume,
10681                VwapDeviationOscillatorParams {
10682                    session_mode: Some(session_mode),
10683                    rolling_period: Some(rolling_period),
10684                    rolling_days: Some(rolling_days),
10685                    use_close: Some(use_close),
10686                    deviation_mode: Some(deviation_mode),
10687                    z_window: Some(z_window),
10688                    pct_vol_lookback: Some(pct_vol_lookback),
10689                    pct_min_sigma: Some(pct_min_sigma),
10690                    abs_vol_lookback: Some(abs_vol_lookback),
10691                },
10692            );
10693            let out = vwap_deviation_oscillator_with_kernel(&input, kernel).map_err(|e| {
10694                IndicatorDispatchError::ComputeFailed {
10695                    indicator: "vwap_deviation_oscillator".to_string(),
10696                    details: e.to_string(),
10697                }
10698            })?;
10699            if output_id.eq_ignore_ascii_case("osc") || output_id.eq_ignore_ascii_case("value") {
10700                return Ok(out.osc);
10701            }
10702            if output_id.eq_ignore_ascii_case("std1") {
10703                return Ok(out.std1);
10704            }
10705            if output_id.eq_ignore_ascii_case("std2") {
10706                return Ok(out.std2);
10707            }
10708            if output_id.eq_ignore_ascii_case("std3") {
10709                return Ok(out.std3);
10710            }
10711            Err(IndicatorDispatchError::UnknownOutput {
10712                indicator: "vwap_deviation_oscillator".to_string(),
10713                output: output_id.to_string(),
10714            })
10715        },
10716    )
10717}
10718
10719fn compute_bulls_v_bears_batch(
10720    req: IndicatorBatchRequest<'_>,
10721    output_id: &str,
10722) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10723    let (high, low, close) = extract_ohlc_input("bulls_v_bears", req.data)?;
10724    let kernel = req.kernel.to_non_batch();
10725    collect_f64(
10726        "bulls_v_bears",
10727        output_id,
10728        req.combos,
10729        close.len(),
10730        |params| {
10731            let period = get_usize_param("bulls_v_bears", params, "period", 14)?;
10732            let ma_type = get_enum_param("bulls_v_bears", params, "ma_type", "ema")?
10733                .parse::<BullsVBearsMaType>()
10734                .map_err(|e| IndicatorDispatchError::InvalidParam {
10735                    indicator: "bulls_v_bears".to_string(),
10736                    key: "ma_type".to_string(),
10737                    reason: e,
10738                })?;
10739            let calculation_method =
10740                get_enum_param("bulls_v_bears", params, "calculation_method", "normalized")?
10741                    .parse::<BullsVBearsCalculationMethod>()
10742                    .map_err(|e| IndicatorDispatchError::InvalidParam {
10743                        indicator: "bulls_v_bears".to_string(),
10744                        key: "calculation_method".to_string(),
10745                        reason: e,
10746                    })?;
10747            let normalized_bars_back =
10748                get_usize_param("bulls_v_bears", params, "normalized_bars_back", 120)?;
10749            let raw_rolling_period =
10750                get_usize_param("bulls_v_bears", params, "raw_rolling_period", 50)?;
10751            let raw_threshold_percentile =
10752                get_f64_param("bulls_v_bears", params, "raw_threshold_percentile", 95.0)?;
10753            let threshold_level = get_f64_param("bulls_v_bears", params, "threshold_level", 80.0)?;
10754            let input = BullsVBearsInput::from_slices(
10755                high,
10756                low,
10757                close,
10758                BullsVBearsParams {
10759                    period: Some(period),
10760                    ma_type: Some(ma_type),
10761                    calculation_method: Some(calculation_method),
10762                    normalized_bars_back: Some(normalized_bars_back),
10763                    raw_rolling_period: Some(raw_rolling_period),
10764                    raw_threshold_percentile: Some(raw_threshold_percentile),
10765                    threshold_level: Some(threshold_level),
10766                },
10767            );
10768            let out = bulls_v_bears_with_kernel(&input, kernel).map_err(|e| {
10769                IndicatorDispatchError::ComputeFailed {
10770                    indicator: "bulls_v_bears".to_string(),
10771                    details: e.to_string(),
10772                }
10773            })?;
10774            if output_id.eq_ignore_ascii_case("value") {
10775                return Ok(out.value);
10776            }
10777            if output_id.eq_ignore_ascii_case("bull") {
10778                return Ok(out.bull);
10779            }
10780            if output_id.eq_ignore_ascii_case("bear") {
10781                return Ok(out.bear);
10782            }
10783            if output_id.eq_ignore_ascii_case("ma") {
10784                return Ok(out.ma);
10785            }
10786            if output_id.eq_ignore_ascii_case("upper") {
10787                return Ok(out.upper);
10788            }
10789            if output_id.eq_ignore_ascii_case("lower") {
10790                return Ok(out.lower);
10791            }
10792            if output_id.eq_ignore_ascii_case("bullish_signal") {
10793                return Ok(out.bullish_signal);
10794            }
10795            if output_id.eq_ignore_ascii_case("bearish_signal") {
10796                return Ok(out.bearish_signal);
10797            }
10798            if output_id.eq_ignore_ascii_case("zero_cross_up") {
10799                return Ok(out.zero_cross_up);
10800            }
10801            if output_id.eq_ignore_ascii_case("zero_cross_down") {
10802                return Ok(out.zero_cross_down);
10803            }
10804            Err(IndicatorDispatchError::UnknownOutput {
10805                indicator: "bulls_v_bears".to_string(),
10806                output: output_id.to_string(),
10807            })
10808        },
10809    )
10810}
10811
10812fn compute_smooth_theil_sen_batch(
10813    req: IndicatorBatchRequest<'_>,
10814    output_id: &str,
10815) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10816    let data = extract_slice_input("smooth_theil_sen", req.data, "close")?;
10817    let kernel = req.kernel.to_non_batch();
10818    collect_f64(
10819        "smooth_theil_sen",
10820        output_id,
10821        req.combos,
10822        data.len(),
10823        |params| {
10824            let length = get_usize_param("smooth_theil_sen", params, "length", 25)?;
10825            let offset = get_usize_param("smooth_theil_sen", params, "offset", 0)?;
10826            let multiplier = get_f64_param("smooth_theil_sen", params, "multiplier", 2.0)?;
10827            let slope_style =
10828                get_enum_param("smooth_theil_sen", params, "slope_style", "smooth_median")?
10829                    .parse::<SmoothTheilSenStatStyle>()
10830                    .map_err(|e| IndicatorDispatchError::InvalidParam {
10831                        indicator: "smooth_theil_sen".to_string(),
10832                        key: "slope_style".to_string(),
10833                        reason: e,
10834                    })?;
10835            let residual_style = get_enum_param(
10836                "smooth_theil_sen",
10837                params,
10838                "residual_style",
10839                "smooth_median",
10840            )?
10841            .parse::<SmoothTheilSenStatStyle>()
10842            .map_err(|e| IndicatorDispatchError::InvalidParam {
10843                indicator: "smooth_theil_sen".to_string(),
10844                key: "residual_style".to_string(),
10845                reason: e,
10846            })?;
10847            let deviation_style =
10848                get_enum_param("smooth_theil_sen", params, "deviation_style", "mad")?
10849                    .parse::<SmoothTheilSenDeviationType>()
10850                    .map_err(|e| IndicatorDispatchError::InvalidParam {
10851                        indicator: "smooth_theil_sen".to_string(),
10852                        key: "deviation_style".to_string(),
10853                        reason: e,
10854                    })?;
10855            let mad_style =
10856                get_enum_param("smooth_theil_sen", params, "mad_style", "smooth_median")?
10857                    .parse::<SmoothTheilSenStatStyle>()
10858                    .map_err(|e| IndicatorDispatchError::InvalidParam {
10859                        indicator: "smooth_theil_sen".to_string(),
10860                        key: "mad_style".to_string(),
10861                        reason: e,
10862                    })?;
10863            let include_prediction_in_deviation = get_bool_param(
10864                "smooth_theil_sen",
10865                params,
10866                "include_prediction_in_deviation",
10867                false,
10868            )?;
10869            let input = SmoothTheilSenInput::from_slice(
10870                data,
10871                SmoothTheilSenParams {
10872                    length: Some(length),
10873                    offset: Some(offset),
10874                    multiplier: Some(multiplier),
10875                    slope_style: Some(slope_style),
10876                    residual_style: Some(residual_style),
10877                    deviation_style: Some(deviation_style),
10878                    mad_style: Some(mad_style),
10879                    include_prediction_in_deviation: Some(include_prediction_in_deviation),
10880                },
10881            );
10882            let out = smooth_theil_sen_with_kernel(&input, kernel).map_err(|e| {
10883                IndicatorDispatchError::ComputeFailed {
10884                    indicator: "smooth_theil_sen".to_string(),
10885                    details: e.to_string(),
10886                }
10887            })?;
10888            if output_id.eq_ignore_ascii_case("value") {
10889                return Ok(out.value);
10890            }
10891            if output_id.eq_ignore_ascii_case("upper") {
10892                return Ok(out.upper);
10893            }
10894            if output_id.eq_ignore_ascii_case("lower") {
10895                return Ok(out.lower);
10896            }
10897            if output_id.eq_ignore_ascii_case("slope") {
10898                return Ok(out.slope);
10899            }
10900            if output_id.eq_ignore_ascii_case("intercept") {
10901                return Ok(out.intercept);
10902            }
10903            if output_id.eq_ignore_ascii_case("deviation") {
10904                return Ok(out.deviation);
10905            }
10906            Err(IndicatorDispatchError::UnknownOutput {
10907                indicator: "smooth_theil_sen".to_string(),
10908                output: output_id.to_string(),
10909            })
10910        },
10911    )
10912}
10913
10914fn compute_regression_slope_oscillator_batch(
10915    req: IndicatorBatchRequest<'_>,
10916    output_id: &str,
10917) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10918    let data_len = match req.data {
10919        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
10920        IndicatorDataRef::Slice { values } => values.len(),
10921        _ => {
10922            return Err(IndicatorDispatchError::MissingRequiredInput {
10923                indicator: "regression_slope_oscillator".to_string(),
10924                input: IndicatorInputKind::Slice,
10925            })
10926        }
10927    };
10928    let kernel = req.kernel.to_non_batch();
10929    collect_f64(
10930        "regression_slope_oscillator",
10931        output_id,
10932        req.combos,
10933        data_len,
10934        |params| {
10935            let min_range =
10936                get_usize_param("regression_slope_oscillator", params, "min_range", 10)?;
10937            let max_range =
10938                get_usize_param("regression_slope_oscillator", params, "max_range", 100)?;
10939            let step = get_usize_param("regression_slope_oscillator", params, "step", 5)?;
10940            let signal_line =
10941                get_usize_param("regression_slope_oscillator", params, "signal_line", 7)?;
10942            let input = match req.data {
10943                IndicatorDataRef::Candles { candles, .. } => {
10944                    RegressionSlopeOscillatorInput::from_candles(
10945                        candles,
10946                        RegressionSlopeOscillatorParams {
10947                            min_range: Some(min_range),
10948                            max_range: Some(max_range),
10949                            step: Some(step),
10950                            signal_line: Some(signal_line),
10951                        },
10952                    )
10953                }
10954                IndicatorDataRef::Slice { values } => RegressionSlopeOscillatorInput::from_slice(
10955                    values,
10956                    RegressionSlopeOscillatorParams {
10957                        min_range: Some(min_range),
10958                        max_range: Some(max_range),
10959                        step: Some(step),
10960                        signal_line: Some(signal_line),
10961                    },
10962                ),
10963                _ => unreachable!(),
10964            };
10965            let out = regression_slope_oscillator_with_kernel(&input, kernel).map_err(|e| {
10966                IndicatorDispatchError::ComputeFailed {
10967                    indicator: "regression_slope_oscillator".to_string(),
10968                    details: e.to_string(),
10969                }
10970            })?;
10971            if output_id.eq_ignore_ascii_case("value") {
10972                return Ok(out.value);
10973            }
10974            if output_id.eq_ignore_ascii_case("signal") {
10975                return Ok(out.signal);
10976            }
10977            if output_id.eq_ignore_ascii_case("bullish_reversal") {
10978                return Ok(out.bullish_reversal);
10979            }
10980            if output_id.eq_ignore_ascii_case("bearish_reversal") {
10981                return Ok(out.bearish_reversal);
10982            }
10983            Err(IndicatorDispatchError::UnknownOutput {
10984                indicator: "regression_slope_oscillator".to_string(),
10985                output: output_id.to_string(),
10986            })
10987        },
10988    )
10989}
10990
10991fn compute_linear_regression_intensity_batch(
10992    req: IndicatorBatchRequest<'_>,
10993    output_id: &str,
10994) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
10995    expect_value_output("linear_regression_intensity", output_id)?;
10996    let data_len = match req.data {
10997        IndicatorDataRef::Candles { candles, source } => {
10998            source_type(candles, source.unwrap_or("close")).len()
10999        }
11000        IndicatorDataRef::Slice { values } => values.len(),
11001        _ => {
11002            return Err(IndicatorDispatchError::MissingRequiredInput {
11003                indicator: "linear_regression_intensity".to_string(),
11004                input: IndicatorInputKind::Slice,
11005            })
11006        }
11007    };
11008    let kernel = req.kernel.to_non_batch();
11009    collect_f64(
11010        "linear_regression_intensity",
11011        output_id,
11012        req.combos,
11013        data_len,
11014        |params| {
11015            let source = get_enum_param("linear_regression_intensity", params, "source", "close")?;
11016            let lookback_period =
11017                get_usize_param("linear_regression_intensity", params, "lookback_period", 12)?;
11018            let range_tolerance = get_f64_param(
11019                "linear_regression_intensity",
11020                params,
11021                "range_tolerance",
11022                90.0,
11023            )?;
11024            let linreg_length =
11025                get_usize_param("linear_regression_intensity", params, "linreg_length", 90)?;
11026            let input = match req.data {
11027                IndicatorDataRef::Candles { candles, .. } => {
11028                    LinearRegressionIntensityInput::from_candles(
11029                        candles,
11030                        &source,
11031                        LinearRegressionIntensityParams {
11032                            lookback_period: Some(lookback_period),
11033                            range_tolerance: Some(range_tolerance),
11034                            linreg_length: Some(linreg_length),
11035                        },
11036                    )
11037                }
11038                IndicatorDataRef::Slice { values } => LinearRegressionIntensityInput::from_slice(
11039                    values,
11040                    LinearRegressionIntensityParams {
11041                        lookback_period: Some(lookback_period),
11042                        range_tolerance: Some(range_tolerance),
11043                        linreg_length: Some(linreg_length),
11044                    },
11045                ),
11046                _ => unreachable!(),
11047            };
11048            let out = linear_regression_intensity_with_kernel(&input, kernel).map_err(|e| {
11049                IndicatorDispatchError::ComputeFailed {
11050                    indicator: "linear_regression_intensity".to_string(),
11051                    details: e.to_string(),
11052                }
11053            })?;
11054            Ok(out.values)
11055        },
11056    )
11057}
11058
11059fn compute_moving_average_cross_probability_batch(
11060    req: IndicatorBatchRequest<'_>,
11061    output_id: &str,
11062) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11063    let data_len = match req.data {
11064        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
11065        IndicatorDataRef::Slice { values } => values.len(),
11066        _ => {
11067            return Err(IndicatorDispatchError::MissingRequiredInput {
11068                indicator: "moving_average_cross_probability".to_string(),
11069                input: IndicatorInputKind::Slice,
11070            })
11071        }
11072    };
11073    let kernel = req.kernel.to_non_batch();
11074    collect_f64(
11075        "moving_average_cross_probability",
11076        output_id,
11077        req.combos,
11078        data_len,
11079        |params| {
11080            let ma_type =
11081                get_enum_param("moving_average_cross_probability", params, "ma_type", "ema")?
11082                    .parse::<MovingAverageCrossProbabilityMaType>()
11083                    .map_err(|e| IndicatorDispatchError::InvalidParam {
11084                        indicator: "moving_average_cross_probability".to_string(),
11085                        key: "ma_type".to_string(),
11086                        reason: e,
11087                    })?;
11088            let smoothing_window = get_usize_param(
11089                "moving_average_cross_probability",
11090                params,
11091                "smoothing_window",
11092                7,
11093            )?;
11094            let slow_length = get_usize_param(
11095                "moving_average_cross_probability",
11096                params,
11097                "slow_length",
11098                30,
11099            )?;
11100            let fast_length = get_usize_param(
11101                "moving_average_cross_probability",
11102                params,
11103                "fast_length",
11104                14,
11105            )?;
11106            let resolution =
11107                get_usize_param("moving_average_cross_probability", params, "resolution", 50)?;
11108            let params = MovingAverageCrossProbabilityParams {
11109                ma_type: Some(ma_type),
11110                smoothing_window: Some(smoothing_window),
11111                slow_length: Some(slow_length),
11112                fast_length: Some(fast_length),
11113                resolution: Some(resolution),
11114            };
11115            let input = match req.data {
11116                IndicatorDataRef::Candles { candles, .. } => {
11117                    MovingAverageCrossProbabilityInput::from_candles(candles, params)
11118                }
11119                IndicatorDataRef::Slice { values } => {
11120                    MovingAverageCrossProbabilityInput::from_slice(values, params)
11121                }
11122                _ => unreachable!(),
11123            };
11124            let out =
11125                moving_average_cross_probability_with_kernel(&input, kernel).map_err(|e| {
11126                    IndicatorDispatchError::ComputeFailed {
11127                        indicator: "moving_average_cross_probability".to_string(),
11128                        details: e.to_string(),
11129                    }
11130                })?;
11131            if output_id.eq_ignore_ascii_case("value") {
11132                return Ok(out.value);
11133            }
11134            if output_id.eq_ignore_ascii_case("slow_ma") {
11135                return Ok(out.slow_ma);
11136            }
11137            if output_id.eq_ignore_ascii_case("fast_ma") {
11138                return Ok(out.fast_ma);
11139            }
11140            if output_id.eq_ignore_ascii_case("forecast") {
11141                return Ok(out.forecast);
11142            }
11143            if output_id.eq_ignore_ascii_case("upper") {
11144                return Ok(out.upper);
11145            }
11146            if output_id.eq_ignore_ascii_case("lower") {
11147                return Ok(out.lower);
11148            }
11149            if output_id.eq_ignore_ascii_case("direction") {
11150                return Ok(out.direction);
11151            }
11152            Err(IndicatorDispatchError::UnknownOutput {
11153                indicator: "moving_average_cross_probability".to_string(),
11154                output: output_id.to_string(),
11155            })
11156        },
11157    )
11158}
11159
11160fn compute_volume_zone_oscillator_batch(
11161    req: IndicatorBatchRequest<'_>,
11162    output_id: &str,
11163) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11164    expect_value_output("volume_zone_oscillator", output_id)?;
11165    let (close, volume) = extract_close_volume_input("volume_zone_oscillator", req.data, "close")?;
11166    let kernel = req.kernel.to_non_batch();
11167    collect_f64(
11168        "volume_zone_oscillator",
11169        output_id,
11170        req.combos,
11171        close.len(),
11172        |params| {
11173            let length = get_usize_param("volume_zone_oscillator", params, "length", 14)?;
11174            let intraday_smoothing =
11175                get_bool_param("volume_zone_oscillator", params, "intraday_smoothing", true)?;
11176            let noise_filter =
11177                get_usize_param("volume_zone_oscillator", params, "noise_filter", 4)?;
11178            let input = VolumeZoneOscillatorInput::from_slices(
11179                close,
11180                volume,
11181                VolumeZoneOscillatorParams {
11182                    length: Some(length),
11183                    intraday_smoothing: Some(intraday_smoothing),
11184                    noise_filter: Some(noise_filter),
11185                },
11186            );
11187            let out = volume_zone_oscillator_with_kernel(&input, kernel).map_err(|e| {
11188                IndicatorDispatchError::ComputeFailed {
11189                    indicator: "volume_zone_oscillator".to_string(),
11190                    details: e.to_string(),
11191                }
11192            })?;
11193            Ok(out.values)
11194        },
11195    )
11196}
11197
11198fn compute_market_meanness_index_batch(
11199    req: IndicatorBatchRequest<'_>,
11200    output_id: &str,
11201) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11202    let data_len = match req.data {
11203        IndicatorDataRef::Candles { candles, .. } => candles.close.len(),
11204        IndicatorDataRef::Ohlc { close, .. } => close.len(),
11205        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
11206        _ => {
11207            return Err(IndicatorDispatchError::MissingRequiredInput {
11208                indicator: "market_meanness_index".to_string(),
11209                input: IndicatorInputKind::Ohlc,
11210            })
11211        }
11212    };
11213    let kernel = req.kernel.to_non_batch();
11214    collect_f64(
11215        "market_meanness_index",
11216        output_id,
11217        req.combos,
11218        data_len,
11219        |params| {
11220            let length = get_usize_param("market_meanness_index", params, "length", 300)?;
11221            let source_mode =
11222                get_enum_param("market_meanness_index", params, "source_mode", "Price")?;
11223            let input = match req.data {
11224                IndicatorDataRef::Candles { candles, .. } => {
11225                    MarketMeannessIndexInput::from_candles(
11226                        candles,
11227                        MarketMeannessIndexParams {
11228                            length: Some(length),
11229                            source_mode: Some(source_mode),
11230                        },
11231                    )
11232                }
11233                IndicatorDataRef::Ohlc { open, close, .. } => {
11234                    MarketMeannessIndexInput::from_slices(
11235                        open,
11236                        close,
11237                        MarketMeannessIndexParams {
11238                            length: Some(length),
11239                            source_mode: Some(source_mode),
11240                        },
11241                    )
11242                }
11243                IndicatorDataRef::Ohlcv { open, close, .. } => {
11244                    MarketMeannessIndexInput::from_slices(
11245                        open,
11246                        close,
11247                        MarketMeannessIndexParams {
11248                            length: Some(length),
11249                            source_mode: Some(source_mode),
11250                        },
11251                    )
11252                }
11253                _ => unreachable!(),
11254            };
11255            let out = market_meanness_index_with_kernel(&input, kernel).map_err(|e| {
11256                IndicatorDispatchError::ComputeFailed {
11257                    indicator: "market_meanness_index".to_string(),
11258                    details: e.to_string(),
11259                }
11260            })?;
11261            if output_id.eq_ignore_ascii_case("mmi") || output_id.eq_ignore_ascii_case("value") {
11262                return Ok(out.mmi);
11263            }
11264            if output_id.eq_ignore_ascii_case("mmi_smoothed") {
11265                return Ok(out.mmi_smoothed);
11266            }
11267            Err(IndicatorDispatchError::UnknownOutput {
11268                indicator: "market_meanness_index".to_string(),
11269                output: output_id.to_string(),
11270            })
11271        },
11272    )
11273}
11274
11275fn compute_momentum_ratio_oscillator_batch(
11276    req: IndicatorBatchRequest<'_>,
11277    output_id: &str,
11278) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11279    let data_len = match req.data {
11280        IndicatorDataRef::Candles { candles, source } => {
11281            source_type(candles, source.unwrap_or("close")).len()
11282        }
11283        IndicatorDataRef::Slice { values } => values.len(),
11284        _ => {
11285            return Err(IndicatorDispatchError::MissingRequiredInput {
11286                indicator: "momentum_ratio_oscillator".to_string(),
11287                input: IndicatorInputKind::Slice,
11288            })
11289        }
11290    };
11291    let kernel = req.kernel.to_non_batch();
11292    collect_f64(
11293        "momentum_ratio_oscillator",
11294        output_id,
11295        req.combos,
11296        data_len,
11297        |params| {
11298            let source = get_enum_param("momentum_ratio_oscillator", params, "source", "close")?;
11299            let period = get_usize_param("momentum_ratio_oscillator", params, "period", 50)?;
11300            let input = match req.data {
11301                IndicatorDataRef::Candles { candles, .. } => {
11302                    MomentumRatioOscillatorInput::from_candles(
11303                        candles,
11304                        &source,
11305                        MomentumRatioOscillatorParams {
11306                            period: Some(period),
11307                        },
11308                    )
11309                }
11310                IndicatorDataRef::Slice { values } => MomentumRatioOscillatorInput::from_slice(
11311                    values,
11312                    MomentumRatioOscillatorParams {
11313                        period: Some(period),
11314                    },
11315                ),
11316                _ => unreachable!(),
11317            };
11318            let out = momentum_ratio_oscillator_with_kernel(&input, kernel).map_err(|e| {
11319                IndicatorDispatchError::ComputeFailed {
11320                    indicator: "momentum_ratio_oscillator".to_string(),
11321                    details: e.to_string(),
11322                }
11323            })?;
11324            if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
11325                return Ok(out.line);
11326            }
11327            if output_id.eq_ignore_ascii_case("signal") {
11328                return Ok(out.signal);
11329            }
11330            Err(IndicatorDispatchError::UnknownOutput {
11331                indicator: "momentum_ratio_oscillator".to_string(),
11332                output: output_id.to_string(),
11333            })
11334        },
11335    )
11336}
11337
11338fn compute_pretty_good_oscillator_batch(
11339    req: IndicatorBatchRequest<'_>,
11340    output_id: &str,
11341) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11342    expect_value_output("pretty_good_oscillator", output_id)?;
11343    let data_len = match req.data {
11344        IndicatorDataRef::Candles { candles, source } => {
11345            source_type(candles, source.unwrap_or("close")).len()
11346        }
11347        IndicatorDataRef::Ohlc { close, .. } => close.len(),
11348        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
11349        _ => {
11350            return Err(IndicatorDispatchError::MissingRequiredInput {
11351                indicator: "pretty_good_oscillator".to_string(),
11352                input: IndicatorInputKind::Ohlc,
11353            })
11354        }
11355    };
11356    let kernel = req.kernel.to_non_batch();
11357    collect_f64(
11358        "pretty_good_oscillator",
11359        output_id,
11360        req.combos,
11361        data_len,
11362        |params| {
11363            let source = get_enum_param("pretty_good_oscillator", params, "source", "close")?;
11364            let length = get_usize_param("pretty_good_oscillator", params, "length", 14)?;
11365            let input = match req.data {
11366                IndicatorDataRef::Candles { candles, .. } => {
11367                    PrettyGoodOscillatorInput::from_candles(
11368                        candles,
11369                        &source,
11370                        PrettyGoodOscillatorParams {
11371                            length: Some(length),
11372                        },
11373                    )
11374                }
11375                IndicatorDataRef::Ohlc {
11376                    high,
11377                    low,
11378                    close,
11379                    open,
11380                } => {
11381                    ensure_same_len_4(
11382                        "pretty_good_oscillator",
11383                        open.len(),
11384                        high.len(),
11385                        low.len(),
11386                        close.len(),
11387                    )?;
11388                    let src = match source.to_ascii_lowercase().as_str() {
11389                        "open" => open,
11390                        "high" => high,
11391                        "low" => low,
11392                        _ => close,
11393                    };
11394                    PrettyGoodOscillatorInput::from_slices(
11395                        high,
11396                        low,
11397                        close,
11398                        src,
11399                        PrettyGoodOscillatorParams {
11400                            length: Some(length),
11401                        },
11402                    )
11403                }
11404                IndicatorDataRef::Ohlcv {
11405                    high,
11406                    low,
11407                    close,
11408                    open,
11409                    volume,
11410                } => {
11411                    ensure_same_len_5(
11412                        "pretty_good_oscillator",
11413                        open.len(),
11414                        high.len(),
11415                        low.len(),
11416                        close.len(),
11417                        volume.len(),
11418                    )?;
11419                    let src = match source.to_ascii_lowercase().as_str() {
11420                        "open" => open,
11421                        "high" => high,
11422                        "low" => low,
11423                        _ => close,
11424                    };
11425                    PrettyGoodOscillatorInput::from_slices(
11426                        high,
11427                        low,
11428                        close,
11429                        src,
11430                        PrettyGoodOscillatorParams {
11431                            length: Some(length),
11432                        },
11433                    )
11434                }
11435                _ => unreachable!(),
11436            };
11437            let out = pretty_good_oscillator_with_kernel(&input, kernel).map_err(|e| {
11438                IndicatorDispatchError::ComputeFailed {
11439                    indicator: "pretty_good_oscillator".to_string(),
11440                    details: e.to_string(),
11441                }
11442            })?;
11443            Ok(out.values)
11444        },
11445    )
11446}
11447
11448fn compute_price_density_market_noise_batch(
11449    req: IndicatorBatchRequest<'_>,
11450    output_id: &str,
11451) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11452    let (high, low, close) = extract_ohlc_input("price_density_market_noise", req.data)?;
11453    let kernel = req.kernel.to_non_batch();
11454    collect_f64(
11455        "price_density_market_noise",
11456        output_id,
11457        req.combos,
11458        close.len(),
11459        |params| {
11460            let length = get_usize_param("price_density_market_noise", params, "length", 14)?;
11461            let eval_period =
11462                get_usize_param("price_density_market_noise", params, "eval_period", 200)?;
11463            let input = PriceDensityMarketNoiseInput::from_slices(
11464                high,
11465                low,
11466                close,
11467                PriceDensityMarketNoiseParams {
11468                    length: Some(length),
11469                    eval_period: Some(eval_period),
11470                },
11471            );
11472            let out = price_density_market_noise_with_kernel(&input, kernel).map_err(|e| {
11473                IndicatorDispatchError::ComputeFailed {
11474                    indicator: "price_density_market_noise".to_string(),
11475                    details: e.to_string(),
11476                }
11477            })?;
11478            if output_id.eq_ignore_ascii_case("price_density")
11479                || output_id.eq_ignore_ascii_case("value")
11480            {
11481                return Ok(out.price_density);
11482            }
11483            if output_id.eq_ignore_ascii_case("price_density_percent") {
11484                return Ok(out.price_density_percent);
11485            }
11486            Err(IndicatorDispatchError::UnknownOutput {
11487                indicator: "price_density_market_noise".to_string(),
11488                output: output_id.to_string(),
11489            })
11490        },
11491    )
11492}
11493
11494fn compute_psychological_line_batch(
11495    req: IndicatorBatchRequest<'_>,
11496    output_id: &str,
11497) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11498    expect_value_output("psychological_line", output_id)?;
11499    let data_len = match req.data {
11500        IndicatorDataRef::Candles { candles, source } => {
11501            source_type(candles, source.unwrap_or("close")).len()
11502        }
11503        IndicatorDataRef::Slice { values } => values.len(),
11504        _ => {
11505            return Err(IndicatorDispatchError::MissingRequiredInput {
11506                indicator: "psychological_line".to_string(),
11507                input: IndicatorInputKind::Slice,
11508            })
11509        }
11510    };
11511    let kernel = req.kernel.to_non_batch();
11512    collect_f64(
11513        "psychological_line",
11514        output_id,
11515        req.combos,
11516        data_len,
11517        |params| {
11518            let source = get_enum_param("psychological_line", params, "source", "close")?;
11519            let length = get_usize_param("psychological_line", params, "length", 20)?;
11520            let input = match req.data {
11521                IndicatorDataRef::Candles { candles, .. } => PsychologicalLineInput::from_candles(
11522                    candles,
11523                    &source,
11524                    PsychologicalLineParams {
11525                        length: Some(length),
11526                    },
11527                ),
11528                IndicatorDataRef::Slice { values } => PsychologicalLineInput::from_slice(
11529                    values,
11530                    PsychologicalLineParams {
11531                        length: Some(length),
11532                    },
11533                ),
11534                _ => unreachable!(),
11535            };
11536            let out = psychological_line_with_kernel(&input, kernel).map_err(|e| {
11537                IndicatorDispatchError::ComputeFailed {
11538                    indicator: "psychological_line".to_string(),
11539                    details: e.to_string(),
11540                }
11541            })?;
11542            Ok(out.values)
11543        },
11544    )
11545}
11546
11547fn compute_rank_correlation_index_batch(
11548    req: IndicatorBatchRequest<'_>,
11549    output_id: &str,
11550) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11551    expect_value_output("rank_correlation_index", output_id)?;
11552    let data_len = match req.data {
11553        IndicatorDataRef::Candles { candles, source } => {
11554            source_type(candles, source.unwrap_or("close")).len()
11555        }
11556        IndicatorDataRef::Slice { values } => values.len(),
11557        _ => {
11558            return Err(IndicatorDispatchError::MissingRequiredInput {
11559                indicator: "rank_correlation_index".to_string(),
11560                input: IndicatorInputKind::Slice,
11561            })
11562        }
11563    };
11564    let kernel = req.kernel.to_non_batch();
11565    collect_f64(
11566        "rank_correlation_index",
11567        output_id,
11568        req.combos,
11569        data_len,
11570        |params| {
11571            let source = get_enum_param("rank_correlation_index", params, "source", "close")?;
11572            let length = get_usize_param("rank_correlation_index", params, "length", 12)?;
11573            let input = match req.data {
11574                IndicatorDataRef::Candles { candles, .. } => {
11575                    RankCorrelationIndexInput::from_candles(
11576                        candles,
11577                        &source,
11578                        RankCorrelationIndexParams {
11579                            length: Some(length),
11580                        },
11581                    )
11582                }
11583                IndicatorDataRef::Slice { values } => RankCorrelationIndexInput::from_slice(
11584                    values,
11585                    RankCorrelationIndexParams {
11586                        length: Some(length),
11587                    },
11588                ),
11589                _ => unreachable!(),
11590            };
11591            let out = rank_correlation_index_with_kernel(&input, kernel).map_err(|e| {
11592                IndicatorDispatchError::ComputeFailed {
11593                    indicator: "rank_correlation_index".to_string(),
11594                    details: e.to_string(),
11595                }
11596            })?;
11597            Ok(out.values)
11598        },
11599    )
11600}
11601
11602fn compute_smoothed_gaussian_trend_filter_batch(
11603    req: IndicatorBatchRequest<'_>,
11604    output_id: &str,
11605) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11606    let (high, low, close) = extract_ohlc_input("smoothed_gaussian_trend_filter", req.data)?;
11607    let kernel = req.kernel.to_non_batch();
11608    collect_f64(
11609        "smoothed_gaussian_trend_filter",
11610        output_id,
11611        req.combos,
11612        close.len(),
11613        |params| {
11614            let gaussian_length = get_usize_param(
11615                "smoothed_gaussian_trend_filter",
11616                params,
11617                "gaussian_length",
11618                15,
11619            )?;
11620            let poles = get_usize_param("smoothed_gaussian_trend_filter", params, "poles", 3)?;
11621            let smoothing_length = get_usize_param(
11622                "smoothed_gaussian_trend_filter",
11623                params,
11624                "smoothing_length",
11625                22,
11626            )?;
11627            let linreg_offset =
11628                get_usize_param("smoothed_gaussian_trend_filter", params, "linreg_offset", 7)?;
11629            let input = SmoothedGaussianTrendFilterInput::from_slices(
11630                high,
11631                low,
11632                close,
11633                SmoothedGaussianTrendFilterParams {
11634                    gaussian_length: Some(gaussian_length),
11635                    poles: Some(poles),
11636                    smoothing_length: Some(smoothing_length),
11637                    linreg_offset: Some(linreg_offset),
11638                },
11639            );
11640            let out = smoothed_gaussian_trend_filter_with_kernel(&input, kernel).map_err(|e| {
11641                IndicatorDispatchError::ComputeFailed {
11642                    indicator: "smoothed_gaussian_trend_filter".to_string(),
11643                    details: e.to_string(),
11644                }
11645            })?;
11646            if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
11647                return Ok(out.filter);
11648            }
11649            if output_id.eq_ignore_ascii_case("supertrend") {
11650                return Ok(out.supertrend);
11651            }
11652            if output_id.eq_ignore_ascii_case("trend") {
11653                return Ok(out.trend);
11654            }
11655            if output_id.eq_ignore_ascii_case("ranging") {
11656                return Ok(out.ranging);
11657            }
11658            Err(IndicatorDispatchError::UnknownOutput {
11659                indicator: "smoothed_gaussian_trend_filter".to_string(),
11660                output: output_id.to_string(),
11661            })
11662        },
11663    )
11664}
11665
11666fn compute_stochastic_adaptive_d_batch(
11667    req: IndicatorBatchRequest<'_>,
11668    output_id: &str,
11669) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11670    let (high, low, close) = extract_ohlc_input("stochastic_adaptive_d", req.data)?;
11671    let kernel = req.kernel.to_non_batch();
11672    collect_f64(
11673        "stochastic_adaptive_d",
11674        output_id,
11675        req.combos,
11676        close.len(),
11677        |params| {
11678            let k_length = get_usize_param("stochastic_adaptive_d", params, "k_length", 20)?;
11679            let d_smoothing = get_usize_param("stochastic_adaptive_d", params, "d_smoothing", 9)?;
11680            let pre_smooth = get_usize_param("stochastic_adaptive_d", params, "pre_smooth", 20)?;
11681            let attenuation = get_f64_param("stochastic_adaptive_d", params, "attenuation", 2.0)?;
11682            let input = StochasticAdaptiveDInput::from_slices(
11683                high,
11684                low,
11685                close,
11686                StochasticAdaptiveDParams {
11687                    k_length: Some(k_length),
11688                    d_smoothing: Some(d_smoothing),
11689                    pre_smooth: Some(pre_smooth),
11690                    attenuation: Some(attenuation),
11691                },
11692            );
11693            let out = stochastic_adaptive_d_with_kernel(&input, kernel).map_err(|e| {
11694                IndicatorDispatchError::ComputeFailed {
11695                    indicator: "stochastic_adaptive_d".to_string(),
11696                    details: e.to_string(),
11697                }
11698            })?;
11699            if output_id.eq_ignore_ascii_case("standard_d")
11700                || output_id.eq_ignore_ascii_case("value")
11701            {
11702                return Ok(out.standard_d);
11703            }
11704            if output_id.eq_ignore_ascii_case("adaptive_d") {
11705                return Ok(out.adaptive_d);
11706            }
11707            if output_id.eq_ignore_ascii_case("difference") {
11708                return Ok(out.difference);
11709            }
11710            Err(IndicatorDispatchError::UnknownOutput {
11711                indicator: "stochastic_adaptive_d".to_string(),
11712                output: output_id.to_string(),
11713            })
11714        },
11715    )
11716}
11717
11718fn compute_stochastic_connors_rsi_batch(
11719    req: IndicatorBatchRequest<'_>,
11720    output_id: &str,
11721) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11722    let data_len = match req.data {
11723        IndicatorDataRef::Candles { candles, source } => {
11724            source_type(candles, source.unwrap_or("close")).len()
11725        }
11726        IndicatorDataRef::Slice { values } => values.len(),
11727        _ => {
11728            return Err(IndicatorDispatchError::MissingRequiredInput {
11729                indicator: "stochastic_connors_rsi".to_string(),
11730                input: IndicatorInputKind::Slice,
11731            })
11732        }
11733    };
11734    let kernel = req.kernel.to_non_batch();
11735    collect_f64(
11736        "stochastic_connors_rsi",
11737        output_id,
11738        req.combos,
11739        data_len,
11740        |params| {
11741            let source = get_enum_param("stochastic_connors_rsi", params, "source", "close")?;
11742            let stoch_length =
11743                get_usize_param("stochastic_connors_rsi", params, "stoch_length", 3)?;
11744            let smooth_k = get_usize_param("stochastic_connors_rsi", params, "smooth_k", 3)?;
11745            let smooth_d = get_usize_param("stochastic_connors_rsi", params, "smooth_d", 3)?;
11746            let rsi_length = get_usize_param("stochastic_connors_rsi", params, "rsi_length", 3)?;
11747            let updown_length =
11748                get_usize_param("stochastic_connors_rsi", params, "updown_length", 2)?;
11749            let roc_length = get_usize_param("stochastic_connors_rsi", params, "roc_length", 100)?;
11750            let input = match req.data {
11751                IndicatorDataRef::Candles { candles, .. } => {
11752                    StochasticConnorsRsiInput::from_candles(
11753                        candles,
11754                        &source,
11755                        StochasticConnorsRsiParams {
11756                            stoch_length: Some(stoch_length),
11757                            smooth_k: Some(smooth_k),
11758                            smooth_d: Some(smooth_d),
11759                            rsi_length: Some(rsi_length),
11760                            updown_length: Some(updown_length),
11761                            roc_length: Some(roc_length),
11762                        },
11763                    )
11764                }
11765                IndicatorDataRef::Slice { values } => StochasticConnorsRsiInput::from_slice(
11766                    values,
11767                    StochasticConnorsRsiParams {
11768                        stoch_length: Some(stoch_length),
11769                        smooth_k: Some(smooth_k),
11770                        smooth_d: Some(smooth_d),
11771                        rsi_length: Some(rsi_length),
11772                        updown_length: Some(updown_length),
11773                        roc_length: Some(roc_length),
11774                    },
11775                ),
11776                _ => unreachable!(),
11777            };
11778            let out = stochastic_connors_rsi_with_kernel(&input, kernel).map_err(|e| {
11779                IndicatorDispatchError::ComputeFailed {
11780                    indicator: "stochastic_connors_rsi".to_string(),
11781                    details: e.to_string(),
11782                }
11783            })?;
11784            if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
11785                return Ok(out.k);
11786            }
11787            if output_id.eq_ignore_ascii_case("d") {
11788                return Ok(out.d);
11789            }
11790            Err(IndicatorDispatchError::UnknownOutput {
11791                indicator: "stochastic_connors_rsi".to_string(),
11792                output: output_id.to_string(),
11793            })
11794        },
11795    )
11796}
11797
11798fn compute_supertrend_oscillator_batch(
11799    req: IndicatorBatchRequest<'_>,
11800    output_id: &str,
11801) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11802    let data_len = match req.data {
11803        IndicatorDataRef::Candles { candles, source } => {
11804            source_type(candles, source.unwrap_or("close")).len()
11805        }
11806        IndicatorDataRef::Ohlc { close, .. } => close.len(),
11807        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
11808        _ => {
11809            return Err(IndicatorDispatchError::MissingRequiredInput {
11810                indicator: "supertrend_oscillator".to_string(),
11811                input: IndicatorInputKind::Ohlc,
11812            })
11813        }
11814    };
11815    let kernel = req.kernel.to_non_batch();
11816    collect_f64(
11817        "supertrend_oscillator",
11818        output_id,
11819        req.combos,
11820        data_len,
11821        |params| {
11822            let source = get_enum_param("supertrend_oscillator", params, "source", "close")?;
11823            let length = get_usize_param("supertrend_oscillator", params, "length", 10)?;
11824            let mult = get_f64_param("supertrend_oscillator", params, "mult", 2.0)?;
11825            let smooth = get_usize_param("supertrend_oscillator", params, "smooth", 72)?;
11826            let input = match req.data {
11827                IndicatorDataRef::Candles { candles, .. } => {
11828                    SuperTrendOscillatorInput::from_candles(
11829                        candles,
11830                        &source,
11831                        SuperTrendOscillatorParams {
11832                            length: Some(length),
11833                            mult: Some(mult),
11834                            smooth: Some(smooth),
11835                        },
11836                    )
11837                }
11838                IndicatorDataRef::Ohlc {
11839                    high,
11840                    low,
11841                    close,
11842                    open,
11843                } => {
11844                    ensure_same_len_4(
11845                        "supertrend_oscillator",
11846                        open.len(),
11847                        high.len(),
11848                        low.len(),
11849                        close.len(),
11850                    )?;
11851                    let src = match source.to_ascii_lowercase().as_str() {
11852                        "open" => open,
11853                        "high" => high,
11854                        "low" => low,
11855                        _ => close,
11856                    };
11857                    SuperTrendOscillatorInput::from_slices(
11858                        high,
11859                        low,
11860                        src,
11861                        SuperTrendOscillatorParams {
11862                            length: Some(length),
11863                            mult: Some(mult),
11864                            smooth: Some(smooth),
11865                        },
11866                    )
11867                }
11868                IndicatorDataRef::Ohlcv {
11869                    high,
11870                    low,
11871                    close,
11872                    open,
11873                    volume,
11874                } => {
11875                    ensure_same_len_5(
11876                        "supertrend_oscillator",
11877                        open.len(),
11878                        high.len(),
11879                        low.len(),
11880                        close.len(),
11881                        volume.len(),
11882                    )?;
11883                    let src = match source.to_ascii_lowercase().as_str() {
11884                        "open" => open,
11885                        "high" => high,
11886                        "low" => low,
11887                        _ => close,
11888                    };
11889                    SuperTrendOscillatorInput::from_slices(
11890                        high,
11891                        low,
11892                        src,
11893                        SuperTrendOscillatorParams {
11894                            length: Some(length),
11895                            mult: Some(mult),
11896                            smooth: Some(smooth),
11897                        },
11898                    )
11899                }
11900                _ => unreachable!(),
11901            };
11902            let out = supertrend_oscillator_with_kernel(&input, kernel).map_err(|e| {
11903                IndicatorDispatchError::ComputeFailed {
11904                    indicator: "supertrend_oscillator".to_string(),
11905                    details: e.to_string(),
11906                }
11907            })?;
11908            if output_id.eq_ignore_ascii_case("oscillator")
11909                || output_id.eq_ignore_ascii_case("value")
11910            {
11911                return Ok(out.oscillator);
11912            }
11913            if output_id.eq_ignore_ascii_case("signal") {
11914                return Ok(out.signal);
11915            }
11916            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
11917            {
11918                return Ok(out.histogram);
11919            }
11920            Err(IndicatorDispatchError::UnknownOutput {
11921                indicator: "supertrend_oscillator".to_string(),
11922                output: output_id.to_string(),
11923            })
11924        },
11925    )
11926}
11927
11928fn compute_trend_continuation_factor_batch(
11929    req: IndicatorBatchRequest<'_>,
11930    output_id: &str,
11931) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11932    let data_len = match req.data {
11933        IndicatorDataRef::Candles { candles, source } => {
11934            source_type(candles, source.unwrap_or("close")).len()
11935        }
11936        IndicatorDataRef::Slice { values } => values.len(),
11937        _ => {
11938            return Err(IndicatorDispatchError::MissingRequiredInput {
11939                indicator: "trend_continuation_factor".to_string(),
11940                input: IndicatorInputKind::Slice,
11941            })
11942        }
11943    };
11944    let kernel = req.kernel.to_non_batch();
11945    collect_f64(
11946        "trend_continuation_factor",
11947        output_id,
11948        req.combos,
11949        data_len,
11950        |params| {
11951            let source = get_enum_param("trend_continuation_factor", params, "source", "close")?;
11952            let length = get_usize_param("trend_continuation_factor", params, "length", 35)?;
11953            let input = match req.data {
11954                IndicatorDataRef::Candles { candles, .. } => {
11955                    TrendContinuationFactorInput::from_candles(
11956                        candles,
11957                        &source,
11958                        TrendContinuationFactorParams {
11959                            length: Some(length),
11960                        },
11961                    )
11962                }
11963                IndicatorDataRef::Slice { values } => TrendContinuationFactorInput::from_slice(
11964                    values,
11965                    TrendContinuationFactorParams {
11966                        length: Some(length),
11967                    },
11968                ),
11969                _ => unreachable!(),
11970            };
11971            let out = trend_continuation_factor_with_kernel(&input, kernel).map_err(|e| {
11972                IndicatorDispatchError::ComputeFailed {
11973                    indicator: "trend_continuation_factor".to_string(),
11974                    details: e.to_string(),
11975                }
11976            })?;
11977            if output_id.eq_ignore_ascii_case("plus_tcf") || output_id.eq_ignore_ascii_case("value")
11978            {
11979                return Ok(out.plus_tcf);
11980            }
11981            if output_id.eq_ignore_ascii_case("minus_tcf") {
11982                return Ok(out.minus_tcf);
11983            }
11984            Err(IndicatorDispatchError::UnknownOutput {
11985                indicator: "trend_continuation_factor".to_string(),
11986                output: output_id.to_string(),
11987            })
11988        },
11989    )
11990}
11991
11992fn compute_volume_weighted_stochastic_rsi_batch(
11993    req: IndicatorBatchRequest<'_>,
11994    output_id: &str,
11995) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
11996    let (source, volume) =
11997        extract_close_volume_input("volume_weighted_stochastic_rsi", req.data, "close")?;
11998    let kernel = req.kernel.to_non_batch();
11999    collect_f64(
12000        "volume_weighted_stochastic_rsi",
12001        output_id,
12002        req.combos,
12003        source.len(),
12004        |params| {
12005            let rsi_length =
12006                get_usize_param("volume_weighted_stochastic_rsi", params, "rsi_length", 14)?;
12007            let stoch_length =
12008                get_usize_param("volume_weighted_stochastic_rsi", params, "stoch_length", 14)?;
12009            let k_length =
12010                get_usize_param("volume_weighted_stochastic_rsi", params, "k_length", 3)?;
12011            let d_length =
12012                get_usize_param("volume_weighted_stochastic_rsi", params, "d_length", 3)?;
12013            let ma_type =
12014                get_enum_param("volume_weighted_stochastic_rsi", params, "ma_type", "WSMA")?;
12015            let input = VolumeWeightedStochasticRsiInput::from_slices(
12016                source,
12017                volume,
12018                VolumeWeightedStochasticRsiParams {
12019                    rsi_length: Some(rsi_length),
12020                    stoch_length: Some(stoch_length),
12021                    k_length: Some(k_length),
12022                    d_length: Some(d_length),
12023                    ma_type: Some(ma_type),
12024                },
12025            );
12026            let out = volume_weighted_stochastic_rsi_with_kernel(&input, kernel).map_err(|e| {
12027                IndicatorDispatchError::ComputeFailed {
12028                    indicator: "volume_weighted_stochastic_rsi".to_string(),
12029                    details: e.to_string(),
12030                }
12031            })?;
12032            if output_id.eq_ignore_ascii_case("k") || output_id.eq_ignore_ascii_case("value") {
12033                return Ok(out.k);
12034            }
12035            if output_id.eq_ignore_ascii_case("d") {
12036                return Ok(out.d);
12037            }
12038            Err(IndicatorDispatchError::UnknownOutput {
12039                indicator: "volume_weighted_stochastic_rsi".to_string(),
12040                output: output_id.to_string(),
12041            })
12042        },
12043    )
12044}
12045
12046fn compute_logarithmic_moving_average_batch(
12047    req: IndicatorBatchRequest<'_>,
12048    output_id: &str,
12049) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12050    let data_len = match req.data {
12051        IndicatorDataRef::Candles { candles, source } => {
12052            source_type(candles, source.unwrap_or("close")).len()
12053        }
12054        IndicatorDataRef::Slice { values } => values.len(),
12055        IndicatorDataRef::CloseVolume { close, .. } => close.len(),
12056        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
12057        _ => {
12058            return Err(IndicatorDispatchError::MissingRequiredInput {
12059                indicator: "logarithmic_moving_average".to_string(),
12060                input: IndicatorInputKind::CloseVolume,
12061            })
12062        }
12063    };
12064    let kernel = req.kernel.to_non_batch();
12065    collect_f64(
12066        "logarithmic_moving_average",
12067        output_id,
12068        req.combos,
12069        data_len,
12070        |params| {
12071            let source = get_enum_param("logarithmic_moving_average", params, "source", "close")?;
12072            let period = get_usize_param("logarithmic_moving_average", params, "period", 100)?;
12073            let steepness = get_f64_param("logarithmic_moving_average", params, "steepness", 2.5)?;
12074            let ma_type = get_enum_param("logarithmic_moving_average", params, "ma_type", "ema")?;
12075            let smooth = get_usize_param("logarithmic_moving_average", params, "smooth", 10)?;
12076            let momentum_weight =
12077                get_f64_param("logarithmic_moving_average", params, "momentum_weight", 1.2)?;
12078            let long_threshold =
12079                get_f64_param("logarithmic_moving_average", params, "long_threshold", 0.5)?;
12080            let short_threshold = get_f64_param(
12081                "logarithmic_moving_average",
12082                params,
12083                "short_threshold",
12084                -0.5,
12085            )?;
12086            let params = LogarithmicMovingAverageParams {
12087                period: Some(period),
12088                steepness: Some(steepness),
12089                ma_type: Some(ma_type),
12090                smooth: Some(smooth),
12091                momentum_weight: Some(momentum_weight),
12092                long_threshold: Some(long_threshold),
12093                short_threshold: Some(short_threshold),
12094            };
12095            let input = match req.data {
12096                IndicatorDataRef::Candles { candles, .. } => {
12097                    LogarithmicMovingAverageInput::from_candles(candles, &source, params)
12098                }
12099                IndicatorDataRef::Slice { values } => {
12100                    LogarithmicMovingAverageInput::from_slice(values, params)
12101                }
12102                IndicatorDataRef::CloseVolume { close, volume } => {
12103                    LogarithmicMovingAverageInput::from_slice_with_volume(close, volume, params)
12104                }
12105                IndicatorDataRef::Ohlcv {
12106                    open,
12107                    high,
12108                    low,
12109                    close,
12110                    volume,
12111                } => {
12112                    let price = match source.to_ascii_lowercase().as_str() {
12113                        "open" => open,
12114                        "high" => high,
12115                        "low" => low,
12116                        _ => close,
12117                    };
12118                    LogarithmicMovingAverageInput::from_slice_with_volume(price, volume, params)
12119                }
12120                _ => unreachable!(),
12121            };
12122            let out = logarithmic_moving_average_with_kernel(&input, kernel).map_err(|e| {
12123                IndicatorDispatchError::ComputeFailed {
12124                    indicator: "logarithmic_moving_average".to_string(),
12125                    details: e.to_string(),
12126                }
12127            })?;
12128            if output_id.eq_ignore_ascii_case("lma") || output_id.eq_ignore_ascii_case("value") {
12129                return Ok(out.lma);
12130            }
12131            if output_id.eq_ignore_ascii_case("signal") {
12132                return Ok(out.signal);
12133            }
12134            if output_id.eq_ignore_ascii_case("position") {
12135                return Ok(out.position);
12136            }
12137            if output_id.eq_ignore_ascii_case("momentum_confirmed") {
12138                return Ok(out.momentum_confirmed);
12139            }
12140            Err(IndicatorDispatchError::UnknownOutput {
12141                indicator: "logarithmic_moving_average".to_string(),
12142                output: output_id.to_string(),
12143            })
12144        },
12145    )
12146}
12147
12148fn compute_adaptive_schaff_trend_cycle_batch(
12149    req: IndicatorBatchRequest<'_>,
12150    output_id: &str,
12151) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12152    let (high, low, close) = extract_ohlc_input("adaptive_schaff_trend_cycle", req.data)?;
12153    let kernel = req.kernel.to_non_batch();
12154    collect_f64(
12155        "adaptive_schaff_trend_cycle",
12156        output_id,
12157        req.combos,
12158        close.len(),
12159        |params| {
12160            let adaptive_length =
12161                get_usize_param("adaptive_schaff_trend_cycle", params, "adaptive_length", 55)?;
12162            let stc_length =
12163                get_usize_param("adaptive_schaff_trend_cycle", params, "stc_length", 12)?;
12164            let smoothing_factor = get_f64_param(
12165                "adaptive_schaff_trend_cycle",
12166                params,
12167                "smoothing_factor",
12168                0.45,
12169            )?;
12170            let fast_length =
12171                get_usize_param("adaptive_schaff_trend_cycle", params, "fast_length", 26)?;
12172            let slow_length =
12173                get_usize_param("adaptive_schaff_trend_cycle", params, "slow_length", 50)?;
12174            let input = AdaptiveSchaffTrendCycleInput::from_slices(
12175                high,
12176                low,
12177                close,
12178                AdaptiveSchaffTrendCycleParams {
12179                    adaptive_length: Some(adaptive_length),
12180                    stc_length: Some(stc_length),
12181                    smoothing_factor: Some(smoothing_factor),
12182                    fast_length: Some(fast_length),
12183                    slow_length: Some(slow_length),
12184                },
12185            );
12186            let out = adaptive_schaff_trend_cycle_with_kernel(&input, kernel).map_err(|e| {
12187                IndicatorDispatchError::ComputeFailed {
12188                    indicator: "adaptive_schaff_trend_cycle".to_string(),
12189                    details: e.to_string(),
12190                }
12191            })?;
12192            if output_id.eq_ignore_ascii_case("stc") || output_id.eq_ignore_ascii_case("value") {
12193                return Ok(out.stc);
12194            }
12195            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
12196            {
12197                return Ok(out.histogram);
12198            }
12199            Err(IndicatorDispatchError::UnknownOutput {
12200                indicator: "adaptive_schaff_trend_cycle".to_string(),
12201                output: output_id.to_string(),
12202            })
12203        },
12204    )
12205}
12206
12207fn compute_ehlers_detrending_filter_batch(
12208    req: IndicatorBatchRequest<'_>,
12209    output_id: &str,
12210) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12211    let data_len = match req.data {
12212        IndicatorDataRef::Candles { candles, source } => {
12213            source_type(candles, source.unwrap_or("hlcc4")).len()
12214        }
12215        IndicatorDataRef::Slice { values } => values.len(),
12216        _ => {
12217            return Err(IndicatorDispatchError::MissingRequiredInput {
12218                indicator: "ehlers_detrending_filter".to_string(),
12219                input: IndicatorInputKind::Slice,
12220            })
12221        }
12222    };
12223    let kernel = req.kernel.to_non_batch();
12224    collect_f64(
12225        "ehlers_detrending_filter",
12226        output_id,
12227        req.combos,
12228        data_len,
12229        |params| {
12230            let source = get_enum_param("ehlers_detrending_filter", params, "source", "hlcc4")?;
12231            let length = get_usize_param("ehlers_detrending_filter", params, "length", 10)?;
12232            let input = match req.data {
12233                IndicatorDataRef::Candles { candles, .. } => {
12234                    EhlersDetrendingFilterInput::from_candles(
12235                        candles,
12236                        &source,
12237                        EhlersDetrendingFilterParams {
12238                            length: Some(length),
12239                        },
12240                    )
12241                }
12242                IndicatorDataRef::Slice { values } => EhlersDetrendingFilterInput::from_slice(
12243                    values,
12244                    EhlersDetrendingFilterParams {
12245                        length: Some(length),
12246                    },
12247                ),
12248                _ => unreachable!(),
12249            };
12250            let out = ehlers_detrending_filter_with_kernel(&input, kernel).map_err(|e| {
12251                IndicatorDispatchError::ComputeFailed {
12252                    indicator: "ehlers_detrending_filter".to_string(),
12253                    details: e.to_string(),
12254                }
12255            })?;
12256            if output_id.eq_ignore_ascii_case("edf") || output_id.eq_ignore_ascii_case("value") {
12257                return Ok(out.edf);
12258            }
12259            if output_id.eq_ignore_ascii_case("signal") {
12260                return Ok(out.signal);
12261            }
12262            Err(IndicatorDispatchError::UnknownOutput {
12263                indicator: "ehlers_detrending_filter".to_string(),
12264                output: output_id.to_string(),
12265            })
12266        },
12267    )
12268}
12269
12270fn compute_hypertrend_batch(
12271    req: IndicatorBatchRequest<'_>,
12272    output_id: &str,
12273) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12274    let data_len = match req.data {
12275        IndicatorDataRef::Candles { candles, source } => {
12276            source_type(candles, source.unwrap_or("close")).len()
12277        }
12278        IndicatorDataRef::Ohlc { close, .. } => close.len(),
12279        IndicatorDataRef::Ohlcv { close, .. } => close.len(),
12280        _ => {
12281            return Err(IndicatorDispatchError::MissingRequiredInput {
12282                indicator: "hypertrend".to_string(),
12283                input: IndicatorInputKind::Ohlc,
12284            })
12285        }
12286    };
12287    let kernel = req.kernel.to_non_batch();
12288    collect_f64("hypertrend", output_id, req.combos, data_len, |params| {
12289        let source = get_enum_param("hypertrend", params, "source", "close")?;
12290        let factor = get_f64_param("hypertrend", params, "factor", 5.0)?;
12291        let slope = get_f64_param("hypertrend", params, "slope", 14.0)?;
12292        let width_percent = get_f64_param("hypertrend", params, "width_percent", 80.0)?;
12293        let input = match req.data {
12294            IndicatorDataRef::Candles { candles, .. } => HyperTrendInput::from_candles(
12295                candles,
12296                &source,
12297                HyperTrendParams {
12298                    factor: Some(factor),
12299                    slope: Some(slope),
12300                    width_percent: Some(width_percent),
12301                },
12302            ),
12303            IndicatorDataRef::Ohlc {
12304                high,
12305                low,
12306                close,
12307                open,
12308            } => {
12309                ensure_same_len_4("hypertrend", open.len(), high.len(), low.len(), close.len())?;
12310                let src = match source.to_ascii_lowercase().as_str() {
12311                    "open" => open,
12312                    "high" => high,
12313                    "low" => low,
12314                    _ => close,
12315                };
12316                HyperTrendInput::from_slices(
12317                    high,
12318                    low,
12319                    src,
12320                    HyperTrendParams {
12321                        factor: Some(factor),
12322                        slope: Some(slope),
12323                        width_percent: Some(width_percent),
12324                    },
12325                )
12326            }
12327            IndicatorDataRef::Ohlcv {
12328                high,
12329                low,
12330                close,
12331                open,
12332                volume,
12333            } => {
12334                ensure_same_len_5(
12335                    "hypertrend",
12336                    open.len(),
12337                    high.len(),
12338                    low.len(),
12339                    close.len(),
12340                    volume.len(),
12341                )?;
12342                let src = match source.to_ascii_lowercase().as_str() {
12343                    "open" => open,
12344                    "high" => high,
12345                    "low" => low,
12346                    _ => close,
12347                };
12348                HyperTrendInput::from_slices(
12349                    high,
12350                    low,
12351                    src,
12352                    HyperTrendParams {
12353                        factor: Some(factor),
12354                        slope: Some(slope),
12355                        width_percent: Some(width_percent),
12356                    },
12357                )
12358            }
12359            _ => unreachable!(),
12360        };
12361        let out = hypertrend_with_kernel(&input, kernel).map_err(|e| {
12362            IndicatorDispatchError::ComputeFailed {
12363                indicator: "hypertrend".to_string(),
12364                details: e.to_string(),
12365            }
12366        })?;
12367        if output_id.eq_ignore_ascii_case("upper") {
12368            return Ok(out.upper);
12369        }
12370        if output_id.eq_ignore_ascii_case("average") || output_id.eq_ignore_ascii_case("value") {
12371            return Ok(out.average);
12372        }
12373        if output_id.eq_ignore_ascii_case("lower") {
12374            return Ok(out.lower);
12375        }
12376        if output_id.eq_ignore_ascii_case("trend") {
12377            return Ok(out.trend);
12378        }
12379        if output_id.eq_ignore_ascii_case("changed") {
12380            return Ok(out.changed);
12381        }
12382        Err(IndicatorDispatchError::UnknownOutput {
12383            indicator: "hypertrend".to_string(),
12384            output: output_id.to_string(),
12385        })
12386    })
12387}
12388
12389fn compute_ict_propulsion_block_batch(
12390    req: IndicatorBatchRequest<'_>,
12391    output_id: &str,
12392) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12393    let (open, high, low, close) = extract_ohlc_full_input("ict_propulsion_block", req.data)?;
12394    let kernel = req.kernel.to_non_batch();
12395    collect_f64(
12396        "ict_propulsion_block",
12397        output_id,
12398        req.combos,
12399        close.len(),
12400        |params| {
12401            let swing_length = get_usize_param("ict_propulsion_block", params, "swing_length", 3)?;
12402            let mitigation_price =
12403                match get_enum_param("ict_propulsion_block", params, "mitigation_price", "close")?
12404                    .to_ascii_lowercase()
12405                    .as_str()
12406                {
12407                    "close" => IctPropulsionBlockMitigationPrice::Close,
12408                    "wick" => IctPropulsionBlockMitigationPrice::Wick,
12409                    other => {
12410                        return Err(IndicatorDispatchError::InvalidParam {
12411                            indicator: "ict_propulsion_block".to_string(),
12412                            key: "mitigation_price".to_string(),
12413                            reason: format!("unsupported value '{other}'"),
12414                        })
12415                    }
12416                };
12417            let input = IctPropulsionBlockInput::from_slices(
12418                open,
12419                high,
12420                low,
12421                close,
12422                IctPropulsionBlockParams {
12423                    swing_length: Some(swing_length),
12424                    mitigation_price: Some(mitigation_price),
12425                },
12426            );
12427            let out = ict_propulsion_block_with_kernel(&input, kernel).map_err(|e| {
12428                IndicatorDispatchError::ComputeFailed {
12429                    indicator: "ict_propulsion_block".to_string(),
12430                    details: e.to_string(),
12431                }
12432            })?;
12433            if output_id.eq_ignore_ascii_case("bullish_high") {
12434                return Ok(out.bullish_high);
12435            }
12436            if output_id.eq_ignore_ascii_case("bullish_low") {
12437                return Ok(out.bullish_low);
12438            }
12439            if output_id.eq_ignore_ascii_case("bullish_kind") {
12440                return Ok(out.bullish_kind);
12441            }
12442            if output_id.eq_ignore_ascii_case("bullish_active") {
12443                return Ok(out.bullish_active);
12444            }
12445            if output_id.eq_ignore_ascii_case("bullish_mitigated") {
12446                return Ok(out.bullish_mitigated);
12447            }
12448            if output_id.eq_ignore_ascii_case("bullish_new") {
12449                return Ok(out.bullish_new);
12450            }
12451            if output_id.eq_ignore_ascii_case("bearish_high") {
12452                return Ok(out.bearish_high);
12453            }
12454            if output_id.eq_ignore_ascii_case("bearish_low") {
12455                return Ok(out.bearish_low);
12456            }
12457            if output_id.eq_ignore_ascii_case("bearish_kind") {
12458                return Ok(out.bearish_kind);
12459            }
12460            if output_id.eq_ignore_ascii_case("bearish_active") {
12461                return Ok(out.bearish_active);
12462            }
12463            if output_id.eq_ignore_ascii_case("bearish_mitigated") {
12464                return Ok(out.bearish_mitigated);
12465            }
12466            if output_id.eq_ignore_ascii_case("bearish_new") {
12467                return Ok(out.bearish_new);
12468            }
12469            Err(IndicatorDispatchError::UnknownOutput {
12470                indicator: "ict_propulsion_block".to_string(),
12471                output: output_id.to_string(),
12472            })
12473        },
12474    )
12475}
12476
12477fn compute_impulse_macd_batch(
12478    req: IndicatorBatchRequest<'_>,
12479    output_id: &str,
12480) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12481    let (high, low, close) = extract_ohlc_input("impulse_macd", req.data)?;
12482    let kernel = req.kernel.to_non_batch();
12483    collect_f64(
12484        "impulse_macd",
12485        output_id,
12486        req.combos,
12487        close.len(),
12488        |params| {
12489            let length_ma = get_usize_param("impulse_macd", params, "length_ma", 34)?;
12490            let length_signal = get_usize_param("impulse_macd", params, "length_signal", 9)?;
12491            let input = ImpulseMacdInput::from_slices(
12492                high,
12493                low,
12494                close,
12495                ImpulseMacdParams {
12496                    length_ma: Some(length_ma),
12497                    length_signal: Some(length_signal),
12498                },
12499            );
12500            let out = impulse_macd_with_kernel(&input, kernel).map_err(|e| {
12501                IndicatorDispatchError::ComputeFailed {
12502                    indicator: "impulse_macd".to_string(),
12503                    details: e.to_string(),
12504                }
12505            })?;
12506            if output_id.eq_ignore_ascii_case("impulse_macd")
12507                || output_id.eq_ignore_ascii_case("value")
12508            {
12509                return Ok(out.impulse_macd);
12510            }
12511            if output_id.eq_ignore_ascii_case("impulse_histo")
12512                || output_id.eq_ignore_ascii_case("histogram")
12513                || output_id.eq_ignore_ascii_case("hist")
12514            {
12515                return Ok(out.impulse_histo);
12516            }
12517            if output_id.eq_ignore_ascii_case("signal") {
12518                return Ok(out.signal);
12519            }
12520            Err(IndicatorDispatchError::UnknownOutput {
12521                indicator: "impulse_macd".to_string(),
12522                output: output_id.to_string(),
12523            })
12524        },
12525    )
12526}
12527
12528fn compute_keltner_channel_width_oscillator_batch(
12529    req: IndicatorBatchRequest<'_>,
12530    output_id: &str,
12531) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12532    let (high, low, close) = extract_ohlc_input("keltner_channel_width_oscillator", req.data)?;
12533    let kernel = req.kernel.to_non_batch();
12534    collect_f64(
12535        "keltner_channel_width_oscillator",
12536        output_id,
12537        req.combos,
12538        close.len(),
12539        |params| {
12540            let source = get_enum_param(
12541                "keltner_channel_width_oscillator",
12542                params,
12543                "source",
12544                "close",
12545            )?;
12546            let length = get_usize_param("keltner_channel_width_oscillator", params, "length", 20)?;
12547            let multiplier = get_f64_param(
12548                "keltner_channel_width_oscillator",
12549                params,
12550                "multiplier",
12551                2.0,
12552            )?;
12553            let use_exponential = get_bool_param(
12554                "keltner_channel_width_oscillator",
12555                params,
12556                "use_exponential",
12557                true,
12558            )?;
12559            let bands_style = get_enum_param(
12560                "keltner_channel_width_oscillator",
12561                params,
12562                "bands_style",
12563                "Average True Range",
12564            )?;
12565            let atr_length =
12566                get_usize_param("keltner_channel_width_oscillator", params, "atr_length", 10)?;
12567            let src = match req.data {
12568                IndicatorDataRef::Candles { candles, .. } => source_type(candles, &source),
12569                IndicatorDataRef::Ohlc {
12570                    open,
12571                    high,
12572                    low,
12573                    close,
12574                } => {
12575                    ensure_same_len_4(
12576                        "keltner_channel_width_oscillator",
12577                        open.len(),
12578                        high.len(),
12579                        low.len(),
12580                        close.len(),
12581                    )?;
12582                    match source.to_ascii_lowercase().as_str() {
12583                        "open" => open,
12584                        "high" => high,
12585                        "low" => low,
12586                        _ => close,
12587                    }
12588                }
12589                IndicatorDataRef::Ohlcv {
12590                    open,
12591                    high,
12592                    low,
12593                    close,
12594                    volume,
12595                } => {
12596                    ensure_same_len_5(
12597                        "keltner_channel_width_oscillator",
12598                        open.len(),
12599                        high.len(),
12600                        low.len(),
12601                        close.len(),
12602                        volume.len(),
12603                    )?;
12604                    match source.to_ascii_lowercase().as_str() {
12605                        "open" => open,
12606                        "high" => high,
12607                        "low" => low,
12608                        _ => close,
12609                    }
12610                }
12611                _ => close,
12612            };
12613            let input = KeltnerChannelWidthOscillatorInput::from_slices(
12614                high,
12615                low,
12616                close,
12617                src,
12618                KeltnerChannelWidthOscillatorParams {
12619                    length: Some(length),
12620                    multiplier: Some(multiplier),
12621                    use_exponential: Some(use_exponential),
12622                    bands_style: Some(bands_style),
12623                    atr_length: Some(atr_length),
12624                },
12625            );
12626            let out =
12627                keltner_channel_width_oscillator_with_kernel(&input, kernel).map_err(|e| {
12628                    IndicatorDispatchError::ComputeFailed {
12629                        indicator: "keltner_channel_width_oscillator".to_string(),
12630                        details: e.to_string(),
12631                    }
12632                })?;
12633            if output_id.eq_ignore_ascii_case("kbw") || output_id.eq_ignore_ascii_case("value") {
12634                return Ok(out.kbw);
12635            }
12636            if output_id.eq_ignore_ascii_case("kbw_sma") {
12637                return Ok(out.kbw_sma);
12638            }
12639            Err(IndicatorDispatchError::UnknownOutput {
12640                indicator: "keltner_channel_width_oscillator".to_string(),
12641                output: output_id.to_string(),
12642            })
12643        },
12644    )
12645}
12646
12647fn compute_leavitt_convolution_acceleration_batch(
12648    req: IndicatorBatchRequest<'_>,
12649    output_id: &str,
12650) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12651    let data_len = match req.data {
12652        IndicatorDataRef::Candles { candles, source } => {
12653            source_type(candles, source.unwrap_or("close")).len()
12654        }
12655        IndicatorDataRef::Slice { values } => values.len(),
12656        _ => {
12657            return Err(IndicatorDispatchError::MissingRequiredInput {
12658                indicator: "leavitt_convolution_acceleration".to_string(),
12659                input: IndicatorInputKind::Slice,
12660            })
12661        }
12662    };
12663    let kernel = req.kernel.to_non_batch();
12664    collect_f64(
12665        "leavitt_convolution_acceleration",
12666        output_id,
12667        req.combos,
12668        data_len,
12669        |params| {
12670            let source = get_enum_param(
12671                "leavitt_convolution_acceleration",
12672                params,
12673                "source",
12674                "close",
12675            )?;
12676            let length = get_usize_param("leavitt_convolution_acceleration", params, "length", 70)?;
12677            let norm_length = get_usize_param(
12678                "leavitt_convolution_acceleration",
12679                params,
12680                "norm_length",
12681                150,
12682            )?;
12683            let use_norm_hyperbolic = get_bool_param(
12684                "leavitt_convolution_acceleration",
12685                params,
12686                "use_norm_hyperbolic",
12687                true,
12688            )?;
12689            let input = match req.data {
12690                IndicatorDataRef::Candles { candles, .. } => {
12691                    LeavittConvolutionAccelerationInput::from_candles(
12692                        candles,
12693                        &source,
12694                        LeavittConvolutionAccelerationParams {
12695                            length: Some(length),
12696                            norm_length: Some(norm_length),
12697                            use_norm_hyperbolic: Some(use_norm_hyperbolic),
12698                        },
12699                    )
12700                }
12701                IndicatorDataRef::Slice { values } => {
12702                    LeavittConvolutionAccelerationInput::from_slice(
12703                        values,
12704                        LeavittConvolutionAccelerationParams {
12705                            length: Some(length),
12706                            norm_length: Some(norm_length),
12707                            use_norm_hyperbolic: Some(use_norm_hyperbolic),
12708                        },
12709                    )
12710                }
12711                _ => unreachable!(),
12712            };
12713            let out =
12714                leavitt_convolution_acceleration_with_kernel(&input, kernel).map_err(|e| {
12715                    IndicatorDispatchError::ComputeFailed {
12716                        indicator: "leavitt_convolution_acceleration".to_string(),
12717                        details: e.to_string(),
12718                    }
12719                })?;
12720            if output_id.eq_ignore_ascii_case("conv_acceleration")
12721                || output_id.eq_ignore_ascii_case("value")
12722            {
12723                return Ok(out.conv_acceleration);
12724            }
12725            if output_id.eq_ignore_ascii_case("signal") {
12726                return Ok(out.signal);
12727            }
12728            Err(IndicatorDispatchError::UnknownOutput {
12729                indicator: "leavitt_convolution_acceleration".to_string(),
12730                output: output_id.to_string(),
12731            })
12732        },
12733    )
12734}
12735
12736fn compute_squeeze_index_batch(
12737    req: IndicatorBatchRequest<'_>,
12738    output_id: &str,
12739) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12740    expect_value_output("squeeze_index", output_id)?;
12741    let data = extract_slice_input("squeeze_index", req.data, "close")?;
12742    let kernel = req.kernel.to_non_batch();
12743    collect_f64(
12744        "squeeze_index",
12745        output_id,
12746        req.combos,
12747        data.len(),
12748        |params| {
12749            let conv = get_f64_param("squeeze_index", params, "conv", 50.0)?;
12750            let length = get_usize_param("squeeze_index", params, "length", 20)?;
12751            let input = SqueezeIndexInput::from_slice(
12752                data,
12753                SqueezeIndexParams {
12754                    conv: Some(conv),
12755                    length: Some(length),
12756                },
12757            );
12758            let out = squeeze_index_with_kernel(&input, kernel).map_err(|e| {
12759                IndicatorDispatchError::ComputeFailed {
12760                    indicator: "squeeze_index".to_string(),
12761                    details: e.to_string(),
12762                }
12763            })?;
12764            Ok(out.values)
12765        },
12766    )
12767}
12768
12769fn compute_stochastic_distance_batch(
12770    req: IndicatorBatchRequest<'_>,
12771    output_id: &str,
12772) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12773    if !output_id.eq_ignore_ascii_case("oscillator") && !output_id.eq_ignore_ascii_case("signal") {
12774        return Err(IndicatorDispatchError::UnknownOutput {
12775            indicator: "stochastic_distance".to_string(),
12776            output: output_id.to_string(),
12777        });
12778    }
12779    let data = extract_slice_input("stochastic_distance", req.data, "close")?;
12780    let kernel = req.kernel.to_non_batch();
12781    collect_f64(
12782        "stochastic_distance",
12783        output_id,
12784        req.combos,
12785        data.len(),
12786        |params| {
12787            let lookback_length =
12788                get_usize_param("stochastic_distance", params, "lookback_length", 200)?;
12789            let length1 = get_usize_param("stochastic_distance", params, "length1", 12)?;
12790            let length2 = get_usize_param("stochastic_distance", params, "length2", 3)?;
12791            let ob_level = get_i32_param("stochastic_distance", params, "ob_level", 40)?;
12792            let os_level = get_i32_param("stochastic_distance", params, "os_level", -40)?;
12793            let input = StochasticDistanceInput::from_slice(
12794                data,
12795                StochasticDistanceParams {
12796                    lookback_length: Some(lookback_length),
12797                    length1: Some(length1),
12798                    length2: Some(length2),
12799                    ob_level: Some(ob_level),
12800                    os_level: Some(os_level),
12801                },
12802            );
12803            let out = stochastic_distance_with_kernel(&input, kernel).map_err(|e| {
12804                IndicatorDispatchError::ComputeFailed {
12805                    indicator: "stochastic_distance".to_string(),
12806                    details: e.to_string(),
12807                }
12808            })?;
12809            match output_id {
12810                "oscillator" => Ok(out.oscillator),
12811                "signal" => Ok(out.signal),
12812                _ => Err(IndicatorDispatchError::UnknownOutput {
12813                    indicator: "stochastic_distance".to_string(),
12814                    output: output_id.to_string(),
12815                }),
12816            }
12817        },
12818    )
12819}
12820
12821fn compute_vertical_horizontal_filter_batch(
12822    req: IndicatorBatchRequest<'_>,
12823    output_id: &str,
12824) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12825    expect_value_output("vertical_horizontal_filter", output_id)?;
12826    let data = extract_slice_input("vertical_horizontal_filter", req.data, "close")?;
12827    let kernel = req.kernel.to_non_batch();
12828    collect_f64(
12829        "vertical_horizontal_filter",
12830        output_id,
12831        req.combos,
12832        data.len(),
12833        |params| {
12834            let length = get_usize_param("vertical_horizontal_filter", params, "length", 28)?;
12835            let input = VerticalHorizontalFilterInput::from_slice(
12836                data,
12837                VerticalHorizontalFilterParams {
12838                    length: Some(length),
12839                },
12840            );
12841            let out = vertical_horizontal_filter_with_kernel(&input, kernel).map_err(|e| {
12842                IndicatorDispatchError::ComputeFailed {
12843                    indicator: "vertical_horizontal_filter".to_string(),
12844                    details: e.to_string(),
12845                }
12846            })?;
12847            Ok(out.values)
12848        },
12849    )
12850}
12851
12852fn compute_intraday_momentum_index_batch(
12853    req: IndicatorBatchRequest<'_>,
12854    output_id: &str,
12855) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12856    let (open, _high, _low, close) = extract_ohlc_full_input("intraday_momentum_index", req.data)?;
12857    let kernel = req.kernel.to_non_batch();
12858    collect_f64(
12859        "intraday_momentum_index",
12860        output_id,
12861        req.combos,
12862        open.len(),
12863        |params| {
12864            let length = get_usize_param("intraday_momentum_index", params, "length", 14)?;
12865            let length_ma = get_usize_param("intraday_momentum_index", params, "length_ma", 6)?;
12866            let mult = get_f64_param("intraday_momentum_index", params, "mult", 2.0)?;
12867            let length_bb = get_usize_param("intraday_momentum_index", params, "length_bb", 20)?;
12868            let apply_smoothing =
12869                get_bool_param("intraday_momentum_index", params, "apply_smoothing", false)?;
12870            let low_band = get_usize_param("intraday_momentum_index", params, "low_band", 10)?;
12871            let input = IntradayMomentumIndexInput::from_slices(
12872                open,
12873                close,
12874                IntradayMomentumIndexParams {
12875                    length: Some(length),
12876                    length_ma: Some(length_ma),
12877                    mult: Some(mult),
12878                    length_bb: Some(length_bb),
12879                    apply_smoothing: Some(apply_smoothing),
12880                    low_band: Some(low_band),
12881                },
12882            );
12883            let out = intraday_momentum_index_with_kernel(&input, kernel).map_err(|e| {
12884                IndicatorDispatchError::ComputeFailed {
12885                    indicator: "intraday_momentum_index".to_string(),
12886                    details: e.to_string(),
12887                }
12888            })?;
12889            if output_id.eq_ignore_ascii_case("imi") || output_id.eq_ignore_ascii_case("value") {
12890                return Ok(out.imi);
12891            }
12892            if output_id.eq_ignore_ascii_case("upper_hit") {
12893                return Ok(out.upper_hit);
12894            }
12895            if output_id.eq_ignore_ascii_case("lower_hit") {
12896                return Ok(out.lower_hit);
12897            }
12898            if output_id.eq_ignore_ascii_case("signal") {
12899                return Ok(out.signal);
12900            }
12901            Err(IndicatorDispatchError::UnknownOutput {
12902                indicator: "intraday_momentum_index".to_string(),
12903                output: output_id.to_string(),
12904            })
12905        },
12906    )
12907}
12908
12909fn compute_vwap_zscore_with_signals_batch(
12910    req: IndicatorBatchRequest<'_>,
12911    output_id: &str,
12912) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12913    let (close, volume) =
12914        extract_close_volume_input("vwap_zscore_with_signals", req.data, "close")?;
12915    let kernel = req.kernel.to_non_batch();
12916    collect_f64(
12917        "vwap_zscore_with_signals",
12918        output_id,
12919        req.combos,
12920        close.len(),
12921        |params| {
12922            let length = get_usize_param("vwap_zscore_with_signals", params, "length", 20)?;
12923            let upper_bottom =
12924                get_f64_param("vwap_zscore_with_signals", params, "upper_bottom", 2.5)?;
12925            let lower_bottom =
12926                get_f64_param("vwap_zscore_with_signals", params, "lower_bottom", -2.5)?;
12927            let input = VwapZscoreWithSignalsInput::from_slices(
12928                close,
12929                volume,
12930                VwapZscoreWithSignalsParams {
12931                    length: Some(length),
12932                    upper_bottom: Some(upper_bottom),
12933                    lower_bottom: Some(lower_bottom),
12934                },
12935            );
12936            let out = vwap_zscore_with_signals_with_kernel(&input, kernel).map_err(|e| {
12937                IndicatorDispatchError::ComputeFailed {
12938                    indicator: "vwap_zscore_with_signals".to_string(),
12939                    details: e.to_string(),
12940                }
12941            })?;
12942            if output_id.eq_ignore_ascii_case("zvwap") || output_id.eq_ignore_ascii_case("value") {
12943                return Ok(out.zvwap);
12944            }
12945            if output_id.eq_ignore_ascii_case("support_signal") {
12946                return Ok(out.support_signal);
12947            }
12948            if output_id.eq_ignore_ascii_case("resistance_signal") {
12949                return Ok(out.resistance_signal);
12950            }
12951            Err(IndicatorDispatchError::UnknownOutput {
12952                indicator: "vwap_zscore_with_signals".to_string(),
12953                output: output_id.to_string(),
12954            })
12955        },
12956    )
12957}
12958
12959fn compute_hema_trend_levels_batch(
12960    req: IndicatorBatchRequest<'_>,
12961    output_id: &str,
12962) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
12963    let (open, high, low, close) = extract_ohlc_full_input("hema_trend_levels", req.data)?;
12964    let kernel = req.kernel.to_non_batch();
12965    collect_f64(
12966        "hema_trend_levels",
12967        output_id,
12968        req.combos,
12969        close.len(),
12970        |params| {
12971            let fast_length = get_usize_param("hema_trend_levels", params, "fast_length", 20)?;
12972            let slow_length = get_usize_param("hema_trend_levels", params, "slow_length", 40)?;
12973            let input = HemaTrendLevelsInput::from_slices(
12974                open,
12975                high,
12976                low,
12977                close,
12978                HemaTrendLevelsParams {
12979                    fast_length: Some(fast_length),
12980                    slow_length: Some(slow_length),
12981                },
12982            );
12983            let out = hema_trend_levels_with_kernel(&input, kernel).map_err(|e| {
12984                IndicatorDispatchError::ComputeFailed {
12985                    indicator: "hema_trend_levels".to_string(),
12986                    details: e.to_string(),
12987                }
12988            })?;
12989            if output_id.eq_ignore_ascii_case("fast_hema")
12990                || output_id.eq_ignore_ascii_case("value")
12991            {
12992                return Ok(out.fast_hema);
12993            }
12994            if output_id.eq_ignore_ascii_case("slow_hema") {
12995                return Ok(out.slow_hema);
12996            }
12997            if output_id.eq_ignore_ascii_case("trend_direction")
12998                || output_id.eq_ignore_ascii_case("trend")
12999            {
13000                return Ok(out.trend_direction);
13001            }
13002            if output_id.eq_ignore_ascii_case("bar_state") {
13003                return Ok(out.bar_state);
13004            }
13005            if output_id.eq_ignore_ascii_case("bullish_crossover")
13006                || output_id.eq_ignore_ascii_case("buy_signal")
13007                || output_id.eq_ignore_ascii_case("buy")
13008            {
13009                return Ok(out.bullish_crossover);
13010            }
13011            if output_id.eq_ignore_ascii_case("bearish_crossunder")
13012                || output_id.eq_ignore_ascii_case("sell_signal")
13013                || output_id.eq_ignore_ascii_case("sell")
13014            {
13015                return Ok(out.bearish_crossunder);
13016            }
13017            if output_id.eq_ignore_ascii_case("box_offset") {
13018                return Ok(out.box_offset);
13019            }
13020            if output_id.eq_ignore_ascii_case("bull_box_top") {
13021                return Ok(out.bull_box_top);
13022            }
13023            if output_id.eq_ignore_ascii_case("bull_box_bottom") {
13024                return Ok(out.bull_box_bottom);
13025            }
13026            if output_id.eq_ignore_ascii_case("bear_box_top") {
13027                return Ok(out.bear_box_top);
13028            }
13029            if output_id.eq_ignore_ascii_case("bear_box_bottom") {
13030                return Ok(out.bear_box_bottom);
13031            }
13032            if output_id.eq_ignore_ascii_case("bullish_test") {
13033                return Ok(out.bullish_test);
13034            }
13035            if output_id.eq_ignore_ascii_case("bearish_test") {
13036                return Ok(out.bearish_test);
13037            }
13038            if output_id.eq_ignore_ascii_case("bullish_test_level") {
13039                return Ok(out.bullish_test_level);
13040            }
13041            if output_id.eq_ignore_ascii_case("bearish_test_level") {
13042                return Ok(out.bearish_test_level);
13043            }
13044            Err(IndicatorDispatchError::UnknownOutput {
13045                indicator: "hema_trend_levels".to_string(),
13046                output: output_id.to_string(),
13047            })
13048        },
13049    )
13050}
13051
13052fn compute_macd_wave_signal_pro_batch(
13053    req: IndicatorBatchRequest<'_>,
13054    output_id: &str,
13055) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13056    let (open, high, low, close) = extract_ohlc_full_input("macd_wave_signal_pro", req.data)?;
13057    let kernel = req.kernel.to_non_batch();
13058    collect_f64(
13059        "macd_wave_signal_pro",
13060        output_id,
13061        req.combos,
13062        close.len(),
13063        |_params| {
13064            let input =
13065                MacdWaveSignalProInput::from_slices(open, high, low, close, Default::default());
13066            let out = macd_wave_signal_pro_with_kernel(&input, kernel).map_err(|e| {
13067                IndicatorDispatchError::ComputeFailed {
13068                    indicator: "macd_wave_signal_pro".to_string(),
13069                    details: e.to_string(),
13070                }
13071            })?;
13072            if output_id.eq_ignore_ascii_case("diff") || output_id.eq_ignore_ascii_case("value") {
13073                return Ok(out.diff);
13074            }
13075            if output_id.eq_ignore_ascii_case("dea") {
13076                return Ok(out.dea);
13077            }
13078            if output_id.eq_ignore_ascii_case("macd_histogram")
13079                || output_id.eq_ignore_ascii_case("macd")
13080                || output_id.eq_ignore_ascii_case("histogram")
13081                || output_id.eq_ignore_ascii_case("hist")
13082            {
13083                return Ok(out.macd_histogram);
13084            }
13085            if output_id.eq_ignore_ascii_case("line_convergence")
13086                || output_id.eq_ignore_ascii_case("line_conv")
13087            {
13088                return Ok(out.line_convergence);
13089            }
13090            if output_id.eq_ignore_ascii_case("buy_signal") || output_id.eq_ignore_ascii_case("buy")
13091            {
13092                return Ok(out.buy_signal);
13093            }
13094            if output_id.eq_ignore_ascii_case("sell_signal")
13095                || output_id.eq_ignore_ascii_case("sell")
13096            {
13097                return Ok(out.sell_signal);
13098            }
13099            Err(IndicatorDispatchError::UnknownOutput {
13100                indicator: "macd_wave_signal_pro".to_string(),
13101                output: output_id.to_string(),
13102            })
13103        },
13104    )
13105}
13106
13107fn compute_demand_index_batch(
13108    req: IndicatorBatchRequest<'_>,
13109    output_id: &str,
13110) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13111    let (high, low, close, volume) = extract_hlcv_input("demand_index", req.data)?;
13112    let kernel = req.kernel.to_non_batch();
13113    collect_f64(
13114        "demand_index",
13115        output_id,
13116        req.combos,
13117        high.len(),
13118        |params| {
13119            let len_bs = get_usize_param("demand_index", params, "len_bs", 19)?;
13120            let len_bs_ma = get_usize_param("demand_index", params, "len_bs_ma", 19)?;
13121            let len_di_ma = get_usize_param("demand_index", params, "len_di_ma", 19)?;
13122            let ma_type = get_enum_param("demand_index", params, "ma_type", "ema")?;
13123            let input = DemandIndexInput::from_slices(
13124                high,
13125                low,
13126                close,
13127                volume,
13128                DemandIndexParams {
13129                    len_bs: Some(len_bs),
13130                    len_bs_ma: Some(len_bs_ma),
13131                    len_di_ma: Some(len_di_ma),
13132                    ma_type: Some(ma_type),
13133                },
13134            );
13135            let out = demand_index_with_kernel(&input, kernel).map_err(|e| {
13136                IndicatorDispatchError::ComputeFailed {
13137                    indicator: "demand_index".to_string(),
13138                    details: e.to_string(),
13139                }
13140            })?;
13141            if output_id.eq_ignore_ascii_case("demand_index")
13142                || output_id.eq_ignore_ascii_case("value")
13143            {
13144                return Ok(out.demand_index);
13145            }
13146            if output_id.eq_ignore_ascii_case("signal") {
13147                return Ok(out.signal);
13148            }
13149            Err(IndicatorDispatchError::UnknownOutput {
13150                indicator: "demand_index".to_string(),
13151                output: output_id.to_string(),
13152            })
13153        },
13154    )
13155}
13156
13157fn compute_kase_peak_oscillator_with_divergences_batch(
13158    req: IndicatorBatchRequest<'_>,
13159    output_id: &str,
13160) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13161    let (high, low, close) = extract_ohlc_input("kase_peak_oscillator_with_divergences", req.data)?;
13162    let kernel = req.kernel.to_non_batch();
13163    collect_f64(
13164        "kase_peak_oscillator_with_divergences",
13165        output_id,
13166        req.combos,
13167        close.len(),
13168        |params| {
13169            let deviations = get_f64_param(
13170                "kase_peak_oscillator_with_divergences",
13171                params,
13172                "deviations",
13173                2.0,
13174            )?;
13175            let short_cycle = get_usize_param(
13176                "kase_peak_oscillator_with_divergences",
13177                params,
13178                "short_cycle",
13179                8,
13180            )?;
13181            let long_cycle = get_usize_param(
13182                "kase_peak_oscillator_with_divergences",
13183                params,
13184                "long_cycle",
13185                65,
13186            )?;
13187            let sensitivity = get_f64_param(
13188                "kase_peak_oscillator_with_divergences",
13189                params,
13190                "sensitivity",
13191                40.0,
13192            )?;
13193            let all_peaks_mode = get_bool_param(
13194                "kase_peak_oscillator_with_divergences",
13195                params,
13196                "all_peaks_mode",
13197                true,
13198            )?;
13199            let lb_r = get_usize_param("kase_peak_oscillator_with_divergences", params, "lb_r", 5)?;
13200            let lb_l = get_usize_param("kase_peak_oscillator_with_divergences", params, "lb_l", 5)?;
13201            let range_upper = get_usize_param(
13202                "kase_peak_oscillator_with_divergences",
13203                params,
13204                "range_upper",
13205                60,
13206            )?;
13207            let range_lower = get_usize_param(
13208                "kase_peak_oscillator_with_divergences",
13209                params,
13210                "range_lower",
13211                5,
13212            )?;
13213            let plot_bull = get_bool_param(
13214                "kase_peak_oscillator_with_divergences",
13215                params,
13216                "plot_bull",
13217                true,
13218            )?;
13219            let plot_hidden_bull = get_bool_param(
13220                "kase_peak_oscillator_with_divergences",
13221                params,
13222                "plot_hidden_bull",
13223                false,
13224            )?;
13225            let plot_bear = get_bool_param(
13226                "kase_peak_oscillator_with_divergences",
13227                params,
13228                "plot_bear",
13229                true,
13230            )?;
13231            let plot_hidden_bear = get_bool_param(
13232                "kase_peak_oscillator_with_divergences",
13233                params,
13234                "plot_hidden_bear",
13235                false,
13236            )?;
13237            let input = KasePeakOscillatorWithDivergencesInput::from_slices(
13238                high,
13239                low,
13240                close,
13241                KasePeakOscillatorWithDivergencesParams {
13242                    deviations: Some(deviations),
13243                    short_cycle: Some(short_cycle),
13244                    long_cycle: Some(long_cycle),
13245                    sensitivity: Some(sensitivity),
13246                    all_peaks_mode: Some(all_peaks_mode),
13247                    lb_r: Some(lb_r),
13248                    lb_l: Some(lb_l),
13249                    range_upper: Some(range_upper),
13250                    range_lower: Some(range_lower),
13251                    plot_bull: Some(plot_bull),
13252                    plot_hidden_bull: Some(plot_hidden_bull),
13253                    plot_bear: Some(plot_bear),
13254                    plot_hidden_bear: Some(plot_hidden_bear),
13255                },
13256            );
13257            let out =
13258                kase_peak_oscillator_with_divergences_with_kernel(&input, kernel).map_err(|e| {
13259                    IndicatorDispatchError::ComputeFailed {
13260                        indicator: "kase_peak_oscillator_with_divergences".to_string(),
13261                        details: e.to_string(),
13262                    }
13263                })?;
13264            if output_id.eq_ignore_ascii_case("oscillator")
13265                || output_id.eq_ignore_ascii_case("value")
13266            {
13267                return Ok(out.oscillator);
13268            }
13269            if output_id.eq_ignore_ascii_case("hist") || output_id.eq_ignore_ascii_case("histogram")
13270            {
13271                return Ok(out.histogram);
13272            }
13273            if output_id.eq_ignore_ascii_case("max_peak_value") {
13274                return Ok(out.max_peak_value);
13275            }
13276            if output_id.eq_ignore_ascii_case("min_peak_value") {
13277                return Ok(out.min_peak_value);
13278            }
13279            if output_id.eq_ignore_ascii_case("market_extreme") {
13280                return Ok(out.market_extreme);
13281            }
13282            if output_id.eq_ignore_ascii_case("regular_bullish") {
13283                return Ok(out.regular_bullish);
13284            }
13285            if output_id.eq_ignore_ascii_case("hidden_bullish") {
13286                return Ok(out.hidden_bullish);
13287            }
13288            if output_id.eq_ignore_ascii_case("regular_bearish") {
13289                return Ok(out.regular_bearish);
13290            }
13291            if output_id.eq_ignore_ascii_case("hidden_bearish") {
13292                return Ok(out.hidden_bearish);
13293            }
13294            if output_id.eq_ignore_ascii_case("go_long") {
13295                return Ok(out.go_long);
13296            }
13297            if output_id.eq_ignore_ascii_case("go_short") {
13298                return Ok(out.go_short);
13299            }
13300            Err(IndicatorDispatchError::UnknownOutput {
13301                indicator: "kase_peak_oscillator_with_divergences".to_string(),
13302                output: output_id.to_string(),
13303            })
13304        },
13305    )
13306}
13307
13308fn compute_gopalakrishnan_range_index_batch(
13309    req: IndicatorBatchRequest<'_>,
13310    output_id: &str,
13311) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13312    expect_value_output("gopalakrishnan_range_index", output_id)?;
13313    let (high, low) = extract_high_low_input("gopalakrishnan_range_index", req.data)?;
13314    let kernel = req.kernel.to_non_batch();
13315    collect_f64(
13316        "gopalakrishnan_range_index",
13317        output_id,
13318        req.combos,
13319        high.len(),
13320        |params| {
13321            let length = get_usize_param("gopalakrishnan_range_index", params, "length", 5)?;
13322            let input = GopalakrishnanRangeIndexInput::from_slices(
13323                high,
13324                low,
13325                GopalakrishnanRangeIndexParams {
13326                    length: Some(length),
13327                },
13328            );
13329            let out = gopalakrishnan_range_index_with_kernel(&input, kernel).map_err(|e| {
13330                IndicatorDispatchError::ComputeFailed {
13331                    indicator: "gopalakrishnan_range_index".to_string(),
13332                    details: e.to_string(),
13333                }
13334            })?;
13335            Ok(out.values)
13336        },
13337    )
13338}
13339
13340fn compute_acosc_batch(
13341    req: IndicatorBatchRequest<'_>,
13342    output_id: &str,
13343) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13344    let (high, low) = extract_high_low_input("acosc", req.data)?;
13345    let kernel = req.kernel.to_non_batch();
13346    collect_f64("acosc", output_id, req.combos, high.len(), |_params| {
13347        let input = AcoscInput::from_slices(high, low, AcoscParams::default());
13348        let out = acosc_with_kernel(&input, kernel).map_err(|e| {
13349            IndicatorDispatchError::ComputeFailed {
13350                indicator: "acosc".to_string(),
13351                details: e.to_string(),
13352            }
13353        })?;
13354        if output_id.eq_ignore_ascii_case("osc") || output_id.eq_ignore_ascii_case("value") {
13355            return Ok(out.osc);
13356        }
13357        if output_id.eq_ignore_ascii_case("change") {
13358            return Ok(out.change);
13359        }
13360        Err(IndicatorDispatchError::UnknownOutput {
13361            indicator: "acosc".to_string(),
13362            output: output_id.to_string(),
13363        })
13364    })
13365}
13366
13367fn compute_alligator_batch(
13368    req: IndicatorBatchRequest<'_>,
13369    output_id: &str,
13370) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13371    let data = extract_slice_input("alligator", req.data, "hl2")?;
13372    let kernel = req.kernel.to_non_batch();
13373    collect_f64("alligator", output_id, req.combos, data.len(), |params| {
13374        let jaw_period = get_usize_param("alligator", params, "jaw_period", 13)?;
13375        let jaw_offset = get_usize_param("alligator", params, "jaw_offset", 8)?;
13376        let teeth_period = get_usize_param("alligator", params, "teeth_period", 8)?;
13377        let teeth_offset = get_usize_param("alligator", params, "teeth_offset", 5)?;
13378        let lips_period = get_usize_param("alligator", params, "lips_period", 5)?;
13379        let lips_offset = get_usize_param("alligator", params, "lips_offset", 3)?;
13380        let input = AlligatorInput::from_slice(
13381            data,
13382            AlligatorParams {
13383                jaw_period: Some(jaw_period),
13384                jaw_offset: Some(jaw_offset),
13385                teeth_period: Some(teeth_period),
13386                teeth_offset: Some(teeth_offset),
13387                lips_period: Some(lips_period),
13388                lips_offset: Some(lips_offset),
13389            },
13390        );
13391        let out = alligator_with_kernel(&input, kernel).map_err(|e| {
13392            IndicatorDispatchError::ComputeFailed {
13393                indicator: "alligator".to_string(),
13394                details: e.to_string(),
13395            }
13396        })?;
13397        if output_id.eq_ignore_ascii_case("jaw") || output_id.eq_ignore_ascii_case("value") {
13398            return Ok(out.jaw);
13399        }
13400        if output_id.eq_ignore_ascii_case("teeth") {
13401            return Ok(out.teeth);
13402        }
13403        if output_id.eq_ignore_ascii_case("lips") {
13404            return Ok(out.lips);
13405        }
13406        Err(IndicatorDispatchError::UnknownOutput {
13407            indicator: "alligator".to_string(),
13408            output: output_id.to_string(),
13409        })
13410    })
13411}
13412
13413fn compute_alphatrend_batch(
13414    req: IndicatorBatchRequest<'_>,
13415    output_id: &str,
13416) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13417    let (open, high, low, close, volume) = extract_ohlcv_full_input("alphatrend", req.data)?;
13418    let kernel = req.kernel.to_non_batch();
13419    collect_f64("alphatrend", output_id, req.combos, close.len(), |params| {
13420        let coeff = get_f64_param("alphatrend", params, "coeff", 1.0)?;
13421        let period = get_usize_param("alphatrend", params, "period", 14)?;
13422        let no_volume = get_bool_param("alphatrend", params, "no_volume", false)?;
13423        let input = AlphaTrendInput::from_slices(
13424            open,
13425            high,
13426            low,
13427            close,
13428            volume,
13429            AlphaTrendParams {
13430                coeff: Some(coeff),
13431                period: Some(period),
13432                no_volume: Some(no_volume),
13433            },
13434        );
13435        let out = alphatrend_with_kernel(&input, kernel).map_err(|e| {
13436            IndicatorDispatchError::ComputeFailed {
13437                indicator: "alphatrend".to_string(),
13438                details: e.to_string(),
13439            }
13440        })?;
13441        if output_id.eq_ignore_ascii_case("k1") || output_id.eq_ignore_ascii_case("value") {
13442            return Ok(out.k1);
13443        }
13444        if output_id.eq_ignore_ascii_case("k2") {
13445            return Ok(out.k2);
13446        }
13447        Err(IndicatorDispatchError::UnknownOutput {
13448            indicator: "alphatrend".to_string(),
13449            output: output_id.to_string(),
13450        })
13451    })
13452}
13453
13454fn compute_aso_batch(
13455    req: IndicatorBatchRequest<'_>,
13456    output_id: &str,
13457) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13458    let (open, high, low, close) = match req.data {
13459        IndicatorDataRef::Candles { candles, source } => (
13460            candles.open.as_slice(),
13461            candles.high.as_slice(),
13462            candles.low.as_slice(),
13463            source_type(candles, source.unwrap_or("close")),
13464        ),
13465        IndicatorDataRef::Ohlc {
13466            open,
13467            high,
13468            low,
13469            close,
13470        } => {
13471            ensure_same_len_4("aso", open.len(), high.len(), low.len(), close.len())?;
13472            (open, high, low, close)
13473        }
13474        IndicatorDataRef::Ohlcv {
13475            open,
13476            high,
13477            low,
13478            close,
13479            volume,
13480        } => {
13481            ensure_same_len_5(
13482                "aso",
13483                open.len(),
13484                high.len(),
13485                low.len(),
13486                close.len(),
13487                volume.len(),
13488            )?;
13489            (open, high, low, close)
13490        }
13491        _ => {
13492            return Err(IndicatorDispatchError::MissingRequiredInput {
13493                indicator: "aso".to_string(),
13494                input: IndicatorInputKind::Ohlc,
13495            });
13496        }
13497    };
13498    let kernel = req.kernel.to_non_batch();
13499    collect_f64("aso", output_id, req.combos, close.len(), |params| {
13500        let period = get_usize_param("aso", params, "period", 10)?;
13501        let mode = get_usize_param("aso", params, "mode", 0)?;
13502        let input = AsoInput::from_slices(
13503            open,
13504            high,
13505            low,
13506            close,
13507            AsoParams {
13508                period: Some(period),
13509                mode: Some(mode),
13510            },
13511        );
13512        let out =
13513            aso_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
13514                indicator: "aso".to_string(),
13515                details: e.to_string(),
13516            })?;
13517        if output_id.eq_ignore_ascii_case("bulls") || output_id.eq_ignore_ascii_case("value") {
13518            return Ok(out.bulls);
13519        }
13520        if output_id.eq_ignore_ascii_case("bears") {
13521            return Ok(out.bears);
13522        }
13523        Err(IndicatorDispatchError::UnknownOutput {
13524            indicator: "aso".to_string(),
13525            output: output_id.to_string(),
13526        })
13527    })
13528}
13529
13530fn compute_avsl_batch(
13531    req: IndicatorBatchRequest<'_>,
13532    output_id: &str,
13533) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13534    expect_value_output("avsl", output_id)?;
13535    let (_high, low, close, volume) = extract_hlcv_input("avsl", req.data)?;
13536    let kernel = req.kernel.to_non_batch();
13537    collect_f64("avsl", output_id, req.combos, close.len(), |params| {
13538        let fast_period = get_usize_param("avsl", params, "fast_period", 12)?;
13539        let slow_period = get_usize_param("avsl", params, "slow_period", 26)?;
13540        let multiplier = get_f64_param("avsl", params, "multiplier", 2.0)?;
13541        let input = AvslInput::from_slices(
13542            close,
13543            low,
13544            volume,
13545            AvslParams {
13546                fast_period: Some(fast_period),
13547                slow_period: Some(slow_period),
13548                multiplier: Some(multiplier),
13549            },
13550        );
13551        let out = avsl_with_kernel(&input, kernel).map_err(|e| {
13552            IndicatorDispatchError::ComputeFailed {
13553                indicator: "avsl".to_string(),
13554                details: e.to_string(),
13555            }
13556        })?;
13557        Ok(out.values)
13558    })
13559}
13560
13561fn compute_bandpass_batch(
13562    req: IndicatorBatchRequest<'_>,
13563    output_id: &str,
13564) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13565    let data = extract_slice_input("bandpass", req.data, "close")?;
13566    let kernel = req.kernel.to_non_batch();
13567    collect_f64("bandpass", output_id, req.combos, data.len(), |params| {
13568        let period = get_usize_param("bandpass", params, "period", 20)?;
13569        let bandwidth = get_f64_param("bandpass", params, "bandwidth", 0.3)?;
13570        let input = BandPassInput::from_slice(
13571            data,
13572            BandPassParams {
13573                period: Some(period),
13574                bandwidth: Some(bandwidth),
13575            },
13576        );
13577        let out = bandpass_with_kernel(&input, kernel).map_err(|e| {
13578            IndicatorDispatchError::ComputeFailed {
13579                indicator: "bandpass".to_string(),
13580                details: e.to_string(),
13581            }
13582        })?;
13583        if output_id.eq_ignore_ascii_case("bp") || output_id.eq_ignore_ascii_case("value") {
13584            return Ok(out.bp);
13585        }
13586        if output_id.eq_ignore_ascii_case("bp_normalized")
13587            || output_id.eq_ignore_ascii_case("normalized")
13588        {
13589            return Ok(out.bp_normalized);
13590        }
13591        if output_id.eq_ignore_ascii_case("signal") {
13592            return Ok(out.signal);
13593        }
13594        if output_id.eq_ignore_ascii_case("trigger") {
13595            return Ok(out.trigger);
13596        }
13597        Err(IndicatorDispatchError::UnknownOutput {
13598            indicator: "bandpass".to_string(),
13599            output: output_id.to_string(),
13600        })
13601    })
13602}
13603
13604fn compute_chande_batch(
13605    req: IndicatorBatchRequest<'_>,
13606    output_id: &str,
13607) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13608    expect_value_output("chande", output_id)?;
13609    let (high, low, close) = extract_ohlc_input("chande", req.data)?;
13610    let kernel = req.kernel.to_non_batch();
13611    collect_f64("chande", output_id, req.combos, close.len(), |params| {
13612        let period = get_usize_param("chande", params, "period", 22)?;
13613        let mult = get_f64_param("chande", params, "mult", 3.0)?;
13614        let direction = get_enum_param("chande", params, "direction", "long")?;
13615        let input = ChandeInput::from_slices(
13616            high,
13617            low,
13618            close,
13619            ChandeParams {
13620                period: Some(period),
13621                mult: Some(mult),
13622                direction: Some(direction.to_string()),
13623            },
13624        );
13625        let out = chande_with_kernel(&input, kernel).map_err(|e| {
13626            IndicatorDispatchError::ComputeFailed {
13627                indicator: "chande".to_string(),
13628                details: e.to_string(),
13629            }
13630        })?;
13631        Ok(out.values)
13632    })
13633}
13634
13635fn compute_chandelier_exit_batch(
13636    req: IndicatorBatchRequest<'_>,
13637    output_id: &str,
13638) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13639    let (high, low, close) = extract_ohlc_input("chandelier_exit", req.data)?;
13640    let kernel = req.kernel.to_non_batch();
13641    collect_f64(
13642        "chandelier_exit",
13643        output_id,
13644        req.combos,
13645        close.len(),
13646        |params| {
13647            let period = get_usize_param("chandelier_exit", params, "period", 22)?;
13648            let mult = get_f64_param("chandelier_exit", params, "mult", 3.0)?;
13649            let use_close = get_bool_param("chandelier_exit", params, "use_close", true)?;
13650            let input = ChandelierExitInput::from_slices(
13651                high,
13652                low,
13653                close,
13654                ChandelierExitParams {
13655                    period: Some(period),
13656                    mult: Some(mult),
13657                    use_close: Some(use_close),
13658                },
13659            );
13660            let out = chandelier_exit_with_kernel(&input, kernel).map_err(|e| {
13661                IndicatorDispatchError::ComputeFailed {
13662                    indicator: "chandelier_exit".to_string(),
13663                    details: e.to_string(),
13664                }
13665            })?;
13666            if output_id.eq_ignore_ascii_case("long_stop")
13667                || output_id.eq_ignore_ascii_case("value")
13668            {
13669                return Ok(out.long_stop);
13670            }
13671            if output_id.eq_ignore_ascii_case("short_stop") {
13672                return Ok(out.short_stop);
13673            }
13674            Err(IndicatorDispatchError::UnknownOutput {
13675                indicator: "chandelier_exit".to_string(),
13676                output: output_id.to_string(),
13677            })
13678        },
13679    )
13680}
13681
13682fn compute_cksp_batch(
13683    req: IndicatorBatchRequest<'_>,
13684    output_id: &str,
13685) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13686    let (high, low, close) = extract_ohlc_input("cksp", req.data)?;
13687    let kernel = req.kernel.to_non_batch();
13688    collect_f64("cksp", output_id, req.combos, close.len(), |params| {
13689        let p = get_usize_param("cksp", params, "p", 10)?;
13690        let x = get_f64_param("cksp", params, "x", 1.0)?;
13691        let q = get_usize_param("cksp", params, "q", 9)?;
13692        let input = CkspInput::from_slices(
13693            high,
13694            low,
13695            close,
13696            CkspParams {
13697                p: Some(p),
13698                x: Some(x),
13699                q: Some(q),
13700            },
13701        );
13702        let out = cksp_with_kernel(&input, kernel).map_err(|e| {
13703            IndicatorDispatchError::ComputeFailed {
13704                indicator: "cksp".to_string(),
13705                details: e.to_string(),
13706            }
13707        })?;
13708        if output_id.eq_ignore_ascii_case("long_values")
13709            || output_id.eq_ignore_ascii_case("long")
13710            || output_id.eq_ignore_ascii_case("value")
13711        {
13712            return Ok(out.long_values);
13713        }
13714        if output_id.eq_ignore_ascii_case("short_values") || output_id.eq_ignore_ascii_case("short")
13715        {
13716            return Ok(out.short_values);
13717        }
13718        Err(IndicatorDispatchError::UnknownOutput {
13719            indicator: "cksp".to_string(),
13720            output: output_id.to_string(),
13721        })
13722    })
13723}
13724
13725fn compute_correlation_cycle_batch(
13726    req: IndicatorBatchRequest<'_>,
13727    output_id: &str,
13728) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13729    let data = extract_slice_input("correlation_cycle", req.data, "close")?;
13730    let kernel = req.kernel.to_non_batch();
13731    collect_f64(
13732        "correlation_cycle",
13733        output_id,
13734        req.combos,
13735        data.len(),
13736        |params| {
13737            let period = get_usize_param("correlation_cycle", params, "period", 20)?;
13738            let threshold = get_f64_param("correlation_cycle", params, "threshold", 9.0)?;
13739            let input = CorrelationCycleInput::from_slice(
13740                data,
13741                CorrelationCycleParams {
13742                    period: Some(period),
13743                    threshold: Some(threshold),
13744                },
13745            );
13746            let out = correlation_cycle_with_kernel(&input, kernel).map_err(|e| {
13747                IndicatorDispatchError::ComputeFailed {
13748                    indicator: "correlation_cycle".to_string(),
13749                    details: e.to_string(),
13750                }
13751            })?;
13752            if output_id.eq_ignore_ascii_case("real") || output_id.eq_ignore_ascii_case("value") {
13753                return Ok(out.real);
13754            }
13755            if output_id.eq_ignore_ascii_case("imag") {
13756                return Ok(out.imag);
13757            }
13758            if output_id.eq_ignore_ascii_case("angle") {
13759                return Ok(out.angle);
13760            }
13761            if output_id.eq_ignore_ascii_case("state") {
13762                return Ok(out.state);
13763            }
13764            Err(IndicatorDispatchError::UnknownOutput {
13765                indicator: "correlation_cycle".to_string(),
13766                output: output_id.to_string(),
13767            })
13768        },
13769    )
13770}
13771
13772fn compute_damiani_volatmeter_batch(
13773    req: IndicatorBatchRequest<'_>,
13774    output_id: &str,
13775) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13776    let data = extract_slice_input("damiani_volatmeter", req.data, "close")?;
13777    let kernel = req.kernel.to_non_batch();
13778    collect_f64(
13779        "damiani_volatmeter",
13780        output_id,
13781        req.combos,
13782        data.len(),
13783        |params| {
13784            let vis_atr = get_usize_param("damiani_volatmeter", params, "vis_atr", 13)?;
13785            let vis_std = get_usize_param("damiani_volatmeter", params, "vis_std", 20)?;
13786            let sed_atr = get_usize_param("damiani_volatmeter", params, "sed_atr", 40)?;
13787            let sed_std = get_usize_param("damiani_volatmeter", params, "sed_std", 100)?;
13788            let threshold = get_f64_param("damiani_volatmeter", params, "threshold", 1.4)?;
13789            let input = DamianiVolatmeterInput::from_slice(
13790                data,
13791                DamianiVolatmeterParams {
13792                    vis_atr: Some(vis_atr),
13793                    vis_std: Some(vis_std),
13794                    sed_atr: Some(sed_atr),
13795                    sed_std: Some(sed_std),
13796                    threshold: Some(threshold),
13797                },
13798            );
13799            let out = damiani_volatmeter_with_kernel(&input, kernel).map_err(|e| {
13800                IndicatorDispatchError::ComputeFailed {
13801                    indicator: "damiani_volatmeter".to_string(),
13802                    details: e.to_string(),
13803                }
13804            })?;
13805            if output_id.eq_ignore_ascii_case("vol") || output_id.eq_ignore_ascii_case("value") {
13806                return Ok(out.vol);
13807            }
13808            if output_id.eq_ignore_ascii_case("anti") {
13809                return Ok(out.anti);
13810            }
13811            Err(IndicatorDispatchError::UnknownOutput {
13812                indicator: "damiani_volatmeter".to_string(),
13813                output: output_id.to_string(),
13814            })
13815        },
13816    )
13817}
13818
13819fn compute_dvdiqqe_batch(
13820    req: IndicatorBatchRequest<'_>,
13821    output_id: &str,
13822) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13823    let (open, high, low, close, volume) = match req.data {
13824        IndicatorDataRef::Candles { candles, .. } => (
13825            candles.open.as_slice(),
13826            candles.high.as_slice(),
13827            candles.low.as_slice(),
13828            candles.close.as_slice(),
13829            Some(candles.volume.as_slice()),
13830        ),
13831        IndicatorDataRef::Ohlcv {
13832            open,
13833            high,
13834            low,
13835            close,
13836            volume,
13837        } => {
13838            ensure_same_len_5(
13839                "dvdiqqe",
13840                open.len(),
13841                high.len(),
13842                low.len(),
13843                close.len(),
13844                volume.len(),
13845            )?;
13846            (open, high, low, close, Some(volume))
13847        }
13848        IndicatorDataRef::Ohlc {
13849            open,
13850            high,
13851            low,
13852            close,
13853        } => {
13854            ensure_same_len_4("dvdiqqe", open.len(), high.len(), low.len(), close.len())?;
13855            (open, high, low, close, None)
13856        }
13857        _ => {
13858            return Err(IndicatorDispatchError::MissingRequiredInput {
13859                indicator: "dvdiqqe".to_string(),
13860                input: IndicatorInputKind::Ohlc,
13861            })
13862        }
13863    };
13864    let kernel = req.kernel.to_non_batch();
13865    collect_f64("dvdiqqe", output_id, req.combos, close.len(), |params| {
13866        let period = get_usize_param("dvdiqqe", params, "period", 13)?;
13867        let smoothing_period = get_usize_param("dvdiqqe", params, "smoothing_period", 6)?;
13868        let fast_multiplier = get_f64_param("dvdiqqe", params, "fast_multiplier", 2.618)?;
13869        let slow_multiplier = get_f64_param("dvdiqqe", params, "slow_multiplier", 4.236)?;
13870        let volume_type = get_enum_param("dvdiqqe", params, "volume_type", "default")?;
13871        let center_type = get_enum_param("dvdiqqe", params, "center_type", "dynamic")?;
13872        let tick_size = get_f64_param("dvdiqqe", params, "tick_size", 0.01)?;
13873        let input = DvdiqqeInput::from_slices(
13874            open,
13875            high,
13876            low,
13877            close,
13878            volume,
13879            DvdiqqeParams {
13880                period: Some(period),
13881                smoothing_period: Some(smoothing_period),
13882                fast_multiplier: Some(fast_multiplier),
13883                slow_multiplier: Some(slow_multiplier),
13884                volume_type: Some(volume_type),
13885                center_type: Some(center_type),
13886                tick_size: Some(tick_size),
13887            },
13888        );
13889        let out = dvdiqqe_with_kernel(&input, kernel).map_err(|e| {
13890            IndicatorDispatchError::ComputeFailed {
13891                indicator: "dvdiqqe".to_string(),
13892                details: e.to_string(),
13893            }
13894        })?;
13895        if output_id.eq_ignore_ascii_case("dvdi") || output_id.eq_ignore_ascii_case("value") {
13896            return Ok(out.dvdi);
13897        }
13898        if output_id.eq_ignore_ascii_case("fast_tl") || output_id.eq_ignore_ascii_case("fast") {
13899            return Ok(out.fast_tl);
13900        }
13901        if output_id.eq_ignore_ascii_case("slow_tl") || output_id.eq_ignore_ascii_case("slow") {
13902            return Ok(out.slow_tl);
13903        }
13904        if output_id.eq_ignore_ascii_case("center_line") || output_id.eq_ignore_ascii_case("center")
13905        {
13906            return Ok(out.center_line);
13907        }
13908        Err(IndicatorDispatchError::UnknownOutput {
13909            indicator: "dvdiqqe".to_string(),
13910            output: output_id.to_string(),
13911        })
13912    })
13913}
13914
13915fn compute_emd_batch(
13916    req: IndicatorBatchRequest<'_>,
13917    output_id: &str,
13918) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13919    let (high, low, close, volume) = extract_hlcv_input("emd", req.data)?;
13920    let kernel = req.kernel.to_non_batch();
13921    collect_f64("emd", output_id, req.combos, close.len(), |params| {
13922        let period = get_usize_param("emd", params, "period", 20)?;
13923        let delta = get_f64_param("emd", params, "delta", 0.5)?;
13924        let fraction = get_f64_param("emd", params, "fraction", 0.1)?;
13925        let input = EmdInput::from_slices(
13926            high,
13927            low,
13928            close,
13929            volume,
13930            EmdParams {
13931                period: Some(period),
13932                delta: Some(delta),
13933                fraction: Some(fraction),
13934            },
13935        );
13936        let out =
13937            emd_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
13938                indicator: "emd".to_string(),
13939                details: e.to_string(),
13940            })?;
13941        if output_id.eq_ignore_ascii_case("upperband")
13942            || output_id.eq_ignore_ascii_case("upper")
13943            || output_id.eq_ignore_ascii_case("value")
13944        {
13945            return Ok(out.upperband);
13946        }
13947        if output_id.eq_ignore_ascii_case("middleband") || output_id.eq_ignore_ascii_case("middle")
13948        {
13949            return Ok(out.middleband);
13950        }
13951        if output_id.eq_ignore_ascii_case("lowerband") || output_id.eq_ignore_ascii_case("lower") {
13952            return Ok(out.lowerband);
13953        }
13954        Err(IndicatorDispatchError::UnknownOutput {
13955            indicator: "emd".to_string(),
13956            output: output_id.to_string(),
13957        })
13958    })
13959}
13960
13961fn compute_emd_trend_batch(
13962    req: IndicatorBatchRequest<'_>,
13963    output_id: &str,
13964) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
13965    let (open, high, low, close) = extract_ohlc_full_input("emd_trend", req.data)?;
13966    let kernel = req.kernel.to_non_batch();
13967    collect_f64("emd_trend", output_id, req.combos, close.len(), |params| {
13968        let source = get_enum_param("emd_trend", params, "source", "close")?;
13969        let avg_type = get_enum_param("emd_trend", params, "avg_type", "SMA")?;
13970        let length = get_usize_param("emd_trend", params, "length", 28)?;
13971        let mult = get_f64_param("emd_trend", params, "mult", 1.0)?;
13972        let input = EmdTrendInput::from_slices(
13973            open,
13974            high,
13975            low,
13976            close,
13977            EmdTrendParams {
13978                source: Some(source),
13979                avg_type: Some(avg_type),
13980                length: Some(length),
13981                mult: Some(mult),
13982            },
13983        );
13984        let out = emd_trend_with_kernel(&input, kernel).map_err(|e| {
13985            IndicatorDispatchError::ComputeFailed {
13986                indicator: "emd_trend".to_string(),
13987                details: e.to_string(),
13988            }
13989        })?;
13990        if output_id.eq_ignore_ascii_case("direction") {
13991            return Ok(out.direction);
13992        }
13993        if output_id.eq_ignore_ascii_case("average") || output_id.eq_ignore_ascii_case("value") {
13994            return Ok(out.average);
13995        }
13996        if output_id.eq_ignore_ascii_case("upper") {
13997            return Ok(out.upper);
13998        }
13999        if output_id.eq_ignore_ascii_case("lower") {
14000            return Ok(out.lower);
14001        }
14002        Err(IndicatorDispatchError::UnknownOutput {
14003            indicator: "emd_trend".to_string(),
14004            output: output_id.to_string(),
14005        })
14006    })
14007}
14008
14009fn compute_cyberpunk_value_trend_analyzer_batch(
14010    req: IndicatorBatchRequest<'_>,
14011    output_id: &str,
14012) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14013    let (open, high, low, close) =
14014        extract_ohlc_full_input("cyberpunk_value_trend_analyzer", req.data)?;
14015    let kernel = req.kernel.to_non_batch();
14016    collect_f64(
14017        "cyberpunk_value_trend_analyzer",
14018        output_id,
14019        req.combos,
14020        close.len(),
14021        |params| {
14022            let entry_level =
14023                get_usize_param("cyberpunk_value_trend_analyzer", params, "entry_level", 30)?;
14024            let exit_level =
14025                get_usize_param("cyberpunk_value_trend_analyzer", params, "exit_level", 75)?;
14026            let input = CyberpunkValueTrendAnalyzerInput::from_slices(
14027                open,
14028                high,
14029                low,
14030                close,
14031                CyberpunkValueTrendAnalyzerParams {
14032                    entry_level: Some(entry_level),
14033                    exit_level: Some(exit_level),
14034                },
14035            );
14036            let out = cyberpunk_value_trend_analyzer_with_kernel(&input, kernel).map_err(|e| {
14037                IndicatorDispatchError::ComputeFailed {
14038                    indicator: "cyberpunk_value_trend_analyzer".to_string(),
14039                    details: e.to_string(),
14040                }
14041            })?;
14042            if output_id.eq_ignore_ascii_case("value_trend")
14043                || output_id.eq_ignore_ascii_case("value")
14044            {
14045                return Ok(out.value_trend);
14046            }
14047            if output_id.eq_ignore_ascii_case("value_trend_lag")
14048                || output_id.eq_ignore_ascii_case("lag")
14049            {
14050                return Ok(out.value_trend_lag);
14051            }
14052            if output_id.eq_ignore_ascii_case("deviation_index") {
14053                return Ok(out.deviation_index);
14054            }
14055            if output_id.eq_ignore_ascii_case("overbought_signal")
14056                || output_id.eq_ignore_ascii_case("overbought")
14057            {
14058                return Ok(out.overbought_signal);
14059            }
14060            if output_id.eq_ignore_ascii_case("buy_signal") {
14061                return Ok(out.buy_signal);
14062            }
14063            if output_id.eq_ignore_ascii_case("sell_signal") {
14064                return Ok(out.sell_signal);
14065            }
14066            Err(IndicatorDispatchError::UnknownOutput {
14067                indicator: "cyberpunk_value_trend_analyzer".to_string(),
14068                output: output_id.to_string(),
14069            })
14070        },
14071    )
14072}
14073
14074fn compute_eri_batch(
14075    req: IndicatorBatchRequest<'_>,
14076    output_id: &str,
14077) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14078    let (high, low, source) = match req.data {
14079        IndicatorDataRef::Candles { candles, source } => (
14080            candles.high.as_slice(),
14081            candles.low.as_slice(),
14082            source_type(candles, source.unwrap_or("close")),
14083        ),
14084        IndicatorDataRef::Ohlc {
14085            open,
14086            high,
14087            low,
14088            close,
14089        } => {
14090            ensure_same_len_4("eri", open.len(), high.len(), low.len(), close.len())?;
14091            (high, low, close)
14092        }
14093        IndicatorDataRef::Ohlcv {
14094            open,
14095            high,
14096            low,
14097            close,
14098            volume,
14099        } => {
14100            ensure_same_len_5(
14101                "eri",
14102                open.len(),
14103                high.len(),
14104                low.len(),
14105                close.len(),
14106                volume.len(),
14107            )?;
14108            (high, low, close)
14109        }
14110        _ => {
14111            return Err(IndicatorDispatchError::MissingRequiredInput {
14112                indicator: "eri".to_string(),
14113                input: IndicatorInputKind::Ohlc,
14114            });
14115        }
14116    };
14117    let kernel = req.kernel.to_non_batch();
14118    collect_f64("eri", output_id, req.combos, source.len(), |params| {
14119        let period = get_usize_param("eri", params, "period", 13)?;
14120        let ma_type = get_enum_param("eri", params, "ma_type", "ema")?;
14121        let input = EriInput::from_slices(
14122            high,
14123            low,
14124            source,
14125            EriParams {
14126                period: Some(period),
14127                ma_type: Some(ma_type),
14128            },
14129        );
14130        let out =
14131            eri_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14132                indicator: "eri".to_string(),
14133                details: e.to_string(),
14134            })?;
14135        if output_id.eq_ignore_ascii_case("bull") || output_id.eq_ignore_ascii_case("value") {
14136            return Ok(out.bull);
14137        }
14138        if output_id.eq_ignore_ascii_case("bear") {
14139            return Ok(out.bear);
14140        }
14141        Err(IndicatorDispatchError::UnknownOutput {
14142            indicator: "eri".to_string(),
14143            output: output_id.to_string(),
14144        })
14145    })
14146}
14147
14148fn compute_fisher_batch(
14149    req: IndicatorBatchRequest<'_>,
14150    output_id: &str,
14151) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14152    let (high, low) = extract_high_low_input("fisher", req.data)?;
14153    let kernel = req.kernel.to_non_batch();
14154    collect_f64("fisher", output_id, req.combos, high.len(), |params| {
14155        let period = get_usize_param("fisher", params, "period", 9)?;
14156        let input = FisherInput::from_slices(
14157            high,
14158            low,
14159            FisherParams {
14160                period: Some(period),
14161            },
14162        );
14163        let out = fisher_with_kernel(&input, kernel).map_err(|e| {
14164            IndicatorDispatchError::ComputeFailed {
14165                indicator: "fisher".to_string(),
14166                details: e.to_string(),
14167            }
14168        })?;
14169        if output_id.eq_ignore_ascii_case("fisher") || output_id.eq_ignore_ascii_case("value") {
14170            return Ok(out.fisher);
14171        }
14172        if output_id.eq_ignore_ascii_case("signal") {
14173            return Ok(out.signal);
14174        }
14175        Err(IndicatorDispatchError::UnknownOutput {
14176            indicator: "fisher".to_string(),
14177            output: output_id.to_string(),
14178        })
14179    })
14180}
14181
14182fn compute_fvg_positioning_average_batch(
14183    req: IndicatorBatchRequest<'_>,
14184    output_id: &str,
14185) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14186    let (open, high, low, close) = extract_ohlc_full_input("fvg_positioning_average", req.data)?;
14187    let kernel = req.kernel.to_non_batch();
14188    collect_f64(
14189        "fvg_positioning_average",
14190        output_id,
14191        req.combos,
14192        close.len(),
14193        |params| {
14194            let lookback = get_usize_param("fvg_positioning_average", params, "lookback", 30)?;
14195            let lookback_type = get_enum_param(
14196                "fvg_positioning_average",
14197                params,
14198                "lookback_type",
14199                "Bar Count",
14200            )?;
14201            let atr_multiplier =
14202                get_f64_param("fvg_positioning_average", params, "atr_multiplier", 0.25)?;
14203            let input = FvgPositioningAverageInput::from_slices(
14204                open,
14205                high,
14206                low,
14207                close,
14208                FvgPositioningAverageParams {
14209                    lookback: Some(lookback),
14210                    lookback_type: Some(lookback_type),
14211                    atr_multiplier: Some(atr_multiplier),
14212                },
14213            );
14214            let out = fvg_positioning_average_with_kernel(&input, kernel).map_err(|e| {
14215                IndicatorDispatchError::ComputeFailed {
14216                    indicator: "fvg_positioning_average".to_string(),
14217                    details: e.to_string(),
14218                }
14219            })?;
14220            if output_id.eq_ignore_ascii_case("bull_average")
14221                || output_id.eq_ignore_ascii_case("value")
14222            {
14223                return Ok(out.bull_average);
14224            }
14225            if output_id.eq_ignore_ascii_case("bear_average") {
14226                return Ok(out.bear_average);
14227            }
14228            if output_id.eq_ignore_ascii_case("bull_mid") {
14229                return Ok(out.bull_mid);
14230            }
14231            if output_id.eq_ignore_ascii_case("bear_mid") {
14232                return Ok(out.bear_mid);
14233            }
14234            Err(IndicatorDispatchError::UnknownOutput {
14235                indicator: "fvg_positioning_average".to_string(),
14236                output: output_id.to_string(),
14237            })
14238        },
14239    )
14240}
14241
14242fn compute_fvg_trailing_stop_batch(
14243    req: IndicatorBatchRequest<'_>,
14244    output_id: &str,
14245) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14246    let (high, low, close) = extract_ohlc_input("fvg_trailing_stop", req.data)?;
14247    let kernel = req.kernel.to_non_batch();
14248    collect_f64(
14249        "fvg_trailing_stop",
14250        output_id,
14251        req.combos,
14252        close.len(),
14253        |params| {
14254            let lookback =
14255                get_usize_param("fvg_trailing_stop", params, "unmitigated_fvg_lookback", 5)?;
14256            let smoothing_length =
14257                get_usize_param("fvg_trailing_stop", params, "smoothing_length", 9)?;
14258            let reset_on_cross =
14259                get_bool_param("fvg_trailing_stop", params, "reset_on_cross", false)?;
14260            let input = FvgTrailingStopInput::from_slices(
14261                high,
14262                low,
14263                close,
14264                FvgTrailingStopParams {
14265                    unmitigated_fvg_lookback: Some(lookback),
14266                    smoothing_length: Some(smoothing_length),
14267                    reset_on_cross: Some(reset_on_cross),
14268                },
14269            );
14270            let out = fvg_trailing_stop_with_kernel(&input, kernel).map_err(|e| {
14271                IndicatorDispatchError::ComputeFailed {
14272                    indicator: "fvg_trailing_stop".to_string(),
14273                    details: e.to_string(),
14274                }
14275            })?;
14276            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
14277                return Ok(out.upper);
14278            }
14279            if output_id.eq_ignore_ascii_case("lower") {
14280                return Ok(out.lower);
14281            }
14282            if output_id.eq_ignore_ascii_case("upper_ts") {
14283                return Ok(out.upper_ts);
14284            }
14285            if output_id.eq_ignore_ascii_case("lower_ts") {
14286                return Ok(out.lower_ts);
14287            }
14288            Err(IndicatorDispatchError::UnknownOutput {
14289                indicator: "fvg_trailing_stop".to_string(),
14290                output: output_id.to_string(),
14291            })
14292        },
14293    )
14294}
14295
14296fn compute_gatorosc_batch(
14297    req: IndicatorBatchRequest<'_>,
14298    output_id: &str,
14299) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14300    let data = extract_slice_input("gatorosc", req.data, "close")?;
14301    let kernel = req.kernel.to_non_batch();
14302    collect_f64("gatorosc", output_id, req.combos, data.len(), |params| {
14303        let jaws_length = get_usize_param("gatorosc", params, "jaws_length", 13)?;
14304        let jaws_shift = get_usize_param("gatorosc", params, "jaws_shift", 8)?;
14305        let teeth_length = get_usize_param("gatorosc", params, "teeth_length", 8)?;
14306        let teeth_shift = get_usize_param("gatorosc", params, "teeth_shift", 5)?;
14307        let lips_length = get_usize_param("gatorosc", params, "lips_length", 5)?;
14308        let lips_shift = get_usize_param("gatorosc", params, "lips_shift", 3)?;
14309        let input = GatorOscInput::from_slice(
14310            data,
14311            GatorOscParams {
14312                jaws_length: Some(jaws_length),
14313                jaws_shift: Some(jaws_shift),
14314                teeth_length: Some(teeth_length),
14315                teeth_shift: Some(teeth_shift),
14316                lips_length: Some(lips_length),
14317                lips_shift: Some(lips_shift),
14318            },
14319        );
14320        let out = gatorosc_with_kernel(&input, kernel).map_err(|e| {
14321            IndicatorDispatchError::ComputeFailed {
14322                indicator: "gatorosc".to_string(),
14323                details: e.to_string(),
14324            }
14325        })?;
14326        if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
14327            return Ok(out.upper);
14328        }
14329        if output_id.eq_ignore_ascii_case("lower") {
14330            return Ok(out.lower);
14331        }
14332        if output_id.eq_ignore_ascii_case("upper_change") {
14333            return Ok(out.upper_change);
14334        }
14335        if output_id.eq_ignore_ascii_case("lower_change") {
14336            return Ok(out.lower_change);
14337        }
14338        Err(IndicatorDispatchError::UnknownOutput {
14339            indicator: "gatorosc".to_string(),
14340            output: output_id.to_string(),
14341        })
14342    })
14343}
14344
14345fn compute_halftrend_batch(
14346    req: IndicatorBatchRequest<'_>,
14347    output_id: &str,
14348) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14349    let (high, low, close) = extract_ohlc_input("halftrend", req.data)?;
14350    let kernel = req.kernel.to_non_batch();
14351    collect_f64("halftrend", output_id, req.combos, close.len(), |params| {
14352        let amplitude = get_usize_param("halftrend", params, "amplitude", 2)?;
14353        let channel_deviation = get_f64_param("halftrend", params, "channel_deviation", 2.0)?;
14354        let atr_period = get_usize_param("halftrend", params, "atr_period", 100)?;
14355        let input = HalfTrendInput::from_slices(
14356            high,
14357            low,
14358            close,
14359            HalfTrendParams {
14360                amplitude: Some(amplitude),
14361                channel_deviation: Some(channel_deviation),
14362                atr_period: Some(atr_period),
14363            },
14364        );
14365        let out = halftrend_with_kernel(&input, kernel).map_err(|e| {
14366            IndicatorDispatchError::ComputeFailed {
14367                indicator: "halftrend".to_string(),
14368                details: e.to_string(),
14369            }
14370        })?;
14371        if output_id.eq_ignore_ascii_case("halftrend") || output_id.eq_ignore_ascii_case("value") {
14372            return Ok(out.halftrend);
14373        }
14374        if output_id.eq_ignore_ascii_case("trend") {
14375            return Ok(out.trend);
14376        }
14377        if output_id.eq_ignore_ascii_case("atr_high") {
14378            return Ok(out.atr_high);
14379        }
14380        if output_id.eq_ignore_ascii_case("atr_low") {
14381            return Ok(out.atr_low);
14382        }
14383        if output_id.eq_ignore_ascii_case("buy_signal") || output_id.eq_ignore_ascii_case("buy") {
14384            return Ok(out.buy_signal);
14385        }
14386        if output_id.eq_ignore_ascii_case("sell_signal") || output_id.eq_ignore_ascii_case("sell") {
14387            return Ok(out.sell_signal);
14388        }
14389        Err(IndicatorDispatchError::UnknownOutput {
14390            indicator: "halftrend".to_string(),
14391            output: output_id.to_string(),
14392        })
14393    })
14394}
14395
14396fn compute_safezonestop_batch(
14397    req: IndicatorBatchRequest<'_>,
14398    output_id: &str,
14399) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14400    let (high, low) = extract_high_low_input("safezonestop", req.data)?;
14401    let kernel = req.kernel.to_non_batch();
14402    collect_f64(
14403        "safezonestop",
14404        output_id,
14405        req.combos,
14406        high.len(),
14407        |params| {
14408            let period = get_usize_param("safezonestop", params, "period", 22)?;
14409            let mult = get_f64_param("safezonestop", params, "mult", 2.5)?;
14410            let max_lookback = get_usize_param("safezonestop", params, "max_lookback", 3)?;
14411            let direction = get_enum_param("safezonestop", params, "direction", "long")?;
14412            let input = SafeZoneStopInput::from_slices(
14413                high,
14414                low,
14415                direction.as_str(),
14416                SafeZoneStopParams {
14417                    period: Some(period),
14418                    mult: Some(mult),
14419                    max_lookback: Some(max_lookback),
14420                },
14421            );
14422            let out = safezonestop_with_kernel(&input, kernel).map_err(|e| {
14423                IndicatorDispatchError::ComputeFailed {
14424                    indicator: "safezonestop".to_string(),
14425                    details: e.to_string(),
14426                }
14427            })?;
14428            if output_id.eq_ignore_ascii_case("value") {
14429                return Ok(out.values);
14430            }
14431            Err(IndicatorDispatchError::UnknownOutput {
14432                indicator: "safezonestop".to_string(),
14433                output: output_id.to_string(),
14434            })
14435        },
14436    )
14437}
14438
14439fn compute_devstop_batch(
14440    req: IndicatorBatchRequest<'_>,
14441    output_id: &str,
14442) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14443    let (high, low) = extract_high_low_input("devstop", req.data)?;
14444    let kernel = req.kernel.to_non_batch();
14445    collect_f64("devstop", output_id, req.combos, high.len(), |params| {
14446        let period = get_usize_param("devstop", params, "period", 20)?;
14447        let mult = get_f64_param("devstop", params, "mult", 0.0)?;
14448        let devtype = get_usize_param("devstop", params, "devtype", 0)?;
14449        let direction = get_enum_param("devstop", params, "direction", "long")?;
14450        let ma_type = get_enum_param("devstop", params, "ma_type", "sma")?;
14451        let input = DevStopInput::from_slices(
14452            high,
14453            low,
14454            DevStopParams {
14455                period: Some(period),
14456                mult: Some(mult),
14457                devtype: Some(devtype),
14458                direction: Some(direction),
14459                ma_type: Some(ma_type),
14460            },
14461        );
14462        let out = devstop_with_kernel(&input, kernel).map_err(|e| {
14463            IndicatorDispatchError::ComputeFailed {
14464                indicator: "devstop".to_string(),
14465                details: e.to_string(),
14466            }
14467        })?;
14468        if output_id.eq_ignore_ascii_case("value") {
14469            return Ok(out.values);
14470        }
14471        Err(IndicatorDispatchError::UnknownOutput {
14472            indicator: "devstop".to_string(),
14473            output: output_id.to_string(),
14474        })
14475    })
14476}
14477
14478fn compute_chop_batch(
14479    req: IndicatorBatchRequest<'_>,
14480    output_id: &str,
14481) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14482    let (high, low, close) = extract_ohlc_input("chop", req.data)?;
14483    let kernel = req.kernel.to_non_batch();
14484    collect_f64("chop", output_id, req.combos, close.len(), |params| {
14485        let period = get_usize_param("chop", params, "period", 14)?;
14486        let scalar = get_f64_param("chop", params, "scalar", 100.0)?;
14487        let drift = get_usize_param("chop", params, "drift", 1)?;
14488        let input = ChopInput::from_slices(
14489            high,
14490            low,
14491            close,
14492            ChopParams {
14493                period: Some(period),
14494                scalar: Some(scalar),
14495                drift: Some(drift),
14496            },
14497        );
14498        let out = chop_with_kernel(&input, kernel).map_err(|e| {
14499            IndicatorDispatchError::ComputeFailed {
14500                indicator: "chop".to_string(),
14501                details: e.to_string(),
14502            }
14503        })?;
14504        if output_id.eq_ignore_ascii_case("value") {
14505            return Ok(out.values);
14506        }
14507        Err(IndicatorDispatchError::UnknownOutput {
14508            indicator: "chop".to_string(),
14509            output: output_id.to_string(),
14510        })
14511    })
14512}
14513
14514fn compute_kst_batch(
14515    req: IndicatorBatchRequest<'_>,
14516    output_id: &str,
14517) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14518    let data = extract_slice_input("kst", req.data, "close")?;
14519    let kernel = req.kernel.to_non_batch();
14520    collect_f64("kst", output_id, req.combos, data.len(), |params| {
14521        let sma_period1 = get_usize_param("kst", params, "sma_period1", 10)?;
14522        let sma_period2 = get_usize_param("kst", params, "sma_period2", 10)?;
14523        let sma_period3 = get_usize_param("kst", params, "sma_period3", 10)?;
14524        let sma_period4 = get_usize_param("kst", params, "sma_period4", 15)?;
14525        let roc_period1 = get_usize_param("kst", params, "roc_period1", 10)?;
14526        let roc_period2 = get_usize_param("kst", params, "roc_period2", 15)?;
14527        let roc_period3 = get_usize_param("kst", params, "roc_period3", 20)?;
14528        let roc_period4 = get_usize_param("kst", params, "roc_period4", 30)?;
14529        let signal_period = get_usize_param("kst", params, "signal_period", 9)?;
14530        let input = KstInput::from_slice(
14531            data,
14532            KstParams {
14533                sma_period1: Some(sma_period1),
14534                sma_period2: Some(sma_period2),
14535                sma_period3: Some(sma_period3),
14536                sma_period4: Some(sma_period4),
14537                roc_period1: Some(roc_period1),
14538                roc_period2: Some(roc_period2),
14539                roc_period3: Some(roc_period3),
14540                roc_period4: Some(roc_period4),
14541                signal_period: Some(signal_period),
14542            },
14543        );
14544        let out =
14545            kst_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14546                indicator: "kst".to_string(),
14547                details: e.to_string(),
14548            })?;
14549        if output_id.eq_ignore_ascii_case("line") || output_id.eq_ignore_ascii_case("value") {
14550            return Ok(out.line);
14551        }
14552        if output_id.eq_ignore_ascii_case("signal") {
14553            return Ok(out.signal);
14554        }
14555        Err(IndicatorDispatchError::UnknownOutput {
14556            indicator: "kst".to_string(),
14557            output: output_id.to_string(),
14558        })
14559    })
14560}
14561
14562fn compute_kaufmanstop_batch(
14563    req: IndicatorBatchRequest<'_>,
14564    output_id: &str,
14565) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14566    expect_value_output("kaufmanstop", output_id)?;
14567    let (high, low) = extract_high_low_input("kaufmanstop", req.data)?;
14568    let kernel = req.kernel.to_non_batch();
14569    collect_f64("kaufmanstop", output_id, req.combos, high.len(), |params| {
14570        let period = get_usize_param("kaufmanstop", params, "period", 22)?;
14571        let mult = get_f64_param("kaufmanstop", params, "mult", 2.0)?;
14572        let direction = get_enum_param("kaufmanstop", params, "direction", "long")?;
14573        let ma_type = get_enum_param("kaufmanstop", params, "ma_type", "sma")?;
14574        let input = KaufmanstopInput::from_slices(
14575            high,
14576            low,
14577            KaufmanstopParams {
14578                period: Some(period),
14579                mult: Some(mult),
14580                direction: Some(direction),
14581                ma_type: Some(ma_type),
14582            },
14583        );
14584        let out = kaufmanstop_with_kernel(&input, kernel).map_err(|e| {
14585            IndicatorDispatchError::ComputeFailed {
14586                indicator: "kaufmanstop".to_string(),
14587                details: e.to_string(),
14588            }
14589        })?;
14590        Ok(out.values)
14591    })
14592}
14593
14594fn compute_lpc_batch(
14595    req: IndicatorBatchRequest<'_>,
14596    output_id: &str,
14597) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14598    let (high, low, close, src) = match req.data {
14599        IndicatorDataRef::Candles { candles, source } => (
14600            candles.high.as_slice(),
14601            candles.low.as_slice(),
14602            candles.close.as_slice(),
14603            source_type(candles, source.unwrap_or("close")),
14604        ),
14605        IndicatorDataRef::Ohlc {
14606            open,
14607            high,
14608            low,
14609            close,
14610        } => {
14611            ensure_same_len_4("lpc", open.len(), high.len(), low.len(), close.len())?;
14612            (high, low, close, close)
14613        }
14614        IndicatorDataRef::Ohlcv {
14615            open,
14616            high,
14617            low,
14618            close,
14619            volume,
14620        } => {
14621            ensure_same_len_5(
14622                "lpc",
14623                open.len(),
14624                high.len(),
14625                low.len(),
14626                close.len(),
14627                volume.len(),
14628            )?;
14629            (high, low, close, close)
14630        }
14631        _ => {
14632            return Err(IndicatorDispatchError::MissingRequiredInput {
14633                indicator: "lpc".to_string(),
14634                input: IndicatorInputKind::Ohlc,
14635            });
14636        }
14637    };
14638    let kernel = req.kernel.to_non_batch();
14639    collect_f64("lpc", output_id, req.combos, src.len(), |params| {
14640        let cutoff_type = get_enum_param("lpc", params, "cutoff_type", "adaptive")?;
14641        let fixed_period = get_usize_param("lpc", params, "fixed_period", 20)?;
14642        let max_cycle_limit = get_usize_param("lpc", params, "max_cycle_limit", 60)?;
14643        let cycle_mult = get_f64_param("lpc", params, "cycle_mult", 1.0)?;
14644        let tr_mult = get_f64_param("lpc", params, "tr_mult", 1.0)?;
14645        let input = LpcInput::from_slices(
14646            high,
14647            low,
14648            close,
14649            src,
14650            LpcParams {
14651                cutoff_type: Some(cutoff_type),
14652                fixed_period: Some(fixed_period),
14653                max_cycle_limit: Some(max_cycle_limit),
14654                cycle_mult: Some(cycle_mult),
14655                tr_mult: Some(tr_mult),
14656            },
14657        );
14658        let out =
14659            lpc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14660                indicator: "lpc".to_string(),
14661                details: e.to_string(),
14662            })?;
14663        if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
14664            return Ok(out.filter);
14665        }
14666        if output_id.eq_ignore_ascii_case("high_band") || output_id.eq_ignore_ascii_case("high") {
14667            return Ok(out.high_band);
14668        }
14669        if output_id.eq_ignore_ascii_case("low_band") || output_id.eq_ignore_ascii_case("low") {
14670            return Ok(out.low_band);
14671        }
14672        Err(IndicatorDispatchError::UnknownOutput {
14673            indicator: "lpc".to_string(),
14674            output: output_id.to_string(),
14675        })
14676    })
14677}
14678
14679fn compute_mab_batch(
14680    req: IndicatorBatchRequest<'_>,
14681    output_id: &str,
14682) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14683    let data = extract_slice_input("mab", req.data, "close")?;
14684    let kernel = req.kernel.to_non_batch();
14685    collect_f64("mab", output_id, req.combos, data.len(), |params| {
14686        let fast_period = get_usize_param("mab", params, "fast_period", 10)?;
14687        let slow_period = get_usize_param("mab", params, "slow_period", 50)?;
14688        let devup = get_f64_param("mab", params, "devup", 1.0)?;
14689        let devdn = get_f64_param("mab", params, "devdn", 1.0)?;
14690        let fast_ma_type = get_enum_param("mab", params, "fast_ma_type", "sma")?;
14691        let slow_ma_type = get_enum_param("mab", params, "slow_ma_type", "sma")?;
14692        let input = MabInput::from_slice(
14693            data,
14694            MabParams {
14695                fast_period: Some(fast_period),
14696                slow_period: Some(slow_period),
14697                devup: Some(devup),
14698                devdn: Some(devdn),
14699                fast_ma_type: Some(fast_ma_type),
14700                slow_ma_type: Some(slow_ma_type),
14701            },
14702        );
14703        let out =
14704            mab_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14705                indicator: "mab".to_string(),
14706                details: e.to_string(),
14707            })?;
14708        if output_id.eq_ignore_ascii_case("upperband")
14709            || output_id.eq_ignore_ascii_case("upper")
14710            || output_id.eq_ignore_ascii_case("value")
14711        {
14712            return Ok(out.upperband);
14713        }
14714        if output_id.eq_ignore_ascii_case("middleband") || output_id.eq_ignore_ascii_case("middle")
14715        {
14716            return Ok(out.middleband);
14717        }
14718        if output_id.eq_ignore_ascii_case("lowerband") || output_id.eq_ignore_ascii_case("lower") {
14719            return Ok(out.lowerband);
14720        }
14721        Err(IndicatorDispatchError::UnknownOutput {
14722            indicator: "mab".to_string(),
14723            output: output_id.to_string(),
14724        })
14725    })
14726}
14727
14728fn compute_macz_batch(
14729    req: IndicatorBatchRequest<'_>,
14730    output_id: &str,
14731) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14732    let (data, volume) = match req.data {
14733        IndicatorDataRef::Slice { values } => (values, None),
14734        IndicatorDataRef::Candles { candles, source } => (
14735            source_type(candles, source.unwrap_or("close")),
14736            Some(candles.volume.as_slice()),
14737        ),
14738        IndicatorDataRef::CloseVolume { close, volume } => {
14739            ensure_same_len_2("macz", close.len(), volume.len())?;
14740            (close, Some(volume))
14741        }
14742        IndicatorDataRef::Ohlc {
14743            open,
14744            high,
14745            low,
14746            close,
14747        } => {
14748            ensure_same_len_4("macz", open.len(), high.len(), low.len(), close.len())?;
14749            (close, None)
14750        }
14751        IndicatorDataRef::Ohlcv {
14752            open,
14753            high,
14754            low,
14755            close,
14756            volume,
14757        } => {
14758            ensure_same_len_5(
14759                "macz",
14760                open.len(),
14761                high.len(),
14762                low.len(),
14763                close.len(),
14764                volume.len(),
14765            )?;
14766            (close, Some(volume))
14767        }
14768        IndicatorDataRef::HighLow { .. } => {
14769            return Err(IndicatorDispatchError::MissingRequiredInput {
14770                indicator: "macz".to_string(),
14771                input: IndicatorInputKind::Slice,
14772            })
14773        }
14774    };
14775    let kernel = req.kernel.to_non_batch();
14776    collect_f64("macz", output_id, req.combos, data.len(), |params| {
14777        let fast_length = get_usize_param("macz", params, "fast_length", 12)?;
14778        let slow_length = get_usize_param("macz", params, "slow_length", 25)?;
14779        let signal_length = get_usize_param("macz", params, "signal_length", 9)?;
14780        let lengthz = get_usize_param("macz", params, "lengthz", 20)?;
14781        let length_stdev = get_usize_param("macz", params, "length_stdev", 25)?;
14782        let a = get_f64_param("macz", params, "a", 1.0)?;
14783        let b = get_f64_param("macz", params, "b", 1.0)?;
14784        let use_lag = get_bool_param("macz", params, "use_lag", false)?;
14785        let gamma = get_f64_param("macz", params, "gamma", 0.02)?;
14786        let macz_params = MaczParams {
14787            fast_length: Some(fast_length),
14788            slow_length: Some(slow_length),
14789            signal_length: Some(signal_length),
14790            lengthz: Some(lengthz),
14791            length_stdev: Some(length_stdev),
14792            a: Some(a),
14793            b: Some(b),
14794            use_lag: Some(use_lag),
14795            gamma: Some(gamma),
14796        };
14797        let input = if let Some(vol) = volume {
14798            MaczInput::from_slice_with_volume(data, vol, macz_params)
14799        } else {
14800            MaczInput::from_slice(data, macz_params)
14801        };
14802        let out = macz_with_kernel(&input, kernel).map_err(|e| {
14803            IndicatorDispatchError::ComputeFailed {
14804                indicator: "macz".to_string(),
14805                details: e.to_string(),
14806            }
14807        })?;
14808        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
14809            return Ok(out.values);
14810        }
14811        Err(IndicatorDispatchError::UnknownOutput {
14812            indicator: "macz".to_string(),
14813            output: output_id.to_string(),
14814        })
14815    })
14816}
14817
14818fn compute_minmax_batch(
14819    req: IndicatorBatchRequest<'_>,
14820    output_id: &str,
14821) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14822    let (high, low) = extract_high_low_input("minmax", req.data)?;
14823    let kernel = req.kernel.to_non_batch();
14824    collect_f64("minmax", output_id, req.combos, high.len(), |params| {
14825        let order = get_usize_param("minmax", params, "order", 3)?;
14826        let input = MinmaxInput::from_slices(high, low, MinmaxParams { order: Some(order) });
14827        let out = minmax_with_kernel(&input, kernel).map_err(|e| {
14828            IndicatorDispatchError::ComputeFailed {
14829                indicator: "minmax".to_string(),
14830                details: e.to_string(),
14831            }
14832        })?;
14833        if output_id.eq_ignore_ascii_case("is_min") || output_id.eq_ignore_ascii_case("value") {
14834            return Ok(out.is_min);
14835        }
14836        if output_id.eq_ignore_ascii_case("is_max") {
14837            return Ok(out.is_max);
14838        }
14839        if output_id.eq_ignore_ascii_case("last_min") {
14840            return Ok(out.last_min);
14841        }
14842        if output_id.eq_ignore_ascii_case("last_max") {
14843            return Ok(out.last_max);
14844        }
14845        Err(IndicatorDispatchError::UnknownOutput {
14846            indicator: "minmax".to_string(),
14847            output: output_id.to_string(),
14848        })
14849    })
14850}
14851
14852fn compute_mod_god_mode_batch(
14853    req: IndicatorBatchRequest<'_>,
14854    output_id: &str,
14855) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14856    let (high, low, close, volume) = match req.data {
14857        IndicatorDataRef::Candles { candles, .. } => (
14858            candles.high.as_slice(),
14859            candles.low.as_slice(),
14860            candles.close.as_slice(),
14861            Some(candles.volume.as_slice()),
14862        ),
14863        IndicatorDataRef::Ohlc {
14864            open,
14865            high,
14866            low,
14867            close,
14868        } => {
14869            ensure_same_len_4(
14870                "mod_god_mode",
14871                open.len(),
14872                high.len(),
14873                low.len(),
14874                close.len(),
14875            )?;
14876            (high, low, close, None)
14877        }
14878        IndicatorDataRef::Ohlcv {
14879            open,
14880            high,
14881            low,
14882            close,
14883            volume,
14884        } => {
14885            ensure_same_len_5(
14886                "mod_god_mode",
14887                open.len(),
14888                high.len(),
14889                low.len(),
14890                close.len(),
14891                volume.len(),
14892            )?;
14893            (high, low, close, Some(volume))
14894        }
14895        _ => {
14896            return Err(IndicatorDispatchError::MissingRequiredInput {
14897                indicator: "mod_god_mode".to_string(),
14898                input: IndicatorInputKind::Ohlc,
14899            });
14900        }
14901    };
14902
14903    collect_f64(
14904        "mod_god_mode",
14905        output_id,
14906        req.combos,
14907        close.len(),
14908        |params| {
14909            let n1 = get_usize_param("mod_god_mode", params, "n1", 17)?;
14910            let n2 = get_usize_param("mod_god_mode", params, "n2", 6)?;
14911            let n3 = get_usize_param("mod_god_mode", params, "n3", 4)?;
14912            let mode = get_enum_param("mod_god_mode", params, "mode", "tradition_mg")?;
14913            let use_volume = get_bool_param("mod_god_mode", params, "use_volume", true)?;
14914            let mode = match mode.as_str() {
14915                "godmode" => ModGodModeMode::Godmode,
14916                "tradition" => ModGodModeMode::Tradition,
14917                "godmode_mg" => ModGodModeMode::GodmodeMg,
14918                "tradition_mg" => ModGodModeMode::TraditionMg,
14919                other => {
14920                    return Err(IndicatorDispatchError::InvalidParam {
14921                        indicator: "mod_god_mode".to_string(),
14922                        key: "mode".to_string(),
14923                        reason: format!("unknown mode: {other}"),
14924                    });
14925                }
14926            };
14927            let input = ModGodModeInput {
14928                data: ModGodModeData::Slices {
14929                    high,
14930                    low,
14931                    close,
14932                    volume: if use_volume { volume } else { None },
14933                },
14934                params: ModGodModeParams {
14935                    n1: Some(n1),
14936                    n2: Some(n2),
14937                    n3: Some(n3),
14938                    mode: Some(mode),
14939                    use_volume: Some(use_volume),
14940                },
14941            };
14942            let out = mod_god_mode(&input).map_err(|e| IndicatorDispatchError::ComputeFailed {
14943                indicator: "mod_god_mode".to_string(),
14944                details: e.to_string(),
14945            })?;
14946            if output_id.eq_ignore_ascii_case("wavetrend")
14947                || output_id.eq_ignore_ascii_case("wt1")
14948                || output_id.eq_ignore_ascii_case("value")
14949            {
14950                return Ok(out.wavetrend);
14951            }
14952            if output_id.eq_ignore_ascii_case("signal") || output_id.eq_ignore_ascii_case("wt2") {
14953                return Ok(out.signal);
14954            }
14955            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
14956            {
14957                return Ok(out.histogram);
14958            }
14959            Err(IndicatorDispatchError::UnknownOutput {
14960                indicator: "mod_god_mode".to_string(),
14961                output: output_id.to_string(),
14962            })
14963        },
14964    )
14965}
14966
14967fn compute_msw_batch(
14968    req: IndicatorBatchRequest<'_>,
14969    output_id: &str,
14970) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
14971    let data = extract_slice_input("msw", req.data, "close")?;
14972    let kernel = req.kernel.to_non_batch();
14973    collect_f64("msw", output_id, req.combos, data.len(), |params| {
14974        let period = get_usize_param("msw", params, "period", 5)?;
14975        let input = MswInput::from_slice(
14976            data,
14977            MswParams {
14978                period: Some(period),
14979            },
14980        );
14981        let out =
14982            msw_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
14983                indicator: "msw".to_string(),
14984                details: e.to_string(),
14985            })?;
14986        if output_id.eq_ignore_ascii_case("sine") || output_id.eq_ignore_ascii_case("value") {
14987            return Ok(out.sine);
14988        }
14989        if output_id.eq_ignore_ascii_case("lead") {
14990            return Ok(out.lead);
14991        }
14992        Err(IndicatorDispatchError::UnknownOutput {
14993            indicator: "msw".to_string(),
14994            output: output_id.to_string(),
14995        })
14996    })
14997}
14998
14999fn compute_nadaraya_watson_envelope_batch(
15000    req: IndicatorBatchRequest<'_>,
15001    output_id: &str,
15002) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15003    let data = extract_slice_input("nadaraya_watson_envelope", req.data, "close")?;
15004    let kernel = req.kernel.to_non_batch();
15005    collect_f64(
15006        "nadaraya_watson_envelope",
15007        output_id,
15008        req.combos,
15009        data.len(),
15010        |params| {
15011            let bandwidth = get_f64_param("nadaraya_watson_envelope", params, "bandwidth", 8.0)?;
15012            let multiplier = get_f64_param("nadaraya_watson_envelope", params, "multiplier", 3.0)?;
15013            let lookback = get_usize_param("nadaraya_watson_envelope", params, "lookback", 500)?;
15014            let input = NweInput::from_slice(
15015                data,
15016                NweParams {
15017                    bandwidth: Some(bandwidth),
15018                    multiplier: Some(multiplier),
15019                    lookback: Some(lookback),
15020                },
15021            );
15022            let out = nadaraya_watson_envelope_with_kernel(&input, kernel).map_err(|e| {
15023                IndicatorDispatchError::ComputeFailed {
15024                    indicator: "nadaraya_watson_envelope".to_string(),
15025                    details: e.to_string(),
15026                }
15027            })?;
15028            if output_id.eq_ignore_ascii_case("upper") || output_id.eq_ignore_ascii_case("value") {
15029                return Ok(out.upper);
15030            }
15031            if output_id.eq_ignore_ascii_case("lower") {
15032                return Ok(out.lower);
15033            }
15034            Err(IndicatorDispatchError::UnknownOutput {
15035                indicator: "nadaraya_watson_envelope".to_string(),
15036                output: output_id.to_string(),
15037            })
15038        },
15039    )
15040}
15041
15042fn compute_otto_batch(
15043    req: IndicatorBatchRequest<'_>,
15044    output_id: &str,
15045) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15046    let data = extract_slice_input("otto", req.data, "close")?;
15047    let kernel = req.kernel.to_non_batch();
15048    collect_f64("otto", output_id, req.combos, data.len(), |params| {
15049        let ott_period = get_usize_param("otto", params, "ott_period", 2)?;
15050        let ott_percent = get_f64_param("otto", params, "ott_percent", 0.6)?;
15051        let fast_vidya_length = get_usize_param("otto", params, "fast_vidya_length", 10)?;
15052        let slow_vidya_length = get_usize_param("otto", params, "slow_vidya_length", 25)?;
15053        let correcting_constant = get_f64_param("otto", params, "correcting_constant", 100000.0)?;
15054        let ma_type = get_enum_param("otto", params, "ma_type", "VAR")?;
15055        let input = OttoInput::from_slice(
15056            data,
15057            OttoParams {
15058                ott_period: Some(ott_period),
15059                ott_percent: Some(ott_percent),
15060                fast_vidya_length: Some(fast_vidya_length),
15061                slow_vidya_length: Some(slow_vidya_length),
15062                correcting_constant: Some(correcting_constant),
15063                ma_type: Some(ma_type),
15064            },
15065        );
15066        let out = otto_with_kernel(&input, kernel).map_err(|e| {
15067            IndicatorDispatchError::ComputeFailed {
15068                indicator: "otto".to_string(),
15069                details: e.to_string(),
15070            }
15071        })?;
15072        if output_id.eq_ignore_ascii_case("hott") || output_id.eq_ignore_ascii_case("value") {
15073            return Ok(out.hott);
15074        }
15075        if output_id.eq_ignore_ascii_case("lott") {
15076            return Ok(out.lott);
15077        }
15078        Err(IndicatorDispatchError::UnknownOutput {
15079            indicator: "otto".to_string(),
15080            output: output_id.to_string(),
15081        })
15082    })
15083}
15084
15085fn compute_vidya_batch(
15086    req: IndicatorBatchRequest<'_>,
15087    output_id: &str,
15088) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15089    let data = extract_slice_input("vidya", req.data, "close")?;
15090    let kernel = req.kernel.to_non_batch();
15091    collect_f64("vidya", output_id, req.combos, data.len(), |params| {
15092        let short_period = get_usize_param("vidya", params, "short_period", 2)?;
15093        let long_period = get_usize_param("vidya", params, "long_period", 5)?;
15094        let alpha = get_f64_param("vidya", params, "alpha", 0.2)?;
15095        let input = VidyaInput::from_slice(
15096            data,
15097            VidyaParams {
15098                short_period: Some(short_period),
15099                long_period: Some(long_period),
15100                alpha: Some(alpha),
15101            },
15102        );
15103        let out = vidya_with_kernel(&input, kernel).map_err(|e| {
15104            IndicatorDispatchError::ComputeFailed {
15105                indicator: "vidya".to_string(),
15106                details: e.to_string(),
15107            }
15108        })?;
15109        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15110            return Ok(out.values);
15111        }
15112        Err(IndicatorDispatchError::UnknownOutput {
15113            indicator: "vidya".to_string(),
15114            output: output_id.to_string(),
15115        })
15116    })
15117}
15118
15119fn compute_vlma_batch(
15120    req: IndicatorBatchRequest<'_>,
15121    output_id: &str,
15122) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15123    let data = extract_slice_input("vlma", req.data, "close")?;
15124    let kernel = req.kernel.to_non_batch();
15125    collect_f64("vlma", output_id, req.combos, data.len(), |params| {
15126        let min_period = get_usize_param("vlma", params, "min_period", 5)?;
15127        let max_period = get_usize_param("vlma", params, "max_period", 50)?;
15128        let matype = get_enum_param("vlma", params, "matype", "sma")?;
15129        let devtype = get_usize_param("vlma", params, "devtype", 0)?;
15130        let input = VlmaInput::from_slice(
15131            data,
15132            VlmaParams {
15133                min_period: Some(min_period),
15134                max_period: Some(max_period),
15135                matype: Some(matype),
15136                devtype: Some(devtype),
15137            },
15138        );
15139        let out = vlma_with_kernel(&input, kernel).map_err(|e| {
15140            IndicatorDispatchError::ComputeFailed {
15141                indicator: "vlma".to_string(),
15142                details: e.to_string(),
15143            }
15144        })?;
15145        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15146            return Ok(out.values);
15147        }
15148        Err(IndicatorDispatchError::UnknownOutput {
15149            indicator: "vlma".to_string(),
15150            output: output_id.to_string(),
15151        })
15152    })
15153}
15154
15155fn compute_pma_batch(
15156    req: IndicatorBatchRequest<'_>,
15157    output_id: &str,
15158) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15159    let data = extract_slice_input("pma", req.data, "close")?;
15160    let kernel = req.kernel.to_non_batch();
15161    collect_f64("pma", output_id, req.combos, data.len(), |_params| {
15162        let input = PmaInput::from_slice(data, PmaParams::default());
15163        let out =
15164            pma_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15165                indicator: "pma".to_string(),
15166                details: e.to_string(),
15167            })?;
15168        if output_id.eq_ignore_ascii_case("predict") || output_id.eq_ignore_ascii_case("value") {
15169            return Ok(out.predict);
15170        }
15171        if output_id.eq_ignore_ascii_case("trigger") {
15172            return Ok(out.trigger);
15173        }
15174        Err(IndicatorDispatchError::UnknownOutput {
15175            indicator: "pma".to_string(),
15176            output: output_id.to_string(),
15177        })
15178    })
15179}
15180
15181fn compute_ehlers_adaptive_cg_batch(
15182    req: IndicatorBatchRequest<'_>,
15183    output_id: &str,
15184) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15185    let data = extract_slice_input("ehlers_adaptive_cg", req.data, "hl2")?;
15186    let kernel = req.kernel.to_non_batch();
15187    collect_f64(
15188        "ehlers_adaptive_cg",
15189        output_id,
15190        req.combos,
15191        data.len(),
15192        |params| {
15193            let alpha = get_f64_param("ehlers_adaptive_cg", params, "alpha", 0.07)?;
15194            let input = EhlersAdaptiveCgInput::from_slice(
15195                data,
15196                EhlersAdaptiveCgParams { alpha: Some(alpha) },
15197            );
15198            let out = ehlers_adaptive_cg_with_kernel(&input, kernel).map_err(|e| {
15199                IndicatorDispatchError::ComputeFailed {
15200                    indicator: "ehlers_adaptive_cg".to_string(),
15201                    details: e.to_string(),
15202                }
15203            })?;
15204            if output_id.eq_ignore_ascii_case("cg") || output_id.eq_ignore_ascii_case("value") {
15205                return Ok(out.cg);
15206            }
15207            if output_id.eq_ignore_ascii_case("trigger") {
15208                return Ok(out.trigger);
15209            }
15210            Err(IndicatorDispatchError::UnknownOutput {
15211                indicator: "ehlers_adaptive_cg".to_string(),
15212                output: output_id.to_string(),
15213            })
15214        },
15215    )
15216}
15217
15218fn compute_prb_batch(
15219    req: IndicatorBatchRequest<'_>,
15220    output_id: &str,
15221) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15222    let data = extract_slice_input("prb", req.data, "close")?;
15223    let kernel = req.kernel.to_non_batch();
15224    collect_f64("prb", output_id, req.combos, data.len(), |params| {
15225        let smooth_data = get_bool_param("prb", params, "smooth_data", true)?;
15226        let smooth_period = get_usize_param("prb", params, "smooth_period", 10)?;
15227        let regression_period = get_usize_param("prb", params, "regression_period", 100)?;
15228        let polynomial_order = get_usize_param("prb", params, "polynomial_order", 2)?;
15229        let regression_offset = get_i32_param("prb", params, "regression_offset", 0)?;
15230        let ndev = get_f64_param("prb", params, "ndev", 2.0)?;
15231        let equ_from = get_usize_param("prb", params, "equ_from", 0)?;
15232        let input = PrbInput::from_slice(
15233            data,
15234            PrbParams {
15235                smooth_data: Some(smooth_data),
15236                smooth_period: Some(smooth_period),
15237                regression_period: Some(regression_period),
15238                polynomial_order: Some(polynomial_order),
15239                regression_offset: Some(regression_offset),
15240                ndev: Some(ndev),
15241                equ_from: Some(equ_from),
15242            },
15243        );
15244        let out =
15245            prb_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15246                indicator: "prb".to_string(),
15247                details: e.to_string(),
15248            })?;
15249        if output_id.eq_ignore_ascii_case("values") || output_id.eq_ignore_ascii_case("value") {
15250            return Ok(out.values);
15251        }
15252        if output_id.eq_ignore_ascii_case("upper_band") || output_id.eq_ignore_ascii_case("upper") {
15253            return Ok(out.upper_band);
15254        }
15255        if output_id.eq_ignore_ascii_case("lower_band") || output_id.eq_ignore_ascii_case("lower") {
15256            return Ok(out.lower_band);
15257        }
15258        Err(IndicatorDispatchError::UnknownOutput {
15259            indicator: "prb".to_string(),
15260            output: output_id.to_string(),
15261        })
15262    })
15263}
15264
15265fn compute_qqe_batch(
15266    req: IndicatorBatchRequest<'_>,
15267    output_id: &str,
15268) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15269    let data = extract_slice_input("qqe", req.data, "close")?;
15270    let kernel = req.kernel.to_non_batch();
15271    collect_f64("qqe", output_id, req.combos, data.len(), |params| {
15272        let rsi_period = get_usize_param("qqe", params, "rsi_period", 14)?;
15273        let smoothing_factor = get_usize_param("qqe", params, "smoothing_factor", 5)?;
15274        let fast_factor = get_f64_param("qqe", params, "fast_factor", 4.236)?;
15275        let input = QqeInput::from_slice(
15276            data,
15277            QqeParams {
15278                rsi_period: Some(rsi_period),
15279                smoothing_factor: Some(smoothing_factor),
15280                fast_factor: Some(fast_factor),
15281            },
15282        );
15283        let out =
15284            qqe_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15285                indicator: "qqe".to_string(),
15286                details: e.to_string(),
15287            })?;
15288        if output_id.eq_ignore_ascii_case("fast") || output_id.eq_ignore_ascii_case("value") {
15289            return Ok(out.fast);
15290        }
15291        if output_id.eq_ignore_ascii_case("slow") {
15292            return Ok(out.slow);
15293        }
15294        Err(IndicatorDispatchError::UnknownOutput {
15295            indicator: "qqe".to_string(),
15296            output: output_id.to_string(),
15297        })
15298    })
15299}
15300
15301fn compute_qqe_weighted_oscillator_batch(
15302    req: IndicatorBatchRequest<'_>,
15303    output_id: &str,
15304) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15305    let data = extract_slice_input("qqe_weighted_oscillator", req.data, "close")?;
15306    let kernel = req.kernel.to_non_batch();
15307    collect_f64(
15308        "qqe_weighted_oscillator",
15309        output_id,
15310        req.combos,
15311        data.len(),
15312        |params| {
15313            let length = get_usize_param("qqe_weighted_oscillator", params, "length", 14)?;
15314            let factor = get_f64_param("qqe_weighted_oscillator", params, "factor", 4.236)?;
15315            let smooth = get_usize_param("qqe_weighted_oscillator", params, "smooth", 5)?;
15316            let weight = get_f64_param("qqe_weighted_oscillator", params, "weight", 2.0)?;
15317            let input = QqeWeightedOscillatorInput::from_slice(
15318                data,
15319                QqeWeightedOscillatorParams {
15320                    length: Some(length),
15321                    factor: Some(factor),
15322                    smooth: Some(smooth),
15323                    weight: Some(weight),
15324                },
15325            );
15326            let out = qqe_weighted_oscillator_with_kernel(&input, kernel).map_err(|e| {
15327                IndicatorDispatchError::ComputeFailed {
15328                    indicator: "qqe_weighted_oscillator".to_string(),
15329                    details: e.to_string(),
15330                }
15331            })?;
15332            if output_id.eq_ignore_ascii_case("rsi") || output_id.eq_ignore_ascii_case("value") {
15333                return Ok(out.rsi);
15334            }
15335            if output_id.eq_ignore_ascii_case("trailing_stop")
15336                || output_id.eq_ignore_ascii_case("ts")
15337            {
15338                return Ok(out.trailing_stop);
15339            }
15340            Err(IndicatorDispatchError::UnknownOutput {
15341                indicator: "qqe_weighted_oscillator".to_string(),
15342                output: output_id.to_string(),
15343            })
15344        },
15345    )
15346}
15347
15348fn compute_forward_backward_exponential_oscillator_batch(
15349    req: IndicatorBatchRequest<'_>,
15350    output_id: &str,
15351) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15352    let data = extract_slice_input("forward_backward_exponential_oscillator", req.data, "close")?;
15353    let kernel = req.kernel.to_non_batch();
15354    collect_f64(
15355        "forward_backward_exponential_oscillator",
15356        output_id,
15357        req.combos,
15358        data.len(),
15359        |params| {
15360            let length = get_usize_param(
15361                "forward_backward_exponential_oscillator",
15362                params,
15363                "length",
15364                20,
15365            )?;
15366            let smooth = get_usize_param(
15367                "forward_backward_exponential_oscillator",
15368                params,
15369                "smooth",
15370                10,
15371            )?;
15372            let input = ForwardBackwardExponentialOscillatorInput::from_slice(
15373                data,
15374                ForwardBackwardExponentialOscillatorParams {
15375                    length: Some(length),
15376                    smooth: Some(smooth),
15377                },
15378            );
15379            let out = forward_backward_exponential_oscillator_with_kernel(&input, kernel).map_err(
15380                |e| IndicatorDispatchError::ComputeFailed {
15381                    indicator: "forward_backward_exponential_oscillator".to_string(),
15382                    details: e.to_string(),
15383                },
15384            )?;
15385            if output_id.eq_ignore_ascii_case("forward_backward")
15386                || output_id.eq_ignore_ascii_case("value")
15387                || output_id.eq_ignore_ascii_case("fb")
15388            {
15389                return Ok(out.forward_backward);
15390            }
15391            if output_id.eq_ignore_ascii_case("backward")
15392                || output_id.eq_ignore_ascii_case("bwrd")
15393                || output_id.eq_ignore_ascii_case("bw")
15394            {
15395                return Ok(out.backward);
15396            }
15397            if output_id.eq_ignore_ascii_case("histogram") || output_id.eq_ignore_ascii_case("hist")
15398            {
15399                return Ok(out.histogram);
15400            }
15401            Err(IndicatorDispatchError::UnknownOutput {
15402                indicator: "forward_backward_exponential_oscillator".to_string(),
15403                output: output_id.to_string(),
15404            })
15405        },
15406    )
15407}
15408
15409fn compute_range_oscillator_batch(
15410    req: IndicatorBatchRequest<'_>,
15411    output_id: &str,
15412) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15413    let (high, low, close) = extract_ohlc_input("range_oscillator", req.data)?;
15414    let kernel = req.kernel.to_non_batch();
15415    collect_f64(
15416        "range_oscillator",
15417        output_id,
15418        req.combos,
15419        close.len(),
15420        |params| {
15421            let length = get_usize_param("range_oscillator", params, "length", 50)?;
15422            let mult = get_f64_param("range_oscillator", params, "mult", 2.0)?;
15423            let input = RangeOscillatorInput::from_slices(
15424                high,
15425                low,
15426                close,
15427                RangeOscillatorParams {
15428                    length: Some(length),
15429                    mult: Some(mult),
15430                },
15431            );
15432            let out = range_oscillator_with_kernel(&input, kernel).map_err(|e| {
15433                IndicatorDispatchError::ComputeFailed {
15434                    indicator: "range_oscillator".to_string(),
15435                    details: e.to_string(),
15436                }
15437            })?;
15438            if output_id.eq_ignore_ascii_case("oscillator")
15439                || output_id.eq_ignore_ascii_case("osc")
15440                || output_id.eq_ignore_ascii_case("value")
15441            {
15442                return Ok(out.oscillator);
15443            }
15444            if output_id.eq_ignore_ascii_case("ma") {
15445                return Ok(out.ma);
15446            }
15447            if output_id.eq_ignore_ascii_case("upper_band")
15448                || output_id.eq_ignore_ascii_case("upper")
15449            {
15450                return Ok(out.upper_band);
15451            }
15452            if output_id.eq_ignore_ascii_case("lower_band")
15453                || output_id.eq_ignore_ascii_case("lower")
15454            {
15455                return Ok(out.lower_band);
15456            }
15457            if output_id.eq_ignore_ascii_case("range_width")
15458                || output_id.eq_ignore_ascii_case("width")
15459            {
15460                return Ok(out.range_width);
15461            }
15462            if output_id.eq_ignore_ascii_case("in_range") {
15463                return Ok(out.in_range);
15464            }
15465            if output_id.eq_ignore_ascii_case("trend") {
15466                return Ok(out.trend);
15467            }
15468            if output_id.eq_ignore_ascii_case("break_up") {
15469                return Ok(out.break_up);
15470            }
15471            if output_id.eq_ignore_ascii_case("break_down") {
15472                return Ok(out.break_down);
15473            }
15474            Err(IndicatorDispatchError::UnknownOutput {
15475                indicator: "range_oscillator".to_string(),
15476                output: output_id.to_string(),
15477            })
15478        },
15479    )
15480}
15481
15482fn compute_market_structure_confluence_batch(
15483    req: IndicatorBatchRequest<'_>,
15484    output_id: &str,
15485) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15486    let (high, low, close) = extract_ohlc_input("market_structure_confluence", req.data)?;
15487    let kernel = req.kernel.to_non_batch();
15488    collect_f64(
15489        "market_structure_confluence",
15490        output_id,
15491        req.combos,
15492        close.len(),
15493        |params| {
15494            let swing_size =
15495                get_usize_param("market_structure_confluence", params, "swing_size", 10)?;
15496            let bos_confirmation = get_enum_param(
15497                "market_structure_confluence",
15498                params,
15499                "bos_confirmation",
15500                "Candle Close",
15501            )?;
15502            let basis_length =
15503                get_usize_param("market_structure_confluence", params, "basis_length", 100)?;
15504            let atr_length =
15505                get_usize_param("market_structure_confluence", params, "atr_length", 14)?;
15506            let atr_smooth =
15507                get_usize_param("market_structure_confluence", params, "atr_smooth", 21)?;
15508            let vol_mult = get_f64_param("market_structure_confluence", params, "vol_mult", 2.0)?;
15509            let input = MarketStructureConfluenceInput::from_slices(
15510                high,
15511                low,
15512                close,
15513                MarketStructureConfluenceParams {
15514                    swing_size: Some(swing_size),
15515                    bos_confirmation: Some(bos_confirmation),
15516                    basis_length: Some(basis_length),
15517                    atr_length: Some(atr_length),
15518                    atr_smooth: Some(atr_smooth),
15519                    vol_mult: Some(vol_mult),
15520                },
15521            );
15522            let out = market_structure_confluence_with_kernel(&input, kernel).map_err(|e| {
15523                IndicatorDispatchError::ComputeFailed {
15524                    indicator: "market_structure_confluence".to_string(),
15525                    details: e.to_string(),
15526                }
15527            })?;
15528            if output_id.eq_ignore_ascii_case("basis") {
15529                return Ok(out.basis);
15530            }
15531            if output_id.eq_ignore_ascii_case("upper_band")
15532                || output_id.eq_ignore_ascii_case("upper")
15533            {
15534                return Ok(out.upper_band);
15535            }
15536            if output_id.eq_ignore_ascii_case("lower_band")
15537                || output_id.eq_ignore_ascii_case("lower")
15538            {
15539                return Ok(out.lower_band);
15540            }
15541            if output_id.eq_ignore_ascii_case("structure_direction")
15542                || output_id.eq_ignore_ascii_case("direction")
15543                || output_id.eq_ignore_ascii_case("trend")
15544            {
15545                return Ok(out.structure_direction);
15546            }
15547            if output_id.eq_ignore_ascii_case("bullish_arrow") {
15548                return Ok(out.bullish_arrow);
15549            }
15550            if output_id.eq_ignore_ascii_case("bearish_arrow") {
15551                return Ok(out.bearish_arrow);
15552            }
15553            if output_id.eq_ignore_ascii_case("bullish_change") {
15554                return Ok(out.bullish_change);
15555            }
15556            if output_id.eq_ignore_ascii_case("bearish_change") {
15557                return Ok(out.bearish_change);
15558            }
15559            if output_id.eq_ignore_ascii_case("hh") {
15560                return Ok(out.hh);
15561            }
15562            if output_id.eq_ignore_ascii_case("lh") {
15563                return Ok(out.lh);
15564            }
15565            if output_id.eq_ignore_ascii_case("hl") {
15566                return Ok(out.hl);
15567            }
15568            if output_id.eq_ignore_ascii_case("ll") {
15569                return Ok(out.ll);
15570            }
15571            if output_id.eq_ignore_ascii_case("bullish_bos") {
15572                return Ok(out.bullish_bos);
15573            }
15574            if output_id.eq_ignore_ascii_case("bullish_choch") {
15575                return Ok(out.bullish_choch);
15576            }
15577            if output_id.eq_ignore_ascii_case("bearish_bos") {
15578                return Ok(out.bearish_bos);
15579            }
15580            if output_id.eq_ignore_ascii_case("bearish_choch") {
15581                return Ok(out.bearish_choch);
15582            }
15583            Err(IndicatorDispatchError::UnknownOutput {
15584                indicator: "market_structure_confluence".to_string(),
15585                output: output_id.to_string(),
15586            })
15587        },
15588    )
15589}
15590
15591fn compute_range_filtered_trend_signals_batch(
15592    req: IndicatorBatchRequest<'_>,
15593    output_id: &str,
15594) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15595    let (high, low, close) = extract_ohlc_input("range_filtered_trend_signals", req.data)?;
15596    let kernel = req.kernel.to_non_batch();
15597    collect_f64(
15598        "range_filtered_trend_signals",
15599        output_id,
15600        req.combos,
15601        close.len(),
15602        |params| {
15603            let kalman_alpha =
15604                get_f64_param("range_filtered_trend_signals", params, "kalman_alpha", 0.01)?;
15605            let kalman_beta =
15606                get_f64_param("range_filtered_trend_signals", params, "kalman_beta", 0.1)?;
15607            let kalman_period =
15608                get_usize_param("range_filtered_trend_signals", params, "kalman_period", 77)?;
15609            let dev = get_f64_param("range_filtered_trend_signals", params, "dev", 1.2)?;
15610            let supertrend_factor = get_f64_param(
15611                "range_filtered_trend_signals",
15612                params,
15613                "supertrend_factor",
15614                0.7,
15615            )?;
15616            let supertrend_atr_period = get_usize_param(
15617                "range_filtered_trend_signals",
15618                params,
15619                "supertrend_atr_period",
15620                7,
15621            )?;
15622            let input = RangeFilteredTrendSignalsInput::from_slices(
15623                high,
15624                low,
15625                close,
15626                RangeFilteredTrendSignalsParams {
15627                    kalman_alpha: Some(kalman_alpha),
15628                    kalman_beta: Some(kalman_beta),
15629                    kalman_period: Some(kalman_period),
15630                    dev: Some(dev),
15631                    supertrend_factor: Some(supertrend_factor),
15632                    supertrend_atr_period: Some(supertrend_atr_period),
15633                },
15634            );
15635            let out = range_filtered_trend_signals_with_kernel(&input, kernel).map_err(|e| {
15636                IndicatorDispatchError::ComputeFailed {
15637                    indicator: "range_filtered_trend_signals".to_string(),
15638                    details: e.to_string(),
15639                }
15640            })?;
15641            if output_id.eq_ignore_ascii_case("kalman") {
15642                return Ok(out.kalman);
15643            }
15644            if output_id.eq_ignore_ascii_case("supertrend") {
15645                return Ok(out.supertrend);
15646            }
15647            if output_id.eq_ignore_ascii_case("upper_band")
15648                || output_id.eq_ignore_ascii_case("upper")
15649            {
15650                return Ok(out.upper_band);
15651            }
15652            if output_id.eq_ignore_ascii_case("lower_band")
15653                || output_id.eq_ignore_ascii_case("lower")
15654            {
15655                return Ok(out.lower_band);
15656            }
15657            if output_id.eq_ignore_ascii_case("trend") {
15658                return Ok(out.trend);
15659            }
15660            if output_id.eq_ignore_ascii_case("kalman_trend")
15661                || output_id.eq_ignore_ascii_case("long_trend")
15662            {
15663                return Ok(out.kalman_trend);
15664            }
15665            if output_id.eq_ignore_ascii_case("state") {
15666                return Ok(out.state);
15667            }
15668            if output_id.eq_ignore_ascii_case("market_trending") {
15669                return Ok(out.market_trending);
15670            }
15671            if output_id.eq_ignore_ascii_case("market_ranging") {
15672                return Ok(out.market_ranging);
15673            }
15674            if output_id.eq_ignore_ascii_case("short_term_bullish") {
15675                return Ok(out.short_term_bullish);
15676            }
15677            if output_id.eq_ignore_ascii_case("short_term_bearish") {
15678                return Ok(out.short_term_bearish);
15679            }
15680            if output_id.eq_ignore_ascii_case("long_term_bullish") {
15681                return Ok(out.long_term_bullish);
15682            }
15683            if output_id.eq_ignore_ascii_case("long_term_bearish") {
15684                return Ok(out.long_term_bearish);
15685            }
15686            Err(IndicatorDispatchError::UnknownOutput {
15687                indicator: "range_filtered_trend_signals".to_string(),
15688                output: output_id.to_string(),
15689            })
15690        },
15691    )
15692}
15693
15694fn compute_volume_weighted_relative_strength_index_batch(
15695    req: IndicatorBatchRequest<'_>,
15696    output_id: &str,
15697) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15698    let (source, volume) =
15699        extract_close_volume_input("volume_weighted_relative_strength_index", req.data, "close")?;
15700    let kernel = req.kernel.to_non_batch();
15701    collect_f64(
15702        "volume_weighted_relative_strength_index",
15703        output_id,
15704        req.combos,
15705        source.len(),
15706        |params| {
15707            let rsi_length = get_usize_param(
15708                "volume_weighted_relative_strength_index",
15709                params,
15710                "rsi_length",
15711                14,
15712            )?;
15713            let range_length = get_usize_param(
15714                "volume_weighted_relative_strength_index",
15715                params,
15716                "range_length",
15717                10,
15718            )?;
15719            let ma_length = get_usize_param(
15720                "volume_weighted_relative_strength_index",
15721                params,
15722                "ma_length",
15723                14,
15724            )?;
15725            let ma_type = get_enum_param(
15726                "volume_weighted_relative_strength_index",
15727                params,
15728                "ma_type",
15729                "EMA",
15730            )?;
15731            let input = VolumeWeightedRelativeStrengthIndexInput::from_slices(
15732                source,
15733                volume,
15734                VolumeWeightedRelativeStrengthIndexParams {
15735                    rsi_length: Some(rsi_length),
15736                    range_length: Some(range_length),
15737                    ma_length: Some(ma_length),
15738                    ma_type: Some(ma_type),
15739                },
15740            );
15741            let out = volume_weighted_relative_strength_index_with_kernel(&input, kernel).map_err(
15742                |e| IndicatorDispatchError::ComputeFailed {
15743                    indicator: "volume_weighted_relative_strength_index".to_string(),
15744                    details: e.to_string(),
15745                },
15746            )?;
15747            if output_id.eq_ignore_ascii_case("rsi") || output_id.eq_ignore_ascii_case("value") {
15748                return Ok(out.rsi);
15749            }
15750            if output_id.eq_ignore_ascii_case("consolidation_strength")
15751                || output_id.eq_ignore_ascii_case("consolidation")
15752            {
15753                return Ok(out.consolidation_strength);
15754            }
15755            if output_id.eq_ignore_ascii_case("rsi_ma") || output_id.eq_ignore_ascii_case("ma") {
15756                return Ok(out.rsi_ma);
15757            }
15758            if output_id.eq_ignore_ascii_case("bearish_tp") {
15759                return Ok(out.bearish_tp);
15760            }
15761            if output_id.eq_ignore_ascii_case("bullish_tp") {
15762                return Ok(out.bullish_tp);
15763            }
15764            Err(IndicatorDispatchError::UnknownOutput {
15765                indicator: "volume_weighted_relative_strength_index".to_string(),
15766                output: output_id.to_string(),
15767            })
15768        },
15769    )
15770}
15771
15772fn compute_range_filter_batch(
15773    req: IndicatorBatchRequest<'_>,
15774    output_id: &str,
15775) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15776    let data = extract_slice_input("range_filter", req.data, "close")?;
15777    let kernel = req.kernel.to_non_batch();
15778    collect_f64(
15779        "range_filter",
15780        output_id,
15781        req.combos,
15782        data.len(),
15783        |params| {
15784            let range_size = get_f64_param("range_filter", params, "range_size", 2.618)?;
15785            let range_period = get_usize_param("range_filter", params, "range_period", 14)?;
15786            let smooth_range = get_bool_param("range_filter", params, "smooth_range", true)?;
15787            let smooth_period = get_usize_param("range_filter", params, "smooth_period", 27)?;
15788            let input = RangeFilterInput::from_slice(
15789                data,
15790                RangeFilterParams {
15791                    range_size: Some(range_size),
15792                    range_period: Some(range_period),
15793                    smooth_range: Some(smooth_range),
15794                    smooth_period: Some(smooth_period),
15795                },
15796            );
15797            let out = range_filter_with_kernel(&input, kernel).map_err(|e| {
15798                IndicatorDispatchError::ComputeFailed {
15799                    indicator: "range_filter".to_string(),
15800                    details: e.to_string(),
15801                }
15802            })?;
15803            if output_id.eq_ignore_ascii_case("filter") || output_id.eq_ignore_ascii_case("value") {
15804                return Ok(out.filter);
15805            }
15806            if output_id.eq_ignore_ascii_case("high_band") || output_id.eq_ignore_ascii_case("high")
15807            {
15808                return Ok(out.high_band);
15809            }
15810            if output_id.eq_ignore_ascii_case("low_band") || output_id.eq_ignore_ascii_case("low") {
15811                return Ok(out.low_band);
15812            }
15813            Err(IndicatorDispatchError::UnknownOutput {
15814                indicator: "range_filter".to_string(),
15815                output: output_id.to_string(),
15816            })
15817        },
15818    )
15819}
15820
15821fn compute_rsmk_batch(
15822    req: IndicatorBatchRequest<'_>,
15823    output_id: &str,
15824) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15825    let (main, compare) = match req.data {
15826        IndicatorDataRef::CloseVolume { close, volume } => {
15827            ensure_same_len_2("rsmk", close.len(), volume.len())?;
15828            (close, volume)
15829        }
15830        IndicatorDataRef::Ohlcv {
15831            open,
15832            high,
15833            low,
15834            close,
15835            volume,
15836        } => {
15837            ensure_same_len_5(
15838                "rsmk",
15839                open.len(),
15840                high.len(),
15841                low.len(),
15842                close.len(),
15843                volume.len(),
15844            )?;
15845            (close, volume)
15846        }
15847        IndicatorDataRef::Candles { candles, source } => (
15848            source_type(candles, source.unwrap_or("close")),
15849            candles.volume.as_slice(),
15850        ),
15851        _ => {
15852            return Err(IndicatorDispatchError::MissingRequiredInput {
15853                indicator: "rsmk".to_string(),
15854                input: IndicatorInputKind::CloseVolume,
15855            });
15856        }
15857    };
15858    let kernel = req.kernel.to_non_batch();
15859    collect_f64("rsmk", output_id, req.combos, main.len(), |params| {
15860        let lookback = get_usize_param("rsmk", params, "lookback", 90)?;
15861        let period = get_usize_param("rsmk", params, "period", 3)?;
15862        let signal_period = get_usize_param("rsmk", params, "signal_period", 20)?;
15863        let matype = get_enum_param("rsmk", params, "matype", "ema")?;
15864        let signal_matype = get_enum_param("rsmk", params, "signal_matype", "ema")?;
15865        let input = RsmkInput::from_slices(
15866            main,
15867            compare,
15868            RsmkParams {
15869                lookback: Some(lookback),
15870                period: Some(period),
15871                signal_period: Some(signal_period),
15872                matype: Some(matype),
15873                signal_matype: Some(signal_matype),
15874            },
15875        );
15876        let out = rsmk_with_kernel(&input, kernel).map_err(|e| {
15877            IndicatorDispatchError::ComputeFailed {
15878                indicator: "rsmk".to_string(),
15879                details: e.to_string(),
15880            }
15881        })?;
15882        if output_id.eq_ignore_ascii_case("indicator") || output_id.eq_ignore_ascii_case("value") {
15883            return Ok(out.indicator);
15884        }
15885        if output_id.eq_ignore_ascii_case("signal") {
15886            return Ok(out.signal);
15887        }
15888        Err(IndicatorDispatchError::UnknownOutput {
15889            indicator: "rsmk".to_string(),
15890            output: output_id.to_string(),
15891        })
15892    })
15893}
15894
15895fn compute_voss_batch(
15896    req: IndicatorBatchRequest<'_>,
15897    output_id: &str,
15898) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15899    let data = extract_slice_input("voss", req.data, "close")?;
15900    let kernel = req.kernel.to_non_batch();
15901    collect_f64("voss", output_id, req.combos, data.len(), |params| {
15902        let period = get_usize_param("voss", params, "period", 20)?;
15903        let predict = get_usize_param("voss", params, "predict", 3)?;
15904        let bandwidth = get_f64_param("voss", params, "bandwidth", 0.25)?;
15905        let input = VossInput::from_slice(
15906            data,
15907            VossParams {
15908                period: Some(period),
15909                predict: Some(predict),
15910                bandwidth: Some(bandwidth),
15911            },
15912        );
15913        let out = voss_with_kernel(&input, kernel).map_err(|e| {
15914            IndicatorDispatchError::ComputeFailed {
15915                indicator: "voss".to_string(),
15916                details: e.to_string(),
15917            }
15918        })?;
15919        if output_id.eq_ignore_ascii_case("voss") || output_id.eq_ignore_ascii_case("value") {
15920            return Ok(out.voss);
15921        }
15922        if output_id.eq_ignore_ascii_case("filt") || output_id.eq_ignore_ascii_case("filter") {
15923            return Ok(out.filt);
15924        }
15925        Err(IndicatorDispatchError::UnknownOutput {
15926            indicator: "voss".to_string(),
15927            output: output_id.to_string(),
15928        })
15929    })
15930}
15931
15932fn compute_stc_batch(
15933    req: IndicatorBatchRequest<'_>,
15934    output_id: &str,
15935) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15936    let data = extract_slice_input("stc", req.data, "close")?;
15937    let kernel = req.kernel.to_non_batch();
15938    collect_f64("stc", output_id, req.combos, data.len(), |params| {
15939        let fast_period = get_usize_param("stc", params, "fast_period", 23)?;
15940        let slow_period = get_usize_param("stc", params, "slow_period", 50)?;
15941        let k_period = get_usize_param("stc", params, "k_period", 10)?;
15942        let d_period = get_usize_param("stc", params, "d_period", 3)?;
15943        let input = StcInput::from_slice(
15944            data,
15945            StcParams {
15946                fast_period: Some(fast_period),
15947                slow_period: Some(slow_period),
15948                k_period: Some(k_period),
15949                d_period: Some(d_period),
15950                fast_ma_type: Some("ema".to_string()),
15951                slow_ma_type: Some("ema".to_string()),
15952            },
15953        );
15954        let out =
15955            stc_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15956                indicator: "stc".to_string(),
15957                details: e.to_string(),
15958            })?;
15959        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15960            return Ok(out.values);
15961        }
15962        Err(IndicatorDispatchError::UnknownOutput {
15963            indicator: "stc".to_string(),
15964            output: output_id.to_string(),
15965        })
15966    })
15967}
15968
15969fn compute_rvi_batch(
15970    req: IndicatorBatchRequest<'_>,
15971    output_id: &str,
15972) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
15973    let data = extract_slice_input("rvi", req.data, "close")?;
15974    let kernel = req.kernel.to_non_batch();
15975    collect_f64("rvi", output_id, req.combos, data.len(), |params| {
15976        let period = get_usize_param("rvi", params, "period", 10)?;
15977        let ma_len = get_usize_param("rvi", params, "ma_len", 14)?;
15978        let matype = get_usize_param("rvi", params, "matype", 1)?;
15979        let devtype = get_usize_param("rvi", params, "devtype", 0)?;
15980        let input = RviInput::from_slice(
15981            data,
15982            RviParams {
15983                period: Some(period),
15984                ma_len: Some(ma_len),
15985                matype: Some(matype),
15986                devtype: Some(devtype),
15987            },
15988        );
15989        let out =
15990            rvi_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
15991                indicator: "rvi".to_string(),
15992                details: e.to_string(),
15993            })?;
15994        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
15995            return Ok(out.values);
15996        }
15997        Err(IndicatorDispatchError::UnknownOutput {
15998            indicator: "rvi".to_string(),
15999            output: output_id.to_string(),
16000        })
16001    })
16002}
16003
16004fn compute_coppock_batch(
16005    req: IndicatorBatchRequest<'_>,
16006    output_id: &str,
16007) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
16008    let data = extract_slice_input("coppock", req.data, "close")?;
16009    let kernel = req.kernel.to_non_batch();
16010    collect_f64("coppock", output_id, req.combos, data.len(), |params| {
16011        let short_roc_period = get_usize_param("coppock", params, "short_roc_period", 11)?;
16012        let long_roc_period = get_usize_param("coppock", params, "long_roc_period", 14)?;
16013        let ma_period = get_usize_param("coppock", params, "ma_period", 10)?;
16014        let input = CoppockInput::from_slice(
16015            data,
16016            CoppockParams {
16017                short_roc_period: Some(short_roc_period),
16018                long_roc_period: Some(long_roc_period),
16019                ma_period: Some(ma_period),
16020                ma_type: Some("wma".to_string()),
16021            },
16022        );
16023        let out = coppock_with_kernel(&input, kernel).map_err(|e| {
16024            IndicatorDispatchError::ComputeFailed {
16025                indicator: "coppock".to_string(),
16026                details: e.to_string(),
16027            }
16028        })?;
16029        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
16030            return Ok(out.values);
16031        }
16032        Err(IndicatorDispatchError::UnknownOutput {
16033            indicator: "coppock".to_string(),
16034            output: output_id.to_string(),
16035        })
16036    })
16037}
16038
16039fn compute_correl_hl_batch(
16040    req: IndicatorBatchRequest<'_>,
16041    output_id: &str,
16042) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
16043    expect_value_output("correl_hl", output_id)?;
16044    let (high, low) = extract_high_low_input("correl_hl", req.data)?;
16045    let kernel = req.kernel.to_non_batch();
16046    collect_f64("correl_hl", output_id, req.combos, high.len(), |params| {
16047        let period = get_usize_param("correl_hl", params, "period", 9)?;
16048        let input = CorrelHlInput::from_slices(
16049            high,
16050            low,
16051            CorrelHlParams {
16052                period: Some(period),
16053            },
16054        );
16055        let out = correl_hl_with_kernel(&input, kernel).map_err(|e| {
16056            IndicatorDispatchError::ComputeFailed {
16057                indicator: "correl_hl".to_string(),
16058                details: e.to_string(),
16059            }
16060        })?;
16061        Ok(out.values)
16062    })
16063}
16064
16065fn compute_net_myrsi_batch(
16066    req: IndicatorBatchRequest<'_>,
16067    output_id: &str,
16068) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
16069    let data = extract_slice_input("net_myrsi", req.data, "close")?;
16070    let kernel = req.kernel.to_non_batch();
16071    collect_f64("net_myrsi", output_id, req.combos, data.len(), |params| {
16072        let period = get_usize_param("net_myrsi", params, "period", 14)?;
16073        let input = NetMyrsiInput::from_slice(
16074            data,
16075            NetMyrsiParams {
16076                period: Some(period),
16077            },
16078        );
16079        let out = net_myrsi_with_kernel(&input, kernel).map_err(|e| {
16080            IndicatorDispatchError::ComputeFailed {
16081                indicator: "net_myrsi".to_string(),
16082                details: e.to_string(),
16083            }
16084        })?;
16085        if output_id.eq_ignore_ascii_case("value") || output_id.eq_ignore_ascii_case("values") {
16086            return Ok(out.values);
16087        }
16088        Err(IndicatorDispatchError::UnknownOutput {
16089            indicator: "net_myrsi".to_string(),
16090            output: output_id.to_string(),
16091        })
16092    })
16093}
16094
16095fn compute_pivot_batch(
16096    req: IndicatorBatchRequest<'_>,
16097    output_id: &str,
16098) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
16099    let (open, high, low, close) = extract_ohlc_full_input("pivot", req.data)?;
16100    let kernel = req.kernel.to_non_batch();
16101    collect_f64("pivot", output_id, req.combos, close.len(), |params| {
16102        let mode = get_usize_param("pivot", params, "mode", 3)?;
16103        let input =
16104            PivotInput::from_slices(high, low, close, open, PivotParams { mode: Some(mode) });
16105        let out = pivot_with_kernel(&input, kernel).map_err(|e| {
16106            IndicatorDispatchError::ComputeFailed {
16107                indicator: "pivot".to_string(),
16108                details: e.to_string(),
16109            }
16110        })?;
16111        if output_id.eq_ignore_ascii_case("pp") || output_id.eq_ignore_ascii_case("value") {
16112            return Ok(out.pp);
16113        }
16114        if output_id.eq_ignore_ascii_case("r1") {
16115            return Ok(out.r1);
16116        }
16117        if output_id.eq_ignore_ascii_case("r2") {
16118            return Ok(out.r2);
16119        }
16120        if output_id.eq_ignore_ascii_case("r3") {
16121            return Ok(out.r3);
16122        }
16123        if output_id.eq_ignore_ascii_case("r4") {
16124            return Ok(out.r4);
16125        }
16126        if output_id.eq_ignore_ascii_case("s1") {
16127            return Ok(out.s1);
16128        }
16129        if output_id.eq_ignore_ascii_case("s2") {
16130            return Ok(out.s2);
16131        }
16132        if output_id.eq_ignore_ascii_case("s3") {
16133            return Ok(out.s3);
16134        }
16135        if output_id.eq_ignore_ascii_case("s4") {
16136            return Ok(out.s4);
16137        }
16138        Err(IndicatorDispatchError::UnknownOutput {
16139            indicator: "pivot".to_string(),
16140            output: output_id.to_string(),
16141        })
16142    })
16143}
16144
16145fn compute_wad_batch(
16146    req: IndicatorBatchRequest<'_>,
16147    output_id: &str,
16148) -> Result<IndicatorBatchOutput, IndicatorDispatchError> {
16149    expect_value_output("wad", output_id)?;
16150    let (_open, high, low, close) = extract_ohlc_full_input("wad", req.data)?;
16151    let kernel = req.kernel.to_non_batch();
16152    collect_f64("wad", output_id, req.combos, close.len(), |_params| {
16153        let input = WadInput::from_slices(high, low, close);
16154        let out =
16155            wad_with_kernel(&input, kernel).map_err(|e| IndicatorDispatchError::ComputeFailed {
16156                indicator: "wad".to_string(),
16157                details: e.to_string(),
16158            })?;
16159        Ok(out.values)
16160    })
16161}
16162
16163fn ma_data_from_req<'a>(
16164    indicator: &str,
16165    data: IndicatorDataRef<'a>,
16166) -> Result<MaData<'a>, IndicatorDispatchError> {
16167    match data {
16168        IndicatorDataRef::Slice { values } => Ok(MaData::Slice(values)),
16169        IndicatorDataRef::Candles { candles, source } => Ok(MaData::Candles {
16170            candles,
16171            source: source.unwrap_or("close"),
16172        }),
16173        IndicatorDataRef::Ohlc { close, .. } => Ok(MaData::Slice(close)),
16174        IndicatorDataRef::Ohlcv { close, .. } => Ok(MaData::Slice(close)),
16175        IndicatorDataRef::CloseVolume { close, .. } => Ok(MaData::Slice(close)),
16176        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
16177            indicator: indicator.to_string(),
16178            input: IndicatorInputKind::Slice,
16179        }),
16180    }
16181}
16182
16183fn ma_len_from_req(
16184    indicator: &str,
16185    data: IndicatorDataRef<'_>,
16186) -> Result<usize, IndicatorDispatchError> {
16187    match data {
16188        IndicatorDataRef::Slice { values } => Ok(values.len()),
16189        IndicatorDataRef::Candles { candles, source } => {
16190            Ok(source_type(candles, source.unwrap_or("close")).len())
16191        }
16192        IndicatorDataRef::Ohlc { close, .. } => Ok(close.len()),
16193        IndicatorDataRef::Ohlcv { close, .. } => Ok(close.len()),
16194        IndicatorDataRef::CloseVolume { close, .. } => Ok(close.len()),
16195        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
16196            indicator: indicator.to_string(),
16197            input: IndicatorInputKind::Slice,
16198        }),
16199    }
16200}
16201
16202fn ma_period_for_combo(
16203    info: &IndicatorInfo,
16204    params: &[ParamKV<'_>],
16205) -> Result<usize, IndicatorDispatchError> {
16206    if let Some(v) = find_param(params, "period") {
16207        return parse_usize_param_value(info.id, "period", v);
16208    }
16209    if let Some(default) = info
16210        .params
16211        .iter()
16212        .find(|p| p.key.eq_ignore_ascii_case("period"))
16213        .and_then(|p| p.default.as_ref())
16214    {
16215        if let ParamValueStatic::Int(v) = default {
16216            if *v >= 0 {
16217                return Ok(*v as usize);
16218            }
16219        }
16220    }
16221    Ok(14)
16222}
16223
16224fn convert_ma_params<'a>(
16225    params: &'a [ParamKV<'a>],
16226    indicator: &str,
16227    output_id: &str,
16228) -> Result<Vec<MaBatchParamKV<'a>>, IndicatorDispatchError> {
16229    let mut out = Vec::with_capacity(params.len());
16230    for p in params {
16231        if p.key.eq_ignore_ascii_case("period") {
16232            continue;
16233        }
16234        if p.key.eq_ignore_ascii_case("output") {
16235            let selected = match p.value {
16236                ParamValue::EnumString(v) => v,
16237                _ => {
16238                    return Err(IndicatorDispatchError::InvalidParam {
16239                        indicator: indicator.to_string(),
16240                        key: "output".to_string(),
16241                        reason: "expected EnumString".to_string(),
16242                    })
16243                }
16244            };
16245            if !selected.eq_ignore_ascii_case(output_id) {
16246                return Err(IndicatorDispatchError::InvalidParam {
16247                    indicator: indicator.to_string(),
16248                    key: "output".to_string(),
16249                    reason: format!(
16250                        "param output '{}' does not match requested output_id '{}'",
16251                        selected, output_id
16252                    ),
16253                });
16254            }
16255        }
16256        let value = match p.value {
16257            ParamValue::Int(v) => MaBatchParamValue::Int(v),
16258            ParamValue::Float(v) => {
16259                if !v.is_finite() {
16260                    return Err(IndicatorDispatchError::InvalidParam {
16261                        indicator: indicator.to_string(),
16262                        key: p.key.to_string(),
16263                        reason: "expected finite float".to_string(),
16264                    });
16265                }
16266                MaBatchParamValue::Float(v)
16267            }
16268            ParamValue::Bool(v) => MaBatchParamValue::Bool(v),
16269            ParamValue::EnumString(v) => MaBatchParamValue::EnumString(v),
16270        };
16271        out.push(MaBatchParamKV { key: p.key, value });
16272    }
16273    Ok(out)
16274}
16275
16276fn extract_slice_input<'a>(
16277    indicator: &str,
16278    data: IndicatorDataRef<'a>,
16279    default_source: &'a str,
16280) -> Result<&'a [f64], IndicatorDispatchError> {
16281    match data {
16282        IndicatorDataRef::Slice { values } => Ok(values),
16283        IndicatorDataRef::Candles { candles, source } => {
16284            Ok(source_type(candles, source.unwrap_or(default_source)))
16285        }
16286        IndicatorDataRef::Ohlc { close, .. } => Ok(close),
16287        IndicatorDataRef::Ohlcv { close, .. } => Ok(close),
16288        IndicatorDataRef::CloseVolume { close, .. } => Ok(close),
16289        IndicatorDataRef::HighLow { .. } => Err(IndicatorDispatchError::MissingRequiredInput {
16290            indicator: indicator.to_string(),
16291            input: IndicatorInputKind::Slice,
16292        }),
16293    }
16294}
16295
16296fn extract_ohlc_input<'a>(
16297    indicator: &str,
16298    data: IndicatorDataRef<'a>,
16299) -> Result<(&'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16300    match data {
16301        IndicatorDataRef::Candles { candles, .. } => Ok((
16302            candles.high.as_slice(),
16303            candles.low.as_slice(),
16304            candles.close.as_slice(),
16305        )),
16306        IndicatorDataRef::Ohlc {
16307            high,
16308            low,
16309            close,
16310            open,
16311        } => {
16312            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
16313            Ok((high, low, close))
16314        }
16315        IndicatorDataRef::Ohlcv {
16316            high,
16317            low,
16318            close,
16319            open,
16320            volume,
16321        } => {
16322            ensure_same_len_5(
16323                indicator,
16324                open.len(),
16325                high.len(),
16326                low.len(),
16327                close.len(),
16328                volume.len(),
16329            )?;
16330            Ok((high, low, close))
16331        }
16332        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16333            indicator: indicator.to_string(),
16334            input: IndicatorInputKind::Ohlc,
16335        }),
16336    }
16337}
16338
16339fn extract_ohlc_full_input<'a>(
16340    indicator: &str,
16341    data: IndicatorDataRef<'a>,
16342) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16343    match data {
16344        IndicatorDataRef::Candles { candles, .. } => Ok((
16345            candles.open.as_slice(),
16346            candles.high.as_slice(),
16347            candles.low.as_slice(),
16348            candles.close.as_slice(),
16349        )),
16350        IndicatorDataRef::Ohlc {
16351            open,
16352            high,
16353            low,
16354            close,
16355        } => {
16356            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
16357            Ok((open, high, low, close))
16358        }
16359        IndicatorDataRef::Ohlcv {
16360            open,
16361            high,
16362            low,
16363            close,
16364            volume,
16365        } => {
16366            ensure_same_len_5(
16367                indicator,
16368                open.len(),
16369                high.len(),
16370                low.len(),
16371                close.len(),
16372                volume.len(),
16373            )?;
16374            Ok((open, high, low, close))
16375        }
16376        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16377            indicator: indicator.to_string(),
16378            input: IndicatorInputKind::Ohlc,
16379        }),
16380    }
16381}
16382
16383fn extract_ohlcv_full_input<'a>(
16384    indicator: &str,
16385    data: IndicatorDataRef<'a>,
16386) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16387    match data {
16388        IndicatorDataRef::Candles { candles, .. } => Ok((
16389            candles.open.as_slice(),
16390            candles.high.as_slice(),
16391            candles.low.as_slice(),
16392            candles.close.as_slice(),
16393            candles.volume.as_slice(),
16394        )),
16395        IndicatorDataRef::Ohlcv {
16396            open,
16397            high,
16398            low,
16399            close,
16400            volume,
16401        } => {
16402            ensure_same_len_5(
16403                indicator,
16404                open.len(),
16405                high.len(),
16406                low.len(),
16407                close.len(),
16408                volume.len(),
16409            )?;
16410            Ok((open, high, low, close, volume))
16411        }
16412        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16413            indicator: indicator.to_string(),
16414            input: IndicatorInputKind::Ohlcv,
16415        }),
16416    }
16417}
16418
16419fn extract_high_low_input<'a>(
16420    indicator: &str,
16421    data: IndicatorDataRef<'a>,
16422) -> Result<(&'a [f64], &'a [f64]), IndicatorDispatchError> {
16423    match data {
16424        IndicatorDataRef::Candles { candles, .. } => {
16425            Ok((candles.high.as_slice(), candles.low.as_slice()))
16426        }
16427        IndicatorDataRef::Ohlc {
16428            high,
16429            low,
16430            open,
16431            close,
16432        } => {
16433            ensure_same_len_4(indicator, open.len(), high.len(), low.len(), close.len())?;
16434            Ok((high, low))
16435        }
16436        IndicatorDataRef::Ohlcv {
16437            high,
16438            low,
16439            open,
16440            close,
16441            volume,
16442        } => {
16443            ensure_same_len_5(
16444                indicator,
16445                open.len(),
16446                high.len(),
16447                low.len(),
16448                close.len(),
16449                volume.len(),
16450            )?;
16451            Ok((high, low))
16452        }
16453        IndicatorDataRef::HighLow { high, low } => {
16454            ensure_same_len_2(indicator, high.len(), low.len())?;
16455            Ok((high, low))
16456        }
16457        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16458            indicator: indicator.to_string(),
16459            input: IndicatorInputKind::HighLow,
16460        }),
16461    }
16462}
16463
16464fn extract_hlcv_input<'a>(
16465    indicator: &str,
16466    data: IndicatorDataRef<'a>,
16467) -> Result<(&'a [f64], &'a [f64], &'a [f64], &'a [f64]), IndicatorDispatchError> {
16468    match data {
16469        IndicatorDataRef::Candles { candles, .. } => Ok((
16470            candles.high.as_slice(),
16471            candles.low.as_slice(),
16472            candles.close.as_slice(),
16473            candles.volume.as_slice(),
16474        )),
16475        IndicatorDataRef::Ohlcv {
16476            open,
16477            high,
16478            low,
16479            close,
16480            volume,
16481        } => {
16482            ensure_same_len_5(
16483                indicator,
16484                open.len(),
16485                high.len(),
16486                low.len(),
16487                close.len(),
16488                volume.len(),
16489            )?;
16490            Ok((high, low, close, volume))
16491        }
16492        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16493            indicator: indicator.to_string(),
16494            input: IndicatorInputKind::Ohlcv,
16495        }),
16496    }
16497}
16498
16499fn extract_volume_input<'a>(
16500    indicator: &str,
16501    data: IndicatorDataRef<'a>,
16502) -> Result<&'a [f64], IndicatorDispatchError> {
16503    match data {
16504        IndicatorDataRef::Slice { values } => Ok(values),
16505        IndicatorDataRef::Candles { candles, source } => {
16506            Ok(source_type(candles, source.unwrap_or("volume")))
16507        }
16508        IndicatorDataRef::CloseVolume { close, volume } => {
16509            ensure_same_len_2(indicator, close.len(), volume.len())?;
16510            Ok(volume)
16511        }
16512        IndicatorDataRef::Ohlcv {
16513            open,
16514            high,
16515            low,
16516            close,
16517            volume,
16518        } => {
16519            ensure_same_len_5(
16520                indicator,
16521                open.len(),
16522                high.len(),
16523                low.len(),
16524                close.len(),
16525                volume.len(),
16526            )?;
16527            Ok(volume)
16528        }
16529        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16530            indicator: indicator.to_string(),
16531            input: IndicatorInputKind::Slice,
16532        }),
16533    }
16534}
16535
16536fn extract_close_volume_input<'a>(
16537    indicator: &str,
16538    data: IndicatorDataRef<'a>,
16539    default_close_source: &'a str,
16540) -> Result<(&'a [f64], &'a [f64]), IndicatorDispatchError> {
16541    match data {
16542        IndicatorDataRef::CloseVolume { close, volume } => {
16543            ensure_same_len_2(indicator, close.len(), volume.len())?;
16544            Ok((close, volume))
16545        }
16546        IndicatorDataRef::Ohlcv {
16547            close,
16548            volume,
16549            open,
16550            high,
16551            low,
16552        } => {
16553            ensure_same_len_5(
16554                indicator,
16555                open.len(),
16556                high.len(),
16557                low.len(),
16558                close.len(),
16559                volume.len(),
16560            )?;
16561            Ok((close, volume))
16562        }
16563        IndicatorDataRef::Candles { candles, source } => {
16564            let close = source_type(candles, source.unwrap_or(default_close_source));
16565            let volume = candles.volume.as_slice();
16566            ensure_same_len_2(indicator, close.len(), volume.len())?;
16567            Ok((close, volume))
16568        }
16569        _ => Err(IndicatorDispatchError::MissingRequiredInput {
16570            indicator: indicator.to_string(),
16571            input: IndicatorInputKind::CloseVolume,
16572        }),
16573    }
16574}
16575
16576fn f64_output(output_id: &str, rows: usize, cols: usize, values: Vec<f64>) -> IndicatorBatchOutput {
16577    IndicatorBatchOutput {
16578        output_id: output_id.to_string(),
16579        rows,
16580        cols,
16581        values_f64: Some(values),
16582        values_i32: None,
16583        values_bool: None,
16584    }
16585}
16586
16587fn bool_output(
16588    output_id: &str,
16589    rows: usize,
16590    cols: usize,
16591    values: Vec<bool>,
16592) -> IndicatorBatchOutput {
16593    IndicatorBatchOutput {
16594        output_id: output_id.to_string(),
16595        rows,
16596        cols,
16597        values_f64: None,
16598        values_i32: None,
16599        values_bool: Some(values),
16600    }
16601}
16602
16603fn expect_value_output(indicator: &str, output_id: &str) -> Result<(), IndicatorDispatchError> {
16604    if output_id.eq_ignore_ascii_case("value") {
16605        return Ok(());
16606    }
16607    Err(IndicatorDispatchError::UnknownOutput {
16608        indicator: indicator.to_string(),
16609        output: output_id.to_string(),
16610    })
16611}
16612
16613fn ensure_len(indicator: &str, expected: usize, got: usize) -> Result<(), IndicatorDispatchError> {
16614    if expected == got {
16615        return Ok(());
16616    }
16617    Err(IndicatorDispatchError::DataLengthMismatch {
16618        details: format!("{indicator}: expected output length {expected}, got {got}"),
16619    })
16620}
16621
16622fn ensure_same_len_2(indicator: &str, a: usize, b: usize) -> Result<(), IndicatorDispatchError> {
16623    if a == b {
16624        return Ok(());
16625    }
16626    Err(IndicatorDispatchError::DataLengthMismatch {
16627        details: format!("{indicator}: expected equal lengths, got {a} and {b}"),
16628    })
16629}
16630
16631fn ensure_same_len_3(
16632    indicator: &str,
16633    a: usize,
16634    b: usize,
16635    c: usize,
16636) -> Result<(), IndicatorDispatchError> {
16637    if a == b && b == c {
16638        return Ok(());
16639    }
16640    Err(IndicatorDispatchError::DataLengthMismatch {
16641        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}"),
16642    })
16643}
16644
16645fn ensure_same_len_4(
16646    indicator: &str,
16647    a: usize,
16648    b: usize,
16649    c: usize,
16650    d: usize,
16651) -> Result<(), IndicatorDispatchError> {
16652    if a == b && b == c && c == d {
16653        return Ok(());
16654    }
16655    Err(IndicatorDispatchError::DataLengthMismatch {
16656        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}, {d}"),
16657    })
16658}
16659
16660fn ensure_same_len_5(
16661    indicator: &str,
16662    a: usize,
16663    b: usize,
16664    c: usize,
16665    d: usize,
16666    e: usize,
16667) -> Result<(), IndicatorDispatchError> {
16668    if a == b && b == c && c == d && d == e {
16669        return Ok(());
16670    }
16671    Err(IndicatorDispatchError::DataLengthMismatch {
16672        details: format!("{indicator}: expected equal lengths, got {a}, {b}, {c}, {d}, {e}"),
16673    })
16674}
16675
16676fn has_key(params: &[ParamKV<'_>], key: &str) -> bool {
16677    params.iter().any(|kv| kv.key.eq_ignore_ascii_case(key))
16678}
16679
16680fn find_param<'a>(params: &'a [ParamKV<'a>], key: &str) -> Option<&'a ParamValue<'a>> {
16681    params
16682        .iter()
16683        .rev()
16684        .find(|kv| kv.key.eq_ignore_ascii_case(key))
16685        .map(|kv| &kv.value)
16686}
16687
16688fn get_usize_param(
16689    indicator: &str,
16690    params: &[ParamKV<'_>],
16691    key: &str,
16692    default: usize,
16693) -> Result<usize, IndicatorDispatchError> {
16694    match find_param(params, key) {
16695        Some(v) => parse_usize_param_value(indicator, key, v),
16696        None => Ok(default),
16697    }
16698}
16699
16700fn get_usize_param_with_aliases(
16701    indicator: &str,
16702    params: &[ParamKV<'_>],
16703    keys: &[&str],
16704    default: usize,
16705) -> Result<usize, IndicatorDispatchError> {
16706    for key in keys {
16707        if let Some(v) = find_param(params, key) {
16708            return parse_usize_param_value(indicator, key, v);
16709        }
16710    }
16711    Ok(default)
16712}
16713
16714fn get_f64_param_with_aliases(
16715    indicator: &str,
16716    params: &[ParamKV<'_>],
16717    keys: &[&str],
16718    default: f64,
16719) -> Result<f64, IndicatorDispatchError> {
16720    for key in keys {
16721        match find_param(params, key) {
16722            Some(ParamValue::Int(v)) => return Ok(*v as f64),
16723            Some(ParamValue::Float(v)) => {
16724                if v.is_finite() {
16725                    return Ok(*v);
16726                }
16727                return Err(IndicatorDispatchError::InvalidParam {
16728                    indicator: indicator.to_string(),
16729                    key: key.to_string(),
16730                    reason: "expected finite float".to_string(),
16731                });
16732            }
16733            Some(_) => {
16734                return Err(IndicatorDispatchError::InvalidParam {
16735                    indicator: indicator.to_string(),
16736                    key: key.to_string(),
16737                    reason: "expected Int or Float".to_string(),
16738                });
16739            }
16740            None => continue,
16741        }
16742    }
16743    Ok(default)
16744}
16745
16746fn parse_usize_param_value(
16747    indicator: &str,
16748    key: &str,
16749    value: &ParamValue<'_>,
16750) -> Result<usize, IndicatorDispatchError> {
16751    match value {
16752        ParamValue::Int(v) => {
16753            if *v < 0 {
16754                return Err(IndicatorDispatchError::InvalidParam {
16755                    indicator: indicator.to_string(),
16756                    key: key.to_string(),
16757                    reason: "expected integer >= 0".to_string(),
16758                });
16759            }
16760            Ok(*v as usize)
16761        }
16762        ParamValue::Float(v) => {
16763            if !v.is_finite() {
16764                return Err(IndicatorDispatchError::InvalidParam {
16765                    indicator: indicator.to_string(),
16766                    key: key.to_string(),
16767                    reason: "expected finite number".to_string(),
16768                });
16769            }
16770            if *v < 0.0 {
16771                return Err(IndicatorDispatchError::InvalidParam {
16772                    indicator: indicator.to_string(),
16773                    key: key.to_string(),
16774                    reason: "expected number >= 0".to_string(),
16775                });
16776            }
16777            let r = v.round();
16778            if (*v - r).abs() > 1e-9 {
16779                return Err(IndicatorDispatchError::InvalidParam {
16780                    indicator: indicator.to_string(),
16781                    key: key.to_string(),
16782                    reason: "expected integer value".to_string(),
16783                });
16784            }
16785            Ok(r as usize)
16786        }
16787        _ => Err(IndicatorDispatchError::InvalidParam {
16788            indicator: indicator.to_string(),
16789            key: key.to_string(),
16790            reason: "expected Int or Float".to_string(),
16791        }),
16792    }
16793}
16794
16795fn get_f64_param(
16796    indicator: &str,
16797    params: &[ParamKV<'_>],
16798    key: &str,
16799    default: f64,
16800) -> Result<f64, IndicatorDispatchError> {
16801    match find_param(params, key) {
16802        Some(ParamValue::Int(v)) => Ok(*v as f64),
16803        Some(ParamValue::Float(v)) => {
16804            if v.is_finite() {
16805                Ok(*v)
16806            } else {
16807                Err(IndicatorDispatchError::InvalidParam {
16808                    indicator: indicator.to_string(),
16809                    key: key.to_string(),
16810                    reason: "expected finite float".to_string(),
16811                })
16812            }
16813        }
16814        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16815            indicator: indicator.to_string(),
16816            key: key.to_string(),
16817            reason: "expected Int or Float".to_string(),
16818        }),
16819        None => Ok(default),
16820    }
16821}
16822
16823fn get_bool_param(
16824    indicator: &str,
16825    params: &[ParamKV<'_>],
16826    key: &str,
16827    default: bool,
16828) -> Result<bool, IndicatorDispatchError> {
16829    match find_param(params, key) {
16830        Some(ParamValue::Bool(v)) => Ok(*v),
16831        Some(ParamValue::Int(v)) => match *v {
16832            0 => Ok(false),
16833            1 => Ok(true),
16834            _ => Err(IndicatorDispatchError::InvalidParam {
16835                indicator: indicator.to_string(),
16836                key: key.to_string(),
16837                reason: "expected Bool or Int(0/1)".to_string(),
16838            }),
16839        },
16840        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16841            indicator: indicator.to_string(),
16842            key: key.to_string(),
16843            reason: "expected Bool".to_string(),
16844        }),
16845        None => Ok(default),
16846    }
16847}
16848
16849fn get_enum_string_param<'a>(
16850    indicator: &str,
16851    params: &'a [ParamKV<'a>],
16852    key: &str,
16853    default: &'a str,
16854) -> Result<&'a str, IndicatorDispatchError> {
16855    match find_param(params, key) {
16856        Some(ParamValue::EnumString(v)) => Ok(v),
16857        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16858            indicator: indicator.to_string(),
16859            key: key.to_string(),
16860            reason: "expected EnumString".to_string(),
16861        }),
16862        None => Ok(default),
16863    }
16864}
16865
16866fn get_i32_param(
16867    indicator: &str,
16868    params: &[ParamKV<'_>],
16869    key: &str,
16870    default: i32,
16871) -> Result<i32, IndicatorDispatchError> {
16872    match find_param(params, key) {
16873        Some(ParamValue::Int(v)) => {
16874            if *v < i32::MIN as i64 || *v > i32::MAX as i64 {
16875                return Err(IndicatorDispatchError::InvalidParam {
16876                    indicator: indicator.to_string(),
16877                    key: key.to_string(),
16878                    reason: "integer out of i32 range".to_string(),
16879                });
16880            }
16881            Ok(*v as i32)
16882        }
16883        Some(ParamValue::Float(v)) => {
16884            if !v.is_finite() {
16885                return Err(IndicatorDispatchError::InvalidParam {
16886                    indicator: indicator.to_string(),
16887                    key: key.to_string(),
16888                    reason: "expected finite number".to_string(),
16889                });
16890            }
16891            let r = v.round();
16892            if (*v - r).abs() > 1e-9 || r < i32::MIN as f64 || r > i32::MAX as f64 {
16893                return Err(IndicatorDispatchError::InvalidParam {
16894                    indicator: indicator.to_string(),
16895                    key: key.to_string(),
16896                    reason: "expected i32-compatible whole number".to_string(),
16897                });
16898            }
16899            Ok(r as i32)
16900        }
16901        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16902            indicator: indicator.to_string(),
16903            key: key.to_string(),
16904            reason: "expected Int or Float".to_string(),
16905        }),
16906        None => Ok(default),
16907    }
16908}
16909
16910fn get_enum_param(
16911    indicator: &str,
16912    params: &[ParamKV<'_>],
16913    key: &str,
16914    default: &str,
16915) -> Result<String, IndicatorDispatchError> {
16916    match find_param(params, key) {
16917        Some(ParamValue::EnumString(v)) => Ok((*v).to_string()),
16918        Some(_) => Err(IndicatorDispatchError::InvalidParam {
16919            indicator: indicator.to_string(),
16920            key: key.to_string(),
16921            reason: "expected EnumString".to_string(),
16922        }),
16923        None => Ok(default.to_string()),
16924    }
16925}
16926
16927#[cfg(test)]
16928mod tests {
16929    use super::*;
16930    use crate::indicators::absolute_strength_index_oscillator::{
16931        absolute_strength_index_oscillator_with_kernel, AbsoluteStrengthIndexOscillatorInput,
16932        AbsoluteStrengthIndexOscillatorParams,
16933    };
16934    use crate::indicators::accumulation_swing_index::{
16935        accumulation_swing_index_with_kernel, AccumulationSwingIndexInput,
16936        AccumulationSwingIndexParams,
16937    };
16938    use crate::indicators::ad::{ad_with_kernel, AdInput, AdParams};
16939    use crate::indicators::adaptive_bandpass_trigger_oscillator::{
16940        adaptive_bandpass_trigger_oscillator_with_kernel, AdaptiveBandpassTriggerOscillatorInput,
16941        AdaptiveBandpassTriggerOscillatorParams,
16942    };
16943    use crate::indicators::advance_decline_line::{
16944        advance_decline_line_with_kernel, AdvanceDeclineLineInput, AdvanceDeclineLineParams,
16945    };
16946    use crate::indicators::adx::{adx_with_kernel, AdxInput, AdxParams};
16947    use crate::indicators::ao::{ao_with_kernel, AoInput, AoParams};
16948    use crate::indicators::apo::{apo_with_kernel, ApoInput, ApoParams};
16949    use crate::indicators::atr_percentile::{
16950        atr_percentile_with_kernel, AtrPercentileInput, AtrPercentileParams,
16951    };
16952    use crate::indicators::bull_power_vs_bear_power::{
16953        bull_power_vs_bear_power_with_kernel, BullPowerVsBearPowerInput, BullPowerVsBearPowerParams,
16954    };
16955    use crate::indicators::cg::{cg_with_kernel, CgInput, CgParams};
16956    use crate::indicators::cmo::{cmo_with_kernel, CmoInput, CmoParams};
16957    use crate::indicators::cycle_channel_oscillator::{
16958        cycle_channel_oscillator_with_kernel, CycleChannelOscillatorInput,
16959        CycleChannelOscillatorParams,
16960    };
16961    use crate::indicators::daily_factor::{
16962        daily_factor_with_kernel, DailyFactorInput, DailyFactorParams,
16963    };
16964    use crate::indicators::decisionpoint_breadth_swenlin_trading_oscillator::{
16965        decisionpoint_breadth_swenlin_trading_oscillator_with_kernel,
16966        DecisionPointBreadthSwenlinTradingOscillatorInput,
16967        DecisionPointBreadthSwenlinTradingOscillatorParams,
16968    };
16969    use crate::indicators::demand_index::{
16970        demand_index_with_kernel, DemandIndexInput, DemandIndexParams,
16971    };
16972    use crate::indicators::deviation::{deviation_with_kernel, DeviationInput, DeviationParams};
16973    use crate::indicators::dx::{
16974        dx_batch_with_kernel, dx_with_kernel, DxBatchRange, DxInput, DxParams,
16975    };
16976    use crate::indicators::efi::{efi_with_kernel, EfiInput, EfiParams};
16977    use crate::indicators::ehlers_adaptive_cyber_cycle::{
16978        ehlers_adaptive_cyber_cycle_with_kernel, EhlersAdaptiveCyberCycleInput,
16979        EhlersAdaptiveCyberCycleParams,
16980    };
16981    use crate::indicators::ehlers_linear_extrapolation_predictor::{
16982        ehlers_linear_extrapolation_predictor_with_kernel, EhlersLinearExtrapolationPredictorInput,
16983        EhlersLinearExtrapolationPredictorParams,
16984    };
16985    use crate::indicators::ehlers_simple_cycle_indicator::{
16986        ehlers_simple_cycle_indicator_with_kernel, EhlersSimpleCycleIndicatorInput,
16987        EhlersSimpleCycleIndicatorParams,
16988    };
16989    use crate::indicators::ehlers_smoothed_adaptive_momentum::{
16990        ehlers_smoothed_adaptive_momentum_with_kernel, EhlersSmoothedAdaptiveMomentumInput,
16991        EhlersSmoothedAdaptiveMomentumParams,
16992    };
16993    use crate::indicators::ewma_volatility::{
16994        ewma_volatility_with_kernel, EwmaVolatilityInput, EwmaVolatilityParams,
16995    };
16996    use crate::indicators::fibonacci_entry_bands::{
16997        fibonacci_entry_bands_with_kernel, FibonacciEntryBandsInput, FibonacciEntryBandsParams,
16998    };
16999    use crate::indicators::fibonacci_trailing_stop::{
17000        fibonacci_trailing_stop_with_kernel, FibonacciTrailingStopInput,
17001        FibonacciTrailingStopParams,
17002    };
17003    use crate::indicators::fosc::{fosc_with_kernel, FoscInput, FoscParams};
17004    use crate::indicators::garman_klass_volatility::{
17005        garman_klass_volatility_with_kernel, GarmanKlassVolatilityInput,
17006        GarmanKlassVolatilityParams,
17007    };
17008    use crate::indicators::gopalakrishnan_range_index::{
17009        gopalakrishnan_range_index_with_kernel, GopalakrishnanRangeIndexInput,
17010        GopalakrishnanRangeIndexParams,
17011    };
17012    use crate::indicators::grover_llorens_cycle_oscillator::{
17013        grover_llorens_cycle_oscillator_with_kernel, GroverLlorensCycleOscillatorInput,
17014        GroverLlorensCycleOscillatorParams,
17015    };
17016    use crate::indicators::hema_trend_levels::{
17017        hema_trend_levels_with_kernel, HemaTrendLevelsInput, HemaTrendLevelsParams,
17018    };
17019    use crate::indicators::historical_volatility::{
17020        historical_volatility_with_kernel, HistoricalVolatilityInput, HistoricalVolatilityParams,
17021    };
17022    use crate::indicators::historical_volatility_percentile::{
17023        historical_volatility_percentile_with_kernel, HistoricalVolatilityPercentileInput,
17024        HistoricalVolatilityPercentileParams,
17025    };
17026    use crate::indicators::hull_butterfly_oscillator::{
17027        hull_butterfly_oscillator_with_kernel, HullButterflyOscillatorInput,
17028        HullButterflyOscillatorParams,
17029    };
17030    use crate::indicators::ichimoku_oscillator::{
17031        ichimoku_oscillator_with_kernel, IchimokuOscillatorInput, IchimokuOscillatorNormalizeMode,
17032        IchimokuOscillatorParams,
17033    };
17034    use crate::indicators::ift_rsi::{ift_rsi_with_kernel, IftRsiInput, IftRsiParams};
17035    use crate::indicators::intraday_momentum_index::{
17036        intraday_momentum_index_with_kernel, IntradayMomentumIndexInput,
17037        IntradayMomentumIndexParams,
17038    };
17039    use crate::indicators::kvo::{kvo_with_kernel, KvoInput, KvoParams};
17040    use crate::indicators::l2_ehlers_signal_to_noise::{
17041        l2_ehlers_signal_to_noise_with_kernel, L2EhlersSignalToNoiseInput,
17042        L2EhlersSignalToNoiseParams,
17043    };
17044    use crate::indicators::linearreg_angle::{
17045        linearreg_angle_with_kernel, Linearreg_angleInput, Linearreg_angleParams,
17046    };
17047    use crate::indicators::linearreg_intercept::{
17048        linearreg_intercept_with_kernel, LinearRegInterceptInput, LinearRegInterceptParams,
17049    };
17050    use crate::indicators::linearreg_slope::{
17051        linearreg_slope_with_kernel, LinearRegSlopeInput, LinearRegSlopeParams,
17052    };
17053    use crate::indicators::macd::{macd_with_kernel, MacdInput, MacdParams};
17054    use crate::indicators::macd_wave_signal_pro::{
17055        macd_wave_signal_pro_with_kernel, MacdWaveSignalProInput,
17056    };
17057    use crate::indicators::mean_ad::{mean_ad_with_kernel, MeanAdInput, MeanAdParams};
17058    use crate::indicators::medprice::{medprice_with_kernel, MedpriceInput, MedpriceParams};
17059    use crate::indicators::mesa_stochastic_multi_length::{
17060        mesa_stochastic_multi_length_with_kernel, MesaStochasticMultiLengthInput,
17061        MesaStochasticMultiLengthParams,
17062    };
17063    use crate::indicators::mfi::{
17064        mfi_batch_with_kernel, mfi_with_kernel, MfiBatchRange, MfiInput, MfiParams,
17065    };
17066    use crate::indicators::monotonicity_index::{
17067        monotonicity_index_with_kernel, MonotonicityIndexInput, MonotonicityIndexMode,
17068        MonotonicityIndexParams,
17069    };
17070    use crate::indicators::moving_averages::ma::MaData;
17071    use crate::indicators::moving_averages::ma_batch::{
17072        ma_batch_with_kernel_and_typed_params, MaBatchParamKV, MaBatchParamValue,
17073    };
17074    use crate::indicators::multi_length_stochastic_average::{
17075        multi_length_stochastic_average_with_kernel, MultiLengthStochasticAverageInput,
17076        MultiLengthStochasticAverageParams,
17077    };
17078    use crate::indicators::natr::{natr_with_kernel, NatrInput, NatrParams};
17079    use crate::indicators::neighboring_trailing_stop::{
17080        neighboring_trailing_stop_with_kernel, NeighboringTrailingStopInput,
17081        NeighboringTrailingStopParams,
17082    };
17083    use crate::indicators::percentile_nearest_rank::{
17084        percentile_nearest_rank_with_kernel, PercentileNearestRankInput,
17085        PercentileNearestRankParams,
17086    };
17087    use crate::indicators::ppo::{ppo_with_kernel, PpoInput, PpoParams};
17088    use crate::indicators::premier_rsi_oscillator::{
17089        premier_rsi_oscillator_with_kernel, PremierRsiOscillatorInput, PremierRsiOscillatorParams,
17090    };
17091    use crate::indicators::price_moving_average_ratio_percentile::{
17092        price_moving_average_ratio_percentile_with_kernel, PriceMovingAverageRatioPercentileInput,
17093        PriceMovingAverageRatioPercentileLineMode, PriceMovingAverageRatioPercentileMaType,
17094        PriceMovingAverageRatioPercentileParams,
17095    };
17096    use crate::indicators::pvi::{pvi_with_kernel, PviInput, PviParams};
17097    use crate::indicators::random_walk_index::{
17098        random_walk_index_with_kernel, RandomWalkIndexInput, RandomWalkIndexParams,
17099    };
17100    use crate::indicators::registry::{list_indicators, IndicatorParamKind};
17101    use crate::indicators::spearman_correlation::{
17102        spearman_correlation_with_kernel, SpearmanCorrelationInput, SpearmanCorrelationParams,
17103    };
17104    use crate::indicators::squeeze_index::{
17105        squeeze_index_with_kernel, SqueezeIndexInput, SqueezeIndexParams,
17106    };
17107    use crate::indicators::stochastic_distance::{
17108        stochastic_distance_with_kernel, StochasticDistanceInput, StochasticDistanceParams,
17109    };
17110    use crate::indicators::trend_trigger_factor::{
17111        trend_trigger_factor_with_kernel, TrendTriggerFactorInput, TrendTriggerFactorParams,
17112    };
17113    use crate::indicators::trix::{
17114        trix_batch_with_kernel, trix_with_kernel, TrixBatchRange, TrixInput, TrixParams,
17115    };
17116    use crate::indicators::ttm_trend::{ttm_trend_with_kernel, TtmTrendInput, TtmTrendParams};
17117    use crate::indicators::velocity_acceleration_convergence_divergence_indicator::{
17118        velocity_acceleration_convergence_divergence_indicator_with_kernel,
17119        VelocityAccelerationConvergenceDivergenceIndicatorInput,
17120        VelocityAccelerationConvergenceDivergenceIndicatorParams,
17121    };
17122    use crate::indicators::velocity_acceleration_indicator::{
17123        velocity_acceleration_indicator_with_kernel, VelocityAccelerationIndicatorInput,
17124        VelocityAccelerationIndicatorParams,
17125    };
17126    use crate::indicators::volatility_quality_index::{
17127        volatility_quality_index_with_kernel, VolatilityQualityIndexInput,
17128        VolatilityQualityIndexParams,
17129    };
17130    use crate::indicators::volatility_ratio_adaptive_rsx::{
17131        volatility_ratio_adaptive_rsx_with_kernel, VolatilityRatioAdaptiveRsxInput,
17132        VolatilityRatioAdaptiveRsxParams,
17133    };
17134    use crate::indicators::volume_energy_reservoirs::{
17135        volume_energy_reservoirs_with_kernel, VolumeEnergyReservoirsInput,
17136        VolumeEnergyReservoirsParams,
17137    };
17138    use crate::indicators::volume_zone_oscillator::{
17139        volume_zone_oscillator_with_kernel, VolumeZoneOscillatorInput, VolumeZoneOscillatorParams,
17140    };
17141    use crate::indicators::vpci::{vpci_with_kernel, VpciInput, VpciParams};
17142    use crate::indicators::vwap_deviation_oscillator::{
17143        vwap_deviation_oscillator_with_kernel, VwapDeviationMode, VwapDeviationOscillatorInput,
17144        VwapDeviationOscillatorParams, VwapDeviationSessionMode,
17145    };
17146    use crate::indicators::vwap_zscore_with_signals::{
17147        vwap_zscore_with_signals_with_kernel, VwapZscoreWithSignalsInput,
17148        VwapZscoreWithSignalsParams,
17149    };
17150    use crate::indicators::yang_zhang_volatility::{
17151        yang_zhang_volatility_with_kernel, YangZhangVolatilityInput, YangZhangVolatilityParams,
17152    };
17153    use crate::indicators::zscore::{zscore_with_kernel, ZscoreInput, ZscoreParams};
17154    use crate::utilities::data_loader::Candles;
17155    use crate::utilities::enums::Kernel;
17156    use std::time::Instant;
17157
17158    fn sample_series() -> Vec<f64> {
17159        (1..=64).map(|v| v as f64).collect()
17160    }
17161
17162    fn sample_ohlc() -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
17163        let open: Vec<f64> = (0..128).map(|i| 100.0 + (i as f64 * 0.1)).collect();
17164        let high: Vec<f64> = open.iter().map(|v| v + 1.25).collect();
17165        let low: Vec<f64> = open.iter().map(|v| v - 1.1).collect();
17166        let close: Vec<f64> = open.iter().map(|v| v + 0.3).collect();
17167        (open, high, low, close)
17168    }
17169
17170    fn sample_candles() -> crate::utilities::data_loader::Candles {
17171        let (open, high, low, close) = sample_ohlc();
17172        let volume: Vec<f64> = (0..close.len()).map(|i| 1000.0 + (i as f64)).collect();
17173        let timestamp: Vec<i64> = (0..close.len()).map(|i| i as i64).collect();
17174        crate::utilities::data_loader::Candles::new(timestamp, open, high, low, close, volume)
17175    }
17176
17177    fn assert_series_eq(actual: &[f64], expected: &[f64], tol: f64) {
17178        assert_eq!(actual.len(), expected.len());
17179        for i in 0..actual.len() {
17180            let a = actual[i];
17181            let b = expected[i];
17182            if a.is_nan() && b.is_nan() {
17183                continue;
17184            }
17185            assert!(
17186                (a - b).abs() <= tol,
17187                "mismatch at index {i}: actual={a}, expected={b}, tol={tol}"
17188            );
17189        }
17190    }
17191
17192    #[test]
17193    fn unknown_indicator_is_rejected() {
17194        let data = sample_series();
17195        let req = IndicatorBatchRequest {
17196            indicator_id: "not_real",
17197            output_id: None,
17198            data: IndicatorDataRef::Slice { values: &data },
17199            combos: &[],
17200            kernel: Kernel::Auto,
17201        };
17202        let err = compute_cpu_batch(req).unwrap_err();
17203        assert!(matches!(
17204            err,
17205            IndicatorDispatchError::UnknownIndicator { .. }
17206        ));
17207    }
17208
17209    #[test]
17210    fn bucket_b_ma_indicator_is_supported() {
17211        let data = sample_series();
17212        let combos = [IndicatorParamSet { params: &[] }];
17213        let req = IndicatorBatchRequest {
17214            indicator_id: "mama",
17215            output_id: Some("mama"),
17216            data: IndicatorDataRef::Slice { values: &data },
17217            combos: &combos,
17218            kernel: Kernel::Auto,
17219        };
17220        let out = compute_cpu_batch(req).unwrap();
17221        assert_eq!(out.rows, 1);
17222        assert_eq!(out.cols, data.len());
17223        assert!(out.values_f64.is_some());
17224    }
17225
17226    #[test]
17227    fn strict_mode_rejects_convenience_mfi_ohlcv() {
17228        let (open, high, low, close) = sample_ohlc();
17229        let volume: Vec<f64> = (0..close.len()).map(|i| 1200.0 + (i as f64)).collect();
17230        let combo = [ParamKV {
17231            key: "period",
17232            value: ParamValue::Int(14),
17233        }];
17234        let combos = [IndicatorParamSet { params: &combo }];
17235        let req = IndicatorBatchRequest {
17236            indicator_id: "mfi",
17237            output_id: Some("value"),
17238            data: IndicatorDataRef::Ohlcv {
17239                open: &open,
17240                high: &high,
17241                low: &low,
17242                close: &close,
17243                volume: &volume,
17244            },
17245            combos: &combos,
17246            kernel: Kernel::Auto,
17247        };
17248        let err = compute_cpu_batch_strict(req).unwrap_err();
17249        match err {
17250            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
17251                assert_eq!(indicator, "mfi");
17252                assert_eq!(input, IndicatorInputKind::CloseVolume);
17253            }
17254            other => panic!("expected MissingRequiredInput, got {other:?}"),
17255        }
17256    }
17257
17258    #[test]
17259    fn strict_mode_accepts_precomputed_mfi_close_volume() {
17260        let (_open, high, low, close) = sample_ohlc();
17261        let volume: Vec<f64> = (0..close.len())
17262            .map(|i| 1000.0 + (i as f64 * 2.0))
17263            .collect();
17264        let typical: Vec<f64> = high
17265            .iter()
17266            .zip(&low)
17267            .zip(&close)
17268            .map(|((h, l), c)| (h + l + c) / 3.0)
17269            .collect();
17270        let combo = [ParamKV {
17271            key: "period",
17272            value: ParamValue::Int(14),
17273        }];
17274        let combos = [IndicatorParamSet { params: &combo }];
17275        let req = IndicatorBatchRequest {
17276            indicator_id: "mfi",
17277            output_id: Some("value"),
17278            data: IndicatorDataRef::CloseVolume {
17279                close: &typical,
17280                volume: &volume,
17281            },
17282            combos: &combos,
17283            kernel: Kernel::Auto,
17284        };
17285        let strict = compute_cpu_batch_strict(req).unwrap();
17286        let input = MfiInput::from_slices(&typical, &volume, MfiParams { period: Some(14) });
17287        let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
17288            .unwrap()
17289            .values;
17290        assert_series_eq(strict.values_f64.as_ref().unwrap(), &direct, 1e-12);
17291    }
17292
17293    #[test]
17294    fn strict_mode_rejects_ao_high_low_and_requires_slice() {
17295        let (_open, high, low, _close) = sample_ohlc();
17296        let combo = [
17297            ParamKV {
17298                key: "short_period",
17299                value: ParamValue::Int(5),
17300            },
17301            ParamKV {
17302                key: "long_period",
17303                value: ParamValue::Int(34),
17304            },
17305        ];
17306        let combos = [IndicatorParamSet { params: &combo }];
17307        let req = IndicatorBatchRequest {
17308            indicator_id: "ao",
17309            output_id: Some("value"),
17310            data: IndicatorDataRef::HighLow {
17311                high: &high,
17312                low: &low,
17313            },
17314            combos: &combos,
17315            kernel: Kernel::Auto,
17316        };
17317        let err = compute_cpu_batch_strict(req).unwrap_err();
17318        match err {
17319            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
17320                assert_eq!(indicator, "ao");
17321                assert_eq!(input, IndicatorInputKind::Slice);
17322            }
17323            other => panic!("expected MissingRequiredInput, got {other:?}"),
17324        }
17325    }
17326
17327    #[test]
17328    fn strict_mode_rejects_ttm_trend_ohlc_and_requires_candles() {
17329        let (open, high, low, close) = sample_ohlc();
17330        let combo = [ParamKV {
17331            key: "period",
17332            value: ParamValue::Int(5),
17333        }];
17334        let combos = [IndicatorParamSet { params: &combo }];
17335        let req = IndicatorBatchRequest {
17336            indicator_id: "ttm_trend",
17337            output_id: Some("value"),
17338            data: IndicatorDataRef::Ohlc {
17339                open: &open,
17340                high: &high,
17341                low: &low,
17342                close: &close,
17343            },
17344            combos: &combos,
17345            kernel: Kernel::Auto,
17346        };
17347        let err = compute_cpu_batch_strict(req).unwrap_err();
17348        match err {
17349            IndicatorDispatchError::MissingRequiredInput { indicator, input } => {
17350                assert_eq!(indicator, "ttm_trend");
17351                assert_eq!(input, IndicatorInputKind::Candles);
17352            }
17353            other => panic!("expected MissingRequiredInput, got {other:?}"),
17354        }
17355    }
17356
17357    #[test]
17358    fn strict_mode_accepts_ttm_trend_candles() {
17359        let candles = sample_candles();
17360        let combo = [ParamKV {
17361            key: "period",
17362            value: ParamValue::Int(5),
17363        }];
17364        let combos = [IndicatorParamSet { params: &combo }];
17365        let req = IndicatorBatchRequest {
17366            indicator_id: "ttm_trend",
17367            output_id: Some("value"),
17368            data: IndicatorDataRef::Candles {
17369                candles: &candles,
17370                source: Some("hl2"),
17371            },
17372            combos: &combos,
17373            kernel: Kernel::Auto,
17374        };
17375        let strict = compute_cpu_batch_strict(req).unwrap();
17376        let input = TtmTrendInput::from_slices(
17377            candles.hl2.as_slice(),
17378            candles.close.as_slice(),
17379            TtmTrendParams { period: Some(5) },
17380        );
17381        let direct = ttm_trend_with_kernel(&input, Kernel::Auto.to_non_batch())
17382            .unwrap()
17383            .values;
17384        let got = strict.values_bool.unwrap();
17385        assert_eq!(got, direct);
17386    }
17387
17388    #[test]
17389    fn rsi_cpu_batch_smoke() {
17390        let data = sample_series();
17391        let combo_1 = [ParamKV {
17392            key: "period",
17393            value: ParamValue::Int(7),
17394        }];
17395        let combo_2 = [ParamKV {
17396            key: "period",
17397            value: ParamValue::Int(14),
17398        }];
17399        let combos = [
17400            IndicatorParamSet { params: &combo_1 },
17401            IndicatorParamSet { params: &combo_2 },
17402        ];
17403        let req = IndicatorBatchRequest {
17404            indicator_id: "rsi",
17405            output_id: Some("value"),
17406            data: IndicatorDataRef::Slice { values: &data },
17407            combos: &combos,
17408            kernel: Kernel::Auto,
17409        };
17410        let out = compute_cpu_batch(req).unwrap();
17411        assert_eq!(out.output_id, "value");
17412        assert_eq!(out.rows, 2);
17413        assert_eq!(out.cols, data.len());
17414        assert_eq!(out.values_f64.as_ref().map(Vec::len), Some(2 * data.len()));
17415    }
17416
17417    #[test]
17418    fn ma_dispatch_regression_sma_matches_existing_ma_batch_api() {
17419        let data = sample_series();
17420        let combo = [ParamKV {
17421            key: "period",
17422            value: ParamValue::Int(14),
17423        }];
17424        let combos = [IndicatorParamSet { params: &combo }];
17425        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17426            indicator_id: "sma",
17427            output_id: Some("value"),
17428            data: IndicatorDataRef::Slice { values: &data },
17429            combos: &combos,
17430            kernel: Kernel::Auto,
17431        })
17432        .unwrap();
17433
17434        let direct = ma_batch_with_kernel_and_typed_params(
17435            "sma",
17436            MaData::Slice(&data),
17437            (14, 14, 0),
17438            Kernel::Auto,
17439            &[],
17440        )
17441        .unwrap();
17442        assert_eq!(dispatch.rows, direct.rows);
17443        assert_eq!(dispatch.cols, direct.cols);
17444        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17445    }
17446
17447    #[test]
17448    fn ma_dispatch_sma_period_sweep_matches_direct_batch() {
17449        let data = sample_series();
17450        let combo_1 = [ParamKV {
17451            key: "period",
17452            value: ParamValue::Int(5),
17453        }];
17454        let combo_2 = [ParamKV {
17455            key: "period",
17456            value: ParamValue::Int(7),
17457        }];
17458        let combo_3 = [ParamKV {
17459            key: "period",
17460            value: ParamValue::Int(9),
17461        }];
17462        let combos = [
17463            IndicatorParamSet { params: &combo_1 },
17464            IndicatorParamSet { params: &combo_2 },
17465            IndicatorParamSet { params: &combo_3 },
17466        ];
17467        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17468            indicator_id: "sma",
17469            output_id: Some("value"),
17470            data: IndicatorDataRef::Slice { values: &data },
17471            combos: &combos,
17472            kernel: Kernel::Auto,
17473        })
17474        .unwrap();
17475
17476        let direct = ma_batch_with_kernel_and_typed_params(
17477            "sma",
17478            MaData::Slice(&data),
17479            (5, 9, 2),
17480            Kernel::Auto,
17481            &[],
17482        )
17483        .unwrap();
17484        assert_eq!(dispatch.rows, direct.rows);
17485        assert_eq!(dispatch.cols, direct.cols);
17486        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17487    }
17488
17489    #[test]
17490    fn mfi_dispatch_period_sweep_matches_direct_batch() {
17491        let (_open, high, low, close) = sample_ohlc();
17492        let volume: Vec<f64> = (0..close.len())
17493            .map(|i| 1000.0 + (i as f64 * 2.0))
17494            .collect();
17495        let typical: Vec<f64> = high
17496            .iter()
17497            .zip(&low)
17498            .zip(&close)
17499            .map(|((h, l), c)| (h + l + c) / 3.0)
17500            .collect();
17501        let combo_1 = [ParamKV {
17502            key: "period",
17503            value: ParamValue::Int(5),
17504        }];
17505        let combo_2 = [ParamKV {
17506            key: "period",
17507            value: ParamValue::Int(7),
17508        }];
17509        let combo_3 = [ParamKV {
17510            key: "period",
17511            value: ParamValue::Int(9),
17512        }];
17513        let combos = [
17514            IndicatorParamSet { params: &combo_1 },
17515            IndicatorParamSet { params: &combo_2 },
17516            IndicatorParamSet { params: &combo_3 },
17517        ];
17518        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17519            indicator_id: "mfi",
17520            output_id: Some("value"),
17521            data: IndicatorDataRef::CloseVolume {
17522                close: &typical,
17523                volume: &volume,
17524            },
17525            combos: &combos,
17526            kernel: Kernel::Auto,
17527        })
17528        .unwrap();
17529        let direct = mfi_batch_with_kernel(
17530            &typical,
17531            &volume,
17532            &MfiBatchRange { period: (5, 9, 2) },
17533            Kernel::Auto,
17534        )
17535        .unwrap();
17536        assert_eq!(dispatch.rows, direct.rows);
17537        assert_eq!(dispatch.cols, direct.cols);
17538        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17539    }
17540
17541    #[test]
17542    fn dx_dispatch_period_sweep_keeps_requested_row_order() {
17543        let (open, high, low, close) = sample_ohlc();
17544        let combo_1 = [ParamKV {
17545            key: "period",
17546            value: ParamValue::Int(9),
17547        }];
17548        let combo_2 = [ParamKV {
17549            key: "period",
17550            value: ParamValue::Int(7),
17551        }];
17552        let combo_3 = [ParamKV {
17553            key: "period",
17554            value: ParamValue::Int(5),
17555        }];
17556        let combos = [
17557            IndicatorParamSet { params: &combo_1 },
17558            IndicatorParamSet { params: &combo_2 },
17559            IndicatorParamSet { params: &combo_3 },
17560        ];
17561        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17562            indicator_id: "dx",
17563            output_id: Some("value"),
17564            data: IndicatorDataRef::Ohlc {
17565                open: &open,
17566                high: &high,
17567                low: &low,
17568                close: &close,
17569            },
17570            combos: &combos,
17571            kernel: Kernel::Auto,
17572        })
17573        .unwrap();
17574        let direct = dx_batch_with_kernel(
17575            &high,
17576            &low,
17577            &close,
17578            &DxBatchRange { period: (9, 5, 2) },
17579            Kernel::Auto,
17580        )
17581        .unwrap();
17582        let direct_periods: Vec<usize> = direct
17583            .combos
17584            .iter()
17585            .map(|combo| combo.period.unwrap_or(14))
17586            .collect();
17587        let period_to_row: std::collections::HashMap<usize, usize> = direct_periods
17588            .iter()
17589            .copied()
17590            .enumerate()
17591            .map(|(row, period)| (period, row))
17592            .collect();
17593        let requested = [9usize, 7usize, 5usize];
17594        let mut expected = Vec::with_capacity(requested.len() * direct.cols);
17595        for period in requested {
17596            let row = period_to_row[&period];
17597            let start = row * direct.cols;
17598            let end = start + direct.cols;
17599            expected.extend_from_slice(&direct.values[start..end]);
17600        }
17601        assert_eq!(dispatch.rows, requested.len());
17602        assert_eq!(dispatch.cols, direct.cols);
17603        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &expected, 1e-12);
17604    }
17605
17606    #[test]
17607    fn ma_dispatch_regression_alma_typed_params_match_existing_ma_batch_api() {
17608        let data = sample_series();
17609        let combo = [
17610            ParamKV {
17611                key: "period",
17612                value: ParamValue::Int(14),
17613            },
17614            ParamKV {
17615                key: "offset",
17616                value: ParamValue::Float(0.87),
17617            },
17618            ParamKV {
17619                key: "sigma",
17620                value: ParamValue::Float(5.5),
17621            },
17622        ];
17623        let combos = [IndicatorParamSet { params: &combo }];
17624        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
17625            indicator_id: "alma",
17626            output_id: Some("value"),
17627            data: IndicatorDataRef::Slice { values: &data },
17628            combos: &combos,
17629            kernel: Kernel::Auto,
17630        })
17631        .unwrap();
17632
17633        let typed = [
17634            MaBatchParamKV {
17635                key: "offset",
17636                value: MaBatchParamValue::Float(0.87),
17637            },
17638            MaBatchParamKV {
17639                key: "sigma",
17640                value: MaBatchParamValue::Float(5.5),
17641            },
17642        ];
17643        let direct = ma_batch_with_kernel_and_typed_params(
17644            "alma",
17645            MaData::Slice(&data),
17646            (14, 14, 0),
17647            Kernel::Auto,
17648            &typed,
17649        )
17650        .unwrap();
17651        assert_eq!(dispatch.rows, direct.rows);
17652        assert_eq!(dispatch.cols, direct.cols);
17653        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &direct.values, 1e-12);
17654    }
17655
17656    #[test]
17657    fn macd_signal_output_matches_direct() {
17658        let data = sample_series();
17659        let combo_1 = [
17660            ParamKV {
17661                key: "fast_period",
17662                value: ParamValue::Int(8),
17663            },
17664            ParamKV {
17665                key: "slow_period",
17666                value: ParamValue::Int(21),
17667            },
17668            ParamKV {
17669                key: "signal_period",
17670                value: ParamValue::Int(5),
17671            },
17672        ];
17673        let combo_2 = [
17674            ParamKV {
17675                key: "fast_period",
17676                value: ParamValue::Int(12),
17677            },
17678            ParamKV {
17679                key: "slow_period",
17680                value: ParamValue::Int(26),
17681            },
17682            ParamKV {
17683                key: "signal_period",
17684                value: ParamValue::Int(9),
17685            },
17686        ];
17687        let combos = [
17688            IndicatorParamSet { params: &combo_1 },
17689            IndicatorParamSet { params: &combo_2 },
17690        ];
17691        let req = IndicatorBatchRequest {
17692            indicator_id: "macd",
17693            output_id: Some("signal"),
17694            data: IndicatorDataRef::Slice { values: &data },
17695            combos: &combos,
17696            kernel: Kernel::Auto,
17697        };
17698        let out = compute_cpu_batch(req).unwrap();
17699        let matrix = out.values_f64.unwrap();
17700        for (row, combo) in combos.iter().enumerate() {
17701            let fast = match combo.params[0].value {
17702                ParamValue::Int(v) => v as usize,
17703                _ => unreachable!(),
17704            };
17705            let slow = match combo.params[1].value {
17706                ParamValue::Int(v) => v as usize,
17707                _ => unreachable!(),
17708            };
17709            let signal = match combo.params[2].value {
17710                ParamValue::Int(v) => v as usize,
17711                _ => unreachable!(),
17712            };
17713            let input = MacdInput::from_slice(
17714                &data,
17715                MacdParams {
17716                    fast_period: Some(fast),
17717                    slow_period: Some(slow),
17718                    signal_period: Some(signal),
17719                    ma_type: Some("ema".to_string()),
17720                },
17721            );
17722            let direct = macd_with_kernel(&input, Kernel::Auto.to_non_batch())
17723                .unwrap()
17724                .signal;
17725            let start = row * out.cols;
17726            let end = start + out.cols;
17727            assert_series_eq(&matrix[start..end], direct.as_slice(), 1e-12);
17728        }
17729    }
17730
17731    #[test]
17732    fn adx_output_matches_direct() {
17733        let (open, high, low, close) = sample_ohlc();
17734        let combo = [ParamKV {
17735            key: "period",
17736            value: ParamValue::Int(14),
17737        }];
17738        let combos = [IndicatorParamSet { params: &combo }];
17739        let req = IndicatorBatchRequest {
17740            indicator_id: "adx",
17741            output_id: Some("value"),
17742            data: IndicatorDataRef::Ohlc {
17743                open: &open,
17744                high: &high,
17745                low: &low,
17746                close: &close,
17747            },
17748            combos: &combos,
17749            kernel: Kernel::Auto,
17750        };
17751        let out = compute_cpu_batch(req).unwrap();
17752        let matrix = out.values_f64.unwrap();
17753        let input = AdxInput::from_slices(&high, &low, &close, AdxParams { period: Some(14) });
17754        let direct = adx_with_kernel(&input, Kernel::Auto.to_non_batch())
17755            .unwrap()
17756            .values;
17757        assert_series_eq(&matrix, &direct, 1e-12);
17758    }
17759
17760    #[test]
17761    fn garman_klass_output_matches_direct() {
17762        let (open, high, low, close) = sample_ohlc();
17763        let combo = [ParamKV {
17764            key: "lookback",
17765            value: ParamValue::Int(17),
17766        }];
17767        let combos = [IndicatorParamSet { params: &combo }];
17768        let req = IndicatorBatchRequest {
17769            indicator_id: "garman_klass_volatility",
17770            output_id: Some("value"),
17771            data: IndicatorDataRef::Ohlc {
17772                open: &open,
17773                high: &high,
17774                low: &low,
17775                close: &close,
17776            },
17777            combos: &combos,
17778            kernel: Kernel::Auto,
17779        };
17780        let out = compute_cpu_batch(req).unwrap();
17781        let got = out.values_f64.unwrap();
17782        let input = GarmanKlassVolatilityInput::from_slices(
17783            &open,
17784            &high,
17785            &low,
17786            &close,
17787            GarmanKlassVolatilityParams { lookback: Some(17) },
17788        );
17789        let direct = garman_klass_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
17790            .unwrap()
17791            .values;
17792        assert_series_eq(&got, &direct, 1e-12);
17793    }
17794
17795    #[test]
17796    fn cmo_output_matches_direct() {
17797        let data = sample_series();
17798        let combo = [ParamKV {
17799            key: "period",
17800            value: ParamValue::Int(14),
17801        }];
17802        let combos = [IndicatorParamSet { params: &combo }];
17803        let req = IndicatorBatchRequest {
17804            indicator_id: "cmo",
17805            output_id: Some("value"),
17806            data: IndicatorDataRef::Slice { values: &data },
17807            combos: &combos,
17808            kernel: Kernel::Auto,
17809        };
17810        let out = compute_cpu_batch(req).unwrap();
17811        let input = CmoInput::from_slice(&data, CmoParams { period: Some(14) });
17812        let direct = cmo_with_kernel(&input, Kernel::Auto.to_non_batch())
17813            .unwrap()
17814            .values;
17815        let got = out.values_f64.unwrap();
17816        assert_series_eq(&got, &direct, 1e-12);
17817    }
17818
17819    #[test]
17820    fn ppo_output_matches_direct() {
17821        let data = sample_series();
17822        let combo = [
17823            ParamKV {
17824                key: "fast_period",
17825                value: ParamValue::Int(12),
17826            },
17827            ParamKV {
17828                key: "slow_period",
17829                value: ParamValue::Int(26),
17830            },
17831            ParamKV {
17832                key: "ma_type",
17833                value: ParamValue::EnumString("sma"),
17834            },
17835        ];
17836        let combos = [IndicatorParamSet { params: &combo }];
17837        let req = IndicatorBatchRequest {
17838            indicator_id: "ppo",
17839            output_id: Some("value"),
17840            data: IndicatorDataRef::Slice { values: &data },
17841            combos: &combos,
17842            kernel: Kernel::Auto,
17843        };
17844        let out = compute_cpu_batch(req).unwrap();
17845        let input = PpoInput::from_slice(
17846            &data,
17847            PpoParams {
17848                fast_period: Some(12),
17849                slow_period: Some(26),
17850                ma_type: Some("sma".to_string()),
17851            },
17852        );
17853        let direct = ppo_with_kernel(&input, Kernel::Auto.to_non_batch())
17854            .unwrap()
17855            .values;
17856        let got = out.values_f64.unwrap();
17857        assert_series_eq(&got, &direct, 1e-12);
17858    }
17859
17860    #[test]
17861    fn apo_output_matches_direct() {
17862        let data = sample_series();
17863        let combo = [
17864            ParamKV {
17865                key: "short_period",
17866                value: ParamValue::Int(10),
17867            },
17868            ParamKV {
17869                key: "long_period",
17870                value: ParamValue::Int(20),
17871            },
17872        ];
17873        let combos = [IndicatorParamSet { params: &combo }];
17874        let req = IndicatorBatchRequest {
17875            indicator_id: "apo",
17876            output_id: Some("value"),
17877            data: IndicatorDataRef::Slice { values: &data },
17878            combos: &combos,
17879            kernel: Kernel::Auto,
17880        };
17881        let out = compute_cpu_batch(req).unwrap();
17882        let input = ApoInput::from_slice(
17883            &data,
17884            ApoParams {
17885                short_period: Some(10),
17886                long_period: Some(20),
17887            },
17888        );
17889        let direct = apo_with_kernel(&input, Kernel::Auto.to_non_batch())
17890            .unwrap()
17891            .values;
17892        let got = out.values_f64.unwrap();
17893        assert_series_eq(&got, &direct, 1e-12);
17894    }
17895
17896    #[test]
17897    fn natr_output_matches_direct() {
17898        let (open, high, low, close) = sample_ohlc();
17899        let combo = [ParamKV {
17900            key: "period",
17901            value: ParamValue::Int(14),
17902        }];
17903        let combos = [IndicatorParamSet { params: &combo }];
17904        let req = IndicatorBatchRequest {
17905            indicator_id: "natr",
17906            output_id: Some("value"),
17907            data: IndicatorDataRef::Ohlc {
17908                open: &open,
17909                high: &high,
17910                low: &low,
17911                close: &close,
17912            },
17913            combos: &combos,
17914            kernel: Kernel::Auto,
17915        };
17916        let out = compute_cpu_batch(req).unwrap();
17917        let input = NatrInput::from_slices(&high, &low, &close, NatrParams { period: Some(14) });
17918        let direct = natr_with_kernel(&input, Kernel::Auto.to_non_batch())
17919            .unwrap()
17920            .values;
17921        let got = out.values_f64.unwrap();
17922        assert_series_eq(&got, &direct, 1e-12);
17923    }
17924
17925    #[test]
17926    fn ad_output_matches_direct() {
17927        let (open, high, low, close) = sample_ohlc();
17928        let volume: Vec<f64> = (0..close.len())
17929            .map(|i| 1000.0 + (i as f64 * 3.0))
17930            .collect();
17931        let combos = [IndicatorParamSet { params: &[] }];
17932        let req = IndicatorBatchRequest {
17933            indicator_id: "ad",
17934            output_id: Some("value"),
17935            data: IndicatorDataRef::Ohlcv {
17936                open: &open,
17937                high: &high,
17938                low: &low,
17939                close: &close,
17940                volume: &volume,
17941            },
17942            combos: &combos,
17943            kernel: Kernel::Auto,
17944        };
17945        let out = compute_cpu_batch(req).unwrap();
17946        let input = AdInput::from_slices(&high, &low, &close, &volume, AdParams::default());
17947        let direct = ad_with_kernel(&input, Kernel::Auto.to_non_batch())
17948            .unwrap()
17949            .values;
17950        let got = out.values_f64.unwrap();
17951        assert_series_eq(&got, &direct, 1e-12);
17952    }
17953
17954    #[test]
17955    fn ao_output_matches_direct() {
17956        let (open, high, low, close) = sample_ohlc();
17957        let combo = [
17958            ParamKV {
17959                key: "short_period",
17960                value: ParamValue::Int(5),
17961            },
17962            ParamKV {
17963                key: "long_period",
17964                value: ParamValue::Int(34),
17965            },
17966        ];
17967        let combos = [IndicatorParamSet { params: &combo }];
17968        let req = IndicatorBatchRequest {
17969            indicator_id: "ao",
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 source: Vec<f64> = high.iter().zip(&low).map(|(h, l)| 0.5 * (h + l)).collect();
17982        let input = AoInput::from_slice(
17983            &source,
17984            AoParams {
17985                short_period: Some(5),
17986                long_period: Some(34),
17987            },
17988        );
17989        let direct = ao_with_kernel(&input, Kernel::Auto.to_non_batch())
17990            .unwrap()
17991            .values;
17992        let got = out.values_f64.unwrap();
17993        assert_series_eq(&got, &direct, 1e-12);
17994    }
17995
17996    #[test]
17997    fn pvi_output_matches_direct() {
17998        let data = sample_series();
17999        let volume: Vec<f64> = (0..data.len()).map(|i| 900.0 + (i as f64 * 5.0)).collect();
18000        let combo = [ParamKV {
18001            key: "initial_value",
18002            value: ParamValue::Float(1000.0),
18003        }];
18004        let combos = [IndicatorParamSet { params: &combo }];
18005        let req = IndicatorBatchRequest {
18006            indicator_id: "pvi",
18007            output_id: Some("value"),
18008            data: IndicatorDataRef::CloseVolume {
18009                close: &data,
18010                volume: &volume,
18011            },
18012            combos: &combos,
18013            kernel: Kernel::Auto,
18014        };
18015        let out = compute_cpu_batch(req).unwrap();
18016        let input = PviInput::from_slices(
18017            &data,
18018            &volume,
18019            PviParams {
18020                initial_value: Some(1000.0),
18021            },
18022        );
18023        let direct = pvi_with_kernel(&input, Kernel::Auto.to_non_batch())
18024            .unwrap()
18025            .values;
18026        let got = out.values_f64.unwrap();
18027        assert_series_eq(&got, &direct, 1e-12);
18028    }
18029
18030    #[test]
18031    fn efi_output_matches_direct() {
18032        let data = sample_series();
18033        let volume: Vec<f64> = (0..data.len()).map(|i| 1000.0 + (i as f64 * 4.0)).collect();
18034        let combo = [ParamKV {
18035            key: "period",
18036            value: ParamValue::Int(13),
18037        }];
18038        let combos = [IndicatorParamSet { params: &combo }];
18039        let req = IndicatorBatchRequest {
18040            indicator_id: "efi",
18041            output_id: Some("value"),
18042            data: IndicatorDataRef::CloseVolume {
18043                close: &data,
18044                volume: &volume,
18045            },
18046            combos: &combos,
18047            kernel: Kernel::Auto,
18048        };
18049        let out = compute_cpu_batch(req).unwrap();
18050        let input = EfiInput::from_slices(&data, &volume, EfiParams { period: Some(13) });
18051        let direct = efi_with_kernel(&input, Kernel::Auto.to_non_batch())
18052            .unwrap()
18053            .values;
18054        let got = out.values_f64.unwrap();
18055        assert_series_eq(&got, &direct, 1e-12);
18056    }
18057
18058    #[test]
18059    fn mfi_output_matches_direct() {
18060        let (open, high, low, close) = sample_ohlc();
18061        let volume: Vec<f64> = (0..close.len()).map(|i| 900.0 + (i as f64 * 6.0)).collect();
18062        let combo = [ParamKV {
18063            key: "period",
18064            value: ParamValue::Int(14),
18065        }];
18066        let combos = [IndicatorParamSet { params: &combo }];
18067        let req = IndicatorBatchRequest {
18068            indicator_id: "mfi",
18069            output_id: Some("value"),
18070            data: IndicatorDataRef::Ohlcv {
18071                open: &open,
18072                high: &high,
18073                low: &low,
18074                close: &close,
18075                volume: &volume,
18076            },
18077            combos: &combos,
18078            kernel: Kernel::Auto,
18079        };
18080        let out = compute_cpu_batch(req).unwrap();
18081        let typical_price: Vec<f64> = high
18082            .iter()
18083            .zip(&low)
18084            .zip(&close)
18085            .map(|((h, l), c)| (h + l + c) / 3.0)
18086            .collect();
18087        let input = MfiInput::from_slices(&typical_price, &volume, MfiParams { period: Some(14) });
18088        let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
18089            .unwrap()
18090            .values;
18091        let got = out.values_f64.unwrap();
18092        assert_series_eq(&got, &direct, 1e-12);
18093    }
18094
18095    #[test]
18096    fn mfi_non_sweep_fallback_rows_match_direct() {
18097        let (open, high, low, close) = sample_ohlc();
18098        let volume: Vec<f64> = (0..close.len()).map(|i| 950.0 + (i as f64 * 5.0)).collect();
18099        let combo_1 = [ParamKV {
18100            key: "period",
18101            value: ParamValue::Int(5),
18102        }];
18103        let combo_2 = [ParamKV {
18104            key: "period",
18105            value: ParamValue::Int(9),
18106        }];
18107        let combo_3 = [ParamKV {
18108            key: "period",
18109            value: ParamValue::Int(8),
18110        }];
18111        let combos = [
18112            IndicatorParamSet { params: &combo_1 },
18113            IndicatorParamSet { params: &combo_2 },
18114            IndicatorParamSet { params: &combo_3 },
18115        ];
18116        let req = IndicatorBatchRequest {
18117            indicator_id: "mfi",
18118            output_id: Some("value"),
18119            data: IndicatorDataRef::Ohlcv {
18120                open: &open,
18121                high: &high,
18122                low: &low,
18123                close: &close,
18124                volume: &volume,
18125            },
18126            combos: &combos,
18127            kernel: Kernel::Auto,
18128        };
18129        let out = compute_cpu_batch(req).unwrap();
18130        let matrix = out.values_f64.unwrap();
18131        let typical_price: Vec<f64> = high
18132            .iter()
18133            .zip(&low)
18134            .zip(&close)
18135            .map(|((h, l), c)| (h + l + c) / 3.0)
18136            .collect();
18137        for (row, period) in [5usize, 9usize, 8usize].iter().enumerate() {
18138            let input = MfiInput::from_slices(
18139                &typical_price,
18140                &volume,
18141                MfiParams {
18142                    period: Some(*period),
18143                },
18144            );
18145            let direct = mfi_with_kernel(&input, Kernel::Auto.to_non_batch())
18146                .unwrap()
18147                .values;
18148            let start = row * close.len();
18149            let end = start + close.len();
18150            assert_series_eq(&matrix[start..end], &direct, 1e-12);
18151        }
18152    }
18153
18154    #[test]
18155    fn kvo_output_matches_direct() {
18156        let (open, high, low, close) = sample_ohlc();
18157        let volume: Vec<f64> = (0..close.len())
18158            .map(|i| 1200.0 + (i as f64 * 5.0))
18159            .collect();
18160        let combo = [
18161            ParamKV {
18162                key: "short_period",
18163                value: ParamValue::Int(2),
18164            },
18165            ParamKV {
18166                key: "long_period",
18167                value: ParamValue::Int(5),
18168            },
18169        ];
18170        let combos = [IndicatorParamSet { params: &combo }];
18171        let req = IndicatorBatchRequest {
18172            indicator_id: "kvo",
18173            output_id: Some("value"),
18174            data: IndicatorDataRef::Ohlcv {
18175                open: &open,
18176                high: &high,
18177                low: &low,
18178                close: &close,
18179                volume: &volume,
18180            },
18181            combos: &combos,
18182            kernel: Kernel::Auto,
18183        };
18184        let out = compute_cpu_batch(req).unwrap();
18185        let input = KvoInput::from_slices(
18186            &high,
18187            &low,
18188            &close,
18189            &volume,
18190            KvoParams {
18191                short_period: Some(2),
18192                long_period: Some(5),
18193            },
18194        );
18195        let direct = kvo_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 dx_output_matches_direct() {
18204        let (open, high, low, close) = sample_ohlc();
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: "dx",
18212            output_id: Some("value"),
18213            data: IndicatorDataRef::Ohlc {
18214                open: &open,
18215                high: &high,
18216                low: &low,
18217                close: &close,
18218            },
18219            combos: &combos,
18220            kernel: Kernel::Auto,
18221        };
18222        let out = compute_cpu_batch(req).unwrap();
18223        let input = DxInput::from_hlc_slices(&high, &low, &close, DxParams { period: Some(14) });
18224        let direct = dx_with_kernel(&input, Kernel::Auto.to_non_batch())
18225            .unwrap()
18226            .values;
18227        let got = out.values_f64.unwrap();
18228        assert_series_eq(&got, &direct, 1e-12);
18229    }
18230
18231    #[test]
18232    fn dx_non_sweep_fallback_rows_match_direct() {
18233        let (open, high, low, close) = sample_ohlc();
18234        let combo_1 = [ParamKV {
18235            key: "period",
18236            value: ParamValue::Int(9),
18237        }];
18238        let combo_2 = [ParamKV {
18239            key: "period",
18240            value: ParamValue::Int(5),
18241        }];
18242        let combo_3 = [ParamKV {
18243            key: "period",
18244            value: ParamValue::Int(8),
18245        }];
18246        let combos = [
18247            IndicatorParamSet { params: &combo_1 },
18248            IndicatorParamSet { params: &combo_2 },
18249            IndicatorParamSet { params: &combo_3 },
18250        ];
18251        let req = IndicatorBatchRequest {
18252            indicator_id: "dx",
18253            output_id: Some("value"),
18254            data: IndicatorDataRef::Ohlc {
18255                open: &open,
18256                high: &high,
18257                low: &low,
18258                close: &close,
18259            },
18260            combos: &combos,
18261            kernel: Kernel::Auto,
18262        };
18263        let out = compute_cpu_batch(req).unwrap();
18264        let matrix = out.values_f64.unwrap();
18265        for (row, period) in [9usize, 5usize, 8usize].iter().enumerate() {
18266            let input = DxInput::from_hlc_slices(
18267                &high,
18268                &low,
18269                &close,
18270                DxParams {
18271                    period: Some(*period),
18272                },
18273            );
18274            let direct = dx_with_kernel(&input, Kernel::Auto.to_non_batch())
18275                .unwrap()
18276                .values;
18277            let start = row * close.len();
18278            let end = start + close.len();
18279            assert_series_eq(&matrix[start..end], &direct, 1e-12);
18280        }
18281    }
18282
18283    #[test]
18284    fn trix_dispatch_period_sweep_keeps_requested_row_order() {
18285        let data = sample_series();
18286        let combo_1 = [ParamKV {
18287            key: "period",
18288            value: ParamValue::Int(9),
18289        }];
18290        let combo_2 = [ParamKV {
18291            key: "period",
18292            value: ParamValue::Int(7),
18293        }];
18294        let combo_3 = [ParamKV {
18295            key: "period",
18296            value: ParamValue::Int(5),
18297        }];
18298        let combos = [
18299            IndicatorParamSet { params: &combo_1 },
18300            IndicatorParamSet { params: &combo_2 },
18301            IndicatorParamSet { params: &combo_3 },
18302        ];
18303        let dispatch = compute_cpu_batch(IndicatorBatchRequest {
18304            indicator_id: "trix",
18305            output_id: Some("value"),
18306            data: IndicatorDataRef::Slice { values: &data },
18307            combos: &combos,
18308            kernel: Kernel::Auto,
18309        })
18310        .unwrap();
18311
18312        let direct =
18313            trix_batch_with_kernel(&data, &TrixBatchRange { period: (9, 5, 2) }, Kernel::Auto)
18314                .unwrap();
18315        let direct_periods: Vec<usize> = direct
18316            .combos
18317            .iter()
18318            .map(|combo| combo.period.unwrap_or(18))
18319            .collect();
18320        let period_to_row: std::collections::HashMap<usize, usize> = direct_periods
18321            .iter()
18322            .copied()
18323            .enumerate()
18324            .map(|(row, period)| (period, row))
18325            .collect();
18326        let requested = [9usize, 7usize, 5usize];
18327        let mut expected = Vec::with_capacity(requested.len() * direct.cols);
18328        for period in requested {
18329            let row = period_to_row[&period];
18330            let start = row * direct.cols;
18331            let end = start + direct.cols;
18332            expected.extend_from_slice(&direct.values[start..end]);
18333        }
18334        assert_eq!(dispatch.rows, requested.len());
18335        assert_eq!(dispatch.cols, direct.cols);
18336        assert_series_eq(dispatch.values_f64.as_ref().unwrap(), &expected, 1e-12);
18337    }
18338
18339    #[test]
18340    fn trix_non_sweep_fallback_rows_match_direct() {
18341        let data = sample_series();
18342        let combo_1 = [ParamKV {
18343            key: "period",
18344            value: ParamValue::Int(9),
18345        }];
18346        let combo_2 = [ParamKV {
18347            key: "period",
18348            value: ParamValue::Int(5),
18349        }];
18350        let combo_3 = [ParamKV {
18351            key: "period",
18352            value: ParamValue::Int(8),
18353        }];
18354        let combos = [
18355            IndicatorParamSet { params: &combo_1 },
18356            IndicatorParamSet { params: &combo_2 },
18357            IndicatorParamSet { params: &combo_3 },
18358        ];
18359        let out = compute_cpu_batch(IndicatorBatchRequest {
18360            indicator_id: "trix",
18361            output_id: Some("value"),
18362            data: IndicatorDataRef::Slice { values: &data },
18363            combos: &combos,
18364            kernel: Kernel::Auto,
18365        })
18366        .unwrap();
18367        let matrix = out.values_f64.unwrap();
18368        for (row, period) in [9usize, 5usize, 8usize].iter().enumerate() {
18369            let input = TrixInput::from_slice(
18370                &data,
18371                TrixParams {
18372                    period: Some(*period),
18373                },
18374            );
18375            let direct = trix_with_kernel(&input, Kernel::Auto.to_non_batch())
18376                .unwrap()
18377                .values;
18378            let start = row * data.len();
18379            let end = start + data.len();
18380            assert_series_eq(&matrix[start..end], &direct, 1e-12);
18381        }
18382    }
18383
18384    #[test]
18385    fn ift_rsi_output_matches_direct() {
18386        let data = sample_series();
18387        let combo = [
18388            ParamKV {
18389                key: "rsi_period",
18390                value: ParamValue::Int(6),
18391            },
18392            ParamKV {
18393                key: "wma_period",
18394                value: ParamValue::Int(10),
18395            },
18396        ];
18397        let combos = [IndicatorParamSet { params: &combo }];
18398        let req = IndicatorBatchRequest {
18399            indicator_id: "ift_rsi",
18400            output_id: Some("value"),
18401            data: IndicatorDataRef::Slice { values: &data },
18402            combos: &combos,
18403            kernel: Kernel::Auto,
18404        };
18405        let out = compute_cpu_batch(req).unwrap();
18406        let input = IftRsiInput::from_slice(
18407            &data,
18408            IftRsiParams {
18409                rsi_period: Some(6),
18410                wma_period: Some(10),
18411            },
18412        );
18413        let direct = ift_rsi_with_kernel(&input, Kernel::Auto.to_non_batch())
18414            .unwrap()
18415            .values;
18416        let got = out.values_f64.unwrap();
18417        assert_series_eq(&got, &direct, 1e-12);
18418    }
18419
18420    #[test]
18421    fn fosc_output_matches_direct() {
18422        let data = sample_series();
18423        let combo = [ParamKV {
18424            key: "period",
18425            value: ParamValue::Int(8),
18426        }];
18427        let combos = [IndicatorParamSet { params: &combo }];
18428        let req = IndicatorBatchRequest {
18429            indicator_id: "fosc",
18430            output_id: Some("value"),
18431            data: IndicatorDataRef::Slice { values: &data },
18432            combos: &combos,
18433            kernel: Kernel::Auto,
18434        };
18435        let out = compute_cpu_batch(req).unwrap();
18436        let input = FoscInput::from_slice(&data, FoscParams { period: Some(8) });
18437        let direct = fosc_with_kernel(&input, Kernel::Auto.to_non_batch())
18438            .unwrap()
18439            .values;
18440        let got = out.values_f64.unwrap();
18441        assert_series_eq(&got, &direct, 1e-12);
18442    }
18443
18444    #[test]
18445    fn linearreg_angle_output_matches_direct() {
18446        let data = sample_series();
18447        let combo = [ParamKV {
18448            key: "period",
18449            value: ParamValue::Int(14),
18450        }];
18451        let combos = [IndicatorParamSet { params: &combo }];
18452        let req = IndicatorBatchRequest {
18453            indicator_id: "linearreg_angle",
18454            output_id: Some("value"),
18455            data: IndicatorDataRef::Slice { values: &data },
18456            combos: &combos,
18457            kernel: Kernel::Auto,
18458        };
18459        let out = compute_cpu_batch(req).unwrap();
18460        let input =
18461            Linearreg_angleInput::from_slice(&data, Linearreg_angleParams { period: Some(14) });
18462        let direct = linearreg_angle_with_kernel(&input, Kernel::Auto.to_non_batch())
18463            .unwrap()
18464            .values;
18465        let got = out.values_f64.unwrap();
18466        assert_series_eq(&got, &direct, 1e-12);
18467    }
18468
18469    #[test]
18470    fn linearreg_intercept_output_matches_direct() {
18471        let data = sample_series();
18472        let combo = [ParamKV {
18473            key: "period",
18474            value: ParamValue::Int(14),
18475        }];
18476        let combos = [IndicatorParamSet { params: &combo }];
18477        let req = IndicatorBatchRequest {
18478            indicator_id: "linearreg_intercept",
18479            output_id: Some("value"),
18480            data: IndicatorDataRef::Slice { values: &data },
18481            combos: &combos,
18482            kernel: Kernel::Auto,
18483        };
18484        let out = compute_cpu_batch(req).unwrap();
18485        let input = LinearRegInterceptInput::from_slice(
18486            &data,
18487            LinearRegInterceptParams { period: Some(14) },
18488        );
18489        let direct = linearreg_intercept_with_kernel(&input, Kernel::Auto.to_non_batch())
18490            .unwrap()
18491            .values;
18492        let got = out.values_f64.unwrap();
18493        assert_series_eq(&got, &direct, 1e-12);
18494    }
18495
18496    #[test]
18497    fn cg_output_matches_direct() {
18498        let data = sample_series();
18499        let combo = [ParamKV {
18500            key: "period",
18501            value: ParamValue::Int(10),
18502        }];
18503        let combos = [IndicatorParamSet { params: &combo }];
18504        let req = IndicatorBatchRequest {
18505            indicator_id: "cg",
18506            output_id: Some("value"),
18507            data: IndicatorDataRef::Slice { values: &data },
18508            combos: &combos,
18509            kernel: Kernel::Auto,
18510        };
18511        let out = compute_cpu_batch(req).unwrap();
18512        let input = CgInput::from_slice(&data, CgParams { period: Some(10) });
18513        let direct = cg_with_kernel(&input, Kernel::Auto.to_non_batch())
18514            .unwrap()
18515            .values;
18516        let got = out.values_f64.unwrap();
18517        assert_series_eq(&got, &direct, 1e-12);
18518    }
18519
18520    #[test]
18521    fn linearreg_slope_output_matches_direct() {
18522        let data = sample_series();
18523        let combo = [ParamKV {
18524            key: "period",
18525            value: ParamValue::Int(14),
18526        }];
18527        let combos = [IndicatorParamSet { params: &combo }];
18528        let req = IndicatorBatchRequest {
18529            indicator_id: "linearreg_slope",
18530            output_id: Some("value"),
18531            data: IndicatorDataRef::Slice { values: &data },
18532            combos: &combos,
18533            kernel: Kernel::Auto,
18534        };
18535        let out = compute_cpu_batch(req).unwrap();
18536        let input =
18537            LinearRegSlopeInput::from_slice(&data, LinearRegSlopeParams { period: Some(14) });
18538        let direct = linearreg_slope_with_kernel(&input, Kernel::Auto.to_non_batch())
18539            .unwrap()
18540            .values;
18541        let got = out.values_f64.unwrap();
18542        assert_series_eq(&got, &direct, 1e-12);
18543    }
18544
18545    #[test]
18546    fn mean_ad_output_matches_direct() {
18547        let data = sample_series();
18548        let combo = [ParamKV {
18549            key: "period",
18550            value: ParamValue::Int(7),
18551        }];
18552        let combos = [IndicatorParamSet { params: &combo }];
18553        let req = IndicatorBatchRequest {
18554            indicator_id: "mean_ad",
18555            output_id: Some("value"),
18556            data: IndicatorDataRef::Slice { values: &data },
18557            combos: &combos,
18558            kernel: Kernel::Auto,
18559        };
18560        let out = compute_cpu_batch(req).unwrap();
18561        let input = MeanAdInput::from_slice(&data, MeanAdParams { period: Some(7) });
18562        let direct = mean_ad_with_kernel(&input, Kernel::Auto.to_non_batch())
18563            .unwrap()
18564            .values;
18565        let got = out.values_f64.unwrap();
18566        assert_series_eq(&got, &direct, 1e-12);
18567    }
18568
18569    #[test]
18570    fn deviation_output_matches_direct() {
18571        let data = sample_series();
18572        let combo = [
18573            ParamKV {
18574                key: "period",
18575                value: ParamValue::Int(9),
18576            },
18577            ParamKV {
18578                key: "devtype",
18579                value: ParamValue::Int(2),
18580            },
18581        ];
18582        let combos = [IndicatorParamSet { params: &combo }];
18583        let req = IndicatorBatchRequest {
18584            indicator_id: "deviation",
18585            output_id: Some("value"),
18586            data: IndicatorDataRef::Slice { values: &data },
18587            combos: &combos,
18588            kernel: Kernel::Auto,
18589        };
18590        let out = compute_cpu_batch(req).unwrap();
18591        let input = DeviationInput::from_slice(
18592            &data,
18593            DeviationParams {
18594                period: Some(9),
18595                devtype: Some(2),
18596            },
18597        );
18598        let direct = deviation_with_kernel(&input, Kernel::Auto.to_non_batch())
18599            .unwrap()
18600            .values;
18601        let got = out.values_f64.unwrap();
18602        assert_series_eq(&got, &direct, 1e-12);
18603    }
18604
18605    #[test]
18606    fn medprice_output_matches_direct() {
18607        let (_open, high, low, _close) = sample_ohlc();
18608        let combos = [IndicatorParamSet { params: &[] }];
18609        let req = IndicatorBatchRequest {
18610            indicator_id: "medprice",
18611            output_id: Some("value"),
18612            data: IndicatorDataRef::HighLow {
18613                high: &high,
18614                low: &low,
18615            },
18616            combos: &combos,
18617            kernel: Kernel::Auto,
18618        };
18619        let out = compute_cpu_batch(req).unwrap();
18620        let input = MedpriceInput::from_slices(&high, &low, MedpriceParams::default());
18621        let direct = medprice_with_kernel(&input, Kernel::Auto.to_non_batch())
18622            .unwrap()
18623            .values;
18624        let got = out.values_f64.unwrap();
18625        assert_series_eq(&got, &direct, 1e-12);
18626    }
18627
18628    #[test]
18629    fn percentile_nearest_rank_output_matches_direct() {
18630        let data = sample_series();
18631        let combo = [
18632            ParamKV {
18633                key: "length",
18634                value: ParamValue::Int(12),
18635            },
18636            ParamKV {
18637                key: "percentage",
18638                value: ParamValue::Float(70.0),
18639            },
18640        ];
18641        let combos = [IndicatorParamSet { params: &combo }];
18642        let req = IndicatorBatchRequest {
18643            indicator_id: "percentile_nearest_rank",
18644            output_id: Some("value"),
18645            data: IndicatorDataRef::Slice { values: &data },
18646            combos: &combos,
18647            kernel: Kernel::Auto,
18648        };
18649        let out = compute_cpu_batch(req).unwrap();
18650        let input = PercentileNearestRankInput::from_slice(
18651            &data,
18652            PercentileNearestRankParams {
18653                length: Some(12),
18654                percentage: Some(70.0),
18655            },
18656        );
18657        let direct = percentile_nearest_rank_with_kernel(&input, Kernel::Auto.to_non_batch())
18658            .unwrap()
18659            .values;
18660        let got = out.values_f64.unwrap();
18661        assert_series_eq(&got, &direct, 1e-12);
18662    }
18663
18664    #[test]
18665    fn zscore_output_matches_direct() {
18666        let data = sample_series();
18667        let combo = [
18668            ParamKV {
18669                key: "period",
18670                value: ParamValue::Int(14),
18671            },
18672            ParamKV {
18673                key: "ma_type",
18674                value: ParamValue::EnumString("ema"),
18675            },
18676            ParamKV {
18677                key: "nbdev",
18678                value: ParamValue::Float(1.25),
18679            },
18680            ParamKV {
18681                key: "devtype",
18682                value: ParamValue::Int(1),
18683            },
18684        ];
18685        let combos = [IndicatorParamSet { params: &combo }];
18686        let req = IndicatorBatchRequest {
18687            indicator_id: "zscore",
18688            output_id: Some("value"),
18689            data: IndicatorDataRef::Slice { values: &data },
18690            combos: &combos,
18691            kernel: Kernel::Auto,
18692        };
18693        let out = compute_cpu_batch(req).unwrap();
18694        let input = ZscoreInput::from_slice(
18695            &data,
18696            ZscoreParams {
18697                period: Some(14),
18698                ma_type: Some("ema".to_string()),
18699                nbdev: Some(1.25),
18700                devtype: Some(1),
18701            },
18702        );
18703        let direct = zscore_with_kernel(&input, Kernel::Auto.to_non_batch())
18704            .unwrap()
18705            .values;
18706        let got = out.values_f64.unwrap();
18707        assert_series_eq(&got, &direct, 1e-12);
18708    }
18709
18710    #[test]
18711    fn vpci_secondary_output_matches_direct() {
18712        let close = sample_series();
18713        let volume: Vec<f64> = (0..close.len())
18714            .map(|i| 1000.0 + (i as f64 * 7.0))
18715            .collect();
18716        let combo = [
18717            ParamKV {
18718                key: "short_range",
18719                value: ParamValue::Int(5),
18720            },
18721            ParamKV {
18722                key: "long_range",
18723                value: ParamValue::Int(25),
18724            },
18725        ];
18726        let combos = [IndicatorParamSet { params: &combo }];
18727        let req = IndicatorBatchRequest {
18728            indicator_id: "vpci",
18729            output_id: Some("vpcis"),
18730            data: IndicatorDataRef::CloseVolume {
18731                close: &close,
18732                volume: &volume,
18733            },
18734            combos: &combos,
18735            kernel: Kernel::Auto,
18736        };
18737        let out = compute_cpu_batch(req).unwrap();
18738        let input = VpciInput::from_slices(
18739            &close,
18740            &volume,
18741            VpciParams {
18742                short_range: Some(5),
18743                long_range: Some(25),
18744            },
18745        );
18746        let direct = vpci_with_kernel(&input, Kernel::Auto.to_non_batch())
18747            .unwrap()
18748            .vpcis;
18749        let got = out.values_f64.unwrap();
18750        assert_series_eq(&got, &direct, 1e-12);
18751    }
18752
18753    #[test]
18754    fn yang_zhang_secondary_output_matches_direct() {
18755        let (open, high, low, close) = sample_ohlc();
18756        let combo = [
18757            ParamKV {
18758                key: "lookback",
18759                value: ParamValue::Int(21),
18760            },
18761            ParamKV {
18762                key: "k_override",
18763                value: ParamValue::Bool(true),
18764            },
18765            ParamKV {
18766                key: "k",
18767                value: ParamValue::Float(0.28),
18768            },
18769        ];
18770        let combos = [IndicatorParamSet { params: &combo }];
18771        let req = IndicatorBatchRequest {
18772            indicator_id: "yang_zhang_volatility",
18773            output_id: Some("rs"),
18774            data: IndicatorDataRef::Ohlc {
18775                open: &open,
18776                high: &high,
18777                low: &low,
18778                close: &close,
18779            },
18780            combos: &combos,
18781            kernel: Kernel::Auto,
18782        };
18783        let out = compute_cpu_batch(req).unwrap();
18784        let input = YangZhangVolatilityInput::from_slices(
18785            &open,
18786            &high,
18787            &low,
18788            &close,
18789            YangZhangVolatilityParams {
18790                lookback: Some(21),
18791                k_override: Some(true),
18792                k: Some(0.28),
18793            },
18794        );
18795        let direct = yang_zhang_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
18796            .unwrap()
18797            .rs;
18798        let got = out.values_f64.unwrap();
18799        assert_series_eq(&got, &direct, 1e-12);
18800    }
18801
18802    #[test]
18803    fn historical_volatility_percentile_signal_output_matches_direct() {
18804        let data = sample_series();
18805        let combo = [
18806            ParamKV {
18807                key: "length",
18808                value: ParamValue::Int(5),
18809            },
18810            ParamKV {
18811                key: "annual_length",
18812                value: ParamValue::Int(10),
18813            },
18814        ];
18815        let combos = [IndicatorParamSet { params: &combo }];
18816        let req = IndicatorBatchRequest {
18817            indicator_id: "historical_volatility_percentile",
18818            output_id: Some("hvp_sma"),
18819            data: IndicatorDataRef::Slice { values: &data },
18820            combos: &combos,
18821            kernel: Kernel::Auto,
18822        };
18823        let out = compute_cpu_batch(req).unwrap();
18824        let input = HistoricalVolatilityPercentileInput::from_slice(
18825            &data,
18826            HistoricalVolatilityPercentileParams {
18827                length: Some(5),
18828                annual_length: Some(10),
18829            },
18830        );
18831        let direct =
18832            historical_volatility_percentile_with_kernel(&input, Kernel::Auto.to_non_batch())
18833                .unwrap()
18834                .hvp_sma;
18835        let got = out.values_f64.unwrap();
18836        assert_series_eq(&got, &direct, 1e-12);
18837    }
18838
18839    #[test]
18840    fn volatility_ratio_adaptive_rsx_signal_output_matches_direct() {
18841        let data = sample_series();
18842        let combo = [
18843            ParamKV {
18844                key: "period",
18845                value: ParamValue::Int(6),
18846            },
18847            ParamKV {
18848                key: "speed",
18849                value: ParamValue::Float(0.5),
18850            },
18851        ];
18852        let combos = [IndicatorParamSet { params: &combo }];
18853        let req = IndicatorBatchRequest {
18854            indicator_id: "volatility_ratio_adaptive_rsx",
18855            output_id: Some("signal"),
18856            data: IndicatorDataRef::Slice { values: &data },
18857            combos: &combos,
18858            kernel: Kernel::Auto,
18859        };
18860        let out = compute_cpu_batch(req).unwrap();
18861        let input = VolatilityRatioAdaptiveRsxInput::from_slice(
18862            &data,
18863            VolatilityRatioAdaptiveRsxParams {
18864                period: Some(6),
18865                speed: Some(0.5),
18866            },
18867        );
18868        let direct = volatility_ratio_adaptive_rsx_with_kernel(&input, Kernel::Auto.to_non_batch())
18869            .unwrap()
18870            .signal;
18871        let got = out.values_f64.unwrap();
18872        assert_series_eq(&got, &direct, 1e-12);
18873    }
18874
18875    #[test]
18876    fn on_balance_volume_oscillator_signal_output_matches_direct() {
18877        let close = sample_series();
18878        let volume: Vec<f64> = (0..close.len()).map(|i| 1000.0 + i as f64 * 3.0).collect();
18879        let combo = [
18880            ParamKV {
18881                key: "obv_length",
18882                value: ParamValue::Int(20),
18883            },
18884            ParamKV {
18885                key: "ema_length",
18886                value: ParamValue::Int(9),
18887            },
18888        ];
18889        let combos = [IndicatorParamSet { params: &combo }];
18890        let req = IndicatorBatchRequest {
18891            indicator_id: "on_balance_volume_oscillator",
18892            output_id: Some("signal"),
18893            data: IndicatorDataRef::CloseVolume {
18894                close: &close,
18895                volume: &volume,
18896            },
18897            combos: &combos,
18898            kernel: Kernel::Auto,
18899        };
18900        let out = compute_cpu_batch(req).unwrap();
18901        let input = OnBalanceVolumeOscillatorInput::from_slices(
18902            &close,
18903            &volume,
18904            OnBalanceVolumeOscillatorParams {
18905                obv_length: Some(20),
18906                ema_length: Some(9),
18907            },
18908        );
18909        let direct = on_balance_volume_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
18910            .unwrap()
18911            .signal;
18912        let got = out.values_f64.unwrap();
18913        assert_series_eq(&got, &direct, 1e-12);
18914    }
18915
18916    #[test]
18917    fn twiggs_money_flow_smoothed_output_matches_direct() {
18918        let open = vec![10.0, 10.2, 10.4, 10.7, 10.9, 11.1, 11.3, 11.5, 11.7, 11.9];
18919        let high = vec![10.4, 10.7, 10.9, 11.1, 11.4, 11.6, 11.8, 12.0, 12.2, 12.4];
18920        let low = vec![9.8, 10.0, 10.2, 10.5, 10.7, 10.9, 11.1, 11.3, 11.5, 11.7];
18921        let close = vec![10.1, 10.5, 10.7, 10.9, 11.2, 11.4, 11.6, 11.8, 12.0, 12.2];
18922        let volume = vec![
18923            1000.0, 1015.0, 1030.0, 1045.0, 1060.0, 1075.0, 1090.0, 1105.0, 1120.0, 1135.0,
18924        ];
18925        let combo = [
18926            ParamKV {
18927                key: "length",
18928                value: ParamValue::Int(5),
18929            },
18930            ParamKV {
18931                key: "smoothing_length",
18932                value: ParamValue::Int(4),
18933            },
18934            ParamKV {
18935                key: "ma_type",
18936                value: ParamValue::EnumString("WMA"),
18937            },
18938        ];
18939        let combos = [IndicatorParamSet { params: &combo }];
18940        let req = IndicatorBatchRequest {
18941            indicator_id: "twiggs_money_flow",
18942            output_id: Some("smoothed"),
18943            data: IndicatorDataRef::Ohlcv {
18944                open: &open,
18945                high: &high,
18946                low: &low,
18947                close: &close,
18948                volume: &volume,
18949            },
18950            combos: &combos,
18951            kernel: Kernel::Auto,
18952        };
18953        let out = compute_cpu_batch(req).unwrap();
18954        let input = TwiggsMoneyFlowInput::from_slices(
18955            &high,
18956            &low,
18957            &close,
18958            &volume,
18959            TwiggsMoneyFlowParams {
18960                length: Some(5),
18961                smoothing_length: Some(4),
18962                ma_type: Some("WMA".to_string()),
18963            },
18964        );
18965        let direct = twiggs_money_flow_with_kernel(&input, Kernel::Auto.to_non_batch())
18966            .unwrap()
18967            .smoothed;
18968        let got = out.values_f64.unwrap();
18969        assert_series_eq(&got, &direct, 1e-12);
18970    }
18971
18972    #[test]
18973    fn parkinson_variance_output_matches_direct() {
18974        let (_open, high, low, _close) = sample_ohlc();
18975        let combo = [ParamKV {
18976            key: "period",
18977            value: ParamValue::Int(9),
18978        }];
18979        let combos = [IndicatorParamSet { params: &combo }];
18980        let req = IndicatorBatchRequest {
18981            indicator_id: "parkinson_volatility",
18982            output_id: Some("variance"),
18983            data: IndicatorDataRef::HighLow {
18984                high: &high,
18985                low: &low,
18986            },
18987            combos: &combos,
18988            kernel: Kernel::Auto,
18989        };
18990        let out = compute_cpu_batch(req).unwrap();
18991        let input = ParkinsonVolatilityInput::from_slices(
18992            &high,
18993            &low,
18994            ParkinsonVolatilityParams { period: Some(9) },
18995        );
18996        let direct = parkinson_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
18997            .unwrap()
18998            .variance;
18999        let got = out.values_f64.unwrap();
19000        assert_series_eq(&got, &direct, 1e-12);
19001    }
19002
19003    #[test]
19004    fn l2_ehlers_signal_to_noise_output_matches_direct() {
19005        let candles = sample_candles();
19006        let combo = [
19007            ParamKV {
19008                key: "source",
19009                value: ParamValue::EnumString("hl2"),
19010            },
19011            ParamKV {
19012                key: "smooth_period",
19013                value: ParamValue::Int(10),
19014            },
19015        ];
19016        let combos = [IndicatorParamSet { params: &combo }];
19017        let req = IndicatorBatchRequest {
19018            indicator_id: "l2_ehlers_signal_to_noise",
19019            output_id: Some("value"),
19020            data: IndicatorDataRef::Candles {
19021                candles: &candles,
19022                source: Some("hl2"),
19023            },
19024            combos: &combos,
19025            kernel: Kernel::Auto,
19026        };
19027        let out = compute_cpu_batch(req).unwrap();
19028        let input = L2EhlersSignalToNoiseInput::from_slices(
19029            crate::utilities::data_loader::source_type(&candles, "hl2"),
19030            candles.high.as_slice(),
19031            candles.low.as_slice(),
19032            L2EhlersSignalToNoiseParams {
19033                smooth_period: Some(10),
19034            },
19035        );
19036        let direct = l2_ehlers_signal_to_noise_with_kernel(&input, Kernel::Auto.to_non_batch())
19037            .unwrap()
19038            .values;
19039        let got = out.values_f64.unwrap();
19040        assert_series_eq(&got, &direct, 1e-12);
19041    }
19042
19043    #[test]
19044    fn cycle_channel_oscillator_output_matches_direct() {
19045        let candles = sample_candles();
19046        let combo = [
19047            ParamKV {
19048                key: "source",
19049                value: ParamValue::EnumString("close"),
19050            },
19051            ParamKV {
19052                key: "short_cycle_length",
19053                value: ParamValue::Int(10),
19054            },
19055            ParamKV {
19056                key: "medium_cycle_length",
19057                value: ParamValue::Int(30),
19058            },
19059            ParamKV {
19060                key: "short_multiplier",
19061                value: ParamValue::Float(1.0),
19062            },
19063            ParamKV {
19064                key: "medium_multiplier",
19065                value: ParamValue::Float(3.0),
19066            },
19067        ];
19068        let combos = [IndicatorParamSet { params: &combo }];
19069        let req = IndicatorBatchRequest {
19070            indicator_id: "cycle_channel_oscillator",
19071            output_id: Some("fast"),
19072            data: IndicatorDataRef::Candles {
19073                candles: &candles,
19074                source: Some("close"),
19075            },
19076            combos: &combos,
19077            kernel: Kernel::Auto,
19078        };
19079        let out = compute_cpu_batch(req).unwrap();
19080        let input = CycleChannelOscillatorInput::from_slices(
19081            crate::utilities::data_loader::source_type(&candles, "close"),
19082            candles.high.as_slice(),
19083            candles.low.as_slice(),
19084            candles.close.as_slice(),
19085            CycleChannelOscillatorParams {
19086                short_cycle_length: Some(10),
19087                medium_cycle_length: Some(30),
19088                short_multiplier: Some(1.0),
19089                medium_multiplier: Some(3.0),
19090            },
19091        );
19092        let direct = cycle_channel_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
19093            .unwrap()
19094            .fast;
19095        let got = out.values_f64.unwrap();
19096        assert_series_eq(&got, &direct, 1e-12);
19097    }
19098
19099    #[test]
19100    fn andean_oscillator_output_matches_direct() {
19101        let candles = sample_candles();
19102        let combo = [
19103            ParamKV {
19104                key: "length",
19105                value: ParamValue::Int(50),
19106            },
19107            ParamKV {
19108                key: "signal_length",
19109                value: ParamValue::Int(9),
19110            },
19111        ];
19112        let combos = [IndicatorParamSet { params: &combo }];
19113        let req = IndicatorBatchRequest {
19114            indicator_id: "andean_oscillator",
19115            output_id: Some("bull"),
19116            data: IndicatorDataRef::Candles {
19117                candles: &candles,
19118                source: None,
19119            },
19120            combos: &combos,
19121            kernel: Kernel::Auto,
19122        };
19123        let out = compute_cpu_batch(req).unwrap();
19124        let input = AndeanOscillatorInput::from_slices(
19125            candles.open.as_slice(),
19126            candles.close.as_slice(),
19127            AndeanOscillatorParams {
19128                length: Some(50),
19129                signal_length: Some(9),
19130            },
19131        );
19132        let direct = andean_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
19133            .unwrap()
19134            .bull;
19135        let got = out.values_f64.unwrap();
19136        assert_series_eq(&got, &direct, 1e-12);
19137    }
19138
19139    #[test]
19140    fn daily_factor_output_matches_direct() {
19141        let (open, high, low, close) = sample_ohlc();
19142        let combo = [ParamKV {
19143            key: "threshold_level",
19144            value: ParamValue::Float(0.35),
19145        }];
19146        let combos = [IndicatorParamSet { params: &combo }];
19147        let req = IndicatorBatchRequest {
19148            indicator_id: "daily_factor",
19149            output_id: Some("signal"),
19150            data: IndicatorDataRef::Ohlc {
19151                open: &open,
19152                high: &high,
19153                low: &low,
19154                close: &close,
19155            },
19156            combos: &combos,
19157            kernel: Kernel::Auto,
19158        };
19159        let out = compute_cpu_batch(req).unwrap();
19160        let input = DailyFactorInput::from_slices(
19161            &open,
19162            &high,
19163            &low,
19164            &close,
19165            DailyFactorParams {
19166                threshold_level: Some(0.35),
19167            },
19168        );
19169        let direct = daily_factor_with_kernel(&input, Kernel::Auto.to_non_batch())
19170            .unwrap()
19171            .signal;
19172        let got = out.values_f64.unwrap();
19173        assert_series_eq(&got, &direct, 1e-12);
19174    }
19175
19176    #[test]
19177    fn ehlers_adaptive_cyber_cycle_output_matches_direct() {
19178        let candles = sample_candles();
19179        let combo = [
19180            ParamKV {
19181                key: "source",
19182                value: ParamValue::EnumString("hl2"),
19183            },
19184            ParamKV {
19185                key: "alpha",
19186                value: ParamValue::Float(0.07),
19187            },
19188        ];
19189        let combos = [IndicatorParamSet { params: &combo }];
19190        let req = IndicatorBatchRequest {
19191            indicator_id: "ehlers_adaptive_cyber_cycle",
19192            output_id: Some("cycle"),
19193            data: IndicatorDataRef::Candles {
19194                candles: &candles,
19195                source: Some("hl2"),
19196            },
19197            combos: &combos,
19198            kernel: Kernel::Auto,
19199        };
19200        let out = compute_cpu_batch(req).unwrap();
19201        let input = EhlersAdaptiveCyberCycleInput::from_slice(
19202            crate::utilities::data_loader::source_type(&candles, "hl2"),
19203            EhlersAdaptiveCyberCycleParams { alpha: Some(0.07) },
19204        );
19205        let direct = ehlers_adaptive_cyber_cycle_with_kernel(&input, Kernel::Auto.to_non_batch())
19206            .unwrap()
19207            .cycle;
19208        let got = out.values_f64.unwrap();
19209        assert_series_eq(&got, &direct, 1e-12);
19210    }
19211
19212    #[test]
19213    fn ehlers_simple_cycle_indicator_output_matches_direct() {
19214        let candles = sample_candles();
19215        let combo = [
19216            ParamKV {
19217                key: "source",
19218                value: ParamValue::EnumString("hl2"),
19219            },
19220            ParamKV {
19221                key: "alpha",
19222                value: ParamValue::Float(0.07),
19223            },
19224        ];
19225        let combos = [IndicatorParamSet { params: &combo }];
19226        let req = IndicatorBatchRequest {
19227            indicator_id: "ehlers_simple_cycle_indicator",
19228            output_id: Some("cycle"),
19229            data: IndicatorDataRef::Candles {
19230                candles: &candles,
19231                source: Some("hl2"),
19232            },
19233            combos: &combos,
19234            kernel: Kernel::Auto,
19235        };
19236        let out = compute_cpu_batch(req).unwrap();
19237        let input = EhlersSimpleCycleIndicatorInput::from_slice(
19238            crate::utilities::data_loader::source_type(&candles, "hl2"),
19239            EhlersSimpleCycleIndicatorParams { alpha: Some(0.07) },
19240        );
19241        let direct = ehlers_simple_cycle_indicator_with_kernel(&input, Kernel::Auto.to_non_batch())
19242            .unwrap()
19243            .cycle;
19244        let got = out.values_f64.unwrap();
19245        assert_series_eq(&got, &direct, 1e-12);
19246    }
19247
19248    #[test]
19249    fn l1_ehlers_phasor_output_matches_direct() {
19250        let candles = sample_candles();
19251        let combo = [ParamKV {
19252            key: "domestic_cycle_length",
19253            value: ParamValue::Int(15),
19254        }];
19255        let combos = [IndicatorParamSet { params: &combo }];
19256        let req = IndicatorBatchRequest {
19257            indicator_id: "l1_ehlers_phasor",
19258            output_id: Some("value"),
19259            data: IndicatorDataRef::Candles {
19260                candles: &candles,
19261                source: Some("close"),
19262            },
19263            combos: &combos,
19264            kernel: Kernel::Auto,
19265        };
19266        let out = compute_cpu_batch(req).unwrap();
19267        let input = L1EhlersPhasorInput::from_slice(
19268            candles.close.as_slice(),
19269            L1EhlersPhasorParams {
19270                domestic_cycle_length: Some(15),
19271            },
19272        );
19273        let direct = l1_ehlers_phasor_with_kernel(&input, Kernel::Auto.to_non_batch())
19274            .unwrap()
19275            .values;
19276        let got = out.values_f64.unwrap();
19277        assert_series_eq(&got, &direct, 1e-12);
19278    }
19279
19280    #[test]
19281    fn ehlers_smoothed_adaptive_momentum_output_matches_direct() {
19282        let candles = sample_candles();
19283        let combo = [
19284            ParamKV {
19285                key: "source",
19286                value: ParamValue::EnumString("hl2"),
19287            },
19288            ParamKV {
19289                key: "alpha",
19290                value: ParamValue::Float(0.07),
19291            },
19292            ParamKV {
19293                key: "cutoff",
19294                value: ParamValue::Float(8.0),
19295            },
19296        ];
19297        let combos = [IndicatorParamSet { params: &combo }];
19298        let req = IndicatorBatchRequest {
19299            indicator_id: "ehlers_smoothed_adaptive_momentum",
19300            output_id: Some("value"),
19301            data: IndicatorDataRef::Candles {
19302                candles: &candles,
19303                source: Some("hl2"),
19304            },
19305            combos: &combos,
19306            kernel: Kernel::Auto,
19307        };
19308        let out = compute_cpu_batch(req).unwrap();
19309        let input = EhlersSmoothedAdaptiveMomentumInput::from_slice(
19310            crate::utilities::data_loader::source_type(&candles, "hl2"),
19311            EhlersSmoothedAdaptiveMomentumParams {
19312                alpha: Some(0.07),
19313                cutoff: Some(8.0),
19314            },
19315        );
19316        let direct =
19317            ehlers_smoothed_adaptive_momentum_with_kernel(&input, Kernel::Auto.to_non_batch())
19318                .unwrap()
19319                .values;
19320        let got = out.values_f64.unwrap();
19321        assert_series_eq(&got, &direct, 1e-12);
19322    }
19323
19324    #[test]
19325    fn ewma_volatility_output_matches_direct() {
19326        let close = sample_series();
19327        let combo = [ParamKV {
19328            key: "lambda",
19329            value: ParamValue::Float(0.94),
19330        }];
19331        let combos = [IndicatorParamSet { params: &combo }];
19332        let req = IndicatorBatchRequest {
19333            indicator_id: "ewma_volatility",
19334            output_id: Some("value"),
19335            data: IndicatorDataRef::Slice { values: &close },
19336            combos: &combos,
19337            kernel: Kernel::Auto,
19338        };
19339        let out = compute_cpu_batch(req).unwrap();
19340        let input =
19341            EwmaVolatilityInput::from_slice(&close, EwmaVolatilityParams { lambda: Some(0.94) });
19342        let direct = ewma_volatility_with_kernel(&input, Kernel::Auto.to_non_batch())
19343            .unwrap()
19344            .values;
19345        let got = out.values_f64.unwrap();
19346        assert_series_eq(&got, &direct, 1e-12);
19347    }
19348
19349    #[test]
19350    fn random_walk_index_output_matches_direct() {
19351        let open = sample_series();
19352        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
19353        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
19354        let close: Vec<f64> = open
19355            .iter()
19356            .enumerate()
19357            .map(|(i, v)| v + 0.1 * (i as f64 + 1.0))
19358            .collect();
19359        let combo = [ParamKV {
19360            key: "length",
19361            value: ParamValue::Int(14),
19362        }];
19363        let combos = [IndicatorParamSet { params: &combo }];
19364        let req = IndicatorBatchRequest {
19365            indicator_id: "random_walk_index",
19366            output_id: Some("high"),
19367            data: IndicatorDataRef::Ohlc {
19368                open: &open,
19369                high: &high,
19370                low: &low,
19371                close: &close,
19372            },
19373            combos: &combos,
19374            kernel: Kernel::Auto,
19375        };
19376        let out = compute_cpu_batch(req).unwrap();
19377        let input = RandomWalkIndexInput::from_slices(
19378            &high,
19379            &low,
19380            &close,
19381            RandomWalkIndexParams { length: Some(14) },
19382        );
19383        let direct = random_walk_index_with_kernel(&input, Kernel::Auto.to_non_batch())
19384            .unwrap()
19385            .high;
19386        let got = out.values_f64.unwrap();
19387        assert_series_eq(&got, &direct, 1e-12);
19388    }
19389
19390    #[test]
19391    fn price_moving_average_ratio_percentile_output_matches_direct() {
19392        let open = sample_series();
19393        let high: Vec<f64> = open
19394            .iter()
19395            .enumerate()
19396            .map(|(i, v)| v + 1.0 + (i as f64 * 0.03).sin() * 0.15)
19397            .collect();
19398        let low: Vec<f64> = open
19399            .iter()
19400            .enumerate()
19401            .map(|(i, v)| v - 1.0 - (i as f64 * 0.05).cos() * 0.12)
19402            .collect();
19403        let close: Vec<f64> = open
19404            .iter()
19405            .enumerate()
19406            .map(|(i, v)| v + 0.12 * (i as f64 + 1.0))
19407            .collect();
19408        let volume: Vec<f64> = (0..open.len())
19409            .map(|i| 1_000.0 + i as f64 * 2.0 + (i as f64 * 0.09).sin() * 40.0)
19410            .collect();
19411        let combo = [
19412            ParamKV {
19413                key: "source",
19414                value: ParamValue::EnumString("close"),
19415            },
19416            ParamKV {
19417                key: "ma_length",
19418                value: ParamValue::Int(20),
19419            },
19420            ParamKV {
19421                key: "ma_type",
19422                value: ParamValue::EnumString("vwma"),
19423            },
19424            ParamKV {
19425                key: "pmarp_lookback",
19426                value: ParamValue::Int(30),
19427            },
19428            ParamKV {
19429                key: "signal_ma_length",
19430                value: ParamValue::Int(10),
19431            },
19432            ParamKV {
19433                key: "signal_ma_type",
19434                value: ParamValue::EnumString("sma"),
19435            },
19436            ParamKV {
19437                key: "line_mode",
19438                value: ParamValue::EnumString("pmarp"),
19439            },
19440        ];
19441        let combos = [IndicatorParamSet { params: &combo }];
19442        let req = IndicatorBatchRequest {
19443            indicator_id: "price_moving_average_ratio_percentile",
19444            output_id: Some("plotline"),
19445            data: IndicatorDataRef::Candles {
19446                candles: &crate::utilities::data_loader::Candles {
19447                    timestamp: vec![0; open.len()],
19448                    open: open.clone(),
19449                    high: high.clone(),
19450                    low: low.clone(),
19451                    close: close.clone(),
19452                    volume: volume.clone(),
19453                    fields: crate::utilities::data_loader::CandleFieldFlags {
19454                        open: true,
19455                        high: true,
19456                        low: true,
19457                        close: true,
19458                        volume: true,
19459                    },
19460                    hl2: high
19461                        .iter()
19462                        .zip(low.iter())
19463                        .map(|(h, l)| (h + l) * 0.5)
19464                        .collect(),
19465                    hlc3: high
19466                        .iter()
19467                        .zip(low.iter())
19468                        .zip(close.iter())
19469                        .map(|((h, l), c)| (h + l + c) / 3.0)
19470                        .collect(),
19471                    ohlc4: open
19472                        .iter()
19473                        .zip(high.iter())
19474                        .zip(low.iter())
19475                        .zip(close.iter())
19476                        .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19477                        .collect(),
19478                    hlcc4: high
19479                        .iter()
19480                        .zip(low.iter())
19481                        .zip(close.iter())
19482                        .map(|((h, l), c)| (h + l + c + c) * 0.25)
19483                        .collect(),
19484                },
19485                source: Some("close"),
19486            },
19487            combos: &combos,
19488            kernel: Kernel::Auto,
19489        };
19490        let out = compute_cpu_batch(req).unwrap();
19491        let input = PriceMovingAverageRatioPercentileInput::from_slices(
19492            &close,
19493            &volume,
19494            PriceMovingAverageRatioPercentileParams {
19495                ma_length: Some(20),
19496                ma_type: Some(PriceMovingAverageRatioPercentileMaType::Vwma),
19497                pmarp_lookback: Some(30),
19498                signal_ma_length: Some(10),
19499                signal_ma_type: Some(PriceMovingAverageRatioPercentileMaType::Sma),
19500                line_mode: Some(PriceMovingAverageRatioPercentileLineMode::Pmarp),
19501            },
19502        );
19503        let direct =
19504            price_moving_average_ratio_percentile_with_kernel(&input, Kernel::Auto.to_non_batch())
19505                .unwrap()
19506                .plotline;
19507        let got = out.values_f64.unwrap();
19508        assert_series_eq(&got, &direct, 1e-12);
19509    }
19510
19511    #[test]
19512    fn trend_trigger_factor_output_matches_direct() {
19513        let base = sample_series();
19514        let high: Vec<f64> = base
19515            .iter()
19516            .enumerate()
19517            .map(|(i, v)| v + 1.0 + (i as f64 * 0.03).sin() * 0.15)
19518            .collect();
19519        let low: Vec<f64> = base
19520            .iter()
19521            .enumerate()
19522            .map(|(i, v)| v - 1.0 - (i as f64 * 0.05).cos() * 0.12)
19523            .collect();
19524        let combo = [ParamKV {
19525            key: "length",
19526            value: ParamValue::Int(15),
19527        }];
19528        let combos = [IndicatorParamSet { params: &combo }];
19529        let req = IndicatorBatchRequest {
19530            indicator_id: "trend_trigger_factor",
19531            output_id: Some("value"),
19532            data: IndicatorDataRef::HighLow {
19533                high: &high,
19534                low: &low,
19535            },
19536            combos: &combos,
19537            kernel: Kernel::Auto,
19538        };
19539        let out = compute_cpu_batch(req).unwrap();
19540        let input = TrendTriggerFactorInput::from_slices(
19541            &high,
19542            &low,
19543            TrendTriggerFactorParams { length: Some(15) },
19544        );
19545        let direct = trend_trigger_factor_with_kernel(&input, Kernel::Auto.to_non_batch())
19546            .unwrap()
19547            .values;
19548        let got = out.values_f64.unwrap();
19549        assert_series_eq(&got, &direct, 1e-12);
19550    }
19551
19552    #[test]
19553    fn mesa_stochastic_multi_length_output_matches_direct() {
19554        let source: Vec<f64> = (0..180)
19555            .map(|i| 100.0 + (i as f64 * 0.09).sin() * 2.0 + i as f64 * 0.015)
19556            .collect();
19557        let high: Vec<f64> = source.iter().map(|v| v + 1.0).collect();
19558        let low: Vec<f64> = source.iter().map(|v| v - 1.0).collect();
19559        let open = source.clone();
19560        let volume: Vec<f64> = (0..180).map(|i| 1000.0 + i as f64).collect();
19561        let combo = [
19562            ParamKV {
19563                key: "source",
19564                value: ParamValue::EnumString("close"),
19565            },
19566            ParamKV {
19567                key: "length_1",
19568                value: ParamValue::Int(48),
19569            },
19570            ParamKV {
19571                key: "length_2",
19572                value: ParamValue::Int(21),
19573            },
19574            ParamKV {
19575                key: "length_3",
19576                value: ParamValue::Int(9),
19577            },
19578            ParamKV {
19579                key: "length_4",
19580                value: ParamValue::Int(6),
19581            },
19582            ParamKV {
19583                key: "trigger_length",
19584                value: ParamValue::Int(2),
19585            },
19586        ];
19587        let combos = [IndicatorParamSet { params: &combo }];
19588        let req = IndicatorBatchRequest {
19589            indicator_id: "mesa_stochastic_multi_length",
19590            output_id: Some("mesa_1"),
19591            data: IndicatorDataRef::Candles {
19592                candles: &crate::utilities::data_loader::Candles {
19593                    timestamp: vec![0; source.len()],
19594                    open: open.clone(),
19595                    high: high.clone(),
19596                    low: low.clone(),
19597                    close: source.clone(),
19598                    volume,
19599                    fields: crate::utilities::data_loader::CandleFieldFlags {
19600                        open: true,
19601                        high: true,
19602                        low: true,
19603                        close: true,
19604                        volume: true,
19605                    },
19606                    hl2: high
19607                        .iter()
19608                        .zip(low.iter())
19609                        .map(|(h, l)| (h + l) * 0.5)
19610                        .collect(),
19611                    hlc3: high
19612                        .iter()
19613                        .zip(low.iter())
19614                        .zip(source.iter())
19615                        .map(|((h, l), c)| (h + l + c) / 3.0)
19616                        .collect(),
19617                    ohlc4: open
19618                        .iter()
19619                        .zip(high.iter())
19620                        .zip(low.iter())
19621                        .zip(source.iter())
19622                        .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19623                        .collect(),
19624                    hlcc4: high
19625                        .iter()
19626                        .zip(low.iter())
19627                        .zip(source.iter())
19628                        .map(|((h, l), c)| (h + l + c + c) * 0.25)
19629                        .collect(),
19630                },
19631                source: Some("close"),
19632            },
19633            combos: &combos,
19634            kernel: Kernel::Auto,
19635        };
19636        let out = compute_cpu_batch(req).unwrap();
19637        let input = MesaStochasticMultiLengthInput::from_slices(
19638            &source,
19639            MesaStochasticMultiLengthParams::default(),
19640        );
19641        let direct = mesa_stochastic_multi_length_with_kernel(&input, Kernel::Auto.to_non_batch())
19642            .unwrap()
19643            .mesa_1;
19644        let got = out.values_f64.unwrap();
19645        assert_series_eq(&got, &direct, 1e-12);
19646    }
19647
19648    #[test]
19649    fn spearman_correlation_output_matches_direct() {
19650        let close: Vec<f64> = (0..180)
19651            .map(|i| 100.0 + (i as f64 * 0.13).sin() * 2.0 + i as f64 * 0.02)
19652            .collect();
19653        let open: Vec<f64> = (0..180)
19654            .map(|i| 98.0 + (i as f64 * 0.07).cos() * 1.6 + i as f64 * 0.015)
19655            .collect();
19656        let high: Vec<f64> = close.iter().map(|v| v + 1.0).collect();
19657        let low: Vec<f64> = close.iter().map(|v| v - 1.0).collect();
19658        let volume: Vec<f64> = (0..180).map(|i| 1000.0 + i as f64).collect();
19659        let combo = [
19660            ParamKV {
19661                key: "source",
19662                value: ParamValue::EnumString("close"),
19663            },
19664            ParamKV {
19665                key: "comparison_source",
19666                value: ParamValue::EnumString("open"),
19667            },
19668            ParamKV {
19669                key: "lookback",
19670                value: ParamValue::Int(30),
19671            },
19672            ParamKV {
19673                key: "smoothing_length",
19674                value: ParamValue::Int(3),
19675            },
19676        ];
19677        let combos = [IndicatorParamSet { params: &combo }];
19678        let req = IndicatorBatchRequest {
19679            indicator_id: "spearman_correlation",
19680            output_id: Some("smoothed"),
19681            data: IndicatorDataRef::Candles {
19682                candles: &crate::utilities::data_loader::Candles {
19683                    timestamp: vec![0; close.len()],
19684                    open: open.clone(),
19685                    high: high.clone(),
19686                    low: low.clone(),
19687                    close: close.clone(),
19688                    volume,
19689                    fields: crate::utilities::data_loader::CandleFieldFlags {
19690                        open: true,
19691                        high: true,
19692                        low: true,
19693                        close: true,
19694                        volume: true,
19695                    },
19696                    hl2: high
19697                        .iter()
19698                        .zip(low.iter())
19699                        .map(|(h, l)| (h + l) * 0.5)
19700                        .collect(),
19701                    hlc3: high
19702                        .iter()
19703                        .zip(low.iter())
19704                        .zip(close.iter())
19705                        .map(|((h, l), c)| (h + l + c) / 3.0)
19706                        .collect(),
19707                    ohlc4: open
19708                        .iter()
19709                        .zip(high.iter())
19710                        .zip(low.iter())
19711                        .zip(close.iter())
19712                        .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19713                        .collect(),
19714                    hlcc4: high
19715                        .iter()
19716                        .zip(low.iter())
19717                        .zip(close.iter())
19718                        .map(|((h, l), c)| (h + l + c + c) * 0.25)
19719                        .collect(),
19720                },
19721                source: Some("close"),
19722            },
19723            combos: &combos,
19724            kernel: Kernel::Auto,
19725        };
19726        let out = compute_cpu_batch(req).unwrap();
19727        let input = SpearmanCorrelationInput::from_slices(
19728            &close,
19729            &open,
19730            SpearmanCorrelationParams {
19731                lookback: Some(30),
19732                smoothing_length: Some(3),
19733            },
19734        );
19735        let direct = spearman_correlation_with_kernel(&input, Kernel::Auto.to_non_batch())
19736            .unwrap()
19737            .smoothed;
19738        let got = out.values_f64.unwrap();
19739        assert_series_eq(&got, &direct, 1e-12);
19740    }
19741
19742    #[test]
19743    fn relative_strength_index_wave_indicator_output_matches_direct() {
19744        let open = sample_series();
19745        let close: Vec<f64> = open
19746            .iter()
19747            .enumerate()
19748            .map(|(i, v)| v + 0.2 * (i as f64 * 0.1).sin())
19749            .collect();
19750        let high: Vec<f64> = close.iter().map(|v| v + 0.9).collect();
19751        let low: Vec<f64> = close.iter().map(|v| v - 0.8).collect();
19752        let volume: Vec<f64> = (0..close.len()).map(|i| 1_000.0 + i as f64).collect();
19753        let candles = crate::utilities::data_loader::Candles {
19754            timestamp: vec![0; close.len()],
19755            open: open.clone(),
19756            high: high.clone(),
19757            low: low.clone(),
19758            close: close.clone(),
19759            volume,
19760            fields: crate::utilities::data_loader::CandleFieldFlags {
19761                open: true,
19762                high: true,
19763                low: true,
19764                close: true,
19765                volume: true,
19766            },
19767            hl2: high
19768                .iter()
19769                .zip(low.iter())
19770                .map(|(h, l)| (h + l) * 0.5)
19771                .collect(),
19772            hlc3: high
19773                .iter()
19774                .zip(low.iter())
19775                .zip(close.iter())
19776                .map(|((h, l), c)| (h + l + c) / 3.0)
19777                .collect(),
19778            ohlc4: open
19779                .iter()
19780                .zip(high.iter())
19781                .zip(low.iter())
19782                .zip(close.iter())
19783                .map(|(((o, h), l), c)| (o + h + l + c) * 0.25)
19784                .collect(),
19785            hlcc4: high
19786                .iter()
19787                .zip(low.iter())
19788                .zip(close.iter())
19789                .map(|((h, l), c)| (h + l + 2.0 * c) * 0.25)
19790                .collect(),
19791        };
19792        let combo = [
19793            ParamKV {
19794                key: "source",
19795                value: ParamValue::EnumString("hlcc4"),
19796            },
19797            ParamKV {
19798                key: "rsi_length",
19799                value: ParamValue::Int(14),
19800            },
19801            ParamKV {
19802                key: "length1",
19803                value: ParamValue::Int(2),
19804            },
19805            ParamKV {
19806                key: "length2",
19807                value: ParamValue::Int(5),
19808            },
19809            ParamKV {
19810                key: "length3",
19811                value: ParamValue::Int(9),
19812            },
19813            ParamKV {
19814                key: "length4",
19815                value: ParamValue::Int(13),
19816            },
19817        ];
19818        let combos = [IndicatorParamSet { params: &combo }];
19819        let req = IndicatorBatchRequest {
19820            indicator_id: "relative_strength_index_wave_indicator",
19821            output_id: Some("rsi_ma1"),
19822            data: IndicatorDataRef::Candles {
19823                candles: &candles,
19824                source: Some("hlcc4"),
19825            },
19826            combos: &combos,
19827            kernel: Kernel::Auto,
19828        };
19829        let out = compute_cpu_batch(req).unwrap();
19830        let input = RelativeStrengthIndexWaveIndicatorInput::from_slices(
19831            &candles.hlcc4,
19832            &high,
19833            &low,
19834            RelativeStrengthIndexWaveIndicatorParams {
19835                rsi_length: Some(14),
19836                length1: Some(2),
19837                length2: Some(5),
19838                length3: Some(9),
19839                length4: Some(13),
19840            },
19841        );
19842        let direct =
19843            relative_strength_index_wave_indicator_with_kernel(&input, Kernel::Auto.to_non_batch())
19844                .unwrap()
19845                .rsi_ma1;
19846        let got = out.values_f64.unwrap();
19847        assert_series_eq(&got, &direct, 1e-12);
19848    }
19849
19850    #[test]
19851    fn accumulation_swing_index_output_matches_direct() {
19852        let open = sample_series();
19853        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
19854        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
19855        let close: Vec<f64> = open
19856            .iter()
19857            .enumerate()
19858            .map(|(i, v)| v + 0.1 * (i as f64 + 1.0))
19859            .collect();
19860        let combo = [ParamKV {
19861            key: "daily_limit",
19862            value: ParamValue::Float(10_000.0),
19863        }];
19864        let combos = [IndicatorParamSet { params: &combo }];
19865        let req = IndicatorBatchRequest {
19866            indicator_id: "accumulation_swing_index",
19867            output_id: Some("value"),
19868            data: IndicatorDataRef::Ohlc {
19869                open: &open,
19870                high: &high,
19871                low: &low,
19872                close: &close,
19873            },
19874            combos: &combos,
19875            kernel: Kernel::Auto,
19876        };
19877        let out = compute_cpu_batch(req).unwrap();
19878        let input = AccumulationSwingIndexInput::from_slices(
19879            &open,
19880            &high,
19881            &low,
19882            &close,
19883            AccumulationSwingIndexParams {
19884                daily_limit: Some(10_000.0),
19885            },
19886        );
19887        let direct = accumulation_swing_index_with_kernel(&input, Kernel::Auto.to_non_batch())
19888            .unwrap()
19889            .values;
19890        let got = out.values_f64.unwrap();
19891        assert_series_eq(&got, &direct, 1e-12);
19892    }
19893
19894    #[test]
19895    fn ichimoku_oscillator_output_matches_direct() {
19896        let open: Vec<f64> = (0..160)
19897            .map(|i| 100.0 + (i as f64 * 0.07).sin() * 3.0 + i as f64 * 0.02)
19898            .collect();
19899        let high: Vec<f64> = open
19900            .iter()
19901            .enumerate()
19902            .map(|(i, v)| v + 1.2 + (i as f64 * 0.03).sin() * 0.25)
19903            .collect();
19904        let low: Vec<f64> = open
19905            .iter()
19906            .enumerate()
19907            .map(|(i, v)| v - 1.1 - (i as f64 * 0.05).cos() * 0.2)
19908            .collect();
19909        let close: Vec<f64> = open
19910            .iter()
19911            .enumerate()
19912            .map(|(i, v)| v + 0.12 * (i as f64 + 1.0))
19913            .collect();
19914        let combo = [
19915            ParamKV {
19916                key: "conversion_periods",
19917                value: ParamValue::Int(9),
19918            },
19919            ParamKV {
19920                key: "base_periods",
19921                value: ParamValue::Int(26),
19922            },
19923            ParamKV {
19924                key: "lagging_span_periods",
19925                value: ParamValue::Int(52),
19926            },
19927            ParamKV {
19928                key: "displacement",
19929                value: ParamValue::Int(26),
19930            },
19931            ParamKV {
19932                key: "ma_length",
19933                value: ParamValue::Int(12),
19934            },
19935            ParamKV {
19936                key: "smoothing_length",
19937                value: ParamValue::Int(3),
19938            },
19939            ParamKV {
19940                key: "extra_smoothing",
19941                value: ParamValue::Bool(true),
19942            },
19943            ParamKV {
19944                key: "normalize",
19945                value: ParamValue::EnumString("window"),
19946            },
19947            ParamKV {
19948                key: "window_size",
19949                value: ParamValue::Int(20),
19950            },
19951            ParamKV {
19952                key: "clamp",
19953                value: ParamValue::Bool(true),
19954            },
19955            ParamKV {
19956                key: "top_band",
19957                value: ParamValue::Float(2.0),
19958            },
19959            ParamKV {
19960                key: "mid_band",
19961                value: ParamValue::Float(1.5),
19962            },
19963        ];
19964        let combos = [IndicatorParamSet { params: &combo }];
19965        let req = IndicatorBatchRequest {
19966            indicator_id: "ichimoku_oscillator",
19967            output_id: Some("signal"),
19968            data: IndicatorDataRef::Ohlc {
19969                open: &open,
19970                high: &high,
19971                low: &low,
19972                close: &close,
19973            },
19974            combos: &combos,
19975            kernel: Kernel::Auto,
19976        };
19977        let out = compute_cpu_batch(req).unwrap();
19978        let input = IchimokuOscillatorInput::from_slices(
19979            &high,
19980            &low,
19981            &close,
19982            &close,
19983            IchimokuOscillatorParams {
19984                conversion_periods: Some(9),
19985                base_periods: Some(26),
19986                lagging_span_periods: Some(52),
19987                displacement: Some(26),
19988                ma_length: Some(12),
19989                smoothing_length: Some(3),
19990                extra_smoothing: Some(true),
19991                normalize: Some(IchimokuOscillatorNormalizeMode::Window),
19992                window_size: Some(20),
19993                clamp: Some(true),
19994                top_band: Some(2.0),
19995                mid_band: Some(1.5),
19996            },
19997        );
19998        let direct = ichimoku_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
19999            .unwrap()
20000            .signal;
20001        let got = out.values_f64.unwrap();
20002        assert_series_eq(&got, &direct, 1e-12);
20003    }
20004
20005    #[test]
20006    fn volatility_quality_index_output_matches_direct() {
20007        let open = sample_series();
20008        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
20009        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
20010        let close: Vec<f64> = open
20011            .iter()
20012            .enumerate()
20013            .map(|(i, v)| v + 0.2 * (i as f64 + 1.0))
20014            .collect();
20015        let combo = [
20016            ParamKV {
20017                key: "fast_length",
20018                value: ParamValue::Int(9),
20019            },
20020            ParamKV {
20021                key: "slow_length",
20022                value: ParamValue::Int(21),
20023            },
20024        ];
20025        let combos = [IndicatorParamSet { params: &combo }];
20026        let req = IndicatorBatchRequest {
20027            indicator_id: "volatility_quality_index",
20028            output_id: Some("fast_sma"),
20029            data: IndicatorDataRef::Ohlc {
20030                open: &open,
20031                high: &high,
20032                low: &low,
20033                close: &close,
20034            },
20035            combos: &combos,
20036            kernel: Kernel::Auto,
20037        };
20038        let out = compute_cpu_batch(req).unwrap();
20039        let input = VolatilityQualityIndexInput::from_slices(
20040            &open,
20041            &high,
20042            &low,
20043            &close,
20044            VolatilityQualityIndexParams {
20045                fast_length: Some(9),
20046                slow_length: Some(21),
20047            },
20048        );
20049        let direct = volatility_quality_index_with_kernel(&input, Kernel::Auto.to_non_batch())
20050            .unwrap()
20051            .fast_sma;
20052        let got = out.values_f64.unwrap();
20053        assert_series_eq(&got, &direct, 1e-12);
20054    }
20055
20056    #[test]
20057    fn vwap_deviation_oscillator_output_matches_direct() {
20058        let open = sample_series();
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
20062            .iter()
20063            .enumerate()
20064            .map(|(i, v)| v + 0.15 * (i as f64 + 1.0))
20065            .collect();
20066        let volume: Vec<f64> = (0..close.len())
20067            .map(|i| 1000.0 + (i as f64 * 11.0))
20068            .collect();
20069        let timestamps: Vec<i64> = (0..close.len())
20070            .map(|i| 1_700_000_000_000i64 + (i as i64) * 14_400_000)
20071            .collect();
20072        let candles = Candles::new(
20073            timestamps.clone(),
20074            open.clone(),
20075            high.clone(),
20076            low.clone(),
20077            close.clone(),
20078            volume.clone(),
20079        );
20080        let combo = [
20081            ParamKV {
20082                key: "session_mode",
20083                value: ParamValue::EnumString("rolling_bars"),
20084            },
20085            ParamKV {
20086                key: "rolling_period",
20087                value: ParamValue::Int(20),
20088            },
20089            ParamKV {
20090                key: "rolling_days",
20091                value: ParamValue::Int(30),
20092            },
20093            ParamKV {
20094                key: "use_close",
20095                value: ParamValue::Bool(false),
20096            },
20097            ParamKV {
20098                key: "deviation_mode",
20099                value: ParamValue::EnumString("zscore"),
20100            },
20101            ParamKV {
20102                key: "z_window",
20103                value: ParamValue::Int(25),
20104            },
20105        ];
20106        let combos = [IndicatorParamSet { params: &combo }];
20107        let req = IndicatorBatchRequest {
20108            indicator_id: "vwap_deviation_oscillator",
20109            output_id: Some("osc"),
20110            data: IndicatorDataRef::Candles {
20111                candles: &candles,
20112                source: None,
20113            },
20114            combos: &combos,
20115            kernel: Kernel::Auto,
20116        };
20117        let out = compute_cpu_batch(req).unwrap();
20118        let input = VwapDeviationOscillatorInput::from_slices(
20119            &timestamps,
20120            &high,
20121            &low,
20122            &close,
20123            &volume,
20124            VwapDeviationOscillatorParams {
20125                session_mode: Some(VwapDeviationSessionMode::RollingBars),
20126                rolling_period: Some(20),
20127                rolling_days: Some(30),
20128                use_close: Some(false),
20129                deviation_mode: Some(VwapDeviationMode::ZScore),
20130                z_window: Some(25),
20131                pct_vol_lookback: Some(100),
20132                pct_min_sigma: Some(0.1),
20133                abs_vol_lookback: Some(100),
20134            },
20135        );
20136        let direct = vwap_deviation_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
20137            .unwrap()
20138            .osc;
20139        let got = out.values_f64.unwrap();
20140        assert_series_eq(&got, &direct, 1e-12);
20141    }
20142
20143    #[test]
20144    fn volume_zone_oscillator_output_matches_direct() {
20145        let close = sample_series();
20146        let volume: Vec<f64> = close
20147            .iter()
20148            .enumerate()
20149            .map(|(i, _)| 1000.0 + (i as f64 * 17.0))
20150            .collect();
20151        let combo = [
20152            ParamKV {
20153                key: "length",
20154                value: ParamValue::Int(14),
20155            },
20156            ParamKV {
20157                key: "intraday_smoothing",
20158                value: ParamValue::Bool(true),
20159            },
20160            ParamKV {
20161                key: "noise_filter",
20162                value: ParamValue::Int(4),
20163            },
20164        ];
20165        let combos = [IndicatorParamSet { params: &combo }];
20166        let req = IndicatorBatchRequest {
20167            indicator_id: "volume_zone_oscillator",
20168            output_id: Some("value"),
20169            data: IndicatorDataRef::CloseVolume {
20170                close: &close,
20171                volume: &volume,
20172            },
20173            combos: &combos,
20174            kernel: Kernel::Auto,
20175        };
20176        let out = compute_cpu_batch(req).unwrap();
20177        let input = VolumeZoneOscillatorInput::from_slices(
20178            &close,
20179            &volume,
20180            VolumeZoneOscillatorParams {
20181                length: Some(14),
20182                intraday_smoothing: Some(true),
20183                noise_filter: Some(4),
20184            },
20185        );
20186        let direct = volume_zone_oscillator_with_kernel(&input, Kernel::Auto.to_non_batch())
20187            .unwrap()
20188            .values;
20189        let got = out.values_f64.unwrap();
20190        assert_series_eq(&got, &direct, 1e-12);
20191    }
20192
20193    #[test]
20194    fn ttm_trend_bool_output_matches_direct() {
20195        let (open, high, low, close) = sample_ohlc();
20196        let combo = [ParamKV {
20197            key: "period",
20198            value: ParamValue::Int(5),
20199        }];
20200        let combos = [IndicatorParamSet { params: &combo }];
20201        let req = IndicatorBatchRequest {
20202            indicator_id: "ttm_trend",
20203            output_id: Some("value"),
20204            data: IndicatorDataRef::Ohlc {
20205                open: &open,
20206                high: &high,
20207                low: &low,
20208                close: &close,
20209            },
20210            combos: &combos,
20211            kernel: Kernel::Auto,
20212        };
20213        let out = compute_cpu_batch(req).unwrap();
20214        let source: Vec<f64> = high.iter().zip(&low).map(|(h, l)| 0.5 * (h + l)).collect();
20215        let input = TtmTrendInput::from_slices(&source, &close, TtmTrendParams { period: Some(5) });
20216        let direct = ttm_trend_with_kernel(&input, Kernel::Auto.to_non_batch())
20217            .unwrap()
20218            .values;
20219        assert_eq!(out.values_bool.unwrap(), direct);
20220    }
20221
20222    fn build_default_params_for_indicator(
20223        info: &crate::indicators::registry::IndicatorInfo,
20224    ) -> Option<Vec<ParamKV<'static>>> {
20225        let mut params: Vec<ParamKV<'static>> = Vec::new();
20226        for p in &info.params {
20227            if p.key.eq_ignore_ascii_case("output") {
20228                continue;
20229            }
20230            let value = if let Some(default) = p.default {
20231                match default {
20232                    crate::indicators::registry::ParamValueStatic::Int(v) => {
20233                        Some(ParamValue::Int(v))
20234                    }
20235                    crate::indicators::registry::ParamValueStatic::Float(v) => {
20236                        Some(ParamValue::Float(v))
20237                    }
20238                    crate::indicators::registry::ParamValueStatic::Bool(v) => {
20239                        Some(ParamValue::Bool(v))
20240                    }
20241                    crate::indicators::registry::ParamValueStatic::EnumString(v) => {
20242                        Some(ParamValue::EnumString(v))
20243                    }
20244                }
20245            } else {
20246                match p.kind {
20247                    IndicatorParamKind::Int => {
20248                        let mut v = p.min.unwrap_or(14.0).round() as i64;
20249                        if v < 0 {
20250                            v = 0;
20251                        }
20252                        if let Some(max) = p.max {
20253                            v = v.min(max.round() as i64);
20254                        }
20255                        Some(ParamValue::Int(v))
20256                    }
20257                    IndicatorParamKind::Float => {
20258                        let mut v = p.min.unwrap_or(1.0);
20259                        if !v.is_finite() {
20260                            v = 1.0;
20261                        }
20262                        if let Some(max) = p.max {
20263                            v = v.min(max);
20264                        }
20265                        Some(ParamValue::Float(v))
20266                    }
20267                    IndicatorParamKind::Bool => Some(ParamValue::Bool(false)),
20268                    IndicatorParamKind::EnumString => {
20269                        p.enum_values.first().copied().map(ParamValue::EnumString)
20270                    }
20271                }
20272            };
20273
20274            match value {
20275                Some(v) => params.push(ParamKV {
20276                    key: p.key,
20277                    value: v,
20278                }),
20279                None => {
20280                    if p.required {
20281                        return None;
20282                    }
20283                }
20284            }
20285        }
20286        Some(params)
20287    }
20288
20289    fn median_ns(mut samples: Vec<u128>) -> u128 {
20290        samples.sort_unstable();
20291        samples[samples.len() / 2]
20292    }
20293
20294    #[test]
20295    #[ignore]
20296    fn full_cpu_dispatch_perf_sweep_vs_direct_route() {
20297        const LEN: usize = 10_000;
20298        const REPS: usize = 5;
20299
20300        let open: Vec<f64> = (0..LEN).map(|i| 100.0 + (i as f64 * 0.01)).collect();
20301        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
20302        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
20303        let close: Vec<f64> = open.iter().map(|v| v + 0.25).collect();
20304        let volume: Vec<f64> = (0..LEN).map(|i| 1000.0 + (i as f64 * 0.5)).collect();
20305        let timestamp: Vec<i64> = (0..LEN).map(|i| i as i64).collect();
20306        let candles = crate::utilities::data_loader::Candles::new(
20307            timestamp,
20308            open.clone(),
20309            high.clone(),
20310            low.clone(),
20311            close.clone(),
20312            volume.clone(),
20313        );
20314
20315        let infos: Vec<_> = list_indicators()
20316            .iter()
20317            .filter(|i| i.capabilities.supports_cpu_batch)
20318            .collect();
20319        let mut rows: Vec<(String, f64, f64, f64)> = Vec::new();
20320        let mut failures: Vec<String> = Vec::new();
20321
20322        for info in infos {
20323            let Some(output) = info.outputs.first() else {
20324                failures.push(format!("{}: no outputs", info.id));
20325                continue;
20326            };
20327            let output_id = output.id;
20328            let Some(params_vec) = build_default_params_for_indicator(info) else {
20329                failures.push(format!("{}: missing required param defaults", info.id));
20330                continue;
20331            };
20332            let combos = [IndicatorParamSet {
20333                params: params_vec.as_slice(),
20334            }];
20335            let data = match info.input_kind {
20336                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
20337                    values: close.as_slice(),
20338                },
20339                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
20340                    candles: &candles,
20341                    source: None,
20342                },
20343                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
20344                    open: open.as_slice(),
20345                    high: high.as_slice(),
20346                    low: low.as_slice(),
20347                    close: close.as_slice(),
20348                },
20349                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
20350                    open: open.as_slice(),
20351                    high: high.as_slice(),
20352                    low: low.as_slice(),
20353                    close: close.as_slice(),
20354                    volume: volume.as_slice(),
20355                },
20356                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
20357                    high: high.as_slice(),
20358                    low: low.as_slice(),
20359                },
20360                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
20361                    close: close.as_slice(),
20362                    volume: volume.as_slice(),
20363                },
20364            };
20365
20366            let req = IndicatorBatchRequest {
20367                indicator_id: info.id,
20368                output_id: Some(output_id),
20369                data,
20370                combos: &combos,
20371                kernel: Kernel::Auto,
20372            };
20373
20374            let dispatch_once = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20375                compute_cpu_batch(req)
20376            })) {
20377                Ok(Ok(v)) => v,
20378                Ok(Err(e)) => {
20379                    failures.push(format!("{}: dispatch error: {}", info.id, e));
20380                    continue;
20381                }
20382                Err(_) => {
20383                    failures.push(format!("{}: dispatch panic", info.id));
20384                    continue;
20385                }
20386            };
20387            let direct_once = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20388                dispatch_cpu_batch_by_indicator(req, info.id, output_id)
20389            })) {
20390                Ok(Ok(v)) => v,
20391                Ok(Err(e)) => {
20392                    failures.push(format!("{}: direct-route error: {}", info.id, e));
20393                    continue;
20394                }
20395                Err(_) => {
20396                    failures.push(format!("{}: direct-route panic", info.id));
20397                    continue;
20398                }
20399            };
20400
20401            if dispatch_once.rows != direct_once.rows || dispatch_once.cols != direct_once.cols {
20402                failures.push(format!(
20403                    "{}: shape mismatch dispatch=({},{}) direct=({},{})",
20404                    info.id,
20405                    dispatch_once.rows,
20406                    dispatch_once.cols,
20407                    direct_once.rows,
20408                    direct_once.cols
20409                ));
20410                continue;
20411            }
20412
20413            let mut dispatch_samples = Vec::with_capacity(REPS);
20414            let mut direct_samples = Vec::with_capacity(REPS);
20415            let mut panicked = false;
20416            for _ in 0..REPS {
20417                let t0 = Instant::now();
20418                let dispatch_iter = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20419                    compute_cpu_batch(req)
20420                }));
20421                if !matches!(dispatch_iter, Ok(Ok(_))) {
20422                    failures.push(format!("{}: dispatch panic/error during sample", info.id));
20423                    panicked = true;
20424                    break;
20425                }
20426                dispatch_samples.push(t0.elapsed().as_nanos());
20427
20428                let t1 = Instant::now();
20429                let direct_iter = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
20430                    dispatch_cpu_batch_by_indicator(req, info.id, output_id)
20431                }));
20432                if !matches!(direct_iter, Ok(Ok(_))) {
20433                    failures.push(format!(
20434                        "{}: direct-route panic/error during sample",
20435                        info.id
20436                    ));
20437                    panicked = true;
20438                    break;
20439                }
20440                direct_samples.push(t1.elapsed().as_nanos());
20441            }
20442            if panicked {
20443                continue;
20444            }
20445
20446            let dispatch_median = median_ns(dispatch_samples) as f64 / 1_000_000.0;
20447            let direct_median = median_ns(direct_samples) as f64 / 1_000_000.0;
20448            let delta_pct = if direct_median > 0.0 {
20449                ((dispatch_median - direct_median) / direct_median) * 100.0
20450            } else {
20451                0.0
20452            };
20453            rows.push((
20454                info.id.to_string(),
20455                direct_median,
20456                dispatch_median,
20457                delta_pct,
20458            ));
20459        }
20460
20461        rows.sort_by(|a, b| b.3.partial_cmp(&a.3).unwrap_or(std::cmp::Ordering::Equal));
20462
20463        println!("id,direct_ms,dispatch_ms,delta_pct");
20464        for (id, direct_ms, dispatch_ms, delta_pct) in &rows {
20465            println!("{id},{direct_ms:.6},{dispatch_ms:.6},{delta_pct:.2}");
20466        }
20467        println!("total_indicators={}", rows.len());
20468
20469        assert!(
20470            failures.is_empty(),
20471            "perf sweep failures: {}",
20472            failures.join(" | ")
20473        );
20474        assert!(!rows.is_empty(), "no indicators were swept");
20475    }
20476
20477    #[test]
20478    fn multi_output_requires_output_id() {
20479        let data = sample_series();
20480        let combos: [IndicatorParamSet<'_>; 0] = [];
20481        let req = IndicatorBatchRequest {
20482            indicator_id: "macd",
20483            output_id: None,
20484            data: IndicatorDataRef::Slice { values: &data },
20485            combos: &combos,
20486            kernel: Kernel::Auto,
20487        };
20488        let err = compute_cpu_batch(req).unwrap_err();
20489        assert!(matches!(err, IndicatorDispatchError::InvalidParam { .. }));
20490    }
20491
20492    #[test]
20493    fn multi_output_unknown_output_is_rejected_globally() {
20494        let (open, high, low, close) = sample_ohlc();
20495        let volume: Vec<f64> = (0..close.len())
20496            .map(|i| 1000.0 + (i as f64 * 0.5))
20497            .collect();
20498        let timestamp: Vec<i64> = (0..close.len()).map(|i| i as i64).collect();
20499        let candles = crate::utilities::data_loader::Candles::new(
20500            timestamp,
20501            open.clone(),
20502            high.clone(),
20503            low.clone(),
20504            close.clone(),
20505            volume.clone(),
20506        );
20507
20508        for info in list_indicators()
20509            .iter()
20510            .filter(|i| i.capabilities.supports_cpu_batch && i.outputs.len() > 1)
20511        {
20512            let Some(params_vec) = build_default_params_for_indicator(info) else {
20513                continue;
20514            };
20515            let combos = [IndicatorParamSet {
20516                params: params_vec.as_slice(),
20517            }];
20518            let data = match info.input_kind {
20519                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
20520                    values: close.as_slice(),
20521                },
20522                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
20523                    candles: &candles,
20524                    source: None,
20525                },
20526                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
20527                    open: open.as_slice(),
20528                    high: high.as_slice(),
20529                    low: low.as_slice(),
20530                    close: close.as_slice(),
20531                },
20532                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
20533                    open: open.as_slice(),
20534                    high: high.as_slice(),
20535                    low: low.as_slice(),
20536                    close: close.as_slice(),
20537                    volume: volume.as_slice(),
20538                },
20539                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
20540                    high: high.as_slice(),
20541                    low: low.as_slice(),
20542                },
20543                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
20544                    close: close.as_slice(),
20545                    volume: volume.as_slice(),
20546                },
20547            };
20548            let req = IndicatorBatchRequest {
20549                indicator_id: info.id,
20550                output_id: Some("__unknown_output__"),
20551                data,
20552                combos: &combos,
20553                kernel: Kernel::Auto,
20554            };
20555            let err = compute_cpu_batch(req).unwrap_err();
20556            assert!(
20557                matches!(err, IndicatorDispatchError::UnknownOutput { .. }),
20558                "indicator {} returned unexpected error for unknown output: {:?}",
20559                info.id,
20560                err
20561            );
20562        }
20563    }
20564
20565    #[test]
20566    fn strict_mode_rejects_mismatched_input_kind_globally() {
20567        let data = sample_series();
20568        let candles = sample_candles();
20569
20570        for info in list_indicators()
20571            .iter()
20572            .filter(|i| i.capabilities.supports_cpu_batch)
20573        {
20574            let Some(output) = info.outputs.first() else {
20575                continue;
20576            };
20577            let Some(params_vec) = build_default_params_for_indicator(info) else {
20578                continue;
20579            };
20580            let combos = [IndicatorParamSet {
20581                params: params_vec.as_slice(),
20582            }];
20583            let expected = strict_expected_input_kind(info.id, info.input_kind);
20584            let mismatched = match expected {
20585                IndicatorInputKind::Slice => IndicatorDataRef::Candles {
20586                    candles: &candles,
20587                    source: None,
20588                },
20589                IndicatorInputKind::Candles => IndicatorDataRef::Slice { values: &data },
20590                IndicatorInputKind::Ohlc
20591                | IndicatorInputKind::Ohlcv
20592                | IndicatorInputKind::HighLow
20593                | IndicatorInputKind::CloseVolume => IndicatorDataRef::Slice { values: &data },
20594            };
20595            let req = IndicatorBatchRequest {
20596                indicator_id: info.id,
20597                output_id: Some(output.id),
20598                data: mismatched,
20599                combos: &combos,
20600                kernel: Kernel::Auto,
20601            };
20602            let err = compute_cpu_batch_strict(req).unwrap_err();
20603            assert!(
20604                matches!(err, IndicatorDispatchError::MissingRequiredInput { .. }),
20605                "indicator {} did not reject strict mismatched input: {:?}",
20606                info.id,
20607                err
20608            );
20609        }
20610    }
20611
20612    #[test]
20613    fn full_cpu_dispatch_parity_vs_direct_route_for_all_outputs() {
20614        const LEN: usize = 4096;
20615        let open: Vec<f64> = (0..LEN).map(|i| 100.0 + (i as f64 * 0.01)).collect();
20616        let high: Vec<f64> = open.iter().map(|v| v + 1.0).collect();
20617        let low: Vec<f64> = open.iter().map(|v| v - 1.0).collect();
20618        let close: Vec<f64> = open.iter().map(|v| v + 0.25).collect();
20619        let volume: Vec<f64> = (0..LEN).map(|i| 1000.0 + (i as f64 * 0.5)).collect();
20620        let timestamp: Vec<i64> = (0..LEN).map(|i| i as i64).collect();
20621        let candles = crate::utilities::data_loader::Candles::new(
20622            timestamp,
20623            open.clone(),
20624            high.clone(),
20625            low.clone(),
20626            close.clone(),
20627            volume.clone(),
20628        );
20629
20630        for info in list_indicators()
20631            .iter()
20632            .filter(|i| i.capabilities.supports_cpu_batch)
20633        {
20634            let Some(params_vec) = build_default_params_for_indicator(info) else {
20635                continue;
20636            };
20637            let combos = [IndicatorParamSet {
20638                params: params_vec.as_slice(),
20639            }];
20640            let data = match info.input_kind {
20641                IndicatorInputKind::Slice => IndicatorDataRef::Slice {
20642                    values: close.as_slice(),
20643                },
20644                IndicatorInputKind::Candles => IndicatorDataRef::Candles {
20645                    candles: &candles,
20646                    source: None,
20647                },
20648                IndicatorInputKind::Ohlc => IndicatorDataRef::Ohlc {
20649                    open: open.as_slice(),
20650                    high: high.as_slice(),
20651                    low: low.as_slice(),
20652                    close: close.as_slice(),
20653                },
20654                IndicatorInputKind::Ohlcv => IndicatorDataRef::Ohlcv {
20655                    open: open.as_slice(),
20656                    high: high.as_slice(),
20657                    low: low.as_slice(),
20658                    close: close.as_slice(),
20659                    volume: volume.as_slice(),
20660                },
20661                IndicatorInputKind::HighLow => IndicatorDataRef::HighLow {
20662                    high: high.as_slice(),
20663                    low: low.as_slice(),
20664                },
20665                IndicatorInputKind::CloseVolume => IndicatorDataRef::CloseVolume {
20666                    close: close.as_slice(),
20667                    volume: volume.as_slice(),
20668                },
20669            };
20670
20671            for output in info.outputs.iter() {
20672                let req = IndicatorBatchRequest {
20673                    indicator_id: info.id,
20674                    output_id: Some(output.id),
20675                    data,
20676                    combos: &combos,
20677                    kernel: Kernel::Auto,
20678                };
20679                let generic = compute_cpu_batch(req).unwrap_or_else(|e| {
20680                    panic!(
20681                        "generic dispatch failed for {}:{}: {}",
20682                        info.id, output.id, e
20683                    )
20684                });
20685                let direct = dispatch_cpu_batch_by_indicator(req, info.id, output.id)
20686                    .unwrap_or_else(|e| {
20687                        panic!("direct route failed for {}:{}: {}", info.id, output.id, e)
20688                    });
20689
20690                assert_eq!(
20691                    generic.rows, direct.rows,
20692                    "rows mismatch for {}:{}",
20693                    info.id, output.id
20694                );
20695                assert_eq!(
20696                    generic.cols, direct.cols,
20697                    "cols mismatch for {}:{}",
20698                    info.id, output.id
20699                );
20700                assert_eq!(
20701                    generic.output_id, direct.output_id,
20702                    "output id mismatch for {}:{}",
20703                    info.id, output.id
20704                );
20705
20706                match (
20707                    generic.values_f64.as_ref(),
20708                    direct.values_f64.as_ref(),
20709                    generic.values_i32.as_ref(),
20710                    direct.values_i32.as_ref(),
20711                    generic.values_bool.as_ref(),
20712                    direct.values_bool.as_ref(),
20713                ) {
20714                    (Some(g), Some(d), None, None, None, None) => assert_series_eq(g, d, 1e-9),
20715                    (None, None, Some(g), Some(d), None, None) => assert_eq!(g, d),
20716                    (None, None, None, None, Some(g), Some(d)) => assert_eq!(g, d),
20717                    _ => panic!("value type mismatch for {}:{}", info.id, output.id),
20718                }
20719            }
20720        }
20721    }
20722
20723    #[test]
20724    fn compute_cpu_batch_bull_power_vs_bear_power_matches_direct() {
20725        let open: Vec<f64> = (0..256)
20726            .map(|i| 100.0 + (i as f64 * 0.03).sin() + i as f64 * 0.02)
20727            .collect();
20728        let close: Vec<f64> = open
20729            .iter()
20730            .enumerate()
20731            .map(|(i, &o)| o + (i as f64 * 0.025).cos() * 0.8)
20732            .collect();
20733        let high: Vec<f64> = open
20734            .iter()
20735            .zip(close.iter())
20736            .enumerate()
20737            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.013).sin().abs() * 0.2)
20738            .collect();
20739        let low: Vec<f64> = open
20740            .iter()
20741            .zip(close.iter())
20742            .enumerate()
20743            .map(|(i, (&o, &c))| o.min(c) - 0.4 - (i as f64 * 0.017).cos().abs() * 0.15)
20744            .collect();
20745        let params = [ParamKV {
20746            key: "period",
20747            value: ParamValue::Int(5),
20748        }];
20749        let combos = [IndicatorParamSet { params: &params }];
20750        let candles = crate::utilities::data_loader::Candles::new(
20751            vec![0; close.len()],
20752            open.clone(),
20753            high.clone(),
20754            low.clone(),
20755            close.clone(),
20756            vec![0.0; close.len()],
20757        );
20758
20759        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20760            indicator_id: "bull_power_vs_bear_power",
20761            output_id: Some("value"),
20762            data: IndicatorDataRef::Candles {
20763                candles: &candles,
20764                source: None,
20765            },
20766            combos: &combos,
20767            kernel: Kernel::Auto,
20768        })
20769        .unwrap();
20770
20771        let direct = bull_power_vs_bear_power_with_kernel(
20772            &BullPowerVsBearPowerInput::from_slices(
20773                &open,
20774                &high,
20775                &low,
20776                &close,
20777                BullPowerVsBearPowerParams { period: Some(5) },
20778            ),
20779            Kernel::Auto,
20780        )
20781        .unwrap();
20782
20783        let values = dispatched.values_f64.as_ref().unwrap();
20784        assert_eq!(values.len(), close.len());
20785        assert_series_eq(values, &direct.values, 1e-9);
20786    }
20787
20788    #[test]
20789    fn compute_cpu_batch_advance_decline_line_matches_direct() {
20790        let close: Vec<f64> = (0..256)
20791            .map(|i| ((i as f64) * 0.05).sin() * 100.0 + ((i as f64) * 0.02).cos() * 25.0)
20792            .collect();
20793        let combos = [IndicatorParamSet { params: &[] }];
20794
20795        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20796            indicator_id: "advance_decline_line",
20797            output_id: Some("value"),
20798            data: IndicatorDataRef::Slice { values: &close },
20799            combos: &combos,
20800            kernel: Kernel::Auto,
20801        })
20802        .unwrap();
20803
20804        let direct = advance_decline_line_with_kernel(
20805            &AdvanceDeclineLineInput::from_slice(&close, AdvanceDeclineLineParams),
20806            Kernel::Auto,
20807        )
20808        .unwrap();
20809
20810        let values = dispatched.values_f64.as_ref().unwrap();
20811        assert_eq!(values.len(), close.len());
20812        assert_series_eq(values, &direct.values, 1e-9);
20813    }
20814
20815    #[test]
20816    fn compute_cpu_batch_decisionpoint_breadth_swenlin_trading_oscillator_matches_direct() {
20817        let advancing: Vec<f64> = (0..256)
20818            .map(|i| 1500.0 + i as f64 * 0.8 + (i as f64 * 0.07).sin() * 120.0 + 40.0)
20819            .collect();
20820        let declining: Vec<f64> = (0..256)
20821            .map(|i| 1300.0 + i as f64 * 0.5 + (i as f64 * 0.05).cos() * 95.0 + 30.0)
20822            .collect();
20823        let combos = [IndicatorParamSet { params: &[] }];
20824
20825        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20826            indicator_id: "decisionpoint_breadth_swenlin_trading_oscillator",
20827            output_id: Some("value"),
20828            data: IndicatorDataRef::HighLow {
20829                high: &advancing,
20830                low: &declining,
20831            },
20832            combos: &combos,
20833            kernel: Kernel::Auto,
20834        })
20835        .unwrap();
20836
20837        let direct = decisionpoint_breadth_swenlin_trading_oscillator_with_kernel(
20838            &DecisionPointBreadthSwenlinTradingOscillatorInput::from_slices(
20839                &advancing,
20840                &declining,
20841                DecisionPointBreadthSwenlinTradingOscillatorParams,
20842            ),
20843            Kernel::Auto,
20844        )
20845        .unwrap();
20846
20847        let values = dispatched.values_f64.as_ref().unwrap();
20848        assert_eq!(values.len(), advancing.len());
20849        assert_series_eq(values, &direct.values, 1e-9);
20850    }
20851
20852    #[test]
20853    fn compute_cpu_batch_velocity_acceleration_indicator_matches_direct() {
20854        let open: Vec<f64> = (0..256)
20855            .map(|i| 100.0 + i as f64 * 0.04 + (i as f64 * 0.09).sin())
20856            .collect();
20857        let close: Vec<f64> = open
20858            .iter()
20859            .enumerate()
20860            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.9)
20861            .collect();
20862        let high: Vec<f64> = open
20863            .iter()
20864            .zip(close.iter())
20865            .enumerate()
20866            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.03).sin().abs() * 0.2)
20867            .collect();
20868        let low: Vec<f64> = open
20869            .iter()
20870            .zip(close.iter())
20871            .enumerate()
20872            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.05).cos().abs() * 0.2)
20873            .collect();
20874        let candles = crate::utilities::data_loader::Candles::new(
20875            (0..256_i64).collect(),
20876            open,
20877            high,
20878            low,
20879            close,
20880            vec![1_000.0; 256],
20881        );
20882        let params = [
20883            ParamKV {
20884                key: "length",
20885                value: ParamValue::Int(21),
20886            },
20887            ParamKV {
20888                key: "smooth_length",
20889                value: ParamValue::Int(5),
20890            },
20891            ParamKV {
20892                key: "source",
20893                value: ParamValue::EnumString("hlcc4"),
20894            },
20895        ];
20896        let combos = [IndicatorParamSet { params: &params }];
20897
20898        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20899            indicator_id: "velocity_acceleration_indicator",
20900            output_id: Some("value"),
20901            data: IndicatorDataRef::Candles {
20902                candles: &candles,
20903                source: Some("hlcc4"),
20904            },
20905            combos: &combos,
20906            kernel: Kernel::Auto,
20907        })
20908        .unwrap();
20909
20910        let direct = velocity_acceleration_indicator_with_kernel(
20911            &VelocityAccelerationIndicatorInput::from_candles(
20912                &candles,
20913                "hlcc4",
20914                VelocityAccelerationIndicatorParams {
20915                    length: Some(21),
20916                    smooth_length: Some(5),
20917                },
20918            ),
20919            Kernel::Auto,
20920        )
20921        .unwrap();
20922
20923        let values = dispatched.values_f64.as_ref().unwrap();
20924        assert_eq!(values.len(), candles.close.len());
20925        assert_series_eq(values, &direct.values, 1e-9);
20926    }
20927
20928    #[test]
20929    fn compute_cpu_batch_normalized_resonator_matches_direct() {
20930        let open: Vec<f64> = (0..256)
20931            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.07).sin())
20932            .collect();
20933        let close: Vec<f64> = open
20934            .iter()
20935            .enumerate()
20936            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.8)
20937            .collect();
20938        let high: Vec<f64> = open
20939            .iter()
20940            .zip(close.iter())
20941            .enumerate()
20942            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.05).sin().abs() * 0.2)
20943            .collect();
20944        let low: Vec<f64> = open
20945            .iter()
20946            .zip(close.iter())
20947            .enumerate()
20948            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.03).cos().abs() * 0.2)
20949            .collect();
20950        let candles = crate::utilities::data_loader::Candles::new(
20951            (0..256_i64).collect(),
20952            open,
20953            high,
20954            low,
20955            close,
20956            vec![1_000.0; 256],
20957        );
20958        let params = [
20959            ParamKV {
20960                key: "period",
20961                value: ParamValue::Int(48),
20962            },
20963            ParamKV {
20964                key: "delta",
20965                value: ParamValue::Float(0.4),
20966            },
20967            ParamKV {
20968                key: "lookback_mult",
20969                value: ParamValue::Float(1.2),
20970            },
20971            ParamKV {
20972                key: "signal_length",
20973                value: ParamValue::Int(7),
20974            },
20975            ParamKV {
20976                key: "source",
20977                value: ParamValue::EnumString("hl2"),
20978            },
20979        ];
20980        let combos = [IndicatorParamSet { params: &params }];
20981
20982        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
20983            indicator_id: "normalized_resonator",
20984            output_id: Some("oscillator"),
20985            data: IndicatorDataRef::Candles {
20986                candles: &candles,
20987                source: Some("hl2"),
20988            },
20989            combos: &combos,
20990            kernel: Kernel::Auto,
20991        })
20992        .unwrap();
20993
20994        let direct = normalized_resonator_with_kernel(
20995            &NormalizedResonatorInput::from_candles(
20996                &candles,
20997                "hl2",
20998                NormalizedResonatorParams {
20999                    period: Some(48),
21000                    delta: Some(0.4),
21001                    lookback_mult: Some(1.2),
21002                    signal_length: Some(7),
21003                },
21004            ),
21005            Kernel::Auto,
21006        )
21007        .unwrap();
21008
21009        let values = dispatched.values_f64.as_ref().unwrap();
21010        assert_eq!(values.len(), candles.close.len());
21011        assert_series_eq(values, &direct.oscillator, 1e-9);
21012    }
21013
21014    #[test]
21015    fn compute_cpu_batch_monotonicity_index_matches_direct() {
21016        let open: Vec<f64> = (0..256)
21017            .map(|i| 100.0 + i as f64 * 0.04 + (i as f64 * 0.08).sin())
21018            .collect();
21019        let close: Vec<f64> = open
21020            .iter()
21021            .enumerate()
21022            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.9)
21023            .collect();
21024        let high: Vec<f64> = open
21025            .iter()
21026            .zip(close.iter())
21027            .enumerate()
21028            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.05).sin().abs() * 0.2)
21029            .collect();
21030        let low: Vec<f64> = open
21031            .iter()
21032            .zip(close.iter())
21033            .enumerate()
21034            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.03).cos().abs() * 0.2)
21035            .collect();
21036        let candles = crate::utilities::data_loader::Candles::new(
21037            (0..256_i64).collect(),
21038            open,
21039            high,
21040            low,
21041            close,
21042            vec![1_000.0; 256],
21043        );
21044        let params = [
21045            ParamKV {
21046                key: "length",
21047                value: ParamValue::Int(20),
21048            },
21049            ParamKV {
21050                key: "mode",
21051                value: ParamValue::EnumString("efficiency"),
21052            },
21053            ParamKV {
21054                key: "index_smooth",
21055                value: ParamValue::Int(5),
21056            },
21057            ParamKV {
21058                key: "source",
21059                value: ParamValue::EnumString("close"),
21060            },
21061        ];
21062        let combos = [IndicatorParamSet { params: &params }];
21063
21064        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21065            indicator_id: "monotonicity_index",
21066            output_id: Some("index"),
21067            data: IndicatorDataRef::Candles {
21068                candles: &candles,
21069                source: Some("close"),
21070            },
21071            combos: &combos,
21072            kernel: Kernel::Auto,
21073        })
21074        .unwrap();
21075
21076        let direct = monotonicity_index_with_kernel(
21077            &MonotonicityIndexInput::from_candles(
21078                &candles,
21079                "close",
21080                MonotonicityIndexParams {
21081                    length: Some(20),
21082                    mode: Some(MonotonicityIndexMode::Efficiency),
21083                    index_smooth: Some(5),
21084                },
21085            ),
21086            Kernel::Auto,
21087        )
21088        .unwrap();
21089
21090        let values = dispatched.values_f64.as_ref().unwrap();
21091        assert_eq!(values.len(), candles.close.len());
21092        assert_series_eq(values, &direct.index, 1e-9);
21093    }
21094
21095    #[test]
21096    fn compute_cpu_batch_half_causal_estimator_matches_direct() {
21097        let len = 240usize;
21098        let slots_per_day = 60usize;
21099        let close: Vec<f64> = (0..len)
21100            .map(|i| {
21101                let slot = (i % slots_per_day) as f64;
21102                let day = (i / slots_per_day) as f64;
21103                1000.0
21104                    + day * 4.0
21105                    + (slot * 0.13).sin() * 25.0
21106                    + (slot * 0.04).cos() * 9.0
21107                    + slot * 0.2
21108            })
21109            .collect();
21110        let params = [
21111            ParamKV {
21112                key: "slots_per_day",
21113                value: ParamValue::Int(slots_per_day as i64),
21114            },
21115            ParamKV {
21116                key: "data_period",
21117                value: ParamValue::Int(5),
21118            },
21119            ParamKV {
21120                key: "filter_length",
21121                value: ParamValue::Int(20),
21122            },
21123            ParamKV {
21124                key: "kernel_width",
21125                value: ParamValue::Float(20.0),
21126            },
21127            ParamKV {
21128                key: "kernel_type",
21129                value: ParamValue::EnumString("epanechnikov"),
21130            },
21131            ParamKV {
21132                key: "confidence_adjust",
21133                value: ParamValue::EnumString("symmetric"),
21134            },
21135            ParamKV {
21136                key: "maximum_confidence_adjust",
21137                value: ParamValue::Float(100.0),
21138            },
21139            ParamKV {
21140                key: "enable_expected_value",
21141                value: ParamValue::Bool(true),
21142            },
21143            ParamKV {
21144                key: "extra_smoothing",
21145                value: ParamValue::Int(0),
21146            },
21147        ];
21148        let combos = [IndicatorParamSet { params: &params }];
21149
21150        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21151            indicator_id: "half_causal_estimator",
21152            output_id: Some("estimate"),
21153            data: IndicatorDataRef::Slice { values: &close },
21154            combos: &combos,
21155            kernel: Kernel::Auto,
21156        })
21157        .unwrap();
21158
21159        let direct = crate::indicators::half_causal_estimator::half_causal_estimator_with_kernel(
21160            &crate::indicators::half_causal_estimator::HalfCausalEstimatorInput::from_slice(
21161                &close,
21162                crate::indicators::half_causal_estimator::HalfCausalEstimatorParams {
21163                    slots_per_day: Some(slots_per_day),
21164                    data_period: Some(5),
21165                    filter_length: Some(20),
21166                    kernel_width: Some(20.0),
21167                    kernel_type: Some(
21168                        crate::indicators::half_causal_estimator::HalfCausalEstimatorKernelType::Epanechnikov,
21169                    ),
21170                    confidence_adjust: Some(
21171                        crate::indicators::half_causal_estimator::HalfCausalEstimatorConfidenceAdjust::Symmetric,
21172                    ),
21173                    maximum_confidence_adjust: Some(100.0),
21174                    enable_expected_value: Some(true),
21175                    extra_smoothing: Some(0),
21176                },
21177            ),
21178            Kernel::Auto,
21179        )
21180        .unwrap();
21181
21182        let values = dispatched.values_f64.as_ref().unwrap();
21183        assert_eq!(values.len(), close.len());
21184        assert_series_eq(values, &direct.estimate, 1e-9);
21185    }
21186
21187    #[test]
21188    fn compute_cpu_batch_didi_index_matches_direct() {
21189        let close: Vec<f64> = (0..256)
21190            .map(|i| 100.0 + ((i as f64) * 0.09).sin() * 7.0 + (i as f64) * 0.03)
21191            .collect();
21192        let params = [
21193            ParamKV {
21194                key: "short_length",
21195                value: ParamValue::Int(3),
21196            },
21197            ParamKV {
21198                key: "medium_length",
21199                value: ParamValue::Int(8),
21200            },
21201            ParamKV {
21202                key: "long_length",
21203                value: ParamValue::Int(20),
21204            },
21205        ];
21206        let combos = [IndicatorParamSet { params: &params }];
21207
21208        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21209            indicator_id: "didi_index",
21210            output_id: Some("short"),
21211            data: IndicatorDataRef::Slice { values: &close },
21212            combos: &combos,
21213            kernel: Kernel::Auto,
21214        })
21215        .unwrap();
21216
21217        let direct = didi_index_with_kernel(
21218            &DidiIndexInput::from_slice(
21219                &close,
21220                DidiIndexParams {
21221                    short_length: Some(3),
21222                    medium_length: Some(8),
21223                    long_length: Some(20),
21224                },
21225            ),
21226            Kernel::Auto,
21227        )
21228        .unwrap();
21229
21230        let values = dispatched.values_f64.as_ref().unwrap();
21231        assert_eq!(values.len(), close.len());
21232        assert_series_eq(values, &direct.short, 1e-9);
21233    }
21234
21235    #[test]
21236    fn compute_cpu_batch_ehlers_autocorrelation_periodogram_matches_direct() {
21237        let close: Vec<f64> = (0..256)
21238            .map(|i| {
21239                let phase = 2.0 * std::f64::consts::PI * i as f64 / 20.0;
21240                phase.sin() + 0.15 * (phase * 0.5).cos()
21241            })
21242            .collect();
21243        let params = [
21244            ParamKV {
21245                key: "min_period",
21246                value: ParamValue::Int(8),
21247            },
21248            ParamKV {
21249                key: "max_period",
21250                value: ParamValue::Int(48),
21251            },
21252            ParamKV {
21253                key: "avg_length",
21254                value: ParamValue::Int(3),
21255            },
21256            ParamKV {
21257                key: "enhance",
21258                value: ParamValue::Bool(true),
21259            },
21260        ];
21261        let combos = [IndicatorParamSet { params: &params }];
21262
21263        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21264            indicator_id: "ehlers_autocorrelation_periodogram",
21265            output_id: Some("dominant_cycle"),
21266            data: IndicatorDataRef::Slice { values: &close },
21267            combos: &combos,
21268            kernel: Kernel::Auto,
21269        })
21270        .unwrap();
21271
21272        let direct = ehlers_autocorrelation_periodogram_with_kernel(
21273            &EhlersAutocorrelationPeriodogramInput::from_slice(
21274                &close,
21275                EhlersAutocorrelationPeriodogramParams {
21276                    min_period: Some(8),
21277                    max_period: Some(48),
21278                    avg_length: Some(3),
21279                    enhance: Some(true),
21280                },
21281            ),
21282            Kernel::Auto,
21283        )
21284        .unwrap();
21285
21286        let values = dispatched.values_f64.as_ref().unwrap();
21287        assert_eq!(values.len(), close.len());
21288        assert_series_eq(values, &direct.dominant_cycle, 1e-9);
21289    }
21290
21291    #[test]
21292    fn compute_cpu_batch_ehlers_linear_extrapolation_predictor_matches_direct() {
21293        let close: Vec<f64> = (0..256)
21294            .map(|i| 100.0 + ((i as f64) * 0.09).sin() * 2.0 + (i as f64 * 0.03))
21295            .collect();
21296        let params = [
21297            ParamKV {
21298                key: "high_pass_length",
21299                value: ParamValue::Int(125),
21300            },
21301            ParamKV {
21302                key: "low_pass_length",
21303                value: ParamValue::Int(12),
21304            },
21305            ParamKV {
21306                key: "gain",
21307                value: ParamValue::Float(0.7),
21308            },
21309            ParamKV {
21310                key: "bars_forward",
21311                value: ParamValue::Int(5),
21312            },
21313            ParamKV {
21314                key: "signal_mode",
21315                value: ParamValue::EnumString("predict_filter_crosses"),
21316            },
21317        ];
21318        let combos = [IndicatorParamSet { params: &params }];
21319
21320        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21321            indicator_id: "ehlers_linear_extrapolation_predictor",
21322            output_id: Some("prediction"),
21323            data: IndicatorDataRef::Slice { values: &close },
21324            combos: &combos,
21325            kernel: Kernel::Auto,
21326        })
21327        .unwrap();
21328
21329        let direct = ehlers_linear_extrapolation_predictor_with_kernel(
21330            &EhlersLinearExtrapolationPredictorInput::from_slice(
21331                &close,
21332                EhlersLinearExtrapolationPredictorParams {
21333                    high_pass_length: Some(125),
21334                    low_pass_length: Some(12),
21335                    gain: Some(0.7),
21336                    bars_forward: Some(5),
21337                    signal_mode: Some("predict_filter_crosses".to_string()),
21338                },
21339            ),
21340            Kernel::Auto,
21341        )
21342        .unwrap();
21343
21344        let values = dispatched.values_f64.as_ref().unwrap();
21345        assert_eq!(values.len(), close.len());
21346        assert_series_eq(values, &direct.prediction, 1e-9);
21347    }
21348
21349    #[test]
21350    fn compute_cpu_batch_grover_llorens_cycle_oscillator_matches_direct() {
21351        let mut open = Vec::with_capacity(256);
21352        let mut high = Vec::with_capacity(256);
21353        let mut low = Vec::with_capacity(256);
21354        let mut close = Vec::with_capacity(256);
21355        let mut prev = 100.0;
21356        for i in 0..256 {
21357            let x = i as f64;
21358            let wave = (x * 0.11).sin() * 2.4 + (x * 0.037).cos() * 1.3;
21359            let o = prev + wave * 0.35;
21360            let c = o + (x * 0.19).sin() * 1.1 - (x * 0.07).cos() * 0.4;
21361            let h = o.max(c) + 0.6 + (x * 0.03).sin().abs() * 0.25;
21362            let l = o.min(c) - 0.6 - (x * 0.02).cos().abs() * 0.25;
21363            open.push(o);
21364            high.push(h);
21365            low.push(l);
21366            close.push(c);
21367            prev = c;
21368        }
21369
21370        let params = [
21371            ParamKV {
21372                key: "length",
21373                value: ParamValue::Int(60),
21374            },
21375            ParamKV {
21376                key: "mult",
21377                value: ParamValue::Float(8.0),
21378            },
21379            ParamKV {
21380                key: "source",
21381                value: ParamValue::EnumString("hlc3"),
21382            },
21383            ParamKV {
21384                key: "smooth",
21385                value: ParamValue::Bool(true),
21386            },
21387            ParamKV {
21388                key: "rsi_period",
21389                value: ParamValue::Int(14),
21390            },
21391        ];
21392        let combos = [IndicatorParamSet { params: &params }];
21393
21394        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21395            indicator_id: "grover_llorens_cycle_oscillator",
21396            output_id: Some("value"),
21397            data: IndicatorDataRef::Ohlc {
21398                open: &open,
21399                high: &high,
21400                low: &low,
21401                close: &close,
21402            },
21403            combos: &combos,
21404            kernel: Kernel::Auto,
21405        })
21406        .unwrap();
21407
21408        let direct = grover_llorens_cycle_oscillator_with_kernel(
21409            &GroverLlorensCycleOscillatorInput::from_slices(
21410                &open,
21411                &high,
21412                &low,
21413                &close,
21414                GroverLlorensCycleOscillatorParams {
21415                    length: Some(60),
21416                    mult: Some(8.0),
21417                    source: Some("hlc3".to_string()),
21418                    smooth: Some(true),
21419                    rsi_period: Some(14),
21420                },
21421            ),
21422            Kernel::Auto,
21423        )
21424        .unwrap();
21425
21426        let values = dispatched.values_f64.as_ref().unwrap();
21427        assert_eq!(values.len(), close.len());
21428        assert_series_eq(values, &direct.values, 1e-9);
21429    }
21430
21431    #[test]
21432    fn compute_cpu_batch_historical_volatility_matches_direct() {
21433        let close: Vec<f64> = (0..256)
21434            .map(|i| 100.0 + ((i as f64) * 0.02).sin() + (i as f64 * 0.1))
21435            .collect();
21436        let params = [
21437            ParamKV {
21438                key: "lookback",
21439                value: ParamValue::Int(20),
21440            },
21441            ParamKV {
21442                key: "annualization_days",
21443                value: ParamValue::Float(252.0),
21444            },
21445        ];
21446        let combos = [IndicatorParamSet { params: &params }];
21447
21448        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21449            indicator_id: "historical_volatility",
21450            output_id: Some("value"),
21451            data: IndicatorDataRef::Slice { values: &close },
21452            combos: &combos,
21453            kernel: Kernel::Auto,
21454        })
21455        .unwrap();
21456
21457        let direct = historical_volatility_with_kernel(
21458            &HistoricalVolatilityInput::from_slice(
21459                &close,
21460                HistoricalVolatilityParams {
21461                    lookback: Some(20),
21462                    annualization_days: Some(252.0),
21463                },
21464            ),
21465            Kernel::Auto,
21466        )
21467        .unwrap();
21468
21469        let values = dispatched.values_f64.as_ref().unwrap();
21470        assert_eq!(values.len(), close.len());
21471        assert_series_eq(values, &direct.values, 1e-9);
21472    }
21473
21474    #[test]
21475    fn compute_cpu_batch_stochastic_distance_matches_direct() {
21476        let close: Vec<f64> = (0..256)
21477            .map(|i| 100.0 + (i as f64 * 0.07).sin() * 1.3 + i as f64 * 0.03)
21478            .collect();
21479        let params = [
21480            ParamKV {
21481                key: "lookback_length",
21482                value: ParamValue::Int(50),
21483            },
21484            ParamKV {
21485                key: "length1",
21486                value: ParamValue::Int(8),
21487            },
21488            ParamKV {
21489                key: "length2",
21490                value: ParamValue::Int(4),
21491            },
21492            ParamKV {
21493                key: "ob_level",
21494                value: ParamValue::Int(40),
21495            },
21496            ParamKV {
21497                key: "os_level",
21498                value: ParamValue::Int(-40),
21499            },
21500        ];
21501        let combos = [IndicatorParamSet { params: &params }];
21502
21503        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21504            indicator_id: "stochastic_distance",
21505            output_id: Some("oscillator"),
21506            data: IndicatorDataRef::Slice { values: &close },
21507            combos: &combos,
21508            kernel: Kernel::Auto,
21509        })
21510        .unwrap();
21511
21512        let direct = stochastic_distance_with_kernel(
21513            &StochasticDistanceInput::from_slice(
21514                &close,
21515                StochasticDistanceParams {
21516                    lookback_length: Some(50),
21517                    length1: Some(8),
21518                    length2: Some(4),
21519                    ob_level: Some(40),
21520                    os_level: Some(-40),
21521                },
21522            ),
21523            Kernel::Auto,
21524        )
21525        .unwrap();
21526
21527        let values = dispatched.values_f64.as_ref().unwrap();
21528        assert_eq!(values.len(), close.len());
21529        assert_series_eq(values, &direct.oscillator, 1e-9);
21530    }
21531
21532    #[test]
21533    fn compute_cpu_batch_adaptive_bandpass_trigger_oscillator_matches_direct() {
21534        let close: Vec<f64> = (0..256)
21535            .map(|i| 100.0 + (i as f64 * 0.07).sin() * 1.3 + (i as f64 * 0.03).cos() * 0.6)
21536            .collect();
21537        let params = [
21538            ParamKV {
21539                key: "delta",
21540                value: ParamValue::Float(0.1),
21541            },
21542            ParamKV {
21543                key: "alpha",
21544                value: ParamValue::Float(0.07),
21545            },
21546        ];
21547        let combos = [IndicatorParamSet { params: &params }];
21548
21549        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21550            indicator_id: "adaptive_bandpass_trigger_oscillator",
21551            output_id: Some("in_phase"),
21552            data: IndicatorDataRef::Slice { values: &close },
21553            combos: &combos,
21554            kernel: Kernel::Auto,
21555        })
21556        .unwrap();
21557
21558        let direct = adaptive_bandpass_trigger_oscillator_with_kernel(
21559            &AdaptiveBandpassTriggerOscillatorInput::from_slice(
21560                &close,
21561                AdaptiveBandpassTriggerOscillatorParams {
21562                    delta: Some(0.1),
21563                    alpha: Some(0.07),
21564                },
21565            ),
21566            Kernel::Auto,
21567        )
21568        .unwrap();
21569
21570        let values = dispatched.values_f64.as_ref().unwrap();
21571        assert_eq!(values.len(), close.len());
21572        assert_series_eq(values, &direct.in_phase, 1e-9);
21573    }
21574
21575    #[test]
21576    fn compute_cpu_batch_squeeze_index_matches_direct() {
21577        let close: Vec<f64> = (0..256)
21578            .map(|i| 100.0 + ((i as f64) * 0.11).sin() * 1.2 + (i as f64 * 0.02))
21579            .collect();
21580        let params = [
21581            ParamKV {
21582                key: "conv",
21583                value: ParamValue::Float(50.0),
21584            },
21585            ParamKV {
21586                key: "length",
21587                value: ParamValue::Int(20),
21588            },
21589        ];
21590        let combos = [IndicatorParamSet { params: &params }];
21591
21592        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21593            indicator_id: "squeeze_index",
21594            output_id: Some("value"),
21595            data: IndicatorDataRef::Slice { values: &close },
21596            combos: &combos,
21597            kernel: Kernel::Auto,
21598        })
21599        .unwrap();
21600
21601        let direct = squeeze_index_with_kernel(
21602            &SqueezeIndexInput::from_slice(
21603                &close,
21604                SqueezeIndexParams {
21605                    conv: Some(50.0),
21606                    length: Some(20),
21607                },
21608            ),
21609            Kernel::Auto,
21610        )
21611        .unwrap();
21612
21613        let values = dispatched.values_f64.as_ref().unwrap();
21614        assert_eq!(values.len(), close.len());
21615        assert_series_eq(values, &direct.values, 1e-9);
21616    }
21617
21618    #[test]
21619    fn compute_cpu_batch_absolute_strength_index_oscillator_matches_direct() {
21620        let close: Vec<f64> = (0..256)
21621            .map(|i| 100.0 + ((i as f64) * 0.17).sin() * 1.8 + ((i % 7) as f64 - 3.0) * 0.04)
21622            .collect();
21623        let params = [
21624            ParamKV {
21625                key: "ema_length",
21626                value: ParamValue::Int(21),
21627            },
21628            ParamKV {
21629                key: "signal_length",
21630                value: ParamValue::Int(34),
21631            },
21632        ];
21633        let combos = [IndicatorParamSet { params: &params }];
21634
21635        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21636            indicator_id: "absolute_strength_index_oscillator",
21637            output_id: Some("oscillator"),
21638            data: IndicatorDataRef::Slice { values: &close },
21639            combos: &combos,
21640            kernel: Kernel::Auto,
21641        })
21642        .unwrap();
21643
21644        let direct = absolute_strength_index_oscillator_with_kernel(
21645            &AbsoluteStrengthIndexOscillatorInput::from_slice(
21646                &close,
21647                AbsoluteStrengthIndexOscillatorParams {
21648                    ema_length: Some(21),
21649                    signal_length: Some(34),
21650                },
21651            ),
21652            Kernel::Auto,
21653        )
21654        .unwrap();
21655
21656        let values = dispatched.values_f64.as_ref().unwrap();
21657        assert_eq!(values.len(), close.len());
21658        assert_series_eq(values, &direct.oscillator, 1e-9);
21659    }
21660
21661    #[test]
21662    fn compute_cpu_batch_premier_rsi_oscillator_matches_direct() {
21663        let close: Vec<f64> = (0..256)
21664            .map(|i| 100.0 + ((i as f64) * 0.13).sin() * 1.4 + ((i % 11) as f64 - 5.0) * 0.03)
21665            .collect();
21666        let params = [
21667            ParamKV {
21668                key: "rsi_length",
21669                value: ParamValue::Int(14),
21670            },
21671            ParamKV {
21672                key: "stoch_length",
21673                value: ParamValue::Int(8),
21674            },
21675            ParamKV {
21676                key: "smooth_length",
21677                value: ParamValue::Int(25),
21678            },
21679        ];
21680        let combos = [IndicatorParamSet { params: &params }];
21681
21682        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21683            indicator_id: "premier_rsi_oscillator",
21684            output_id: Some("value"),
21685            data: IndicatorDataRef::Slice { values: &close },
21686            combos: &combos,
21687            kernel: Kernel::Auto,
21688        })
21689        .unwrap();
21690
21691        let direct = premier_rsi_oscillator_with_kernel(
21692            &PremierRsiOscillatorInput::from_slice(
21693                &close,
21694                PremierRsiOscillatorParams {
21695                    rsi_length: Some(14),
21696                    stoch_length: Some(8),
21697                    smooth_length: Some(25),
21698                },
21699            ),
21700            Kernel::Auto,
21701        )
21702        .unwrap();
21703
21704        let values = dispatched.values_f64.as_ref().unwrap();
21705        assert_eq!(values.len(), close.len());
21706        assert_series_eq(values, &direct.values, 1e-9);
21707    }
21708
21709    #[test]
21710    fn compute_cpu_batch_multi_length_stochastic_average_matches_direct() {
21711        let open: Vec<f64> = (0..256)
21712            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.09).sin())
21713            .collect();
21714        let close: Vec<f64> = open
21715            .iter()
21716            .enumerate()
21717            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.8)
21718            .collect();
21719        let high: Vec<f64> = open
21720            .iter()
21721            .zip(close.iter())
21722            .enumerate()
21723            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.05).sin().abs() * 0.2)
21724            .collect();
21725        let low: Vec<f64> = open
21726            .iter()
21727            .zip(close.iter())
21728            .enumerate()
21729            .map(|(i, (&o, &c))| o.min(c) - 0.5 - (i as f64 * 0.07).cos().abs() * 0.2)
21730            .collect();
21731        let candles = crate::utilities::data_loader::Candles::new(
21732            (0..256_i64).collect(),
21733            open,
21734            high,
21735            low,
21736            close,
21737            vec![1_000.0; 256],
21738        );
21739        let params = [
21740            ParamKV {
21741                key: "length",
21742                value: ParamValue::Int(14),
21743            },
21744            ParamKV {
21745                key: "presmooth",
21746                value: ParamValue::Int(10),
21747            },
21748            ParamKV {
21749                key: "premethod",
21750                value: ParamValue::EnumString("sma"),
21751            },
21752            ParamKV {
21753                key: "postsmooth",
21754                value: ParamValue::Int(10),
21755            },
21756            ParamKV {
21757                key: "postmethod",
21758                value: ParamValue::EnumString("lsma"),
21759            },
21760            ParamKV {
21761                key: "source",
21762                value: ParamValue::EnumString("hlc3"),
21763            },
21764        ];
21765        let combos = [IndicatorParamSet { params: &params }];
21766
21767        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21768            indicator_id: "multi_length_stochastic_average",
21769            output_id: Some("value"),
21770            data: IndicatorDataRef::Candles {
21771                candles: &candles,
21772                source: Some("hlc3"),
21773            },
21774            combos: &combos,
21775            kernel: Kernel::Auto,
21776        })
21777        .unwrap();
21778
21779        let direct = multi_length_stochastic_average_with_kernel(
21780            &MultiLengthStochasticAverageInput::from_candles(
21781                &candles,
21782                "hlc3",
21783                MultiLengthStochasticAverageParams {
21784                    length: Some(14),
21785                    presmooth: Some(10),
21786                    premethod: Some("sma".to_string()),
21787                    postsmooth: Some(10),
21788                    postmethod: Some("lsma".to_string()),
21789                },
21790            ),
21791            Kernel::Auto,
21792        )
21793        .unwrap();
21794
21795        let values = dispatched.values_f64.as_ref().unwrap();
21796        assert_eq!(values.len(), candles.close.len());
21797        assert_series_eq(values, &direct.values, 1e-9);
21798    }
21799
21800    #[test]
21801    fn compute_cpu_batch_hull_butterfly_oscillator_matches_direct() {
21802        let open: Vec<f64> = (0..256)
21803            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.09).sin())
21804            .collect();
21805        let close: Vec<f64> = open
21806            .iter()
21807            .enumerate()
21808            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.8)
21809            .collect();
21810        let high: Vec<f64> = open
21811            .iter()
21812            .zip(close.iter())
21813            .enumerate()
21814            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.05).sin().abs() * 0.2)
21815            .collect();
21816        let low: Vec<f64> = open
21817            .iter()
21818            .zip(close.iter())
21819            .enumerate()
21820            .map(|(i, (&o, &c))| o.min(c) - 0.5 - (i as f64 * 0.07).cos().abs() * 0.2)
21821            .collect();
21822        let candles = crate::utilities::data_loader::Candles::new(
21823            (0..256_i64).collect(),
21824            open,
21825            high,
21826            low,
21827            close,
21828            vec![1_000.0; 256],
21829        );
21830        let params = [
21831            ParamKV {
21832                key: "length",
21833                value: ParamValue::Int(14),
21834            },
21835            ParamKV {
21836                key: "mult",
21837                value: ParamValue::Float(1.75),
21838            },
21839            ParamKV {
21840                key: "source",
21841                value: ParamValue::EnumString("hlc3"),
21842            },
21843        ];
21844        let combos = [IndicatorParamSet { params: &params }];
21845
21846        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21847            indicator_id: "hull_butterfly_oscillator",
21848            output_id: Some("oscillator"),
21849            data: IndicatorDataRef::Candles {
21850                candles: &candles,
21851                source: Some("hlc3"),
21852            },
21853            combos: &combos,
21854            kernel: Kernel::Auto,
21855        })
21856        .unwrap();
21857
21858        let direct = hull_butterfly_oscillator_with_kernel(
21859            &HullButterflyOscillatorInput::from_candles(
21860                &candles,
21861                "hlc3",
21862                HullButterflyOscillatorParams {
21863                    length: Some(14),
21864                    mult: Some(1.75),
21865                },
21866            ),
21867            Kernel::Auto,
21868        )
21869        .unwrap();
21870
21871        let values = dispatched.values_f64.as_ref().unwrap();
21872        assert_eq!(values.len(), candles.close.len());
21873        assert_series_eq(values, &direct.oscillator, 1e-9);
21874    }
21875
21876    #[test]
21877    fn compute_cpu_batch_fibonacci_trailing_stop_matches_direct() {
21878        let open: Vec<f64> = (0..256)
21879            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.09).sin())
21880            .collect();
21881        let close: Vec<f64> = open
21882            .iter()
21883            .enumerate()
21884            .map(|(i, &o)| o + (i as f64 * 0.13).cos() * 0.8)
21885            .collect();
21886        let high: Vec<f64> = open
21887            .iter()
21888            .zip(close.iter())
21889            .enumerate()
21890            .map(|(i, (&o, &c))| o.max(c) + 0.5 + (i as f64 * 0.05).sin().abs() * 0.2)
21891            .collect();
21892        let low: Vec<f64> = open
21893            .iter()
21894            .zip(close.iter())
21895            .enumerate()
21896            .map(|(i, (&o, &c))| o.min(c) - 0.5 - (i as f64 * 0.07).cos().abs() * 0.2)
21897            .collect();
21898
21899        let params = [
21900            ParamKV {
21901                key: "left_bars",
21902                value: ParamValue::Int(12),
21903            },
21904            ParamKV {
21905                key: "right_bars",
21906                value: ParamValue::Int(2),
21907            },
21908            ParamKV {
21909                key: "level",
21910                value: ParamValue::Float(-0.236),
21911            },
21912            ParamKV {
21913                key: "trigger",
21914                value: ParamValue::EnumString("wick"),
21915            },
21916        ];
21917        let combos = [IndicatorParamSet { params: &params }];
21918
21919        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21920            indicator_id: "fibonacci_trailing_stop",
21921            output_id: Some("trailing_stop"),
21922            data: IndicatorDataRef::Ohlc {
21923                open: &open,
21924                high: &high,
21925                low: &low,
21926                close: &close,
21927            },
21928            combos: &combos,
21929            kernel: Kernel::Auto,
21930        })
21931        .unwrap();
21932
21933        let direct = fibonacci_trailing_stop_with_kernel(
21934            &FibonacciTrailingStopInput::from_slices(
21935                &high,
21936                &low,
21937                &close,
21938                FibonacciTrailingStopParams {
21939                    left_bars: Some(12),
21940                    right_bars: Some(2),
21941                    level: Some(-0.236),
21942                    trigger: Some("wick".to_string()),
21943                },
21944            ),
21945            Kernel::Auto,
21946        )
21947        .unwrap();
21948
21949        let values = dispatched.values_f64.as_ref().unwrap();
21950        assert_eq!(values.len(), close.len());
21951        assert_series_eq(values, &direct.trailing_stop, 1e-9);
21952    }
21953
21954    #[test]
21955    fn compute_cpu_batch_volume_energy_reservoirs_matches_direct() {
21956        let open: Vec<f64> = (0..256)
21957            .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.08).sin())
21958            .collect();
21959        let close: Vec<f64> = open
21960            .iter()
21961            .enumerate()
21962            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.9)
21963            .collect();
21964        let high: Vec<f64> = open
21965            .iter()
21966            .zip(close.iter())
21967            .enumerate()
21968            .map(|(i, (&o, &c))| o.max(c) + 0.6 + (i as f64 * 0.03).sin().abs() * 0.25)
21969            .collect();
21970        let low: Vec<f64> = open
21971            .iter()
21972            .zip(close.iter())
21973            .enumerate()
21974            .map(|(i, (&o, &c))| o.min(c) - 0.6 - (i as f64 * 0.05).cos().abs() * 0.2)
21975            .collect();
21976        let volume: Vec<f64> = (0..256)
21977            .map(|i| 1_000.0 + i as f64 * 4.0 + (i as f64 * 0.09).sin() * 180.0)
21978            .collect();
21979
21980        let params = [
21981            ParamKV {
21982                key: "length",
21983                value: ParamValue::Int(18),
21984            },
21985            ParamKV {
21986                key: "sensitivity",
21987                value: ParamValue::Float(1.7),
21988            },
21989        ];
21990        let combos = [IndicatorParamSet { params: &params }];
21991
21992        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
21993            indicator_id: "volume_energy_reservoirs",
21994            output_id: Some("momentum"),
21995            data: IndicatorDataRef::Ohlcv {
21996                open: &open,
21997                high: &high,
21998                low: &low,
21999                close: &close,
22000                volume: &volume,
22001            },
22002            combos: &combos,
22003            kernel: Kernel::Auto,
22004        })
22005        .unwrap();
22006
22007        let direct = volume_energy_reservoirs_with_kernel(
22008            &VolumeEnergyReservoirsInput::from_slices(
22009                &high,
22010                &low,
22011                &close,
22012                &volume,
22013                VolumeEnergyReservoirsParams {
22014                    length: Some(18),
22015                    sensitivity: Some(1.7),
22016                },
22017            ),
22018            Kernel::Auto,
22019        )
22020        .unwrap();
22021
22022        let values = dispatched.values_f64.as_ref().unwrap();
22023        assert_eq!(values.len(), close.len());
22024        assert_series_eq(values, &direct.momentum, 1e-9);
22025    }
22026
22027    #[test]
22028    fn compute_cpu_batch_neighboring_trailing_stop_matches_direct() {
22029        let open: Vec<f64> = (0..256)
22030            .map(|i| 100.0 + i as f64 * 0.04 + (i as f64 * 0.07).sin())
22031            .collect();
22032        let close: Vec<f64> = open
22033            .iter()
22034            .enumerate()
22035            .map(|(i, &o)| o + (i as f64 * 0.11).cos() * 0.85)
22036            .collect();
22037        let high: Vec<f64> = open
22038            .iter()
22039            .zip(close.iter())
22040            .enumerate()
22041            .map(|(i, (&o, &c))| o.max(c) + 0.55 + (i as f64 * 0.03).sin().abs() * 0.2)
22042            .collect();
22043        let low: Vec<f64> = open
22044            .iter()
22045            .zip(close.iter())
22046            .enumerate()
22047            .map(|(i, (&o, &c))| o.min(c) - 0.55 - (i as f64 * 0.05).cos().abs() * 0.2)
22048            .collect();
22049
22050        let params = [
22051            ParamKV {
22052                key: "buffer_size",
22053                value: ParamValue::Int(180),
22054            },
22055            ParamKV {
22056                key: "k",
22057                value: ParamValue::Int(30),
22058            },
22059            ParamKV {
22060                key: "percentile",
22061                value: ParamValue::Float(87.5),
22062            },
22063            ParamKV {
22064                key: "smooth",
22065                value: ParamValue::Int(4),
22066            },
22067        ];
22068        let combos = [IndicatorParamSet { params: &params }];
22069
22070        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22071            indicator_id: "neighboring_trailing_stop",
22072            output_id: Some("trailing_stop"),
22073            data: IndicatorDataRef::Ohlc {
22074                open: &open,
22075                high: &high,
22076                low: &low,
22077                close: &close,
22078            },
22079            combos: &combos,
22080            kernel: Kernel::Auto,
22081        })
22082        .unwrap();
22083
22084        let direct = neighboring_trailing_stop_with_kernel(
22085            &NeighboringTrailingStopInput::from_slices(
22086                &high,
22087                &low,
22088                &close,
22089                NeighboringTrailingStopParams {
22090                    buffer_size: Some(180),
22091                    k: Some(30),
22092                    percentile: Some(87.5),
22093                    smooth: Some(4),
22094                },
22095            ),
22096            Kernel::Auto,
22097        )
22098        .unwrap();
22099
22100        let values = dispatched.values_f64.as_ref().unwrap();
22101        assert_eq!(values.len(), close.len());
22102        assert_series_eq(values, &direct.trailing_stop, 1e-9);
22103    }
22104
22105    #[test]
22106    fn compute_cpu_batch_macd_wave_signal_pro_matches_direct() {
22107        let open: Vec<f64> = (0..256)
22108            .map(|i| 100.0 + i as f64 * 0.08 + ((i as f64) * 0.05).sin() * 0.7)
22109            .collect();
22110        let close: Vec<f64> = open
22111            .iter()
22112            .enumerate()
22113            .map(|(i, o)| o + ((i as f64) * 0.09).cos() * 0.9)
22114            .collect();
22115        let high: Vec<f64> = open
22116            .iter()
22117            .zip(close.iter())
22118            .enumerate()
22119            .map(|(i, (&o, &c))| o.max(c) + 0.55 + (i as f64 * 0.03).sin().abs() * 0.2)
22120            .collect();
22121        let low: Vec<f64> = open
22122            .iter()
22123            .zip(close.iter())
22124            .enumerate()
22125            .map(|(i, (&o, &c))| o.min(c) - 0.55 - (i as f64 * 0.05).cos().abs() * 0.2)
22126            .collect();
22127        let combos = [IndicatorParamSet { params: &[] }];
22128
22129        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22130            indicator_id: "macd_wave_signal_pro",
22131            output_id: Some("line_convergence"),
22132            data: IndicatorDataRef::Ohlc {
22133                open: &open,
22134                high: &high,
22135                low: &low,
22136                close: &close,
22137            },
22138            combos: &combos,
22139            kernel: Kernel::Auto,
22140        })
22141        .unwrap();
22142
22143        let direct = macd_wave_signal_pro_with_kernel(
22144            &MacdWaveSignalProInput::from_slices(&open, &high, &low, &close, Default::default()),
22145            Kernel::Auto,
22146        )
22147        .unwrap();
22148
22149        let values = dispatched.values_f64.as_ref().unwrap();
22150        assert_eq!(values.len(), close.len());
22151        assert_series_eq(values, &direct.line_convergence, 1e-9);
22152    }
22153
22154    #[test]
22155    fn compute_cpu_batch_hema_trend_levels_matches_direct() {
22156        let open: Vec<f64> = (0..256)
22157            .map(|i| 100.0 + i as f64 * 0.05 + ((i as f64) * 0.09).sin() * 1.3)
22158            .collect();
22159        let close: Vec<f64> = open
22160            .iter()
22161            .enumerate()
22162            .map(|(i, o)| o + ((i as f64) * 0.07).cos() * 1.1)
22163            .collect();
22164        let high: Vec<f64> = open
22165            .iter()
22166            .zip(close.iter())
22167            .enumerate()
22168            .map(|(i, (&o, &c))| o.max(c) + 0.65 + (i as f64 * 0.03).sin().abs() * 0.25)
22169            .collect();
22170        let low: Vec<f64> = open
22171            .iter()
22172            .zip(close.iter())
22173            .enumerate()
22174            .map(|(i, (&o, &c))| o.min(c) - 0.65 - (i as f64 * 0.05).cos().abs() * 0.25)
22175            .collect();
22176        let params = [
22177            ParamKV {
22178                key: "fast_length",
22179                value: ParamValue::Int(20),
22180            },
22181            ParamKV {
22182                key: "slow_length",
22183                value: ParamValue::Int(40),
22184            },
22185        ];
22186        let combos = [IndicatorParamSet { params: &params }];
22187
22188        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22189            indicator_id: "hema_trend_levels",
22190            output_id: Some("bullish_test_level"),
22191            data: IndicatorDataRef::Ohlc {
22192                open: &open,
22193                high: &high,
22194                low: &low,
22195                close: &close,
22196            },
22197            combos: &combos,
22198            kernel: Kernel::Auto,
22199        })
22200        .unwrap();
22201
22202        let direct = hema_trend_levels_with_kernel(
22203            &HemaTrendLevelsInput::from_slices(
22204                &open,
22205                &high,
22206                &low,
22207                &close,
22208                HemaTrendLevelsParams {
22209                    fast_length: Some(20),
22210                    slow_length: Some(40),
22211                },
22212            ),
22213            Kernel::Auto,
22214        )
22215        .unwrap();
22216
22217        let values = dispatched.values_f64.as_ref().unwrap();
22218        assert_eq!(values.len(), close.len());
22219        assert_series_eq(values, &direct.bullish_test_level, 1e-9);
22220    }
22221
22222    #[test]
22223    fn compute_cpu_batch_fibonacci_entry_bands_matches_direct() {
22224        let open: Vec<f64> = (0..256)
22225            .map(|i| 100.0 + i as f64 * 0.05 + ((i as f64) * 0.09).sin() * 1.3)
22226            .collect();
22227        let close: Vec<f64> = open
22228            .iter()
22229            .enumerate()
22230            .map(|(i, o)| o + ((i as f64) * 0.07).cos() * 1.1)
22231            .collect();
22232        let high: Vec<f64> = open
22233            .iter()
22234            .zip(close.iter())
22235            .enumerate()
22236            .map(|(i, (&o, &c))| o.max(c) + 0.65 + (i as f64 * 0.03).sin().abs() * 0.25)
22237            .collect();
22238        let low: Vec<f64> = open
22239            .iter()
22240            .zip(close.iter())
22241            .enumerate()
22242            .map(|(i, (&o, &c))| o.min(c) - 0.65 - (i as f64 * 0.05).cos().abs() * 0.25)
22243            .collect();
22244        let params = [
22245            ParamKV {
22246                key: "source",
22247                value: ParamValue::EnumString("hlc3"),
22248            },
22249            ParamKV {
22250                key: "length",
22251                value: ParamValue::Int(20),
22252            },
22253            ParamKV {
22254                key: "atr_length",
22255                value: ParamValue::Int(11),
22256            },
22257            ParamKV {
22258                key: "use_atr",
22259                value: ParamValue::Bool(true),
22260            },
22261            ParamKV {
22262                key: "tp_aggressiveness",
22263                value: ParamValue::EnumString("medium"),
22264            },
22265        ];
22266        let combos = [IndicatorParamSet { params: &params }];
22267
22268        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22269            indicator_id: "fibonacci_entry_bands",
22270            output_id: Some("tp_long_band"),
22271            data: IndicatorDataRef::Ohlc {
22272                open: &open,
22273                high: &high,
22274                low: &low,
22275                close: &close,
22276            },
22277            combos: &combos,
22278            kernel: Kernel::Auto,
22279        })
22280        .unwrap();
22281
22282        let direct = fibonacci_entry_bands_with_kernel(
22283            &FibonacciEntryBandsInput::from_slices(
22284                &open,
22285                &high,
22286                &low,
22287                &close,
22288                FibonacciEntryBandsParams {
22289                    source: Some("hlc3".to_string()),
22290                    length: Some(20),
22291                    atr_length: Some(11),
22292                    use_atr: Some(true),
22293                    tp_aggressiveness: Some("medium".to_string()),
22294                },
22295            ),
22296            Kernel::Auto,
22297        )
22298        .unwrap();
22299
22300        let values = dispatched.values_f64.as_ref().unwrap();
22301        assert_eq!(values.len(), close.len());
22302        assert_series_eq(values, &direct.tp_long_band, 1e-9);
22303    }
22304
22305    #[test]
22306    fn compute_cpu_batch_vertical_horizontal_filter_matches_direct() {
22307        let close: Vec<f64> = (0..256)
22308            .map(|i| 100.0 + ((i as f64) * 0.02).sin() + (i as f64 * 0.1))
22309            .collect();
22310        let params = [ParamKV {
22311            key: "length",
22312            value: ParamValue::Int(28),
22313        }];
22314        let combos = [IndicatorParamSet { params: &params }];
22315
22316        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22317            indicator_id: "vertical_horizontal_filter",
22318            output_id: Some("value"),
22319            data: IndicatorDataRef::Slice { values: &close },
22320            combos: &combos,
22321            kernel: Kernel::Auto,
22322        })
22323        .unwrap();
22324
22325        let direct = vertical_horizontal_filter_with_kernel(
22326            &VerticalHorizontalFilterInput::from_slice(
22327                &close,
22328                VerticalHorizontalFilterParams { length: Some(28) },
22329            ),
22330            Kernel::Auto,
22331        )
22332        .unwrap();
22333
22334        let values = dispatched.values_f64.as_ref().unwrap();
22335        assert_eq!(values.len(), close.len());
22336        assert_series_eq(values, &direct.values, 1e-9);
22337    }
22338
22339    #[test]
22340    fn compute_cpu_batch_intraday_momentum_index_matches_direct() {
22341        let open: Vec<f64> = (0..256)
22342            .map(|i| 100.0 + i as f64 * 0.1 + ((i as f64) * 0.05).cos() * 0.2)
22343            .collect();
22344        let high: Vec<f64> = open.iter().map(|v| v + 0.9).collect();
22345        let low: Vec<f64> = open.iter().map(|v| v - 0.8).collect();
22346        let close: Vec<f64> = open
22347            .iter()
22348            .enumerate()
22349            .map(|(i, o)| o + ((i as f64) * 0.09).sin() * 0.6)
22350            .collect();
22351        let params = [
22352            ParamKV {
22353                key: "length",
22354                value: ParamValue::Int(14),
22355            },
22356            ParamKV {
22357                key: "length_ma",
22358                value: ParamValue::Int(6),
22359            },
22360            ParamKV {
22361                key: "mult",
22362                value: ParamValue::Float(2.0),
22363            },
22364            ParamKV {
22365                key: "length_bb",
22366                value: ParamValue::Int(20),
22367            },
22368            ParamKV {
22369                key: "apply_smoothing",
22370                value: ParamValue::Bool(true),
22371            },
22372            ParamKV {
22373                key: "low_band",
22374                value: ParamValue::Int(10),
22375            },
22376        ];
22377        let combos = [IndicatorParamSet { params: &params }];
22378
22379        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22380            indicator_id: "intraday_momentum_index",
22381            output_id: Some("imi"),
22382            data: IndicatorDataRef::Ohlc {
22383                open: &open,
22384                high: &high,
22385                low: &low,
22386                close: &close,
22387            },
22388            combos: &combos,
22389            kernel: Kernel::Auto,
22390        })
22391        .unwrap();
22392
22393        let direct = intraday_momentum_index_with_kernel(
22394            &IntradayMomentumIndexInput::from_slices(
22395                &open,
22396                &close,
22397                IntradayMomentumIndexParams {
22398                    length: Some(14),
22399                    length_ma: Some(6),
22400                    mult: Some(2.0),
22401                    length_bb: Some(20),
22402                    apply_smoothing: Some(true),
22403                    low_band: Some(10),
22404                },
22405            ),
22406            Kernel::Auto,
22407        )
22408        .unwrap();
22409
22410        let values = dispatched.values_f64.as_ref().unwrap();
22411        assert_eq!(values.len(), close.len());
22412        assert_series_eq(values, &direct.imi, 1e-9);
22413    }
22414
22415    #[test]
22416    fn compute_cpu_batch_atr_percentile_matches_direct() {
22417        let high: Vec<f64> = (0..256)
22418            .map(|i| 100.0 + i as f64 * 0.1 + ((i as f64) * 0.03).sin().abs())
22419            .collect();
22420        let low: Vec<f64> = high
22421            .iter()
22422            .enumerate()
22423            .map(|(i, h)| h - 0.75 - ((i as f64) * 0.02).cos().abs() * 0.2)
22424            .collect();
22425        let close: Vec<f64> = low
22426            .iter()
22427            .zip(high.iter())
22428            .enumerate()
22429            .map(|(i, (l, h))| l + (h - l) * (0.35 + 0.2 * ((i as f64) * 0.05).sin().abs()))
22430            .collect();
22431        let params = [
22432            ParamKV {
22433                key: "atr_length",
22434                value: ParamValue::Int(10),
22435            },
22436            ParamKV {
22437                key: "percentile_length",
22438                value: ParamValue::Int(20),
22439            },
22440        ];
22441        let combos = [IndicatorParamSet { params: &params }];
22442
22443        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22444            indicator_id: "atr_percentile",
22445            output_id: Some("value"),
22446            data: IndicatorDataRef::Ohlc {
22447                open: &close,
22448                high: &high,
22449                low: &low,
22450                close: &close,
22451            },
22452            combos: &combos,
22453            kernel: Kernel::Auto,
22454        })
22455        .unwrap();
22456
22457        let direct = atr_percentile_with_kernel(
22458            &AtrPercentileInput::from_slices(
22459                &high,
22460                &low,
22461                &close,
22462                AtrPercentileParams {
22463                    atr_length: Some(10),
22464                    percentile_length: Some(20),
22465                },
22466            ),
22467            Kernel::Auto,
22468        )
22469        .unwrap();
22470
22471        let values = dispatched.values_f64.as_ref().unwrap();
22472        assert_eq!(values.len(), close.len());
22473        assert_series_eq(values, &direct.values, 1e-9);
22474    }
22475
22476    #[test]
22477    fn compute_cpu_batch_demand_index_matches_direct() {
22478        let high: Vec<f64> = (0..256)
22479            .map(|i| 100.0 + i as f64 * 0.15 + ((i as f64) * 0.03).sin().abs())
22480            .collect();
22481        let low: Vec<f64> = high
22482            .iter()
22483            .enumerate()
22484            .map(|(i, h)| h - 0.9 - ((i as f64) * 0.04).cos().abs() * 0.3)
22485            .collect();
22486        let close: Vec<f64> = low
22487            .iter()
22488            .zip(high.iter())
22489            .enumerate()
22490            .map(|(i, (l, h))| l + (h - l) * (0.25 + 0.5 * ((i as f64) * 0.07).sin().abs()))
22491            .collect();
22492        let open: Vec<f64> = close
22493            .iter()
22494            .enumerate()
22495            .map(|(i, c)| c - 0.2 + ((i as f64) * 0.05).cos() * 0.1)
22496            .collect();
22497        let volume: Vec<f64> = (0..256)
22498            .map(|i| 1000.0 + (i as f64) * 3.0 + ((i as f64) * 0.11).sin().abs() * 40.0)
22499            .collect();
22500        let params = [
22501            ParamKV {
22502                key: "len_bs",
22503                value: ParamValue::Int(19),
22504            },
22505            ParamKV {
22506                key: "len_bs_ma",
22507                value: ParamValue::Int(19),
22508            },
22509            ParamKV {
22510                key: "len_di_ma",
22511                value: ParamValue::Int(19),
22512            },
22513            ParamKV {
22514                key: "ma_type",
22515                value: ParamValue::EnumString("ema"),
22516            },
22517        ];
22518        let combos = [IndicatorParamSet { params: &params }];
22519
22520        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22521            indicator_id: "demand_index",
22522            output_id: Some("demand_index"),
22523            data: IndicatorDataRef::Ohlcv {
22524                open: &open,
22525                high: &high,
22526                low: &low,
22527                close: &close,
22528                volume: &volume,
22529            },
22530            combos: &combos,
22531            kernel: Kernel::Auto,
22532        })
22533        .unwrap();
22534
22535        let direct = demand_index_with_kernel(
22536            &DemandIndexInput::from_slices(
22537                &high,
22538                &low,
22539                &close,
22540                &volume,
22541                DemandIndexParams {
22542                    len_bs: Some(19),
22543                    len_bs_ma: Some(19),
22544                    len_di_ma: Some(19),
22545                    ma_type: Some("ema".to_string()),
22546                },
22547            ),
22548            Kernel::Auto,
22549        )
22550        .unwrap();
22551
22552        let values = dispatched.values_f64.as_ref().unwrap();
22553        assert_eq!(values.len(), close.len());
22554        assert_series_eq(values, &direct.demand_index, 1e-9);
22555    }
22556
22557    #[test]
22558    fn compute_cpu_batch_vwap_zscore_with_signals_matches_direct() {
22559        let close: Vec<f64> = (0..192).map(|i| 100.0 + (i as f64 * 0.15)).collect();
22560        let volume: Vec<f64> = (0..192).map(|i| 1_000.0 + (i as f64 * 2.0)).collect();
22561        let req = IndicatorBatchRequest {
22562            indicator_id: "vwap_zscore_with_signals",
22563            output_id: Some("zvwap"),
22564            data: IndicatorDataRef::CloseVolume {
22565                close: &close,
22566                volume: &volume,
22567            },
22568            combos: &[IndicatorParamSet {
22569                params: &[
22570                    ParamKV {
22571                        key: "length",
22572                        value: ParamValue::Int(20),
22573                    },
22574                    ParamKV {
22575                        key: "upper_bottom",
22576                        value: ParamValue::Float(2.5),
22577                    },
22578                    ParamKV {
22579                        key: "lower_bottom",
22580                        value: ParamValue::Float(-2.5),
22581                    },
22582                ],
22583            }],
22584            kernel: Kernel::Auto,
22585        };
22586
22587        let out = compute_cpu_batch(req).unwrap();
22588        let values = out.values_f64.as_ref().unwrap();
22589        let direct = vwap_zscore_with_signals_with_kernel(
22590            &VwapZscoreWithSignalsInput::from_slices(
22591                &close,
22592                &volume,
22593                VwapZscoreWithSignalsParams {
22594                    length: Some(20),
22595                    upper_bottom: Some(2.5),
22596                    lower_bottom: Some(-2.5),
22597                },
22598            ),
22599            Kernel::Auto,
22600        )
22601        .unwrap();
22602        assert_eq!(out.rows, 1);
22603        assert_eq!(out.cols, close.len());
22604        assert_series_eq(values, &direct.zvwap, 1e-9);
22605    }
22606
22607    #[test]
22608    fn compute_cpu_batch_gopalakrishnan_range_index_matches_direct() {
22609        let high: Vec<f64> = (0..256)
22610            .map(|i| 100.0 + i as f64 * 0.1 + ((i as f64) * 0.03).sin().abs())
22611            .collect();
22612        let low: Vec<f64> = high
22613            .iter()
22614            .enumerate()
22615            .map(|(i, h)| h - 0.75 - ((i as f64) * 0.02).cos().abs() * 0.2)
22616            .collect();
22617        let params = [ParamKV {
22618            key: "length",
22619            value: ParamValue::Int(5),
22620        }];
22621        let combos = [IndicatorParamSet { params: &params }];
22622
22623        let dispatched = compute_cpu_batch(IndicatorBatchRequest {
22624            indicator_id: "gopalakrishnan_range_index",
22625            output_id: Some("value"),
22626            data: IndicatorDataRef::HighLow {
22627                high: &high,
22628                low: &low,
22629            },
22630            combos: &combos,
22631            kernel: Kernel::Auto,
22632        })
22633        .unwrap();
22634
22635        let direct = gopalakrishnan_range_index_with_kernel(
22636            &GopalakrishnanRangeIndexInput::from_slices(
22637                &high,
22638                &low,
22639                GopalakrishnanRangeIndexParams { length: Some(5) },
22640            ),
22641            Kernel::Auto,
22642        )
22643        .unwrap();
22644
22645        let values = dispatched.values_f64.as_ref().unwrap();
22646        assert_eq!(values.len(), high.len());
22647        assert_series_eq(values, &direct.values, 1e-9);
22648    }
22649}