1use std::{fmt::Display, num::NonZero};
2
3use crate::{
4 Indicator, IndicatorConfig, IndicatorConfigBuilder, Price, Timestamp,
5 internals::RollingExtremes,
6};
7
8#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
24pub struct DcConfig {
25 length: usize,
26}
27
28impl IndicatorConfig for DcConfig {
29 type Builder = DcConfigBuilder;
30
31 #[inline]
32 fn builder() -> Self::Builder {
33 DcConfigBuilder::new()
34 }
35
36 #[inline]
37 fn source(&self) -> crate::PriceSource {
38 crate::PriceSource::Close
39 }
40
41 #[inline]
42 fn convergence(&self) -> usize {
43 self.length
44 }
45}
46
47impl DcConfig {
48 #[must_use]
49 #[inline]
50 pub fn length(&self) -> usize {
51 self.length
52 }
53}
54
55impl Display for DcConfig {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "DcConfig(l: {})", self.length)
58 }
59}
60
61pub struct DcConfigBuilder {
65 length: usize,
66}
67
68impl DcConfigBuilder {
69 fn new() -> Self {
70 DcConfigBuilder { length: 20 }
71 }
72
73 #[inline]
74 #[must_use]
75 pub fn length(mut self, length: NonZero<usize>) -> Self {
76 self.length = length.get();
77 self
78 }
79}
80
81impl IndicatorConfigBuilder<DcConfig> for DcConfigBuilder {
82 fn source(self, _source: crate::PriceSource) -> Self {
83 self
84 }
85
86 fn build(self) -> DcConfig {
87 DcConfig {
88 length: self.length,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
101pub struct DcValue {
102 upper: Price,
103 middle: Price,
104 lower: Price,
105}
106
107impl DcValue {
108 #[inline]
110 #[must_use]
111 pub fn upper(&self) -> Price {
112 self.upper
113 }
114
115 #[inline]
117 #[must_use]
118 pub fn middle(&self) -> Price {
119 self.middle
120 }
121
122 #[inline]
124 #[must_use]
125 pub fn lower(&self) -> Price {
126 self.lower
127 }
128}
129
130impl Display for DcValue {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 write!(
133 f,
134 "DcValue(u: {}, m: {}, l: {})",
135 self.upper, self.middle, self.lower
136 )
137 }
138}
139
140#[derive(Clone, Debug)]
177pub struct Dc {
178 config: DcConfig,
179 extremes: RollingExtremes,
180 current: Option<DcValue>,
181 last_open_time: Option<Timestamp>,
182}
183
184impl Indicator for Dc {
185 type Config = DcConfig;
186 type Output = DcValue;
187
188 fn new(config: Self::Config) -> Self {
189 Dc {
190 config,
191 extremes: RollingExtremes::new(config.length),
192 current: None,
193 last_open_time: None,
194 }
195 }
196
197 #[inline]
198 fn compute(&mut self, ohlcv: &impl crate::Ohlcv) -> Option<Self::Output> {
199 debug_assert!(
200 self.last_open_time.is_none_or(|t| t <= ohlcv.open_time()),
201 "open_time must be non-decreasing: last={}, got={}",
202 self.last_open_time.unwrap_or(0),
203 ohlcv.open_time(),
204 );
205
206 let is_next_bar = self.last_open_time.is_none_or(|t| t < ohlcv.open_time());
207
208 let (highest_high, lowest_low) = if is_next_bar {
209 self.last_open_time = Some(ohlcv.open_time());
210 self.extremes.push(ohlcv)
211 } else {
212 self.extremes.replace(ohlcv)
213 };
214
215 self.current = if self.extremes.is_ready() {
216 Some(DcValue {
217 upper: highest_high,
218 middle: (highest_high + lowest_low) * 0.5,
219 lower: lowest_low,
220 })
221 } else {
222 None
223 };
224
225 self.current
226 }
227
228 #[inline]
229 fn value(&self) -> Option<Self::Output> {
230 self.current
231 }
232}
233
234impl Display for Dc {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 write!(f, "Dc(l: {})", self.config.length)
237 }
238}
239
240#[cfg(test)]
241#[allow(clippy::float_cmp)]
242mod tests {
243 use super::*;
244 use crate::test_util::{nz, ohlc};
245
246 fn dc(length: usize) -> Dc {
247 Dc::new(DcConfig::builder().length(nz(length)).build())
248 }
249
250 fn seeded_dc() -> Dc {
254 let mut d = dc(3);
255 d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1));
256 d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2));
257 d.compute(&ohlc(13.0, 13.0, 10.0, 10.0, 3));
258 d
259 }
260
261 mod convergence {
262 use super::*;
263
264 #[test]
265 fn returns_none_during_filling() {
266 let mut d = dc(3);
267 assert!(d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1)).is_none());
268 assert!(d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2)).is_none());
269 }
270
271 #[test]
272 fn first_value_at_length_bars() {
273 let mut d = dc(3);
274 d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1));
275 d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2));
276 let val = d.compute(&ohlc(13.0, 13.0, 10.0, 10.0, 3));
277 assert!(val.is_some());
278 }
279
280 #[test]
281 fn length_one_converges_immediately() {
282 let mut d = dc(1);
283 let val = d.compute(&ohlc(10.0, 15.0, 5.0, 12.0, 1));
284 assert!(val.is_some());
285 }
286 }
287
288 mod computation {
289 use super::*;
290
291 #[test]
292 fn upper_lower_middle_math() {
293 let d = seeded_dc();
295 let val = d.value().unwrap();
296 assert_eq!(val.upper(), 14.0);
297 assert_eq!(val.lower(), 8.0);
298 assert_eq!(val.middle(), 11.0);
299 }
300
301 #[test]
302 fn sliding_window_drops_oldest() {
303 let mut d = seeded_dc();
304 let val = d.compute(&ohlc(12.0, 15.0, 11.0, 12.0, 4)).unwrap();
308 assert_eq!(val.upper(), 15.0);
309 assert_eq!(val.lower(), 9.0);
310 assert_eq!(val.middle(), 12.0);
311 }
312
313 #[test]
314 fn slides_across_many_bars() {
315 let mut d = dc(2);
316 d.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 1));
317 d.compute(&ohlc(10.0, 18.0, 8.0, 12.0, 2));
318 d.compute(&ohlc(10.0, 16.0, 10.0, 14.0, 3));
319 let val = d.compute(&ohlc(10.0, 12.0, 7.0, 10.0, 4)).unwrap();
321 assert_eq!(val.upper(), 16.0);
322 assert_eq!(val.lower(), 7.0);
323 assert_eq!(val.middle(), 11.5);
324 }
325
326 #[test]
327 fn flat_market() {
328 let mut d = dc(3);
329 for t in 1..=5 {
330 let val = d.compute(&ohlc(10.0, 10.0, 10.0, 10.0, t));
331 if let Some(v) = val {
332 assert_eq!(v.upper(), 10.0);
333 assert_eq!(v.lower(), 10.0);
334 assert_eq!(v.middle(), 10.0);
335 }
336 }
337 }
338 }
339
340 mod repaints {
341 use super::*;
342
343 #[test]
344 fn repaint_updates_value() {
345 let mut d = seeded_dc();
346 let original = d.compute(&ohlc(12.0, 16.0, 11.0, 13.0, 4)).unwrap();
347 let repainted = d.compute(&ohlc(12.0, 20.0, 11.0, 13.0, 4)).unwrap();
349 assert!(repainted.upper() > original.upper());
350 }
351
352 #[test]
353 fn multiple_repaints_match_single() {
354 let mut d = seeded_dc();
355 d.compute(&ohlc(12.0, 16.0, 11.0, 13.0, 4));
356 d.compute(&ohlc(12.0, 20.0, 6.0, 15.0, 4)); d.compute(&ohlc(12.0, 14.0, 10.0, 11.0, 4)); let final_val = d.compute(&ohlc(12.0, 15.0, 9.0, 12.0, 4)).unwrap();
359
360 let mut clean = seeded_dc();
361 let expected = clean.compute(&ohlc(12.0, 15.0, 9.0, 12.0, 4)).unwrap();
362
363 assert_eq!(final_val.upper(), expected.upper());
364 assert_eq!(final_val.lower(), expected.lower());
365 assert_eq!(final_val.middle(), expected.middle());
366 }
367
368 #[test]
369 fn repaint_then_advance_uses_repainted() {
370 let mut d = seeded_dc();
371 d.compute(&ohlc(12.0, 16.0, 11.0, 13.0, 4));
372 d.compute(&ohlc(12.0, 16.0, 7.0, 13.0, 4)); let after = d.compute(&ohlc(14.0, 17.0, 12.0, 14.0, 5)).unwrap();
374
375 let mut clean = seeded_dc();
376 clean.compute(&ohlc(12.0, 16.0, 7.0, 13.0, 4));
377 let expected = clean.compute(&ohlc(14.0, 17.0, 12.0, 14.0, 5)).unwrap();
378
379 assert_eq!(after.upper(), expected.upper());
380 assert_eq!(after.lower(), expected.lower());
381 assert_eq!(after.middle(), expected.middle());
382 }
383
384 #[test]
385 fn repaint_during_filling_has_no_effect_on_convergence() {
386 let mut d = dc(3);
387 d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1));
388 d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2));
389 d.compute(&ohlc(11.0, 16.0, 7.0, 15.0, 2)); assert!(d.value().is_none()); let val = d.compute(&ohlc(13.0, 13.0, 10.0, 10.0, 3));
392 assert!(val.is_some()); }
394 }
395
396 mod clone {
397 use super::*;
398
399 #[test]
400 fn produces_independent_state() {
401 let mut d = seeded_dc();
402 let mut cloned = d.clone();
403
404 let orig = d.compute(&ohlc(12.0, 20.0, 11.0, 16.0, 4)).unwrap();
405 let clone_val = cloned.compute(&ohlc(12.0, 13.0, 7.0, 9.0, 4)).unwrap();
406
407 assert!(
408 (orig.upper() - clone_val.upper()).abs() > 1e-10,
409 "divergent inputs should give different upper"
410 );
411 }
412 }
413
414 mod display {
415 use super::*;
416
417 #[test]
418 fn display_config() {
419 let config = DcConfig::builder().length(nz(20)).build();
420 assert_eq!(config.to_string(), "DcConfig(l: 20)");
421 }
422
423 #[test]
424 fn display_dc() {
425 let d = dc(14);
426 assert_eq!(d.to_string(), "Dc(l: 14)");
427 }
428
429 #[test]
430 fn display_value() {
431 let v = DcValue {
432 upper: 100.0,
433 middle: 75.0,
434 lower: 50.0,
435 };
436 assert_eq!(v.to_string(), "DcValue(u: 100, m: 75, l: 50)");
437 }
438 }
439
440 mod config {
441 use super::*;
442 use std::collections::HashSet;
443
444 #[test]
445 fn default_length_is_20() {
446 let config = DcConfig::builder().build();
447 assert_eq!(config.length(), 20);
448 }
449
450 #[test]
451 fn custom_length() {
452 let config = DcConfig::builder().length(nz(50)).build();
453 assert_eq!(config.length(), 50);
454 }
455
456 #[test]
457 fn convergence_equals_length() {
458 let config = DcConfig::builder().length(nz(14)).build();
459 assert_eq!(config.convergence(), 14);
460
461 let config = DcConfig::builder().build();
462 assert_eq!(config.convergence(), 20);
463 }
464
465 #[test]
466 fn source_is_always_close() {
467 let config = DcConfig::builder().source(crate::PriceSource::HL2).build();
468 assert_eq!(config.source(), crate::PriceSource::Close);
469 }
470
471 #[test]
472 fn eq_and_hash() {
473 let a = DcConfig::builder().length(nz(20)).build();
474 let b = DcConfig::builder().length(nz(20)).build();
475 let c = DcConfig::builder().length(nz(10)).build();
476 assert_eq!(a, b);
477 assert_ne!(a, c);
478
479 let mut set = HashSet::new();
480 set.insert(a);
481 assert!(set.contains(&b));
482 assert!(!set.contains(&c));
483 }
484 }
485
486 mod value_accessor {
487 use super::*;
488
489 #[test]
490 fn none_before_convergence() {
491 let d = dc(3);
492 assert_eq!(d.value(), None);
493 }
494
495 #[test]
496 fn matches_last_compute() {
497 let mut d = seeded_dc();
498 let computed = d.compute(&ohlc(12.0, 16.0, 11.0, 14.0, 4));
499 assert_eq!(d.value(), computed);
500 }
501
502 #[test]
503 fn accessors_return_correct_values() {
504 let d = seeded_dc();
505 let val = d.value().unwrap();
506 assert!(val.upper().is_finite());
507 assert!(val.lower().is_finite());
508 assert!(val.middle().is_finite());
509 assert!(val.upper() >= val.lower());
510 assert!(val.middle() >= val.lower());
511 assert!(val.middle() <= val.upper());
512 }
513 }
514
515 #[cfg(debug_assertions)]
516 mod invariants {
517 use super::*;
518
519 #[test]
520 #[should_panic(expected = "open_time must be non-decreasing")]
521 fn panics_on_decreasing_open_time() {
522 let mut d = dc(3);
523 d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 2));
524 d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 1));
525 }
526 }
527}