termichart_data/
transform.rs1use termichart_core::{Candle, Point, Range};
2
3pub fn auto_range(candles: &[Candle]) -> (Range, Range) {
11 if candles.is_empty() {
12 return (
13 Range { min: 0.0, max: 1.0 },
14 Range { min: 0.0, max: 1.0 },
15 );
16 }
17
18 let mut x_min = candles[0].time;
19 let mut x_max = candles[0].time;
20 let mut y_min = candles[0].low;
21 let mut y_max = candles[0].high;
22
23 for c in &candles[1..] {
24 if c.time < x_min {
25 x_min = c.time;
26 }
27 if c.time > x_max {
28 x_max = c.time;
29 }
30 if c.low < y_min {
31 y_min = c.low;
32 }
33 if c.high > y_max {
34 y_max = c.high;
35 }
36 }
37
38 let x_range = Range { min: x_min, max: x_max }.with_padding(0.05);
39 let y_range = Range { min: y_min, max: y_max }.with_padding(0.05);
40
41 (x_range, y_range)
42}
43
44pub fn auto_range_points(points: &[Point]) -> (Range, Range) {
49 if points.is_empty() {
50 return (
51 Range { min: 0.0, max: 1.0 },
52 Range { min: 0.0, max: 1.0 },
53 );
54 }
55
56 let mut x_min = points[0].x;
57 let mut x_max = points[0].x;
58 let mut y_min = points[0].y;
59 let mut y_max = points[0].y;
60
61 for p in &points[1..] {
62 if p.x < x_min {
63 x_min = p.x;
64 }
65 if p.x > x_max {
66 x_max = p.x;
67 }
68 if p.y < y_min {
69 y_min = p.y;
70 }
71 if p.y > y_max {
72 y_max = p.y;
73 }
74 }
75
76 let x_range = Range { min: x_min, max: x_max }.with_padding(0.05);
77 let y_range = Range { min: y_min, max: y_max }.with_padding(0.05);
78
79 (x_range, y_range)
80}
81
82fn nice_number(x: f64, round: bool) -> f64 {
88 if x == 0.0 {
89 return 0.0;
90 }
91
92 let exp = x.abs().log10().floor();
93 let frac = x.abs() / 10.0_f64.powf(exp);
94 let sign = if x < 0.0 { -1.0 } else { 1.0 };
95
96 let nice_frac = if round {
97 if frac < 1.5 {
98 1.0
99 } else if frac < 3.0 {
100 2.0
101 } else if frac < 7.0 {
102 5.0
103 } else {
104 10.0
105 }
106 } else {
107 if frac <= 1.0 {
109 1.0
110 } else if frac <= 2.0 {
111 2.0
112 } else if frac <= 5.0 {
113 5.0
114 } else {
115 10.0
116 }
117 };
118
119 sign * nice_frac * 10.0_f64.powf(exp)
120}
121
122pub fn nice_ticks(range: &Range, max_ticks: usize) -> Vec<f64> {
130 if max_ticks == 0 {
131 return Vec::new();
132 }
133
134 let span = range.span();
135 if span == 0.0 {
136 return vec![range.min];
137 }
138
139 let step = nice_number(span / max_ticks as f64, true);
140 if step == 0.0 {
141 return vec![range.min];
142 }
143
144 let start = (range.min / step).floor() * step;
145 let mut ticks = Vec::new();
146 let mut v = start;
147
148 let limit = max_ticks * 4;
150 let mut count = 0;
151
152 while v <= range.max + step * 0.5 {
153 if v >= range.min - step * 0.5 {
154 let rounded = (v * 1e12).round() / 1e12;
156 ticks.push(rounded);
157 }
158 v += step;
159 count += 1;
160 if count > limit {
161 break;
162 }
163 }
164
165 ticks
166}
167
168pub fn to_heikin_ashi(candles: &[Candle]) -> Vec<Candle> {
178 if candles.is_empty() {
179 return Vec::new();
180 }
181
182 let mut ha = Vec::with_capacity(candles.len());
183
184 let first = &candles[0];
185 let ha_close = (first.open + first.high + first.low + first.close) / 4.0;
186 let ha_open = (first.open + first.close) / 2.0;
187 let ha_high = first.high.max(ha_open).max(ha_close);
188 let ha_low = first.low.min(ha_open).min(ha_close);
189 ha.push(Candle {
190 time: first.time,
191 open: ha_open,
192 high: ha_high,
193 low: ha_low,
194 close: ha_close,
195 volume: first.volume,
196 });
197
198 for i in 1..candles.len() {
199 let c = &candles[i];
200 let prev = &ha[i - 1];
201
202 let ha_close = (c.open + c.high + c.low + c.close) / 4.0;
203 let ha_open = (prev.open + prev.close) / 2.0;
204 let ha_high = c.high.max(ha_open).max(ha_close);
205 let ha_low = c.low.min(ha_open).min(ha_close);
206
207 ha.push(Candle {
208 time: c.time,
209 open: ha_open,
210 high: ha_high,
211 low: ha_low,
212 close: ha_close,
213 volume: c.volume,
214 });
215 }
216
217 ha
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn auto_range_basic() {
226 let candles = vec![
227 Candle { time: 1.0, open: 10.0, high: 15.0, low: 8.0, close: 12.0, volume: 100.0 },
228 Candle { time: 2.0, open: 12.0, high: 20.0, low: 10.0, close: 18.0, volume: 200.0 },
229 Candle { time: 3.0, open: 18.0, high: 22.0, low: 16.0, close: 20.0, volume: 150.0 },
230 ];
231 let (xr, yr) = auto_range(&candles);
232 assert!((xr.min - 0.9).abs() < 1e-10);
234 assert!((xr.max - 3.1).abs() < 1e-10);
235 assert!((yr.min - 7.3).abs() < 1e-10);
237 assert!((yr.max - 22.7).abs() < 1e-10);
238 }
239
240 #[test]
241 fn auto_range_empty() {
242 let (xr, yr) = auto_range(&[]);
243 assert_eq!(xr.min, 0.0);
244 assert_eq!(xr.max, 1.0);
245 assert_eq!(yr.min, 0.0);
246 assert_eq!(yr.max, 1.0);
247 }
248
249 #[test]
250 fn auto_range_points_basic() {
251 let points = vec![
252 Point { x: 0.0, y: 10.0 },
253 Point { x: 100.0, y: 50.0 },
254 ];
255 let (xr, yr) = auto_range_points(&points);
256 assert!(xr.min < 0.0);
257 assert!(xr.max > 100.0);
258 assert!(yr.min < 10.0);
259 assert!(yr.max > 50.0);
260 }
261
262 #[test]
263 fn nice_ticks_basic() {
264 let range = Range { min: 0.0, max: 100.0 };
265 let ticks = nice_ticks(&range, 5);
266 assert!(!ticks.is_empty());
267 for &t in &ticks {
269 assert!(t >= -10.0 && t <= 110.0);
270 }
271 for w in ticks.windows(2) {
273 assert!(w[1] > w[0]);
274 }
275 }
276
277 #[test]
278 fn nice_ticks_zero_span() {
279 let range = Range { min: 5.0, max: 5.0 };
280 let ticks = nice_ticks(&range, 5);
281 assert_eq!(ticks, vec![5.0]);
282 }
283
284 #[test]
285 fn nice_ticks_zero_max() {
286 let range = Range { min: 0.0, max: 100.0 };
287 let ticks = nice_ticks(&range, 0);
288 assert!(ticks.is_empty());
289 }
290
291 #[test]
292 fn nice_number_rounds() {
293 assert!((nice_number(12.3, true) - 10.0).abs() < 1e-10);
294 assert!((nice_number(45.0, true) - 50.0).abs() < 1e-10);
295 assert!((nice_number(0.073, true) - 0.1).abs() < 1e-10);
296 }
297}