From 53a3dcbbbb5bdf02aecee407e2f43f924c955a76 Mon Sep 17 00:00:00 2001 From: minikie Date: Tue, 18 Jan 2022 20:12:40 +0900 Subject: [PATCH 01/54] 0.8.36.0 --- README.md | 11 ++++++- scenario/usage.py | 73 ++++++++++++++++++++++++++--------------------- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 214ed29..bae0fea 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ MxDevTool(Beta) : Financial Library ![image](https://img.shields.io/badge/platform-windows_64|_linux_64-red) ![image](https://img.shields.io/badge/python-3.6|3.7|3.8|3.9-blue) -![image](https://img.shields.io/badge/version-0.8.35.3-green.svg) +![image](https://img.shields.io/badge/version-0.8.36.0-green.svg) ![image](https://img.shields.io/badge/platform-macOS_64-red) ![image](https://img.shields.io/badge/python-3.8|3.9-blue) @@ -250,6 +250,11 @@ hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startPeriod=mx.Period(6, mx. hw1f_discountFactor = hw1f.discountFactor('hw1f_discountFactor') hw1f_discountBond3m = hw1f.discountBond('hw1f_discountBond3m', maturity=mx.Period(3, mx.Months)) +r_t = 0.02 +hw1f.model_discountBond(0.0, 1.0, r_t) +hw1f.model_spot(1.0, 2.0, r_t) # continuous compounding +hw1f.model_forward(1.0, 2.0, 3.0, r_t) # continuous compounding +hw1f.model_discount(1.0) ``` ### Calcs @@ -806,6 +811,10 @@ For source code, check this repository. # Release History +## 0.8.36.0 (2022-1-18) +- Model Calculation Methods(spot, forward in shortrate model) are added +- Build Process is Changed to Docker + ## 0.8.35.3 (2021-8-12) - Library Dependencies are removed ( pandas, jinja2, matplot ) - ZeroYieldCurve CurveType error bug fix diff --git a/scenario/usage.py b/scenario/usage.py index a7534a7..b1ef6db 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -48,7 +48,7 @@ def test(): # yieldCurve rfCurve = ts.ZeroYieldCurve(ref_date, tenors, rf_rates, interpolator1DType, extrapolator1DType) divCurve = ts.ZeroYieldCurve(ref_date, tenors, div_rates, interpolator1DType, extrapolator1DType) - + utils.check_hashCode(rfCurve, divCurve) # variance termstructure @@ -69,7 +69,7 @@ def test(): alphaPara = xen.DeterministicParameter(['1y', '20y', '100y'], [0.1, 0.15, 0.15]) sigmaPara = xen.DeterministicParameter(['20y', '100y'], [0.01, 0.015]) - + hw1f = xen.HullWhite1F('hw1f', fittingCurve=rfCurve, alphaPara=alphaPara, sigmaPara=sigmaPara) bk1f = xen.BK1F('bk1f', fittingCurve=rfCurve, alphaPara=alphaPara, sigmaPara=sigmaPara) cir1f = xen.CIR1F('cir1f', r0=0.02, alpha=0.1, longterm=0.042, sigma=0.03) @@ -82,12 +82,19 @@ def test(): hw1f_discountFactor = hw1f.discountFactor('hw1f_discountFactor') hw1f_discountBond3m = hw1f.discountBond('hw1f_discountBond3m', maturityTenor=mx.Period(3, mx.Months)) + # model calculation + r_t = 0.02 + hw1f.model_discountBond(0.0, 1.0, r_t) + hw1f.model_spot(1.0, 2.0, r_t) # continuous compounding + hw1f.model_forward(1.0, 2.0, 3.0, r_t) # continuous compounding + hw1f.model_discount(1.0) + # calcs constantValue = xen.ConstantValue('constantValue', 15) constantArr = xen.ConstantArray('constantArr', [15,14,13]) oper1 = gbmconst + gbm - oper2 = gbmconst - gbm + oper2 = gbmconst - gbm oper3 = (gbmconst * gbm).withName('multiple_gbmconst_gbm') oper4 = gbmconst / gbm @@ -107,8 +114,8 @@ def test(): shiftRight1 = xen.Shift('shiftRight1', hw1f, shift=5) shiftRight2 = hw1f.shift('shiftRight2', shift=5, fill_value=0.0) - shiftLeft1 = xen.Shift('shiftLeft1', cir1f, shift=-5) - shiftLeft2 = cir1f.shift('shiftLeft2', shift=-5, fill_value=0.0) + shiftLeft1 = xen.Shift('shiftLeft1', cir1f, shift=-5) + shiftLeft2 = cir1f.shift('shiftLeft2', shift=-5, fill_value=0.0) returns1 = xen.Returns('returns1', gbm, 'return') returns2 = gbm.returns('returns2', 'return') @@ -147,7 +154,7 @@ def test(): # random pseudo_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='pseudo', subType='mersennetwister', randomTransformType='boxmullernormal') sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') - + # single model filename1='./single_model.npz' results1 = xen.generate1d(model=gbm, calcs=None, timegrid=timegrid1, rsg=pseudo_rsg, filename=filename1, isMomentMatching=False) @@ -169,10 +176,10 @@ def test(): constantValue, constantArr, oper1, oper2, oper3, oper4, oper5, oper6, oper7, oper8, oper9, oper10, oper11, oper12, linearOper1, linearOper2, shiftRight1, shiftRight2, shiftLeft1, shiftLeft2, returns1, returns2, logreturns1, logreturns2, cumreturns1, cumreturns2, cumlogreturns1, cumlogreturns2, fixedRateBond ] - + filename4='./multiple_model_with_calc_all.npz' corrMatrix2 = mx.IdentityMatrix(len(all_models)) - + corrMatrix2[1][0] = 0.5 corrMatrix2[0][1] = 0.5 @@ -185,17 +192,17 @@ def test(): results.randomSubtype, results.randomType, results.seed, results.shape ) ndarray = results.toNumpyArr() # pre load all scenario data to ndarray - + t_pos = 1 scenCount = 15 - + # scenario path of selected scenCount - # ((100.0, 82.94953421561434, 110.87375162324332, 91.96798678908293, 70.29920544659505, ... ), - # (100.0, 96.98838977927142, 97.0643112022828, 91.19803393176569, 104.94407125936456, ... ), + # ((100.0, 82.94953421561434, 110.87375162324332, 91.96798678908293, 70.29920544659505, ... ), + # (100.0, 96.98838977927142, 97.0643112022828, 91.19803393176569, 104.94407125936456, ... ), # ... # (200.0, 179.93792399488575, 207.93806282552612, 183.16602072084862, ... ), # (9546.93761943355, 9969.778029330208, 10758.449206155927, 11107.968356394866, ... )) - multipath = results[scenCount] + multipath = results[scenCount] multipath_arr = ndarray[scenCount] # t_pos data @@ -221,11 +228,11 @@ def test(): for pv in all_pv_list: analyticPath = pv.analyticPath(timegrid2) - + input_arr = [0.01, 0.02, 0.03, 0.04, 0.05] input_arr2d = [[0.01, 0.02, 0.03, 0.04, 0.05], [0.06, 0.07, 0.08, 0.09, 0.1]] - + for pv in all_calcs: if pv.sourceNum == 1: calculatePath = pv.calculatePath(input_arr, timegrid1) @@ -233,8 +240,8 @@ def test(): calculatePath = pv.calculatePath(input_arr2d, timegrid1) else: pass - - # repository + + # repository repo_path = './xenrepo' repo_config = { 'location': repo_path } repo = mx_dr.FolderRepository(repo_config) @@ -245,7 +252,7 @@ def test(): filename5 = 'scen_all.npz' scen_all = xen.Scenario(models=all_models, calcs=all_calcs, corr=corrMatrix2, timegrid=timegrid4, rsg=sobol_rsg, filename=filename5, isMomentMatching=False) - + filename6 = 'scen_multiple.npz' scen_multiple = xen.Scenario(models=models, calcs=[], corr=corrMatrix, timegrid=timegrid4, rsg=pseudo_rsg, filename=filename6, isMomentMatching=False) @@ -253,7 +260,7 @@ def test(): # scenario - save, load, list name1 = 'name1' - xm.save_xen(name1, scen_all) # + xm.save_xen(name1, scen_all) # scen_name1 = xm.load_xen(name=name1) scen_name1.filename = './reloaded_scenfile.npz' @@ -267,7 +274,7 @@ def test(): # generate in result directory xm.generate_xen(scenList[0]) - + # scenario template builder using market data sb = xen.ScenarioBuilder() @@ -293,7 +300,7 @@ def test(): sb.addCalc(xen.ConstantValue.__name__, 'constantValue', v=15) sb.addCalc(xen.ConstantArray.__name__, 'constantArr', arr=[15,14,13]) - + sb.addCalc(xen.AdditionOper.__name__, 'addOper1', pc1='gbmconst', pc2='gbm') sb.addCalc(xen.SubtractionOper.__name__, 'subtOper1', pc1='gbmconst', pc2='gbm') sb.addCalc(xen.MultiplicationOper.__name__, 'multiple_gbmconst_gbm', pc1='gbmconst', pc2='gbm') @@ -368,7 +375,7 @@ def test(): # shock definition quote1 = mx_q.SimpleQuote('quote1', 100) - + qst_add = mx_s.QuoteShockTrait(name='add_up1', value=10, operand='add') qst_mul = mx_s.QuoteShockTrait('mul_up1', 1.1, 'mul') qst_ass = mx_s.QuoteShockTrait('assign_up1', 0.03, 'assign') @@ -411,9 +418,9 @@ def test(): shocked_mrk1 = mx_s.build_shockedMrk(shock1, mrk) shock2 = shock1.clone(name='shock2') shocked_mrk2 = mx_s.build_shockedMrk(shock2, mrk) - + utils.check_hashCode(shock1, shock2, shocked_mrk1, shocked_mrk2) - + shockedScen_list = mx_s.build_shockedScen([shock1, shock2], sb, mrk) shm = mx_s.ShockScenarioModel('shm1', basescen=scen, s_up=shockedScen_list[0], s_down=shockedScen_list[1]) @@ -425,14 +432,14 @@ def test(): # compare ? csr = xen.CompositeScenarioResults(shm.shocked_scen_res_d, basescen_name, gbmconst='s_down') - + csr_arr = csr.toNumpyArr() base_arr = scen.getResults().toNumpyArr() assert base_arr[0][0][0] + qst_add.value == csr_arr[0][0][0] # replaced(gbmconst) assert base_arr[0][1][0] == csr_arr[0][1][0] # not replaced(gbm) - # shock manager - save, load, list + # shock manager - save, load, list # extensions : shock(.shk), shocktrait(.sht), shockscenariomodel(.shm) sfm = repo.shock_manager @@ -440,7 +447,7 @@ def test(): sht_name = 'shocktraits' sfm.save_shts(sht_name, *shocktrait_list) reloaded_sht_d = sfm.load_shts(sht_name) - + for s in shocktrait_list: utils.check_hashCode(s, reloaded_sht_d[s.name]) utils.compare_hashCode(s, reloaded_sht_d[s.name]) @@ -449,7 +456,7 @@ def test(): shk_name = 'shocks' sfm.save_shks(shk_name, shock1, shock2) reloaded_shk_d = sfm.load_shks(shk_name) - + for s in [shock1, shock2]: utils.check_hashCode(s, reloaded_shk_d[s.name]) utils.compare_hashCode(s, reloaded_shk_d[s.name]) @@ -474,10 +481,10 @@ def test(): except: print('fail to check bloomberg') # instruments pricing - + # this is built-in instruments # option1 = mx_i.EuropeanOption(option_type='c', strike=400, maturityDate=ref_date + 365) - + # this is inherit instrument for user output class EuropeanOptionForUserOutput(mx_i.EuropeanOption): def userfunc_test(self, scen_data_d, calc_kwargs): @@ -495,8 +502,8 @@ def userfunc_test(self, scen_data_d, calc_kwargs): test_output = mx_io.UserFunc(scen='basescen', userfunc=option.userfunc_test, abc=10) # calculate from scenario - results1 = option.calculateScen(outputs=[npv, discount_cf, delta, gamma, test_output], shm=shm, reduce='aver', - path_kwargs={'s1': 'gbmconst', 'discount': 'hw1f_discountFactor'}, + results1 = option.calculateScen(outputs=[npv, discount_cf, delta, gamma, test_output], shm=shm, reduce='aver', + path_kwargs={'s1': 'gbmconst', 'discount': 'hw1f_discountFactor'}, calc_kwargs={'calc_arg1': 10}) # calculate from model @@ -504,7 +511,7 @@ def userfunc_test(self, scen_data_d, calc_kwargs): gbmconst_basescen = basescen.getModel('gbmconst') arg_d = { 'x0': gbmconst_basescen._x0, 'rf': gbmconst_basescen._rf, 'div': gbmconst_basescen._div, 'vol': gbmconst_basescen._vol } assert option.setPricingParams_GBMConst(**arg_d).NPV() == option.setPricingParams_Model(gbmconst_basescen).NPV() - + # calendar holiday mydates = [mx.Date(2022, 10, 11), mx.Date(2022, 10, 12), mx.Date(2022, 10, 13), mx.Date(2022, 11, 11)] From 9b67f738a9f87050d0cf266c0a39c6fada3b6c35 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Tue, 18 Jan 2022 22:58:07 +0900 Subject: [PATCH 02/54] 0.8.36.0 --- scenario/usage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenario/usage.py b/scenario/usage.py index b1ef6db..318a4d0 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -83,7 +83,7 @@ def test(): hw1f_discountBond3m = hw1f.discountBond('hw1f_discountBond3m', maturityTenor=mx.Period(3, mx.Months)) # model calculation - r_t = 0.02 + r_t = 0.02 # short rate hw1f.model_discountBond(0.0, 1.0, r_t) hw1f.model_spot(1.0, 2.0, r_t) # continuous compounding hw1f.model_forward(1.0, 2.0, 3.0, r_t) # continuous compounding From a197a52e29717715ff9b037b7fd29b8206c5a061 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Tue, 18 Jan 2022 23:04:42 +0900 Subject: [PATCH 03/54] 0.8.36.0 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bae0fea..a3a20c9 100644 --- a/README.md +++ b/README.md @@ -250,11 +250,11 @@ hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startPeriod=mx.Period(6, mx. hw1f_discountFactor = hw1f.discountFactor('hw1f_discountFactor') hw1f_discountBond3m = hw1f.discountBond('hw1f_discountBond3m', maturity=mx.Period(3, mx.Months)) -r_t = 0.02 +r_t = 0.02 # short rate hw1f.model_discountBond(0.0, 1.0, r_t) hw1f.model_spot(1.0, 2.0, r_t) # continuous compounding hw1f.model_forward(1.0, 2.0, 3.0, r_t) # continuous compounding -hw1f.model_discount(1.0) +hw1f.model_discount(1.0) # ``` ### Calcs From e5a7bb13bd9860c53ab78e8acc4ba8821b158d89 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Sun, 10 Apr 2022 19:55:26 +0900 Subject: [PATCH 04/54] 0.8.37.1 --- README.md | 7 ++++++- scenario/usage.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a3a20c9..23b05f9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ MxDevTool(Beta) : Financial Library ![image](https://img.shields.io/badge/platform-windows_64|_linux_64-red) ![image](https://img.shields.io/badge/python-3.6|3.7|3.8|3.9-blue) -![image](https://img.shields.io/badge/version-0.8.36.0-green.svg) +![image](https://img.shields.io/badge/version-0.8.37.1-green.svg) ![image](https://img.shields.io/badge/platform-macOS_64-red) ![image](https://img.shields.io/badge/python-3.8|3.9-blue) @@ -811,6 +811,11 @@ For source code, check this repository. # Release History +## 0.8.37.1 (2022-4-10) +- ExternalRsg(using numpy file) is added for external random number +- Output file contents is updated(correlation, random) - v1.1.0 +- Correlation matrix bug is fixed(cholesky decomposition) + ## 0.8.36.0 (2022-1-18) - Model Calculation Methods(spot, forward in shortrate model) are added - Build Process is Changed to Docker diff --git a/scenario/usage.py b/scenario/usage.py index 318a4d0..0b5ed9a 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -154,10 +154,13 @@ def test(): # random pseudo_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='pseudo', subType='mersennetwister', randomTransformType='boxmullernormal') sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') + arr = np.random.random((1000, 365 * 3)) # timegrid1 + np.save('./external_rsg.npy', arr) + external_rsg = xen.RsgExternal(sampleNum=1000, dimension=365 * 3, filename='./external_rsg.npy') # single model filename1='./single_model.npz' - results1 = xen.generate1d(model=gbm, calcs=None, timegrid=timegrid1, rsg=pseudo_rsg, filename=filename1, isMomentMatching=False) + results1 = xen.generate1d(model=gbm, calcs=None, timegrid=timegrid1, rsg=external_rsg, filename=filename1, isMomentMatching=False) # multiple model filename2='./multiple_model.npz' From a975d450dc3611e845bebcc114ba8eb0dfd1fbcf Mon Sep 17 00:00:00 2001 From: minikie Date: Tue, 8 Nov 2022 20:51:44 +0900 Subject: [PATCH 05/54] 0.8.38.0 --- README.md | 61 ++++++++++++++++++++++++++++++++++----------- pricing/Swaption.py | 2 +- scenario/usage.py | 61 +++++++++++++++++++++++++++++---------------- 3 files changed, 86 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 23b05f9..a5b006c 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ MxDevTool(Beta) : Financial Library ========================== ![image](https://img.shields.io/badge/platform-windows_64|_linux_64-red) -![image](https://img.shields.io/badge/python-3.6|3.7|3.8|3.9-blue) -![image](https://img.shields.io/badge/version-0.8.37.1-green.svg) +![image](https://img.shields.io/badge/python-3.6|3.7|3.8|3.9|3.10|3.11-blue) +![image](https://img.shields.io/badge/version-0.8.38.0-green.svg) ![image](https://img.shields.io/badge/platform-macOS_64-red) -![image](https://img.shields.io/badge/python-3.8|3.9-blue) +![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) MxDevTool is a Integrated Developing Tools for financial analysis. Now is Beta Release version. The Engine is developed by C++ @@ -345,8 +345,25 @@ timeGrid16 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endo ## Random Sequence Generator ```python -pseudo_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=0, skip=0, isMomentMatching=False, randomType='pseudo', subType='mersennetwister', randomTransformType='boxmullernormal') -sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=0, skip=0, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') +pseudo_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='pseudo', subType='mersennetwister', randomTransformType='boxmullernormal') +pseudo_rsg2 = xen.RsgPseudo(sampleNum=1000, dimension=365, randomTransformType='uniform') + +halton_rsg = xen.RsgHalton(sampleNum=1000, dimension=365) +faure_rsg = xen.RsgFaure(sampleNum=1000, dimension=365) + +sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=2048, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') +sobol_rsg2 = xen.RsgSobol(sampleNum=1000, dimension=365, skip=2048) + +latinhs_rsg = xen.RsgLatinHs(pseudo_rsg2) + +arr = np.random.random((1000, 365 * 3)) # timegrid1, rand is not fixed +np.save('./external_rsg.npy', arr) +external_rsg = xen.RsgExternal(sampleNum=1000, dimension=365 * 3, filename='./external_rsg.npy') + +rsg_list = [pseudo_rsg, pseudo_rsg2, halton_rsg, faure_rsg, sobol_rsg, sobol_rsg2, latinhs_rsg, external_rsg] +# for rsg in rsg_list: +# print(rsg.type(), rsg.nextSequence()[0:3], rsg.nextSequence()[0:3]) + ``` ## Scenario Generate @@ -393,32 +410,34 @@ resultsInfo = ( results.genInfo, results.refDate, results.maxDate, results.maxTi ndarray = results.toNumpyArr() # pre load all scenario data to ndarray -t_pos = 1 +t_pos = 264 scenCount = 15 -# scenario path of selected scenCount -# ((100.0, 82.94953421561434, 110.87375162324332, 91.96798678908293, 70.29920544659505, ... ), -# (100.0, 96.98838977927142, 97.0643112022828, 91.19803393176569, 104.94407125936456, ... ), -# ... -# (200.0, 179.93792399488575, 207.93806282552612, 183.16602072084862, ... ), -# (9546.93761943355, 9969.778029330208, 10758.449206155927, 11107.968356394866, ... )) +precalculated_tpos_15_264 = [617.9412514170386, 486.16360609386476, 257.91188339511893, 0.008636462100567083, 0.041740761012241466, 0.017748505256947305, 0.07747329523241774, 0.03993668004547873, 0.00886112680520279, 0.011046137001885281, 0.9707768360048887, 0.9977969060281185, 15.0, 0.0, 1104.1048575109035, -131.77764532317389, 300420.54714306304, 0.7867472918809246, 627.9412514170386, -607.9412514170386, 679.7353765587426, 0.0017801044961434816, 627.9412514170386, 607.9412514170386, 679.7353765587426, 561.7647740154896, 689.7353765587426, 689.7353765587426, 0.010504541340189934, 0.010504541340189934, 0.022027700532335222, 0.022027700532335222, -0.01896096586180629, -0.01896096586180629, -0.031324320207885586, -0.031324320207885586, -0.3859240871544787, -0.3859240871544787, 0.14629049402642338, 0.14629049402642338, 8034.953047634017] + +calculated_tpos_264 = results.tPosSlice(t_pos, scenCount) + +for i, v in enumerate(zip(calculated_tpos_264, precalculated_tpos_15_264)): + diff = v[0] - v[1] + if diff != 0.0: print(results.genInfo[i][1], diff) + multipath = results[scenCount] multipath_arr = ndarray[scenCount] # t_pos data -multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (82.94953421561434, 96.98838977927142, 0.015097688448292656, 0.02390612251701627, ... ) +multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (617.9412514170386, 486.16360609386476, 257.91188339511893, ...) multipath_t_pos_arr = ndarray[scenCount,:,t_pos] multipath_all_t_pos = results.tPosSlice(t_pos=t_pos) # all t_pos data # t_pos data of using date t_date = ref_date + 10 -multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) # (99.5327905069975, 99.91747715856324, 0.015099936660211026, 0.020107033880707947, ... ) +multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) # (398.5270922971255, 418.0131595105432, 424.67199724665664, 0.014226265796137581, ... ) multipath_all_using_date = results.dateSlice(date=t_date) # all t_pos data # t_pos data of using time t_time = 1.32 -multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) # (91.88967340028992, 97.01269656928498, 0.018200574048792405, 0.02436896520516243, ... ) +multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) # (381.7433430556487, 434.9766465395624, 368.76856978958483, -0.0022718317886738877, ... ) multipath_all_using_time = results.timeSlice(time=t_time) # all t_pos data ``` @@ -514,6 +533,7 @@ sb.corr[2][0] = 'kospi2_ni225_corr' sb.addCalc(xen.SpotRate.__name__, 'hw1f_spot3m', ir_pc='hw1f', maturityTenor='3m', compounding=mx.Compounded) sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m', ir_pc='hw1f', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) +sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m_2', ir_pc='hw1f', startTenor=0.5, maturityTenor=0.25, compounding=mx.Compounded) sb.addCalc(xen.DiscountFactor.__name__, 'hw1f_discountFactor', ir_pc='hw1f') sb.addCalc(xen.DiscountBond.__name__, 'hw1f_discountBond3m', ir_pc='hw1f', maturityTenor=mx.Period(3, mx.Months)) @@ -811,6 +831,17 @@ For source code, check this repository. # Release History +## 0.8.38.0 (2022-11-07) +- Rsg classes are redesigned +- Latin Hypercube sampling is added +- Random number consuming method is changed to timeside first +- Model Generation performance is improved +- BondReturn, Libor, SwapRate associated to shortrate(affinemodel) model is added +- Some clone method is added for curve(yield, vol) shock and model copy +- Structectured payoffs for pricing are testing(alpha version) +- Linux aarch64 platform Support is started +- Python 3.10, 3.11 version Support is started + ## 0.8.37.1 (2022-4-10) - ExternalRsg(using numpy file) is added for external random number - Output file contents is updated(correlation, random) - v1.1.0 diff --git a/pricing/Swaption.py b/pricing/Swaption.py index 5de06e9..ddd12f0 100644 --- a/pricing/Swaption.py +++ b/pricing/Swaption.py @@ -58,7 +58,7 @@ def test(): swaption = mx_i.makeSwaption(yieldCurve=yield_curve) print('npv : ', swaption.NPV()) - print('blackvol : ', swaption.impliedVolatility(swaption.NPV() * 0.9 )) + print('blackvol : ', swaption.impliedVolatility(swaption.NPV() * 0.9, yield_curve, 0.3)) if __name__ == "__main__": test() \ No newline at end of file diff --git a/scenario/usage.py b/scenario/usage.py index 0b5ed9a..b21d16c 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -1,5 +1,4 @@ -from mxdevtool.xenarix.core import Scenario -import os +import os, time import numpy as np import mxdevtool as mx import mxdevtool.shock as mx_s @@ -12,6 +11,7 @@ import mxdevtool.instruments.outputs as mx_io import mxdevtool.utils as utils + def test(): ref_date = mx.Date.todaysDate() null_calendar = mx.NullCalendar() @@ -78,12 +78,13 @@ def test(): # calcs in models hw1f_spot3m = hw1f.spot('hw1f_spot3m', maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) - hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) + # hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) + hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startTenor=0.5, maturityTenor=3.0, compounding=mx.Compounded) hw1f_discountFactor = hw1f.discountFactor('hw1f_discountFactor') hw1f_discountBond3m = hw1f.discountBond('hw1f_discountBond3m', maturityTenor=mx.Period(3, mx.Months)) # model calculation - r_t = 0.02 # short rate + r_t = 0.02 hw1f.model_discountBond(0.0, 1.0, r_t) hw1f.model_spot(1.0, 2.0, r_t) # continuous compounding hw1f.model_forward(1.0, 2.0, 3.0, r_t) # continuous compounding @@ -153,11 +154,24 @@ def test(): # random pseudo_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='pseudo', subType='mersennetwister', randomTransformType='boxmullernormal') - sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') - arr = np.random.random((1000, 365 * 3)) # timegrid1 + pseudo_rsg2 = xen.RsgPseudo(sampleNum=1000, dimension=365, randomTransformType='uniform') + + halton_rsg = xen.RsgHalton(sampleNum=1000, dimension=365) + faure_rsg = xen.RsgFaure(sampleNum=1000, dimension=365) + + sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=2048, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') + sobol_rsg2 = xen.RsgSobol(sampleNum=1000, dimension=365, skip=2048) + + latinhs_rsg = xen.RsgLatinHs(pseudo_rsg2) + + arr = np.random.random((1000, 365 * 3)) # timegrid1, rand is not fixed np.save('./external_rsg.npy', arr) external_rsg = xen.RsgExternal(sampleNum=1000, dimension=365 * 3, filename='./external_rsg.npy') + rsg_list = [pseudo_rsg, pseudo_rsg2, halton_rsg, faure_rsg, sobol_rsg, sobol_rsg2, latinhs_rsg, external_rsg] + # for rsg in rsg_list: + # print(rsg.type(), rsg.nextSequence()[0:3], rsg.nextSequence()[0:3]) + # single model filename1='./single_model.npz' results1 = xen.generate1d(model=gbm, calcs=None, timegrid=timegrid1, rsg=external_rsg, filename=filename1, isMomentMatching=False) @@ -172,7 +186,7 @@ def test(): # multiple model with calc filename3='./multiple_model_with_calc.npz' calcs = [oper1, oper3, linearOper1, linearOper2, shiftLeft2, returns1, fixedRateBond, hw1f_spot3m] - results3 = xen.generate(models=models, calcs=calcs, corr=corrMatrix, timegrid=timegrid4, rsg=sobol_rsg, filename=filename3, isMomentMatching=False) + results3 = xen.generate(models=models, calcs=calcs, corr=corrMatrix, timegrid=timegrid4, rsg=pseudo_rsg, filename=filename3, isMomentMatching=False) all_models = [ gbmconst, gbm, heston, hw1f, bk1f, cir1f, vasicek1f, g2ext ] all_calcs = [ hw1f_spot3m, hw1f_forward6m3m, hw1f_discountFactor, hw1f_discountBond3m, @@ -189,39 +203,43 @@ def test(): results4 = xen.generate(models=all_models, calcs=all_calcs, corr=corrMatrix2, timegrid=timegrid4, rsg=sobol_rsg, filename=filename4, isMomentMatching=False) # results - results = results3 + results = results4 resultsInfo = (results.genInfo, results.refDate, results.maxDate, results.maxTime, results.randomMomentMatch, results.randomSubtype, results.randomType, results.seed, results.shape ) ndarray = results.toNumpyArr() # pre load all scenario data to ndarray - t_pos = 1 + t_pos = 264 scenCount = 15 - # scenario path of selected scenCount - # ((100.0, 82.94953421561434, 110.87375162324332, 91.96798678908293, 70.29920544659505, ... ), - # (100.0, 96.98838977927142, 97.0643112022828, 91.19803393176569, 104.94407125936456, ... ), - # ... - # (200.0, 179.93792399488575, 207.93806282552612, 183.16602072084862, ... ), - # (9546.93761943355, 9969.778029330208, 10758.449206155927, 11107.968356394866, ... )) + precalculated_tpos_15_264 = [617.9412514170386, 486.16360609386476, 257.91188339511893, 0.008636462100567083, 0.041740761012241466, 0.017748505256947305, 0.07747329523241774, 0.03993668004547873, 0.00886112680520279, 0.011046137001885281, 0.9707768360048887, 0.9977969060281185, 15.0, 0.0, + 1104.1048575109035, -131.77764532317389, 300420.54714306304, 0.7867472918809246, 627.9412514170386, -607.9412514170386, 679.7353765587426, 0.0017801044961434816, 627.9412514170386, 607.9412514170386, 679.7353765587426, 561.7647740154896, 689.7353765587426, 689.7353765587426, 0.010504541340189934, + 0.010504541340189934, 0.022027700532335222, 0.022027700532335222, -0.01896096586180629, -0.01896096586180629, -0.031324320207885586, -0.031324320207885586, -0.3859240871544787, -0.3859240871544787, 0.14629049402642338, 0.14629049402642338, 8034.953047634017] + + calculated_tpos_264 = results.tPosSlice(t_pos, scenCount) + + for i, v in enumerate(zip(calculated_tpos_264, precalculated_tpos_15_264)): + diff = v[0] - v[1] + if diff != 0.0: print(results.genInfo[i][1], diff) + multipath = results[scenCount] multipath_arr = ndarray[scenCount] # t_pos data - multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (82.94953421561434, 96.98838977927142, 0.015097688448292656, 0.02390612251701627, ... ) + multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (617.9412514170386, 486.16360609386476, 257.91188339511893, ...) multipath_t_pos_arr = ndarray[scenCount,:,t_pos] multipath_all_t_pos = results.tPosSlice(t_pos=t_pos) # all t_pos data # t_pos data of using date t_date = ref_date + 10 - multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) # (99.5327905069975, 99.91747715856324, 0.015099936660211026, 0.020107033880707947, ... ) + multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) # (398.5270922971255, 418.0131595105432, 424.67199724665664, 0.014226265796137581, ... ) multipath_all_using_date = results.dateSlice(date=t_date) # all t_pos data # t_pos data of using time t_time = 1.32 - multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) # (91.88967340028992, 97.01269656928498, 0.018200574048792405, 0.02436896520516243, ... ) + multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) # (381.7433430556487, 434.9766465395624, 368.76856978958483, -0.0022718317886738877, ... ) multipath_all_using_time = results.timeSlice(time=t_time) # all t_pos data # analyticPath and test calculation @@ -298,6 +316,7 @@ def test(): sb.addCalc(xen.SpotRate.__name__, 'hw1f_spot3m', ir_pc='hw1f', maturityTenor='3m', compounding=mx.Compounded) sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m', ir_pc='hw1f', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) + sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m_2', ir_pc='hw1f', startTenor=0.5, maturityTenor=0.25, compounding=mx.Compounded) sb.addCalc(xen.DiscountFactor.__name__, 'hw1f_discountFactor', ir_pc='hw1f') sb.addCalc(xen.DiscountBond.__name__, 'hw1f_discountBond3m', ir_pc='hw1f', maturityTenor=mx.Period(3, mx.Months)) @@ -479,10 +498,6 @@ def test(): xm.save_xen(name, scen) res = scen.generate_clone(filename=name) - # bloomberg provider(blpapi) checking to request sample if available - try: mx_dp.check_bloomberg() - except: print('fail to check bloomberg') - # instruments pricing # this is built-in instruments @@ -548,4 +563,6 @@ def userfunc_test(self, scen_data_d, calc_kwargs): if __name__ == "__main__": + start = time.time() test() + print("time : {0}".format(time.time() - start)) \ No newline at end of file From 3f66d48d096cee58a3cd85a8546572769568e232 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Wed, 9 Nov 2022 02:06:21 +0900 Subject: [PATCH 06/54] 0.8.38.0 --- .gitignore | 3 ++- scenario/usage.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index bcfe6a0..48c976e 100644 --- a/.gitignore +++ b/.gitignore @@ -134,8 +134,9 @@ dmypy.json # csv results *.csv -# npz results +# numpy results *.npz +*.npy # vscode ./.vscode diff --git a/scenario/usage.py b/scenario/usage.py index b21d16c..7231de6 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -213,9 +213,8 @@ def test(): t_pos = 264 scenCount = 15 - precalculated_tpos_15_264 = [617.9412514170386, 486.16360609386476, 257.91188339511893, 0.008636462100567083, 0.041740761012241466, 0.017748505256947305, 0.07747329523241774, 0.03993668004547873, 0.00886112680520279, 0.011046137001885281, 0.9707768360048887, 0.9977969060281185, 15.0, 0.0, - 1104.1048575109035, -131.77764532317389, 300420.54714306304, 0.7867472918809246, 627.9412514170386, -607.9412514170386, 679.7353765587426, 0.0017801044961434816, 627.9412514170386, 607.9412514170386, 679.7353765587426, 561.7647740154896, 689.7353765587426, 689.7353765587426, 0.010504541340189934, - 0.010504541340189934, 0.022027700532335222, 0.022027700532335222, -0.01896096586180629, -0.01896096586180629, -0.031324320207885586, -0.031324320207885586, -0.3859240871544787, -0.3859240871544787, 0.14629049402642338, 0.14629049402642338, 8034.953047634017] + # output on window platform + precalculated_tpos_15_264 = [617.9365212878333, 486.1307570137023, 257.89830088440857, 0.008637497400253935, 0.04174075631684831, 0.017748044660834593, 0.07747048265598008, 0.03993634981174743, 0.008862151937972573, 0.011046919002466904, 0.9707722172501217, 0.9977966525557552, 15.0, 0.0, 1104.0672783015357, -131.80576427413104, 300397.9488800682, 0.7867001548970817, 627.9365212878333, -607.9365212878333, 679.7301734166167, 0.0017801181223396614, 627.9365212878333, 607.9365212878333, 679.7301734166167, 561.7604738980302, 689.7301734166167, 689.7301734166167, 0.01050559163878392, 0.01050559163878392, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678901] calculated_tpos_264 = results.tPosSlice(t_pos, scenCount) From fb2433e8fcf33fe29b5277c10f10617722a20ec2 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Wed, 9 Nov 2022 13:42:51 +0900 Subject: [PATCH 07/54] 0.8.38.0 --- .gitignore | 2 ++ README.md | 7 ++++--- scenario/usage.py | 17 ++++++++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 48c976e..dff21f8 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,5 @@ settings.json # repository xenrepo/ + +clear_output.bat \ No newline at end of file diff --git a/README.md b/README.md index a5b006c..6f75bbe 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ MxDevTool(Beta) : Financial Library ![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) MxDevTool is a Integrated Developing Tools for financial analysis. -Now is Beta Release version. The Engine is developed by C++ -and based on QuantLib. +Now is Beta Release version. The Engine is developed by C++. Xenarix(Economic Scenario Generator) is moved into submodule of MxDevTool. @@ -126,7 +125,7 @@ if __name__ == "__main__": Import MxDevTool Library : ```python -import os +import os, time, platform import numpy as np import mxdevtool as mx import mxdevtool.shock as mx_s @@ -135,6 +134,8 @@ import mxdevtool.termstructures as ts import mxdevtool.quotes as mx_q import mxdevtool.data.providers as mx_dp import mxdevtool.data.repositories as mx_dr +import mxdevtool.instruments as mx_i +import mxdevtool.instruments.outputs as mx_io import mxdevtool.utils as utils ``` diff --git a/scenario/usage.py b/scenario/usage.py index 7231de6..0a6ea9b 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -1,4 +1,4 @@ -import os, time +import os, time, platform import numpy as np import mxdevtool as mx import mxdevtool.shock as mx_s @@ -12,6 +12,8 @@ import mxdevtool.utils as utils +enviroment = '{0}-{1}'.format(platform.system(), platform.machine()) + def test(): ref_date = mx.Date.todaysDate() null_calendar = mx.NullCalendar() @@ -213,8 +215,16 @@ def test(): t_pos = 264 scenCount = 15 - # output on window platform - precalculated_tpos_15_264 = [617.9365212878333, 486.1307570137023, 257.89830088440857, 0.008637497400253935, 0.04174075631684831, 0.017748044660834593, 0.07747048265598008, 0.03993634981174743, 0.008862151937972573, 0.011046919002466904, 0.9707722172501217, 0.9977966525557552, 15.0, 0.0, 1104.0672783015357, -131.80576427413104, 300397.9488800682, 0.7867001548970817, 627.9365212878333, -607.9365212878333, 679.7301734166167, 0.0017801181223396614, 627.9365212878333, 607.9365212878333, 679.7301734166167, 561.7604738980302, 689.7301734166167, 689.7301734166167, 0.01050559163878392, 0.01050559163878392, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678901] + precalculated_tpos_15_264 = [] + + if enviroment == 'Windows-AMD64': + precalculated_tpos_15_264 = [617.9365212878333, 486.1307570137023, 257.89830088440857, 0.008637497400253935, 0.04174075631684831, 0.017748044660834593, 0.07747048265598008, 0.03993634981174743, 0.008862151937972573, 0.011046919002466904, 0.9707722172501217, 0.9977966525557552, 15.0, 0.0, 1104.0672783015357, -131.80576427413104, 300397.9488800682, 0.7867001548970817, 627.9365212878333, -607.9365212878333, 679.7301734166167, 0.0017801181223396614, 627.9365212878333, 607.9365212878333, 679.7301734166167, 561.7604738980302, 689.7301734166167, 689.7301734166167, 0.01050559163878392, 0.01050559163878392, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678901] + elif enviroment == 'Linux-x86_64': + precalculated_tpos_15_264 = [617.9357218168522, 486.1381428609564, 257.9051759086099, 0.008637296682977602, 0.041740750179767666, 0.017748170111667804, 0.0774710888641746, 0.03993729011265557, 0.008861953191711569, 0.011046767393152468, 0.9707731058865956, 0.9977967016973495, 15.0, 0.0, 1104.0738646778086, -131.79757895589574, 300402.1242114891, 0.7867131251639167, 627.9357218168522, -607.9357218168522, 679.7292939985374, 0.001780120425415421, 627.9357218168522, 607.9357218168522, 679.7292939985374, 561.7597471062292, 689.7292939985374, 689.7292939985374, 0.010505388013615481, 0.010505388013615481, 0.022026453783620698, 0.022026453783620698, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38594005736045256, -0.38594005736045256, 0.14623811680311463, 0.14623811680311463, 8035.006232005751] + elif enviroment == 'Linux-aarch64': + precalculated_tpos_15_264 = [617.9357218168448, 486.13814286095663, 257.90517590861185, 0.008637296682977559, 0.04174075017756056, 0.017748170111667617, 0.0774710888641739, 0.03993729011265639, 0.008861953191711569, 0.011046767393152468, 0.9707731058865953, 0.9977967016973495, 15.0, 0.0, 1104.0738646778013, -131.79757895588813, 300402.1242114856, 0.7867131251639264, 627.9357218168448, -607.9357218168448, 679.7292939985293, 0.0017801204254154423, 627.9357218168448, 607.9357218168448, 679.7292939985293, 561.7597471062224, 689.7292939985293, 689.7292939985293, 0.010505388013615441, 0.010505388013615441, 0.022026453783620507, 0.022026453783620507, -0.018960965861806955, -0.018960965861806955, -0.0313243202078865, -0.0313243202078865, -0.385940057360448, -0.385940057360448, 0.14623811680311521, 0.14623811680311521, 8035.006232005768] + elif enviroment == 'Darwin-arm64': + precalculated_tpos_15_264 = [617.9365212878333, 486.1307570137023, 257.89830088440857, 0.008637497400253935, 0.04174075631684831, 0.017748044660834593, 0.07747048265598008, 0.03993634981174743, 0.008862151937972573, 0.011046919002466904, 0.9707722172501217, 0.9977966525557552, 15.0, 0.0, 1104.0672783015357, -131.80576427413104, 300397.9488800682, 0.7867001548970817, 627.9365212878333, -607.9365212878333, 679.7301734166167, 0.0017801181223396614, 627.9365212878333, 607.9365212878333, 679.7301734166167, 561.7604738980302, 689.7301734166167, 689.7301734166167, 0.01050559163878392, 0.01050559163878392, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678901] calculated_tpos_264 = results.tPosSlice(t_pos, scenCount) @@ -562,6 +572,7 @@ def userfunc_test(self, scen_data_d, calc_kwargs): if __name__ == "__main__": + print(enviroment) start = time.time() test() print("time : {0}".format(time.time() - start)) \ No newline at end of file From a8c773a8365239bcd20ab96177e4d423532ce875 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Wed, 9 Nov 2022 13:58:45 +0900 Subject: [PATCH 08/54] 0.8.38.0 --- scenario/usage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenario/usage.py b/scenario/usage.py index 0a6ea9b..2252989 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -224,7 +224,7 @@ def test(): elif enviroment == 'Linux-aarch64': precalculated_tpos_15_264 = [617.9357218168448, 486.13814286095663, 257.90517590861185, 0.008637296682977559, 0.04174075017756056, 0.017748170111667617, 0.0774710888641739, 0.03993729011265639, 0.008861953191711569, 0.011046767393152468, 0.9707731058865953, 0.9977967016973495, 15.0, 0.0, 1104.0738646778013, -131.79757895588813, 300402.1242114856, 0.7867131251639264, 627.9357218168448, -607.9357218168448, 679.7292939985293, 0.0017801204254154423, 627.9357218168448, 607.9357218168448, 679.7292939985293, 561.7597471062224, 689.7292939985293, 689.7292939985293, 0.010505388013615441, 0.010505388013615441, 0.022026453783620507, 0.022026453783620507, -0.018960965861806955, -0.018960965861806955, -0.0313243202078865, -0.0313243202078865, -0.385940057360448, -0.385940057360448, 0.14623811680311521, 0.14623811680311521, 8035.006232005768] elif enviroment == 'Darwin-arm64': - precalculated_tpos_15_264 = [617.9365212878333, 486.1307570137023, 257.89830088440857, 0.008637497400253935, 0.04174075631684831, 0.017748044660834593, 0.07747048265598008, 0.03993634981174743, 0.008862151937972573, 0.011046919002466904, 0.9707722172501217, 0.9977966525557552, 15.0, 0.0, 1104.0672783015357, -131.80576427413104, 300397.9488800682, 0.7867001548970817, 627.9365212878333, -607.9365212878333, 679.7301734166167, 0.0017801181223396614, 627.9365212878333, 607.9365212878333, 679.7301734166167, 561.7604738980302, 689.7301734166167, 689.7301734166167, 0.01050559163878392, 0.01050559163878392, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678901] + precalculated_tpos_15_264 = [617.9365212878336, 486.1307570137023, 257.89830088440857, 0.008637497400253921, 0.04174075631357103, 0.017748044660834593, 0.07747048265598006, 0.03993634981174748, 0.008862151937972573, 0.011046919002466904, 0.9707722172500804, 0.9977966525557552, 15.0, 0.0, 1104.067278301536, -131.80576427413126, 300397.9488800683, 0.7867001548970814, 627.9365212878336, -607.9365212878336, 679.7301734166169, 0.0017801181223396608, 627.9365212878336, 607.9365212878336, 679.7301734166169, 561.7604738980305, 689.7301734166169, 689.7301734166169, 0.010505591638783906, 0.010505591638783906, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678902] calculated_tpos_264 = results.tPosSlice(t_pos, scenCount) From cb3a99e372e78f1aa05f88c9604909de2347c380 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Fri, 11 Nov 2022 11:17:10 +0900 Subject: [PATCH 09/54] 0.8.38.0 --- scenario/usage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenario/usage.py b/scenario/usage.py index 2252989..7bd599b 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -218,7 +218,7 @@ def test(): precalculated_tpos_15_264 = [] if enviroment == 'Windows-AMD64': - precalculated_tpos_15_264 = [617.9365212878333, 486.1307570137023, 257.89830088440857, 0.008637497400253935, 0.04174075631684831, 0.017748044660834593, 0.07747048265598008, 0.03993634981174743, 0.008862151937972573, 0.011046919002466904, 0.9707722172501217, 0.9977966525557552, 15.0, 0.0, 1104.0672783015357, -131.80576427413104, 300397.9488800682, 0.7867001548970817, 627.9365212878333, -607.9365212878333, 679.7301734166167, 0.0017801181223396614, 627.9365212878333, 607.9365212878333, 679.7301734166167, 561.7604738980302, 689.7301734166167, 689.7301734166167, 0.01050559163878392, 0.01050559163878392, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678901] + precalculated_tpos_15_264 = [617.9345915126742, 486.1147337574154, 257.87250276969263, 0.008637780797736926, 0.04174077708194368, 0.017747870441934384, 0.07746864014263777, 0.03993465765980259, 0.008862432552598776, 0.011047133063292636, 0.9707708566387044, 0.9977965831715768, 15.0, 0.0, 1104.0493252700896, -131.81985775525874, 300387.1094326809, 0.786676681374043, 627.9345915126742, -607.9345915126742, 679.7280506639416, 0.0017801236815489694, 627.9345915126742, 607.9345915126742, 679.7280506639416, 561.7587195569764, 689.7280506639416, 689.7280506639416, 0.010505879141988755, 0.010505879141988755, 0.022025245541777094, 0.022025245541777094, -0.01896096586180629, -0.01896096586180629, -0.0313243202078857, -0.0313243202078857, -0.3860178505483509, -0.3860178505483509, 0.14618996245128205, 0.14618996245128205, 8035.065258700987] elif enviroment == 'Linux-x86_64': precalculated_tpos_15_264 = [617.9357218168522, 486.1381428609564, 257.9051759086099, 0.008637296682977602, 0.041740750179767666, 0.017748170111667804, 0.0774710888641746, 0.03993729011265557, 0.008861953191711569, 0.011046767393152468, 0.9707731058865956, 0.9977967016973495, 15.0, 0.0, 1104.0738646778086, -131.79757895589574, 300402.1242114891, 0.7867131251639167, 627.9357218168522, -607.9357218168522, 679.7292939985374, 0.001780120425415421, 627.9357218168522, 607.9357218168522, 679.7292939985374, 561.7597471062292, 689.7292939985374, 689.7292939985374, 0.010505388013615481, 0.010505388013615481, 0.022026453783620698, 0.022026453783620698, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38594005736045256, -0.38594005736045256, 0.14623811680311463, 0.14623811680311463, 8035.006232005751] elif enviroment == 'Linux-aarch64': From fdaa5ca88fe3e28ebf4d54a068ffbf5ef9e0d390 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Sat, 19 Nov 2022 20:18:24 +0900 Subject: [PATCH 10/54] 0.8.38.0 --- .gitignore | 3 ++- scenario/usage.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index dff21f8..4d0d36a 100644 --- a/.gitignore +++ b/.gitignore @@ -145,4 +145,5 @@ settings.json # repository xenrepo/ -clear_output.bat \ No newline at end of file +clear_output.bat +*.html \ No newline at end of file diff --git a/scenario/usage.py b/scenario/usage.py index 7bd599b..3a97c9f 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -218,7 +218,7 @@ def test(): precalculated_tpos_15_264 = [] if enviroment == 'Windows-AMD64': - precalculated_tpos_15_264 = [617.9345915126742, 486.1147337574154, 257.87250276969263, 0.008637780797736926, 0.04174077708194368, 0.017747870441934384, 0.07746864014263777, 0.03993465765980259, 0.008862432552598776, 0.011047133063292636, 0.9707708566387044, 0.9977965831715768, 15.0, 0.0, 1104.0493252700896, -131.81985775525874, 300387.1094326809, 0.786676681374043, 627.9345915126742, -607.9345915126742, 679.7280506639416, 0.0017801236815489694, 627.9345915126742, 607.9345915126742, 679.7280506639416, 561.7587195569764, 689.7280506639416, 689.7280506639416, 0.010505879141988755, 0.010505879141988755, 0.022025245541777094, 0.022025245541777094, -0.01896096586180629, -0.01896096586180629, -0.0313243202078857, -0.0313243202078857, -0.3860178505483509, -0.3860178505483509, 0.14618996245128205, 0.14618996245128205, 8035.065258700987] + precalculated_tpos_15_264 = [617.9524151122586, 486.1324942886278, 257.86113224359457, 0.00863805143945109, 0.04174084373279414, 0.01774797817939525, 0.07746550423520306, 0.03993400910000309, 0.008862700536786061, 0.011047337489246356, 0.9707690335980336, 0.9977965169103966, 15.0, 0.0, 1104.0849094008863, -131.81992082363075, 300406.7489102038, 0.7866827321976174, 627.9524151122586, -607.9524151122586, 679.7476566234844, 0.001780072337447167, 627.9524151122586, 607.9524151122586, 679.7476566234844, 561.7749228293259, 689.7476566234844, 689.7476566234844, 0.010506153704625517, 0.010506153704625517, 0.02202363630292186, 0.02202363630292186, -0.01896096586180629, -0.01896096586180629, -0.03132432020788547, -0.03132432020788547, -0.38604492322953676, -0.38604492322953676, 0.1462264974615181, 0.1462264974615181, 8035.14085071921] elif enviroment == 'Linux-x86_64': precalculated_tpos_15_264 = [617.9357218168522, 486.1381428609564, 257.9051759086099, 0.008637296682977602, 0.041740750179767666, 0.017748170111667804, 0.0774710888641746, 0.03993729011265557, 0.008861953191711569, 0.011046767393152468, 0.9707731058865956, 0.9977967016973495, 15.0, 0.0, 1104.0738646778086, -131.79757895589574, 300402.1242114891, 0.7867131251639167, 627.9357218168522, -607.9357218168522, 679.7292939985374, 0.001780120425415421, 627.9357218168522, 607.9357218168522, 679.7292939985374, 561.7597471062292, 689.7292939985374, 689.7292939985374, 0.010505388013615481, 0.010505388013615481, 0.022026453783620698, 0.022026453783620698, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38594005736045256, -0.38594005736045256, 0.14623811680311463, 0.14623811680311463, 8035.006232005751] elif enviroment == 'Linux-aarch64': @@ -236,7 +236,7 @@ def test(): multipath_arr = ndarray[scenCount] # t_pos data - multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (617.9412514170386, 486.16360609386476, 257.91188339511893, ...) + multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (617.9524151122586, 486.1324942886278, 257.86113224359457, ...) multipath_t_pos_arr = ndarray[scenCount,:,t_pos] multipath_all_t_pos = results.tPosSlice(t_pos=t_pos) # all t_pos data From 3e5675fc2a78908d846f374c1b686e347c3674f7 Mon Sep 17 00:00:00 2001 From: minikie Date: Mon, 16 Jan 2023 23:24:21 +0900 Subject: [PATCH 11/54] test... --- .github/workflows/github-actions-demo.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/github-actions-demo.yml diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml new file mode 100644 index 0000000..8a9c1ff --- /dev/null +++ b/.github/workflows/github-actions-demo.yml @@ -0,0 +1,18 @@ +name: GitHub Actions Demo +run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 +on: [push] +jobs: + Explore-GitHub-Actions: + runs-on: ubuntu-latest + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v3 + - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ github.workspace }} + - run: echo "🍏 This job's status is ${{ job.status }}." \ No newline at end of file From ca4ab28b67812d7ede29063abe34e353b0e6ed6a Mon Sep 17 00:00:00 2001 From: minikie Date: Tue, 24 Jan 2023 22:35:19 +0900 Subject: [PATCH 12/54] working... --- quantlib/README.md | 20 + quantlib/american-option.py | 146 + quantlib/basket-option.py | 113 + quantlib/bermudan-swaption.py | 236 ++ quantlib/bonds.py | 319 ++ quantlib/cashflows.py | 158 + quantlib/cds.py | 112 + quantlib/european-option.py | 193 + quantlib/gaussian1d-models.py | 489 +++ quantlib/global-bootstrap.py | 168 + quantlib/isda-engine.py | 178 + quantlib/slv.py | 131 + quantlib/swap.py | 353 ++ quantlib/swing.py | 86 + quantlib/test/QuantLibTestSuite.py | 117 + quantlib/test/americanquantooption.py | 103 + quantlib/test/assetswap.py | 5357 +++++++++++++++++++++++++ quantlib/test/blackformula.py | 141 + quantlib/test/bonds.py | 364 ++ quantlib/test/capfloor.py | 119 + quantlib/test/cms.py | 308 ++ quantlib/test/coupons.py | 477 +++ quantlib/test/currencies.py | 48 + quantlib/test/date.py | 77 + quantlib/test/daycounters.py | 27 + quantlib/test/extrapolation.py | 46 + quantlib/test/fdm.py | 651 +++ quantlib/test/iborindex.py | 76 + quantlib/test/inflation.py | 417 ++ quantlib/test/instruments.py | 68 + quantlib/test/integrals.py | 71 + quantlib/test/marketelements.py | 63 + quantlib/test/ode.py | 48 + quantlib/test/options.py | 112 + quantlib/test/ratehelpers.py | 745 ++++ quantlib/test/sabr.py | 122 + quantlib/test/slv.py | 156 + quantlib/test/solvers1d.py | 93 + quantlib/test/swap.py | 142 + quantlib/test/swaption.py | 197 + quantlib/test/termstructures.py | 325 ++ quantlib/test/volatilities.py | 631 +++ 42 files changed, 13803 insertions(+) create mode 100644 quantlib/README.md create mode 100644 quantlib/american-option.py create mode 100644 quantlib/basket-option.py create mode 100644 quantlib/bermudan-swaption.py create mode 100644 quantlib/bonds.py create mode 100644 quantlib/cashflows.py create mode 100644 quantlib/cds.py create mode 100644 quantlib/european-option.py create mode 100644 quantlib/gaussian1d-models.py create mode 100644 quantlib/global-bootstrap.py create mode 100644 quantlib/isda-engine.py create mode 100644 quantlib/slv.py create mode 100644 quantlib/swap.py create mode 100644 quantlib/swing.py create mode 100644 quantlib/test/QuantLibTestSuite.py create mode 100644 quantlib/test/americanquantooption.py create mode 100644 quantlib/test/assetswap.py create mode 100644 quantlib/test/blackformula.py create mode 100644 quantlib/test/bonds.py create mode 100644 quantlib/test/capfloor.py create mode 100644 quantlib/test/cms.py create mode 100644 quantlib/test/coupons.py create mode 100644 quantlib/test/currencies.py create mode 100644 quantlib/test/date.py create mode 100644 quantlib/test/daycounters.py create mode 100644 quantlib/test/extrapolation.py create mode 100644 quantlib/test/fdm.py create mode 100644 quantlib/test/iborindex.py create mode 100644 quantlib/test/inflation.py create mode 100644 quantlib/test/instruments.py create mode 100644 quantlib/test/integrals.py create mode 100644 quantlib/test/marketelements.py create mode 100644 quantlib/test/ode.py create mode 100644 quantlib/test/options.py create mode 100644 quantlib/test/ratehelpers.py create mode 100644 quantlib/test/sabr.py create mode 100644 quantlib/test/slv.py create mode 100644 quantlib/test/solvers1d.py create mode 100644 quantlib/test/swap.py create mode 100644 quantlib/test/swaption.py create mode 100644 quantlib/test/termstructures.py create mode 100644 quantlib/test/volatilities.py diff --git a/quantlib/README.md b/quantlib/README.md new file mode 100644 index 0000000..e57e92b --- /dev/null +++ b/quantlib/README.md @@ -0,0 +1,20 @@ +# QuantLib Python examples + +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/lballabio/QuantLib-SWIG/binder?urlpath=tree/Python/examples) + +This directory contains a number of examples of using QuantLib from +Python. They can also run as Jupyter notebooks by means of +[Jupytext](https://jupytext.readthedocs.io/). + +You can try them online thanks to [Binder](https://mybinder.org/). +If you're seeing this file as a notebook, you're probably there already. +If not, you can click the "Launch Binder" badge at the top of this file. + +If you want to run these examples locally, you'll need the modules listed in the +`requirements.txt` file at the root of the [QuantLib-SWIG +repository](https://github.com/lballabio/QuantLib-SWIG); to install +them, you can execute + + pip install -r requirements.txt + +from that directory. diff --git a/quantlib/american-option.py b/quantlib/american-option.py new file mode 100644 index 0000000..225a9be --- /dev/null +++ b/quantlib/american-option.py @@ -0,0 +1,146 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# # American options +# +# Copyright (©) 2004, 2005, 2006, 2007 StatPro Italia srl +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% +import mxdevtool as ql +import pandas as pd + +# %% [markdown] +# ### Global parameters + +# %% +todaysDate = ql.Date(15, ql.May, 1998) +ql.Settings.instance().evaluationDate = todaysDate + +# %% +interactive = "get_ipython" in globals() + +# %% [markdown] +# ### Option construction + +# %% +exercise = ql.AmericanExercise(todaysDate, ql.Date(17, ql.May, 1999)) +payoff = ql.PlainVanillaPayoff(ql.Option.Put, 40.0) + +# %% +option = ql.VanillaOption(payoff, exercise) + +# %% [markdown] +# ### Market data + +# %% +underlying = ql.SimpleQuote(36.0) +dividendYield = ql.FlatForward(todaysDate, 0.00, ql.Actual365Fixed()) +volatility = ql.BlackConstantVol(todaysDate, ql.TARGET(), 0.20, ql.Actual365Fixed()) +riskFreeRate = ql.FlatForward(todaysDate, 0.06, ql.Actual365Fixed()) + +# %% +process = ql.BlackScholesMertonProcess( + ql.QuoteHandle(underlying), + ql.YieldTermStructureHandle(dividendYield), + ql.YieldTermStructureHandle(riskFreeRate), + ql.BlackVolTermStructureHandle(volatility), +) + +# %% [markdown] +# ### Pricing +# +# We'll collect tuples of method name, option value, and estimated error from the analytic formula. + +# %% +results = [] + +# %% [markdown] +# #### Analytic approximations + +# %% +option.setPricingEngine(ql.BaroneAdesiWhaleyApproximationEngine(process)) +results.append(("Barone-Adesi-Whaley", option.NPV())) + +# %% +option.setPricingEngine(ql.BjerksundStenslandApproximationEngine(process)) +results.append(("Bjerksund-Stensland", option.NPV())) + +# %% [markdown] +# #### Finite-difference method + +# %% +timeSteps = 801 +gridPoints = 800 + +# %% +option.setPricingEngine(ql.FdBlackScholesVanillaEngine(process, timeSteps, gridPoints)) +results.append(("finite differences", option.NPV())) + + +# %% [markdown] +# #### Li, M. QD+ American engine + +# %% +option.setPricingEngine(ql.QdPlusAmericanEngine(process)) +results.append(("QD+", option.NPV())) + + +# %% [markdown] +# #### Leif Andersen, Mark Lake and Dimitri Offengenden high performance American engine + +# %% +option.setPricingEngine(ql.QdFpAmericanEngine( + process, ql.QdFpAmericanEngine.accurateScheme())) +results.append(("QD+ fixed point", option.NPV())) + + +# %% [markdown] +# #### Binomial method + +# %% +timeSteps = 801 + +# %% +for tree in ["JR", "CRR", "EQP", "Trigeorgis", "Tian", "LR", "Joshi4"]: + option.setPricingEngine(ql.BinomialVanillaEngine(process, tree, timeSteps)) + results.append(("Binomial (%s)" % tree, option.NPV())) + +# %% [markdown] +# ### Results + +# %% +df = pd.DataFrame(results, columns=["Method", "Option value"]) +df.style.hide_index() + +# %% [markdown] +# The following displays the results when this is run as a Python script (in which case the cell above is not displayed). + +# %% +if not interactive: + print(df) diff --git a/quantlib/basket-option.py b/quantlib/basket-option.py new file mode 100644 index 0000000..362a4a8 --- /dev/null +++ b/quantlib/basket-option.py @@ -0,0 +1,113 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # Basket options +# +# Copyright (©) 2004, 2005, 2006 StatPro Italia srl +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +import mxdevtool as ql + +# ### Global data + +todaysDate = ql.Date(15, ql.May, 1998) +ql.Settings.instance().evaluationDate = todaysDate +settlementDate = ql.Date(17, ql.May, 1998) +riskFreeRate = ql.FlatForward(settlementDate, 0.05, ql.Actual365Fixed()) + +# ### Option parameters + +exercise = ql.EuropeanExercise(ql.Date(17, ql.May, 1999)) +payoff = ql.PlainVanillaPayoff(ql.Option.Call, 8.0) + +# ### Market data + + +underlying1 = ql.SimpleQuote(7.0) +volatility1 = ql.BlackConstantVol(todaysDate, ql.TARGET(), 0.10, ql.Actual365Fixed()) +dividendYield1 = ql.FlatForward(settlementDate, 0.05, ql.Actual365Fixed()) +underlying2 = ql.SimpleQuote(7.0) +volatility2 = ql.BlackConstantVol(todaysDate, ql.TARGET(), 0.10, ql.Actual365Fixed()) +dividendYield2 = ql.FlatForward(settlementDate, 0.05, ql.Actual365Fixed()) + + +process1 = ql.BlackScholesMertonProcess( + ql.QuoteHandle(underlying1), + ql.YieldTermStructureHandle(dividendYield1), + ql.YieldTermStructureHandle(riskFreeRate), + ql.BlackVolTermStructureHandle(volatility1), +) + +process2 = ql.BlackScholesMertonProcess( + ql.QuoteHandle(underlying2), + ql.YieldTermStructureHandle(dividendYield2), + ql.YieldTermStructureHandle(riskFreeRate), + ql.BlackVolTermStructureHandle(volatility2), +) + +matrix = ql.Matrix(2, 2) +matrix[0][0] = 1.0 +matrix[1][1] = 1.0 +matrix[0][1] = 0.5 +matrix[1][0] = 0.5 + +process = ql.StochasticProcessArray([process1, process2], matrix) + +# ### Pricing + +basketoption = ql.BasketOption(ql.MaxBasketPayoff(payoff), exercise) +basketoption.setPricingEngine( + ql.MCEuropeanBasketEngine(process, "pseudorandom", timeStepsPerYear=1, requiredTolerance=0.02, seed=42) +) +print(basketoption.NPV()) + +basketoption = ql.BasketOption(ql.MinBasketPayoff(payoff), exercise) +basketoption.setPricingEngine( + ql.MCEuropeanBasketEngine(process, "pseudorandom", timeStepsPerYear=1, requiredTolerance=0.02, seed=42) +) +print(basketoption.NPV()) + +basketoption = ql.BasketOption(ql.AverageBasketPayoff(payoff, 2), exercise) +basketoption.setPricingEngine( + ql.MCEuropeanBasketEngine(process, "pseudorandom", timeStepsPerYear=1, requiredTolerance=0.02, seed=42) +) +print(basketoption.NPV()) + +americanExercise = ql.AmericanExercise(settlementDate, ql.Date(17, ql.May, 1999)) +americanbasketoption = ql.BasketOption(ql.MaxBasketPayoff(payoff), americanExercise) +americanbasketoption.setPricingEngine( + ql.MCAmericanBasketEngine( + process, + "pseudorandom", + timeSteps=10, + requiredTolerance=0.02, + seed=42, + polynomOrder=5, + polynomType=ql.LsmBasisSystem.Hermite, + ) +) +print(americanbasketoption.NPV()) diff --git a/quantlib/bermudan-swaption.py b/quantlib/bermudan-swaption.py new file mode 100644 index 0000000..3a4e0a8 --- /dev/null +++ b/quantlib/bermudan-swaption.py @@ -0,0 +1,236 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # Bermudan swaptions +# +# Copyright (©) 2004, 2005, 2006, 2007 StatPro Italia srl +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +import mxdevtool as ql +import pandas as pd + +# ### Setup + +todaysDate = ql.Date(15, ql.February, 2002) +ql.Settings.instance().evaluationDate = todaysDate +calendar = ql.TARGET() +settlementDate = ql.Date(19, ql.February, 2002) + + +def calibrate(model, helpers, l, name): + print("Model: %s" % name) + + method = ql.Simplex(l) + model.calibrate(helpers, method, ql.EndCriteria(1000, 250, 1e-7, 1e-7, 1e-7)) + + print("Parameters: %s" % model.params()) + + totalError = 0.0 + data = [] + for swaption, helper in zip(swaptionVols, helpers): + maturity, length, vol = swaption + NPV = helper.modelValue() + implied = helper.impliedVolatility(NPV, 1.0e-4, 1000, 0.05, 0.50) + error = implied - vol + totalError += abs(error) + data.append((maturity, length, vol, implied, error)) + averageError = totalError / len(helpers) + + print(pd.DataFrame(data, columns=["maturity", "length", "volatility", "implied", "error"])) + + print("Average error: %.4f" % averageError) + + +# ### Market data + +swaptionVols = [ + # maturity, length, volatility + (ql.Period(1, ql.Years), ql.Period(5, ql.Years), 0.1148), + (ql.Period(2, ql.Years), ql.Period(4, ql.Years), 0.1108), + (ql.Period(3, ql.Years), ql.Period(3, ql.Years), 0.1070), + (ql.Period(4, ql.Years), ql.Period(2, ql.Years), 0.1021), + (ql.Period(5, ql.Years), ql.Period(1, ql.Years), 0.1000), +] + +# This is a flat yield term structure implying a 1x5 swap at 5%. + +rate = ql.QuoteHandle(ql.SimpleQuote(0.04875825)) +termStructure = ql.YieldTermStructureHandle(ql.FlatForward(settlementDate, rate, ql.Actual365Fixed())) + +# Define the ATM/OTM/ITM swaps: + +swapEngine = ql.DiscountingSwapEngine(termStructure) + +fixedLegFrequency = ql.Annual +fixedLegTenor = ql.Period(1, ql.Years) +fixedLegConvention = ql.Unadjusted +floatingLegConvention = ql.ModifiedFollowing +fixedLegDayCounter = ql.Thirty360(ql.Thirty360.European) +floatingLegFrequency = ql.Semiannual +floatingLegTenor = ql.Period(6, ql.Months) + +payFixed = ql.Swap.Payer +fixingDays = 2 +index = ql.Euribor6M(termStructure) +floatingLegDayCounter = index.dayCounter() + +swapStart = calendar.advance(settlementDate, 1, ql.Years, floatingLegConvention) +swapEnd = calendar.advance(swapStart, 5, ql.Years, floatingLegConvention) + +fixedSchedule = ql.Schedule( + swapStart, + swapEnd, + fixedLegTenor, + calendar, + fixedLegConvention, + fixedLegConvention, + ql.DateGeneration.Forward, + False, +) +floatingSchedule = ql.Schedule( + swapStart, + swapEnd, + floatingLegTenor, + calendar, + floatingLegConvention, + floatingLegConvention, + ql.DateGeneration.Forward, + False, +) + +dummy = ql.VanillaSwap( + payFixed, 100.0, fixedSchedule, 0.0, fixedLegDayCounter, floatingSchedule, index, 0.0, floatingLegDayCounter +) +dummy.setPricingEngine(swapEngine) +atmRate = dummy.fairRate() + +atmSwap = ql.VanillaSwap( + payFixed, 1000.0, fixedSchedule, atmRate, fixedLegDayCounter, + floatingSchedule, index, 0.0, floatingLegDayCounter +) + +otmSwap = ql.VanillaSwap( + payFixed, 1000.0, fixedSchedule, atmRate * 1.2, fixedLegDayCounter, + floatingSchedule, index, 0.0, floatingLegDayCounter +) + +itmSwap = ql.VanillaSwap( + payFixed, 1000.0, fixedSchedule, atmRate * 0.8, fixedLegDayCounter, + floatingSchedule, index, 0.0, floatingLegDayCounter +) + +atmSwap.setPricingEngine(swapEngine) +otmSwap.setPricingEngine(swapEngine) +itmSwap.setPricingEngine(swapEngine) + +helpers = [ + ql.SwaptionHelper( + maturity, + length, + ql.QuoteHandle(ql.SimpleQuote(vol)), + index, + index.tenor(), + index.dayCounter(), + index.dayCounter(), + termStructure, + ) + for maturity, length, vol in swaptionVols +] + +times = {} +for h in helpers: + for t in h.times(): + times[t] = 1 +times = sorted(times.keys()) + +grid = ql.TimeGrid(times, 30) + +G2model = ql.G2(termStructure) +HWmodel = ql.HullWhite(termStructure) +HWmodel2 = ql.HullWhite(termStructure) +BKmodel = ql.BlackKarasinski(termStructure) + +# ### Calibrations + +for h in helpers: + h.setPricingEngine(ql.G2SwaptionEngine(G2model, 6.0, 16)) +calibrate(G2model, helpers, 0.05, "G2 (analytic formulae)") + +for h in helpers: + h.setPricingEngine(ql.JamshidianSwaptionEngine(HWmodel)) +calibrate(HWmodel, helpers, 0.05, "Hull-White (analytic formulae)") + +for h in helpers: + h.setPricingEngine(ql.TreeSwaptionEngine(HWmodel2, grid)) +calibrate(HWmodel2, helpers, 0.05, "Hull-White (numerical calibration)") + +for h in helpers: + h.setPricingEngine(ql.TreeSwaptionEngine(BKmodel, grid)) +calibrate(BKmodel, helpers, 0.05, "Black-Karasinski (numerical calibration)") + + +# ### Price Bermudan swaptions on defined swaps + +bermudanDates = [d for d in fixedSchedule][:-1] +exercise = ql.BermudanExercise(bermudanDates) + +atmSwaption = ql.Swaption(atmSwap, exercise) +otmSwaption = ql.Swaption(otmSwap, exercise) +itmSwaption = ql.Swaption(itmSwap, exercise) + +data = [] + +# + +atmSwaption.setPricingEngine(ql.TreeSwaptionEngine(G2model, 50)) +otmSwaption.setPricingEngine(ql.TreeSwaptionEngine(G2model, 50)) +itmSwaption.setPricingEngine(ql.TreeSwaptionEngine(G2model, 50)) + +data.append(("G2 analytic", itmSwaption.NPV(), atmSwaption.NPV(), otmSwaption.NPV())) + +# + +atmSwaption.setPricingEngine(ql.TreeSwaptionEngine(HWmodel, 50)) +otmSwaption.setPricingEngine(ql.TreeSwaptionEngine(HWmodel, 50)) +itmSwaption.setPricingEngine(ql.TreeSwaptionEngine(HWmodel, 50)) + +data.append(("HW analytic", itmSwaption.NPV(), atmSwaption.NPV(), otmSwaption.NPV())) + +# + +atmSwaption.setPricingEngine(ql.TreeSwaptionEngine(HWmodel2, 50)) +otmSwaption.setPricingEngine(ql.TreeSwaptionEngine(HWmodel2, 50)) +itmSwaption.setPricingEngine(ql.TreeSwaptionEngine(HWmodel2, 50)) + +data.append(("HW numerical", itmSwaption.NPV(), atmSwaption.NPV(), otmSwaption.NPV())) + +# + +atmSwaption.setPricingEngine(ql.TreeSwaptionEngine(BKmodel, 50)) +otmSwaption.setPricingEngine(ql.TreeSwaptionEngine(BKmodel, 50)) +itmSwaption.setPricingEngine(ql.TreeSwaptionEngine(BKmodel, 50)) + +data.append(("BK numerical", itmSwaption.NPV(), atmSwaption.NPV(), otmSwaption.NPV())) +# - + +print(pd.DataFrame(data, columns=["model", "in-the-money", "at-the-money", "out-of-the-money"])) diff --git a/quantlib/bonds.py b/quantlib/bonds.py new file mode 100644 index 0000000..bd2b51d --- /dev/null +++ b/quantlib/bonds.py @@ -0,0 +1,319 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # Bonds +# +# Copyright (©) 2008 Florent Grenier +# Copyright (©) 2010 Lluis Pujol Bajador +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it +# under the terms of the QuantLib license. You should have received a +# # copy of the license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# This example shows how to set up a term structure and then price +# some simple bonds. The last part is dedicated to peripherical +# computations such as "Yield to Price" or "Price to Yield" + +import mxdevtool as ql +import pandas as pd + +interactive = 'get_ipython' in globals() + +# ### Global data + +calendar = ql.TARGET() +settlementDate = ql.Date(18, ql.September, 2008) +settlementDate = calendar.adjust(settlementDate) + +fixingDays = 3 +settlementDays = 3 + +todaysDate = calendar.advance(settlementDate, -fixingDays, ql.Days) +ql.Settings.instance().evaluationDate = todaysDate + +print("Today: " + str(todaysDate)) +print("Settlement Date: " + str(settlementDate)) + +# ### Market quotes + +zcQuotes = [(0.0096, ql.Period(3, ql.Months)), (0.0145, ql.Period(6, ql.Months)), (0.0194, ql.Period(1, ql.Years))] + +zcBondsDayCounter = ql.Actual365Fixed() + +zcHelpers = [ + ql.DepositRateHelper( + ql.QuoteHandle(ql.SimpleQuote(r)), tenor, fixingDays, calendar, ql.ModifiedFollowing, True, zcBondsDayCounter + ) + for (r, tenor) in zcQuotes +] + +# ### Setup bonds + +redemption = 100.0 +numberOfBonds = 5 + +bondQuotes = [ + (ql.Date(15, ql.March, 2005), ql.Date(31, ql.August, 2010), 0.02375, 100.390625), + (ql.Date(15, ql.June, 2005), ql.Date(31, ql.August, 2011), 0.04625, 106.21875), + (ql.Date(30, ql.June, 2006), ql.Date(31, ql.August, 2013), 0.03125, 100.59375), + (ql.Date(15, ql.November, 2002), ql.Date(15, ql.August, 2018), 0.04000, 101.6875), + (ql.Date(15, ql.May, 1987), ql.Date(15, ql.May, 2038), 0.04500, 102.140625), +] + +# ### Definition of the rate helpers + +bondsHelpers = [] + +for issueDate, maturity, couponRate, marketQuote in bondQuotes: + schedule = ql.Schedule( + issueDate, + maturity, + ql.Period(ql.Semiannual), + ql.UnitedStates(ql.UnitedStates.GovernmentBond), + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + bondsHelpers.append( + ql.FixedRateBondHelper( + ql.QuoteHandle(ql.SimpleQuote(marketQuote)), + settlementDays, + 100.0, + schedule, + [couponRate], + ql.ActualActual(ql.ActualActual.Bond), + ql.Unadjusted, + redemption, + issueDate, + ) + ) + +# ### Curve building + +termStructureDayCounter = ql.ActualActual(ql.ActualActual.ISDA) + +bondInstruments = zcHelpers + bondsHelpers + +bondDiscountingTermStructure = ql.PiecewiseFlatForward(settlementDate, bondInstruments, termStructureDayCounter) + +# ### Building of the LIBOR forecasting curve + +dQuotes = [ + (0.043375, ql.Period(1, ql.Weeks)), + (0.031875, ql.Period(1, ql.Months)), + (0.0320375, ql.Period(3, ql.Months)), + (0.03385, ql.Period(6, ql.Months)), + (0.0338125, ql.Period(9, ql.Months)), + (0.0335125, ql.Period(1, ql.Years)), +] +sQuotes = [ + (0.0295, ql.Period(2, ql.Years)), + (0.0323, ql.Period(3, ql.Years)), + (0.0359, ql.Period(5, ql.Years)), + (0.0412, ql.Period(10, ql.Years)), + (0.0433, ql.Period(15, ql.Years)), +] + +depositDayCounter = ql.Actual360() +depositHelpers = [ + ql.DepositRateHelper( + ql.QuoteHandle(ql.SimpleQuote(rate)), tenor, fixingDays, calendar, ql.ModifiedFollowing, True, depositDayCounter + ) + for rate, tenor in dQuotes +] + +swFixedLegFrequency = ql.Annual +swFixedLegConvention = ql.Unadjusted +swFixedLegDayCounter = ql.Thirty360(ql.Thirty360.European) +swFloatingLegIndex = ql.Euribor6M() +forwardStart = ql.Period(1, ql.Days) +swapHelpers = [ + ql.SwapRateHelper( + ql.QuoteHandle(ql.SimpleQuote(rate)), + tenor, + calendar, + swFixedLegFrequency, + swFixedLegConvention, + swFixedLegDayCounter, + swFloatingLegIndex, + ql.QuoteHandle(), + forwardStart, + ) + for rate, tenor in sQuotes +] + +depoSwapInstruments = depositHelpers + swapHelpers + +depoSwapTermStructure = ql.PiecewiseFlatForward(settlementDate, depoSwapInstruments, termStructureDayCounter) + +# ### Pricing +# +# Term structures that will be used for pricing: +# the one used for discounting cash flows... + +discountingTermStructure = ql.RelinkableYieldTermStructureHandle() + +# ...and the one used for forward rate forecasting. + +forecastingTermStructure = ql.RelinkableYieldTermStructureHandle() + +# Bonds to be priced: + +faceAmount = 100 + +bondEngine = ql.DiscountingBondEngine(discountingTermStructure) + +# a zero coupon bond... + +zeroCouponBond = ql.ZeroCouponBond( + settlementDays, + ql.UnitedStates(ql.UnitedStates.GovernmentBond), + faceAmount, + ql.Date(15, ql.August, 2013), + ql.Following, + 116.92, + ql.Date(15, ql.August, 2003), +) + +zeroCouponBond.setPricingEngine(bondEngine) + +# ...a fixed 4.5% US Treasury note... + +fixedBondSchedule = ql.Schedule( + ql.Date(15, ql.May, 2007), + ql.Date(15, ql.May, 2017), + ql.Period(ql.Semiannual), + ql.UnitedStates(ql.UnitedStates.GovernmentBond), + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, +) + +fixedRateBond = ql.FixedRateBond( + settlementDays, + faceAmount, + fixedBondSchedule, + [0.045], + ql.ActualActual(ql.ActualActual.Bond), + ql.ModifiedFollowing, + 100.0, + ql.Date(15, ql.May, 2007), +) + +fixedRateBond.setPricingEngine(bondEngine) + +# ...and a floating rate bond paying 3M USD Libor + 0.1% +# (should and will be priced on another curve later). + +liborTermStructure = ql.RelinkableYieldTermStructureHandle() + +libor3m = ql.USDLibor(ql.Period(3, ql.Months), liborTermStructure) +libor3m.addFixing(ql.Date(17, ql.April, 2008), 0.028175) +libor3m.addFixing(ql.Date(17, ql.July, 2008), 0.0278625) + +floatingBondSchedule = ql.Schedule( + ql.Date(21, ql.October, 2005), + ql.Date(21, ql.October, 2010), + ql.Period(ql.Quarterly), + ql.UnitedStates(ql.UnitedStates.NYSE), + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + True, +) + +floatingRateBond = ql.FloatingRateBond( + settlementDays, + faceAmount, + floatingBondSchedule, + libor3m, + ql.Actual360(), + ql.ModifiedFollowing, + spreads=[0.001], + issueDate=ql.Date(21, ql.October, 2005), +) + +floatingRateBond.setPricingEngine(bondEngine) + +forecastingTermStructure.linkTo(depoSwapTermStructure) +discountingTermStructure.linkTo(bondDiscountingTermStructure) + +liborTermStructure.linkTo(depoSwapTermStructure) + +# + +data = [] +data.append( + (zeroCouponBond.cleanPrice(), fixedRateBond.cleanPrice(), floatingRateBond.cleanPrice()) +) +data.append( + (zeroCouponBond.dirtyPrice(), fixedRateBond.dirtyPrice(), floatingRateBond.dirtyPrice()) +) +data.append( + (zeroCouponBond.accruedAmount(), + fixedRateBond.accruedAmount(), + floatingRateBond.accruedAmount()) +) +data.append( + (None, fixedRateBond.previousCouponRate(), floatingRateBond.previousCouponRate()) +) +data.append( + (None, fixedRateBond.nextCouponRate(), floatingRateBond.nextCouponRate()) +) +data.append( + (zeroCouponBond.bondYield(ql.Actual360(), ql.Compounded, ql.Annual), + fixedRateBond.bondYield(ql.Actual360(), ql.Compounded, ql.Annual), + floatingRateBond.bondYield(ql.Actual360(), ql.Compounded, ql.Annual)) +) + +df = pd.DataFrame(data, columns=["ZC", "Fixed", "Floating"], + index=["Clean price", "Dirty price", "Accrued coupon", + "Previous coupon rate", "Next coupon rate", "Yield"]) +if not interactive: + print(df) +df +# - + +# A few other computations: + +# Yield to clean price: + +floatingRateBond.cleanPrice( + floatingRateBond.bondYield(ql.Actual360(), ql.Compounded, ql.Annual), + ql.Actual360(), + ql.Compounded, + ql.Annual, + settlementDate, +) + +# Clean price to yield: + +floatingRateBond.bondYield( + floatingRateBond.cleanPrice(), + ql.Actual360(), + ql.Compounded, + ql.Annual, + settlementDate +) diff --git a/quantlib/cashflows.py b/quantlib/cashflows.py new file mode 100644 index 0000000..c57add6 --- /dev/null +++ b/quantlib/cashflows.py @@ -0,0 +1,158 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.6.0 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Cash-flow analysis +# +# Copyright (©) 2020 StatPro Italia srl +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% +import mxdevtool as ql +import pandas as pd + +# %% +interactive = "get_ipython" in globals() + +# %% +today = ql.Date(19, ql.October, 2020) +ql.Settings.instance().evaluationDate = today + +# %% [markdown] +# ### Term structure construction + +# %% +dates = [ + ql.Date(19,10,2020), + ql.Date(19,11,2020), + ql.Date(19, 1,2021), + ql.Date(19, 4,2021), + ql.Date(19,10,2021), + ql.Date(19, 4,2022), + ql.Date(19,10,2022), + ql.Date(19,10,2023), + ql.Date(19,10,2025), + ql.Date(19,10,2030), + ql.Date(19,10,2035), + ql.Date(19,10,2040), +] + +rates = [ + -0.004, + -0.002, + 0.001, + 0.005, + 0.009, + 0.010, + 0.010, + 0.012, + 0.017, + 0.019, + 0.028, + 0.032, +] + +forecast_curve = ql.ZeroCurve(dates, rates, ql.Actual365Fixed()) + +# %% +forecast_handle = ql.YieldTermStructureHandle(forecast_curve) + +# %% [markdown] +# ### Swap construction +# +# We'll use an overnight swap as an example. We're keeping the initialization simple, but the analysis work in the same way for more complex ones, as well as for other kinds of swaps and bonds (once we extract the cashflows from them using the proper methods). + +# %% +swap = ql.MakeOIS(swapTenor=ql.Period(5, ql.Years), + overnightIndex=ql.Eonia(forecast_handle), + fixedRate=0.002) + +# %% [markdown] +# ### Cash-flow analysis +# +# The fixed-rate coupons can be extracted from the swap using the `fixedLeg` method. They are returned as instances of the base `Cashflow` class, so the only methods we have directly available are from that class interface: + +# %% +fixed_leg = swap.fixedLeg() + +# %% +df = pd.DataFrame([(c.date(), c.amount()) for c in fixed_leg if c.date() > today], + columns=['date', 'amount']) +df + +# %% [markdown] +# The following displays the results when this is run as a Python script (in which case the cell above is not displayed). + +# %% +if not interactive: + print(df) + +# %% [markdown] +# If we want to extract more information, we need to upcast the coupons to a more specific class. This can be done by using the `as_fixed_rate_coupon` method. In this case, the upcast works by construction; but in the general case we might have cashflows for which the upcast fails (e.g., the redemption for a bond) so we have to check for nulls. + +# %% +coupons = [] +for cf in fixed_leg: + c = ql.as_fixed_rate_coupon(cf) + if c: + coupons.append(c) + +# %% [markdown] +# We can now access methods from the coupon class. + +# %% +df = pd.DataFrame([(c.date(), c.amount(), c.rate(), c.accrualStartDate(), c.accrualEndDate(), c.accrualPeriod()) + for c in coupons if c.date() > today], + columns=['payment date', 'amount', 'rate', 'start date', 'end date', 'accrual period']) +df + +# %% +if not interactive: + print(df) + +# %% [markdown] +# The same goes for the floating leg: in this case, we need to upcast to floating-rate coupons in order to access the specific methods we'll need. + +# %% +floating_leg = swap.overnightLeg() + +# %% +coupons = [] +for cf in floating_leg: + c = ql.as_floating_rate_coupon(cf) + if c: + coupons.append(c) + +# %% +df = pd.DataFrame([(c.date(), c.amount(), c.rate(), c.accrualStartDate(), c.accrualEndDate(), c.accrualPeriod()) + for c in coupons if c.date() > today], + columns=['payment date', 'amount', 'rate', 'start date', 'end date', 'accrual period']) +df + +# %% +if not interactive: + print(df) diff --git a/quantlib/cds.py b/quantlib/cds.py new file mode 100644 index 0000000..c7a480c --- /dev/null +++ b/quantlib/cds.py @@ -0,0 +1,112 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # Credit default swaps +# +# Copyright (©) 2014 Thema Consulting SA +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# ### Setup + +import mxdevtool as ql + +calendar = ql.TARGET() + +todaysDate = ql.Date(15, ql.May, 2007) +ql.Settings.instance().evaluationDate = todaysDate + +risk_free_rate = ql.YieldTermStructureHandle(ql.FlatForward(todaysDate, 0.01, ql.Actual365Fixed())) + +# ### CDS parameters + +recovery_rate = 0.5 +quoted_spreads = [0.0150, 0.0150, 0.0150, 0.0150] +tenors = [ql.Period(3, ql.Months), ql.Period(6, ql.Months), ql.Period(1, ql.Years), ql.Period(2, ql.Years)] +maturities = [calendar.adjust(todaysDate + x, ql.Following) for x in tenors] + +instruments = [ + ql.SpreadCdsHelper( + ql.QuoteHandle(ql.SimpleQuote(s)), + tenor, + 0, + calendar, + ql.Quarterly, + ql.Following, + ql.DateGeneration.TwentiethIMM, + ql.Actual365Fixed(), + recovery_rate, + risk_free_rate, + ) + for s, tenor in zip(quoted_spreads, tenors) +] + +hazard_curve = ql.PiecewiseFlatHazardRate(todaysDate, instruments, ql.Actual365Fixed()) +print("Calibrated hazard rate values: ") +for x in hazard_curve.nodes(): + print("hazard rate on %s is %.7f" % x) + +print("Some survival probability values: ") +print( + "1Y survival probability: %.4g, \n\t\texpected %.4g" + % (hazard_curve.survivalProbability(todaysDate + ql.Period("1Y")), 0.9704) +) +print( + "2Y survival probability: %.4g, \n\t\texpected %.4g" + % (hazard_curve.survivalProbability(todaysDate + ql.Period("2Y")), 0.9418) +) + +# ### Reprice instruments + +nominal = 1000000.0 +probability = ql.DefaultProbabilityTermStructureHandle(hazard_curve) + +# We'll create a cds for every maturity: + +all_cds = [] +for maturity, s in zip(maturities, quoted_spreads): + schedule = ql.Schedule( + todaysDate, + maturity, + ql.Period(ql.Quarterly), + calendar, + ql.Following, + ql.Unadjusted, + ql.DateGeneration.TwentiethIMM, + False, + ) + cds = ql.CreditDefaultSwap(ql.Protection.Seller, nominal, s, schedule, ql.Following, ql.Actual365Fixed()) + engine = ql.MidPointCdsEngine(probability, recovery_rate, risk_free_rate) + cds.setPricingEngine(engine) + all_cds.append(cds) + +print("Repricing of quoted CDSs employed for calibration: ") +for cds, tenor in zip(all_cds, tenors): + print("%s fair spread: %.7g" % (tenor, cds.fairSpread())) + print(" NPV: %g" % cds.NPV()) + print(" default leg: %.7g" % cds.defaultLegNPV()) + print(" coupon leg: %.7g" % cds.couponLegNPV()) + print("") diff --git a/quantlib/european-option.py b/quantlib/european-option.py new file mode 100644 index 0000000..59c6665 --- /dev/null +++ b/quantlib/european-option.py @@ -0,0 +1,193 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# # European options +# +# Copyright (©) 2004, 2005, 2006, 2007 StatPro Italia srl +# +# This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% +import mxdevtool as ql +import pandas as pd + +# %% [markdown] +# ### Global parameters + +# %% +todaysDate = ql.Date(15, ql.May, 1998) +ql.Settings.instance().evaluationDate = todaysDate + +# %% +interactive = 'get_ipython' in globals() + +# %% [markdown] +# ### Option construction + +# %% +exercise = ql.EuropeanExercise(ql.Date(17, ql.May, 1999)) +payoff = ql.PlainVanillaPayoff(ql.Option.Call, 8.0) + +# %% +option = ql.VanillaOption(payoff, exercise) + +# %% [markdown] +# ### Market data + +# %% +underlying = ql.SimpleQuote(7.0) +dividendYield = ql.FlatForward(todaysDate, 0.05, ql.Actual365Fixed()) +volatility = ql.BlackConstantVol(todaysDate, ql.TARGET(), 0.10, ql.Actual365Fixed()) +riskFreeRate = ql.FlatForward(todaysDate, 0.05, ql.Actual365Fixed()) + +# %% [markdown] +# ### Processes and models + +# %% +process = ql.BlackScholesMertonProcess( + ql.QuoteHandle(underlying), + ql.YieldTermStructureHandle(dividendYield), + ql.YieldTermStructureHandle(riskFreeRate), + ql.BlackVolTermStructureHandle(volatility), +) + +# %% +hestonProcess = ql.HestonProcess( + ql.YieldTermStructureHandle(riskFreeRate), + ql.YieldTermStructureHandle(dividendYield), + ql.QuoteHandle(underlying), + 0.1 * 0.1, + 1.0, + 0.1 * 0.1, + 0.0001, + 0.0, +) +hestonModel = ql.HestonModel(hestonProcess) + +# %% [markdown] +# ### Pricing +# +# We'll collect tuples of method name, option value, estimated error, and discrepancy from the analytic formula. + +# %% +results = [] + +# %% [markdown] +# #### Analytic formula + +# %% +option.setPricingEngine(ql.AnalyticEuropeanEngine(process)) +value = option.NPV() +refValue = value + +results.append(('Analytic', value, None, None)) + +# %% [markdown] +# #### Heston semi-analytic formula + +# %% +option.setPricingEngine(ql.AnalyticHestonEngine(hestonModel)) +value = option.NPV() + +results.append(('Heston analytic', value, None, abs(value - refValue))) + +# %% [markdown] +# #### Heston COS method + +# %% +option.setPricingEngine(ql.COSHestonEngine(hestonModel)) +value = option.NPV() + +results.append(('Heston COS', value, None, abs(value - refValue))) + +# %% [markdown] +# #### Integral method + +# %% +option.setPricingEngine(ql.IntegralEngine(process)) +value = option.NPV() + +results.append(('Integral', value, None, abs(value - refValue))) + +# %% [markdown] +# #### Finite-difference method + +# %% +timeSteps = 801 +gridPoints = 800 + +# %% +option.setPricingEngine(ql.FdBlackScholesVanillaEngine(process, timeSteps, gridPoints)) +value = option.NPV() + +results.append(('Finite diff.', value, None, abs(value - refValue))) + +# %% [markdown] +# #### Binomial method + +# %% +timeSteps = 801 + +# %% +for tree in ["JR", "CRR", "EQP", "Trigeorgis", "Tian", "LR", "Joshi4"]: + option.setPricingEngine(ql.BinomialVanillaEngine(process, tree, timeSteps)) + value = option.NPV() + + results.append(('Binomial (%s)' % tree, value, None, abs(value - refValue))) + +# %% [markdown] +# #### Monte Carlo method + +# %% +option.setPricingEngine(ql.MCEuropeanEngine(process, "pseudorandom", timeSteps=1, + requiredTolerance=0.02, seed=42)) +value = option.NPV() + +results.append(("Monte Carlo (pseudo-random)", value, option.errorEstimate(), abs(value - refValue))) + +# %% +option.setPricingEngine(ql.MCEuropeanEngine(process, "lowdiscrepancy", timeSteps=1, + requiredSamples=32768)) +value = option.NPV() + +results.append(("Monte Carlo (low-discrepancy)", value, None, abs(value - refValue))) + +# %% [markdown] +# ### Results + +# %% +df = pd.DataFrame(results, + columns=["Method", "Option value", "Error estimate", "Actual error"]) + +# %% +df.style.hide_index() + +# %% [markdown] +# The following displays the results when this is run as a Python script (in which case the cell above is not displayed). + +# %% +if not interactive: + print(df) diff --git a/quantlib/gaussian1d-models.py b/quantlib/gaussian1d-models.py new file mode 100644 index 0000000..05c5270 --- /dev/null +++ b/quantlib/gaussian1d-models.py @@ -0,0 +1,489 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Gaussian 1D models +# +# Copyright (©) 2018 Angus Lee +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% [markdown] +# ### Setup + +# %% +import mxdevtool as ql +import pandas as pd + +# %% +interactive = "get_ipython" in globals() + + +def show(x): + if not interactive: + print(x) + return x + + +# %% +def basket_data(basket): + data = [] + for helper in basket: + h = ql.as_swaption_helper(helper) + data.append( + ( + h.swaptionExpiryDate().to_date(), + h.swaptionMaturityDate().to_date(), + h.swaptionNominal(), + h.volatility().value(), + h.swaptionStrike(), + ) + ) + return pd.DataFrame(data, columns=["Expiry", "Maturity", "Nominal", "Rate", "Market vol"]) + + +# %% +def calibration_data(basket, volatilities): + data = [] + for helper, sigma in zip(basket, volatilities): + h = ql.as_swaption_helper(helper) + modelValue = h.modelValue() + data.append( + ( + h.swaptionExpiryDate().to_date(), + sigma, + modelValue, + h.marketValue(), + h.impliedVolatility(modelValue, 1e-6, 1000, 0.0, 2.0), + h.volatility().value(), + ) + ) + return pd.DataFrame( + data, columns=["Expiry", "Model sigma", "Model price", "Market price", "Model imp.vol", "Market imp.vol"] + ) + + +# %% [markdown] +# ### Calculations + +# %% [markdown] +# This exercise tries to replicate the Quantlib C++ `Gaussian1dModel` example on how to use the GSR and Markov Functional model. + +# %% +refDate = ql.Date(30, 4, 2014) +ql.Settings.instance().setEvaluationDate(refDate) + +# %% [markdown] +# We assume a multicurve setup, for simplicity with flat yield term structures. +# +# The discounting curve is an Eonia curve at a level of 2% and the forwarding curve is an Euribor 6m curve at a level of 2.5%. +# +# For the volatility we assume a flat swaption volatility at 20%. + +# %% +forward6mQuote = ql.QuoteHandle(ql.SimpleQuote(0.025)) +oisQuote = ql.QuoteHandle(ql.SimpleQuote(0.02)) +volQuote = ql.QuoteHandle(ql.SimpleQuote(0.2)) + +# %% +dc = ql.Actual365Fixed() +yts6m = ql.FlatForward(refDate, forward6mQuote, dc) +ytsOis = ql.FlatForward(refDate, oisQuote, dc) +yts6m.enableExtrapolation() +ytsOis.enableExtrapolation() +hyts6m = ql.RelinkableYieldTermStructureHandle(yts6m) +t0_curve = ql.YieldTermStructureHandle(yts6m) +t0_Ois = ql.YieldTermStructureHandle(ytsOis) +euribor6m = ql.Euribor6M(hyts6m) +swaptionVol = ql.ConstantSwaptionVolatility(0, ql.TARGET(), ql.ModifiedFollowing, volQuote, ql.Actual365Fixed()) + +# %% +effectiveDate = ql.TARGET().advance(refDate, ql.Period('2D')) +maturityDate = ql.TARGET().advance(effectiveDate, ql.Period('10Y')) + +# %% +fixedSchedule = ql.Schedule(effectiveDate, + maturityDate, + ql.Period('1Y'), + ql.TARGET(), + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Forward, False) + +# %% +floatSchedule = ql.Schedule(effectiveDate, + maturityDate, + ql.Period('6M'), + ql.TARGET(), + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Forward, False) + +# %% [markdown] +# We consider a standard 10-years Bermudan payer swaption with yearly exercises at a strike of 4%. + +# %% +fixedNominal = [1]*(len(fixedSchedule)-1) +floatingNominal = [1]*(len(floatSchedule)-1) +strike = [0.04]*(len(fixedSchedule)-1) +gearing = [1]*(len(floatSchedule)-1) +spread = [0]*(len(floatSchedule)-1) + +# %% +underlying = ql.NonstandardSwap( + ql.Swap.Payer, + fixedNominal, floatingNominal, fixedSchedule, strike, + ql.Thirty360(ql.Thirty360.BondBasis), floatSchedule, + euribor6m, gearing, spread, ql.Actual360(), False, False, ql.ModifiedFollowing) + +# %% +exerciseDates = [ql.TARGET().advance(x, -ql.Period('2D')) for x in fixedSchedule] +exerciseDates = exerciseDates[1:-1] +exercise = ql.BermudanExercise(exerciseDates) +swaption = ql.NonstandardSwaption(underlying,exercise,ql.Settlement.Physical) + +# %% [markdown] +# The model is a one factor Hull White model with piecewise volatility adapted to our exercise dates. +# +# The reversion is just kept constant at a level of 1%. + +# %% +stepDates = exerciseDates[:-1] +sigmas = [ql.QuoteHandle(ql.SimpleQuote(0.01)) for x in range(1, 10)] +reversion = [ql.QuoteHandle(ql.SimpleQuote(0.01))] + +# %% [markdown] +# The model's curve is set to the 6m forward curve. Note that the model adapts automatically to other curves where appropriate (e.g. if an index requires a different forwarding curve) or where explicitly specified (e.g. in a swaption pricing engine). + +# %% +gsr = ql.Gsr(t0_curve, stepDates, sigmas, reversion) +swaptionEngine = ql.Gaussian1dSwaptionEngine(gsr, 64, 7.0, True, False, t0_Ois) +nonstandardSwaptionEngine = ql.Gaussian1dNonstandardSwaptionEngine( + gsr, 64, 7.0, True, False, ql.QuoteHandle(ql.SimpleQuote(0)), t0_Ois) + +# %% +swaption.setPricingEngine(nonstandardSwaptionEngine) + +# %% +swapBase = ql.EuriborSwapIsdaFixA(ql.Period('10Y'), t0_curve, t0_Ois) +basket = swaption.calibrationBasket(swapBase, swaptionVol, 'Naive') + +# %% +for basket_i in basket: + ql.as_black_helper(basket_i).setPricingEngine(swaptionEngine) + +# %% +method = ql.LevenbergMarquardt() +ec = ql.EndCriteria(1000, 10, 1e-8, 1e-8, 1e-8) + +# %% +gsr.calibrateVolatilitiesIterative(basket, method, ec) + + +# %% [markdown] +# The engine can generate a calibration basket in two modes. +# +# The first one is called Naive and generates ATM swaptions adapted to the exercise dates of the swaption and its maturity date. The resulting basket looks as follows: + +# %% +show(basket_data(basket)) + +# %% [markdown] +# Let's calibrate our model to this basket. We use a specialized calibration method calibrating the sigma function one by one to the calibrating vanilla swaptions. The result of this is as follows: + +# %% +show(calibration_data(basket, gsr.volatility())) + +# %% [markdown] +# Bermudan swaption NPV (ATM calibrated GSR): + +# %% +print(swaption.NPV()) + +# %% [markdown] +# There is another mode to generate a calibration basket called `MaturityStrikeByDeltaGamma`. This means that the maturity, the strike and the nominal of the calibrating swaptions are obtained matching the NPV, first derivative and second derivative of the swap you will exercise into at at each bermudan call date. The derivatives are taken with respect to the model's state variable. +# +# Let's try this in our case. + +# %% +basket = swaption.calibrationBasket(swapBase, swaptionVol, 'MaturityStrikeByDeltaGamma') +show(basket_data(basket)) + +# %% +for basket_i in basket: + ql.as_black_helper(basket_i).setPricingEngine(swaptionEngine) + +# %% [markdown] +# The calibrated nominal is close to the exotics nominal. The expiries and maturity dates of the vanillas are the same as in the case above. The difference is the strike which is now equal to the exotics strike. +# +# Let's see how this affects the exotics NPV. The recalibrated model is: + +# %% +gsr.calibrateVolatilitiesIterative(basket, method, ec) +show(calibration_data(basket, gsr.volatility())) + +# %% [markdown] +# Bermudan swaption NPV (deal strike calibrated GSR): + +# %% +print(swaption.NPV()) + +# %% [markdown] +# We can do more complicated things. Let's e.g. modify the nominal schedule to be linear amortizing and see what the effect on the generated calibration basket is: + +# %% +for i in range(0,len(fixedSchedule)-1): + tmp = 1 - i/ (len(fixedSchedule)-1) + fixedNominal[i] = tmp + floatingNominal[i*2] = tmp + floatingNominal[i*2+1] = tmp + +# %% +underlying2 = ql.NonstandardSwap(ql.Swap.Payer, + fixedNominal, floatingNominal, fixedSchedule, strike, + ql.Thirty360(ql.Thirty360.BondBasis), floatSchedule, + euribor6m, gearing, spread, ql.Actual360(), False, False, ql.ModifiedFollowing) + +# %% +swaption2 = ql.NonstandardSwaption(underlying2,exercise,ql.Settlement.Physical) + +# %% +swaption2.setPricingEngine(nonstandardSwaptionEngine) +basket = swaption2.calibrationBasket(swapBase, swaptionVol, 'MaturityStrikeByDeltaGamma') + +# %% +show(basket_data(basket)) + +# %% [markdown] +# The notional is weighted over the underlying exercised into and the maturity is adjusted downwards. The rate, on the other hand, is not affected. + +# %% [markdown] +# You can also price exotic bond's features. If you have e.g. a Bermudan callable fixed bond you can set up the call right as a swaption to enter into a one leg swap with notional reimbursement at maturity. The exercise should then be written as a rebated exercise paying the notional in case of exercise. The calibration basket looks like this: + +# %% +fixedNominal2 = [1]*(len(fixedSchedule)-1) +floatingNominal2 = [0]*(len(floatSchedule)-1) #null the second leg + +# %% +underlying3 = ql.NonstandardSwap(ql.Swap.Receiver, + fixedNominal2, floatingNominal2, fixedSchedule, strike, + ql.Thirty360(ql.Thirty360.BondBasis), floatSchedule, + euribor6m, gearing, spread, ql.Actual360(), False, True, ql.ModifiedFollowing) + +# %% +rebateAmount = [-1]*len(exerciseDates) +exercise2 = ql.RebatedExercise(exercise, rebateAmount, 2, ql.TARGET()) +swaption3 = ql.NonstandardSwaption(underlying3,exercise2,ql.Settlement.Physical) + +# %% +oas0 = ql.SimpleQuote(0) +oas100 = ql.SimpleQuote(0.01) +oas = ql.RelinkableQuoteHandle(oas0) + +# %% +nonstandardSwaptionEngine2 = ql.Gaussian1dNonstandardSwaptionEngine( + gsr, 64, 7.0, True, False, oas, t0_curve) # Change discounting to 6m + +# %% +swaption3.setPricingEngine(nonstandardSwaptionEngine2) +basket = swaption3.calibrationBasket(swapBase, swaptionVol, 'MaturityStrikeByDeltaGamma') + +# %% +show(basket_data(basket)) + +# %% [markdown] +# Note that nominals are not exactly 1.0 here. This is because we do our bond discounting on 6m level while the swaptions are still discounted on OIS level. (You can try this by changing the OIS level to the 6m level, which will produce nominals near 1.0). +# +# The NPV of the call right is (after recalibrating the model): + +# %% +for basket_i in basket: + ql.as_black_helper(basket_i).setPricingEngine(swaptionEngine) + +# %% +gsr.calibrateVolatilitiesIterative(basket, method, ec) + +# %% +print(swaption3.NPV()) + +# %% [markdown] +# Up to now, no credit spread is included in the pricing. We can do so by specifying an oas in the pricing engine. Let's set the spread level to 100bp and regenerate the calibration basket. + +# %% +oas.linkTo(oas100) +basket = swaption3.calibrationBasket(swapBase, swaptionVol, 'MaturityStrikeByDeltaGamma') +show(basket_data(basket)) + +# %% [markdown] +# The adjusted basket takes the credit spread into account. This is consistent to a hedge where you would have a margin on the float leg around 100bp,too. + +# %% +for basket_i in basket: + ql.as_black_helper(basket_i).setPricingEngine(swaptionEngine) + +# %% +gsr.calibrateVolatilitiesIterative(basket, method, ec) + +# %% +print(swaption3.NPV()) + +# %% [markdown] +# The next instrument we look at is a CMS 10Y vs Euribor 6M swaption. The maturity is again 10 years and the option is exercisable on a yearly basis. + +# %% +CMSNominal = [1]*(len(fixedSchedule)-1) +CMSgearing = [1]*(len(fixedSchedule)-1) +CMSspread = [0]*(len(fixedSchedule)-1) +EuriborNominal = [1]*(len(floatSchedule)-1) +Euriborgearing = [1]*(len(floatSchedule)-1) +Euriborspread = [0.001]*(len(floatSchedule)-1) +underlying4 = ql.FloatFloatSwap(ql.Swap.Payer, + CMSNominal, EuriborNominal, + fixedSchedule, swapBase, ql.Thirty360(ql.Thirty360.BondBasis), + floatSchedule, euribor6m, ql.Actual360(), + False, False, CMSgearing, CMSspread, [], [], + Euriborgearing, Euriborspread) + +# %% +swaption4 = ql.FloatFloatSwaption(underlying4, exercise) +floatSwaptionEngine = ql.Gaussian1dFloatFloatSwaptionEngine( + gsr, 64, 7.0, True, False, ql.QuoteHandle(ql.SimpleQuote(0)), t0_Ois, True) +swaption4.setPricingEngine(floatSwaptionEngine) + +# %% [markdown] +# Since the underlying is quite exotic already, we start with pricing this using the `LinearTsrPricer` for CMS coupon estimation. + +# %% +leg0 = underlying4.leg(0) +leg1 = underlying4.leg(1) +reversionQuote = ql.QuoteHandle(ql.SimpleQuote(0.01)) +swaptionVolHandle = ql.SwaptionVolatilityStructureHandle(swaptionVol) +cmsPricer = ql.LinearTsrPricer(swaptionVolHandle, reversionQuote) +iborPricer = ql.BlackIborCouponPricer() + +# %% +ql.setCouponPricer(leg0, cmsPricer) +ql.setCouponPricer(leg1, iborPricer) + +# %% +swapPricer = ql.DiscountingSwapEngine(t0_Ois) +underlying4.setPricingEngine(swapPricer) + +# %% +print("Underlying CMS Swap NPV = %f" % underlying4.NPV()) +print("Underlying CMS Leg NPV = %f" % underlying4.legNPV(0)) +print("Underlying Euribor NPV = %f" % underlying4.legNPV(1)) + +# %% [markdown] +# We generate a naive calibration basket and calibrate the GSR model to it: + +# %% +basket = swaption4.calibrationBasket(swapBase, swaptionVol, 'Naive') + +# %% +for basket_i in basket: + ql.as_black_helper(basket_i).setPricingEngine(swaptionEngine) + +# %% +gsr.calibrateVolatilitiesIterative(basket, method, ec) +show(basket_data(basket)) + +# %% +show(calibration_data(basket, gsr.volatility())) + +# %% [markdown] +# The npv of the bermudan swaption is: + +# %% +print(swaption4.NPV()) + +# %% [markdown] +# In this case it is also interesting to look at the underlying swap NPV in the GSR model. + +# %% +print(swaption4.underlyingValue()) + +# %% [markdown] +# Not surprisingly, the underlying is priced differently compared to the `LinearTsrPricer`, since a different smile is implied by the GSR model. +# +# This is exactly where the Markov functional model comes into play, because it can calibrate to any given underlying smile (as long as it is arbitrage free). We try this now. Of course the usual use case is not to calibrate to a flat smile as in our simple example, still it should be possible, of course... + +# %% +markovStepDates = exerciseDates +cmsFixingDates = markovStepDates +markovSimgas = [0.01]* (len(markovStepDates)+1) +tenors = [ql.Period('10Y')]*len(cmsFixingDates) +markov = ql.MarkovFunctional(t0_curve, reversionQuote.value(), markovStepDates, markovSimgas, swaptionVolHandle, + cmsFixingDates, tenors, swapBase) + +# %% +swaptionEngineMarkov = ql.Gaussian1dSwaptionEngine(markov, 8, 5.0, True, + False, t0_Ois) + +# %% +floatEngineMarkov = ql.Gaussian1dFloatFloatSwaptionEngine( + markov, 16, 7.0, True, False, ql.QuoteHandle(ql.SimpleQuote(0)), t0_Ois, True) + +# %% [markdown] +# The option npv is the markov model is: + +# %% +swaption4.setPricingEngine(floatEngineMarkov) +print(swaption4.NPV()) + +# %% [markdown] +# This is not too far from the GSR price. More interesting is the question how well the Markov model did its job to match our input smile. For this we look at the underlying npv under the Markov model. + +# %% +print(swaption4.underlyingValue()) + +# %% [markdown] +# This is closer to our terminal swap rate model price. A perfect match is not expected anyway, because the dynamics of the underlying rate in the linear model is different from the Markov model, of course. +# +# The Markov model can not only calibrate to the underlying smile, but has at the same time a sigma function (similar to the GSR model) which can be used to calibrate to a second instrument set. We do this here to calibrate to our coterminal ATM swaptions from above. +# +# This is a computationally demanding task, so depending on your machine, this may take a while now... + +# %% +for basket_i in basket: + ql.as_black_helper(basket_i).setPricingEngine(swaptionEngineMarkov) + +# %% +markov.calibrate(basket, method, ec) +show(calibration_data(basket, markov.volatility())) + +# %% [markdown] +# Now let's have a look again at the underlying pricing. It shouldn't have changed much, because the underlying smile is still matched. + +# %% +print(swaption4.underlyingValue()) + +# %% [markdown] +# This is close to the previous value as expected. +# +# As a final remark we note that the calibration to coterminal swaptions is not particularly reasonable here, because the European call rights are not well represented by these swaptions. Secondly, our CMS swaption is sensitive to the correlation between the 10y swap rate and the Euribor 6M rate. Since the Markov model is one factor it will most probably underestimate the market value by construction. +# +# That was it. Thank you for running this demo. Bye. diff --git a/quantlib/global-bootstrap.py b/quantlib/global-bootstrap.py new file mode 100644 index 0000000..73f6ad1 --- /dev/null +++ b/quantlib/global-bootstrap.py @@ -0,0 +1,168 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Global curve bootstrap +# +# Copyright (©) 2020 StatPro Italia srl +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% +import mxdevtool as ql +import pandas as pd + +# %% +interactive = "get_ipython" in globals() + +# %% [markdown] +# ### Setup + +# %% +today = ql.Date(26, 9, 2019) +spot = ql.TARGET().advance(today, 2, ql.Days) + +# %% +ql.Settings.instance().evaluationDate = today + +# %% [markdown] +# ### Data +# +# We'll use the following data as input: + +# %% +refMktRates = [ + -0.373, + -0.388, + -0.402, + -0.418, + -0.431, + -0.441, + -0.45, + -0.457, + -0.463, + -0.469, + -0.461, + -0.463, + -0.479, + -0.4511, + -0.45418, + -0.439, + -0.4124, + -0.37703, + -0.3335, + -0.28168, + -0.22725, + -0.1745, + -0.12425, + -0.07746, + 0.0385, + 0.1435, + 0.17525, + 0.17275, + 0.1515, + 0.1225, + 0.095, + 0.0644, +] + +# %% [markdown] +# ### Market instruments + +# %% +index = ql.Euribor6M() + +# %% [markdown] +# The first market rate is for the 6-months deposit... + +# %% +helpers = [ + ql.DepositRateHelper( + refMktRates[0] / 100.0, ql.Period(6, ql.Months), 2, ql.TARGET(), ql.ModifiedFollowing, True, ql.Actual360() + ) +] + +# %% [markdown] +# ...the next 12 are for FRAs... + +# %% +helpers += [ql.FraRateHelper(r / 100.0, i + 1, index) for i, r in enumerate(refMktRates[1:13])] + +# %% [markdown] +# ...and the others are swap rates. + +# %% +swapTenors = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 20, 25, 30, 35, 40, 45, 50] +helpers += [ + ql.SwapRateHelper( + r / 100.0, ql.Period(T, ql.Years), ql.TARGET(), ql.Annual, ql.ModifiedFollowing, ql.Thirty360(ql.Thirty360.BondBasis), index + ) + for r, T in zip(refMktRates[13:32], swapTenors) +] + +# %% [markdown] +# We'll also add a few synthetic helpers: + +# %% +additional_helpers = [ql.FraRateHelper(-0.004, 12 + i, index) for i in range(7)] +additional_dates = [ql.TARGET().advance(spot, 1 + i, ql.Months) for i in range(5)] + + +# %% [markdown] +# ### Global bootstrap +# +# This curve takes into account the market instruments, as well as the passed additional ones. + +# %% +curve = ql.GlobalLinearSimpleZeroCurve( + spot, helpers, ql.Actual365Fixed(), ql.GlobalBootstrap(additional_helpers, additional_dates, 1.0e-12) +) +curve.enableExtrapolation() + + +# %% [markdown] +# ### Report + +# %% +data = [] +for i, h in enumerate(helpers): + pillar = h.pillarDate() + + if i < 13: + day_counter = ql.Actual360() + compounding = ql.Simple + else: + day_counter = ql.Thirty360(ql.Thirty360.BondBasis) + compounding = ql.SimpleThenCompounded + + r = curve.zeroRate(pillar, day_counter, compounding, ql.Annual).rate() + data.append((pillar.to_date(), r * 100)) + +# %% +df = pd.DataFrame(data, columns=["pillar", "zero rate"]) +if not interactive: + print(df) +df diff --git a/quantlib/isda-engine.py b/quantlib/isda-engine.py new file mode 100644 index 0000000..eb65aa4 --- /dev/null +++ b/quantlib/isda-engine.py @@ -0,0 +1,178 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.11.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # ISDA CDS engine +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +import mxdevtool as ql +import pandas as pd + +interactive = 'get_ipython' in globals() + +trade_date = ql.Date(21,5,2009) +ql.Settings.instance().setEvaluationDate(trade_date) + +ql.IborCoupon.createAtParCoupons() + +dep_tenors = [1,2,3,6,9,12] +dep_quotes = [0.003081,0.005525,0.007163,0.012413,0.014,0.015488] +isdaRateHelpers = [ql.DepositRateHelper(dep_quotes[i], + dep_tenors[i]*ql.Period(ql.Monthly), + 2,ql.WeekendsOnly(), + ql.ModifiedFollowing, + False,ql.Actual360()) + for i in range(len(dep_tenors))] + +swap_tenors = [2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 25, 30] +swap_quotes = [0.011907, + 0.01699, + 0.021198, + 0.02444, + 0.026937, + 0.028967, + 0.030504, + 0.031719, + 0.03279, + 0.034535, + 0.036217, + 0.036981, + 0.037246, + 0.037605] + +isda_ibor = ql.IborIndex('IsdaIbor',3*ql.Period(ql.Monthly),2, + ql.USDCurrency(),ql.WeekendsOnly(), + ql.ModifiedFollowing,False,ql.Actual360()) +isdaRateHelpers = isdaRateHelpers + [ + ql.SwapRateHelper(swap_quotes[i],swap_tenors[i]*ql.Period(ql.Annual), + ql.WeekendsOnly(),ql.Semiannual,ql.ModifiedFollowing, + ql.Thirty360(ql.Thirty360.BondBasis),isda_ibor) + for i in range(len(swap_tenors))] + +spot_date = ql.WeekendsOnly().advance(trade_date, 2 * ql.Period(ql.Daily)) + +# Technically, the model requires the discount factor to be 1 at spot; +# but we can't do that and also have the discount curve extend back to +# the trade date. For the time being, we'll keep discount = 1 at trade. +# The results match anyway. + +swap_curve = ql.PiecewiseFlatForward(trade_date, isdaRateHelpers, ql.Actual365Fixed()) +discountCurve = ql.YieldTermStructureHandle(swap_curve) + +probabilityCurve = ql.RelinkableDefaultProbabilityTermStructureHandle() + +termDates = [ql.Date(20, 6, 2010), + ql.Date(20, 6, 2011), + ql.Date(20, 6, 2012), + ql.Date(20, 6, 2016), + ql.Date(20, 6, 2019)] + +spreads = [0.001, 0.1] +recoveries = [0.2, 0.4] + +markitValues = [97798.29358, #0.001 + 97776.11889, #0.001 + -914971.5977, #0.1 + -894985.6298, #0.1 + 186921.3594, #0.001 + 186839.8148, #0.001 + -1646623.672, #0.1 + -1579803.626, #0.1 + 274298.9203, + 274122.4725, + -2279730.93, + -2147972.527, + 592420.2297, + 591571.2294, + -3993550.206, + -3545843.418, + 797501.1422, + 795915.9787, + -4702034.688, + -4042340.999] + +tolerance = 1.0e-2 + +l = 0 +distance = 0 + +data = [] +upfront_date = ql.WeekendsOnly().advance(trade_date, 3 * ql.Period(ql.Daily)) +for termDate in termDates: + for spread in spreads: + for recovery in recoveries: + + cdsSchedule = ql.Schedule(trade_date, termDate, + 3*ql.Period(ql.Monthly), + ql.WeekendsOnly(), + ql.Following, ql.Unadjusted, + ql.DateGeneration.CDS, False) + + quotedTrade = ql.CreditDefaultSwap( + ql.Protection.Buyer,10000000,0,spread,cdsSchedule, + ql.Following,ql.Actual360(),True,True,trade_date, + upfront_date, ql.FaceValueClaim(), ql.Actual360(True)) + + h = quotedTrade.impliedHazardRate(0,discountCurve,ql.Actual365Fixed(), + recovery,1e-10, + ql.CreditDefaultSwap.ISDA) + + probabilityCurve.linkTo( + ql.FlatHazardRate(0,ql.WeekendsOnly(), + ql.QuoteHandle(ql.SimpleQuote(h)), + ql.Actual365Fixed())) + + engine = ql.IsdaCdsEngine(probabilityCurve,recovery,discountCurve) + conventionalTrade = ql.CreditDefaultSwap( + ql.Protection.Buyer,10000000,0,0.01,cdsSchedule, + ql.Following,ql.Actual360(),True,True,trade_date, + upfront_date, ql.FaceValueClaim(), ql.Actual360(True)) + conventionalTrade.setPricingEngine(engine) + + upfront = conventionalTrade.notional() * conventionalTrade.fairUpfront() + + data.append( + (termDate, + spread, + recovery, + h, + upfront, + markitValues[l], + abs(upfront-markitValues[l]), + abs(upfront-markitValues[l]). The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% [markdown] +# This notebook only works with Python 3, at least on Travis. + +# %% +import sys + +if sys.version_info.major < 3: + sys.exit() + +# %% +import mxdevtool as ql +from matplotlib import pyplot as plt +import numpy as np +import math + +# %matplotlib inline + +# %% +is_interactive = 'get_ipython' in globals() + +# %% +todaysDate = ql.Date(15, ql.May, 2019) +ql.Settings.instance().evaluationDate = todaysDate + +# %% +settlementDate = todaysDate + ql.Period(2, ql.Days) +exerciseDate = todaysDate + ql.Period(4, ql.Years) + +# %% +dc = ql.Actual365Fixed() + +spot = 100 +underlying = ql.QuoteHandle(ql.SimpleQuote(spot)) + +riskFreeRate = ql.YieldTermStructureHandle(ql.FlatForward(settlementDate, 0.05, dc)) +dividendYield = ql.YieldTermStructureHandle(ql.FlatForward(settlementDate, 0.025, dc)) + +vol = 0.30 +blackVol = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(settlementDate, ql.TARGET(), vol, dc)) + +# %% +localVol = ql.LocalVolSurface( + blackVol, + riskFreeRate, + dividendYield, + underlying, +) + +hestonProcess = ql.HestonProcess(riskFreeRate, dividendYield, underlying, 0.09, 1.0, 0.06, 0.4, -0.75) + +hestonModel = ql.HestonModel(hestonProcess) + +# %% +leverageFct = ql.HestonSLVMCModel( + localVol, hestonModel, ql.MTBrownianGeneratorFactory(1234), exerciseDate, 91 +).leverageFunction() + +# %% +tSteps = 40 +uSteps = 30 + +tv = np.linspace(0.1, dc.yearFraction(settlementDate, exerciseDate), tSteps) + +t = np.empty(tSteps * uSteps) +s = np.empty(tSteps * uSteps) +z = np.empty(tSteps * uSteps) + +for i in range(0, tSteps): + scale = min(4, math.exp(3 * math.sqrt(tv[i]) * vol)) + sv = np.linspace(spot / scale, spot * scale, uSteps) + + for j in range(0, uSteps): + idx = i * uSteps + j + t[idx] = tv[i] + s[idx] = math.log(sv[j]) + z[idx] = leverageFct.localVol(t[idx], sv[j]) + +# %% +fig = plt.figure(figsize=(12,8)) +ax = plt.axes(projection="3d") + +surf = ax.plot_trisurf(s, t, z, cmap=plt.cm.viridis, linewidth=0, antialiased=False, edgecolor="none") +ax.view_init(30, -120) + +ax.set_xlabel("ln(S)") +ax.set_ylabel("Time") +ax.text2D(0.225, 0.985, "Leverage Function with $\eta=1.0$", transform=ax.transAxes) + +fig.colorbar(surf, shrink=0.75, aspect=14) + +plt.show(block=False) + +# %% [markdown] +# When this is run as a Python script (i.e., from Travis), we need to close the figure in order to terminate. + +# %% +if not is_interactive: + plt.pause(3) + plt.close() diff --git a/quantlib/swap.py b/quantlib/swap.py new file mode 100644 index 0000000..8d7bd3e --- /dev/null +++ b/quantlib/swap.py @@ -0,0 +1,353 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Interest-rate swaps +# +# Copyright (©) 2004, 2005, 2006, 2007 StatPro Italia srl +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +# %% +import mxdevtool as ql + +# %% [markdown] +# ### Global data + +# %% +calendar = ql.TARGET() +todaysDate = ql.Date(6, ql.November, 2001) +ql.Settings.instance().evaluationDate = todaysDate +settlementDate = ql.Date(8, ql.November, 2001) + +# %% [markdown] +# ### Market quotes + +# %% +deposits = { + (3, ql.Months): 0.0363, +} + +# %% +FRAs = {(3, 6): 0.037125, (6, 9): 0.037125, (9, 12): 0.037125} + +# %% +futures = { + ql.Date(19, 12, 2001): 96.2875, + ql.Date(20, 3, 2002): 96.7875, + ql.Date(19, 6, 2002): 96.9875, + ql.Date(18, 9, 2002): 96.6875, + ql.Date(18, 12, 2002): 96.4875, + ql.Date(19, 3, 2003): 96.3875, + ql.Date(18, 6, 2003): 96.2875, + ql.Date(17, 9, 2003): 96.0875, +} + +# %% +swaps = { + (2, ql.Years): 0.037125, + (3, ql.Years): 0.0398, + (5, ql.Years): 0.0443, + (10, ql.Years): 0.05165, + (15, ql.Years): 0.055175, +} + +# %% [markdown] +# We'll convert them to `Quote` objects... + +# %% +for n, unit in deposits.keys(): + deposits[(n, unit)] = ql.SimpleQuote(deposits[(n, unit)]) +for n, m in FRAs.keys(): + FRAs[(n, m)] = ql.SimpleQuote(FRAs[(n, m)]) +for d in futures.keys(): + futures[d] = ql.SimpleQuote(futures[d]) +for n, unit in swaps.keys(): + swaps[(n, unit)] = ql.SimpleQuote(swaps[(n, unit)]) + +# %% [markdown] +# ...and build rate helpers. + +# %% +dayCounter = ql.Actual360() +settlementDays = 2 +depositHelpers = [ + ql.DepositRateHelper( + ql.QuoteHandle(deposits[(n, unit)]), + ql.Period(n, unit), + settlementDays, + calendar, + ql.ModifiedFollowing, + False, + dayCounter, + ) + for n, unit in deposits.keys() +] + +# %% +dayCounter = ql.Actual360() +settlementDays = 2 +fraHelpers = [ + ql.FraRateHelper( + ql.QuoteHandle(FRAs[(n, m)]), n, m, settlementDays, calendar, ql.ModifiedFollowing, False, dayCounter + ) + for n, m in FRAs.keys() +] + +# %% +dayCounter = ql.Actual360() +months = 3 +futuresHelpers = [ + ql.FuturesRateHelper( + ql.QuoteHandle(futures[d]), + d, + months, + calendar, + ql.ModifiedFollowing, + True, + dayCounter, + ql.QuoteHandle(ql.SimpleQuote(0.0)), + ) + for d in futures.keys() +] + +# %% [markdown] +# The discount curve for the swaps will come from elsewhere. A real application would use some kind of risk-free curve; here we're using a flat one for convenience. + +# %% +discountTermStructure = ql.YieldTermStructureHandle( + ql.FlatForward(settlementDate, 0.04, ql.Actual360())) + +# %% +settlementDays = 2 +fixedLegFrequency = ql.Annual +fixedLegTenor = ql.Period(1, ql.Years) +fixedLegAdjustment = ql.Unadjusted +fixedLegDayCounter = ql.Thirty360(ql.Thirty360.BondBasis) +floatingLegFrequency = ql.Quarterly +floatingLegTenor = ql.Period(3, ql.Months) +floatingLegAdjustment = ql.ModifiedFollowing +swapHelpers = [ + ql.SwapRateHelper( + ql.QuoteHandle(swaps[(n, unit)]), + ql.Period(n, unit), + calendar, + fixedLegFrequency, + fixedLegAdjustment, + fixedLegDayCounter, + ql.Euribor3M(), + ql.QuoteHandle(), + ql.Period("0D"), + discountTermStructure, + ) + for n, unit in swaps.keys() +] + +# %% [markdown] +# ### Term structure construction + +# %% +forecastTermStructure = ql.RelinkableYieldTermStructureHandle() + +# %% +helpers = depositHelpers + futuresHelpers + swapHelpers[1:] +depoFuturesSwapCurve = ql.PiecewiseFlatForward(settlementDate, helpers, ql.Actual360()) + +# %% +helpers = depositHelpers + fraHelpers + swapHelpers +depoFraSwapCurve = ql.PiecewiseFlatForward(settlementDate, helpers, ql.Actual360()) + +# %% [markdown] +# ### Swap pricing + +# %% +swapEngine = ql.DiscountingSwapEngine(discountTermStructure) + +# %% +nominal = 1000000 +length = 5 +maturity = calendar.advance(settlementDate, length, ql.Years) +payFixed = True + +# %% +fixedLegFrequency = ql.Annual +fixedLegAdjustment = ql.Unadjusted +fixedLegDayCounter = ql.Thirty360(ql.Thirty360.BondBasis) +fixedRate = 0.04 + +# %% +floatingLegFrequency = ql.Quarterly +spread = 0.0 +fixingDays = 2 +index = ql.Euribor3M(forecastTermStructure) +floatingLegAdjustment = ql.ModifiedFollowing +floatingLegDayCounter = index.dayCounter() + +# %% +fixedSchedule = ql.Schedule( + settlementDate, + maturity, + fixedLegTenor, + calendar, + fixedLegAdjustment, + fixedLegAdjustment, + ql.DateGeneration.Forward, + False, +) +floatingSchedule = ql.Schedule( + settlementDate, + maturity, + floatingLegTenor, + calendar, + floatingLegAdjustment, + floatingLegAdjustment, + ql.DateGeneration.Forward, + False, +) + +# %% [markdown] +# We'll build a 5-years swap starting spot... + +# %% +spot = ql.VanillaSwap( + ql.Swap.Payer, + nominal, + fixedSchedule, + fixedRate, + fixedLegDayCounter, + floatingSchedule, + index, + spread, + floatingLegDayCounter, +) +spot.setPricingEngine(swapEngine) + +# %% [markdown] +# ...and one starting 1 year forward. + +# %% +forwardStart = calendar.advance(settlementDate, 1, ql.Years) +forwardEnd = calendar.advance(forwardStart, length, ql.Years) +fixedSchedule = ql.Schedule( + forwardStart, + forwardEnd, + fixedLegTenor, + calendar, + fixedLegAdjustment, + fixedLegAdjustment, + ql.DateGeneration.Forward, + False, +) +floatingSchedule = ql.Schedule( + forwardStart, + forwardEnd, + floatingLegTenor, + calendar, + floatingLegAdjustment, + floatingLegAdjustment, + ql.DateGeneration.Forward, + False, +) + +# %% +forward = ql.VanillaSwap( + ql.Swap.Payer, + nominal, + fixedSchedule, + fixedRate, + fixedLegDayCounter, + floatingSchedule, + index, + spread, + floatingLegDayCounter, +) +forward.setPricingEngine(swapEngine) + +# %% [markdown] +# We'll price them both on the bootstrapped curves. +# +# This is the quoted 5-years market rate; we expect the fair rate of the spot swap to match it. + + +# %% +print(swaps[(5, ql.Years)].value()) + + +# %% +def show(swap): + print("NPV = %.2f" % swap.NPV()) + print("Fair spread = %.4f %%" % (swap.fairSpread()*100)) + print("Fair rate = %.4f %%" % (swap.fairRate()*100)) + + +# %% [markdown] +# These are the results for the 5-years spot swap on the deposit/futures/swap curve... + +# %% +forecastTermStructure.linkTo(depoFuturesSwapCurve) +show(spot) + +# %% [markdown] +# ...and these are on the deposit/fra/swap curve. + +# %% +forecastTermStructure.linkTo(depoFraSwapCurve) +show(spot) + +# %% [markdown] +# The same goes for the 1-year forward swap, except for the fair rate not matching the spot rate. + +# %% +forecastTermStructure.linkTo(depoFuturesSwapCurve) +show(forward) + +# %% +forecastTermStructure.linkTo(depoFraSwapCurve) +show(forward) + +# %% [markdown] +# Modifying the 5-years swap rate and repricing will change the results: + +# %% +swaps[(5, ql.Years)].setValue(0.046) + +# %% +forecastTermStructure.linkTo(depoFuturesSwapCurve) + +# %% +show(spot) + +# %% +show(forward) + +# %% +forecastTermStructure.linkTo(depoFraSwapCurve) + +# %% +show(spot) + +# %% +show(forward) diff --git a/quantlib/swing.py b/quantlib/swing.py new file mode 100644 index 0000000..d9b26af --- /dev/null +++ b/quantlib/swing.py @@ -0,0 +1,86 @@ +# --- +# jupyter: +# jupytext: +# formats: py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.4.2 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # Swing options +# +# Copyright (©) 2018 Klaus Spanderen +# +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - https://www.quantlib.org/ +# +# QuantLib is free software: you can redistribute it and/or modify it under the +# terms of the QuantLib license. You should have received a copy of the +# license along with this program; if not, please email +# . The license is also available online at +# . +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the license for more details. + +import mxdevtool as ql +import math + +todaysDate = ql.Date(30, ql.September, 2018) +ql.Settings.instance().evaluationDate = todaysDate +settlementDate = todaysDate +riskFreeRate = ql.FlatForward(settlementDate, 0.0, ql.Actual365Fixed()) +dividendYield = ql.FlatForward(settlementDate, 0.0, ql.Actual365Fixed()) +underlying = ql.SimpleQuote(30.0) +volatility = ql.BlackConstantVol(todaysDate, ql.TARGET(), 0.20, ql.Actual365Fixed()) + + +exerciseDates = [ql.Date(1, ql.January, 2019) + i for i in range(31)] + +swingOption = ql.VanillaSwingOption( + ql.VanillaForwardPayoff(ql.Option.Call, underlying.value()), ql.SwingExercise(exerciseDates), 0, len(exerciseDates) +) + +bsProcess = ql.BlackScholesMertonProcess( + ql.QuoteHandle(underlying), + ql.YieldTermStructureHandle(dividendYield), + ql.YieldTermStructureHandle(riskFreeRate), + ql.BlackVolTermStructureHandle(volatility), +) + +swingOption.setPricingEngine(ql.FdSimpleBSSwingEngine(bsProcess)) + +print("Black Scholes Price: %f" % swingOption.NPV()) + +x0 = 0.0 +x1 = 0.0 + +beta = 4.0 +eta = 4.0 +jumpIntensity = 1.0 +speed = 1.0 +volatility = 0.1 + +curveShape = [] +for d in exerciseDates: + t = ql.Actual365Fixed().yearFraction(todaysDate, d) + gs = ( + math.log(underlying.value()) + - volatility * volatility / (4 * speed) * (1 - math.exp(-2 * speed * t)) + - jumpIntensity / beta * math.log((eta - math.exp(-beta * t)) / (eta - 1.0)) + ) + curveShape.append((t, gs)) + +ouProcess = ql.ExtendedOrnsteinUhlenbeckProcess(speed, volatility, x0, lambda x: x0) +jProcess = ql.ExtOUWithJumpsProcess(ouProcess, x1, beta, jumpIntensity, eta) + +swingOption.setPricingEngine(ql.FdSimpleExtOUJumpSwingEngine(jProcess, riskFreeRate, 25, 25, 200, curveShape)) + +print("Kluge Model Price : %f" % swingOption.NPV()) diff --git a/quantlib/test/QuantLibTestSuite.py b/quantlib/test/QuantLibTestSuite.py new file mode 100644 index 0000000..0b8fa5e --- /dev/null +++ b/quantlib/test/QuantLibTestSuite.py @@ -0,0 +1,117 @@ +""" + Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl + Copyright (C) 2009 Joseph Malicki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import sys +import unittest + +from date import DateTest +from daycounters import DayCountersTest +from instruments import InstrumentTest +from marketelements import MarketElementTest +from integrals import IntegralTest +from solvers1d import Solver1DTest +from termstructures import TermStructureTest +from bonds import ( + FixedRateBondTest, + FixedRateBondKwargsTest, + AmortizingFixedRateBondTest) +from ratehelpers import ( + FixedRateBondHelperTest, + FxSwapRateHelperTest, + OISRateHelperTest, + CrossCurrencyBasisSwapRateHelperTest) +from cms import CmsTest +from assetswap import AssetSwapTest +from capfloor import CapFloorTest +from blackformula import BlackFormulaTest +from blackformula import BlackDeltaCalculatorTest +from iborindex import IborIndexTest +from sabr import SabrTest +from slv import SlvTest +from ode import OdeTest +from americanquantooption import AmericanQuantoOptionTest +from extrapolation import ExtrapolationTest +from fdm import FdmTest +from swaption import SwaptionTest +from volatilities import SviSmileSectionTest, SwaptionVolatilityCubeTest, AndreasenHugeVolatilityTest +from inflation import InflationTest +from coupons import ( + CashFlowsTest, + SubPeriodsCouponTest, + IborCouponTest, + OvernightCouponTest, + FixedRateCouponTest) +from options import OptionsTest +from swap import ZeroCouponSwapTest +from currencies import CurrencyTest + + +def test(): + import mxdevtool + print('testing QuantLib ' + mxdevtool.__version__) + + suite = unittest.TestSuite() + + suite.addTest(unittest.makeSuite(DateTest, 'test')) + suite.addTest(DayCountersTest()) + suite.addTest(unittest.makeSuite(InstrumentTest, 'test')) + suite.addTest(unittest.makeSuite(MarketElementTest, 'test')) + suite.addTest(unittest.makeSuite(IntegralTest, 'test')) + suite.addTest(Solver1DTest()) + suite.addTest(unittest.makeSuite(TermStructureTest, 'test')) + suite.addTest(unittest.makeSuite(FixedRateBondTest, 'test')) + suite.addTest(unittest.makeSuite(FixedRateBondKwargsTest, 'test')) + suite.addTest(unittest.makeSuite(AmortizingFixedRateBondTest, 'test')) + suite.addTest(unittest.makeSuite(FixedRateBondHelperTest, 'test')) + suite.addTest(unittest.makeSuite(CmsTest, 'test')) + suite.addTest(unittest.makeSuite(AssetSwapTest, 'test')) + suite.addTest(unittest.makeSuite(OISRateHelperTest, "test")) + suite.addTest(unittest.makeSuite(FxSwapRateHelperTest, 'test')) + suite.addTest(unittest.makeSuite(CapFloorTest, 'test')) + suite.addTest(unittest.makeSuite(BlackFormulaTest, 'test')) + suite.addTest(unittest.makeSuite(BlackDeltaCalculatorTest, 'test')) + suite.addTest(unittest.makeSuite(IborIndexTest, 'test')) + suite.addTest(unittest.makeSuite(SabrTest, 'test')) + suite.addTest(unittest.makeSuite(SlvTest, 'test')) + suite.addTest(unittest.makeSuite(OdeTest, 'test')) + suite.addTest(unittest.makeSuite(AmericanQuantoOptionTest, 'test')) + suite.addTest(unittest.makeSuite(ExtrapolationTest, 'test')) + suite.addTest(unittest.makeSuite(FdmTest, 'test')) + suite.addTest(unittest.makeSuite(SwaptionTest, "test")) + suite.addTest(unittest.makeSuite(SwaptionVolatilityCubeTest, 'test')) + suite.addTest(unittest.makeSuite(InflationTest, "test")) + suite.addTest(unittest.makeSuite(CrossCurrencyBasisSwapRateHelperTest, "test")) + suite.addTest(unittest.makeSuite(CashFlowsTest, "test")) + suite.addTest(unittest.makeSuite(SubPeriodsCouponTest, "test")) + suite.addTest(unittest.makeSuite(IborCouponTest, "test")) + suite.addTest(unittest.makeSuite(OvernightCouponTest, "test")) + suite.addTest(unittest.makeSuite(FixedRateCouponTest, "test")) + suite.addTest(unittest.makeSuite(OptionsTest, "test")) + suite.addTest(unittest.makeSuite(ZeroCouponSwapTest, "test")) + suite.addTest(unittest.makeSuite(CurrencyTest, "test")) + suite.addTest(unittest.makeSuite(SviSmileSectionTest, "test")) + suite.addTest(unittest.makeSuite(AndreasenHugeVolatilityTest, "test")) + + result = unittest.TextTestRunner(verbosity=2).run(suite) + + if not result.wasSuccessful(): + sys.exit(1) + + +if __name__ == '__main__': + test() diff --git a/quantlib/test/americanquantooption.py b/quantlib/test/americanquantooption.py new file mode 100644 index 0000000..368e9e4 --- /dev/null +++ b/quantlib/test/americanquantooption.py @@ -0,0 +1,103 @@ +""" + Copyright (C) 2019 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import mxdevtool as ql + +class AmericanQuantoOptionTest(unittest.TestCase): + def setUp(self): + self.today = ql.Date(21, ql.April, 2019) + self.dc = ql.Actual365Fixed() + ql.Settings.instance().evaluationDate = self.today + + self.domesticTS = ql.FlatForward(self.today, 0.025, self.dc) + self.foreignTS = ql.FlatForward(self.today, 0.075, self.dc) + self.fxVolTS = ql.BlackConstantVol(self.today, ql.TARGET(), 0.15, self.dc) + + self.quantoHelper = ql.FdmQuantoHelper( + self.domesticTS, self.foreignTS, self.fxVolTS, -0.75, 1.0) + + self.divYieldTS = ql.FlatForward(self.today, 0.03, self.dc) + + divDate = ql.DateVector() + divDate.push_back(self.today + ql.Period(6, ql.Months)) + + divAmount = ql.DoubleVector() + divAmount.push_back(8.0) + + maturityDate = self.today + ql.Period(9, ql.Months) + + self.option = ql.DividendVanillaOption( + ql.PlainVanillaPayoff(ql.Option.Call, 105), + ql.AmericanExercise(self.today, maturityDate), + divDate, + divAmount) + + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def testAmericanBSQuantoOption(self): + """ Testing American Black-Scholes quanto option """ + + volTS = ql.BlackConstantVol(self.today, ql.TARGET(), 0.3, self.dc) + + bsmProcess = ql.BlackScholesMertonProcess( + ql.QuoteHandle(ql.SimpleQuote(100)), + ql.YieldTermStructureHandle(self.divYieldTS), + ql.YieldTermStructureHandle(self.domesticTS), + ql.BlackVolTermStructureHandle(volTS)) + + fdmBlackScholesEngine = ql.FdBlackScholesVanillaEngine( + bsmProcess, self.quantoHelper, 100, 400, 1) + + self.option.setPricingEngine(fdmBlackScholesEngine) + + fdmPrice = self.option.NPV() + expected = 8.90611734 + + self.assertAlmostEqual(fdmPrice, expected, 3, + msg="Unable to reproduce American BS quanto option price.") + + + def testAmericanHestonQuantoOption(self): + """ Testing American Heston quanto option """ + + hestonModel = ql.HestonModel( + ql.HestonProcess( + ql.YieldTermStructureHandle(self.domesticTS), + ql.YieldTermStructureHandle(self.divYieldTS), + ql.QuoteHandle(ql.SimpleQuote(100)), + 0.09, 1.0, 0.09, 1e-4, 0.0)) + + fdmHestonVanillaEngine = ql.FdHestonVanillaEngine( + hestonModel, self.quantoHelper, 100, 400, 3, 1) + + self.option.setPricingEngine(fdmHestonVanillaEngine) + + fdmPrice = self.option.NPV() + expected = 8.90611734 + + self.assertAlmostEqual(fdmPrice, expected, 3, + msg="Unable to reproduce American Heston quanto option price.") + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(AmericanQuantoOptionTest,'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/assetswap.py b/quantlib/test/assetswap.py new file mode 100644 index 0000000..3b508fa --- /dev/null +++ b/quantlib/test/assetswap.py @@ -0,0 +1,5357 @@ +""" + Copyright (C) 2011 Lluis Pujol Bajador + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + + +class AssetSwapTest(unittest.TestCase): + def setUp(self): + # initial setup + self.termStructure = ql.RelinkableYieldTermStructureHandle() + self.swapSettlementDays = 2 + self.faceAmount = 100.0 + self.fixedConvention = ql.Unadjusted + self.compounding = ql.Continuous + self.fixedFrequency = ql.Annual + self.floatingFrequency = ql.Semiannual + self.iborIndex = ql.Euribor(ql.Period(self.floatingFrequency), self.termStructure) + self.calendar = self.iborIndex.fixingCalendar() + self.swapIndex = ql.SwapIndex( + "EuriborSwapIsdaFixA", + ql.Period(10, ql.Years), + self.swapSettlementDays, + self.iborIndex.currency(), + self.calendar, + ql.Period(self.fixedFrequency), + self.fixedConvention, + self.iborIndex.dayCounter(), + self.iborIndex, + ) + self.spread = 0.0 + self.nonnullspread = 0.003 + self.today = ql.Date(24, ql.April, 2007) + ql.Settings.instance().evaluationDate = self.today + self.termStructure.linkTo(ql.FlatForward(self.today, 0.05, ql.Actual365Fixed())) + self.yieldCurve = ql.FlatForward(self.today, 0.05, ql.Actual365Fixed()) + self.pricer = ql.BlackIborCouponPricer() + self.swaptionVolatilityStructure = ql.SwaptionVolatilityStructureHandle( + ql.ConstantSwaptionVolatility(self.today, ql.NullCalendar(), ql.Following, 0.2, ql.Actual365Fixed()) + ) + self.meanReversionQuote = ql.QuoteHandle(ql.SimpleQuote(0.01)) + self.cmspricer = ql.AnalyticHaganPricer( + self.swaptionVolatilityStructure, ql.GFunctionFactory.Standard, self.meanReversionQuote + ) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def testConsistency(self): + """Testing consistency between fair price and fair spread...""" + bondCalendar = ql.TARGET() + settlementDays = 3 + + ## Fixed Underlying bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + + bondSchedule = ql.Schedule( + ql.Date(4, ql.January, 2005), + ql.Date(4, ql.January, 2037), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + bond = ql.FixedRateBond( + settlementDays, + self.faceAmount, + bondSchedule, + [0.04], + ql.ActualActual(ql.ActualActual.ISDA), + ql.Following, + 100.0, + ql.Date(4, ql.January, 2005), + ) + + payFixedRate = True + bondPrice = 95.0 + isPar = True + parAssetSwap = ql.AssetSwap( + payFixedRate, + bond, + bondPrice, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + isPar, + ) + + swapEngine = ql.DiscountingSwapEngine( + self.termStructure, True, bond.settlementDate(), ql.Settings.instance().evaluationDate + ) + + parAssetSwap.setPricingEngine(swapEngine) + fairCleanPrice = parAssetSwap.fairCleanPrice() + fairSpread = parAssetSwap.fairSpread() + + tolerance = 1.0e-13 + + assetSwap2 = ql.AssetSwap( + payFixedRate, + bond, + fairCleanPrice, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + isPar, + ) + + assetSwap2.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap2.NPV()) > tolerance, + "\npar asset swap fair clean price doesn't zero the NPV: " + + "\n clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(fairCleanPrice) + + "\n NPV: " + + str(assetSwap2.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap2.fairCleanPrice() - fairCleanPrice) > tolerance, + "\npar asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(fairCleanPrice) + + "\n fair clean price: " + + str(assetSwap2.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap2.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap2.fairSpread() - self.spread) > tolerance, + "\npar asset swap fair spread doesn't equal input spread " + + "at zero NPV: " + + "\n input spread: " + + str(self.spread) + + "\n fair spread: " + + str(assetSwap2.fairSpread()) + + "\n NPV: " + + str(assetSwap2.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + assetSwap3 = ql.AssetSwap( + payFixedRate, bond, bondPrice, self.iborIndex, fairSpread, ql.Schedule(), self.iborIndex.dayCounter(), isPar + ) + assetSwap3.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap3.NPV()) > tolerance, + "\npar asset swap fair spread doesn't zero the NPV: " + + "\n spread: " + + str(self.spread) + + "\n fair spread: " + + str(fairSpread) + + "\n NPV: " + + str(assetSwap3.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap3.fairCleanPrice() - bondPrice) > tolerance, + "\npar asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(assetSwap3.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap3.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap3.fairSpread() - fairSpread) > tolerance, + "\npar asset swap fair spread doesn't equal input spread at" + + " zero NPV: " + + "\n input spread: " + + str(fairSpread) + + "\n fair spread: " + + str(assetSwap3.fairSpread()) + + "\n NPV: " + + str(assetSwap3.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + ## let's change the npv date + swapEngine = ql.DiscountingSwapEngine(self.termStructure, True, bond.settlementDate(), bond.settlementDate()) + + parAssetSwap.setPricingEngine(swapEngine) + ## fair clean price and fair spread should not change + self.assertFalse( + abs(parAssetSwap.fairCleanPrice() - fairCleanPrice) > tolerance, + "\npar asset swap fair clean price changed with NpvDate:" + + "\n expected clean price: " + + str(fairCleanPrice) + + "\n fair clean price: " + + str(parAssetSwap.fairCleanPrice()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(parAssetSwap.fairSpread() - fairSpread) > tolerance, + "\npar asset swap fair spread changed with NpvDate:" + + "\n expected spread: " + + str(fairSpread) + + "\n fair spread: " + + str(parAssetSwap.fairSpread()) + + "\n tolerance: " + + str(tolerance), + ) + + assetSwap2 = ql.AssetSwap( + payFixedRate, + bond, + fairCleanPrice, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + isPar, + ) + assetSwap2.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap2.NPV()) > tolerance, + "\npar asset swap fair clean price doesn't zero the NPV: " + + "\n clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(fairCleanPrice) + + "\n NPV: " + + str(assetSwap2.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap2.fairCleanPrice() - fairCleanPrice) > tolerance, + "\npar asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(fairCleanPrice) + + "\n fair clean price: " + + str(assetSwap2.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap2.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap2.fairSpread() - self.spread) > tolerance, + "\npar asset swap fair spread doesn't equal input spread at zero NPV: " + + "\n input spread: " + + str(self.spread) + + "\n fair spread: " + + str(assetSwap2.fairSpread()) + + "\n NPV: " + + str(assetSwap2.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + assetSwap3 = ql.AssetSwap( + payFixedRate, bond, bondPrice, self.iborIndex, fairSpread, ql.Schedule(), self.iborIndex.dayCounter(), isPar + ) + assetSwap3.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap3.NPV()) > tolerance, + "\npar asset swap fair spread doesn't zero the NPV: " + + "\n spread: " + + str(self.spread) + + "\n fair spread: " + + str(fairSpread) + + "\n NPV: " + + str(assetSwap3.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap3.fairCleanPrice() - bondPrice) > tolerance, + "\npar asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(assetSwap3.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap3.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap3.fairSpread() - fairSpread) > tolerance, + "\npar asset swap fair spread doesn't equal input spread at zero NPV: " + + "\n input spread: " + + str(fairSpread) + + "\n fair spread: " + + str(assetSwap3.fairSpread()) + + "\n NPV: " + + str(assetSwap3.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + ## now market asset swap + isPar = False + mktAssetSwap = ql.AssetSwap( + payFixedRate, + bond, + bondPrice, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + isPar, + ) + + swapEngine = ql.DiscountingSwapEngine( + self.termStructure, True, bond.settlementDate(), ql.Settings.instance().evaluationDate + ) + + mktAssetSwap.setPricingEngine(swapEngine) + fairCleanPrice = mktAssetSwap.fairCleanPrice() + fairSpread = mktAssetSwap.fairSpread() + + assetSwap4 = ql.AssetSwap( + payFixedRate, + bond, + fairCleanPrice, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + isPar, + ) + assetSwap4.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap4.NPV()) > tolerance, + "\nmarket asset swap fair clean price doesn't zero the NPV: " + + "\n clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(fairCleanPrice) + + "\n NPV: " + + str(assetSwap4.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap4.fairCleanPrice() - fairCleanPrice) > tolerance, + "\nmarket asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(fairCleanPrice) + + "\n fair clean price: " + + str(assetSwap4.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap4.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap4.fairSpread() - self.spread) > tolerance, + "\nmarket asset swap fair spread doesn't equal input spread" + + " at zero NPV: " + + "\n input spread: " + + str(self.spread) + + "\n fair spread: " + + str(assetSwap4.fairSpread()) + + "\n NPV: " + + str(assetSwap4.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + assetSwap5 = ql.AssetSwap( + payFixedRate, bond, bondPrice, self.iborIndex, fairSpread, ql.Schedule(), self.iborIndex.dayCounter(), isPar + ) + assetSwap5.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap5.NPV()) > tolerance, + "\nmarket asset swap fair spread doesn't zero the NPV: " + + "\n spread: " + + str(self.spread) + + "\n fair spread: " + + str(fairSpread) + + "\n NPV: " + + str(assetSwap5.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap5.fairCleanPrice() - bondPrice) > tolerance, + "\nmarket asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(assetSwap5.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap5.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap5.fairSpread() - fairSpread) > tolerance, + "\nmarket asset swap fair spread doesn't equal input spread at zero NPV: " + + "\n input spread: " + + str(fairSpread) + + "\n fair spread: " + + str(assetSwap5.fairSpread()) + + "\n NPV: " + + str(assetSwap5.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + ## let's change the npv date + swapEngine = ql.DiscountingSwapEngine(self.termStructure, True, bond.settlementDate(), bond.settlementDate()) + + mktAssetSwap.setPricingEngine(swapEngine) + ## fair clean price and fair spread should not change + self.assertFalse( + abs(mktAssetSwap.fairCleanPrice() - fairCleanPrice) > tolerance, + "\nmarket asset swap fair clean price changed with NpvDate:" + + "\n expected clean price: " + + str(fairCleanPrice) + + "\n fair clean price: " + + str(mktAssetSwap.fairCleanPrice()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(mktAssetSwap.fairSpread() - fairSpread) > tolerance, + "\nmarket asset swap fair spread changed with NpvDate:" + + "\n expected spread: " + + str(fairSpread) + + "\n fair spread: " + + str(mktAssetSwap.fairSpread()) + + "\n tolerance: " + + str(tolerance), + ) + + assetSwap4 = ql.AssetSwap( + payFixedRate, + bond, + fairCleanPrice, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + isPar, + ) + assetSwap4.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap4.NPV()) > tolerance, + "\nmarket asset swap fair clean price doesn't zero the NPV: " + + "\n clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(fairCleanPrice) + + "\n NPV: " + + str(assetSwap4.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap4.fairCleanPrice() - fairCleanPrice) > tolerance, + "\nmarket asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(fairCleanPrice) + + "\n fair clean price: " + + str(assetSwap4.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap4.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap4.fairSpread() - self.spread) > tolerance, + "\nmarket asset swap fair spread doesn't equal input spread at zero NPV: " + + "\n input spread: " + + str(self.spread) + + "\n fair spread: " + + str(assetSwap4.fairSpread()) + + "\n NPV: " + + str(assetSwap4.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + assetSwap5 = ql.AssetSwap( + payFixedRate, bond, bondPrice, self.iborIndex, fairSpread, ql.Schedule(), self.iborIndex.dayCounter(), isPar + ) + assetSwap5.setPricingEngine(swapEngine) + self.assertFalse( + abs(assetSwap5.NPV()) > tolerance, + "\nmarket asset swap fair spread doesn't zero the NPV: " + + "\n spread: " + + str(self.spread) + + "\n fair spread: " + + str(fairSpread) + + "\n NPV: " + + str(assetSwap5.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap5.fairCleanPrice() - bondPrice) > tolerance, + "\nmarket asset swap fair clean price doesn't equal input " + + "clean price at zero NPV: " + + "\n input clean price: " + + str(bondPrice) + + "\n fair clean price: " + + str(assetSwap5.fairCleanPrice()) + + "\n NPV: " + + str(assetSwap5.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + self.assertFalse( + abs(assetSwap5.fairSpread() - fairSpread) > tolerance, + "\nmarket asset swap fair spread doesn't equal input spread at zero NPV: " + + "\n input spread: " + + str(fairSpread) + + "\n fair spread: " + + str(assetSwap5.fairSpread()) + + "\n NPV: " + + str(assetSwap5.NPV()) + + "\n tolerance: " + + str(tolerance), + ) + + def testImpliedValue(self): + """Testing implied bond value against asset-swap fair price with null spread...""" + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + payFixedRate = True + parAssetSwap = True + inArrears = False + + ## Fixed Underlying bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + + fixedBondSchedule1 = ql.Schedule( + ql.Date(4, ql.January, 2005), + ql.Date(4, ql.January, 2037), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBond1 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule1, + [0.04], + ql.ActualActual(ql.ActualActual.ISDA), + ql.Following, + 100.0, + ql.Date(4, ql.January, 2005), + ) + + bondEngine = ql.DiscountingBondEngine(self.termStructure) + swapEngine = ql.DiscountingSwapEngine(self.termStructure, False) + fixedBond1.setPricingEngine(bondEngine) + + fixedBondPrice1 = fixedBond1.cleanPrice() + fixedBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondAssetSwap1.setPricingEngine(swapEngine) + fixedBondAssetSwapPrice1 = fixedBondAssetSwap1.fairCleanPrice() + tolerance = 1.0e-13 + + error1 = abs(fixedBondAssetSwapPrice1 - fixedBondPrice1) + + self.assertFalse( + error1 > tolerance, + "wrong zero spread asset swap price for fixed bond:" + + "\n bond's clean price: " + + str(fixedBondPrice1) + + "\n asset swap fair price: " + + str(fixedBondAssetSwapPrice1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + ## Fixed Underlying bond (Isin: IT0006527060 IBRD 5 02/05/19) + ## maturity occurs on a business day + + fixedBondSchedule2 = ql.Schedule( + ql.Date(5, ql.February, 2005), + ql.Date(5, ql.February, 2019), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBond2 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule2, + [0.05], + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + 100.0, + ql.Date(5, ql.February, 2005), + ) + + fixedBond2.setPricingEngine(bondEngine) + + fixedBondPrice2 = fixedBond2.cleanPrice() + fixedBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondAssetSwap2.setPricingEngine(swapEngine) + fixedBondAssetSwapPrice2 = fixedBondAssetSwap2.fairCleanPrice() + error2 = abs(fixedBondAssetSwapPrice2 - fixedBondPrice2) + + self.assertFalse( + error2 > tolerance, + "wrong zero spread asset swap price for fixed bond:" + + "\n bond's clean price: " + + str(fixedBondPrice2) + + "\n asset swap fair price: " + + str(fixedBondAssetSwapPrice2) + + "\n error: " + + str(error2) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ## maturity doesn't occur on a business day + + floatingBondSchedule1 = ql.Schedule( + ql.Date(29, ql.September, 2003), + ql.Date(29, ql.September, 2013), + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + + floatingBond1 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + fixingDays, + [1], + [0.0056], + [], + [], + inArrears, + 100.0, + ql.Date(29, ql.September, 2003), + ) + + floatingBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + floatingBondPrice1 = floatingBond1.cleanPrice() + floatingBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondAssetSwap1.setPricingEngine(swapEngine) + floatingBondAssetSwapPrice1 = floatingBondAssetSwap1.fairCleanPrice() + error3 = abs(floatingBondAssetSwapPrice1 - floatingBondPrice1) + + self.assertFalse( + error3 > tolerance, + "wrong zero spread asset swap price for floater:" + + "\n bond's clean price: " + + str(floatingBondPrice1) + + "\n asset swap fair price: " + + str(floatingBondAssetSwapPrice1) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: XS0090566539 COE 0 09/24/18) + ## maturity occurs on a business day + + floatingBondSchedule2 = ql.Schedule( + ql.Date(24, ql.September, 2004), + ql.Date(24, ql.September, 2018), + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBond2 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + fixingDays, + [1], + [0.0025], + [], + [], + inArrears, + 100.0, + ql.Date(24, ql.September, 2004), + ) + + floatingBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + currentCoupon = 0.04013 + 0.0025 + floatingCurrentCoupon = floatingBond2.nextCouponRate() + error4 = abs(floatingCurrentCoupon - currentCoupon) + self.assertFalse( + error4 > tolerance, + "wrong current coupon is returned for floater bond:" + + "\n bond's calculated current coupon: " + + str(currentCoupon) + + "\n current coupon asked to the bond: " + + str(floatingCurrentCoupon) + + "\n error: " + + str(error4) + + "\n tolerance: " + + str(tolerance), + ) + + floatingBondPrice2 = floatingBond2.cleanPrice() + floatingBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondAssetSwap2.setPricingEngine(swapEngine) + floatingBondAssetSwapPrice2 = floatingBondAssetSwap2.fairCleanPrice() + error5 = abs(floatingBondAssetSwapPrice2 - floatingBondPrice2) + + self.assertFalse( + error5 > tolerance, + "wrong zero spread asset swap price for floater:" + + "\n bond's clean price: " + + str(floatingBondPrice2) + + "\n asset swap fair price: " + + str(floatingBondAssetSwapPrice2) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0228052402 CRDIT 0 8/22/20) + ## maturity doesn't occur on a business day + + cmsBondSchedule1 = ql.Schedule( + ql.Date(22, ql.August, 2005), + ql.Date(22, ql.August, 2020), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBond1 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [1.0], + [0.0], + [0.055], + [0.025], + inArrears, + 100.0, + ql.Date(22, ql.August, 2005), + ) + + cmsBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondPrice1 = cmsBond1.cleanPrice() + cmsBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondAssetSwap1.setPricingEngine(swapEngine) + cmsBondAssetSwapPrice1 = cmsBondAssetSwap1.fairCleanPrice() + error6 = abs(cmsBondAssetSwapPrice1 - cmsBondPrice1) + + self.assertFalse( + error6 > tolerance, + "wrong zero spread asset swap price for cms bond:" + + "\n bond's clean price: " + + str(cmsBondPrice1) + + "\n asset swap fair price: " + + str(cmsBondAssetSwapPrice1) + + "\n error: " + + str(error6) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ## maturity occurs on a business day + + cmsBondSchedule2 = ql.Schedule( + ql.Date(6, ql.May, 2005), + ql.Date(6, ql.May, 2015), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBond2 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [0.84], + [0.0], + [], + [], + inArrears, + 100.0, + ql.Date(6, ql.May, 2005), + ) + + cmsBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondPrice2 = cmsBond2.cleanPrice() + cmsBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondAssetSwap2.setPricingEngine(swapEngine) + cmsBondAssetSwapPrice2 = cmsBondAssetSwap2.fairCleanPrice() + error7 = abs(cmsBondAssetSwapPrice2 - cmsBondPrice2) + + self.assertFalse( + error7 > tolerance, + "wrong zero spread asset swap price for cms bond:" + + "\n bond's clean price: " + + str(cmsBondPrice2) + + "\n asset swap fair price: " + + str(cmsBondAssetSwapPrice2) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + + zeroCpnBond1 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(20, ql.December, 2015), + ql.Following, + 100.0, + ql.Date(19, ql.December, 1985), + ) + + zeroCpnBond1.setPricingEngine(bondEngine) + + zeroCpnBondPrice1 = zeroCpnBond1.cleanPrice() + zeroCpnAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnAssetSwap1.setPricingEngine(swapEngine) + zeroCpnBondAssetSwapPrice1 = zeroCpnAssetSwap1.fairCleanPrice() + error8 = abs(cmsBondAssetSwapPrice1 - cmsBondPrice1) + + self.assertFalse( + error8 > tolerance, + "wrong zero spread asset swap price for zero cpn bond:" + + "\n bond's clean price: " + + str(zeroCpnBondPrice1) + + "\n asset swap fair price: " + + str(zeroCpnBondAssetSwapPrice1) + + "\n error: " + + str(error8) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity occurs on a business day + + zeroCpnBond2 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(17, ql.February, 2028), + ql.Following, + 100.0, + ql.Date(17, ql.February, 1998), + ) + + zeroCpnBond2.setPricingEngine(bondEngine) + + zeroCpnBondPrice2 = zeroCpnBond2.cleanPrice() + zeroCpnAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnAssetSwap2.setPricingEngine(swapEngine) + zeroCpnBondAssetSwapPrice2 = zeroCpnAssetSwap2.fairCleanPrice() + error9 = abs(cmsBondAssetSwapPrice2 - cmsBondPrice2) + + self.assertFalse( + error9 > tolerance, + "wrong zero spread asset swap price for zero cpn bond:" + + "\n bond's clean price: " + + str(zeroCpnBondPrice2) + + "\n asset swap fair price: " + + str(zeroCpnBondAssetSwapPrice2) + + "\n error: " + + str(error9) + + "\n tolerance: " + + str(tolerance), + ) + + def testMarketASWSpread(self): + """Testing relationship between market asset swap and par asset swap...""" + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + payFixedRate = True + parAssetSwap = True + mktAssetSwap = False + inArrears = False + + ## Fixed Underlying bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + + fixedBondSchedule1 = ql.Schedule( + ql.Date(4, ql.January, 2005), + ql.Date(4, ql.January, 2037), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBond1 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule1, + [0.04], + ql.ActualActual(ql.ActualActual.ISDA), + ql.Following, + 100.0, + ql.Date(4, ql.January, 2005), + ) + + bondEngine = ql.DiscountingBondEngine(self.termStructure) + swapEngine = ql.DiscountingSwapEngine(self.termStructure, False) + fixedBond1.setPricingEngine(bondEngine) + + fixedBondMktPrice1 = 89.22 ## market price observed on 7th June 2007 + fixedBondMktFullPrice1 = fixedBondMktPrice1 + fixedBond1.accruedAmount() + fixedBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondParAssetSwap1.setPricingEngine(swapEngine) + fixedBondParAssetSwapSpread1 = fixedBondParAssetSwap1.fairSpread() + fixedBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + fixedBondMktAssetSwap1.setPricingEngine(swapEngine) + fixedBondMktAssetSwapSpread1 = fixedBondMktAssetSwap1.fairSpread() + + tolerance = 1.0e-13 + error1 = abs(fixedBondMktAssetSwapSpread1 - 100 * fixedBondParAssetSwapSpread1 / fixedBondMktFullPrice1) + + self.assertFalse( + error1 > tolerance, + "wrong asset swap spreads for fixed bond:" + + "\n market ASW spread: " + + str(fixedBondMktAssetSwapSpread1) + + "\n par ASW spread: " + + str(fixedBondParAssetSwapSpread1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + ## Fixed Underlying bond (Isin: IT0006527060 IBRD 5 02/05/19) + ## maturity occurs on a business day + + fixedBondSchedule2 = ql.Schedule( + ql.Date(5, ql.February, 2005), + ql.Date(5, ql.February, 2019), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBond2 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule2, + [0.05], + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + 100.0, + ql.Date(5, ql.February, 2005), + ) + + fixedBond2.setPricingEngine(bondEngine) + + fixedBondMktPrice2 = 99.98 ## market price observed on 7th June 2007 + fixedBondMktFullPrice2 = fixedBondMktPrice2 + fixedBond2.accruedAmount() + fixedBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondParAssetSwap2.setPricingEngine(swapEngine) + fixedBondParAssetSwapSpread2 = fixedBondParAssetSwap2.fairSpread() + fixedBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + fixedBondMktAssetSwap2.setPricingEngine(swapEngine) + fixedBondMktAssetSwapSpread2 = fixedBondMktAssetSwap2.fairSpread() + error2 = abs(fixedBondMktAssetSwapSpread2 - 100 * fixedBondParAssetSwapSpread2 / fixedBondMktFullPrice2) + + self.assertFalse( + error2 > tolerance, + "wrong asset swap spreads for fixed bond:" + + "\n market ASW spread: " + + str(fixedBondMktAssetSwapSpread2) + + "\n par ASW spread: " + + str(fixedBondParAssetSwapSpread2) + + "\n error: " + + str(error2) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ## maturity doesn't occur on a business day + + floatingBondSchedule1 = ql.Schedule( + ql.Date(29, ql.September, 2003), + ql.Date(29, ql.September, 2013), + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + + floatingBond1 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + fixingDays, + [1], + [0.0056], + [], + [], + inArrears, + 100.0, + ql.Date(29, ql.September, 2003), + ) + + floatingBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + ## market price observed on 7th June 2007 + floatingBondMktPrice1 = 101.64 + floatingBondMktFullPrice1 = floatingBondMktPrice1 + floatingBond1.accruedAmount() + floatingBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondParAssetSwap1.setPricingEngine(swapEngine) + floatingBondParAssetSwapSpread1 = floatingBondParAssetSwap1.fairSpread() + floatingBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + floatingBondMktAssetSwap1.setPricingEngine(swapEngine) + floatingBondMktAssetSwapSpread1 = floatingBondMktAssetSwap1.fairSpread() + error3 = abs( + floatingBondMktAssetSwapSpread1 - 100 * floatingBondParAssetSwapSpread1 / floatingBondMktFullPrice1 + ) + + self.assertFalse( + error3 > tolerance, + "wrong asset swap spreads for floating bond:" + + "\n market ASW spread: " + + str(floatingBondMktAssetSwapSpread1) + + "\n par ASW spread: " + + str(floatingBondParAssetSwapSpread1) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: XS0090566539 COE 0 09/24/18) + ## maturity occurs on a business day + + floatingBondSchedule2 = ql.Schedule( + ql.Date(24, ql.September, 2004), + ql.Date(24, ql.September, 2018), + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBond2 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + fixingDays, + [1], + [0.0025], + [], + [], + inArrears, + 100.0, + ql.Date(24, ql.September, 2004), + ) + + floatingBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + ## market price observed on 7th June 2007 + floatingBondMktPrice2 = 101.248 + floatingBondMktFullPrice2 = floatingBondMktPrice2 + floatingBond2.accruedAmount() + floatingBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondParAssetSwap2.setPricingEngine(swapEngine) + floatingBondParAssetSwapSpread2 = floatingBondParAssetSwap2.fairSpread() + floatingBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + floatingBondMktAssetSwap2.setPricingEngine(swapEngine) + floatingBondMktAssetSwapSpread2 = floatingBondMktAssetSwap2.fairSpread() + error4 = abs( + floatingBondMktAssetSwapSpread2 - 100 * floatingBondParAssetSwapSpread2 / floatingBondMktFullPrice2 + ) + + self.assertFalse( + error4 > tolerance, + "wrong asset swap spreads for floating bond:" + + "\n market ASW spread: " + + str(floatingBondMktAssetSwapSpread2) + + "\n par ASW spread: " + + str(floatingBondParAssetSwapSpread2) + + "\n error: " + + str(error4) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0228052402 CRDIT 0 8/22/20) + ## maturity doesn't occur on a business day + + cmsBondSchedule1 = ql.Schedule( + ql.Date(22, ql.August, 2005), + ql.Date(22, ql.August, 2020), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBond1 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [1, 1.0], + [0.0], + [0.055], + [0.025], + inArrears, + 100.0, + ql.Date(22, ql.August, 2005), + ) + + cmsBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondMktPrice1 = 88.45 ## market price observed on 7th June 2007 + cmsBondMktFullPrice1 = cmsBondMktPrice1 + cmsBond1.accruedAmount() + cmsBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondParAssetSwap1.setPricingEngine(swapEngine) + cmsBondParAssetSwapSpread1 = cmsBondParAssetSwap1.fairSpread() + cmsBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + cmsBondMktAssetSwap1.setPricingEngine(swapEngine) + cmsBondMktAssetSwapSpread1 = cmsBondMktAssetSwap1.fairSpread() + error5 = abs(cmsBondMktAssetSwapSpread1 - 100 * cmsBondParAssetSwapSpread1 / cmsBondMktFullPrice1) + + self.assertFalse( + error5 > tolerance, + "wrong asset swap spreads for cms bond:" + + "\n market ASW spread: " + + str(cmsBondMktAssetSwapSpread1) + + "\n par ASW spread: " + + str(cmsBondParAssetSwapSpread1) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ## maturity occurs on a business day + + cmsBondSchedule2 = ql.Schedule( + ql.Date(6, ql.May, 2005), + ql.Date(6, ql.May, 2015), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBond2 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [0.84], + [0.0], + [], + [], + inArrears, + 100.0, + ql.Date(6, ql.May, 2005), + ) + + cmsBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondMktPrice2 = 94.08 ## market price observed on 7th June 2007 + cmsBondMktFullPrice2 = cmsBondMktPrice2 + cmsBond2.accruedAmount() + cmsBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondParAssetSwap2.setPricingEngine(swapEngine) + cmsBondParAssetSwapSpread2 = cmsBondParAssetSwap2.fairSpread() + cmsBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + cmsBondMktAssetSwap2.setPricingEngine(swapEngine) + cmsBondMktAssetSwapSpread2 = cmsBondMktAssetSwap2.fairSpread() + error6 = abs(cmsBondMktAssetSwapSpread2 - 100 * cmsBondParAssetSwapSpread2 / cmsBondMktFullPrice2) + + self.assertFalse( + error6 > tolerance, + "wrong asset swap spreads for cms bond:" + + "\n market ASW spread: " + + str(cmsBondMktAssetSwapSpread2) + + "\n par ASW spread: " + + str(cmsBondParAssetSwapSpread2) + + "\n error: " + + str(error6) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + + zeroCpnBond1 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(20, ql.December, 2015), + ql.Following, + 100.0, + ql.Date(19, ql.December, 1985), + ) + + zeroCpnBond1.setPricingEngine(bondEngine) + + ## market price observed on 12th June 2007 + zeroCpnBondMktPrice1 = 70.436 + zeroCpnBondMktFullPrice1 = zeroCpnBondMktPrice1 + zeroCpnBond1.accruedAmount() + zeroCpnBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondParAssetSwap1.setPricingEngine(swapEngine) + zeroCpnBondParAssetSwapSpread1 = zeroCpnBondParAssetSwap1.fairSpread() + zeroCpnBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + zeroCpnBondMktAssetSwap1.setPricingEngine(swapEngine) + zeroCpnBondMktAssetSwapSpread1 = zeroCpnBondMktAssetSwap1.fairSpread() + error7 = abs(zeroCpnBondMktAssetSwapSpread1 - 100 * zeroCpnBondParAssetSwapSpread1 / zeroCpnBondMktFullPrice1) + + self.assertFalse( + error7 > tolerance, + "wrong asset swap spreads for zero cpn bond:" + + "\n market ASW spread: " + + str(zeroCpnBondMktAssetSwapSpread1) + + "\n par ASW spread: " + + str(zeroCpnBondParAssetSwapSpread1) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity occurs on a business day + + zeroCpnBond2 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(17, ql.February, 2028), + ql.Following, + 100.0, + ql.Date(17, ql.February, 1998), + ) + + zeroCpnBond2.setPricingEngine(bondEngine) + + ## zeroCpnBondPrice2 = zeroCpnBond2.cleanPrice() + + ## market price observed on 12th June 2007 + zeroCpnBondMktPrice2 = 35.160 + zeroCpnBondMktFullPrice2 = zeroCpnBondMktPrice2 + zeroCpnBond2.accruedAmount() + zeroCpnBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondParAssetSwap2.setPricingEngine(swapEngine) + zeroCpnBondParAssetSwapSpread2 = zeroCpnBondParAssetSwap2.fairSpread() + zeroCpnBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + zeroCpnBondMktAssetSwap2.setPricingEngine(swapEngine) + zeroCpnBondMktAssetSwapSpread2 = zeroCpnBondMktAssetSwap2.fairSpread() + error8 = abs(zeroCpnBondMktAssetSwapSpread2 - 100 * zeroCpnBondParAssetSwapSpread2 / zeroCpnBondMktFullPrice2) + + self.assertFalse( + error8 > tolerance, + "wrong asset swap spreads for zero cpn bond:" + + "\n market ASW spread: " + + str(zeroCpnBondMktAssetSwapSpread2) + + "\n par ASW spread: " + + str(zeroCpnBondParAssetSwapSpread2) + + "\n error: " + + str(error8) + + "\n tolerance: " + + str(tolerance), + ) + + def testZSpread(self): + """Testing clean and dirty price with null Z-spread against theoretical prices...""" + + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + inArrears = False + + ## Fixed bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + + fixedBondSchedule1 = ql.Schedule( + ql.Date(4, ql.January, 2005), + ql.Date(4, ql.January, 2037), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBond1 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule1, + [0.04], + ql.ActualActual(ql.ActualActual.ISDA), + ql.Following, + 100.0, + ql.Date(4, ql.January, 2005), + ) + + bondEngine = ql.DiscountingBondEngine(self.termStructure) + fixedBond1.setPricingEngine(bondEngine) + + fixedBondImpliedValue1 = fixedBond1.cleanPrice() + fixedBondSettlementDate1 = fixedBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YC... + fixedBondCleanPrice1 = ql.cleanPriceFromZSpread( + fixedBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + fixedBondSettlementDate1, + ) + + tolerance = 1.0e-13 + error1 = abs(fixedBondImpliedValue1 - fixedBondCleanPrice1) + self.assertFalse( + error1 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(fixedBondImpliedValue1) + + "\n par asset swap spread: " + + str(fixedBondCleanPrice1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + ## Fixed bond (Isin: IT0006527060 IBRD 5 02/05/19) + ## maturity occurs on a business day + + fixedBondSchedule2 = ql.Schedule( + ql.Date(5, ql.February, 2005), + ql.Date(5, ql.February, 2019), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBond2 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule2, + [0.05], + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + 100.0, + ql.Date(5, ql.February, 2005), + ) + + fixedBond2.setPricingEngine(bondEngine) + + fixedBondImpliedValue2 = fixedBond2.cleanPrice() + fixedBondSettlementDate2 = fixedBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + fixedBondCleanPrice2 = ql.cleanPriceFromZSpread( + fixedBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + fixedBondSettlementDate2, + ) + error3 = abs(fixedBondImpliedValue2 - fixedBondCleanPrice2) + self.assertFalse( + error3 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(fixedBondImpliedValue2) + + "\n par asset swap spread: " + + str(fixedBondCleanPrice2) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ## maturity doesn't occur on a business day + + floatingBondSchedule1 = ql.Schedule( + ql.Date(29, ql.September, 2003), + ql.Date(29, ql.September, 2013), + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + + floatingBond1 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + fixingDays, + [1], + [0.0056], + [], + [], + inArrears, + 100.0, + ql.Date(29, ql.September, 2003), + ) + + floatingBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + floatingBondImpliedValue1 = floatingBond1.cleanPrice() + floatingBondSettlementDate1 = floatingBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + floatingBondCleanPrice1 = ql.cleanPriceFromZSpread( + floatingBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Semiannual, + floatingBondSettlementDate1, + ) + error5 = abs(floatingBondImpliedValue1 - floatingBondCleanPrice1) + self.assertFalse( + error5 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(floatingBondImpliedValue1) + + "\n par asset swap spread: " + + str(floatingBondCleanPrice1) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN bond (Isin: XS0090566539 COE 0 09/24/18) + ## maturity occurs on a business day + + floatingBondSchedule2 = ql.Schedule( + ql.Date(24, ql.September, 2004), + ql.Date(24, ql.September, 2018), + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBond2 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + fixingDays, + [1], + [0.0025], + [], + [], + inArrears, + 100.0, + ql.Date(24, ql.September, 2004), + ) + + floatingBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + floatingBondImpliedValue2 = floatingBond2.cleanPrice() + floatingBondSettlementDate2 = floatingBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + floatingBondCleanPrice2 = ql.cleanPriceFromZSpread( + floatingBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Semiannual, + floatingBondSettlementDate2, + ) + error7 = abs(floatingBondImpliedValue2 - floatingBondCleanPrice2) + self.assertFalse( + error7 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(floatingBondImpliedValue2) + + "\n par asset swap spread: " + + str(floatingBondCleanPrice2) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + #### CMS bond (Isin: XS0228052402 CRDIT 0 8/22/20) + #### maturity doesn't occur on a business day + + cmsBondSchedule1 = ql.Schedule( + ql.Date(22, ql.August, 2005), + ql.Date(22, ql.August, 2020), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBond1 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [1.0], + [0.0], + [0.055], + [0.025], + inArrears, + 100.0, + ql.Date(22, ql.August, 2005), + ) + + cmsBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondImpliedValue1 = cmsBond1.cleanPrice() + cmsBondSettlementDate1 = cmsBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + cmsBondCleanPrice1 = ql.cleanPriceFromZSpread( + cmsBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + cmsBondSettlementDate1, + ) + error9 = abs(cmsBondImpliedValue1 - cmsBondCleanPrice1) + self.assertFalse( + error9 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(cmsBondImpliedValue1) + + "\n par asset swap spread: " + + str(cmsBondCleanPrice1) + + "\n error: " + + str(error9) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ## maturity occurs on a business day + + cmsBondSchedule2 = ql.Schedule( + ql.Date(6, ql.May, 2005), + ql.Date(6, ql.May, 2015), + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBond2 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [0.84], + [0.0], + [], + [], + inArrears, + 100.0, + ql.Date(6, ql.May, 2005), + ) + + cmsBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondImpliedValue2 = cmsBond2.cleanPrice() + cmsBondSettlementDate2 = cmsBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + cmsBondCleanPrice2 = ql.cleanPriceFromZSpread( + cmsBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + cmsBondSettlementDate2, + ) + error11 = abs(cmsBondImpliedValue2 - cmsBondCleanPrice2) + self.assertFalse( + error11 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(cmsBondImpliedValue2) + + "\n par asset swap spread: " + + str(cmsBondCleanPrice2) + + "\n error: " + + str(error11) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero-Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + + zeroCpnBond1 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(20, ql.December, 2015), + ql.Following, + 100.0, + ql.Date(19, ql.December, 1985), + ) + + zeroCpnBond1.setPricingEngine(bondEngine) + + zeroCpnBondImpliedValue1 = zeroCpnBond1.cleanPrice() + zeroCpnBondSettlementDate1 = zeroCpnBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + zeroCpnBondCleanPrice1 = ql.cleanPriceFromZSpread( + zeroCpnBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + zeroCpnBondSettlementDate1, + ) + error13 = abs(zeroCpnBondImpliedValue1 - zeroCpnBondCleanPrice1) + self.assertFalse( + error13 > tolerance, + "wrong clean price for zero coupon bond:" + + "\n zero cpn implied value: " + + str(zeroCpnBondImpliedValue1) + + "\n zero cpn price: " + + str(zeroCpnBondCleanPrice1) + + "\n error: " + + str(error13) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity doesn't occur on a business day + + zeroCpnBond2 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(17, ql.February, 2028), + ql.Following, + 100.0, + ql.Date(17, ql.February, 1998), + ) + + zeroCpnBond2.setPricingEngine(bondEngine) + + zeroCpnBondImpliedValue2 = zeroCpnBond2.cleanPrice() + zeroCpnBondSettlementDate2 = zeroCpnBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + zeroCpnBondCleanPrice2 = ql.cleanPriceFromZSpread( + zeroCpnBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + zeroCpnBondSettlementDate2, + ) + error15 = abs(zeroCpnBondImpliedValue2 - zeroCpnBondCleanPrice2) + self.assertFalse( + error15 > tolerance, + "wrong clean price for zero coupon bond:" + + "\n zero cpn implied value: " + + str(zeroCpnBondImpliedValue2) + + "\n zero cpn price: " + + str(zeroCpnBondCleanPrice2) + + "\n error: " + + str(error15) + + "\n tolerance: " + + str(tolerance), + ) + + def testGenericBondImplied(self): + """Testing implied generic-bond value against asset-swap fair price with null spread...""" + + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + payFixedRate = True + parAssetSwap = True + inArrears = False + + ## Fixed Underlying bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + fixedBondStartDate1 = ql.Date(4, ql.January, 2005) + fixedBondMaturityDate1 = ql.Date(4, ql.January, 2037) + fixedBondSchedule1 = ql.Schedule( + fixedBondStartDate1, + fixedBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg1 = list( + ql.FixedRateLeg(fixedBondSchedule1, ql.ActualActual(ql.ActualActual.ISDA), [self.faceAmount], [0.04]) + ) + fixedbondRedemption1 = bondCalendar.adjust(fixedBondMaturityDate1, ql.Following) + fixedBondLeg1.append(ql.SimpleCashFlow(100.0, fixedbondRedemption1)) + fixedBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + fixedBondMaturityDate1, + fixedBondStartDate1, + tuple(fixedBondLeg1), + ) + bondEngine = ql.DiscountingBondEngine(self.termStructure) + swapEngine = ql.DiscountingSwapEngine(self.termStructure, True) + fixedBond1.setPricingEngine(bondEngine) + + fixedBondPrice1 = fixedBond1.cleanPrice() + fixedBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondAssetSwap1.setPricingEngine(swapEngine) + fixedBondAssetSwapPrice1 = fixedBondAssetSwap1.fairCleanPrice() + tolerance = 1.0e-13 + error1 = abs(fixedBondAssetSwapPrice1 - fixedBondPrice1) + + self.assertFalse( + error1 > tolerance, + "wrong zero spread asset swap price for fixed bond:" + + "\n bond's clean price: " + + str(fixedBondPrice1) + + "\n asset swap fair price: " + + str(fixedBondAssetSwapPrice1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + ## Fixed Underlying bond (Isin: IT0006527060 IBRD 5 02/05/19) + ## maturity occurs on a business day + fixedBondStartDate2 = ql.Date(5, ql.February, 2005) + fixedBondMaturityDate2 = ql.Date(5, ql.February, 2019) + fixedBondSchedule2 = ql.Schedule( + fixedBondStartDate2, + fixedBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg2 = list( + ql.FixedRateLeg(fixedBondSchedule2, ql.Thirty360(ql.Thirty360.BondBasis), [self.faceAmount], [0.05]) + ) + fixedbondRedemption2 = bondCalendar.adjust(fixedBondMaturityDate2, ql.Following) + fixedBondLeg2.append(ql.SimpleCashFlow(100.0, fixedbondRedemption2)) + fixedBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + fixedBondMaturityDate2, + fixedBondStartDate2, + tuple(fixedBondLeg2), + ) + fixedBond2.setPricingEngine(bondEngine) + + fixedBondPrice2 = fixedBond2.cleanPrice() + fixedBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondAssetSwap2.setPricingEngine(swapEngine) + fixedBondAssetSwapPrice2 = fixedBondAssetSwap2.fairCleanPrice() + error2 = abs(fixedBondAssetSwapPrice2 - fixedBondPrice2) + + self.assertFalse( + error2 > tolerance, + "wrong zero spread asset swap price for fixed bond:" + + "\n bond's clean price: " + + str(fixedBondPrice2) + + "\n asset swap fair price: " + + str(fixedBondAssetSwapPrice2) + + "\n error: " + + str(error2) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ## maturity doesn't occur on a business day + floatingBondStartDate1 = ql.Date(29, ql.September, 2003) + floatingBondMaturityDate1 = ql.Date(29, ql.September, 2013) + floatingBondSchedule1 = ql.Schedule( + floatingBondStartDate1, + floatingBondMaturityDate1, + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg1 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + [fixingDays], + [], + [0.0056], + [], + [], + inArrears, + ) + ) + floatingbondRedemption1 = bondCalendar.adjust(floatingBondMaturityDate1, ql.Following) + floatingBondLeg1.append(ql.SimpleCashFlow(100.0, floatingbondRedemption1)) + floatingBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate1, + floatingBondStartDate1, + tuple(floatingBondLeg1), + ) + floatingBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + floatingBondPrice1 = floatingBond1.cleanPrice() + floatingBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondAssetSwap1.setPricingEngine(swapEngine) + floatingBondAssetSwapPrice1 = floatingBondAssetSwap1.fairCleanPrice() + error3 = abs(floatingBondAssetSwapPrice1 - floatingBondPrice1) + + self.assertFalse( + error3 > tolerance, + "wrong zero spread asset swap price for floater:" + + "\n bond's clean price: " + + str(floatingBondPrice1) + + "\n asset swap fair price: " + + str(floatingBondAssetSwapPrice1) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: XS0090566539 COE 0 09/24/18) + ## maturity occurs on a business day + floatingBondStartDate2 = ql.Date(24, ql.September, 2004) + floatingBondMaturityDate2 = ql.Date(24, ql.September, 2018) + floatingBondSchedule2 = ql.Schedule( + floatingBondStartDate2, + floatingBondMaturityDate2, + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg2 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + [fixingDays], + [], + [0.0025], + [], + [], + inArrears, + ) + ) + floatingbondRedemption2 = bondCalendar.adjust(floatingBondMaturityDate2, ql.ModifiedFollowing) + floatingBondLeg2.append(ql.SimpleCashFlow(100.0, floatingbondRedemption2)) + floatingBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate2, + floatingBondStartDate2, + tuple(floatingBondLeg2), + ) + floatingBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + currentCoupon = 0.04013 + 0.0025 + floatingCurrentCoupon = floatingBond2.nextCouponRate() + error4 = abs(floatingCurrentCoupon - currentCoupon) + self.assertFalse( + error4 > tolerance, + "wrong current coupon is returned for floater bond:" + + "\n bond's calculated current coupon: " + + str(currentCoupon) + + "\n current coupon asked to the bond: " + + str(floatingCurrentCoupon) + + "\n error: " + + str(error4) + + "\n tolerance: " + + str(tolerance), + ) + + floatingBondPrice2 = floatingBond2.cleanPrice() + floatingBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondAssetSwap2.setPricingEngine(swapEngine) + floatingBondAssetSwapPrice2 = floatingBondAssetSwap2.fairCleanPrice() + error5 = abs(floatingBondAssetSwapPrice2 - floatingBondPrice2) + + self.assertFalse( + error5 > tolerance, + "wrong zero spread asset swap price for floater:" + + "\n bond's clean price: " + + str(floatingBondPrice2) + + "\n asset swap fair price: " + + str(floatingBondAssetSwapPrice2) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0228052402 CRDIT 0 8/22/20) + ## maturity doesn't occur on a business day + + cmsBondStartDate1 = ql.Date(22, ql.August, 2005) + cmsBondMaturityDate1 = ql.Date(22, ql.August, 2020) + cmsBondSchedule1 = ql.Schedule( + cmsBondStartDate1, + cmsBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg1 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [], + [0.055], + [0.025], + [], + inArrears, + ) + ) + cmsbondRedemption1 = bondCalendar.adjust(cmsBondMaturityDate1, ql.Following) + cmsBondLeg1.append(ql.SimpleCashFlow(100.0, cmsbondRedemption1)) + cmsBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate1, cmsBondStartDate1, tuple(cmsBondLeg1) + ) + cmsBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondPrice1 = cmsBond1.cleanPrice() + cmsBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondAssetSwap1.setPricingEngine(swapEngine) + cmsBondAssetSwapPrice1 = cmsBondAssetSwap1.fairCleanPrice() + error6 = abs(cmsBondAssetSwapPrice1 - cmsBondPrice1) + + self.assertFalse( + error6 > tolerance, + "wrong zero spread asset swap price for cms bond:" + + "\n bond's clean price: " + + str(cmsBondPrice1) + + "\n asset swap fair price: " + + str(cmsBondAssetSwapPrice1) + + "\n error: " + + str(error6) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ## maturity occurs on a business day + cmsBondStartDate2 = ql.Date(6, ql.May, 2005) + cmsBondMaturityDate2 = ql.Date(6, ql.May, 2015) + cmsBondSchedule2 = ql.Schedule( + cmsBondStartDate2, + cmsBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg2 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [0.84], + [], + [], + [], + inArrears, + ) + ) + cmsbondRedemption2 = bondCalendar.adjust(cmsBondMaturityDate2, ql.Following) + cmsBondLeg2.append(ql.SimpleCashFlow(100.0, cmsbondRedemption2)) + cmsBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate2, cmsBondStartDate2, tuple(cmsBondLeg2) + ) + cmsBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondPrice2 = cmsBond2.cleanPrice() + cmsBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondAssetSwap2.setPricingEngine(swapEngine) + cmsBondAssetSwapPrice2 = cmsBondAssetSwap2.fairCleanPrice() + error7 = abs(cmsBondAssetSwapPrice2 - cmsBondPrice2) + + self.assertFalse( + error7 > tolerance, + "wrong zero spread asset swap price for cms bond:" + + "\n bond's clean price: " + + str(cmsBondPrice2) + + "\n asset swap fair price: " + + str(cmsBondAssetSwapPrice2) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + + zeroCpnBondStartDate1 = ql.Date(19, ql.December, 1985) + zeroCpnBondMaturityDate1 = ql.Date(20, ql.December, 2015) + zeroCpnBondRedemption1 = bondCalendar.adjust(zeroCpnBondMaturityDate1, ql.Following) + zeroCpnBondLeg1 = ql.Leg([ql.SimpleCashFlow(100.0, zeroCpnBondRedemption1)]) + zeroCpnBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate1, + zeroCpnBondStartDate1, + zeroCpnBondLeg1, + ) + zeroCpnBond1.setPricingEngine(bondEngine) + + zeroCpnBondPrice1 = zeroCpnBond1.cleanPrice() + zeroCpnAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnAssetSwap1.setPricingEngine(swapEngine) + zeroCpnBondAssetSwapPrice1 = zeroCpnAssetSwap1.fairCleanPrice() + error8 = abs(zeroCpnBondAssetSwapPrice1 - zeroCpnBondPrice1) + + self.assertFalse( + error8 > tolerance, + "wrong zero spread asset swap price for zero cpn bond:" + + "\n bond's clean price: " + + str(zeroCpnBondPrice1) + + "\n asset swap fair price: " + + str(zeroCpnBondAssetSwapPrice1) + + "\n error: " + + str(error8) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity occurs on a business day + zeroCpnBondStartDate2 = ql.Date(17, ql.February, 1998) + zeroCpnBondMaturityDate2 = ql.Date(17, ql.February, 2028) + zerocpbondRedemption2 = bondCalendar.adjust(zeroCpnBondMaturityDate2, ql.Following) + zeroCpnBondLeg2 = ql.Leg([ql.SimpleCashFlow(100.0, zerocpbondRedemption2)]) + zeroCpnBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate2, + zeroCpnBondStartDate2, + zeroCpnBondLeg2, + ) + zeroCpnBond2.setPricingEngine(bondEngine) + + zeroCpnBondPrice2 = zeroCpnBond2.cleanPrice() + zeroCpnAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnAssetSwap2.setPricingEngine(swapEngine) + zeroCpnBondAssetSwapPrice2 = zeroCpnAssetSwap2.fairCleanPrice() + error9 = abs(cmsBondAssetSwapPrice2 - cmsBondPrice2) + + self.assertFalse( + error9 > tolerance, + "wrong zero spread asset swap price for zero cpn bond:" + + "\n bond's clean price: " + + str(zeroCpnBondPrice2) + + "\n asset swap fair price: " + + str(zeroCpnBondAssetSwapPrice2) + + "\n error: " + + str(error9) + + "\n tolerance: " + + str(tolerance), + ) + + def testMASWWithGenericBond(self): + """Testing market asset swap against par asset swap with generic bond...""" + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + payFixedRate = True + parAssetSwap = True + mktAssetSwap = False + inArrears = False + + ## Fixed Underlying bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + + fixedBondStartDate1 = ql.Date(4, ql.January, 2005) + fixedBondMaturityDate1 = ql.Date(4, ql.January, 2037) + fixedBondSchedule1 = ql.Schedule( + fixedBondStartDate1, + fixedBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg1 = list( + ql.FixedRateLeg(fixedBondSchedule1, ql.ActualActual(ql.ActualActual.ISDA), [self.faceAmount], [0.04]) + ) + fixedbondRedemption1 = bondCalendar.adjust(fixedBondMaturityDate1, ql.Following) + fixedBondLeg1.append(ql.SimpleCashFlow(100.0, fixedbondRedemption1)) + fixedBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate1, fixedBondStartDate1, fixedBondLeg1 + ) + bondEngine = ql.DiscountingBondEngine(self.termStructure) + swapEngine = ql.DiscountingSwapEngine(self.termStructure, False) + fixedBond1.setPricingEngine(bondEngine) + + fixedBondMktPrice1 = 89.22 ## market price observed on 7th June 2007 + fixedBondMktFullPrice1 = fixedBondMktPrice1 + fixedBond1.accruedAmount() + fixedBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondParAssetSwap1.setPricingEngine(swapEngine) + fixedBondParAssetSwapSpread1 = fixedBondParAssetSwap1.fairSpread() + fixedBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + fixedBondMktAssetSwap1.setPricingEngine(swapEngine) + fixedBondMktAssetSwapSpread1 = fixedBondMktAssetSwap1.fairSpread() + + tolerance = 1.0e-13 + error1 = abs(fixedBondMktAssetSwapSpread1 - 100 * fixedBondParAssetSwapSpread1 / fixedBondMktFullPrice1) + + self.assertFalse( + error1 > tolerance, + "wrong asset swap spreads for fixed bond:" + + "\n market asset swap spread: " + + str(fixedBondMktAssetSwapSpread1) + + "\n par asset swap spread: " + + str(fixedBondParAssetSwapSpread1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + ## Fixed Underlying bond (Isin: IT0006527060 IBRD 5 02/05/19) + ## maturity occurs on a business day + + fixedBondStartDate2 = ql.Date(5, ql.February, 2005) + fixedBondMaturityDate2 = ql.Date(5, ql.February, 2019) + fixedBondSchedule2 = ql.Schedule( + fixedBondStartDate2, + fixedBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg2 = list( + ql.FixedRateLeg(fixedBondSchedule2, ql.Thirty360(ql.Thirty360.BondBasis), [self.faceAmount], [0.05]) + ) + fixedbondRedemption2 = bondCalendar.adjust(fixedBondMaturityDate2, ql.Following) + fixedBondLeg2.append(ql.SimpleCashFlow(100.0, fixedbondRedemption2)) + fixedBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate2, fixedBondStartDate2, fixedBondLeg2 + ) + fixedBond2.setPricingEngine(bondEngine) + + fixedBondMktPrice2 = 99.98 ## market price observed on 7th June 2007 + fixedBondMktFullPrice2 = fixedBondMktPrice2 + fixedBond2.accruedAmount() + fixedBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondParAssetSwap2.setPricingEngine(swapEngine) + fixedBondParAssetSwapSpread2 = fixedBondParAssetSwap2.fairSpread() + fixedBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + fixedBondMktAssetSwap2.setPricingEngine(swapEngine) + fixedBondMktAssetSwapSpread2 = fixedBondMktAssetSwap2.fairSpread() + error2 = abs(fixedBondMktAssetSwapSpread2 - 100 * fixedBondParAssetSwapSpread2 / fixedBondMktFullPrice2) + + self.assertFalse( + error2 > tolerance, + "wrong asset swap spreads for fixed bond:" + + "\n market asset swap spread: " + + str(fixedBondMktAssetSwapSpread2) + + "\n par asset swap spread: " + + str(fixedBondParAssetSwapSpread2) + + "\n error: " + + str(error2) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ## maturity doesn't occur on a business day + + floatingBondStartDate1 = ql.Date(29, ql.September, 2003) + floatingBondMaturityDate1 = ql.Date(29, ql.September, 2013) + floatingBondSchedule1 = ql.Schedule( + floatingBondStartDate1, + floatingBondMaturityDate1, + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg1 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + [fixingDays], + [], + [0.0056], + [], + [], + inArrears, + ) + ) + floatingbondRedemption1 = bondCalendar.adjust(floatingBondMaturityDate1, ql.Following) + floatingBondLeg1.append(ql.SimpleCashFlow(100.0, floatingbondRedemption1)) + floatingBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate1, + floatingBondStartDate1, + floatingBondLeg1, + ) + floatingBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + ## market price observed on 7th June 2007 + floatingBondMktPrice1 = 101.64 + floatingBondMktFullPrice1 = floatingBondMktPrice1 + floatingBond1.accruedAmount() + floatingBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondParAssetSwap1.setPricingEngine(swapEngine) + floatingBondParAssetSwapSpread1 = floatingBondParAssetSwap1.fairSpread() + floatingBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + floatingBondMktAssetSwap1.setPricingEngine(swapEngine) + floatingBondMktAssetSwapSpread1 = floatingBondMktAssetSwap1.fairSpread() + error3 = abs( + floatingBondMktAssetSwapSpread1 - 100 * floatingBondParAssetSwapSpread1 / floatingBondMktFullPrice1 + ) + + self.assertFalse( + error3 > tolerance, + "wrong asset swap spreads for floating bond:" + + "\n market asset swap spread: " + + str(floatingBondMktAssetSwapSpread1) + + "\n par asset swap spread: " + + str(floatingBondParAssetSwapSpread1) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: XS0090566539 COE 0 09/24/18) + ## maturity occurs on a business day + + floatingBondStartDate2 = ql.Date(24, ql.September, 2004) + floatingBondMaturityDate2 = ql.Date(24, ql.September, 2018) + floatingBondSchedule2 = ql.Schedule( + floatingBondStartDate2, + floatingBondMaturityDate2, + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg2 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + [fixingDays], + [], + [0.0025], + [], + [], + inArrears, + ) + ) + floatingbondRedemption2 = bondCalendar.adjust(floatingBondMaturityDate2, ql.ModifiedFollowing) + floatingBondLeg2.append(ql.SimpleCashFlow(100.0, floatingbondRedemption2)) + floatingBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate2, + floatingBondStartDate2, + floatingBondLeg2, + ) + floatingBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + ## market price observed on 7th June 2007 + floatingBondMktPrice2 = 101.248 + floatingBondMktFullPrice2 = floatingBondMktPrice2 + floatingBond2.accruedAmount() + floatingBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondParAssetSwap2.setPricingEngine(swapEngine) + floatingBondParAssetSwapSpread2 = floatingBondParAssetSwap2.fairSpread() + floatingBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + floatingBondMktAssetSwap2.setPricingEngine(swapEngine) + floatingBondMktAssetSwapSpread2 = floatingBondMktAssetSwap2.fairSpread() + error4 = abs( + floatingBondMktAssetSwapSpread2 - 100 * floatingBondParAssetSwapSpread2 / floatingBondMktFullPrice2 + ) + + self.assertFalse( + error4 > tolerance, + "wrong asset swap spreads for floating bond:" + + "\n market asset swap spread: " + + str(floatingBondMktAssetSwapSpread2) + + "\n par asset swap spread: " + + str(floatingBondParAssetSwapSpread2) + + "\n error: " + + str(error4) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0228052402 CRDIT 0 8/22/20) + ## maturity doesn't occur on a business day + + cmsBondStartDate1 = ql.Date(22, ql.August, 2005) + cmsBondMaturityDate1 = ql.Date(22, ql.August, 2020) + cmsBondSchedule1 = ql.Schedule( + cmsBondStartDate1, + cmsBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg1 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [], + [], + [0.055], + [0.025], + inArrears, + ) + ) + cmsbondRedemption1 = bondCalendar.adjust(cmsBondMaturityDate1, ql.Following) + cmsBondLeg1.append(ql.SimpleCashFlow(100.0, cmsbondRedemption1)) + cmsBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate1, cmsBondStartDate1, cmsBondLeg1 + ) + cmsBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondMktPrice1 = 88.45 ## market price observed on 7th June 2007 + cmsBondMktFullPrice1 = cmsBondMktPrice1 + cmsBond1.accruedAmount() + cmsBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondParAssetSwap1.setPricingEngine(swapEngine) + cmsBondParAssetSwapSpread1 = cmsBondParAssetSwap1.fairSpread() + cmsBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + cmsBondMktAssetSwap1.setPricingEngine(swapEngine) + cmsBondMktAssetSwapSpread1 = cmsBondMktAssetSwap1.fairSpread() + error5 = abs(cmsBondMktAssetSwapSpread1 - 100 * cmsBondParAssetSwapSpread1 / cmsBondMktFullPrice1) + + self.assertFalse( + error5 > tolerance, + "wrong asset swap spreads for cms bond:" + + "\n market asset swap spread: " + + str(cmsBondMktAssetSwapSpread1) + + "\n par asset swap spread: " + + str(100 * cmsBondParAssetSwapSpread1 / cmsBondMktFullPrice1) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ## maturity occurs on a business day + + cmsBondStartDate2 = ql.Date(6, ql.May, 2005) + cmsBondMaturityDate2 = ql.Date(6, ql.May, 2015) + cmsBondSchedule2 = ql.Schedule( + cmsBondStartDate2, + cmsBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg2 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [0.84], + [], + [], + [], + inArrears, + ) + ) + cmsbondRedemption2 = bondCalendar.adjust(cmsBondMaturityDate2, ql.Following) + cmsBondLeg2.append(ql.SimpleCashFlow(100.0, cmsbondRedemption2)) + cmsBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate2, cmsBondStartDate2, cmsBondLeg2 + ) + cmsBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondMktPrice2 = 94.08 ## market price observed on 7th June 2007 + cmsBondMktFullPrice2 = cmsBondMktPrice2 + cmsBond2.accruedAmount() + cmsBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondParAssetSwap2.setPricingEngine(swapEngine) + cmsBondParAssetSwapSpread2 = cmsBondParAssetSwap2.fairSpread() + cmsBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + cmsBondMktAssetSwap2.setPricingEngine(swapEngine) + cmsBondMktAssetSwapSpread2 = cmsBondMktAssetSwap2.fairSpread() + error6 = abs(cmsBondMktAssetSwapSpread2 - 100 * cmsBondParAssetSwapSpread2 / cmsBondMktFullPrice2) + + self.assertFalse( + error6 > tolerance, + "wrong asset swap spreads for cms bond:" + + "\n market asset swap spread: " + + str(cmsBondMktAssetSwapSpread2) + + "\n par asset swap spread: " + + str(cmsBondParAssetSwapSpread2) + + "\n error: " + + str(error6) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + + zeroCpnBondStartDate1 = ql.Date(19, ql.December, 1985) + zeroCpnBondMaturityDate1 = ql.Date(20, ql.December, 2015) + zeroCpnBondRedemption1 = bondCalendar.adjust(zeroCpnBondMaturityDate1, ql.Following) + zeroCpnBondLeg1 = ql.Leg([ql.SimpleCashFlow(100.0, zeroCpnBondRedemption1)]) + zeroCpnBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate1, + zeroCpnBondStartDate1, + zeroCpnBondLeg1, + ) + zeroCpnBond1.setPricingEngine(bondEngine) + + ## market price observed on 12th June 2007 + zeroCpnBondMktPrice1 = 70.436 + zeroCpnBondMktFullPrice1 = zeroCpnBondMktPrice1 + zeroCpnBond1.accruedAmount() + zeroCpnBondParAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondParAssetSwap1.setPricingEngine(swapEngine) + zeroCpnBondParAssetSwapSpread1 = zeroCpnBondParAssetSwap1.fairSpread() + zeroCpnBondMktAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + zeroCpnBondMktAssetSwap1.setPricingEngine(swapEngine) + zeroCpnBondMktAssetSwapSpread1 = zeroCpnBondMktAssetSwap1.fairSpread() + error7 = abs(zeroCpnBondMktAssetSwapSpread1 - 100 * zeroCpnBondParAssetSwapSpread1 / zeroCpnBondMktFullPrice1) + + self.assertFalse( + error7 > tolerance, + "wrong asset swap spreads for zero cpn bond:" + + "\n market asset swap spread: " + + str(zeroCpnBondMktAssetSwapSpread1) + + "\n par asset swap spread: " + + str(zeroCpnBondParAssetSwapSpread1) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity occurs on a business day + + zeroCpnBondStartDate2 = ql.Date(17, ql.February, 1998) + zeroCpnBondMaturityDate2 = ql.Date(17, ql.February, 2028) + zerocpbondRedemption2 = bondCalendar.adjust(zeroCpnBondMaturityDate2, ql.Following) + zeroCpnBondLeg2 = ql.Leg([ql.SimpleCashFlow(100.0, zerocpbondRedemption2)]) + zeroCpnBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate2, + zeroCpnBondStartDate2, + zeroCpnBondLeg2, + ) + zeroCpnBond2.setPricingEngine(bondEngine) + + ## zeroCpnBondPrice2 = zeroCpnBond2.cleanPrice() + ## market price observed on 12th June 2007 + zeroCpnBondMktPrice2 = 35.160 + zeroCpnBondMktFullPrice2 = zeroCpnBondMktPrice2 + zeroCpnBond2.accruedAmount() + zeroCpnBondParAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondParAssetSwap2.setPricingEngine(swapEngine) + zeroCpnBondParAssetSwapSpread2 = zeroCpnBondParAssetSwap2.fairSpread() + zeroCpnBondMktAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + mktAssetSwap, + ) + zeroCpnBondMktAssetSwap2.setPricingEngine(swapEngine) + zeroCpnBondMktAssetSwapSpread2 = zeroCpnBondMktAssetSwap2.fairSpread() + error8 = abs(zeroCpnBondMktAssetSwapSpread2 - 100 * zeroCpnBondParAssetSwapSpread2 / zeroCpnBondMktFullPrice2) + + self.assertFalse( + error8 > tolerance, + "wrong asset swap spreads for zero cpn bond:" + + "\n market asset swap spread: " + + str(zeroCpnBondMktAssetSwapSpread2) + + "\n par asset swap spread: " + + str(zeroCpnBondParAssetSwapSpread2) + + "\n error: " + + str(error8) + + "\n tolerance: " + + str(tolerance), + ) + + def testZSpreadWithGenericBond(self): + """Testing clean and dirty price with null Z-spread against theoretical prices...""" + + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + inArrears = False + + ## Fixed Underlying bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + + fixedBondStartDate1 = ql.Date(4, ql.January, 2005) + fixedBondMaturityDate1 = ql.Date(4, ql.January, 2037) + fixedBondSchedule1 = ql.Schedule( + fixedBondStartDate1, + fixedBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg1 = list( + ql.FixedRateLeg(fixedBondSchedule1, ql.ActualActual(ql.ActualActual.ISDA), [self.faceAmount], [0.04]) + ) + fixedbondRedemption1 = bondCalendar.adjust(fixedBondMaturityDate1, ql.Following) + fixedBondLeg1.append(ql.SimpleCashFlow(100.0, fixedbondRedemption1)) + fixedBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate1, fixedBondStartDate1, fixedBondLeg1 + ) + bondEngine = ql.DiscountingBondEngine(self.termStructure) + fixedBond1.setPricingEngine(bondEngine) + + fixedBondImpliedValue1 = fixedBond1.cleanPrice() + fixedBondSettlementDate1 = fixedBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + fixedBondCleanPrice1 = ql.cleanPriceFromZSpread( + fixedBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + fixedBondSettlementDate1, + ) + tolerance = 1.0e-13 + error1 = abs(fixedBondImpliedValue1 - fixedBondCleanPrice1) + self.assertFalse( + error1 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(fixedBondImpliedValue1) + + "\n par asset swap spread: " + + str(fixedBondCleanPrice1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + ## Fixed Underlying bond (Isin: IT0006527060 IBRD 5 02/05/19) + ## maturity occurs on a business day + + fixedBondStartDate2 = ql.Date(5, ql.February, 2005) + fixedBondMaturityDate2 = ql.Date(5, ql.February, 2019) + fixedBondSchedule2 = ql.Schedule( + fixedBondStartDate2, + fixedBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg2 = list( + ql.FixedRateLeg(fixedBondSchedule2, ql.Thirty360(ql.Thirty360.BondBasis), [self.faceAmount], [0.05]) + ) + fixedbondRedemption2 = bondCalendar.adjust(fixedBondMaturityDate2, ql.Following) + fixedBondLeg2.append(ql.SimpleCashFlow(100.0, fixedbondRedemption2)) + fixedBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate2, fixedBondStartDate2, fixedBondLeg2 + ) + fixedBond2.setPricingEngine(bondEngine) + + fixedBondImpliedValue2 = fixedBond2.cleanPrice() + fixedBondSettlementDate2 = fixedBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + + fixedBondCleanPrice2 = ql.cleanPriceFromZSpread( + fixedBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + fixedBondSettlementDate2, + ) + error3 = abs(fixedBondImpliedValue2 - fixedBondCleanPrice2) + self.assertFalse( + error3 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(fixedBondImpliedValue2) + + "\n par asset swap spread: " + + str(fixedBondCleanPrice2) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ## maturity doesn't occur on a business day + + floatingBondStartDate1 = ql.Date(29, ql.September, 2003) + floatingBondMaturityDate1 = ql.Date(29, ql.September, 2013) + floatingBondSchedule1 = ql.Schedule( + floatingBondStartDate1, + floatingBondMaturityDate1, + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg1 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + [fixingDays], + [], + [0.0056], + [], + [], + inArrears, + ) + ) + floatingbondRedemption1 = bondCalendar.adjust(floatingBondMaturityDate1, ql.Following) + floatingBondLeg1.append(ql.SimpleCashFlow(100.0, floatingbondRedemption1)) + floatingBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate1, + floatingBondStartDate1, + floatingBondLeg1, + ) + floatingBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + floatingBondImpliedValue1 = floatingBond1.cleanPrice() + floatingBondSettlementDate1 = floatingBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + floatingBondCleanPrice1 = ql.cleanPriceFromZSpread( + floatingBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Semiannual, + floatingBondSettlementDate1, + ) + error5 = abs(floatingBondImpliedValue1 - floatingBondCleanPrice1) + self.assertFalse( + error5 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(floatingBondImpliedValue1) + + "\n par asset swap spread: " + + str(floatingBondCleanPrice1) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: XS0090566539 COE 0 09/24/18) + ## maturity occurs on a business day + + floatingBondStartDate2 = ql.Date(24, ql.September, 2004) + floatingBondMaturityDate2 = ql.Date(24, ql.September, 2018) + floatingBondSchedule2 = ql.Schedule( + floatingBondStartDate2, + floatingBondMaturityDate2, + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg2 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + [fixingDays], + [], + [0.0025], + [], + [], + inArrears, + ) + ) + floatingbondRedemption2 = bondCalendar.adjust(floatingBondMaturityDate2, ql.ModifiedFollowing) + floatingBondLeg2.append(ql.SimpleCashFlow(100.0, floatingbondRedemption2)) + floatingBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate2, + floatingBondStartDate2, + floatingBondLeg2, + ) + floatingBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + floatingBondImpliedValue2 = floatingBond2.cleanPrice() + floatingBondSettlementDate2 = floatingBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + floatingBondCleanPrice2 = ql.cleanPriceFromZSpread( + floatingBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Semiannual, + floatingBondSettlementDate2, + ) + error7 = abs(floatingBondImpliedValue2 - floatingBondCleanPrice2) + self.assertFalse( + error7 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(floatingBondImpliedValue2) + + "\n par asset swap spread: " + + str(floatingBondCleanPrice2) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0228052402 CRDIT 0 8/22/20) + ## maturity doesn't occur on a business day + + cmsBondStartDate1 = ql.Date(22, ql.August, 2005) + cmsBondMaturityDate1 = ql.Date(22, ql.August, 2020) + cmsBondSchedule1 = ql.Schedule( + cmsBondStartDate1, + cmsBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg1 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [], + [], + [0.055], + [0.025], + inArrears, + ) + ) + cmsbondRedemption1 = bondCalendar.adjust(cmsBondMaturityDate1, ql.Following) + cmsBondLeg1.append(ql.SimpleCashFlow(100.0, cmsbondRedemption1)) + cmsBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate1, cmsBondStartDate1, cmsBondLeg1 + ) + cmsBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondImpliedValue1 = cmsBond1.cleanPrice() + cmsBondSettlementDate1 = cmsBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + cmsBondCleanPrice1 = ql.cleanPriceFromZSpread( + cmsBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + cmsBondSettlementDate1, + ) + error9 = abs(cmsBondImpliedValue1 - cmsBondCleanPrice1) + self.assertFalse( + error9 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(cmsBondImpliedValue1) + + "\n par asset swap spread: " + + str(cmsBondCleanPrice1) + + "\n error: " + + str(error9) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ## maturity occurs on a business day + + cmsBondStartDate2 = ql.Date(6, ql.May, 2005) + cmsBondMaturityDate2 = ql.Date(6, ql.May, 2015) + cmsBondSchedule2 = ql.Schedule( + cmsBondStartDate2, + cmsBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg2 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [0.84], + [], + [], + [], + inArrears, + ) + ) + cmsbondRedemption2 = bondCalendar.adjust(cmsBondMaturityDate2, ql.Following) + cmsBondLeg2.append(ql.SimpleCashFlow(100.0, cmsbondRedemption2)) + cmsBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate2, cmsBondStartDate2, cmsBondLeg2 + ) + cmsBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondImpliedValue2 = cmsBond2.cleanPrice() + cmsBondSettlementDate2 = cmsBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + cmsBondCleanPrice2 = ql.cleanPriceFromZSpread( + cmsBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + cmsBondSettlementDate2, + ) + error11 = abs(cmsBondImpliedValue2 - cmsBondCleanPrice2) + self.assertFalse( + error11 > tolerance, + "wrong clean price for fixed bond:" + + "\n market asset swap spread: " + + str(cmsBondImpliedValue2) + + "\n par asset swap spread: " + + str(cmsBondCleanPrice2) + + "\n error: " + + str(error11) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + + zeroCpnBondStartDate1 = ql.Date(19, ql.December, 1985) + zeroCpnBondMaturityDate1 = ql.Date(20, ql.December, 2015) + zeroCpnBondRedemption1 = bondCalendar.adjust(zeroCpnBondMaturityDate1, ql.Following) + zeroCpnBondLeg1 = ql.Leg([ql.SimpleCashFlow(100.0, zeroCpnBondRedemption1)]) + zeroCpnBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate1, + zeroCpnBondStartDate1, + zeroCpnBondLeg1, + ) + zeroCpnBond1.setPricingEngine(bondEngine) + + zeroCpnBondImpliedValue1 = zeroCpnBond1.cleanPrice() + zeroCpnBondSettlementDate1 = zeroCpnBond1.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + zeroCpnBondCleanPrice1 = ql.cleanPriceFromZSpread( + zeroCpnBond1, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + zeroCpnBondSettlementDate1, + ) + error13 = abs(zeroCpnBondImpliedValue1 - zeroCpnBondCleanPrice1) + self.assertFalse( + error13 > tolerance, + "wrong clean price for zero coupon bond:" + + "\n zero cpn implied value: " + + str(zeroCpnBondImpliedValue1) + + "\n zero cpn price: " + + str(zeroCpnBondCleanPrice1) + + "\n error: " + + str(error13) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity occurs on a business day + + zeroCpnBondStartDate2 = ql.Date(17, ql.February, 1998) + zeroCpnBondMaturityDate2 = ql.Date(17, ql.February, 2028) + zerocpbondRedemption2 = bondCalendar.adjust(zeroCpnBondMaturityDate2, ql.Following) + zeroCpnBondLeg2 = ql.Leg([ql.SimpleCashFlow(100.0, zerocpbondRedemption2)]) + zeroCpnBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate2, + zeroCpnBondStartDate2, + zeroCpnBondLeg2, + ) + zeroCpnBond2.setPricingEngine(bondEngine) + + zeroCpnBondImpliedValue2 = zeroCpnBond2.cleanPrice() + zeroCpnBondSettlementDate2 = zeroCpnBond2.settlementDate() + ## standard market conventions: + ## bond's frequency + coumpounding and daycounter of the YieldCurve + zeroCpnBondCleanPrice2 = ql.cleanPriceFromZSpread( + zeroCpnBond2, + self.yieldCurve, + self.spread, + ql.Actual365Fixed(), + self.compounding, + ql.Annual, + zeroCpnBondSettlementDate2, + ) + error15 = abs(zeroCpnBondImpliedValue2 - zeroCpnBondCleanPrice2) + + self.assertFalse( + error15 > tolerance, + "wrong clean price for zero coupon bond:" + + "\n zero cpn implied value: " + + str(zeroCpnBondImpliedValue2) + + "\n zero cpn price: " + + str(zeroCpnBondCleanPrice2) + + "\n error: " + + str(error15) + + "\n tolerance: " + + str(tolerance), + ) + + def testSpecializedBondVsGenericBond(self): + """Testing clean and dirty prices for specialized bond against equivalent generic bond...""" + + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + inArrears = False + + ## Fixed Underlying bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + fixedBondStartDate1 = ql.Date(4, ql.January, 2005) + fixedBondMaturityDate1 = ql.Date(4, ql.January, 2037) + fixedBondSchedule1 = ql.Schedule( + fixedBondStartDate1, + fixedBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg1 = list( + ql.FixedRateLeg(fixedBondSchedule1, ql.ActualActual(ql.ActualActual.ISDA), [self.faceAmount], [0.04]) + ) + fixedbondRedemption1 = bondCalendar.adjust(fixedBondMaturityDate1, ql.Following) + fixedBondLeg1.append(ql.SimpleCashFlow(100.0, fixedbondRedemption1)) + ## generic bond + fixedBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate1, fixedBondStartDate1, fixedBondLeg1 + ) + bondEngine = ql.DiscountingBondEngine(self.termStructure) + fixedBond1.setPricingEngine(bondEngine) + + ## equivalent specialized fixed rate bond + fixedSpecializedBond1 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule1, + [0.04], + ql.ActualActual(ql.ActualActual.ISDA), + ql.Following, + 100.0, + ql.Date(4, ql.January, 2005), + ) + fixedSpecializedBond1.setPricingEngine(bondEngine) + + fixedBondTheoValue1 = fixedBond1.cleanPrice() + fixedSpecializedBondTheoValue1 = fixedSpecializedBond1.cleanPrice() + tolerance = 1.0e-13 + error1 = abs(fixedBondTheoValue1 - fixedSpecializedBondTheoValue1) + + self.assertFalse( + error1 > tolerance, + "wrong clean price for fixed bond:" + + "\n specialized fixed rate bond's theo clean price: " + + str(fixedBondTheoValue1) + + "\n generic equivalent bond's theo clean price: " + + str(fixedSpecializedBondTheoValue1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + fixedBondTheoDirty1 = fixedBondTheoValue1 + fixedBond1.accruedAmount() + fixedSpecializedTheoDirty1 = fixedSpecializedBondTheoValue1 + fixedSpecializedBond1.accruedAmount() + error2 = abs(fixedBondTheoDirty1 - fixedSpecializedTheoDirty1) + + self.assertFalse( + error2 > tolerance, + "wrong dirty price for fixed bond:" + + "\n specialized fixed rate bond's theo dirty price: " + + str(fixedBondTheoDirty1) + + "\n generic equivalent bond's theo dirty price: " + + str(fixedSpecializedTheoDirty1) + + "\n error: " + + str(error2) + + "\n tolerance: " + + str(tolerance), + ) + + ## Fixed Underlying bond (Isin: IT0006527060 IBRD 5 02/05/19) + ## maturity occurs on a business day + fixedBondStartDate2 = ql.Date(5, ql.February, 2005) + fixedBondMaturityDate2 = ql.Date(5, ql.February, 2019) + fixedBondSchedule2 = ql.Schedule( + fixedBondStartDate2, + fixedBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg2 = list( + ql.FixedRateLeg(fixedBondSchedule2, ql.Thirty360(ql.Thirty360.BondBasis), [self.faceAmount], [0.05]) + ) + fixedbondRedemption2 = bondCalendar.adjust(fixedBondMaturityDate2, ql.Following) + fixedBondLeg2.append(ql.SimpleCashFlow(100.0, fixedbondRedemption2)) + + ## generic bond + fixedBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate2, fixedBondStartDate2, fixedBondLeg2 + ) + fixedBond2.setPricingEngine(bondEngine) + + ## equivalent specialized fixed rate bond + fixedSpecializedBond2 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule2, + [0.05], + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + 100.0, + ql.Date(5, ql.February, 2005), + ) + fixedSpecializedBond2.setPricingEngine(bondEngine) + + fixedBondTheoValue2 = fixedBond2.cleanPrice() + fixedSpecializedBondTheoValue2 = fixedSpecializedBond2.cleanPrice() + + error3 = abs(fixedBondTheoValue2 - fixedSpecializedBondTheoValue2) + self.assertFalse( + error3 > tolerance, + "wrong clean price for fixed bond:" + + "\n specialized fixed rate bond's theo clean price: " + + str(fixedBondTheoValue2) + + "\n generic equivalent bond's theo clean price: " + + str(fixedSpecializedBondTheoValue2) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + fixedBondTheoDirty2 = fixedBondTheoValue2 + fixedBond2.accruedAmount() + fixedSpecializedBondTheoDirty2 = fixedSpecializedBondTheoValue2 + fixedSpecializedBond2.accruedAmount() + + error4 = abs(fixedBondTheoDirty2 - fixedSpecializedBondTheoDirty2) + self.assertFalse( + error4 > tolerance, + "wrong dirty price for fixed bond:" + + "\n specialized fixed rate bond's dirty clean price: " + + str(fixedBondTheoDirty2) + + "\n generic equivalent bond's theo dirty price: " + + str(fixedSpecializedBondTheoDirty2) + + "\n error: " + + str(error4) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ## maturity doesn't occur on a business day + floatingBondStartDate1 = ql.Date(29, ql.September, 2003) + floatingBondMaturityDate1 = ql.Date(29, ql.September, 2013) + floatingBondSchedule1 = ql.Schedule( + floatingBondStartDate1, + floatingBondMaturityDate1, + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg1 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + [fixingDays], + [], + [0.0056], + [], + [], + inArrears, + ) + ) + floatingbondRedemption1 = bondCalendar.adjust(floatingBondMaturityDate1, ql.Following) + floatingBondLeg1.append(ql.SimpleCashFlow(100.0, floatingbondRedemption1)) + ## generic bond + floatingBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate1, + floatingBondStartDate1, + floatingBondLeg1, + ) + floatingBond1.setPricingEngine(bondEngine) + + ## equivalent specialized floater + floatingSpecializedBond1 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + fixingDays, + [1], + [0.0056], + [], + [], + inArrears, + 100.0, + ql.Date(29, ql.September, 2003), + ) + floatingSpecializedBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + ql.setCouponPricer(floatingSpecializedBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + floatingBondTheoValue1 = floatingBond1.cleanPrice() + floatingSpecializedBondTheoValue1 = floatingSpecializedBond1.cleanPrice() + + error5 = abs(floatingBondTheoValue1 - floatingSpecializedBondTheoValue1) + self.assertFalse( + error5 > tolerance, + "wrong clean price for fixed bond:" + + "\n generic fixed rate bond's theo clean price: " + + str(floatingBondTheoValue1) + + "\n equivalent specialized bond's theo clean price: " + + str(floatingSpecializedBondTheoValue1) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + floatingBondTheoDirty1 = floatingBondTheoValue1 + floatingBond1.accruedAmount() + floatingSpecializedBondTheoDirty1 = floatingSpecializedBondTheoValue1 + floatingSpecializedBond1.accruedAmount() + error6 = abs(floatingBondTheoDirty1 - floatingSpecializedBondTheoDirty1) + self.assertFalse( + error6 > tolerance, + "wrong dirty price for frn bond:" + + "\n generic frn bond's dirty clean price: " + + str(floatingBondTheoDirty1) + + "\n equivalent specialized bond's theo dirty price: " + + str(floatingSpecializedBondTheoDirty1) + + "\n error: " + + str(error6) + + "\n tolerance: " + + str(tolerance), + ) + + ## FRN Underlying bond (Isin: XS0090566539 COE 0 09/24/18) + ## maturity occurs on a business day + floatingBondStartDate2 = ql.Date(24, ql.September, 2004) + floatingBondMaturityDate2 = ql.Date(24, ql.September, 2018) + floatingBondSchedule2 = ql.Schedule( + floatingBondStartDate2, + floatingBondMaturityDate2, + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg2 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + [fixingDays], + [], + [0.0025], + [], + [], + inArrears, + ) + ) + floatingbondRedemption2 = bondCalendar.adjust(floatingBondMaturityDate2, ql.ModifiedFollowing) + floatingBondLeg2.append(ql.SimpleCashFlow(100.0, floatingbondRedemption2)) + ## generic bond + floatingBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate2, + floatingBondStartDate2, + floatingBondLeg2, + ) + floatingBond2.setPricingEngine(bondEngine) + + ## equivalent specialized floater + floatingSpecializedBond2 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + fixingDays, + [1], + [0.0025], + [], + [], + inArrears, + 100.0, + ql.Date(24, ql.September, 2004), + ) + floatingSpecializedBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + ql.setCouponPricer(floatingSpecializedBond2.cashflows(), self.pricer) + + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + + floatingBondTheoValue2 = floatingBond2.cleanPrice() + floatingSpecializedBondTheoValue2 = floatingSpecializedBond2.cleanPrice() + + error7 = abs(floatingBondTheoValue2 - floatingSpecializedBondTheoValue2) + self.assertFalse( + error7 > tolerance, + "wrong clean price for floater bond:" + + "\n generic floater bond's theo clean price: " + + str(floatingBondTheoValue2) + + "\n equivalent specialized bond's theo clean price: " + + str(floatingSpecializedBondTheoValue2) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + floatingBondTheoDirty2 = floatingBondTheoValue2 + floatingBond2.accruedAmount() + floatingSpecializedTheoDirty2 = floatingSpecializedBondTheoValue2 + floatingSpecializedBond2.accruedAmount() + + error8 = abs(floatingBondTheoDirty2 - floatingSpecializedTheoDirty2) + self.assertFalse( + error8 > tolerance, + "wrong dirty price for floater bond:" + + "\n generic floater bond's theo dirty price: " + + str(floatingBondTheoDirty2) + + "\n equivalent specialized bond's theo dirty price: " + + str(floatingSpecializedTheoDirty2) + + "\n error: " + + str(error8) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0228052402 CRDIT 0 8/22/20) + ## maturity doesn't occur on a business day + cmsBondStartDate1 = ql.Date(22, ql.August, 2005) + cmsBondMaturityDate1 = ql.Date(22, ql.August, 2020) + cmsBondSchedule1 = ql.Schedule( + cmsBondStartDate1, + cmsBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg1 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [], + [], + [0.055], + [0.025], + inArrears, + ) + ) + cmsbondRedemption1 = bondCalendar.adjust(cmsBondMaturityDate1, ql.Following) + cmsBondLeg1.append(ql.SimpleCashFlow(100.0, cmsbondRedemption1)) + ## generic cms bond + cmsBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate1, cmsBondStartDate1, cmsBondLeg1 + ) + cmsBond1.setPricingEngine(bondEngine) + + ## equivalent specialized cms bond + cmsSpecializedBond1 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [1.0], + [0.0], + [0.055], + [0.025], + inArrears, + 100.0, + ql.Date(22, ql.August, 2005), + ) + cmsSpecializedBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + ql.setCouponPricer(cmsSpecializedBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondTheoValue1 = cmsBond1.cleanPrice() + cmsSpecializedBondTheoValue1 = cmsSpecializedBond1.cleanPrice() + error9 = abs(cmsBondTheoValue1 - cmsSpecializedBondTheoValue1) + self.assertFalse( + error9 > tolerance, + "wrong clean price for cms bond:" + + "\n generic cms bond's theo clean price: " + + str(cmsBondTheoValue1) + + "\n equivalent specialized bond's theo clean price: " + + str(cmsSpecializedBondTheoValue1) + + "\n error: " + + str(error9) + + "\n tolerance: " + + str(tolerance), + ) + + cmsBondTheoDirty1 = cmsBondTheoValue1 + cmsBond1.accruedAmount() + cmsSpecializedBondTheoDirty1 = cmsSpecializedBondTheoValue1 + cmsSpecializedBond1.accruedAmount() + error10 = abs(cmsBondTheoDirty1 - cmsSpecializedBondTheoDirty1) + self.assertFalse( + error10 > tolerance, + "wrong dirty price for cms bond:" + + "\n generic cms bond's theo dirty price: " + + str(cmsBondTheoDirty1) + + "\n specialized cms bond's theo dirty price: " + + str(cmsSpecializedBondTheoDirty1) + + "\n error: " + + str(error10) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS Underlying bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ## maturity occurs on a business day + cmsBondStartDate2 = ql.Date(6, ql.May, 2005) + cmsBondMaturityDate2 = ql.Date(6, ql.May, 2015) + cmsBondSchedule2 = ql.Schedule( + cmsBondStartDate2, + cmsBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg2 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [0.84], + [], + [], + [], + inArrears, + ) + ) + cmsbondRedemption2 = bondCalendar.adjust(cmsBondMaturityDate2, ql.Following) + cmsBondLeg2.append(ql.SimpleCashFlow(100.0, cmsbondRedemption2)) + ## generic bond + cmsBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate2, cmsBondStartDate2, cmsBondLeg2 + ) + cmsBond2.setPricingEngine(bondEngine) + + ## equivalent specialized cms bond + cmsSpecializedBond2 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [0.84], + [0.0], + [], + [], + inArrears, + 100.0, + ql.Date(6, ql.May, 2005), + ) + cmsSpecializedBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + ql.setCouponPricer(cmsSpecializedBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondTheoValue2 = cmsBond2.cleanPrice() + cmsSpecializedBondTheoValue2 = cmsSpecializedBond2.cleanPrice() + + error11 = abs(cmsBondTheoValue2 - cmsSpecializedBondTheoValue2) + self.assertFalse( + error11 > tolerance, + "wrong clean price for cms bond:" + + "\n generic cms bond's theo clean price: " + + str(cmsBondTheoValue2) + + "\n cms bond's theo clean price: " + + str(cmsSpecializedBondTheoValue2) + + "\n error: " + + str(error11) + + "\n tolerance: " + + str(tolerance), + ) + + cmsBondTheoDirty2 = cmsBondTheoValue2 + cmsBond2.accruedAmount() + cmsSpecializedBondTheoDirty2 = cmsSpecializedBondTheoValue2 + cmsSpecializedBond2.accruedAmount() + error12 = abs(cmsBondTheoDirty2 - cmsSpecializedBondTheoDirty2) + self.assertFalse( + error12 > tolerance, + "wrong dirty price for cms bond:" + + "\n generic cms bond's dirty price: " + + str(cmsBondTheoDirty2) + + "\n specialized cms bond's theo dirty price: " + + str(cmsSpecializedBondTheoDirty2) + + "\n error: " + + str(error12) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + zeroCpnBondStartDate1 = ql.Date(19, ql.December, 1985) + zeroCpnBondMaturityDate1 = ql.Date(20, ql.December, 2015) + zeroCpnBondRedemption1 = bondCalendar.adjust(zeroCpnBondMaturityDate1, ql.Following) + zeroCpnBondLeg1 = ql.Leg([ql.SimpleCashFlow(100.0, zeroCpnBondRedemption1)]) + + ## generic bond + zeroCpnBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate1, + zeroCpnBondStartDate1, + zeroCpnBondLeg1, + ) + zeroCpnBond1.setPricingEngine(bondEngine) + + ## specialized zerocpn bond + zeroCpnSpecializedBond1 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(20, ql.December, 2015), + ql.Following, + 100.0, + ql.Date(19, ql.December, 1985), + ) + zeroCpnSpecializedBond1.setPricingEngine(bondEngine) + + zeroCpnBondTheoValue1 = zeroCpnBond1.cleanPrice() + zeroCpnSpecializedBondTheoValue1 = zeroCpnSpecializedBond1.cleanPrice() + + error13 = abs(zeroCpnBondTheoValue1 - zeroCpnSpecializedBondTheoValue1) + self.assertFalse( + error13 > tolerance, + "wrong clean price for zero coupon bond:" + + "\n generic zero bond's clean price: " + + str(zeroCpnBondTheoValue1) + + "\n specialized zero bond's clean price: " + + str(zeroCpnSpecializedBondTheoValue1) + + "\n error: " + + str(error13) + + "\n tolerance: " + + str(tolerance), + ) + + zeroCpnBondTheoDirty1 = zeroCpnBondTheoValue1 + zeroCpnBond1.accruedAmount() + zeroCpnSpecializedBondTheoDirty1 = zeroCpnSpecializedBondTheoValue1 + zeroCpnSpecializedBond1.accruedAmount() + error14 = abs(zeroCpnBondTheoDirty1 - zeroCpnSpecializedBondTheoDirty1) + self.assertFalse( + error14 > tolerance, + "wrong dirty price for zero bond:" + + "\n generic zerocpn bond's dirty price: " + + str(zeroCpnBondTheoDirty1) + + "\n specialized zerocpn bond's clean price: " + + str(zeroCpnSpecializedBondTheoDirty1) + + "\n error: " + + str(error14) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity occurs on a business day + zeroCpnBondStartDate2 = ql.Date(17, ql.February, 1998) + zeroCpnBondMaturityDate2 = ql.Date(17, ql.February, 2028) + zerocpbondRedemption2 = bondCalendar.adjust(zeroCpnBondMaturityDate2, ql.Following) + zeroCpnBondLeg2 = ql.Leg([ql.SimpleCashFlow(100.0, zerocpbondRedemption2)]) + ## generic bond + zeroCpnBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate2, + zeroCpnBondStartDate2, + zeroCpnBondLeg2, + ) + zeroCpnBond2.setPricingEngine(bondEngine) + + ## specialized zerocpn bond + zeroCpnSpecializedBond2 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(17, ql.February, 2028), + ql.Following, + 100.0, + ql.Date(17, ql.February, 1998), + ) + zeroCpnSpecializedBond2.setPricingEngine(bondEngine) + + zeroCpnBondTheoValue2 = zeroCpnBond2.cleanPrice() + zeroCpnSpecializedBondTheoValue2 = zeroCpnSpecializedBond2.cleanPrice() + + error15 = abs(zeroCpnBondTheoValue2 - zeroCpnSpecializedBondTheoValue2) + self.assertFalse( + error15 > tolerance, + "wrong clean price for zero coupon bond:" + + "\n generic zerocpn bond's clean price: " + + str(zeroCpnBondTheoValue2) + + "\n specialized zerocpn bond's clean price: " + + str(zeroCpnSpecializedBondTheoValue2) + + "\n error: " + + str(error15) + + "\n tolerance: " + + str(tolerance), + ) + + zeroCpnBondTheoDirty2 = zeroCpnBondTheoValue2 + zeroCpnBond2.accruedAmount() + + zeroCpnSpecializedBondTheoDirty2 = zeroCpnSpecializedBondTheoValue2 + zeroCpnSpecializedBond2.accruedAmount() + + error16 = abs(zeroCpnBondTheoDirty2 - zeroCpnSpecializedBondTheoDirty2) + self.assertFalse( + error16 > tolerance, + "wrong dirty price for zero coupon bond:" + + "\n generic zerocpn bond's dirty price: " + + str(zeroCpnBondTheoDirty2) + + "\n specialized zerocpn bond's dirty price: " + + str(zeroCpnSpecializedBondTheoDirty2) + + "\n error: " + + str(error16) + + "\n tolerance: " + + str(tolerance), + ) + + def testSpecializedBondVsGenericBondUsingAsw(self): + """Testing asset-swap prices and spreads for specialized bond against equivalent generic bond...""" + + bondCalendar = ql.TARGET() + settlementDays = 3 + fixingDays = 2 + payFixedRate = True + parAssetSwap = True + inArrears = False + + ## Fixed bond (Isin: DE0001135275 DBR 4 01/04/37) + ## maturity doesn't occur on a business day + fixedBondStartDate1 = ql.Date(4, ql.January, 2005) + fixedBondMaturityDate1 = ql.Date(4, ql.January, 2037) + fixedBondSchedule1 = ql.Schedule( + fixedBondStartDate1, + fixedBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg1 = list( + ql.FixedRateLeg(fixedBondSchedule1, ql.ActualActual(ql.ActualActual.ISDA), [self.faceAmount], [0.04]) + ) + fixedbondRedemption1 = bondCalendar.adjust(fixedBondMaturityDate1, ql.Following) + fixedBondLeg1.append(ql.SimpleCashFlow(100.0, fixedbondRedemption1)) + ## generic bond + fixedBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate1, fixedBondStartDate1, fixedBondLeg1 + ) + + bondEngine = ql.DiscountingBondEngine(self.termStructure) + swapEngine = ql.DiscountingSwapEngine(self.termStructure, False) + fixedBond1.setPricingEngine(bondEngine) + + ## equivalent specialized fixed rate bond + fixedSpecializedBond1 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule1, + [0.04], + ql.ActualActual(ql.ActualActual.ISDA), + ql.Following, + 100.0, + ql.Date(4, ql.January, 2005), + ) + fixedSpecializedBond1.setPricingEngine(bondEngine) + + fixedBondPrice1 = fixedBond1.cleanPrice() + fixedSpecializedBondPrice1 = fixedSpecializedBond1.cleanPrice() + fixedBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondAssetSwap1.setPricingEngine(swapEngine) + fixedSpecializedBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + fixedSpecializedBond1, + fixedSpecializedBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedSpecializedBondAssetSwap1.setPricingEngine(swapEngine) + fixedBondAssetSwapPrice1 = fixedBondAssetSwap1.fairCleanPrice() + fixedSpecializedBondAssetSwapPrice1 = fixedSpecializedBondAssetSwap1.fairCleanPrice() + tolerance = 1.0e-13 + error1 = abs(fixedBondAssetSwapPrice1 - fixedSpecializedBondAssetSwapPrice1) + self.assertFalse( + error1 > tolerance, + "wrong clean price for fixed bond:" + + "\n generic fixed rate bond's clean price: " + + str(fixedBondAssetSwapPrice1) + + "\n equivalent specialized bond's clean price: " + + str(fixedSpecializedBondAssetSwapPrice1) + + "\n error: " + + str(error1) + + "\n tolerance: " + + str(tolerance), + ) + + ## market executable price as of 4th sept 2007 + fixedBondMktPrice1 = 91.832 + fixedBondASW1 = ql.AssetSwap( + payFixedRate, + fixedBond1, + fixedBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondASW1.setPricingEngine(swapEngine) + fixedSpecializedBondASW1 = ql.AssetSwap( + payFixedRate, + fixedSpecializedBond1, + fixedBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedSpecializedBondASW1.setPricingEngine(swapEngine) + fixedBondASWSpread1 = fixedBondASW1.fairSpread() + fixedSpecializedBondASWSpread1 = fixedSpecializedBondASW1.fairSpread() + error2 = abs(fixedBondASWSpread1 - fixedSpecializedBondASWSpread1) + self.assertFalse( + error2 > tolerance, + "wrong asw spread for fixed bond:" + + "\n generic fixed rate bond's asw spread: " + + str(fixedBondASWSpread1) + + "\n equivalent specialized bond's asw spread: " + + str(fixedSpecializedBondASWSpread1) + + "\n error: " + + str(error2) + + "\n tolerance: " + + str(tolerance), + ) + + ##Fixed bond (Isin: IT0006527060 IBRD 5 02/05/19) + ##maturity occurs on a business day + + fixedBondStartDate2 = ql.Date(5, ql.February, 2005) + fixedBondMaturityDate2 = ql.Date(5, ql.February, 2019) + fixedBondSchedule2 = ql.Schedule( + fixedBondStartDate2, + fixedBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + fixedBondLeg2 = list( + ql.FixedRateLeg(fixedBondSchedule2, ql.Thirty360(ql.Thirty360.BondBasis), [self.faceAmount], [0.05]) + ) + fixedbondRedemption2 = bondCalendar.adjust(fixedBondMaturityDate2, ql.Following) + fixedBondLeg2.append(ql.SimpleCashFlow(100.0, fixedbondRedemption2)) + + ## generic bond + fixedBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, fixedBondMaturityDate2, fixedBondStartDate2, fixedBondLeg2 + ) + fixedBond2.setPricingEngine(bondEngine) + + ## equivalent specialized fixed rate bond + fixedSpecializedBond2 = ql.FixedRateBond( + settlementDays, + self.faceAmount, + fixedBondSchedule2, + [0.05], + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + 100.0, + ql.Date(5, ql.February, 2005), + ) + fixedSpecializedBond2.setPricingEngine(bondEngine) + + fixedBondPrice2 = fixedBond2.cleanPrice() + fixedSpecializedBondPrice2 = fixedSpecializedBond2.cleanPrice() + fixedBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondAssetSwap2.setPricingEngine(swapEngine) + fixedSpecializedBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + fixedSpecializedBond2, + fixedSpecializedBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedSpecializedBondAssetSwap2.setPricingEngine(swapEngine) + fixedBondAssetSwapPrice2 = fixedBondAssetSwap2.fairCleanPrice() + fixedSpecializedBondAssetSwapPrice2 = fixedSpecializedBondAssetSwap2.fairCleanPrice() + + error3 = abs(fixedBondAssetSwapPrice2 - fixedSpecializedBondAssetSwapPrice2) + self.assertFalse( + error3 > tolerance, + "wrong clean price for fixed bond:" + + "\n generic fixed rate bond's clean price: " + + str(fixedBondAssetSwapPrice2) + + "\n equivalent specialized bond's clean price: " + + str(fixedSpecializedBondAssetSwapPrice2) + + "\n error: " + + str(error3) + + "\n tolerance: " + + str(tolerance), + ) + + ## market executable price as of 4th sept 2007 + fixedBondMktPrice2 = 102.178 + fixedBondASW2 = ql.AssetSwap( + payFixedRate, + fixedBond2, + fixedBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedBondASW2.setPricingEngine(swapEngine) + fixedSpecializedBondASW2 = ql.AssetSwap( + payFixedRate, + fixedSpecializedBond2, + fixedBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + fixedSpecializedBondASW2.setPricingEngine(swapEngine) + fixedBondASWSpread2 = fixedBondASW2.fairSpread() + fixedSpecializedBondASWSpread2 = fixedSpecializedBondASW2.fairSpread() + error4 = abs(fixedBondASWSpread2 - fixedSpecializedBondASWSpread2) + self.assertFalse( + error4 > tolerance, + "wrong asw spread for fixed bond:" + + "\n generic fixed rate bond's asw spread: " + + str(fixedBondASWSpread2) + + "\n equivalent specialized bond's asw spread: " + + str(fixedSpecializedBondASWSpread2) + + "\n error: " + + str(error4) + + "\n tolerance: " + + str(tolerance), + ) + + ##FRN bond (Isin: IT0003543847 ISPIM 0 09/29/13) + ##maturity doesn't occur on a business day + floatingBondStartDate1 = ql.Date(29, ql.September, 2003) + floatingBondMaturityDate1 = ql.Date(29, ql.September, 2013) + floatingBondSchedule1 = ql.Schedule( + floatingBondStartDate1, + floatingBondMaturityDate1, + ql.Period(ql.Semiannual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg1 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + [fixingDays], + [], + [0.0056], + [], + [], + inArrears, + ) + ) + floatingbondRedemption1 = bondCalendar.adjust(floatingBondMaturityDate1, ql.Following) + floatingBondLeg1.append(ql.SimpleCashFlow(100.0, floatingbondRedemption1)) + ## generic bond + floatingBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate1, + floatingBondStartDate1, + floatingBondLeg1, + ) + floatingBond1.setPricingEngine(bondEngine) + + ## equivalent specialized floater + floatingSpecializedBond1 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule1, + self.iborIndex, + ql.Actual360(), + ql.Following, + fixingDays, + [1], + [0.0056], + [], + [], + inArrears, + 100.0, + ql.Date(29, ql.September, 2003), + ) + floatingSpecializedBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond1.cashflows(), self.pricer) + ql.setCouponPricer(floatingSpecializedBond1.cashflows(), self.pricer) + self.iborIndex.addFixing(ql.Date(27, ql.March, 2007), 0.0402) + floatingBondPrice1 = floatingBond1.cleanPrice() + floatingSpecializedBondPrice1 = floatingSpecializedBond1.cleanPrice() + floatingBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondAssetSwap1.setPricingEngine(swapEngine) + floatingSpecializedBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + floatingSpecializedBond1, + floatingSpecializedBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingSpecializedBondAssetSwap1.setPricingEngine(swapEngine) + floatingBondAssetSwapPrice1 = floatingBondAssetSwap1.fairCleanPrice() + floatingSpecializedBondAssetSwapPrice1 = floatingSpecializedBondAssetSwap1.fairCleanPrice() + + error5 = abs(floatingBondAssetSwapPrice1 - floatingSpecializedBondAssetSwapPrice1) + self.assertFalse( + error5 > tolerance, + "wrong clean price for frnbond:" + + "\n generic frn rate bond's clean price: " + + str(floatingBondAssetSwapPrice1) + + "\n equivalent specialized bond's price: " + + str(floatingSpecializedBondAssetSwapPrice1) + + "\n error: " + + str(error5) + + "\n tolerance: " + + str(tolerance), + ) + + ## market executable price as of 4th sept 2007 + floatingBondMktPrice1 = 101.33 + floatingBondASW1 = ql.AssetSwap( + payFixedRate, + floatingBond1, + floatingBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondASW1.setPricingEngine(swapEngine) + floatingSpecializedBondASW1 = ql.AssetSwap( + payFixedRate, + floatingSpecializedBond1, + floatingBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingSpecializedBondASW1.setPricingEngine(swapEngine) + floatingBondASWSpread1 = floatingBondASW1.fairSpread() + floatingSpecializedBondASWSpread1 = floatingSpecializedBondASW1.fairSpread() + error6 = abs(floatingBondASWSpread1 - floatingSpecializedBondASWSpread1) + self.assertFalse( + error6 > tolerance, + "wrong asw spread for fixed bond:" + + "\n generic frn rate bond's asw spread: " + + str(floatingBondASWSpread1) + + "\n equivalent specialized bond's asw spread: " + + str(floatingSpecializedBondASWSpread1) + + "\n error: " + + str(error6) + + "\n tolerance: " + + str(tolerance), + ) + + ##FRN bond (Isin: XS0090566539 COE 0 09/24/18) + ##maturity occurs on a business day + floatingBondStartDate2 = ql.Date(24, ql.September, 2004) + floatingBondMaturityDate2 = ql.Date(24, ql.September, 2018) + floatingBondSchedule2 = ql.Schedule( + floatingBondStartDate2, + floatingBondMaturityDate2, + ql.Period(ql.Semiannual), + bondCalendar, + ql.ModifiedFollowing, + ql.ModifiedFollowing, + ql.DateGeneration.Backward, + False, + ) + floatingBondLeg2 = list( + ql.IborLeg( + [self.faceAmount], + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + [fixingDays], + [], + [0.0025], + [], + [], + inArrears, + ) + ) + floatingbondRedemption2 = bondCalendar.adjust(floatingBondMaturityDate2, ql.ModifiedFollowing) + floatingBondLeg2.append(ql.SimpleCashFlow(100.0, floatingbondRedemption2)) + ## generic bond + floatingBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + floatingBondMaturityDate2, + floatingBondStartDate2, + floatingBondLeg2, + ) + floatingBond2.setPricingEngine(bondEngine) + + ## equivalent specialized floater + floatingSpecializedBond2 = ql.FloatingRateBond( + settlementDays, + self.faceAmount, + floatingBondSchedule2, + self.iborIndex, + ql.Actual360(), + ql.ModifiedFollowing, + fixingDays, + [1], + [0.0025], + [], + [], + inArrears, + 100.0, + ql.Date(24, ql.September, 2004), + ) + floatingSpecializedBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(floatingBond2.cashflows(), self.pricer) + ql.setCouponPricer(floatingSpecializedBond2.cashflows(), self.pricer) + + self.iborIndex.addFixing(ql.Date(22, ql.March, 2007), 0.04013) + + floatingBondPrice2 = floatingBond2.cleanPrice() + floatingSpecializedBondPrice2 = floatingSpecializedBond2.cleanPrice() + floatingBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondAssetSwap2.setPricingEngine(swapEngine) + floatingSpecializedBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + floatingSpecializedBond2, + floatingSpecializedBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingSpecializedBondAssetSwap2.setPricingEngine(swapEngine) + floatingBondAssetSwapPrice2 = floatingBondAssetSwap2.fairCleanPrice() + floatingSpecializedBondAssetSwapPrice2 = floatingSpecializedBondAssetSwap2.fairCleanPrice() + error7 = abs(floatingBondAssetSwapPrice2 - floatingSpecializedBondAssetSwapPrice2) + self.assertFalse( + error7 > tolerance, + "wrong clean price for frnbond:" + + "\n generic frn rate bond's clean price: " + + str(floatingBondAssetSwapPrice2) + + "\n equivalent specialized frn bond's price: " + + str(floatingSpecializedBondAssetSwapPrice2) + + "\n error: " + + str(error7) + + "\n tolerance: " + + str(tolerance), + ) + + ## market executable price as of 4th sept 2007 + floatingBondMktPrice2 = 101.26 + floatingBondASW2 = ql.AssetSwap( + payFixedRate, + floatingBond2, + floatingBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingBondASW2.setPricingEngine(swapEngine) + floatingSpecializedBondASW2 = ql.AssetSwap( + payFixedRate, + floatingSpecializedBond2, + floatingBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + floatingSpecializedBondASW2.setPricingEngine(swapEngine) + floatingBondASWSpread2 = floatingBondASW2.fairSpread() + floatingSpecializedBondASWSpread2 = floatingSpecializedBondASW2.fairSpread() + error8 = abs(floatingBondASWSpread2 - floatingSpecializedBondASWSpread2) + self.assertFalse( + error8 > tolerance, + "wrong asw spread for frn bond:" + + "\n generic frn rate bond's asw spread: " + + str(floatingBondASWSpread2) + + "\n equivalent specialized bond's asw spread: " + + str(floatingSpecializedBondASWSpread2) + + "\n error: " + + str(error8) + + "\n tolerance: " + + str(tolerance), + ) + + ## CMS bond (Isin: XS0228052402 CRDIT 0 8/22/20) + ## maturity doesn't occur on a business day + + cmsBondStartDate1 = ql.Date(22, ql.August, 2005) + cmsBondMaturityDate1 = ql.Date(22, ql.August, 2020) + cmsBondSchedule1 = ql.Schedule( + cmsBondStartDate1, + cmsBondMaturityDate1, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg1 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [], + [], + [0.055], + [0.025], + inArrears, + ) + ) + cmsbondRedemption1 = bondCalendar.adjust(cmsBondMaturityDate1, ql.Following) + cmsBondLeg1.append(ql.SimpleCashFlow(100.0, cmsbondRedemption1)) + ## generic cms bond + cmsBond1 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate1, cmsBondStartDate1, cmsBondLeg1 + ) + cmsBond1.setPricingEngine(bondEngine) + + ## equivalent specialized cms bond + cmsSpecializedBond1 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule1, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [1.0], + [0.0], + [0.055], + [0.025], + inArrears, + 100.0, + ql.Date(22, ql.August, 2005), + ) + cmsSpecializedBond1.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond1.cashflows(), self.cmspricer) + ql.setCouponPricer(cmsSpecializedBond1.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(18, ql.August, 2006), 0.04158) + cmsBondPrice1 = cmsBond1.cleanPrice() + cmsSpecializedBondPrice1 = cmsSpecializedBond1.cleanPrice() + cmsBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondAssetSwap1.setPricingEngine(swapEngine) + cmsSpecializedBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + cmsSpecializedBond1, + cmsSpecializedBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsSpecializedBondAssetSwap1.setPricingEngine(swapEngine) + cmsBondAssetSwapPrice1 = cmsBondAssetSwap1.fairCleanPrice() + cmsSpecializedBondAssetSwapPrice1 = cmsSpecializedBondAssetSwap1.fairCleanPrice() + error9 = abs(cmsBondAssetSwapPrice1 - cmsSpecializedBondAssetSwapPrice1) + self.assertFalse( + error9 > tolerance, + "wrong clean price for cmsbond:" + + "\n generic bond's clean price: " + + str(cmsBondAssetSwapPrice1) + + "\n equivalent specialized cms rate bond's price: " + + str(cmsSpecializedBondAssetSwapPrice1) + + "\n error: " + + str(error9) + + "\n tolerance: " + + str(tolerance), + ) + + cmsBondMktPrice1 = 87.02 ## market executable price as of 4th sept 2007 + cmsBondASW1 = ql.AssetSwap( + payFixedRate, + cmsBond1, + cmsBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondASW1.setPricingEngine(swapEngine) + cmsSpecializedBondASW1 = ql.AssetSwap( + payFixedRate, + cmsSpecializedBond1, + cmsBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsSpecializedBondASW1.setPricingEngine(swapEngine) + cmsBondASWSpread1 = cmsBondASW1.fairSpread() + cmsSpecializedBondASWSpread1 = cmsSpecializedBondASW1.fairSpread() + error10 = abs(cmsBondASWSpread1 - cmsSpecializedBondASWSpread1) + self.assertFalse( + error10 > tolerance, + "wrong asw spread for cm bond:" + + "\n generic cms rate bond's asw spread: " + + str(cmsBondASWSpread1) + + "\n equivalent specialized bond's asw spread: " + + str(cmsSpecializedBondASWSpread1) + + "\n error: " + + str(error10) + + "\n tolerance: " + + str(tolerance), + ) + + ##CMS bond (Isin: XS0218766664 ISPIM 0 5/6/15) + ##maturity occurs on a business day + cmsBondStartDate2 = ql.Date(6, ql.May, 2005) + cmsBondMaturityDate2 = ql.Date(6, ql.May, 2015) + cmsBondSchedule2 = ql.Schedule( + cmsBondStartDate2, + cmsBondMaturityDate2, + ql.Period(ql.Annual), + bondCalendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + cmsBondLeg2 = list( + ql.CmsLeg( + [self.faceAmount], + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + [fixingDays], + [0.84], + [], + [], + [], + inArrears, + ) + ) + cmsbondRedemption2 = bondCalendar.adjust(cmsBondMaturityDate2, ql.Following) + cmsBondLeg2.append(ql.SimpleCashFlow(100.0, cmsbondRedemption2)) + ## generic bond + cmsBond2 = ql.Bond( + settlementDays, bondCalendar, self.faceAmount, cmsBondMaturityDate2, cmsBondStartDate2, cmsBondLeg2 + ) + cmsBond2.setPricingEngine(bondEngine) + + ## equivalent specialized cms bond + cmsSpecializedBond2 = ql.CmsRateBond( + settlementDays, + self.faceAmount, + cmsBondSchedule2, + self.swapIndex, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Following, + fixingDays, + [0.84], + [0.0], + [], + [], + inArrears, + 100.0, + ql.Date(6, ql.May, 2005), + ) + cmsSpecializedBond2.setPricingEngine(bondEngine) + + ql.setCouponPricer(cmsBond2.cashflows(), self.cmspricer) + ql.setCouponPricer(cmsSpecializedBond2.cashflows(), self.cmspricer) + self.swapIndex.addFixing(ql.Date(4, ql.May, 2006), 0.04217) + cmsBondPrice2 = cmsBond2.cleanPrice() + cmsSpecializedBondPrice2 = cmsSpecializedBond2.cleanPrice() + cmsBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondAssetSwap2.setPricingEngine(swapEngine) + cmsSpecializedBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + cmsSpecializedBond2, + cmsSpecializedBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsSpecializedBondAssetSwap2.setPricingEngine(swapEngine) + cmsBondAssetSwapPrice2 = cmsBondAssetSwap2.fairCleanPrice() + cmsSpecializedBondAssetSwapPrice2 = cmsSpecializedBondAssetSwap2.fairCleanPrice() + error11 = abs(cmsBondAssetSwapPrice2 - cmsSpecializedBondAssetSwapPrice2) + self.assertFalse( + error11 > tolerance, + "wrong clean price for cmsbond:" + + "\n generic bond's clean price: " + + str(cmsBondAssetSwapPrice2) + + "\n equivalent specialized cms rate bond's price: " + + str(cmsSpecializedBondAssetSwapPrice2) + + "\n error: " + + str(error11) + + "\n tolerance: " + + str(tolerance), + ) + + cmsBondMktPrice2 = 94.35 ## market executable price as of 4th sept 2007 + cmsBondASW2 = ql.AssetSwap( + payFixedRate, + cmsBond2, + cmsBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsBondASW2.setPricingEngine(swapEngine) + cmsSpecializedBondASW2 = ql.AssetSwap( + payFixedRate, + cmsSpecializedBond2, + cmsBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + cmsSpecializedBondASW2.setPricingEngine(swapEngine) + cmsBondASWSpread2 = cmsBondASW2.fairSpread() + cmsSpecializedBondASWSpread2 = cmsSpecializedBondASW2.fairSpread() + error12 = abs(cmsBondASWSpread2 - cmsSpecializedBondASWSpread2) + self.assertFalse( + error12 > tolerance, + "wrong asw spread for cm bond:" + + "\n generic cms rate bond's asw spread: " + + str(cmsBondASWSpread2) + + "\n equivalent specialized bond's asw spread: " + + str(cmsSpecializedBondASWSpread2) + + "\n error: " + + str(error12) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero-Coupon bond (Isin: DE0004771662 IBRD 0 12/20/15) + ## maturity doesn't occur on a business day + zeroCpnBondStartDate1 = ql.Date(19, ql.December, 1985) + zeroCpnBondMaturityDate1 = ql.Date(20, ql.December, 2015) + zeroCpnBondRedemption1 = bondCalendar.adjust(zeroCpnBondMaturityDate1, ql.Following) + zeroCpnBondLeg1 = ql.Leg([ql.SimpleCashFlow(100.0, zeroCpnBondRedemption1)]) + ## generic bond + zeroCpnBond1 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate1, + zeroCpnBondStartDate1, + zeroCpnBondLeg1, + ) + zeroCpnBond1.setPricingEngine(bondEngine) + + ## specialized zerocpn bond + zeroCpnSpecializedBond1 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(20, ql.December, 2015), + ql.Following, + 100.0, + ql.Date(19, ql.December, 1985), + ) + zeroCpnSpecializedBond1.setPricingEngine(bondEngine) + + zeroCpnBondPrice1 = zeroCpnBond1.cleanPrice() + zeroCpnSpecializedBondPrice1 = zeroCpnSpecializedBond1.cleanPrice() + zeroCpnBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondAssetSwap1.setPricingEngine(swapEngine) + zeroCpnSpecializedBondAssetSwap1 = ql.AssetSwap( + payFixedRate, + zeroCpnSpecializedBond1, + zeroCpnSpecializedBondPrice1, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnSpecializedBondAssetSwap1.setPricingEngine(swapEngine) + zeroCpnBondAssetSwapPrice1 = zeroCpnBondAssetSwap1.fairCleanPrice() + zeroCpnSpecializedBondAssetSwapPrice1 = zeroCpnSpecializedBondAssetSwap1.fairCleanPrice() + error13 = abs(zeroCpnBondAssetSwapPrice1 - zeroCpnSpecializedBondAssetSwapPrice1) + self.assertFalse( + error13 > tolerance, + "wrong clean price for zerocpn bond:" + + "\n generic zero cpn bond's clean price: " + + str(zeroCpnBondAssetSwapPrice1) + + "\n specialized equivalent bond's price: " + + str(zeroCpnSpecializedBondAssetSwapPrice1) + + "\n error: " + + str(error13) + + "\n tolerance: " + + str(tolerance), + ) + + ## market executable price as of 4th sept 2007 + zeroCpnBondMktPrice1 = 72.277 + zeroCpnBondASW1 = ql.AssetSwap( + payFixedRate, + zeroCpnBond1, + zeroCpnBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondASW1.setPricingEngine(swapEngine) + zeroCpnSpecializedBondASW1 = ql.AssetSwap( + payFixedRate, + zeroCpnSpecializedBond1, + zeroCpnBondMktPrice1, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnSpecializedBondASW1.setPricingEngine(swapEngine) + zeroCpnBondASWSpread1 = zeroCpnBondASW1.fairSpread() + zeroCpnSpecializedBondASWSpread1 = zeroCpnSpecializedBondASW1.fairSpread() + error14 = abs(zeroCpnBondASWSpread1 - zeroCpnSpecializedBondASWSpread1) + self.assertFalse( + error14 > tolerance, + "wrong asw spread for zeroCpn bond:" + + "\n generic zeroCpn bond's asw spread: " + + str(zeroCpnBondASWSpread1) + + "\n equivalent specialized bond's asw spread: " + + str(zeroCpnSpecializedBondASWSpread1) + + "\n error: " + + str(error14) + + "\n tolerance: " + + str(tolerance), + ) + + ## Zero Coupon bond (Isin: IT0001200390 ISPIM 0 02/17/28) + ## maturity doesn't occur on a business day + zeroCpnBondStartDate2 = ql.Date(17, ql.February, 1998) + zeroCpnBondMaturityDate2 = ql.Date(17, ql.February, 2028) + zerocpbondRedemption2 = bondCalendar.adjust(zeroCpnBondMaturityDate2, ql.Following) + zeroCpnBondLeg2 = ql.Leg([ql.SimpleCashFlow(100.0, zerocpbondRedemption2)]) + ## generic bond + zeroCpnBond2 = ql.Bond( + settlementDays, + bondCalendar, + self.faceAmount, + zeroCpnBondMaturityDate2, + zeroCpnBondStartDate2, + zeroCpnBondLeg2, + ) + zeroCpnBond2.setPricingEngine(bondEngine) + + ## specialized zerocpn bond + zeroCpnSpecializedBond2 = ql.ZeroCouponBond( + settlementDays, + bondCalendar, + self.faceAmount, + ql.Date(17, ql.February, 2028), + ql.Following, + 100.0, + ql.Date(17, ql.February, 1998), + ) + zeroCpnSpecializedBond2.setPricingEngine(bondEngine) + + zeroCpnBondPrice2 = zeroCpnBond2.cleanPrice() + zeroCpnSpecializedBondPrice2 = zeroCpnSpecializedBond2.cleanPrice() + + zeroCpnBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondAssetSwap2.setPricingEngine(swapEngine) + zeroCpnSpecializedBondAssetSwap2 = ql.AssetSwap( + payFixedRate, + zeroCpnSpecializedBond2, + zeroCpnSpecializedBondPrice2, + self.iborIndex, + self.nonnullspread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnSpecializedBondAssetSwap2.setPricingEngine(swapEngine) + zeroCpnBondAssetSwapPrice2 = zeroCpnBondAssetSwap2.fairCleanPrice() + zeroCpnSpecializedBondAssetSwapPrice2 = zeroCpnSpecializedBondAssetSwap2.fairCleanPrice() + error15 = abs(zeroCpnBondAssetSwapPrice2 - zeroCpnSpecializedBondAssetSwapPrice2) + self.assertFalse( + error8 > tolerance, + "wrong clean price for zerocpn bond:" + + "\n generic zero cpn bond's clean price: " + + str(zeroCpnBondAssetSwapPrice2) + + "\n equivalent specialized bond's price: " + + str(zeroCpnSpecializedBondAssetSwapPrice2) + + "\n error: " + + str(error15) + + "\n tolerance: " + + str(tolerance), + ) + + ## market executable price as of 4th sept 2007 + zeroCpnBondMktPrice2 = 72.277 + zeroCpnBondASW2 = ql.AssetSwap( + payFixedRate, + zeroCpnBond2, + zeroCpnBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnBondASW2.setPricingEngine(swapEngine) + zeroCpnSpecializedBondASW2 = ql.AssetSwap( + payFixedRate, + zeroCpnSpecializedBond2, + zeroCpnBondMktPrice2, + self.iborIndex, + self.spread, + ql.Schedule(), + self.iborIndex.dayCounter(), + parAssetSwap, + ) + zeroCpnSpecializedBondASW2.setPricingEngine(swapEngine) + zeroCpnBondASWSpread2 = zeroCpnBondASW2.fairSpread() + zeroCpnSpecializedBondASWSpread2 = zeroCpnSpecializedBondASW2.fairSpread() + error16 = abs(zeroCpnBondASWSpread2 - zeroCpnSpecializedBondASWSpread2) + self.assertFalse( + error16 > tolerance, + "wrong asw spread for zeroCpn bond:" + + "\n generic zeroCpn bond's asw spread: " + + str(zeroCpnBondASWSpread2) + + "\n equivalent specialized bond's asw spread: " + + str(zeroCpnSpecializedBondASWSpread2) + + "\n error: " + + str(error16) + + "\n tolerance: " + + str(tolerance), + ) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(AssetSwapTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/blackformula.py b/quantlib/test/blackformula.py new file mode 100644 index 0000000..24c02f8 --- /dev/null +++ b/quantlib/test/blackformula.py @@ -0,0 +1,141 @@ +# coding=utf-8-unix +""" + Copyright (C) 2017 Wojciech Ślusarski + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" +import unittest +import math +import mxdevtool as ql + +class BlackFormulaTest(unittest.TestCase): + + def setUp(self): + # define the market and option parameters + self.option_type = ql.Option.Call + self.spot = 100.0 + self.strike = 100.0 + self.risk_free_rate = 0.05 + self.expiry = 1.0 + self.forward = self.spot * math.exp(self.risk_free_rate * self.expiry) + self.df = math.exp(-self.risk_free_rate * self.expiry) + self.vol = 0.2 * math.sqrt(self.expiry) + self.displacement = 0.0 + + def test_blackFormula(self): + """Testing blackFormula in a simple Black-Scholes World...""" + #Anyone interested, feel free to provide more accurate number + expected = 10.4506 + res = ql.blackFormula(self.option_type, + self.strike, + self.forward, + self.vol, + self.df, + self.displacement) + self.assertAlmostEquals(expected, res, delta=1e-4, + msg="Failed to calculate simple " + "Black-Scholes-Merton price rounded to " + "four decimal places.") + + def test_black_formula_implied_stdev(self): + """Testing implied volatility calculator""" + expected = 0.2 * math.sqrt(self.expiry) + black_price = 10.4506 + res = ql.blackFormulaImpliedStdDev(self.option_type, + self.strike, + self.forward, + black_price, + self.df) + self.assertAlmostEquals(expected, res, delta=1e-4, + msg="Failed to determine Implied Vol rounded " + "to a single vol bps.") + + +class BlackDeltaCalculatorTest(unittest.TestCase): + + def setUp(self): + self.todaysDate = ql.Date(5, ql.September, 2017) + ql.Settings.instance().evaluationDate = self.todaysDate + self.spotDate = ql.Date(7, ql.September, 2017) + self.domestic_rate = ql.FlatForward(self.spotDate, 0.017, + ql.Actual365Fixed()) + self.foreign_rate = ql.FlatForward(self.spotDate, 0.013, + ql.Actual365Fixed()) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def test_single_spot_delta(self): + """Test for a single strike for call spot delta 75""" + volatility = 0.2 + expiry = 2 + spot_price = 3.6 + domDf = self.domestic_rate.discount(expiry) + forDf = self.foreign_rate.discount(expiry) + forward = spot_price * forDf / domDf + + spot_delta_level = 0.75 + stDev = volatility * expiry ** 0.5 + + inv_norm_dist = ql.InverseCumulativeNormal() + expected_strike = inv_norm_dist(spot_delta_level / forDf) + expected_strike *= stDev + expected_strike -= 0.5 * stDev ** 2 + expected_strike = math.exp(expected_strike) / forward + expected_strike = 1 / expected_strike + + option_type = ql.Option.Call + delta_type = ql.DeltaVolQuote.Spot + + black_calculator = ql.BlackDeltaCalculator(option_type, + delta_type, + spot_price, + domDf, + forDf, + stDev) + + + + strike = black_calculator.strikeFromDelta(spot_delta_level) + + self.assertAlmostEquals(expected_strike, strike, delta=1e-4) + + def test_spot_atm_delta_calculator(self): + """Test for 0-delta straddle strike""" + volatility = 0.2 + expiry = 2 + spot_price = 3.6 + domDf = self.domestic_rate.discount(expiry) + forDf = self.foreign_rate.discount(expiry) + forward = spot_price * forDf / domDf + expected_strike = forward * math.exp(-0.5 * volatility ** 2 * expiry) + + option_type = ql.Option.Call + delta_type = ql.DeltaVolQuote.AtmDeltaNeutral + stDev = volatility * expiry ** 0.5 + + black_calculator = ql.BlackDeltaCalculator(option_type, + delta_type, + spot_price, + domDf, + forDf, + stDev) + + strike = black_calculator.atmStrike(ql.DeltaVolQuote.AtmDeltaNeutral) + + self.assertAlmostEquals(expected_strike, strike, delta=1e-4) + + +if __name__ == '__main__': + unittest.main() diff --git a/quantlib/test/bonds.py b/quantlib/test/bonds.py new file mode 100644 index 0000000..054b9fd --- /dev/null +++ b/quantlib/test/bonds.py @@ -0,0 +1,364 @@ +""" + Copyright (C) 2009 Joseph Malicki + Copyright (C) 2019 Prasad Somwanshi + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + + +class FixedRateBondTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = ql.Date(2, 1, 2010) + self.settlement_days = 3 + self.face_amount = 100.0 + self.redemption = 100.0 + self.issue_date = ql.Date(2, 1, 2008) + self.maturity_date = ql.Date(2, 1, 2018) + self.calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + self.day_counter = ql.ActualActual(ql.ActualActual.Bond) + self.sched = ql.Schedule( + self.issue_date, + self.maturity_date, + ql.Period(ql.Semiannual), + self.calendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + self.coupons = [0.05] + + self.bond = ql.FixedRateBond( + self.settlement_days, + self.face_amount, + self.sched, + self.coupons, + self.day_counter, + ql.Following, + self.redemption, + self.issue_date, + ) + + self.flat_forward = ql.FlatForward( + self.issue_date, self.coupons[0], self.day_counter, ql.Compounded, ql.Semiannual + ) + self.term_structure_handle = ql.RelinkableYieldTermStructureHandle(self.flat_forward) + bondEngine = ql.DiscountingBondEngine(self.term_structure_handle) + self.bond.setPricingEngine(bondEngine) + + def testFrequency(self): + """ Testing FixedRateBond frequency() method. """ + self.assertEqual(self.bond.frequency(), ql.Semiannual) + + def testDayCounter(self): + """ Testing FixedRateBond dayCounter() method. """ + self.assertEqual(self.bond.dayCounter(), self.day_counter) + + def testSimpleInspectors(self): + """ Testing FixedRateBond simple inspectors. """ + self.assertEqual(self.bond.settlementDays(), self.settlement_days) + self.assertEqual(self.bond.notional(), self.face_amount) + self.assertEqual(self.bond.issueDate(), self.issue_date) + self.assertEqual(self.bond.maturityDate(), self.maturity_date) + + # def testSettlementValue(self): + # """ Testing FixedRateBond settlement value. """ + # orig_date = ql.Settings.evaluationDate + # ql.Settings.evaluationDate = self.issue_date + 1*ql.Months + # self.assertEqual(round(self.bond.settlementValue(100.0), 4), 102.3098) + # ql.Settings.evaluationDate = orig_date + + def testCashFlows(self): + """ Testing that the FixedRateBond gives the expected cash flows. """ + self.assertEqual( + [round(cf.amount(), 4) for cf in self.bond.cashflows()], + 20 * [round(self.face_amount * self.coupons[0] / 2, 4)] + [round(self.redemption, 4)], + ) + + def testRedemption(self): + """ Testing FixedRateBond redemption value and date. """ + self.assertEqual(self.bond.redemption().date(), self.maturity_date) + self.assertEqual(self.bond.redemption().amount(), self.redemption) + + def testRedemptions(self): + """ Testing FixedRateBond redemptions. """ + redemptions = self.bond.redemptions() + self.assertEqual(len(redemptions), 1) + self.assertEqual(redemptions[0].date(), self.maturity_date) + self.assertEqual(redemptions[0].amount(), self.redemption) + + def testNotional(self): + """ Testing FixedRateBond notional values. """ + self.assertEqual(self.bond.notional(), 100.0) + self.assertEqual(self.bond.notionals(), (100.0, 0)) + + def testNextCoupon(self): + """ Testing FixedRateBond correct next coupon amount. """ + self.assertEqual(self.bond.nextCouponRate(self.issue_date), 0.05) + + def testPrevCoupon(self): + """ Testing FixedRateBond correct previous coupon amount. """ + self.assertEqual(self.bond.previousCouponRate(), 0.05) + + def testCleanPrice(self): + """ Testing FixedRateBond clean price. """ + self.assertEqual( + round(self.bond.cleanPrice(0.05, self.day_counter, ql.Compounded, ql.Semiannual, self.issue_date), 4), + 99.9964, + ) + self.assertEqual( + round( + self.bond.cleanPrice( + 0.05, self.day_counter, ql.Compounded, ql.Semiannual, self.issue_date + ql.Period(1, ql.Months) + ), + 4, + ), + 99.9921, + ) + + self.assertEqual( + round( + self.bond.cleanPrice( + 0.06, self.day_counter, ql.Compounded, ql.Semiannual, self.issue_date + ql.Period(1, ql.Months) + ), + 4, + ), + 92.5985, + ) + + def testDirtyPrice(self): + """ Testing FixedRateBond dirty price. """ + self.assertEqual( + round(self.bond.dirtyPrice(0.05, self.day_counter, ql.Compounded, ql.Semiannual, self.issue_date), 4), + 99.9964, + ) + self.assertEqual( + round( + self.bond.dirtyPrice( + 0.05, self.day_counter, ql.Compounded, ql.Semiannual, self.issue_date + ql.Period(1, ql.Months) + ), + 4, + ), + 100.4179, + ) + self.assertEqual( + round( + self.bond.dirtyPrice( + 0.06, self.day_counter, ql.Compounded, ql.Semiannual, self.issue_date + ql.Period(1, ql.Months) + ), + 4, + ), + 93.0244, + ) + + def testCleanPriceFromZSpread(self): + """ Testing FixedRateBond clean price derived from Z-spread. """ + self.assertEqual( + round( + ql.cleanPriceFromZSpread( + self.bond, + self.flat_forward, + 0.01, + self.day_counter, + ql.Compounded, + ql.Semiannual, + self.issue_date + ql.Period(1, ql.Months), + ), + 4, + ), + 92.5926, + ) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + +class FixedRateBondKwargsTest(unittest.TestCase): + def setUp(self): + self.settlement_days = 3 + self.face_amount = 100.0 + self.redemption = 100.0 + self.issue_date = ql.Date(2, 1, 2008) + self.maturity_date = ql.Date(2, 1, 2018) + self.calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + self.day_counter = ql.ActualActual(ql.ActualActual.Bond) + self.sched = ql.Schedule( + self.issue_date, + self.maturity_date, + ql.Period(ql.Semiannual), + self.calendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + self.coupons = [0.05] + + def check_construction(self, bond): + self.assertTrue(type(bond) is ql.FixedRateBond) + self.assertEqual(bond.dayCounter(), self.day_counter) + self.assertEqual(bond.settlementDays(), self.settlement_days) + self.assertEqual(bond.issueDate(), self.issue_date) + self.assertEqual(bond.maturityDate(), self.maturity_date) + self.assertEqual(bond.redemption().date(), self.maturity_date) + self.assertEqual(bond.redemption().amount(), self.redemption) + self.assertEqual(bond.notional(self.issue_date), 100.0) + self.assertEqual(bond.notionals(), (100.0, 0)) + + def testFromRates(self): + """ Testing FixedRateBond from_rates method. """ + bond = ql.FixedRateBond.from_rates( + settlementDays=self.settlement_days, + schedule=self.sched, + paymentDayCounter=self.day_counter, + issueDate=self.issue_date, + coupons=self.coupons, + faceAmount=self.face_amount, + ) + self.check_construction(bond) + + def testFromInterestRates(self): + """ Testing FixedRateBond from_interest_rates method. """ + bond = ql.FixedRateBond.from_interest_rates( + settlementDays=self.settlement_days, + faceAmount=self.face_amount, + schedule=self.sched, + coupons=[ql.InterestRate(0.05, self.day_counter, ql.Continuous, ql.Annual)], + issueDate=self.issue_date, + ) + self.check_construction(bond) + + def testFromDateInfo(self): + """ Testing FixedRateBond from_interest_rates method. """ + bond = ql.FixedRateBond.from_date_info( + settlementDays=self.settlement_days, + faceAmount=self.face_amount, + coupons=self.coupons, + issueDate=self.issue_date, + couponCalendar=ql.UnitedStates(ql.UnitedStates.GovernmentBond), + startDate=ql.Date(2, 1, 2010), + maturityDate=self.maturity_date, + tenor=ql.Period(3, ql.Months), + accrualDayCounter=self.day_counter, + ) + self.check_construction(bond) + +class AmortizingFixedRateBondTest(unittest.TestCase): + def test_interest_rates(self): + # see AmortizingBondTest::testBrazilianAmortizingFixedRateBond + # in the C++ test suite + + nominals = [ + 1000 , 983.33300000, 966.66648898, 950.00019204, + 933.33338867, 916.66685434, 900.00001759, 883.33291726, + 866.66619177, 849.99933423, 833.33254728, 816.66589633, + 799.99937871, 783.33299165, 766.66601558, 749.99946306, + 733.33297499, 716.66651646, 699.99971995, 683.33272661, + 666.66624140, 649.99958536, 633.33294599, 616.66615618, + 599.99951997, 583.33273330, 566.66633377, 549.99954356, + 533.33290739, 516.66625403, 499.99963400, 483.33314619, + 466.66636930, 449.99984658, 433.33320226, 416.66634063, + 399.99968700, 383.33290004, 366.66635221, 349.99953317, + 333.33290539, 316.66626012, 299.99948151, 283.33271031, + 266.66594695, 249.99932526, 233.33262024, 216.66590450, + 199.99931312, 183.33277035, 166.66617153, 149.99955437, + 133.33295388, 116.66633464, 99.99973207, 83.33307672, + 66.66646137, 49.99984602, 33.33324734, 16.66662367 + ] + + expected_amortizations = [ + 16.66700000, 16.66651102, 16.66629694, 16.66680337, + 16.66653432, 16.66683675, 16.66710033, 16.66672548, + 16.66685753, 16.66678695, 16.66665095, 16.66651761, + 16.66638706, 16.66697606, 16.66655251, 16.66648807, + 16.66645852, 16.66679651, 16.66699333, 16.66648520, + 16.66665604, 16.66663937, 16.66678981, 16.66663620, + 16.66678667, 16.66639952, 16.66679021, 16.66663617, + 16.66665336, 16.66662002, 16.66648780, 16.66677688, + 16.66652271, 16.66664432, 16.66686163, 16.66665363, + 16.66678696, 16.66654783, 16.66681904, 16.66662777, + 16.66664527, 16.66677860, 16.66677119, 16.66676335, + 16.66662168, 16.66670502, 16.66671573, 16.66659137, + 16.66654276, 16.66659882, 16.66661715, 16.66660049, + 16.66661924, 16.66660257, 16.66665534, 16.66661534, + 16.66661534, 16.66659867, 16.66662367, 16.66662367 + ] + + expected_coupons = [ + 5.97950399, 4.85474255, 5.27619136, 5.18522454, + 5.33753111, 5.24221882, 4.91231709, 4.59116258, + 4.73037674, 4.63940686, 4.54843737, 3.81920094, + 4.78359948, 3.86733691, 4.38439657, 4.09359456, + 4.00262671, 4.28531030, 3.82068947, 3.55165259, + 3.46502778, 3.71720657, 3.62189368, 2.88388676, + 3.58769952, 2.72800044, 3.38838360, 3.00196900, + 2.91100034, 3.08940793, 2.59877059, 2.63809514, + 2.42551945, 2.45615766, 2.59111761, 1.94857222, + 2.28751141, 1.79268582, 2.19248291, 1.81913832, + 1.90625855, 1.89350716, 1.48110584, 1.62031828, + 1.38600825, 1.23425366, 1.39521333, 1.06968563, + 1.03950542, 1.00065409, 0.90968563, 0.81871706, + 0.79726493, 0.63678002, 0.57187676, 0.49829046, + 0.32913418, 0.27290565, 0.19062560, 0.08662552 + ] + + settlementDays = 0 + issueDate = ql.Date(2, ql.March, 2020) + maturityDate = ql.Date(2, ql.March, 2025) + + schedule = ql.Schedule(issueDate, + maturityDate, + ql.Period(ql.Monthly), + ql.Brazil(ql.Brazil.Settlement), + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False) + + coupons = ql.FixedRateLeg( + schedule, + nominals = nominals, + couponRates = [0.0675], + dayCount = ql.Business252(ql.Brazil()), + compounding = ql.Compounded, + compoundingFrequency = ql.Annual, + paymentAdjustment = ql.Following, + ) + + bond = ql.Bond( + settlementDays, + schedule.calendar(), + issueDate, + coupons + ) + + cashflows = bond.cashflows() + + self.assertEqual(len(cashflows), 2 * len(nominals)) + + for k in range(len(nominals)): + self.assertEqual(round(expected_coupons[k], 5), round(cashflows[2*k].amount(), 5)) + self.assertEqual(round(expected_amortizations[k], 5), round(cashflows[2*k+1].amount(), 5)) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(FixedRateBondTest, "test")) + suite.addTest(unittest.makeSuite(FixedRateBondKwargsTest, "test")) + suite.addTest(unittest.makeSuite(AmortizingFixedRateBondTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/capfloor.py b/quantlib/test/capfloor.py new file mode 100644 index 0000000..206ed78 --- /dev/null +++ b/quantlib/test/capfloor.py @@ -0,0 +1,119 @@ +# coding=utf-8-unix +""" + Copyright (C) 2016 Wojciech Ślusarski + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + +class CapFloorTest(unittest.TestCase): + def setUp(self): + self.today_date = ql.Date(9, 9, 2016) + self.settlement_days = 2 + self.notional = 1e4 + + self.calendar = ql.TARGET() + self.flat_forward_rate = 0.01 + self.rate_day_counter = ql.Actual360() + self.flat_forward = ql.FlatForward(self.today_date, + self.flat_forward_rate, + self.rate_day_counter, + ql.Continuous, ql.Annual) + self.term_structure_handle = \ + ql.RelinkableYieldTermStructureHandle(self.flat_forward) + + self.interpolation = ql.Linear() + + self.start_date = ql.Date(13, 9, 2016) + self.maturity_date = ql.Date(13, 9, 2017) + self.period = ql.Period(6, ql.Months) + self.buss_convention = ql.ModifiedFollowing + self.date_gen_rule = ql.DateGeneration.Forward + self.eom_rule = False + + self.schedule = ql.Schedule(self.start_date, + self.maturity_date, + self.period, + self.calendar, + self.buss_convention, + self.buss_convention, + self.date_gen_rule, + self.eom_rule) + + ql.Settings.instance().evaluationDate = self.today_date + + self.ibor_index = ql.Euribor(self.period, self.term_structure_handle) + + self.ibor_index.addFixing(ql.Date(9, 9, 2016), 0.01) + + self.ibor_leg = ql.IborLeg([self.notional], + self.schedule, + self.ibor_index) + self.strike = 0.01 + self.cap = ql.Cap(self.ibor_leg, [self.strike]) + self.cap_npv = 8.54 + + self.black_vol = ql.QuoteHandle(ql.SimpleQuote(0.6)) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def testBlackCapFloorEngine(self): + """ Testing BlackCapFloorEngine """ + black_engine = ql.BlackCapFloorEngine(self.term_structure_handle, + self.black_vol) + + self.cap.setPricingEngine(black_engine) + npv = self.cap.NPV() + self.assertAlmostEqual(npv, self.cap_npv, + places=1, msg="NPV method is broken") + vol_guess = 0.5 + imp_vol = self.cap.impliedVolatility(npv, + self.term_structure_handle, + vol_guess) + self.assertAlmostEqual(self.black_vol.value(), + imp_vol, places=4, + msg="Implied volatility method is broken") + + + def testBachelierCapFloorEngine(self): + """ Testing BachelierCapFloorEngine """ + + bpvol = self.black_vol.value() * self.flat_forward_rate + bachelier_engine = ql.BachelierCapFloorEngine(self.term_structure_handle, + ql.QuoteHandle( + ql.SimpleQuote(bpvol))) + + self.cap.setPricingEngine(bachelier_engine) + + # 50 bps + vol_guess = 50 / 1e4 + + imp_vol = self.cap.impliedVolatility(self.cap_npv, + self.term_structure_handle, + vol_guess, + type=ql.Normal) + + self.assertAlmostEqual(bpvol, imp_vol, places=4, + msg="Normal Implied volatility method is broken") + + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CapFloorTest,'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/cms.py b/quantlib/test/cms.py new file mode 100644 index 0000000..7d4d33f --- /dev/null +++ b/quantlib/test/cms.py @@ -0,0 +1,308 @@ +""" + Copyright (C) 2011 Lluis Pujol Bajador + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + + +class CmsTest(unittest.TestCase): + def setUp(self): + # global data + self.calendar = ql.TARGET() + self.referenceDate = self.calendar.adjust(ql.Date.todaysDate()) + ql.Settings.instance().evaluationDate = self.referenceDate + self.termStructure = ql.RelinkableYieldTermStructureHandle() + self.termStructure.linkTo( + ql.FlatForward(self.referenceDate, ql.QuoteHandle(ql.SimpleQuote(0.05)), ql.Actual365Fixed()) + ) + self.yieldCurveModels = [] + self.numericalPricers = [] + self.analyticPricers = [] + + # ATM Volatility structure + self.atmOptionTenors = [ + ql.Period(1, ql.Months), + ql.Period(6, ql.Months), + ql.Period(1, ql.Years), + ql.Period(5, ql.Years), + ql.Period(10, ql.Years), + ql.Period(30, ql.Years), + ] + + self.atmSwapTenors = [ + ql.Period(1, ql.Years), + ql.Period(5, ql.Years), + ql.Period(10, ql.Years), + ql.Period(30, ql.Years), + ] + + self.m = [ + [0.1300, 0.1560, 0.1390, 0.1220], + [0.1440, 0.1580, 0.1460, 0.1260], + [0.1600, 0.1590, 0.1470, 0.1290], + [0.1640, 0.1470, 0.1370, 0.1220], + [0.1400, 0.1300, 0.1250, 0.1100], + [0.1130, 0.1090, 0.1070, 0.0930], + ] + + self.atmVol = ql.SwaptionVolatilityStructureHandle( + ql.SwaptionVolatilityMatrix( + self.calendar, + ql.Following, + self.atmOptionTenors, + self.atmSwapTenors, + ql.Matrix(self.m), + ql.Actual365Fixed(), + ) + ) + + ###Vol cubes + self.optionTenors = [ql.Period(1, ql.Years), ql.Period(10, ql.Years), ql.Period(30, ql.Years)] + + self.swapTenors = [ql.Period(2, ql.Years), ql.Period(10, ql.Years), ql.Period(30, ql.Years)] + + self.strikeSpreads = [-0.020, -0.005, +0.000, +0.005, +0.020] + + self.nRows = len(self.optionTenors) * len(self.swapTenors) + self.nCols = len(self.strikeSpreads) + self.volSpreadsMatrix = [ + [0.0599, 0.0049, 0.0000, -0.0001, 0.0127], + [0.0729, 0.0086, 0.0000, -0.0024, 0.0098], + [0.0738, 0.0102, 0.0000, -0.0039, 0.0065], + [0.0465, 0.0063, 0.0000, -0.0032, -0.0010], + [0.0558, 0.0084, 0.0000, -0.0050, -0.0057], + [0.0576, 0.0083, 0.0000, -0.0043, -0.0014], + [0.0437, 0.0059, 0.0000, -0.0030, -0.0006], + [0.0533, 0.0078, 0.0000, -0.0045, -0.0046], + [0.0545, 0.0079, 0.0000, -0.0042, -0.0020], + ] + + self.volSpreads = [] + for i in range(self.nRows): + self.volSpreadsRow = [] + for j in range(self.nCols): + self.volSpreadsRow.append(ql.QuoteHandle(ql.SimpleQuote(self.volSpreadsMatrix[i][j]))) + self.volSpreads.append(self.volSpreadsRow) + + self.iborIndex = ql.Euribor6M(self.termStructure) + self.swapIndexBase = ql.EuriborSwapIsdaFixA(ql.Period(10, ql.Years), self.termStructure) + self.shortSwapIndexBase = ql.EuriborSwapIsdaFixA(ql.Period(2, ql.Years), self.termStructure) + + self.vegaWeightedSmileFit = False + self.SabrVolCube2 = ql.SwaptionVolatilityStructureHandle( + ql.SwaptionVolCube2( + self.atmVol, + self.optionTenors, + self.swapTenors, + self.strikeSpreads, + self.volSpreads, + self.swapIndexBase, + self.shortSwapIndexBase, + self.vegaWeightedSmileFit, + ) + ) + self.SabrVolCube2.enableExtrapolation() + + self.guess = [] + self.guessMatrix = [ + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + [0.2, 0.5, 0.4, 0.0], + ] + + for i in range(self.nRows): + self.guessRow = [] + for j in range(4): + self.guessRow.append(ql.QuoteHandle(ql.SimpleQuote(self.guessMatrix[i][j]))) + self.guess.append(self.guessRow) + + self.isParameterFixed = [False, True, False, False] + ### FIXME + self.isAtmCalibrated = False + ## + self.SabrVolCube1 = ql.SwaptionVolatilityStructureHandle( + ql.SwaptionVolCube1( + self.atmVol, + self.optionTenors, + self.swapTenors, + self.strikeSpreads, + self.volSpreads, + self.swapIndexBase, + self.shortSwapIndexBase, + self.vegaWeightedSmileFit, + self.guess, + self.isParameterFixed, + self.isAtmCalibrated, + ) + ) + ##SabrVolCube1.enableExtrapolation() + + self.yieldCurveModels = [ + ql.GFunctionFactory.Standard, + ql.GFunctionFactory.ExactYield, + ql.GFunctionFactory.ParallelShifts, + ql.GFunctionFactory.NonParallelShifts, + ] + + self.zeroMeanRev = ql.QuoteHandle(ql.SimpleQuote(0.0)) + + self.numericalPricers = {} + self.analyticPricers = {} + for m in self.yieldCurveModels: + self.numericalPricers[m] = ql.NumericHaganPricer(self.atmVol, m, self.zeroMeanRev) + self.analyticPricers[m] = ql.AnalyticHaganPricer(self.atmVol, m, self.zeroMeanRev) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def testFairRate(self): + """Testing Hagan-pricer flat-vol equivalence for coupons...""" + swapIndex = ql.SwapIndex( + "EuriborSwapIsdaFixA", + ql.Period(10, ql.Years), + self.iborIndex.fixingDays(), + self.iborIndex.currency(), + self.iborIndex.fixingCalendar(), + ql.Period(1, ql.Years), + ql.Unadjusted, + self.iborIndex.dayCounter(), + self.iborIndex, + ) + startDate = self.termStructure.referenceDate() + ql.Period(20, ql.Years) + paymentDate = startDate + ql.Period(1, ql.Years) + endDate = paymentDate + nominal = 1.0 + infiniteCap = ql.nullDouble() + infiniteFloor = ql.nullDouble() + gearing = 1.0 + spread = 0.0 + coupon = ql.CappedFlooredCmsCoupon( + paymentDate, + nominal, + startDate, + endDate, + swapIndex.fixingDays(), + swapIndex, + gearing, + spread, + infiniteCap, + infiniteFloor, + startDate, + endDate, + self.iborIndex.dayCounter(), + False, + ) + + for m in self.yieldCurveModels: + self.numericalPricers[m].setSwaptionVolatility(self.atmVol) + coupon.setPricer(self.numericalPricers[m]) + rate0 = coupon.rate() + self.analyticPricers[m].setSwaptionVolatility(self.atmVol) + coupon.setPricer(self.analyticPricers[m]) + rate1 = coupon.rate() + difference = abs(rate1 - rate0) + tol = 2.0e-4 + self.assertTrue(difference < tol) + + def testParity(self): + """Testing put-call parity for capped-floored CMS coupons...""" + swaptionVols = [self.atmVol, self.SabrVolCube1, self.SabrVolCube2] + swapIndex = ql.EuriborSwapIsdaFixA(ql.Period(10, ql.Years), self.iborIndex.forwardingTermStructure()) + startDate = self.termStructure.referenceDate() + ql.Period(20, ql.Years) + paymentDate = startDate + ql.Period(1, ql.Years) + endDate = paymentDate + nominal = 1.0 + infiniteCap = ql.nullDouble() + infiniteFloor = ql.nullDouble() + gearing = 1.0 + spread = 0.0 + discount = self.termStructure.discount(paymentDate) + swaplet = ql.CappedFlooredCmsCoupon( + paymentDate, + nominal, + startDate, + endDate, + swapIndex.fixingDays(), + swapIndex, + gearing, + spread, + infiniteCap, + infiniteFloor, + startDate, + endDate, + self.iborIndex.dayCounter(), + ) + strikes = [0.02, 0.07] + for k in strikes: + caplet = ql.CappedFlooredCmsCoupon( + paymentDate, + nominal, + startDate, + endDate, + swapIndex.fixingDays(), + swapIndex, + gearing, + spread, + k, + infiniteFloor, + startDate, + endDate, + self.iborIndex.dayCounter(), + ) + floorlet = ql.CappedFlooredCmsCoupon( + paymentDate, + nominal, + startDate, + endDate, + swapIndex.fixingDays(), + swapIndex, + gearing, + spread, + infiniteCap, + k, + startDate, + endDate, + self.iborIndex.dayCounter(), + ) + for vol in swaptionVols: + for m in self.yieldCurveModels: + self.numericalPricers[m].setSwaptionVolatility(vol) + self.analyticPricers[m].setSwaptionVolatility(vol) + pricers = [self.numericalPricers[m], self.analyticPricers[m]] + for p in pricers: + swaplet.setPricer(p) + caplet.setPricer(p) + floorlet.setPricer(p) + swapletPrice = swaplet.price(self.termStructure) + swaplet.accrualPeriod() * k * discount + capletPrice = caplet.price(self.termStructure) + floorletPrice = floorlet.price(self.termStructure) + difference = abs(capletPrice + floorletPrice - swapletPrice) + tol = 2.0e-5 + self.assertTrue(difference < tol) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CmsTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/coupons.py b/quantlib/test/coupons.py new file mode 100644 index 0000000..cb86e23 --- /dev/null +++ b/quantlib/test/coupons.py @@ -0,0 +1,477 @@ +""" + Copyright (C) 2021 Marcin Rybacki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import mxdevtool as ql + + +EPSILON = 1.e-9 + +CAL = ql.TARGET() + +VALUATION_DATE = CAL.adjust(ql.Date(15, ql.March, 2021)) + +RATE_AVERAGING_MAP = {ql.RateAveraging.Compound: 'Compounded', + ql.RateAveraging.Simple: 'Simple'} + + +def flat_rate(rate): + return ql.FlatForward( + 2, CAL, ql.QuoteHandle(ql.SimpleQuote(rate)), ql.Actual365Fixed()) + + +def create_ibor_leg(ibor_idx, start, end, payment_lag=0): + bdc = ibor_idx.businessDayConvention() + sch = ql.MakeSchedule(effectiveDate=start, + terminationDate=end, + tenor=ibor_idx.tenor(), + calendar=CAL, + convention=bdc, + backwards=True) + return ql.IborLeg([1.0], + sch, + ibor_idx, + paymentDayCounter=ibor_idx.dayCounter(), + paymentConvention=bdc, + paymentCalendar=CAL, + paymentLag=payment_lag) + + +def create_overnight_leg(overnight_idx, start, end, payment_lag=0): + sch = ql.MakeSchedule(effectiveDate=start, + terminationDate=end, + tenor=ql.Period(1, ql.Years), + calendar=CAL, + convention=ql.Following, + backwards=True) + return ql.OvernightLeg([1.0], + sch, + overnight_idx, + paymentDayCounter=ql.Actual365Fixed(), + paymentConvention=ql.Following, + paymentCalendar=CAL, + paymentLag=payment_lag) + + +def create_fixed_rate_leg(start, end, payment_lag=0): + sch = ql.MakeSchedule(effectiveDate=start, + terminationDate=end, + tenor=ql.Period(1, ql.Years), + calendar=CAL, + convention=ql.Following, + backwards=True) + return ql.FixedRateLeg(sch, + ql.Actual365Fixed(), + [1.0], + [0.005], + paymentAdjustment=ql.Following, + paymentCalendar=CAL, + paymentLag=payment_lag) + + +class CashFlowsTest(unittest.TestCase): + def setUp(self): + self.cash_flows = [ql.SimpleCashFlow(1.e6, ql.Date(22, 6, 2022)), + ql.SimpleCashFlow(5.e4, ql.Date(22, 6, 2022))] + + def test_previous_cash_flow_amount(self): + """Testing previous cash flows amount""" + reference_date = ql.Date(28, 6, 2022) + expected_amount = 1.05e6 + include_settlement_date_flows = False + actual_amount = ql.CashFlows.previousCashFlowAmount( + self.cash_flows, include_settlement_date_flows, reference_date) + fail_msg = """ Unable to replicate previous cash flow amount: + calculated: {actual} + expected: {expected} + """.format(actual=actual_amount, + expected=expected_amount) + self.assertEqual(actual_amount, expected_amount, msg=fail_msg) + + def test_next_cash_flow_amount(self): + """Testing next cash flows amount""" + reference_date = ql.Date(21, 6, 2022) + expected_amount = 1.05e6 + include_settlement_date_flows = False + actual_amount = ql.CashFlows.nextCashFlowAmount( + self.cash_flows, include_settlement_date_flows, reference_date) + fail_msg = """ Unable to replicate next cash flow amount: + calculated: {actual} + expected: {expected} + """.format(actual=actual_amount, + expected=expected_amount) + self.assertEqual(actual_amount, expected_amount, msg=fail_msg) + + +class IborCouponTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = VALUATION_DATE + self.nominal_ts_handle = ql.YieldTermStructureHandle(flat_rate(0.007)) + self.ibor_idx = ql.Euribor6M(self.nominal_ts_handle) + + def test_payment_lag(self): + """Testing payment lag of an Ibor leg""" + start = ql.Date(17, ql.March, 2021) + end = ql.Date(17, ql.March, 2031) + pay_lag = 2 + leg_without_lag = create_ibor_leg(self.ibor_idx, start, end) + leg_with_lag = create_ibor_leg(self.ibor_idx, start, end, pay_lag) + for c_f_without_lag, c_f_with_lag in zip(leg_without_lag, leg_with_lag): + actual_payment_date = c_f_with_lag.date() + expected_payment_date = CAL.advance( + c_f_without_lag.date(), + pay_lag, + ql.Days, + self.ibor_idx.businessDayConvention()) + + fail_msg = """ Unable to replicate Ibor coupon payment date: + calculated: {actual} + expected: {expected} + """.format(actual=actual_payment_date, + expected=expected_payment_date) + self.assertEqual(actual_payment_date, + expected_payment_date, + msg=fail_msg) + + +class OvernightCouponTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = VALUATION_DATE + self.nominal_ts_handle = ql.YieldTermStructureHandle(flat_rate(0.007)) + self.overnight_idx = ql.Eonia(self.nominal_ts_handle) + + def test_payment_lag(self): + """Testing payment lag of an overnight leg""" + start = ql.Date(17, ql.March, 2021) + end = ql.Date(17, ql.March, 2031) + pay_lag = 2 + leg_without_lag = create_overnight_leg(self.overnight_idx, start, end) + leg_with_lag = create_overnight_leg( + self.overnight_idx, start, end, pay_lag) + for c_f_without_lag, c_f_with_lag in zip(leg_without_lag, leg_with_lag): + actual_payment_date = c_f_with_lag.date() + expected_payment_date = CAL.advance( + c_f_without_lag.date(), pay_lag, ql.Days, ql.Following) + + fail_msg = """ Unable to replicate overnight coupon payment date: + calculated: {actual} + expected: {expected} + """.format(actual=actual_payment_date, + expected=expected_payment_date) + self.assertEqual(actual_payment_date, + expected_payment_date, + msg=fail_msg) + + +class FixedRateCouponTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = VALUATION_DATE + self.nominal_ts_handle = ql.YieldTermStructureHandle(flat_rate(0.007)) + + def test_payment_lag(self): + """Testing payment lag of a fixed rate leg""" + start = ql.Date(17, ql.March, 2021) + end = ql.Date(17, ql.March, 2031) + pay_lag = 2 + leg_without_lag = create_fixed_rate_leg(start, end) + leg_with_lag = create_fixed_rate_leg(start, end, pay_lag) + for c_f_without_lag, c_f_with_lag in zip(leg_without_lag, leg_with_lag): + actual_payment_date = c_f_with_lag.date() + expected_payment_date = CAL.advance( + c_f_without_lag.date(), pay_lag, ql.Days, ql.Following) + + fail_msg = """ Unable to replicate fixed rate coupon payment date: + calculated: {actual} + expected: {expected} + """.format(actual=actual_payment_date, + expected=expected_payment_date) + self.assertEqual(actual_payment_date, + expected_payment_date, + msg=fail_msg) + + +def create_sub_periods_coupon( + ibor_idx, start, end, averaging_method=ql.RateAveraging.Compound): + payment_calendar = ibor_idx.fixingCalendar() + payment_bdc = ibor_idx.businessDayConvention() + payment_date = payment_calendar.adjust(end, payment_bdc) + fixing_delay = ibor_idx.fixingDays() + cpn = ql.SubPeriodsCoupon( + payment_date, 1.0, start, end, fixing_delay, ibor_idx) + use_compounded_rate = (averaging_method == ql.RateAveraging.Compound) + if use_compounded_rate: + cpn.setPricer(ql.CompoundingRatePricer()) + else: + cpn.setPricer(ql.AveragingRatePricer()) + return cpn + + +def create_sub_periods_leg( + ibor_idx, start, end, cpn_frequency, averaging_method): + sch = ql.MakeSchedule(effectiveDate=start, + terminationDate=end, + tenor=cpn_frequency, + calendar=ibor_idx.fixingCalendar(), + convention=ibor_idx.businessDayConvention(), + backwards=True) + return ql.SubPeriodsLeg( + [1.0], + sch, + ibor_idx, + averagingMethod=averaging_method) + + +def sum_leg_payments(leg): + return sum([cf.amount() for cf in leg]) + + +def compounded_leg_payment(leg): + compound = 1.0 + for cf in leg: + floating_cf = ql.as_floating_rate_coupon(cf) + year_fraction = floating_cf.accrualPeriod() + fixing = floating_cf.indexFixing() + compound *= (1.0 + year_fraction * fixing) + return compound - 1.0 + + +def averaged_leg_payment(leg): + acc = 0.0 + for cf in leg: + floating_cf = ql.as_floating_rate_coupon(cf) + year_fraction = floating_cf.accrualPeriod() + fixing = floating_cf.indexFixing() + acc += year_fraction * fixing + return acc + + +class SubPeriodsCouponTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = VALUATION_DATE + self.nominal_ts_handle = ql.YieldTermStructureHandle(flat_rate(0.007)) + self.ibor_idx = ql.Euribor6M(self.nominal_ts_handle) + self.ibor_idx.addFixing(ql.Date(10, ql.February, 2021), 0.0085) + + def check_single_period_coupon_replication(self, start, end, averaging): + ibor_leg = create_ibor_leg(self.ibor_idx, start, end) + sub_periods_cpn = create_sub_periods_coupon( + self.ibor_idx, start, end, averaging) + + actual_payment = sub_periods_cpn.amount() + expected_payment = sum_leg_payments(ibor_leg) + + fail_msg = """ Unable to replicate single period coupon payment: + calculated: {actual} + expected: {expected} + start: {start} + end: {end} + """.format(actual=actual_payment, + expected=expected_payment, + start=start, + end=end) + self.assertTrue( + abs(actual_payment - expected_payment) < EPSILON, + msg=fail_msg) + + def check_multiple_compounded_sub_periods_coupon_replication( + self, start, end): + ibor_leg = create_ibor_leg(self.ibor_idx, start, end) + sub_periods_cpn = create_sub_periods_coupon( + self.ibor_idx, start, end, ql.RateAveraging.Compound) + + actual_payment = sub_periods_cpn.amount() + expected_payment = compounded_leg_payment(ibor_leg) + + fail_msg = """ Unable to replicate compounded multiple sub-period coupon payment: + calculated: {actual} + expected: {expected} + start: {start} + end: {end} + """.format(actual=actual_payment, + expected=expected_payment, + start=start, + end=end) + self.assertTrue( + abs(actual_payment - expected_payment) < EPSILON, + msg=fail_msg) + + def check_multiple_averaged_sub_periods_coupon_replication( + self, start, end): + ibor_leg = create_ibor_leg(self.ibor_idx, start, end) + sub_periods_cpn = create_sub_periods_coupon( + self.ibor_idx, start, end, ql.RateAveraging.Simple) + + actual_payment = sub_periods_cpn.amount() + expected_payment = averaged_leg_payment(ibor_leg) + + fail_msg = """ Unable to replicate averaged multiple sub-period coupon payment: + calculated: {actual} + expected: {expected} + start: {start} + end: {end} + """.format(actual=actual_payment, + expected=expected_payment, + start=start, + end=end) + self.assertTrue( + abs(actual_payment - expected_payment) < EPSILON, + msg=fail_msg) + + def check_sub_periods_leg_replication(self, averaging_method): + start = ql.Date(18, ql.March, 2021) + end = ql.Date(18, ql.March, 2022) + + sub_periods_cpn = create_sub_periods_coupon( + self.ibor_idx, start, end, averaging_method) + sub_periods_leg = create_sub_periods_leg( + self.ibor_idx, start, end, ql.Period(1, ql.Years), averaging_method) + + actual_payment = sum_leg_payments(sub_periods_leg) + expected_payment = sub_periods_cpn.amount() + + fail_msg = """ Unable to replicate sub-period leg payments: + calculated: {actual} + expected: {expected} + averaging: {averaging} + """.format(actual=actual_payment, + expected=expected_payment, + averaging=RATE_AVERAGING_MAP[averaging_method]) + self.assertTrue( + abs(actual_payment - expected_payment) < EPSILON, + msg=fail_msg) + + def test_regular_single_period_forward_starting_coupon(self): + """Testing regular single period forward starting coupon""" + start = ql.Date(15, ql.April, 2021) + end = ql.Date(15, ql.October, 2021) + + self.check_single_period_coupon_replication( + start, end, ql.RateAveraging.Simple) + self.check_single_period_coupon_replication( + start, end, ql.RateAveraging.Compound) + + def test_regular_single_period_coupon_after_fixing(self): + """Testing regular single period coupon after fixing""" + start = ql.Date(12, ql.February, 2021) + end = ql.Date(12, ql.August, 2021) + + self.check_single_period_coupon_replication( + start, end, ql.RateAveraging.Simple) + self.check_single_period_coupon_replication( + start, end, ql.RateAveraging.Compound) + + def test_irregular_single_period_coupon_after_fixing(self): + """Testing irregular single period coupon after fixing""" + start = ql.Date(12, ql.February, 2021) + end = ql.Date(12, ql.June, 2021) + + self.check_single_period_coupon_replication( + start, end, ql.RateAveraging.Simple) + self.check_single_period_coupon_replication( + start, end, ql.RateAveraging.Compound) + + def test_regular_compounded_forward_starting_coupon_with_multiple_sub_periods(self): + """Testing regular forward starting coupon with multiple compounded sub-periods""" + start = ql.Date(15, ql.April, 2021) + end = ql.Date(15, ql.April, 2022) + + self.check_multiple_compounded_sub_periods_coupon_replication( + start, end) + + def test_regular_averaged_forward_starting_coupon_with_multiple_sub_periods(self): + """Testing regular forward starting coupon with multiple averaged sub-periods""" + start = ql.Date(15, ql.April, 2021) + end = ql.Date(15, ql.April, 2022) + + self.check_multiple_averaged_sub_periods_coupon_replication(start, end) + + def test_sub_periods_leg_cash_flows(self): + """Testing sub-periods leg replication""" + self.check_sub_periods_leg_replication(ql.RateAveraging.Compound) + self.check_sub_periods_leg_replication(ql.RateAveraging.Simple) + + def test_casting(self): + """Testing casting to sub periods coupon""" + start = ql.Date(18, ql.March, 2021) + end = ql.Date(18, ql.March, 2022) + sub_periods_leg = create_sub_periods_leg( + self.ibor_idx, start, end, ql.Period(1, ql.Years), ql.RateAveraging.Compound) + cf = sub_periods_leg[0] + self.assertTrue(not isinstance(cf, ql.SubPeriodsCoupon)) + self.assertTrue(isinstance( + ql.as_sub_periods_coupon(cf), ql.SubPeriodsCoupon)) + + def test_sub_period_coupon_fixing_dates(self): + """Testing sub-period coupon fixing dates""" + start = ql.Date(15, ql.April, 2021) + end = ql.Date(15, ql.April, 2022) + cpn = ql.as_sub_periods_coupon( + create_sub_periods_coupon(self.ibor_idx, start, end)) + actual_dates = cpn.fixingDates() + expected_dates = (ql.Date(13, 4, 2021), ql.Date(13, 10, 2021)) + + fail_msg = """ Unable to replicate sub-period coupon fixing dates: + calculated: {actual} + expected: {expected} + """.format(actual=actual_dates, + expected=expected_dates) + self.assertTupleEqual(actual_dates, expected_dates, msg=fail_msg) + + def test_sub_period_coupon_value_dates(self): + """Testing sub-period coupon value dates""" + start = ql.Date(15, ql.April, 2021) + end = ql.Date(15, ql.April, 2022) + cpn = ql.as_sub_periods_coupon( + create_sub_periods_coupon(self.ibor_idx, start, end)) + actual_dates = cpn.valueDates() + expected_dates = (ql.Date(15, 4, 2021), + ql.Date(15, 10, 2021), + ql.Date(19, 4, 2022)) + + fail_msg = """ Unable to replicate sub-period coupon value dates: + calculated: {actual} + expected: {expected} + """.format(actual=actual_dates, + expected=expected_dates) + self.assertTupleEqual(actual_dates, expected_dates, msg=fail_msg) + + def test_sub_period_coupon_rate_spread(self): + """Testing sub-period coupon rate spread""" + start = ql.Date(15, ql.April, 2021) + end = ql.Date(15, ql.April, 2022) + cpn = ql.as_sub_periods_coupon( + create_sub_periods_coupon(self.ibor_idx, start, end)) + actual_spread = cpn.rateSpread() + expected_spread = 0.0 + + fail_msg = """ Unable to replicate sub-period coupon rate spread: + calculated: {actual} + expected: {expected} + """.format(actual=actual_spread, + expected=expected_spread) + self.assertEqual(actual_spread, expected_spread, msg=fail_msg) + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CashFlowsTest, 'test')) + suite.addTest(unittest.makeSuite(SubPeriodsCouponTest, 'test')) + suite.addTest(unittest.makeSuite(IborCouponTest, 'test')) + suite.addTest(unittest.makeSuite(OvernightCouponTest, 'test')) + suite.addTest(unittest.makeSuite(FixedRateCouponTest, 'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/currencies.py b/quantlib/test/currencies.py new file mode 100644 index 0000000..80d2b5e --- /dev/null +++ b/quantlib/test/currencies.py @@ -0,0 +1,48 @@ +""" + Copyright (C) 2021 Marcin Rybacki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import mxdevtool as ql + + +class CurrencyTest(unittest.TestCase): + + def test_default_currency_constructor(self): + """Testing default currency constructor""" + fail_msg = "Failed to create default currency." + default_ccy = ql.Currency() + self.assertTrue(default_ccy.empty(), fail_msg) + + def test_eur_constructor(self): + """Testing EUR constructor""" + fail_msg = "Failed to create EUR currency." + eur = ql.EURCurrency() + self.assertFalse(eur.empty(), fail_msg) + + def test_bespoke_currency_constructor(self): + """Testing bespoke currency constructor""" + fail_msg = "Failed to create bespoke currency." + custom_ccy = ql.Currency( + "CCY", "CCY", 100, "#", "", 100, ql.Rounding(), "") + self.assertFalse(custom_ccy.empty(), fail_msg) + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CurrencyTest, 'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/date.py b/quantlib/test/date.py new file mode 100644 index 0000000..df3a989 --- /dev/null +++ b/quantlib/test/date.py @@ -0,0 +1,77 @@ +""" + Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + + +class DateTest(unittest.TestCase): + def setUp(self): + pass + + def testArithmetics(self): + "Testing date arithmetics" + today = ql.Date.todaysDate() + date = today - ql.Period(30, ql.Years) + end_date = today + ql.Period(30, ql.Years) + + dold = date.dayOfMonth() + mold = date.month() + yold = date.year() + + while date < end_date: + date += 1 + + d = date.dayOfMonth() + m = date.month() + y = date.year() + + # check if skipping any date + if not ( + (d == dold + 1 and m == mold and y == yold) + or (d == 1 and m == mold + 1 and y == yold) + or (d == 1 and m == 1 and y == yold + 1) + ): + self.fail( + """ +wrong day, month, year increment + date: %(t)s + day, month, year: %(d)d, %(m)d, %(y)d + previous: %(dold)d, %(mold)d, %(yold)d + """ + % locals() + ) + dold = d + mold = m + yold = y + + def testHolidayList(self): + """ Testing Calendar testHolidayList() method. """ + holidayLstFunction = ql.Calendar.holidayList(ql.Poland(), ql.Date(31, 12, 2014), ql.Date(3, 4, 2015), False) + holidayLstManual = (ql.Date(1, 1, 2015), ql.Date(6, 1, 2015)) + # check if dates both from function and from manual imput are the same + self.assertTrue(all([(a == b) for a, b in zip(holidayLstFunction, holidayLstManual)])) + + def tearDown(self): + pass + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(DateTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/daycounters.py b/quantlib/test/daycounters.py new file mode 100644 index 0000000..a850c20 --- /dev/null +++ b/quantlib/test/daycounters.py @@ -0,0 +1,27 @@ +import mxdevtool as ql +import unittest + + +class DayCountersTest(unittest.TestCase): + def runTest(self): + "Testing daycounters" + + calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + + # + # Check that SWIG signature for Business252 calendar allows to + # pass custom calendar into the class constructor. Old + # QuantLib-SWIG versions allow only to create Business252 + # calendar with default constructor parameter (Brazil + # calendar), and generate an exception when trying to pass a + # custom calendar as a parameter. So we just check here that + # no exception occurs. + # + ql.Business252(calendar) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(DayCountersTest()) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/extrapolation.py b/quantlib/test/extrapolation.py new file mode 100644 index 0000000..3d81f94 --- /dev/null +++ b/quantlib/test/extrapolation.py @@ -0,0 +1,46 @@ +""" + Copyright (C) 2019 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import math +import unittest + +import mxdevtool as ql + + +class ExtrapolationTest(unittest.TestCase): + def testKnownExpExtrapolation(self): + """Testing Richardson extrapolation of e^x at x->1 with known order of convergence""" + f = lambda x: math.exp(1+x) + x = ql.RichardsonExtrapolation(f, 0.01, 1.0)(4.0) + + self.assertAlmostEqual(x, math.exp(1), 4, + msg="Unable to extrapolate exp(x) at x->1") + + def testUnknownExpExtrapolation(self): + """Testing Richardson extrapolation of e^x at x->1 with unknown order of convergence""" + f = lambda x: math.exp(1+x) + x = ql.RichardsonExtrapolation(f, 0.01)(4.0, 2.0) + + self.assertAlmostEqual(x, math.exp(1), 4, + msg="Unable to extrapolate exp(x) at x->1") + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ExtrapolationTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/fdm.py b/quantlib/test/fdm.py new file mode 100644 index 0000000..45ae384 --- /dev/null +++ b/quantlib/test/fdm.py @@ -0,0 +1,651 @@ +""" + Copyright (C) 2020 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import math +import unittest + +import mxdevtool as ql + + +class FdmTest(unittest.TestCase): + def setUp(self): + self.todaysDate = ql.Date(15, ql.May, 2019) + ql.Settings.instance().evaluationDate = self.todaysDate + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + + def test1dMesher(self): + """Testing one dimensional mesher""" + + m = ql.Concentrating1dMesher(0, 1, 10) + self.assertEqual(m.size(), 10) + for i in range(0,10): + self.assertAlmostEqual(m.location(i), i/9.0, 14) + + m = ql.Concentrating1dMesher(0, 1, 10, + [ql.Concentrating1dMesherPoint(0.75, 0.01,False), + ql.Concentrating1dMesherPoint(0.5, 0.01, True)]) + + self.assertEqual(m.size(), 10) + self.assertAlmostEqual(m.location(0), 0.0, 14) + self.assertAlmostEqual(m.location(9), 1.0, 14) + + p = list(x for x in m.locations() if ql.close_enough(x, 0.5)) + self.assertEqual(len(p), 1) + p = list(x for x in m.locations() if ql.close_enough(x, 0.75)) + self.assertEqual(len(p), 0) + + m = ql.Predefined1dMesher([0,2,4]) + self.assertEqual(m.size(), 3) + self.assertEqual(m.location(0), 0) + self.assertEqual(m.location(1), 2) + self.assertEqual(m.location(2), 4) + + def testFdmLinearOpIterator(self): + """Testing iterators for linear operators""" + + dim = [2,2,3] + pos = [0,0,0] + idx = 0 + opIter = ql.FdmLinearOpIterator(dim, pos, idx) + + self.assertEqual(opIter.index(), 0) + + opIter.increment() + self.assertEqual(opIter.index(), 1) + self.assertEqual(opIter.coordinates(), (1, 0, 0)) + opIter.increment() + self.assertEqual(opIter.coordinates(), (0, 1, 0)) + + opIter2 = ql.FdmLinearOpIterator(dim, pos, idx) + self.assertEqual(opIter.notEqual(opIter2), True) + self.assertEqual(opIter.notEqual(opIter), False) + + + def testFdmLinearOpLayout(self): + """Testing memory layout for linear operators""" + + dim = [2,2,3] + + m = ql.FdmLinearOpLayout(dim) + + self.assertEqual(m.size(), 2*2*3) + self.assertEqual(m.dim(), (2, 2, 3)) + self.assertEqual(m.spacing(), (1, 2, 4)) + self.assertEqual(m.index((0,1,2)), 10) + self.assertEqual(m.neighbourhood(m.begin(), 0, 1), 1) + self.assertEqual(m.neighbourhood(m.begin(), 2, 2), 8) + self.assertEqual(m.neighbourhood(m.begin(), 0, 1, 2, 2), 9) + + n = m.iter_neighbourhood(m.begin(), 0, 1) + opIter = m.begin() + opIter.increment() + + self.assertEqual(opIter.notEqual(n), False) + + def testFdmMesherComposite(self): + """Testing mesher composites""" + + m1 = ql.Concentrating1dMesher(0, 1, 2) + m2 = ql.Uniform1dMesher(0, 2, 3) + + m = ql.FdmMesherComposite(m1, m2) + self.assertEqual(len(m.getFdm1dMeshers()), 2) + + locations = m.locations(0) + self.assertEqual(len(locations), 6) + + self.assertEqual(list(map(lambda x: int(x+0.5), locations)), [0, 1, 0, 1, 0, 1]) + + locations = m.locations(1) + self.assertEqual(list(map(lambda x: int(x+0.5), locations)), [0, 0, 1, 1, 2, 2]) + + def testFdmLinearOpComposite(self): + """Testing linear operator composites""" + + class Foo: + t1 = 0.0 + t2 = 0.0 + + @classmethod + def size(self): + return 42 + + def setTime(self, t1, t2): + self.t1 = t1 + self.t2 = t2 + + @classmethod + def apply(self, r): + return 2*r + + @classmethod + def apply_mixed(self, r): + return 3*r + + @classmethod + def apply_direction(self, direction , r): + return direction*r + + @classmethod + def solve_splitting(self, direction , r, s): + return direction*s*r + + @classmethod + def preconditioner(self, r, s): + return s*r + + + foo = Foo() + + c = ql.FdmLinearOpCompositeProxy(foo) + + self.assertEqual(c.size(), foo.size()) + + c.setTime(1.0, 2.0) + self.assertAlmostEqual(foo.t1, 1.0, 14) + self.assertAlmostEqual(foo.t2, 2.0, 14) + + r = ql.Array([1,2,3,4]) + self.assertEqual(list(c.apply(r)), list(2*r)) + self.assertEqual(list(c.apply_mixed(r)), list(3*r)) + self.assertEqual(list(c.apply_direction(7, r)), list(7*r)) + + s = list(c.solve_splitting(7, r, 0.5)) + self.assertEqual(len(s), len(r)) + for i, x in enumerate(s): + self.assertAlmostEqual(x, 3.5*r[i], 14) + + self.assertEqual(list(c.preconditioner(r, 4)), list(4*r)) + + class Bar: + @classmethod + def apply(self, r): + return 1 + + def apply_mixed(self, r): + pass + + with self.assertRaises(RuntimeError): + ql.FdmLinearOpCompositeProxy(Bar()).apply(r) + + with self.assertRaises(RuntimeError): + ql.FdmLinearOpCompositeProxy(Bar()).apply_mixed(r) + + + def testFdmBlackScholesOp(self): + """Testing linear Black-Scholes operator""" + + todaysDate = ql.Date(1, ql.January, 2020) + ql.Settings.instance().evaluationDate = todaysDate + dc = ql.Actual365Fixed() + + settlementDate = todaysDate + 2 + riskFreeRate = ql.FlatForward(settlementDate, 0.05, dc) + + exercise = ql.EuropeanExercise(ql.Date(27, ql.December, 2020)) + maturity = dc.yearFraction(todaysDate, exercise.lastDate()) + + strike = 110.0 + payoff = ql.PlainVanillaPayoff(ql.Option.Call, strike) + + underlying = ql.SimpleQuote(100.0) + volatility = ql.BlackConstantVol(settlementDate, ql.TARGET(), 0.10, dc) + dividendYield = ql.FlatForward(settlementDate, 0.05, dc) + + process = ql.BlackScholesMertonProcess( + ql.QuoteHandle(underlying), + ql.YieldTermStructureHandle(dividendYield), + ql.YieldTermStructureHandle(riskFreeRate), + ql.BlackVolTermStructureHandle(volatility) + ) + + mesher = ql.FdmMesherComposite( + ql.FdmBlackScholesMesher(10, process, maturity, strike)) + + op = ql.FdmBlackScholesOp(mesher, process, strike) + self.assertEqual(op.size(), 1) + + op.setTime(0, 0.1) + + c = list(map(lambda x: payoff(math.exp(x)), mesher.locations(0))) + p = op.apply(c) + + e = [ 0.0, 0.0, 0.0, 0.0, 0.0, + 3.18353, 0.755402, -1.30583, -2.19881, -4.0271 ] + + for i, x in enumerate(e): + self.assertAlmostEqual(x, p[i], 5) + + + def testFdmFirstOrderOperator(self): + """Testing first order operator""" + + mesher = ql.Uniform1dMesher(0.0, math.pi, 1000) + + op = ql.FirstDerivativeOp(0, ql.FdmMesherComposite(mesher)) + + l = mesher.locations() + + x = list(map(math.sin, l)) + + y = op.apply(x) + + for u, v in zip(l, y): + self.assertAlmostEqual(v, math.cos(u), 4) + + + def testFdmSecondOrderOperator(self): + """Testing second order operator""" + + mesher = ql.Uniform1dMesher(0.0, math.pi, 1000) + + op = ql.SecondDerivativeOp(0, ql.FdmMesherComposite(mesher)) + + x = list(map(math.sin, mesher.locations())) + + y = op.apply(x) + + for u, v in zip(x, y): + self.assertAlmostEqual(v, -u, 4) + + def testFdmBoundaryCondition(self): + """Testing Dirichlet Boundary conditions""" + + m = ql.FdmMesherComposite( + ql.Uniform1dMesher(0.0, 1.0, 5)) + + b = ql.FdmDirichletBoundary( + m, math.pi, 0, ql.FdmBoundaryCondition.Upper) + + x = ql.Array(len(m.locations(0)), 0.0) + + b.applyAfterApplying(x) + + self.assertEqual(list(x), [0,0,0,0, math.pi]) + + s = ql.FdmBoundaryConditionSet() + s.push_back(b) + + self.assertEqual(len(s), 1) + + def testFdmStepConditionCallBack(self): + """Testing step condition call back function""" + + class Foo: + @classmethod + def applyTo(self, a, t): + for i in range(5): + a[i] = t+1.0 + + m = ql.FdmStepConditionProxy(Foo()) + + x = ql.Array(5) + + m.applyTo(x, 2.0) + + self.assertEqual(len(x), 5) + self.assertEqual(list(x), [3.0, 3.0, 3.0, 3.0, 3.0]) + + def testFdmInnerValueCalculatorCallBack(self): + """Testing inner value call back function""" + + class Foo: + @classmethod + def innerValue(self, opIter, t): + return opIter.index() + t + + @classmethod + def avgInnerValue(self, opIter, t): + return opIter.index() + 2*t + + m = ql.FdmInnerValueCalculatorProxy(Foo()) + + dim = [2,2,3] + pos = [0,0,0] + + opIter = ql.FdmLinearOpIterator(dim, pos, 0) + + while (opIter.index() < 2*2*3): + idx = opIter.index() + + self.assertEqual(m.innerValue(opIter, 2.0), idx + 2.0) + self.assertEqual(m.avgInnerValue(opIter, 2.0), idx + 4.0) + + opIter.increment() + + + def testFdmLogInnerValueCalculator(self): + """Testing log inner value calculator""" + + m = ql.FdmMesherComposite( + ql.Uniform1dMesher(math.log(50), math.log(150), 11)) + + p = ql.PlainVanillaPayoff(ql.Option.Call, 100) + + v = ql.FdmLogInnerValue(p, m, 0) + + opIter = m.layout().begin() + while opIter.notEqual(m.layout().end()): + x = math.exp(m.location(opIter, 0)); + self.assertAlmostEqual(p(x), v.innerValue(opIter, 1.0), 14) + opIter.increment() + + + def testAmericanOptionPricing(self): + """Testing Black-Scholes and Heston American Option pricing""" + + xSteps = 100 + tSteps = 25 + dampingSteps = 0 + + todaysDate = ql.Date(15, ql.January, 2020) + ql.Settings.instance().evaluationDate = todaysDate + + dc = ql.Actual365Fixed() + + riskFreeRate = ql.YieldTermStructureHandle( + ql.FlatForward(todaysDate, 0.06, dc)) + dividendYield = ql.YieldTermStructureHandle( + ql.FlatForward(todaysDate, 0.02, dc)) + + strike = 110.0 + payoff = ql.PlainVanillaPayoff(ql.Option.Put, strike) + + maturityDate = todaysDate + ql.Period(1, ql.Years) + maturity = dc.yearFraction(todaysDate, maturityDate) + + exercise = ql.AmericanExercise(todaysDate, maturityDate) + + spot = ql.QuoteHandle(ql.SimpleQuote(100.0)) + volatility = ql.BlackConstantVol(todaysDate, ql.TARGET(), 0.20, dc) + + process = ql.BlackScholesMertonProcess( + spot, dividendYield, riskFreeRate, + ql.BlackVolTermStructureHandle(volatility) + ) + + option = ql.VanillaOption(payoff, exercise) + option.setPricingEngine(ql.FdBlackScholesVanillaEngine.make( + process, xGrid = xSteps, tGrid = tSteps, + dampingSteps = dampingSteps) + ) + + expected = option.NPV() + + equityMesher = ql.FdmBlackScholesMesher( + xSteps, process, maturity, + strike, cPoint = (strike, 0.1) + ) + + mesher = ql.FdmMesherComposite(equityMesher) + + op = ql.FdmBlackScholesOp(mesher, process, strike) + + innerValueCalculator = ql.FdmLogInnerValue(payoff, mesher, 0) + + x = [] + rhs = [] + layout = mesher.layout() + opIter = layout.begin() + while (opIter.notEqual(layout.end())): + x.append(mesher.location(opIter, 0)) + rhs.append(innerValueCalculator.avgInnerValue(opIter, maturity)) + opIter.increment() + + rhs = ql.Array(rhs) + + bcSet = ql.FdmBoundaryConditionSet() + stepCondition = ql.FdmStepConditionComposite.vanillaComposite( + ql.DividendSchedule(), exercise, mesher, + innerValueCalculator, todaysDate, dc + ) + + # only to test an Operator defined in python + class OperatorProxy: + def __init__(self, op): + self.op = op + + def size(self): + return self.op.size() + + def setTime(self, t1, t2): + return self.op.setTime(t1, t2) + + def apply(self, r): + return self.op.apply(r) + + def apply_direction(self, i, r): + return self.op.apply_direction(i, r) + + def solve_splitting(self, i, r, s): + return self.op.solve_splitting(i, r, s) + + + proxyOp = ql.FdmLinearOpCompositeProxy(OperatorProxy(op)) + + solver = ql.FdmBackwardSolver( + proxyOp, bcSet, stepCondition, ql.FdmSchemeDesc.Douglas() + ) + + solver.rollback(rhs, maturity, 0.0, tSteps, dampingSteps) + + spline = ql.CubicNaturalSpline(x, rhs); + + logS = math.log(spot.value()) + + calculated = spline(logS) + + self.assertAlmostEqual(calculated, expected, 1) + + solverDesc = ql.FdmSolverDesc( + mesher, bcSet, stepCondition, innerValueCalculator, + maturity, tSteps, dampingSteps) + + calculated = ql.Fdm1DimSolver( + solverDesc, ql.FdmSchemeDesc.Douglas(), op).interpolateAt(logS) + + self.assertAlmostEqual(calculated, expected, 2) + + v0 = 0.4*0.4 + kappa = 1.0 + theta = v0 + sigma = 1e-4 + rho = 0.0 + + hestonProcess = ql.HestonProcess( + riskFreeRate, dividendYield, + spot, v0, kappa, theta, sigma, rho) + + leverageFct = ql.LocalVolSurface( + ql.BlackVolTermStructureHandle( + ql.BlackConstantVol(todaysDate, ql.TARGET(), 0.50, dc)), + riskFreeRate, + dividendYield, + spot.value() + ) + + vSteps = 3 + + vMesher = ql.FdmHestonLocalVolatilityVarianceMesher( + vSteps, hestonProcess, leverageFct, maturity) + + avgVolaEstimate = vMesher.volaEstimate() + + self.assertAlmostEqual(avgVolaEstimate, 0.2, 5) + + mesher = ql.FdmMesherComposite(equityMesher, vMesher) + + innerValueCalculator = ql.FdmLogInnerValue(payoff, mesher, 0) + + stepCondition = ql.FdmStepConditionComposite.vanillaComposite( + ql.DividendSchedule(), exercise, mesher, + innerValueCalculator, todaysDate, dc + ) + + solverDesc = ql.FdmSolverDesc( + mesher, bcSet, stepCondition, innerValueCalculator, + maturity, tSteps, dampingSteps) + + calculated = ql.FdmHestonSolver( + hestonProcess, solverDesc, leverageFct = leverageFct).valueAt( + spot.value(), 0.16) + + self.assertAlmostEqual(calculated, expected, 1) + + + def testBSMRNDCalculator(self): + """Testing Black-Scholes risk neutral density calculator""" + + dc = ql.Actual365Fixed() + todaysDate = ql.Date(15, ql.January, 2020) + + r = 0.0 + q = 0.0 + vol = 0.2 + s0 = 100 + + process = ql.BlackScholesMertonProcess( + ql.QuoteHandle(ql.SimpleQuote(s0)), + ql.YieldTermStructureHandle( + ql.FlatForward(todaysDate, q, dc)), + ql.YieldTermStructureHandle( + ql.FlatForward(todaysDate, r, dc)), + ql.BlackVolTermStructureHandle( + ql.BlackConstantVol(todaysDate, ql.TARGET(), vol, dc)) + ) + + rnd = ql.BSMRNDCalculator(process) + + t = 1.2 + x = math.log(80.0) + + mu = math.log(s0) + (r-q-0.5*vol*vol)*t + + calculated = rnd.pdf(x, t) + + stdev = vol * math.sqrt(t) + + expected = (1.0/(math.sqrt(2*math.pi)*stdev) * + math.exp( -0.5*math.pow((x-mu)/stdev, 2.0) )) + + self.assertAlmostEqual(calculated, expected, 8) + + + def testOrnsteinUhlenbeckVsBachelier(self): + """Testing Fdm Ornstein-Uhlenbeck pricing""" + + todaysDate = ql.Date(15, ql.January, 2020) + ql.Settings.instance().evaluationDate = todaysDate + + dc = ql.Actual365Fixed() + + rTS = ql.FlatForward(todaysDate, 0.06, dc) + + strike = 110.0 + payoff = ql.PlainVanillaPayoff(ql.Option.Put, strike) + + maturityDate = todaysDate + ql.Period(2, ql.Years) + + exercise = ql.EuropeanExercise(maturityDate) + + option = ql.VanillaOption(payoff, exercise) + + x0 = 100 + sigma = 20.0 + speed = 5 + + pdeEngine = ql.FdOrnsteinUhlenbeckVanillaEngine( + ql.OrnsteinUhlenbeckProcess(speed, sigma, x0, x0), rTS, 50 + ) + + option.setPricingEngine(pdeEngine) + calculated = option.NPV() + + stdev = math.sqrt(sigma*sigma/(2*speed)) + + expected = ql.bachelierBlackFormula( + ql.Option.Put, + strike, x0, stdev, + rTS.discount(maturityDate) + ) + + self.assertAlmostEqual(calculated, expected, 2) + + + def testSparseLinearMatrixSolver(self): + """Testing sparse linear matrix solver""" + + A = ql.Matrix([ + [1.0, 0.0, 1.0], + [0.0, 1.0, 0.5], + [1.0, 0.5, 1.0] + ]) + + b = ql.Array([ 1.0, 0.2, 0.5 ]) + + expected = ql.inverse(A)*b + + def foo(x): + return A*x + + calculated = ql.BiCGstab( + ql.MatrixMultiplicationProxy(foo), 100, 1e-6).solve(b) + + for i in range(3): + self.assertAlmostEqual(expected[i], calculated[i], 4) + + calculated = ql.GMRES( + ql.MatrixMultiplicationProxy(foo), 100, 1e-6).solve(b) + + for i in range(3): + self.assertAlmostEqual(expected[i], calculated[i], 4) + + def preconditioner(x): + return ql.inverse(A)*x + + calculated = ql.BiCGstab( + ql.MatrixMultiplicationProxy(foo), 100, 1e-6, + ql.MatrixMultiplicationProxy(preconditioner)).solve(b) + + for i in range(3): + self.assertAlmostEqual(expected[i], calculated[i], 4) + + def testGlued1dMesher(self): + """Testing sparse linear matrix solver""" + + m1 = ql.Uniform1dMesher(0, 2, 3) + m2 = ql.Uniform1dMesher(2, 4, 3) + + m3 = ql.Glued1dMesher(m1, m2) + + self.assertEqual(m3.locations(), (0,1,2,3,4)) + + def testFdmZeroInnerValue(self): + """Testing FdmZeroInnerValue""" + opIter = ql.FdmLinearOpIterator([1], [0], 0) + + self.assertEqual(ql.FdmZeroInnerValue().innerValue(opIter, 1.0), 0.0) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(FdmTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/iborindex.py b/quantlib/test/iborindex.py new file mode 100644 index 0000000..55dd56f --- /dev/null +++ b/quantlib/test/iborindex.py @@ -0,0 +1,76 @@ +# coding=utf-8-unix +""" + Copyright (C) 2018 Wojciech Ślusarski + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + + +class IborIndexTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.euribor3m = ql.Euribor3M() + + def setUp(self): + self.euribor3m.clearFixings() + # values are not real due to copyrights of the fixing + self.euribor3m.addFixing(ql.Date(17, 7, 2018), -0.3) + self.euribor3m.addFixings([ql.Date(12, 7, 2018), ql.Date(13, 7, 2018)], [-0.3, -0.3]) + + def testAddFixingFail(self): + """Testing for RuntimeError while trying to overwrite fixing value""" + + with self.assertRaises(RuntimeError): + # attempt to overwrite value that is already set at different level + self.euribor3m.addFixing(ql.Date(17, 7, 2018), -0.4) + + with self.assertRaises(RuntimeError): + # attempt to overwrite value that is already set at different level + self.euribor3m.addFixings([ql.Date(12, 7, 2018), ql.Date(13, 7, 2018)], [-0.4, -0.4]) + + def testAddFixing(self): + """Testing for overwriting fixing value""" + + force_overwrite = True + try: + # attempt to overwrite value that is already set at different level + self.euribor3m.addFixing(ql.Date(17, 7, 2018), -0.4, force_overwrite) + self.euribor3m.addFixings([ql.Date(12, 7, 2018), ql.Date(13, 7, 2018)], [-0.4, -0.4], force_overwrite) + # try clearFixings and repeat with original levels + self.euribor3m.clearFixings() + self.euribor3m.addFixing(ql.Date(17, 7, 2018), -0.3) + self.euribor3m.addFixings([ql.Date(12, 7, 2018), ql.Date(13, 7, 2018)], [-0.3, -0.3]) + + except RuntimeError as err: + raise AssertionError("Failed to overwrite index fixixng " + "{}".format(err)) + + def testTimeSeries(self): + """Testing for getting time series of the fixing""" + + dates = (ql.Date(12, 7, 2018), ql.Date(13, 7, 2018), ql.Date(17, 7, 2018)) + values = (-0.3, -0.3, -0.3) + for expected, actual in zip(dates, self.euribor3m.timeSeries().dates()): + self.assertTrue(expected == actual) + for expected, actual in zip(values, self.euribor3m.timeSeries().values()): + self.assertTrue(expected == actual) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(IborIndexTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/inflation.py b/quantlib/test/inflation.py new file mode 100644 index 0000000..80721a4 --- /dev/null +++ b/quantlib/test/inflation.py @@ -0,0 +1,417 @@ +""" + Copyright (C) 2020 Marcin Rybacki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import mxdevtool as ql + + +EPSILON = 1.e-9 + +# Hypothetical market data +EUR_ZERO_RATES = [(ql.Period(1, ql.Days), 0.0048), + (ql.Period(1, ql.Years), 0.0048), + (ql.Period(2, ql.Years), 0.00475), + (ql.Period(3, ql.Years), 0.005), + (ql.Period(5, ql.Years), 0.0055), + (ql.Period(10, ql.Years), 0.007)] + +EUR_BEI_SWAP_RATES = [(ql.Period(1, ql.Years), 0.0301), + (ql.Period(2, ql.Years), 0.0299), + (ql.Period(3, ql.Years), 0.0305), + (ql.Period(5, ql.Years), 0.0315), + (ql.Period(10, ql.Years), 0.0355)] + +# Source: +# https://ec.europa.eu/eurostat/web/products-datasets/-/teicp240. +EU_FIXING_DATA = [(ql.Date(1, ql.April, 2018), 103.11), + (ql.Date(1, ql.May, 2018), 103.64), + (ql.Date(1, ql.June, 2018), 103.76), + (ql.Date(1, ql.July, 2018), 103.41), + (ql.Date(1, ql.August, 2018), 103.58)] + +CAL = ql.TARGET() + +DAY_COUNTER = ql.ActualActual(ql.ActualActual.ISDA) + +BDC = ql.ModifiedFollowing + +VALUATION_DATE = CAL.adjust(ql.Date(10, ql.September, 2018)) + +OBSERVATION_LAG = ql.Period(3, ql.Months) + + +def create_inflation_swap_helper( + reference_date, + inflation_data, + inflation_index, + interpolation, + discount_curve_handle, + observation_lag=OBSERVATION_LAG, + calendar=CAL, + business_day_convention=BDC, + day_counter=DAY_COUNTER): + maturity = CAL.advance(reference_date, inflation_data[0]) + quote = ql.QuoteHandle(ql.SimpleQuote(inflation_data[1])) + return ql.ZeroCouponInflationSwapHelper( + quote, + observation_lag, + maturity, + calendar, + business_day_convention, + day_counter, + inflation_index, + interpolation, + discount_curve_handle) + + +def build_nominal_term_structure( + reference_date, + nominal_data): + nominal_dc = ql.Actual365Fixed() + dates = [CAL.advance(reference_date, x[0]) for x in nominal_data] + rates = [x[1] for x in nominal_data] + return ql.ZeroCurve(dates, rates, nominal_dc) + + +def build_hicp_index( + fixing_data, + inflation_crv_handle, + interpolated=False): + index = ql.EUHICP(interpolated, inflation_crv_handle) + for x in fixing_data: + # force override in case of multiple use + index.addFixing(x[0], x[1], True) + return index + + +SEASONAL = {ql.January: 1.0, ql.February: 1.01, ql.March: 1.011, + ql.April: 1.009, ql.May: 1.008, ql.June: 1.012, + ql.July: 1.0078, ql.August: 1.006, + ql.September: 1.0085, ql.October: 1.0096, + ql.November: 1.0067, ql.December: 1.0055} + + +def construct_seasonality(reference_date): + frequency = ql.Monthly + seasonality_base_date = ql.Date(1, ql.January, reference_date.year()) + factors = list(SEASONAL.values()) + return ql.MultiplicativePriceSeasonality( + seasonality_base_date, frequency, factors) + + +def build_inflation_term_structure( + reference_date, + zero_coupon_swaps_data, + inflation_index, + interpolation, + nominal_term_structure_handle, + observation_lag=OBSERVATION_LAG, + include_seasonality=False): + helpers = [create_inflation_swap_helper(reference_date, + x, + inflation_index, + interpolation, + nominal_term_structure_handle) + for x in zero_coupon_swaps_data] + base_zero_rate = zero_coupon_swaps_data[0][1] + cpi_term_structure = ql.PiecewiseZeroInflation( + reference_date, + CAL, + DAY_COUNTER, + observation_lag, + inflation_index.frequency(), + base_zero_rate, + helpers) + if include_seasonality: + seasonality = construct_seasonality(reference_date) + cpi_term_structure.setSeasonality(seasonality) + return cpi_term_structure + + +def create_inflation_swap( + inflation_idx, + start_date, + end_date, + rate, + interpolation, + observation_lag=OBSERVATION_LAG, + nominal=1.e6, + payer=ql.Swap.Payer): + return ql.ZeroCouponInflationSwap( + payer, + nominal, + start_date, + end_date, + CAL, + BDC, + DAY_COUNTER, + rate, + inflation_idx, + observation_lag, + interpolation) + + +def interpolate_historic_index( + inflation_idx, fixing_date, observation_lag=OBSERVATION_LAG): + first_dt = ql.Date(1, fixing_date.month(), fixing_date.year()) + second_dt = ql.Date.endOfMonth(fixing_date) + 1 + slope_numerator = fixing_date - first_dt + slope_denominator = ( + (second_dt + observation_lag) - (first_dt + observation_lag)) + slope = float(slope_numerator) / float(slope_denominator) + return inflation_idx.fixing(first_dt) + slope * ( + inflation_idx.fixing(second_dt) - inflation_idx.fixing(first_dt)) + + +class InflationTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = VALUATION_DATE + self.inflation_ts_handle = ql.RelinkableZeroInflationTermStructureHandle() + self.nominal_ts_handle = ql.RelinkableYieldTermStructureHandle() + self.nominal_ts_handle.linkTo( + build_nominal_term_structure(VALUATION_DATE, EUR_ZERO_RATES)) + self.discount_engine = ql.DiscountingSwapEngine(self.nominal_ts_handle) + + def test_par_swap_pricing_fom_indexation_without_seasonality(self): + """Testing pricing of par inflation swap for First-Of-Month indexation""" + + inflation_idx = build_hicp_index( + EU_FIXING_DATA, self.inflation_ts_handle) + inflation_ts = build_inflation_term_structure( + VALUATION_DATE, + EUR_BEI_SWAP_RATES, + inflation_idx, + ql.CPI.Flat, + self.nominal_ts_handle) + self.inflation_ts_handle.linkTo(inflation_ts) + + zciis = create_inflation_swap( + inflation_idx, + VALUATION_DATE, + CAL.advance(VALUATION_DATE, ql.Period(10, ql.Years)), + 0.0355, + ql.CPI.Flat) + zciis.setPricingEngine(self.discount_engine) + npv = zciis.NPV() + # Check whether swap prices to par + fail_msg = """ Failed to price zero coupon inflation swap to par: + index: {inflation_idx} + end date: {end_date} + observation lag: {observation_lag} + npv: {npv} + expected npv: {expected_npv} + tolerance: {tolerance} + """.format(inflation_idx=inflation_idx.familyName(), + end_date=zciis.maturityDate(), + observation_lag=OBSERVATION_LAG, + npv=npv, + expected_npv=0.0, + tolerance=EPSILON) + self.assertTrue( + abs(npv) < EPSILON, + msg=fail_msg) + + def test_inflation_leg_payment_fom_indexation_without_seasonality(self): + """Testing inflation leg payment for First-Of-Month indexation""" + + inflation_idx = build_hicp_index( + EU_FIXING_DATA, self.inflation_ts_handle) + inflation_ts = build_inflation_term_structure( + VALUATION_DATE, + EUR_BEI_SWAP_RATES, + inflation_idx, + ql.CPI.Flat, + self.nominal_ts_handle) + self.inflation_ts_handle.linkTo(inflation_ts) + + zciis = create_inflation_swap( + inflation_idx, + VALUATION_DATE, + CAL.advance(VALUATION_DATE, ql.Period(10, ql.Years)), + 0.0355, + ql.CPI.Flat) + zciis.setPricingEngine(self.discount_engine) + + inflation_cf = ql.as_indexed_cashflow( + zciis.inflationLeg()[0]) + # Obtaining base index for the inflation swap + swap_base_dt = inflation_cf.baseDate() + swap_base_fixing = inflation_idx.fixing(swap_base_dt) + # Replicate fixing projection + fixing_dt = inflation_cf.fixingDate() + ts_base_dt = inflation_ts.baseDate() + ts_base_fixing = inflation_idx.fixing(ts_base_dt) + # Apply FOM indexation rule + effective_fixing_dt = ql.Date( + 1, fixing_dt.month(), fixing_dt.year()) + fraction = inflation_ts.dayCounter().yearFraction( + ts_base_dt, effective_fixing_dt) + t = inflation_ts.timeFromReference(effective_fixing_dt) + zero_rate = inflation_ts.zeroRate(t) + expected_fixing = ts_base_fixing * ( + 1.0 + zero_rate)**fraction + + expected_inflation_leg_payment = ( + expected_fixing / swap_base_fixing - 1.0) * inflation_cf.notional() + actual_inflation_leg_payment = inflation_cf.amount() + + fail_msg = """ Failed to replicate inflation leg payment + for First-Of-Month indexation: + index: {inflation_idx} + end date: {end_date} + observation lag: {observation_lag} + inflation leg payment: {actual_payment} + replicated payment: {expected_payment} + tolerance: {tolerance} + """.format(inflation_idx=inflation_idx.familyName(), + end_date=zciis.maturityDate(), + observation_lag=OBSERVATION_LAG, + actual_payment=actual_inflation_leg_payment, + expected_payment=expected_inflation_leg_payment, + tolerance=EPSILON) + self.assertAlmostEquals( + first=actual_inflation_leg_payment, + second=expected_inflation_leg_payment, + delta=EPSILON, + msg=fail_msg) + + def test_swap_base_fixing_linear_indexation_without_seasonality(self): + """Testing swap base fixing for linear indexation""" + + inflation_idx = build_hicp_index( + EU_FIXING_DATA, self.inflation_ts_handle, interpolated=True) + inflation_ts = build_inflation_term_structure( + VALUATION_DATE, + EUR_BEI_SWAP_RATES, + inflation_idx, + ql.CPI.Linear, + self.nominal_ts_handle) + self.inflation_ts_handle.linkTo(inflation_ts) + + zciis = create_inflation_swap( + inflation_idx, + ql.Date(24, ql.August, 2018), + ql.Date(24, ql.August, 2023), + 0.032, + ql.CPI.Linear) + zciis.setPricingEngine(self.discount_engine) + + inflation_cf = ql.as_indexed_cashflow( + zciis.inflationLeg()[0]) + + swap_base_dt = inflation_cf.baseDate() + swap_base_fixing = inflation_idx.fixing(swap_base_dt) + expected_swap_base_index = interpolate_historic_index( + inflation_idx, swap_base_dt) + + fail_msg = """ Failed to replicate inflation swap base index fixing + for linear indexation: + index: {inflation_idx} + end date: {end_date} + observation lag: {observation_lag} + base index fixing: {base_index} + replicated base index fixing: {expected_base_index} + tolerance: {tolerance} + """.format(inflation_idx=inflation_idx.familyName(), + end_date=zciis.maturityDate(), + observation_lag=OBSERVATION_LAG, + base_index=swap_base_fixing, + expected_base_index=expected_swap_base_index, + tolerance=EPSILON) + self.assertAlmostEquals( + first=swap_base_fixing, + second=expected_swap_base_index, + delta=EPSILON, + msg=fail_msg) + + def test_inflation_curve_base_fixing(self): + """Testing inflation curve base fixing for linear indexation""" + + inflation_idx = build_hicp_index( + EU_FIXING_DATA, self.inflation_ts_handle, interpolated=True) + inflation_ts = build_inflation_term_structure( + VALUATION_DATE, + EUR_BEI_SWAP_RATES, + inflation_idx, + ql.CPI.Linear, + self.nominal_ts_handle) + self.inflation_ts_handle.linkTo(inflation_ts) + + curve_base_dt = inflation_ts.baseDate() + curve_base_fixing = inflation_idx.fixing(curve_base_dt) + expected_curve_base_fixing = interpolate_historic_index( + inflation_idx, curve_base_dt) + + fail_msg = """ Failed to replicate inflation curve base index fixing + for linear indexation: + index: {inflation_idx} + inflation curve base date : {base_date} + inflation curve base fixing: {base_fixing} + expected base fixing: {expected_base_fixing} + tolerance: {tolerance} + """.format(inflation_idx=inflation_idx.familyName(), + base_date=curve_base_dt, + base_fixing=curve_base_fixing, + expected_base_fixing=expected_curve_base_fixing, + tolerance=EPSILON) + self.assertAlmostEquals( + first=curve_base_fixing, + second=expected_curve_base_fixing, + msg=fail_msg, + delta=EPSILON) + + def test_lagged_fixing_method(self): + """Testing lagged fixing method""" + + inflation_idx = build_hicp_index( + EU_FIXING_DATA, self.inflation_ts_handle) + inflation_ts = build_inflation_term_structure( + VALUATION_DATE, + EUR_BEI_SWAP_RATES, + inflation_idx, + ql.CPI.Flat, + self.nominal_ts_handle) + self.inflation_ts_handle.linkTo(inflation_ts) + + maturity_date = ql.Date(25, ql.October, 2027) + lag = ql.Period(3, ql.Months) + indexation = ql.CPI.Flat + + actual_fixing = ql.CPI.laggedFixing(inflation_idx, maturity_date, lag, indexation) + expected_fixing = inflation_idx.fixing(ql.Date(1, ql.July, 2027)) + + fail_msg = """ Failed to replicate lagged fixing: + index: {inflation_idx} + actual fixing: {actual_fixing} + expected fixing: {expected_fixing} + tolerance: {tolerance} + """.format(inflation_idx=inflation_idx.familyName(), + actual_fixing=actual_fixing, + expected_fixing=expected_fixing, + tolerance=EPSILON) + self.assertAlmostEquals( + first=actual_fixing, + second=expected_fixing, + msg=fail_msg, + delta=EPSILON) + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(InflationTest, 'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/instruments.py b/quantlib/test/instruments.py new file mode 100644 index 0000000..c129eb3 --- /dev/null +++ b/quantlib/test/instruments.py @@ -0,0 +1,68 @@ +""" + Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + +flag = None + + +def raiseFlag(): + global flag + flag = 1 + + +class InstrumentTest(unittest.TestCase): + def testObservable(self): + "Testing observability of stocks" + global flag + flag = None + me1 = ql.SimpleQuote(0.0) + h = ql.RelinkableQuoteHandle(me1) + s = ql.Stock(h) + s.NPV() + + obs = ql.Observer(raiseFlag) + obs.registerWith(s) + + me1.setValue(3.14) + if not flag: + self.fail("Observer was not notified of instrument change") + + s.NPV() + flag = None + me2 = ql.SimpleQuote(0.0) + h.linkTo(me2) + if not flag: + self.fail("Observer was not notified of instrument change") + + s.NPV() + flag = None + s.freeze() + me2.setValue(2.71) + if flag: + self.fail("Observer was notified of frozen instrument change") + s.unfreeze() + if not flag: + self.fail("Observer was not notified of instrument change") + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(InstrumentTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/integrals.py b/quantlib/test/integrals.py new file mode 100644 index 0000000..eebbd91 --- /dev/null +++ b/quantlib/test/integrals.py @@ -0,0 +1,71 @@ +""" + Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest +import math + + +class IntegralTest(unittest.TestCase): + def Gauss(self, x): + return math.exp(-x * x / 2.0) / math.sqrt(2 * math.pi) + + def singleTest(self, I): + tolerance = 1e-4 + cases = [ + ["f(x) = 1", lambda x: 1, 0.0, 1.0, 1.0], + ["f(x) = x", lambda x: x, 0.0, 1.0, 0.5], + ["f(x) = x^2", lambda x: x * x, 0.0, 1.0, 1.0 / 3.0], + ["f(x) = sin(x)", math.sin, 0.0, math.pi, 2.0], + ["f(x) = cos(x)", math.cos, 0.0, math.pi, 0.0], + ["f(x) = Gauss(x)", self.Gauss, -10.0, 10.0, 1.0], + ] + + for tag, f, a, b, expected in cases: + calculated = I(f, a, b) + if not (abs(calculated - expected) <= tolerance): + self.fail( + """ +integrating %(tag)s + calculated: %(calculated)f + expected : %(expected)f + """ + % locals() + ) + + def testSegment(self): + "Testing segment integration" + self.singleTest(ql.SegmentIntegral(10000)) + + def testTrapezoid(self): + "Testing trapezoid integration" + self.singleTest(ql.TrapezoidIntegralDefault(1.0e-4, 1000)) + + def testSimpson(self): + "Testing Simpson integration" + self.singleTest(ql.SimpsonIntegral(1.0e-4, 1000)) + + def testKronrod(self): + "Testing Gauss-Kronrod integration" + self.singleTest(ql.GaussKronrodAdaptive(1.0e-4)) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(IntegralTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/marketelements.py b/quantlib/test/marketelements.py new file mode 100644 index 0000000..525e51c --- /dev/null +++ b/quantlib/test/marketelements.py @@ -0,0 +1,63 @@ +""" + Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + +flag = None + + +def raiseFlag(): + global flag + flag = 1 + + +class MarketElementTest(unittest.TestCase): + def testObservable(self): + "Testing observability of market elements" + global flag + flag = None + me = ql.SimpleQuote(0.0) + obs = ql.Observer(raiseFlag) + obs.registerWith(me) + me.setValue(3.14) + if not flag: + self.fail("Observer was not notified of market element change") + + def testObservableHandle(self): + "Testing observability of market element handles" + global flag + flag = None + me1 = ql.SimpleQuote(0.0) + h = ql.RelinkableQuoteHandle(me1) + obs = ql.Observer(raiseFlag) + obs.registerWith(h) + me1.setValue(3.14) + if not flag: + self.fail("Observer was not notified of market element change") + flag = None + me2 = ql.SimpleQuote(0.0) + h.linkTo(me2) + if not flag: + self.fail("Observer was not notified of market element change") + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(MarketElementTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/ode.py b/quantlib/test/ode.py new file mode 100644 index 0000000..5647254 --- /dev/null +++ b/quantlib/test/ode.py @@ -0,0 +1,48 @@ +""" + Copyright (C) 2019 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import math +import unittest + +import mxdevtool as ql + +class OdeTest(unittest.TestCase): + + def test1dODE(self): + """ Testing one dimesnional ODE """ + + yEnd = ql.RungeKutta(1e-8)(lambda x, y : y, 1, 0, 1) + + self.assertAlmostEqual(yEnd, math.exp(1), 5, + msg="Unable to reproduce one dimensional ODE solution.") + + + def test2dODE(self): + """ Testing multi-dimesnional ODE """ + + yEnd = ql.RungeKutta(1e-8)(lambda x, y : [y[1], -y[0]], + [0, 1], 0, 0.5*math.pi)[0] + + self.assertAlmostEqual(yEnd, 1.0, 5, + msg="Unable to reproduce multi-dimensional ODE solution.") + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(OdeTest,'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/options.py b/quantlib/test/options.py new file mode 100644 index 0000000..c199cd8 --- /dev/null +++ b/quantlib/test/options.py @@ -0,0 +1,112 @@ +""" + Copyright (C) 2021 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest + +import mxdevtool as ql + +class OptionsTest(unittest.TestCase): + + def testFdHestonHullWhite(self): + """ Testing FDM Heston Hull-White pricing """ + + dc = ql.Actual365Fixed() + todays_date = ql.Date(19, ql.May, 2021) + + r = ql.YieldTermStructureHandle(ql.FlatForward(todays_date, 0.075, dc)) + d = ql.YieldTermStructureHandle(ql.FlatForward(todays_date, 0.01, dc)) + + s0 = 8.0 + + v0 = 0.2*0.2 + kappa = 1.0 + theta = v0 + sigma = 0.4 + rho = -0.75 + + a = 0.00883 + sig = 0.00631 + + underlying = ql.QuoteHandle(ql.SimpleQuote(s0)) + + option = ql.VanillaOption( + ql.PlainVanillaPayoff(ql.Option.Call, s0), + ql.EuropeanExercise(todays_date + ql.Period(1, ql.Years)) + ) + + hull_white_process = ql.HullWhiteProcess(r, a, sig) + heston_process = ql.HestonProcess(r, d, underlying, v0, kappa, theta, sigma, rho) + + option.setPricingEngine( + ql.FdHestonHullWhiteVanillaEngine( + ql.HestonModel(heston_process), hull_white_process, -0.5, + 10, 200, 25, 10, + controlVariate=True + ) + ) + + self.assertAlmostEqual(0.87628, option.NPV(), 4) + + def testAnalyticHestonHullWhite(self): + """ Testing Analytic Heston Hull-White pricing """ + today = ql.Date.todaysDate() + dc = ql.Actual365Fixed() + + maturityDate = today + ql.Period(10 * 365, ql.Days) + + v0 = 0.04 + kappa = 0.5 + theta = 0.04 + sigma = 1.0 + sig = 0.09 + rho = -0.9 + a = 0.08 + + r = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.05, dc)) + q = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.03, dc)) + + option = ql.VanillaOption( + ql.PlainVanillaPayoff(ql.Option.Call, 100.0), + ql.EuropeanExercise(maturityDate) + ) + + expected = 40.028973 + + s0 = 100 + underlying = ql.QuoteHandle(ql.SimpleQuote(s0)) + + hull_white_model = ql.HullWhite(r, a, sig) + heston_model = ql.HestonModel( + ql.HestonProcess(r, q, underlying, v0, kappa, theta, sigma, rho) + ) + + option.setPricingEngine( + ql.AnalyticHestonHullWhiteEngine(heston_model, hull_white_model) + ) + self.assertAlmostEqual(expected, option.NPV(), 5) + + option.setPricingEngine( + ql.AnalyticH1HWEngine(heston_model, hull_white_model, 0.0) + ) + self.assertAlmostEqual(expected, option.NPV(), 5) + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(OptionsTest,'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/ratehelpers.py b/quantlib/test/ratehelpers.py new file mode 100644 index 0000000..803ab97 --- /dev/null +++ b/quantlib/test/ratehelpers.py @@ -0,0 +1,745 @@ +# coding=utf-8-unix +""" + Copyright (C) 2009 Joseph Malicki + Copyright (C) 2016, 2019 Wojciech Ślusarski + Copyright (C) 2021 Marcin Rybacki + + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + + +class FixedRateBondHelperTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = ql.Date(2, 1, 2010) + self.settlement_days = 3 + self.face_amount = 100.0 + self.redemption = 100.0 + self.quote_handle = ql.QuoteHandle(ql.SimpleQuote(100.0)) + + self.issue_date = ql.Date(2, 1, 2008) + self.maturity_date = ql.Date(2, 1, 2018) + self.calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + self.day_counter = ql.ActualActual(ql.ActualActual.Bond) + self.sched = ql.Schedule( + self.issue_date, + self.maturity_date, + ql.Period(ql.Semiannual), + self.calendar, + ql.Unadjusted, + ql.Unadjusted, + ql.DateGeneration.Backward, + False, + ) + self.coupons = [0.05] + + self.bond_helper = ql.FixedRateBondHelper( + self.quote_handle, + self.settlement_days, + self.face_amount, + self.sched, + self.coupons, + self.day_counter, + ql.Following, + self.redemption, + self.issue_date, + ) + + def testBond(self): + """ Testing FixedRateBondHelper bond() method. """ + bond = self.bond_helper.bond() + self.assertEqual(bond.issueDate(), self.issue_date) + self.assertEqual(bond.nextCouponRate(), self.coupons[0]) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + +class OISRateHelperTest(unittest.TestCase): + def setUp(self): + + # Market rates are artificial, just close to real ones. + self.default_quote_date = ql.Date(26, 8, 2016) + ql.Settings.instance().evaluationDate = self.default_quote_date + self.build_eur_curve(self.default_quote_date) + + def build_eur_curve(self, quotes_date): + """ + Builds the EUR OIS curve as the collateral currency discount curve + :param quotes_date: date from which it is assumed all market data are + valid + :return: tuple consisting of objects related to EUR OIS discounting + curve: ql.PiecewiseFlatForward, + ql.YieldTermStructureHandle + ql.RelinkableYieldTermStructureHandle + """ + calendar = ql.TARGET() + settlementDays = 2 + + todaysDate = quotes_date + ql.Settings.instance().evaluationDate = todaysDate + + todays_Eonia_quote = -0.00341 + + # market quotes + # deposits, key structure as (settlement_days_number, number_of_units_ + # for_maturity, unit) + deposits = {(0, 1, ql.Days): todays_Eonia_quote} + + self.discounting_yts_handle = ql.RelinkableYieldTermStructureHandle() + self.on_index = ql.Eonia(self.discounting_yts_handle) + self.on_index.addFixing(todaysDate, todays_Eonia_quote / 100.0) + + self.ois = { + (1, ql.Weeks): -0.342, + (1, ql.Months): -0.344, + (3, ql.Months): -0.349, + (6, ql.Months): -0.363, + (1, ql.Years): -0.389, + } + + # convert them to Quote objects + for sett_num, n, unit in deposits.keys(): + deposits[(sett_num, n, unit)] = ql.SimpleQuote( + deposits[(sett_num, n, unit)] / 100.0) + + for n, unit in self.ois.keys(): + self.ois[(n, unit)] = ql.SimpleQuote(self.ois[(n, unit)] / 100.0) + + # build rate helpers + dayCounter = ql.Actual360() + # looping left if somone wants two add more deposits to tests, e.g. T/N + + self.depositHelpers = [ + ql.DepositRateHelper( + ql.QuoteHandle(deposits[(sett_num, n, unit)]), + ql.Period(n, unit), + sett_num, + calendar, + ql.ModifiedFollowing, + True, + dayCounter, + ) + for sett_num, n, unit in deposits.keys() + ] + + self.oisHelpers = [ + ql.OISRateHelper( + settlementDays, ql.Period(n, unit), + ql.QuoteHandle(self.ois[(n, unit)]), self.on_index, + self.discounting_yts_handle) + for n, unit in self.ois.keys() + ] + + rateHelpers = self.depositHelpers + self.oisHelpers + + # term-structure construction + self.oisSwapCurve = ql.PiecewiseFlatForward(todaysDate, rateHelpers, + ql.Actual360()) + self.oisSwapCurve.enableExtrapolation() + self.discounting_yts_handle.linkTo(self.oisSwapCurve) + + def test_ois_ratehelper_impliedquote(self): + """Test if OISRateHelper.impliedQuote provides original quote from curve""" + # initiate curves - required due to lazy evaluation + self.discounting_yts_handle.discount(0.0) + + for key, rate_helper in zip(self.ois.keys(), self.oisHelpers): + expected = self.ois[key].value() + # based on bootstrapped_curve + calculated = rate_helper.impliedQuote() + self.assertAlmostEqual(expected, calculated, + delta=1e-8, + msg="Calculated implied quote differes too " + "much from original market value") + + def test_ois_pricing_with_calibrated_discount_curve(self): + """Test repricing of swaps built with MakeOIS class""" + for n, unit in self.ois.keys(): + quote_rate = self.ois.get((n, unit)).value() + ois = ql.MakeOIS(ql.Period(n, unit), self.on_index, + fixedRate=quote_rate, + nominal=10000, + discountingTermStructure=self.discounting_yts_handle) + calculated_rate = ois.fairRate() + diff = (quote_rate - calculated_rate) * 1E4 + self.assertAlmostEqual(quote_rate, calculated_rate, + delta=1e-10, + msg="Failed to reprice swap {n} {unit}" + " with a npv difference of {diff}bps" + "".format(n=n, unit=unit, diff=diff)) + + def test_ois_default_calendar(self): + """Test if ois built using MakeOIS has proper default calendar + + MakeOIS class constructor in C++ is hardcoded with default calendar set + to the same as of the overnightIndex. The methods available in the class + allow for assigning different paymentCalendar, but the start date is + already set and additional calendar will have no impact. The test checks + if the constructor exposed to Python maintains this desired property and + verifies that the start date of a EUR plain vanilla OIS traded on March + 29th, 2018 is equal to April 4th, 2018 (du to holiday on March 30th, + 2018 in TARGET calendar. + """ + test_date = ql.Date(29, 3, 2018) + ql.Settings.instance().evaluationDate = test_date + eonia = ql.Eonia() + calendar = eonia.fixingCalendar() + expected_date = calendar.advance(test_date, + ql.Period('2d'), + ql.Following) + self.assertEqual(expected_date, ql.Date(4, 4, 2018)) + ois = ql.MakeOIS(ql.Period('1Y'), eonia, -0.003, ql.Period(0, ql.Days)) + print(ois.startDate()) + self.assertEqual(expected_date, ois.startDate()) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + +class FxSwapRateHelperTest(unittest.TestCase): + def setUp(self): + + # Market rates are artificial, just close to real ones. + self.default_quote_date = ql.Date(26, 8, 2016) + + self.fx_swap_quotes = { + (1, ql.Months): 20e-4, + (3, ql.Months): 60e-4, + (6, ql.Months): 120e-4, + (1, ql.Years): 240e-4, + } + + # Valid only for the quote date of ql.Date(26, 8, 2016) + self.maturities = [ql.Date(30, 9, 2016), ql.Date(30, 11, 2016), + ql.Date(28, 2, 2017), ql.Date(30, 8, 2017)] + + self.fx_spot_quote_EURPLN = 4.3 + self.fx_spot_quote_EURUSD = 1.1 + + def build_eur_curve(self, quotes_date): + """ + Builds the EUR OIS curve as the collateral currency discount curve + :param quotes_date: date fro which it is assumed all market data are + valid + :return: tuple consisting of objects related to EUR OIS discounting + curve: ql.PiecewiseFlatForward, + ql.YieldTermStructureHandle + ql.RelinkableYieldTermStructureHandle + """ + calendar = ql.TARGET() + settlementDays = 2 + + todaysDate = quotes_date + ql.Settings.instance().evaluationDate = todaysDate + + todays_Eonia_quote = -0.00341 + + # market quotes + # deposits, key structure as (settlement_days_number, number_of_units_ + # for_maturity, unit) + deposits = {(0, 1, ql.Days): todays_Eonia_quote} + + discounting_yts_handle = ql.RelinkableYieldTermStructureHandle() + on_index = ql.Eonia(discounting_yts_handle) + on_index.addFixing(todaysDate, todays_Eonia_quote / 100.0) + + ois = { + (1, ql.Weeks): -0.342, + (1, ql.Months): -0.344, + (3, ql.Months): -0.349, + (6, ql.Months): -0.363, + (1, ql.Years): -0.389, + } + + # convert them to Quote objects + for sett_num, n, unit in deposits.keys(): + deposits[(sett_num, n, unit)] = ql.SimpleQuote( + deposits[(sett_num, n, unit)] / 100.0) + + for n, unit in ois.keys(): + ois[(n, unit)] = ql.SimpleQuote(ois[(n, unit)] / 100.0) + + # build rate helpers + dayCounter = ql.Actual360() + # looping left if somone wants two add more deposits to tests, e.g. T/N + + depositHelpers = [ + ql.DepositRateHelper( + ql.QuoteHandle(deposits[(sett_num, n, unit)]), + ql.Period(n, unit), + sett_num, + calendar, + ql.ModifiedFollowing, + True, + dayCounter, + ) + for sett_num, n, unit in deposits.keys() + ] + + oisHelpers = [ + ql.OISRateHelper( + settlementDays, ql.Period(n, unit), + ql.QuoteHandle(ois[(n, unit)]), on_index, discounting_yts_handle + ) + for n, unit in ois.keys() + ] + + rateHelpers = depositHelpers + oisHelpers + + # term-structure construction + oisSwapCurve = ql.PiecewiseFlatForward(todaysDate, rateHelpers, + ql.Actual360()) + oisSwapCurve.enableExtrapolation() + return ( + oisSwapCurve, + ql.YieldTermStructureHandle(oisSwapCurve), + ql.RelinkableYieldTermStructureHandle(oisSwapCurve), + ) + + def build_pln_fx_swap_curve(self, base_ccy_yts, fx_swaps, fx_spot): + """ + Build curve implied from fx swap curve. + :param base_ccy_yts: + Relinkable yield term structure handle to curve in base currency. + :param fx_swaps: + Dictionary with swap points, already divided by 10,000 + :param fx_spot: + Float value of fx spot exchange rate. + :return: tuple consisting of objects related to fx swap implied curve: + ql.PiecewiseFlatForward, + ql.YieldTermStructureHandle + ql.RelinkableYieldTermStructureHandle + list of ql.FxSwapRateHelper + """ + todaysDate = base_ccy_yts.referenceDate() + # I am not sure if that is required, but I guss it is worth setting + # up just in case somewhere another thread updates this setting. + ql.Settings.instance().evaluationDate = todaysDate + + calendar = ql.JointCalendar(ql.TARGET(), ql.Poland()) + spot_date_lag = 2 + trading_calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + + # build rate helpers + + spotFx = ql.SimpleQuote(fx_spot) + + fxSwapHelpers = [ + ql.FxSwapRateHelper( + ql.QuoteHandle(ql.SimpleQuote(fx_swaps[(n, unit)])), + ql.QuoteHandle(spotFx), + ql.Period(n, unit), + spot_date_lag, + calendar, + ql.ModifiedFollowing, + True, + True, + base_ccy_yts, + trading_calendar, + ) + for n, unit in fx_swaps.keys() + ] + + # term-structure construction + fxSwapCurve = ql.PiecewiseFlatForward(todaysDate, fxSwapHelpers, + ql.Actual365Fixed()) + fxSwapCurve.enableExtrapolation() + return ( + fxSwapCurve, + ql.YieldTermStructureHandle(fxSwapCurve), + ql.RelinkableYieldTermStructureHandle(fxSwapCurve), + fxSwapHelpers, + ) + + def build_curves(self, quote_date): + """ + Build all the curves in one call for a specified quote date + + :param quote_date: date for which quotes are valid, + e.g. ql.Date(26, 8, 2016) + """ + self.today = quote_date + self.eur_ois_curve, self.eur_ois_handle, self.eur_ois_rel_handle = self.build_eur_curve( + self.today) + + self.pln_eur_implied_curve, self.pln_eur_implied_curve_handle, self.pln_eur_implied_curve_relinkable_handle, self.eur_pln_fx_swap_helpers = self.build_pln_fx_swap_curve( + self.eur_ois_rel_handle, self.fx_swap_quotes, + self.fx_spot_quote_EURPLN + ) + + def testQuote(self): + """ Testing FxSwapRateHelper.quote() method. """ + self.build_curves(self.default_quote_date) + # Not sure if all Python versions and machine will guarantee that the + # lists are not messed, probably some ordered maps should be used + # here while retrieving values from fx_swap_quotes dictionary + original_quotes = list(self.fx_swap_quotes.values()) + for n in range(len(original_quotes)): + original_quote = original_quotes[n] + rate_helper_quote = self.eur_pln_fx_swap_helpers[n].quote().value() + self.assertEqual(original_quote, rate_helper_quote) + + def testLatestDate(self): + """ Testing FxSwapRateHelper.latestDate() method. """ + self.build_curves(self.default_quote_date) + # Check if still the test date is unchanged, otherwise all other + # tests here make no sense. + self.assertEqual(self.today, ql.Date(26, 8, 2016)) + + # Hard coded expected maturities of fx swaps + for n in range(len(self.maturities)): + self.assertEqual(self.maturities[n], + self.eur_pln_fx_swap_helpers[n].latestDate()) + + def testImpliedRates(self): + """ + Testing if rates implied from the curve are returning fx forwards + very close to those used for bootstrapping + """ + self.build_curves(self.default_quote_date) + # Not sure if all Python versions and machine will guarantee that the + # lists are not messed, probably some ordered maps should be used + # here while retrieving values from fx_swap_quotes dictionary + original_quotes = list(self.fx_swap_quotes.values()) + spot_date = ql.Date(30, 8, 2016) + spot_df = self.eur_ois_curve.discount( + spot_date) / self.pln_eur_implied_curve.discount(spot_date) + + for original_quote, maturity in zip(original_quotes, self.maturities): + original_forward = self.fx_spot_quote_EURPLN + original_quote + curve_impl_forward = ( + self.fx_spot_quote_EURPLN + * self.eur_ois_curve.discount(maturity) + / self.pln_eur_implied_curve.discount(maturity) + / spot_df + ) + + self.assertAlmostEqual(original_forward, curve_impl_forward, + places=6) + + def testFxMarketConventionsForCrossRate(self): + """ + Testing if ql.FxSwapRateHelper obeys the fx spot market + conventions for cross rates. + """ + today = ql.Date(1, 7, 2016) + spot_date = ql.Date(5, 7, 2016) + self.build_curves(today) + + us_calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + + joint_calendar = ql.JointCalendar(ql.TARGET(), ql.Poland()) + + settlement_calendar = ql.JointCalendar(joint_calendar, us_calendar) + + # Settlement should be on a day where all three centers are operating + # and follow EndOfMonth rule + maturities = [ + settlement_calendar.advance(spot_date, n, unit, + ql.ModifiedFollowing, True) + for n, unit in self.fx_swap_quotes.keys() + ] + + for n in range(len(maturities)): + self.assertEqual(maturities[n], + self.eur_pln_fx_swap_helpers[n].latestDate()) + + def testFxMarketConventionsForCrossRateONPeriod(self): + """ + Testing if ql.FxSwapRateHelper obeys the fx spot market + conventions for cross rates' ON Period. + """ + today = ql.Date(1, 7, 2016) + ql.Settings.instance().evaluationDate = today + + spot_date = ql.Date(5, 7, 2016) + fwd_points = 4.0 + # critical for ON rate helper + on_period = ql.Period("1d") + fixing_days = 0 + + # empty RelinkableYieldTermStructureHandle is sufficient for testing + # dates + base_ccy_yts = ql.RelinkableYieldTermStructureHandle() + + us_calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + + joint_calendar = ql.JointCalendar(ql.TARGET(), ql.Poland()) + + # Settlement should be on a day where all three centers are operating + # and follow EndOfMonth rule + on_rate_helper = ql.FxSwapRateHelper( + ql.QuoteHandle(ql.SimpleQuote(fwd_points)), + ql.QuoteHandle(ql.SimpleQuote(self.fx_spot_quote_EURPLN)), + on_period, + fixing_days, + joint_calendar, + ql.ModifiedFollowing, + False, + True, + base_ccy_yts, + us_calendar, + ) + + self.assertEqual(spot_date, on_rate_helper.latestDate()) + + def testFxMarketConventionsForCrossRateAdjustedSpotDate(self): + """ + Testing if ql.FxSwapRateHelper obeys the fx spot market + conventions + """ + today = ql.Date(30, 6, 2016) + spot_date = ql.Date(5, 7, 2016) + self.build_curves(today) + us_calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + joint_calendar = ql.JointCalendar(ql.TARGET(), ql.Poland()) + + settlement_calendar = ql.JointCalendar(joint_calendar, us_calendar) + # Settlement should be on a day where all three centers are operating + # and follow EndOfMonth rule + maturities = [ + joint_calendar.advance(spot_date, n, unit, ql.ModifiedFollowing, + True) + for n, unit in self.fx_swap_quotes.keys() + ] + + maturities = [settlement_calendar.adjust(date) for date in maturities] + + for helper, maturity in zip(self.eur_pln_fx_swap_helpers, maturities): + self.assertEqual(maturity, helper.latestDate()) + + def testFxMarketConventionsForDatesInEURUSD_ON_Period(self): + """ + Testing if ql.FxSwapRateHelper obeys the fx spot market + conventions for EURUSD settlement dates on the ON Period. + """ + today = ql.Date(1, 7, 2016) + ql.Settings.instance().evaluationDate = today + + spot_date = ql.Date(5, 7, 2016) + fwd_points = 4.0 + # critical for ON rate helper + on_period = ql.Period("1d") + fixing_days = 0 + + # empty RelinkableYieldTermStructureHandle is sufficient for testing + # dates + base_ccy_yts = ql.RelinkableYieldTermStructureHandle() + + # In EURUSD, there must be two days to spot date in Target calendar + # and one day in US, therefore it is sufficient to pass only Target + # as a base calendar + calendar = ql.TARGET() + trading_calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + + on_rate_helper = ql.FxSwapRateHelper( + ql.QuoteHandle(ql.SimpleQuote(fwd_points)), + ql.QuoteHandle(ql.SimpleQuote(self.fx_spot_quote_EURUSD)), + on_period, + fixing_days, + calendar, + ql.ModifiedFollowing, + False, + True, + base_ccy_yts, + trading_calendar, + ) + + self.assertEqual(spot_date, on_rate_helper.latestDate()) + + def testFxMarketConventionsForDatesInEURUSD_ShortEnd(self): + """ + Testing if ql.FxSwapRateHelper obeys the fx spot market + conventions for EURUSD settlement dates on the 3M tenor. + """ + today = ql.Date(1, 7, 2016) + ql.Settings.instance().evaluationDate = today + + expected_3M_date = ql.Date(5, 10, 2016) + fwd_points = 4.0 + # critical for ON rate helper + period = ql.Period("3M") + fixing_days = 2 + + # empty RelinkableYieldTermStructureHandle is sufficient for testing + # dates + base_ccy_yts = ql.RelinkableYieldTermStructureHandle() + + # In EURUSD, there must be two days to spot date in Target calendar + # and one day in US, therefore it is sufficient to pass only Target + # as a base calendar. Passing joint calendar would result in wrong + # spot date of the trade + calendar = ql.TARGET() + trading_calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) + + rate_helper = ql.FxSwapRateHelper( + ql.QuoteHandle(ql.SimpleQuote(fwd_points)), + ql.QuoteHandle(ql.SimpleQuote(self.fx_spot_quote_EURUSD)), + period, + fixing_days, + calendar, + ql.ModifiedFollowing, + True, + True, + base_ccy_yts, + trading_calendar, + ) + + self.assertEqual(expected_3M_date, rate_helper.latestDate()) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + +def flat_rate(rate): + return ql.FlatForward( + 0, ql.NullCalendar(), ql.QuoteHandle(ql.SimpleQuote(rate)), ql.Actual365Fixed()) + + +class CrossCurrencyBasisSwapRateHelperTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = ql.Date(26, 5, 2021) + + self.basis_point = 1.0e-4 + self.settlement_days = 2 + self.business_day_convention = ql.Following + self.calendar = ql.TARGET() + self.day_count = ql.Actual365Fixed() + self.end_of_month = False + base_ccy_idx_handle = ql.YieldTermStructureHandle(flat_rate(0.007)) + quoted_ccy_idx_handle = ql.YieldTermStructureHandle(flat_rate(0.015)) + self.base_ccy_idx = ql.Euribor3M(base_ccy_idx_handle) + self.quote_ccy_idx = ql.USDLibor( + ql.Period(3, ql.Months), quoted_ccy_idx_handle) + self.collateral_ccy_handle = ql.YieldTermStructureHandle( + flat_rate(0.009)) + # Cross currency basis swaps data source: + # N. Moreni, A. Pallavicini (2015) + # FX Modelling in Collateralized Markets: foreign measures, basis curves + # and pricing formulae. + # section 4.2.1, Table 2. + self.cross_currency_basis_quotes = ((ql.Period(1, ql.Years), -14.5), + (ql.Period(18, ql.Months), -18.5), + (ql.Period(2, ql.Years), -20.5), + (ql.Period(3, ql.Years), -23.75), + (ql.Period(4, ql.Years), -25.5), + (ql.Period(5, ql.Years), -26.5), + (ql.Period(7, ql.Years), -26.75), + (ql.Period(10, ql.Years), -26.25), + (ql.Period(15, ql.Years), -24.75), + (ql.Period(20, ql.Years), -23.25), + (ql.Period(30, ql.Years), -20.50)) + + def buildRateHelper( + self, + quote_tuple, + is_fx_base_ccy_collateral_ccy, + is_basis_on_fx_base_ccy_leg): + tenor, rate = quote_tuple + quote_handle = ql.QuoteHandle(ql.SimpleQuote(rate * self.basis_point)) + return ql.ConstNotionalCrossCurrencyBasisSwapRateHelper( + quote_handle, + tenor, + self.settlement_days, + self.calendar, + self.business_day_convention, + self.end_of_month, + self.base_ccy_idx, + self.quote_ccy_idx, + self.collateral_ccy_handle, + is_fx_base_ccy_collateral_ccy, + is_basis_on_fx_base_ccy_leg) + + def assertImpliedQuotes( + self, + is_fx_base_ccy_collateral_ccy, + is_basis_on_fx_base_ccy_leg): + eps = 1.0e-8 + helpers = [self.buildRateHelper(q, + is_fx_base_ccy_collateral_ccy, + is_basis_on_fx_base_ccy_leg) + for q in self.cross_currency_basis_quotes] + term_structure = ql.PiecewiseLogLinearDiscount( + self.settlement_days, self.calendar, helpers, self.day_count) + settlement_date = term_structure.referenceDate() + + # Trigger bootstrap + discount_at_origin = term_structure.discount(settlement_date) + self.assertAlmostEquals( + first=discount_at_origin, second=1.0, delta=eps) + + for q, h in zip(self.cross_currency_basis_quotes, helpers): + tenor, expected_rate = q + actual_rate = h.impliedQuote() / self.basis_point + + fail_msg = """ Failed to replicate cross currency basis: + tenor: {tenor} + actual basis: {actual_rate} + expected basis: {expected_rate} + tolerance: {tolerance} + """.format(tenor=tenor, + actual_rate=actual_rate, + expected_rate=expected_rate, + tolerance=eps) + self.assertAlmostEquals( + first=actual_rate, + second=expected_rate, + delta=eps, + msg=fail_msg) + + def testFxBasisSwapsWithCollateralInBaseAndBasisInQuoteCcy(self): + """ Testing basis swaps instruments with collateral in base ccy and basis in quote ccy... """ + is_fx_base_ccy_collateral_ccy = True + is_basis_on_fx_base_currency_leg = False + self.assertImpliedQuotes( + is_fx_base_ccy_collateral_ccy, is_basis_on_fx_base_currency_leg) + + def testFxBasisSwapsWithCollateralInQuoteAndBasisInBaseCcy(self): + """ Testing basis swaps instruments with collateral in quote ccy and basis in base ccy... """ + is_fx_base_ccy_collateral_ccy = False + is_basis_on_fx_base_currency_leg = True + self.assertImpliedQuotes( + is_fx_base_ccy_collateral_ccy, is_basis_on_fx_base_currency_leg) + + def testFxBasisSwapsWithCollateralAndBasisInBaseCcy(self): + """ Testing basis swaps instruments with collateral and basis in base ccy... """ + is_fx_base_ccy_collateral_ccy = True + is_basis_on_fx_base_currency_leg = True + self.assertImpliedQuotes( + is_fx_base_ccy_collateral_ccy, is_basis_on_fx_base_currency_leg) + + def testFxBasisSwapsWithCollateralAndBasisInQuoteCcy(self): + """ Testing basis swaps instruments with collateral and basis in quote ccy... """ + is_fx_base_ccy_collateral_ccy = False + is_basis_on_fx_base_currency_leg = False + self.assertImpliedQuotes( + is_fx_base_ccy_collateral_ccy, is_basis_on_fx_base_currency_leg) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(FixedRateBondHelperTest, "test")) + suite.addTest(unittest.makeSuite(OISRateHelperTest, "test")) + suite.addTest(unittest.makeSuite(FxSwapRateHelperTest, "test")) + suite.addTest(unittest.makeSuite( + CrossCurrencyBasisSwapRateHelperTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/sabr.py b/quantlib/test/sabr.py new file mode 100644 index 0000000..16257be --- /dev/null +++ b/quantlib/test/sabr.py @@ -0,0 +1,122 @@ +""" + Copyright (C) 2019 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import math +import unittest + +import mxdevtool as ql + +class SabrTest(unittest.TestCase): + + def testHagenFormula(self): + """ Testing Hagen et al. formula """ + + today = ql.Date(9,1,2019) + dc = ql.Actual365Fixed() + maturityDate = today + ql.Period(6, ql.Months) + maturityTime = dc.yearFraction(today, maturityDate) + + alpha = 0.35 + beta = 0.85 + nu = 0.75 + rho = 0.85 + f0 = 100.0 + strike = 110.0 + + sabrVol = ql.sabrVolatility(strike, f0, maturityTime, alpha, beta, nu, rho) + + self.assertAlmostEqual(sabrVol, 0.205953, 6, + msg="Unable to reproduce Hagen et al. SABR volatility") + + flochKennedyVol = ql.sabrFlochKennedyVolatility( + strike, f0, maturityTime, alpha, beta, nu, rho) + + self.assertAlmostEqual(flochKennedyVol, 0.205447, 6, + msg="Unable to reproduce Le Floc'h-Kennedy SABR volatility") + + def testPdeSolver(self): + """ Testing BENCHOP-SLV SABR example value """ + + today = ql.Date(8, 1, 2019) + dc = ql.Actual365Fixed() + maturityDate = today + ql.Period(10 * 365, ql.Days) + maturityTime = dc.yearFraction(today, maturityDate) + + f0 = 0.07 + alpha = 0.4 + nu = 0.8 + beta = 0.5 + rho = -0.6 + strike = f0 * math.exp(-0.1 * math.sqrt(maturityTime)) + + rTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.0, dc)) + + # see https://ir.cwi.nl/pub/28249 + expected = 0.052450313614407 + + option = ql.VanillaOption( + ql.PlainVanillaPayoff(ql.Option.Call, strike), + ql.EuropeanExercise(maturityDate)) + + option.setPricingEngine(ql.FdSabrVanillaEngine(f0, alpha, beta, nu, rho, rTS, 30, 800, 30, 1, 0.8)) + + calculated = option.NPV() + + self.assertAlmostEqual(calculated, expected, 4, + msg="Unable to reproduce Le Floc'h-Kennedy SABR volatility") + + + def testSabrPdeVsCevPdeVsAnalyticCev(self): + """ Testing SABR PDE vs CEV PDE vs Analytic CEV """ + + today = ql.Date(1, 3, 2019) + dc = ql.Actual365Fixed() + + maturityDate = today + ql.Period(12, ql.Months) + f0 = 1.2 + alpha = 0.35 + beta = 0.9 + nu = 1e-3 + rho = 0.25 + strike = 1.1 + + rTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.05, dc)) + + option = ql.VanillaOption( + ql.PlainVanillaPayoff(ql.Option.Call, strike), + ql.EuropeanExercise(maturityDate)) + + option.setPricingEngine(ql.FdSabrVanillaEngine(f0, alpha, beta, nu, rho, rTS, 30, 400, 3)) + fdSabrNPV = option.NPV() + + option.setPricingEngine(ql.FdCEVVanillaEngine(f0, alpha, beta, rTS, 30, 400)) + fdCevNPV = option.NPV() + + option.setPricingEngine(ql.AnalyticCEVEngine(f0, alpha, beta, rTS)) + analyticCevNPV = option.NPV() + + self.assertAlmostEqual(fdSabrNPV, analyticCevNPV, 4, + msg="Unable to match PDE SABR value with analytic CEV value") + + self.assertAlmostEqual(fdCevNPV, analyticCevNPV, 4, + msg="Unable to match PDE CEV value with analytic CEV value") + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(SabrTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/slv.py b/quantlib/test/slv.py new file mode 100644 index 0000000..7ed084b --- /dev/null +++ b/quantlib/test/slv.py @@ -0,0 +1,156 @@ +""" + Copyright (C) 2019 Klaus Spanderen + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest + +import mxdevtool as ql + + +class SlvTest(unittest.TestCase): + def setUp(self): + self.todaysDate = ql.Date(15, ql.May, 2019) + ql.Settings.instance().evaluationDate = self.todaysDate + self.settlementDate = self.todaysDate + ql.Period(2, ql.Days) + self.dc = ql.Actual365Fixed() + self.riskFreeRate = ql.YieldTermStructureHandle(ql.FlatForward(self.settlementDate, 0.05, self.dc)) + self.dividendYield = ql.YieldTermStructureHandle(ql.FlatForward(self.settlementDate, 0.025, self.dc)) + self.underlying = ql.QuoteHandle(ql.SimpleQuote(100.0)) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def constVol(self, vol): + return ql.BlackVolTermStructureHandle(ql.BlackConstantVol(self.settlementDate, ql.TARGET(), vol, self.dc)) + + def testSlvProcess(self): + """ Testing HestonSLVProcess generation """ + + hestonProcess = ql.HestonProcess( + self.riskFreeRate, self.riskFreeRate, self.underlying, 0.1 * 0.1, 1.0, 0.25 * 0.25, 0.15, -0.75 + ) + + localVol = ql.LocalVolSurface( + ql.BlackVolTermStructureHandle(ql.BlackConstantVol(self.settlementDate, ql.TARGET(), 0.10, self.dc)), + self.riskFreeRate, + self.riskFreeRate, + self.underlying, + ) + + ql.HestonSLVProcess(hestonProcess, localVol) + + def testSlvProcessAsBlackScholes(self): + """ Testing HestonSLVProcess equal to Black-Scholes process """ + + hestonProcess = ql.HestonProcess( + self.riskFreeRate, self.dividendYield, self.underlying, 0.01, 1.0, 0.01, 1e-4, 0.0 + ) + + exercise = ql.EuropeanExercise(self.todaysDate + ql.Period(1, ql.Years)) + payoff = ql.PlainVanillaPayoff(ql.Option.Call, self.underlying.value()) + + option = ql.VanillaOption(payoff, exercise) + + hestonModel = ql.HestonModel(hestonProcess) + option.setPricingEngine(ql.FdHestonVanillaEngine(hestonModel, 20, 100, 3)) + + hestonNPV = option.NPV() + + option.setPricingEngine( + ql.AnalyticEuropeanEngine( + ql.BlackScholesMertonProcess(self.underlying, self.dividendYield, self.riskFreeRate, self.constVol(0.1)) + ) + ) + + bsNPV = option.NPV() + + self.assertAlmostEqual( + hestonNPV, bsNPV, 2, msg="Unable to reproduce Heston vanilla option price with Black-Scholes process" + ) + + leverageFct = ql.LocalVolSurface(self.constVol(2.0), self.riskFreeRate, self.dividendYield, self.underlying) + + option.setPricingEngine( + ql.FdHestonVanillaEngine( + hestonModel, + 20, + 100, + 3, + 1, + ql.FdmSchemeDesc.Hundsdorfer(), + leverageFct, + ) + ) + + slvNPV = option.NPV() + + bsmProcess = ql.BlackScholesMertonProcess(self.underlying, self.dividendYield, self.riskFreeRate, self.constVol(0.2)) + + option.setPricingEngine(ql.AnalyticEuropeanEngine(bsmProcess)) + + bsNPV = option.NPV() + + self.assertAlmostEqual( + slvNPV, + bsNPV, + 2, + msg="Unable to reproduce Heston plus constant local vol option price with Black-Scholes formula", + ) + + barrier_lo = 70.0 + barrier_hi = 130.0 + + barrierOption = ql.DoubleBarrierOption( + ql.DoubleBarrier.KnockOut, + barrier_lo, + barrier_hi, + 0.0, + ql.CashOrNothingPayoff(ql.Option.Call, 0.0, 1.0), + exercise); + + barrierOption.setPricingEngine( + ql.FdHestonDoubleBarrierEngine( + hestonModel, + 400, + 100, + 2, + 1, + ql.FdmSchemeDesc.Hundsdorfer(), + leverageFct, + ) + ) + + slvBarrierNPV = barrierOption.NPV() + + barrierOption.setPricingEngine(ql.AnalyticDoubleBarrierBinaryEngine(bsmProcess)) + + bsmBarrierNPV = barrierOption.NPV() + + self.assertAlmostEqual( + slvBarrierNPV, + bsmBarrierNPV, + 2, + msg="Unable to reproduce Heston plus constant local vol " + "double barrier option price with Black-Scholes Double Barrier Binary Engine", + ) + + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(SlvTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/solvers1d.py b/quantlib/test/solvers1d.py new file mode 100644 index 0000000..1866cdc --- /dev/null +++ b/quantlib/test/solvers1d.py @@ -0,0 +1,93 @@ +""" + Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import mxdevtool as ql + + +class Foo: + def __call__(self, x): + return x * x - 1.0 + + def derivative(self, x): + return 2.0 * x + + +class Solver1DTest(unittest.TestCase): + def runTest(self): + "Testing 1-D solvers" + for factory in [ql.Brent, ql.Bisection, ql.FalsePosition, ql.Ridder, ql.Secant]: + solver = factory() + for accuracy in [1.0e-4, 1.0e-6, 1.0e-8]: + root = solver.solve(lambda x: x * x - 1.0, accuracy, 1.5, 0.1) + if not (abs(root - 1.0) < accuracy): + self.fail( + """ +%(factory)s + solve(): + expected: 1.0 + calculated root: %(root)g + accuracy: %(accuracy)s + """ + % locals() + ) + root = solver.solve(lambda x: x * x - 1.0, accuracy, 1.5, 0.0, 1.0) + if not (abs(root - 1.0) < accuracy): + self.fail( + """ +%(factory)s + bracketed solve(): + expected: 1.0 + calculated root: %(root)g + accuracy: %(accuracy)s + """ + % locals() + ) + for factory in [ql.Newton, ql.NewtonSafe]: + solver = factory() + for accuracy in [1.0e-4, 1.0e-6, 1.0e-8]: + root = solver.solve(Foo(), accuracy, 1.5, 0.1) + if not (abs(root - 1.0) < accuracy): + self.fail( + """ +%(factory)s + solve(): + expected: 1.0 + calculated root: %(root)g + accuracy: %(accuracy)s + """ + % locals() + ) + root = solver.solve(Foo(), accuracy, 1.5, 0.0, 1.0) + if not (abs(root - 1.0) < accuracy): + self.fail( + """ +%(factory)s + bracketed solve(): + expected: 1.0 + calculated root: %(root)g + accuracy: %(accuracy)s + """ + % locals() + ) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(Solver1DTest()) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/swap.py b/quantlib/test/swap.py new file mode 100644 index 0000000..b15976a --- /dev/null +++ b/quantlib/test/swap.py @@ -0,0 +1,142 @@ +""" + Copyright (C) 2021 Marcin Rybacki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import mxdevtool as ql + + +EPSILON = 1.e-8 + +CAL = ql.TARGET() + +DCT = ql.Actual365Fixed() + +VALUATION_DATE = CAL.adjust(ql.Date(1, ql.June, 2021)) + + +def flat_rate(rate): + return ql.FlatForward( + 2, CAL, ql.QuoteHandle(ql.SimpleQuote(rate)), ql.Actual365Fixed()) + + +class ZeroCouponSwapTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = VALUATION_DATE + self.nominal_ts_handle = ql.YieldTermStructureHandle(flat_rate(0.007)) + self.ibor_idx = ql.Euribor6M(self.nominal_ts_handle) + self.engine = ql.DiscountingSwapEngine(self.nominal_ts_handle) + + def build_zcs_from_fixed_payment(self, amount): + return ql.ZeroCouponSwap(ql.Swap.Receiver, + 1.0e6, + ql.Date(3, ql.June, 2021), + ql.Date(3, ql.June, 2051), + amount, + self.ibor_idx, + CAL) + + def build_zcs_from_rate(self, rate): + return ql.ZeroCouponSwap(ql.Swap.Receiver, + 1.0e6, + ql.Date(3, ql.June, 2021), + ql.Date(3, ql.June, 2051), + rate, + DCT, + self.ibor_idx, + CAL) + + def test_zero_coupon_swap_inspectors(self): + """Testing zero coupon swap inspectors""" + swap = self.build_zcs_from_fixed_payment(1.5e6) + fail_msg = "Unable to replicate the properties of a ZC swap." + + self.assertEqual(swap.type(), ql.Swap.Receiver, + msg=fail_msg) + self.assertEqual(swap.startDate(), ql.Date(3, ql.June, 2021), + msg=fail_msg) + self.assertEqual(swap.maturityDate(), ql.Date(3, ql.June, 2051), + msg=fail_msg) + self.assertAlmostEqual(swap.baseNominal(), 1.0e6, + delta=EPSILON, msg=fail_msg) + self.assertAlmostEqual(swap.fixedPayment(), 1.5e6, + delta=EPSILON, msg=fail_msg) + + def test_npvs_of_par_zero_coupon_swap_with_fixed_payment(self): + """Testing NPVs of a zero coupon swap with fixed payment""" + swap = self.build_zcs_from_fixed_payment(1.5e6) + swap.setPricingEngine(self.engine) + fair_payment = swap.fairFixedPayment() + par_swap = self.build_zcs_from_fixed_payment(fair_payment) + par_swap.setPricingEngine(self.engine) + npv = par_swap.NPV() + fail_npv_msg = """ Unable to replicate par zero coupon swap NPV: + calculated: {actual} + expected: {expected} + """.format(actual=npv, + expected=0.0) + self.assertAlmostEqual(npv, 0.0, delta=EPSILON, msg=fail_npv_msg) + + fxd_leg_npv = par_swap.fixedLegNPV() + flt_leg_npv = par_swap.floatingLegNPV() + fail_legs_npv_msg = """ Unable to replicate the NPVs of a par zero coupon swap legs: + fixed leg NPV: {fxd_leg} + floating leg NPV: {flt_leg} + """.format(fxd_leg=fxd_leg_npv, + flt_leg=flt_leg_npv) + self.assertAlmostEqual(abs(fxd_leg_npv), abs(flt_leg_npv), + delta=EPSILON, + msg=fail_legs_npv_msg) + + def test_npvs_of_par_zero_coupon_swap_with_fixed_rate(self): + """Testing NPVs of a zero coupon swap with fixed rate""" + swap = self.build_zcs_from_fixed_payment(1.5e6) + swap.setPricingEngine(self.engine) + fair_rate = swap.fairFixedRate(DCT) + par_swap = self.build_zcs_from_rate(fair_rate) + par_swap.setPricingEngine(self.engine) + npv = par_swap.NPV() + fail_msg = """ Unable to replicate par zero coupon swap NPV: + calculated: {actual} + expected: {expected} + """.format(actual=npv, + expected=0.0) + self.assertAlmostEqual(npv, 0.0, delta=EPSILON, msg=fail_msg) + + def test_zero_coupon_swap_legs(self): + """Testing zero coupon swap legs""" + swap = self.build_zcs_from_rate(0.01) + fxd_leg = swap.fixedLeg() + fxd_cf = ql.as_fixed_rate_coupon(fxd_leg[0]) + fail_msg_fxd = """Fixed leg cash flow type should be FixedRateCoupon + but was {actual}. + """.format(actual=type(fxd_cf)) + self.assertTrue(isinstance(fxd_cf, ql.FixedRateCoupon), + msg=fail_msg_fxd) + + flt_leg = swap.floatingLeg() + flt_cf = ql.as_sub_periods_coupon(flt_leg[0]) + fail_msg_flt = """Floating leg cash flow type should be SubPeriodsCoupon + but was {actual}. + """.format(actual=type(flt_cf)) + self.assertTrue(isinstance(flt_cf, ql.SubPeriodsCoupon), msg=fail_msg_flt) + + +if __name__ == '__main__': + print('testing QuantLib ' + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ZeroCouponSwapTest, 'test')) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/swaption.py b/quantlib/test/swaption.py new file mode 100644 index 0000000..3cf6808 --- /dev/null +++ b/quantlib/test/swaption.py @@ -0,0 +1,197 @@ +""" + Copyright (C) 2020 Marcin Rybacki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import mxdevtool as ql + + +BASIS_POINT = 1e-4 + +EPSILON = 1.e-10 + +OPTION_TYPE_MAP = {ql.Swap.Receiver: 'Receiver', + ql.Swap.Payer: 'Payer'} + +SETTLEMENT_TYPE_MAP = {ql.Settlement.Physical: 'Physical', + ql.Settlement.Cash: 'Cash'} + +SETTLEMENT_METHOD_MAP = {ql.Settlement.PhysicalOTC: 'Physical OTC', + ql.Settlement.CollateralizedCashPrice: ( + 'Collateralized Cash Price'), + ql.Settlement.ParYieldCurve: 'Par Yield Curve'} + + +def compounded_annual_constant_rate_discount( + rate, + day_counter): + def _calc(start, end): + time = day_counter.yearFraction(start, end) + return (1.0 + rate) ** (-time) + return _calc + + +def par_yield_bps(underlying, + discount_handle): + fixed_leg = underlying.fixedLeg() + first_coupon = ql.as_fixed_rate_coupon(fixed_leg[0]) + discount_date = first_coupon.accrualStartDate() + discount = discount_handle.discount(discount_date) + fixed_rate = underlying.fixedRate() + fixed_dct = underlying.fixedDayCount() + fair_rate = underlying.fairRate() + ir_func = compounded_annual_constant_rate_discount(fair_rate, fixed_dct) + bps = sum([ir_func(discount_date, c_f.date()) * c_f.amount() / fixed_rate + for c_f in fixed_leg + if c_f.date() > discount_date]) + return abs(bps) * discount + + +def swap_pv01(underlying): + return abs(underlying.fixedLegBPS()) / BASIS_POINT + + +def make_const_black_vol_engine(discount_handle, volatility): + h = ql.QuoteHandle(ql.SimpleQuote(volatility)) + return ql.BlackSwaptionEngine(discount_handle, h) + + +def make_const_bachelier_vol_engine(discount_handle, volatility): + h = ql.QuoteHandle(ql.SimpleQuote(volatility)) + return ql.BachelierSwaptionEngine(discount_handle, h) + + +class SwaptionTest(unittest.TestCase): + def setUp(self): + self.calendar = ql.TARGET() + self.today = self.calendar.adjust(ql.Date.todaysDate()) + ql.Settings.instance().evaluationDate = self.today + + projection_curve_handle = ql.RelinkableYieldTermStructureHandle() + self.projection_rate = 0.01 + self.projection_quote_handle = ql.RelinkableQuoteHandle() + projection_curve = ql.FlatForward( + self.today, self.projection_quote_handle, ql.Actual365Fixed()) + projection_curve_handle.linkTo(projection_curve) + + self.discount_handle = ql.YieldTermStructureHandle(ql.FlatForward( + self.today, ql.QuoteHandle(ql.SimpleQuote(0.0085)), ql.Actual365Fixed())) + self.swap_engine = ql.DiscountingSwapEngine(self.discount_handle) + + self.idx = ql.Euribor6M(projection_curve_handle) + + self.exercises = [ql.Period(1, ql.Years), ql.Period(2, ql.Years), + ql.Period(3, ql.Years), ql.Period(5, ql.Years), + ql.Period(7, ql.Years), ql.Period(10, ql.Years)] + self.lengths = [ql.Period(1, ql.Years), ql.Period(2, ql.Years), + ql.Period(3, ql.Years), ql.Period(5, ql.Years), + ql.Period(7, ql.Years), ql.Period(10, ql.Years), + ql.Period(15, ql.Years), ql.Period(20, ql.Years)] + self.swap_type = [ql.Swap.Receiver, ql.Swap.Payer] + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def _assert_swaption_annuity(self, + swaption_pricer_func, + use_bachelier_vol): + self.projection_quote_handle.linkTo( + ql.SimpleQuote(self.projection_rate)) + + settle_type = ql.Settlement.Cash + methods = [ql.Settlement.ParYieldCurve, + ql.Settlement.CollateralizedCashPrice] + + for e in self.exercises: + for l in self.lengths: + for t in self.swap_type: + for m in methods: + volatility = 0.003 if use_bachelier_vol else 0.3 + strike = 0.03 + swaption_engine = swaption_pricer_func( + self.discount_handle, volatility) + exercise_date = self.calendar.advance( + self.today, e) + start_date = self.calendar.advance( + exercise_date, ql.Period(2, ql.Days)) + + underlying = ql.MakeVanillaSwap( + l, self.idx, strike, ql.Period(0, ql.Days), + effectiveDate=start_date, + fixedLegTenor=ql.Period(1, ql.Years), + fixedLegDayCount=ql.Thirty360(ql.Thirty360.BondBasis), + floatingLegSpread=0.0, + swapType=t) + underlying.setPricingEngine(self.swap_engine) + + swaption = ql.Swaption(underlying, + ql.EuropeanExercise( + exercise_date), + settle_type, + m) + swaption.setPricingEngine(swaption_engine) + + annuity = swaption.annuity() + expected_annuity = 0.0 + if (m == ql.Settlement.CollateralizedCashPrice): + expected_annuity = swap_pv01(underlying) + if (m == ql.Settlement.ParYieldCurve): + expected_annuity = par_yield_bps( + underlying, self.discount_handle) + + fail_msg = """ Swaption annuity test failed for: + option tenor: {option_tenor} + volatility : {volatility} + option type: {option_type} + swap tenor: {swap_tenor} + strike: {strike} + settlement: {settle_type} + method: {method} + annuity: {annuity} + replicated annuity: {expected_annuity} + """.format(option_tenor=e, + volatility=volatility, + option_type=OPTION_TYPE_MAP[t], + swap_tenor=l, + strike=strike, + settle_type=SETTLEMENT_TYPE_MAP[settle_type], + method=SETTLEMENT_METHOD_MAP[m], + annuity=annuity, + expected_annuity=expected_annuity) + self.assertAlmostEquals( + first=annuity, + second=expected_annuity, + delta=EPSILON, + msg=fail_msg) + + def test_swaption_annuity_black_model(self): + """Testing swaption annuity in Black model""" + self._assert_swaption_annuity( + swaption_pricer_func=make_const_black_vol_engine, + use_bachelier_vol=False) + + def test_swaption_annuity_bachelier_model(self): + """Testing swaption annuity in Bachelier model""" + self._assert_swaption_annuity( + swaption_pricer_func=make_const_bachelier_vol_engine, + use_bachelier_vol=True) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(SwaptionTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/termstructures.py b/quantlib/test/termstructures.py new file mode 100644 index 0000000..f1c981d --- /dev/null +++ b/quantlib/test/termstructures.py @@ -0,0 +1,325 @@ +""" + Copyright (C) 2000, 2001, 2002, 2003 RiskMap srl + Copyright (C) 2007 StatPro Italia srl + Copyright (C) 2020 Marcin Rybacki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest +import math + +flag = None + + +def raiseFlag(): + global flag + flag = 1 + + +def binaryFunction(x, y): + return 2.0 * x + y + + +def extrapolatedForwardRate( + firstSmoothingPoint, + lastLiquidForwardRate, + ultimateForwardRate, + alpha): + + def calculate(t): + deltaT = t - firstSmoothingPoint + beta = (1.0 - math.exp(-alpha * deltaT)) / (alpha * deltaT) + return ultimateForwardRate + ( + lastLiquidForwardRate - ultimateForwardRate) * beta + + return calculate + + +class TermStructureTest(unittest.TestCase): + def setUp(self): + self.calendar = ql.TARGET() + today = self.calendar.adjust(ql.Date.todaysDate()) + ql.Settings.instance().evaluationDate = today + self.settlementDays = 2 + self.dayCounter = ql.Actual360() + settlement = self.calendar.advance(today, self.settlementDays, ql.Days) + deposits = [ + ql.DepositRateHelper( + ql.QuoteHandle(ql.SimpleQuote(rate / 100)), + ql.Period(n, units), + self.settlementDays, + self.calendar, + ql.ModifiedFollowing, + False, + self.dayCounter, + ) + for (n, units, rate) in [ + (1, ql.Months, 4.581), + (2, ql.Months, 4.573), + (3, ql.Months, 4.557), + (6, ql.Months, 4.496), + (9, ql.Months, 4.490), + ] + ] + swaps = [ + ql.SwapRateHelper( + ql.QuoteHandle(ql.SimpleQuote(rate / 100)), + ql.Period(years, ql.Years), + self.calendar, + ql.Annual, + ql.Unadjusted, + ql.Thirty360(ql.Thirty360.BondBasis), + ql.Euribor6M(), + ) + for (years, rate) in [(1, 4.54), (5, 4.99), (10, 5.47), (20, 5.89), (30, 5.96)] + ] + + self.termStructure = ql.PiecewiseFlatForward( + settlement, deposits + swaps, self.dayCounter) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def testImpliedObs(self): + "Testing observability of implied term structure" + global flag + flag = None + h = ql.RelinkableYieldTermStructureHandle() + settlement = self.termStructure.referenceDate() + new_settlement = self.calendar.advance(settlement, 3, ql.Years) + implied = ql.ImpliedTermStructure(h, new_settlement) + obs = ql.Observer(raiseFlag) + obs.registerWith(implied) + h.linkTo(self.termStructure) + if not flag: + self.fail("Observer was not notified of term structure change") + + def testFSpreadedObs(self): + "Testing observability of forward-spreaded term structure" + global flag + flag = None + me = ql.SimpleQuote(0.01) + mh = ql.QuoteHandle(me) + h = ql.RelinkableYieldTermStructureHandle() + spreaded = ql.ForwardSpreadedTermStructure(h, mh) + obs = ql.Observer(raiseFlag) + obs.registerWith(spreaded) + h.linkTo(self.termStructure) + if not flag: + self.fail("Observer was not notified of term structure change") + flag = None + me.setValue(0.005) + if not flag: + self.fail("Observer was not notified of spread change") + + def testZSpreadedObs(self): + "Testing observability of zero-spreaded term structure" + global flag + flag = None + me = ql.SimpleQuote(0.01) + mh = ql.QuoteHandle(me) + h = ql.RelinkableYieldTermStructureHandle() + spreaded = ql.ZeroSpreadedTermStructure(h, mh) + obs = ql.Observer(raiseFlag) + obs.registerWith(spreaded) + h.linkTo(self.termStructure) + if not flag: + self.fail("Observer was not notified of term structure change") + flag = None + me.setValue(0.005) + if not flag: + self.fail("Observer was not notified of spread change") + + def testCompositeZeroYieldStructure(self): + """Testing composite zero yield structure""" + settlement = self.termStructure.referenceDate() + compounding = ql.Compounded + freq = ql.Semiannual + flatTs = ql.FlatForward( + settlement, + ql.QuoteHandle(ql.SimpleQuote(0.0085)), + self.dayCounter) + firstHandle = ql.YieldTermStructureHandle(flatTs) + secondHandle = ql.YieldTermStructureHandle(self.termStructure) + compositeTs = ql.CompositeZeroYieldStructure( + firstHandle, secondHandle, binaryFunction, compounding, freq) + maturity = settlement + ql.Period(20, ql.Years) + expectedZeroRate = binaryFunction( + firstHandle.zeroRate( + maturity, self.dayCounter, compounding, freq).rate(), + secondHandle.zeroRate( + maturity, self.dayCounter, compounding, freq).rate()) + actualZeroRate = compositeTs.zeroRate( + maturity, self.dayCounter, compounding, freq).rate() + failMsg = """ Composite zero yield structure rate replication failed: + expected zero rate: {expected} + actual zero rate: {actual} + """.format(expected=expectedZeroRate, + actual=actualZeroRate) + self.assertAlmostEquals( + first=expectedZeroRate, + second=actualZeroRate, + delta=1.0e-12, + msg=failMsg) + + def testUltimateForwardTermStructure(self): + """Testing ultimate forward term structure""" + settlement = self.termStructure.referenceDate() + ufr = ql.QuoteHandle(ql.SimpleQuote(0.06)) + llfr = ql.QuoteHandle(ql.SimpleQuote(0.05)) + fsp = ql.Period(20, ql.Years) + alpha = 0.05 + baseCrvHandle = ql.YieldTermStructureHandle(self.termStructure) + ufrCrv = ql.UltimateForwardTermStructure( + baseCrvHandle, llfr, ufr, fsp, alpha) + cutOff = ufrCrv.timeFromReference(settlement + fsp) + forwardCalculator = extrapolatedForwardRate( + cutOff, llfr.value(), ufr.value(), alpha) + times = [ufrCrv.timeFromReference(settlement + ql.Period(x, ql.Years)) + for x in [21, 30, 40, 50, 60, 70, 80, 90, 100]] + for t in times: + actualForward = ufrCrv.forwardRate( + cutOff, t, ql.Continuous, ql.NoFrequency, True).rate() + expectedForward = forwardCalculator(t) + failMsg = """ UFR term structure forward replication failed for: + time to maturity: {timeToMaturity} + expected forward rate: {expected} + actual forward rate: {actual} + """.format(timeToMaturity=t, + expected=expectedForward, + actual=actualForward) + self.assertAlmostEquals( + first=expectedForward, + second=actualForward, + delta=1.0e-12, + msg=failMsg) + + def testQuantoTermStructure(self): + """Testing quanto term structure""" + today = ql.Date.todaysDate() + + dividend_ts = ql.YieldTermStructureHandle( + ql.FlatForward( + today, + ql.QuoteHandle(ql.SimpleQuote(0.055)), + self.dayCounter + ) + ) + r_domestic_ts = ql.YieldTermStructureHandle( + ql.FlatForward( + today, + ql.QuoteHandle(ql.SimpleQuote(-0.01)), + self.dayCounter + ) + ) + r_foreign_ts = ql.YieldTermStructureHandle( + ql.FlatForward( + today, + ql.QuoteHandle(ql.SimpleQuote(0.02)), + self.dayCounter + ) + ) + sigma_s = ql.BlackVolTermStructureHandle( + ql.BlackConstantVol( + today, + self.calendar, + ql.QuoteHandle(ql.SimpleQuote(0.25)), + self.dayCounter + ) + ) + sigma_fx = ql.BlackVolTermStructureHandle( + ql.BlackConstantVol( + today, + self.calendar, + ql.QuoteHandle(ql.SimpleQuote(0.05)), + self.dayCounter + ) + ) + rho = ql.QuoteHandle(ql.SimpleQuote(0.3)) + s_0 = ql.QuoteHandle(ql.SimpleQuote(100.0)) + + exercise = ql.EuropeanExercise(self.calendar.advance(today, 6, ql.Months)) + payoff = ql.PlainVanillaPayoff(ql.Option.Call, 95.0) + + vanilla_option = ql.VanillaOption(payoff, exercise) + quanto_ts = ql.YieldTermStructureHandle( + ql.QuantoTermStructure( + dividend_ts, + r_domestic_ts, + r_foreign_ts, + sigma_s, + ql.nullDouble(), + sigma_fx, + ql.nullDouble(), + rho.value() + ) + ) + gbm_quanto = ql.BlackScholesMertonProcess(s_0, quanto_ts, r_domestic_ts, sigma_s) + vanilla_engine = ql.AnalyticEuropeanEngine(gbm_quanto) + vanilla_option.setPricingEngine(vanilla_engine) + + quanto_option = ql.QuantoVanillaOption(payoff, exercise) + gbm_vanilla = ql.BlackScholesMertonProcess(s_0, dividend_ts, r_domestic_ts, sigma_s) + quanto_engine = ql.QuantoEuropeanEngine(gbm_vanilla, r_foreign_ts, sigma_fx, rho) + quanto_option.setPricingEngine(quanto_engine) + + quanto_option_pv = quanto_option.NPV() + vanilla_option_pv = vanilla_option.NPV() + + message = """Failed to reproduce QuantoOption / EuropeanQuantoEngine NPV: + {quanto_pv} + by using the QuantoTermStructure as the dividend together with + VanillaOption / AnalyticEuropeanEngine: + {vanilla_pv} + """.format( + quanto_pv=quanto_option_pv, + vanilla_pv=vanilla_option_pv + ) + + self.assertAlmostEquals( + quanto_option_pv, + vanilla_option_pv, + delta=1e-12, + msg=message + ) + + def testLazyObject(self): + evaluationDate = ql.Settings.instance().evaluationDate + nodes = self.termStructure.nodes() + self.termStructure.freeze() + + ql.Settings.instance().evaluationDate = self.calendar.advance(evaluationDate, 100, ql.Days) + + # Check that dates and rates are unchanged + for i in range(len(self.termStructure.nodes())): + self.assertEqual(nodes[i][0], self.termStructure.nodes()[i][0]) + self.assertEqual(nodes[i][1], self.termStructure.nodes()[i][1]) + + self.termStructure.recalculate() + + # Check that dates have changed (except the reference, which is fixed) + for i in range(1, len(self.termStructure.nodes())): + self.assertNotEqual(nodes[i][0], self.termStructure.nodes()[i][0]) + + ql.Settings.instance().evaluationDate = evaluationDate + + self.termStructure.unfreeze() + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TermStructureTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/quantlib/test/volatilities.py b/quantlib/test/volatilities.py new file mode 100644 index 0000000..bf99c3b --- /dev/null +++ b/quantlib/test/volatilities.py @@ -0,0 +1,631 @@ +""" + Copyright (C) 2020 Marcin Rybacki + Copyright (C) 2022 Skandinaviska Enskilda Banken AB (publ) + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import unittest +import math +import mxdevtool as ql + + +TOLERANCE = 1.e-10 +SABR_ATM_TOLERANCE = 3.0e-4 +SABR_SPREAD_TOLERANCE = 12.0e-4 + +CAL = ql.TARGET() + +# Data source: +# https://quantlib-python-docs.readthedocs.io/en/latest/termstructures.html#swaption-volatility +ATM_NORM_VOLS = ( + (0.0086, 0.00128, 0.00195, 0.00269, 0.00327, 0.00361, 0.00387, + 0.00409, 0.00427, 0.00443, 0.00488, 0.00504, 0.00508, 0.00504), + (0.0092, 0.00134, 0.00197, 0.00264, 0.00319, 0.00352, 0.00383, + 0.00402, 0.00419, 0.00431, 0.00478, 0.00499, 0.00507, 0.00503), + (0.00112, 0.00153, 0.00210, 0.00276, 0.00327, 0.00353, 0.00384, + 0.00408, 0.00426, 0.00445, 0.00486, 0.00505, 0.00509, 0.00510), + (0.00129, 0.00171, 0.00226, 0.00288, 0.00335, 0.00360, 0.00388, + 0.00410, 0.00430, 0.00446, 0.00487, 0.00506, 0.00511, 0.00510), + (0.00146, 0.00187, 0.00246, 0.00301, 0.00342, 0.00369, 0.00393, + 0.00413, 0.00432, 0.00449, 0.00489, 0.00510, 0.00513, 0.00515), + (0.00165, 0.00209, 0.00263, 0.00313, 0.00350, 0.00376, 0.00400, + 0.00420, 0.00437, 0.00453, 0.00488, 0.00509, 0.00514, 0.00517), + (0.00209, 0.00253, 0.00300, 0.00340, 0.00370, 0.00395, 0.00419, + 0.00434, 0.00450, 0.00464, 0.00493, 0.00510, 0.00513, 0.00519), + (0.00251, 0.00289, 0.00332, 0.00362, 0.00392, 0.00412, 0.00432, + 0.00447, 0.00460, 0.00473, 0.00496, 0.00510, 0.00513, 0.00516), + (0.00340, 0.00366, 0.00392, 0.00411, 0.00432, 0.00445, 0.00461, + 0.00472, 0.00480, 0.00490, 0.00503, 0.00513, 0.00513, 0.00512), + (0.00403, 0.00418, 0.00436, 0.00449, 0.00461, 0.00471, 0.00482, + 0.00492, 0.00499, 0.00505, 0.00512, 0.00513, 0.00509, 0.00507), + (0.00440, 0.00448, 0.00460, 0.00471, 0.00484, 0.00491, 0.00499, + 0.00507, 0.00514, 0.00519, 0.00516, 0.00514, 0.00506, 0.00502), + (0.00496, 0.00497, 0.00504, 0.00512, 0.00518, 0.00522, 0.00526, + 0.00529, 0.00533, 0.00538, 0.00526, 0.00517, 0.00504, 0.00496), + (0.00539, 0.00537, 0.00540, 0.00542, 0.00544, 0.00545, 0.00545, + 0.00544, 0.00544, 0.00549, 0.00531, 0.00518, 0.00501, 0.00491), + (0.00540, 0.00537, 0.00538, 0.00537, 0.00535, 0.00536, 0.00535, + 0.00533, 0.00535, 0.00537, 0.00514, 0.00498, 0.00479, 0.00466), + (0.00528, 0.00524, 0.00526, 0.00523, 0.00522, 0.00523, 0.00520, + 0.00519, 0.00518, 0.00518, 0.00495, 0.00474, 0.00454, 0.00438), + (0.00514, 0.00512, 0.00513, 0.00510, 0.00508, 0.00507, 0.00503, + 0.00499, 0.00498, 0.00497, 0.00476, 0.00453, 0.00431, 0.00414), + (0.00496, 0.00496, 0.00497, 0.00495, 0.00495, 0.00492, 0.00486, + 0.00479, 0.00474, 0.00471, 0.00451, 0.00429, 0.00408, 0.00392)) + +ATM_NORM_VOL_OPT_TENORS = (ql.Period(1, ql.Months), ql.Period(2, ql.Months), + ql.Period(3, ql.Months), ql.Period(6, ql.Months), + ql.Period(9, ql.Months), ql.Period(1, ql.Years), + ql.Period(18, ql.Months), ql.Period(2, ql.Years), + ql.Period(3, ql.Years), ql.Period(4, ql.Years), + ql.Period(5, ql.Years), ql.Period(7, ql.Years), + ql.Period(10, ql.Years), ql.Period(15, ql.Years), + ql.Period(20, ql.Years), ql.Period(25, ql.Years), + ql.Period(30, ql.Years)) + +ATM_NORM_VOL_SWAP_TENORS = (ql.Period(1, ql.Years), ql.Period(2, ql.Years), + ql.Period(3, ql.Years), ql.Period(4, ql.Years), + ql.Period(5, ql.Years), ql.Period(6, ql.Years), + ql.Period(7, ql.Years), ql.Period(8, ql.Years), + ql.Period(9, ql.Years), ql.Period(10, ql.Years), + ql.Period(15, ql.Years), ql.Period(20, ql.Years), + ql.Period(25, ql.Years), ql.Period(30, ql.Years)) + +ATM_LOGNORM_VOLS = ( + (0.1300, 0.1560, 0.1390, 0.1220), + (0.1440, 0.1580, 0.1460, 0.1260), + (0.1600, 0.1590, 0.1470, 0.1290), + (0.1640, 0.1470, 0.1370, 0.1220), + (0.1400, 0.1300, 0.1250, 0.1100), + (0.1130, 0.1090, 0.1070, 0.0930)) + +ATM_LOGNORM_VOL_OPT_TENORS = (ql.Period(1, ql.Months), + ql.Period(6, ql.Months), + ql.Period(3, ql.Years), + ql.Period(5, ql.Years), + ql.Period(10, ql.Years), + ql.Period(25, ql.Years)) + +ATM_LOGNORM_VOL_SWAP_TENORS = (ql.Period(1, ql.Years), + ql.Period(3, ql.Years), + ql.Period(10, ql.Years), + ql.Period(25, ql.Years)) + +SMILE_OPT_TENORS = (ql.Period(1, ql.Years), + ql.Period(10, ql.Years), + ql.Period(30, ql.Years)) + +SMILE_SWAP_TENORS = (ql.Period(2, ql.Years), + ql.Period(10, ql.Years), + ql.Period(30, ql.Years)) + +STRIKE_SPREADS = (-0.02, -0.005, 0.0, 0.005, 0.02) + +NORM_VOL_SPREADS = ( + (-0.0006, 0.0005, 0.0, 0.0006, 0.0006), + (-0.0006, 0.0005, 0.0, 0.00065, 0.0006), + (-0.0006, 0.0001, 0.0, 0.0006, 0.0006), + (-0.0006, 0.0005, 0.0, 0.0006, 0.0006), + (-0.0006, 0.0005, 0.0, 0.0006, 0.0006), + (-0.0003, 0.0005, 0.0, 0.0003, 0.0003), + (-0.0006, 0.0005, 0.0, 0.0006, 0.0006), + (-0.0006, 0.0005, 0.0, 0.0006, 0.0006), + (-0.0003, 0.0005, 0.0, 0.0003, 0.0003)) + +LOGNORM_VOL_SPREADS = ( + (0.0599, 0.0049, 0.0000, -0.0001, 0.0127), + (0.0729, 0.0086, 0.0000, -0.0024, 0.0098), + (0.0738, 0.0102, 0.0000, -0.0039, 0.0065), + (0.0465, 0.0063, 0.0000, -0.0032, -0.0010), + (0.0558, 0.0084, 0.0000, -0.0050, -0.0057), + (0.0576, 0.0083, 0.0000, -0.0043, -0.0014), + (0.0437, 0.0059, 0.0000, -0.0030, -0.0006), + (0.0533, 0.0078, 0.0000, -0.0045, -0.0046), + (0.0545, 0.0079, 0.0000, -0.0042, -0.0020)) + +ZERO_COUPON_DATA = ( + (ql.Period(1, ql.Days), 0.013), + (ql.Period(1, ql.Years), 0.013), + (ql.Period(2, ql.Years), 0.015), + (ql.Period(3, ql.Years), 0.016), + (ql.Period(4, ql.Years), 0.017), + (ql.Period(5, ql.Years), 0.019), + (ql.Period(10, ql.Years), 0.021), + (ql.Period(15, ql.Years), 0.024), + (ql.Period(20, ql.Years), 0.026), + (ql.Period(30, ql.Years), 0.029)) + +NORM_VOL_MATRIX = ql.SwaptionVolatilityMatrix( + CAL, + ql.ModifiedFollowing, + ATM_NORM_VOL_OPT_TENORS, + ATM_NORM_VOL_SWAP_TENORS, + ql.Matrix(ATM_NORM_VOLS), + ql.Actual365Fixed(), + False, + ql.Normal) + +LOGNORM_VOL_MATRIX = ql.SwaptionVolatilityMatrix( + CAL, + ql.ModifiedFollowing, + ATM_LOGNORM_VOL_OPT_TENORS, + ATM_LOGNORM_VOL_SWAP_TENORS, + ql.Matrix(ATM_LOGNORM_VOLS), + ql.Actual365Fixed(), + False, + ql.ShiftedLognormal) + + +def build_euribor_swap_idx( + projection_curve_handle): + return ql.EuriborSwapIsdaFixA(ql.Period(1, ql.Years), + projection_curve_handle) + + +def build_nominal_term_structure(valuation_date, nominal_quotes): + dates, rates = zip(*[(CAL.advance(valuation_date, x[0]), x[1]) + for x in nominal_quotes]) + crv = ql.ZeroCurve(dates, rates, ql.Actual365Fixed()) + crv.enableExtrapolation() + return crv + + +def build_linear_swaption_cube( + volatility_matrix, + spread_opt_tenors, + spread_swap_tenors, + strike_spreads, + vol_spreads, + swap_index_base, + short_swap_index_base=None, + vega_weighted_smile_fit=False): + vol_spreads = [[ql.QuoteHandle(ql.SimpleQuote(v)) for v in row] + for row in vol_spreads] + cube = ql.SwaptionVolCube2( + ql.SwaptionVolatilityStructureHandle(volatility_matrix), + spread_opt_tenors, + spread_swap_tenors, + strike_spreads, + vol_spreads, + swap_index_base, + short_swap_index_base if short_swap_index_base else swap_index_base, + vega_weighted_smile_fit) + cube.enableExtrapolation() + return cube + + +def sabr_parameters_guess(number_of_options, number_of_swaps): + n_elements = number_of_options * number_of_swaps + guess = n_elements * [0] + for n in range(n_elements): + guess[n] = (ql.QuoteHandle(ql.SimpleQuote(0.2)), + ql.QuoteHandle(ql.SimpleQuote(0.5)), + ql.QuoteHandle(ql.SimpleQuote(0.4)), + ql.QuoteHandle(ql.SimpleQuote(0.0))) + return guess + + +def build_sabr_swaption_cube( + volatility_matrix, + spread_opt_tenors, + spread_swap_tenors, + strike_spreads, + vol_spreads, + swap_index_base, + short_swap_index_base=None, + vega_weighted_smile_fit=False, + is_parameter_fixed=(False, False, False, False), + is_atm_calibrated=True): + v_spreads = [[ql.QuoteHandle(ql.SimpleQuote(v)) for v in row] + for row in vol_spreads] + guess = sabr_parameters_guess( + len(spread_opt_tenors), len(spread_swap_tenors)) + cube = ql.SwaptionVolCube1( + ql.SwaptionVolatilityStructureHandle(volatility_matrix), + spread_opt_tenors, + spread_swap_tenors, + strike_spreads, + v_spreads, + swap_index_base, + short_swap_index_base if short_swap_index_base else swap_index_base, + vega_weighted_smile_fit, + guess, + is_parameter_fixed, + is_atm_calibrated) + cube.enableExtrapolation() + return cube + + +class SwaptionVolatilityCubeTest(unittest.TestCase): + def setUp(self): + self.today = CAL.adjust(ql.Date.todaysDate()) + ql.Settings.instance().evaluationDate = self.today + + curve_handle = ql.RelinkableYieldTermStructureHandle() + curve = build_nominal_term_structure(self.today, ZERO_COUPON_DATA) + curve_handle.linkTo(curve) + + self.idx = ql.Euribor6M(curve_handle) + self.swap_idx = build_euribor_swap_idx(curve_handle) + self.swap_engine = ql.DiscountingSwapEngine(curve_handle) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def _get_fair_rate(self, exercise_date, swap_tenor): + start_date = CAL.advance(exercise_date, ql.Period(2, ql.Days)) + underlying = ql.MakeVanillaSwap( + swap_tenor, self.idx, 0.0, ql.Period(0, ql.Days), + effectiveDate=start_date, + fixedLegTenor=ql.Period(1, ql.Years), + fixedLegDayCount=ql.Thirty360(ql.Thirty360.BondBasis), + floatingLegSpread=0.0, + swapType=ql.Swap.Receiver) + underlying.setPricingEngine(self.swap_engine) + return underlying.fairRate() + + def _assert_atm_strike( + self, cube, interpolation, vol_type): + opt_tenor = ql.Period(1, ql.Years) + swap_tenor = ql.Period(10, ql.Years) + exercise_date = cube.optionDateFromTenor(opt_tenor) + expected_atm_strike = self._get_fair_rate(exercise_date, swap_tenor) + actual_atm_strike = cube.atmStrike(exercise_date, swap_tenor) + fail_msg = """ ATM strike test failed for: + cube interpolation: {interpolation} + volatility_type: {vol_type} + option tenor: {option_tenor} + swap tenor: {swap_tenor} + strike: {strike} + replicated strike: {replicated_strike} + """.format(interpolation=interpolation, + vol_type=vol_type, + option_tenor=opt_tenor, + swap_tenor=swap_tenor, + strike=actual_atm_strike, + replicated_strike=expected_atm_strike) + self.assertAlmostEquals( + first=actual_atm_strike, + second=expected_atm_strike, + delta=TOLERANCE, + msg=fail_msg) + + def _assert_atm_vol( + self, + cube, + opt_tenor, + swap_tenor, + expected_vol, + interpolation, + vol_type, + epsilon=TOLERANCE): + option_date = cube.optionDateFromTenor(opt_tenor) + strike = cube.atmStrike(option_date, swap_tenor) + actual_vol = cube.volatility(option_date, swap_tenor, strike) + fail_msg = """ ATM vol test failed for: + cube interpolation: {interpolation} + volatility_type: {vol_type} + option tenor: {option_tenor} + swap tenor: {swap_tenor} + strike: {strike} + volatility: {vol} + expected volatility: {expected_vol} + epsilon: {eps} + """.format(interpolation=interpolation, + vol_type=vol_type, + option_tenor=opt_tenor, + swap_tenor=swap_tenor, + strike=strike, + vol=actual_vol, + expected_vol=expected_vol, + eps=epsilon) + self.assertAlmostEquals( + first=actual_vol, + second=expected_vol, + delta=epsilon, + msg=fail_msg) + + def _assert_vol_spread( + self, + cube, + opt_tenor, + swap_tenor, + strike_spread, + expected_vol, + interpolation, + vol_type, + epsilon=TOLERANCE): + option_date = cube.optionDateFromTenor(opt_tenor) + strike = cube.atmStrike(option_date, swap_tenor) + strike_spread + actual_vol = cube.volatility(option_date, swap_tenor, strike) + fail_msg = """ Vol spread test failed for: + cube interpolation: {interpolation} + volatility_type: {vol_type} + option tenor: {option_tenor} + swap tenor: {swap_tenor} + strike: {strike} + volatility: {vol} + expected volatility: {expected_vol} + epsilon: {eps} + """.format(interpolation=interpolation, + vol_type=vol_type, + option_tenor=opt_tenor, + swap_tenor=swap_tenor, + strike=strike, + vol=actual_vol, + expected_vol=expected_vol, + eps=epsilon) + self.assertAlmostEquals( + first=actual_vol, + second=expected_vol, + delta=epsilon, + msg=fail_msg) + + def test_linear_normal_cube_at_the_money_strike(self): + """Testing ATM strike for linearly interpolated normal vol cube""" + linear_cube = build_linear_swaption_cube( + NORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + NORM_VOL_SPREADS, + self.swap_idx) + self._assert_atm_strike( + cube=linear_cube, + interpolation='linear', + vol_type='normal') + + def test_linear_lognormal_cube_at_the_money_strike(self): + """Testing ATM strike for linearly interpolated log-normal vol cube""" + linear_cube = build_linear_swaption_cube( + LOGNORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + LOGNORM_VOL_SPREADS, + self.swap_idx) + self._assert_atm_strike( + cube=linear_cube, + interpolation='linear', + vol_type='log-normal') + + def test_sabr_lognormal_cube_at_the_money_strike(self): + """Testing ATM strike for SABR interpolated log-normal vol cube""" + sabr_cube = build_sabr_swaption_cube( + LOGNORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + LOGNORM_VOL_SPREADS, + self.swap_idx) + self._assert_atm_strike( + cube=sabr_cube, + interpolation='SABR', + vol_type='log-normal') + + def test_linear_normal_cube_at_the_money_vol(self): + """Testing ATM volatility for linearly interpolated normal vol cube""" + linear_cube = build_linear_swaption_cube( + NORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + NORM_VOL_SPREADS, + self.swap_idx) + self._assert_atm_vol( + cube=linear_cube, + opt_tenor=ql.Period(1, ql.Years), + swap_tenor=ql.Period(10, ql.Years), + expected_vol=0.00453, + interpolation='linear', + vol_type='normal') + + def test_linear_lognormal_cube_at_the_money_vol(self): + """Testing ATM volatility for linearly interpolated log-normal vol cube""" + linear_cube = build_linear_swaption_cube( + LOGNORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + LOGNORM_VOL_SPREADS, + self.swap_idx) + self._assert_atm_vol( + cube=linear_cube, + opt_tenor=ql.Period(10, ql.Years), + swap_tenor=ql.Period(10, ql.Years), + expected_vol=0.1250, + interpolation='linear', + vol_type='log-normal') + + def test_sabr_lognormal_cube_at_the_money_vol(self): + """Testing ATM volatility for SABR interpolated log-normal vol cube""" + sabr_cube = build_sabr_swaption_cube( + LOGNORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + LOGNORM_VOL_SPREADS, + self.swap_idx) + self._assert_atm_vol( + cube=sabr_cube, + opt_tenor=ql.Period(10, ql.Years), + swap_tenor=ql.Period(10, ql.Years), + expected_vol=0.1250, + interpolation='SABR', + vol_type='log-normal', + epsilon=SABR_ATM_TOLERANCE) + + def test_linear_normal_cube_spread_vol(self): + """Testing spread volatility for linearly interpolated normal cube""" + linear_cube = build_linear_swaption_cube( + NORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + NORM_VOL_SPREADS, + self.swap_idx) + self._assert_vol_spread( + cube=linear_cube, + opt_tenor=ql.Period(1, ql.Years), + swap_tenor=ql.Period(10, ql.Years), + strike_spread=-0.02, + expected_vol=0.00453 - 0.0006, + interpolation='linear', + vol_type='normal') + + def test_linear_lognormal_cube_spread_vol(self): + """Testing spread volatility for linearly interpolated log-normal cube""" + linear_cube = build_linear_swaption_cube( + LOGNORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + LOGNORM_VOL_SPREADS, + self.swap_idx) + self._assert_vol_spread( + cube=linear_cube, + opt_tenor=ql.Period(10, ql.Years), + swap_tenor=ql.Period(10, ql.Years), + strike_spread=-0.02, + expected_vol=0.125 + 0.0558, + interpolation='linear', + vol_type='log-normal') + + def test_sabr_lognormal_cube_spread_vol(self): + """Testing spread volatility for SABR interpolated log-normal cube""" + sabr_cube = build_sabr_swaption_cube( + LOGNORM_VOL_MATRIX, + SMILE_OPT_TENORS, + SMILE_SWAP_TENORS, + STRIKE_SPREADS, + LOGNORM_VOL_SPREADS, + self.swap_idx) + self._assert_vol_spread( + cube=sabr_cube, + opt_tenor=ql.Period(10, ql.Years), + swap_tenor=ql.Period(10, ql.Years), + strike_spread=-0.02, + expected_vol=0.125 + 0.0558, + interpolation='SABR', + vol_type='log-normal', + epsilon=SABR_SPREAD_TOLERANCE) + + +class SviSmileSectionTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = ql.Date(3, ql.May, 2022) + + def test_svi_smile_section(self): + """Testing the SviSmileSection against already fitted parameters""" + expiry_date = ql.Date(16, ql.December, 2022) + forward = 100 + atm_vol = 0.325819 + # parameters = a, b, sigma, rho, m + svi_parameters = [-0.651304, 0.986546, 0.838493, 0.520853, 0.695177] + + smile = ql.SviSmileSection(expiry_date, forward, svi_parameters) + + self.assertAlmostEqual(smile.volatility(forward), atm_vol, places=5) + self.assertAlmostEqual(smile.volatility(257.328), 0.739775, places=5) + + def test_svi_interpolated_smile_section(self): + """Testing the SviInterpolatedSmileSection's parameter fitting against given vols""" + expiry_date = ql.Date(16, ql.December, 2022) + forward = 100 + strikes = [25.6134, 48.5585, 71.5027, 94.4478, 117.3920, 140.3372, 163.2814, 186.2265, 209.1707, 232.1149] + has_floating_strikes = False + atm_vol = 0.325819 + vols = [0.881504, 0.627807, 0.456964, 0.343740, 0.297482, 0.321816, 0.390772, 0.476758, 0.565635, 0.651507] + + a = -0.6 + b = 0.9 + sigma = 0.8 + rho = 0.5 + m = 0.6 + + interpolated_smile = ql.SviInterpolatedSmileSection( + expiry_date, forward, strikes, has_floating_strikes, atm_vol, vols, + a, b, sigma, rho, m, + False, False, False, False, False + ) + + self.assertAlmostEqual(interpolated_smile.volatility(forward), atm_vol, places=5) + self.assertAlmostEqual(interpolated_smile.volatility(257.328), 0.739775, places=5) + + +class AndreasenHugeVolatilityTest(unittest.TestCase): + def testLocalVolCalibration(self): + """ Testing Andreasen-Huge Local Volatility calibration""" + + today = ql.Settings.instance().evaluationDate + + spot = ql.QuoteHandle(ql.SimpleQuote(100)) + + dc = ql.Actual365Fixed() + qTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.025, dc)) + rTS = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.05, dc)) + + vol_data = [ + # maturity in days, strike, volatility + (30, 75, 0.13), + (30, 100, 0.26), + (30, 125, 0.3), + (180, 80, 0.4), + (180, 150, 0.6), + (365, 110, 0.5)] + + calibration_set = ql.CalibrationSet( + [( + ql.VanillaOption( + ql.PlainVanillaPayoff(ql.Option.Call, strike), + ql.EuropeanExercise(today + ql.Period(maturity_in_days, ql.Days)) + ), + ql.SimpleQuote(volatility) + ) for maturity_in_days, strike, volatility in vol_data] + ) + + local_vol = ql.LocalVolTermStructureHandle( + ql.AndreasenHugeLocalVolAdapter( + ql.AndreasenHugeVolatilityInterpl(calibration_set, spot, rTS, qTS) + ) + ) + + option = calibration_set[-2][0] # maturity in days: 180, strike: 150, vol: 0.6 + + dummy_vol = ql.BlackVolTermStructureHandle() + local_vol_process = ql.GeneralizedBlackScholesProcess(spot, qTS, rTS, dummy_vol, local_vol) + + option.setPricingEngine(ql.MCEuropeanEngine( + local_vol_process, "lowdiscrepancy", + timeSteps=75, brownianBridge=True, requiredSamples=16000, seed=42) + ) + + t = dc.yearFraction(today, option.exercise().lastDate()) + fwd = spot.value() * qTS.discount(t) / rTS.discount(t) + vol = calibration_set[-2][1].value() + + expected = ql.BlackCalculator( + ql.as_plain_vanilla_payoff(option.payoff()), fwd, vol * math.sqrt(t), rTS.discount(t)).value() + + self.assertAlmostEqual(expected, option.NPV(), delta=0.2) + + +if __name__ == "__main__": + print("testing QuantLib " + ql.__version__) + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(SwaptionVolatilityCubeTest, "test")) + suite.addTest(unittest.makeSuite(SviSmileSectionTest, "test")) + suite.addTest(unittest.makeSuite(AndreasenHugeVolatilityTest, "test")) + unittest.TextTestRunner(verbosity=2).run(suite) From 3d568ac4a3a42fb551dec7b734bcb43199c3e199 Mon Sep 17 00:00:00 2001 From: minikie Date: Sat, 28 Jan 2023 22:48:05 +0900 Subject: [PATCH 13/54] working... --- README.md | 158 +++++++++++++++++++--------------------------- scenario/usage.py | 149 ++++++++++++++----------------------------- 2 files changed, 115 insertions(+), 192 deletions(-) diff --git a/README.md b/README.md index 6f75bbe..cc9ade1 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ MxDevTool(Beta) : Financial Library ![image](https://img.shields.io/badge/platform-windows_64|_linux_64-red) ![image](https://img.shields.io/badge/python-3.6|3.7|3.8|3.9|3.10|3.11-blue) -![image](https://img.shields.io/badge/version-0.8.38.0-green.svg) +![image](https://img.shields.io/badge/version-1.0.29.0-green.svg) ![image](https://img.shields.io/badge/platform-macOS_64-red) ![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) MxDevTool is a Integrated Developing Tools for financial analysis. -Now is Beta Release version. The Engine is developed by C++. +Now is Beta Release version. The Project is built on top of QuantLib-Python. Xenarix(Economic Scenario Generator) is moved into submodule of MxDevTool. @@ -29,6 +29,12 @@ Functionalty :
+# Version Rule + +Version syntex is {Major}-{Miner}-{QuantLib_Version}-{Patch} + +
+ # Installation To install MxDevTool, simply use pip : @@ -109,7 +115,7 @@ def test(): print('hw1f test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) rsg = xen.Rsg(sampleNum=5000) results = xen.generate1d(m, None, timeGrid, rsg, filename, False) @@ -134,8 +140,6 @@ import mxdevtool.termstructures as ts import mxdevtool.quotes as mx_q import mxdevtool.data.providers as mx_dp import mxdevtool.data.repositories as mx_dr -import mxdevtool.instruments as mx_i -import mxdevtool.instruments.outputs as mx_io import mxdevtool.utils as utils ``` @@ -326,22 +330,23 @@ fixedRateBond = xen.FixedRateBond('fixedRateBond', vasicek1f, notional=10000, fi ## TimeGrid ```python -timegrid1 = mx.TimeEqualGrid(refDate=ref_date, maxYear=3, nPerYear=365) -timegrid2 = mx.TimeArrayGrid(refDate=ref_date, times=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) -timeGrid3 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='day') -timeGrid4 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='week') -timeGrid5 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) -timeGrid6 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) -timeGrid7 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) -timeGrid8 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) -timeGrid9 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') -timeGrid10 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') -timeGrid11 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') -timeGrid12 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') -timeGrid13 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') -timeGrid14 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') -timeGrid15 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') -timeGrid16 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') +timegrid1 = mx.TimeDateGrid_Equal(refDate=ref_date, maxYear=3, nPerYear=365) +timegrid2 = mx.TimeDateGrid_Times(refDate=ref_date, times=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) +timegrid3 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='day') +timegrid4 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='week') +timegrid5 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) +timegrid6 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) +timegrid7 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) +timegrid8 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) +timegrid9 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') +timegrid10 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') +timegrid11 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') +timegrid12 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') +timegrid13 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') +timegrid14 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') +timegrid15 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') +timegrid16 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') + ``` ## Random Sequence Generator @@ -350,7 +355,6 @@ pseudo_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatc pseudo_rsg2 = xen.RsgPseudo(sampleNum=1000, dimension=365, randomTransformType='uniform') halton_rsg = xen.RsgHalton(sampleNum=1000, dimension=365) -faure_rsg = xen.RsgFaure(sampleNum=1000, dimension=365) sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=2048, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') sobol_rsg2 = xen.RsgSobol(sampleNum=1000, dimension=365, skip=2048) @@ -414,31 +418,25 @@ ndarray = results.toNumpyArr() # pre load all scenario data to ndarray t_pos = 264 scenCount = 15 -precalculated_tpos_15_264 = [617.9412514170386, 486.16360609386476, 257.91188339511893, 0.008636462100567083, 0.041740761012241466, 0.017748505256947305, 0.07747329523241774, 0.03993668004547873, 0.00886112680520279, 0.011046137001885281, 0.9707768360048887, 0.9977969060281185, 15.0, 0.0, 1104.1048575109035, -131.77764532317389, 300420.54714306304, 0.7867472918809246, 627.9412514170386, -607.9412514170386, 679.7353765587426, 0.0017801044961434816, 627.9412514170386, 607.9412514170386, 679.7353765587426, 561.7647740154896, 689.7353765587426, 689.7353765587426, 0.010504541340189934, 0.010504541340189934, 0.022027700532335222, 0.022027700532335222, -0.01896096586180629, -0.01896096586180629, -0.031324320207885586, -0.031324320207885586, -0.3859240871544787, -0.3859240871544787, 0.14629049402642338, 0.14629049402642338, 8034.953047634017] - calculated_tpos_264 = results.tPosSlice(t_pos, scenCount) -for i, v in enumerate(zip(calculated_tpos_264, precalculated_tpos_15_264)): - diff = v[0] - v[1] - if diff != 0.0: print(results.genInfo[i][1], diff) - multipath = results[scenCount] multipath_arr = ndarray[scenCount] # t_pos data -multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (617.9412514170386, 486.16360609386476, 257.91188339511893, ...) +multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) multipath_t_pos_arr = ndarray[scenCount,:,t_pos] multipath_all_t_pos = results.tPosSlice(t_pos=t_pos) # all t_pos data # t_pos data of using date t_date = ref_date + 10 -multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) # (398.5270922971255, 418.0131595105432, 424.67199724665664, 0.014226265796137581, ... ) +multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) multipath_all_using_date = results.dateSlice(date=t_date) # all t_pos data # t_pos data of using time t_time = 1.32 -multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) # (381.7433430556487, 434.9766465395624, 368.76856978958483, -0.0022718317886738877, ... ) +multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) multipath_all_using_time = results.timeSlice(time=t_time) # all t_pos data ``` @@ -532,42 +530,42 @@ sb.corr[0][1] = 0.5 sb.corr[0][2] = 'kospi2_ni225_corr' sb.corr[2][0] = 'kospi2_ni225_corr' -sb.addCalc(xen.SpotRate.__name__, 'hw1f_spot3m', ir_pc='hw1f', maturityTenor='3m', compounding=mx.Compounded) -sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m', ir_pc='hw1f', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) -sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m_2', ir_pc='hw1f', startTenor=0.5, maturityTenor=0.25, compounding=mx.Compounded) -sb.addCalc(xen.DiscountFactor.__name__, 'hw1f_discountFactor', ir_pc='hw1f') -sb.addCalc(xen.DiscountBond.__name__, 'hw1f_discountBond3m', ir_pc='hw1f', maturityTenor=mx.Period(3, mx.Months)) +sb.addCalc(xen.SpotRate.__name__, 'hw1f_spot3m', ir_pv='hw1f', maturityTenor='3m', compounding=mx.Compounded) +sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m', ir_pv='hw1f', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) +sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m_2', ir_pv='hw1f', startTenor=0.5, maturityTenor=0.25, compounding=mx.Compounded) +sb.addCalc(xen.DiscountFactor.__name__, 'hw1f_discountFactor', ir_pv='hw1f') +sb.addCalc(xen.DiscountBond.__name__, 'hw1f_discountBond3m', ir_pv='hw1f', maturityTenor=mx.Period(3, mx.Months)) sb.addCalc(xen.ConstantValue.__name__, 'constantValue', v=15) sb.addCalc(xen.ConstantArray.__name__, 'constantArr', arr=[15,14,13]) -sb.addCalc(xen.AdditionOper.__name__, 'addOper1', pc1='gbmconst', pc2='gbm') -sb.addCalc(xen.SubtractionOper.__name__, 'subtOper1', pc1='gbmconst', pc2='gbm') -sb.addCalc(xen.MultiplicationOper.__name__, 'multiple_gbmconst_gbm', pc1='gbmconst', pc2='gbm') -sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pc1='gbmconst', pc2='gbm') +sb.addCalc(xen.AdditionOper.__name__, 'addOper1', pv1='gbmconst', pv2='gbm') +sb.addCalc(xen.SubtractionOper.__name__, 'subtOper1', pv1='gbmconst', pv2='gbm') +sb.addCalc(xen.MultiplicationOper.__name__, 'multiple_gbmconst_gbm', pv1='gbmconst', pv2='gbm') +sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pv1='gbmconst', pv2='gbm') -sb.addCalc(xen.AdditionOper.__name__, 'addOper2', pc1='gbmconst', pc2=10) -sb.addCalc(xen.SubtractionOper.__name__, 'subtOper2', pc1='gbmconst', pc2=10) -sb.addCalc(xen.MultiplicationOper.__name__, 'mulOper2', pc1='gbmconst', pc2=1.1) -sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pc1='gbmconst', pc2=1.1) +sb.addCalc(xen.AdditionConstOper.__name__, 'addOper2', pv1='gbmconst', v=10) +sb.addCalc(xen.SubtractionConstOper.__name__, 'subtOper2', pv1='gbmconst', v=10) +sb.addCalc(xen.MultiplicationConstOper.__name__, 'mulOper2', pv1='gbmconst', v=1.1) +sb.addCalc(xen.DivisionConstOper.__name__, 'divOper1', pv1='gbmconst', v=1.1) -sb.addCalc(xen.AdditionOper.__name__, 'addOper2', pc1=10, pc2='gbmconst') -sb.addCalc(xen.SubtractionOper.__name__, 'subtOper2', pc1=10, pc2='gbmconst') -sb.addCalc(xen.MultiplicationOper.__name__, 'mulOper2', pc1=1.1, pc2='gbmconst') -sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pc1=1.1, pc2='gbmconst') +sb.addCalc(xen.AdditionConstReverseOper.__name__, 'addOper2', v=10, pv2='gbmconst') +sb.addCalc(xen.SubtractionConstReverseOper.__name__, 'subtOper2', v=10, pv2='gbmconst') +sb.addCalc(xen.MultiplicationConstReverseOper.__name__, 'mulOper2', v=1.1, pv2='gbmconst') +sb.addCalc(xen.DivisionConstReverseOper.__name__, 'divOper1', v=1.1, pv2='gbmconst') -sb.addCalc(xen.LinearOper.__name__, 'linearOper1', pc='gbm', multiple=1.1, spread=10) -sb.addCalc(xen.Shift.__name__, 'shiftRight1', pc='hw1f', shift=5, fill_value=0.0) -sb.addCalc(xen.Shift.__name__, 'shiftLeft1', pc='cir1f', shift=-5, fill_value=0.0) +sb.addCalc(xen.LinearOper.__name__, 'linearOper1', pv='gbm', multiple=1.1, spread=10) +sb.addCalc(xen.Shift.__name__, 'shiftRight1', pv='hw1f', shift=5, fill_value=0.0) +sb.addCalc(xen.Shift.__name__, 'shiftLeft1', pv='cir1f', shift=-5, fill_value=0.0) -sb.addCalc(xen.Returns.__name__, 'returns1', pc='gbm', return_type='return') -sb.addCalc(xen.Returns.__name__, 'logreturns1', pc='gbmconst', return_type='logreturn') -sb.addCalc(xen.Returns.__name__, 'cumreturns1', pc='heston', return_type='cumreturn') -sb.addCalc(xen.Returns.__name__, 'cumlogreturns1', pc='gbm', return_type='cumlogreturn') +sb.addCalc(xen.Returns.__name__, 'returns1', pv='gbm', return_type='return') +sb.addCalc(xen.Returns.__name__, 'logreturns1', pv='gbmconst', return_type='logreturn') +sb.addCalc(xen.Returns.__name__, 'cumreturns1', pv='heston', return_type='cumreturn') +sb.addCalc(xen.Returns.__name__, 'cumlogreturns1', pv='gbm', return_type='cumlogreturn') -sb.addCalc(xen.FixedRateBond.__name__, 'fixedRateBond', ir_pc='vasicek1f', notional=10000, fixedRate=0.0, couponTenor=mx.Period(3, mx.Months), maturityTenor=mx.Period(3, mx.Years), discountCurve=rfCurve) +sb.addCalc(xen.FixedRateBond.__name__, 'fixedRateBond', ir_pv='vasicek1f', notional=10000, fixedRate=0.0, couponTenor=mx.Period(3, mx.Months), maturityTenor=mx.Period(3, mx.Years), discountCurve=rfCurve) -sb.addCalc(xen.AdditionOper.__name__, 'addOper_for_remove', pc1='gbmconst', pc2='gbm') +sb.addCalc(xen.AdditionOper.__name__, 'addOper_for_remove', pv1='gbmconst', pv2='gbm') sb.removeCalc('addOper_for_remove') # scenarioBuilder - save, load, list @@ -578,7 +576,7 @@ sb.setRsgCls(pseudo_rsg) xm.save_xnb('sb2', sb=sb) -sb.setTimeGrid(mx.TimeGrid.__name__, refDate=ref_date, maxYear=10, frequency_type='endofmonth') +sb.setTimeGrid(mx.TimeDateGrid_Custom.__name__, refDate=ref_date, maxYear=10, frequency_type='endofmonth') sb.setRsg(xen.Rsg.__name__, sampleNum=1000) xm.save_xnb('sb3', sb=sb) @@ -729,43 +727,10 @@ try: mx_dp.check_bloomberg() except: print('fail to check bloomberg') ``` -## Instruments Pricing -```python -# this is built-in instruments -# option1 = mx_i.EuropeanOption(option_type='c', strike=400, maturityDate=ref_date + 365) - -# this is inherit instrument for user output -class EuropeanOptionForUserOutput(mx_i.EuropeanOption): - def userfunc_test(self, scen_data_d, calc_kwargs): - v = calc_kwargs['calc_arg1'] - return v + 99 - -option = EuropeanOptionForUserOutput(option_type='c', strike=400, maturityDate=ref_date + 365) - -# outputs -delta = mx_io.Delta(up='s_up', down='s_down') -gamma = mx_io.Gamma(up='s_up', center='basescen', down='s_down') - -npv = mx_io.Npv(scen='basescen', currency='krw') -discount_cf = mx_io.CashFlow(scen='basescen', currency='krw', discount=None) -test_output = mx_io.UserFunc(scen='basescen', userfunc=option.userfunc_test, abc=10) - -# calculate from scenario -results1 = option.calculateScen(outputs=[npv, discount_cf, delta, gamma, test_output], shm=shm, reduce='aver', - path_kwargs={'s1': 'gbmconst', 'discount': 'hw1f_discountFactor'}, - calc_kwargs={'calc_arg1': 10}) - -# calculate from model -basescen = shm.getScenario('basescen') -gbmconst_basescen = basescen.getModel('gbmconst') -arg_d = { 'x0': gbmconst_basescen._x0, 'rf': gbmconst_basescen._rf, 'div': gbmconst_basescen._div, 'vol': gbmconst_basescen._vol } -assert option.setPricingParams_GBMConst(**arg_d).NPV() == option.setPricingParams_Model(gbmconst_basescen).NPV() -``` - ## Settings ```python # calendar holiday -mydates = [mx.Date(2022, 10, 11), mx.Date(2022, 10, 12), mx.Date(2022, 10, 13), mx.Date(2022, 11, 11)] +mydates = [mx.Date(11, 10, 2022), mx.Date(12, 10, 2022), mx.Date(13, 10, 2022), mx.Date(11, 11, 2022)] kr_cal = mx.SouthKorea() user_cal = mx.UserCalendar('testcal') @@ -832,6 +797,15 @@ For source code, check this repository. # Release History +## 1.0.29.0 (2023-01-28) +- QuantLib dependency is redegined +- Version Syntex is changed +- Instruments pricings are removed for reconstruction +- Faure Random is removed +- TimeGrid is replaced by TimeDateGrid_Custom (because of QuantLib.TimeGrid) +- Some arguments are changed (ex: pc -> pv in ProcessValue Operator) +- 'test' branch is added for CI/CD Testing + ## 0.8.38.0 (2022-11-07) - Rsg classes are redesigned - Latin Hypercube sampling is added diff --git a/scenario/usage.py b/scenario/usage.py index 3a97c9f..61ce200 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -7,8 +7,6 @@ import mxdevtool.quotes as mx_q import mxdevtool.data.providers as mx_dp import mxdevtool.data.repositories as mx_dr -import mxdevtool.instruments as mx_i -import mxdevtool.instruments.outputs as mx_io import mxdevtool.utils as utils @@ -137,29 +135,28 @@ def test(): # timegrid maxYear = 10 - timegrid1 = mx.TimeEqualGrid(refDate=ref_date, maxYear=3, nPerYear=365) - timegrid2 = mx.TimeArrayGrid(refDate=ref_date, times=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) - timegrid3 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='day') - timegrid4 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='week') - timegrid5 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) - timegrid6 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) - timegrid7 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) - timegrid8 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) - timegrid9 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') - timegrid10 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') - timegrid11 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') - timegrid12 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') - timegrid13 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') - timegrid14 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') - timegrid15 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') - timegrid16 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') + timegrid1 = mx.TimeDateGrid_Equal(refDate=ref_date, maxYear=3, nPerYear=365) + timegrid2 = mx.TimeDateGrid_Times(refDate=ref_date, times=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) + timegrid3 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='day') + timegrid4 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='week') + timegrid5 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) + timegrid6 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) + timegrid7 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) + timegrid8 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) + timegrid9 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') + timegrid10 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') + timegrid11 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') + timegrid12 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') + timegrid13 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') + timegrid14 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') + timegrid15 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') + timegrid16 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') # random pseudo_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='pseudo', subType='mersennetwister', randomTransformType='boxmullernormal') pseudo_rsg2 = xen.RsgPseudo(sampleNum=1000, dimension=365, randomTransformType='uniform') halton_rsg = xen.RsgHalton(sampleNum=1000, dimension=365) - faure_rsg = xen.RsgFaure(sampleNum=1000, dimension=365) sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=2048, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal') sobol_rsg2 = xen.RsgSobol(sampleNum=1000, dimension=365, skip=2048) @@ -170,7 +167,7 @@ def test(): np.save('./external_rsg.npy', arr) external_rsg = xen.RsgExternal(sampleNum=1000, dimension=365 * 3, filename='./external_rsg.npy') - rsg_list = [pseudo_rsg, pseudo_rsg2, halton_rsg, faure_rsg, sobol_rsg, sobol_rsg2, latinhs_rsg, external_rsg] + rsg_list = [pseudo_rsg, pseudo_rsg2, halton_rsg, sobol_rsg, sobol_rsg2, latinhs_rsg, external_rsg] # for rsg in rsg_list: # print(rsg.type(), rsg.nextSequence()[0:3], rsg.nextSequence()[0:3]) @@ -215,40 +212,25 @@ def test(): t_pos = 264 scenCount = 15 - precalculated_tpos_15_264 = [] - - if enviroment == 'Windows-AMD64': - precalculated_tpos_15_264 = [617.9524151122586, 486.1324942886278, 257.86113224359457, 0.00863805143945109, 0.04174084373279414, 0.01774797817939525, 0.07746550423520306, 0.03993400910000309, 0.008862700536786061, 0.011047337489246356, 0.9707690335980336, 0.9977965169103966, 15.0, 0.0, 1104.0849094008863, -131.81992082363075, 300406.7489102038, 0.7866827321976174, 627.9524151122586, -607.9524151122586, 679.7476566234844, 0.001780072337447167, 627.9524151122586, 607.9524151122586, 679.7476566234844, 561.7749228293259, 689.7476566234844, 689.7476566234844, 0.010506153704625517, 0.010506153704625517, 0.02202363630292186, 0.02202363630292186, -0.01896096586180629, -0.01896096586180629, -0.03132432020788547, -0.03132432020788547, -0.38604492322953676, -0.38604492322953676, 0.1462264974615181, 0.1462264974615181, 8035.14085071921] - elif enviroment == 'Linux-x86_64': - precalculated_tpos_15_264 = [617.9357218168522, 486.1381428609564, 257.9051759086099, 0.008637296682977602, 0.041740750179767666, 0.017748170111667804, 0.0774710888641746, 0.03993729011265557, 0.008861953191711569, 0.011046767393152468, 0.9707731058865956, 0.9977967016973495, 15.0, 0.0, 1104.0738646778086, -131.79757895589574, 300402.1242114891, 0.7867131251639167, 627.9357218168522, -607.9357218168522, 679.7292939985374, 0.001780120425415421, 627.9357218168522, 607.9357218168522, 679.7292939985374, 561.7597471062292, 689.7292939985374, 689.7292939985374, 0.010505388013615481, 0.010505388013615481, 0.022026453783620698, 0.022026453783620698, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38594005736045256, -0.38594005736045256, 0.14623811680311463, 0.14623811680311463, 8035.006232005751] - elif enviroment == 'Linux-aarch64': - precalculated_tpos_15_264 = [617.9357218168448, 486.13814286095663, 257.90517590861185, 0.008637296682977559, 0.04174075017756056, 0.017748170111667617, 0.0774710888641739, 0.03993729011265639, 0.008861953191711569, 0.011046767393152468, 0.9707731058865953, 0.9977967016973495, 15.0, 0.0, 1104.0738646778013, -131.79757895588813, 300402.1242114856, 0.7867131251639264, 627.9357218168448, -607.9357218168448, 679.7292939985293, 0.0017801204254154423, 627.9357218168448, 607.9357218168448, 679.7292939985293, 561.7597471062224, 689.7292939985293, 689.7292939985293, 0.010505388013615441, 0.010505388013615441, 0.022026453783620507, 0.022026453783620507, -0.018960965861806955, -0.018960965861806955, -0.0313243202078865, -0.0313243202078865, -0.385940057360448, -0.385940057360448, 0.14623811680311521, 0.14623811680311521, 8035.006232005768] - elif enviroment == 'Darwin-arm64': - precalculated_tpos_15_264 = [617.9365212878336, 486.1307570137023, 257.89830088440857, 0.008637497400253921, 0.04174075631357103, 0.017748044660834593, 0.07747048265598006, 0.03993634981174748, 0.008862151937972573, 0.011046919002466904, 0.9707722172500804, 0.9977966525557552, 15.0, 0.0, 1104.067278301536, -131.80576427413126, 300397.9488800683, 0.7867001548970814, 627.9365212878336, -607.9365212878336, 679.7301734166169, 0.0017801181223396608, 627.9365212878336, 607.9365212878336, 679.7301734166169, 561.7604738980305, 689.7301734166169, 689.7301734166169, 0.010505591638783906, 0.010505591638783906, 0.0220260230020498, 0.0220260230020498, -0.018960965861806733, -0.018960965861806733, -0.03132432020788627, -0.03132432020788627, -0.38595642646569384, -0.38595642646569384, 0.14622292378962792, 0.14622292378962792, 8035.020844678902] - calculated_tpos_264 = results.tPosSlice(t_pos, scenCount) - for i, v in enumerate(zip(calculated_tpos_264, precalculated_tpos_15_264)): - diff = v[0] - v[1] - if diff != 0.0: print(results.genInfo[i][1], diff) - multipath = results[scenCount] multipath_arr = ndarray[scenCount] # t_pos data - multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) # (617.9524151122586, 486.1324942886278, 257.86113224359457, ...) + multipath_t_pos = results.tPosSlice(t_pos=t_pos, scenCount=scenCount) multipath_t_pos_arr = ndarray[scenCount,:,t_pos] multipath_all_t_pos = results.tPosSlice(t_pos=t_pos) # all t_pos data # t_pos data of using date t_date = ref_date + 10 - multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) # (398.5270922971255, 418.0131595105432, 424.67199724665664, 0.014226265796137581, ... ) + multipath_using_date = results.dateSlice(date=t_date, scenCount=scenCount) multipath_all_using_date = results.dateSlice(date=t_date) # all t_pos data # t_pos data of using time t_time = 1.32 - multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) # (381.7433430556487, 434.9766465395624, 368.76856978958483, -0.0022718317886738877, ... ) + multipath_using_time = results.timeSlice(time=t_time, scenCount=scenCount) multipath_all_using_time = results.timeSlice(time=t_time) # all t_pos data # analyticPath and test calculation @@ -260,8 +242,7 @@ def test(): analyticPath = pv.analyticPath(timegrid2) input_arr = [0.01, 0.02, 0.03, 0.04, 0.05] - input_arr2d = [[0.01, 0.02, 0.03, 0.04, 0.05], - [0.06, 0.07, 0.08, 0.09, 0.1]] + input_arr2d = [[0.01, 0.02, 0.03, 0.04, 0.05], [0.06, 0.07, 0.08, 0.09, 0.1]] for pv in all_calcs: if pv.sourceNum == 1: @@ -323,42 +304,42 @@ def test(): sb.corr[0][2] = 'kospi2_ni225_corr' sb.corr[2][0] = 'kospi2_ni225_corr' - sb.addCalc(xen.SpotRate.__name__, 'hw1f_spot3m', ir_pc='hw1f', maturityTenor='3m', compounding=mx.Compounded) - sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m', ir_pc='hw1f', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) - sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m_2', ir_pc='hw1f', startTenor=0.5, maturityTenor=0.25, compounding=mx.Compounded) - sb.addCalc(xen.DiscountFactor.__name__, 'hw1f_discountFactor', ir_pc='hw1f') - sb.addCalc(xen.DiscountBond.__name__, 'hw1f_discountBond3m', ir_pc='hw1f', maturityTenor=mx.Period(3, mx.Months)) + sb.addCalc(xen.SpotRate.__name__, 'hw1f_spot3m', ir_pv='hw1f', maturityTenor='3m', compounding=mx.Compounded) + sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m', ir_pv='hw1f', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) + sb.addCalc(xen.ForwardRate.__name__, 'hw1f_forward6m3m_2', ir_pv='hw1f', startTenor=0.5, maturityTenor=0.25, compounding=mx.Compounded) + sb.addCalc(xen.DiscountFactor.__name__, 'hw1f_discountFactor', ir_pv='hw1f') + sb.addCalc(xen.DiscountBond.__name__, 'hw1f_discountBond3m', ir_pv='hw1f', maturityTenor=mx.Period(3, mx.Months)) sb.addCalc(xen.ConstantValue.__name__, 'constantValue', v=15) sb.addCalc(xen.ConstantArray.__name__, 'constantArr', arr=[15,14,13]) - sb.addCalc(xen.AdditionOper.__name__, 'addOper1', pc1='gbmconst', pc2='gbm') - sb.addCalc(xen.SubtractionOper.__name__, 'subtOper1', pc1='gbmconst', pc2='gbm') - sb.addCalc(xen.MultiplicationOper.__name__, 'multiple_gbmconst_gbm', pc1='gbmconst', pc2='gbm') - sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pc1='gbmconst', pc2='gbm') + sb.addCalc(xen.AdditionOper.__name__, 'addOper1', pv1='gbmconst', pv2='gbm') + sb.addCalc(xen.SubtractionOper.__name__, 'subtOper1', pv1='gbmconst', pv2='gbm') + sb.addCalc(xen.MultiplicationOper.__name__, 'multiple_gbmconst_gbm', pv1='gbmconst', pv2='gbm') + sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pv1='gbmconst', pv2='gbm') - sb.addCalc(xen.AdditionOper.__name__, 'addOper2', pc1='gbmconst', pc2=10) - sb.addCalc(xen.SubtractionOper.__name__, 'subtOper2', pc1='gbmconst', pc2=10) - sb.addCalc(xen.MultiplicationOper.__name__, 'mulOper2', pc1='gbmconst', pc2=1.1) - sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pc1='gbmconst', pc2=1.1) + sb.addCalc(xen.AdditionConstOper.__name__, 'addOper2', pv1='gbmconst', v=10) + sb.addCalc(xen.SubtractionConstOper.__name__, 'subtOper2', pv1='gbmconst', v=10) + sb.addCalc(xen.MultiplicationConstOper.__name__, 'mulOper2', pv1='gbmconst', v=1.1) + sb.addCalc(xen.DivisionConstOper.__name__, 'divOper1', pv1='gbmconst', v=1.1) - sb.addCalc(xen.AdditionOper.__name__, 'addOper2', pc1=10, pc2='gbmconst') - sb.addCalc(xen.SubtractionOper.__name__, 'subtOper2', pc1=10, pc2='gbmconst') - sb.addCalc(xen.MultiplicationOper.__name__, 'mulOper2', pc1=1.1, pc2='gbmconst') - sb.addCalc(xen.DivisionOper.__name__, 'divOper1', pc1=1.1, pc2='gbmconst') + sb.addCalc(xen.AdditionConstReverseOper.__name__, 'addOper2', v=10, pv2='gbmconst') + sb.addCalc(xen.SubtractionConstReverseOper.__name__, 'subtOper2', v=10, pv2='gbmconst') + sb.addCalc(xen.MultiplicationConstReverseOper.__name__, 'mulOper2', v=1.1, pv2='gbmconst') + sb.addCalc(xen.DivisionConstReverseOper.__name__, 'divOper1', v=1.1, pv2='gbmconst') - sb.addCalc(xen.LinearOper.__name__, 'linearOper1', pc='gbm', multiple=1.1, spread=10) - sb.addCalc(xen.Shift.__name__, 'shiftRight1', pc='hw1f', shift=5, fill_value=0.0) - sb.addCalc(xen.Shift.__name__, 'shiftLeft1', pc='cir1f', shift=-5, fill_value=0.0) + sb.addCalc(xen.LinearOper.__name__, 'linearOper1', pv='gbm', multiple=1.1, spread=10) + sb.addCalc(xen.Shift.__name__, 'shiftRight1', pv='hw1f', shift=5, fill_value=0.0) + sb.addCalc(xen.Shift.__name__, 'shiftLeft1', pv='cir1f', shift=-5, fill_value=0.0) - sb.addCalc(xen.Returns.__name__, 'returns1', pc='gbm', return_type='return') - sb.addCalc(xen.Returns.__name__, 'logreturns1', pc='gbmconst', return_type='logreturn') - sb.addCalc(xen.Returns.__name__, 'cumreturns1', pc='heston', return_type='cumreturn') - sb.addCalc(xen.Returns.__name__, 'cumlogreturns1', pc='gbm', return_type='cumlogreturn') + sb.addCalc(xen.Returns.__name__, 'returns1', pv='gbm', return_type='return') + sb.addCalc(xen.Returns.__name__, 'logreturns1', pv='gbmconst', return_type='logreturn') + sb.addCalc(xen.Returns.__name__, 'cumreturns1', pv='heston', return_type='cumreturn') + sb.addCalc(xen.Returns.__name__, 'cumlogreturns1', pv='gbm', return_type='cumlogreturn') - sb.addCalc(xen.FixedRateBond.__name__, 'fixedRateBond', ir_pc='vasicek1f', notional=10000, fixedRate=0.0, couponTenor=mx.Period(3, mx.Months), maturityTenor=mx.Period(3, mx.Years), discountCurve=rfCurve) + sb.addCalc(xen.FixedRateBond.__name__, 'fixedRateBond', ir_pv='vasicek1f', notional=10000, fixedRate=0.0, couponTenor=mx.Period(3, mx.Months), maturityTenor=mx.Period(3, mx.Years), discountCurve=rfCurve) - sb.addCalc(xen.AdditionOper.__name__, 'addOper_for_remove', pc1='gbmconst', pc2='gbm') + sb.addCalc(xen.AdditionOper.__name__, 'addOper_for_remove', pv1='gbmconst', pv2='gbm') sb.removeCalc('addOper_for_remove') # scenarioBuilder - save, load, list @@ -373,7 +354,7 @@ def test(): xm.save_xnb('sb2', sb=sb) - sb.setTimeGrid(mx.TimeGrid.__name__, refDate=ref_date, maxYear=10, frequency_type='endofmonth') + sb.setTimeGrid(mx.TimeDateGrid_Custom.__name__, refDate=ref_date, maxYear=10, frequency_type='endofmonth') sb.setRsg(xen.Rsg.__name__, sampleNum=1000) xm.save_xnb('sb3', sb=sb) @@ -507,40 +488,8 @@ def test(): xm.save_xen(name, scen) res = scen.generate_clone(filename=name) - # instruments pricing - - # this is built-in instruments - # option1 = mx_i.EuropeanOption(option_type='c', strike=400, maturityDate=ref_date + 365) - - # this is inherit instrument for user output - class EuropeanOptionForUserOutput(mx_i.EuropeanOption): - def userfunc_test(self, scen_data_d, calc_kwargs): - v = calc_kwargs['calc_arg1'] - return v + 99 - - option = EuropeanOptionForUserOutput(option_type='c', strike=400, maturityDate=ref_date + 365) - - # outputs - delta = mx_io.Delta(up='s_up', down='s_down') - gamma = mx_io.Gamma(up='s_up', center='basescen', down='s_down') - - npv = mx_io.Npv(scen='basescen', currency='krw') - discount_cf = mx_io.CashFlow(scen='basescen', currency='krw', discount=None) - test_output = mx_io.UserFunc(scen='basescen', userfunc=option.userfunc_test, abc=10) - - # calculate from scenario - results1 = option.calculateScen(outputs=[npv, discount_cf, delta, gamma, test_output], shm=shm, reduce='aver', - path_kwargs={'s1': 'gbmconst', 'discount': 'hw1f_discountFactor'}, - calc_kwargs={'calc_arg1': 10}) - - # calculate from model - basescen = shm.getScenario('basescen') - gbmconst_basescen = basescen.getModel('gbmconst') - arg_d = { 'x0': gbmconst_basescen._x0, 'rf': gbmconst_basescen._rf, 'div': gbmconst_basescen._div, 'vol': gbmconst_basescen._vol } - assert option.setPricingParams_GBMConst(**arg_d).NPV() == option.setPricingParams_Model(gbmconst_basescen).NPV() - # calendar holiday - mydates = [mx.Date(2022, 10, 11), mx.Date(2022, 10, 12), mx.Date(2022, 10, 13), mx.Date(2022, 11, 11)] + mydates = [mx.Date(11, 10, 2022), mx.Date(12, 10, 2022), mx.Date(13, 10, 2022), mx.Date(11, 11, 2022)] kr_cal = mx.SouthKorea() user_cal = mx.UserCalendar('testcal') From 68f1c169c2e36f74d3b04ba99d4eca3b18e97370 Mon Sep 17 00:00:00 2001 From: minikie Date: Sat, 28 Jan 2023 23:59:30 +0900 Subject: [PATCH 14/54] actions --- .github/workflows/mxdevtool-package.yml | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/mxdevtool-package.yml diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml new file mode 100644 index 0000000..24a1c70 --- /dev/null +++ b/.github/workflows/mxdevtool-package.yml @@ -0,0 +1,50 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: mxdevtool package test + +on: + push: + branches: [ "master", "test" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["windows-2019", "ubuntu-20.04"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + include: + - os: "macos-11" + python-version: ["3.8", "3.9", "3.10", "3.11"] + + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 numpy mxdevtool + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + # You can test your matrix by printing the current Python version + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - name: Display package version + run: pip show mxdevtool + - name: Test + run: | + python run_test_usage.py + python run_test_random.py From d4a133dd69bb3a1368bb1df13e4447fcbe39cd6e Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:01:52 +0900 Subject: [PATCH 15/54] working... --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 24a1c70..7155afb 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-2019", "ubuntu-20.04"] + os: ["windows-2019", "ubuntu-20.04", "macos-11"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] include: - os: "macos-11" From 7b3ab9cd61108ff83afff188a4043004bc6e9d16 Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:08:35 +0900 Subject: [PATCH 16/54] working... --- .github/workflows/mxdevtool-package.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 7155afb..b2b3d1a 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -21,7 +21,6 @@ jobs: - os: "macos-11" python-version: ["3.8", "3.9", "3.10", "3.11"] - steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} @@ -32,7 +31,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 numpy mxdevtool + python -m pip install flake8 numpy + python -m pip install --index-url https://test.pypi.org/simple mxdevtool -U - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names @@ -46,5 +46,5 @@ jobs: run: pip show mxdevtool - name: Test run: | - python run_test_usage.py - python run_test_random.py + python run_test_usage.py || exit 1 + python run_test_random.py || exit 1 From 69c654191b208f65d753cd86306ce495989d5a10 Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:11:04 +0900 Subject: [PATCH 17/54] working... --- run_test_etc.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/run_test_etc.py b/run_test_etc.py index 862c295..ac41182 100644 --- a/run_test_etc.py +++ b/run_test_etc.py @@ -9,28 +9,27 @@ def timegrid_test(): maxYear = 10 # TimeGrid - timeGrid1 = mx.TimeEqualGrid(ref_date, 3, 365) - timeGrid2 = mx.TimeArrayGrid(ref_date, [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) - timeGrid3 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') - timeGrid4 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='day') - timeGrid4 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='week') - timeGrid5 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) - timeGrid6 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) - timeGrid7 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) - timeGrid8 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) - timeGrid9 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') - timeGrid10 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') - timeGrid11 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') - timeGrid12 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') - timeGrid13 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') - timeGrid14 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') - timeGrid15 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') - timeGrid16 = mx.TimeGrid(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') + timegrid1 = mx.TimeDateGrid_Equal(refDate=ref_date, maxYear=3, nPerYear=365) + timegrid2 = mx.TimeDateGrid_Times(refDate=ref_date, times=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) + timegrid3 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='day') + timegrid4 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='week') + timegrid5 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) + timegrid6 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) + timegrid7 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) + timegrid8 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) + timegrid9 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') + timegrid10 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') + timegrid11 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') + timegrid12 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') + timegrid13 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') + timegrid14 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') + timegrid15 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') + timegrid16 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') - timeGrids = [ timeGrid1, timeGrid2, timeGrid3, timeGrid4, timeGrid5, timeGrid6, timeGrid7, timeGrid8, timeGrid9, timeGrid10, - timeGrid11, timeGrid12, timeGrid13, timeGrid14, timeGrid15, timeGrid16] + timegrids = [ timegrid1, timegrid2, timegrid3, timegrid4, timegrid5, timegrid6, timegrid7, timegrid8, timegrid9, timegrid10, + timegrid11, timegrid12, timegrid13, timegrid14, timegrid15, timegrid16] - for tg, i in zip(timeGrids, range(len(timeGrids))): + for tg, i in zip(timegrids, range(len(timegrids))): print('timeGrid{0} :'.format(i+1), tg.dates()[:10]) From 97a20e73001ad9ec14a28c42c3073b210d4b690d Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:15:22 +0900 Subject: [PATCH 18/54] working... --- .github/workflows/github-actions-demo.yml | 18 ------------------ .github/workflows/mxdevtool-package.yml | 7 ++++--- 2 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 .github/workflows/github-actions-demo.yml diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml deleted file mode 100644 index 8a9c1ff..0000000 --- a/.github/workflows/github-actions-demo.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: GitHub Actions Demo -run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 -on: [push] -jobs: - Explore-GitHub-Actions: - runs-on: ubuntu-latest - steps: - - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - - name: Check out repository code - uses: actions/checkout@v3 - - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - - run: echo "🖥️ The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ github.workspace }} - - run: echo "🍏 This job's status is ${{ job.status }}." \ No newline at end of file diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index b2b3d1a..b936701 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -17,10 +17,11 @@ jobs: matrix: os: ["windows-2019", "ubuntu-20.04", "macos-11"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] - include: + exclude: - os: "macos-11" - python-version: ["3.8", "3.9", "3.10", "3.11"] - + python-version: "3.6" + - os: "macos-11" + python-version: "3.7" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From 1be9bc5a32e74697aa32242ba12a69d90a669081 Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:22:34 +0900 Subject: [PATCH 19/54] working... --- run_test_etc.py | 11 ++++++----- scenario/usage.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/run_test_etc.py b/run_test_etc.py index ac41182..fd4c014 100644 --- a/run_test_etc.py +++ b/run_test_etc.py @@ -4,6 +4,8 @@ import math def timegrid_test(): + print('timegrid test...') + ref_date = mx.Date.todaysDate() maxYear = 10 @@ -29,8 +31,8 @@ def timegrid_test(): timegrids = [ timegrid1, timegrid2, timegrid3, timegrid4, timegrid5, timegrid6, timegrid7, timegrid8, timegrid9, timegrid10, timegrid11, timegrid12, timegrid13, timegrid14, timegrid15, timegrid16] - for tg, i in zip(timegrids, range(len(timegrids))): - print('timeGrid{0} :'.format(i+1), tg.dates()[:10]) + # for tg, i in zip(timegrids, range(len(timegrids))): + # print('timeGrid{0} :'.format(i+1), tg.dates()[:10]) def correlation_test(): @@ -94,10 +96,9 @@ def correlation_test(): _shift_row = shift_row[1:] # except date mrk_data_return.append([ math.log(v[1]/v[0]) for v in zip(_row, _shift_row) ]) - corr_arr = np.corrcoef(np.transpose(mrk_data_return)) + corr_arr = np.corrcoef(np.transpose(mrk_data_return)) + corr = mx.Matrix(corr_arr.tolist()) - corr = mx.Matrix(corr_arr) - print(corr) if __name__ == "__main__": timegrid_test() diff --git a/scenario/usage.py b/scenario/usage.py index 61ce200..46c2f02 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -13,6 +13,7 @@ enviroment = '{0}-{1}'.format(platform.system(), platform.machine()) def test(): + print('usage test...') ref_date = mx.Date.todaysDate() null_calendar = mx.NullCalendar() From acf0a8a09ad72913b8a251e96786f0254ed919e6 Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:27:39 +0900 Subject: [PATCH 20/54] working... --- pricing/CCP_SwapCurve.py | 59 ------------ pricing/ELSStepDown.py | 168 ---------------------------------- pricing/ExoticOption.py | 47 ---------- pricing/IRS_Calculator.py | 80 ---------------- pricing/Interpolation.py | 45 --------- pricing/Swaption.py | 64 ------------- pricing/VanillaOption.py | 76 --------------- pricing/VanillaOptionGraph.py | 62 ------------- run_test_pricing.py | 34 +++---- 9 files changed, 17 insertions(+), 618 deletions(-) delete mode 100644 pricing/CCP_SwapCurve.py delete mode 100644 pricing/ELSStepDown.py delete mode 100644 pricing/ExoticOption.py delete mode 100644 pricing/IRS_Calculator.py delete mode 100644 pricing/Interpolation.py delete mode 100644 pricing/Swaption.py delete mode 100644 pricing/VanillaOption.py delete mode 100644 pricing/VanillaOptionGraph.py diff --git a/pricing/CCP_SwapCurve.py b/pricing/CCP_SwapCurve.py deleted file mode 100644 index ece65ae..0000000 --- a/pricing/CCP_SwapCurve.py +++ /dev/null @@ -1,59 +0,0 @@ -# excel link : https://blog.naver.com/montrix/221396471087 - -import mxdevtool as mx -import mxdevtool.termstructures as ts - -def test(): - #calendar = mx.SouthKorea() - calendar = mx.Calendar('kr') - - daycounter = mx.Actual365Fixed() - ref_date = mx.Date(2020, 1, 9) - effective_date = calendar.advance(ref_date, '1D') - #mx.Settings.instance().setEvaluationDate(ref_date) - - # ref_date - marketQuotes = [('1D','Cash',0.012779015127), - ('3M','Cash',0.0146), - ('6M','Swap',0.014260714286), - ('9M','Swap',0.014110714286), - ('1Y','Swap',0.013975), - ('18M','Swap',0.0138), - ('2Y','Swap',0.013653571429), - ('3Y','Swap',0.0137), - ('4Y','Swap',0.013775), - ('5Y','Swap',0.013814285714), - ('6Y','Swap',0.013817857143), - ('7Y','Swap',0.013835714286), - ('8Y','Swap',0.013921428571), - ('9Y','Swap',0.014042857143), - ('10Y','Swap',0.014185714286), - ('12Y','Swap',0.014360714286), - ('15Y','Swap',0.014146428571), - ('20Y','Swap',0.013175)] - - swap_quote_tenors = [] - swap_quote_types = [] - swap_quote_values = [] - - for q in marketQuotes: - swap_quote_tenors.append(q[0]) - swap_quote_types.append(q[1]) - swap_quote_values.append(q[2]) - - interpolator1DType = mx.Interpolator1D.Linear - extrapolation = mx.FlatExtrapolation('forward') - - family_name = 'irskrw_krccp' - forSettlement = True - - yield_curve = ts.BootstapSwapCurveCCP(ref_date, swap_quote_tenors, swap_quote_types, swap_quote_values, interpolator1DType, extrapolation, family_name, forSettlement) - - for q in marketQuotes: - tenor = q[0] - swap_rate = q[2] - zero = yield_curve.zeroRate(calendar.advance(effective_date, tenor) ,daycounter, mx.Continuous) - discount = yield_curve.discount(calendar.advance(effective_date, tenor)) - -if __name__ == "__main__": - test() \ No newline at end of file diff --git a/pricing/ELSStepDown.py b/pricing/ELSStepDown.py deleted file mode 100644 index ba378a0..0000000 --- a/pricing/ELSStepDown.py +++ /dev/null @@ -1,168 +0,0 @@ -import numpy as np -import mxdevtool as mx -import mxdevtool.xenarix as xen -import mxdevtool.termstructures as ts - -filename = './test_stepdown.npz' -refDate = mx.Date(2012,8,22) -riskFree = 0.0307 - -class StepDownPayoff: - def __init__(self, notional, issue_date, maturity_date, initial_values, ki, ki_flag, coupons): - self.notional = notional - self.issue_date = issue_date - self.maturity_date = maturity_date - self.initial_values = initial_values - self.ki = ki - self.ki_flag = ki_flag - self.coupons = coupons - - # t_pos for calculation - self.coupon_tpos = [] - self.discount_factors = [] - - def initialize_timeGrid(self, timeGrid): - if not isinstance(timeGrid, mx.core_TimeGrid): - raise Exception('timeGrid is required') - - for cpn in self.coupons: - d = cpn[0] - t_pos = timeGrid.closestIndex_Date(d) - self.coupon_tpos.append(t_pos) - - def precalculation_discountFactors(self, discountCurve): - if not isinstance(discountCurve, mx.YieldTermStructure): - return - - if len(self.discount_factors) > 0: - return - - for cpn in self.coupons: - d = cpn[0] - self.discount_factors.append(discountCurve.discount(d)) - - def get_min_return(self, multi_path, t_pos): - min_return = 1.0 - - for i, initial_value in enumerate(self.initial_values): - min_return = min(min_return, multi_path[i][t_pos] / initial_value) - - return min_return - - def check_ki(self, multi_path): - if self.ki_flag: - return True - - for i, initial_value in enumerate(self.initial_values): - min_return = np.min(np.array(multi_path[i]) / initial_value) - if min_return <= self.ki: - return True - - return False - - def value(self, multi_path, discount): - self.precalculation_discountFactors(discount) - - for cpn, t_pos, disc in zip(self.coupons[:-1], self.coupon_tpos[:-1], self.discount_factors[:-1]): - min_return = self.get_min_return(multi_path, t_pos) - ex_level = cpn[1] - if min_return >= ex_level: # early exercise - rate = cpn[2] - return self.notional * (1.0 + rate) * disc - - - - last_cpn = self.coupons[-1] - last_t_pos = self.coupon_tpos[-1] - last_disc = self.discount_factors[-1] - - min_return = self.get_min_return(multi_path, last_t_pos) - - if min_return >= last_cpn[1]: # last exercise - return self.notional * (1.0 + last_cpn[2]) * last_disc - else: - if self.check_ki(multi_path): - return self.notional * min_return * last_disc - else: - return self.notional * (1.0 + last_cpn[2]) * last_disc - - -def build_stepdown(): - notional = 10000 - issue_date = mx.Date(2012,8,22) - maturity_date = mx.Date(2012,8,22) - - initial_values = [387833, 27450] - - ki = 0.35 - ki_flag = False - - coupons = [(mx.Date(2013,2,13), 0.9, 0.06), - (mx.Date(2013,8,13), 0.9, 0.12), - (mx.Date(2014,2,13), 0.85, 0.18), - (mx.Date(2014,8,13), 0.85, 0.24), - (mx.Date(2015,2,13), 0.8, 0.30), - (mx.Date(2015,8,13), 0.8, 0.36)] - - return StepDownPayoff(notional, issue_date, maturity_date, initial_values, ki, ki_flag, coupons) - - -def build_scenario(overwrite=True): - print('stepdown test...', filename) - - if not overwrite: - return - - initialValues = [387833, 27450] - dividends = [0.0247, 0.0181] - volatilities = [0.2809, 0.5795] - - gbmconst1 = xen.GBMConst('gbmconst1', initialValues[0], riskFree, dividends[0], volatilities[0]) - gbmconst2 = xen.GBMConst('gbmconst2', initialValues[1], riskFree, dividends[1], volatilities[1]) - - models = [gbmconst1, gbmconst2] - corr = 0.6031 - - corrMatrix = mx.IdentityMatrix(len(models)) - corrMatrix[0][1] = corr - corrMatrix[1][0] = corr - - timeGrid = mx.TimeEqualGrid(refDate, 3, 365) - - # random - rsg = xen.Rsg(sampleNum=5000) - xen.generate(models, None, corrMatrix, timeGrid, rsg, filename, False) - - -def pricing(): - results = xen.ScenarioResults(filename) - - payoff = build_stepdown() - payoff.initialize_timeGrid(results.timegrid) - - simulNum = results.simulNum - discount_curve = ts.FlatForward(refDate, 0.0307) - - v = 0 - - for i in range(simulNum): - path = results[i] - - v += payoff.value(path, discount_curve) - - if i != 0 and i % 5000 == 0: - print(i, v / i) - - print(simulNum, v / simulNum) - - -def test(): - mx.Settings.instance().setEvaluationDate(refDate) - - build_scenario(overwrite=True) - pricing() - -if __name__ == "__main__": - test() - - #mx.npzee_view(filename) \ No newline at end of file diff --git a/pricing/ExoticOption.py b/pricing/ExoticOption.py deleted file mode 100644 index 0baa66e..0000000 --- a/pricing/ExoticOption.py +++ /dev/null @@ -1,47 +0,0 @@ -# python script for OptionCalculator_v1_1_0.xlsm file -# excel link : https://blog.naver.com/montrix/221378282753 -# python link : https://blog.naver.com/montrix/*********** - -import mxdevtool as mx -import mxdevtool.instruments as mx_i - -# vanilla option - -def test(): - print('option pricing test...') - - x0 = 255 - strike = 254 - rf = 0.02 - div = 0.0 - vol = 0.16 - maturityDate = mx.Date(2020,8,15) - exDates = [ mx.Date(2020,8,15), mx.Date(2020,9,15)] - - european_option = mx_i.EuropeanOption(mx.Option.Call, strike, maturityDate).setPricingParams_GBMConst(x0, rf, div, vol) - american_option = mx_i.AmericanOption(mx.Option.Call, strike, maturityDate).setPricingParams_GBMConst(x0, rf, div, vol) - bermudan_option = mx_i.BermudanOption(mx.Option.Call, strike, exDates).setPricingParams_GBMConst(x0, rf, div, vol) - - barrierType = mx.Barrier.UpIn - barrier = 280 - rebate = 5 - barrier_option = mx_i.BarrierOption(mx.Option.Call, barrierType, barrier, rebate, strike, maturityDate).setPricingParams_GBMConst(x0, rf, div, vol) - - options = [european_option, american_option, bermudan_option, barrier_option] - - for option in options: - print('---------------------------------') - print('NPV :', option.NPV()) - print('delta :', option.delta()) - print('gamma :', option.gamma()) - print('vega :', option.vega()) - print('theta :', option.thetaPerDay()) - print('rho :', option.rho()) - print('div_rho :', option.dividendRho()) - print('impliedVolatility :', option.impliedVolatility(option.NPV())) - - #option1.imvol(1.2) - -if __name__ == "__main__": - test() - diff --git a/pricing/IRS_Calculator.py b/pricing/IRS_Calculator.py deleted file mode 100644 index c4101fc..0000000 --- a/pricing/IRS_Calculator.py +++ /dev/null @@ -1,80 +0,0 @@ -# excel link : https://blog.naver.com/montrix/221853410218 - -import mxdevtool as mx -import mxdevtool.termstructures as ts -import mxdevtool.instruments as inst - -ref_date = mx.Date.todaysDate() - -def yieldCurve(): - - # ref_date - marketQuotes = [('1D','Cash',0.012779015127), - ('3M','Cash',0.0146), - ('6M','Swap',0.014260714286), - ('9M','Swap',0.014110714286), - ('1Y','Swap',0.013975), - ('18M','Swap',0.0138), - ('2Y','Swap',0.013653571429), - ('3Y','Swap',0.0137), - ('4Y','Swap',0.013775), - ('5Y','Swap',0.013814285714), - ('6Y','Swap',0.013817857143), - ('7Y','Swap',0.013835714286), - ('8Y','Swap',0.013921428571), - ('9Y','Swap',0.014042857143), - ('10Y','Swap',0.014185714286), - ('12Y','Swap',0.014360714286), - ('15Y','Swap',0.014146428571), - ('20Y','Swap',0.013175)] - - swap_quote_tenors = [] - swap_quote_types = [] - swap_quote_values = [] - - for q in marketQuotes: - swap_quote_tenors.append(q[0]) - swap_quote_types.append(q[1]) - swap_quote_values.append(q[2]) - - interpolator1DType = mx.Interpolator1D.Linear - # extrapolation = mx.FlatExtrapolation('forward') - extrapolation = mx.SmithWilsonExtrapolation(0.14, 0.042) - - family_name = 'irskrw_krccp' - forSettlement = True - - yield_curve = ts.BootstapSwapCurveCCP(ref_date, swap_quote_tenors, swap_quote_types, swap_quote_values, interpolator1DType, extrapolation, family_name, forSettlement) - - return yield_curve - - -def test(): - print('irs pricing test...') - - yield_curve = yieldCurve() - - family_name = 'irskrw_krccp' - - side = mx.VanillaSwap.Receiver - nominal = 10000000000 - settlementDate = ref_date + 1 - maturityTenor = mx.Period('20Y') - fixedRate = 0.013175 - spread = 0.0 - - swap = inst.makeSwap(side, nominal, maturityTenor, fixedRate, spread, settlementDate, yield_curve, family_name) - - # print(swap.iborIndex.familyName()) - - print('npv : ', swap.NPV()) - print('rho : ', swap.rho(mx.LegResultType.Net)) - print('conv : ', swap.convexity(mx.LegResultType.Net)) - - print('leg rho(Pay) : ', swap.rho(mx.LegResultType.Pay)) - print('leg rho(Rec) : ', swap.rho(mx.LegResultType.Receive)) - print('leg rho(Fix) : ', swap.rho(mx.LegResultType.Fixed)) - print('leg rho(Flo) : ', swap.rho(mx.LegResultType.Floating)) - -if __name__ == "__main__": - test() \ No newline at end of file diff --git a/pricing/Interpolation.py b/pricing/Interpolation.py deleted file mode 100644 index d8d7a23..0000000 --- a/pricing/Interpolation.py +++ /dev/null @@ -1,45 +0,0 @@ -# excel link : https://blog.naver.com/montrix/221410043168 - -import mxdevtool as mx - -def test(): - print('interpolation test...') - - # 1 dimension - print('1 dim ---------') - data = [(1.0, 6.0), - (2.0, 5.0), - (3.0, 8.0), - (4.0, 6.0), - (5.0, 4.0), - (6.0, 1.0), - (7.0, 2.0), - (8.0, 3.0), - (9.0, 6.0), - (10.0, 5.0), - (11.0, 4.0), - (12.0, 2.0)] - - x = [v[0] for v in data] - y = [v[1] for v in data] - - interpolation1d = mx.Interpolation1D(mx.Interpolator1D.ForwardFlat, x, y) - - print(interpolation1d.interpolate(1.17)) - print(interpolation1d.interpolate([1.17, 2,23])) - - # 2 dimension - print('2 dim ---------') - z = mx.Matrix(len(x), len(y)) - - for i in range(len(x)): - for j in range(len(y)): - z[i][j] = i*(pow(j, 0.5)) - - interpolation2d = mx.Interpolation2D(mx.Interpolator2D.Bilinear, x, y, z) - - print(interpolation2d.interpolate(1.17, 2.33)) - print(interpolation2d.interpolate([1.17, 2.23], [4.3, 3.3])) - -if __name__ == "__main__": - test() diff --git a/pricing/Swaption.py b/pricing/Swaption.py deleted file mode 100644 index ddd12f0..0000000 --- a/pricing/Swaption.py +++ /dev/null @@ -1,64 +0,0 @@ -# python script for IRS_Calculator.xlsm file -# excel link : https://blog.naver.com/montrix/221853410218 - -import mxdevtool as mx -import mxdevtool.termstructures as ts -import mxdevtool.instruments as mx_i - - - -def yieldCurve(): - - ref_date = mx.Date.todaysDate() - - # ref_date - marketQuotes = [('1D','Cash',0.012779015127), - ('3M','Cash',0.0146), - ('6M','Swap',0.014260714286), - ('9M','Swap',0.014110714286), - ('1Y','Swap',0.013975), - ('18M','Swap',0.0138), - ('2Y','Swap',0.013653571429), - ('3Y','Swap',0.0137), - ('4Y','Swap',0.013775), - ('5Y','Swap',0.013814285714), - ('6Y','Swap',0.013817857143), - ('7Y','Swap',0.013835714286), - ('8Y','Swap',0.013921428571), - ('9Y','Swap',0.014042857143), - ('10Y','Swap',0.014185714286), - ('12Y','Swap',0.014360714286), - ('15Y','Swap',0.014146428571), - ('20Y','Swap',0.013175)] - - swap_quote_tenors = [] - swap_quote_types = [] - swap_quote_values = [] - - for q in marketQuotes: - swap_quote_tenors.append(q[0]) - swap_quote_types.append(q[1]) - swap_quote_values.append(q[2]) - - interpolator1DType = mx.Interpolator1D.Linear - # extrapolation = mx.FlatExtrapolation('forward') - extrapolation = mx.SmithWilsonExtrapolation(0.14, 0.042) - - family_name = 'irskrw_krccp' - forSettlement = True - - yield_curve = ts.BootstapSwapCurveCCP(ref_date, swap_quote_tenors, swap_quote_types, swap_quote_values, interpolator1DType, extrapolation, family_name, forSettlement) - - return yield_curve - - -def test(): - - yield_curve = yieldCurve() - swaption = mx_i.makeSwaption(yieldCurve=yield_curve) - - print('npv : ', swaption.NPV()) - print('blackvol : ', swaption.impliedVolatility(swaption.NPV() * 0.9, yield_curve, 0.3)) - -if __name__ == "__main__": - test() \ No newline at end of file diff --git a/pricing/VanillaOption.py b/pricing/VanillaOption.py deleted file mode 100644 index c2b0080..0000000 --- a/pricing/VanillaOption.py +++ /dev/null @@ -1,76 +0,0 @@ -# python script for VanillaOption_v1_1_0.xlsm file -# excel link : https://blog.naver.com/montrix/221378282753 -# python link : https://blog.naver.com/montrix/*********** - -import mxdevtool as mx -import mxdevtool.instruments as mx_i -# import pandas as pd - -# vanilla option - -def test(): - print('option pricing test...') - - refDate = mx.Date(2020,7,13) - multiplier = 250000 - mx.Settings.instance().setEvaluationDate(refDate) - - column_names = ['Name', 'Contracts', 'Type', 'X0', 'Strike', 'Rf', 'Div', 'Vol', 'Maturity'] - maturityDate = mx.Date(2020, 8, 13) - - option1 = ['option1', 10, mx.Option.Call, 285, 280, 0.02, 0, 0.16, maturityDate] - option2 = ['option2', -8, mx.Option.Put, 285, 275, 0.02, 0, 0.16, maturityDate] - option3 = ['option3', 10, mx.Option.Call, 285, 265, 0.02, 0, 0.16, maturityDate] - option4 = ['option4', 10, mx.Option.Call, 285, 261.5, 0.02, 0, 0.16, maturityDate] - - option_arr = [option1, option2, option3, option4] - - # option_df = pd.DataFrame(opsion_arr, columns=column_names) - - results = [] - - for opt in option_arr: - - option_type = opt[2] - strike = opt[4] - maturity = opt[8] - x0 = opt[3] - rf = opt[5] - div = opt[6] - vol = opt[7] - - option = mx_i.EuropeanOption(option_type, strike, maturity).setPricingParams_GBMConst(x0, rf, div, vol) - - Name = opt[0] - Contracts = opt[1] - NPV = multiplier * Contracts * option.NPV() - Delta = multiplier * Contracts * option.delta() - Gamma = multiplier * Contracts * option.gamma() - Vega = multiplier * Contracts * option.vega() - Theta = multiplier * Contracts * option.thetaPerDay() - Rho = multiplier * Contracts * option.rho() - Div_Rho = multiplier * Contracts * option.dividendRho() - ImVol = option.impliedVolatility(option.NPV()) - UnitNPV = option.NPV() - - results.append([Name, NPV, Delta, Gamma, Vega, Theta, Rho, Div_Rho, ImVol, UnitNPV]) - - print(Name + ' ---------------------------------') - print('NPV :', NPV) - print('delta :', Delta) - print('gamma :', Gamma) - print('vega :', Vega) - print('theta :', Theta) - print('rho :', Rho) - print('div_rho :', Div_Rho) - print('impliedVolatility :', ImVol) - print('UnitNPV :', UnitNPV) - print() - - # print('export csv file') - # results_df = pd.DataFrame(results, columns=['Name', 'NPV', 'Delta', 'Gamma', 'Vega', 'Theta', 'Rho', 'DivRho', 'ImVol', 'UnitNPV']) - # results_df.to_csv('VanillaOptionResults.csv') - -if __name__ == "__main__": - test() - diff --git a/pricing/VanillaOptionGraph.py b/pricing/VanillaOptionGraph.py deleted file mode 100644 index 025173f..0000000 --- a/pricing/VanillaOptionGraph.py +++ /dev/null @@ -1,62 +0,0 @@ -# python script for VanillaOptionGraph_v1_2_0.xlsm file -# excel link : https://blog.naver.com/montrix/222135609534 - -import mxdevtool as mx -# import pandas as pd -import numpy as np -# import matplotlib.pyplot as plt -import mxdevtool.instruments as mx_i - -# vanilla option - -def test(): - print('option graph test...') - - refDate = mx.Date(2020,7,13) - mx.setEvaluationDate(refDate) - multiplier = 250000 - - maturityDate = mx.Date(2020, 8, 13) - - x0 = 300 - rf = 0.02 - div = 0.0 - vol = 0.16 - - option1 = mx_i.EuropeanOption(mx.Option.Call, 285, maturityDate).setPricingParams_GBMConst(x0, rf, div, vol) - option2 = mx_i.EuropeanOption('c', 270, maturityDate).setPricingParams_GBMConst(x0, rf, div, vol) - option3 = mx_i.EuropeanOption(mx.Option.Put, 283, maturityDate).setPricingParams_GBMConst(x0, rf, div, vol) - option4 = mx_i.EuropeanOption(mx.Option.Call, 310, maturityDate).setPricingParams_GBMConst(x0, rf, div, vol) - - options = [ option1, option2, option3, option4 ] - multiples = np.array([ 20,-15, 15, 20 ]) * multiplier - - portfolio = mx.Portfolio(multiples.tolist(), options) - - # n = 200 - x_grid = { - 'spot' : np.arange(200, 400, 1) , - 'rf' : np.arange(0.001, 0.05, 0.00025), - 'div' : np.arange(0.001, 0.05, 0.00025), - 'vol' : np.arange(0.01, 0.8, 0.00395) - } - - parameter = 'spot' # spot, rf, div, vol - target = 'npv' # delta, gamma, vega, theta, rho - results1 = portfolio.calculateMany(parameter, '=', x_grid[parameter].tolist(), target) - - # plt.plot(x_grid[parameter], results1) - # plt.xlabel(parameter) - # plt.title(target) - # plt.show() - - #results1_df = pd.DataFrame(results1) - #results1_df.to_csv('./excel/pricing/VanillaOptionGraphResults1.csv') - - #results2 = portfolio.calculateMany(['spot','rf'], ['=','='], [spot_grid.tolist(), rf_grid.tolist()], 'npv') - #results2_df = pd.DataFrame(results2) - #results2_df.to_csv('./excel/pricing/VanillaOptionGraphResults2.csv') - -if __name__ == "__main__": - test() - diff --git a/run_test_pricing.py b/run_test_pricing.py index 261d895..4c6d241 100644 --- a/run_test_pricing.py +++ b/run_test_pricing.py @@ -1,22 +1,22 @@ -import pricing.ELSStepDown as stepdown -import pricing.ExoticOption as exotic -import pricing.CCP_SwapCurve as ccpswap -import pricing.Interpolation as interp -import pricing.IRS_Calculator as irscalc -import pricing.Swaption as swaption -import pricing.VanillaOption as vanillaoptioncalc -import pricing.VanillaOptionGraph as vanillaoptiongraphcalc +# import pricing.ELSStepDown as stepdown +# import pricing.ExoticOption as exotic +# import pricing.CCP_SwapCurve as ccpswap +# import pricing.Interpolation as interp +# import pricing.IRS_Calculator as irscalc +# import pricing.Swaption as swaption +# import pricing.VanillaOption as vanillaoptioncalc +# import pricing.VanillaOptionGraph as vanillaoptiongraphcalc -if __name__ == "__main__": - stepdown.test() - exotic.test() - ccpswap.test() - interp.test() - irscalc.test() - swaption.test() - vanillaoptioncalc.test() - vanillaoptiongraphcalc.test() +# if __name__ == "__main__": +# stepdown.test() +# exotic.test() +# ccpswap.test() +# interp.test() +# irscalc.test() +# swaption.test() +# vanillaoptioncalc.test() +# vanillaoptiongraphcalc.test() From 22ee61f60c6fd96c84fca3f50b46340994a3d8b0 Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:28:34 +0900 Subject: [PATCH 21/54] working... --- run_test_etc.py | 106 -------------------------------------------- run_test_pricing.py | 23 ---------- 2 files changed, 129 deletions(-) delete mode 100644 run_test_etc.py delete mode 100644 run_test_pricing.py diff --git a/run_test_etc.py b/run_test_etc.py deleted file mode 100644 index fd4c014..0000000 --- a/run_test_etc.py +++ /dev/null @@ -1,106 +0,0 @@ -import mxdevtool as mx -import mxdevtool.xenarix as xen -import numpy as np -import math - -def timegrid_test(): - print('timegrid test...') - - ref_date = mx.Date.todaysDate() - - maxYear = 10 - - # TimeGrid - timegrid1 = mx.TimeDateGrid_Equal(refDate=ref_date, maxYear=3, nPerYear=365) - timegrid2 = mx.TimeDateGrid_Times(refDate=ref_date, times=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) - timegrid3 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='day') - timegrid4 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='week') - timegrid5 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) - timegrid6 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) - timegrid7 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) - timegrid8 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) - timegrid9 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') - timegrid10 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') - timegrid11 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') - timegrid12 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') - timegrid13 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') - timegrid14 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') - timegrid15 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') - timegrid16 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') - - timegrids = [ timegrid1, timegrid2, timegrid3, timegrid4, timegrid5, timegrid6, timegrid7, timegrid8, timegrid9, timegrid10, - timegrid11, timegrid12, timegrid13, timegrid14, timegrid15, timegrid16] - - # for tg, i in zip(timegrids, range(len(timegrids))): - # print('timeGrid{0} :'.format(i+1), tg.dates()[:10]) - - -def correlation_test(): - # (date, SPX Index, NKY Index, SHCOMP Index, US0003M Index ) - bloomberg ticker - mrk_data =[ - [ '2020-01-06',3246.28, 23204.86 ,3083.408, 1.87225 ], - [ '2020-01-07',3237.18, 23575.72 ,3104.802, 1.878 ], - [ '2020-01-08',3253.05, 23204.76 ,3066.893, 1.834 ], - [ '2020-01-09',3274.7, 23739.87 ,3094.882, 1.84788 ], - [ '2020-01-10',3265.35, 23850.57 ,3092.291, 1.83775 ], - [ '2020-01-14',3283.15, 24025.17 ,3106.82, 1.84263 ], - [ '2020-01-15',3289.29, 23916.58 ,3090.038, 1.83613 ], - [ '2020-01-16',3316.81, 23933.13 ,3074.081, 1.82663 ], - [ '2020-01-17',3329.62, 24041.26 ,3075.496, 1.81913 ], - [ '2020-01-21',3320.79, 23864.56 ,3052.142, 1.80625 ], - [ '2020-01-22',3321.75, 24031.35 ,3060.754, 1.80088 ], - [ '2020-01-23',3325.54, 23795.44 ,2976.528, 1.79413 ], - [ '2020-02-03',3248.92, 22971.94 ,2746.606, 1.741 ], - [ '2020-02-04',3297.59, 23084.59 ,2783.288, 1.73738 ], - [ '2020-02-05',3334.69, 23319.56 ,2818.088, 1.74163 ], - [ '2020-02-06',3345.78, 23873.59 ,2866.51, 1.73413 ], - [ '2020-02-07',3327.71, 23827.98 ,2875.964, 1.73088 ], - [ '2020-02-10',3352.09, 23685.98 ,2890.488, 1.71313 ], - [ '2020-02-12',3379.45, 23861.21 ,2926.899, 1.70375 ], - [ '2020-02-13',3373.94, 23827.73 ,2906.073, 1.69163 ], - [ '2020-02-14',3380.16, 23687.59 ,2917.008, 1.69175 ], - [ '2020-02-18',3370.29, 23193.8 ,2984.972, 1.69463 ], - [ '2020-02-19',3386.15, 23400.7 ,2975.402, 1.696 ], - [ '2020-02-20',3373.23, 23479.15 ,3030.154, 1.68275 ], - [ '2020-02-21',3337.75, 23386.74 ,3039.669, 1.67925 ], - [ '2020-02-25',3128.21, 22605.41 ,3013.05, 1.63763 ], - [ '2020-02-26',3116.39, 22426.19 ,2987.929, 1.61325 ], - [ '2020-02-27',2978.76, 21948.23 ,2991.329, 1.58038 ], - [ '2020-02-28',2954.22, 21142.96 ,2880.304, 1.46275 ], - [ '2020-03-02',3090.23, 21344.08 ,2970.931, 1.25375 ], - [ '2020-03-03',3003.37, 21082.73 ,2992.897, 1.31425 ], - [ '2020-03-04',3130.12, 21100.06 ,3011.666, 1.00063 ], - [ '2020-03-05',3023.94, 21329.12 ,3071.677, 0.99888 ], - [ '2020-03-06',2972.37, 20749.75 ,3034.511, 0.896 ], - [ '2020-03-09',2746.56, 19698.76 ,2943.291, 0.76813 ], - [ '2020-03-10',2882.23, 19867.12 ,2996.762, 0.78413 ], - [ '2020-03-11',2741.38, 19416.06 ,2968.517, 0.7725 ], - [ '2020-03-12',2480.64, 18559.63 ,2923.486, 0.7405 ], - [ '2020-03-13',2711.02, 17431.05 ,2887.427, 0.84313 ], - [ '2020-03-16',2386.13, 17002.04 ,2789.254, 0.88938 ], - [ '2020-03-17',2529.19, 17011.53 ,2779.641, 1.05188 ], - [ '2020-03-18',2398.1, 16726.55 ,2728.756, 1.11575 ], - [ '2020-03-19',2409.39, 16552.83 ,2702.13, 1.19513 ], - [ '2020-03-23',2237.4, 16887.78 ,2660.167, 1.21563 ], - [ '2020-03-24',2447.33, 18092.35 ,2722.438, 1.23238 ], - [ '2020-03-25',2475.56, 19546.63 ,2781.591, 1.267 ], - [ '2020-03-26',2630.07, 18664.6 ,2764.911, 1.37463 ], - [ '2020-03-27',2541.47, 19389.43 ,2772.203, 1.45013 ], - [ '2020-03-30',2626.65, 19084.97 ,2747.214, 1.43338 ], - [ '2020-03-31',2584.59, 18917.01 ,2750.296, 1.4505 ] ] - - mrk_data_return = [] - - for row, shift_row in zip(mrk_data[:-1], mrk_data[1:]): - _row = row[1:] # except date - _shift_row = shift_row[1:] # except date - mrk_data_return.append([ math.log(v[1]/v[0]) for v in zip(_row, _shift_row) ]) - - corr_arr = np.corrcoef(np.transpose(mrk_data_return)) - corr = mx.Matrix(corr_arr.tolist()) - - -if __name__ == "__main__": - timegrid_test() - correlation_test() - diff --git a/run_test_pricing.py b/run_test_pricing.py deleted file mode 100644 index 4c6d241..0000000 --- a/run_test_pricing.py +++ /dev/null @@ -1,23 +0,0 @@ -# import pricing.ELSStepDown as stepdown -# import pricing.ExoticOption as exotic -# import pricing.CCP_SwapCurve as ccpswap -# import pricing.Interpolation as interp -# import pricing.IRS_Calculator as irscalc -# import pricing.Swaption as swaption -# import pricing.VanillaOption as vanillaoptioncalc -# import pricing.VanillaOptionGraph as vanillaoptiongraphcalc - - -# if __name__ == "__main__": -# stepdown.test() -# exotic.test() -# ccpswap.test() -# interp.test() -# irscalc.test() -# swaption.test() -# vanillaoptioncalc.test() -# vanillaoptiongraphcalc.test() - - - - From 061d8eb0080130cc9bceaa2519bbcd10ab512833 Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:29:56 +0900 Subject: [PATCH 22/54] working... --- run_test_etc.py | 107 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 run_test_etc.py diff --git a/run_test_etc.py b/run_test_etc.py new file mode 100644 index 0000000..1005a4f --- /dev/null +++ b/run_test_etc.py @@ -0,0 +1,107 @@ +import mxdevtool as mx +import mxdevtool.xenarix as xen +import numpy as np +import math + +def timegrid_test(): + print('timegrid test...') + + ref_date = mx.Date.todaysDate() + + maxYear = 10 + + # TimeGrid + timegrid1 = mx.TimeDateGrid_Equal(refDate=ref_date, maxYear=3, nPerYear=365) + timegrid2 = mx.TimeDateGrid_Times(refDate=ref_date, times=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]) + timegrid3 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='day') + timegrid4 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='week') + timegrid5 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='month', frequency_day=10) + timegrid6 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='quarter', frequency_day=10) + timegrid7 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='semiannual', frequency_day=10) + timegrid8 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='annual', frequency_month=8, frequency_day=10) + timegrid9 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofmonth') + timegrid10 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofquarter') + timegrid11 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofsemiannual') + timegrid12 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='firstofannual') + timegrid13 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofmonth') + timegrid14 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofquarter') + timegrid15 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofsemiannual') + timegrid16 = mx.TimeDateGrid_Custom(refDate=ref_date, maxYear=maxYear, frequency_type='endofannual') + + timegrids = [ timegrid1, timegrid2, timegrid3, timegrid4, timegrid5, timegrid6, timegrid7, timegrid8, timegrid9, timegrid10, + timegrid11, timegrid12, timegrid13, timegrid14, timegrid15, timegrid16] + + # for tg, i in zip(timegrids, range(len(timegrids))): + # print('timeGrid{0} :'.format(i+1), tg.dates()[:10]) + + +def correlation_test(): + print('correlation test...') + # (date, SPX Index, NKY Index, SHCOMP Index, US0003M Index ) - bloomberg ticker + mrk_data =[ + [ '2020-01-06',3246.28, 23204.86 ,3083.408, 1.87225 ], + [ '2020-01-07',3237.18, 23575.72 ,3104.802, 1.878 ], + [ '2020-01-08',3253.05, 23204.76 ,3066.893, 1.834 ], + [ '2020-01-09',3274.7, 23739.87 ,3094.882, 1.84788 ], + [ '2020-01-10',3265.35, 23850.57 ,3092.291, 1.83775 ], + [ '2020-01-14',3283.15, 24025.17 ,3106.82, 1.84263 ], + [ '2020-01-15',3289.29, 23916.58 ,3090.038, 1.83613 ], + [ '2020-01-16',3316.81, 23933.13 ,3074.081, 1.82663 ], + [ '2020-01-17',3329.62, 24041.26 ,3075.496, 1.81913 ], + [ '2020-01-21',3320.79, 23864.56 ,3052.142, 1.80625 ], + [ '2020-01-22',3321.75, 24031.35 ,3060.754, 1.80088 ], + [ '2020-01-23',3325.54, 23795.44 ,2976.528, 1.79413 ], + [ '2020-02-03',3248.92, 22971.94 ,2746.606, 1.741 ], + [ '2020-02-04',3297.59, 23084.59 ,2783.288, 1.73738 ], + [ '2020-02-05',3334.69, 23319.56 ,2818.088, 1.74163 ], + [ '2020-02-06',3345.78, 23873.59 ,2866.51, 1.73413 ], + [ '2020-02-07',3327.71, 23827.98 ,2875.964, 1.73088 ], + [ '2020-02-10',3352.09, 23685.98 ,2890.488, 1.71313 ], + [ '2020-02-12',3379.45, 23861.21 ,2926.899, 1.70375 ], + [ '2020-02-13',3373.94, 23827.73 ,2906.073, 1.69163 ], + [ '2020-02-14',3380.16, 23687.59 ,2917.008, 1.69175 ], + [ '2020-02-18',3370.29, 23193.8 ,2984.972, 1.69463 ], + [ '2020-02-19',3386.15, 23400.7 ,2975.402, 1.696 ], + [ '2020-02-20',3373.23, 23479.15 ,3030.154, 1.68275 ], + [ '2020-02-21',3337.75, 23386.74 ,3039.669, 1.67925 ], + [ '2020-02-25',3128.21, 22605.41 ,3013.05, 1.63763 ], + [ '2020-02-26',3116.39, 22426.19 ,2987.929, 1.61325 ], + [ '2020-02-27',2978.76, 21948.23 ,2991.329, 1.58038 ], + [ '2020-02-28',2954.22, 21142.96 ,2880.304, 1.46275 ], + [ '2020-03-02',3090.23, 21344.08 ,2970.931, 1.25375 ], + [ '2020-03-03',3003.37, 21082.73 ,2992.897, 1.31425 ], + [ '2020-03-04',3130.12, 21100.06 ,3011.666, 1.00063 ], + [ '2020-03-05',3023.94, 21329.12 ,3071.677, 0.99888 ], + [ '2020-03-06',2972.37, 20749.75 ,3034.511, 0.896 ], + [ '2020-03-09',2746.56, 19698.76 ,2943.291, 0.76813 ], + [ '2020-03-10',2882.23, 19867.12 ,2996.762, 0.78413 ], + [ '2020-03-11',2741.38, 19416.06 ,2968.517, 0.7725 ], + [ '2020-03-12',2480.64, 18559.63 ,2923.486, 0.7405 ], + [ '2020-03-13',2711.02, 17431.05 ,2887.427, 0.84313 ], + [ '2020-03-16',2386.13, 17002.04 ,2789.254, 0.88938 ], + [ '2020-03-17',2529.19, 17011.53 ,2779.641, 1.05188 ], + [ '2020-03-18',2398.1, 16726.55 ,2728.756, 1.11575 ], + [ '2020-03-19',2409.39, 16552.83 ,2702.13, 1.19513 ], + [ '2020-03-23',2237.4, 16887.78 ,2660.167, 1.21563 ], + [ '2020-03-24',2447.33, 18092.35 ,2722.438, 1.23238 ], + [ '2020-03-25',2475.56, 19546.63 ,2781.591, 1.267 ], + [ '2020-03-26',2630.07, 18664.6 ,2764.911, 1.37463 ], + [ '2020-03-27',2541.47, 19389.43 ,2772.203, 1.45013 ], + [ '2020-03-30',2626.65, 19084.97 ,2747.214, 1.43338 ], + [ '2020-03-31',2584.59, 18917.01 ,2750.296, 1.4505 ] ] + + mrk_data_return = [] + + for row, shift_row in zip(mrk_data[:-1], mrk_data[1:]): + _row = row[1:] # except date + _shift_row = shift_row[1:] # except date + mrk_data_return.append([ math.log(v[1]/v[0]) for v in zip(_row, _shift_row) ]) + + corr_arr = np.corrcoef(np.transpose(mrk_data_return)) + corr = mx.Matrix(corr_arr.tolist()) + + +if __name__ == "__main__": + timegrid_test() + correlation_test() + From cdae20f11a128362b1f938992c58740008509cfe Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 00:34:47 +0900 Subject: [PATCH 23/54] working... --- scenario/blog/MultipleAssets.py | 2 +- scenario/models/BK1F.py | 2 +- scenario/models/CIR1F.py | 2 +- scenario/models/GBM.py | 2 +- scenario/models/GBMConst.py | 2 +- scenario/models/GTwoExt.py | 2 +- scenario/models/Heston.py | 2 +- scenario/models/HullWhite1F.py | 2 +- scenario/models/MultipleModels.py | 2 +- scenario/models/Vasicek1F.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scenario/blog/MultipleAssets.py b/scenario/blog/MultipleAssets.py index 4d9a75f..2d7c751 100644 --- a/scenario/blog/MultipleAssets.py +++ b/scenario/blog/MultipleAssets.py @@ -24,7 +24,7 @@ corrMatrix[0][2] = gbmconst1_vasicek_corr # 시간 간격 및 최대 생성 구간 설정 -timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) +timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random filename = './multipleassets.npz' diff --git a/scenario/models/BK1F.py b/scenario/models/BK1F.py index bda645c..c82789c 100644 --- a/scenario/models/BK1F.py +++ b/scenario/models/BK1F.py @@ -48,7 +48,7 @@ def test(): print('bk1f test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/CIR1F.py b/scenario/models/CIR1F.py index 8a0752d..6d20d75 100644 --- a/scenario/models/CIR1F.py +++ b/scenario/models/CIR1F.py @@ -23,7 +23,7 @@ def test(): print('cir1f test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/GBM.py b/scenario/models/GBM.py index 555492c..2f9f2ef 100644 --- a/scenario/models/GBM.py +++ b/scenario/models/GBM.py @@ -52,7 +52,7 @@ def test(): print('gbm test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/GBMConst.py b/scenario/models/GBMConst.py index 6d90c2e..3047b78 100644 --- a/scenario/models/GBMConst.py +++ b/scenario/models/GBMConst.py @@ -24,7 +24,7 @@ def test(): print('gbmconst test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/GTwoExt.py b/scenario/models/GTwoExt.py index 9dbbdf0..f40537c 100644 --- a/scenario/models/GTwoExt.py +++ b/scenario/models/GTwoExt.py @@ -51,7 +51,7 @@ def test(): print('gtwoext test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/Heston.py b/scenario/models/Heston.py index e89f01a..fc650c5 100644 --- a/scenario/models/Heston.py +++ b/scenario/models/Heston.py @@ -57,7 +57,7 @@ def test(): print('heston test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/HullWhite1F.py b/scenario/models/HullWhite1F.py index 537690d..4cf77d5 100644 --- a/scenario/models/HullWhite1F.py +++ b/scenario/models/HullWhite1F.py @@ -51,7 +51,7 @@ def test(): m = model() # timegrid - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random sequence rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/MultipleModels.py b/scenario/models/MultipleModels.py index ffdc487..14e8581 100644 --- a/scenario/models/MultipleModels.py +++ b/scenario/models/MultipleModels.py @@ -23,7 +23,7 @@ def test(): # corrMatrix = mx.Matrix([[1.0, 0.0],[0.0, 1.0]]) corrMatrix = mx.IdentityMatrix(len(models)) - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) diff --git a/scenario/models/Vasicek1F.py b/scenario/models/Vasicek1F.py index 035f6a7..24d2dcd 100644 --- a/scenario/models/Vasicek1F.py +++ b/scenario/models/Vasicek1F.py @@ -23,7 +23,7 @@ def test(): print('vasicek1f test...', filename) m = model() - timeGrid = mx.TimeEqualGrid(ref_date, 3, 365) + timeGrid = mx.TimeDateGrid_Equal(ref_date, 3, 365) # random rsg = xen.Rsg(sampleNum=5000) From d7711e06022425bfbd4f421b226804679853875d Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 15:19:35 +0900 Subject: [PATCH 24/54] working... --- .github/workflows/mxdevtool-package.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index b936701..05c89de 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -47,5 +47,8 @@ jobs: run: pip show mxdevtool - name: Test run: | + python ./quantlib/test/QuantLibTestSuite.py || exit 1 python run_test_usage.py || exit 1 python run_test_random.py || exit 1 + python run_test_etc.py || exit 1 + python run_test_scenario.py || exit 1 From 6dd3de08971d50e79b84f05f1f637a3296078196 Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 22:57:59 +0900 Subject: [PATCH 25/54] working... --- .github/workflows/mxdevtool-package.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 05c89de..f1a4d71 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -15,13 +15,17 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-2019", "ubuntu-20.04", "macos-11"] + os: ["windows-2019", "ubuntu-20.04", "macos-11", "macos-12"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - os: "macos-11" python-version: "3.6" - os: "macos-11" python-version: "3.7" + - os: "macos-12" + python-version: "3.6" + - os: "macos-12" + python-version: "3.7" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From bbd7aff4bb4becbbe2b0480f86dd3bae432ab67c Mon Sep 17 00:00:00 2001 From: minikie Date: Sun, 29 Jan 2023 23:22:04 +0900 Subject: [PATCH 26/54] working... --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index f1a4d71..86737f3 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-2019", "ubuntu-20.04", "macos-11", "macos-12"] + os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macos-11", "macos-12"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - os: "macos-11" From 0f665468f3707e444be965ecd5afeeff801ad87f Mon Sep 17 00:00:00 2001 From: minikie Date: Mon, 30 Jan 2023 01:54:17 +0900 Subject: [PATCH 27/54] working... --- .github/workflows/mxdevtool-package.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 86737f3..ca7c130 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -18,6 +18,8 @@ jobs: os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macos-11", "macos-12"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: + - os: "ubuntu-22.04" + python-version: "3.6" - os: "macos-11" python-version: "3.6" - os: "macos-11" @@ -36,7 +38,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 numpy + python -m pip install flake8 numpy QuantLib python -m pip install --index-url https://test.pypi.org/simple mxdevtool -U - name: Lint with flake8 run: | From b6a76cdd75f568ba9b0d68c46712159a987367e7 Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 11:21:06 +0900 Subject: [PATCH 28/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index ca7c130..24c79a4 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macos-11", "macos-12"] + os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "self-hosted"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - os: "ubuntu-22.04" From 56cf08b85597f50fdcb9449c16481cfda900ff96 Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 11:22:34 +0900 Subject: [PATCH 29/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 24c79a4..7be1e7a 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -15,19 +15,15 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "self-hosted"] + os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macOS"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - os: "ubuntu-22.04" python-version: "3.6" - - os: "macos-11" + - os: "macOS" python-version: "3.6" - - os: "macos-11" + - os: "macOS" python-version: "3.7" - - os: "macos-12" - python-version: "3.6" - - os: "macos-12" - python-version: "3.7" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From c3f5e6292db2256bee91992c738adbb249ae2998 Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:04:23 +0900 Subject: [PATCH 30/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 7be1e7a..0a4802d 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -15,11 +15,9 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macOS"] + os: ["macOS"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - - os: "ubuntu-22.04" - python-version: "3.6" - os: "macOS" python-version: "3.6" - os: "macOS" @@ -30,6 +28,9 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} + architecture: "x64" + env: + AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache - name: Install dependencies run: | From 1fb691666ba4c41283bdc58447256da2469a0569 Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:20:34 +0900 Subject: [PATCH 31/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 0a4802d..2b8367b 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -29,8 +29,7 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: "x64" - env: - AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache + - name: Install dependencies run: | From 4adcc8f33f96bd3bb7cee90a59410f05c5d0acc2 Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:34:37 +0900 Subject: [PATCH 32/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 2b8367b..79a7a1f 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -22,14 +22,13 @@ jobs: python-version: "3.6" - os: "macOS" python-version: "3.7" + steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - architecture: "x64" - - name: Install dependencies run: | From 34e0d4f5a56fc7e01779ae90cf691bc4eb0c4c80 Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:41:10 +0900 Subject: [PATCH 33/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 79a7a1f..dc54d29 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -29,7 +29,10 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - + architecture: "x64" + env: + AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache + - name: Install dependencies run: | python -m pip install --upgrade pip From 662d80be62980867c94036cb20b00f2d7142ca9e Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:47:21 +0900 Subject: [PATCH 34/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index dc54d29..aadeff7 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -29,7 +29,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - architecture: "x64" + architecture: "aarch64" env: AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache From df512958063251126f7eef2d1c8e6c1c86e2dbcc Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Tue, 31 Jan 2023 13:50:00 +0900 Subject: [PATCH 35/54] Update mxdevtool-package.yml --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index aadeff7..93028ba 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -29,7 +29,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - architecture: "aarch64" + architecture: "arm64" env: AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache From 17c7b26962af68852dfe5a54b858ae5561bc38eb Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 22:57:41 +0900 Subject: [PATCH 36/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index ca7c130..1f1f6f1 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -15,19 +15,20 @@ jobs: strategy: fail-fast: false matrix: - os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macos-11", "macos-12"] + # os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macos-11", "macos-12"] + os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - os: "ubuntu-22.04" python-version: "3.6" - - os: "macos-11" - python-version: "3.6" - - os: "macos-11" - python-version: "3.7" - - os: "macos-12" - python-version: "3.6" - - os: "macos-12" - python-version: "3.7" + # - os: "macos-11" + # python-version: "3.6" + # - os: "macos-11" + # python-version: "3.7" + # - os: "macos-12" + # python-version: "3.6" + # - os: "macos-12" + # python-version: "3.7" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From 7a138ec282da08a02493477f5fdbca4ec6838fb2 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:02:10 +0900 Subject: [PATCH 37/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 411b6b2..fa645b9 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -35,7 +35,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - architecture: "arm64" + # architecture: "arm64" env: AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache From 45390cfc991f38520efb916ad641efc610d720b1 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:03:42 +0900 Subject: [PATCH 38/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index fa645b9..2ed6b2b 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -10,7 +10,7 @@ on: branches: [ "master" ] jobs: - build: + Python Test: runs-on: ${{ matrix.os }} strategy: fail-fast: false From d3236788ada33deb76d0011c18328af9f56e21f8 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:04:01 +0900 Subject: [PATCH 39/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 2ed6b2b..fcf7bc3 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -19,8 +19,6 @@ jobs: os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - - os: "macOS" - python-version: "3.6" # - os: "macos-11" # python-version: "3.6" # - os: "macos-11" From faa3208d31e33ea4ae6d51716ee806d86061301d Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:06:09 +0900 Subject: [PATCH 40/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index fcf7bc3..08d6bf5 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -10,7 +10,7 @@ on: branches: [ "master" ] jobs: - Python Test: + build: runs-on: ${{ matrix.os }} strategy: fail-fast: false From ee17dbf754a5c359b712261b0df275378529a851 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:06:46 +0900 Subject: [PATCH 41/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 08d6bf5..498e7a9 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -18,7 +18,7 @@ jobs: # os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macos-11", "macos-12"] os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] - exclude: + # exclude: # - os: "macos-11" # python-version: "3.6" # - os: "macos-11" From e8b22eee8a0782c65ec9de65abcbbd7bc2d5a69e Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:07:05 +0900 Subject: [PATCH 42/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 498e7a9..34746ea 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -10,7 +10,7 @@ on: branches: [ "master" ] jobs: - build: + Python Test: runs-on: ${{ matrix.os }} strategy: fail-fast: false From 5e6d6d3e20dd86163bcaf5e1b7e13e4381539b29 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:08:40 +0900 Subject: [PATCH 43/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 34746ea..3855db6 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -10,7 +10,7 @@ on: branches: [ "master" ] jobs: - Python Test: + test: runs-on: ${{ matrix.os }} strategy: fail-fast: false From db03cf1e4604a3bc3ad6a455808a9072222d5590 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:09:34 +0900 Subject: [PATCH 44/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 3855db6..1f6bf57 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -18,9 +18,11 @@ jobs: # os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04", "macos-11", "macos-12"] os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] - # exclude: + exclude: + - os: "ubuntu-22.04" + python-version: "3.6" # - os: "macos-11" - # python-version: "3.6" + # python-version: "3.6" # - os: "macos-11" # python-version: "3.7" # - os: "macos-12" From a8d28ddab52dd443a027f4e9af1ed9d087d36634 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:12:27 +0900 Subject: [PATCH 45/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index 1f6bf57..e60d431 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -19,8 +19,10 @@ jobs: os: ["windows-2022", "windows-2019", "ubuntu-22.04", "ubuntu-20.04"] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] exclude: - - os: "ubuntu-22.04" + - os: "windows-2022" python-version: "3.6" + - os: "ubuntu-22.04" + python-version: "3.6" # - os: "macos-11" # python-version: "3.6" # - os: "macos-11" From f9a5e1da2c92951e14a2509daa40eba2dbe10911 Mon Sep 17 00:00:00 2001 From: minikie Date: Fri, 3 Feb 2023 23:18:28 +0900 Subject: [PATCH 46/54] 1.0.29.17 --- .github/workflows/mxdevtool-package.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml index e60d431..451a87d 100644 --- a/.github/workflows/mxdevtool-package.yml +++ b/.github/workflows/mxdevtool-package.yml @@ -38,6 +38,7 @@ jobs: with: python-version: ${{ matrix.python-version }} # architecture: "arm64" + env: AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache @@ -45,7 +46,8 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install flake8 numpy QuantLib - python -m pip install --index-url https://test.pypi.org/simple mxdevtool -U + python -m pip install mxdevtool -U + # python -m pip install --index-url https://test.pypi.org/simple mxdevtool -U - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 0982e16413ebacd2848d52d4e4e2c0fe74fe197f Mon Sep 17 00:00:00 2001 From: MontrixDev <54784078+montrixdev@users.noreply.github.com> Date: Sun, 5 Feb 2023 19:48:50 +0900 Subject: [PATCH 47/54] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cc9ade1..65834c0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ MxDevTool(Beta) : Financial Library ![image](https://img.shields.io/badge/platform-windows_64|_linux_64-red) ![image](https://img.shields.io/badge/python-3.6|3.7|3.8|3.9|3.10|3.11-blue) -![image](https://img.shields.io/badge/version-1.0.29.0-green.svg) +![image](https://img.shields.io/badge/version-1.0.29.17-green.svg) ![image](https://img.shields.io/badge/platform-macOS_64-red) ![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) @@ -797,7 +797,7 @@ For source code, check this repository. # Release History -## 1.0.29.0 (2023-01-28) +## 1.0.29.17 (2023-01-28) - QuantLib dependency is redegined - Version Syntex is changed - Instruments pricings are removed for reconstruction From 245f38d1a28a4286157f6397c286237e5b42ae8d Mon Sep 17 00:00:00 2001 From: mathansang Date: Mon, 16 Oct 2023 23:10:59 +0900 Subject: [PATCH 48/54] add stepdown --- pricing/stepdown.py | 169 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 pricing/stepdown.py diff --git a/pricing/stepdown.py b/pricing/stepdown.py new file mode 100644 index 0000000..ffad8f9 --- /dev/null +++ b/pricing/stepdown.py @@ -0,0 +1,169 @@ +import numpy as np +import mxdevtool as mx +import mxdevtool.xenarix as xen +import mxdevtool.termstructures as ts + +filename = 'D:/test_stepdown.npz' +refDate = mx.Date(22, 8, 2012) +riskFree = 0.0307 + + +class StepDownPayoff: + def __init__(self, notional, issue_date, maturity_date, initial_values, ki, ki_flag, coupons): + self.notional = notional + self.issue_date = issue_date + self.maturity_date = maturity_date + self.initial_values = initial_values + self.ki = ki + self.ki_flag = ki_flag + self.coupons = coupons + + # t_pos for calculation + self.coupon_tpos = [] + self.discount_factors = [] + + def initialize_timeGrid(self, timeGrid: mx.TimeDateGrid): + if not isinstance(timeGrid, mx.TimeDateGrid): + raise Exception('timeDateGrid is required') + + for cpn in self.coupons: + d = cpn[0] + t_pos = timeGrid.closestIndex_Date(d) + self.coupon_tpos.append(t_pos) + + def precalculation_discountFactors(self, discountCurve): + if not isinstance(discountCurve, mx.YieldTermStructure): + return + + if len(self.discount_factors) > 0: + return + + for cpn in self.coupons: + d = cpn[0] + self.discount_factors.append(discountCurve.discount(d)) + + def get_min_return(self, multi_path, t_pos): + min_return = 1.0 + + for i, initial_value in enumerate(self.initial_values): + min_return = min(min_return, multi_path[i][t_pos] / initial_value) + + return min_return + + def check_ki(self, multi_path): + if self.ki_flag: + return True + + for i, initial_value in enumerate(self.initial_values): + min_return = np.min(np.array(multi_path[i]) / initial_value) + if min_return <= self.ki: + return True + + return False + + def value(self, multi_path, discount): + self.precalculation_discountFactors(discount) + + for cpn, t_pos, disc in zip(self.coupons[:-1], self.coupon_tpos[:-1], self.discount_factors[:-1]): + min_return = self.get_min_return(multi_path, t_pos) + ex_level = cpn[1] + if min_return >= ex_level: # early exercise + rate = cpn[2] + return self.notional * (1.0 + rate) * disc + + last_cpn = self.coupons[-1] + last_t_pos = self.coupon_tpos[-1] + last_disc = self.discount_factors[-1] + + min_return = self.get_min_return(multi_path, last_t_pos) + + if min_return >= last_cpn[1]: # last exercise + return self.notional * (1.0 + last_cpn[2]) * last_disc + else: + if self.check_ki(multi_path): + return self.notional * min_return * last_disc + else: + return self.notional * (1.0 + last_cpn[2]) * last_disc + + +def build_stepdown(): + notional = 10000 + issue_date = mx.Date(22, 8, 2012) + maturity_date = mx.Date(22, 8, 2015) + + initial_values = [387833, 27450] + + ki = 0.35 + ki_flag = False + + coupons = [(mx.Date(13, 2, 2013), 0.9, 0.06), + (mx.Date(13, 8, 2013), 0.9, 0.12), + (mx.Date(13, 2, 2014), 0.85, 0.18), + (mx.Date(13, 8, 2014), 0.85, 0.24), + (mx.Date(13, 2, 2015), 0.8, 0.30), + (mx.Date(13, 8, 2015), 0.8, 0.36)] + + return StepDownPayoff(notional, issue_date, maturity_date, initial_values, ki, ki_flag, coupons) + + +def build_scenario(overwrite=True): + print('stepdown test...', filename) + + if not overwrite: + return + + initialValues = [387833, 27450] + dividends = [0.0247, 0.0181] + volatilities = [0.2809, 0.5795] + + gbmconst1 = xen.GBMConst('gbmconst1', initialValues[0], riskFree, dividends[0], volatilities[0]) + gbmconst2 = xen.GBMConst('gbmconst2', initialValues[1], riskFree, dividends[1], volatilities[1]) + + models = [gbmconst1, gbmconst2] + corr = 0.6031 + + corrMatrix = mx.IdentityMatrix(len(models)) + corrMatrix[0][1] = corr + corrMatrix[1][0] = corr + + timeGrid = mx.TimeDateGrid_Equal(refDate, 3, 365) + + # random + rsg = xen.Rsg(sampleNum=10000) + xen.generate(models, None, corrMatrix, timeGrid, rsg, filename, False) + + +class McPricer: + def __init__(self): + pass + + def npv(scen, payoff: StepDownPayoff, discount_curve): + results = xen.ScenarioResults(filename) + + payoff.initialize_timeGrid(results.timegrid) + + simulNum = results.simulNum + + v = 0 + + for i in range(simulNum): + path = results[i] + v += payoff.value(path, discount_curve) + + return v / simulNum + + +def test(): + mx.Settings.instance().setEvaluationDate(refDate) + + build_scenario(overwrite=True) + stepdown = build_stepdown() + curve = ts.FlatForward(refDate, 0.03, mx.Actual365Fixed(), mx.Semiannual) + npv = McPricer().npv(stepdown, curve) + print(npv) + + +if __name__ == "__main__": + test() + + #mx.npzee_view(filename) \ No newline at end of file From ad07ef98d55f4a688061e05b16addca47b95f2c1 Mon Sep 17 00:00:00 2001 From: mathansang Date: Fri, 20 Oct 2023 15:34:10 +0900 Subject: [PATCH 49/54] update gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4d0d36a..11083db 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,6 @@ settings.json xenrepo/ clear_output.bat -*.html \ No newline at end of file +*.html + +*.json \ No newline at end of file From 19949966b698132becc4046a3d5a04ca193094a3 Mon Sep 17 00:00:00 2001 From: mathansang Date: Sun, 22 Oct 2023 15:46:24 +0900 Subject: [PATCH 50/54] add calcs ( overnight , ibor, swap, bond ) --- scenario/usage.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scenario/usage.py b/scenario/usage.py index 46c2f02..6da0ae5 100644 --- a/scenario/usage.py +++ b/scenario/usage.py @@ -5,6 +5,7 @@ import mxdevtool.xenarix as xen import mxdevtool.termstructures as ts import mxdevtool.quotes as mx_q +import mxdevtool.marketconvension as mx_m import mxdevtool.data.providers as mx_dp import mxdevtool.data.repositories as mx_dr import mxdevtool.utils as utils @@ -79,6 +80,11 @@ def test(): # calcs in models hw1f_spot3m = hw1f.spot('hw1f_spot3m', maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) + hw1f_overnight = hw1f.overnight('hw1f_sofr', mx_m.IndexFactory().get_overnightIndex('sofr')) + hw1f_libor = hw1f.ibor('libor3m', mx_m.IndexFactory().get_iborIndex('libor', mx.Period(3, mx.Months))) + hw1f_swap = hw1f.swaprate('cms5y', mx_m.IndexFactory().get_swapIndex('krwirs', mx.Period(5, mx.Years), mx.Period(3, mx.Months))) + hw1f_bond = hw1f.bondrate('cmt10y', mx_m.IndexFactory().get_bondIndex('ktb', mx.Period(5, mx.Years), mx.Period(6, mx.Months))) + # hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startTenor=0.5, maturityTenor=3.0, compounding=mx.Compounded) hw1f_discountFactor = hw1f.discountFactor('hw1f_discountFactor') @@ -185,7 +191,7 @@ def test(): # multiple model with calc filename3='./multiple_model_with_calc.npz' - calcs = [oper1, oper3, linearOper1, linearOper2, shiftLeft2, returns1, fixedRateBond, hw1f_spot3m] + calcs = [oper1, oper3, linearOper1, linearOper2, shiftLeft2, returns1, fixedRateBond, hw1f_spot3m, hw1f_overnight, hw1f_libor, hw1f_swap, hw1f_bond] results3 = xen.generate(models=models, calcs=calcs, corr=corrMatrix, timegrid=timegrid4, rsg=pseudo_rsg, filename=filename3, isMomentMatching=False) all_models = [ gbmconst, gbm, heston, hw1f, bk1f, cir1f, vasicek1f, g2ext ] From 736975b15fd08e39153bfb57c381a75da058d66f Mon Sep 17 00:00:00 2001 From: mathansang Date: Sat, 28 Oct 2023 21:35:10 +0900 Subject: [PATCH 51/54] update ql test py --- quantlib/test/QuantLibTestSuite.py | 94 +------------- quantlib/test/__init__.py | 0 ...option.py => test_americanquantooption.py} | 6 +- .../test/{assetswap.py => test_assetswap.py} | 6 +- .../{blackformula.py => test_blackformula.py} | 21 +-- quantlib/test/{bonds.py => test_bonds.py} | 8 +- quantlib/test/test_calendars.py | 40 ++++++ .../test/{capfloor.py => test_capfloor.py} | 6 +- quantlib/test/{cms.py => test_cms.py} | 10 +- quantlib/test/{coupons.py => test_coupons.py} | 10 +- .../{currencies.py => test_currencies.py} | 6 +- quantlib/test/{date.py => test_date.py} | 6 +- .../{daycounters.py => test_daycounters.py} | 10 +- quantlib/test/test_equityindex.py | 67 ++++++++++ ...extrapolation.py => test_extrapolation.py} | 6 +- quantlib/test/{fdm.py => test_fdm.py} | 6 +- .../test/{iborindex.py => test_iborindex.py} | 6 +- .../test/{inflation.py => test_inflation.py} | 14 +- .../{instruments.py => test_instruments.py} | 6 +- .../test/{integrals.py => test_integrals.py} | 6 +- ...rketelements.py => test_marketelements.py} | 6 +- quantlib/test/{ode.py => test_ode.py} | 6 +- quantlib/test/{options.py => test_options.py} | 9 +- .../{ratehelpers.py => test_ratehelpers.py} | 20 +-- quantlib/test/{sabr.py => test_sabr.py} | 6 +- quantlib/test/{slv.py => test_slv.py} | 30 ++++- .../test/{solvers1d.py => test_solvers1d.py} | 8 +- quantlib/test/{swap.py => test_swap.py} | 120 +++++++++++++++++- .../test/{swaption.py => test_swaption.py} | 8 +- ...rmstructures.py => test_termstructures.py} | 12 +- .../{volatilities.py => test_volatilities.py} | 25 ++-- 31 files changed, 340 insertions(+), 244 deletions(-) create mode 100644 quantlib/test/__init__.py rename quantlib/test/{americanquantooption.py => test_americanquantooption.py} (94%) rename quantlib/test/{assetswap.py => test_assetswap.py} (99%) rename quantlib/test/{blackformula.py => test_blackformula.py} (88%) rename quantlib/test/{bonds.py => test_bonds.py} (97%) create mode 100644 quantlib/test/test_calendars.py rename quantlib/test/{capfloor.py => test_capfloor.py} (95%) rename quantlib/test/{cms.py => test_cms.py} (97%) rename quantlib/test/{coupons.py => test_coupons.py} (97%) rename quantlib/test/{currencies.py => test_currencies.py} (89%) rename quantlib/test/{date.py => test_date.py} (92%) rename quantlib/test/{daycounters.py => test_daycounters.py} (74%) create mode 100644 quantlib/test/test_equityindex.py rename quantlib/test/{extrapolation.py => test_extrapolation.py} (88%) rename quantlib/test/{fdm.py => test_fdm.py} (99%) rename quantlib/test/{iborindex.py => test_iborindex.py} (93%) rename quantlib/test/{inflation.py => test_inflation.py} (97%) rename quantlib/test/{instruments.py => test_instruments.py} (90%) rename quantlib/test/{integrals.py => test_integrals.py} (92%) rename quantlib/test/{marketelements.py => test_marketelements.py} (90%) rename quantlib/test/{ode.py => test_ode.py} (88%) rename quantlib/test/{options.py => test_options.py} (92%) rename quantlib/test/{ratehelpers.py => test_ratehelpers.py} (97%) rename quantlib/test/{sabr.py => test_sabr.py} (95%) rename quantlib/test/{slv.py => test_slv.py} (82%) rename quantlib/test/{solvers1d.py => test_solvers1d.py} (93%) rename quantlib/test/{swap.py => test_swap.py} (54%) rename quantlib/test/{swaption.py => test_swaption.py} (97%) rename quantlib/test/{termstructures.py => test_termstructures.py} (97%) rename quantlib/test/{volatilities.py => test_volatilities.py} (97%) diff --git a/quantlib/test/QuantLibTestSuite.py b/quantlib/test/QuantLibTestSuite.py index 0b8fa5e..9fdd9bb 100644 --- a/quantlib/test/QuantLibTestSuite.py +++ b/quantlib/test/QuantLibTestSuite.py @@ -16,101 +16,17 @@ FOR A PARTICULAR PURPOSE. See the license for more details. """ +import os import sys import unittest -from date import DateTest -from daycounters import DayCountersTest -from instruments import InstrumentTest -from marketelements import MarketElementTest -from integrals import IntegralTest -from solvers1d import Solver1DTest -from termstructures import TermStructureTest -from bonds import ( - FixedRateBondTest, - FixedRateBondKwargsTest, - AmortizingFixedRateBondTest) -from ratehelpers import ( - FixedRateBondHelperTest, - FxSwapRateHelperTest, - OISRateHelperTest, - CrossCurrencyBasisSwapRateHelperTest) -from cms import CmsTest -from assetswap import AssetSwapTest -from capfloor import CapFloorTest -from blackformula import BlackFormulaTest -from blackformula import BlackDeltaCalculatorTest -from iborindex import IborIndexTest -from sabr import SabrTest -from slv import SlvTest -from ode import OdeTest -from americanquantooption import AmericanQuantoOptionTest -from extrapolation import ExtrapolationTest -from fdm import FdmTest -from swaption import SwaptionTest -from volatilities import SviSmileSectionTest, SwaptionVolatilityCubeTest, AndreasenHugeVolatilityTest -from inflation import InflationTest -from coupons import ( - CashFlowsTest, - SubPeriodsCouponTest, - IborCouponTest, - OvernightCouponTest, - FixedRateCouponTest) -from options import OptionsTest -from swap import ZeroCouponSwapTest -from currencies import CurrencyTest +import mxdevtool as ql def test(): - import mxdevtool - print('testing QuantLib ' + mxdevtool.__version__) - - suite = unittest.TestSuite() - - suite.addTest(unittest.makeSuite(DateTest, 'test')) - suite.addTest(DayCountersTest()) - suite.addTest(unittest.makeSuite(InstrumentTest, 'test')) - suite.addTest(unittest.makeSuite(MarketElementTest, 'test')) - suite.addTest(unittest.makeSuite(IntegralTest, 'test')) - suite.addTest(Solver1DTest()) - suite.addTest(unittest.makeSuite(TermStructureTest, 'test')) - suite.addTest(unittest.makeSuite(FixedRateBondTest, 'test')) - suite.addTest(unittest.makeSuite(FixedRateBondKwargsTest, 'test')) - suite.addTest(unittest.makeSuite(AmortizingFixedRateBondTest, 'test')) - suite.addTest(unittest.makeSuite(FixedRateBondHelperTest, 'test')) - suite.addTest(unittest.makeSuite(CmsTest, 'test')) - suite.addTest(unittest.makeSuite(AssetSwapTest, 'test')) - suite.addTest(unittest.makeSuite(OISRateHelperTest, "test")) - suite.addTest(unittest.makeSuite(FxSwapRateHelperTest, 'test')) - suite.addTest(unittest.makeSuite(CapFloorTest, 'test')) - suite.addTest(unittest.makeSuite(BlackFormulaTest, 'test')) - suite.addTest(unittest.makeSuite(BlackDeltaCalculatorTest, 'test')) - suite.addTest(unittest.makeSuite(IborIndexTest, 'test')) - suite.addTest(unittest.makeSuite(SabrTest, 'test')) - suite.addTest(unittest.makeSuite(SlvTest, 'test')) - suite.addTest(unittest.makeSuite(OdeTest, 'test')) - suite.addTest(unittest.makeSuite(AmericanQuantoOptionTest, 'test')) - suite.addTest(unittest.makeSuite(ExtrapolationTest, 'test')) - suite.addTest(unittest.makeSuite(FdmTest, 'test')) - suite.addTest(unittest.makeSuite(SwaptionTest, "test")) - suite.addTest(unittest.makeSuite(SwaptionVolatilityCubeTest, 'test')) - suite.addTest(unittest.makeSuite(InflationTest, "test")) - suite.addTest(unittest.makeSuite(CrossCurrencyBasisSwapRateHelperTest, "test")) - suite.addTest(unittest.makeSuite(CashFlowsTest, "test")) - suite.addTest(unittest.makeSuite(SubPeriodsCouponTest, "test")) - suite.addTest(unittest.makeSuite(IborCouponTest, "test")) - suite.addTest(unittest.makeSuite(OvernightCouponTest, "test")) - suite.addTest(unittest.makeSuite(FixedRateCouponTest, "test")) - suite.addTest(unittest.makeSuite(OptionsTest, "test")) - suite.addTest(unittest.makeSuite(ZeroCouponSwapTest, "test")) - suite.addTest(unittest.makeSuite(CurrencyTest, "test")) - suite.addTest(unittest.makeSuite(SviSmileSectionTest, "test")) - suite.addTest(unittest.makeSuite(AndreasenHugeVolatilityTest, "test")) - - result = unittest.TextTestRunner(verbosity=2).run(suite) - - if not result.wasSuccessful(): - sys.exit(1) + print('testing QuantLib', ql.__version__) + sys.argv[1:1] = ['discover', '-s', os.path.dirname(__file__)] + unittest.main(module=None, verbosity=2) if __name__ == '__main__': diff --git a/quantlib/test/__init__.py b/quantlib/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/quantlib/test/americanquantooption.py b/quantlib/test/test_americanquantooption.py similarity index 94% rename from quantlib/test/americanquantooption.py rename to quantlib/test/test_americanquantooption.py index 368e9e4..d2a7e32 100644 --- a/quantlib/test/americanquantooption.py +++ b/quantlib/test/test_americanquantooption.py @@ -97,7 +97,5 @@ def testAmericanHestonQuantoOption(self): if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(AmericanQuantoOptionTest,'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/assetswap.py b/quantlib/test/test_assetswap.py similarity index 99% rename from quantlib/test/assetswap.py rename to quantlib/test/test_assetswap.py index 3b508fa..659e780 100644 --- a/quantlib/test/assetswap.py +++ b/quantlib/test/test_assetswap.py @@ -5351,7 +5351,5 @@ def testSpecializedBondVsGenericBondUsingAsw(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(AssetSwapTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/blackformula.py b/quantlib/test/test_blackformula.py similarity index 88% rename from quantlib/test/blackformula.py rename to quantlib/test/test_blackformula.py index 24c02f8..6f72f62 100644 --- a/quantlib/test/blackformula.py +++ b/quantlib/test/test_blackformula.py @@ -43,10 +43,10 @@ def test_blackFormula(self): self.vol, self.df, self.displacement) - self.assertAlmostEquals(expected, res, delta=1e-4, - msg="Failed to calculate simple " - "Black-Scholes-Merton price rounded to " - "four decimal places.") + self.assertAlmostEqual(expected, res, delta=1e-4, + msg="Failed to calculate simple " + "Black-Scholes-Merton price rounded to " + "four decimal places.") def test_black_formula_implied_stdev(self): """Testing implied volatility calculator""" @@ -57,9 +57,9 @@ def test_black_formula_implied_stdev(self): self.forward, black_price, self.df) - self.assertAlmostEquals(expected, res, delta=1e-4, - msg="Failed to determine Implied Vol rounded " - "to a single vol bps.") + self.assertAlmostEqual(expected, res, delta=1e-4, + msg="Failed to determine Implied Vol rounded " + "to a single vol bps.") class BlackDeltaCalculatorTest(unittest.TestCase): @@ -109,7 +109,7 @@ def test_single_spot_delta(self): strike = black_calculator.strikeFromDelta(spot_delta_level) - self.assertAlmostEquals(expected_strike, strike, delta=1e-4) + self.assertAlmostEqual(expected_strike, strike, delta=1e-4) def test_spot_atm_delta_calculator(self): """Test for 0-delta straddle strike""" @@ -134,8 +134,9 @@ def test_spot_atm_delta_calculator(self): strike = black_calculator.atmStrike(ql.DeltaVolQuote.AtmDeltaNeutral) - self.assertAlmostEquals(expected_strike, strike, delta=1e-4) + self.assertAlmostEqual(expected_strike, strike, delta=1e-4) if __name__ == '__main__': - unittest.main() + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/bonds.py b/quantlib/test/test_bonds.py similarity index 97% rename from quantlib/test/bonds.py rename to quantlib/test/test_bonds.py index 054b9fd..5eba185 100644 --- a/quantlib/test/bonds.py +++ b/quantlib/test/test_bonds.py @@ -356,9 +356,5 @@ def test_interest_rates(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(FixedRateBondTest, "test")) - suite.addTest(unittest.makeSuite(FixedRateBondKwargsTest, "test")) - suite.addTest(unittest.makeSuite(AmortizingFixedRateBondTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/test_calendars.py b/quantlib/test/test_calendars.py new file mode 100644 index 0000000..3bc1e95 --- /dev/null +++ b/quantlib/test/test_calendars.py @@ -0,0 +1,40 @@ +""" + Copyright (C) 2023 Skandinaviska Enskilda Banken AB (publ) + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" +import itertools +import unittest + +import mxdevtool as ql + + +class JointCalendarTest(unittest.TestCase): + + def test_joint_calendar_holidays(self): + base_calendars = [ql.Sweden(), ql.Denmark(), ql.Finland(), ql.Norway(), ql.Iceland()] + joint_nordics = ql.JointCalendar(base_calendars) + start_date = ql.Date(1, ql.January, 2023) + end_date = ql.Date(31, ql.December, 2023) + + joint_holidays = set(joint_nordics.holidayList(start_date, end_date)) + base_holidays = [calendar.holidayList(start_date, end_date) for calendar in base_calendars] + base_holidays = set(itertools.chain.from_iterable(base_holidays)) + for holiday in base_holidays: + self.assertIn(holiday, joint_holidays) + + +if __name__ == "__main__": + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/capfloor.py b/quantlib/test/test_capfloor.py similarity index 95% rename from quantlib/test/capfloor.py rename to quantlib/test/test_capfloor.py index 206ed78..1ba4a4a 100644 --- a/quantlib/test/capfloor.py +++ b/quantlib/test/test_capfloor.py @@ -113,7 +113,5 @@ def testBachelierCapFloorEngine(self): if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CapFloorTest,'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/cms.py b/quantlib/test/test_cms.py similarity index 97% rename from quantlib/test/cms.py rename to quantlib/test/test_cms.py index 7d4d33f..9c5408f 100644 --- a/quantlib/test/cms.py +++ b/quantlib/test/test_cms.py @@ -104,7 +104,7 @@ def setUp(self): self.vegaWeightedSmileFit = False self.SabrVolCube2 = ql.SwaptionVolatilityStructureHandle( - ql.SwaptionVolCube2( + ql.InterpolatedSwaptionVolatilityCube( self.atmVol, self.optionTenors, self.swapTenors, @@ -141,7 +141,7 @@ def setUp(self): self.isAtmCalibrated = False ## self.SabrVolCube1 = ql.SwaptionVolatilityStructureHandle( - ql.SwaptionVolCube1( + ql.SabrSwaptionVolatilityCube( self.atmVol, self.optionTenors, self.swapTenors, @@ -302,7 +302,5 @@ def testParity(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CmsTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/coupons.py b/quantlib/test/test_coupons.py similarity index 97% rename from quantlib/test/coupons.py rename to quantlib/test/test_coupons.py index cb86e23..507c4b3 100644 --- a/quantlib/test/coupons.py +++ b/quantlib/test/test_coupons.py @@ -467,11 +467,5 @@ def test_sub_period_coupon_rate_spread(self): if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CashFlowsTest, 'test')) - suite.addTest(unittest.makeSuite(SubPeriodsCouponTest, 'test')) - suite.addTest(unittest.makeSuite(IborCouponTest, 'test')) - suite.addTest(unittest.makeSuite(OvernightCouponTest, 'test')) - suite.addTest(unittest.makeSuite(FixedRateCouponTest, 'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/currencies.py b/quantlib/test/test_currencies.py similarity index 89% rename from quantlib/test/currencies.py rename to quantlib/test/test_currencies.py index 80d2b5e..fbb0e97 100644 --- a/quantlib/test/currencies.py +++ b/quantlib/test/test_currencies.py @@ -42,7 +42,5 @@ def test_bespoke_currency_constructor(self): if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CurrencyTest, 'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/date.py b/quantlib/test/test_date.py similarity index 92% rename from quantlib/test/date.py rename to quantlib/test/test_date.py index df3a989..447aea4 100644 --- a/quantlib/test/date.py +++ b/quantlib/test/test_date.py @@ -71,7 +71,5 @@ def tearDown(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(DateTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/daycounters.py b/quantlib/test/test_daycounters.py similarity index 74% rename from quantlib/test/daycounters.py rename to quantlib/test/test_daycounters.py index a850c20..91779c3 100644 --- a/quantlib/test/daycounters.py +++ b/quantlib/test/test_daycounters.py @@ -3,8 +3,8 @@ class DayCountersTest(unittest.TestCase): - def runTest(self): - "Testing daycounters" + def test_bus252(self): + """Test Business252 daycounter""" calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond) @@ -21,7 +21,5 @@ def runTest(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(DayCountersTest()) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/test_equityindex.py b/quantlib/test/test_equityindex.py new file mode 100644 index 0000000..7330f5f --- /dev/null +++ b/quantlib/test/test_equityindex.py @@ -0,0 +1,67 @@ +""" + Copyright (C) 2023 Marcin Rybacki + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +""" + +import mxdevtool as ql +import unittest + + +EPSILON = 1.e-2 + +CAL = ql.TARGET() +DCT = ql.Actual365Fixed() +VALUATION_DATE = CAL.adjust(ql.Date(31, ql.January, 2023)) + + +def flat_rate(rate): + return ql.FlatForward( + 2, CAL, ql.QuoteHandle(ql.SimpleQuote(rate)), DCT) + + +class EquityIndexTest(unittest.TestCase): + def setUp(self): + ql.Settings.instance().evaluationDate = VALUATION_DATE + + self.interest_handle = ql.YieldTermStructureHandle(flat_rate(0.03)) + self.dividend_handle = ql.YieldTermStructureHandle(flat_rate(0.01)) + spot_handle = ql.QuoteHandle(ql.SimpleQuote(8690.0)) + + ql.IndexManager.instance().clearHistory("eq_idx") + self.equity_idx = ql.EquityIndex( + "eq_idx", CAL, self.interest_handle, self.dividend_handle, spot_handle) + + def test_equity_index_inspectors(self): + """Testing equity index inspectors""" + fail_msg = "Unable to replicate the properties of an equity index." + + self.assertEqual(self.equity_idx.name(), "eq_idx", msg=fail_msg) + self.assertEqual(self.equity_idx.fixingCalendar(), CAL, msg=fail_msg) + + def test_equity_index_projections(self): + """Testing equity index projections""" + fail_msg = "Failed to calculate the expected index projection." + + self.assertAlmostEqual( + self.equity_idx.fixing(VALUATION_DATE), 8690.0, delta=EPSILON, msg=fail_msg) + + future_dt = ql.Date(20, ql.May, 2030) + self.assertAlmostEqual( + self.equity_idx.fixing(future_dt), 10055.76, delta=EPSILON, msg=fail_msg) + + +if __name__ == '__main__': + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/extrapolation.py b/quantlib/test/test_extrapolation.py similarity index 88% rename from quantlib/test/extrapolation.py rename to quantlib/test/test_extrapolation.py index 3d81f94..2effb58 100644 --- a/quantlib/test/extrapolation.py +++ b/quantlib/test/test_extrapolation.py @@ -40,7 +40,5 @@ def testUnknownExpExtrapolation(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ExtrapolationTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/fdm.py b/quantlib/test/test_fdm.py similarity index 99% rename from quantlib/test/fdm.py rename to quantlib/test/test_fdm.py index 45ae384..cf09110 100644 --- a/quantlib/test/fdm.py +++ b/quantlib/test/test_fdm.py @@ -645,7 +645,5 @@ def testFdmZeroInnerValue(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(FdmTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/iborindex.py b/quantlib/test/test_iborindex.py similarity index 93% rename from quantlib/test/iborindex.py rename to quantlib/test/test_iborindex.py index 55dd56f..7eff3ad 100644 --- a/quantlib/test/iborindex.py +++ b/quantlib/test/test_iborindex.py @@ -70,7 +70,5 @@ def testTimeSeries(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(IborIndexTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/inflation.py b/quantlib/test/test_inflation.py similarity index 97% rename from quantlib/test/inflation.py rename to quantlib/test/test_inflation.py index 80721a4..2ef1bfa 100644 --- a/quantlib/test/inflation.py +++ b/quantlib/test/test_inflation.py @@ -283,7 +283,7 @@ def test_inflation_leg_payment_fom_indexation_without_seasonality(self): actual_payment=actual_inflation_leg_payment, expected_payment=expected_inflation_leg_payment, tolerance=EPSILON) - self.assertAlmostEquals( + self.assertAlmostEqual( first=actual_inflation_leg_payment, second=expected_inflation_leg_payment, delta=EPSILON, @@ -332,7 +332,7 @@ def test_swap_base_fixing_linear_indexation_without_seasonality(self): base_index=swap_base_fixing, expected_base_index=expected_swap_base_index, tolerance=EPSILON) - self.assertAlmostEquals( + self.assertAlmostEqual( first=swap_base_fixing, second=expected_swap_base_index, delta=EPSILON, @@ -368,7 +368,7 @@ def test_inflation_curve_base_fixing(self): base_fixing=curve_base_fixing, expected_base_fixing=expected_curve_base_fixing, tolerance=EPSILON) - self.assertAlmostEquals( + self.assertAlmostEqual( first=curve_base_fixing, second=expected_curve_base_fixing, msg=fail_msg, @@ -403,7 +403,7 @@ def test_lagged_fixing_method(self): actual_fixing=actual_fixing, expected_fixing=expected_fixing, tolerance=EPSILON) - self.assertAlmostEquals( + self.assertAlmostEqual( first=actual_fixing, second=expected_fixing, msg=fail_msg, @@ -411,7 +411,5 @@ def test_lagged_fixing_method(self): if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(InflationTest, 'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/instruments.py b/quantlib/test/test_instruments.py similarity index 90% rename from quantlib/test/instruments.py rename to quantlib/test/test_instruments.py index c129eb3..5a4ca3d 100644 --- a/quantlib/test/instruments.py +++ b/quantlib/test/test_instruments.py @@ -62,7 +62,5 @@ def testObservable(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(InstrumentTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/integrals.py b/quantlib/test/test_integrals.py similarity index 92% rename from quantlib/test/integrals.py rename to quantlib/test/test_integrals.py index eebbd91..bbebdf5 100644 --- a/quantlib/test/integrals.py +++ b/quantlib/test/test_integrals.py @@ -65,7 +65,5 @@ def testKronrod(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(IntegralTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/marketelements.py b/quantlib/test/test_marketelements.py similarity index 90% rename from quantlib/test/marketelements.py rename to quantlib/test/test_marketelements.py index 525e51c..322f49f 100644 --- a/quantlib/test/marketelements.py +++ b/quantlib/test/test_marketelements.py @@ -57,7 +57,5 @@ def testObservableHandle(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(MarketElementTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/ode.py b/quantlib/test/test_ode.py similarity index 88% rename from quantlib/test/ode.py rename to quantlib/test/test_ode.py index 5647254..cd26ce7 100644 --- a/quantlib/test/ode.py +++ b/quantlib/test/test_ode.py @@ -42,7 +42,5 @@ def test2dODE(self): if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(OdeTest,'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/options.py b/quantlib/test/test_options.py similarity index 92% rename from quantlib/test/options.py rename to quantlib/test/test_options.py index c199cd8..ecf46dd 100644 --- a/quantlib/test/options.py +++ b/quantlib/test/test_options.py @@ -54,8 +54,7 @@ def testFdHestonHullWhite(self): option.setPricingEngine( ql.FdHestonHullWhiteVanillaEngine( ql.HestonModel(heston_process), hull_white_process, -0.5, - 10, 200, 25, 10, - controlVariate=True + 10, 200, 25, 10, 0, True ) ) @@ -106,7 +105,5 @@ def testAnalyticHestonHullWhite(self): if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(OptionsTest,'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/ratehelpers.py b/quantlib/test/test_ratehelpers.py similarity index 97% rename from quantlib/test/ratehelpers.py rename to quantlib/test/test_ratehelpers.py index 803ab97..b6a5acd 100644 --- a/quantlib/test/ratehelpers.py +++ b/quantlib/test/test_ratehelpers.py @@ -140,8 +140,7 @@ def build_eur_curve(self, quotes_date): self.oisHelpers = [ ql.OISRateHelper( settlementDays, ql.Period(n, unit), - ql.QuoteHandle(self.ois[(n, unit)]), self.on_index, - self.discounting_yts_handle) + ql.QuoteHandle(self.ois[(n, unit)]), self.on_index) for n, unit in self.ois.keys() ] @@ -204,7 +203,6 @@ def test_ois_default_calendar(self): ql.Following) self.assertEqual(expected_date, ql.Date(4, 4, 2018)) ois = ql.MakeOIS(ql.Period('1Y'), eonia, -0.003, ql.Period(0, ql.Days)) - print(ois.startDate()) self.assertEqual(expected_date, ois.startDate()) def tearDown(self): @@ -294,7 +292,7 @@ def build_eur_curve(self, quotes_date): oisHelpers = [ ql.OISRateHelper( settlementDays, ql.Period(n, unit), - ql.QuoteHandle(ois[(n, unit)]), on_index, discounting_yts_handle + ql.QuoteHandle(ois[(n, unit)]), on_index ) for n, unit in ois.keys() ] @@ -680,7 +678,7 @@ def assertImpliedQuotes( # Trigger bootstrap discount_at_origin = term_structure.discount(settlement_date) - self.assertAlmostEquals( + self.assertAlmostEqual( first=discount_at_origin, second=1.0, delta=eps) for q, h in zip(self.cross_currency_basis_quotes, helpers): @@ -696,7 +694,7 @@ def assertImpliedQuotes( actual_rate=actual_rate, expected_rate=expected_rate, tolerance=eps) - self.assertAlmostEquals( + self.assertAlmostEqual( first=actual_rate, second=expected_rate, delta=eps, @@ -735,11 +733,5 @@ def tearDown(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(FixedRateBondHelperTest, "test")) - suite.addTest(unittest.makeSuite(OISRateHelperTest, "test")) - suite.addTest(unittest.makeSuite(FxSwapRateHelperTest, "test")) - suite.addTest(unittest.makeSuite( - CrossCurrencyBasisSwapRateHelperTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/sabr.py b/quantlib/test/test_sabr.py similarity index 95% rename from quantlib/test/sabr.py rename to quantlib/test/test_sabr.py index 16257be..e6cea5d 100644 --- a/quantlib/test/sabr.py +++ b/quantlib/test/test_sabr.py @@ -116,7 +116,5 @@ def testSabrPdeVsCevPdeVsAnalyticCev(self): msg="Unable to match PDE CEV value with analytic CEV value") if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(SabrTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/slv.py b/quantlib/test/test_slv.py similarity index 82% rename from quantlib/test/slv.py rename to quantlib/test/test_slv.py index 7ed084b..091d32f 100644 --- a/quantlib/test/slv.py +++ b/quantlib/test/test_slv.py @@ -147,10 +147,32 @@ def testSlvProcessAsBlackScholes(self): "double barrier option price with Black-Scholes Double Barrier Binary Engine", ) + def testFixedLocalVolSurface(self): + """ Testing FixedLocalVolSurface interpolation """ + + dc = ql.Actual365Fixed() + maturities = [ql.Date(1, 3, 2020), ql.Date(1, 6, 2020)] + strikes = [60, 100, 130] + local_vols = [[0.2, 0.3], [0.25, 0.4], [0.3, 0.4]] + + fixed_local_vol_surf = ql.FixedLocalVolSurface( + self.todaysDate, + [dc.yearFraction(self.todaysDate, d) for d in maturities], + strikes, + local_vols, + dc + ) + + fixed_local_vol_surf.setInterpolation("linear") + + self.assertEqual(60, fixed_local_vol_surf.minStrike()) + self.assertEqual(130, fixed_local_vol_surf.maxStrike()) + + self.assertAlmostEqual(0.2, fixed_local_vol_surf.localVol(ql.Date(1, 3, 2020), 60)) + self.assertAlmostEqual(0.3, fixed_local_vol_surf.localVol(ql.Date(1, 6, 2020), 60)) + self.assertAlmostEqual(0.25, fixed_local_vol_surf.localVol(ql.Date(16, 4, 2020), 60)) if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(SlvTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/solvers1d.py b/quantlib/test/test_solvers1d.py similarity index 93% rename from quantlib/test/solvers1d.py rename to quantlib/test/test_solvers1d.py index 1866cdc..49d67f3 100644 --- a/quantlib/test/solvers1d.py +++ b/quantlib/test/test_solvers1d.py @@ -28,7 +28,7 @@ def derivative(self, x): class Solver1DTest(unittest.TestCase): - def runTest(self): + def test_solve(self): "Testing 1-D solvers" for factory in [ql.Brent, ql.Bisection, ql.FalsePosition, ql.Ridder, ql.Secant]: solver = factory() @@ -87,7 +87,5 @@ def runTest(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(Solver1DTest()) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/swap.py b/quantlib/test/test_swap.py similarity index 54% rename from quantlib/test/swap.py rename to quantlib/test/test_swap.py index b15976a..70978af 100644 --- a/quantlib/test/swap.py +++ b/quantlib/test/test_swap.py @@ -1,5 +1,6 @@ """ Copyright (C) 2021 Marcin Rybacki + Copyright (C) 2023 Marcin Rybacki This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - http://quantlib.org/ @@ -25,7 +26,21 @@ DCT = ql.Actual365Fixed() -VALUATION_DATE = CAL.adjust(ql.Date(1, ql.June, 2021)) +IR_FIXINGS = [(ql.Date(3, ql.January, 2023), 0.033), + (ql.Date(4, ql.January, 2023), 0.033), + (ql.Date(5, ql.January, 2023), 0.033), + (ql.Date(6, ql.January, 2023), 0.033), + (ql.Date(9, ql.January, 2023), 0.03), + (ql.Date(10, ql.January, 2023), 0.03), + (ql.Date(11, ql.January, 2023), 0.03), + (ql.Date(12, ql.January, 2023), 0.03), + (ql.Date(13, ql.January, 2023), 0.03), + (ql.Date(17, ql.January, 2023), 0.03), + (ql.Date(20, ql.January, 2023), 0.03), + (ql.Date(23, ql.January, 2023), 0.03), + (ql.Date(24, ql.January, 2023), 0.03), + (ql.Date(25, ql.January, 2023), 0.03), + (ql.Date(26, ql.January, 2023), 0.03)] def flat_rate(rate): @@ -35,7 +50,8 @@ def flat_rate(rate): class ZeroCouponSwapTest(unittest.TestCase): def setUp(self): - ql.Settings.instance().evaluationDate = VALUATION_DATE + valuation_date = CAL.adjust(ql.Date(1, ql.June, 2021)) + ql.Settings.instance().evaluationDate = valuation_date self.nominal_ts_handle = ql.YieldTermStructureHandle(flat_rate(0.007)) self.ibor_idx = ql.Euribor6M(self.nominal_ts_handle) self.engine = ql.DiscountingSwapEngine(self.nominal_ts_handle) @@ -132,11 +148,101 @@ def test_zero_coupon_swap_legs(self): fail_msg_flt = """Floating leg cash flow type should be SubPeriodsCoupon but was {actual}. """.format(actual=type(flt_cf)) - self.assertTrue(isinstance(flt_cf, ql.SubPeriodsCoupon), msg=fail_msg_flt) + self.assertTrue(isinstance( + flt_cf, ql.SubPeriodsCoupon), msg=fail_msg_flt) + + +class EquityTotalReturnSwapTest(unittest.TestCase): + def setUp(self): + valuation_date = ql.Date(27, ql.January, 2023) + ql.Settings.instance().evaluationDate = valuation_date + + self.interest_handle = ql.YieldTermStructureHandle(flat_rate(0.03)) + self.dividend_handle = ql.YieldTermStructureHandle(flat_rate(0.0)) + equity_spot = ql.QuoteHandle(ql.SimpleQuote(8690.0)) + + self.equity_idx = ql.EquityIndex( + "eq_idx", + CAL, + self.interest_handle, + self.dividend_handle, + equity_spot) + ql.IndexManager.instance().clearHistory(self.equity_idx.name()) + self.equity_idx.addFixing(ql.Date(5, ql.January, 2023), 9010.0) + + self.ibor_idx = ql.USDLibor( + ql.Period(3, ql.Months), self.interest_handle) + ql.IndexManager.instance().clearHistory(self.ibor_idx.name()) + self.sofr_idx = ql.Sofr(self.interest_handle) + ql.IndexManager.instance().clearHistory(self.sofr_idx.name()) + + for f_dt, f_val in IR_FIXINGS: + self.ibor_idx.addFixing(f_dt, f_val) + self.sofr_idx.addFixing(f_dt, f_val) + + def build_trs(self, interest_idx, start, end, margin=0.025): + schedule = ql.Schedule( + start, + end, + interest_idx.tenor(), + interest_idx.fixingCalendar(), + interest_idx.businessDayConvention(), + interest_idx.businessDayConvention(), + ql.DateGeneration.Backward, + False) + return ql.EquityTotalReturnSwap(ql.Swap.Receiver, + 1.0e6, + schedule, + self.equity_idx, + interest_idx, + DCT, + margin) + + def test_trs_interest_rate_index(self): + """Testing equity total return swap interest rate index""" + start = ql.Date(5, ql.January, 2023) + end = ql.Date(5, ql.April, 2023) + + trs_vs_ibor = self.build_trs(self.ibor_idx, start, end) + trs_vs_sofr = self.build_trs(self.sofr_idx, start, end) + + fail_msg = "Incorrect interest rate index set to TRS." + + self.assertEqual(trs_vs_ibor.interestRateIndex().name(), + "USDLibor3M Actual/360", + msg=fail_msg) + self.assertEqual(trs_vs_sofr.interestRateIndex().name(), + "SOFRON Actual/360", + msg=fail_msg) + + def test_trs_npv(self): + """Testing equity total return swap NPV""" + start = ql.Date(5, ql.January, 2023) + end = ql.Date(5, ql.April, 2023) + + pricer = ql.DiscountingSwapEngine(self.interest_handle) + + trs_vs_ibor = self.build_trs(self.ibor_idx, start, end) + trs_vs_ibor.setPricingEngine(pricer) + + trs_vs_sofr = self.build_trs(self.sofr_idx, start, end) + trs_vs_sofr.setPricingEngine(pricer) + + par_trs_vs_ibor = self.build_trs( + self.ibor_idx, start, end, trs_vs_ibor.fairMargin()) + par_trs_vs_ibor.setPricingEngine(pricer) + par_trs_vs_sofr = self.build_trs( + self.sofr_idx, start, end, trs_vs_sofr.fairMargin()) + par_trs_vs_sofr.setPricingEngine(pricer) + + fail_msg = "Par TRS expected to have NPV equal to zero." + + self.assertAlmostEqual( + par_trs_vs_ibor.NPV(), 0.0, delta=EPSILON, msg=fail_msg) + self.assertAlmostEqual( + par_trs_vs_sofr.NPV(), 0.0, delta=EPSILON, msg=fail_msg) if __name__ == '__main__': - print('testing QuantLib ' + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ZeroCouponSwapTest, 'test')) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/swaption.py b/quantlib/test/test_swaption.py similarity index 97% rename from quantlib/test/swaption.py rename to quantlib/test/test_swaption.py index 3cf6808..cf6ee5f 100644 --- a/quantlib/test/swaption.py +++ b/quantlib/test/test_swaption.py @@ -171,7 +171,7 @@ def _assert_swaption_annuity(self, method=SETTLEMENT_METHOD_MAP[m], annuity=annuity, expected_annuity=expected_annuity) - self.assertAlmostEquals( + self.assertAlmostEqual( first=annuity, second=expected_annuity, delta=EPSILON, @@ -191,7 +191,5 @@ def test_swaption_annuity_bachelier_model(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(SwaptionTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/termstructures.py b/quantlib/test/test_termstructures.py similarity index 97% rename from quantlib/test/termstructures.py rename to quantlib/test/test_termstructures.py index f1c981d..7e3260d 100644 --- a/quantlib/test/termstructures.py +++ b/quantlib/test/test_termstructures.py @@ -169,7 +169,7 @@ def testCompositeZeroYieldStructure(self): actual zero rate: {actual} """.format(expected=expectedZeroRate, actual=actualZeroRate) - self.assertAlmostEquals( + self.assertAlmostEqual( first=expectedZeroRate, second=actualZeroRate, delta=1.0e-12, @@ -201,7 +201,7 @@ def testUltimateForwardTermStructure(self): """.format(timeToMaturity=t, expected=expectedForward, actual=actualForward) - self.assertAlmostEquals( + self.assertAlmostEqual( first=expectedForward, second=actualForward, delta=1.0e-12, @@ -289,7 +289,7 @@ def testQuantoTermStructure(self): vanilla_pv=vanilla_option_pv ) - self.assertAlmostEquals( + self.assertAlmostEqual( quanto_option_pv, vanilla_option_pv, delta=1e-12, @@ -319,7 +319,5 @@ def testLazyObject(self): self.termStructure.unfreeze() if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TermStructureTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/quantlib/test/volatilities.py b/quantlib/test/test_volatilities.py similarity index 97% rename from quantlib/test/volatilities.py rename to quantlib/test/test_volatilities.py index bf99c3b..c59d4ec 100644 --- a/quantlib/test/volatilities.py +++ b/quantlib/test/test_volatilities.py @@ -193,7 +193,7 @@ def build_linear_swaption_cube( vega_weighted_smile_fit=False): vol_spreads = [[ql.QuoteHandle(ql.SimpleQuote(v)) for v in row] for row in vol_spreads] - cube = ql.SwaptionVolCube2( + cube = ql.InterpolatedSwaptionVolatilityCube( ql.SwaptionVolatilityStructureHandle(volatility_matrix), spread_opt_tenors, spread_swap_tenors, @@ -232,7 +232,7 @@ def build_sabr_swaption_cube( for row in vol_spreads] guess = sabr_parameters_guess( len(spread_opt_tenors), len(spread_swap_tenors)) - cube = ql.SwaptionVolCube1( + cube = ql.SabrSwaptionVolatilityCube( ql.SwaptionVolatilityStructureHandle(volatility_matrix), spread_opt_tenors, spread_swap_tenors, @@ -296,7 +296,7 @@ def _assert_atm_strike( swap_tenor=swap_tenor, strike=actual_atm_strike, replicated_strike=expected_atm_strike) - self.assertAlmostEquals( + self.assertAlmostEqual( first=actual_atm_strike, second=expected_atm_strike, delta=TOLERANCE, @@ -331,7 +331,7 @@ def _assert_atm_vol( vol=actual_vol, expected_vol=expected_vol, eps=epsilon) - self.assertAlmostEquals( + self.assertAlmostEqual( first=actual_vol, second=expected_vol, delta=epsilon, @@ -367,7 +367,7 @@ def _assert_vol_spread( vol=actual_vol, expected_vol=expected_vol, eps=epsilon) - self.assertAlmostEquals( + self.assertAlmostEqual( first=actual_vol, second=expected_vol, delta=epsilon, @@ -527,6 +527,13 @@ class SviSmileSectionTest(unittest.TestCase): def setUp(self): ql.Settings.instance().evaluationDate = ql.Date(3, ql.May, 2022) + def tearDown(self): + # The objects created in this test are not immediately garbage-collected + # by PyPy, and can throw errors when the global evaluation date changes. + # Thus, we need to force a collection when we're done with them. + import gc + gc.collect() + def test_svi_smile_section(self): """Testing the SviSmileSection against already fitted parameters""" expiry_date = ql.Date(16, ql.December, 2022) @@ -623,9 +630,5 @@ def testLocalVolCalibration(self): if __name__ == "__main__": - print("testing QuantLib " + ql.__version__) - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(SwaptionVolatilityCubeTest, "test")) - suite.addTest(unittest.makeSuite(SviSmileSectionTest, "test")) - suite.addTest(unittest.makeSuite(AndreasenHugeVolatilityTest, "test")) - unittest.TextTestRunner(verbosity=2).run(suite) + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) From 0877358d082888081672a9b5016a8324a79d6a92 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Sat, 28 Oct 2023 21:54:11 +0900 Subject: [PATCH 52/54] update readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 65834c0..73ee4b9 100644 --- a/README.md +++ b/README.md @@ -797,6 +797,10 @@ For source code, check this repository. # Release History +## 1.0.32.2 (2023-10-28) +- update base Quatlib 1.32 +- add calcs ( overnight, ibor, swap, bond ) + ## 1.0.29.17 (2023-01-28) - QuantLib dependency is redegined - Version Syntex is changed @@ -933,6 +937,11 @@ All scenario results are generated by npz file format. you can read directly usi You can download Npzee Viewer in [WindowStore](https://www.microsoft.com/store/apps/9N19KHP7G2P4) or [WebPage](https://npzee.montrix.co.kr). +
+ +# Donation +Etherium - aaaaaaaaaaaaaaaa +
# License From c05a29e6f0fdebdd421a35ea2d954f7fdf2d6ff1 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Sat, 28 Oct 2023 22:15:20 +0900 Subject: [PATCH 53/54] update 1.0.32.3 --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 73ee4b9..0350d63 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ MxDevTool(Beta) : Financial Library ========================== ![image](https://img.shields.io/badge/platform-windows_64|_linux_64-red) -![image](https://img.shields.io/badge/python-3.6|3.7|3.8|3.9|3.10|3.11-blue) -![image](https://img.shields.io/badge/version-1.0.29.17-green.svg) +![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) +![image](https://img.shields.io/badge/version-1.0.32.3-green.svg) ![image](https://img.shields.io/badge/platform-macOS_64-red) ![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) @@ -138,6 +138,7 @@ import mxdevtool.shock as mx_s import mxdevtool.xenarix as xen import mxdevtool.termstructures as ts import mxdevtool.quotes as mx_q +import mxdevtool.marketconvension as mx_m import mxdevtool.data.providers as mx_dp import mxdevtool.data.repositories as mx_dr import mxdevtool.utils as utils @@ -251,7 +252,13 @@ ShortRate Model : ```python hw1f_spot3m = hw1f.spot('hw1f_spot3m', maturity=mx.Period(3, mx.Months), compounding=mx.Compounded) -hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startPeriod=mx.Period(6, mx.Months), maturity=mx.Period(3, mx.Months), compounding=mx.Compounded) +hw1f_overnight = hw1f.overnight('hw1f_sofr', mx_m.IndexFactory().get_overnightIndex('sofr')) +hw1f_libor = hw1f.ibor('libor3m', mx_m.IndexFactory().get_iborIndex('libor', mx.Period(3, mx.Months))) +hw1f_swap = hw1f.swaprate('cms5y', mx_m.IndexFactory().get_swapIndex('krwirs', mx.Period(5, mx.Years), mx.Period(3, mx.Months))) +hw1f_bond = hw1f.bondrate('cmt10y', mx_m.IndexFactory().get_bondIndex('ktb', mx.Period(5, mx.Years), mx.Period(6, mx.Months))) + +# hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startTenor=mx.Period(6, mx.Months), maturityTenor=mx.Period(3, mx.Months), compounding=mx.Compounded) +hw1f_forward6m3m = hw1f.forward('hw1f_forward6m3m', startTenor=0.5, maturityTenor=3.0, compounding=mx.Compounded) hw1f_discountFactor = hw1f.discountFactor('hw1f_discountFactor') hw1f_discountBond3m = hw1f.discountBond('hw1f_discountBond3m', maturity=mx.Period(3, mx.Months)) @@ -800,6 +807,8 @@ For source code, check this repository. ## 1.0.32.2 (2023-10-28) - update base Quatlib 1.32 - add calcs ( overnight, ibor, swap, bond ) +- terminate support python 3.6 3.7 +- add coin address for donation ## 1.0.29.17 (2023-01-28) - QuantLib dependency is redegined @@ -940,7 +949,9 @@ You can download Npzee Viewer in [WindowStore](https://www.microsoft.com/store/a
# Donation -Etherium - aaaaaaaaaaaaaaaa + +Bitcoin - 3CK4Two4zCndGi5bSvNPEFMEjnzSExAyDs +Etherium - 0x976a09a3cbb38def4eda10291080c28c41926318
From 98e9c02f67659af39ccbd2df774aab4fb6710b46 Mon Sep 17 00:00:00 2001 From: montrixdev Date: Mon, 30 Oct 2023 21:30:49 +0900 Subject: [PATCH 54/54] update version --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0350d63..52bc74f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ MxDevTool(Beta) : Financial Library ========================== -![image](https://img.shields.io/badge/platform-windows_64|_linux_64-red) +![image](https://img.shields.io/badge/platform-windows_64_|_linux_64-red) ![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) ![image](https://img.shields.io/badge/version-1.0.32.3-green.svg) ![image](https://img.shields.io/badge/platform-macOS_64-red) -![image](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue) +![image](https://img.shields.io/badge/python-3.10|3.11-blue) MxDevTool is a Integrated Developing Tools for financial analysis. Now is Beta Release version. The Project is built on top of QuantLib-Python. @@ -807,7 +807,8 @@ For source code, check this repository. ## 1.0.32.2 (2023-10-28) - update base Quatlib 1.32 - add calcs ( overnight, ibor, swap, bond ) -- terminate support python 3.6 3.7 +- terminate support python 3.6 3.7 on linux +- terminate support python 3.8 3.9 on macos - add coin address for donation ## 1.0.29.17 (2023-01-28) @@ -950,8 +951,8 @@ You can download Npzee Viewer in [WindowStore](https://www.microsoft.com/store/a # Donation -Bitcoin - 3CK4Two4zCndGi5bSvNPEFMEjnzSExAyDs -Etherium - 0x976a09a3cbb38def4eda10291080c28c41926318 +* Bitcoin - 3CK4Two4zCndGi5bSvNPEFMEjnzSExAyDs +* Etherium - 0x976a09a3cbb38def4eda10291080c28c41926318