From b862dadd7fdc3f4dfaa90b670247e416c4933619 Mon Sep 17 00:00:00 2001 From: Abderrazak DERDOURI Date: Mon, 2 Jun 2025 19:19:22 +0200 Subject: [PATCH] =?UTF-8?q?Cr=C3=A9=C3=A9=20=C3=A0=20l'aide=20de=20Colab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ql_notebooks/gjrgarchmodel.ipynb | 312 +++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 ql_notebooks/gjrgarchmodel.ipynb diff --git a/ql_notebooks/gjrgarchmodel.ipynb b/ql_notebooks/gjrgarchmodel.ipynb new file mode 100644 index 0000000..99a4a3b --- /dev/null +++ b/ql_notebooks/gjrgarchmodel.ipynb @@ -0,0 +1,312 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "code", + "source": [ + "!pip install QuantLib-Python" + ], + "metadata": { + "id": "FF0CFP5vvEOB", + "outputId": "d7a26383-1ab5-4c70-ee82-e76a2e762d4c", + "colab": { + "base_uri": "https://localhost:8080/" + } + }, + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Collecting QuantLib-Python\n", + " Downloading QuantLib_Python-1.18-py2.py3-none-any.whl.metadata (1.0 kB)\n", + "Collecting QuantLib (from QuantLib-Python)\n", + " Downloading quantlib-1.38-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.1 kB)\n", + "Downloading QuantLib_Python-1.18-py2.py3-none-any.whl (1.4 kB)\n", + "Downloading quantlib-1.38-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20.0 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m20.0/20.0 MB\u001b[0m \u001b[31m25.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hInstalling collected packages: QuantLib, QuantLib-Python\n", + "Successfully installed QuantLib-1.38 QuantLib-Python-1.18\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "import QuantLib as ql\n", + "import unittest\n", + "import math\n", + "\n", + "# Helper to create a flat yield term structure\n", + "def flat_rate(today, forward_rate, day_counter):\n", + " return ql.FlatForward(today, ql.QuoteHandle(ql.SimpleQuote(forward_rate)), day_counter)\n", + "\n", + "class GJRGARCHModelTests(unittest.TestCase):\n", + "\n", + " @unittest.skipIf(False, \"Test marked as Slow in C++\") # To run, set to False\n", + " def test_engines(self):\n", + " print(\"Testing Monte Carlo GJR-GARCH engine against analytic GJR-GARCH engine...\")\n", + "\n", + " day_counter = ql.ActualActual(ql.ActualActual.ISDA)\n", + " today = ql.Date.todaysDate()\n", + " # Settings.instance().evaluationDate = today # Usually needed for options\n", + " # However, the pricing engines and process take term structures with explicit reference dates.\n", + "\n", + " risk_free_ts = ql.YieldTermStructureHandle(flat_rate(today, 0.05, day_counter))\n", + " dividend_ts = ql.YieldTermStructureHandle(flat_rate(today, 0.0, day_counter))\n", + "\n", + " s0_val = 50.0\n", + " omega = 2.0e-6\n", + " alpha = 0.024\n", + " beta = 0.93\n", + " gamma = 0.059\n", + " days_per_year = 365.0 # number of trading days per year\n", + "\n", + " maturities_days = [90, 180]\n", + " strikes = [35.0, 40.0, 45.0, 50.0, 55.0, 60.0]\n", + " lambdas_param = [0.0, 0.1, 0.2] # Renamed from Lambda due to Python keyword\n", + "\n", + " # Expected analytic values [lambda_idx][maturity_idx][strike_idx]\n", + " analytic_expected = {\n", + " 0: { # Lambda = 0.0\n", + " 0: [15.4315, 10.5552, 5.9625, 2.3282, 0.5408, 0.0835], # Mat 90\n", + " 1: [15.8969, 11.2173, 6.9112, 3.4788, 1.3769, 0.4357] # Mat 180\n", + " },\n", + " 1: { # Lambda = 0.1\n", + " 0: [15.4556, 10.6929, 6.2381, 2.6831, 0.7822, 0.1738],\n", + " 1: [16.0587, 11.5338, 7.3170, 3.9074, 1.7279, 0.6568]\n", + " },\n", + " 2: { # Lambda = 0.2\n", + " 0: [15.8000, 11.2734, 7.0376, 3.6767, 1.5871, 0.5934],\n", + " 1: [16.9286, 12.3170, 8.0405, 4.6348, 2.3429, 1.0590]\n", + " }\n", + " }\n", + " # Expected MC values\n", + " mc_expected = {\n", + " 0: {\n", + " 0: [15.4332, 10.5453, 5.9351, 2.3521, 0.5597, 0.0776],\n", + " 1: [15.8910, 11.1772, 6.8827, 3.5096, 1.4196, 0.4502]\n", + " },\n", + " 1: {\n", + " 0: [15.4580, 10.6433, 6.2019, 2.7513, 0.8374, 0.1706],\n", + " 1: [15.9884, 11.4139, 7.3103, 4.0497, 1.8862, 0.7322]\n", + " },\n", + " 2: {\n", + " 0: [15.6619, 11.1263, 7.0968, 3.9152, 1.8133, 0.7010],\n", + " 1: [16.5195, 12.3181, 8.6085, 5.5700, 3.3103, 1.8053]\n", + " }\n", + " }\n", + "\n", + " norm_dist = ql.CumulativeNormalDistribution()\n", + "\n", + " for k_idx, lambda_val in enumerate(lambdas_param):\n", + " m1 = (beta + (alpha + gamma * norm_dist(lambda_val)) * (1.0 + lambda_val * lambda_val) +\n", + " gamma * lambda_val * math.exp(-lambda_val * lambda_val / 2.0) / math.sqrt(2.0 * math.pi))\n", + " v0 = omega / (1.0 - m1)\n", + "\n", + " s0_quote = ql.QuoteHandle(ql.SimpleQuote(s0_val))\n", + "\n", + " process = ql.GJRGARCHProcess(risk_free_ts, dividend_ts, s0_quote, v0,\n", + " omega, alpha, beta, gamma, lambda_val, days_per_year)\n", + "\n", + " # Monte Carlo Engine\n", + " # C++: MakeMCEuropeanGJRGARCHEngine(process).withStepsPerYear(20)...\n", + " # Python: ql.McEuropeanGJRGARCHEngine(process, \"pseudorandom\", stepsPerYear=20, ...)\n", + " mc_engine = ql.McEuropeanGJRGARCHEngine(process, \"pseudorandom\",\n", + " stepsPerYear=20, # C++ stepsPerYear\n", + " requiredTolerance=0.02, # C++ absoluteTolerance\n", + " seed=1234)\n", + "\n", + " # Analytic Engine\n", + " # In C++, GJRGARCHModel is created from the process\n", + " gjr_model = ql.GJRGARCHModel(process)\n", + " analytic_engine = ql.AnalyticGJRGARCHEngine(gjr_model)\n", + "\n", + " for i_idx, mat_days in enumerate(maturities_days):\n", + " for j_idx, strike_val in enumerate(strikes):\n", + " payoff = ql.PlainVanillaPayoff(ql.Option.Call, strike_val)\n", + " exercise_date = today + ql.Period(mat_days, ql.Days)\n", + " exercise = ql.EuropeanExercise(exercise_date)\n", + "\n", + " option = ql.VanillaOption(payoff, exercise)\n", + "\n", + " # Test Monte Carlo Engine\n", + " option.setPricingEngine(mc_engine)\n", + " mc_calculated = option.NPV()\n", + "\n", + " # Test Analytic Engine\n", + " option.setPricingEngine(analytic_engine)\n", + " analytic_calculated = option.NPV()\n", + "\n", + " tolerance = 7.5e-2\n", + "\n", + " expected_analytic_val = analytic_expected[k_idx][i_idx][j_idx]\n", + " expected_mc_val = mc_expected[k_idx][i_idx][j_idx]\n", + "\n", + " self.assertAlmostEqual(analytic_calculated, expected_analytic_val, delta=2.0 * tolerance,\n", + " msg=(f\"Analytic Engine mismatch for Lambda={lambda_val}, Mat={mat_days}d, K={strike_val}\\n\"\n", + " f\" Correct value: {expected_analytic_val}\\n\"\n", + " f\" Analytic Approx.: {analytic_calculated}\"))\n", + "\n", + " self.assertAlmostEqual(mc_calculated, expected_mc_val, delta=2.0 * tolerance,\n", + " msg=(f\"MC Engine mismatch for Lambda={lambda_val}, Mat={mat_days}d, K={strike_val}\\n\"\n", + " f\" Correct value: {expected_mc_val}\\n\"\n", + " f\" Monte Carlo: {mc_calculated}\"))\n", + "\n", + " # @unittest.skipIf(True, \"Test marked as Fast in C++, but can be slow to run frequently\")\n", + " def test_dax_calibration(self):\n", + " print(\"Testing GJR-GARCH model calibration using DAX volatility data...\")\n", + "\n", + " settlement_date = ql.Date(5, ql.July, 2002)\n", + " ql.Settings.instance().evaluationDate = settlement_date\n", + "\n", + " day_counter = ql.Actual365Fixed()\n", + " calendar = ql.TARGET()\n", + "\n", + " # Term structure data from C++\n", + " t_days = [13, 41, 75, 165, 256, 345, 524, 703]\n", + " r_rates = [0.0357, 0.0349, 0.0341, 0.0355, 0.0359, 0.0368, 0.0386, 0.0401]\n", + "\n", + " dates = [settlement_date]\n", + " rates = [0.0357] # First rate matching C++\n", + " for i in range(len(t_days)):\n", + " dates.append(settlement_date + ql.Period(t_days[i], ql.Days))\n", + " rates.append(r_rates[i])\n", + "\n", + " risk_free_ts = ql.YieldTermStructureHandle(ql.ZeroCurve(dates, rates, day_counter))\n", + " dividend_ts = ql.YieldTermStructureHandle(flat_rate(settlement_date, 0.0, day_counter))\n", + "\n", + " # Volatility surface data\n", + " vols_surface_data = [\n", + " 0.6625,0.4875,0.4204,0.3667,0.3431,0.3267,0.3121,0.3121,\n", + " 0.6007,0.4543,0.3967,0.3511,0.3279,0.3154,0.2984,0.2921,\n", + " 0.5084,0.4221,0.3718,0.3327,0.3155,0.3027,0.2919,0.2889,\n", + " 0.4541,0.3869,0.3492,0.3149,0.2963,0.2926,0.2819,0.2800,\n", + " 0.4060,0.3607,0.3330,0.2999,0.2887,0.2811,0.2751,0.2775,\n", + " 0.3726,0.3396,0.3108,0.2781,0.2788,0.2722,0.2661,0.2686,\n", + " 0.3550,0.3277,0.3012,0.2781,0.2781,0.2661,0.2661,0.2681,\n", + " 0.3428,0.3209,0.2958,0.2740,0.2688,0.2627,0.2580,0.2620,\n", + " 0.3302,0.3062,0.2799,0.2631,0.2573,0.2533,0.2504,0.2544,\n", + " 0.3343,0.2959,0.2705,0.2540,0.2504,0.2464,0.2448,0.2462,\n", + " 0.3460,0.2845,0.2624,0.2463,0.2425,0.2385,0.2373,0.2422,\n", + " 0.3857,0.2860,0.2578,0.2399,0.2357,0.2327,0.2312,0.2351,\n", + " 0.3976,0.2860,0.2607,0.2356,0.2297,0.2268,0.2241,0.2320\n", + " ]\n", + " # C++ used a 2D array implicitly via v[s*8+m].\n", + " # In Python, we can reshape or access carefully.\n", + " # The loops are for s=3..9 (7 strike levels) and m=0..2 (3 maturity levels).\n", + " # Total 7*3 = 21 options.\n", + "\n", + " s0_quote_val = 4468.17\n", + " s0_handle = ql.QuoteHandle(ql.SimpleQuote(s0_quote_val))\n", + "\n", + " strikes_calib = [3400, 3600, 3800, 4000, 4200, 4400,\n", + " 4500, 4600, 4800, 5000, 5200, 5400, 5600]\n", + "\n", + " # Initial GJR-GARCH parameters from C++\n", + " omega_init = 2.0e-6\n", + " alpha_init = 0.024\n", + " beta_init = 0.93\n", + " gamma_init = 0.059\n", + " lambda_init = 0.1 # Python keyword, so _init\n", + " days_per_year_calib = 365.0\n", + "\n", + " norm_dist = ql.CumulativeNormalDistribution()\n", + " m1_init = (beta_init + (alpha_init + gamma_init * norm_dist(lambda_init)) *\n", + " (1.0 + lambda_init * lambda_init) +\n", + " gamma_init * lambda_init * math.exp(-lambda_init * lambda_init / 2.0) /\n", + " math.sqrt(2.0 * math.pi))\n", + " v0_init = omega_init / (1.0 - m1_init)\n", + "\n", + " process = ql.GJRGARCHProcess(risk_free_ts, dividend_ts, s0_handle, v0_init,\n", + " omega_init, alpha_init, beta_init, gamma_init,\n", + " lambda_init, days_per_year_calib)\n", + "\n", + " model = ql.GJRGARCHModel(process)\n", + " engine = ql.AnalyticGJRGARCHEngine(model)\n", + "\n", + " calibration_helpers = []\n", + " # C++ loop: for (Size s = 3; s < 10; ++s) { for (Size m = 0; m < 3; ++m) { ... } }\n", + " # s index for strikes_calib and for row of vol_surface_data\n", + " # m index for maturities (t_days) and for col of vol_surface_data\n", + "\n", + " maturities_calib_indices = [0, 1, 2] # Corresponds to m=0,1,2 (first 3 maturities in t_days)\n", + " strike_indices_calib = range(3, 10) # Corresponds to s=3..9\n", + "\n", + " for s_idx_loopval in strike_indices_calib: # This s_idx_loopval is 's' in C++\n", + " current_strike = strikes_calib[s_idx_loopval]\n", + " for m_idx_loopval in maturities_calib_indices: # This m_idx_loopval is 'm' in C++\n", + " # C++: v[s*8+m]\n", + " # Python: vols_surface_data is 1D. Index is s_idx_loopval * 8 + m_idx_loopval\n", + " vol_val = vols_surface_data[s_idx_loopval * 8 + m_idx_loopval]\n", + " vol_handle = ql.QuoteHandle(ql.SimpleQuote(vol_val))\n", + "\n", + " # Maturity from t_days, C++ rounds to weeks.\n", + " # (t[m]+3)/7. t_days[m_idx_loopval]\n", + " maturity_in_days = t_days[m_idx_loopval]\n", + " maturity_in_weeks = int((maturity_in_days + 3) / 7.0)\n", + " maturity_period = ql.Period(maturity_in_weeks, ql.Weeks)\n", + "\n", + " # C++ uses HestonModelHelper as a BlackCalibrationHelper\n", + " helper = ql.HestonModelHelper(maturity_period, calendar,\n", + " s0_quote_val, current_strike, vol_handle,\n", + " risk_free_ts, dividend_ts,\n", + " ql.BlackCalibrationHelper.ImpliedVolError)\n", + " helper.setPricingEngine(engine)\n", + " calibration_helpers.append(helper)\n", + "\n", + " opt_method = ql.Simplex(0.05)\n", + " end_criteria = ql.EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8)\n", + "\n", + " model.calibrate(calibration_helpers, opt_method, end_criteria)\n", + "\n", + " sse = 0.0\n", + " for helper_item in calibration_helpers:\n", + " diff = helper_item.calibrationError() * 100.0 # Error is in vol points\n", + " sse += diff * diff\n", + "\n", + " max_expected_sse = 15.0 # From C++ test\n", + "\n", + " print(f\"DAX Calibration SSE: {sse}\")\n", + " self.assertTrue(sse <= max_expected_sse,\n", + " (f\"Failed to reproduce calibration error\\n\"\n", + " f\" calculated SSE: {sse}\\n\"\n", + " f\" expected SSE: < {max_expected_sse}\"))\n", + "\n", + "\n", + "if __name__ == '__main__':\n", + " print(\"Running Python QuantLib GJRGARCHModel tests...\")\n", + " unittest.main(argv=['first-arg-is-ignored'], exit=False)" + ], + "metadata": { + "id": "eDZrNJgTprIO" + }, + "execution_count": null, + "outputs": [] + } + ], + "metadata": { + "colab": { + "toc_visible": true, + "provenance": [], + "include_colab_link": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file