use std::collections::VecDeque;
use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::tick_consumer::TickConsumer;
use crate::core::types::Tick;
#[derive(Debug, Clone)]
pub struct TradeClusterDetector {
price_bucket: f64,
cluster_threshold: usize,
window_ms: i64,
recent_ticks: VecDeque<(i64, i64, bool, f64)>,
last_cluster_price: f64,
last_cluster_size: f64,
last_signal: f64,
}
impl TradeClusterDetector {
pub fn new(price_bucket: f64, cluster_threshold: usize, window_ms: i64) -> Self {
let bucket = if price_bucket > 0.0 { price_bucket } else { 0.01 };
Self {
price_bucket: bucket,
cluster_threshold: cluster_threshold.max(2),
window_ms: window_ms.max(1),
recent_ticks: VecDeque::with_capacity(256),
last_cluster_price: 0.0,
last_cluster_size: 0.0,
last_signal: 0.0,
}
}
}
impl TickConsumer for TradeClusterDetector {
fn update_tick(&mut self, tick: &Tick) -> IndicatorValue {
let bucket_id = (tick.price / self.price_bucket).floor() as i64;
self.recent_ticks.push_back((bucket_id, tick.time, tick.is_buy, tick.size));
while let Some(&(_, ts, _, _)) = self.recent_ticks.front() {
if tick.time - ts > self.window_ms {
self.recent_ticks.pop_front();
} else {
break;
}
}
let count = self.recent_ticks.iter()
.filter(|&&(b, _, _, _)| b == bucket_id)
.count();
if count >= self.cluster_threshold {
self.last_cluster_price = bucket_id as f64 * self.price_bucket;
self.last_cluster_size = self.recent_ticks.iter()
.filter(|&&(b, _, _, _)| b == bucket_id)
.map(|&(_, _, _, s)| s)
.sum();
let buy_count = self.recent_ticks.iter()
.filter(|&&(b, _, is_buy, _)| b == bucket_id && is_buy)
.count();
let sell_count = count - buy_count;
self.last_signal = if buy_count > sell_count {
1.0
} else if sell_count > buy_count {
-1.0
} else {
0.0
};
} else {
self.last_signal = 0.0;
}
IndicatorValue::Triple(self.last_signal, self.last_cluster_price, self.last_cluster_size)
}
fn value(&self) -> IndicatorValue {
IndicatorValue::Triple(self.last_signal, self.last_cluster_price, self.last_cluster_size)
}
fn reset(&mut self) {
self.recent_ticks.clear();
self.last_cluster_price = 0.0;
self.last_cluster_size = 0.0;
self.last_signal = 0.0;
}
fn is_ready(&self) -> bool {
!self.recent_ticks.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::Tick;
fn tick_at(price: f64, is_buy: bool, time_ms: i64) -> Tick {
Tick::new(time_ms, price, 1.0, is_buy)
}
#[test]
fn cluster_detected_when_threshold_reached() {
let mut det = TradeClusterDetector::new(1.0, 3, 10_000);
for i in 0..3 {
det.update_tick(&tick_at(100.0, true, i as i64 * 100));
}
match det.value() {
IndicatorValue::Triple(signal, price, size) => {
assert!((signal - 1.0).abs() < 1e-9, "expected buy signal: {}", signal);
assert!((price - 100.0).abs() < 1e-9, "expected price 100: {}", price);
assert!((size - 3.0).abs() < 1e-9, "expected size 3: {}", size);
}
other => panic!("expected Triple, got {:?}", other),
}
}
#[test]
fn no_cluster_below_threshold() {
let mut det = TradeClusterDetector::new(1.0, 5, 10_000);
for i in 0..4 {
det.update_tick(&tick_at(100.0, true, i as i64 * 100));
}
assert_eq!(det.value(), IndicatorValue::Triple(0.0, 0.0, 0.0));
}
#[test]
fn old_ticks_evicted_by_time_window() {
let mut det = TradeClusterDetector::new(1.0, 3, 1_000); det.update_tick(&tick_at(100.0, true, 0));
det.update_tick(&tick_at(100.0, true, 500));
det.update_tick(&tick_at(100.0, true, 900));
assert!((det.value().main() - 1.0).abs() < 1e-9);
det.update_tick(&tick_at(100.0, true, 2_100));
assert_eq!(det.last_signal, 0.0);
}
#[test]
fn reset_clears_state() {
let mut det = TradeClusterDetector::new(1.0, 3, 10_000);
for i in 0..3 {
det.update_tick(&tick_at(100.0, true, i as i64 * 100));
}
det.reset();
assert!(!det.is_ready());
assert_eq!(det.value(), IndicatorValue::Triple(0.0, 0.0, 0.0));
}
}