use crate::curves::Point2D;
use crate::error::{CurveError, MetricsError};
use crate::geometrics::{AnalysisResult, powu_checked};
use optionstratlib_core::error::DecimalError;
use optionstratlib_core::model::decimal::{d_add, d_div, d_sqrt, d_sub};
use optionstratlib_core::{impl_json_debug_pretty, impl_json_display};
use rust_decimal::Decimal;
use serde::Serialize;
#[derive(Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct Metrics {
pub basic: BasicMetrics,
pub shape: ShapeMetrics,
pub range: RangeMetrics,
pub trend: TrendMetrics,
pub risk: RiskMetrics,
}
impl_json_debug_pretty!(Metrics);
impl_json_display!(Metrics);
impl Metrics {
#[must_use]
pub fn new(
basic: BasicMetrics,
shape: ShapeMetrics,
range: RangeMetrics,
trend: TrendMetrics,
risk: RiskMetrics,
) -> Self {
Self {
basic,
shape,
range,
trend,
risk,
}
}
pub fn analysis_result(&self) -> Result<AnalysisResult, CurveError> {
Ok(AnalysisResult {
statistics: self.basic,
shape_metrics: self.shape.clone(),
})
}
}
#[derive(Clone, Copy, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct BasicMetrics {
pub mean: Decimal,
pub median: Decimal,
pub mode: Decimal,
pub std_dev: Decimal,
}
impl_json_debug_pretty!(BasicMetrics);
impl_json_display!(BasicMetrics);
#[derive(Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct ShapeMetrics {
pub skewness: Decimal,
pub kurtosis: Decimal,
pub peaks: Vec<Point2D>,
pub valleys: Vec<Point2D>,
pub inflection_points: Vec<Point2D>,
}
impl_json_debug_pretty!(ShapeMetrics);
impl_json_display!(ShapeMetrics);
#[derive(Clone, Copy, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct RangeMetrics {
pub min: Point2D,
pub max: Point2D,
pub range: Decimal,
pub quartiles: (Decimal, Decimal, Decimal),
pub interquartile_range: Decimal,
}
impl_json_debug_pretty!(RangeMetrics);
impl_json_display!(RangeMetrics);
#[derive(Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct TrendMetrics {
pub slope: Decimal,
pub intercept: Decimal,
pub r_squared: Decimal,
pub moving_average: Vec<Point2D>,
}
impl_json_debug_pretty!(TrendMetrics);
impl_json_display!(TrendMetrics);
#[derive(Clone, Copy, Serialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct RiskMetrics {
pub volatility: Decimal,
pub value_at_risk: Decimal,
pub expected_shortfall: Decimal,
pub coefficient_of_variation: Decimal,
pub sharpe_ratio: Decimal,
}
impl_json_debug_pretty!(RiskMetrics);
impl_json_display!(RiskMetrics);
pub(crate) fn population_std_dev(
values: &[Decimal],
mean: Decimal,
op: &'static str,
) -> Result<Decimal, DecimalError> {
let mut sum_sq = Decimal::ZERO;
for &value in values {
let centered = d_sub(value, mean, op)?;
sum_sq = d_add(sum_sq, powu_checked(centered, 2, op)?, op)?;
}
let variance = d_div(sum_sq, Decimal::from(values.len()), op)?;
d_sqrt(variance, op)
}
pub(crate) fn coefficient_of_variation(
std_dev: Decimal,
mean: Decimal,
op: &'static str,
) -> Result<Decimal, MetricsError> {
if mean.is_zero() {
return Err(MetricsError::ZeroMean {
metric: "coefficient of variation",
});
}
d_div(std_dev, mean, op).map_err(|e| MetricsError::RiskError(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::FromPrimitive;
use rust_decimal_macros::dec;
fn create_test_point(x: f64, y: f64) -> Point2D {
Point2D::new(Decimal::from_f64(x).unwrap(), Decimal::from_f64(y).unwrap())
}
mod test_basic_metrics {
use super::*;
#[test]
fn test_basic_metrics_creation() {
let metrics = BasicMetrics {
mean: dec!(10.5),
median: dec!(10.0),
mode: dec!(9.0),
std_dev: dec!(1.2),
};
assert_eq!(metrics.mean, dec!(10.5));
assert_eq!(metrics.median, dec!(10.0));
assert_eq!(metrics.mode, dec!(9.0));
assert_eq!(metrics.std_dev, dec!(1.2));
}
#[test]
fn test_basic_metrics_clone() {
let metrics = BasicMetrics {
mean: dec!(10.5),
median: dec!(10.0),
mode: dec!(9.0),
std_dev: dec!(1.2),
};
let cloned_metrics = metrics;
assert_eq!(metrics.mean, cloned_metrics.mean);
assert_eq!(metrics.median, cloned_metrics.median);
assert_eq!(metrics.mode, cloned_metrics.mode);
assert_eq!(metrics.std_dev, cloned_metrics.std_dev);
}
}
mod test_shape_metrics {
use super::*;
#[test]
fn test_shape_metrics_creation() {
let metrics = ShapeMetrics {
skewness: dec!(0.5),
kurtosis: dec!(3.0),
peaks: vec![create_test_point(1.0, 10.0)],
valleys: vec![create_test_point(2.0, 5.0)],
inflection_points: vec![create_test_point(1.5, 7.5)],
};
assert_eq!(metrics.skewness, dec!(0.5));
assert_eq!(metrics.kurtosis, dec!(3.0));
assert_eq!(metrics.peaks.len(), 1);
assert_eq!(metrics.valleys.len(), 1);
assert_eq!(metrics.inflection_points.len(), 1);
}
#[test]
fn test_shape_metrics_clone() {
let metrics = ShapeMetrics {
skewness: dec!(0.5),
kurtosis: dec!(3.0),
peaks: vec![create_test_point(1.0, 10.0)],
valleys: vec![create_test_point(2.0, 5.0)],
inflection_points: vec![create_test_point(1.5, 7.5)],
};
let cloned_metrics = metrics.clone();
assert_eq!(metrics.skewness, cloned_metrics.skewness);
assert_eq!(metrics.kurtosis, cloned_metrics.kurtosis);
assert_eq!(metrics.peaks.len(), cloned_metrics.peaks.len());
assert_eq!(metrics.valleys.len(), cloned_metrics.valleys.len());
assert_eq!(
metrics.inflection_points.len(),
cloned_metrics.inflection_points.len()
);
}
}
mod test_range_metrics {
use super::*;
#[test]
fn test_range_metrics_creation() {
let metrics = RangeMetrics {
min: create_test_point(0.0, 5.0),
max: create_test_point(10.0, 15.0),
range: dec!(10.0),
quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
interquartile_range: dec!(6.0),
};
assert_eq!(metrics.min.x, dec!(0.0));
assert_eq!(metrics.max.x, dec!(10.0));
assert_eq!(metrics.range, dec!(10.0));
assert_eq!(metrics.quartiles.0, dec!(7.0));
assert_eq!(metrics.interquartile_range, dec!(6.0));
}
#[test]
fn test_range_metrics_clone() {
let metrics = RangeMetrics {
min: create_test_point(0.0, 5.0),
max: create_test_point(10.0, 15.0),
range: dec!(10.0),
quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
interquartile_range: dec!(6.0),
};
let cloned_metrics = metrics;
assert_eq!(metrics.min.x, cloned_metrics.min.x);
assert_eq!(metrics.max.x, cloned_metrics.max.x);
assert_eq!(metrics.range, cloned_metrics.range);
assert_eq!(metrics.quartiles.0, cloned_metrics.quartiles.0);
assert_eq!(
metrics.interquartile_range,
cloned_metrics.interquartile_range
);
}
}
mod test_trend_metrics {
use super::*;
#[test]
fn test_trend_metrics_creation() {
let metrics = TrendMetrics {
slope: dec!(1.5),
intercept: dec!(2.0),
r_squared: dec!(0.95),
moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
};
assert_eq!(metrics.slope, dec!(1.5));
assert_eq!(metrics.intercept, dec!(2.0));
assert_eq!(metrics.r_squared, dec!(0.95));
assert_eq!(metrics.moving_average.len(), 2);
}
#[test]
fn test_trend_metrics_clone() {
let metrics = TrendMetrics {
slope: dec!(1.5),
intercept: dec!(2.0),
r_squared: dec!(0.95),
moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
};
let cloned_metrics = metrics.clone();
assert_eq!(metrics.slope, cloned_metrics.slope);
assert_eq!(metrics.intercept, cloned_metrics.intercept);
assert_eq!(metrics.r_squared, cloned_metrics.r_squared);
assert_eq!(
metrics.moving_average.len(),
cloned_metrics.moving_average.len()
);
}
}
mod test_risk_metrics {
use super::*;
#[test]
fn test_risk_metrics_creation() {
let metrics = RiskMetrics {
volatility: dec!(0.15),
value_at_risk: dec!(0.05),
expected_shortfall: dec!(0.07),
coefficient_of_variation: dec!(1.2),
sharpe_ratio: dec!(2.5),
};
assert_eq!(metrics.volatility, dec!(0.15));
assert_eq!(metrics.value_at_risk, dec!(0.05));
assert_eq!(metrics.expected_shortfall, dec!(0.07));
assert_eq!(metrics.coefficient_of_variation, dec!(1.2));
assert_eq!(metrics.sharpe_ratio, dec!(2.5));
}
#[test]
fn test_risk_metrics_clone() {
let metrics = RiskMetrics {
volatility: dec!(0.15),
value_at_risk: dec!(0.05),
expected_shortfall: dec!(0.07),
coefficient_of_variation: dec!(1.2),
sharpe_ratio: dec!(2.5),
};
let cloned_metrics = metrics;
assert_eq!(metrics.volatility, cloned_metrics.volatility);
assert_eq!(metrics.value_at_risk, cloned_metrics.value_at_risk);
assert_eq!(
metrics.expected_shortfall,
cloned_metrics.expected_shortfall
);
assert_eq!(
metrics.coefficient_of_variation,
cloned_metrics.coefficient_of_variation
);
assert_eq!(metrics.sharpe_ratio, cloned_metrics.sharpe_ratio);
}
}
mod test_curve_metrics {
use super::*;
fn create_test_curve_metrics() -> Metrics {
Metrics {
basic: BasicMetrics {
mean: dec!(10.5),
median: dec!(10.0),
mode: dec!(9.0),
std_dev: dec!(1.2),
},
shape: ShapeMetrics {
skewness: dec!(0.5),
kurtosis: dec!(3.0),
peaks: vec![create_test_point(1.0, 10.0)],
valleys: vec![create_test_point(2.0, 5.0)],
inflection_points: vec![create_test_point(1.5, 7.5)],
},
range: RangeMetrics {
min: create_test_point(0.0, 5.0),
max: create_test_point(10.0, 15.0),
range: dec!(10.0),
quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
interquartile_range: dec!(6.0),
},
trend: TrendMetrics {
slope: dec!(1.5),
intercept: dec!(2.0),
r_squared: dec!(0.95),
moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
},
risk: RiskMetrics {
volatility: dec!(0.15),
value_at_risk: dec!(0.05),
expected_shortfall: dec!(0.07),
coefficient_of_variation: dec!(1.2),
sharpe_ratio: dec!(2.5),
},
}
}
#[test]
fn test_curve_metrics_creation() {
let metrics = create_test_curve_metrics();
assert_eq!(metrics.basic.mean, dec!(10.5));
assert_eq!(metrics.shape.skewness, dec!(0.5));
assert_eq!(metrics.range.range, dec!(10.0));
assert_eq!(metrics.trend.slope, dec!(1.5));
assert_eq!(metrics.risk.volatility, dec!(0.15));
}
#[test]
fn test_curve_metrics_clone() {
let metrics = create_test_curve_metrics();
let cloned_metrics = metrics.clone();
assert_eq!(metrics.basic.mean, cloned_metrics.basic.mean);
assert_eq!(metrics.shape.skewness, cloned_metrics.shape.skewness);
assert_eq!(metrics.range.range, cloned_metrics.range.range);
assert_eq!(metrics.trend.slope, cloned_metrics.trend.slope);
assert_eq!(metrics.risk.volatility, cloned_metrics.risk.volatility);
}
#[test]
fn test_curve_analysis_result() {
let metrics = create_test_curve_metrics();
let result = metrics.analysis_result();
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.statistics.mean, metrics.basic.mean);
assert_eq!(analysis.shape_metrics.skewness, metrics.shape.skewness);
}
#[test]
fn test_curve_metrics_new() {
let basic = BasicMetrics {
mean: dec!(10.5),
median: dec!(10.0),
mode: dec!(9.0),
std_dev: dec!(1.2),
};
let shape = ShapeMetrics {
skewness: dec!(0.5),
kurtosis: dec!(3.0),
peaks: vec![create_test_point(1.0, 10.0)],
valleys: vec![create_test_point(2.0, 5.0)],
inflection_points: vec![create_test_point(1.5, 7.5)],
};
let range = RangeMetrics {
min: create_test_point(0.0, 5.0),
max: create_test_point(10.0, 15.0),
range: dec!(10.0),
quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
interquartile_range: dec!(6.0),
};
let trend = TrendMetrics {
slope: dec!(1.5),
intercept: dec!(2.0),
r_squared: dec!(0.95),
moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
};
let risk = RiskMetrics {
volatility: dec!(0.15),
value_at_risk: dec!(0.05),
expected_shortfall: dec!(0.07),
coefficient_of_variation: dec!(1.2),
sharpe_ratio: dec!(2.5),
};
let metrics = Metrics::new(basic, shape, range, trend, risk);
assert_eq!(metrics.basic.mean, dec!(10.5));
assert_eq!(metrics.shape.skewness, dec!(0.5));
assert_eq!(metrics.range.range, dec!(10.0));
assert_eq!(metrics.trend.slope, dec!(1.5));
assert_eq!(metrics.risk.volatility, dec!(0.15));
}
}
}