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