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