finance_solution/stocks/ta/
donchian.rs1use crate::stocks::ta::common::{opt_cell, require_same_len, validate_series};
45use crate::stocks::ta::ring::{SlidingMax, SlidingMin};
46use crate::util::error::{require_finite, FinanceError, FinanceResult};
47use crate::util::primitives::PeriodLength;
48use crate::{columns_with_strings, print_table_locale_opt};
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52pub struct DonchianParams {
53 pub period: usize,
54}
55
56impl DonchianParams {
57 pub const fn new(period: usize) -> Self {
58 Self { period }
59 }
60
61 pub const fn period_20() -> Self {
63 Self { period: 20 }
64 }
65
66 pub const fn period_55() -> Self {
67 Self { period: 55 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
72pub struct ValidatedDonchian {
73 params: DonchianParams,
74}
75
76impl ValidatedDonchian {
77 pub fn new(params: DonchianParams) -> FinanceResult<Self> {
78 PeriodLength::new(params.period)?;
79 Ok(Self { params })
80 }
81
82 pub fn params(self) -> DonchianParams {
83 self.params
84 }
85
86 pub fn compute(self, high: &[f64], low: &[f64]) -> FinanceResult<DonchianSeries> {
87 donchian_validated(high, low, self)
88 }
89}
90
91#[derive(Clone, Debug, PartialEq)]
92pub struct DonchianSeries {
93 pub upper: Vec<Option<f64>>,
94 pub lower: Vec<Option<f64>>,
95 pub middle: Vec<Option<f64>>,
96 pub params: DonchianParams,
97}
98
99impl DonchianSeries {
100 pub fn last(&self) -> Option<(f64, f64, f64)> {
101 let n = self.upper.len();
102 if n == 0 {
103 return None;
104 }
105 Some((self.upper[n - 1]?, self.middle[n - 1]?, self.lower[n - 1]?))
106 }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct DonchianBarOutput {
111 pub upper: f64,
112 pub middle: f64,
113 pub lower: f64,
114}
115
116#[derive(Clone, Debug)]
120pub struct DonchianState {
121 params: DonchianParams,
122 high_max: SlidingMax,
123 low_min: SlidingMin,
124 last: Option<DonchianBarOutput>,
125}
126
127impl DonchianState {
128 pub fn new(params: DonchianParams) -> FinanceResult<Self> {
129 let _ = ValidatedDonchian::new(params)?;
130 Ok(Self {
131 params,
132 high_max: SlidingMax::with_window(params.period),
133 low_min: SlidingMin::with_window(params.period),
134 last: None,
135 })
136 }
137
138 pub fn from_history(params: DonchianParams, high: &[f64], low: &[f64]) -> FinanceResult<Self> {
139 let mut s = Self::new(params)?;
140 let _ = s.push_bars(high, low)?;
141 Ok(s)
142 }
143
144 pub fn params(&self) -> DonchianParams {
145 self.params
146 }
147
148 pub fn reset(&mut self) {
149 self.high_max.clear();
150 self.low_min.clear();
151 self.last = None;
152 }
153
154 pub fn push(&mut self, high: f64, low: f64) -> FinanceResult<Option<DonchianBarOutput>> {
155 require_finite("high", high)?;
156 require_finite("low", low)?;
157 if high < low {
158 return Err(FinanceError::InvalidCashflow {
159 message: "high must be >= low for each bar",
160 });
161 }
162 let u = self.high_max.push(high).unwrap();
163 let l = self.low_min.push(low).unwrap();
164 if !self.high_max.is_full() {
165 self.last = None;
166 return Ok(None);
167 }
168 let out = DonchianBarOutput {
169 upper: u,
170 middle: 0.5 * (u + l),
171 lower: l,
172 };
173 self.last = Some(out);
174 Ok(Some(out))
175 }
176
177 pub fn push_bars(
178 &mut self,
179 high: &[f64],
180 low: &[f64],
181 ) -> FinanceResult<Vec<Option<DonchianBarOutput>>> {
182 validate_series("high", high)?;
183 validate_series("low", low)?;
184 require_same_len(high, low, "high/low")?;
185 let mut out = Vec::with_capacity(high.len());
186 for i in 0..high.len() {
187 out.push(self.push(high[i], low[i])?);
188 }
189 Ok(out)
190 }
191
192 pub fn last(&self) -> Option<DonchianBarOutput> {
193 self.last
194 }
195}
196
197#[derive(Clone, Debug)]
198pub struct DonchianSolution {
199 series: DonchianSeries,
200 close: Option<Vec<f64>>,
201 formula: String,
202}
203
204impl DonchianSolution {
205 pub fn series(&self) -> &DonchianSeries {
206 &self.series
207 }
208 pub fn formula(&self) -> &str {
209 &self.formula
210 }
211
212 pub fn print_table(&self) {
213 self.print_table_locale_opt(None, None);
214 }
215
216 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
217 self.print_table_locale_opt(Some(locale), Some(precision));
218 }
219
220 fn print_table_locale_opt(
221 &self,
222 locale: Option<&num_format::Locale>,
223 precision: Option<usize>,
224 ) {
225 let columns = columns_with_strings(&[
226 ("period", "i", true),
227 ("upper", "f", true),
228 ("middle", "f", true),
229 ("lower", "f", true),
230 ]);
231 let n = self.series.upper.len();
232 let data = (0..n)
233 .map(|i| {
234 vec![
235 i.to_string(),
236 opt_cell(self.series.upper[i]),
237 opt_cell(self.series.middle[i]),
238 opt_cell(self.series.lower[i]),
239 ]
240 })
241 .collect();
242 let _ = &self.close;
243 print_table_locale_opt(&columns, data, locale, precision);
244 }
245}
246
247pub fn donchian(
248 high: &[f64],
249 low: &[f64],
250 params: DonchianParams,
251) -> FinanceResult<DonchianSeries> {
252 ValidatedDonchian::new(params)?.compute(high, low)
253}
254
255pub fn donchian_solution(
256 high: &[f64],
257 low: &[f64],
258 params: DonchianParams,
259) -> FinanceResult<DonchianSolution> {
260 let series = donchian(high, low, params)?;
261 Ok(DonchianSolution {
262 series,
263 close: None,
264 formula: format!(
265 "Donchian({}): upper=max(high,N), lower=min(low,N), mid=(u+l)/2",
266 params.period
267 ),
268 })
269}
270
271fn donchian_validated(
272 high: &[f64],
273 low: &[f64],
274 eng: ValidatedDonchian,
275) -> FinanceResult<DonchianSeries> {
276 validate_series("high", high)?;
277 validate_series("low", low)?;
278 require_same_len(high, low, "high/low")?;
279 for i in 0..high.len() {
280 if high[i] < low[i] {
281 return Err(FinanceError::InvalidCashflow {
282 message: "high must be >= low for each bar",
283 });
284 }
285 }
286 let mut st = DonchianState::new(eng.params)?;
287 let bars = st.push_bars(high, low)?;
288 let n = high.len();
289 let mut upper = vec![None; n];
290 let mut lower = vec![None; n];
291 let mut middle = vec![None; n];
292 for (i, b) in bars.into_iter().enumerate() {
293 if let Some(o) = b {
294 upper[i] = Some(o.upper);
295 lower[i] = Some(o.lower);
296 middle[i] = Some(o.middle);
297 }
298 }
299 Ok(DonchianSeries {
300 upper,
301 lower,
302 middle,
303 params: eng.params,
304 })
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn basic_window() {
313 let h = [11.0, 12.0, 13.0, 14.0];
314 let l = [9.0, 10.0, 11.0, 12.0];
315 let s = donchian(&h, &l, DonchianParams::new(3)).unwrap();
316 assert!(s.upper[1].is_none());
317 assert!((s.upper[3].unwrap() - 14.0).abs() < 1e-12);
318 assert!((s.lower[3].unwrap() - 10.0).abs() < 1e-12); assert!((s.middle[3].unwrap() - 12.0).abs() < 1e-12);
320 }
321
322 #[test]
323 fn state_parity() {
324 let n = 40usize;
325 let h: Vec<_> = (0..n).map(|i| 100.0 + (i % 7) as f64).collect();
326 let l: Vec<_> = (0..n).map(|i| 90.0 + (i % 7) as f64).collect();
327 let batch = donchian(&h, &l, DonchianParams::period_20()).unwrap();
328 let st = DonchianState::from_history(DonchianParams::period_20(), &h, &l).unwrap();
329 let (u, m, lo) = batch.last().unwrap();
330 let o = st.last().unwrap();
331 assert!((u - o.upper).abs() < 1e-12);
332 assert!((m - o.middle).abs() < 1e-12);
333 assert!((lo - o.lower).abs() < 1e-12);
334 }
335
336 #[test]
337 fn period_one_is_raw_hl() {
338 let h = [5.0, 7.0];
339 let l = [4.0, 6.0];
340 let s = donchian(&h, &l, DonchianParams::new(1)).unwrap();
341 assert_eq!(s.upper[0], Some(5.0));
342 assert_eq!(s.lower[0], Some(4.0));
343 assert_eq!(s.middle[0], Some(4.5));
344 assert_eq!(s.upper[1], Some(7.0));
345 }
346
347 #[test]
348 fn high_lt_low_err() {
349 assert!(donchian(&[1.0, 0.0], &[0.0, 1.0], DonchianParams::new(2)).is_err());
350 }
351
352 #[test]
353 fn reset_clears() {
354 let h = [10.0; 25];
355 let l = [9.0; 25];
356 let mut st = DonchianState::from_history(DonchianParams::period_20(), &h, &l).unwrap();
357 assert!(st.last().is_some());
358 st.reset();
359 assert!(st.last().is_none());
360 }
361}