use anofox_forecast::postprocess::{BacktestConfig, PointForecasts, PostProcessor};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Backtesting PostProcessors ===\n");
let n = 200;
let forecasts: Vec<f64> = (0..n).map(|i| 50.0 + 0.2 * i as f64).collect();
let actuals: Vec<f64> = forecasts
.iter()
.enumerate()
.map(|(i, &f)| f + 2.0 * ((i as f64 * 0.1).sin()) + 0.5)
.collect();
let point_forecasts = PointForecasts::from_values(forecasts);
println!("Data: {} samples\n", n);
println!("--- Basic Backtest (Expanding Window) ---");
let processor = PostProcessor::conformal(0.90);
let config = BacktestConfig::new()
.initial_window(50)
.step(10)
.horizon(5)
.expanding(true);
let result = processor.backtest(&point_forecasts, &actuals, config)?;
println!("Configuration:");
println!(" Initial window: 50");
println!(" Step: 10");
println!(" Horizon: 5");
println!(" Window type: Expanding");
println!();
println!("Results:");
println!(" Number of folds: {}", result.n_folds());
println!(" Overall coverage: {:.1}%", result.coverage() * 100.0);
println!(" Average interval width: {:.3}", result.interval_widths());
println!(
" Calibration error (vs 90%): {:.1}%",
result.calibration_error(0.90) * 100.0
);
println!("\nPer-fold summary (first 5 folds):");
println!(
"{:<6} {:>12} {:>10} {:>12} {:>10}",
"Fold", "Train Size", "Test Size", "Coverage", "Width"
);
println!("{:-<55}", "");
for fold in result.folds().take(5) {
println!(
"{:<6} {:>12} {:>10} {:>11.1}% {:>10.3}",
fold.fold_idx,
fold.train_size(),
fold.test_size(),
fold.coverage() * 100.0,
fold.avg_width()
);
}
println!(" ...");
println!("\n--- Rolling Window Backtest ---");
let config_rolling = BacktestConfig::new()
.initial_window(50)
.step(10)
.horizon(5)
.expanding(false);
let result_rolling = processor.backtest(&point_forecasts, &actuals, config_rolling)?;
println!("Results:");
println!(" Number of folds: {}", result_rolling.n_folds());
println!(
" Overall coverage: {:.1}%",
result_rolling.coverage() * 100.0
);
println!(
" Average interval width: {:.3}",
result_rolling.interval_widths()
);
let fold0 = result_rolling.fold(0).unwrap();
let fold_last = result_rolling.fold(result_rolling.n_folds() - 1).unwrap();
println!(
" First fold train size: {}, Last fold train size: {}",
fold0.train_size(),
fold_last.train_size()
);
println!("\n--- Horizon-Aware Backtest ---");
let config_horizon = BacktestConfig::new()
.initial_window(50)
.step(10)
.horizon(7)
.expanding(true)
.horizon_aware(true);
let result_horizon = processor.backtest(&point_forecasts, &actuals, config_horizon)?;
println!("Coverage by horizon:");
let coverage_by_h = result_horizon.coverage_by_horizon();
let widths_by_h = result_horizon.widths_by_horizon();
println!("{:<10} {:>12} {:>12}", "Horizon", "Coverage", "Avg Width");
println!("{:-<36}", "");
for h in 1..=7 {
let cov = coverage_by_h.get(&h).unwrap_or(&0.0);
let width = widths_by_h.get(&h).unwrap_or(&0.0);
println!("{:<10} {:>11.1}% {:>12.3}", h, cov * 100.0, width);
}
println!("\nNote: Coverage/width may vary by horizon due to error growth.");
println!("\n--- Training Calibrated Model from Backtest ---");
let calibrated = result_horizon.calibrated_model(&processor)?;
println!(
"Pooled model trained on {} samples",
result_horizon.pooled_forecasts().len()
);
let new_forecasts = PointForecasts::from_values(vec![90.0, 90.5, 91.0, 91.5, 92.0]);
let intervals = processor.predict_intervals(&calibrated, &new_forecasts)?;
println!("\nPredictions for new forecasts:");
println!("{:<10} {:>10} {:>10}", "Forecast", "Lower", "Upper");
println!("{:-<32}", "");
for i in 0..5 {
println!(
"{:<10.1} {:>10.2} {:>10.2}",
new_forecasts.values()[i],
intervals.lower()[i],
intervals.upper()[i]
);
}
println!("\n--- Horizon-Specific Calibration ---");
let horizon_models = result_horizon.calibrated_model_by_horizon(&processor)?;
println!("Trained {} horizon-specific models", horizon_models.len());
println!("Horizons: {:?}", horizon_models.horizons());
let horizon_intervals =
processor.predict_intervals_by_horizon(&horizon_models, &new_forecasts)?;
println!("\nPredictions with horizon-specific calibration:");
println!(
"{:<10} {:>10} {:>10} {:>10}",
"Horizon", "Forecast", "Lower", "Upper"
);
println!("{:-<44}", "");
for i in 0..5 {
println!(
"{:<10} {:>10.1} {:>10.2} {:>10.2}",
i + 1,
new_forecasts.values()[i],
horizon_intervals.lower()[i],
horizon_intervals.upper()[i]
);
}
println!("\n--- Method Comparison via Backtest ---");
let methods: Vec<(&str, PostProcessor)> = vec![
("Conformal", PostProcessor::conformal(0.90)),
(
"HistoricalSim",
PostProcessor::historical_sim(vec![0.05, 0.5, 0.95]),
),
("Normal", PostProcessor::normal(vec![0.05, 0.5, 0.95])),
];
let config_compare = BacktestConfig::new().initial_window(50).step(10).horizon(5);
println!(
"{:<15} {:>12} {:>12} {:>12}",
"Method", "Coverage", "Width", "Cal Error"
);
println!("{:-<54}", "");
for (name, proc) in &methods {
let res = proc.backtest(&point_forecasts, &actuals, config_compare.clone())?;
println!(
"{:<15} {:>11.1}% {:>12.3} {:>11.1}%",
name,
res.coverage() * 100.0,
res.interval_widths(),
res.calibration_error(0.90) * 100.0
);
}
println!("\n=== Summary ===");
println!("- Use BacktestConfig to configure rolling/expanding windows");
println!("- horizon_aware=true tracks metrics per forecast horizon");
println!("- calibrated_model() returns pooled model for production");
println!("- calibrated_model_by_horizon() returns per-horizon models");
println!("- predict_intervals_by_horizon() applies horizon-specific calibration");
Ok(())
}