use rangebar::{AggTrade, FixedPoint, RangeBar, RangeBarProcessor};
#[cfg(feature = "providers")]
use rangebar::{get_tier1_symbols, is_tier1_symbol};
use rangebar_core::test_data_loader::load_btcusdt_test_data;
#[test]
fn test_range_bar_processing_integration() {
let mut processor = RangeBarProcessor::new(250).expect("Failed to create processor");
let trades = load_btcusdt_test_data().expect("Failed to load BTCUSDT test data");
let range_bars = processor
.process_agg_trade_records(&trades)
.expect("Failed to process AggTrade records");
assert!(
!range_bars.is_empty(),
"Should produce at least one range bar from real data"
);
println!(
"Real data integration test: {} trades → {} range bars (threshold=0.25%)",
trades.len(),
range_bars.len()
);
for (i, bar) in range_bars.iter().enumerate() {
assert!(
bar.high >= bar.open,
"Bar {}: High ({}) should be >= open ({})",
i,
bar.high,
bar.open
);
assert!(
bar.high >= bar.close,
"Bar {}: High ({}) should be >= close ({})",
i,
bar.high,
bar.close
);
assert!(
bar.low <= bar.open,
"Bar {}: Low ({}) should be <= open ({})",
i,
bar.low,
bar.open
);
assert!(
bar.low <= bar.close,
"Bar {}: Low ({}) should be <= close ({})",
i,
bar.low,
bar.close
);
assert!(
bar.volume > FixedPoint(0),
"Bar {}: Volume should be positive",
i
);
assert!(
bar.open_time <= bar.close_time,
"Bar {}: Open time should be <= close time",
i
);
}
}
#[cfg(feature = "providers")]
#[test]
fn test_tier1_symbol_integration() {
let tier1_symbols = get_tier1_symbols();
assert!(!tier1_symbols.is_empty(), "Should have Tier-1 symbols");
assert!(
tier1_symbols.len() >= 18,
"Should have at least 18 Tier-1 symbols"
);
assert!(is_tier1_symbol("BTC"), "BTC should be Tier-1");
assert!(is_tier1_symbol("ETH"), "ETH should be Tier-1");
assert!(is_tier1_symbol("SOL"), "SOL should be Tier-1");
assert!(!is_tier1_symbol("SHIB"), "SHIB should not be Tier-1");
assert!(!is_tier1_symbol("PEPE"), "PEPE should not be Tier-1");
}
#[test]
fn test_zero_duration_bars_are_valid() {
let mut processor = RangeBarProcessor::new(100).expect("Failed to create processor");
let same_timestamp = 1609459200000;
let base_price = FixedPoint::from_str("50000.00000000").unwrap();
let breach_price = FixedPoint::from_str("50100.00000000").unwrap();
let trades = vec![
AggTrade {
agg_trade_id: 1,
price: base_price,
volume: FixedPoint::from_str("1.0").unwrap(),
first_trade_id: 1,
last_trade_id: 1,
timestamp: same_timestamp,
is_buyer_maker: false,
is_best_match: None,
},
AggTrade {
agg_trade_id: 2,
price: breach_price,
volume: FixedPoint::from_str("1.0").unwrap(),
first_trade_id: 2,
last_trade_id: 2,
timestamp: same_timestamp, is_buyer_maker: false,
is_best_match: None,
},
];
let range_bars = processor
.process_agg_trade_records(&trades)
.expect("Failed to process trades");
assert_eq!(range_bars.len(), 1, "Should produce exactly one range bar");
let bar = &range_bars[0];
assert_eq!(bar.open_time, bar.close_time, "Zero-duration bar is valid");
assert_eq!(bar.open, base_price, "Open should be first trade price");
assert_eq!(
bar.close, breach_price,
"Close should be breach trade price"
);
}
#[test]
fn test_fixed_point_precision() {
let price1 = FixedPoint::from_str("50000.12345678").unwrap();
let price2 = FixedPoint::from_str("50000.87654321").unwrap();
assert_eq!(price1.to_string(), "50000.12345678");
assert_eq!(price2.to_string(), "50000.87654321");
let sum = FixedPoint(price1.0 + price2.0);
assert_eq!(sum.to_string(), "100000.99999999");
}
#[test]
fn test_cross_mode_algorithm_consistency() {
let mut processor = RangeBarProcessor::new(500).expect("Failed to create processor");
let test_trades = create_deterministic_breach_sequence();
let range_bars = processor
.process_agg_trade_records(&test_trades)
.expect("Failed to process trades");
validate_algorithm_invariants(&range_bars, &test_trades);
validate_breach_consistency(&range_bars);
assert!(
!range_bars.is_empty(),
"Expected at least 1 bar from deterministic sequence, got {}",
range_bars.len()
);
for (i, bar) in range_bars.iter().enumerate() {
assert!(bar.high >= bar.open, "Bar {} high < open", i);
assert!(bar.high >= bar.close, "Bar {} high < close", i);
assert!(bar.low <= bar.open, "Bar {} low > open", i);
assert!(bar.low <= bar.close, "Bar {} low > close", i);
assert!(bar.volume > FixedPoint(0), "Bar {} zero volume", i);
assert!(bar.open_time <= bar.close_time, "Bar {} time inversion", i);
}
}
fn create_deterministic_breach_sequence() -> Vec<AggTrade> {
let base_price = 50000.0;
let base_timestamp = 1609459200000;
vec![
create_trade(1, base_price, base_timestamp),
create_trade(2, base_price * 1.003, base_timestamp + 1000), create_trade(3, base_price * 1.006, base_timestamp + 2000), create_trade(4, base_price * 1.007, base_timestamp + 3000), create_trade(5, base_price * 1.009, base_timestamp + 4000), create_trade(6, base_price * 1.002, base_timestamp + 5000), create_trade(7, base_price * 1.003, base_timestamp + 6000), create_trade(8, base_price * 1.008, base_timestamp + 7000), ]
}
fn validate_algorithm_invariants(range_bars: &[RangeBar], test_trades: &[AggTrade]) {
for bar in range_bars {
let open_found = test_trades.iter().any(|trade| trade.price == bar.open);
assert!(
open_found,
"Bar open price {} not found in trades (non-lookahead violation)",
bar.open
);
}
let mut processor_for_volume_check =
RangeBarProcessor::new(500).expect("Failed to create processor"); let all_bars = processor_for_volume_check
.process_agg_trade_records_with_incomplete(test_trades)
.expect("Failed to process trades with incomplete for volume check");
let total_bar_volume: i64 = all_bars.iter().map(|bar| bar.volume.0).sum();
let total_trade_volume: i64 = test_trades.iter().map(|trade| trade.volume.0).sum();
assert_eq!(
total_bar_volume,
total_trade_volume,
"Volume conservation violation: bars={} (from {} bars), trades={} (from {} trades), discrepancy={}. \
Algorithm must account for ALL trade volume across all bars (completed + incomplete).",
total_bar_volume,
all_bars.len(),
total_trade_volume,
test_trades.len(),
total_trade_volume - total_bar_volume
);
for i in 1..range_bars.len() {
assert!(
range_bars[i - 1].close_time <= range_bars[i].open_time,
"Temporal ordering violation: bar {} close_time > bar {} open_time",
i - 1,
i
);
}
}
fn validate_breach_consistency(range_bars: &[RangeBar]) {
let threshold_decimal_bps = 50;
for (i, bar) in range_bars.iter().enumerate() {
let threshold_decimal = threshold_decimal_bps as f64 / 10000.0;
let upper_threshold = bar.open.to_f64() * (1.0 + threshold_decimal);
let lower_threshold = bar.open.to_f64() * (1.0 - threshold_decimal);
let high_breaches = bar.high.to_f64() >= upper_threshold;
let low_breaches = bar.low.to_f64() <= lower_threshold;
if high_breaches {
assert!(
bar.close.to_f64() >= upper_threshold,
"Bar {} breach consistency violation: high breaches ({} >= {}) but close doesn't ({} < {})",
i,
bar.high.to_f64(),
upper_threshold,
bar.close.to_f64(),
upper_threshold
);
}
if low_breaches {
assert!(
bar.close.to_f64() <= lower_threshold,
"Bar {} breach consistency violation: low breaches ({} <= {}) but close doesn't ({} > {})",
i,
bar.low.to_f64(),
lower_threshold,
bar.close.to_f64(),
lower_threshold
);
}
}
}
#[test]
fn test_non_lookahead_bias_compliance() {
let mut processor = RangeBarProcessor::new(500).expect("Failed to create processor");
let base_price = 50000.0;
let trades = vec![
create_trade(1, base_price, 1609459200000),
create_trade(2, base_price * 1.003, 1609459201000), create_trade(3, base_price * 1.006, 1609459202000), ];
let range_bars = processor
.process_agg_trade_records(&trades)
.expect("Failed to process trades");
assert_eq!(range_bars.len(), 1, "Should produce exactly one range bar");
let bar = &range_bars[0];
let expected_upper_threshold = base_price * 1.005;
assert!(
bar.close.to_f64() > expected_upper_threshold,
"Close should breach threshold computed from open: close={}, threshold={}",
bar.close.to_f64(),
expected_upper_threshold
);
}
fn create_trade(id: i64, price: f64, timestamp: i64) -> AggTrade {
AggTrade {
agg_trade_id: id,
price: FixedPoint::from_str(&format!("{:.8}", price)).unwrap(),
volume: FixedPoint::from_str("1.50000000").unwrap(),
first_trade_id: id * 10,
last_trade_id: id * 10 + 5,
timestamp,
is_buyer_maker: id % 2 == 0,
is_best_match: None,
}
}