1use std::fmt::Display;
4
5use crate::{Error, IndexEntry, IndexEntryLike};
6
7#[derive(Debug, Clone)]
9pub struct Candlestick {
10    pub at: u64,
11    pub open: f64,
12    pub high: f64,
13    pub low: f64,
14    pub close: f64,
15    pub volume: f64,
16}
17
18impl Display for Candlestick {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(
21            f,
22            "Candlestick(at={} o={} h={} l={} c={} v={})",
23            self.at, self.open, self.high, self.low, self.close, self.volume
24        )
25    }
26}
27
28impl IndexEntryLike for Candlestick {
29    fn get_at(&self) -> u64 {
30        self.at
31    }
32
33    fn get_value(&self) -> f64 {
34        self.close
35    }
36}
37
38impl Candlestick {
39    pub fn new(at: u64, open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candlestick {
41        Candlestick {
42            at,
43            open,
44            high,
45            low,
46            close,
47            volume,
48        }
49    }
50
51    pub(crate) fn validate_list(xs: &[Self]) -> Result<(), Error> {
52        for x in xs {
53            IndexEntry::validate_field(x.at, x.open, "open")?;
54            IndexEntry::validate_field(x.at, x.high, "high")?;
55            IndexEntry::validate_field(x.at, x.low, "low")?;
56            IndexEntry::validate_field(x.at, x.close, "close")?;
57            IndexEntry::validate_field(x.at, x.volume, "volume")?;
58        }
59        Ok(())
60    }
61
62    pub fn to_volume_entry(&self) -> IndexEntry {
64        IndexEntry {
65            at: self.at,
66            value: self.volume,
67        }
68    }
69
70    pub fn typical_price(&self) -> f64 {
72        (self.high + self.low + self.close) / 3.0
73    }
74
75    pub fn to_typical_price_entry(&self) -> IndexEntry {
77        IndexEntry {
78            at: self.at,
79            value: self.typical_price(),
80        }
81    }
82
83    pub fn is_bullish(&self) -> bool {
85        self.open < self.close
86    }
87
88    pub fn is_bearish(&self) -> bool {
90        self.open > self.close
91    }
92
93    pub fn body_size(&self) -> f64 {
95        (self.open - self.close).abs()
96    }
97
98    pub fn body_high(&self) -> f64 {
100        self.open.max(self.close)
101    }
102
103    pub fn body_low(&self) -> f64 {
105        self.open.min(self.close)
106    }
107
108    pub fn upper_shadow_size(&self) -> f64 {
110        self.high - self.open.max(self.close)
111    }
112
113    pub fn lower_shadow_size(&self) -> f64 {
115        self.open.min(self.close) - self.low
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::f64::{INFINITY, NAN};
122
123    use crate::{Candlestick, Error, IndexEntryLike};
124
125    #[test]
126    fn test_candlestick_to_volume_entry() {
127        let c1 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 9.0, 123.0);
128        assert_eq!(1001, c1.get_at());
129        assert_eq!(9.0, c1.get_value());
130
131        let got = c1.to_volume_entry();
132        assert_eq!(1001, got.get_at());
133        assert_eq!(123.0, got.get_value());
134    }
135
136    #[test]
137    fn test_candlestick_typical_price() {
138        let h = 20.0;
139        let l = 5.0;
140        let c = 9.0;
141        let c1 = super::Candlestick::new(1001, 10.0, h, l, c, 123.0);
142        assert_eq!((h + l + c) / 3.0, c1.typical_price());
143    }
144
145    #[test]
146    fn test_candlestick_to_typical_price_entry() {
147        let h = 20.0;
148        let l = 5.0;
149        let c = 9.0;
150        let c1 = super::Candlestick::new(1001, 10.0, h, l, c, 123.0);
151        assert_eq!(1001, c1.get_at());
152        assert_eq!(c, c1.get_value());
153
154        let got = c1.to_typical_price_entry();
155        assert_eq!(1001, got.get_at());
156        assert_eq!((h + l + c) / 3.0, got.get_value());
157    }
158
159    #[test]
160    fn test_candlestick_is_bullish_or_bearish() {
161        let c1 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 15.0, 123.0);
162        assert!(c1.is_bullish());
163        assert!(!c1.is_bearish());
164
165        let c2 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 5.0, 123.0);
166        assert!(!c2.is_bullish());
167        assert!(c2.is_bearish());
168
169        let c3 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 10.0, 123.0);
170        assert!(!c3.is_bullish());
171        assert!(!c3.is_bearish());
172    }
173
174    #[test]
175    fn test_candlestick_body() {
176        let c1 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 15.0, 123.0);
177        assert_eq!(5.0, c1.body_size());
178        assert_eq!(15.0, c1.body_high());
179        assert_eq!(10.0, c1.body_low());
180
181        let c2 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 6.0, 123.0);
182        assert_eq!(4.0, c2.body_size());
183        assert_eq!(10.0, c2.body_high());
184        assert_eq!(6.0, c2.body_low());
185
186        let c3 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 10.0, 123.0);
187        assert_eq!(0.0, c3.body_size());
188        assert_eq!(10.0, c3.body_high());
189        assert_eq!(10.0, c3.body_low());
190    }
191
192    #[test]
193    fn test_candlestick_shadow() {
194        let c1 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 15.0, 123.0);
195        assert_eq!(5.0, c1.upper_shadow_size());
196        assert_eq!(5.0, c1.lower_shadow_size());
197
198        let c2 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 6.0, 123.0);
199        assert_eq!(10.0, c2.upper_shadow_size());
200        assert_eq!(1.0, c2.lower_shadow_size());
201
202        let c3 = super::Candlestick::new(1001, 10.0, 20.0, 5.0, 10.0, 123.0);
203        assert_eq!(10.0, c3.upper_shadow_size());
204        assert_eq!(5.0, c3.lower_shadow_size());
205
206        let c4 = super::Candlestick::new(1001, 10.0, 10.0, 5.0, 10.0, 123.0);
207        assert_eq!(0.0, c4.upper_shadow_size());
208        assert_eq!(5.0, c4.lower_shadow_size());
209
210        let c5 = super::Candlestick::new(1001, 10.0, 20.0, 10.0, 10.0, 123.0);
211        assert_eq!(10.0, c5.upper_shadow_size());
212        assert_eq!(0.0, c5.lower_shadow_size());
213    }
214
215    #[test]
216    fn test_validate_list() {
217        let res = Candlestick::validate_list(&vec![
219            Candlestick::new(1719400001, 100.0, 130.0, 90.0, 110.0, 1000.0),
220            Candlestick::new(1719400002, 110.0, 140.0, 100.0, 130.0, 1000.0),
221            Candlestick::new(1719400003, 130.0, 135.0, 120.0, 120.0, 1000.0),
222            Candlestick::new(1719400004, 120.0, 130.0, 80.0, 95.0, 1000.0),
223            Candlestick::new(1719400005, 90.0, 100.0, 70.0, 82.0, 1000.0),
224        ]);
225        assert!(res.is_ok());
226
227        let res = Candlestick::validate_list(&vec![
229            Candlestick::new(1719400001, 100.0, 130.0, 90.0, 110.0, 1000.0),
230            Candlestick::new(1719400002, 110.0, 140.0, 100.0, 130.0, 1000.0),
231            Candlestick::new(1719400003, 130.0, NAN, 120.0, 120.0, 1000.0),
232            Candlestick::new(1719400004, 120.0, 130.0, 80.0, 95.0, 1000.0),
233            Candlestick::new(1719400005, 90.0, 100.0, 70.0, 82.0, 1000.0),
234        ]);
235        assert!(
236            matches!(res, Err(Error::ContainsNaN { at: 1719400003, field }) if field == "high")
237        );
238
239        let res = Candlestick::validate_list(&vec![
241            Candlestick::new(1719400001, 100.0, 130.0, 90.0, 110.0, 1000.0),
242            Candlestick::new(1719400002, 110.0, 140.0, 100.0, 130.0, 1000.0),
243            Candlestick::new(1719400003, 130.0, 135.0, 120.0, 120.0, 1000.0),
244            Candlestick::new(1719400004, 120.0, 130.0, 80.0, 95.0, INFINITY),
245            Candlestick::new(1719400005, 90.0, 100.0, 70.0, 82.0, 1000.0),
246        ]);
247        assert!(
248            matches!(res, Err(Error::ContainsInfinite { at: 1719400004, field }) if field == "volume")
249        );
250    }
251}