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