google / tf-quant-finance

High-performance TensorFlow library for quantitative finance.
Apache License 2.0
4.57k stars 581 forks source link

Hull White Model - calibration not aligning to input prices #91

Closed mchandlernz closed 1 year ago

mchandlernz commented 1 year ago

I'm observing an issue when trying to calibrate the hull white model using calibration_from_swaptions function. Specifically, when I try to recalculate the prices using the calibrated parameters, I do not return to my source prices. I've included a demo of the issue below:

import numpy as np
import tensorflow as tf
import tf_quant_finance as tff

notional = 100.
prices = np.array([28.54, 27.63, 26.82, 25.76, 24.26])
expiries = np.array([10, 10, 10, 10, 15])
zero_rate_fn = lambda x: 0.01 * tf.ones_like(x, dtype=tf.float64)
start_times = np.array([
    [10. , 10.5, 11. , 11. , 11. , 11. , 11. , 11. , 11. , 11. ],
    [10. , 10.5, 11. , 11.5, 12. , 12. , 12. , 12. , 12. , 12. ],
    [10. , 10.5, 11. , 11.5, 12. , 12.5, 13. , 13. , 13. , 13. ],
    [10. , 10.5, 11. , 11.5, 12. , 12.5, 13. , 13.5, 14. , 14.5],
    [15. , 15.5, 16. , 16. , 16. , 16. , 16. , 16. , 16. , 16. ]])

end_times = np.array([
    [10.5, 11. , 11. , 11. , 11. , 11. , 11. , 11. , 11. , 11. ],
    [10.5, 11. , 11.5, 12. , 12. , 12. , 12. , 12. , 12. , 12. ],
    [10.5, 11. , 11.5, 12. , 12.5, 13. , 13. , 13. , 13. , 13. ],
    [10.5, 11. , 11.5, 12. , 12.5, 13. , 13.5, 14. , 14.5, 15. ],
    [15.5, 16. , 16. , 16. , 16. , 16. , 16. , 16. , 16. , 16. ]])

coupons = np.array([
    [0.03057629, 0.03057629, 0.        , 0.        , 0.        ,
    0.        , 0.        , 0.        , 0.        , 0.        ],
    [0.030797  , 0.030797  , 0.030797  , 0.030797  , 0.        ,
     0.        , 0.        , 0.        , 0.        , 0.        ],
    [0.03072953, 0.03072953, 0.03072953, 0.03072953, 0.03072953,
    0.03072953, 0.        , 0.        , 0.        , 0.        ],
    [0.03086061, 0.03086061, 0.03086061, 0.03086061, 0.03086061,
    0.03086061, 0.03086061, 0.03086061, 0.03086061, 0.03086061],
    [0.02808885, 0.02808885, 0.        , 0.        , 0.        ,
    0.        , 0.        , 0.        , 0.        , 0.        ]])

init_mr_guess = [0.05]
init_vol_guess = [0.05]

calibrated_params, converged, _ = tff.models.hull_white.calibration_from_swaptions(
    prices=prices,
    expiries=expiries,
    floating_leg_start_times=start_times,
    floating_leg_end_times=end_times,
    fixed_leg_payment_times=end_times,
    floating_leg_daycount_fractions=end_times - start_times,
    fixed_leg_daycount_fractions=end_times - start_times,
    fixed_leg_coupon=coupons,
    reference_rate_fn=zero_rate_fn,
    mean_reversion=init_mr_guess,
    volatility=init_vol_guess,
    maximum_iterations=50,
    notional=notional,
    use_analytic_pricing=True,
    dtype=tf.float64,
)

print("Calibrated params")
print("Vol:", calibrated_params.volatility.values())
print("Mean reversion:", calibrated_params.mean_reversion.values())
print("Is converged:", converged)
print("")`

recalced_prices = tff.models.hull_white.swaption_price(
    expiries=expiries,
    floating_leg_start_times=start_times,
    floating_leg_end_times=end_times,
    fixed_leg_payment_times=end_times,
    floating_leg_daycount_fractions=end_times-start_times,
    fixed_leg_daycount_fractions=end_times-start_times,
    fixed_leg_coupon=coupons,
    reference_rate_fn=zero_rate_fn,
    notional=notional,
    mean_reversion=calibrated_params.mean_reversion,
    volatility=calibrated_params.volatility,
    dtype=tf.float64,
)

print("Source prices: ", prices)
print("Recalculated prices: ", recalced_prices.numpy())

Output I'm seeing: Source prices: [28.54, 27.63, 26.82, 25.76, 24.26] Recalculated prices: [10.43644126, 20.46670051, 29.87954086, 46.01337643, 12.38118779]

Thanks in advance!

cyrilchim commented 1 year ago

I'll take a look into that!

cyrilchim commented 1 year ago

It seems that volatility_upper_bound and mean_reversion_upper_bound are hit by the algo. I tried adjusting the bounds a bit as well as the starting point

init_mr_guess = np.array([2.3])
init_vol_guess = np.array([4.0])
volatility_upper_bound = np.array(10.0)
mean_reversion_upper_bound = np.array(10.0)

calibrated_params, converged, num_iterations = tff.models.hull_white.calibration_from_swaptions( 
    prices=prices,
    expiries=expiries,
    floating_leg_start_times=start_times,
    floating_leg_end_times=end_times,
    fixed_leg_payment_times=end_times,
    floating_leg_daycount_fractions=end_times - start_times,
    fixed_leg_daycount_fractions=end_times - start_times,
    fixed_leg_coupon=coupons,
    reference_rate_fn=zero_rate_fn,
    mean_reversion=init_mr_guess,
    volatility=init_vol_guess,
    maximum_iterations=50,
    volatility_upper_bound=volatility_upper_bound,
    mean_reversion_upper_bound=mean_reversion_upper_bound,
    notional=notional,
    use_analytic_pricing=True,
    dtype=tf.float64,
)

I got estimated values as

Vol: tf.Tensor([6.89840182], shape=(1,), dtype=float64)
Mean reversion: tf.Tensor([3.26636998], shape=(1,), dtype=float64)

The recalculated prices are now

Source prices:  [28.54 27.63 26.82 25.76 24.26]
Recalculated prices:  [27.25619778 27.63154018 27.09365106 25.98051968 26.00355828]

which is a much better result.

mchandlernz commented 1 year ago

Ah, that makes sense! Thank you for your help :)