1use crate::{BarField, SourceId, ValueType};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum NumericUnit {
7 Bool,
8 Number,
9 Price,
10 Ratio,
11 Percent,
12 BarCount,
13 PricePerObservation,
14 PricePerObservationSquared,
15 RatioPerObservation,
16 RatioPerObservationSquared,
17 LogReturn,
18 LogReturnVariance,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum NumericInputs {
25 Scalar(Vec<ValueType>),
26 CompletedBarFields(&'static [BarField]),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum BarShapeCalculation {
32 BodySignedAtr,
33 BodyAbsAtr,
34 BodyFraction,
35 BodyDirectionFraction,
36 UpperWickFraction,
37 LowerWickFraction,
38 ClosePosition,
39 CloseLocationValue,
40 RangeAtr,
41 GapAtr,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum PriceChangeCalculation {
47 LogReturn,
48 Roc,
49 MoveAtr,
50 HighChangeAtr,
51 LowChangeAtr,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum PriceInputCalculation {
57 Close,
58 Hl2,
59 Hlc3,
60 Ohlc4,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64#[non_exhaustive]
65pub enum MovingAverageCalculation {
66 Sma,
67 Ema,
68 Rma,
69 Wma,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[non_exhaustive]
74pub enum RecursiveCalculation {
75 Hma,
76 Kama,
77 KamaSmoothingConstant,
78 KeltnerMiddle,
79 KeltnerUpper,
80 KeltnerLower,
81 BollingerKeltnerSqueeze,
82 SuperTrendLevel,
83 SuperTrendDirection,
84 HeikinAshiOpen,
85 HeikinAshiHigh,
86 HeikinAshiLow,
87 HeikinAshiClose,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[non_exhaustive]
92pub enum NormalizedCalculation {
93 BodySignedAtr,
94 BodyAbsAtr,
95 RangeAtr,
96 GapAtr,
97 MoveAtr,
98 HighChangeAtr,
99 LowChangeAtr,
100 DonchianUpperDistanceAtr,
101 DonchianLowerDistanceAtr,
102 MacdAtr,
103 MacdSignalAtr,
104 MacdHistogramAtr,
105 MaDistanceAtr,
106 MaSlopeAtr,
107 MaAccelerationAtr,
108 MaGapAtr,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112#[non_exhaustive]
113pub enum StatisticalCalculation {
114 DonchianUpper,
115 DonchianLower,
116 ChannelWidth,
117 ChannelMidDistance,
118 RangePosition,
119 PreviousRangePosition,
120 ZScore,
121 BollingerUpper,
122 BollingerLower,
123 BollingerPercentB,
124 BollingerWidth,
125 RollingMedian,
126 MedianDeviation,
127 RealizedVariance,
128 RealizedVolatility,
129 ReturnRms,
130 ReturnStdDev,
131 EwmaVolatility,
132 PositiveSemivariance,
133 NegativeSemivariance,
134 VolatilityAsymmetry,
135 HistoricalPercentile,
136 EfficiencyRatio,
137 RegressionSlope,
138 RegressionR2,
139 RegressionResidualRms,
140 RegressionDeviation,
141 RollingHighAge,
142 RollingLowAge,
143 AroonUp,
144 AroonDown,
145 Choppiness,
146 ReturnAutocorrelation,
147 RangeExpansion,
148 AtrPercent,
149 AtrRatio,
150 AtrChange,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154#[non_exhaustive]
155pub enum MomentumCalculation {
156 WilderRsi,
157 RsiChange,
158 StochasticFastK,
159 StochasticSlowK,
160 StochasticSlowD,
161 Macd,
162 MacdSignal,
163 MacdHistogram,
164 Cci,
165 PlusDi,
166 MinusDi,
167 DiDifference,
168 Dx,
169 Adx,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173#[non_exhaustive]
174pub enum MaDerivativeCalculation {
175 Distance,
176 Slope,
177 Acceleration,
178 Gap,
179 Alignment,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183#[non_exhaustive]
184pub enum BarStructureCalculation {
185 InsideBar,
186 OutsideBar,
187 BodyEngulfing,
188 EngulfSizeRatio,
189 NarrowRange,
190 WideRange,
191 RelativeRange,
192 BarOverlap,
193 ThreeBarGapUp,
194 ThreeBarGapDown,
195}
196
197#[derive(Debug, Clone, PartialEq)]
199#[non_exhaustive]
200pub enum NumericCalculation {
201 ObservedSma {
202 period: usize,
203 },
204 SmaSeededEma {
205 period: usize,
206 alpha: f64,
207 },
208 BarShape(BarShapeCalculation),
209 PriceChange {
210 calculation: PriceChangeCalculation,
211 horizon: usize,
212 },
213 BarStructure {
214 calculation: BarStructureCalculation,
215 period: Option<usize>,
216 },
217 PriceInput(PriceInputCalculation),
218 MovingAverage {
219 calculation: MovingAverageCalculation,
220 period: usize,
221 },
222 TrueRange,
223 StrictAtr {
224 period: usize,
225 },
226 MaDerivative {
227 calculation: MaDerivativeCalculation,
228 horizon: Option<usize>,
229 },
230 Momentum {
231 calculation: MomentumCalculation,
232 periods: [usize; 3],
233 },
234 Statistical {
235 calculation: StatisticalCalculation,
236 periods: [usize; 3],
237 parameter: Option<u64>,
238 },
239 Recursive {
240 calculation: RecursiveCalculation,
241 periods: [usize; 4],
242 parameters: [u64; 2],
243 },
244 Normalized {
245 calculation: NormalizedCalculation,
246 atr_period: usize,
247 horizon: usize,
248 current_atr: bool,
249 },
250 NormalizedPair {
251 calculation: NormalizedCalculation,
252 },
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257#[non_exhaustive]
258pub enum NumericMissingPolicy {
259 ConsumeWindowSlot,
260 ResetAndReseed,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq)]
264#[non_exhaustive]
265pub enum NumericRange {
266 Unbounded,
267 Inclusive { minimum: f64, maximum: f64 },
268}
269
270#[derive(Debug, Clone, PartialEq)]
272#[non_exhaustive]
273pub struct NumericDescriptor {
274 pub calculation: NumericCalculation,
275 pub source_clock: SourceId,
276 pub inputs: NumericInputs,
277 pub output_type: ValueType,
278 pub unit: NumericUnit,
279 pub range: NumericRange,
280 pub missing: NumericMissingPolicy,
281 pub first_output_observations: usize,
283 pub required_lookback: usize,
285 pub max_state_bytes: usize,
287 pub exact_aliases: &'static [&'static str],
288}
289
290impl NumericDescriptor {
291 pub(crate) fn validate(&self, inputs: &[ValueType]) -> Result<(), String> {
292 if self.first_output_observations == 0
293 || self.required_lookback < self.first_output_observations
294 || self.max_state_bytes > crate::MAX_MATERIAL_STATE_BYTES
295 {
296 return Err("numeric descriptor has an invalid history or state bound".into());
297 }
298 match &self.inputs {
299 NumericInputs::Scalar(expected) if expected == inputs => {}
300 NumericInputs::CompletedBarFields(fields)
301 if inputs.is_empty() && !fields.is_empty() => {}
302 _ => return Err("numeric descriptor disagrees with its material inputs".into()),
303 }
304 let scalar = self.output_type.scalar;
305 let unit_matches = match self.unit {
306 NumericUnit::Bool => scalar == crate::ScalarType::Bool,
307 NumericUnit::BarCount => scalar == crate::ScalarType::Integer,
308 NumericUnit::Price => scalar == crate::ScalarType::Price,
309 NumericUnit::Ratio => scalar == crate::ScalarType::Ratio,
310 NumericUnit::Percent => scalar == crate::ScalarType::Percent,
311 NumericUnit::PricePerObservation => scalar == crate::ScalarType::PricePerObservation,
312 NumericUnit::PricePerObservationSquared => {
313 scalar == crate::ScalarType::PricePerObservationSquared
314 }
315 NumericUnit::RatioPerObservation => scalar == crate::ScalarType::RatioPerObservation,
316 NumericUnit::RatioPerObservationSquared => {
317 scalar == crate::ScalarType::RatioPerObservationSquared
318 }
319 NumericUnit::LogReturn => scalar == crate::ScalarType::LogReturn,
320 NumericUnit::LogReturnVariance => scalar == crate::ScalarType::LogReturnVariance,
321 NumericUnit::Number => scalar == crate::ScalarType::Number,
322 };
323 if !unit_matches {
324 return Err("numeric descriptor unit disagrees with its output scalar".into());
325 }
326 if let NumericRange::Inclusive { minimum, maximum } = self.range
327 && (!minimum.is_finite() || !maximum.is_finite() || minimum > maximum)
328 {
329 return Err("numeric descriptor has an invalid range".into());
330 }
331 match self.calculation {
332 NumericCalculation::ObservedSma { period } => {
333 self.validate_average(inputs, period, NumericMissingPolicy::ConsumeWindowSlot)?;
334 }
335 NumericCalculation::SmaSeededEma { period, alpha } => {
336 if alpha != 2.0 / (period as f64 + 1.0) {
337 return Err("numeric descriptor has an inconsistent EMA weight".into());
338 }
339 self.validate_average(inputs, period, NumericMissingPolicy::ResetAndReseed)?;
340 }
341 NumericCalculation::BarShape(_)
342 | NumericCalculation::PriceChange { .. }
343 | NumericCalculation::BarStructure { .. }
344 | NumericCalculation::PriceInput(_)
345 | NumericCalculation::TrueRange
346 | NumericCalculation::StrictAtr { .. } => {
347 if !matches!(self.inputs, NumericInputs::CompletedBarFields(_)) {
348 return Err("bar calculation requires completed-bar fields".into());
349 }
350 }
351 NumericCalculation::MovingAverage { period, .. } => {
352 self.validate_average(inputs, period, self.missing)?;
353 }
354 NumericCalculation::MaDerivative { .. } => {
355 if !matches!(self.inputs, NumericInputs::Scalar(_)) {
356 return Err("MA derivative requires scalar material inputs".into());
357 }
358 }
359 NumericCalculation::Momentum { .. }
360 | NumericCalculation::Statistical { .. }
361 | NumericCalculation::Recursive { .. }
362 | NumericCalculation::Normalized { .. }
363 | NumericCalculation::NormalizedPair { .. } => {}
364 }
365 Ok(())
366 }
367
368 fn validate_average(
369 &self,
370 inputs: &[ValueType],
371 period: usize,
372 missing: NumericMissingPolicy,
373 ) -> Result<(), String> {
374 if !(1..=crate::numeric::MAX_STRICT_PERIOD).contains(&period)
375 || inputs.len() != 1
376 || !matches!(
377 inputs[0].scalar,
378 crate::ScalarType::Number
379 | crate::ScalarType::Price
380 | crate::ScalarType::Ratio
381 | crate::ScalarType::Percent
382 | crate::ScalarType::PricePerObservation
383 | crate::ScalarType::PricePerObservationSquared
384 | crate::ScalarType::RatioPerObservation
385 | crate::ScalarType::RatioPerObservationSquared
386 | crate::ScalarType::LogReturn
387 | crate::ScalarType::LogReturnVariance
388 )
389 || self.output_type != ValueType::optional(inputs[0].scalar)
390 || self.missing != missing
391 || self.first_output_observations != period
392 || self.required_lookback != period
393 {
394 return Err("numeric average descriptor is inconsistent".into());
395 }
396 Ok(())
397 }
398
399 pub const fn flat_input_is_defined(&self) -> bool {
400 matches!(
401 self.calculation,
402 NumericCalculation::ObservedSma { .. } | NumericCalculation::SmaSeededEma { .. }
403 )
404 }
405}