Skip to main content

optionstratlib_math/geometrics/analysis/
metrics.rs

1use crate::curves::Point2D;
2use crate::error::{CurveError, MetricsError};
3use crate::geometrics::{AnalysisResult, powu_checked};
4use optionstratlib_core::error::DecimalError;
5use optionstratlib_core::model::decimal::{d_add, d_div, d_sqrt, d_sub};
6use optionstratlib_core::{impl_json_debug_pretty, impl_json_display};
7use rust_decimal::Decimal;
8use serde::Serialize;
9
10/// Represents a comprehensive set of statistical and analytical metrics for curve data.
11///
12/// # Overview
13/// The `CurveMetrics` structure aggregates various metrics that describe different
14/// aspects of a curve in a unified form. It provides an encapsulated representation of
15/// curve information that spans different categories, including basic statistical measures,
16/// shape characteristics, range details, trend analysis, and risk evaluation.
17///
18/// ## Usage
19/// This structure is particularly helpful in domains requiring holistic curve analysis,
20/// such as:
21///
22/// - **Financial Analysis**:
23///   Used to analyze return curves and assess the risk-return trade-offs for financial products or strategies.
24/// - **Data Science**:
25///   Provides comprehensive insights into a dataset's distribution, shape, and trends over time.
26/// - **Scientific Research**:
27///   Useful for analyzing phenomena modeled by curves in domains like physics, biology, or economics.
28///
29/// ## Field Descriptions
30///
31/// - **basic**: [`BasicMetrics`]
32///   - Contains mean, median, mode, and standard deviation of the dataset.
33/// - **shape**: [`ShapeMetrics`]
34///   - Captures the shape-related characteristics such as skewness, kurtosis, and extrema (peaks/valleys).
35/// - **range**: [`RangeMetrics`]
36///   - Includes range, min/max points, and quartiles for the dataset.
37/// - **trend**: [`TrendMetrics`]
38///   - Represents the directional tendencies, moving averages, and regression characteristics.
39/// - **risk**: [`RiskMetrics`]
40///   - Evaluates financial risk metrics such as volatility and Sharpe ratio.
41///
42/// ## Example Workflow
43/// The `CurveMetrics` structure is usually constructed by combining its fields using the individual
44/// metric structures (`BasicMetrics`, `ShapeMetrics`, `RangeMetrics`, `TrendMetrics`, `RiskMetrics`).
45/// It is often initialized as part of a larger curve analysis operation and may be transformed or queried for
46/// generating insights.
47///
48/// ## Related Concepts
49/// - [`BasicMetrics`]: Encodes fundamental statistics.
50/// - [`ShapeMetrics`]: Provides characteristics associated with curve shape.
51/// - [`RangeMetrics`]: Range and quartile information for a curve.
52/// - [`TrendMetrics`]: Trendline and regression fit metrics.
53/// - [`RiskMetrics`]: Quantifies financial risk.
54///
55/// ## Examples of Associated Tools
56/// - Statistical Analysis: Plots, descriptive statistics, trend analysis.
57/// - Visualizations: Understand curve behavior (e.g., peaks, valleys).
58/// - Financial Metrics: Sharpe ratio, coefficient of variation, and VaR for understanding portfolio risks.
59///
60/// ## Remarks
61/// The `CurveMetrics` struct is designed to be reusable across various analytical contexts,
62/// providing a versatile and standardized way to represent curve characteristics.
63#[derive(Clone, Serialize)]
64#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
65pub struct Metrics {
66    /// - **Basic Metrics (`basic`)**:
67    ///   Includes fundamental statistical measures such as mean, median, mode, and standard deviation.
68    ///   These measures provide a quick overview of the distribution of the observations within the curve.
69    pub basic: BasicMetrics,
70    /// - **Shape Metrics (`shape`)**:
71    ///   Captures the structural characteristics of the curve, such as skewness, kurtosis,
72    ///   the locations of peaks and valleys, and the points where the curve inflects.
73    ///   Useful for understanding curve symmetry, tail behavior, and general shape nuances.
74    pub shape: ShapeMetrics,
75    /// - **Range Metrics (`range`)**:
76    ///   Describes the range of the data, including minimum and maximum observed points,
77    ///   the extent between these points, and quartile-based statistical details such as
78    ///   interquartile range. Particularly helpful when analyzing variability in data distribution.
79    pub range: RangeMetrics,
80    /// - **Trend Metrics (`trend`)**:
81    ///   Measures the directional tendencies of the curve over time. Includes the slope,
82    ///   intercept, and statistical goodness-of-fit (R² value) as well as moving averages.
83    ///   Ideal for identifying long-term trends and evaluating the predictive nature of the curve.
84    pub trend: TrendMetrics,
85    /// - **Risk Metrics (`risk`)**:
86    ///   Quantifies curve risk using various financial metrics, such as volatility, value-at-risk (VaR),
87    ///   expected shortfall, coefficient of variation, and the Sharpe ratio. These metrics are often used to evaluate
88    ///   the risk-return profile in financial contexts.
89    pub risk: RiskMetrics,
90}
91
92impl_json_debug_pretty!(Metrics);
93impl_json_display!(Metrics);
94
95/// Represents a set of metrics associated with analyzing and interpreting a curve.
96///
97/// This structure encapsulates multiple types of metrics, each responsible for
98/// a specific aspect of curve analysis. These include basic statistical measures,
99/// shape-related properties, range characteristics, trend analysis, and risk factors.
100///
101/// ## Fields
102/// - `basic`: Basic statistical metrics such as mean, median, mode, and standard deviation.
103/// - `shape`: Metrics that describe the shape of the curve, such as skewness, kurtosis,
104///   and points of interest (peaks, valleys, and inflection points).
105/// - `range`: Range-based metrics specifying properties like the minimum and maximum
106///   values on the curve, quartiles, and interquartile range.
107/// - `trend`: Metrics related to overall trends in the curve, such as slope,
108///   intercept, R-squared value, and moving average data points.
109/// - `risk`: Risk-related metrics, including volatility, value at risk (VaR),
110///   expected shortfall, coefficient of variation, and the Sharpe ratio.
111///
112/// ## Notes
113/// This implementation ensures modularity by separating distinct aspects of curve
114/// analysis into specific metric structures. These metrics can be used individually
115/// or collectively for advanced data analysis or curve interpretation tasks.
116///
117/// ## Related Structures
118/// - [`BasicMetrics`]: Provides basic statistical metrics about the dataset.
119/// - [`ShapeMetrics`]: Describes the geometric properties of the curve.
120/// - [`RangeMetrics`]: Assesses the range and quartile characteristics of the curve.
121/// - [`TrendMetrics`]: Analyzes trends within the data to understand directional behavior.
122/// - [`RiskMetrics`]: Highlights risk-based metrics for financial, statistical, or analytical use cases.
123/// - [`AnalysisResult`]: The result type combining key metrics into a single analytic perspective.
124/// - [`CurveError`]: Represents potential errors that may arise during curve analysis operations.
125impl Metrics {
126    /// ### `new`
127    /// Constructs a new instance of `CurveMetrics` and initializes all relevant fields with the provided
128    /// metric structures.
129    ///
130    /// #### Parameters:
131    /// - `basic`: An instance of [`BasicMetrics`], holding essential statistical information.
132    /// - `shape`: An instance of [`ShapeMetrics`], measuring the geometric properties of the curve.
133    /// - `range`: An instance of [`RangeMetrics`], describing the range and distribution of the curve.
134    /// - `trend`: An instance of [`TrendMetrics`], detailing trend-based analytical results.
135    /// - `risk`: An instance of [`RiskMetrics`], specifying the risk characteristics related to the curve.
136    ///
137    /// #### Returns:
138    /// - A new `CurveMetrics` instance containing the provided metrics.
139    ///
140    #[must_use]
141    pub fn new(
142        basic: BasicMetrics,
143        shape: ShapeMetrics,
144        range: RangeMetrics,
145        trend: TrendMetrics,
146        risk: RiskMetrics,
147    ) -> Self {
148        Self {
149            basic,
150            shape,
151            range,
152            trend,
153            risk,
154        }
155    }
156
157    /// ### `curve_analysis_result`
158    /// Generates a high-level analysis result from the metrics encapsulated within the `CurveMetrics` instance.
159    ///
160    /// #### Returns:
161    /// - `Ok(CurveAnalysisResult)`: A result that contains analyzed data in the form of a
162    ///   [`AnalysisResult`] structure with basic statistics and shape metrics.
163    /// - `Err(CurvesError)`: An error of type [`CurveError`] when analysis fails.
164    ///
165    /// The result provides the basic statistical measures (`BasicMetrics`) and
166    /// shape metrics (`ShapeMetrics`) that were part of the `CurveMetrics` instance.
167    ///
168    /// # Errors
169    ///
170    /// Currently infallible - the conversion is a direct field
171    /// re-pack. The `Result` signature is retained so future
172    /// implementations that validate the metrics invariants (e.g.
173    /// non-negative variance) can surface
174    /// [`CurveError::MetricsError`] without breaking callers.
175    pub fn analysis_result(&self) -> Result<AnalysisResult, CurveError> {
176        Ok(AnalysisResult {
177            statistics: self.basic,
178            shape_metrics: self.shape.clone(),
179        })
180    }
181}
182
183/// Represents a collection of fundamental statistical metrics.
184///
185/// # Overview
186/// The `BasicMetrics` structure encapsulates the core descriptive statistics
187/// for a dataset, providing a quick summary of its central tendency, spread,
188/// and distribution properties. These metrics are widely used in statistical analysis,
189/// curve analysis, and various financial or scientific computations.
190///
191/// # Fields
192///
193/// * `mean` - The arithmetic average of the dataset. Represents the central value where the sum
194///   of all data points is divided by the total number of points.
195///   Useful for understanding the overall trend or expected value of the dataset.
196///
197/// * `median` - The middle value in the dataset when sorted. If the dataset has an even number
198///   of elements, the median is the average of the two middle elements.
199///   A robust measure of central tendency, particularly in datasets with outliers.
200///
201/// * `mode` - The most frequently occurring value in the dataset. If no value repeats, the
202///   mode might not be well-defined (or may represent multi-modal distributions).
203///   Useful for identifying common or dominant values in a dataset.
204///
205/// * `std_dev` - The standard deviation of the dataset, which quantifies the amount of variation
206///   or dispersion from the mean.
207///   A key measure of data spread, often used for assessing volatility or risk in
208///   financial contexts.
209///
210/// # Applications
211/// The `BasicMetrics` structure is utilized in various domains requiring descriptive statistics:
212///
213/// - **Financial Analysis**:
214///   - Evaluate price variations, returns, and risks.
215///
216/// - **Scientific Research**:
217///   - Summarize observations and patterns in experimental data.
218///
219/// - **Data Science**:
220///   - Understand distributions, clean data, and preprocess datasets for machine learning models.
221///
222/// # Integration
223/// The `BasicMetrics` structure is often used as part of larger metric aggregations, such as:
224/// - [`Metrics`]: Combines `BasicMetrics` with other
225///   metrics for a detailed analysis of curve behavior.
226/// - [`AnalysisResult`]: Provides a high-level
227///   result of statistical and shape analysis for curves.
228///
229/// # Remarks
230/// - The values are expressed as [`Decimal`] to maintain precision, important in financial
231///   computations or datasets requiring high accuracy.
232/// - The `BasicMetrics` structure is immutable and can be cloned/copied, making it efficient for use in
233///   concurrent or parallel computations.
234///
235/// # Example Workflow
236/// Typically, `BasicMetrics` is computed from a dataset using statistical functions
237/// and then integrated into a more comprehensive analysis pipeline.
238///
239/// # Related Concepts
240/// - [`ShapeMetrics`]: Captures shape-related properties like skewness and kurtosis.
241/// - [`RiskMetrics`]: Measures risk characteristics of financial instruments.
242/// - [`TrendMetrics`]: Represents time-based trends in data series.
243#[derive(Clone, Copy, Serialize)]
244#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
245pub struct BasicMetrics {
246    /// The arithmetic mean (average) of the dataset.
247    /// Calculated by summing all values and dividing by the count of values.
248    /// Provides a measure of central tendency that is affected by all values,
249    /// including outliers.
250    pub mean: Decimal,
251
252    /// The median value of the dataset.
253    /// Represents the middle value when all data points are arranged in ascending order.
254    /// For datasets with even count, it's the average of the two middle values.
255    /// Less sensitive to outliers than the mean.
256    pub median: Decimal,
257
258    /// The most frequently occurring value in the dataset.
259    /// If multiple values occur with equal frequency, this typically represents
260    /// the first encountered mode or a calculated central mode.
261    /// For multi-modal distributions, this is a simplification.
262    pub mode: Decimal,
263
264    /// The standard deviation of the dataset.
265    /// Measures the amount of variation or dispersion from the mean.
266    /// A low standard deviation indicates values tend to be close to the mean,
267    /// while a high standard deviation indicates values are spread over a wider range.
268    pub std_dev: Decimal,
269}
270
271impl_json_debug_pretty!(BasicMetrics);
272impl_json_display!(BasicMetrics);
273
274/// Represents shape-related analysis metrics for a given curve.
275///
276/// # Overview
277/// The `ShapeMetrics` structure encapsulates shape-related properties and critical
278/// points describing a curve's geometrical behavior. It's primarily used for
279/// mathematical and statistical analysis of curves representing data distributions
280/// or mathematical functions.
281///
282/// # Fields
283/// * `skewness` - A measure of distribution asymmetry. Positive values indicate right
284///   tailing while negative values indicate left tailing.
285///
286/// * `kurtosis` - A measure of the "tailedness" or presence of outliers in the distribution.
287///   Higher values indicate heavier tails and more outliers compared to a normal distribution.
288///
289/// * `peaks` - Collection of points representing local or global maxima along the curve,
290///   stored as `Point2D` coordinates.
291///
292/// * `valleys` - Collection of points representing local or global minima along the curve,
293///   stored as `Point2D` coordinates.
294///
295/// * `inflection_points` - Points where the curve changes concavity (transitioning from
296///   concave up to concave down or vice versa), stored as `Point2D` coordinates.
297///
298/// # Applications
299/// This structure is commonly used in:
300/// - Financial analysis to identify trend reversals or extreme price levels
301/// - Statistical distribution analysis to characterize data shape properties
302/// - Mathematical modeling to identify critical points in curve functions
303/// - Visualization and interpretation of complex curve behaviors
304///
305/// # Relationship with Other Structures
306/// `ShapeMetrics` is typically part of the larger `AnalysisResult` structure
307/// that provides comprehensive curve analysis. It uses `Point2D` to represent
308/// all positional data with high-precision `Decimal` values.
309#[derive(Clone, Serialize)]
310#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
311pub struct ShapeMetrics {
312    /// Describes the asymmetry of the curve's distribution.
313    /// A positive value indicates a right tail (right-skewed distribution),
314    /// while a negative value indicates a left tail (left-skewed distribution).
315    /// A value close to zero suggests symmetry.
316    pub skewness: Decimal,
317
318    /// Indicates the "tailedness" or presence of outliers in the curve's distribution.
319    /// Higher values correspond to heavier tails and more outliers compared to
320    /// a normal distribution. Standard normal distribution has kurtosis of 3.0.
321    /// Often expressed as excess kurtosis (kurtosis - 3).
322    pub kurtosis: Decimal,
323
324    /// Collection of `Point2D` instances representing local or global maxima (peaks)
325    /// along the curve. These points have higher y-values than their immediate
326    /// neighboring points.
327    pub peaks: Vec<Point2D>,
328
329    /// Collection of `Point2D` instances representing local or global minima (valleys)
330    /// along the curve. These points have lower y-values than their immediate
331    /// neighboring points.
332    pub valleys: Vec<Point2D>,
333
334    /// Points where the curve changes concavity, transitioning from concave up to
335    /// concave down (or vice versa). At these points, the second derivative equals zero
336    /// while changing sign.
337    pub inflection_points: Vec<Point2D>,
338}
339
340impl_json_debug_pretty!(ShapeMetrics);
341impl_json_display!(ShapeMetrics);
342
343/// Represents statistical and range-related metrics for a dataset.
344///
345/// `RangeMetrics` provides key statistical measures related to data ranges and distributions.
346/// This structure is particularly useful when analyzing numerical datasets from financial curves,
347/// scientific measurements, or computational geometry applications.
348///
349/// # Fields
350///
351/// * `min` - The minimum point in the dataset, containing both x and y coordinates as the smallest values.
352///   This defines the lower boundary of the data range.
353///
354/// * `max` - The maximum point in the dataset, containing both x and y coordinates as the largest values.
355///   This defines the upper boundary of the data range.
356///
357/// * `range` - The numerical difference between the maximum and minimum values.
358///   Provides a simple measure of data dispersion or spread.
359///
360/// * `quartiles` - A tuple containing first quartile (Q1), median (Q2), and third quartile (Q3).
361///   These values divide the dataset into four equal parts, providing insight into data distribution.
362///
363/// * `interquartile_range` - The difference between the third and first quartiles (Q3 - Q1).
364///   A robust statistical measure of dispersion that is less sensitive to outliers than the full range.
365///
366/// # Usage
367///
368/// `RangeMetrics` is typically calculated as part of a larger statistical analysis workflow.
369/// It provides essential information for box plots, outlier detection, and distribution analysis.
370///
371/// This structure is part of the curves analysis module that provides comprehensive
372/// statistical and financial analysis tools for mathematical curves.
373#[derive(Clone, Copy, Serialize)]
374#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
375pub struct RangeMetrics {
376    /// The minimum point in the dataset, containing the smallest x and y coordinates observed.
377    pub min: Point2D,
378
379    /// The maximum point in the dataset, containing the largest x and y coordinates observed.
380    pub max: Point2D,
381
382    /// The numerical difference between the maximum and minimum values,
383    /// representing the total span of the data.
384    pub range: Decimal,
385
386    /// A tuple of three values representing the first quartile (Q1),
387    /// median (Q2), and third quartile (Q3) of the dataset.
388    pub quartiles: (Decimal, Decimal, Decimal),
389
390    /// The difference between the third and first quartiles (Q3 - Q1),
391    /// providing a measure of statistical dispersion that ignores extremes.
392    pub interquartile_range: Decimal,
393}
394
395impl_json_debug_pretty!(RangeMetrics);
396impl_json_display!(RangeMetrics);
397
398/// Represents key metrics for analyzing trends within a dataset or curve.
399///
400/// # Overview
401/// This structure stores comprehensive metrics that describe the linear relationship
402/// and trend behavior in a dataset. It provides statistical values derived from
403/// linear regression analysis along with smoothed data points for visualization
404/// and further analysis.
405///
406/// # Fields
407/// - **slope**: The rate of change in the dependent variable (y-axis) with respect
408///   to the independent variable (x-axis), calculated from linear regression.
409/// - **intercept**: The y-coordinate where the regression line intersects the y-axis,
410///   representing the value when x = 0.
411/// - **r_squared**: The coefficient of determination that measures how well the
412///   data fits the regression model, with values closer to 1 indicating a better fit.
413/// - **moving_average**: A series of smoothed data points derived from the original
414///   dataset to reduce noise and highlight the underlying trend pattern.
415///
416/// # Purpose
417/// `TrendMetrics` is particularly useful for:
418/// - Analyzing time-series data in financial markets
419/// - Evaluating performance trends over time
420/// - Quantifying the strength and direction of relationships between variables
421/// - Providing a basis for predictive modeling and forecasting
422///
423/// This structure is a key component of the curve analysis module and works
424/// alongside other metric types to provide comprehensive statistical analysis.
425#[derive(Clone, Serialize)]
426#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
427pub struct TrendMetrics {
428    /// The slope coefficient of the linear regression line, indicating the
429    /// rate and direction of change in the dataset.
430    pub slope: Decimal,
431
432    /// The y-intercept of the regression line, showing the baseline value
433    /// when the independent variable equals zero.
434    pub intercept: Decimal,
435
436    /// The coefficient of determination (R²), measuring how well the regression
437    /// line fits the actual data points. Values range from 0 to 1, where 1
438    /// represents a perfect fit.
439    pub r_squared: Decimal,
440
441    /// A collection of points representing the smoothed version of the original
442    /// data, calculated using moving average techniques to reduce noise and
443    /// highlight the underlying trend.
444    pub moving_average: Vec<Point2D>,
445}
446
447impl_json_debug_pretty!(TrendMetrics);
448impl_json_display!(TrendMetrics);
449
450/// Represents a collection of key financial risk metrics used in risk analysis
451/// and performance evaluation.
452///
453/// # Overview
454/// The `RiskMetrics` structure encapsulates a set of quantitative measures
455/// that assess the risk and performance characteristics of a financial portfolio,
456/// asset, or investment strategy. These metrics provide insights into the
457/// variability, potential losses, and overall return of the analyzed entity.
458///
459/// # Applications
460/// The `RiskMetrics` structure is commonly utilized in:
461///
462/// - **Portfolio Management**: To assess and balance risk-return trade-offs
463/// - **Risk Management**: To estimate potential losses and evaluate market exposures
464/// - **Performance Analysis**: To compare against benchmarks and alternative strategies
465/// - **Financial Modeling**: To perform stress testing and scenario analysis
466///
467/// # Related Concepts
468/// - [`Metrics`]: Aggregates risk metrics alongside other statistical measures
469/// - **Financial Risk Indicators**: Additional measures like maximum drawdown,
470///   Treynor ratio, and Sortino ratio
471///
472/// # Note
473/// For accuracy, ensure the data used to compute these metrics represents a
474/// sufficiently long time horizon and is free from anomalies.
475#[derive(Clone, Copy, Serialize)]
476#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
477pub struct RiskMetrics {
478    /// Measures the degree of variation in returns over time.
479    /// Represents the standard deviation of returns and is a widely used
480    /// indicator of market risk.
481    pub volatility: Decimal,
482
483    /// Quantifies the maximum expected loss over a defined time horizon
484    /// with a given confidence level (typically 95% or 99%).
485    /// Essential for understanding tail-risk and evaluating potential losses.
486    pub value_at_risk: Decimal,
487
488    /// Represents the average loss in worst-case scenarios beyond the VaR threshold.
489    /// Provides a more comprehensive assessment of tail risk than VaR alone.
490    pub expected_shortfall: Decimal,
491
492    /// The coefficient of variation, `std_dev / mean`: the population
493    /// standard deviation of the values (`sqrt(sum((x - mean)^2) / n)`, the
494    /// `std_dev` of [`BasicMetrics`]) over their mean, signed as the mean.
495    /// It measures dispersion relative to level and is unitless. Curves and
496    /// surfaces compute it the same way; it is undefined at a zero mean,
497    /// which the computation reports as [`MetricsError::ZeroMean`], and zero
498    /// for empty data, like every field here. It replaces `beta`, which
499    /// promised a sensitivity to market returns that no curve or surface has
500    /// the data for (#824).
501    pub coefficient_of_variation: Decimal,
502
503    /// Indicates the risk-adjusted return by comparing the portfolio's excess return
504    /// (return above the risk-free rate) to its volatility.
505    /// Helps evaluate whether returns justify the associated risk profile.
506    pub sharpe_ratio: Decimal,
507}
508
509impl_json_debug_pretty!(RiskMetrics);
510impl_json_display!(RiskMetrics);
511
512/// The population standard deviation `sqrt(sum((x - mean)^2) / n)` of a
513/// non-empty sample about `mean`: the [`RiskMetrics::volatility`] of curves
514/// and surfaces, shared so both compute it with the same operations in the
515/// same order (#840).
516///
517/// # Errors
518///
519/// Returns [`DecimalError`] when a deviation, its square, their sum, the
520/// division by `n` or the square root leaves the `Decimal` range, and when
521/// `values` is empty (`n = 0`).
522pub(crate) fn population_std_dev(
523    values: &[Decimal],
524    mean: Decimal,
525    op: &'static str,
526) -> Result<Decimal, DecimalError> {
527    let mut sum_sq = Decimal::ZERO;
528    for &value in values {
529        let centered = d_sub(value, mean, op)?;
530        sum_sq = d_add(sum_sq, powu_checked(centered, 2, op)?, op)?;
531    }
532    let variance = d_div(sum_sq, Decimal::from(values.len()), op)?;
533    d_sqrt(variance, op)
534}
535
536/// The coefficient of variation `std_dev / mean` of
537/// [`RiskMetrics::coefficient_of_variation`], shared so curves and surfaces
538/// compute it the same way (#824).
539///
540/// # Errors
541///
542/// Returns [`MetricsError::ZeroMean`] when `mean` is zero and
543/// [`MetricsError::RiskError`] when the quotient leaves the `Decimal` range.
544pub(crate) fn coefficient_of_variation(
545    std_dev: Decimal,
546    mean: Decimal,
547    op: &'static str,
548) -> Result<Decimal, MetricsError> {
549    if mean.is_zero() {
550        return Err(MetricsError::ZeroMean {
551            metric: "coefficient of variation",
552        });
553    }
554    d_div(std_dev, mean, op).map_err(|e| MetricsError::RiskError(e.to_string()))
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use num_traits::FromPrimitive;
561    use rust_decimal_macros::dec;
562
563    // Helper function to create a Point2D for testing
564    fn create_test_point(x: f64, y: f64) -> Point2D {
565        Point2D::new(Decimal::from_f64(x).unwrap(), Decimal::from_f64(y).unwrap())
566    }
567
568    mod test_basic_metrics {
569        use super::*;
570
571        #[test]
572        fn test_basic_metrics_creation() {
573            let metrics = BasicMetrics {
574                mean: dec!(10.5),
575                median: dec!(10.0),
576                mode: dec!(9.0),
577                std_dev: dec!(1.2),
578            };
579
580            assert_eq!(metrics.mean, dec!(10.5));
581            assert_eq!(metrics.median, dec!(10.0));
582            assert_eq!(metrics.mode, dec!(9.0));
583            assert_eq!(metrics.std_dev, dec!(1.2));
584        }
585
586        #[test]
587        fn test_basic_metrics_clone() {
588            let metrics = BasicMetrics {
589                mean: dec!(10.5),
590                median: dec!(10.0),
591                mode: dec!(9.0),
592                std_dev: dec!(1.2),
593            };
594
595            let cloned_metrics = metrics;
596            assert_eq!(metrics.mean, cloned_metrics.mean);
597            assert_eq!(metrics.median, cloned_metrics.median);
598            assert_eq!(metrics.mode, cloned_metrics.mode);
599            assert_eq!(metrics.std_dev, cloned_metrics.std_dev);
600        }
601    }
602
603    mod test_shape_metrics {
604        use super::*;
605
606        #[test]
607        fn test_shape_metrics_creation() {
608            let metrics = ShapeMetrics {
609                skewness: dec!(0.5),
610                kurtosis: dec!(3.0),
611                peaks: vec![create_test_point(1.0, 10.0)],
612                valleys: vec![create_test_point(2.0, 5.0)],
613                inflection_points: vec![create_test_point(1.5, 7.5)],
614            };
615
616            assert_eq!(metrics.skewness, dec!(0.5));
617            assert_eq!(metrics.kurtosis, dec!(3.0));
618            assert_eq!(metrics.peaks.len(), 1);
619            assert_eq!(metrics.valleys.len(), 1);
620            assert_eq!(metrics.inflection_points.len(), 1);
621        }
622
623        #[test]
624        fn test_shape_metrics_clone() {
625            let metrics = ShapeMetrics {
626                skewness: dec!(0.5),
627                kurtosis: dec!(3.0),
628                peaks: vec![create_test_point(1.0, 10.0)],
629                valleys: vec![create_test_point(2.0, 5.0)],
630                inflection_points: vec![create_test_point(1.5, 7.5)],
631            };
632
633            let cloned_metrics = metrics.clone();
634            assert_eq!(metrics.skewness, cloned_metrics.skewness);
635            assert_eq!(metrics.kurtosis, cloned_metrics.kurtosis);
636            assert_eq!(metrics.peaks.len(), cloned_metrics.peaks.len());
637            assert_eq!(metrics.valleys.len(), cloned_metrics.valleys.len());
638            assert_eq!(
639                metrics.inflection_points.len(),
640                cloned_metrics.inflection_points.len()
641            );
642        }
643    }
644
645    mod test_range_metrics {
646        use super::*;
647
648        #[test]
649        fn test_range_metrics_creation() {
650            let metrics = RangeMetrics {
651                min: create_test_point(0.0, 5.0),
652                max: create_test_point(10.0, 15.0),
653                range: dec!(10.0),
654                quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
655                interquartile_range: dec!(6.0),
656            };
657
658            assert_eq!(metrics.min.x, dec!(0.0));
659            assert_eq!(metrics.max.x, dec!(10.0));
660            assert_eq!(metrics.range, dec!(10.0));
661            assert_eq!(metrics.quartiles.0, dec!(7.0));
662            assert_eq!(metrics.interquartile_range, dec!(6.0));
663        }
664
665        #[test]
666        fn test_range_metrics_clone() {
667            let metrics = RangeMetrics {
668                min: create_test_point(0.0, 5.0),
669                max: create_test_point(10.0, 15.0),
670                range: dec!(10.0),
671                quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
672                interquartile_range: dec!(6.0),
673            };
674
675            let cloned_metrics = metrics;
676            assert_eq!(metrics.min.x, cloned_metrics.min.x);
677            assert_eq!(metrics.max.x, cloned_metrics.max.x);
678            assert_eq!(metrics.range, cloned_metrics.range);
679            assert_eq!(metrics.quartiles.0, cloned_metrics.quartiles.0);
680            assert_eq!(
681                metrics.interquartile_range,
682                cloned_metrics.interquartile_range
683            );
684        }
685    }
686
687    mod test_trend_metrics {
688        use super::*;
689
690        #[test]
691        fn test_trend_metrics_creation() {
692            let metrics = TrendMetrics {
693                slope: dec!(1.5),
694                intercept: dec!(2.0),
695                r_squared: dec!(0.95),
696                moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
697            };
698
699            assert_eq!(metrics.slope, dec!(1.5));
700            assert_eq!(metrics.intercept, dec!(2.0));
701            assert_eq!(metrics.r_squared, dec!(0.95));
702            assert_eq!(metrics.moving_average.len(), 2);
703        }
704
705        #[test]
706        fn test_trend_metrics_clone() {
707            let metrics = TrendMetrics {
708                slope: dec!(1.5),
709                intercept: dec!(2.0),
710                r_squared: dec!(0.95),
711                moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
712            };
713
714            let cloned_metrics = metrics.clone();
715            assert_eq!(metrics.slope, cloned_metrics.slope);
716            assert_eq!(metrics.intercept, cloned_metrics.intercept);
717            assert_eq!(metrics.r_squared, cloned_metrics.r_squared);
718            assert_eq!(
719                metrics.moving_average.len(),
720                cloned_metrics.moving_average.len()
721            );
722        }
723    }
724
725    mod test_risk_metrics {
726        use super::*;
727
728        #[test]
729        fn test_risk_metrics_creation() {
730            let metrics = RiskMetrics {
731                volatility: dec!(0.15),
732                value_at_risk: dec!(0.05),
733                expected_shortfall: dec!(0.07),
734                coefficient_of_variation: dec!(1.2),
735                sharpe_ratio: dec!(2.5),
736            };
737
738            assert_eq!(metrics.volatility, dec!(0.15));
739            assert_eq!(metrics.value_at_risk, dec!(0.05));
740            assert_eq!(metrics.expected_shortfall, dec!(0.07));
741            assert_eq!(metrics.coefficient_of_variation, dec!(1.2));
742            assert_eq!(metrics.sharpe_ratio, dec!(2.5));
743        }
744
745        #[test]
746        fn test_risk_metrics_clone() {
747            let metrics = RiskMetrics {
748                volatility: dec!(0.15),
749                value_at_risk: dec!(0.05),
750                expected_shortfall: dec!(0.07),
751                coefficient_of_variation: dec!(1.2),
752                sharpe_ratio: dec!(2.5),
753            };
754
755            let cloned_metrics = metrics;
756            assert_eq!(metrics.volatility, cloned_metrics.volatility);
757            assert_eq!(metrics.value_at_risk, cloned_metrics.value_at_risk);
758            assert_eq!(
759                metrics.expected_shortfall,
760                cloned_metrics.expected_shortfall
761            );
762            assert_eq!(
763                metrics.coefficient_of_variation,
764                cloned_metrics.coefficient_of_variation
765            );
766            assert_eq!(metrics.sharpe_ratio, cloned_metrics.sharpe_ratio);
767        }
768    }
769
770    mod test_curve_metrics {
771        use super::*;
772
773        fn create_test_curve_metrics() -> Metrics {
774            Metrics {
775                basic: BasicMetrics {
776                    mean: dec!(10.5),
777                    median: dec!(10.0),
778                    mode: dec!(9.0),
779                    std_dev: dec!(1.2),
780                },
781                shape: ShapeMetrics {
782                    skewness: dec!(0.5),
783                    kurtosis: dec!(3.0),
784                    peaks: vec![create_test_point(1.0, 10.0)],
785                    valleys: vec![create_test_point(2.0, 5.0)],
786                    inflection_points: vec![create_test_point(1.5, 7.5)],
787                },
788                range: RangeMetrics {
789                    min: create_test_point(0.0, 5.0),
790                    max: create_test_point(10.0, 15.0),
791                    range: dec!(10.0),
792                    quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
793                    interquartile_range: dec!(6.0),
794                },
795                trend: TrendMetrics {
796                    slope: dec!(1.5),
797                    intercept: dec!(2.0),
798                    r_squared: dec!(0.95),
799                    moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
800                },
801                risk: RiskMetrics {
802                    volatility: dec!(0.15),
803                    value_at_risk: dec!(0.05),
804                    expected_shortfall: dec!(0.07),
805                    coefficient_of_variation: dec!(1.2),
806                    sharpe_ratio: dec!(2.5),
807                },
808            }
809        }
810
811        #[test]
812        fn test_curve_metrics_creation() {
813            let metrics = create_test_curve_metrics();
814
815            assert_eq!(metrics.basic.mean, dec!(10.5));
816            assert_eq!(metrics.shape.skewness, dec!(0.5));
817            assert_eq!(metrics.range.range, dec!(10.0));
818            assert_eq!(metrics.trend.slope, dec!(1.5));
819            assert_eq!(metrics.risk.volatility, dec!(0.15));
820        }
821
822        #[test]
823        fn test_curve_metrics_clone() {
824            let metrics = create_test_curve_metrics();
825            let cloned_metrics = metrics.clone();
826
827            assert_eq!(metrics.basic.mean, cloned_metrics.basic.mean);
828            assert_eq!(metrics.shape.skewness, cloned_metrics.shape.skewness);
829            assert_eq!(metrics.range.range, cloned_metrics.range.range);
830            assert_eq!(metrics.trend.slope, cloned_metrics.trend.slope);
831            assert_eq!(metrics.risk.volatility, cloned_metrics.risk.volatility);
832        }
833
834        #[test]
835        fn test_curve_analysis_result() {
836            let metrics = create_test_curve_metrics();
837            let result = metrics.analysis_result();
838
839            assert!(result.is_ok());
840            let analysis = result.unwrap();
841
842            assert_eq!(analysis.statistics.mean, metrics.basic.mean);
843            assert_eq!(analysis.shape_metrics.skewness, metrics.shape.skewness);
844        }
845
846        #[test]
847        fn test_curve_metrics_new() {
848            let basic = BasicMetrics {
849                mean: dec!(10.5),
850                median: dec!(10.0),
851                mode: dec!(9.0),
852                std_dev: dec!(1.2),
853            };
854
855            let shape = ShapeMetrics {
856                skewness: dec!(0.5),
857                kurtosis: dec!(3.0),
858                peaks: vec![create_test_point(1.0, 10.0)],
859                valleys: vec![create_test_point(2.0, 5.0)],
860                inflection_points: vec![create_test_point(1.5, 7.5)],
861            };
862
863            let range = RangeMetrics {
864                min: create_test_point(0.0, 5.0),
865                max: create_test_point(10.0, 15.0),
866                range: dec!(10.0),
867                quartiles: (dec!(7.0), dec!(10.0), dec!(13.0)),
868                interquartile_range: dec!(6.0),
869            };
870
871            let trend = TrendMetrics {
872                slope: dec!(1.5),
873                intercept: dec!(2.0),
874                r_squared: dec!(0.95),
875                moving_average: vec![create_test_point(1.0, 3.5), create_test_point(2.0, 5.0)],
876            };
877
878            let risk = RiskMetrics {
879                volatility: dec!(0.15),
880                value_at_risk: dec!(0.05),
881                expected_shortfall: dec!(0.07),
882                coefficient_of_variation: dec!(1.2),
883                sharpe_ratio: dec!(2.5),
884            };
885
886            let metrics = Metrics::new(basic, shape, range, trend, risk);
887
888            assert_eq!(metrics.basic.mean, dec!(10.5));
889            assert_eq!(metrics.shape.skewness, dec!(0.5));
890            assert_eq!(metrics.range.range, dec!(10.0));
891            assert_eq!(metrics.trend.slope, dec!(1.5));
892            assert_eq!(metrics.risk.volatility, dec!(0.15));
893        }
894    }
895}