use serde::{Deserialize, Serialize};
use crate::candle::Candle;
#[allow(deprecated)]
use crate::technical_analysis::{calculate_indicators, TechnicalIndicators};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MultiTimeframeData {
pub base_candles: Vec<Candle>,
pub htf_candles: Vec<Candle>,
pub multiplier: u32,
}
pub fn aggregate_candles(candles: &[Candle], multiplier: u32) -> Vec<Candle> {
if multiplier == 0 || candles.is_empty() {
return Vec::new();
}
let m = multiplier as usize;
let full_groups = candles.len() / m;
let mut result = Vec::with_capacity(full_groups);
for i in 0..full_groups {
let group = &candles[i * m..(i + 1) * m];
let first = &group[0];
let last = &group[group.len() - 1];
let high = group
.iter()
.map(|c| c.high)
.fold(f64::NEG_INFINITY, f64::max);
let low = group.iter().map(|c| c.low).fold(f64::INFINITY, f64::min);
let volume: f64 = group.iter().map(|c| c.volume).sum();
result.push(Candle {
time: first.time,
open: first.open,
high,
low,
close: last.close,
volume,
});
}
result
}
#[allow(deprecated)]
pub fn compute_htf_indicators(base_candles: &[Candle], multiplier: u32) -> TechnicalIndicators {
let htf = aggregate_candles(base_candles, multiplier);
if htf.is_empty() {
return TechnicalIndicators::empty();
}
calculate_indicators(&htf)
}
pub fn build_multi_timeframe(base_candles: Vec<Candle>, multiplier: u32) -> MultiTimeframeData {
let htf_candles = aggregate_candles(&base_candles, multiplier);
MultiTimeframeData {
base_candles,
htf_candles,
multiplier,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_candle(time: u64, open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
Candle {
time,
open,
high,
low,
close,
volume,
}
}
fn sample_1h_candles() -> Vec<Candle> {
vec![
make_candle(3600 * 0, 100.0, 105.0, 98.0, 103.0, 1000.0),
make_candle(3600 * 1, 103.0, 108.0, 101.0, 106.0, 1200.0),
make_candle(3600 * 2, 106.0, 110.0, 104.0, 109.0, 800.0),
make_candle(3600 * 3, 109.0, 112.0, 107.0, 111.0, 1500.0),
make_candle(3600 * 4, 111.0, 115.0, 109.0, 113.0, 900.0),
make_candle(3600 * 5, 113.0, 116.0, 112.0, 114.0, 1100.0),
make_candle(3600 * 6, 114.0, 118.0, 113.0, 117.0, 1300.0),
make_candle(3600 * 7, 117.0, 120.0, 115.0, 119.0, 1000.0),
]
}
#[test]
fn test_aggregate_4h_from_1h() {
let candles = sample_1h_candles();
let agg = aggregate_candles(&candles, 4);
assert_eq!(agg.len(), 2);
assert_eq!(agg[0].time, 0);
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);
assert_eq!(agg[1].open, 111.0);
assert_eq!(agg[1].close, 119.0);
assert_eq!(agg[1].high, 120.0);
assert_eq!(agg[1].low, 109.0);
assert_eq!(agg[1].volume, 4300.0); }
#[test]
fn test_aggregate_2h_from_1h() {
let candles = sample_1h_candles();
let agg = aggregate_candles(&candles, 2);
assert_eq!(agg.len(), 4);
assert_eq!(agg[0].open, 100.0);
assert_eq!(agg[0].close, 106.0);
assert_eq!(agg[0].high, 108.0);
assert_eq!(agg[0].low, 98.0);
assert_eq!(agg[0].volume, 2200.0);
}
#[test]
fn test_aggregate_drops_incomplete_trailing_group() {
let candles = &sample_1h_candles()[..7]; let agg = aggregate_candles(candles, 4);
assert_eq!(agg.len(), 1);
}
#[test]
fn test_aggregate_multiplier_1_identity() {
let candles = sample_1h_candles();
let agg = aggregate_candles(&candles, 1);
assert_eq!(agg.len(), candles.len());
for (a, c) in agg.iter().zip(candles.iter()) {
assert_eq!(a.open, c.open);
assert_eq!(a.close, c.close);
assert_eq!(a.high, c.high);
assert_eq!(a.low, c.low);
assert_eq!(a.volume, c.volume);
assert_eq!(a.time, c.time);
}
}
#[test]
fn test_aggregate_empty_candles() {
let agg = aggregate_candles(&[], 4);
assert!(agg.is_empty());
}
#[test]
fn test_aggregate_multiplier_zero() {
let candles = sample_1h_candles();
let agg = aggregate_candles(&candles, 0);
assert!(agg.is_empty());
}
#[test]
fn test_aggregate_multiplier_larger_than_input() {
let candles = sample_1h_candles(); let agg = aggregate_candles(&candles, 10);
assert!(agg.is_empty());
}
#[test]
fn test_aggregate_single_candle_multiplier_1() {
let candles = vec![make_candle(100, 50.0, 55.0, 45.0, 52.0, 500.0)];
let agg = aggregate_candles(&candles, 1);
assert_eq!(agg.len(), 1);
assert_eq!(agg[0].open, 50.0);
assert_eq!(agg[0].close, 52.0);
}
#[test]
fn test_aggregate_exactly_one_group() {
let candles = &sample_1h_candles()[..4]; let agg = aggregate_candles(candles, 4);
assert_eq!(agg.len(), 1);
assert_eq!(agg[0].open, 100.0);
assert_eq!(agg[0].close, 111.0);
}
#[test]
fn test_aggregate_volume_is_sum() {
let candles = vec![
make_candle(0, 100.0, 100.0, 100.0, 100.0, 100.0),
make_candle(1, 100.0, 100.0, 100.0, 100.0, 200.0),
make_candle(2, 100.0, 100.0, 100.0, 100.0, 300.0),
];
let agg = aggregate_candles(&candles, 3);
assert_eq!(agg.len(), 1);
assert_eq!(agg[0].volume, 600.0);
}
#[test]
fn test_aggregate_high_is_max_low_is_min() {
let candles = vec![
make_candle(0, 100.0, 200.0, 50.0, 100.0, 100.0),
make_candle(1, 100.0, 150.0, 80.0, 100.0, 100.0),
make_candle(2, 100.0, 300.0, 90.0, 100.0, 100.0),
make_candle(3, 100.0, 180.0, 40.0, 100.0, 100.0),
];
let agg = aggregate_candles(&candles, 4);
assert_eq!(agg[0].high, 300.0);
assert_eq!(agg[0].low, 40.0);
}
#[test]
fn test_compute_htf_indicators_empty() {
let ind = compute_htf_indicators(&[], 4);
assert!(ind.sma_20.is_none());
assert!(ind.rsi_14.is_none());
}
#[test]
fn test_compute_htf_indicators_too_few() {
let candles = &sample_1h_candles()[..3];
let ind = compute_htf_indicators(candles, 4);
assert!(ind.sma_20.is_none());
}
#[test]
fn test_build_multi_timeframe() {
let candles = sample_1h_candles();
let mtf = build_multi_timeframe(candles.clone(), 4);
assert_eq!(mtf.base_candles.len(), 8);
assert_eq!(mtf.htf_candles.len(), 2);
assert_eq!(mtf.multiplier, 4);
}
#[test]
fn test_multi_timeframe_data_serialization() {
let mtf = build_multi_timeframe(sample_1h_candles(), 4);
let json = serde_json::to_string(&mtf).unwrap();
let parsed: MultiTimeframeData = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.multiplier, 4);
assert_eq!(parsed.base_candles.len(), 8);
assert_eq!(parsed.htf_candles.len(), 2);
}
}