working cva computation using quantlib
This commit is contained in:
5
python/cvatesting/.vscode/settings.json
vendored
Normal file
5
python/cvatesting/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:conda",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
909
python/cvatesting/CVA_calculation_I.ipynb
Normal file
909
python/cvatesting/CVA_calculation_I.ipynb
Normal file
File diff suppressed because one or more lines are too long
83
python/cvatesting/driver.py
Normal file
83
python/cvatesting/driver.py
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
import numpy as np
|
||||
import Quantlib as ql
|
||||
|
||||
from hullwhite import (build_calibrated_hw,simulate_hw_state,swap_npv_from_state)
|
||||
from instruments import (make_vanilla_swap,make_swaption_helpers)
|
||||
|
||||
today = ql.Date(15, 1, 2025)
|
||||
ql.Settings.instance().evaluationDate = today
|
||||
calendar = ql.TARGET()
|
||||
day_count = ql.Actual365Fixed()
|
||||
|
||||
|
||||
def expected_exposure(
|
||||
swap,
|
||||
hw,
|
||||
curve_handle,
|
||||
today,
|
||||
time_grid,
|
||||
x_paths
|
||||
):
|
||||
ee = np.zeros(len(time_grid))
|
||||
|
||||
for j, t in enumerate(time_grid):
|
||||
values = []
|
||||
|
||||
for i in range(len(x_paths)):
|
||||
v = swap_npv_from_state(
|
||||
swap, hw, curve_handle, t, x_paths[i, j], today
|
||||
)
|
||||
values.append(max(v, 0.0))
|
||||
|
||||
ee[j] = np.mean(values)
|
||||
|
||||
return ee
|
||||
|
||||
def main():
|
||||
# Time grid
|
||||
time_grid = np.linspace(0, 10, 121)
|
||||
|
||||
flat_rate = 0.02
|
||||
curve = ql.FlatForward(today, flat_rate, day_count)
|
||||
curve_handle = ql.YieldTermStructureHandle(curve)
|
||||
|
||||
#creat and calibrate HW
|
||||
a_init = 0.05
|
||||
sigma_init = 0.01
|
||||
hw = ql.HullWhite(curve_handle, a_init, sigma_init)
|
||||
|
||||
index = ql.Euribor6M(curve_handle)
|
||||
helpers=make_swaption_helpers(curve_handle,index,hw)
|
||||
optimizer = ql.LevenbergMarquardt()
|
||||
end_criteria = ql.EndCriteria(1000, 500, 1e-8, 1e-8, 1e-8)
|
||||
hw.calibrate(helpers, optimizer, end_criteria)
|
||||
|
||||
a, sigma = hw.params()
|
||||
print(f"Calibrated a = {a:.4f}")
|
||||
print(f"Calibrated sigma = {sigma:.4f}")
|
||||
|
||||
|
||||
|
||||
# Simulate model
|
||||
x_paths = simulate_hw_state(
|
||||
hw, curve_handle, time_grid, n_paths=10000
|
||||
)
|
||||
|
||||
# Swap
|
||||
swap = make_receiver_swap(
|
||||
today, curve_handle, maturity_years=10, fixed_rate=0.025
|
||||
)
|
||||
|
||||
# Exposure
|
||||
ee = expected_exposure(
|
||||
swap, hw, curve_handle, today, time_grid, x_paths
|
||||
)
|
||||
|
||||
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ =='__main__':
|
||||
main()
|
||||
66
python/cvatesting/hullwhite.py
Normal file
66
python/cvatesting/hullwhite.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import QuantLib as ql
|
||||
|
||||
|
||||
def build_calibrated_hw(
|
||||
curve_handle,
|
||||
swaption_helpers,
|
||||
a_init=0.05,
|
||||
sigma_init=0.01
|
||||
):
|
||||
hw = ql.HullWhite(curve_handle, a_init, sigma_init)
|
||||
|
||||
optimizer = ql.LevenbergMarquardt()
|
||||
end_criteria = ql.EndCriteria(1000, 500, 1e-8, 1e-8, 1e-8)
|
||||
|
||||
hw.calibrate(swaption_helpers, optimizer, end_criteria)
|
||||
|
||||
return hw
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def simulate_hw_state(
|
||||
hw: ql.HullWhite,
|
||||
curve_handle,
|
||||
time_grid,
|
||||
n_paths,
|
||||
seed=42
|
||||
):
|
||||
a, sigma = hw.params()
|
||||
|
||||
dt = np.diff(time_grid)
|
||||
n_steps = len(time_grid)
|
||||
|
||||
np.random.seed(seed)
|
||||
x = np.zeros((n_paths, n_steps))
|
||||
|
||||
for i in range(1, n_steps):
|
||||
decay = np.exp(-a * dt[i-1])
|
||||
vol = sigma * np.sqrt((1 - np.exp(-2 * a * dt[i-1])) / (2 * a))
|
||||
z = np.random.normal(size=n_paths)
|
||||
|
||||
x[:, i] = x[:, i-1] * decay + vol * z
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def swap_npv_from_state(
|
||||
swap,
|
||||
hw,
|
||||
curve_handle,
|
||||
t,
|
||||
x_t,
|
||||
today
|
||||
):
|
||||
ql.Settings.instance().evaluationDate = today + int(t * 365)
|
||||
|
||||
hw.setState(x_t)
|
||||
|
||||
engine = ql.DiscountingSwapEngine(
|
||||
curve_handle,
|
||||
False,
|
||||
hw
|
||||
)
|
||||
|
||||
swap.setPricingEngine(engine)
|
||||
return swap.NPV()
|
||||
165
python/cvatesting/instruments.py
Normal file
165
python/cvatesting/instruments.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import QuantLib as ql
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Global defaults (override via parameters if needed)
|
||||
# ============================================================
|
||||
|
||||
CALENDAR = ql.TARGET()
|
||||
BUSINESS_CONVENTION = ql.ModifiedFollowing
|
||||
DATE_GEN = ql.DateGeneration.Forward
|
||||
|
||||
FIXED_DAYCOUNT = ql.Thirty360(ql.Thirty360.European)
|
||||
FLOAT_DAYCOUNT = ql.Actual360()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Yield curve factory
|
||||
# ============================================================
|
||||
|
||||
def make_flat_curve(
|
||||
evaluation_date: ql.Date,
|
||||
rate: float,
|
||||
day_count: ql.DayCounter = ql.Actual365Fixed()
|
||||
) -> ql.YieldTermStructureHandle:
|
||||
"""
|
||||
Flat yield curve factory.
|
||||
"""
|
||||
ql.Settings.instance().evaluationDate = evaluation_date
|
||||
curve = ql.FlatForward(evaluation_date, rate, day_count)
|
||||
return ql.YieldTermStructureHandle(curve)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Index factory
|
||||
# ============================================================
|
||||
|
||||
def make_euribor_6m(
|
||||
curve_handle: ql.YieldTermStructureHandle
|
||||
) -> ql.IborIndex:
|
||||
"""
|
||||
Euribor 6M index factory.
|
||||
"""
|
||||
return ql.Euribor6M(curve_handle)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Schedule factory
|
||||
# ============================================================
|
||||
|
||||
def make_schedule(
|
||||
start: ql.Date,
|
||||
maturity: ql.Date,
|
||||
tenor: ql.Period,
|
||||
calendar: ql.Calendar = CALENDAR
|
||||
) -> ql.Schedule:
|
||||
"""
|
||||
Generic schedule factory.
|
||||
"""
|
||||
return ql.Schedule(
|
||||
start,
|
||||
maturity,
|
||||
tenor,
|
||||
calendar,
|
||||
BUSINESS_CONVENTION,
|
||||
BUSINESS_CONVENTION,
|
||||
DATE_GEN,
|
||||
False
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Swap factory
|
||||
# ============================================================
|
||||
|
||||
def make_vanilla_swap(
|
||||
evaluation_date: ql.Date,
|
||||
curve_handle: ql.YieldTermStructureHandle,
|
||||
notional: float,
|
||||
fixed_rate: float,
|
||||
maturity_years: int,
|
||||
pay_fixed: bool = False,
|
||||
fixed_leg_freq: ql.Period = ql.Annual,
|
||||
float_leg_freq: ql.Period = ql.Semiannual
|
||||
) -> ql.VanillaSwap:
|
||||
"""
|
||||
Vanilla fixed-for-float IRS factory.
|
||||
|
||||
pay_fixed = True -> payer swap
|
||||
pay_fixed = False -> receiver swap
|
||||
"""
|
||||
|
||||
ql.Settings.instance().evaluationDate = evaluation_date
|
||||
|
||||
index = make_euribor_6m(curve_handle)
|
||||
|
||||
start = CALENDAR.advance(evaluation_date, 2, ql.Days)
|
||||
maturity = CALENDAR.advance(start, maturity_years, ql.Years)
|
||||
|
||||
fixed_schedule = make_schedule(
|
||||
start, maturity, ql.Period(fixed_leg_freq)
|
||||
)
|
||||
float_schedule = make_schedule(
|
||||
start, maturity, ql.Period(float_leg_freq)
|
||||
)
|
||||
|
||||
swap_type = (
|
||||
ql.VanillaSwap.Payer if pay_fixed else ql.VanillaSwap.Receiver
|
||||
)
|
||||
|
||||
swap = ql.VanillaSwap(
|
||||
swap_type,
|
||||
notional,
|
||||
fixed_schedule,
|
||||
fixed_rate,
|
||||
FIXED_DAYCOUNT,
|
||||
float_schedule,
|
||||
index,
|
||||
0.0,
|
||||
FLOAT_DAYCOUNT
|
||||
)
|
||||
|
||||
swap.setPricingEngine(
|
||||
ql.DiscountingSwapEngine(curve_handle)
|
||||
)
|
||||
|
||||
return swap
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Swaption helper factory (for HW calibration)
|
||||
# ============================================================
|
||||
|
||||
def make_swaption_helpers(
|
||||
swaption_data: list,
|
||||
curve_handle: ql.YieldTermStructureHandle,
|
||||
index: ql.IborIndex,
|
||||
model: ql.HullWhite
|
||||
) -> list:
|
||||
"""
|
||||
Create ATM swaption helpers.
|
||||
|
||||
swaption_data = [(expiry, tenor, vol), ...]
|
||||
"""
|
||||
|
||||
helpers = []
|
||||
|
||||
for expiry, tenor, vol in swaption_data:
|
||||
helper = ql.SwaptionHelper(
|
||||
expiry,
|
||||
tenor,
|
||||
ql.QuoteHandle(ql.SimpleQuote(vol)),
|
||||
index,
|
||||
index.tenor(),
|
||||
index.dayCounter(),
|
||||
index.dayCounter(),
|
||||
curve_handle
|
||||
)
|
||||
|
||||
helper.setPricingEngine(
|
||||
ql.JamshidianSwaptionEngine(model)
|
||||
)
|
||||
|
||||
helpers.append(helper)
|
||||
|
||||
return helpers
|
||||
109
python/cvatesting/main.py
Normal file
109
python/cvatesting/main.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import QuantLib as ql
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# --- UNIT 1: Instrument Setup ---
|
||||
def create_30y_swap(today, rate_quote):
|
||||
ql.Settings.instance().evaluationDate = today
|
||||
|
||||
# We use a Relinkable handle to swap curves per path/time-step
|
||||
yield_handle = ql.RelinkableYieldTermStructureHandle()
|
||||
yield_handle.linkTo(ql.FlatForward(today, rate_quote, ql.Actual365Fixed()))
|
||||
|
||||
calendar = ql.TARGET()
|
||||
settle_date = calendar.advance(today, 2, ql.Days)
|
||||
maturity_date = calendar.advance(settle_date, 30, ql.Years)
|
||||
|
||||
index = ql.Euribor6M(yield_handle)
|
||||
|
||||
fixed_schedule = ql.Schedule(settle_date, maturity_date, ql.Period(ql.Annual),
|
||||
calendar, ql.ModifiedFollowing, ql.ModifiedFollowing,
|
||||
ql.DateGeneration.Forward, False)
|
||||
float_schedule = ql.Schedule(settle_date, maturity_date, ql.Period(ql.Semiannual),
|
||||
calendar, ql.ModifiedFollowing, ql.ModifiedFollowing,
|
||||
ql.DateGeneration.Forward, False)
|
||||
|
||||
swap = ql.VanillaSwap(ql.VanillaSwap.Payer, 1e6, fixed_schedule, rate_quote,
|
||||
ql.Thirty360(ql.Thirty360.BondBasis), float_schedule,
|
||||
index, 0.0, ql.Actual360())
|
||||
|
||||
# Pre-calculate all fixing dates needed for the life of the swap
|
||||
fixing_dates = [index.fixingDate(d) for d in float_schedule]
|
||||
|
||||
return swap, yield_handle, index, fixing_dates
|
||||
|
||||
# --- UNIT 2: The Simulation Loop ---
|
||||
def run_simulation(n_paths=50):
|
||||
today = ql.Date(27, 1, 2026)
|
||||
swap, yield_handle, index, fixing_dates = create_30y_swap(today, 0.03)
|
||||
|
||||
# HW Parameters: a=mean reversion, sigma=vol
|
||||
model = ql.HullWhite(yield_handle, 0.01, 0.01)
|
||||
process = ql.HullWhiteProcess(yield_handle, 0.01, 0.01)
|
||||
|
||||
times = np.arange(0, 31, 1.0) # Annual buckets
|
||||
grid = ql.TimeGrid(times)
|
||||
|
||||
rng = ql.GaussianRandomSequenceGenerator(ql.UniformRandomSequenceGenerator(
|
||||
len(grid)-1, ql.UniformRandomGenerator()))
|
||||
seq = ql.GaussianMultiPathGenerator(process, grid, rng, False)
|
||||
|
||||
npv_matrix = np.zeros((n_paths, len(times)))
|
||||
|
||||
for i in range(n_paths):
|
||||
path = seq.next().value()[0]
|
||||
# 1. Clear previous path's fixings to avoid data pollution
|
||||
ql.IndexManager.instance().clearHistories()
|
||||
|
||||
for j, t in enumerate(times):
|
||||
if t >= 30: continue
|
||||
|
||||
eval_date = ql.TARGET().advance(today, int(t), ql.Years)
|
||||
ql.Settings.instance().evaluationDate = eval_date
|
||||
|
||||
# 2. Update the curve with simulated short rate rt
|
||||
rt = path[j]
|
||||
# Use pillars up to 35Y to avoid extrapolation crashes
|
||||
tenors = [0, 1, 2, 5, 10, 20, 30, 35]
|
||||
dates = [ql.TARGET().advance(eval_date, y, ql.Years) for y in tenors]
|
||||
discounts = [model.discountBond(t, t + y, rt) for y in tenors]
|
||||
|
||||
sim_curve = ql.DiscountCurve(dates, discounts, ql.Actual365Fixed())
|
||||
sim_curve.enableExtrapolation()
|
||||
yield_handle.linkTo(sim_curve)
|
||||
|
||||
# 3. MANUAL FIXING INJECTION
|
||||
# For every fixing date that has passed or is 'today'
|
||||
for fd in fixing_dates:
|
||||
if fd <= eval_date:
|
||||
# Direct manual calculation to avoid index.fixing() error
|
||||
# We calculate the forward rate for the 6M period starting at fd
|
||||
start_date = fd
|
||||
end_date = ql.TARGET().advance(start_date, 6, ql.Months)
|
||||
|
||||
# Manual Euribor rate formula: (P1/P2 - 1) / dt
|
||||
p1 = sim_curve.discount(start_date)
|
||||
p2 = sim_curve.discount(end_date)
|
||||
dt = ql.Actual360().yearFraction(start_date, end_date)
|
||||
fwd_rate = (p1 / p2 - 1.0) / dt
|
||||
|
||||
index.addFixing(fd, fwd_rate, True) # Force overwrite
|
||||
|
||||
# 4. Valuation
|
||||
swap.setPricingEngine(ql.DiscountingSwapEngine(yield_handle))
|
||||
npv_matrix[i, j] = max(swap.NPV(), 0)
|
||||
|
||||
ql.Settings.instance().evaluationDate = today
|
||||
|
||||
return times, npv_matrix
|
||||
|
||||
# --- UNIT 3: Visualization ---
|
||||
times, npv_matrix = run_simulation(n_paths=100)
|
||||
ee = np.mean(npv_matrix, axis=0)
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
plt.plot(times, ee, lw=2, label="Expected Exposure (EE)")
|
||||
plt.fill_between(times, ee, alpha=0.3)
|
||||
plt.title("30Y Swap EE Profile - Hull White")
|
||||
plt.xlabel("Years"); plt.ylabel("Exposure"); plt.grid(True); plt.legend()
|
||||
plt.show()
|
||||
Reference in New Issue
Block a user