use polars::prelude::*;
use std::collections::HashMap;
pub mod options_trading {
use super::*;
struct BlackScholes {
price: f64,
strike: f64,
time_to_expiry: f64, risk_free_rate: f64,
volatility: f64,
}
impl BlackScholes {
fn d1(&self) -> f64 {
let numerator = (self.price / self.strike).ln() +
(self.risk_free_rate + 0.5 * self.volatility.powi(2)) * self.time_to_expiry;
let denominator = self.volatility * self.time_to_expiry.sqrt();
numerator / denominator
}
fn d2(&self) -> f64 {
self.d1() - self.volatility * self.time_to_expiry.sqrt()
}
fn norm_cdf(x: f64) -> f64 {
if x > 6.0 {
1.0
} else if x < -6.0 {
0.0
} else {
let b1 = 0.31938153;
let b2 = -0.356563782;
let b3 = 1.781477937;
let b4 = -1.821255978;
let b5 = 1.330274429;
let p = 0.2316419;
let c = 0.39894228;
let t = 1.0 / (1.0 + p * x.abs());
let poly = t * (b1 + t * (b2 + t * (b3 + t * (b4 + t * b5))));
if x >= 0.0 {
1.0 - c * (-x * x / 2.0).exp() * poly
} else {
c * (-x * x / 2.0).exp() * poly
}
}
}
pub fn call_price(&self) -> f64 {
let d1 = self.d1();
let d2 = self.d2();
self.price * Self::norm_cdf(d1) -
self.strike * (-self.risk_free_rate * self.time_to_expiry).exp() * Self::norm_cdf(d2)
}
pub fn put_price(&self) -> f64 {
let d1 = self.d1();
let d2 = self.d2();
self.strike * (-self.risk_free_rate * self.time_to_expiry).exp() * Self::norm_cdf(-d2) -
self.price * Self::norm_cdf(-d1)
}
}
pub fn calculate_implied_volatility(
price: f64,
strike: f64,
time_to_expiry: f64,
risk_free_rate: f64,
option_price: f64,
is_call: bool,
) -> f64 {
let mut low = 0.001;
let mut high = 4.0; let mut mid;
let accuracy = 0.0001;
let max_iterations = 100;
for _ in 0..max_iterations {
mid = (low + high) / 2.0;
let model = BlackScholes {
price,
strike,
time_to_expiry,
risk_free_rate,
volatility: mid,
};
let model_price = if is_call {
model.call_price()
} else {
model.put_price()
};
let price_diff = model_price - option_price;
if price_diff.abs() < accuracy {
return mid;
}
if price_diff > 0.0 {
high = mid;
} else {
low = mid;
}
}
(low + high) / 2.0
}
pub fn analyze_options_chain(
underlying_price: f64,
options_data: &DataFrame,
) -> PolarsResult<DataFrame> {
Ok(options_data.clone())
}
pub fn evaluate_options_strategy(
underlying_price: f64,
strategy_legs: Vec<(f64, f64, bool, i32)>, price_range: (f64, f64)
) -> PolarsResult<DataFrame> {
let price_points: Vec<f64> = (0..21)
.map(|i| price_range.0 + i as f64 * (price_range.1 - price_range.0) / 20.0)
.collect();
let mut pnl_values = Vec::new();
for price in &price_points {
let mut strategy_pnl = 0.0;
for &(strike, _expiry, is_call, quantity) in &strategy_legs {
let intrinsic = if is_call {
(*price - strike).max(0.0)
} else {
(strike - *price).max(0.0)
};
strategy_pnl += intrinsic * quantity as f64;
}
pnl_values.push(strategy_pnl);
}
let df = DataFrame::new(vec![
Series::new("price".into(), price_points).into(),
Series::new("pnl".into(), pnl_values).into()
])?;
Ok(df)
}
}
mod volatility_analysis;
mod greeks;
mod spreads;
mod volume_analysis;
mod skew_analysis;
mod sentiment;
mod gamma_exposure;
pub use volatility_analysis::*;
pub use greeks::*;
pub use spreads::*;
pub use volume_analysis::*;
pub use skew_analysis::*;
pub use sentiment::{calculate_put_call_oi_ratio, calculate_skew_sentiment};
pub use gamma_exposure::calculate_gamma_exposure;
pub fn add_options_indicators(df: &DataFrame) -> PolarsResult<DataFrame> {
let mut result = df.clone();
volatility_analysis::add_volatility_indicators(&mut result)?;
greeks::add_greeks_indicators(&mut result)?;
spreads::add_spread_indicators(&mut result)?;
volume_analysis::add_volume_indicators(&mut result)?;
skew_analysis::add_skew_indicators(&mut result)?;
Ok(result)
}
pub fn generate_options_trading_signals(df: &DataFrame) -> PolarsResult<Series> {
let required_indicators = [
"iv_percentile", "iv_forecast", "delta",
"gamma_exposure", "volume_oi_ratio"
];
for indicator in required_indicators {
if !df.schema().contains(indicator) {
return Err(PolarsError::ComputeError(
format!("Required indicator '{}' not found", indicator).into(),
));
}
}
let iv_percentile = df.column("iv_percentile")?.f64()?;
let iv_forecast = df.column("iv_forecast")?.f64()?;
let delta = df.column("delta")?.f64()?;
let gamma_exposure = df.column("gamma_exposure")?.f64()?;
let volume_oi_ratio = df.column("volume_oi_ratio")?.f64()?;
let has_skew = df.schema().contains("volatility_skew");
let vol_skew = if has_skew {
Some(df.column("volatility_skew")?.f64()?)
} else {
None
};
let mut combined_signals = Vec::with_capacity(df.height());
for i in 0..df.height() {
let iv_pct = iv_percentile.get(i).unwrap_or(f64::NAN);
let iv_fcst = iv_forecast.get(i).unwrap_or(f64::NAN);
let delta_val = delta.get(i).unwrap_or(f64::NAN);
let gamma_val = gamma_exposure.get(i).unwrap_or(f64::NAN);
let vol_oi = volume_oi_ratio.get(i).unwrap_or(f64::NAN);
if iv_pct.is_nan() || iv_fcst.is_nan() || delta_val.is_nan() || gamma_val.is_nan() || vol_oi.is_nan() {
combined_signals.push(0);
continue;
}
let mut bullish_count = 0;
let mut bearish_count = 0;
if iv_pct > 80.0 { bearish_count += 1; } if iv_pct < 20.0 { bullish_count += 1; }
if iv_fcst > iv_pct * 1.1 { bullish_count += 1; } if iv_fcst < iv_pct * 0.9 { bearish_count += 1; }
let abs_delta = delta_val.abs();
if abs_delta > 0.6 { bullish_count += 1; } if abs_delta < 0.3 { bearish_count += 1; }
if gamma_val > 0.05 { bullish_count += 1; } if gamma_val < 0.01 { bearish_count += 1; }
if vol_oi > 1.5 { bullish_count += 1; } if vol_oi < 0.5 { bearish_count += 1; }
if let Some(skew) = &vol_skew {
let skew_val = skew.get(i).unwrap_or(f64::NAN);
if !skew_val.is_nan() {
if skew_val > 0.1 { bearish_count += 1; } if skew_val < -0.1 { bullish_count += 1; } }
}
if bullish_count >= 3 && bearish_count == 0 {
combined_signals.push(2); } else if bullish_count > bearish_count {
combined_signals.push(1); } else if bearish_count >= 3 && bullish_count == 0 {
combined_signals.push(-2); } else if bearish_count > bullish_count {
combined_signals.push(-1); } else {
combined_signals.push(0); }
}
Ok(Series::new("options_trading_signal".into(), combined_signals))
}
pub fn calculate_options_position_sizing(
df: &DataFrame,
risk_capital: f64,
max_loss_pct: f64,
) -> PolarsResult<Series> {
for col in ["option_price", "max_loss"].iter() {
if !df.schema().contains(col) {
return Err(PolarsError::ComputeError(
format!("Required column '{}' not found", col).into(),
));
}
}
let price = df.column("option_price")?.f64()?;
let max_loss = df.column("max_loss")?.f64()?;
let has_theta = df.schema().contains("theta");
let theta = if has_theta {
Some(df.column("theta")?.f64()?)
} else {
None
};
let has_gamma = df.schema().contains("gamma");
let gamma = if has_gamma {
Some(df.column("gamma")?.f64()?)
} else {
None
};
let mut position_sizes = Vec::with_capacity(df.height());
for i in 0..df.height() {
let option_price = price.get(i).unwrap_or(f64::NAN);
let option_max_loss = max_loss.get(i).unwrap_or(f64::NAN);
if option_price.is_nan() || option_max_loss.is_nan() || option_price <= 0.0 || option_max_loss <= 0.0 {
position_sizes.push(0);
continue;
}
let max_risk = risk_capital * max_loss_pct / 100.0;
let mut contracts = (max_risk / option_max_loss).floor();
if let Some(theta_vals) = &theta {
let theta_val = theta_vals.get(i).unwrap_or(f64::NAN);
if !theta_val.is_nan() && theta_val < 0.0 {
let theta_factor = (1.0 + (theta_val.abs() / option_price).min(0.5));
contracts = (contracts / theta_factor).floor();
}
}
if let Some(gamma_vals) = &gamma {
let gamma_val = gamma_vals.get(i).unwrap_or(f64::NAN);
if !gamma_val.is_nan() && gamma_val > 0.05 {
let gamma_factor = 1.0 + (gamma_val * 10.0).min(1.0);
contracts = (contracts / gamma_factor).floor();
}
}
if contracts > 0.0 && contracts < 1.0 {
contracts = 1.0;
}
position_sizes.push(contracts as i32);
}
Ok(Series::new("suggested_contracts".into(), position_sizes))
}