use polars::prelude::*;
use crate::indicators::moving_averages::{calculate_sma, calculate_bollinger_bands};
use std::collections::HashMap;
pub fn calculate_value_zones(
df: &DataFrame,
period: Option<usize>,
num_zones: Option<usize>,
) -> PolarsResult<Series> {
let lookback = period.unwrap_or(1000);
let zones = num_zones.unwrap_or(5);
let high = df.column("high")?.f64()?;
let low = df.column("low")?.f64()?;
let close = df.column("close")?.f64()?;
let mut value_zones = Vec::with_capacity(df.height());
for i in 0..lookback.min(df.height()) {
value_zones.push(0);
}
for i in lookback..df.height() {
let current_close = close.get(i).unwrap_or(f64::NAN);
if current_close.is_nan() {
value_zones.push(0);
continue;
}
let mut min_price = f64::MAX;
let mut max_price = f64::MIN;
for j in (i - lookback + 1)..=i {
let h = high.get(j).unwrap_or(f64::NAN);
let l = low.get(j).unwrap_or(f64::NAN);
if !h.is_nan() && h > max_price {
max_price = h;
}
if !l.is_nan() && l < min_price {
min_price = l;
}
}
if min_price == f64::MAX || max_price == f64::MIN {
value_zones.push(0);
continue;
}
let price_range = max_price - min_price;
let zone_height = price_range / zones as f64;
let mut price_counts = HashMap::new();
for j in (i - lookback + 1)..=i {
let c = close.get(j).unwrap_or(f64::NAN);
if c.is_nan() {
continue;
}
let zone_index = ((c - min_price) / zone_height).floor() as usize;
let zone = zone_index.min(zones - 1) + 1;
*price_counts.entry(zone).or_insert(0) += 1;
}
let mut max_count = 0;
let mut strongest_zone = 0;
for (zone, count) in &price_counts {
if *count > max_count {
max_count = *count;
strongest_zone = *zone;
}
}
let current_zone = ((current_close - min_price) / zone_height).floor() as usize + 1;
let zone_strength = if let Some(count) = price_counts.get(¤t_zone) {
((*count as f64 / max_count as f64) * 5.0).round() as i32
} else {
0 };
value_zones.push(zone_strength);
}
Ok(Series::new("value_zone_strength", value_zones))
}
pub fn calculate_price_density(
df: &DataFrame,
lookback_period: Option<usize>,
bandwidth: Option<f64>,
) -> PolarsResult<Series> {
let lookback = lookback_period.unwrap_or(1000);
let bw = bandwidth.unwrap_or(5.0) / 100.0;
let close = df.column("close")?.f64()?;
let mut density = Vec::with_capacity(df.height());
for i in 0..lookback.min(df.height()) {
density.push(f64::NAN);
}
for i in lookback..df.height() {
let current_close = close.get(i).unwrap_or(f64::NAN);
if current_close.is_nan() {
density.push(f64::NAN);
continue;
}
let mut count_in_band = 0;
let lower_band = current_close * (1.0 - bw);
let upper_band = current_close * (1.0 + bw);
let mut total_valid = 0;
for j in (i - lookback + 1)..=i {
let c = close.get(j).unwrap_or(f64::NAN);
if !c.is_nan() {
total_valid += 1;
if c >= lower_band && c <= upper_band {
count_in_band += 1;
}
}
}
if total_valid > 0 {
let density_pct = (count_in_band as f64 / total_valid as f64) * 100.0;
density.push(density_pct);
} else {
density.push(f64::NAN);
}
}
Ok(Series::new("price_density", density))
}
pub fn identify_value_ranges(
df: &DataFrame,
lookback_period: Option<usize>,
) -> PolarsResult<(Series, Series)> {
let lookback = lookback_period.unwrap_or(1000);
let close = df.column("close")?.f64()?;
let (middle, upper, lower) = calculate_bollinger_bands(df, lookback / 5, 1.5, "close")?;
let middle_vals = middle.f64()?;
let upper_vals = upper.f64()?;
let lower_vals = lower.f64()?;
let mut value_lower = Vec::with_capacity(df.height());
let mut value_upper = Vec::with_capacity(df.height());
for i in 0..lookback.min(df.height()) {
value_lower.push(f64::NAN);
value_upper.push(f64::NAN);
}
for i in lookback..df.height() {
let m = middle_vals.get(i).unwrap_or(f64::NAN);
let u = upper_vals.get(i).unwrap_or(f64::NAN);
let l = lower_vals.get(i).unwrap_or(f64::NAN);
if m.is_nan() || u.is_nan() || l.is_nan() {
value_lower.push(f64::NAN);
value_upper.push(f64::NAN);
continue;
}
let mut prices = Vec::new();
for j in (i - lookback + 1)..=i {
let c = close.get(j).unwrap_or(f64::NAN);
if !c.is_nan() {
prices.push(c);
}
}
if prices.is_empty() {
value_lower.push(f64::NAN);
value_upper.push(f64::NAN);
continue;
}
prices.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let q1_idx = (prices.len() as f64 * 0.25).floor() as usize;
let q3_idx = (prices.len() as f64 * 0.75).floor() as usize;
let q1 = prices[q1_idx];
let q3 = prices[q3_idx];
let lower_bound = (q1 + l) / 2.0;
let upper_bound = (q3 + u) / 2.0;
value_lower.push(lower_bound);
value_upper.push(upper_bound);
}
Ok((
Series::new("value_range_lower", value_lower),
Series::new("value_range_upper", value_upper),
))
}
pub fn calculate_value_range_position(df: &DataFrame) -> PolarsResult<Series> {
for col in ["value_range_lower", "value_range_upper"].iter() {
if !df.schema().contains(col) {
return Err(PolarsError::ComputeError(
format!("Required column '{}' not found", col).into(),
));
}
}
let lower = df.column("value_range_lower")?.f64()?;
let upper = df.column("value_range_upper")?.f64()?;
let close = df.column("close")?.f64()?;
let mut position = Vec::with_capacity(df.height());
for i in 0..df.height() {
let l = lower.get(i).unwrap_or(f64::NAN);
let u = upper.get(i).unwrap_or(f64::NAN);
let c = close.get(i).unwrap_or(f64::NAN);
if l.is_nan() || u.is_nan() || c.is_nan() || l == u {
position.push(f64::NAN);
continue;
}
let pos_pct = ((c - l) / (u - l) * 100.0).max(0.0).min(100.0);
position.push(pos_pct);
}
Ok(Series::new("value_range_position", position))
}
pub fn add_value_zones_analysis(df: &mut DataFrame) -> PolarsResult<()> {
let zones = calculate_value_zones(df, None, None)?;
df.with_column(zones)?;
let density = calculate_price_density(df, None, None)?;
df.with_column(density)?;
let (lower, upper) = identify_value_ranges(df, None)?;
df.with_column(lower)?;
df.with_column(upper)?;
let position = calculate_value_range_position(df)?;
df.with_column(position)?;
Ok(())
}