BusinessMath Quarterly Series
13 min read
Part 35 of 12-Week BusinessMath Series
Gradient descent is simple but slow (zigzags toward optimum). Newton’s method is fast but requires storing/inverting n × n Hessian matrices. For large problems:
Need: Fast convergence like Newton, memory footprint like gradient descent.
Conjugate gradient chooses search directions that are “conjugate” (orthogonal in a special sense), avoiding the zigzagging of gradient descent while using only O(n) memory. Theoretically solves quadratic problems in at most n iterations.
Business Problem: Find optimal discount rate that minimizes pricing error for bond valuation.
import Foundation
import BusinessMath
// Bond pricing: find discount rate that minimizes squared error
let marketPrice = 95.0 // Observed market price
let faceValue = 100.0
let couponRate = 0.05
let yearsToMaturity = 5.0
// Price a bond given a discount rate
func bondPrice(discountRate: Double) -> Double {
let periods = Int(yearsToMaturity)
var price = 0.0
// Present value of coupons
for t in 1...periods {
let coupon = faceValue * couponRate
price += coupon / pow(1 + discountRate, Double(t))
}
// Present value of face value
price += faceValue / pow(1 + discountRate, yearsToMaturity)
return price
}
// Objective: minimize squared pricing error
func pricingError(discountRate: Double) -> Double {
let predicted = bondPrice(discountRate: discountRate)
let error = marketPrice - predicted
return error * error
}
// Conjugate gradient optimizer (note: async API)
let cg = AsyncConjugateGradientOptimizer(
method: .fletcherReeves, // Classic method for quadratic problems
tolerance: 1e-6,
maxIterations: 100
)
Task {
let result = try await cg.optimize(
objective: pricingError,
constraints: [],
initialGuess: 0.05, // Start with 5% discount rate
bounds: (0.001, 0.20) // Rate must be between 0.1% and 20%
)
print("Bond Yield Estimation via Conjugate Gradient")
print("═══════════════════════════════════════════════════════════")
print("Optimization Results:")
print(" Iterations: \(result.iterations)")
print(" Optimal Discount Rate: \(result.optimalValue.percent(2))")
print(" Final Pricing Error: \(result.objectiveValue.number(3))")
print(" Implied Bond Price: \(bondPrice(discountRate: result.optimalValue).currency(2))")
print(" Market Price: \(marketPrice.currency(2))")
}
Output:
Bond Yield Estimation via Conjugate Gradient
═══════════════════════════════════════════════════════════
Optimization Results:
Iterations: 17
Optimal Discount Rate: 6.1932%
Final Pricing Error: 0.000000
Implied Bond Price: 95.00
Market Price: 95.00
Note: The current BusinessMath API supports univariate conjugate gradient optimization. For multivariate problems (like multi-factor regression), consider using L-BFGS or gradient descent optimizers.
Pattern: Use Polak-Ribière method for nonlinear objectives (option pricing).
// Black-Scholes implied volatility calculation
struct OptionData {
let spotPrice: Double = 100.0
let strikePrice: Double = 105.0
let timeToExpiry: Double = 0.25 // 3 months
let riskFreeRate: Double = 0.05
let marketPrice: Double = 3.50
}
let option = OptionData()
// Black-Scholes call option price
func blackScholesCall(volatility: Double) -> Double {
let S = option.spotPrice
let K = option.strikePrice
let T = option.timeToExpiry
let r = option.riskFreeRate
let d1 = (log(S/K) + (r + volatility*volatility/2)*T) / (volatility*sqrt(T))
let d2 = d1 - volatility*sqrt(T)
// Simplified normal CDF approximation
func normalCDF(_ x: Double) -> Double {
return 0.5 * (1 + erf(x / sqrt(2)))
}
return S * normalCDF(d1) - K * exp(-r*T) * normalCDF(d2)
}
// Objective: minimize squared error between model and market price
func impliedVolError(volatility: Double) -> Double {
let modelPrice = blackScholesCall(volatility: volatility)
let error = option.marketPrice - modelPrice
return error * error
}
// Polak-Ribière method (better for nonlinear problems)
let cgNonlinear = AsyncConjugateGradientOptimizer(
method: .polakRibiere,
tolerance: 1e-8,
maxIterations: 50
)
print("Implied Volatility Calculation (Nonlinear CG)")
print("═══════════════════════════════════════════════════════════")
Task {
let result = try await cgNonlinear.optimize(
objective: impliedVolError,
constraints: [],
initialGuess: 0.20, // Start with 20% volatility
bounds: (0.01, 2.0) // Vol must be between 1% and 200%
)
print("Implied Volatility Calculation (Nonlinear CG)")
print("═══════════════════════════════════════════════════════════")
print(" Implied Volatility: \(result.optimalValue.percent(2))")
print(" Model Price: \(blackScholesCall(volatility: result.optimalValue).currency(2))")
print(" Market Price: \(option.marketPrice.currency(2))")
print(" Pricing Error: \(sqrt(result.objectiveValue).currency(2))")
print(" Iterations: \(result.iterations)")
}
Pattern: Monitor optimization progress in real-time using async streams.
// Option pricing with progress tracking
func pricingObjective(param: Double) -> Double {
// Simulate a complex pricing calculation
let x = param - 0.25
return x*x*x*x - 3*x*x + 2*x + 1 // Quartic function with local minima
}
let asyncCG = AsyncConjugateGradientOptimizer(
method: .fletcherReeves,
tolerance: 1e-8,
maxIterations: 100
)
Task {
// Use async stream to monitor progress
let stream = asyncCG.optimizeWithProgressStream(
objective: pricingObjective,
constraints: [],
initialGuess: 2.0,
bounds: (-5.0, 5.0)
)
print("Optimization with Real-Time Progress")
print("═══════════════════════════════════════════════════════════")
var lastObjective = Double.infinity
for try await progress in stream {
// Print every 10th iteration
if progress.iteration % 10 == 0 {
let improvement = lastObjective - progress.metrics.objectiveValue
print(" Iter \(progress.iteration): obj=\(progress.metrics.objectiveValue.formatted(.number.precision(.fractionLength(6)))), β=\(progress.beta.formatted(.number.precision(.fractionLength(4))))")
lastObjective = progress.metrics.objectiveValue
}
// Access final result when available
if let result = progress.result {
print("\nFinal Result:")
print(" Optimal Value: \(result.optimalValue.formatted(.number.precision(.fractionLength(6))))")
print(" Objective: \(result.objectiveValue.formatted(.number.precision(.fractionLength(8))))")
print(" Converged: \(result.converged)")
print(" Total Iterations: \(result.iterations)")
}
}
}
Advanced: For multivariate optimization, consider using L-BFGS which supports full vector spaces.
| Variant | Best For | Convergence | Robustness |
|---|---|---|---|
| Fletcher-Reeves | Quadratic problems | Guaranteed (quadratic) | Stable |
| Polak-Ribière | Nonlinear problems | Often faster | Can cycle |
| Hestenes-Stiefel | General nonlinear | Middle ground | Good |
| Dai-Yuan | Difficult problems | Robust | Best for tough cases |
Problem: 1,000 variables, quadratic objective
| Method | Memory | Iterations | Time |
|---|---|---|---|
| Gradient Descent | O(n) = 8 KB | 8,420 | 42s |
| Conjugate Gradient | O(n) = 8 KB | 127 | 3.2s |
| BFGS | O(n²) = 8 MB | 95 | 12s |
| L-BFGS (m=10) | O(mn) = 80 KB | 112 | 8s |
| Newton | O(n²) = 8 MB | 45 | 18s (Hessian computation) |
Winner for large quadratic problems: Conjugate Gradient (fast + minimal memory)
Company: Bond trading desk calibrating Nelson-Siegel yield curve modelChallenge: Find optimal parameters that minimize pricing errors across treasury bonds
Problem:
Nelson-Siegel Model:
Y(τ) = β₀ + β₁·[(1-exp(-τ/λ))/(τ/λ)] + β₂·[(1-exp(-τ/λ))/(τ/λ) - exp(-τ/λ)]
Where:
Implementation (Production-Ready):
BusinessMath now includes a complete, tested Nelson-Siegel implementation in Valuation/Debt/NelsonSiegel.swift. This uses multivariate L-BFGS optimization (not scalar conjugate gradient) to properly calibrate all three parameters simultaneously:
import BusinessMath
// Create bond market data
let bonds = [
BondMarketData(maturity: 1.0, couponRate: 0.050, faceValue: 100, marketPrice: 98.8),
BondMarketData(maturity: 2.0, couponRate: 0.052, faceValue: 100, marketPrice: 98.0),
BondMarketData(maturity: 5.0, couponRate: 0.058, faceValue: 100, marketPrice: 96.8),
BondMarketData(maturity: 10.0, couponRate: 0.062, faceValue: 100, marketPrice: 95.5),
]
// Calibrate with comprehensive diagnostics
let result = try NelsonSiegelYieldCurve.calibrateWithDiagnostics(
to: bonds,
fixedLambda: 2.5
)
print("Calibrated Parameters:")
print(" β₀ (level): \(result.curve.parameters.beta0.percent(2))")
print(" β₁ (slope): \(result.curve.parameters.beta1.percent(2))")
print(" β₂ (curvature): \(result.curve.parameters.beta2.percent(2))")
print(" λ (decay): \(result.curve.parameters.lambda.number(2))")
print(" Converged: \(result.converged)")
print(" Iterations: \(result.iterations)")
print(" SSE: \(result.sumSquaredErrors.number(2))")
print(" RMSE: $\(result.rootMeanSquaredError.number(3))")
print(" MAE: $\(result.meanAbsoluteError.number(3))")
// Get yields at any maturity
let yield5Y = result.curve.yield(maturity: 5.0)
let yield10Y = result.curve.yield(maturity: 10.0)
// Price bonds using the fitted curve
let bond = BondMarketData(maturity: 7.0, couponRate: 0.06, faceValue: 100, marketPrice: 0)
let theoreticalPrice = result.curve.price(bond: bond)
// Display fitted yield curve
print("\nFitted Yield Curve:")
let maturities = [0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0]
for maturity in maturities {
let yieldValue = result.curve.yield(maturity: maturity)
print(" \(maturity.number(2))Y: \(yieldValue.percent(2))")
}
Example Output (from 4 Treasury bonds):
Calibrated Parameters:
β₀ (level): 7.32%
β₁ (slope): -1.27%
β₂ (curvature): -1.00%
λ (decay): 2.50
Converged: true
Iterations: 25
SSE: 0.00
RMSE: $0.029
MAE: $0.024
Fitted Yield Curve:
0.25Y: 6.07%
0.50Y: 6.08%
1.00Y: 6.12%
2.00Y: 6.21%
3.00Y: 6.30%
5.00Y: 6.47%
7.00Y: 6.62%
10.00Y: 6.78%
20.00Y: 7.03%
30.00Y: 7.13%
✓ All tests passed - model is production-ready
Results:
Key Lesson:
The original blog post attempted to use scalar AsyncConjugateGradientOptimizer with coordinate descent for a multivariate problem. This was the wrong tool! The production implementation uses MultivariateLBFGS which:
Full Working Example: See the playground for both the scalar CG examples (bond yield, implied volatility) and the production Nelson-Siegel implementation using L-BFGS.
import Foundation
import BusinessMath
// Bond pricing: find discount rate that minimizes squared error
let marketPrice = 95.0 // Observed market price
let faceValue = 100.0
let couponRate = 0.05
let yearsToMaturity = 5.0
// Price a bond given a discount rate
func bondPrice(discountRate: Double) -> Double {
let periods = Int(yearsToMaturity)
var price = 0.0
// Present value of coupons
for t in 1...periods {
let coupon = faceValue * couponRate
price += coupon / pow(1 + discountRate, Double(t))
}
// Present value of face value
price += faceValue / pow(1 + discountRate, yearsToMaturity)
return price
}
// Objective: minimize squared pricing error
func pricingError(discountRate: Double) -> Double {
let predicted = bondPrice(discountRate: discountRate)
let error = marketPrice - predicted
return error * error
}
// Conjugate gradient optimizer (note: async API)
let cg = AsyncConjugateGradientOptimizer(
method: .fletcherReeves, // Classic method for quadratic problems
tolerance: 1e-6,
maxIterations: 100
)
Task {
let result = try await cg.optimize(
objective: pricingError,
constraints: [],
initialGuess: 0.05, // Start with 5% discount rate
bounds: (0.001, 0.20) // Rate must be between 0.1% and 20%
)
print("Bond Yield Estimation via Conjugate Gradient")
print("═══════════════════════════════════════════════════════════")
print("Optimization Results:")
print(" Iterations: \(result.iterations)")
print(" Optimal Discount Rate: \(result.optimalValue.percent(2))")
print(" Final Pricing Error: \(result.objectiveValue.number(3))")
print(" Implied Bond Price: \(bondPrice(discountRate: result.optimalValue).currency(2))")
print(" Market Price: \(marketPrice.currency(2))")
}
// MARK: - Nonlinear Optimization
// Black-Scholes implied volatility calculation
struct OptionData {
let spotPrice: Double = 100.0
let strikePrice: Double = 105.0
let timeToExpiry: Double = 0.25 // 3 months
let riskFreeRate: Double = 0.05
let marketPrice: Double = 3.50
}
let option = OptionData()
// Black-Scholes call option price
func blackScholesCall(volatility: Double) -> Double {
let S = option.spotPrice
let K = option.strikePrice
let T = option.timeToExpiry
let r = option.riskFreeRate
let d1 = (log(S/K) + (r + volatility*volatility/2)*T) / (volatility*sqrt(T))
let d2 = d1 - volatility*sqrt(T)
// Simplified normal CDF approximation
func normalCDF(_ x: Double) -> Double {
return 0.5 * (1 + erf(x / sqrt(2)))
}
return S * normalCDF(d1) - K * exp(-r*T) * normalCDF(d2)
}
// Objective: minimize squared error between model and market price
func impliedVolError(volatility: Double) -> Double {
let modelPrice = blackScholesCall(volatility: volatility)
let error = option.marketPrice - modelPrice
return error * error
}
// Polak-Ribière method (better for nonlinear problems)
let cgNonlinear = AsyncConjugateGradientOptimizer(
method: .polakRibiere,
tolerance: 1e-8,
maxIterations: 50
)
Task {
let result = try await cgNonlinear.optimize(
objective: impliedVolError,
constraints: [],
initialGuess: 0.20, // Start with 20% volatility
bounds: (0.01, 2.0) // Vol must be between 1% and 200%
)
print("Implied Volatility Calculation (Nonlinear CG)")
print("═══════════════════════════════════════════════════════════")
print(" Implied Volatility: \(result.optimalValue.percent(2))")
print(" Model Price: \(blackScholesCall(volatility: result.optimalValue).currency(2))")
print(" Market Price: \(option.marketPrice.currency(2))")
print(" Pricing Error: \(sqrt(result.objectiveValue).currency(2))")
print(" Iterations: \(result.iterations)")
}
// MARK: - Progress Monitoring with AsyncSequence
// Option pricing with progress tracking
func pricingObjective(param: Double) -> Double {
// Simulate a complex pricing calculation
let x = param - 0.25
return x*x*x*x - 3*x*x + 2*x + 1 // Quartic function with local minima
}
let asyncCG = AsyncConjugateGradientOptimizer(
method: .fletcherReeves,
tolerance: 1e-8,
maxIterations: 100
)
Task {
// Use async stream to monitor progress
let stream = asyncCG.optimizeWithProgressStream(
objective: pricingObjective,
constraints: [],
initialGuess: 2.0,
bounds: (-5.0, 5.0)
)
print("Optimization with Real-Time Progress")
print("═══════════════════════════════════════════════════════════")
var lastObjective = Double.infinity
for try await progress in stream {
// Print every 10th iteration
if progress.iteration % 10 == 0 {
let improvement = lastObjective - progress.metrics.objectiveValue
print(" Iter \(progress.iteration): obj=\(progress.metrics.objectiveValue.formatted(.number.precision(.fractionLength(6)))), β=\(progress.beta.formatted(.number.precision(.fractionLength(4))))")
lastObjective = progress.metrics.objectiveValue
}
// Access final result when available
if let result = progress.result {
print("\nFinal Result:")
print(" Optimal Value: \(result.optimalValue.formatted(.number.precision(.fractionLength(6))))")
print(" Objective: \(result.objectiveValue.formatted(.number.precision(.fractionLength(8))))")
print(" Converged: \(result.converged)")
print(" Total Iterations: \(result.iterations)")
}
}
}
// MARK: - Production Nelson-Siegel Implementation (Using L-BFGS)
// Create bond market data
let bonds = [
BondMarketData(maturity: 1.0, couponRate: 0.050, faceValue: 100, marketPrice: 98.8),
BondMarketData(maturity: 2.0, couponRate: 0.052, faceValue: 100, marketPrice: 98.0),
BondMarketData(maturity: 5.0, couponRate: 0.058, faceValue: 100, marketPrice: 96.8),
BondMarketData(maturity: 10.0, couponRate: 0.062, faceValue: 100, marketPrice: 95.5),
]
// Calibrate with comprehensive diagnostics
let result = try NelsonSiegelYieldCurve.calibrateWithDiagnostics(
to: bonds,
fixedLambda: 2.5
)
print("Calibrated Parameters:")
print(" β₀ (level): \(result.curve.parameters.beta0.percent(2))")
print(" β₁ (slope): \(result.curve.parameters.beta1.percent(2))")
print(" β₂ (curvature): \(result.curve.parameters.beta2.percent(2))")
print(" λ (decay): \(result.curve.parameters.lambda.number(2))")
print(" Converged: \(result.converged)")
print(" Iterations: \(result.iterations)")
print(" SSE: \(result.sumSquaredErrors.number(2))")
print(" RMSE: $\(result.rootMeanSquaredError.number(3))")
print(" MAE: $\(result.meanAbsoluteError.number(3))")
// Get yields at any maturity
let yield5Y = result.curve.yield(maturity: 5.0)
let yield10Y = result.curve.yield(maturity: 10.0)
// Price bonds using the fitted curve
let bond = BondMarketData(maturity: 7.0, couponRate: 0.06, faceValue: 100, marketPrice: 0)
let theoreticalPrice = result.curve.price(bond: bond)
// Display fitted yield curve
print("\nFitted Yield Curve:")
let maturities = [0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0]
for maturity in maturities {
let yieldValue = result.curve.yield(maturity: maturity)
print(" \(maturity.number(2))Y: \(yieldValue.percent(2))")
}
→ Full API Reference: BusinessMath Docs – Conjugate Gradient Tutorial
Tomorrow: We’ll conclude Week 10 with Simulated Annealing, a global optimization method that doesn’t require gradients and can escape local minima.
Next Week: Week 11 explores Nelder-Mead Simplex, Particle Swarm Optimization, and Case Study #5: Real-Time Portfolio Rebalancing.
Series: [Week 10 of 12] | Topic: [Part 5 - Advanced Methods] | Case Studies: [4/6 Complete]
Topics Covered: Conjugate gradient • Fletcher-Reeves • Polak-Ribière • Preconditioning • Least squares • Quadratic optimization
Playgrounds: [Week 1-10 available] • [Next: Simulated annealing for global optimization]
Tagged with: businessmath, swift, optimization, conjugate-gradient, gradient-descent, fletcher-reeves, polak-ribiere