use polars::prelude::*;
use std::collections::HashMap;
pub fn identify_key_levels(
df: &DataFrame,
lookback_period: Option<usize>,
price_tolerance: Option<f64>,
min_touches: Option<usize>,
) -> PolarsResult<(Vec<f64>, Vec<f64>)> {
let lookback = lookback_period.unwrap_or(90);
let tolerance = price_tolerance.unwrap_or(1.0) / 100.0; let touches = min_touches.unwrap_or(2);
let high = df.column("high")?.f64()?;
let low = df.column("low")?.f64()?;
let close = df.column("close")?.f64()?;
let start_idx = if df.height() > lookback {
df.height() - lookback
} else {
0
};
let mut swing_highs = Vec::new();
let mut swing_lows = Vec::new();
let min_bars = 2;
for i in (start_idx + min_bars)..(df.height().saturating_sub(min_bars)) {
let mut is_swing_high = true;
for j in 1..=min_bars {
if high.get(i).unwrap_or(f64::NAN) <= high.get(i - j).unwrap_or(f64::NAN) ||
high.get(i).unwrap_or(f64::NAN) <= high.get(i + j).unwrap_or(f64::NAN) {
is_swing_high = false;
break;
}
}
if is_swing_high {
swing_highs.push((i, high.get(i).unwrap_or(f64::NAN)));
}
let mut is_swing_low = true;
for j in 1..=min_bars {
if low.get(i).unwrap_or(f64::NAN) >= low.get(i - j).unwrap_or(f64::NAN) ||
low.get(i).unwrap_or(f64::NAN) >= low.get(i + j).unwrap_or(f64::NAN) {
is_swing_low = false;
break;
}
}
if is_swing_low {
swing_lows.push((i, low.get(i).unwrap_or(f64::NAN)));
}
}
let mut resistance_clusters: HashMap<usize, Vec<f64>> = HashMap::new();
let mut support_clusters: HashMap<usize, Vec<f64>> = HashMap::new();
let mut next_cluster_id = 0;
for (_, price) in &swing_highs {
let mut assigned = false;
let mut target_cluster_id = 0;
for (cluster_id, prices) in &resistance_clusters {
let cluster_avg = prices.iter().sum::<f64>() / prices.len() as f64;
if (price - cluster_avg).abs() < tolerance {
assigned = true;
target_cluster_id = *cluster_id;
break;
}
}
if assigned {
if let Some(cluster) = resistance_clusters.get_mut(&target_cluster_id) {
cluster.push(*price);
}
} else {
let new_cluster_id = resistance_clusters.len();
resistance_clusters.insert(new_cluster_id, vec![*price]);
}
}
for (_, price) in &swing_lows {
let mut assigned = false;
let mut target_cluster_id = 0;
for (cluster_id, prices) in &support_clusters {
let cluster_avg = prices.iter().sum::<f64>() / prices.len() as f64;
if (price - cluster_avg).abs() < tolerance {
assigned = true;
target_cluster_id = *cluster_id;
break;
}
}
if assigned {
if let Some(cluster) = support_clusters.get_mut(&target_cluster_id) {
cluster.push(*price);
}
} else {
let new_cluster_id = support_clusters.len();
support_clusters.insert(new_cluster_id, vec![*price]);
}
}
let mut resistance_levels = Vec::new();
for (_, prices) in resistance_clusters {
if prices.len() >= touches {
let avg_price = prices.iter().sum::<f64>() / prices.len() as f64;
resistance_levels.push(avg_price);
}
}
let mut support_levels = Vec::new();
for (_, prices) in support_clusters {
if prices.len() >= touches {
let avg_price = prices.iter().sum::<f64>() / prices.len() as f64;
support_levels.push(avg_price);
}
}
resistance_levels.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
support_levels.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok((support_levels, resistance_levels))
}
pub fn calculate_nearest_levels(
df: &DataFrame,
support_levels: &[f64],
resistance_levels: &[f64],
) -> PolarsResult<(Series, Series)> {
let close = df.column("close")?.f64()?;
let mut nearest_support = Vec::with_capacity(df.height());
let mut nearest_resistance = Vec::with_capacity(df.height());
for i in 0..df.height() {
let price = close.get(i).unwrap_or(f64::NAN);
if price.is_nan() {
nearest_support.push(f64::NAN);
nearest_resistance.push(f64::NAN);
continue;
}
let mut best_support = f64::NAN;
let mut best_support_diff = f64::MAX;
for &level in support_levels {
if level < price {
let diff = price - level;
if diff < best_support_diff {
best_support_diff = diff;
best_support = level;
}
}
}
let mut best_resistance = f64::NAN;
let mut best_resistance_diff = f64::MAX;
for &level in resistance_levels {
if level > price {
let diff = level - price;
if diff < best_resistance_diff {
best_resistance_diff = diff;
best_resistance = level;
}
}
}
nearest_support.push(best_support);
nearest_resistance.push(best_resistance);
}
Ok((
Series::new("nearest_support", nearest_support),
Series::new("nearest_resistance", nearest_resistance),
))
}
pub fn calculate_level_strength(
df: &DataFrame,
support_level: f64,
resistance_level: f64,
price_tolerance: Option<f64>,
) -> PolarsResult<(i32, i32)> {
let tolerance = price_tolerance.unwrap_or(0.5) / 100.0;
let high = df.column("high")?.f64()?;
let low = df.column("low")?.f64()?;
let close = df.column("close")?.f64()?;
let mut support_tests = 0;
let mut support_breaks = 0;
let mut resistance_tests = 0;
let mut resistance_breaks = 0;
let support_lower = support_level * (1.0 - tolerance);
let support_upper = support_level * (1.0 + tolerance);
let resistance_lower = resistance_level * (1.0 - tolerance);
let resistance_upper = resistance_level * (1.0 + tolerance);
for i in 1..df.height() {
let prev_close = close.get(i - 1).unwrap_or(f64::NAN);
let curr_close = close.get(i).unwrap_or(f64::NAN);
let curr_low = low.get(i).unwrap_or(f64::NAN);
let curr_high = high.get(i).unwrap_or(f64::NAN);
if curr_low.is_nan() || curr_high.is_nan() || prev_close.is_nan() || curr_close.is_nan() {
continue;
}
if curr_low >= support_lower && curr_low <= support_upper {
support_tests += 1;
if curr_close < support_lower {
support_breaks += 1;
}
}
if curr_high >= resistance_lower && curr_high <= resistance_upper {
resistance_tests += 1;
if curr_close > resistance_upper {
resistance_breaks += 1;
}
}
}
let support_strength = if support_tests == 0 {
0 } else {
let respect_ratio = 1.0 - (support_breaks as f64 / support_tests as f64);
let num_tests_factor = (support_tests as f64).min(5.0) / 5.0;
let strength = ((respect_ratio * 0.7 + num_tests_factor * 0.3) * 5.0).round() as i32;
strength.max(1).min(5)
};
let resistance_strength = if resistance_tests == 0 {
0 } else {
let respect_ratio = 1.0 - (resistance_breaks as f64 / resistance_tests as f64);
let num_tests_factor = (resistance_tests as f64).min(5.0) / 5.0;
let strength = ((respect_ratio * 0.7 + num_tests_factor * 0.3) * 5.0).round() as i32;
strength.max(1).min(5)
};
Ok((support_strength, resistance_strength))
}
pub fn calculate_risk_reward_ratio(df: &DataFrame) -> PolarsResult<Series> {
for col in ["close", "nearest_support", "nearest_resistance"].iter() {
if !df.schema().contains(*col) {
return Err(PolarsError::ComputeError(
format!("Required column '{}' not found", col).into(),
));
}
}
let close = df.column("close")?.f64()?;
let support = df.column("nearest_support")?.f64()?;
let resistance = df.column("nearest_resistance")?.f64()?;
let mut risk_reward = Vec::with_capacity(df.height());
for i in 0..df.height() {
let price = close.get(i).unwrap_or(f64::NAN);
let support_level = support.get(i).unwrap_or(f64::NAN);
let resistance_level = resistance.get(i).unwrap_or(f64::NAN);
if price.is_nan() || support_level.is_nan() || resistance_level.is_nan() {
risk_reward.push(f64::NAN);
continue;
}
let potential_reward = resistance_level - price;
let potential_risk = price - support_level;
if potential_risk > 0.0 {
let ratio = potential_reward / potential_risk;
risk_reward.push(ratio);
} else {
risk_reward.push(f64::NAN); }
}
Ok(Series::new("risk_reward_ratio", risk_reward))
}
pub fn add_support_resistance_analysis(df: &mut DataFrame) -> PolarsResult<()> {
let (support_levels, resistance_levels) = identify_key_levels(df, None, None, None)?;
let (nearest_support, nearest_resistance) = calculate_nearest_levels(
df, &support_levels, &resistance_levels
)?;
df.with_column(nearest_support)?;
df.with_column(nearest_resistance)?;
let risk_reward = calculate_risk_reward_ratio(df)?;
df.with_column(risk_reward)?;
let mut all_support = String::new();
for level in support_levels {
all_support.push_str(&format!("{:.2},", level));
}
let mut all_resistance = String::new();
for level in resistance_levels {
all_resistance.push_str(&format!("{:.2},", level));
}
let all_support_series = Series::new(
"all_support_levels",
vec![all_support; df.height()]
);
let all_resistance_series = Series::new(
"all_resistance_levels",
vec![all_resistance; df.height()]
);
df.with_column(all_support_series)?;
df.with_column(all_resistance_series)?;
Ok(())
}