use super::*;
use chrono::{TimeZone, Utc};
use rust_decimal_macros::dec;
#[test]
fn create_valid_candle() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle");
assert_eq!(candle.open(), dec!(100));
assert_eq!(candle.high(), dec!(110));
assert_eq!(candle.low(), dec!(90));
assert_eq!(candle.close(), dec!(105));
assert_eq!(candle.volume(), dec!(1000));
}
#[test]
fn high_below_low_fails() {
let ts = Utc::now();
let result = Candle::new(dec!(100), dec!(80), dec!(90), dec!(85), dec!(1000), ts);
assert!(matches!(result, Err(CandleError::HighBelowLow { .. })));
}
#[test]
fn high_equals_low_succeeds() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(100), dec!(100), dec!(100), dec!(500), ts)
.expect("valid candle");
assert_eq!(candle.high(), candle.low());
}
#[test]
fn zero_volume_succeeds() {
let ts = Utc::now();
let candle =
Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(0), ts).expect("valid candle");
assert_eq!(candle.volume(), dec!(0));
}
#[test]
fn high_below_low_error_contains_values() {
let ts = Utc::now();
let result = Candle::new(dec!(100), dec!(80), dec!(90), dec!(85), dec!(1000), ts);
match result {
Err(CandleError::HighBelowLow { high, low }) => {
assert_eq!(high, dec!(80));
assert_eq!(low, dec!(90));
}
_ => panic!("expected HighBelowLow error"),
}
}
#[test]
fn timestamp_is_preserved() {
let ts = Utc
.with_ymd_and_hms(2026, 1, 15, 12, 0, 0)
.single()
.expect("valid datetime");
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(500), ts)
.expect("valid candle");
assert_eq!(candle.timestamp(), ts);
}
#[test]
fn serde_roundtrip() {
let ts = Utc
.with_ymd_and_hms(2026, 3, 1, 0, 0, 0)
.single()
.expect("valid datetime");
let original = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle");
let json = serde_json::to_string(&original).expect("valid json");
let parsed: Candle = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed, original);
}
#[test]
fn candle_error_display_high_below_low() {
let err = CandleError::HighBelowLow {
high: dec!(80),
low: dec!(90),
};
let msg = err.to_string();
assert!(msg.contains("80"), "should contain high value");
assert!(msg.contains("90"), "should contain low value");
}
#[test]
fn candle_error_display_empty_aggregation() {
let err = CandleError::EmptyAggregation;
assert!(err.to_string().contains("empty"));
}
#[test]
fn candle_range_empty_slice_returns_none() {
assert_eq!(CandleRange::from_candles(&[]), None);
}
#[test]
fn candle_range_single_candle() {
let ts = Utc
.with_ymd_and_hms(2026, 1, 1, 0, 0, 0)
.single()
.expect("valid datetime");
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle");
let range = CandleRange::from_candles(&[candle]).expect("non-empty slice");
assert_eq!(range.earliest, ts);
assert_eq!(range.latest, ts);
}
#[test]
fn candle_range_multiple_candles() {
use chrono::Duration;
let ts1 = Utc
.with_ymd_and_hms(2026, 1, 1, 0, 0, 0)
.single()
.expect("valid datetime");
let ts2 = ts1 + Duration::days(5);
let c1 =
Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts1).expect("valid");
let c2 =
Candle::new(dec!(200), dec!(210), dec!(190), dec!(205), dec!(2000), ts2).expect("valid");
let range = CandleRange::from_candles(&[c1, c2]).expect("non-empty slice");
assert_eq!(range.earliest, ts1);
assert_eq!(range.latest, ts2);
}
#[test]
fn new_candle_defaults_to_unknown_volume_source() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle");
assert_eq!(candle.volume_source(), VolumeSource::Unknown);
}
#[test]
fn with_volume_source_sets_trade_size() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle")
.with_volume_source(VolumeSource::TradeSize);
assert_eq!(candle.volume_source(), VolumeSource::TradeSize);
}
#[test]
fn with_volume_source_sets_tick_count() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(5), ts)
.expect("valid candle")
.with_volume_source(VolumeSource::TickCount);
assert_eq!(candle.volume_source(), VolumeSource::TickCount);
}
#[test]
fn with_volume_source_sets_exchange_reported() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(50000), ts)
.expect("valid candle")
.with_volume_source(VolumeSource::ExchangeReported);
assert_eq!(candle.volume_source(), VolumeSource::ExchangeReported);
}
#[test]
fn volume_source_as_str_roundtrip() {
for vs in [
VolumeSource::TradeSize,
VolumeSource::TickCount,
VolumeSource::ExchangeReported,
VolumeSource::Unknown,
] {
assert_eq!(VolumeSource::from_str_value(vs.as_str()), vs);
}
}
#[test]
fn volume_source_from_unknown_string_defaults_to_unknown() {
assert_eq!(
VolumeSource::from_str_value("garbage"),
VolumeSource::Unknown
);
assert_eq!(VolumeSource::from_str_value(""), VolumeSource::Unknown);
}
#[test]
fn volume_source_display() {
assert_eq!(VolumeSource::TradeSize.to_string(), "trade_size");
assert_eq!(VolumeSource::TickCount.to_string(), "tick_count");
assert_eq!(
VolumeSource::ExchangeReported.to_string(),
"exchange_reported"
);
assert_eq!(VolumeSource::Unknown.to_string(), "unknown");
}
#[test]
fn serde_roundtrip_with_volume_source() {
let ts = Utc
.with_ymd_and_hms(2026, 3, 17, 0, 0, 0)
.single()
.expect("valid datetime");
let original = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle")
.with_volume_source(VolumeSource::TradeSize);
let json = serde_json::to_string(&original).expect("valid json");
let parsed: Candle = serde_json::from_str(&json).expect("valid json");
assert_eq!(parsed.volume_source(), VolumeSource::TradeSize);
assert_eq!(parsed, original);
}
#[test]
fn serde_legacy_json_without_volume_source_defaults_to_unknown() {
let json = r#"{"open":"100","high":"110","low":"90","close":"105","volume":"1000","timestamp":"2026-03-17T00:00:00Z"}"#;
let parsed: Candle = serde_json::from_str(json).expect("valid json");
assert_eq!(parsed.volume_source(), VolumeSource::Unknown);
}
#[test]
fn volume_source_default_is_unknown() {
assert_eq!(VolumeSource::default(), VolumeSource::Unknown);
}
#[test]
fn candle_quality_default_is_complete() {
assert_eq!(CandleQuality::default(), CandleQuality::Complete);
}
#[test]
fn candle_quality_complete_serde_roundtrip() {
let q = CandleQuality::Complete;
let json = serde_json::to_string(&q).expect("serialize");
let parsed: CandleQuality = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, q);
}
#[test]
fn candle_quality_partial_serde_roundtrip() {
let q = CandleQuality::Partial {
reason: "provider timeout".to_string(),
};
let json = serde_json::to_string(&q).expect("serialize");
let parsed: CandleQuality = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, q);
if let CandleQuality::Partial { reason } = &parsed {
assert_eq!(reason, "provider timeout");
} else {
panic!("expected Partial variant");
}
}
#[test]
fn candle_quality_as_str_complete() {
assert_eq!(CandleQuality::Complete.as_str(), "complete");
}
#[test]
fn candle_quality_as_str_partial() {
let q = CandleQuality::Partial {
reason: "gap fill".to_string(),
};
assert_eq!(q.as_str(), "partial");
}
#[test]
fn candle_quality_from_str_value_complete() {
assert_eq!(
CandleQuality::from_str_value("complete"),
CandleQuality::Complete
);
}
#[test]
fn candle_quality_from_str_value_partial() {
assert_eq!(
CandleQuality::from_str_value("partial"),
CandleQuality::Partial {
reason: String::new()
}
);
}
#[test]
fn candle_quality_from_str_value_unknown_defaults_to_complete() {
assert_eq!(
CandleQuality::from_str_value("garbage"),
CandleQuality::Complete
);
assert_eq!(CandleQuality::from_str_value(""), CandleQuality::Complete);
}
#[test]
fn candle_quality_display() {
assert_eq!(CandleQuality::Complete.to_string(), "complete");
assert_eq!(
CandleQuality::Partial {
reason: "x".to_string()
}
.to_string(),
"partial"
);
}
#[test]
fn new_candle_defaults_to_complete_quality() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle");
assert_eq!(candle.quality(), &CandleQuality::Complete);
}
#[test]
fn with_quality_sets_partial() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle")
.with_quality(CandleQuality::Partial {
reason: "window invalidation".to_string(),
});
assert_eq!(
candle.quality(),
&CandleQuality::Partial {
reason: "window invalidation".to_string()
}
);
}
#[test]
fn candle_quality_accessor_returns_ref() {
let ts = Utc::now();
let candle = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle");
let quality_ref: &CandleQuality = candle.quality();
assert_eq!(quality_ref, &CandleQuality::Complete);
}
#[test]
fn serde_legacy_json_without_quality_defaults_to_complete() {
let json = r#"{"open":"100","high":"110","low":"90","close":"105","volume":"1000","timestamp":"2026-03-17T00:00:00Z"}"#;
let parsed: Candle = serde_json::from_str(json).expect("valid json");
assert_eq!(parsed.quality(), &CandleQuality::Complete);
}
#[test]
fn serde_roundtrip_with_quality() {
let ts = Utc
.with_ymd_and_hms(2026, 3, 21, 0, 0, 0)
.single()
.expect("valid datetime");
let original = Candle::new(dec!(100), dec!(110), dec!(90), dec!(105), dec!(1000), ts)
.expect("valid candle")
.with_quality(CandleQuality::Partial {
reason: "provider timeout".to_string(),
});
let json = serde_json::to_string(&original).expect("valid json");
let parsed: Candle = serde_json::from_str(&json).expect("valid json");
assert_eq!(
parsed.quality(),
&CandleQuality::Partial {
reason: "provider timeout".to_string()
}
);
assert_eq!(parsed, original);
}