use crate::engine::traits::{BarSink, SinkError};
use crate::live_engine::CompletedBar;
use opendeviationbar_core::fixed_point::SCALE;
const MIN_US: i64 = 1_000_000_000_000_000;
const MAX_US: i64 = 2_500_000_000_000_000;
const VOLUME_SCALE: f64 = SCALE as f64;
pub fn validate_bar_for_write(bar: &CompletedBar) -> bool {
let b = &bar.bar;
if b.close_time < MIN_US || b.close_time > MAX_US {
tracing::error!(
symbol = %bar.symbol,
threshold = bar.threshold_decimal_bps,
close_time_us = b.close_time,
"pre-write guard: close_time_us out of microsecond range"
);
return false;
}
if b.open_time < MIN_US || b.open_time > MAX_US {
tracing::error!(
symbol = %bar.symbol,
threshold = bar.threshold_decimal_bps,
open_time_us = b.open_time,
"pre-write guard: open_time_us out of microsecond range"
);
return false;
}
let vol = b.volume as f64 / VOLUME_SCALE;
if vol < 0.0 {
tracing::error!(
symbol = %bar.symbol,
threshold = bar.threshold_decimal_bps,
volume = vol,
"pre-write guard: negative volume (overflow)"
);
return false;
}
true
}
pub fn dispatch_to_sinks(
bar: &CompletedBar,
sinks: &std::sync::Arc<std::sync::Mutex<Vec<Box<dyn BarSink>>>>,
) {
if !validate_bar_for_write(bar) {
return;
}
if let Ok(mut locked) = sinks.lock() {
for sink in locked.iter_mut() {
match sink.on_bar(bar) {
Ok(()) => {}
Err(SinkError::Recoverable(msg)) => {
tracing::warn!(
sink = sink.name(),
symbol = %bar.symbol,
threshold = bar.threshold_decimal_bps,
%msg,
"sink recoverable error"
);
}
Err(SinkError::Unrecoverable(msg)) => {
tracing::error!(
sink = sink.name(),
symbol = %bar.symbol,
threshold = bar.threshold_decimal_bps,
%msg,
"sink unrecoverable error"
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use opendeviationbar_core::{FixedPoint, OpenDeviationBar, Tick};
use std::sync::Arc;
fn make_completed_bar(open_time: i64, close_time: i64, volume: i128) -> CompletedBar {
let trade = Tick {
ref_id: 1,
price: FixedPoint::from_str("50000.0").unwrap(),
volume: FixedPoint::from_str("1.0").unwrap(),
first_sub_id: 1,
last_sub_id: 1,
timestamp: 1_700_000_000_000_000, is_buyer_maker: false,
is_best_match: None,
best_bid: None,
best_ask: None,
};
let mut bar = OpenDeviationBar::new(&trade);
bar.open_time = open_time;
bar.close_time = close_time;
bar.volume = volume;
CompletedBar {
symbol: Arc::from("BTCUSDT"),
threshold_decimal_bps: 250,
bar,
}
}
#[test]
fn test_guard_valid_bar() {
let bar = make_completed_bar(
1_700_000_000_000_000, 1_700_000_100_000_000,
5_000_000_000, );
assert!(validate_bar_for_write(&bar));
}
#[test]
fn test_guard_close_time_millisecond_scale() {
let bar = make_completed_bar(
1_700_000_000_000_000,
1_700_000_000_000, 5_000_000_000,
);
assert!(!validate_bar_for_write(&bar));
}
#[test]
fn test_guard_open_time_beyond_2049() {
let bar = make_completed_bar(
3_000_000_000_000_000, 1_700_000_100_000_000,
5_000_000_000,
);
assert!(!validate_bar_for_write(&bar));
}
#[test]
fn test_guard_zero_volume_valid() {
let bar = make_completed_bar(1_700_000_000_000_000, 1_700_000_100_000_000, 0);
assert!(validate_bar_for_write(&bar));
}
#[test]
fn test_guard_negative_volume_rejected() {
let bar = make_completed_bar(
1_700_000_000_000_000,
1_700_000_100_000_000,
-1_000_000_000, );
assert!(!validate_bar_for_write(&bar));
}
#[test]
fn test_guard_normal_bar_all_valid() {
let bar = make_completed_bar(
1_700_000_000_000_000,
1_700_000_100_000_000,
10_000_000_000, );
assert!(validate_bar_for_write(&bar));
}
}