use positive::{Positive, pos_or_panic};
use crate::error::probability::ProbabilityError;
use crate::model::ProfitLossRange;
use crate::pricing::payoff::Profit;
use crate::strategies::base::Strategies;
use crate::strategies::probabilities::analysis::StrategyProbabilityAnalysis;
use crate::strategies::probabilities::utils::{
PriceTrend, VolatilityAdjustment, calculate_single_point_probability,
};
use num_traits::ToPrimitive;
use rust_decimal::Decimal;
use tracing::warn;
pub trait ProbabilityAnalysis: Strategies + Profit {
fn analyze_probabilities(
&self,
volatility_adj: Option<VolatilityAdjustment>,
trend: Option<PriceTrend>,
) -> Result<StrategyProbabilityAnalysis, ProbabilityError> {
let break_even_points = self.get_break_even_points().unwrap();
if volatility_adj.is_none() && trend.is_none() {
let probability_of_profit = self.probability_of_profit(None, None)?;
let expected_value = self.expected_value(None, None)?;
return Ok(StrategyProbabilityAnalysis {
probability_of_profit,
probability_of_max_profit: Positive::ZERO, probability_of_max_loss: Positive::ZERO, expected_value,
break_even_points: break_even_points.to_vec(),
risk_reward_ratio: Positive::new_decimal(self.get_profit_ratio().unwrap())
.unwrap_or(Positive::ZERO),
});
}
let probability_of_profit =
self.probability_of_profit(volatility_adj.clone(), trend.clone())?;
let expected_value = self.expected_value(volatility_adj.clone(), trend.clone())?;
let (prob_max_profit, prob_max_loss) =
self.calculate_extreme_probabilities(volatility_adj, trend)?;
let risk_reward_ratio =
Positive::new_decimal(self.get_profit_ratio().unwrap()).unwrap_or(Positive::ZERO);
Ok(StrategyProbabilityAnalysis {
probability_of_profit,
probability_of_max_profit: prob_max_profit,
probability_of_max_loss: prob_max_loss,
expected_value,
break_even_points: break_even_points.to_vec(),
risk_reward_ratio,
})
}
fn expected_value(
&self,
volatility_adj: Option<VolatilityAdjustment>,
trend: Option<PriceTrend>,
) -> Result<Positive, ProbabilityError> {
if let Some(ref vol_adj) = volatility_adj
&& vol_adj.base_volatility == Positive::ZERO
&& vol_adj.std_dev_adjustment == Positive::ZERO
{
let current_profit = self.calculate_profit_at(self.get_underlying_price())?;
return if current_profit <= Decimal::ZERO {
Ok(Positive::ZERO)
} else {
Ok(Positive::new_decimal(current_profit)?)
};
}
let step = self.get_underlying_price() / 100.0;
let range = self.get_best_range_to_show(step).unwrap();
let expiration = *self.get_expiration().values().next().unwrap();
let mut probabilities = Vec::with_capacity(range.len());
let mut last_prob = Decimal::ZERO;
for price in range.iter() {
let prob = calculate_single_point_probability(
self.get_underlying_price(),
price,
volatility_adj.clone(),
trend.clone(),
expiration,
None,
)?;
let marginal_prob = prob.0 - last_prob;
probabilities.push(marginal_prob);
last_prob = prob.0.to_dec();
}
let expected_value =
range
.iter()
.zip(probabilities.iter())
.fold(0.0, |acc, (price, prob)| {
acc + self.calculate_profit_at(price).unwrap().to_f64().unwrap() * *prob
});
let total_prob: f64 = probabilities.iter().map(|p| p.to_f64()).sum();
if (total_prob - 1.0).abs() > 0.05 {
warn!(
"Sum of probabilities ({}) deviates significantly from 1.0",
total_prob
);
}
if expected_value <= 0.0 {
Ok(Positive::ZERO)
} else {
let trend_adjustment = trend.map_or(1.0, |t| 1.0 / (1.0 + t.drift_rate.abs()));
Ok(pos_or_panic!(expected_value * trend_adjustment))
}
}
fn probability_of_profit(
&self,
volatility_adj: Option<VolatilityAdjustment>,
trend: Option<PriceTrend>,
) -> Result<Positive, ProbabilityError> {
let mut sum_of_probabilities = Positive::ZERO;
let ranges = self.get_profit_ranges()?;
let option = self.one_option();
let expiration = option.expiration_date;
let risk_free_rate = option.risk_free_rate;
let underlying_price = option.underlying_price;
for mut range in ranges {
range.calculate_probability(
&underlying_price,
volatility_adj.clone(),
trend.clone(),
&expiration,
Some(risk_free_rate),
)?;
sum_of_probabilities += range.probability;
}
Ok(sum_of_probabilities)
}
fn probability_of_loss(
&self,
volatility_adj: Option<VolatilityAdjustment>,
trend: Option<PriceTrend>,
) -> Result<Positive, ProbabilityError> {
let mut sum_of_probabilities = Positive::ZERO;
let ranges = self.get_loss_ranges()?;
let option = self.one_option();
let expiration = option.expiration_date;
let risk_free_rate = option.risk_free_rate;
let underlying_price = option.underlying_price;
for mut range in ranges {
range.calculate_probability(
&underlying_price,
volatility_adj.clone(),
trend.clone(),
&expiration,
Some(risk_free_rate),
)?;
sum_of_probabilities += range.probability;
}
Ok(sum_of_probabilities)
}
fn calculate_extreme_probabilities(
&self,
volatility_adj: Option<VolatilityAdjustment>,
trend: Option<PriceTrend>,
) -> Result<(Positive, Positive), ProbabilityError> {
let profit_ranges = self.get_profit_ranges()?;
let loss_ranges = self.get_loss_ranges()?;
let max_profit_range = profit_ranges
.iter()
.find(|range| range.upper_bound.is_none());
let max_loss_range = loss_ranges.iter().find(|range| range.lower_bound.is_none());
let expiration = *self.get_expiration().values().next().unwrap();
let risk_free_rate = *self.get_risk_free_rate().values().next().unwrap();
let underlying_price = self.get_underlying_price();
let mut max_profit_prob = Positive::ZERO;
if let Some(range) = max_profit_range {
let mut range_clone = range.clone();
range_clone.calculate_probability(
underlying_price,
volatility_adj.clone(),
trend.clone(),
expiration,
Some(*risk_free_rate),
)?;
max_profit_prob = range_clone.probability;
}
let mut max_loss_prob = Positive::ZERO;
if let Some(range) = max_loss_range {
let mut range_clone = range.clone();
range_clone.calculate_probability(
underlying_price,
volatility_adj,
trend,
expiration,
Some(*risk_free_rate),
)?;
max_loss_prob = range_clone.probability;
}
Ok((max_profit_prob, max_loss_prob))
}
fn get_profit_ranges(&self) -> Result<Vec<ProfitLossRange>, ProbabilityError>;
fn get_loss_ranges(&self) -> Result<Vec<ProfitLossRange>, ProbabilityError>;
}
#[cfg(test)]
mod tests_probability_analysis {
use super::*;
use crate::ExpirationDate;
use crate::strategies::BullCallSpread;
use rust_decimal_macros::dec;
fn test_strategy() -> BullCallSpread {
BullCallSpread::new(
"GOLD".to_string(),
pos_or_panic!(2505.8), pos_or_panic!(2460.0), pos_or_panic!(2515.0), ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2), dec!(0.05), Positive::ZERO, Positive::ONE, pos_or_panic!(27.26), pos_or_panic!(5.33), pos_or_panic!(0.58), pos_or_panic!(0.58), pos_or_panic!(0.55), pos_or_panic!(0.54), )
}
#[test]
fn test_analyze_probabilities_without_adjustments() {
let strategy = test_strategy();
let result = strategy.analyze_probabilities(None, None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(analysis.probability_of_profit > Positive::ZERO);
assert_eq!(analysis.probability_of_max_profit, Positive::ZERO);
assert_eq!(analysis.probability_of_max_loss, Positive::ZERO);
assert!(analysis.risk_reward_ratio > Positive::ZERO);
}
#[test]
fn test_analyze_probabilities_with_adjustments() {
let strategy = test_strategy();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.2),
std_dev_adjustment: pos_or_panic!(0.05),
});
let trend = Some(PriceTrend {
drift_rate: 0.1,
confidence: 0.95,
});
let result = strategy.analyze_probabilities(vol_adj, trend);
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(analysis.probability_of_profit > Positive::ZERO);
assert!(analysis.probability_of_max_profit >= Positive::ZERO);
assert!(analysis.probability_of_max_loss >= Positive::ZERO);
}
#[test]
fn test_expected_value_calculation() {
let strategy = test_strategy();
let result = strategy.expected_value(None, None);
assert!(result.is_ok());
assert!(result.unwrap() > Positive::ZERO);
}
#[test]
fn test_expected_value_with_trend() {
let strategy = test_strategy();
let trend = Some(PriceTrend {
drift_rate: 0.1,
confidence: 0.95,
});
let result = strategy.expected_value(None, trend);
assert!(result.is_ok());
assert!(result.unwrap() > Positive::ZERO);
}
#[test]
fn test_probability_of_profit() {
let strategy = test_strategy();
let result = strategy.probability_of_profit(None, None);
assert!(result.is_ok());
let prob = result.unwrap();
assert!(prob > Positive::ZERO);
assert!(prob <= Positive::ONE);
}
#[test]
fn test_probability_of_loss() {
let strategy = test_strategy();
let result = strategy.probability_of_loss(None, None);
assert!(result.is_ok());
let prob = result.unwrap();
assert!(prob > Positive::ZERO);
assert!(prob <= Positive::ONE);
}
#[test]
fn test_calculate_extreme_probabilities() {
let strategy = test_strategy();
let result = strategy.calculate_extreme_probabilities(None, None);
assert!(result.is_ok());
let (max_profit_prob, max_loss_prob) = result.unwrap();
assert!(max_profit_prob >= Positive::ZERO);
assert!(max_loss_prob >= Positive::ZERO);
assert!(max_profit_prob + max_loss_prob <= Positive::ONE);
}
#[test]
fn test_extreme_probabilities_with_adjustments() {
let strategy = test_strategy();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.2),
std_dev_adjustment: pos_or_panic!(0.05),
});
let trend = Some(PriceTrend {
drift_rate: 0.1,
confidence: 0.95,
});
let result = strategy.calculate_extreme_probabilities(vol_adj, trend);
assert!(result.is_ok());
let (max_profit_prob, max_loss_prob) = result.unwrap();
assert!(max_profit_prob >= Positive::ZERO);
assert!(max_loss_prob >= Positive::ZERO);
assert!(max_profit_prob + max_loss_prob <= Positive::ONE);
}
#[test]
fn test_expected_value_with_volatility() {
let strategy = test_strategy();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.3),
std_dev_adjustment: pos_or_panic!(0.05),
});
let result = strategy.expected_value(vol_adj, None);
assert!(result.is_ok());
}
}
#[cfg(test)]
mod tests_expected_value {
use super::*;
use crate::ExpirationDate;
use crate::strategies::BullCallSpread;
use rust_decimal_macros::dec;
fn create_test_strategy() -> BullCallSpread {
BullCallSpread::new(
"GOLD".to_string(),
pos_or_panic!(2505.8), pos_or_panic!(2460.0), pos_or_panic!(2515.0), ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2), dec!(0.05), Positive::ZERO, Positive::ONE, pos_or_panic!(27.26), pos_or_panic!(5.33), pos_or_panic!(0.58), pos_or_panic!(0.58), pos_or_panic!(0.55), pos_or_panic!(0.54), )
}
#[test]
fn test_expected_value_basic() {
let strategy = create_test_strategy();
let result = strategy.expected_value(None, None);
assert!(result.is_ok(), "Expected value calculation should succeed");
let ev = result.unwrap();
assert!(
ev >= Positive::ZERO,
"Expected value should be non-negative"
);
}
#[test]
fn test_expected_value_with_volatility() {
let strategy = create_test_strategy();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.25),
std_dev_adjustment: pos_or_panic!(0.1),
});
let result = strategy.expected_value(vol_adj, None);
assert!(result.is_ok());
assert!(result.unwrap() >= Positive::ZERO);
}
#[test]
fn test_expected_value_with_trend() {
let strategy = create_test_strategy();
let trend = Some(PriceTrend {
drift_rate: 0.1,
confidence: 0.95,
});
let result = strategy.expected_value(None, trend);
assert!(result.is_ok());
assert!(result.unwrap() >= Positive::ZERO);
}
#[test]
fn test_expected_value_with_both_adjustments() {
let strategy = create_test_strategy();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.25),
std_dev_adjustment: pos_or_panic!(0.1),
});
let trend = Some(PriceTrend {
drift_rate: 0.1,
confidence: 0.95,
});
let result = strategy.expected_value(vol_adj, trend);
assert!(result.is_ok());
assert!(result.unwrap() >= Positive::ZERO);
}
#[test]
fn test_expected_value_with_high_volatility() {
let strategy = create_test_strategy();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: Positive::ONE,
std_dev_adjustment: pos_or_panic!(0.5),
});
let result = strategy.expected_value(vol_adj, None);
assert!(result.is_ok());
assert!(result.unwrap() >= Positive::ZERO);
}
#[test]
fn test_expected_value_with_negative_trend() {
let strategy = create_test_strategy();
let trend = Some(PriceTrend {
drift_rate: -0.2,
confidence: 0.90,
});
let result = strategy.expected_value(None, trend);
assert!(result.is_ok());
assert!(result.unwrap() >= Positive::ZERO);
}
#[test]
fn test_expected_value_probabilities_sum() {
let strategy = create_test_strategy();
let result = strategy.expected_value(None, None);
assert!(result.is_ok());
}
#[test]
fn test_expected_value_with_minimal_volatility() {
let strategy = create_test_strategy();
let vol_adj = Some(VolatilityAdjustment {
base_volatility: pos_or_panic!(0.0001), std_dev_adjustment: Positive::ZERO,
});
let result = strategy.expected_value(vol_adj, None);
assert!(
result.is_ok(),
"Expected value calculation should succeed with minimal volatility"
);
assert!(
result.unwrap() >= Positive::ZERO,
"Expected value should be non-negative"
);
}
}