1use serde::{Deserialize, Serialize};
2
3use crate::candle::Candle;
4#[allow(deprecated)]
5use crate::technical_analysis::{calculate_indicators, TechnicalIndicators};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct MultiTimeframeData {
16 pub base_candles: Vec<Candle>,
18 pub htf_candles: Vec<Candle>,
20 pub multiplier: u32,
22}
23
24pub fn aggregate_candles(candles: &[Candle], multiplier: u32) -> Vec<Candle> {
39 if multiplier == 0 || candles.is_empty() {
40 return Vec::new();
41 }
42 let m = multiplier as usize;
43 let full_groups = candles.len() / m;
44 let mut result = Vec::with_capacity(full_groups);
45
46 for i in 0..full_groups {
47 let group = &candles[i * m..(i + 1) * m];
48 let first = &group[0];
49 let last = &group[group.len() - 1];
50
51 let high = group
52 .iter()
53 .map(|c| c.high)
54 .fold(f64::NEG_INFINITY, f64::max);
55 let low = group.iter().map(|c| c.low).fold(f64::INFINITY, f64::min);
56 let volume: f64 = group.iter().map(|c| c.volume).sum();
57
58 result.push(Candle {
59 time: first.time,
60 open: first.open,
61 high,
62 low,
63 close: last.close,
64 volume,
65 });
66 }
67
68 result
69}
70
71#[allow(deprecated)]
75pub fn compute_htf_indicators(base_candles: &[Candle], multiplier: u32) -> TechnicalIndicators {
76 let htf = aggregate_candles(base_candles, multiplier);
77 if htf.is_empty() {
78 return TechnicalIndicators::empty();
79 }
80 calculate_indicators(&htf)
81}
82
83pub fn build_multi_timeframe(base_candles: Vec<Candle>, multiplier: u32) -> MultiTimeframeData {
85 let htf_candles = aggregate_candles(&base_candles, multiplier);
86 MultiTimeframeData {
87 base_candles,
88 htf_candles,
89 multiplier,
90 }
91}
92
93#[cfg(test)]
98mod tests {
99 use super::*;
100
101 fn make_candle(time: u64, open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
102 Candle {
103 time,
104 open,
105 high,
106 low,
107 close,
108 volume,
109 }
110 }
111
112 fn sample_1h_candles() -> Vec<Candle> {
113 vec![
114 make_candle(3600 * 0, 100.0, 105.0, 98.0, 103.0, 1000.0),
115 make_candle(3600 * 1, 103.0, 108.0, 101.0, 106.0, 1200.0),
116 make_candle(3600 * 2, 106.0, 110.0, 104.0, 109.0, 800.0),
117 make_candle(3600 * 3, 109.0, 112.0, 107.0, 111.0, 1500.0),
118 make_candle(3600 * 4, 111.0, 115.0, 109.0, 113.0, 900.0),
119 make_candle(3600 * 5, 113.0, 116.0, 112.0, 114.0, 1100.0),
120 make_candle(3600 * 6, 114.0, 118.0, 113.0, 117.0, 1300.0),
121 make_candle(3600 * 7, 117.0, 120.0, 115.0, 119.0, 1000.0),
122 ]
123 }
124
125 #[test]
128 fn test_aggregate_4h_from_1h() {
129 let candles = sample_1h_candles();
130 let agg = aggregate_candles(&candles, 4);
131
132 assert_eq!(agg.len(), 2);
133
134 assert_eq!(agg[0].time, 0);
136 assert_eq!(agg[0].open, 100.0); assert_eq!(agg[0].close, 111.0); assert_eq!(agg[0].high, 112.0); assert_eq!(agg[0].low, 98.0); assert_eq!(agg[0].volume, 4500.0); assert_eq!(agg[1].time, 3600 * 4);
144 assert_eq!(agg[1].open, 111.0);
145 assert_eq!(agg[1].close, 119.0);
146 assert_eq!(agg[1].high, 120.0);
147 assert_eq!(agg[1].low, 109.0);
148 assert_eq!(agg[1].volume, 4300.0); }
150
151 #[test]
152 fn test_aggregate_2h_from_1h() {
153 let candles = sample_1h_candles();
154 let agg = aggregate_candles(&candles, 2);
155
156 assert_eq!(agg.len(), 4);
157
158 assert_eq!(agg[0].open, 100.0);
160 assert_eq!(agg[0].close, 106.0);
161 assert_eq!(agg[0].high, 108.0);
162 assert_eq!(agg[0].low, 98.0);
163 assert_eq!(agg[0].volume, 2200.0);
164 }
165
166 #[test]
167 fn test_aggregate_drops_incomplete_trailing_group() {
168 let candles = &sample_1h_candles()[..7]; let agg = aggregate_candles(candles, 4);
170 assert_eq!(agg.len(), 1);
171 }
172
173 #[test]
174 fn test_aggregate_multiplier_1_identity() {
175 let candles = sample_1h_candles();
176 let agg = aggregate_candles(&candles, 1);
177 assert_eq!(agg.len(), candles.len());
178 for (a, c) in agg.iter().zip(candles.iter()) {
179 assert_eq!(a.open, c.open);
180 assert_eq!(a.close, c.close);
181 assert_eq!(a.high, c.high);
182 assert_eq!(a.low, c.low);
183 assert_eq!(a.volume, c.volume);
184 assert_eq!(a.time, c.time);
185 }
186 }
187
188 #[test]
191 fn test_aggregate_empty_candles() {
192 let agg = aggregate_candles(&[], 4);
193 assert!(agg.is_empty());
194 }
195
196 #[test]
197 fn test_aggregate_multiplier_zero() {
198 let candles = sample_1h_candles();
199 let agg = aggregate_candles(&candles, 0);
200 assert!(agg.is_empty());
201 }
202
203 #[test]
204 fn test_aggregate_multiplier_larger_than_input() {
205 let candles = sample_1h_candles(); let agg = aggregate_candles(&candles, 10);
207 assert!(agg.is_empty());
208 }
209
210 #[test]
211 fn test_aggregate_single_candle_multiplier_1() {
212 let candles = vec![make_candle(100, 50.0, 55.0, 45.0, 52.0, 500.0)];
213 let agg = aggregate_candles(&candles, 1);
214 assert_eq!(agg.len(), 1);
215 assert_eq!(agg[0].open, 50.0);
216 assert_eq!(agg[0].close, 52.0);
217 }
218
219 #[test]
220 fn test_aggregate_exactly_one_group() {
221 let candles = &sample_1h_candles()[..4]; let agg = aggregate_candles(candles, 4);
223 assert_eq!(agg.len(), 1);
224 assert_eq!(agg[0].open, 100.0);
225 assert_eq!(agg[0].close, 111.0);
226 }
227
228 #[test]
231 fn test_aggregate_volume_is_sum() {
232 let candles = vec![
233 make_candle(0, 100.0, 100.0, 100.0, 100.0, 100.0),
234 make_candle(1, 100.0, 100.0, 100.0, 100.0, 200.0),
235 make_candle(2, 100.0, 100.0, 100.0, 100.0, 300.0),
236 ];
237 let agg = aggregate_candles(&candles, 3);
238 assert_eq!(agg.len(), 1);
239 assert_eq!(agg[0].volume, 600.0);
240 }
241
242 #[test]
245 fn test_aggregate_high_is_max_low_is_min() {
246 let candles = vec![
247 make_candle(0, 100.0, 200.0, 50.0, 100.0, 100.0),
248 make_candle(1, 100.0, 150.0, 80.0, 100.0, 100.0),
249 make_candle(2, 100.0, 300.0, 90.0, 100.0, 100.0),
250 make_candle(3, 100.0, 180.0, 40.0, 100.0, 100.0),
251 ];
252 let agg = aggregate_candles(&candles, 4);
253 assert_eq!(agg[0].high, 300.0);
254 assert_eq!(agg[0].low, 40.0);
255 }
256
257 #[test]
260 fn test_compute_htf_indicators_empty() {
261 let ind = compute_htf_indicators(&[], 4);
262 assert!(ind.sma_20.is_none());
263 assert!(ind.rsi_14.is_none());
264 }
265
266 #[test]
267 fn test_compute_htf_indicators_too_few() {
268 let candles = &sample_1h_candles()[..3];
270 let ind = compute_htf_indicators(candles, 4);
271 assert!(ind.sma_20.is_none());
272 }
273
274 #[test]
277 fn test_build_multi_timeframe() {
278 let candles = sample_1h_candles();
279 let mtf = build_multi_timeframe(candles.clone(), 4);
280 assert_eq!(mtf.base_candles.len(), 8);
281 assert_eq!(mtf.htf_candles.len(), 2);
282 assert_eq!(mtf.multiplier, 4);
283 }
284
285 #[test]
288 fn test_multi_timeframe_data_serialization() {
289 let mtf = build_multi_timeframe(sample_1h_candles(), 4);
290 let json = serde_json::to_string(&mtf).unwrap();
291 let parsed: MultiTimeframeData = serde_json::from_str(&json).unwrap();
292 assert_eq!(parsed.multiplier, 4);
293 assert_eq!(parsed.base_candles.len(), 8);
294 assert_eq!(parsed.htf_candles.len(), 2);
295 }
296}