libtectonic/postprocessing/candle/
candlestick_graph.rs

1use super::{TickBars, Candle};
2
3const SYMBOL_STICK: &str = "│";
4const SYMBOL_CANDLE: &str = "┃";
5const SYMBOL_HALF_TOP: &str = "╽";
6const SYMBOL_HALF_BOTTOM: &str = "╿";
7const SYMBOL_HALF_CANDLE_TOP: &str = "╻";
8const SYMBOL_HALF_CANDLE_BOTTOM: &str = "╹";
9const SYMBOL_HALF_STICK_TOP: &str = "╷";
10const SYMBOL_HALF_STICK_BOTTOM: &str = "╵";
11const SYMBOL_NOTHING: &str = " ";
12
13/// plot candle stick graph in terminal
14pub struct CandleStickGraph {
15    height: u32,
16    data: TickBars,
17    global_min: f32,
18    global_max: f32,
19}
20
21impl CandleStickGraph {
22    /// create a new graph
23    pub fn new(height: u32, data: TickBars) -> Self {
24        let global_min = data.get_candles()
25            .map(|candle| candle.low)
26            .min_by(|a, b| a.partial_cmp(b).unwrap())
27            .unwrap();
28        let global_max = data.get_candles()
29            .map(|candle| candle.high)
30            .max_by(|a, b| a.partial_cmp(b).unwrap())
31            .unwrap();
32
33        CandleStickGraph {
34            height,
35            data,
36            global_min,
37            global_max,
38        }
39    }
40
41    /// render the graph to string
42    pub fn draw(&self) -> String {
43        let mut ret = String::new();
44
45        for y in (0..self.height).rev() {
46            if y % 4 == 0 {
47                ret += &format!("{:8.8} ",
48                    self.global_min +
49                    (y as f32 * (self.global_max - self.global_min)
50                        / self.height as f32))
51            } else {
52                ret += "           "
53            }
54
55            for c in self.data.get_candles() {
56                ret += &self.render_candle_at(c, y);
57            }
58            ret += "\n"
59        }
60
61        ret
62    }
63
64    fn to_height_units(&self, x: f32) -> f32 {
65        (x - self.global_min) / (self.global_max - self.global_min)
66            * self.height as f32
67    }
68
69
70    fn render_candle_at(&self, candle: &Candle, height_unit: u32) -> String {
71        let height_unit = height_unit as f32;
72
73        let ts = self.to_height_units(candle.high);
74        let tc = self.to_height_units(candle.open.max(candle.close));
75
76        let bs = self.to_height_units(candle.low);
77        let bc = self.to_height_units(candle.open.min(candle.close));
78
79        if f32::ceil(ts) >= height_unit && height_unit >= f32::floor(tc) {
80            if tc - height_unit > 0.75 {
81                return SYMBOL_CANDLE.to_owned()
82            } else if (tc - height_unit) > 0.25 {
83                if (ts - height_unit) > 0.75 {
84                    return SYMBOL_HALF_TOP.to_owned()
85                } else {
86                    return SYMBOL_HALF_CANDLE_TOP.to_owned()
87                }
88            } else {
89                if (ts - height_unit) > 0.75 {
90                    return SYMBOL_STICK.into()
91                } else if  (ts - height_unit) > 0.25 {
92                    return SYMBOL_HALF_STICK_TOP.into()
93                } else {
94                    return SYMBOL_NOTHING.into()
95                }
96            }
97        } else if f32::floor(tc) >= height_unit && height_unit >= f32::ceil(bc) {
98            return SYMBOL_CANDLE.to_owned()
99        } else if f32::ceil(bc) >= height_unit && height_unit >= f32::floor(bs) {
100            if (bc - height_unit) < 0.25 {
101                return SYMBOL_CANDLE.to_owned()
102            } else if (bc - height_unit) < 0.75 {
103                if (bs - height_unit) < 0.25 {
104                    return SYMBOL_HALF_BOTTOM.to_owned()
105                } else {
106                    return SYMBOL_HALF_CANDLE_BOTTOM.to_owned()
107                }
108            } else {
109                if (bs - height_unit) < 0.25 {
110                    return SYMBOL_STICK.into()
111                } else if (bs - height_unit) < 0.75 {
112                    return SYMBOL_HALF_STICK_BOTTOM.into()
113                } else {
114                    return SYMBOL_NOTHING.into()
115                }
116            }
117        } else {
118            return SYMBOL_NOTHING.into()
119        }
120    }
121}
122
123
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::dtf;
129
130    #[test]
131    fn should_print_candlestick_graph_ok() {
132        static HOUR : u64 = 60 * 60 * 1000 - 1000;
133        static MINUTE : u64 = 60 * 1000;
134
135        let fname: &str = "../../test/test-data/bt_btceth.dtf";
136        let meta = dtf::file_format::read_meta(fname).unwrap();
137
138        let min_ts = meta.min_ts + HOUR;
139        let y_ts = 10 * MINUTE;
140        let max_ts = min_ts + HOUR + y_ts;
141
142        let ups = dtf::file_format::get_range_in_file(fname, min_ts, max_ts).unwrap();
143        let mut candles = TickBars::from(ups.as_slice());
144        candles.insert_continuation_candles();
145        let graph = CandleStickGraph::new(21, candles);
146        let plot = graph.draw();
147        assert_eq!(plot.replace(" ", "").as_str(), "".to_owned()+
148"0.04068785 ╽╽
149150           │                    ╻
151           │ ╷    ╻╷╷           ┃
1520.04055928 │ │    ┃││    ╻    │ ┃╹╻
153           │ │    ┃││╻ ╻ ┃    │ ┃  ╷
154           ╵ ││ ┃ ┃││┃   ┃ ││╻│ ┃  │   ╻  ╻
155                         ┃ ││┃│ ┃  │   ┃  │┃
1560.04043070            ┃  ┃ ││┃│ ┃  │   ┃  │┃ ┃┃│
157                         ┃ ││┃│ ┃  │   ┃  │┃ ┃┃│  ┃
158                         ┃ │╽┃│ ┃  │   ┃  │┃ │┃┃╿╻╹╷ ╻│ ╻╷╻╷
159                         ┃ │┃┃│ ┃  │   ┃  │┃ │┃┃│┃ │ ││ │││┃ ┃    ╻  ╻╻╻
1600.04030213              ┃ ┃┃       ┃┃┃┃│ ╻│┃ │┃┃│┃ │ ││ │││┃ │    ╹╻╻  ┃
161                                       ╵╻╹╵╿ │┃┃│┃ │ │ ┃ ┃   │┃    ┃┃  │ ┃┃┃ │┃
162                                           ╵╻╵╹╹╵╹ ╽╻╵         ┃┃┃ ┃┃  │┃┃┃┃ │┃
163                                                               │┃┃ ┃┃  │┃┃┃┃ │┃
1640.04017356                                                     │┃┃ ┃┃  │┃┃┃┃ │┃
165                                                               │┃┃ ┃┃  │┃┃┃┃ │┃
166                                                               │┃┃ ┃┃  │┃┃┃┃ │┃
167                                                               │┃┃ ┃┃   ┃┃┃┃ ╽┃
1680.04004499                                                     │╹╹ ╹      │
169".replace(" ", "").as_str());
170    }
171}