diff --git a/.github/workflows/mxdevtool-package.yml b/.github/workflows/mxdevtool-package.yml
new file mode 100644
index 0000000..451a87d
--- /dev/null
+++ b/.github/workflows/mxdevtool-package.yml
@@ -0,0 +1,68 @@
+# 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:
+ test:
+ runs-on: ${{ matrix.os }}
+ 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"]
+ python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
+ exclude:
+ - 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"
+ # 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 }}
+ uses: actions/setup-python@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+ # architecture: "arm64"
+
+ env:
+ AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install flake8 numpy QuantLib
+ 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
+ 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 ./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
diff --git a/.gitignore b/.gitignore
index bcfe6a0..11083db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,8 +134,9 @@ dmypy.json
# csv results
*.csv
-# npz results
+# numpy results
*.npz
+*.npy
# vscode
./.vscode
@@ -143,3 +144,8 @@ settings.json
# repository
xenrepo/
+
+clear_output.bat
+*.html
+
+*.json
\ No newline at end of file
diff --git a/README.md b/README.md
index 214ed29..52bc74f 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,15 @@
MxDevTool(Beta) : Financial Library
==========================
-
-
-
+
+
+

-
+
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 Project is built on top of QuantLib-Python.
Xenarix(Economic Scenario Generator) is moved into submodule of MxDevTool.
@@ -30,6 +29,12 @@ Functionalty :
+# Version Rule
+
+Version syntex is {Major}-{Miner}-{QuantLib_Version}-{Patch}
+
+
+
# Installation
To install MxDevTool, simply use pip :
@@ -110,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)
@@ -126,13 +131,14 @@ 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
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
@@ -246,10 +252,21 @@ 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))
+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) #
```
### Calcs
@@ -320,28 +337,45 @@ 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
```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)
+
+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
@@ -388,32 +422,28 @@ 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, ... ))
+calculated_tpos_264 = results.tPosSlice(t_pos, scenCount)
+
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)
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)
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)
multipath_all_using_time = results.timeSlice(time=t_time) # all t_pos data
```
@@ -507,41 +537,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.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
@@ -552,7 +583,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)
@@ -703,43 +734,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')
@@ -806,6 +804,42 @@ 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 )
+- 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)
+- 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
+- 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
+- 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
+
## 0.8.35.3 (2021-8-12)
- Library Dependencies are removed ( pandas, jinja2, matplot )
- ZeroYieldCurve CurveType error bug fix
@@ -913,6 +947,13 @@ 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
+
+* Bitcoin - 3CK4Two4zCndGi5bSvNPEFMEjnzSExAyDs
+* Etherium - 0x976a09a3cbb38def4eda10291080c28c41926318
+
# License
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/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 5de06e9..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 ))
-
-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/pricing/ELSStepDown.py b/pricing/stepdown.py
similarity index 73%
rename from pricing/ELSStepDown.py
rename to pricing/stepdown.py
index ba378a0..ffad8f9 100644
--- a/pricing/ELSStepDown.py
+++ b/pricing/stepdown.py
@@ -3,10 +3,11 @@
import mxdevtool.xenarix as xen
import mxdevtool.termstructures as ts
-filename = './test_stepdown.npz'
-refDate = mx.Date(2012,8,22)
+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
@@ -21,9 +22,9 @@ def __init__(self, notional, issue_date, maturity_date, initial_values, ki, ki_f
self.coupon_tpos = []
self.discount_factors = []
- def initialize_timeGrid(self, timeGrid):
- if not isinstance(timeGrid, mx.core_TimeGrid):
- raise Exception('timeGrid is required')
+ 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]
@@ -43,7 +44,7 @@ def precalculation_discountFactors(self, discountCurve):
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)
@@ -70,8 +71,6 @@ def value(self, multi_path, discount):
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]
@@ -89,20 +88,20 @@ def value(self, multi_path, discount):
def build_stepdown():
notional = 10000
- issue_date = mx.Date(2012,8,22)
- maturity_date = mx.Date(2012,8,22)
+ 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(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)]
+ 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)
@@ -119,48 +118,50 @@ def build_scenario(overwrite=True):
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)
- timeGrid = mx.TimeEqualGrid(refDate, 3, 365)
-
- # random
- rsg = xen.Rsg(sampleNum=5000)
+ # random
+ rsg = xen.Rsg(sampleNum=10000)
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)
+class McPricer:
+ def __init__(self):
+ pass
- v = 0
+ def npv(scen, payoff: StepDownPayoff, discount_curve):
+ results = xen.ScenarioResults(filename)
- for i in range(simulNum):
- path = results[i]
+ payoff.initialize_timeGrid(results.timegrid)
- v += payoff.value(path, discount_curve)
+ simulNum = results.simulNum
+
+ v = 0
- if i != 0 and i % 5000 == 0:
- print(i, v / i)
+ for i in range(simulNum):
+ path = results[i]
+ v += payoff.value(path, discount_curve)
- print(simulNum, v / simulNum)
+ return v / simulNum
def test():
mx.Settings.instance().setEvaluationDate(refDate)
build_scenario(overwrite=True)
- pricing()
+ stepdown = build_stepdown()
+ curve = ts.FlatForward(refDate, 0.03, mx.Actual365Fixed(), mx.Semiannual)
+ npv = McPricer().npv(stepdown, curve)
+ print(npv)
+
if __name__ == "__main__":
test()
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
+
+[](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..9fdd9bb
--- /dev/null
+++ b/quantlib/test/QuantLibTestSuite.py
@@ -0,0 +1,33 @@
+"""
+ 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 os
+import sys
+import unittest
+
+import mxdevtool as ql
+
+
+def test():
+ print('testing QuantLib', ql.__version__)
+ sys.argv[1:1] = ['discover', '-s', os.path.dirname(__file__)]
+ unittest.main(module=None, verbosity=2)
+
+
+if __name__ == '__main__':
+ test()
diff --git a/quantlib/test/__init__.py b/quantlib/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/quantlib/test/test_americanquantooption.py b/quantlib/test/test_americanquantooption.py
new file mode 100644
index 0000000..d2a7e32
--- /dev/null
+++ b/quantlib/test/test_americanquantooption.py
@@ -0,0 +1,101 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_assetswap.py b/quantlib/test/test_assetswap.py
new file mode 100644
index 0000000..659e780
--- /dev/null
+++ b/quantlib/test/test_assetswap.py
@@ -0,0 +1,5355 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_blackformula.py b/quantlib/test/test_blackformula.py
new file mode 100644
index 0000000..6f72f62
--- /dev/null
+++ b/quantlib/test/test_blackformula.py
@@ -0,0 +1,142 @@
+# 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.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"""
+ 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.assertAlmostEqual(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.assertAlmostEqual(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.assertAlmostEqual(expected_strike, strike, delta=1e-4)
+
+
+if __name__ == '__main__':
+ print("testing QuantLib", ql.__version__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_bonds.py b/quantlib/test/test_bonds.py
new file mode 100644
index 0000000..5eba185
--- /dev/null
+++ b/quantlib/test/test_bonds.py
@@ -0,0 +1,360 @@
+"""
+ 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__)
+ 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/test_capfloor.py b/quantlib/test/test_capfloor.py
new file mode 100644
index 0000000..1ba4a4a
--- /dev/null
+++ b/quantlib/test/test_capfloor.py
@@ -0,0 +1,117 @@
+# 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_cms.py b/quantlib/test/test_cms.py
new file mode 100644
index 0000000..9c5408f
--- /dev/null
+++ b/quantlib/test/test_cms.py
@@ -0,0 +1,306 @@
+"""
+ 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.InterpolatedSwaptionVolatilityCube(
+ 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.SabrSwaptionVolatilityCube(
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_coupons.py b/quantlib/test/test_coupons.py
new file mode 100644
index 0000000..507c4b3
--- /dev/null
+++ b/quantlib/test/test_coupons.py
@@ -0,0 +1,471 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_currencies.py b/quantlib/test/test_currencies.py
new file mode 100644
index 0000000..fbb0e97
--- /dev/null
+++ b/quantlib/test/test_currencies.py
@@ -0,0 +1,46 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_date.py b/quantlib/test/test_date.py
new file mode 100644
index 0000000..447aea4
--- /dev/null
+++ b/quantlib/test/test_date.py
@@ -0,0 +1,75 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_daycounters.py b/quantlib/test/test_daycounters.py
new file mode 100644
index 0000000..91779c3
--- /dev/null
+++ b/quantlib/test/test_daycounters.py
@@ -0,0 +1,25 @@
+import mxdevtool as ql
+import unittest
+
+
+class DayCountersTest(unittest.TestCase):
+ def test_bus252(self):
+ """Test Business252 daycounter"""
+
+ 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__)
+ 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/test_extrapolation.py b/quantlib/test/test_extrapolation.py
new file mode 100644
index 0000000..2effb58
--- /dev/null
+++ b/quantlib/test/test_extrapolation.py
@@ -0,0 +1,44 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_fdm.py b/quantlib/test/test_fdm.py
new file mode 100644
index 0000000..cf09110
--- /dev/null
+++ b/quantlib/test/test_fdm.py
@@ -0,0 +1,649 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_iborindex.py b/quantlib/test/test_iborindex.py
new file mode 100644
index 0000000..7eff3ad
--- /dev/null
+++ b/quantlib/test/test_iborindex.py
@@ -0,0 +1,74 @@
+# 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_inflation.py b/quantlib/test/test_inflation.py
new file mode 100644
index 0000000..2ef1bfa
--- /dev/null
+++ b/quantlib/test/test_inflation.py
@@ -0,0 +1,415 @@
+"""
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ first=actual_fixing,
+ second=expected_fixing,
+ msg=fail_msg,
+ delta=EPSILON)
+
+
+if __name__ == '__main__':
+ print("testing QuantLib", ql.__version__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_instruments.py b/quantlib/test/test_instruments.py
new file mode 100644
index 0000000..5a4ca3d
--- /dev/null
+++ b/quantlib/test/test_instruments.py
@@ -0,0 +1,66 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_integrals.py b/quantlib/test/test_integrals.py
new file mode 100644
index 0000000..bbebdf5
--- /dev/null
+++ b/quantlib/test/test_integrals.py
@@ -0,0 +1,69 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_marketelements.py b/quantlib/test/test_marketelements.py
new file mode 100644
index 0000000..322f49f
--- /dev/null
+++ b/quantlib/test/test_marketelements.py
@@ -0,0 +1,61 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_ode.py b/quantlib/test/test_ode.py
new file mode 100644
index 0000000..cd26ce7
--- /dev/null
+++ b/quantlib/test/test_ode.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 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_options.py b/quantlib/test/test_options.py
new file mode 100644
index 0000000..ecf46dd
--- /dev/null
+++ b/quantlib/test/test_options.py
@@ -0,0 +1,109 @@
+"""
+ 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, 0, 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_ratehelpers.py b/quantlib/test/test_ratehelpers.py
new file mode 100644
index 0000000..b6a5acd
--- /dev/null
+++ b/quantlib/test/test_ratehelpers.py
@@ -0,0 +1,737 @@
+# 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)
+ 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))
+ 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
+ )
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_sabr.py b/quantlib/test/test_sabr.py
new file mode 100644
index 0000000..e6cea5d
--- /dev/null
+++ b/quantlib/test/test_sabr.py
@@ -0,0 +1,120 @@
+"""
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_slv.py b/quantlib/test/test_slv.py
new file mode 100644
index 0000000..091d32f
--- /dev/null
+++ b/quantlib/test/test_slv.py
@@ -0,0 +1,178 @@
+"""
+ 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",
+ )
+
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_solvers1d.py b/quantlib/test/test_solvers1d.py
new file mode 100644
index 0000000..49d67f3
--- /dev/null
+++ b/quantlib/test/test_solvers1d.py
@@ -0,0 +1,91 @@
+"""
+ 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 test_solve(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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_swap.py b/quantlib/test/test_swap.py
new file mode 100644
index 0000000..70978af
--- /dev/null
+++ b/quantlib/test/test_swap.py
@@ -0,0 +1,248 @@
+"""
+ 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/
+
+ 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()
+
+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):
+ return ql.FlatForward(
+ 2, CAL, ql.QuoteHandle(ql.SimpleQuote(rate)), ql.Actual365Fixed())
+
+
+class ZeroCouponSwapTest(unittest.TestCase):
+ def setUp(self):
+ 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)
+
+ 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)
+
+
+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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_swaption.py b/quantlib/test/test_swaption.py
new file mode 100644
index 0000000..cf6ee5f
--- /dev/null
+++ b/quantlib/test/test_swaption.py
@@ -0,0 +1,195 @@
+"""
+ 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.assertAlmostEqual(
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_termstructures.py b/quantlib/test/test_termstructures.py
new file mode 100644
index 0000000..7e3260d
--- /dev/null
+++ b/quantlib/test/test_termstructures.py
@@ -0,0 +1,323 @@
+"""
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/quantlib/test/test_volatilities.py b/quantlib/test/test_volatilities.py
new file mode 100644
index 0000000..c59d4ec
--- /dev/null
+++ b/quantlib/test/test_volatilities.py
@@ -0,0 +1,634 @@
+"""
+ 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.InterpolatedSwaptionVolatilityCube(
+ 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.SabrSwaptionVolatilityCube(
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ 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.assertAlmostEqual(
+ 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 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)
+ 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__)
+ unittest.main(verbosity=2)
diff --git a/run_test_etc.py b/run_test_etc.py
index 862c295..1005a4f 100644
--- a/run_test_etc.py
+++ b/run_test_etc.py
@@ -4,37 +4,39 @@
import math
def timegrid_test():
+ print('timegrid test...')
+
ref_date = mx.Date.todaysDate()
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))):
- 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():
+ 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 ],
@@ -95,10 +97,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/run_test_pricing.py b/run_test_pricing.py
deleted file mode 100644
index 261d895..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()
-
-
-
-
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)
diff --git a/scenario/usage.py b/scenario/usage.py
index a7534a7..6da0ae5 100644
--- a/scenario/usage.py
+++ b/scenario/usage.py
@@ -1,18 +1,20 @@
-from mxdevtool.xenarix.core import Scenario
-import os
+import os, time, platform
import numpy as np
import mxdevtool as mx
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.instruments as mx_i
-import mxdevtool.instruments.outputs as mx_io
import mxdevtool.utils as utils
+
+enviroment = '{0}-{1}'.format(platform.system(), platform.machine())
+
def test():
+ print('usage test...')
ref_date = mx.Date.todaysDate()
null_calendar = mx.NullCalendar()
@@ -48,7 +50,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 +71,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)
@@ -78,16 +80,29 @@ 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_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', 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 +122,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')
@@ -127,30 +142,45 @@ 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')
- sobol_rsg = xen.Rsg(sampleNum=1000, dimension=365, seed=1, skip=0, isMomentMatching=False, randomType='sobol', subType='joekuod7', randomTransformType='invnormal')
+ pseudo_rsg2 = xen.RsgPseudo(sampleNum=1000, dimension=365, randomTransformType='uniform')
+
+ halton_rsg = xen.RsgHalton(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, 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=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'
@@ -161,57 +191,53 @@ 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)
+ 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 ]
all_calcs = [ hw1f_spot3m, hw1f_forward6m3m, hw1f_discountFactor, hw1f_discountBond3m,
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
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, ... ))
- multipath = results[scenCount]
+
+ calculated_tpos_264 = results.tPosSlice(t_pos, scenCount)
+
+ 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)
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)
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)
multipath_all_using_time = results.timeSlice(time=t_time) # all t_pos data
# analyticPath and test calculation
@@ -221,11 +247,10 @@ 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]]
-
+ 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 +258,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 +270,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 +278,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 +292,7 @@ def test():
# generate in result directory
xm.generate_xen(scenList[0])
-
+
# scenario template builder using market data
sb = xen.ScenarioBuilder()
@@ -286,41 +311,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.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__, '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.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.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
@@ -335,7 +361,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)
@@ -368,7 +394,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 +437,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 +451,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 +466,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 +475,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])
@@ -469,44 +495,8 @@ 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
- # 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')
@@ -538,4 +528,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