Skip to main content

optionstratlib_math/curves/
traits.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 23/2/25
5******************************************************************************/
6use crate::curves::{Curve, Point2D};
7use crate::error::CurveError;
8use crate::geometrics::{BasicMetrics, MetricsExtractor, RangeMetrics, ShapeMetrics, TrendMetrics};
9use optionstratlib_core::error::OperationErrorKind;
10use optionstratlib_core::model::decimal::{d_sub, decimal_to_f64};
11use rand::rngs::StdRng;
12use rand::{RngExt, SeedableRng};
13use rust_decimal::Decimal;
14use statrs::distribution::{ContinuousCDF, Normal};
15use std::collections::BTreeSet;
16use tracing::error;
17
18/// A trait that defines the behavior of any object that can produce a curve representation.
19///
20/// Types implementing the `Curvable` trait must provide the `curve` method, which generates
21/// a `Curve` object based on the internal state of the implementer. This method returns
22/// a `Result`, allowing for proper error handling in situations where the curve
23/// cannot be generated due to various issues (e.g., invalid data or computation errors).
24///
25/// # Method
26///
27/// - `curve`
28///   - **Returns**:
29///     - A `Result` containing:
30///       - `Curve`: On success, a valid representation of the curve.
31///       - `CurveError`: On failure, detailed information about why the curve could not be generated.
32///
33/// # Error Handling
34///
35/// Given the reliance on precise data and operations, the `Curvable` trait integrates
36/// tightly with the `CurveError` type to handle potential issues, such as:
37/// - Invalid points or coordinates (`Point2DError`)
38/// - Issues in curve construction (`ConstructionError`)
39/// - Errors during interpolation (`InterpolationError`)
40/// - General computation or operational failures (`OperationError`, `StdError`)
41///
42/// # Example Usage
43///
44/// This trait forms the basis for creating highly customizable and precise curve objects,
45/// ensuring compatibility with mathematical, computational, or graphical operations.
46///
47/// Implementing this trait allows an object to seamlessly interact with the higher-level
48/// functionalities in the `curves` module, such as visualization, analysis, and transformation.
49///
50/// # See Also
51/// - [`Curve`]: Represents the mathematical curve generated by this trait.
52/// - [`CurveError`]: Error type encapsulating issues encountered during curve generation.
53pub trait Curvable {
54    /// Generates a `Curve` representation of the implementer.
55    ///
56    /// The `curve` method is the core functionality of this trait. It is expected
57    /// to compute and return a `Curve` object that accurately describes the
58    /// implementer's structure or state in the context of a two-dimensional curve.
59    ///
60    /// # Returns
61    ///
62    /// - `Ok(Curve)`: When the curve is successfully generated.
63    /// - `Err(CurveError)`: If curve generation fails for any reason.
64    ///
65    /// # Errors
66    ///
67    /// The method may return a `CurveError` in scenarios such as:
68    /// - **Point2DError**: If an invalid or missing 2D point is encountered.
69    /// - **ConstructionError**: When the curve cannot be initialized due to invalid input.
70    /// - **InterpolationError**: If there are issues during interpolation of the curve's points.
71    /// - **AnalysisError**: In cases where analytical operations on the curve fail.
72    ///
73    /// This ensures robust error handling for downstream processes and applications.
74    fn curve(&self) -> Result<Curve, CurveError>;
75}
76
77/// A trait for generating statistical curves based on metrics
78///
79/// This trait provides methods to generate curves that match specified
80/// statistical properties. It extends the `MetricsExtractor` trait to
81/// ensure implementing types can both extract and generate metrics.
82pub trait StatisticalCurve: MetricsExtractor {
83    /// Retrieves the x-axis values for the statistical curve.
84    ///
85    /// This method returns a vector of `Decimal` values representing the x-coordinates
86    /// of the points that define the curve. These x-values are essential for plotting
87    /// the curve and performing various statistical analyses.
88    ///
89    /// # Returns
90    ///
91    /// A `Vec<Decimal>` containing the x-values of the statistical curve. Each `Decimal`
92    /// represents a point on the x-axis.
93    fn get_x_values(&self) -> Vec<Decimal>;
94
95    /// Generates a statistical curve with properties that match the provided metrics.
96    ///
97    /// # Overview
98    /// This function creates a curve with statistical properties that approximate the
99    /// specified metrics. It uses a combination of normal distribution sampling and
100    /// transformations to achieve the desired statistical characteristics.
101    ///
102    /// # Parameters
103    /// - `basic_metrics`: Basic statistical properties like mean, median, mode, and standard deviation.
104    /// - `shape_metrics`: Shape-related metrics like skewness and kurtosis.
105    /// - `range_metrics`: Range information including min, max, and quartile data.
106    /// - `trend_metrics`: Trend information including slope and intercept for linear trend.
107    /// - `num_points`: Number of points to generate in the curve.
108    /// - `seed`: Optional random seed for reproducible curve generation.
109    ///
110    /// # Returns
111    /// - `Result<Curve, CurveError>`: A curve matching the specified statistical properties,
112    ///   or an error if generation fails.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`CurveError::ConstructionError`] when the requested
117    /// combination of basic, shape and range-parameter metrics is
118    /// infeasible (e.g. negative variance, inconsistent quantiles), and
119    /// propagates [`CurveError::MetricsError`] when the per-sample
120    /// metric evaluation fails.
121    fn generate_statistical_curve(
122        &self,
123        basic_metrics: &BasicMetrics,
124        shape_metrics: &ShapeMetrics,
125        range_metrics: &RangeMetrics,
126        trend_metrics: &TrendMetrics,
127        num_points: usize,
128        seed: Option<u64>,
129    ) -> Result<Curve, CurveError> {
130        if num_points < 2 {
131            return Err(CurveError::OperationError(
132                OperationErrorKind::InvalidParameters {
133                    operation: "generate_statistical_curve".to_string(),
134                    reason: "Number of points must be at least 2".to_string(),
135                },
136            ));
137        }
138
139        // Every generated sample is placed on one of the curve's own
140        // abscissas, so the request cannot exceed how many there are.
141        let x_values: Vec<Decimal> = self.get_x_values();
142        if num_points > x_values.len() {
143            return Err(CurveError::OperationError(
144                OperationErrorKind::InvalidParameters {
145                    operation: "generate_statistical_curve".to_string(),
146                    reason: format!(
147                        "Number of points ({num_points}) exceeds the {} x-values available on the curve",
148                        x_values.len()
149                    ),
150                },
151            ));
152        }
153
154        // Every `Decimal` reaches `f64` through `decimal_to_f64`, the nearest
155        // `f64` (#828). A value it cannot convert is an error; the previous
156        // conversions fell back to 0, 1 or the slot's own value instead.
157        let to_f64 = |value: Decimal| -> Result<f64, CurveError> {
158            decimal_to_f64(value).map_err(|e| {
159                CurveError::invalid_parameters("generate_statistical_curve", &e.to_string())
160            })
161        };
162        let mean = to_f64(basic_metrics.mean)?;
163        let std_dev = to_f64(basic_metrics.std_dev)?;
164
165        // Initialize random number generator with optional seed
166        let seed_value = seed.unwrap_or_else(rand::random);
167        let mut rng = StdRng::seed_from_u64(seed_value);
168        // Create a normal distribution with the given mean and standard deviation
169
170        let mut y_values: Vec<f64> = if basic_metrics.std_dev != Decimal::ZERO {
171            let normal = Normal::new(mean, std_dev).map_err(|e| {
172                error!(
173                    "Failed to create normal distribution with mean {} and std_dev {}: {}",
174                    basic_metrics.mean, basic_metrics.std_dev, e
175                );
176                CurveError::MetricsError(e.to_string())
177            })?;
178
179            // Generate initial y-values from the normal distribution
180            (0..num_points)
181                .map(|_| {
182                    let u: f64 = rng.random_range(0.0..1.0); // Generate a value between 0 and 1
183                    normal.inverse_cdf(u) // Convert to normal distribution using inverse CDF
184                })
185                .collect()
186        } else {
187            vec![mean; num_points]
188        };
189
190        // Apply transformations to match skewness and kurtosis (simplified approach)
191        let skewness = to_f64(shape_metrics.skewness)?;
192        let kurtosis = to_f64(shape_metrics.kurtosis)?;
193
194        // Apply skewness transformation (simplified approach)
195        if skewness.abs() > 0.01 {
196            for y in &mut y_values {
197                // Apply a simple transformation to induce skewness
198                *y += skewness * (*y - mean).powi(2);
199            }
200        }
201
202        // Apply kurtosis transformation (simplified approach)
203        if kurtosis.abs() > 0.01 {
204            for y in &mut y_values {
205                // Apply a simple transformation to adjust kurtosis
206                let z = (*y - mean) / std_dev;
207                *y += kurtosis * 0.1 * z.powi(3);
208            }
209        }
210
211        // Apply trend (slope and intercept)
212        let slope = to_f64(trend_metrics.slope)?;
213        if slope.abs() > 0.001 {
214            let intercept = to_f64(trend_metrics.intercept)?;
215            for (i, y) in y_values.iter_mut().enumerate() {
216                let x = x_values.get(i).ok_or_else(|| {
217                    CurveError::invalid_parameters(
218                        "generate_statistical_curve",
219                        "fewer x-values than generated samples",
220                    )
221                })?;
222                *y += slope * to_f64(*x)? + intercept;
223            }
224        }
225
226        // Scale y-values to match the range
227        let current_min = y_values.iter().cloned().fold(f64::INFINITY, f64::min);
228        let current_max = y_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
229        let current_range = current_max - current_min;
230
231        let target_min = to_f64(range_metrics.min.y)?;
232        let target_max = to_f64(range_metrics.max.y)?;
233        let target_range = target_max - target_min;
234
235        // Scale and shift the y-values to match the target range
236        if current_range > 0.0 {
237            for y in &mut y_values {
238                *y = ((*y - current_min) / current_range) * target_range + target_min;
239            }
240        }
241
242        // Ensure mode value is included
243        if num_points > 3 {
244            let index = rng.random_range(0..(num_points / 3));
245            let mode = to_f64(basic_metrics.mode)?;
246            if let Some(slot) = y_values.get_mut(index) {
247                *slot = mode;
248            }
249        }
250
251        // Create points and construct curve
252        let mut points = BTreeSet::new();
253        for (x, y) in x_values.iter().zip(&y_values).take(num_points) {
254            let point = Point2D::from_f64_tuple(to_f64(*x)?, *y)?;
255            points.insert(point);
256        }
257
258        // Create the curve
259        Ok(Curve::new(points))
260    }
261
262    /// Generates a refined statistical curve that iteratively adjusts to better match
263    /// the target metrics.
264    ///
265    /// This method extends the basic curve generation by performing multiple attempts
266    /// with adjusted parameters until the resulting curve metrics are within the specified
267    /// tolerance of the target metrics.
268    ///
269    /// # Parameters
270    /// - `basic_metrics`: Target basic statistical metrics
271    /// - `shape_metrics`: Target shape metrics
272    /// - `range_metrics`: Target range metrics
273    /// - `trend_metrics`: Target trend metrics
274    /// - `num_points`: Number of points to generate
275    /// - `max_attempts`: Maximum number of generation attempts (default: 5)
276    /// - `tolerance`: Acceptable difference between target and actual metrics (default: 0.1)
277    /// - `seed`: Optional random seed for reproducibility
278    ///
279    /// # Returns
280    /// - `Result<Curve, CurveError>`: The generated curve or an error
281    ///
282    /// # Errors
283    ///
284    /// Same failure surface as
285    /// [`StatisticalCurve::generate_statistical_curve`], plus
286    /// [`CurveError::ConstructionError`] when the refinement loop
287    /// exhausts its iteration budget without matching the tolerance.
288    #[allow(clippy::too_many_arguments)]
289    fn generate_refined_statistical_curve(
290        &self,
291        basic_metrics: &BasicMetrics,
292        shape_metrics: &ShapeMetrics,
293        range_metrics: &RangeMetrics,
294        trend_metrics: &TrendMetrics,
295        num_points: usize,
296        max_attempts: usize,
297        tolerance: Decimal,
298        seed: Option<u64>,
299    ) -> Result<Curve, CurveError> {
300        let max_tries = if max_attempts == 0 { 5 } else { max_attempts };
301        let mut seed_value = seed.unwrap_or_else(rand::random);
302
303        for _ in 0..max_tries {
304            let curve = self.generate_statistical_curve(
305                basic_metrics,
306                shape_metrics,
307                range_metrics,
308                trend_metrics,
309                num_points,
310                Some(seed_value),
311            )?;
312
313            if self.verify_curve_metrics(&curve, basic_metrics, tolerance)? {
314                return Ok(curve);
315            }
316
317            // Try a different seed for the next attempt
318            seed_value = seed_value.wrapping_add(1); // scan-banned: allow -- the next RNG seed; the seed space is modular, not a financial value
319        }
320
321        // Return the last generated curve even if it doesn't perfectly match
322        self.generate_statistical_curve(
323            basic_metrics,
324            shape_metrics,
325            range_metrics,
326            trend_metrics,
327            num_points,
328            Some(seed_value),
329        )
330    }
331
332    /// Verifies if the metrics of the generated curve match the target metrics
333    /// within the specified tolerance.
334    ///
335    /// # Parameters
336    /// - `curve`: The curve to verify
337    /// - `target_metrics`: The target basic metrics to compare against
338    /// - `tolerance`: Maximum acceptable difference between actual and target metrics
339    ///
340    /// # Returns
341    /// - `Result<bool, CurveError>`: True if metrics match within tolerance, false otherwise
342    ///
343    /// # Errors
344    ///
345    /// Propagates any [`CurveError::MetricsError`] or
346    /// [`CurveError::ConstructionError`] raised while recomputing the
347    /// curve's actual metrics (typically when the curve contains
348    /// non-finite samples or lacks the density required for the
349    /// requested metric). Returns [`CurveError::AnalysisError`] when the gap
350    /// between an actual and a target metric leaves the representable
351    /// `Decimal` range.
352    fn verify_curve_metrics(
353        &self,
354        curve: &Curve,
355        target_metrics: &BasicMetrics,
356        tolerance: Decimal,
357    ) -> Result<bool, CurveError> {
358        let actual_metrics = curve
359            .compute_basic_metrics()
360            .map_err(|e| CurveError::MetricsError(format!("Failed to compute metrics: {e}")))?;
361
362        // Check if the key metrics are within tolerance. A target sitting at
363        // the far end of the `Decimal` range makes the gap itself
364        // unrepresentable, which is a comparison failure rather than a
365        // silently large difference.
366        let mean_diff = d_sub(
367            actual_metrics.mean,
368            target_metrics.mean,
369            "StatisticalCurve::verify_curve_metrics::mean_diff",
370        )
371        .map_err(|e| CurveError::AnalysisError(e.to_string()))?
372        .abs();
373        let std_dev_diff = d_sub(
374            actual_metrics.std_dev,
375            target_metrics.std_dev,
376            "StatisticalCurve::verify_curve_metrics::std_dev_diff",
377        )
378        .map_err(|e| CurveError::AnalysisError(e.to_string()))?
379        .abs();
380
381        Ok(mean_diff <= tolerance && std_dev_diff <= tolerance)
382    }
383}
384
385#[cfg(test)]
386mod tests_statistical_curve {
387    use super::*;
388    use crate::error::MetricsError;
389    use crate::geometrics::RiskMetrics;
390    use optionstratlib_core::utils::Len;
391    use rust_decimal_macros::dec;
392    use std::collections::BTreeSet;
393
394    // A simple struct that implements StatisticalCurve trait for testing
395    struct TestCurveGenerator;
396
397    impl Len for TestCurveGenerator {
398        fn len(&self) -> usize {
399            unreachable!()
400        }
401    }
402
403    impl MetricsExtractor for TestCurveGenerator {
404        fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
405            // Return default metrics for testing
406            Ok(BasicMetrics {
407                mean: dec!(0.0),
408                median: dec!(0.0),
409                mode: dec!(0.0),
410                std_dev: dec!(1.0),
411            })
412        }
413
414        fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
415            Ok(ShapeMetrics {
416                skewness: dec!(0.0),
417                kurtosis: dec!(0.0),
418                peaks: vec![],
419                valleys: vec![],
420                inflection_points: vec![],
421            })
422        }
423
424        fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
425            Ok(RangeMetrics {
426                min: Point2D::new(dec!(0.0), dec!(0.0)),
427                max: Point2D::new(dec!(10.0), dec!(10.0)),
428                range: dec!(10.0),
429                quartiles: (dec!(2.5), dec!(5.0), dec!(7.5)),
430                interquartile_range: dec!(5.0),
431            })
432        }
433
434        fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
435            Ok(TrendMetrics {
436                slope: dec!(0.0),
437                intercept: dec!(0.0),
438                r_squared: dec!(0.0),
439                moving_average: vec![],
440            })
441        }
442
443        fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
444            unreachable!()
445        }
446    }
447
448    impl StatisticalCurve for TestCurveGenerator {
449        fn get_x_values(&self) -> Vec<Decimal> {
450            (0..10).map(Decimal::from).collect()
451        }
452    }
453
454    // Create a struct that implements Curve's compute_basic_metrics for testing
455    impl Curvable for TestCurveGenerator {
456        fn curve(&self) -> Result<Curve, CurveError> {
457            // Create a simple linear curve
458            let points: BTreeSet<Point2D> = (0..10)
459                .map(|i| Point2D::new(Decimal::from(i), Decimal::from(i)))
460                .collect();
461            Ok(Curve::new(points))
462        }
463    }
464
465    #[test]
466    fn test_get_x_values() {
467        let generator = TestCurveGenerator;
468        let x_values = generator.get_x_values();
469
470        assert_eq!(x_values.len(), 10);
471        assert_eq!(x_values[0], dec!(0));
472        assert_eq!(x_values[9], dec!(9));
473    }
474
475    #[test]
476    fn test_generate_statistical_curve_invalid_points() {
477        let generator = TestCurveGenerator;
478
479        let basic_metrics = BasicMetrics::default();
480        let shape_metrics = ShapeMetrics::default();
481        let range_metrics = RangeMetrics::default();
482        let trend_metrics = TrendMetrics::default();
483
484        // Test with less than 2 points
485        let result = generator.generate_statistical_curve(
486            &basic_metrics,
487            &shape_metrics,
488            &range_metrics,
489            &trend_metrics,
490            1, // Invalid: less than 2 points
491            None,
492        );
493
494        assert!(result.is_err());
495        if let Err(CurveError::OperationError(OperationErrorKind::InvalidParameters {
496            operation,
497            reason,
498        })) = result
499        {
500            assert_eq!(operation, "generate_statistical_curve");
501            assert!(reason.contains("Number of points must be at least 2"));
502        } else {
503            panic!("Expected InvalidParameters error");
504        }
505    }
506
507    #[test]
508    fn test_verify_curve_metrics() {
509        let generator = TestCurveGenerator;
510
511        // Create a simple curve
512        let curve = generator.curve().unwrap();
513
514        // Define target metrics close to the actual metrics
515        let target_metrics = BasicMetrics {
516            mean: dec!(4.5), // Close to actual mean (4.5 for points 0..9)
517            median: dec!(4.5),
518            mode: dec!(0.0),
519            std_dev: dec!(3.0), // Close to actual std_dev
520        };
521
522        // Verify with a generous tolerance
523        let result = generator.verify_curve_metrics(&curve, &target_metrics, dec!(1.0));
524        assert!(result.is_ok());
525        assert!(result.unwrap());
526
527        // Verify with a strict tolerance that should fail
528        let result = generator.verify_curve_metrics(&curve, &target_metrics, dec!(0.1));
529        assert!(result.is_ok());
530        // This might fail depending on the actual metrics of the curve
531    }
532
533    // Test Default implementation for the required metrics structs
534    impl Default for BasicMetrics {
535        fn default() -> Self {
536            Self {
537                mean: dec!(0.0),
538                median: dec!(0.0),
539                mode: dec!(0.0),
540                std_dev: dec!(1.0),
541            }
542        }
543    }
544
545    impl Default for ShapeMetrics {
546        fn default() -> Self {
547            Self {
548                skewness: dec!(0.0),
549                kurtosis: dec!(0.0),
550                peaks: vec![],
551                valleys: vec![],
552                inflection_points: vec![],
553            }
554        }
555    }
556
557    impl Default for RangeMetrics {
558        fn default() -> Self {
559            Self {
560                min: Point2D::new(dec!(0.0), dec!(0.0)),
561                max: Point2D::new(dec!(10.0), dec!(10.0)),
562                range: dec!(10.0),
563                quartiles: (dec!(2.5), dec!(5.0), dec!(7.5)),
564                interquartile_range: dec!(5.0),
565            }
566        }
567    }
568
569    impl Default for TrendMetrics {
570        fn default() -> Self {
571            Self {
572                slope: dec!(0.0),
573                intercept: dec!(0.0),
574                r_squared: dec!(0.0),
575                moving_average: vec![],
576            }
577        }
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584    use crate::error::MetricsError;
585    use crate::geometrics::RiskMetrics;
586    use optionstratlib_core::utils::Len;
587    use rust_decimal::Decimal;
588    use rust_decimal_macros::dec;
589    use std::collections::BTreeSet;
590
591    struct MockCurvable {
592        points: BTreeSet<Point2D>,
593        should_fail: bool,
594    }
595
596    impl MockCurvable {
597        fn new(should_fail: bool) -> Self {
598            let mut points = BTreeSet::new();
599
600            if !should_fail {
601                // Create some valid points
602                points.insert(Point2D::new(dec!(1.0), dec!(2.0)));
603                points.insert(Point2D::new(dec!(2.0), dec!(3.0)));
604                points.insert(Point2D::new(dec!(3.0), dec!(4.0)));
605            }
606
607            Self {
608                points,
609                should_fail,
610            }
611        }
612    }
613
614    impl Curvable for MockCurvable {
615        fn curve(&self) -> Result<Curve, CurveError> {
616            if self.should_fail {
617                Err(CurveError::OperationError(
618                    OperationErrorKind::InvalidParameters {
619                        operation: "curve".to_string(),
620                        reason: "Test failure".to_string(),
621                    },
622                ))
623            } else {
624                Ok(Curve::new(self.points.clone()))
625            }
626        }
627    }
628
629    // Mock implementation for StatisticalCurve
630    struct MockStatisticalCurve {
631        x_values: Vec<Decimal>,
632    }
633
634    impl MockStatisticalCurve {
635        fn new() -> Self {
636            let x_values = vec![dec!(1.0), dec!(2.0), dec!(3.0), dec!(4.0), dec!(5.0)];
637            Self { x_values }
638        }
639    }
640
641    impl Len for MockStatisticalCurve {
642        fn len(&self) -> usize {
643            self.x_values.len()
644        }
645    }
646
647    impl MetricsExtractor for MockStatisticalCurve {
648        fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
649            Ok(BasicMetrics {
650                mean: dec!(3.0),
651                median: dec!(3.0),
652                mode: dec!(3.0),
653                std_dev: dec!(1.5),
654            })
655        }
656
657        fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
658            Ok(ShapeMetrics {
659                skewness: dec!(0.0),
660                kurtosis: dec!(0.0),
661                peaks: vec![],
662                valleys: vec![],
663                inflection_points: vec![],
664            })
665        }
666
667        fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
668            Ok(RangeMetrics {
669                min: Point2D::new(dec!(1.0), dec!(1.0)),
670                max: Point2D::new(dec!(5.0), dec!(5.0)),
671                range: dec!(4.0),
672                quartiles: (Default::default(), Default::default(), Default::default()),
673                interquartile_range: Default::default(),
674            })
675        }
676
677        fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
678            Ok(TrendMetrics {
679                slope: dec!(1.0),
680                intercept: dec!(0.0),
681                r_squared: dec!(1.0),
682                moving_average: vec![],
683            })
684        }
685
686        fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
687            Ok(RiskMetrics {
688                volatility: Default::default(),
689                value_at_risk: Default::default(),
690                expected_shortfall: Default::default(),
691                coefficient_of_variation: Default::default(),
692                sharpe_ratio: Default::default(),
693            })
694        }
695    }
696
697    impl StatisticalCurve for MockStatisticalCurve {
698        fn get_x_values(&self) -> Vec<Decimal> {
699            self.x_values.clone()
700        }
701    }
702
703    // Tests para Curvable
704    #[test]
705    fn test_curvable_success() {
706        let mock = MockCurvable::new(false);
707        let result = mock.curve();
708
709        assert!(result.is_ok(), "Curve generation should succeed");
710        let curve = result.unwrap();
711
712        assert_eq!(curve.len(), 3, "Curve should have 3 points");
713    }
714
715    #[test]
716    fn test_curvable_failure() {
717        let mock = MockCurvable::new(true);
718        let result = mock.curve();
719
720        assert!(result.is_err(), "Curve generation should fail");
721
722        if let Err(CurveError::OperationError(OperationErrorKind::InvalidParameters {
723            operation,
724            reason,
725        })) = result
726        {
727            assert_eq!(operation, "curve", "Operation name should match");
728            assert_eq!(reason, "Test failure", "Error reason should match");
729        } else {
730            panic!("Unexpected error type");
731        }
732    }
733
734    // Tests para StatisticalCurve
735    #[test]
736    fn test_get_x_values() {
737        let mock = MockStatisticalCurve::new();
738        let x_values = mock.get_x_values();
739
740        assert_eq!(x_values.len(), 5, "Should return 5 x values");
741        assert_eq!(x_values[0], dec!(1.0), "First x value should be 1.0");
742        assert_eq!(x_values[4], dec!(5.0), "Last x value should be 5.0");
743    }
744
745    #[test]
746    fn test_generate_statistical_curve_invalid_points() {
747        let mock = MockStatisticalCurve::new();
748
749        let basic_metrics = BasicMetrics {
750            mean: dec!(3.0),
751            median: dec!(3.0),
752            mode: dec!(3.0),
753            std_dev: dec!(1.5),
754        };
755
756        let shape_metrics = ShapeMetrics {
757            skewness: dec!(0.0),
758            kurtosis: dec!(0.0),
759            peaks: vec![],
760            valleys: vec![],
761            inflection_points: vec![],
762        };
763
764        let range_metrics = RangeMetrics {
765            min: Point2D::new(dec!(1.0), dec!(1.0)),
766            max: Point2D::new(dec!(5.0), dec!(5.0)),
767            range: dec!(4.0),
768            quartiles: (Default::default(), Default::default(), Default::default()),
769            interquartile_range: Default::default(),
770        };
771
772        let trend_metrics = TrendMetrics {
773            slope: dec!(1.0),
774            intercept: dec!(0.0),
775            r_squared: dec!(1.0),
776            moving_average: vec![],
777        };
778
779        let result = mock.generate_statistical_curve(
780            &basic_metrics,
781            &shape_metrics,
782            &range_metrics,
783            &trend_metrics,
784            1,
785            None,
786        );
787
788        assert!(result.is_err(), "Should fail with less than 2 points");
789        if let Err(CurveError::OperationError(OperationErrorKind::InvalidParameters {
790            operation,
791            reason,
792        })) = result
793        {
794            assert_eq!(operation, "generate_statistical_curve");
795            assert!(reason.contains("at least 2"));
796        } else {
797            panic!("Unexpected error type");
798        }
799    }
800
801    #[test]
802    fn test_generate_statistical_curve_success() {
803        let mock = MockStatisticalCurve::new();
804
805        let basic_metrics = BasicMetrics {
806            mean: dec!(3.0),
807            median: dec!(3.0),
808            mode: dec!(3.0),
809            std_dev: dec!(1.5),
810        };
811
812        let shape_metrics = ShapeMetrics {
813            skewness: dec!(0.0),
814            kurtosis: dec!(0.0),
815            peaks: vec![],
816            valleys: vec![],
817            inflection_points: vec![],
818        };
819
820        let range_metrics = RangeMetrics {
821            min: Point2D::new(dec!(1.0), dec!(1.0)),
822            max: Point2D::new(dec!(5.0), dec!(5.0)),
823            range: dec!(4.0),
824            quartiles: (Default::default(), Default::default(), Default::default()),
825            interquartile_range: Default::default(),
826        };
827
828        let trend_metrics = TrendMetrics {
829            slope: dec!(1.0),
830            intercept: dec!(0.0),
831            r_squared: dec!(1.0),
832            moving_average: vec![],
833        };
834
835        // Test valid curve generation
836        let result = mock.generate_statistical_curve(
837            &basic_metrics,
838            &shape_metrics,
839            &range_metrics,
840            &trend_metrics,
841            5,        // 5 puntos
842            Some(42), // seed fijo para prueba reproducible
843        );
844
845        assert!(result.is_ok(), "Should successfully generate a curve");
846        let curve = result.unwrap();
847        assert!(curve.len() > 0, "Generated curve should contain points");
848    }
849
850    #[test]
851    fn test_verify_curve_metrics() {
852        let mock = MockStatisticalCurve::new();
853
854        // Crear una curva simple para pruebas
855        let mut points = BTreeSet::new();
856        points.insert(Point2D::new(dec!(1.0), dec!(2.0)));
857        points.insert(Point2D::new(dec!(2.0), dec!(3.0)));
858        points.insert(Point2D::new(dec!(3.0), dec!(4.0)));
859        let curve = Curve::new(points);
860
861        // Target metrics close to actual (within tolerance)
862        let target_metrics = BasicMetrics {
863            mean: dec!(3.1),
864            median: dec!(3.0),
865            mode: dec!(3.0),
866            std_dev: dec!(1.0),
867        };
868
869        // Tolerancia suficiente para que pase
870        let result = mock.verify_curve_metrics(&curve, &target_metrics, dec!(0.5));
871        assert!(result.is_ok(), "Verification should not fail");
872        assert!(result.unwrap(), "Metrics should be within tolerance");
873
874        // Tolerancia insuficiente para que pase
875        let result = mock.verify_curve_metrics(&curve, &target_metrics, dec!(0.05));
876        assert!(result.is_ok(), "Verification should not fail");
877        assert!(!result.unwrap(), "Metrics should not be within tolerance");
878    }
879
880    #[test]
881    fn test_refined_statistical_curve() {
882        let mock = MockStatisticalCurve::new();
883
884        let basic_metrics = BasicMetrics {
885            mean: dec!(3.0),
886            median: dec!(3.0),
887            mode: dec!(3.0),
888            std_dev: dec!(1.5),
889        };
890
891        let shape_metrics = ShapeMetrics {
892            skewness: dec!(0.0),
893            kurtosis: dec!(0.0),
894            peaks: vec![],
895            valleys: vec![],
896            inflection_points: vec![],
897        };
898
899        let range_metrics = RangeMetrics {
900            min: Point2D::new(dec!(1.0), dec!(1.0)),
901            max: Point2D::new(dec!(5.0), dec!(5.0)),
902            range: dec!(4.0),
903            quartiles: (Default::default(), Default::default(), Default::default()),
904            interquartile_range: Default::default(),
905        };
906
907        let trend_metrics = TrendMetrics {
908            slope: dec!(1.0),
909            intercept: dec!(0.0),
910            r_squared: dec!(1.0),
911            moving_average: vec![],
912        };
913
914        let result = mock.generate_refined_statistical_curve(
915            &basic_metrics,
916            &shape_metrics,
917            &range_metrics,
918            &trend_metrics,
919            5,
920            3,         // máx 3 intentos
921            dec!(0.2), // tolerancia
922            Some(42),  // seed fijo
923        );
924
925        assert!(
926            result.is_ok(),
927            "Should successfully generate a refined curve"
928        );
929        let curve = result.unwrap();
930        assert!(curve.len() > 0, "Generated curve should contain points");
931    }
932}
933
934#[cfg(test)]
935mod tests_statistical_curve_generation {
936    use super::*;
937    use crate::error::MetricsError;
938    use crate::geometrics::RiskMetrics;
939    use optionstratlib_core::utils::Len;
940    use rust_decimal_macros::dec;
941
942    // Enhanced version of TestCurveGenerator to test edge cases
943    struct EnhancedTestCurveGenerator {
944        x_values: Vec<Decimal>,
945        fail_metrics: bool,
946    }
947
948    impl EnhancedTestCurveGenerator {
949        fn new(fail_metrics: bool) -> Self {
950            Self {
951                x_values: (0..10).map(Decimal::from).collect(),
952                fail_metrics,
953            }
954        }
955    }
956
957    impl Len for EnhancedTestCurveGenerator {
958        fn len(&self) -> usize {
959            self.x_values.len()
960        }
961
962        fn is_empty(&self) -> bool {
963            self.x_values.is_empty()
964        }
965    }
966
967    impl MetricsExtractor for EnhancedTestCurveGenerator {
968        fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
969            if self.fail_metrics {
970                Err(MetricsError::BasicError(
971                    "compute_basic_metrics error".to_string(),
972                ))
973            } else {
974                Ok(BasicMetrics {
975                    mean: dec!(5.0),
976                    median: dec!(5.0),
977                    mode: dec!(5.0),
978                    std_dev: dec!(2.0),
979                })
980            }
981        }
982
983        fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
984            Ok(ShapeMetrics {
985                skewness: dec!(0.5), // Non-zero skewness for testing
986                kurtosis: dec!(0.5), // Non-zero kurtosis for testing
987                peaks: vec![],
988                valleys: vec![],
989                inflection_points: vec![],
990            })
991        }
992
993        fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
994            Ok(RangeMetrics {
995                min: Point2D::new(dec!(0.0), dec!(0.0)),
996                max: Point2D::new(dec!(10.0), dec!(10.0)),
997                range: dec!(10.0),
998                quartiles: (dec!(2.5), dec!(5.0), dec!(7.5)),
999                interquartile_range: dec!(5.0),
1000            })
1001        }
1002
1003        fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
1004            Ok(TrendMetrics {
1005                slope: dec!(1.0),     // Non-zero slope
1006                intercept: dec!(0.5), // Non-zero intercept
1007                r_squared: dec!(0.9),
1008                moving_average: vec![],
1009            })
1010        }
1011
1012        fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
1013            Ok(RiskMetrics {
1014                volatility: dec!(0.5),
1015                value_at_risk: dec!(1.0),
1016                expected_shortfall: dec!(1.5),
1017                coefficient_of_variation: dec!(0.8),
1018                sharpe_ratio: dec!(1.2),
1019            })
1020        }
1021    }
1022
1023    impl StatisticalCurve for EnhancedTestCurveGenerator {
1024        fn get_x_values(&self) -> Vec<Decimal> {
1025            self.x_values.clone()
1026        }
1027    }
1028
1029    impl Curvable for EnhancedTestCurveGenerator {
1030        fn curve(&self) -> Result<Curve, CurveError> {
1031            // Create a simple curve
1032            let points: BTreeSet<Point2D> = (0..10)
1033                .map(|i| Point2D::new(Decimal::from(i), Decimal::from(i)))
1034                .collect();
1035            Ok(Curve::new(points))
1036        }
1037    }
1038
1039    #[test]
1040    fn test_generate_statistical_curve_with_skewness() {
1041        let generator = EnhancedTestCurveGenerator::new(false);
1042
1043        let basic_metrics = generator.compute_basic_metrics().unwrap();
1044        let shape_metrics = generator.compute_shape_metrics().unwrap();
1045        let range_metrics = generator.compute_range_metrics().unwrap();
1046        let trend_metrics = generator.compute_trend_metrics().unwrap();
1047
1048        // Generate curve with significant skewness
1049        let result = generator.generate_statistical_curve(
1050            &basic_metrics,
1051            &shape_metrics,
1052            &range_metrics,
1053            &trend_metrics,
1054            10,       // Sufficient points
1055            Some(42), // Fixed seed for reproducibility
1056        );
1057
1058        assert!(result.is_ok(), "Failed to generate curve with skewness");
1059
1060        // Verify curve properties
1061        let curve = result.unwrap();
1062        assert_eq!(curve.points.len(), 10);
1063    }
1064
1065    #[test]
1066    fn test_generate_statistical_curve_with_kurtosis() {
1067        let generator = EnhancedTestCurveGenerator::new(false);
1068
1069        let basic_metrics = generator.compute_basic_metrics().unwrap();
1070        let mut shape_metrics = generator.compute_shape_metrics().unwrap();
1071        shape_metrics.skewness = dec!(0.0); // Zero skewness
1072        shape_metrics.kurtosis = dec!(1.0); // Significant kurtosis
1073        let range_metrics = generator.compute_range_metrics().unwrap();
1074        let trend_metrics = generator.compute_trend_metrics().unwrap();
1075
1076        let result = generator.generate_statistical_curve(
1077            &basic_metrics,
1078            &shape_metrics,
1079            &range_metrics,
1080            &trend_metrics,
1081            10,
1082            Some(42),
1083        );
1084
1085        assert!(result.is_ok(), "Failed to generate curve with kurtosis");
1086    }
1087
1088    #[test]
1089    fn test_generate_statistical_curve_with_trend() {
1090        // Test curve generation with significant trend (line 142)
1091        let generator = EnhancedTestCurveGenerator::new(false);
1092
1093        let basic_metrics = generator.compute_basic_metrics().unwrap();
1094        let shape_metrics = generator.compute_shape_metrics().unwrap();
1095        let range_metrics = generator.compute_range_metrics().unwrap();
1096        let mut trend_metrics = generator.compute_trend_metrics().unwrap();
1097        trend_metrics.slope = dec!(2.0); // Significant slope
1098
1099        let result = generator.generate_statistical_curve(
1100            &basic_metrics,
1101            &shape_metrics,
1102            &range_metrics,
1103            &trend_metrics,
1104            10,
1105            Some(42),
1106        );
1107
1108        assert!(result.is_ok(), "Failed to generate curve with trend");
1109    }
1110
1111    /// `verify_curve_metrics` recomputes the metrics of the curve it is
1112    /// given, so the generator's failing `compute_basic_metrics` plays no
1113    /// part: the curve `(0, 0), (1, 1)` has mean 0.5 and standard deviation
1114    /// 0.5, which miss the zero targets, so the verification is `Ok(false)`;
1115    /// the asserted `is_ok()` passed either way (#824).
1116    #[test]
1117    fn test_verify_curve_metrics_failure() {
1118        let generator = EnhancedTestCurveGenerator::new(true);
1119
1120        // Create a simple curve for testing
1121        let points = BTreeSet::from_iter(vec![
1122            Point2D::new(dec!(0.0), dec!(0.0)),
1123            Point2D::new(dec!(1.0), dec!(1.0)),
1124        ]);
1125        let curve = Curve::new(points);
1126
1127        // Target metrics for comparison
1128        let target_metrics = BasicMetrics {
1129            mean: dec!(0.0),
1130            median: dec!(-5.0),
1131            mode: dec!(5.0),
1132            std_dev: dec!(0.0),
1133        };
1134
1135        let result = generator.verify_curve_metrics(&curve, &target_metrics, dec!(0.0000001));
1136        assert!(matches!(result, Ok(false)));
1137
1138        // The curve's own metrics verify.
1139        let matching = BasicMetrics {
1140            mean: dec!(0.5),
1141            median: dec!(0.5),
1142            mode: dec!(0.0),
1143            std_dev: dec!(0.5),
1144        };
1145        let result = generator.verify_curve_metrics(&curve, &matching, dec!(0.0000001));
1146        assert!(matches!(result, Ok(true)));
1147    }
1148
1149    #[test]
1150    fn test_generate_refined_statistical_curve() {
1151        // Test refined statistical curve generation (lines 157, 162-163, 165-167, 171)
1152        let generator = EnhancedTestCurveGenerator::new(false);
1153
1154        let basic_metrics = generator.compute_basic_metrics().unwrap();
1155        let shape_metrics = generator.compute_shape_metrics().unwrap();
1156        let range_metrics = generator.compute_range_metrics().unwrap();
1157        let trend_metrics = generator.compute_trend_metrics().unwrap();
1158
1159        // Test with very strict tolerance that will likely need multiple iterations
1160        let result = generator.generate_refined_statistical_curve(
1161            &basic_metrics,
1162            &shape_metrics,
1163            &range_metrics,
1164            &trend_metrics,
1165            10,
1166            3,          // max_attempts = 3
1167            dec!(0.01), // strict tolerance
1168            Some(42),
1169        );
1170
1171        assert!(result.is_ok(), "Failed to generate refined curve");
1172
1173        // Test with zero max_attempts to trigger the default value
1174        let result = generator.generate_refined_statistical_curve(
1175            &basic_metrics,
1176            &shape_metrics,
1177            &range_metrics,
1178            &trend_metrics,
1179            10,
1180            0, // should default to 5
1181            dec!(0.5),
1182            Some(42),
1183        );
1184
1185        assert!(
1186            result.is_ok(),
1187            "Failed to generate refined curve with default attempts"
1188        );
1189    }
1190}
1191
1192#[cfg(test)]
1193mod tests_statistical_curve_edge_cases {
1194    use super::*;
1195    use crate::error::MetricsError;
1196    use crate::geometrics::RiskMetrics;
1197    use optionstratlib_core::utils::Len;
1198    use rust_decimal_macros::dec;
1199
1200    #[allow(dead_code)]
1201    struct EmptyXValuesGenerator;
1202
1203    impl Len for EmptyXValuesGenerator {
1204        fn len(&self) -> usize {
1205            0
1206        }
1207        fn is_empty(&self) -> bool {
1208            true
1209        }
1210    }
1211
1212    impl MetricsExtractor for EmptyXValuesGenerator {
1213        fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
1214            Ok(BasicMetrics {
1215                mean: dec!(0.0),
1216                median: dec!(0.0),
1217                mode: dec!(0.0),
1218                std_dev: dec!(0.0),
1219            })
1220        }
1221
1222        fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
1223            Ok(ShapeMetrics {
1224                skewness: dec!(0.0),
1225                kurtosis: dec!(0.0),
1226                peaks: vec![],
1227                valleys: vec![],
1228                inflection_points: vec![],
1229            })
1230        }
1231
1232        fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
1233            Ok(RangeMetrics {
1234                min: Point2D::new(dec!(0.0), dec!(0.0)),
1235                max: Point2D::new(dec!(0.0), dec!(0.0)),
1236                range: dec!(0.0),
1237                quartiles: (dec!(0.0), dec!(0.0), dec!(0.0)),
1238                interquartile_range: dec!(0.0),
1239            })
1240        }
1241
1242        fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
1243            Ok(TrendMetrics {
1244                slope: dec!(0.0),
1245                intercept: dec!(0.0),
1246                r_squared: dec!(0.0),
1247                moving_average: vec![],
1248            })
1249        }
1250
1251        fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
1252            Ok(RiskMetrics {
1253                volatility: dec!(0.0),
1254                value_at_risk: dec!(0.0),
1255                expected_shortfall: dec!(0.0),
1256                coefficient_of_variation: dec!(0.0),
1257                sharpe_ratio: dec!(0.0),
1258            })
1259        }
1260    }
1261
1262    impl StatisticalCurve for EmptyXValuesGenerator {
1263        fn get_x_values(&self) -> Vec<Decimal> {
1264            vec![] // Empty x values
1265        }
1266    }
1267
1268    // Special generator for testing edge cases in statistical distribution
1269    struct SpecialStatisticalGenerator {
1270        zero_std_dev: bool,
1271    }
1272
1273    impl SpecialStatisticalGenerator {
1274        fn new(zero_std_dev: bool) -> Self {
1275            Self { zero_std_dev }
1276        }
1277    }
1278
1279    impl Len for SpecialStatisticalGenerator {
1280        fn len(&self) -> usize {
1281            10
1282        }
1283        fn is_empty(&self) -> bool {
1284            false
1285        }
1286    }
1287
1288    impl MetricsExtractor for SpecialStatisticalGenerator {
1289        fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
1290            Ok(BasicMetrics {
1291                mean: dec!(5.0),
1292                median: dec!(5.0),
1293                mode: dec!(5.0),
1294                std_dev: if self.zero_std_dev {
1295                    dec!(0.0)
1296                } else {
1297                    dec!(1.0)
1298                },
1299            })
1300        }
1301
1302        fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
1303            Ok(ShapeMetrics {
1304                skewness: dec!(0), // No skewness
1305                kurtosis: dec!(0), // No kurtosis
1306                peaks: vec![],
1307                valleys: vec![],
1308                inflection_points: vec![],
1309            })
1310        }
1311
1312        fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
1313            Ok(RangeMetrics {
1314                min: Point2D::new(dec!(0.0), dec!(0.0)),
1315                max: Point2D::new(dec!(10.0), dec!(10.0)),
1316                range: dec!(10.0),
1317                quartiles: (dec!(2.5), dec!(5.0), dec!(7.5)),
1318                interquartile_range: dec!(5.0),
1319            })
1320        }
1321
1322        fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
1323            Ok(TrendMetrics {
1324                slope: dec!(0.0), // No slope
1325                intercept: dec!(0.0),
1326                r_squared: dec!(1.0),
1327                moving_average: vec![],
1328            })
1329        }
1330
1331        fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
1332            Ok(RiskMetrics {
1333                volatility: dec!(0.0),
1334                value_at_risk: dec!(0.0),
1335                expected_shortfall: dec!(0.0),
1336                coefficient_of_variation: dec!(0.0),
1337                sharpe_ratio: dec!(0.0),
1338            })
1339        }
1340    }
1341
1342    impl StatisticalCurve for SpecialStatisticalGenerator {
1343        fn get_x_values(&self) -> Vec<Decimal> {
1344            (0..10).map(Decimal::from).collect()
1345        }
1346    }
1347
1348    #[test]
1349    fn test_generate_statistical_curve_mode_inclusion() {
1350        // Test including mode in generated curve (lines 183-185)
1351        let generator = SpecialStatisticalGenerator::new(false);
1352
1353        let basic_metrics = BasicMetrics {
1354            mean: dec!(5.0),
1355            median: dec!(5.0),
1356            mode: dec!(7.5), // Specific mode to test inclusion
1357            std_dev: dec!(1.0),
1358        };
1359
1360        let shape_metrics = generator.compute_shape_metrics().unwrap();
1361        let range_metrics = generator.compute_range_metrics().unwrap();
1362        let trend_metrics = generator.compute_trend_metrics().unwrap();
1363
1364        let result = generator.generate_statistical_curve(
1365            &basic_metrics,
1366            &shape_metrics,
1367            &range_metrics,
1368            &trend_metrics,
1369            10,
1370            Some(42),
1371        );
1372
1373        assert!(result.is_ok(), "Failed to generate curve including mode");
1374    }
1375
1376    #[test]
1377    fn test_generate_statistical_curve_with_zero_std_dev() {
1378        // Test handling of zero standard deviation (lines 174-175)
1379        let generator = SpecialStatisticalGenerator::new(true);
1380
1381        let basic_metrics = generator.compute_basic_metrics().unwrap();
1382        let shape_metrics = generator.compute_shape_metrics().unwrap();
1383        let range_metrics = generator.compute_range_metrics().unwrap();
1384        let trend_metrics = generator.compute_trend_metrics().unwrap();
1385
1386        // This should still work but will generate points with little variation
1387        let result = generator.generate_statistical_curve(
1388            &basic_metrics,
1389            &shape_metrics,
1390            &range_metrics,
1391            &trend_metrics,
1392            10,
1393            Some(42),
1394        );
1395
1396        assert!(result.is_ok(), "Failed to handle zero standard deviation");
1397    }
1398
1399    #[test]
1400    fn test_generate_statistical_curve_scale_range() {
1401        // Test scaling to match target range (lines 187-189, 192, 194)
1402        let generator = SpecialStatisticalGenerator::new(false);
1403
1404        let basic_metrics = generator.compute_basic_metrics().unwrap();
1405        let shape_metrics = generator.compute_shape_metrics().unwrap();
1406        let range_metrics = RangeMetrics {
1407            min: Point2D::new(dec!(0.0), dec!(-10.0)), // Negative minimum
1408            max: Point2D::new(dec!(10.0), dec!(10.0)), // Positive maximum
1409            range: dec!(20.0),
1410            quartiles: (dec!(2.5), dec!(5.0), dec!(7.5)),
1411            interquartile_range: dec!(5.0),
1412        };
1413        let trend_metrics = generator.compute_trend_metrics().unwrap();
1414
1415        let result = generator.generate_statistical_curve(
1416            &basic_metrics,
1417            &shape_metrics,
1418            &range_metrics,
1419            &trend_metrics,
1420            10,
1421            Some(42),
1422        );
1423
1424        assert!(result.is_ok(), "Failed to scale curve to target range");
1425
1426        // Test with zero range (current_range = 0)
1427        let basic_metrics = BasicMetrics {
1428            mean: dec!(5.0),
1429            median: dec!(5.0),
1430            mode: dec!(5.0),
1431            std_dev: dec!(0.0), // Zero std_dev will create points with same value
1432        };
1433
1434        let result = generator.generate_statistical_curve(
1435            &basic_metrics,
1436            &shape_metrics,
1437            &range_metrics,
1438            &trend_metrics,
1439            10,
1440            Some(42),
1441        );
1442
1443        assert!(result.is_ok(), "Failed to handle zero current range");
1444    }
1445
1446    #[test]
1447    fn test_normal_distribution_error() {
1448        // Test handling of normal distribution errors (lines 205, 208)
1449        // This would test the error case in line 205 for Normal::new, but it's hard to trigger in a test
1450        // as the implementation uses f64 values which can represent almost any value without error
1451
1452        // Instead we'll test the code surrounding those lines with valid inputs
1453        let generator = SpecialStatisticalGenerator::new(false);
1454
1455        let basic_metrics = BasicMetrics {
1456            mean: dec!(5.0),
1457            median: dec!(5.0),
1458            mode: dec!(5.0),
1459            std_dev: dec!(1.0),
1460        };
1461
1462        let shape_metrics = generator.compute_shape_metrics().unwrap();
1463        let range_metrics = generator.compute_range_metrics().unwrap();
1464        let trend_metrics = generator.compute_trend_metrics().unwrap();
1465
1466        let result = generator.generate_statistical_curve(
1467            &basic_metrics,
1468            &shape_metrics,
1469            &range_metrics,
1470            &trend_metrics,
1471            10,
1472            Some(42),
1473        );
1474
1475        assert!(
1476            result.is_ok(),
1477            "Failed to generate curve with normal distribution"
1478        );
1479    }
1480
1481    #[test]
1482    fn test_generate_statistical_curve_insufficient_points() {
1483        // Test error for insufficient number of points (lines 259-260)
1484        let generator = SpecialStatisticalGenerator::new(false);
1485
1486        let basic_metrics = generator.compute_basic_metrics().unwrap();
1487        let shape_metrics = generator.compute_shape_metrics().unwrap();
1488        let range_metrics = generator.compute_range_metrics().unwrap();
1489        let trend_metrics = generator.compute_trend_metrics().unwrap();
1490
1491        // Try with less than 2 points
1492        let result = generator.generate_statistical_curve(
1493            &basic_metrics,
1494            &shape_metrics,
1495            &range_metrics,
1496            &trend_metrics,
1497            1, // Invalid: needs at least 2 points
1498            Some(42),
1499        );
1500
1501        assert!(result.is_err());
1502        if let Err(CurveError::OperationError(OperationErrorKind::InvalidParameters {
1503            operation,
1504            reason,
1505        })) = result
1506        {
1507            assert_eq!(operation, "generate_statistical_curve");
1508            assert!(reason.contains("at least 2"));
1509        } else {
1510            panic!("Expected InvalidParameters error");
1511        }
1512    }
1513
1514    #[test]
1515    fn test_verify_metrics_within_tolerance() {
1516        // Test verification within tolerance (lines 299-300)
1517        let generator = SpecialStatisticalGenerator::new(false);
1518
1519        // Create a test curve
1520        let points = BTreeSet::from_iter(vec![
1521            Point2D::new(dec!(0.0), dec!(1.0)),
1522            Point2D::new(dec!(1.0), dec!(2.0)),
1523            Point2D::new(dec!(2.0), dec!(3.0)),
1524        ]);
1525        let curve = Curve::new(points);
1526
1527        // Target metrics close to the actual curve metrics
1528        let target_metrics = BasicMetrics {
1529            mean: dec!(2.0), // Actual mean of [1,2,3] is 2.0
1530            median: dec!(2.0),
1531            mode: dec!(2.0),
1532            std_dev: dec!(0.81), // Approximate std dev of [1,2,3]
1533        };
1534
1535        // Should be within tolerance
1536        let result = generator.verify_curve_metrics(&curve, &target_metrics, dec!(0.2));
1537        assert!(result.is_ok());
1538        assert!(result.unwrap(), "Metrics should be within tolerance");
1539
1540        // Should be outside tolerance
1541        let result = generator.verify_curve_metrics(&curve, &target_metrics, dec!(0.001));
1542        assert!(result.is_ok());
1543        assert!(!result.unwrap(), "Metrics should be outside tolerance");
1544    }
1545}