1use super::Bar;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
10pub enum BoxSizeMethod {
11 #[default]
13 Fixed,
14 Atr {
17 period: usize,
19 multiplier: f64,
21 },
22 Percentage(f64),
24}
25
26impl BoxSizeMethod {
27 pub fn resolve(self, fixed: f64, bars: &[Bar]) -> Option<f64> {
31 let positive = |n: f64| n.is_finite() && n > 0.0;
32 let size = match self {
33 Self::Fixed => fixed,
34 Self::Percentage(percent) if positive(percent) => {
35 super::last_with_value(bars)?.close.abs() * (percent / 100.0)
36 }
37 Self::Atr { period, multiplier } if period > 0 && positive(multiplier) => {
39 let (mut average, mut previous, mut count) = (0.0, None::<f64>, 0);
40 for bar in bars.iter().filter(|bar| !bar.is_whitespace()) {
41 let mut range = bar.high - bar.low;
42 if !bar.high.is_finite()
43 || !bar.low.is_finite()
44 || !bar.close.is_finite()
45 || range < 0.0
46 {
47 return None;
48 }
49 if let Some(previous) = previous {
50 range = range
51 .max((bar.high - previous).abs())
52 .max((bar.low - previous).abs());
53 }
54 if count < period {
55 average += range / period as f64;
56 } else {
57 average += (range - average) / period as f64;
58 }
59 previous = Some(bar.close);
60 count += 1;
61 }
62 if count < period {
63 return None;
64 }
65 average * multiplier
66 }
67 _ => return None,
68 };
69 positive(size).then_some(size)
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
75pub enum PriceBasedSource {
76 #[default]
78 Close,
79 HighLow,
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn size_methods_match_wilder_atr_and_last_close_percentage() {
90 let time = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
91 .unwrap()
92 .to_utc();
93 let bars: Vec<Bar> = [(99.0, 101.0), (98.0, 102.0), (97.0, 103.0), (96.0, 104.0)]
95 .into_iter()
96 .enumerate()
97 .map(|(i, (low, high))| {
98 Bar::new(
99 time + chrono::Duration::minutes(i as i64),
100 100.0,
101 high,
102 low,
103 100.0,
104 1.0,
105 )
106 })
107 .collect();
108 for (method, fixed, data, expected) in [
109 (BoxSizeMethod::Fixed, 0.25, &bars[..], Some(0.25)),
110 (BoxSizeMethod::Percentage(2.5), 99.0, &bars[..], Some(2.5)),
111 (
112 BoxSizeMethod::Atr {
113 period: 3,
114 multiplier: 1.5,
115 },
116 99.0,
117 &bars[..],
118 Some(8.0),
119 ),
120 (
121 BoxSizeMethod::Atr {
122 period: 3,
123 multiplier: 1.0,
124 },
125 99.0,
126 &bars[..3],
127 Some(4.0),
128 ),
129 (
130 BoxSizeMethod::Atr {
131 period: 3,
132 multiplier: 1.0,
133 },
134 99.0,
135 &bars[..2],
136 None,
137 ),
138 (
139 BoxSizeMethod::Atr {
140 period: 0,
141 multiplier: 1.0,
142 },
143 99.0,
144 &bars[..],
145 None,
146 ),
147 (BoxSizeMethod::Percentage(1.0), 99.0, &bars[..0], None),
148 (
149 BoxSizeMethod::Percentage(f64::INFINITY),
150 99.0,
151 &bars[..],
152 None,
153 ),
154 (BoxSizeMethod::Fixed, 0.0, &bars[..], None),
155 ] {
156 assert_eq!(method.resolve(fixed, data), expected, "{method:?}");
157 }
158 let expected = 16.0 / 3.0;
159 assert!(
160 (super::super::RenkoConfig::from_atr(&bars, 3, 1.0).brick_size - expected).abs()
161 < 1e-12
162 );
163 assert!(
164 (super::super::KagiConfig::from_atr(&bars, 3, 1.0).reversal_amount - expected).abs()
165 < 1e-12
166 );
167 let mut gap = bars[..2].to_vec();
168 gap[1] = Bar::new(gap[1].time, 110.0, 112.0, 109.0, 111.0, 1.0);
169 assert_eq!(
171 BoxSizeMethod::Atr {
172 period: 2,
173 multiplier: 1.0
174 }
175 .resolve(0.0, &gap),
176 Some(7.0)
177 );
178 }
179}