1mod bisection;
58mod brent;
59mod hybrid;
60mod newton;
61mod secant;
62
63pub use bisection::bisection;
64pub use brent::brent;
65pub use hybrid::{hybrid, hybrid_numerical};
66pub use newton::{newton_raphson, newton_raphson_numerical};
67pub use secant::secant;
68
69use crate::error::MathResult;
70
71pub const DEFAULT_TOLERANCE: f64 = 1e-10;
73
74pub const DEFAULT_MAX_ITERATIONS: u32 = 100;
76
77#[derive(Debug, Clone, Copy)]
79pub struct SolverConfig {
80 pub tolerance: f64,
82 pub max_iterations: u32,
84}
85
86impl Default for SolverConfig {
87 fn default() -> Self {
88 Self {
89 tolerance: DEFAULT_TOLERANCE,
90 max_iterations: DEFAULT_MAX_ITERATIONS,
91 }
92 }
93}
94
95impl SolverConfig {
96 #[must_use]
98 pub fn new(tolerance: f64, max_iterations: u32) -> Self {
99 Self {
100 tolerance,
101 max_iterations,
102 }
103 }
104
105 #[must_use]
107 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
108 self.tolerance = tolerance;
109 self
110 }
111
112 #[must_use]
114 pub fn with_max_iterations(mut self, max_iterations: u32) -> Self {
115 self.max_iterations = max_iterations;
116 self
117 }
118}
119
120pub trait RootFinder {
122 fn find_root<F>(&self, f: F, initial_guess: f64, config: &SolverConfig) -> MathResult<f64>
130 where
131 F: Fn(f64) -> f64;
132}
133
134pub trait Solver: Send + Sync {
153 fn solve<F, D>(
167 &self,
168 f: F,
169 derivative: Option<D>,
170 initial_guess: f64,
171 bounds: Option<(f64, f64)>,
172 config: &SolverConfig,
173 ) -> MathResult<SolverResult>
174 where
175 F: Fn(f64) -> f64,
176 D: Fn(f64) -> f64;
177
178 fn name(&self) -> &'static str;
180}
181
182#[derive(Debug, Clone, Copy, Default)]
184pub struct NewtonSolver;
185
186impl Solver for NewtonSolver {
187 fn solve<F, D>(
188 &self,
189 f: F,
190 derivative: Option<D>,
191 initial_guess: f64,
192 _bounds: Option<(f64, f64)>,
193 config: &SolverConfig,
194 ) -> MathResult<SolverResult>
195 where
196 F: Fn(f64) -> f64,
197 D: Fn(f64) -> f64,
198 {
199 match derivative {
200 Some(df) => newton_raphson(f, df, initial_guess, config),
201 None => newton_raphson_numerical(f, initial_guess, config),
202 }
203 }
204
205 fn name(&self) -> &'static str {
206 "Newton-Raphson"
207 }
208}
209
210#[derive(Debug, Clone, Copy, Default)]
212pub struct BrentSolver;
213
214impl Solver for BrentSolver {
215 fn solve<F, D>(
216 &self,
217 f: F,
218 _derivative: Option<D>,
219 initial_guess: f64,
220 bounds: Option<(f64, f64)>,
221 config: &SolverConfig,
222 ) -> MathResult<SolverResult>
223 where
224 F: Fn(f64) -> f64,
225 D: Fn(f64) -> f64,
226 {
227 let (a, b) = bounds.unwrap_or((initial_guess - 1.0, initial_guess + 1.0));
228 brent(f, a, b, config)
229 }
230
231 fn name(&self) -> &'static str {
232 "Brent"
233 }
234}
235
236#[derive(Debug, Clone, Copy, Default)]
238pub struct BisectionSolver;
239
240impl Solver for BisectionSolver {
241 fn solve<F, D>(
242 &self,
243 f: F,
244 _derivative: Option<D>,
245 initial_guess: f64,
246 bounds: Option<(f64, f64)>,
247 config: &SolverConfig,
248 ) -> MathResult<SolverResult>
249 where
250 F: Fn(f64) -> f64,
251 D: Fn(f64) -> f64,
252 {
253 let (a, b) = bounds.unwrap_or((initial_guess - 1.0, initial_guess + 1.0));
254 bisection(f, a, b, config)
255 }
256
257 fn name(&self) -> &'static str {
258 "Bisection"
259 }
260}
261
262#[derive(Debug, Clone, Copy, Default)]
264pub struct SecantSolver;
265
266impl Solver for SecantSolver {
267 fn solve<F, D>(
268 &self,
269 f: F,
270 _derivative: Option<D>,
271 initial_guess: f64,
272 bounds: Option<(f64, f64)>,
273 config: &SolverConfig,
274 ) -> MathResult<SolverResult>
275 where
276 F: Fn(f64) -> f64,
277 D: Fn(f64) -> f64,
278 {
279 let (x0, x1) = bounds.unwrap_or((initial_guess - 0.1, initial_guess + 0.1));
280 secant(f, x0, x1, config)
281 }
282
283 fn name(&self) -> &'static str {
284 "Secant"
285 }
286}
287
288#[derive(Debug, Clone, Copy, Default)]
290pub struct HybridSolver;
291
292impl Solver for HybridSolver {
293 fn solve<F, D>(
294 &self,
295 f: F,
296 derivative: Option<D>,
297 initial_guess: f64,
298 bounds: Option<(f64, f64)>,
299 config: &SolverConfig,
300 ) -> MathResult<SolverResult>
301 where
302 F: Fn(f64) -> f64,
303 D: Fn(f64) -> f64,
304 {
305 match derivative {
306 Some(df) => hybrid(f, df, initial_guess, bounds, config),
307 None => hybrid_numerical(f, initial_guess, bounds, config),
308 }
309 }
310
311 fn name(&self) -> &'static str {
312 "Hybrid (Newton + Brent)"
313 }
314}
315
316#[derive(Debug, Clone, Copy)]
318pub struct SolverResult {
319 pub root: f64,
321 pub iterations: u32,
323 pub residual: f64,
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use approx::assert_relative_eq;
331
332 #[test]
333 fn test_solver_config() {
334 let config = SolverConfig::default()
335 .with_tolerance(1e-8)
336 .with_max_iterations(50);
337
338 assert!((config.tolerance - 1e-8).abs() < f64::EPSILON);
339 assert_eq!(config.max_iterations, 50);
340 }
341
342 #[test]
343 fn test_solver_trait_newton() {
344 let solver = NewtonSolver;
345 let f = |x: f64| x * x - 2.0;
346 let df = |x: f64| 2.0 * x;
347
348 let result = solver
349 .solve(f, Some(df), 1.5, None, &SolverConfig::default())
350 .unwrap();
351
352 assert_relative_eq!(result.root, std::f64::consts::SQRT_2, epsilon = 1e-10);
353 assert_eq!(solver.name(), "Newton-Raphson");
354 }
355
356 #[test]
357 fn test_solver_trait_brent() {
358 let solver = BrentSolver;
359 let f = |x: f64| x * x - 2.0;
360 let no_deriv: Option<fn(f64) -> f64> = None;
361
362 let result = solver
363 .solve(f, no_deriv, 1.5, Some((1.0, 2.0)), &SolverConfig::default())
364 .unwrap();
365
366 assert_relative_eq!(result.root, std::f64::consts::SQRT_2, epsilon = 1e-10);
367 assert_eq!(solver.name(), "Brent");
368 }
369
370 #[test]
371 fn test_solver_trait_hybrid() {
372 let solver = HybridSolver;
373 let f = |x: f64| x * x - 2.0;
374 let df = |x: f64| 2.0 * x;
375
376 let result = solver
377 .solve(f, Some(df), 1.5, Some((1.0, 2.0)), &SolverConfig::default())
378 .unwrap();
379
380 assert_relative_eq!(result.root, std::f64::consts::SQRT_2, epsilon = 1e-10);
381 }
382
383 fn bond_price(yield_rate: f64, coupon: f64, face: f64, years: i32, freq: i32) -> f64 {
387 let periods = years * freq;
388 let coupon_per_period = coupon / freq as f64;
389 let discount_rate = yield_rate / freq as f64;
390
391 let mut pv = 0.0;
392 for t in 1..=periods {
393 pv += coupon_per_period / (1.0 + discount_rate).powi(t);
394 }
395 pv += face / (1.0 + discount_rate).powi(periods);
396 pv
397 }
398
399 fn bond_price_derivative(
401 yield_rate: f64,
402 coupon: f64,
403 face: f64,
404 years: i32,
405 freq: i32,
406 ) -> f64 {
407 let periods = years * freq;
408 let coupon_per_period = coupon / freq as f64;
409 let discount_rate = yield_rate / freq as f64;
410
411 let mut dpv = 0.0;
412 for t in 1..=periods {
413 dpv -= (t as f64 / freq as f64) * coupon_per_period / (1.0 + discount_rate).powi(t + 1);
414 }
415 dpv -= (periods as f64 / freq as f64) * face / (1.0 + discount_rate).powi(periods + 1);
416 dpv
417 }
418
419 #[test]
420 fn test_ytm_par_bond() {
421 let coupon = 5.0;
423 let face = 100.0;
424 let target_price = 100.0; let years = 10;
426 let freq = 2; let f = |y: f64| bond_price(y, coupon, face, years, freq) - target_price;
429 let df = |y: f64| bond_price_derivative(y, coupon, face, years, freq);
430
431 let result = newton_raphson(f, df, 0.05, &SolverConfig::default()).unwrap();
432
433 assert_relative_eq!(result.root, 0.05, epsilon = 1e-10);
435 }
436
437 #[test]
438 fn test_ytm_discount_bond() {
439 let coupon = 5.0;
441 let face = 100.0;
442 let target_price = 95.0; let years = 5;
444 let freq = 2;
445
446 let f = |y: f64| bond_price(y, coupon, face, years, freq) - target_price;
447 let df = |y: f64| bond_price_derivative(y, coupon, face, years, freq);
448
449 let result = hybrid(f, df, 0.05, Some((0.0, 0.20)), &SolverConfig::default()).unwrap();
450
451 assert!(result.root > 0.05);
453 assert!(f(result.root).abs() < 1e-10);
455 }
456
457 #[test]
458 fn test_ytm_premium_bond() {
459 let coupon = 7.0;
461 let face = 100.0;
462 let target_price = 105.0; let years = 5;
464 let freq = 2;
465
466 let f = |y: f64| bond_price(y, coupon, face, years, freq) - target_price;
467 let df = |y: f64| bond_price_derivative(y, coupon, face, years, freq);
468
469 let result = hybrid(f, df, 0.07, Some((0.0, 0.20)), &SolverConfig::default()).unwrap();
470
471 assert!(result.root < 0.07);
473 assert!(f(result.root).abs() < 1e-10);
474 }
475
476 #[test]
477 fn test_ytm_all_solvers_agree() {
478 let coupon = 6.0;
480 let face = 100.0;
481 let target_price = 98.0;
482 let years = 7;
483 let freq = 2;
484
485 let f = |y: f64| bond_price(y, coupon, face, years, freq) - target_price;
486 let df = |y: f64| bond_price_derivative(y, coupon, face, years, freq);
487 let config = SolverConfig::default();
488
489 let newton_result = newton_raphson(f, df, 0.06, &config).unwrap();
490 let brent_result = brent(f, 0.0, 0.20, &config).unwrap();
491 let hybrid_result = hybrid(f, df, 0.06, Some((0.0, 0.20)), &config).unwrap();
492 let secant_result = secant(f, 0.05, 0.07, &config).unwrap();
493
494 assert_relative_eq!(newton_result.root, brent_result.root, epsilon = 1e-8);
496 assert_relative_eq!(newton_result.root, hybrid_result.root, epsilon = 1e-8);
497 assert_relative_eq!(newton_result.root, secant_result.root, epsilon = 1e-8);
498 }
499
500 #[test]
501 fn test_z_spread_like_calculation() {
502 let zero_rate = 0.03;
505 let target_price = 97.0;
506 let coupon = 5.0;
507 let face = 100.0;
508 let years = 5;
509
510 let price_with_spread = |spread: f64| {
511 let mut pv = 0.0;
512 for t in 1..=years {
513 let discount = (-((zero_rate + spread) * t as f64)).exp();
514 pv += coupon * discount;
515 }
516 pv += face * (-((zero_rate + spread) * years as f64)).exp();
517 pv
518 };
519
520 let f = |spread: f64| price_with_spread(spread) - target_price;
521
522 let result = brent(f, -0.05, 0.10, &SolverConfig::default()).unwrap();
524
525 assert!(result.root > 0.0);
526 assert!(f(result.root).abs() < 1e-10);
527 }
528
529 #[test]
530 fn test_solver_convergence_speed() {
531 let f = |x: f64| x * x - 2.0;
533 let df = |x: f64| 2.0 * x;
534 let config = SolverConfig::default();
535
536 let newton_result = newton_raphson(f, df, 1.5, &config).unwrap();
537 let brent_result = brent(f, 1.0, 2.0, &config).unwrap();
538
539 assert!(newton_result.iterations <= brent_result.iterations);
541 }
542
543 #[test]
544 fn test_high_yield_bond() {
545 let coupon = 8.0;
547 let face = 100.0;
548 let target_price = 85.0; let years = 5;
550 let freq = 2;
551
552 let f = |y: f64| bond_price(y, coupon, face, years, freq) - target_price;
553 let df = |y: f64| bond_price_derivative(y, coupon, face, years, freq);
554
555 let result = hybrid(f, df, 0.10, Some((0.0, 0.30)), &SolverConfig::default()).unwrap();
556
557 assert!(result.root > 0.10);
559 assert!(f(result.root).abs() < 1e-10);
560 }
561
562 #[test]
563 fn test_zero_coupon_bond() {
564 let face = 100.0;
568 let target_price = 62.0921; let years = 5;
570
571 let f = |y: f64| face / (1.0 + y).powi(years) - target_price;
572 let df = |y: f64| -(years as f64) * face / (1.0 + y).powi(years + 1);
573
574 let result = newton_raphson(f, df, 0.08, &SolverConfig::default()).unwrap();
575
576 assert_relative_eq!(result.root, 0.10, epsilon = 0.001);
578 }
579}