Skip to main content

converge_analytics/packs/forecasting/
solver.rs

1use super::types::*;
2use converge_pack::gate::GateResult as Result;
3use converge_pack::gate::{ProblemSpec, ReplayEnvelope, SolverReport};
4
5pub struct ExponentialSmoothingSolver;
6
7impl ExponentialSmoothingSolver {
8    pub fn solve(
9        &self,
10        input: &ForecastingInput,
11        spec: &ProblemSpec,
12    ) -> Result<(ForecastingOutput, SolverReport)> {
13        let alpha = input.alpha;
14        let values = &input.values;
15        let n = values.len();
16
17        // Simple exponential smoothing
18        let mut level = values[0];
19        let mut residuals = Vec::with_capacity(n - 1);
20
21        for &v in &values[1..] {
22            let forecast = level;
23            residuals.push(v - forecast);
24            level = alpha * v + (1.0 - alpha) * level;
25        }
26
27        // Residual standard deviation for confidence intervals
28        let residual_std = if residuals.is_empty() {
29            0.0
30        } else {
31            let mean_r = residuals.iter().sum::<f64>() / residuals.len() as f64;
32            let var = residuals.iter().map(|r| (r - mean_r).powi(2)).sum::<f64>()
33                / residuals.len() as f64;
34            var.sqrt()
35        };
36
37        // Forecast ahead
38        let predictions: Vec<ForecastPoint> = (1..=input.horizon)
39            .map(|step| {
40                let width = 1.96 * residual_std * (step as f64).sqrt();
41                ForecastPoint {
42                    step,
43                    value: level,
44                    lower: level - width,
45                    upper: level + width,
46                }
47            })
48            .collect();
49
50        let output = ForecastingOutput {
51            predictions,
52            residual_std,
53            horizon: input.horizon,
54        };
55
56        let replay = ReplayEnvelope::minimal(spec.seed());
57        let confidence = if residual_std > 0.0 {
58            (1.0 / (1.0 + residual_std)).clamp(0.3, 0.95)
59        } else {
60            0.95
61        };
62        let report = SolverReport::optimal("exponential-smoothing-v1", confidence, replay);
63
64        Ok((output, report))
65    }
66}