use polars::prelude::*;
use crate::indicators::moving_averages::{calculate_sma, calculate_ema};
use crate::indicators::trend::calculate_adx;
pub fn calculate_secular_momentum(
df: &DataFrame,
long_period: Option<usize>,
medium_period: Option<usize>,
short_period: Option<usize>,
) -> PolarsResult<Series> {
let long_ma_period = long_period.unwrap_or(500);
let medium_ma_period = medium_period.unwrap_or(200);
let short_ma_period = short_period.unwrap_or(50);
let long_ma = calculate_sma(df, "close", long_ma_period)?;
let medium_ma = calculate_sma(df, "close", medium_ma_period)?;
let short_ma = calculate_sma(df, "close", short_ma_period)?;
let long_ma_vals = long_ma.f64()?;
let medium_ma_vals = medium_ma.f64()?;
let short_ma_vals = short_ma.f64()?;
let close = df.column("close")?.f64()?;
let mut secular_momentum = Vec::with_capacity(df.height());
let min_periods = long_ma_period;
for i in 0..min_periods.min(df.height()) {
secular_momentum.push(0);
}
for i in min_periods..df.height() {
let long_ma_val = long_ma_vals.get(i).unwrap_or(f64::NAN);
let medium_ma_val = medium_ma_vals.get(i).unwrap_or(f64::NAN);
let short_ma_val = short_ma_vals.get(i).unwrap_or(f64::NAN);
let close_val = close.get(i).unwrap_or(f64::NAN);
if long_ma_val.is_nan() || medium_ma_val.is_nan() || short_ma_val.is_nan() || close_val.is_nan() {
secular_momentum.push(0);
continue;
}
let long_lookback = (long_ma_period / 2).min(i);
let long_ma_prev = long_ma_vals.get(i - long_lookback).unwrap_or(long_ma_val);
let long_slope = (long_ma_val - long_ma_prev) / long_ma_prev * 100.0;
let medium_lookback = (medium_ma_period / 2).min(i);
let medium_ma_prev = medium_ma_vals.get(i - medium_lookback).unwrap_or(medium_ma_val);
let medium_slope = (medium_ma_val - medium_ma_prev) / medium_ma_prev * 100.0;
let aligned_bullish = close_val > short_ma_val && short_ma_val > medium_ma_val && medium_ma_val > long_ma_val;
let aligned_bearish = close_val < short_ma_val && short_ma_val < medium_ma_val && medium_ma_val < long_ma_val;
if aligned_bullish && long_slope > 0.5 && medium_slope > 1.0 {
secular_momentum.push(2);
} else if long_slope > 0.0 && medium_slope > 0.0 && medium_ma_val > long_ma_val {
secular_momentum.push(1);
} else if aligned_bearish && long_slope < -0.5 && medium_slope < -1.0 {
secular_momentum.push(-2);
} else if long_slope < 0.0 && medium_slope < 0.0 && medium_ma_val < long_ma_val {
secular_momentum.push(-1);
} else {
secular_momentum.push(0);
}
}
Ok(Series::new("secular_momentum", secular_momentum))
}
pub fn calculate_secular_trend_duration(df: &DataFrame) -> PolarsResult<Series> {
if !df.schema().contains("secular_momentum") {
return Err(PolarsError::ComputeError(
"secular_momentum column not found. Calculate secular momentum first.".into(),
));
}
let momentum = df.column("secular_momentum")?.i32()?;
let mut duration = Vec::with_capacity(df.height());
if df.height() > 0 {
duration.push(0);
}
for i in 1..df.height() {
let current = momentum.get(i).unwrap_or(0);
let previous = duration[i - 1];
let same_direction = (current > 0 && momentum.get(i - 1).unwrap_or(0) > 0) ||
(current < 0 && momentum.get(i - 1).unwrap_or(0) < 0);
if current != 0 && same_direction {
duration.push(previous + 1);
} else {
duration.push(if current != 0 { 1 } else { 0 });
}
}
Ok(Series::new("secular_trend_duration", duration))
}
pub fn calculate_secular_trend_strength(
df: &DataFrame,
adx_period: Option<usize>,
) -> PolarsResult<Series> {
if !df.schema().contains("secular_momentum") {
return Err(PolarsError::ComputeError(
"secular_momentum column not found. Calculate secular momentum first.".into(),
));
}
let period = adx_period.unwrap_or(200);
let adx = calculate_adx(df, period)?;
let adx_vals = adx.f64()?;
let momentum = df.column("secular_momentum")?.i32()?;
let mut strength = Vec::with_capacity(df.height());
for i in 0..period.min(df.height()) {
strength.push(f64::NAN);
}
for i in period..df.height() {
let adx_val = adx_vals.get(i).unwrap_or(f64::NAN);
let momentum_val = momentum.get(i).unwrap_or(0);
if adx_val.is_nan() {
strength.push(f64::NAN);
continue;
}
let mut trend_strength = adx_val;
let momentum_multiplier = match momentum_val.abs() {
2 => 1.25, 1 => 1.0, _ => 0.75, };
trend_strength *= momentum_multiplier;
if df.schema().contains("secular_trend_duration") {
let duration = df.column("secular_trend_duration")?.i32()?;
let duration_val = duration.get(i).unwrap_or(0);
if duration_val > 500 {
trend_strength *= 1.2; } else if duration_val > 250 {
trend_strength *= 1.1; }
}
strength.push(trend_strength.min(100.0));
}
Ok(Series::new("secular_trend_strength", strength))
}
pub fn identify_secular_levels(
df: &DataFrame,
lookback_years: Option<usize>,
) -> PolarsResult<(Series, Series)> {
let trading_days_per_year = 252;
let years = lookback_years.unwrap_or(10);
let lookback = years * trading_days_per_year;
let high = df.column("high")?.f64()?;
let low = df.column("low")?.f64()?;
let close = df.column("close")?.f64()?;
let mut support_levels = Vec::with_capacity(df.height());
let mut resistance_levels = Vec::with_capacity(df.height());
let min_prominence_pct = 0.05;
for i in 0..lookback.min(df.height()) {
support_levels.push(f64::NAN);
resistance_levels.push(f64::NAN);
}
for i in lookback..df.height() {
let current_close = close.get(i).unwrap_or(f64::NAN);
if current_close.is_nan() {
support_levels.push(f64::NAN);
resistance_levels.push(f64::NAN);
continue;
}
let mut best_support = f64::NAN;
let mut support_strength = 0;
let mut best_resistance = f64::NAN;
let mut resistance_strength = 0;
for j in (i - lookback + 60)..i {
let window_size = 30;
if j < window_size || j + window_size >= i {
continue;
}
let current_low = low.get(j).unwrap_or(f64::NAN);
let mut is_pivot_low = true;
for k in (j - window_size)..(j + window_size) {
let check_low = low.get(k).unwrap_or(f64::NAN);
if !check_low.is_nan() && check_low < current_low {
is_pivot_low = false;
break;
}
}
if is_pivot_low && !current_low.is_nan() {
let mut touches = 0;
let level_tolerance = current_low * 0.01;
for k in (j + window_size)..i {
let check_low = low.get(k).unwrap_or(f64::NAN);
if !check_low.is_nan() && (check_low - current_low).abs() <= level_tolerance {
touches += 1;
}
}
if touches > 2 || (current_close - current_low) / current_close >= min_prominence_pct {
if support_strength < touches {
best_support = current_low;
support_strength = touches;
}
}
}
let current_high = high.get(j).unwrap_or(f64::NAN);
let mut is_pivot_high = true;
for k in (j - window_size)..(j + window_size) {
let check_high = high.get(k).unwrap_or(f64::NAN);
if !check_high.is_nan() && check_high > current_high {
is_pivot_high = false;
break;
}
}
if is_pivot_high && !current_high.is_nan() {
let mut touches = 0;
let level_tolerance = current_high * 0.01;
for k in (j + window_size)..i {
let check_high = high.get(k).unwrap_or(f64::NAN);
if !check_high.is_nan() && (check_high - current_high).abs() <= level_tolerance {
touches += 1;
}
}
if touches > 2 || (current_high - current_close) / current_close >= min_prominence_pct {
if resistance_strength < touches {
best_resistance = current_high;
resistance_strength = touches;
}
}
}
}
support_levels.push(best_support);
resistance_levels.push(best_resistance);
}
Ok((
Series::new("secular_support", support_levels),
Series::new("secular_resistance", resistance_levels),
))
}
pub fn add_secular_trend_analysis(df: &mut DataFrame) -> PolarsResult<()> {
let momentum = calculate_secular_momentum(df, None, None, None)?;
df.with_column(momentum)?;
let duration = calculate_secular_trend_duration(df)?;
df.with_column(duration)?;
let strength = calculate_secular_trend_strength(df, None)?;
df.with_column(strength)?;
let (support, resistance) = identify_secular_levels(df, None)?;
df.with_column(support)?;
df.with_column(resistance)?;
Ok(())
}