From d4bd48c0f174201afe04bd26c03c8b789d7ba56f Mon Sep 17 00:00:00 2001 From: Dominic O'Kane Date: Wed, 31 Jul 2024 23:11:22 +0200 Subject: [PATCH] Fixed pandas issue with append and lots of small bugs --- financepy/__init__.py | 2 +- .../products/rates/ibor_benchmarks_report.py | 2 +- golden_tests/TestFinBondPortfolio.py | 2 +- golden_tests/TestFinBondYieldCurve.py | 2 +- golden_tests/TestFinBondZeroCurve.py | 2 +- golden_tests/TestFinIborBenchmarksReport.py | 105 ++++++ golden_tests/TestFinIborCurveParRateShock.py | 89 +++++ golden_tests/TestFinIborCurveRiskEngine.py | 317 ++++++++++++++++++ golden_tests/data/ibor_benchmarks_example.csv | 14 + requirements.txt | 16 +- ...iltBondPrices.txt => gilt_bond_prices.txt} | 0 unit_tests/test_FinBondYieldCurve.py | 2 +- unit_tests/test_FinBondZSpread.py | 4 +- unit_tests/test_FinBondZeroCurve.py | 2 +- unit_tests/test_FinIborBenchmarksReport.py | 2 +- unit_tests/test_FinIborCurveParRateShock.py | 4 + unit_tests/test_FinIborCurveRiskEngine.py | 12 +- 17 files changed, 553 insertions(+), 24 deletions(-) create mode 100644 golden_tests/TestFinIborBenchmarksReport.py create mode 100644 golden_tests/TestFinIborCurveParRateShock.py create mode 100644 golden_tests/TestFinIborCurveRiskEngine.py create mode 100644 golden_tests/data/ibor_benchmarks_example.csv rename unit_tests/data/{giltBondPrices.txt => gilt_bond_prices.txt} (100%) diff --git a/financepy/__init__.py b/financepy/__init__.py index 99f247385..3a1bb228d 100644 --- a/financepy/__init__.py +++ b/financepy/__init__.py @@ -1,7 +1,7 @@ cr = "\n" s = "####################################################################" + cr -s += "# FINANCEPY BETA Version " + str('0.360') + " - This build: 31 Jul 2024 at 19:44 #" + cr +s += "# FINANCEPY BETA Version " + str('0.360') + " - This build: 31 Jul 2024 at 23:01 #" + cr s += "# This software is distributed FREE AND WITHOUT ANY WARRANTY #" + cr s += "# Report bugs as issues at https://github.com/domokane/FinancePy #" + cr s += "####################################################################" diff --git a/financepy/products/rates/ibor_benchmarks_report.py b/financepy/products/rates/ibor_benchmarks_report.py index a3f415c13..2c817c7a1 100644 --- a/financepy/products/rates/ibor_benchmarks_report.py +++ b/financepy/products/rates/ibor_benchmarks_report.py @@ -36,7 +36,7 @@ def benchmarks_report(benchmarks, if df_bmi is None: df_bmi = pd.DataFrame.from_dict(res, orient='index').T else: - df_bmi = df_bmi.append(res, ignore_index=True) + df_bmi = df_bmi._append(res, ignore_index=True) if include_objects: df_bmi['benchmark_objects'] = benchmarks diff --git a/golden_tests/TestFinBondPortfolio.py b/golden_tests/TestFinBondPortfolio.py index 8c874efb3..e8f098e42 100644 --- a/golden_tests/TestFinBondPortfolio.py +++ b/golden_tests/TestFinBondPortfolio.py @@ -22,7 +22,7 @@ def test_BondPortfolio(): import pandas as pd - path = os.path.join(os.path.dirname(__file__), './data/giltBondPrices.txt') + path = os.path.join(os.path.dirname(__file__), './data/gilt_bond_prices.txt') bondDataFrame = pd.read_csv(path, sep='\t') bondDataFrame['mid'] = 0.5*(bondDataFrame['bid'] + bondDataFrame['ask']) diff --git a/golden_tests/TestFinBondYieldCurve.py b/golden_tests/TestFinBondYieldCurve.py index 892bfa897..0472d6271 100644 --- a/golden_tests/TestFinBondYieldCurve.py +++ b/golden_tests/TestFinBondYieldCurve.py @@ -32,7 +32,7 @@ def test_BondYieldCurve(): ########################################################################### import pandas as pd - path = os.path.join(os.path.dirname(__file__), './data/giltBondPrices.txt') + path = os.path.join(os.path.dirname(__file__), './data/gilt_bond_prices.txt') bondDataFrame = pd.read_csv(path, sep='\t') bondDataFrame['mid'] = 0.5*(bondDataFrame['bid'] + bondDataFrame['ask']) diff --git a/golden_tests/TestFinBondZeroCurve.py b/golden_tests/TestFinBondZeroCurve.py index 064e3599a..b0907942d 100644 --- a/golden_tests/TestFinBondZeroCurve.py +++ b/golden_tests/TestFinBondZeroCurve.py @@ -26,7 +26,7 @@ def test_BondZeroCurve(): import pandas as pd - path = os.path.join(os.path.dirname(__file__), './data/giltBondPrices.txt') + path = os.path.join(os.path.dirname(__file__), './data/gilt_bond_prices.txt') bondDataFrame = pd.read_csv(path, sep='\t') bondDataFrame['mid'] = 0.5*(bondDataFrame['bid'] + bondDataFrame['ask']) diff --git a/golden_tests/TestFinIborBenchmarksReport.py b/golden_tests/TestFinIborBenchmarksReport.py new file mode 100644 index 000000000..0eaf909f6 --- /dev/null +++ b/golden_tests/TestFinIborBenchmarksReport.py @@ -0,0 +1,105 @@ +import pandas as pd +from os.path import dirname, join + +import sys +sys.path.append("..") + + +from financepy.utils.date import Date +from financepy.utils.math import ONE_MILLION +from financepy.utils.global_types import SwapTypes +from financepy.utils.frequency import FrequencyTypes +from financepy.utils.day_count import DayCountTypes +from financepy.utils.calendar import Calendar, CalendarTypes +from financepy.market.curves.interpolator import InterpTypes +from financepy.products.rates.ibor_swap import IborSwap +from financepy.products.rates.ibor_fra import IborFRA +from financepy.products.rates.ibor_deposit import IborDeposit +from financepy.products.rates.ibor_future import IborFuture +from financepy.products.rates.ibor_single_curve import IborSingleCurve + +from financepy.products.rates.ibor_benchmarks_report import ibor_benchmarks_report, dataframe_to_benchmarks + + +def test_ibor_benchmarks_report(): + valuation_date = Date(6, 10, 2001) + cal = CalendarTypes.UNITED_KINGDOM + interp_type = InterpTypes.FLAT_FWD_RATES + + depoDCCType = DayCountTypes.ACT_360 + depos = [] + spot_days = 2 + settlement_date = valuation_date.add_weekdays(spot_days) + depo = IborDeposit(settlement_date, "3M", 4.2/100.0, depoDCCType, cal_type=cal) + depos.append(depo) + + fraDCCType = DayCountTypes.ACT_360 + fras = [] + fra = IborFRA(settlement_date.add_tenor("3M"), "3M", 4.20/100.0, fraDCCType, cal_type=cal) + fras.append(fra) + + swaps = [] + swapType = SwapTypes.PAY + fixedDCCType = DayCountTypes.THIRTY_E_360_ISDA + fixedFreqType = FrequencyTypes.SEMI_ANNUAL + + swap = IborSwap(settlement_date, "1Y", swapType, 4.20/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "2Y", swapType, 4.30/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "3Y", swapType, 4.70/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "5Y", swapType, 5.40/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "7Y", swapType, 5.70/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "10Y", swapType, 6.00/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "12Y", swapType, 6.10/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "15Y", swapType, 5.90/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "20Y", swapType, 5.60/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "25Y", swapType, 5.55/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + + # Create but do not build the initial curve + do_build = True + curve = IborSingleCurve(valuation_date, depos, fras, swaps, + interp_type, check_refit=False, do_build=do_build) + + bechmarks_report = ibor_benchmarks_report(curve) + + # print(bechmarks_report) + + # Confirm that there are no NaNs. In particular this means that different types of benchmarks + # return exactly the same keys, just like we want it, with a couple of exceptions + assert (bechmarks_report + .drop(columns=['fixed_freq_type', 'fixed_leg_type']) + .isnull().values.any() + ) == False + + +def test_dataframe_to_benchmarks(): + path = dirname(__file__) + filename = "ibor_benchmarks_example.csv" + full_filename_path = join(path, "data", filename) + + asof = Date(6, 10, 2001) + + df = pd.read_csv(full_filename_path, index_col=0) + df['start_date'] = pd.to_datetime(df['start_date'], errors='ignore') # allow tenors + df['maturity_date'] = pd.to_datetime(df['maturity_date'], errors='ignore') # allow tenors + + benchmarks = dataframe_to_benchmarks( + df, asof_date=asof, calendar_type=CalendarTypes.UNITED_KINGDOM) + + assert len(benchmarks['IborDeposit']) == 2 + assert len(benchmarks['IborFRA']) == 1 + assert len(benchmarks['IborSwap']) == 10 + + +if __name__ == '__main__': + test_ibor_benchmarks_report() + test_dataframe_to_benchmarks() diff --git a/golden_tests/TestFinIborCurveParRateShock.py b/golden_tests/TestFinIborCurveParRateShock.py new file mode 100644 index 000000000..bde0bf33e --- /dev/null +++ b/golden_tests/TestFinIborCurveParRateShock.py @@ -0,0 +1,89 @@ +import pytest +import pandas as pd + +from financepy.utils.global_types import SwapTypes +from financepy.utils.math import ONE_MILLION +from financepy.utils.global_vars import gBasisPoint +from financepy.market.curves.interpolator import InterpTypes +from financepy.products.rates.ibor_swap import IborSwap +from financepy.products.rates.ibor_fra import IborFRA +from financepy.products.rates.ibor_deposit import IborDeposit +from financepy.products.rates.ibor_future import IborFuture +from financepy.products.rates.ibor_single_curve import IborSingleCurve +from financepy.products.rates.ibor_single_curve_par_shocker import IborSingleCurveParShocker +from financepy.utils.frequency import FrequencyTypes +from financepy.utils.day_count import DayCountTypes +from financepy.utils.date import Date +from financepy.utils.calendar import Calendar, CalendarTypes + + +def test_ibor_curve_par_rate_shocker(): + valuation_date = Date(6, 10, 2001) + cal = CalendarTypes.UNITED_KINGDOM + interp_type = InterpTypes.FLAT_FWD_RATES + + depoDCCType = DayCountTypes.ACT_360 + depos = [] + spot_days = 2 + settlement_date = valuation_date.add_weekdays(spot_days) + depo = IborDeposit(settlement_date, "3M", 4.2/100.0, depoDCCType, cal_type=cal) + depos.append(depo) + + fraDCCType = DayCountTypes.ACT_360 + fras = [] + fra = IborFRA(settlement_date.add_tenor("3M"), "3M", 4.20/100.0, fraDCCType, cal_type=cal) + fras.append(fra) + + swaps = [] + swapType = SwapTypes.PAY + fixedDCCType = DayCountTypes.THIRTY_E_360_ISDA + fixedFreqType = FrequencyTypes.SEMI_ANNUAL + + swap = IborSwap(settlement_date, "1Y", swapType, 4.20/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "2Y", swapType, 4.30/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "3Y", swapType, 4.70/100.0, fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + + base_curve = IborSingleCurve(valuation_date, depos, fras, swaps, InterpTypes.FLAT_FWD_RATES, ) + curve_shocker = IborSingleCurveParShocker(base_curve) + mat_dates = curve_shocker.benchmarks_report()['maturity_date'].values + + # size of bump + par_rate_bump = 1*gBasisPoint + + # expected forward rate changes in the periods before and after the maturity date of the bumped benchmark + # in basis points + expected_fwd_rate_changes = {1: (1.00000, 0.0), + 2: (1.00000, -0.50715), + 4: (2.06214, -2.16773)} + + # Which benchmarks we test-bump. They are + # - The first (and only) depo we used in construction. Here index = 1 (not 0) because + # ibor_single_curve creates a synthetic deposit for the stub implictly + # - The only FRA + # - the 2Y swap + benchmark_idxs = [1, 2, 4] + for benchmark_idx in benchmark_idxs: + bumped_curve = curve_shocker.apply_bump_to_benchmark(benchmark_idx, par_rate_bump) + + d1 = mat_dates[benchmark_idx-1] + d2 = mat_dates[benchmark_idx] + d3 = mat_dates[benchmark_idx+1] + + base_fwd_before = base_curve.fwd_rate(d1, d2) + base_fwd_after = base_curve.fwd_rate(d2, d3) + bumped_fwd_before = bumped_curve.fwd_rate(d1, d2) + bumped_fwd_after = bumped_curve.fwd_rate(d2, d3) + + actual_fwd_rate_changes = ((bumped_fwd_before-base_fwd_before)/gBasisPoint, + (bumped_fwd_after-base_fwd_after)/gBasisPoint) + + assert round(actual_fwd_rate_changes[0], 3) == round(expected_fwd_rate_changes[benchmark_idx][0], 3) + assert round(actual_fwd_rate_changes[1], 3) == round(expected_fwd_rate_changes[benchmark_idx][1], 3) + + +if __name__ == '__main__': + test_ibor_curve_par_rate_shocker(); + diff --git a/golden_tests/TestFinIborCurveRiskEngine.py b/golden_tests/TestFinIborCurveRiskEngine.py new file mode 100644 index 000000000..1ca1af80c --- /dev/null +++ b/golden_tests/TestFinIborCurveRiskEngine.py @@ -0,0 +1,317 @@ +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +from helpers import * +from financepy.utils.date import Date +from financepy.utils.global_vars import gBasisPoint +from financepy.utils.global_types import SwapTypes +from financepy.utils.calendar import CalendarTypes +from financepy.utils.day_count import DayCountTypes +from financepy.utils.frequency import FrequencyTypes +from financepy.market.curves.interpolator import InterpTypes +from financepy.products.rates.ibor_deposit import IborDeposit +from financepy.products.rates.ibor_fra import IborFRA +from financepy.products.rates.ibor_swap import IborSwap +from financepy.products.rates.ibor_single_curve import IborSingleCurve +import financepy.products.rates.ibor_curve_risk_engine as re + +# when set to True this file can be run standalone and will produce some useful output. +# Set to False to use as part of a testing framework +DIAGNOSTICS_MODE = True + + +def test_par_rate_risk_report_cubic_zero(): + valuation_date = Date(6, 10, 2001) + cal = CalendarTypes.UNITED_KINGDOM + interp_type = InterpTypes.FINCUBIC_ZERO_RATES + + depoDCCType = DayCountTypes.ACT_360 + fraDCCType = DayCountTypes.ACT_360 + swapType = SwapTypes.PAY + fixedDCCType = DayCountTypes.THIRTY_E_360_ISDA + fixedFreqType = FrequencyTypes.SEMI_ANNUAL + + settlement_date, base_curve = _generate_base_curve( + valuation_date, cal, interp_type, depoDCCType, fraDCCType, swapType, fixedDCCType, fixedFreqType) + trades = _generate_trades(valuation_date, cal, swapType, + fixedDCCType, fixedFreqType, settlement_date, base_curve) + + # size of bump to apply. In all cases par risk is reported as change in value to 1 bp rate bump + par_rate_bump = 1*gBasisPoint + + # run the report + base_values, risk_report = re.par_rate_risk_report( + base_curve, trades, bump_size=par_rate_bump) + + expected_totals = [0.00122854, -0.25323828, -0.24271177, -0.01423219, 0.31617136, + 4.0262114, 2.03409619, -0.3957559] + actual_totals = risk_report['total'].values + + if DIAGNOSTICS_MODE: + trade_labels = list(base_values.keys()) + np.set_printoptions(suppress=True) + print(base_values) + print(risk_report['total'].values) + print(risk_report[trade_labels + ['total']].sum(axis=0)) + + assert max(np.abs(actual_totals - expected_totals)) <= 1e-4 + + +def test_par_rate_risk_report_flat_forward(): + valuation_date = Date(6, 10, 2022) + base_curve = buildIborSingleCurve(valuation_date, '10Y') + settlement_date = base_curve.used_swaps[0].effective_dt + cal = base_curve.used_swaps[0].fixed_leg.cal_type + fixed_day_count = base_curve.used_swaps[0].fixed_leg.dc_type + fixed_freq_type = base_curve.used_swaps[0].fixed_leg.freq_type + + trades = _generate_trades(valuation_date, cal, SwapTypes.PAY, + fixed_day_count, fixed_freq_type, settlement_date, base_curve) + + # size of bump to apply. In all cases par risk is reported as change in value to 1 bp rate bump + par_rate_bump = 1*gBasisPoint + + # run the report + base_values, risk_report = re.par_rate_risk_report( + base_curve, trades, bump_size=par_rate_bump) + + expected_totals = [-0.08629015, -0.20597528, -0.08628776, -0.07793533, -0.05012633, + -0.00005542, -0.00005726, -0.00005542, -0.00005726, -0.00005726, -0.00008393, -0.00013093, + -0.0001267, 1.01065078, 1.49626527, 3.99996586, 0, 0, 0, 0, 0, 0 + ] + actual_totals = risk_report['total'].values + + if DIAGNOSTICS_MODE: + trade_labels = list(base_values.keys()) + np.set_printoptions(suppress=True) + print(base_values) + print(risk_report['total'].values) + print(risk_report[trade_labels + ['total']].sum(axis=0)) + + assert max(np.abs(actual_totals - expected_totals)) <= 1e-4 + + +def test_forward_rate_risk_report(): + valuation_date = Date(6, 10, 2001) + cal = CalendarTypes.UNITED_KINGDOM + interp_type = InterpTypes.FLAT_FWD_RATES + + depoDCCType = DayCountTypes.ACT_360 + fraDCCType = DayCountTypes.ACT_360 + swapType = SwapTypes.PAY + fixedDCCType = DayCountTypes.THIRTY_E_360_ISDA + fixedFreqType = FrequencyTypes.SEMI_ANNUAL + + settlement_date, base_curve = _generate_base_curve( + valuation_date, cal, interp_type, depoDCCType, fraDCCType, swapType, fixedDCCType, fixedFreqType) + trades = _generate_trades(valuation_date, cal, swapType, + fixedDCCType, fixedFreqType, settlement_date, base_curve) + + # the grid on which we generate the risk report + grid_bucket = '3M' + grid_last_date = max(t.maturity_dt for t in trades) + + # size of bump to apply. In all cases par risk is reported as change in value to 1 bp rate bump + forward_rate_bump = 1*gBasisPoint + + # run the report + base_values, risk_report = re.forward_rate_risk_report( + base_curve, grid_last_date, grid_bucket, trades, bump_size=forward_rate_bump) + + expected_totals = [0.25196322, 0.24648603, 0.48750647, 0.49286362, 0.48160453, 0.47113499, + 0.46547237, 0.47058739, 0.45980829, 0.45481044, 0.23117766, 0.22408547, + 0.2190641, 0.21423566, 0.21169689, 0.21396794, 0.] + actual_totals = risk_report[re.DV01_PREFIX+'total'].values + + if DIAGNOSTICS_MODE: + dv01_trade_labels = [re.DV01_PREFIX + l for l in base_values.keys()] + np.set_printoptions(suppress=True) + print(base_values) + print(risk_report) + print(risk_report[re.DV01_PREFIX + 'total'].values) + print(risk_report[dv01_trade_labels + [re.DV01_PREFIX + 'total']].sum(axis=0)) + + assert max(np.abs(actual_totals - expected_totals)) <= 1e-4 + + +def test_forward_rate_custom_grid_risk_report(): + valuation_date = Date(6, 10, 2001) + cal = CalendarTypes.UNITED_KINGDOM + interp_type = InterpTypes.FLAT_FWD_RATES + + depoDCCType = DayCountTypes.ACT_360 + fraDCCType = DayCountTypes.ACT_360 + swapType = SwapTypes.PAY + fixedDCCType = DayCountTypes.THIRTY_E_360_ISDA + fixedFreqType = FrequencyTypes.SEMI_ANNUAL + + settlement_date, base_curve = _generate_base_curve( + valuation_date, cal, interp_type, depoDCCType, fraDCCType, swapType, fixedDCCType, fixedFreqType) + trades = _generate_trades(valuation_date, cal, swapType, + fixedDCCType, fixedFreqType, settlement_date, base_curve) + + # the grid on which we generate the risk report + grid = [valuation_date, valuation_date.add_tenor( + '3M'), valuation_date.add_tenor('15M'), valuation_date.add_tenor('10Y')] + + # size of bump to apply. In all cases par risk is reported as change in value to 1 bp rate bump + forward_rate_bump = 1*gBasisPoint + + # run the report + base_values, risk_report, *_ = re.forward_rate_risk_report_custom_grid( + base_curve, grid, trades, bump_size=forward_rate_bump) + + expected_totals = [0.24374713, 1.70089994, 3.65138343] + actual_totals = risk_report[re.DV01_PREFIX+'total'].values + + if DIAGNOSTICS_MODE: + dv01_trade_labels = [re.DV01_PREFIX + l for l in base_values.keys()] + np.set_printoptions(suppress=True) + print(base_values) + print(risk_report) + print(risk_report[re.DV01_PREFIX + 'total'].values) + print(risk_report[dv01_trade_labels + [re.DV01_PREFIX + 'total']].sum(axis=0)) + + assert max(np.abs(actual_totals - expected_totals)) <= 1e-4 + + +def test_carry_rolldown_report(): + valuation_date = Date(6, 10, 2001) + cal = CalendarTypes.UNITED_KINGDOM + interp_type = InterpTypes.FLAT_FWD_RATES + + depoDCCType = DayCountTypes.ACT_360 + fraDCCType = DayCountTypes.ACT_360 + swapType = SwapTypes.PAY + fixedDCCType = DayCountTypes.THIRTY_E_360_ISDA + fixedFreqType = FrequencyTypes.SEMI_ANNUAL + + settlement_date, base_curve = _generate_base_curve( + valuation_date, cal, interp_type, depoDCCType, fraDCCType, swapType, fixedDCCType, fixedFreqType) + trades = _generate_trades(valuation_date, cal, swapType, + fixedDCCType, fixedFreqType, settlement_date, base_curve) + + # the grid on which we generate the risk report + grid_bucket = '6M' + grid_last_date = max(t.maturity_dt for t in trades) + + # run the report + base_values, risk_report, *_ = re.carry_rolldown_report( + base_curve, grid_last_date, grid_bucket, trades,) + + if DIAGNOSTICS_MODE: + roll_trade_labels = [re.ROLL_PREFIX + l for l in base_values.keys()] + np.set_printoptions(suppress=True) + print(base_values) + print(risk_report) + print(risk_report[re.ROLL_PREFIX + 'total'].values) + print(risk_report[roll_trade_labels + [re.ROLL_PREFIX + 'total']].sum(axis=0)) + + expected_totals = [-21.07588523, 16.07402002, -27.27637637, -0.02469482, -104.29563056, + 0.32142506, 35.98144427, 0.28310627, 0.] + actual_totals = risk_report[re.ROLL_PREFIX + 'total'].values + assert max(np.abs(actual_totals - expected_totals)) <= 1e-4 + + +def test_parallel_shift_ladder_report(): + valuation_date = Date(6, 10, 2001) + cal = CalendarTypes.UNITED_KINGDOM + interp_type = InterpTypes.FLAT_FWD_RATES + + depoDCCType = DayCountTypes.ACT_360 + fraDCCType = DayCountTypes.ACT_360 + swapType = SwapTypes.PAY + fixedDCCType = DayCountTypes.THIRTY_E_360_ISDA + fixedFreqType = FrequencyTypes.SEMI_ANNUAL + + settlement_date, base_curve = _generate_base_curve( + valuation_date, cal, interp_type, depoDCCType, fraDCCType, swapType, fixedDCCType, fixedFreqType) + trades = _generate_trades(valuation_date, cal, swapType, + fixedDCCType, fixedFreqType, settlement_date, base_curve) + + # the curve shift grids on which we calculate the PV ladder + curve_shifts = np.linspace(-400*gBasisPoint, 400*gBasisPoint, 17, endpoint=True) + + # run the report + base_values, risk_report = re.parallel_shift_ladder_report( + base_curve, curve_shifts, trades,) + + if DIAGNOSTICS_MODE: + pv_trade_labels = [re.PV_PREFIX + l for l in base_values.keys()] + np.set_printoptions(suppress=True) + print(base_values) + print(risk_report) + print(risk_report[re.PV_PREFIX + 'total'].values) + print(risk_report[pv_trade_labels + [re.PV_PREFIX + 'total']].sum(axis=0)) + + # risk_report.plot('shift_bp', re.PV_PREFIX + 'total') + x = risk_report['shift_bp'].values + y = risk_report[re.PV_PREFIX + 'total'].values + plt.plot(x, y - x*(y[-1] - y[0])/(x[-1] - x[0])) + plt.show() + + expected_totals = [-2407.30636808, -2087.15913624, -1772.70446365, -1463.84150813, + -1160.47126898, -862.49655251, -569.82193811, -282.35374508, + -0., 277.32959522, 549.7236947, 817.26933932, + 1080.05198694, 1338.15554197, 1591.66238438, 1840.6533982, + 2085.20799948, ] + actual_totals = risk_report[re.PV_PREFIX + 'total'].values + assert max(np.abs(actual_totals - expected_totals)) <= 1e-4 + + +def _generate_trades(valuation_date, cal, swapType, fixedDCCType, fixedFreqType, settlement_date, base_curve): + trade1 = IborSwap(settlement_date, "4Y", swapType, 4.20 / + 100.0, fixedFreqType, fixedDCCType, cal_type=cal, notional=10000) + atm = trade1.swap_rate(valuation_date, base_curve) + trade1.set_fixed_rate(atm) + trade2 = IborSwap(settlement_date.add_tenor('6M'), "2Y", swapType, + 4.20/100.0, fixedFreqType, fixedDCCType, cal_type=cal, notional=10000) + atm = trade2.swap_rate(valuation_date, base_curve) + trade2.set_fixed_rate(atm) + trades = [trade1, trade2] + return trades + + +def _generate_base_curve(valuation_date, cal, interp_type, depoDCCType, fraDCCType, swapType, fixedDCCType, fixedFreqType): + depos = [] + spot_days = 2 + settlement_date = valuation_date.add_weekdays(spot_days) + depo = IborDeposit(settlement_date, "3M", 4.2/100.0, + depoDCCType, cal_type=cal) + depos.append(depo) + + fras = [] + fra = IborFRA(settlement_date.add_tenor("3M"), "3M", + 4.20/100.0, fraDCCType, cal_type=cal) + fras.append(fra) + + swaps = [] + swap = IborSwap(settlement_date, "1Y", swapType, 4.20/100.0, + fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "2Y", swapType, 4.30/100.0, + fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "3Y", swapType, 4.70/100.0, + fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "5Y", swapType, 4.70/100.0, + fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + swap = IborSwap(settlement_date, "7Y", swapType, 4.70/100.0, + fixedFreqType, fixedDCCType, cal_type=cal) + swaps.append(swap) + + base_curve = IborSingleCurve( + valuation_date, depos, fras, swaps, interp_type, ) + + return settlement_date, base_curve + + +if DIAGNOSTICS_MODE and __name__ == '__main__': + test_par_rate_risk_report_cubic_zero() + # test_forward_rate_risk_report() + # test_forward_rate_custom_grid_risk_report() + # test_carry_rolldown_report() + # test_parallel_shift_ladder_report() diff --git a/golden_tests/data/ibor_benchmarks_example.csv b/golden_tests/data/ibor_benchmarks_example.csv new file mode 100644 index 000000000..ddf61d8ce --- /dev/null +++ b/golden_tests/data/ibor_benchmarks_example.csv @@ -0,0 +1,14 @@ +,type,start_date,maturity_date,day_count_type,notional,contract_rate,market_rate,spot_pvbp,fwd_pvbp,unit_value,value,fixed_leg_type,fixed_freq_type +0,IborDeposit,06-Oct-01,09-Oct-01,ACT_360,100,0.042,0.042,0.00833,0.00833,1,100,NaN,NaN +1,IborDeposit,09-Oct-01,09-Jan-02,ACT_360,100,0.042,0.042,0.252753,0.252842,1,100,NaN,NaN +2,IborFRA,09-Jan-02,09-Apr-02,ACT_360,100,0.042,0.042,0.244689,0.247402,0,0,PAY,NaN +3,IborSwap,09-Oct-01,09-Oct-02,THIRTY_E_360_ISDA,1000000,0.042,0.042,0.968858,0.969197,0,0,PAY,SEMI_ANNUAL +4,IborSwap,09-Oct-01,09-Oct-03,THIRTY_E_360_ISDA,1000000,0.043,0.043,1.897071,1.897735,0,0,PAY,SEMI_ANNUAL +5,IborSwap,09-Oct-01,11-Oct-04,THIRTY_E_360_ISDA,1000000,0.047,0.047,2.782943,2.783917,0,0,PAY,SEMI_ANNUAL +6,IborSwap,09-Oct-01,09-Oct-06,THIRTY_E_360_ISDA,1000000,0.054,0.054,4.381878,4.383412,0,0,PAY,SEMI_ANNUAL +7,IborSwap,09-Oct-01,09-Oct-08,THIRTY_E_360_ISDA,1000000,0.057,0.057,5.789357,5.791383,0,0,PAY,SEMI_ANNUAL +8,IborSwap,09-Oct-01,10-Oct-11,THIRTY_E_360_ISDA,1000000,0.06,0.06,7.575903,7.578554,0,-0.000013,PAY,SEMI_ANNUAL +9,IborSwap,09-Oct-01,09-Oct-13,THIRTY_E_360_ISDA,1000000,0.061,0.061,8.57742,8.580422,0,0,PAY,SEMI_ANNUAL +10,IborSwap,09-Oct-01,10-Oct-16,THIRTY_E_360_ISDA,1000000,0.059,0.059,9.899371,9.902836,0,-0.000001,PAY,SEMI_ANNUAL +11,IborSwap,09-Oct-01,11-Oct-21,THIRTY_E_360_ISDA,1000000,0.056,0.056,11.766369,11.770487,0,-0.000037,PAY,SEMI_ANNUAL +12,IborSwap,09-Oct-01,09-Oct-26,THIRTY_E_360_ISDA,1000000,0.0555,0.0555,13.250223,13.25486,0,-0.000111,PAY,SEMI_ANNUAL diff --git a/requirements.txt b/requirements.txt index d2ec1975e..fe7c9d3c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ ##### Imports required by core library -numba==0.55.2 -numpy==1.21.6 -scipy==1.10.0 -llvmlite==0.38.0 -ipython==8.10.0 -matplotlib==3.5.2 -pandas==1.5.3 -prettytable==3.6.0 \ No newline at end of file +numba==0.60.0 +numpy==1.26.4 +scipy==1.13.1 +llvmlite==0.43.0 +ipython==8.25.0 +matplotlib==3.8.4 +pandas==2.1.2 +prettytable==3.9.0 \ No newline at end of file diff --git a/unit_tests/data/giltBondPrices.txt b/unit_tests/data/gilt_bond_prices.txt similarity index 100% rename from unit_tests/data/giltBondPrices.txt rename to unit_tests/data/gilt_bond_prices.txt diff --git a/unit_tests/test_FinBondYieldCurve.py b/unit_tests/test_FinBondYieldCurve.py index a7bf7964b..850044bd1 100644 --- a/unit_tests/test_FinBondYieldCurve.py +++ b/unit_tests/test_FinBondYieldCurve.py @@ -15,7 +15,7 @@ from financepy.products.bonds.curve_fits import CurveFitNelsonSiegel from financepy.products.bonds.curve_fits import CurveFitNelsonSiegelSvensson -path = os.path.join(os.path.dirname(__file__), './data/giltBondPrices.txt') +path = os.path.join(os.path.dirname(__file__), './data/gilt_bond_prices.txt') bondDataFrame = pd.read_csv(path, sep='\t') bondDataFrame['mid'] = 0.5*(bondDataFrame['bid'] + bondDataFrame['ask']) diff --git a/unit_tests/test_FinBondZSpread.py b/unit_tests/test_FinBondZSpread.py index 82e012413..8d539aede 100644 --- a/unit_tests/test_FinBondZSpread.py +++ b/unit_tests/test_FinBondZSpread.py @@ -48,7 +48,7 @@ def test_z_spread_actual_curve(): def _test_z_spread_for_curve(base_curve: DiscountCurve): - path = os.path.join(os.path.dirname(__file__), './data/giltBondPrices.txt') + path = os.path.join(os.path.dirname(__file__), './data/gilt_bond_prices.txt') bondDataFrame = pd.read_csv(path, sep='\t') bondDataFrame['mid'] = 0.5*(bondDataFrame['bid'] + bondDataFrame['ask']) bondDataFrame['maturity'] = pd.to_datetime(bondDataFrame['maturity']) @@ -78,6 +78,6 @@ def _test_z_spread_for_curve(base_curve: DiscountCurve): assert bondDataFrame['z_spread'].isnull().values.any() == False -if DIAGNOSTICS_MODE and __name__ == '__main__': +if __name__ == '__main__': test_z_spread_flat_curve() test_z_spread_actual_curve() diff --git a/unit_tests/test_FinBondZeroCurve.py b/unit_tests/test_FinBondZeroCurve.py index 3da58be3b..9813a4902 100644 --- a/unit_tests/test_FinBondZeroCurve.py +++ b/unit_tests/test_FinBondZeroCurve.py @@ -12,7 +12,7 @@ import pandas as pd -path = os.path.join(os.path.dirname(__file__), './data/giltBondPrices.txt') +path = os.path.join(os.path.dirname(__file__), './data/gilt_bond_prices.txt') bondDataFrame = pd.read_csv(path, sep='\t') bondDataFrame['mid'] = 0.5*(bondDataFrame['bid'] + bondDataFrame['ask']) diff --git a/unit_tests/test_FinIborBenchmarksReport.py b/unit_tests/test_FinIborBenchmarksReport.py index a5fa07aa0..0eaf909f6 100644 --- a/unit_tests/test_FinIborBenchmarksReport.py +++ b/unit_tests/test_FinIborBenchmarksReport.py @@ -83,7 +83,7 @@ def test_ibor_benchmarks_report(): def test_dataframe_to_benchmarks(): path = dirname(__file__) - filename = "Ibor_Benchmarks_Example.csv" + filename = "ibor_benchmarks_example.csv" full_filename_path = join(path, "data", filename) asof = Date(6, 10, 2001) diff --git a/unit_tests/test_FinIborCurveParRateShock.py b/unit_tests/test_FinIborCurveParRateShock.py index 3de9812b4..a8fa0197c 100644 --- a/unit_tests/test_FinIborCurveParRateShock.py +++ b/unit_tests/test_FinIborCurveParRateShock.py @@ -82,3 +82,7 @@ def test_ibor_curve_par_rate_shocker(): assert round(actual_fwd_rate_changes[0], 3) == round(expected_fwd_rate_changes[benchmark_idx][0], 3) assert round(actual_fwd_rate_changes[1], 3) == round(expected_fwd_rate_changes[benchmark_idx][1], 3) + + +test_ibor_curve_par_rate_shocker() + diff --git a/unit_tests/test_FinIborCurveRiskEngine.py b/unit_tests/test_FinIborCurveRiskEngine.py index 1ca1af80c..a1d1851c8 100644 --- a/unit_tests/test_FinIborCurveRiskEngine.py +++ b/unit_tests/test_FinIborCurveRiskEngine.py @@ -18,7 +18,7 @@ # when set to True this file can be run standalone and will produce some useful output. # Set to False to use as part of a testing framework -DIAGNOSTICS_MODE = True +DIAGNOSTICS_MODE = False def test_par_rate_risk_report_cubic_zero(): @@ -309,9 +309,9 @@ def _generate_base_curve(valuation_date, cal, interp_type, depoDCCType, fraDCCTy return settlement_date, base_curve -if DIAGNOSTICS_MODE and __name__ == '__main__': +if __name__ == '__main__': test_par_rate_risk_report_cubic_zero() - # test_forward_rate_risk_report() - # test_forward_rate_custom_grid_risk_report() - # test_carry_rolldown_report() - # test_parallel_shift_ladder_report() + test_forward_rate_risk_report() + test_forward_rate_custom_grid_risk_report() + test_carry_rolldown_report() + test_parallel_shift_ladder_report()