use polars::prelude::*;
use polars::frame::DataFrame;
use std::collections::HashMap;
pub fn calculate_volume_oi_ratio(
df: &DataFrame,
volume_column: &str,
open_interest_column: &str,
) -> PolarsResult<Series> {
let volume = df.column(volume_column)?.f64()?;
let open_interest = df.column(open_interest_column)?.f64()?;
let len = df.height();
let mut ratio = vec![f64::NAN; len];
for i in 0..len {
let vol = volume.get(i).unwrap_or(f64::NAN);
let oi = open_interest.get(i).unwrap_or(f64::NAN);
if vol.is_nan() || oi.is_nan() || oi <= 0.0 {
continue;
}
ratio[i] = vol / oi;
}
Ok(Series::new("volume_oi_ratio".into(), ratio))
}
pub fn calculate_put_call_ratio(
df: &DataFrame,
volume_column: &str,
is_call_column: &str,
window: usize,
) -> PolarsResult<Series> {
let volume = df.column(volume_column)?.f64()?;
let is_call = df.column(is_call_column)?.bool()?;
let len = df.height();
let mut put_call_ratio = vec![f64::NAN; len];
let mut call_volumes = Vec::with_capacity(len);
let mut put_volumes = Vec::with_capacity(len);
for i in 0..len {
let vol = volume.get(i).unwrap_or(f64::NAN);
let call = is_call.get(i).unwrap_or(false);
if vol.is_nan() {
call_volumes.push(0.0);
put_volumes.push(0.0);
continue;
}
if call {
call_volumes.push(vol);
put_volumes.push(0.0);
} else {
call_volumes.push(0.0);
put_volumes.push(vol);
}
}
for i in 0..len {
if i < window - 1 {
continue;
}
let mut call_sum = 0.0;
let mut put_sum = 0.0;
for j in (i - (window - 1))..=i {
call_sum += call_volumes[j];
put_sum += put_volumes[j];
}
if call_sum > 0.0 {
put_call_ratio[i] = put_sum / call_sum;
}
}
Ok(Series::new("put_call_ratio".into(), put_call_ratio))
}
pub fn calculate_unusual_activity(
df: &DataFrame,
volume_column: &str,
avg_volume_column: &str,
open_interest_column: &str,
) -> PolarsResult<Series> {
let volume = df.column(volume_column)?.f64()?;
let avg_volume = df.column(avg_volume_column)?.f64()?;
let open_interest = df.column(open_interest_column)?.f64()?;
let len = df.height();
let mut unusual_score = vec![f64::NAN; len];
for i in 0..len {
let vol = volume.get(i).unwrap_or(f64::NAN);
let avg_vol = avg_volume.get(i).unwrap_or(f64::NAN);
let oi = open_interest.get(i).unwrap_or(f64::NAN);
if vol.is_nan() || avg_vol.is_nan() || oi.is_nan() || avg_vol <= 0.0 || oi <= 0.0 {
continue;
}
let volume_multiple = vol / avg_vol;
let vol_oi_ratio = vol / oi;
unusual_score[i] = volume_multiple * vol_oi_ratio;
}
Ok(Series::new("unusual_activity".into(), unusual_score))
}
pub fn calculate_oi_change(
df: &DataFrame,
open_interest_column: &str,
) -> PolarsResult<Series> {
let open_interest = df.column(open_interest_column)?.f64()?;
let len = df.height();
let mut oi_change = vec![f64::NAN; len];
for i in 1..len {
let current_oi = open_interest.get(i).unwrap_or(f64::NAN);
let prev_oi = open_interest.get(i-1).unwrap_or(f64::NAN);
if current_oi.is_nan() || prev_oi.is_nan() || prev_oi <= 0.0 {
continue;
}
let absolute_change = current_oi - prev_oi;
oi_change[i] = (absolute_change / prev_oi) * 100.0;
}
Ok(Series::new("oi_change_pct".into(), oi_change))
}
pub fn calculate_options_money_flow(
df: &DataFrame,
volume_column: &str,
price_column: &str,
is_call_column: &str,
buy_probability_column: &str,
) -> PolarsResult<Series> {
let volume = df.column(volume_column)?.f64()?;
let price = df.column(price_column)?.f64()?;
let is_call = df.column(is_call_column)?.bool()?;
let buy_probability = df.column(buy_probability_column)?.f64()?;
let len = df.height();
let mut money_flow = vec![f64::NAN; len];
for i in 0..len {
let vol = volume.get(i).unwrap_or(f64::NAN);
let pr = price.get(i).unwrap_or(f64::NAN);
let call = is_call.get(i).unwrap_or(false);
let buy_prob = buy_probability.get(i).unwrap_or(f64::NAN);
if vol.is_nan() || pr.is_nan() || buy_prob.is_nan() {
continue;
}
let dollar_value = vol * pr * 100.0;
let direction = if call {
buy_prob * 2.0 - 1.0
} else {
1.0 - buy_prob * 2.0
};
money_flow[i] = dollar_value * direction;
}
Ok(Series::new("options_money_flow".into(), money_flow))
}
pub fn add_volume_indicators(df: &mut DataFrame) -> PolarsResult<()> {
if df.schema().contains("volume") && df.schema().contains("open_interest") {
let vol_oi = calculate_volume_oi_ratio(df, "volume", "open_interest")?;
df.with_column(vol_oi)?;
let oi_change = calculate_oi_change(df, "open_interest")?;
df.with_column(oi_change)?;
if df.schema().contains("is_call") {
let put_call = calculate_put_call_ratio(df, "volume", "is_call", 5)?;
df.with_column(put_call)?;
}
if df.schema().contains("avg_volume") {
let unusual = calculate_unusual_activity(df, "volume", "avg_volume", "open_interest")?;
df.with_column(unusual)?;
}
if df.schema().contains("price") && df.schema().contains("is_call") &&
df.schema().contains("buy_probability") {
let money_flow = calculate_options_money_flow(
df, "volume", "price", "is_call", "buy_probability"
)?;
df.with_column(money_flow)?;
}
}
Ok(())
}