use opendeviationbar_core::OpenDeviationBar;
use opendeviationbar_providers::binance::{AdaptiveRateLimiter, ReconnectionPolicy};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use tokio::sync::watch;
use opendeviationbar_core::checkpoint::Checkpoint;
pub(crate) const DAY_US: i64 = 86_400_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OuroborosMode {
#[default]
Day,
Aion,
Week { max_gap_us: i64 },
}
#[derive(Debug, Clone)]
pub struct CompletedBar {
pub symbol: Arc<str>,
pub threshold_decimal_bps: u32,
pub bar: OpenDeviationBar,
}
#[derive(Debug, Clone)]
pub struct FormingBar {
pub symbol: Arc<str>,
pub threshold_decimal_bps: u32,
pub bar: OpenDeviationBar,
pub last_trade_timestamp_us: i64,
}
pub(crate) type FormingBarKey = (Arc<str>, u32);
pub type FormingBarWatches = HashMap<FormingBarKey, watch::Receiver<Option<FormingBar>>>;
#[derive(Debug)]
pub struct LiveEngineMetrics {
pub trades_received: AtomicU64,
pub bars_emitted: AtomicU64,
pub reconnections: AtomicU64,
pub backpressure_events: AtomicU64, pub dropped_bars: AtomicU64, pub max_queue_depth: AtomicU64, pub total_block_time_ms: AtomicU64, pub gap_fills: AtomicU64, pub gap_trades_recovered: AtomicU64, pub bars_suppressed: AtomicU64, }
impl Default for LiveEngineMetrics {
fn default() -> Self {
Self {
trades_received: AtomicU64::new(0),
bars_emitted: AtomicU64::new(0),
reconnections: AtomicU64::new(0),
backpressure_events: AtomicU64::new(0),
dropped_bars: AtomicU64::new(0),
max_queue_depth: AtomicU64::new(0),
total_block_time_ms: AtomicU64::new(0),
gap_fills: AtomicU64::new(0),
gap_trades_recovered: AtomicU64::new(0),
bars_suppressed: AtomicU64::new(0),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum WsMode {
#[default]
Combined,
PerSymbol,
}
pub fn split_symbols_by_volume(symbols: &[String]) -> (Vec<String>, Vec<String>) {
const VOLUME_RANKING: &[&str] = &[
"BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT", "DOGEUSDT", "ADAUSDT", "AVAXUSDT",
"LINKUSDT", "TRXUSDT", "SUIUSDT", "LTCUSDT", "NEARUSDT",
];
if symbols.len() <= 1 {
return (symbols.to_vec(), Vec::new());
}
let mut scored: Vec<(usize, &String)> = symbols
.iter()
.map(|s| {
let rank = VOLUME_RANKING
.iter()
.position(|&r| r == s.as_str())
.unwrap_or(VOLUME_RANKING.len() + 1000);
(rank, s)
})
.collect();
scored.sort_by_key(|(rank, _)| *rank);
let half = scored.len().div_ceil(2); let mut group_a = Vec::with_capacity(half);
let mut group_b = Vec::with_capacity(scored.len() - half);
for (i, (_, sym)) in scored.into_iter().enumerate() {
if i < half {
group_a.push(sym.clone());
} else {
group_b.push(sym.clone());
}
}
(group_a, group_b)
}
#[derive(Debug, Clone)]
pub struct LiveEngineConfig {
pub symbol_thresholds: HashMap<String, Vec<u32>>,
pub include_microstructure: bool,
pub bar_channel_capacity: usize,
pub reconnection_policy: ReconnectionPolicy,
pub initial_checkpoints: HashMap<(String, u32), Checkpoint>,
pub rate_limiter: Option<Arc<AdaptiveRateLimiter>>,
pub compute_tier2: bool,
pub compute_tier3: bool,
pub compute_hurst: Option<bool>,
pub compute_permutation_entropy: Option<bool>,
pub gap_detector_seeds: HashMap<String, i64>,
pub ws_mode: WsMode,
pub symbol_modes: HashMap<String, OuroborosMode>,
pub ws_base_url: String,
pub ws_params: String,
pub rest_base_url: String,
pub rest_headers: Vec<(String, String)>,
}
impl LiveEngineConfig {
fn get_bar_channel_capacity() -> usize {
std::env::var("OPENDEVIATIONBAR_MAX_PENDING_BARS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(10_000)
}
pub fn new(symbol_thresholds: HashMap<String, Vec<u32>>) -> Self {
Self {
symbol_thresholds,
include_microstructure: true,
bar_channel_capacity: Self::get_bar_channel_capacity(),
reconnection_policy: ReconnectionPolicy::default(),
initial_checkpoints: HashMap::new(),
compute_tier2: true,
compute_tier3: false,
compute_hurst: None,
compute_permutation_entropy: None,
rate_limiter: None,
gap_detector_seeds: HashMap::new(),
ws_mode: WsMode::default(),
symbol_modes: HashMap::new(),
ws_base_url: opendeviationbar_providers::binance::DEFAULT_WS_BASE_URL.to_string(),
ws_params: String::new(),
rest_base_url: "https://api.binance.com/api/v3/aggTrades".to_string(),
rest_headers: Vec::new(),
}
}
pub fn symbols(&self) -> Vec<String> {
let mut syms: Vec<String> = self.symbol_thresholds.keys().cloned().collect();
syms.sort();
syms
}
pub fn with_rate_limiter(mut self, limiter: Arc<AdaptiveRateLimiter>) -> Self {
self.rate_limiter = Some(limiter);
self
}
pub fn with_checkpoint(
mut self,
symbol: String,
threshold: u32,
checkpoint: Checkpoint,
) -> Self {
self.initial_checkpoints
.insert((symbol, threshold), checkpoint);
self
}
pub fn with_bar_channel_capacity(mut self, capacity: usize) -> Self {
self.bar_channel_capacity = capacity;
self
}
}
impl LiveEngineMetrics {
pub fn snapshot(&self) -> LiveEngineMetricsSnapshot {
LiveEngineMetricsSnapshot {
trades_received: self.trades_received.load(Ordering::Relaxed),
bars_emitted: self.bars_emitted.load(Ordering::Relaxed),
reconnections: self.reconnections.load(Ordering::Relaxed),
dropped_bars: self.dropped_bars.load(Ordering::Relaxed),
max_queue_depth: self.max_queue_depth.load(Ordering::Relaxed),
backpressure_events: self.backpressure_events.load(Ordering::Relaxed),
gap_fills: self.gap_fills.load(Ordering::Relaxed),
gap_trades_recovered: self.gap_trades_recovered.load(Ordering::Relaxed),
bars_suppressed: self.bars_suppressed.load(Ordering::Relaxed),
}
}
pub fn estimate_queue_depth(&self) -> u64 {
let emitted = self.bars_emitted.load(Ordering::Relaxed);
let dropped = self.dropped_bars.load(Ordering::Relaxed);
let received = self.trades_received.load(Ordering::Relaxed);
if emitted + dropped > received {
0
} else {
received - emitted - dropped
}
}
}
#[derive(Debug, Clone)]
pub struct LiveEngineMetricsSnapshot {
pub trades_received: u64,
pub bars_emitted: u64,
pub reconnections: u64,
pub dropped_bars: u64,
pub max_queue_depth: u64,
pub backpressure_events: u64,
pub gap_fills: u64,
pub gap_trades_recovered: u64,
pub bars_suppressed: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn sym_thresh(pairs: &[(&str, &[u32])]) -> HashMap<String, Vec<u32>> {
let mut m = HashMap::new();
for (sym, thrs) in pairs {
m.insert((*sym).to_string(), thrs.to_vec());
}
m
}
#[test]
fn test_ws_mode_combined_is_default() {
let config =
LiveEngineConfig::new(sym_thresh(&[("BTCUSDT", &[250]), ("ETHUSDT", &[250])]));
assert_eq!(config.ws_mode, WsMode::Combined);
}
#[test]
fn test_ws_mode_per_symbol_can_be_set() {
let mut config = LiveEngineConfig::new(sym_thresh(&[("BTCUSDT", &[250])]));
config.ws_mode = WsMode::PerSymbol;
assert_eq!(config.ws_mode, WsMode::PerSymbol);
}
#[test]
fn test_split_symbols_by_volume_26_symbols() {
let symbols: Vec<String> = vec![
"BTCUSDT",
"ETHUSDT",
"SOLUSDT",
"XRPUSDT",
"BNBUSDT",
"DOGEUSDT",
"ADAUSDT",
"AVAXUSDT",
"LINKUSDT",
"TRXUSDT",
"SUIUSDT",
"LTCUSDT",
"NEARUSDT",
"DOTUSDT",
"MATICUSDT",
"SHIBUSDT",
"BCHUSDT",
"UNIUSDT",
"APTUSDT",
"ICPUSDT",
"ETCUSDT",
"FILUSDT",
"XLMUSDT",
"ATOMUSDT",
"VETUSDT",
"AAVEUSDT",
]
.into_iter()
.map(String::from)
.collect();
let (a, b) = split_symbols_by_volume(&symbols);
assert_eq!(a.len(), 13);
assert_eq!(b.len(), 13);
assert!(a.contains(&"BTCUSDT".to_string()));
assert!(a.contains(&"ETHUSDT".to_string()));
assert!(a.contains(&"SOLUSDT".to_string()));
}
#[test]
fn test_split_symbols_by_volume_odd_count() {
let symbols: Vec<String> = vec!["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT"]
.into_iter()
.map(String::from)
.collect();
let (a, b) = split_symbols_by_volume(&symbols);
assert_eq!(a.len() + b.len(), 5);
assert!(a.len() == 3 || a.len() == 2);
assert!(b.len() == 3 || b.len() == 2);
}
#[test]
fn test_split_symbols_by_volume_single_symbol() {
let symbols: Vec<String> = vec!["BTCUSDT".to_string()];
let (a, b) = split_symbols_by_volume(&symbols);
assert_eq!(a.len(), 1);
assert_eq!(b.len(), 0);
}
}