1use crate::calibration::config::OptimizationConfig;
2use crate::calibration::types::{MarketDataRow, ModelCalibrator};
3use cmaes_lbfgsb::cmaes::{canonical_cmaes_optimize, CmaesCanonicalConfig};
5use cmaes_lbfgsb::lbfgsb_optimize::lbfgsb_optimize;
6
7pub struct CalibrationProcess {
9 model: Box<dyn ModelCalibrator>,
10 config: OptimizationConfig,
11 market_data: Vec<MarketDataRow>,
12 initial_guess: Option<Vec<f64>>,
13}
14
15impl CalibrationProcess {
16 pub fn new(
17 model: Box<dyn ModelCalibrator>,
18 config: OptimizationConfig,
19 market_data: Vec<MarketDataRow>,
20 ) -> Self {
21 Self {
22 model,
23 config,
24 market_data,
25 initial_guess: None,
26 }
27 }
28
29 pub fn with_initial_guess(mut self, guess: Vec<f64>) -> Self {
31 self.initial_guess = Some(guess);
32 self
33 }
34
35 pub fn run(&self) -> (f64, Vec<f64>) {
37 let (best_obj, best_params) = calibrate_model(
38 &*self.model,
39 &self.market_data,
40 &self.config,
41 self.initial_guess.clone(),
42 );
43 (best_obj, best_params)
44 }
45}
46
47pub fn calibrate_model(
50 model: &dyn ModelCalibrator,
51 market_data: &[MarketDataRow],
52 config: &OptimizationConfig,
53 initial_guess: Option<Vec<f64>>,
54) -> (f64, Vec<f64>) {
55 let bounds = model.param_bounds();
57 let obj_fn = |x: &[f64]| model.evaluate_objective(x, market_data);
58
59 let (best_obj, best_sol) = {
62 let relaxed_bounds = model.param_bounds();
64 let relaxed_obj_fn = |x: &[f64]| model.evaluate_objective(x, market_data);
65
66 let cmaes_config = CmaesCanonicalConfig {
68 population_size: config.pop_size,
69 max_generations: config.max_gen,
70 seed: config.cmaes.seed.unwrap_or(123456),
71 c1: None, c_mu: None,
73 c_sigma: None,
74 d_sigma: None,
75 parallel_eval: config.cmaes.parallel_eval,
76 verbosity: config.cmaes.verbosity,
77 ipop_restarts: config.cmaes.ipop_restarts,
78 ipop_increase_factor: config.cmaes.ipop_increase_factor,
79 bipop_restarts: config.cmaes.bipop_restarts,
80 total_evals_budget: config.cmaes.total_evals_budget,
81 use_subrun_budgeting: config.cmaes.use_subrun_budgeting,
82 alpha_mu: None,
83 hsig_threshold_factor: None,
84 bipop_small_population_factor: None,
85 bipop_small_budget_factor: None,
86 bipop_large_budget_factor: None,
87 bipop_large_pop_increase_factor: None,
88 max_bound_iterations: None,
89 eig_precision_threshold: None,
90 min_eig_value: None,
91 matrix_op_threshold: None,
92 stagnation_limit: None,
93 min_sigma: None,
94 };
95
96 if let Some(ref guess) = initial_guess {
98 let use_mini_cmaes = config.cmaes.mini_cmaes_on_refinement;
100
101 if use_mini_cmaes {
102 if config.cmaes.verbosity > 0 {
103 println!(
104 "Using provided initial guess => launching mini CMA-ES around it. \
105 Then local L-BFGS refinement."
106 );
107 }
108
109 let guess_obj = relaxed_obj_fn(guess);
111 if config.cmaes.verbosity > 0 {
112 println!(" Initial guess objective = {:.6}", guess_obj);
113 }
114
115 let cmaes_result = canonical_cmaes_optimize(
117 relaxed_obj_fn,
118 relaxed_bounds,
119 cmaes_config,
120 Some(guess.clone()),
122 );
123
124 let (_, relaxed_params) = cmaes_result.best_solution;
126 let standard_obj = obj_fn(&relaxed_params);
127 (standard_obj, relaxed_params)
128 } else {
129 if config.cmaes.verbosity > 0 {
131 println!(
132 "Using provided initial guess => skipping mini CMA-ES and proceeding directly to L-BFGS-B."
133 );
134 }
135
136 let guess_obj = obj_fn(guess);
138 if config.cmaes.verbosity > 0 {
139 println!(" Initial guess objective = {:.6}", guess_obj);
140 }
141
142 (guess_obj, guess.clone())
143 }
144 } else {
145 if config.cmaes.verbosity > 0 {
147 println!("No initial guess provided => running full CMA-ES with BIPOP restarts");
148 }
149
150 let cmaes_result =
151 canonical_cmaes_optimize(relaxed_obj_fn, relaxed_bounds, cmaes_config, None);
152
153 let (_, relaxed_params) = cmaes_result.best_solution;
155 let standard_obj = obj_fn(&relaxed_params);
156 (standard_obj, relaxed_params)
157 }
158 };
159
160 if config.cmaes.lbfgsb_enabled {
162 if config.cmaes.verbosity > 0 {
163 println!("Running L-BFGS-B refinement on best CMA-ES solution...");
164 }
165
166 let mut refined_solution = best_sol.clone();
167 let refine_res = lbfgsb_optimize(
168 &mut refined_solution,
169 bounds,
170 &obj_fn,
171 config.cmaes.lbfgsb_max_iterations,
172 config.tolerance,
173 if config.cmaes.verbosity >= 1 {
174 Some(|_current_x: &[f64], current_obj: f64| {
175 println!("L-BFGS-B iteration => objective = {:.6}", current_obj);
176 })
177 } else {
178 None
179 },
180 None, );
182
183 match refine_res {
184 Ok((loc_obj, loc_sol)) => {
185 if loc_obj < best_obj {
186 if config.cmaes.verbosity > 0 {
187 println!(
188 "L-BFGS-B improved objective: {:.6} -> {:.6}",
189 best_obj, loc_obj
190 );
191 }
192 (loc_obj, loc_sol)
193 } else {
194 if config.cmaes.verbosity > 0 {
195 println!("L-BFGS-B did not improve objective, keeping CMA-ES solution");
196 }
197 (best_obj, best_sol)
198 }
199 }
200 Err(e) => {
201 if config.cmaes.verbosity > 0 {
202 println!("L-BFGS-B failed: {:?}, keeping CMA-ES solution", e);
203 }
204 (best_obj, best_sol)
205 }
206 }
207 } else {
208 if config.cmaes.verbosity > 0 {
209 println!("L-BFGS-B refinement disabled, using CMA-ES solution directly");
210 }
211 (best_obj, best_sol)
212 }
213}
214
215pub fn calibrate_model_adaptive(
217 mut model: Box<dyn ModelCalibrator>,
218 market_data: &[MarketDataRow],
219 config: &OptimizationConfig,
220 initial_guess: Option<Vec<f64>>,
221) -> (f64, Vec<f64>, Vec<(f64, f64)>) {
222 if !config.adaptive_bounds.enabled {
223 let (obj, params) = calibrate_model(&*model, market_data, config, initial_guess);
224 let bounds = model.param_bounds().to_vec();
225 return (obj, params, bounds);
226 }
227
228 let mut best_obj = f64::MAX;
229 let mut best_params = Vec::new();
230
231 for iter in 0..config.adaptive_bounds.max_iterations {
232 let (obj, params) = calibrate_model(&*model, market_data, config, initial_guess.clone());
233 if obj < best_obj {
234 best_obj = obj;
235 best_params = params.clone();
236 }
237 let adjusted = model.expand_bounds_if_needed(
238 ¶ms,
239 config.adaptive_bounds.proximity_threshold,
240 config.adaptive_bounds.expansion_factor,
241 );
242
243 if config.cmaes.verbosity > 0 {
244 if adjusted {
245 println!(
246 "Adaptive iteration {}: Expanded bounds for next iteration",
247 iter + 1
248 );
249 } else {
250 println!(
251 "Adaptive iteration {}: No expansion needed, stopping early",
252 iter + 1
253 );
254 }
255 }
256
257 if !adjusted {
258 break;
259 }
260 }
261
262 let bounds = model.param_bounds().to_vec();
263 (best_obj, best_params, bounds)
264}