Calibrating a RatesLib Curve with Skewed Swap Instruments
Summary
The document explains why adding intermediate-maturity swap instruments to a regularized interest-rate curve calibration can produce a poor fit or an apparent solver failure. Its central correction is to distinguish a curve’s period rate from an instrument’s par swap rate: the proposed skewed quotes should be based on each swap’s rate under the already calibrated solver, then adjusted by the desired spread. Using curve rates directly gives the wrong targets.
The answer also reports that the expanded calibration may hit its iteration limit despite a small objective value, and recommends loosening function and convergence tolerances. This is presented as a floating-point precision issue with many instruments, not as proof that every such failure is benign. The example concerns a particular RatesLib setup with mixed interpolation, turn instruments, and weighted fitting; it does not establish universal tolerances or replace checking residuals and curve behavior before accepting a calibration.
Key ideas
- A curve period rate is not generally the same as an IRS par rate.
- Set skewed swap targets from the calibrated instrument rates, then apply the intended spread adjustments.
- A solver can reach its iteration cap while its objective value is already small.
- Wider tolerances may help when a large instrument set limits numerical convergence.
Tags
Full text
# Second Layer Instruments in RatesLib
# Second Layer Instruments in RatesLib
I'm attempting to replicate the curve in Chapter 12 of Pricing and Trading Interest Rate Derivatives using the Python RatesLib library based on the code repository notebooks provided here. My question for those that are familiar with the material and the library is as follows...
The first layer mixed interpolation curve, along with the associated turns added, seems to reproduce nicely while achieving regularization.
```
mixed_curve = Curve(
nodes={
dt(2022, 1, 1): 1.00, # today's DF
dt(2022, 2, 3): 1.00, # defined MPC dates..
dt(2022, 3, 17): 1.00,
dt(2022, 3, 31): 1.00, #turn 1
dt(2022, 4, 1): 1.00, #turn 1
dt(2022, 5, 5): 1.00,
dt(2022, 6, 16): 1.00,
dt(2022, 6, 30): 1.00, #turn 2
dt(2022, 7, 1): 1.00, #turn 2
dt(2022, 8, 4): 1.00,
dt(2022, 9, 15): 1.00,
dt(2022, 9, 30): 1.00, #turn 3
dt(2022, 10, 1): 1.00, #turn 3
dt(2022, 11, 3): 1.00,
dt(2022, 12, 15): 1.00,
dt(2022, 12, 31): 1.00, #turn 4
dt(2023, 1, 1): 1.00, #turn 4
dt(2023, 2, 2): 1.00, # provisional MPC dates..
dt(2023, 3, 23): 1.00,
dt(2023, 3, 31): 1.00, #turn 5
dt(2023, 4, 1): 1.00, #turn 5
dt(2023, 5, 11): 1.00,
dt(2023, 6, 22): 1.00,
dt(2023, 6, 30): 1.00, #turn 6
dt(2023, 7, 1): 1.00, #turn 6
dt(2023, 8, 3): 1.00,
dt(2023, 9, 21): 1.00,
dt(2023, 9, 30): 1.00, #turn 7
dt(2023, 10, 1): 1.00, #turn 7
dt(2023, 11, 2): 1.00,
dt(2023, 12, 14): 1.00,
dt(2023, 12, 31): 1.00, #turn 8
dt(2024, 1, 1): 1.00, #turn 8
dt(2024, 2, 8): 1.00, # estimated MPC dates..
dt(2024, 3, 21): 1.00,
dt(2024, 5, 16): 1.00,
dt(2024, 6, 20): 1.00,
dt(2024, 8, 8): 1.00,
dt(2024, 9, 19): 1.00,
dt(2024, 11, 7): 1.00,
dt(2024, 12, 12): 1.00,
dt(2025, 3, 19): 1.00, # long term tenors..
dt(2027, 3, 17): 1.00,
dt(2029, 3, 17): 1.00,
dt(2032, 3, 15): 1.00,
dt(2037, 3, 15): 1.00,
dt(2042, 3, 15): 1.00,
dt(2052, 3, 15): 1.00,
dt(2062, 3, 15): 1.00,
dt(2072, 3, 15): 1.00,
},
interpolation="log_linear",
t = [
dt(2025, 3, 19), dt(2025, 3, 19), dt(2025, 3, 19), dt(2025, 3, 19),
dt(2027, 3, 15),
dt(2029, 3, 15),
dt(2032, 3, 15),
dt(2037, 3, 15),
dt(2042, 3, 15),
dt(2052, 3, 15),
dt(2062, 3, 15),
dt(2072, 3, 15), dt(2072, 3, 15), dt(2072, 3, 15), dt(2072, 3, 15),
],
id="mixed_curve",
)
```
```
args_mpc = dict(termination="1d", spec="usd_irs", curves="mixed_curve")
args_turn = dict(termination="1d", frequency="A", curves="mixed_curve")
mpc_1 = IRS(effective=dt(2022, 2, 3), **args_mpc)
mpc_2 = IRS(effective=dt(2022, 3, 17), **args_mpc)
mpc_3 = IRS(effective=dt(2022, 5, 5), **args_mpc)
mpc_4 = IRS(effective=dt(2022, 6, 16), **args_mpc)
mpc_5 = IRS(effective=dt(2022, 8, 4), **args_mpc)
mpc_6 = IRS(effective=dt(2022, 9, 15), **args_mpc)
mpc_7 = IRS(effective=dt(2022, 11, 3), **args_mpc)
mpc_8 = IRS(effective=dt(2022, 12, 15), **args_mpc)
mpc_9 = IRS(effective=dt(2023, 2, 2), **args_mpc)
mpc_10 = IRS(effective=dt(2023, 3, 23), **args_mpc)
mpc_11 = IRS(effective=dt(2023, 5, 11), **args_mpc)
mpc_12 = IRS(effective=dt(2023, 6, 22), **args_mpc)
mpc_13 = IRS(effective=dt(2023, 8, 3), **args_mpc)
mpc_14 = IRS(effective=dt(2023, 9, 21), **args_mpc)
mpc_15 = IRS(effective=dt(2023, 11, 2), **args_mpc)
mpc_16 = IRS(effective=dt(2023, 12, 14), **args_mpc)
mpc_17 = IRS(effective=dt(2024, 2, 8), **args_mpc)
mpc_18 = IRS(effective=dt(2024, 3, 21), **args_mpc)
mpc_19 = IRS(effective=dt(2024, 5, 16), **args_mpc)
mpc_20 = IRS(effective=dt(2024, 6, 20), **args_mpc)
mpc_21 = IRS(effective=dt(2024, 8, 8), **args_mpc)
mpc_22 = IRS(effective=dt(2024, 9, 19), **args_mpc)
mpc_23 = IRS(effective=dt(2024, 11, 7), **args_mpc)
mpc_24 = IRS(effective=dt(2024, 12, 12), **args_mpc)
mpc_25 = IRS(effective=dt(2025, 3, 20), **args_mpc)
turn_1a = IRS(effective=dt(2022, 3, 30), **args_turn)
turn_1b = IRS(effective=dt(2022, 3, 31), **args_turn)
turn_1c = IRS(effective=dt(2022, 4, 1), **args_turn)
turn_2a = IRS(effective=dt(2022, 6, 29), **args_turn)
turn_2b = IRS(effective=dt(2022, 6, 30), **args_turn)
turn_2c = IRS(effective=dt(2022, 7, 1), **args_turn)
turn_3a = IRS(effective=dt(2022, 9, 29), **args_turn)
turn_3b = IRS(effective=dt(2022, 9, 30), **args_turn)
turn_3c = IRS(effective=dt(2022, 10, 1), **args_turn)
turn_4a = IRS(effective=dt(2022, 12, 30), **args_turn)
turn_4b = IRS(effective=dt(2022, 12, 31), **args_turn)
turn_4c = IRS(effective=dt(2023, 1, 1), **args_turn)
turn_5a = IRS(effective=dt(2023, 3, 30), **args_turn)
turn_5b = IRS(effective=dt(2023, 3, 31), **args_turn)
turn_5c = IRS(effective=dt(2023, 4, 1), **args_turn)
turn_6a = IRS(effective=dt(2023, 6, 29), **args_turn)
turn_6b = IRS(effective=dt(2023, 6, 30), **args_turn)
turn_6c = IRS(effective=dt(2023, 7, 1), **args_turn)
turn_7a = IRS(effective=dt(2023, 9, 29), **args_turn)
turn_7b = IRS(effective=dt(2023, 9, 30), **args_turn)
turn_7c = IRS(effective=dt(2023, 10, 1), **args_turn)
turn_8a = IRS(effective=dt(2023, 12, 30), **args_turn)
turn_8b = IRS(effective=dt(2023, 12, 31), **args_turn)
turn_8c = IRS(effective=dt(2024, 1, 1), **args_turn)
```
```
args = dict(spec="usd_irs", curves="mixed_curve")
mixed_instruments = [
IRS(dt(2022, 1, 1), "34d", "A", **args),
IRS(dt(2022, 2, 3), "42d", "A", **args),
IRS(dt(2022, 3, 16), "3M", "Q", **args),
IRS(dt(2022, 6, 15), "3M", "Q", **args),
IRS(dt(2022, 9, 16), "3M", "Q", **args),
IRS(dt(2022, 12, 21), "3M", "Q", **args),
IRS(dt(2023, 3, 15), "3M", "Q", **args),
IRS(dt(2023, 6, 21), "3M", "Q", **args),
IRS(dt(2023, 9, 20), "3M", "Q", **args),
IRS(dt(2023, 12, 20), "3M", "Q", **args),
IRS(dt(2024, 3, 20), "3M", "Q", **args),
IRS(dt(2024, 6, 19), "3M", "Q", **args),
IRS(dt(2024, 9, 18), "3M", "Q", **args),
IRS(dt(2024, 12, 18), "3M", "Q", **args),
IRS(dt(2022, 1, 1), "5Y", "A", **args),
IRS(dt(2022, 1, 1), "7Y", "A", **args),
IRS(dt(2022, 1, 1), "10Y", "A", **args),
IRS(dt(2022, 1, 1), "15Y", "A", **args),
IRS(dt(2022, 1, 1), "20Y", "A", **args),
IRS(dt(2022, 1, 1), "30Y", "A", **args),
IRS(dt(2022, 1, 1), "40Y", "A", **args),
IRS(dt(2022, 1, 1), "50Y", "A", **args),
Spread(turn_1a, turn_1b),
Spread(turn_1b, turn_1c),
Spread(turn_2a, turn_2b),
Spread(turn_2b, turn_2c),
Spread(turn_3a, turn_3b),
Spread(turn_3b, turn_3c),
Spread(turn_4a, turn_4b),
Spread(turn_4b, turn_4c),
Spread(turn_5a, turn_5b),
Spread(turn_5b, turn_5c),
Spread(turn_6a, turn_6b),
Spread(turn_6b, turn_6c),
Spread(turn_7a, turn_7b),
Spread(turn_7b, turn_7c),
Spread(turn_8a, turn_8b),
Spread(turn_8b, turn_8c),
Fly(mpc_1, mpc_2, mpc_3),
Fly(mpc_2, mpc_3, mpc_4),
Fly(mpc_3, mpc_4, mpc_5),
Fly(mpc_4, mpc_5, mpc_6),
Fly(mpc_5, mpc_6, mpc_7),
Fly(mpc_6, mpc_7, mpc_8),
Fly(mpc_7, mpc_8, mpc_9),
Fly(mpc_8, mpc_9, mpc_10),
Fly(mpc_9, mpc_10, mpc_11),
Fly(mpc_10, mpc_11, mpc_12),
Fly(mpc_11, mpc_12, mpc_13),
Fly(mpc_12, mpc_13, mpc_14),
Fly(mpc_13, mpc_14, mpc_15),
Fly(mpc_14, mpc_15, mpc_16),
Fly(mpc_15, mpc_16, mpc_17),
Fly(mpc_16, mpc_17, mpc_18),
Fly(mpc_17, mpc_18, mpc_19),
Fly(mpc_18, mpc_19, mpc_20),
Fly(mpc_19, mpc_20, mpc_21),
Fly(mpc_20, mpc_21, mpc_22),
Fly(mpc_21, mpc_22, mpc_23),
Fly(mpc_22, mpc_23, mpc_24),
Fly(mpc_23, mpc_24, mpc_25),
]
```
```
s = [
0.695,
0.95,
1.40,
1.89,
2.245,
2.53,
2.69,
2.69,
2.62,
2.5,
2.375,
2.27,
2.215,
2.17,
2.195,
2.193,
2.186,
2.181,
2.162,
2.12,
2.10,
2.09,
-3,
3,
-5,
5,
-3,
3,
-5,
5,
-3,
3,
-5,
5,
-3,
3,
-5,
5,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]
```
```
weights = [
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
1e-09,
]
```
```
mixed_solver = Solver(
curves=[mixed_curve],
instruments=mixed_instruments,
s = s,
weights = weights,
)
```
However, when attempting to add the second layer, with associated skewed instruments, nodes and knots, the solver seems to be failing to reach convergence.
```
# Define the second layer instruments
skew_instruments = [
IRS(dt(2022, 1, 1), "4Y", "A", **args),
IRS(dt(2022, 1, 1), "6Y", "A", **args),
IRS(dt(2022, 1, 1), "8Y", "A", **args),
IRS(dt(2022, 1, 1), "9Y", "A", **args),
IRS(dt(2022, 1, 1), "12Y", "A", **args),
IRS(dt(2022, 1, 1), "25Y", "A", **args),
IRS(dt(2022, 1, 1), "35Y", "A", **args),
IRS(dt(2022, 1, 1), "45Y", "A", **args),
]
# Insert second layer instruments
mixed_instruments[22:22] = skew_instruments # after "50Y IRS"
# Define the new rates and weights
s_skew = [
mixed_curve.rate(dt(2022, 1, 1), dt(2026, 1, 1)).real - 0.0015, #4Y
mixed_curve.rate(dt(2022, 1, 1), dt(2028, 1, 1)).real + 0.0015, #6Y
mixed_curve.rate(dt(2022, 1, 1), dt(2030, 1, 1)).real + 0.0005, #8Y
mixed_curve.rate(dt(2022, 1, 1), dt(2031, 1, 1)).real - 0.0005, #9Y
mixed_curve.rate(dt(2022, 1, 1), dt(2034, 1, 1)).real - 0.001, #12Y
mixed_curve.rate(dt(2022, 1, 1), dt(2047, 1, 1)).real, #25Y
mixed_curve.rate(dt(2022, 1, 1), dt(2057, 1, 1)).real - 0.0005, #35Y
mixed_curve.rate(dt(2022, 1, 1), dt(2067, 1, 1)).real, #45Y
]
weights_skew = [
1,
1,
1,
1,
1,
1,
1,
1,
]
# Insert the new rates and weights
s[22:22] = s_skew
weights[22:22] = weights_skew
```
```
skew_curve = Curve(
nodes={
dt(2022, 1, 1): 1.00, # today's DF
dt(2022, 2, 3): 1.00, # defined MPC dates..
dt(2022, 3, 17): 1.00,
dt(2022, 3, 31): 1.00, #turn 1
dt(2022, 4, 1): 1.00, #turn 1
dt(2022, 5, 5): 1.00,
dt(2022, 6, 16): 1.00,
dt(2022, 6, 30): 1.00, #turn 2
dt(2022, 7, 1): 1.00, #turn 2
dt(2022, 8, 4): 1.00,
dt(2022, 9, 15): 1.00,
dt(2022, 9, 30): 1.00, #turn 3
dt(2022, 10, 1): 1.00, #turn 3
dt(2022, 11, 3): 1.00,
dt(2022, 12, 15): 1.00,
dt(2022, 12, 31): 1.00, #turn 4
dt(2023, 1, 1): 1.00, #turn 4
dt(2023, 2, 2): 1.00, # provisional MPC dates..
dt(2023, 3, 23): 1.00,
dt(2023, 3, 31): 1.00, #turn 5
dt(2023, 4, 1): 1.00, #turn 5
dt(2023, 5, 11): 1.00,
dt(2023, 6, 22): 1.00,
dt(2023, 6, 30): 1.00, #turn 6
dt(2023, 7, 1): 1.00, #turn 6
dt(2023, 8, 3): 1.00,
dt(2023, 9, 21): 1.00,
dt(2023, 9, 30): 1.00, #turn 7
dt(2023, 10, 1): 1.00, #turn 7
dt(2023, 11, 2): 1.00,
dt(2023, 12, 14): 1.00,
dt(2023, 12, 31): 1.00, #turn 8
dt(2024, 1, 1): 1.00, #turn 8
dt(2024, 2, 8): 1.00, # estimated MPC dates..
dt(2024, 3, 21): 1.00,
dt(2024, 5, 16): 1.00,
dt(2024, 6, 20): 1.00,
dt(2024, 8, 8): 1.00,
dt(2024, 9, 19): 1.00,
dt(2024, 11, 7): 1.00,
dt(2024, 12, 12): 1.00,
dt(2025, 3, 19): 1.00, # long term tenors..
dt(2026, 3, 17): 1.00, #4Y
dt(2027, 3, 17): 1.00,
dt(2028, 3, 15): 1.00, #6Y
dt(2029, 3, 17): 1.00,
dt(2030, 3, 17): 1.00, #8Y
dt(2031, 3, 17): 1.00, #9Y
dt(2032, 3, 15): 1.00,
dt(2034, 3, 15): 1.00, #12Y
dt(2037, 3, 15): 1.00,
dt(2042, 3, 15): 1.00,
dt(2047, 3, 15): 1.00, #25Y
dt(2052, 3, 15): 1.00,
dt(2057, 3, 15): 1.00, #35Y
dt(2062, 3, 15): 1.00,
dt(2067, 3, 15): 1.00, #45Y
dt(2072, 3, 15): 1.00,
},
interpolation="log_linear",
t=[
dt(2025, 3, 19), dt(2025, 3, 19), dt(2025, 3, 19), dt(2025, 3, 19),
dt(2026, 3, 15), #4Y
dt(2027, 3, 15),
dt(2028, 3, 15), #6Y
dt(2029, 3, 15),
dt(2030, 3, 15), #8Y
dt(2031, 3, 15), #9Y
dt(2032, 3, 15),
dt(2034, 3, 15), #12Y
dt(2037, 3, 15),
dt(2042, 3, 15),
dt(2047, 3, 15), #25Y
dt(2052, 3, 15),
dt(2057, 3, 15), #35Y
dt(2062, 3, 15),
dt(2067, 3, 15), #45Y
dt(2072, 3, 15), dt(2072, 3, 15), dt(2072, 3, 15), dt(2072, 3, 15),
],
id="mixed_curve",
)
```
```
mixed_solver = Solver(
curves = [skew_curve],
instruments = mixed_instruments,
s = s,
weights = weights,
)
```
For context, I'm fairly new to using the library (for personal academic purposes) and would not be surprised if this is a rather mundane code error on my part. Any and all input from those more experienced is appreciated.
## Answer by Attack68 (score 2, accepted)
https://quant.stackexchange.com/a/81566
You have fallen into this trap: thinking a curve.rate is an IRS.rate. See here Curve rates are simple period rates not Swap rates.
Change this:
```
# Define the new rates and weights
s_skew = [
mixed_curve.rate(dt(2022, 1, 1), dt(2026, 1, 1)).real - 0.0015, #4Y
mixed_curve.rate(dt(2022, 1, 1), dt(2028, 1, 1)).real + 0.0015, #6Y
mixed_curve.rate(dt(2022, 1, 1), dt(2030, 1, 1)).real + 0.0005, #8Y
mixed_curve.rate(dt(2022, 1, 1), dt(2031, 1, 1)).real - 0.0005, #9Y
mixed_curve.rate(dt(2022, 1, 1), dt(2034, 1, 1)).real - 0.001, #12Y
mixed_curve.rate(dt(2022, 1, 1), dt(2047, 1, 1)).real, #25Y
mixed_curve.rate(dt(2022, 1, 1), dt(2057, 1, 1)).real - 0.0005, #35Y
mixed_curve.rate(dt(2022, 1, 1), dt(2067, 1, 1)).real, #45Y
]
```
to
```
skews = [-0.0015, 0.0015, 0.0005, -0.0005, -0.001, 0.0, -0.0005, 0.0]
s_skew = [
inst.rate(solver=mixed_solver).real + skew for (inst,skew) in zip(skew_instruments, skews)
]
```
When you attempt to solve this it will still report:
```
FAILURE: `max_iter` breached after 100 iterations (levenberg_marquardt), `f_val`: 0.00015629075531774895, `time`: 4.1201s
```
However, this is actually a success (small f_val), but due to the floating point error with so many instruments it cannot converge to the necessary precision. The solution is to widen the tolerance.
```
skew_solver = Solver(
curves = [skew_curve],
instruments = mixed_instruments,
s = s,
weights = weights,
func_tol=1e-8, # default 1e-12
conv_tol=1e-10, # default 1e-17
)
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.