use crate::types::Bar;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PocType {
Daily, Session, Weekly, Monthly, Rolling, }
#[derive(Debug, Clone)]
pub struct PocLevel {
pub price: f64, pub volume: f64, pub bar_count: u32, pub poc_type: PocType, pub strength: f64, pub first_touch_time: i64, pub last_touch_time: i64, pub touch_count: u32, pub breakout_probability: f64, pub is_active: bool, }
impl PocLevel {
pub fn new(price: f64, volume: f64, poc_type: PocType, timestamp: i64) -> Self {
Self {
price,
volume,
bar_count: 1,
poc_type,
strength: 0.0,
first_touch_time: timestamp,
last_touch_time: timestamp,
touch_count: 1,
breakout_probability: 0.0,
is_active: true,
}
}
pub fn update(&mut self, volume: f64, timestamp: i64) {
self.volume += volume;
self.bar_count += 1;
self.last_touch_time = timestamp;
self.touch_count += 1;
self.recalculate_strength();
}
fn recalculate_strength(&mut self) {
let volume_factor = (self.volume / 1000000.0).min(1.0); let touch_factor = (self.touch_count as f64 / 10.0).min(1.0); let time_factor = ((self.last_touch_time - self.first_touch_time) as f64 / 86400.0 / 7.0).min(1.0);
self.strength = (volume_factor * 0.5 + touch_factor * 0.3 + time_factor * 0.2).min(1.0);
self.breakout_probability = 1.0 - self.strength;
}
pub fn is_touched(&self, high: f64, low: f64, tolerance: f64) -> bool {
let upper_bound = self.price + tolerance;
let lower_bound = self.price - tolerance;
low <= upper_bound && high >= lower_bound
}
pub fn is_broken(&self, close: f64, tolerance: f64) -> bool {
(close > self.price + tolerance) || (close < self.price - tolerance)
}
}
#[derive(Debug, Clone)]
pub struct PocAnalysis {
pub current_poc: Option<PocLevel>, pub active_levels: Vec<PocLevel>, pub broken_levels: Vec<PocLevel>, pub nearest_support: Option<f64>, pub nearest_resistance: Option<f64>, pub market_structure: MarketStructure, pub volume_imbalance: f64, pub poc_confluence: u32, }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MarketStructure {
Accumulation, Distribution, Trending, Breakout, Consolidation, }
impl Default for PocAnalysis {
fn default() -> Self {
Self {
current_poc: None,
active_levels: Vec::with_capacity(32),
broken_levels: Vec::with_capacity(16),
nearest_support: None,
nearest_resistance: None,
market_structure: MarketStructure::Consolidation,
volume_imbalance: 0.0,
poc_confluence: 0,
}
}
}
#[derive(Clone)]
pub struct PocDetector {
price_precision: u32, min_volume_threshold: f64, level_tolerance: f64, max_levels: usize,
volume_profile: BTreeMap<i64, f64>, price_levels: BTreeMap<i64, PocLevel>,
daily_volume: BTreeMap<i64, f64>, session_volume: BTreeMap<i64, f64>, rolling_volume: BTreeMap<i64, f64>,
rolling_period: usize, session_start_hour: u32, session_end_hour: u32,
bar_history: Vec<Bar>,
analysis: PocAnalysis,
is_ready: bool,
min_bars: usize,
current_day: i64, current_session: i64, }
impl PocDetector {
pub fn new(
price_precision: u32,
min_volume_threshold: f64,
rolling_period: usize,
) -> Self {
Self {
price_precision,
min_volume_threshold,
level_tolerance: 10.0_f64.powi(-(price_precision as i32)), max_levels: 20,
volume_profile: BTreeMap::new(),
price_levels: BTreeMap::new(),
daily_volume: BTreeMap::new(),
session_volume: BTreeMap::new(),
rolling_volume: BTreeMap::new(),
rolling_period,
session_start_hour: 9, session_end_hour: 17, bar_history: Vec::with_capacity(500),
analysis: PocAnalysis::default(),
is_ready: false,
min_bars: rolling_period.max(20),
current_day: 0,
current_session: 0,
}
}
pub fn update(&mut self, bar: &Bar) -> &PocAnalysis {
if self.bar_history.len() >= 500 {
self.bar_history.remove(0);
}
self.bar_history.push(*bar);
self.update_volume_profile(bar);
self.update_time_windows(bar);
self.update_poc_levels(bar);
self.analyze_market_structure(bar);
if !self.is_ready && self.bar_history.len() >= self.min_bars {
self.is_ready = true;
}
&self.analysis
}
fn update_volume_profile(&mut self, bar: &Bar) {
let price_levels = self.get_price_levels_for_bar(bar);
let volume_per_level = bar.volume / price_levels.len().max(1) as f64;
for price_tick in price_levels {
*self.rolling_volume.entry(price_tick).or_insert(0.0) += volume_per_level;
if self.is_same_day(bar.time, self.current_day) {
*self.daily_volume.entry(price_tick).or_insert(0.0) += volume_per_level;
}
if self.is_same_session(bar.time, self.current_session) {
*self.session_volume.entry(price_tick).or_insert(0.0) += volume_per_level;
}
}
if self.bar_history.len() >= self.rolling_period {
let old_bar = self.bar_history[self.bar_history.len() - self.rolling_period];
self.remove_old_volume(&old_bar);
}
const MAX_LEVELS: usize = 500;
if self.rolling_volume.len() > MAX_LEVELS * 2 {
let mut volumes: Vec<_> = self.rolling_volume.values().copied().collect();
volumes.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let threshold = volumes.get(MAX_LEVELS).copied().unwrap_or(0.0);
self.rolling_volume.retain(|_, v| *v >= threshold);
}
}
fn get_price_levels_for_bar(&self, bar: &Bar) -> Vec<i64> {
let tick_multiplier = 10_f64.powi(self.price_precision as i32);
vec![
(bar.open * tick_multiplier).round() as i64,
(bar.high * tick_multiplier).round() as i64,
(bar.low * tick_multiplier).round() as i64,
(bar.close * tick_multiplier).round() as i64,
]
}
fn remove_old_volume(&mut self, old_bar: &Bar) {
let price_levels = self.get_price_levels_for_bar(old_bar);
let volume_per_level = old_bar.volume / price_levels.len() as f64;
for price_tick in price_levels {
if let Some(volume) = self.rolling_volume.get_mut(&price_tick) {
*volume -= volume_per_level;
if *volume <= 0.0 {
self.rolling_volume.remove(&price_tick);
}
}
}
}
fn update_time_windows(&mut self, bar: &Bar) {
let bar_day = self.get_day_timestamp(bar.time);
let bar_session = self.get_session_timestamp(bar.time);
if bar_day != self.current_day {
self.daily_volume.clear();
self.current_day = bar_day;
}
if bar_session != self.current_session {
self.session_volume.clear();
self.current_session = bar_session;
}
}
fn update_poc_levels(&mut self, bar: &Bar) {
let daily_poc = self.find_poc(&self.daily_volume, PocType::Daily, bar.time);
let session_poc = self.find_poc(&self.session_volume, PocType::Session, bar.time);
let rolling_poc = self.find_poc(&self.rolling_volume, PocType::Rolling, bar.time);
self.update_active_levels(daily_poc.clone(), bar);
self.update_active_levels(session_poc.clone(), bar);
self.update_active_levels(rolling_poc.clone(), bar);
self.analysis.current_poc = rolling_poc.or(session_poc).or(daily_poc);
self.cleanup_inactive_levels(bar);
self.update_support_resistance(bar);
}
fn find_poc(&self, volume_data: &BTreeMap<i64, f64>, poc_type: PocType, timestamp: i64) -> Option<PocLevel> {
if volume_data.is_empty() {
return None;
}
let mut max_volume = 0.0;
let mut poc_price_tick = 0i64;
for (&price_tick, &volume) in volume_data {
if volume > max_volume && volume >= self.min_volume_threshold {
max_volume = volume;
poc_price_tick = price_tick;
}
}
if max_volume >= self.min_volume_threshold {
let price = poc_price_tick as f64 / 10_f64.powi(self.price_precision as i32);
Some(PocLevel::new(price, max_volume, poc_type, timestamp))
} else {
None
}
}
fn update_active_levels(&mut self, new_poc: Option<PocLevel>, bar: &Bar) {
if let Some(poc) = new_poc {
let price_tick = (poc.price * 10_f64.powi(self.price_precision as i32)) as i64;
let mut found_similar = false;
for level in &mut self.analysis.active_levels {
let level_tick = (level.price * 10_f64.powi(self.price_precision as i32)) as i64;
if (level_tick - price_tick).abs() <= (self.level_tolerance * 10_f64.powi(self.price_precision as i32)) as i64 {
level.update(poc.volume, bar.time);
found_similar = true;
break;
}
}
if !found_similar {
self.analysis.active_levels.push(poc);
}
}
}
fn cleanup_inactive_levels(&mut self, bar: &Bar) {
let current_time = bar.time;
let max_age = 86400 * 7;
let mut i = 0;
while i < self.analysis.active_levels.len() {
let level = &self.analysis.active_levels[i];
if current_time - level.last_touch_time > max_age || level.is_broken(bar.close, self.level_tolerance) {
let mut broken_level = level.clone();
broken_level.is_active = false;
self.analysis.broken_levels.push(broken_level);
self.analysis.active_levels.remove(i);
} else {
i += 1;
}
}
self.analysis.active_levels.sort_by(|a, b| b.strength.partial_cmp(&a.strength).unwrap_or(std::cmp::Ordering::Equal));
if self.analysis.active_levels.len() > self.max_levels {
self.analysis.active_levels.truncate(self.max_levels);
}
}
fn update_support_resistance(&mut self, bar: &Bar) {
let current_price = bar.close;
let mut nearest_support = None;
let mut nearest_resistance = None;
let mut support_distance = f64::INFINITY;
let mut resistance_distance = f64::INFINITY;
for level in &self.analysis.active_levels {
if level.price < current_price {
let distance = current_price - level.price;
if distance < support_distance {
support_distance = distance;
nearest_support = Some(level.price);
}
} else if level.price > current_price {
let distance = level.price - current_price;
if distance < resistance_distance {
resistance_distance = distance;
nearest_resistance = Some(level.price);
}
}
}
self.analysis.nearest_support = nearest_support;
self.analysis.nearest_resistance = nearest_resistance;
}
fn analyze_market_structure(&mut self, bar: &Bar) {
if self.analysis.active_levels.is_empty() {
self.analysis.market_structure = MarketStructure::Consolidation;
return;
}
let current_price = bar.close;
let volume = bar.volume;
let mut _price_above_poc = 0;
let mut _price_below_poc = 0;
let mut total_volume_above = 0.0;
let mut total_volume_below = 0.0;
for level in &self.analysis.active_levels {
if current_price > level.price {
_price_above_poc += 1;
total_volume_above += level.volume;
} else if current_price < level.price {
_price_below_poc += 1;
total_volume_below += level.volume;
}
}
let total_volume = total_volume_above + total_volume_below;
if total_volume > 0.0 {
self.analysis.volume_imbalance = (total_volume_above - total_volume_below) / total_volume;
}
self.analysis.market_structure = if self.is_near_poc_levels(current_price) {
if volume > self.get_average_volume() * 1.5 {
if self.analysis.volume_imbalance.abs() > 0.3 {
MarketStructure::Distribution
} else {
MarketStructure::Accumulation
}
} else {
MarketStructure::Consolidation
}
} else if self.is_trending() {
MarketStructure::Trending
} else {
MarketStructure::Breakout
};
self.analysis.poc_confluence = self.count_poc_confluence(current_price);
}
fn is_near_poc_levels(&self, price: f64) -> bool {
for level in &self.analysis.active_levels {
if (price - level.price).abs() <= self.level_tolerance * 2.0 {
return true;
}
}
false
}
fn is_trending(&self) -> bool {
if self.bar_history.len() < 20 {
return false;
}
let recent_bars = &self.bar_history[self.bar_history.len().saturating_sub(20)..];
let first_price = recent_bars[0].close;
let last_price = recent_bars[recent_bars.len() - 1].close;
let price_change_pct = ((last_price - first_price) / first_price).abs();
price_change_pct > 0.05 }
fn get_average_volume(&self) -> f64 {
if self.bar_history.is_empty() {
return 0.0;
}
let total_volume: f64 = self.bar_history.iter().map(|bar| bar.volume).sum();
total_volume / self.bar_history.len() as f64
}
fn count_poc_confluence(&self, price: f64) -> u32 {
let mut confluence = 0;
let tolerance = self.level_tolerance * 3.0;
for level in &self.analysis.active_levels {
if (price - level.price).abs() <= tolerance {
confluence += 1;
}
}
confluence
}
fn get_day_timestamp(&self, timestamp: i64) -> i64 {
timestamp / 86400 * 86400
}
fn get_session_timestamp(&self, timestamp: i64) -> i64 {
let day = self.get_day_timestamp(timestamp);
let hour = (timestamp % 86400) / 3600;
if hour >= self.session_start_hour as i64 && hour < self.session_end_hour as i64 {
day + self.session_start_hour as i64 * 3600
} else {
if hour < self.session_start_hour as i64 {
day - 86400 + self.session_start_hour as i64 * 3600
} else {
day + 86400 + self.session_start_hour as i64 * 3600
}
}
}
fn is_same_day(&self, timestamp1: i64, timestamp2: i64) -> bool {
self.get_day_timestamp(timestamp1) == self.get_day_timestamp(timestamp2)
}
fn is_same_session(&self, timestamp1: i64, timestamp2: i64) -> bool {
self.get_session_timestamp(timestamp1) == self.get_session_timestamp(timestamp2)
}
pub fn analysis(&self) -> &PocAnalysis {
&self.analysis
}
pub fn is_ready(&self) -> bool {
self.is_ready
}
pub fn get_active_levels(&self) -> &Vec<PocLevel> {
&self.analysis.active_levels
}
pub fn get_current_poc(&self) -> Option<&PocLevel> {
self.analysis.current_poc.as_ref()
}
pub fn get_market_structure(&self) -> MarketStructure {
self.analysis.market_structure
}
pub fn get_volume_imbalance(&self) -> f64 {
self.analysis.volume_imbalance
}
pub fn has_confluence(&self, price: f64, min_confluence: u32) -> bool {
self.count_poc_confluence(price) >= min_confluence
}
pub fn get_level_strength(&self, price: f64) -> f64 {
let tolerance = self.level_tolerance * 2.0;
for level in &self.analysis.active_levels {
if (price - level.price).abs() <= tolerance {
return level.strength;
}
}
0.0
}
pub fn set_session_hours(&mut self, start_hour: u32, end_hour: u32) {
self.session_start_hour = start_hour;
self.session_end_hour = end_hour;
}
pub fn set_min_volume_threshold(&mut self, threshold: f64) {
self.min_volume_threshold = threshold;
}
pub fn reset(&mut self) {
self.volume_profile.clear();
self.price_levels.clear();
self.daily_volume.clear();
self.session_volume.clear();
self.rolling_volume.clear();
self.bar_history.clear();
self.analysis = PocAnalysis::default();
self.is_ready = false;
self.current_day = 0;
self.current_session = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_bar(time: i64, open: f64, high: f64, low: f64, close: f64, volume: f64) -> Bar {
Bar {
time,
open,
high,
low,
close,
volume,
}
}
#[test]
fn test_poc_detector_creation() {
let detector = PocDetector::new(2, 1000.0, 20);
assert!(!detector.is_ready());
assert_eq!(detector.price_precision, 2);
assert_eq!(detector.min_volume_threshold, 1000.0);
}
#[test]
fn test_poc_level_creation() {
let level = PocLevel::new(100.0, 5000.0, PocType::Daily, 1234567890);
assert_eq!(level.price, 100.0);
assert_eq!(level.volume, 5000.0);
assert_eq!(level.poc_type, PocType::Daily);
assert!(level.is_active);
}
#[test]
fn test_poc_level_touch_detection() {
let level = PocLevel::new(100.0, 5000.0, PocType::Daily, 1234567890);
assert!(level.is_touched(100.5, 99.5, 1.0));
assert!(level.is_touched(101.0, 100.0, 1.0));
assert!(!level.is_touched(102.0, 101.5, 1.0));
assert!(!level.is_touched(98.5, 98.0, 1.0));
}
#[test]
fn test_poc_level_breakout_detection() {
let level = PocLevel::new(100.0, 5000.0, PocType::Daily, 1234567890);
assert!(level.is_broken(102.0, 1.0));
assert!(level.is_broken(98.0, 1.0));
assert!(!level.is_broken(100.5, 1.0));
assert!(!level.is_broken(99.5, 1.0));
}
#[test]
fn test_poc_detector_basic_update() {
let mut detector = PocDetector::new(2, 100.0, 5);
for i in 0..10 {
let bar = create_test_bar(
1234567890 + i * 60,
100.0 + i as f64,
101.0 + i as f64,
99.0 + i as f64,
100.5 + i as f64,
1000.0 + i as f64 * 100.0,
);
detector.update(&bar);
}
let _ = detector.get_current_poc();
let _ = detector.get_market_structure();
}
#[test]
fn test_market_structure_detection() {
let mut detector = PocDetector::new(2, 100.0, 10);
for i in 0..250 {
let price_trend = i as f64 * 0.5; let bar = create_test_bar(
1234567890 + i * 300,
100.0 + price_trend,
101.0 + price_trend,
99.0 + price_trend,
100.5 + price_trend,
1000.0,
);
detector.update(&bar);
}
let structure = detector.get_market_structure();
assert!(matches!(structure,
MarketStructure::Trending |
MarketStructure::Breakout |
MarketStructure::Consolidation
));
}
}