#[derive(Debug, Clone, Copy)]
pub struct Bar {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub timestamp: i64,
}
impl Bar {
pub fn new(open: f64, high: f64, low: f64, close: f64, volume: f64, timestamp: i64) -> Self {
Self { open, high, low, close, volume, timestamp }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DataType {
Smooth,
CandlePatterns,
Gaps,
Divergence,
VolatilityBreakout,
VolatilityClustering,
StructuralBreaks,
Squeeze,
Calendar,
Correlated,
StrongTrend,
Ranging,
SweepReversion,
ZigZagSwings,
TickData,
}
pub fn generate_bars(data_type: DataType, count: usize, start_ts: i64) -> Vec<Bar> {
match data_type {
DataType::Smooth => generate_smooth(count, start_ts),
DataType::CandlePatterns => generate_candle_patterns(count, start_ts),
DataType::Gaps => generate_gaps(count, start_ts),
DataType::Divergence => generate_divergence(count, start_ts),
DataType::VolatilityBreakout => generate_volatility_breakout(count, start_ts),
DataType::VolatilityClustering => generate_volatility_clustering(count, start_ts),
DataType::StructuralBreaks => generate_structural_breaks(count, start_ts),
DataType::Squeeze => generate_squeeze(count, start_ts),
DataType::Calendar => generate_calendar(count, start_ts),
DataType::Correlated => generate_correlated(count, start_ts),
DataType::StrongTrend => generate_strong_trend(count, start_ts),
DataType::Ranging => generate_ranging(count, start_ts),
DataType::SweepReversion => generate_sweep_reversion(count, start_ts),
DataType::ZigZagSwings => generate_zigzag_swings(count, start_ts),
DataType::TickData => generate_tick_data(count, start_ts),
}
}
fn generate_smooth(count: usize, start_ts: i64) -> Vec<Bar> {
(0..count)
.map(|i| {
let base = 100.0 + (i as f64 * 0.1).sin() * 10.0;
let open = base;
let close = base + (i as f64 * 0.15).cos() * 1.0;
let high = open.max(close) + 0.5 + (i as f64 * 0.3).sin().abs() * 1.0;
let low = open.min(close) - 0.5 - (i as f64 * 0.2).cos().abs() * 1.0;
let volume = 1000.0 + (i as f64 * 50.0);
let timestamp = start_ts + (i as i64 * 3600);
Bar::new(open, high, low, close, volume, timestamp)
})
.collect()
}
fn generate_candle_patterns(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let volume = 1000.0 + (i as f64 * 10.0);
let bar = match i % 40 {
5 => {
let low = price - 5.0;
let open = price;
let close = price + 1.0;
let high = price + 1.0; Bar::new(open, high, low, close, volume, timestamp)
}
10 => {
let high = price + 5.0;
let open = price;
let close = price - 1.0;
let low = price - 1.0; Bar::new(open, high, low, close, volume, timestamp)
}
15 => {
let open = price;
let close = price + 0.1;
let high = price + 2.0;
let low = price - 2.0;
Bar::new(open, high, low, close, volume, timestamp)
}
19 => {
let open = price + 1.0;
let close = price; let high = price + 1.2;
let low = price - 0.2;
Bar::new(open, high, low, close, volume, timestamp)
}
20 => {
let open = price - 0.5; let close = price + 1.8; let high = close + 0.2;
let low = open - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.5, timestamp)
}
24 => {
let open = price;
let close = price + 1.0; let high = price + 1.2;
let low = price - 0.2;
Bar::new(open, high, low, close, volume, timestamp)
}
25 => {
let prev_open = price;
let prev_close = price + 1.0;
let open = prev_close + 0.5; let close = prev_open - 0.8; let high = open + 0.2;
let low = close - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.5, timestamp)
}
30 => {
let direction = if i % 80 < 40 { 1.0 } else { -1.0 };
let open = price;
let close = price + direction * 3.0;
let high = open.max(close);
let low = open.min(close);
price = close;
Bar::new(open, high, low, close, volume, timestamp)
}
26 => {
let open = price + 3.0;
let close = price;
let high = open + 0.5;
let low = close - 0.5;
Bar::new(open, high, low, close, volume, timestamp)
}
27 => {
let star_open = price - 2.0; let star_close = star_open + 0.2; let star_high = star_open + 0.5; let star_low = star_open - 0.5;
Bar::new(star_open, star_high, star_low, star_close, volume * 0.5, timestamp)
}
28 => {
let third_open = price - 1.5;
let third_close = price + 2.5; let third_high = third_close + 0.3;
let third_low = third_open - 0.3;
price = third_close;
Bar::new(third_open, third_high, third_low, third_close, volume * 1.5, timestamp)
}
29 => {
let open = price;
let close = price + 3.0;
let high = close + 0.5;
let low = open - 0.5;
Bar::new(open, high, low, close, volume, timestamp)
}
31 => {
let third_open = price + 4.5;
let third_close = price + 0.5; let third_high = third_open + 0.3;
let third_low = third_close - 0.3;
price = third_close;
Bar::new(third_open, third_high, third_low, third_close, volume * 1.5, timestamp)
}
32 => {
let open = price;
let close = price + 2.5;
let high = close + 0.2;
let low = open - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.2, timestamp)
}
33 => {
let open = price - 0.3; let close = price + 2.3; let high = close + 0.2;
let low = open - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.3, timestamp)
}
34 => {
let open = price - 0.3;
let close = price + 2.3;
let high = close + 0.2;
let low = open - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.4, timestamp)
}
35 => {
let open = price;
let close = price - 2.5;
let high = open + 0.2;
let low = close - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.2, timestamp)
}
36 => {
let open = price + 0.3; let close = price - 2.3; let high = open + 0.2;
let low = close - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.3, timestamp)
}
37 => {
let open = price + 0.3;
let close = price - 2.3;
let high = open + 0.2;
let low = close - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.4, timestamp)
}
38 => {
let open = price + 3.0;
let close = price;
let high = open + 0.3;
let low = close - 0.3;
Bar::new(open, high, low, close, volume, timestamp)
}
39 => {
let prev_mid = (price + 3.0 + price) / 2.0; let open = price - 0.5; let close = prev_mid + 0.5; let high = close + 0.2;
let low = open - 0.2;
price = close;
Bar::new(open, high, low, close, volume * 1.3, timestamp)
}
_ => {
let change = (i as f64 * 0.1).sin() * 1.5;
price += change;
let open = price - change;
let close = price;
let high = open.max(close) + 0.5;
let low = open.min(close) - 0.5;
Bar::new(open, high, low, close, volume, timestamp)
}
};
bars.push(bar);
}
bars
}
fn generate_gaps(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let volume = 1000.0 + (i as f64 * 10.0);
let bar = match i % 20 {
5 => {
let open = price;
let close = price + 1.0;
let high = price + 1.5; let low = price - 0.5;
Bar::new(open, high, low, close, volume, timestamp)
}
6 => {
let low = price + 3.0; let open = low + 1.0;
let close = low + 2.0;
let high = low + 3.0;
price = close;
Bar::new(open, high, low, close, volume * 2.0, timestamp)
}
7 => {
let prev_gap_low = price - 2.0; let high = prev_gap_low - 0.5; let open = high - 1.0;
let close = high - 0.5;
let low = high - 2.0;
price = close;
Bar::new(open, high, low, close, volume, timestamp)
}
12 => {
let open = price;
let close = price - 1.0;
let high = price + 0.5;
let low = price - 1.5; Bar::new(open, high, low, close, volume, timestamp)
}
13 => {
let high = price - 3.0; let open = high - 1.0;
let close = high - 2.0;
let low = high - 3.0;
price = close;
Bar::new(open, high, low, close, volume * 2.0, timestamp)
}
14 => {
let prev_gap_high = price + 2.0; let low = prev_gap_high + 0.5; let open = low + 0.5;
let close = low + 1.0;
let high = low + 2.0;
price = close;
Bar::new(open, high, low, close, volume, timestamp)
}
_ => {
let change = (i as f64 * 0.05).sin() * 1.0;
price += change;
let open = price - change;
let close = price;
let high = open.max(close) + 0.8;
let low = open.min(close) - 0.8;
Bar::new(open, high, low, close, volume, timestamp)
}
};
bars.push(bar);
}
bars
}
fn generate_divergence(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let phase = i / 50; let within_phase = i % 50;
let (change, vol_mult) = match phase % 4 {
0 => {
let drop = -0.5 - (within_phase as f64 * 0.02);
(drop, 1.5)
}
1 => {
let rise = 0.3 + (within_phase as f64 * 0.01);
(rise, 0.8)
}
2 => {
let drop = -0.3 - (within_phase as f64 * 0.005);
(drop, 1.2)
}
3 => {
let rise = 0.8 + (within_phase as f64 * 0.03);
(rise, 2.0)
}
_ => (0.0, 1.0),
};
price += change;
let open = price - change;
let close = price;
let volatility = 1.0 + change.abs() * 0.5;
let high = open.max(close) + volatility;
let low = open.min(close) - volatility;
let volume = 1000.0 * vol_mult;
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_volatility_breakout(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let cycle = i / 30;
let within_cycle = i % 30;
let (volatility, trend, vol_mult) = if within_cycle < 20 {
(0.5, (within_cycle as f64 * 0.02).sin() * 0.1, 0.5)
} else {
let breakout_strength = (within_cycle - 20) as f64 * 0.5;
let direction = if cycle % 2 == 0 { 1.0 } else { -1.0 };
(3.0 + breakout_strength, direction * 1.5, 3.0)
};
price += trend;
let open = price;
let close = price + trend;
let high = open.max(close) + volatility;
let low = open.min(close) - volatility;
let volume = 1000.0 * vol_mult;
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_volatility_clustering(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0_f64;
let mut current_vol: f64 = 1.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let vol_shock = if i % 40 == 0 {
3.0
} else {
0.0
};
current_vol = 0.95 * current_vol + 0.05 * 1.0 + vol_shock;
current_vol = current_vol.clamp(0.3, 5.0);
let return_val = (i as f64 * 0.1).sin() * current_vol * 0.5;
price *= 1.0 + return_val / 100.0;
let open = price / (1.0 + return_val / 100.0);
let close = price;
let range = current_vol * 2.0;
let high = open.max(close) + range * 0.6;
let low = open.min(close) - range * 0.4;
let volume = 1000.0 * (1.0 + current_vol * 0.5);
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_structural_breaks(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
let mut trend = 0.1;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
if i % 100 == 50 {
trend = -trend * 1.5; price += trend * 10.0; }
price += trend;
let noise = (i as f64 * 0.2).sin() * 0.5;
let open = price - trend;
let close = price + noise;
let high = open.max(close) + 1.0;
let low = open.min(close) - 1.0;
let volume = 1000.0;
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_squeeze(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let cycle = i / 40;
let within_cycle = i % 40;
let range = if within_cycle < 30 {
let compression = (30 - within_cycle) as f64 / 30.0;
0.5 + compression * 2.0
} else {
let expansion = (within_cycle - 30) as f64;
0.5 + expansion * 1.5
};
let trend = if within_cycle >= 30 {
let direction = if cycle % 2 == 0 { 1.0 } else { -1.0 };
direction * (within_cycle - 30) as f64 * 0.5
} else {
(within_cycle as f64 * 0.05).sin() * 0.2
};
price += trend;
let open = price - trend * 0.5;
let close = price;
let high = open.max(close) + range;
let low = open.min(close) - range;
let volume = 1000.0 * (1.0 + range * 0.3);
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_calendar(count: usize, _start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
let monday_start = 1704067200_i64;
for i in 0..count {
let timestamp = monday_start + (i as i64 * 3600);
let hours_from_start = i as i64;
let day_of_week = (hours_from_start / 24) % 7;
let hour_of_day = hours_from_start % 24;
let session_vol = if (13..=20).contains(&hour_of_day) {
2.0 } else if (7..=15).contains(&hour_of_day) {
1.5 } else if (0..=8).contains(&hour_of_day) {
1.2 } else {
0.8 };
let gap = if day_of_week == 0 && hour_of_day == 0 && i > 0 {
(i as f64 * 0.1).sin() * 3.0
} else {
0.0
};
let eom_vol = if i % 720 > 690 { 1.5 } else { 1.0 };
price += gap + (i as f64 * 0.05).sin() * session_vol * 0.5;
let volatility = session_vol * eom_vol;
let open = price - gap;
let close = price;
let high = open.max(close) + volatility;
let low = open.min(close) - volatility;
let volume = 1000.0 * session_vol;
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_correlated(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
let mut prev_change = 0.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let momentum = prev_change * 0.7; let phase = (i / 50) % 3;
let base_trend = match phase {
0 => 1.5, 1 => -1.2, _ => 0.3, };
let noise = (i as f64 * 0.2).sin() * 0.3;
let price_change = momentum + base_trend * 0.3 + noise;
prev_change = price_change;
let volume_factor = 1.0 + price_change.abs() * 1.5;
price = (price + price_change).max(30.0);
let open = price - price_change * 0.7;
let close = price;
let range = price_change.abs().max(0.5);
let high = open.max(close) + range * 0.3;
let low = open.min(close) - range * 0.3;
let volume = 500.0 + 1000.0 * volume_factor;
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_strong_trend(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0_f64;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let phase = i / 100;
let trend: f64 = if phase % 2 == 0 { 0.5 } else { -0.5 };
let pullback = if i % 20 < 5 { -trend * 0.3 } else { 0.0 };
price += trend + pullback;
let open = price - trend;
let close = price;
let (high, low) = if trend > 0.0 {
(close + 1.5, open - 0.5)
} else {
(open + 0.5, close - 1.5)
};
let volume = 1000.0 * (1.0 + trend.abs());
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_ranging(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let cycle_phase = i % 40;
let change = if (25..=31).contains(&cycle_phase) {
5.0
} else if (32..=39).contains(&cycle_phase) {
-6.0
} else {
(i as f64 * 0.15).sin() * 0.3
};
price = (price + change).clamp(50.0, 150.0);
let open = price - change * 0.4;
let close = price;
let high = open.max(close) + 0.3;
let low = open.min(close) - 0.3;
let volume = 1000.0;
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_sweep_reversion(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
let mut range_high = price + 5.0;
let mut range_low = price - 5.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let volume = 1000.0 + (i as f64 * 10.0);
let cycle = i % 60;
let bar = match cycle {
0..=39 => {
let oscillation = (cycle as f64 * 0.15).sin() * 3.0;
price = 100.0 + oscillation;
let open = price - oscillation * 0.3;
let close = price;
let high = open.max(close) + 0.5;
let low = open.min(close) - 0.5;
if high > range_high { range_high = high; }
if low < range_low { range_low = low; }
Bar::new(open, high, low, close, volume, timestamp)
}
40..=42 => {
let sweep_high = range_high + 2.0 + (cycle - 40) as f64; let close = range_low + (range_high - range_low) * 0.2; let open = sweep_high - 1.0;
let low = close - 0.5;
price = close;
Bar::new(open, sweep_high, low, close, volume * 2.0, timestamp)
}
43..=45 => {
let change = -1.5;
price += change;
let open = price - change;
let close = price;
let high = open.max(close) + 0.3;
let low = open.min(close) - 0.3;
Bar::new(open, high, low, close, volume, timestamp)
}
46..=48 => {
let sweep_low = range_low - 2.0 - (cycle - 46) as f64; let close = range_low + (range_high - range_low) * 0.8; let open = sweep_low + 1.0;
let high = close + 0.5;
price = close;
Bar::new(open, high, sweep_low, close, volume * 2.0, timestamp)
}
_ => {
let oscillation = ((cycle - 49) as f64 * 0.2).sin() * 2.0;
price = 100.0 + oscillation;
range_high = price + 5.0;
range_low = price - 5.0;
let open = price - oscillation * 0.3;
let close = price;
let high = open.max(close) + 0.5;
let low = open.min(close) - 0.5;
Bar::new(open, high, low, close, volume, timestamp)
}
};
bars.push(bar);
}
bars
}
fn generate_zigzag_swings(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars: Vec<Bar> = Vec::with_capacity(count);
for i in 0..count {
let timestamp = start_ts + (i as i64 * 3600);
let volume = 1000.0;
let cycle = i % 20;
let base = 100.0;
let close = match cycle {
0 => base,
1 => base + 2.0,
2 => base + 4.0,
3 => base + 6.0,
4 => base + 8.0,
5 => base + 10.0, 6 => base + 8.0,
7 => base + 6.0,
8 => base + 4.0,
9 => base + 2.0,
10 => base,
11 => base - 2.0,
12 => base - 4.0,
13 => base - 6.0,
14 => base - 8.0,
15 => base - 10.0, 16 => base - 8.0,
17 => base - 6.0,
18 => base - 4.0,
19 => base - 2.0,
_ => base,
};
let prev_close = if i > 0 { bars[i - 1].close } else { close };
let open = prev_close;
let high = open.max(close) + 0.5;
let low = open.min(close) - 0.5;
bars.push(Bar::new(open, high, low, close, volume, timestamp));
}
bars
}
fn generate_tick_data(count: usize, start_ts: i64) -> Vec<Bar> {
let mut bars = Vec::with_capacity(count);
let mut price = 100.0;
for i in 0..count {
let timestamp = start_ts + (i as i64 * 60); let hour = (i / 60) % 24;
let base_ticks = match hour {
9..=11 => 500.0, 12..=13 => 200.0, 14..=16 => 600.0, _ => 100.0, };
let tick_noise = 1.0 + (i as f64 * 0.7).sin() * 0.5;
let tick_count = base_ticks * tick_noise;
let buy_ratio = 0.5 + (i as f64 * 0.1).sin() * 0.2;
let imbalance = buy_ratio - 0.5; let price_change = imbalance * tick_count.sqrt() * 0.01;
price *= 1.0 + price_change;
price = price.clamp(80.0, 120.0);
let open = price / (1.0 + price_change);
let close = price;
let volatility = tick_count.sqrt() * 0.02;
let high = open.max(close) + volatility;
let low = open.min(close) - volatility;
bars.push(Bar::new(open, high, low, close, tick_count, timestamp));
}
bars
}
pub fn recommended_data_type(indicator_name: &str) -> DataType {
let name_lower = indicator_name.to_lowercase();
if name_lower == "tickvolume" || name_lower == "tick_volume" || name_lower == "tickvol" {
return DataType::TickData;
}
if name_lower.contains("hammer") || name_lower.contains("doji") ||
name_lower.contains("engulf") || name_lower.contains("harami") ||
name_lower.contains("star") || name_lower.contains("marubozu") ||
name_lower.contains("tweezer") || name_lower.contains("piercing") ||
name_lower.contains("cloud") || name_lower.contains("soldiers") ||
name_lower.contains("crows") || name_lower.contains("candle") ||
name_lower.contains("pattern")
{
return DataType::CandlePatterns;
}
if name_lower.contains("fvg") || name_lower.contains("gap") ||
name_lower.contains("liq")
{
return DataType::Gaps;
}
if name_lower.contains("div") {
return DataType::Divergence;
}
if name_lower.contains("breakout") || name_lower.contains("vb") ||
name_lower.contains("jump") || name_lower.contains("rbv")
{
return DataType::VolatilityBreakout;
}
if name_lower.contains("arch") || name_lower.contains("garch") {
return DataType::VolatilityClustering;
}
if name_lower.contains("cusum") || name_lower.contains("break") ||
name_lower.contains("bp")
{
return DataType::StructuralBreaks;
}
if name_lower.contains("squeeze") || name_lower.contains("compress") ||
name_lower.contains("rcb") || name_lower.contains("wave")
{
return DataType::Squeeze;
}
if name_lower.contains("month") || name_lower.contains("qtr") ||
name_lower.contains("calendar") || name_lower.contains("tenc") ||
name_lower.contains("som") || name_lower.contains("soq") ||
name_lower.contains("tick")
{
return DataType::Calendar;
}
if name_lower.contains("entropy") || name_lower.contains("mutual") ||
name_lower.contains("transfer") || name_lower.contains("mi") ||
name_lower.contains("te") || name_lower.contains("xmil")
{
return DataType::Correlated;
}
if name_lower.contains("sweep") {
return DataType::SweepReversion;
}
if name_lower.contains("zigzag") {
return DataType::ZigZagSwings;
}
if name_lower.contains("tickvol") || name_lower.contains("tick_vol") {
return DataType::TickData;
}
if name_lower.contains("cipher") || name_lower.contains("mrf") ||
name_lower.contains("fractal") || name_lower.contains("ichimoku") ||
name_lower.contains("bos") || name_lower.contains("avwap")
{
return DataType::StrongTrend;
}
if name_lower.contains("logic") || name_lower.contains("thresh") ||
name_lower.contains("hyst")
{
return DataType::Ranging;
}
if name_lower.contains("stft") || name_lower.contains("spectral") ||
name_lower.contains("fft")
{
return DataType::VolatilityClustering;
}
DataType::Smooth
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_all_types() {
let types = [
DataType::Smooth,
DataType::CandlePatterns,
DataType::Gaps,
DataType::Divergence,
DataType::VolatilityBreakout,
DataType::VolatilityClustering,
DataType::StructuralBreaks,
DataType::Squeeze,
DataType::Calendar,
DataType::Correlated,
DataType::StrongTrend,
DataType::Ranging,
DataType::SweepReversion,
DataType::ZigZagSwings,
DataType::TickData,
];
for data_type in types {
let bars = generate_bars(data_type, 100, 1704067200);
assert_eq!(bars.len(), 100, "{:?} should generate 100 bars", data_type);
for (i, bar) in bars.iter().enumerate() {
assert!(bar.high >= bar.low, "{:?} bar {} high >= low", data_type, i);
assert!(bar.high >= bar.open, "{:?} bar {} high >= open", data_type, i);
assert!(bar.high >= bar.close, "{:?} bar {} high >= close", data_type, i);
assert!(bar.low <= bar.open, "{:?} bar {} low <= open", data_type, i);
assert!(bar.low <= bar.close, "{:?} bar {} low <= close", data_type, i);
assert!(bar.volume > 0.0, "{:?} bar {} volume > 0", data_type, i);
}
}
}
#[test]
fn test_candle_patterns_has_patterns() {
let bars = generate_bars(DataType::CandlePatterns, 100, 1704067200);
let has_hammer = bars.iter().any(|b| {
let body = (b.close - b.open).abs();
let lower_shadow = b.open.min(b.close) - b.low;
lower_shadow > body * 2.0
});
assert!(has_hammer, "Should have hammer-like patterns");
let has_doji = bars.iter().any(|b| {
let body = (b.close - b.open).abs();
let range = b.high - b.low;
body < range * 0.1
});
assert!(has_doji, "Should have doji-like patterns");
}
#[test]
fn test_gaps_has_gaps() {
let bars = generate_bars(DataType::Gaps, 100, 1704067200);
let mut gap_count = 0;
for i in 1..bars.len() {
let prev = &bars[i - 1];
let curr = &bars[i];
if curr.low > prev.high {
gap_count += 1;
}
if curr.high < prev.low {
gap_count += 1;
}
}
assert!(gap_count >= 3, "Should have at least 3 gaps, found {}", gap_count);
}
#[test]
fn test_recommended_data_type() {
assert_eq!(recommended_data_type("Hammer"), DataType::CandlePatterns);
assert_eq!(recommended_data_type("FvgDetector"), DataType::Gaps);
assert_eq!(recommended_data_type("RsiDiv"), DataType::Divergence);
assert_eq!(recommended_data_type("VolatilityBreakout"), DataType::VolatilityBreakout);
assert_eq!(recommended_data_type("ArchLm"), DataType::VolatilityClustering);
assert_eq!(recommended_data_type("BpCusum"), DataType::StructuralBreaks);
assert_eq!(recommended_data_type("Rcb"), DataType::Squeeze);
assert_eq!(recommended_data_type("MonthTurn"), DataType::Calendar);
assert_eq!(recommended_data_type("MutualInformation"), DataType::Correlated);
assert_eq!(recommended_data_type("Sma"), DataType::Smooth);
assert_eq!(recommended_data_type("SweepRev"), DataType::SweepReversion);
assert_eq!(recommended_data_type("TickVolume"), DataType::TickData);
}
#[test]
fn test_sweep_reversion_has_sweeps() {
let bars = generate_bars(DataType::SweepReversion, 200, 1704067200);
let mut found_sweep = false;
for i in 41..bars.len() {
let bar = &bars[i];
let range = bar.high - bar.low;
let close_position = (bar.close - bar.low) / range;
if range > 5.0 && close_position < 0.3 {
found_sweep = true;
break;
}
}
assert!(found_sweep, "Should have sweep patterns");
}
#[test]
fn test_zigzag_has_swings() {
let bars = generate_bars(DataType::ZigZagSwings, 100, 1704067200);
let mut swing_highs = 0;
let mut swing_lows = 0;
for i in 20..bars.len() - 20 {
if (0..20).all(|j| bars[i].high >= bars[i - j - 1].high) &&
(0..20).all(|j| bars[i].high >= bars[i + j + 1].high)
{
swing_highs += 1;
}
if (0..20).all(|j| bars[i].low <= bars[i - j - 1].low) &&
(0..20).all(|j| bars[i].low <= bars[i + j + 1].low)
{
swing_lows += 1;
}
}
assert!(swing_highs >= 1 || swing_lows >= 1, "Should have swing points");
}
#[test]
fn test_tick_data_has_varying_volume() {
let bars = generate_bars(DataType::TickData, 1440, 1704067200);
let volumes: Vec<f64> = bars.iter().map(|b| b.volume).collect();
let max_vol = volumes.iter().cloned().fold(0.0, f64::max);
let min_vol = volumes.iter().cloned().fold(f64::MAX, f64::min);
assert!(max_vol / min_vol > 3.0, "Volume should vary by at least 3x across sessions");
}
}