libtectonic/postprocessing/candle/
candlestick_graph.rs1use 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
13pub struct CandleStickGraph {
15 height: u32,
16 data: TickBars,
17 global_min: f32,
18 global_max: f32,
19}
20
21impl CandleStickGraph {
22 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 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 ╽╽
149 │
150 │ ╻
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}