stochvolmodels 1.0.9__tar.gz → 1.0.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/PKG-INFO +1 -1
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/pyproject.toml +1 -1
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/__init__.py +5 -0
- stochvolmodels-1.0.11/stochvolmodels/data/fetch_option_chain.py +176 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/data/option_chain.py +3 -1
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/data/test_option_chain.py +4 -4
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/mc_payoffs.py +3 -1
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/mgf_pricer.py +9 -5
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/affine_expansion.py +2 -2
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv_pricer.py +99 -86
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/model_pricer.py +71 -47
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/utils/plots.py +1 -1
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/LICENSE.txt +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/README.md +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/data/__init__.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/__init__.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/__init__.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/bsm_pricer.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/config.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/core/normal_pricer.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/hawkes_jd_pricer.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/heston_pricer.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/__init__.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/tests/__init__.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/tests/qv_pricer.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/utils/__init__.py +0 -0
- {stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/utils/funcs.py +0 -0
|
@@ -62,6 +62,11 @@ from stochvolmodels.pricers.logsv_pricer import (
|
|
|
62
62
|
|
|
63
63
|
from stochvolmodels.data.option_chain import OptionChain, OptionSlice
|
|
64
64
|
|
|
65
|
+
from stochvolmodels.data.fetch_option_chain import (generate_vol_chain_np,
|
|
66
|
+
load_option_chain,
|
|
67
|
+
sample_option_chain_at_times,
|
|
68
|
+
load_price_data)
|
|
69
|
+
|
|
65
70
|
from stochvolmodels.data.test_option_chain import (
|
|
66
71
|
get_btc_test_chain_data,
|
|
67
72
|
get_gld_test_chain_data,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
this module is using option-chain-analytics package
|
|
3
|
+
to fetch OptionChain data with options data
|
|
4
|
+
see https://pypi.org/project/option-chain-analytics
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import numpy as np
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import qis
|
|
11
|
+
from qis import TimePeriod
|
|
12
|
+
from typing import Dict, Tuple, Optional, Literal
|
|
13
|
+
from numba.typed import List
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
# chain
|
|
17
|
+
from option_chain_analytics import OptionsDataDFs, create_chain_from_from_options_dfs
|
|
18
|
+
from option_chain_analytics.option_chain import SliceColumn, SlicesChain
|
|
19
|
+
|
|
20
|
+
# analytics
|
|
21
|
+
from stochvolmodels.data.option_chain import OptionChain
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def generate_vol_chain_np(chain: SlicesChain,
|
|
25
|
+
value_time: pd.Timestamp,
|
|
26
|
+
days_map: Dict[str, int] = {'1w': 7, '1m': 21},
|
|
27
|
+
delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
|
|
28
|
+
is_filtered: bool = True
|
|
29
|
+
) -> OptionChain:
|
|
30
|
+
"""
|
|
31
|
+
given SlicesChain generate OptionChain for calibration inputs
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
ttms, future_prices, discfactors = List(), List(), List()
|
|
35
|
+
optiontypes_ttms, strikes_ttms = List(), List()
|
|
36
|
+
bid_ivs, ask_ivs = List(), List()
|
|
37
|
+
bid_prices, ask_prices = List(), List()
|
|
38
|
+
slice_ids = []
|
|
39
|
+
for label, day in days_map.items():
|
|
40
|
+
next_date = value_time + pd.DateOffset(days=day) # if overlapping next date will be last avilable maturity
|
|
41
|
+
slice_date = chain.get_next_slice_after_date(mat_date=next_date)
|
|
42
|
+
slice_t = chain.expiry_slices[slice_date]
|
|
43
|
+
df = slice_t.get_joint_slice(delta_bounds=delta_bounds, is_filtered=is_filtered)
|
|
44
|
+
if not df.empty:
|
|
45
|
+
slice_ids.append(f"{label}: {slice_t.expiry_id}")
|
|
46
|
+
ttms.append(slice_t.get_ttm())
|
|
47
|
+
future_prices.append(slice_t.get_future_price())
|
|
48
|
+
discfactors.append(1.0)
|
|
49
|
+
strikes_ttms.append(df.index.to_numpy())
|
|
50
|
+
optiontypes_ttms.append(df[SliceColumn.OPTION_TYPE].to_numpy(dtype=str))
|
|
51
|
+
bid_ivs.append(df[SliceColumn.BID_IV].to_numpy())
|
|
52
|
+
ask_ivs.append(df[SliceColumn.ASK_IV].to_numpy())
|
|
53
|
+
bid_prices.append(df[SliceColumn.BID_PRICE].to_numpy())
|
|
54
|
+
ask_prices.append(df[SliceColumn.ASK_PRICE].to_numpy())
|
|
55
|
+
|
|
56
|
+
out = OptionChain(ttms=np.array(ttms),
|
|
57
|
+
forwards=np.array(future_prices),
|
|
58
|
+
discfactors=np.array(discfactors),
|
|
59
|
+
ids=np.array(slice_ids),
|
|
60
|
+
strikes_ttms=strikes_ttms,
|
|
61
|
+
optiontypes_ttms=optiontypes_ttms,
|
|
62
|
+
bid_ivs=bid_ivs,
|
|
63
|
+
ask_ivs=ask_ivs,
|
|
64
|
+
bid_prices=bid_prices,
|
|
65
|
+
ask_prices=ask_prices)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_option_chain(options_data_dfs: OptionsDataDFs,
|
|
70
|
+
value_time: pd.Timestamp = pd.Timestamp('2023-02-06 08:00:00+00:00'),
|
|
71
|
+
days_map: Dict[str, int] = {'1w': 7, '1m': 21},
|
|
72
|
+
delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
|
|
73
|
+
is_filtered: bool = True
|
|
74
|
+
) -> Optional[OptionChain]:
|
|
75
|
+
chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time)
|
|
76
|
+
if chain is not None:
|
|
77
|
+
option_chain = generate_vol_chain_np(chain=chain,
|
|
78
|
+
value_time=value_time,
|
|
79
|
+
days_map=days_map,
|
|
80
|
+
delta_bounds=delta_bounds,
|
|
81
|
+
is_filtered=is_filtered)
|
|
82
|
+
else:
|
|
83
|
+
option_chain = None
|
|
84
|
+
|
|
85
|
+
return option_chain
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sample_option_chain_at_times(options_data_dfs: OptionsDataDFs,
|
|
89
|
+
time_period: TimePeriod,
|
|
90
|
+
freq: str = 'W-FRI',
|
|
91
|
+
days_map: Dict[str, int] = {'1w': 7, '1m': 21},
|
|
92
|
+
delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
|
|
93
|
+
hour_offset: int = 8
|
|
94
|
+
) -> Dict[pd.Timestamp, OptionChain]:
|
|
95
|
+
value_times = qis.generate_dates_schedule(time_period=time_period,
|
|
96
|
+
freq=freq,
|
|
97
|
+
hour_offset=hour_offset)
|
|
98
|
+
option_chains = {}
|
|
99
|
+
for value_time in value_times:
|
|
100
|
+
option_chains[value_time] = load_option_chain(options_data_dfs=options_data_dfs,
|
|
101
|
+
value_time=value_time,
|
|
102
|
+
days_map=days_map,
|
|
103
|
+
delta_bounds=delta_bounds,
|
|
104
|
+
is_filtered=True)
|
|
105
|
+
return option_chains
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def load_price_data(options_data_dfs: OptionsDataDFs,
|
|
109
|
+
time_period: TimePeriod = None,
|
|
110
|
+
data: Literal['spot', 'perp', 'funding_rate'] = 'spot',
|
|
111
|
+
freq: Optional[str] = 'D' # to do
|
|
112
|
+
) -> pd.Series:
|
|
113
|
+
#options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker, freq='D', hour_offset=8))
|
|
114
|
+
spot_price = options_data_dfs.get_spot_data()[data]
|
|
115
|
+
if freq is not None:
|
|
116
|
+
spot_price = spot_price.resample(freq).last()
|
|
117
|
+
if time_period is not None:
|
|
118
|
+
spot_price = time_period.locate(spot_price)
|
|
119
|
+
return spot_price
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class UnitTests(Enum):
|
|
123
|
+
PRINT_CHAIN_DATA = 1
|
|
124
|
+
GENERATE_VOL_CHAIN_NP = 2
|
|
125
|
+
SAMPLE_CHAIN_AT_TIMES = 3
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_unit_test(unit_test: UnitTests):
|
|
129
|
+
|
|
130
|
+
ticker = 'BTC' # BTC, ETH
|
|
131
|
+
value_time = pd.Timestamp('2021-10-21 08:00:00+00:00')
|
|
132
|
+
value_time = pd.Timestamp('2023-10-06 08:00:00+00:00')
|
|
133
|
+
|
|
134
|
+
from option_chain_analytics.ts_loaders import ts_data_loader_wrapper
|
|
135
|
+
options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker))
|
|
136
|
+
options_data_dfs.get_start_end_date().print()
|
|
137
|
+
chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time)
|
|
138
|
+
|
|
139
|
+
if unit_test == UnitTests.PRINT_CHAIN_DATA:
|
|
140
|
+
for expiry, eslice in chain.expiry_slices.items():
|
|
141
|
+
eslice.print()
|
|
142
|
+
|
|
143
|
+
elif unit_test == UnitTests.GENERATE_VOL_CHAIN_NP:
|
|
144
|
+
option_chain = generate_vol_chain_np(chain=chain,
|
|
145
|
+
value_time=value_time,
|
|
146
|
+
days_map={'1w': 7},
|
|
147
|
+
delta_bounds=(-0.1, 0.1),
|
|
148
|
+
is_filtered=True)
|
|
149
|
+
option_chain.print()
|
|
150
|
+
skews = option_chain.get_chain_skews(delta=0.35)
|
|
151
|
+
print(skews)
|
|
152
|
+
|
|
153
|
+
elif unit_test == UnitTests.SAMPLE_CHAIN_AT_TIMES:
|
|
154
|
+
time_period = qis.TimePeriod('01Jan2023', '31Jan2023', tz='UTC')
|
|
155
|
+
option_chains = sample_option_chain_at_times(options_data_dfs=options_data_dfs,
|
|
156
|
+
time_period=time_period,
|
|
157
|
+
freq='W-FRI',
|
|
158
|
+
hour_offset=9
|
|
159
|
+
)
|
|
160
|
+
for key, chain in option_chains.items():
|
|
161
|
+
print(f"{key}")
|
|
162
|
+
print(chain)
|
|
163
|
+
|
|
164
|
+
plt.show()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == '__main__':
|
|
168
|
+
|
|
169
|
+
unit_test = UnitTests.SAMPLE_CHAIN_AT_TIMES
|
|
170
|
+
|
|
171
|
+
is_run_all_tests = False
|
|
172
|
+
if is_run_all_tests:
|
|
173
|
+
for unit_test in UnitTests:
|
|
174
|
+
run_unit_test(unit_test=unit_test)
|
|
175
|
+
else:
|
|
176
|
+
run_unit_test(unit_test=unit_test)
|
|
@@ -85,7 +85,9 @@ class OptionChain:
|
|
|
85
85
|
forwards=self.forwards,
|
|
86
86
|
strikes_ttms=self.strikes_ttms,
|
|
87
87
|
optiontypes_ttms=self.optiontypes_ttms,
|
|
88
|
-
ids=self.ids
|
|
88
|
+
ids=self.ids,
|
|
89
|
+
bid_ivs=self.bid_ivs,
|
|
90
|
+
ask_ivs=self.ask_ivs)
|
|
89
91
|
for k, v in this.items():
|
|
90
92
|
print(f"{k}:\n{v}")
|
|
91
93
|
|
|
@@ -847,10 +847,10 @@ def get_qv_options_test_chain_data(num_strikes: int = 21) -> OptionChain:
|
|
|
847
847
|
"""
|
|
848
848
|
BTC implied vols of 21Oct2021
|
|
849
849
|
"""
|
|
850
|
-
ids = array(['1m', '3m', '6m', '12m'])
|
|
851
|
-
ttms = array([0.083333333, 0.25, 0.5, 1.0])
|
|
852
|
-
forwards = array([1.0, 1.0, 1.0, 1.0])
|
|
853
|
-
discfactors = array([1.0, 1.0, 1.0, 1.0])
|
|
850
|
+
ids = array(['1w', '2w', '1m', '3m', '6m', '12m'])
|
|
851
|
+
ttms = array([7.0/365.0, 14.0/365.0, 0.083333333, 0.25, 0.5, 1.0])
|
|
852
|
+
forwards = array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
|
|
853
|
+
discfactors = array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
|
|
854
854
|
strikes_ttm = np.linspace(0.75, 1.5, num_strikes)
|
|
855
855
|
strikes_ttms = (strikes_ttm, strikes_ttm, strikes_ttm, strikes_ttm)
|
|
856
856
|
|
|
@@ -9,7 +9,9 @@ from ...pricers.core.config import VariableType
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@njit(cache=False, fastmath=True)
|
|
12
|
-
def compute_mc_vars_payoff(x0: np.ndarray,
|
|
12
|
+
def compute_mc_vars_payoff(x0: np.ndarray,
|
|
13
|
+
sigma0: np.ndarray,
|
|
14
|
+
qvar0: np.ndarray,
|
|
13
15
|
ttm: float,
|
|
14
16
|
forward: float,
|
|
15
17
|
strikes_ttm: np.ndarray,
|
|
@@ -39,8 +39,10 @@ def get_phi_grid(is_spot_measure: bool = True,
|
|
|
39
39
|
def get_psi_grid() -> np.ndarray:
|
|
40
40
|
"""
|
|
41
41
|
for I = QV variable
|
|
42
|
+
need a lot of step for short-dated options
|
|
43
|
+
todo: find a non-uniform grid for short dated options
|
|
42
44
|
"""
|
|
43
|
-
p = np.linspace(0,
|
|
45
|
+
p = np.linspace(0, 4000, 40000)
|
|
44
46
|
real_p = -0.5
|
|
45
47
|
psi_grid = real_p + 1j * p
|
|
46
48
|
return psi_grid
|
|
@@ -51,8 +53,8 @@ def get_theta_grid() -> np.ndarray:
|
|
|
51
53
|
"""
|
|
52
54
|
for sigma
|
|
53
55
|
"""
|
|
54
|
-
p = np.linspace(0, 600,
|
|
55
|
-
real_p =
|
|
56
|
+
p = np.linspace(0, 600, 5000)
|
|
57
|
+
real_p = 0.0
|
|
56
58
|
theta_grid = real_p + 1j * p
|
|
57
59
|
return theta_grid
|
|
58
60
|
|
|
@@ -303,6 +305,7 @@ def pdf_with_mgf_grid(log_mgf_grid: np.ndarray,
|
|
|
303
305
|
transform_var_grid: np.ndarray,
|
|
304
306
|
space_grid: np.ndarray,
|
|
305
307
|
shift: float = 0.0,
|
|
308
|
+
scale: float = 1.0,
|
|
306
309
|
is_simpson: bool = True
|
|
307
310
|
) -> np.ndarray:
|
|
308
311
|
"""
|
|
@@ -313,8 +316,9 @@ def pdf_with_mgf_grid(log_mgf_grid: np.ndarray,
|
|
|
313
316
|
"""
|
|
314
317
|
dp = compute_integration_weights(var_grid=transform_var_grid, is_simpson=is_simpson) / np.pi
|
|
315
318
|
pdf = np.zeros_like(space_grid)
|
|
316
|
-
|
|
317
|
-
|
|
319
|
+
z = (space_grid - shift) / scale
|
|
320
|
+
for idx, x in enumerate(z):
|
|
321
|
+
pdf[idx] = np.nansum(np.real(dp * np.exp(x * transform_var_grid + log_mgf_grid)))
|
|
318
322
|
dx = space_grid[1] - space_grid[0]
|
|
319
323
|
pdf = dx * pdf
|
|
320
324
|
return pdf
|
{stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/affine_expansion.py
RENAMED
|
@@ -47,12 +47,12 @@ def func_a_ode_quadratic_terms(theta: float,
|
|
|
47
47
|
qv2 = theta2 * vartheta2
|
|
48
48
|
if is_spot_measure:
|
|
49
49
|
lamda = 0
|
|
50
|
-
kappa_p = kappa1 + kappa2 * theta
|
|
51
50
|
kappa2_p = kappa2
|
|
51
|
+
kappa_p = kappa1 + kappa2 * theta
|
|
52
52
|
else:
|
|
53
53
|
lamda = beta*theta2
|
|
54
|
-
kappa_p = kappa1 + kappa2 * theta - 2*beta*theta
|
|
55
54
|
kappa2_p = kappa2-beta
|
|
55
|
+
kappa_p = kappa1 + kappa2 * theta - 2*beta*theta
|
|
56
56
|
|
|
57
57
|
# fill Ms: M should be of same type as L and H for numba, eventhough they are real
|
|
58
58
|
# utilize that M is symmetric
|
|
@@ -62,6 +62,10 @@ class LogSvParams(ModelParams):
|
|
|
62
62
|
def to_dict(self) -> Dict[str, Any]:
|
|
63
63
|
return asdict(self)
|
|
64
64
|
|
|
65
|
+
def to_str(self) -> str:
|
|
66
|
+
return f"sigma0={self.sigma0:0.2f}, theta={self.theta:0.2f}, kappa1={self.kappa1:0.2f}, kappa2={self.kappa2:0.2f}, " \
|
|
67
|
+
f"beta={self.beta:0.2f}, volvol={self.volvol:0.2f}"
|
|
68
|
+
|
|
65
69
|
@property
|
|
66
70
|
def kappa(self) -> float:
|
|
67
71
|
return self.kappa1+self.kappa2*self.theta
|
|
@@ -88,24 +92,35 @@ class LogSvParams(ModelParams):
|
|
|
88
92
|
"""
|
|
89
93
|
return self.kappa1 * self.theta / self.vartheta2 - 1.0
|
|
90
94
|
|
|
91
|
-
def get_x_grid(self, ttm: float = 1.0, n_stdevs:
|
|
95
|
+
def get_x_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
|
|
96
|
+
"""
|
|
97
|
+
spacial grid to compute density of x
|
|
98
|
+
"""
|
|
92
99
|
sigma_t = np.sqrt(ttm * 0.5 * (np.square(self.sigma0) + np.square(self.theta)))
|
|
93
100
|
drift = - 0.5*sigma_t*sigma_t
|
|
94
101
|
stdev = (n_stdevs+1)*sigma_t
|
|
95
102
|
return np.linspace(-stdev+drift, stdev+drift, n)
|
|
96
103
|
|
|
97
|
-
def get_sigma_grid(self, ttm: float = 1.0, n_stdevs:
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
def get_sigma_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
|
|
105
|
+
"""
|
|
106
|
+
spacial grid to compute density of sigma
|
|
107
|
+
"""
|
|
108
|
+
sigma_t = np.sqrt(0.5*(np.square(self.sigma0) + np.square(self.theta)))
|
|
109
|
+
vvol = 0.5*np.sqrt(self.vartheta2*ttm)
|
|
100
110
|
return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
|
|
101
111
|
|
|
102
|
-
def get_qvar_grid(self, ttm: float = 1.0, n_stdevs:
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
def get_qvar_grid(self, ttm: float = 1.0, n_stdevs: float = 3.0, n: int = 200) -> np.ndarray:
|
|
113
|
+
"""
|
|
114
|
+
spacial grid to compute density of i
|
|
115
|
+
"""
|
|
116
|
+
sigma_t = np.sqrt(ttm * (np.square(self.sigma0) + np.square(self.theta)))
|
|
117
|
+
vvol = np.sqrt(self.vartheta2)*ttm
|
|
105
118
|
return np.linspace(0.0, sigma_t+n_stdevs*vvol, n)
|
|
106
119
|
|
|
107
120
|
def get_variable_space_grid(self, variable_type: VariableType = VariableType.LOG_RETURN,
|
|
108
|
-
ttm: float = 1.0,
|
|
121
|
+
ttm: float = 1.0,
|
|
122
|
+
n_stdevs: float = 3,
|
|
123
|
+
n: int = 200
|
|
109
124
|
) -> np.ndarray:
|
|
110
125
|
if variable_type == VariableType.LOG_RETURN:
|
|
111
126
|
return self.get_x_grid(ttm=ttm, n_stdevs=n_stdevs, n=n)
|
|
@@ -177,7 +192,7 @@ LOGSV_BTC_PARAMS = LogSvParams(sigma0=0.8376, theta=1.0413, kappa1=3.1844, kappa
|
|
|
177
192
|
|
|
178
193
|
class LogSVPricer(ModelPricer):
|
|
179
194
|
|
|
180
|
-
@timer
|
|
195
|
+
# @timer
|
|
181
196
|
def price_chain(self,
|
|
182
197
|
option_chain: OptionChain,
|
|
183
198
|
params: LogSvParams,
|
|
@@ -201,7 +216,10 @@ class LogSVPricer(ModelPricer):
|
|
|
201
216
|
def model_mc_price_chain(self,
|
|
202
217
|
option_chain: OptionChain,
|
|
203
218
|
params: LogSvParams,
|
|
219
|
+
is_spot_measure: bool = True,
|
|
220
|
+
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
204
221
|
nb_path: int = 100000,
|
|
222
|
+
nb_steps: Optional[int] = None,
|
|
205
223
|
**kwargs
|
|
206
224
|
) -> (List[np.ndarray], List[np.ndarray]):
|
|
207
225
|
return logsv_mc_chain_pricer(v0=params.sigma0,
|
|
@@ -215,8 +233,18 @@ class LogSVPricer(ModelPricer):
|
|
|
215
233
|
discfactors=option_chain.discfactors,
|
|
216
234
|
strikes_ttms=option_chain.strikes_ttms,
|
|
217
235
|
optiontypes_ttms=option_chain.optiontypes_ttms,
|
|
236
|
+
is_spot_measure=is_spot_measure,
|
|
237
|
+
variable_type=variable_type,
|
|
218
238
|
nb_path=nb_path,
|
|
219
|
-
|
|
239
|
+
nb_steps=nb_steps or int(360*np.max(option_chain.ttms))+1)
|
|
240
|
+
|
|
241
|
+
def set_vol_scaler(self, option_chain: OptionChain) -> float:
|
|
242
|
+
"""
|
|
243
|
+
use chain vols to set the scaler
|
|
244
|
+
"""
|
|
245
|
+
atm0 = option_chain.get_chain_atm_vols()[0]
|
|
246
|
+
ttm0 = option_chain.ttms[0]
|
|
247
|
+
return set_vol_scaler(sigma0=atm0, ttm=ttm0)
|
|
220
248
|
|
|
221
249
|
@timer
|
|
222
250
|
def calibrate_model_params_to_chain(self,
|
|
@@ -233,9 +261,7 @@ class LogSVPricer(ModelPricer):
|
|
|
233
261
|
"""
|
|
234
262
|
implementation of model calibration interface with nonlinear constraints
|
|
235
263
|
"""
|
|
236
|
-
|
|
237
|
-
ttm0 = option_chain.ttms[0]
|
|
238
|
-
vol_scaler = set_vol_scaler(sigma0=atm0, ttm=ttm0)
|
|
264
|
+
vol_scaler = self.set_vol_scaler(option_chain=option_chain)
|
|
239
265
|
|
|
240
266
|
x, market_vols = option_chain.get_chain_data_as_xy()
|
|
241
267
|
market_vols = to_flat_np_array(market_vols) # market mid quotes
|
|
@@ -248,7 +274,46 @@ class LogSVPricer(ModelPricer):
|
|
|
248
274
|
else:
|
|
249
275
|
weights = np.ones_like(market_vols)
|
|
250
276
|
|
|
251
|
-
|
|
277
|
+
def parse_model_params(pars: np.ndarray) -> LogSvParams:
|
|
278
|
+
if model_calibration_type == LogsvModelCalibrationType.PARAMS4:
|
|
279
|
+
fit_params = LogSvParams(sigma0=pars[0],
|
|
280
|
+
theta=pars[1],
|
|
281
|
+
kappa1=params0.kappa1,
|
|
282
|
+
kappa2=params0.kappa2,
|
|
283
|
+
beta=pars[2],
|
|
284
|
+
volvol=pars[3])
|
|
285
|
+
elif model_calibration_type == LogsvModelCalibrationType.PARAMS5:
|
|
286
|
+
fit_params = LogSvParams(sigma0=pars[0],
|
|
287
|
+
theta=pars[1],
|
|
288
|
+
kappa1=pars[2],
|
|
289
|
+
kappa2=None,
|
|
290
|
+
beta=pars[3],
|
|
291
|
+
volvol=pars[4])
|
|
292
|
+
else:
|
|
293
|
+
raise NotImplementedError(f"{model_calibration_type}")
|
|
294
|
+
return fit_params
|
|
295
|
+
|
|
296
|
+
def objective(pars: np.ndarray, args: np.ndarray) -> float:
|
|
297
|
+
params = parse_model_params(pars=pars)
|
|
298
|
+
model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, vol_scaler=vol_scaler)
|
|
299
|
+
resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
|
|
300
|
+
return resid
|
|
301
|
+
|
|
302
|
+
# parametric constraints
|
|
303
|
+
def martingale_measure(pars: np.ndarray) -> float:
|
|
304
|
+
params = parse_model_params(pars=pars)
|
|
305
|
+
return params.kappa2 - params.beta
|
|
306
|
+
|
|
307
|
+
def inverse_measure(pars: np.ndarray) -> float:
|
|
308
|
+
params = parse_model_params(pars=pars)
|
|
309
|
+
return params.kappa2 - 2.0 * params.beta
|
|
310
|
+
|
|
311
|
+
def vol_4thmoment_finite(pars: np.ndarray) -> float:
|
|
312
|
+
params = parse_model_params(pars=pars)
|
|
313
|
+
kappa = params.kappa1 + params.kappa2 * params.theta
|
|
314
|
+
return kappa - 1.5 * params.vartheta2
|
|
315
|
+
|
|
316
|
+
# set initial params
|
|
252
317
|
if model_calibration_type == LogsvModelCalibrationType.PARAMS4:
|
|
253
318
|
# fit: v0, theta, beta, volvol; kappa1, kappa2 is given with params0
|
|
254
319
|
p0 = np.array([params0.sigma0, params0.theta, params0.beta, params0.volvol])
|
|
@@ -257,29 +322,6 @@ class LogSVPricer(ModelPricer):
|
|
|
257
322
|
(params_min.beta, params_max.beta),
|
|
258
323
|
(params_min.volvol, params_max.volvol))
|
|
259
324
|
|
|
260
|
-
def objective(pars: np.ndarray, args: np.ndarray) -> float:
|
|
261
|
-
v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
|
|
262
|
-
params = LogSvParams(sigma0=v0, theta=theta, kappa1=params0.kappa1,
|
|
263
|
-
kappa2=params0.kappa2, beta=beta, volvol=volvol)
|
|
264
|
-
model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params,
|
|
265
|
-
vol_scaler=vol_scaler)
|
|
266
|
-
resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
|
|
267
|
-
return resid
|
|
268
|
-
|
|
269
|
-
def martingale_measure(pars: np.ndarray) -> float:
|
|
270
|
-
v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
|
|
271
|
-
return params0.kappa2 - beta
|
|
272
|
-
|
|
273
|
-
def inverse_measure(pars: np.ndarray) -> float:
|
|
274
|
-
v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
|
|
275
|
-
return params0.kappa2 - 2.0 * beta
|
|
276
|
-
|
|
277
|
-
def vol_4thmoment_finite(pars: np.ndarray) -> float:
|
|
278
|
-
v0, theta, beta, volvol = pars[0], pars[1], pars[2], pars[3]
|
|
279
|
-
vartheta2 = beta * beta + volvol * volvol
|
|
280
|
-
kappa = params0.kappa1 + params0.kappa2 * theta
|
|
281
|
-
return kappa - 1.5 * vartheta2
|
|
282
|
-
|
|
283
325
|
elif model_calibration_type == LogsvModelCalibrationType.PARAMS5:
|
|
284
326
|
# fit: v0, theta, kappa1, beta, volvol; kappa2 is mapped as kappa1 / theta
|
|
285
327
|
p0 = np.array([params0.sigma0, params0.theta, params0.kappa1, params0.beta, params0.volvol])
|
|
@@ -289,28 +331,6 @@ class LogSVPricer(ModelPricer):
|
|
|
289
331
|
(params_min.beta, params_max.beta),
|
|
290
332
|
(params_min.volvol, params_max.volvol))
|
|
291
333
|
|
|
292
|
-
def objective(pars: np.ndarray, args: np.ndarray) -> float:
|
|
293
|
-
v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
|
|
294
|
-
params = LogSvParams(sigma0=v0, theta=theta, kappa1=kappa1, kappa2=None, beta=beta, volvol=volvol)
|
|
295
|
-
model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, vol_scaler=vol_scaler)
|
|
296
|
-
resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
|
|
297
|
-
return resid
|
|
298
|
-
|
|
299
|
-
def martingale_measure(pars: np.ndarray) -> float:
|
|
300
|
-
v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
|
|
301
|
-
return kappa1 / theta - beta
|
|
302
|
-
|
|
303
|
-
def inverse_measure(pars: np.ndarray) -> float:
|
|
304
|
-
v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
|
|
305
|
-
return kappa1 / theta - 2.0*beta
|
|
306
|
-
|
|
307
|
-
def vol_4thmoment_finite(pars: np.ndarray) -> float:
|
|
308
|
-
v0, theta, kappa1, beta, volvol = pars[0], pars[1], pars[2], pars[3], pars[4]
|
|
309
|
-
vartheta2 = beta*beta + volvol*volvol
|
|
310
|
-
kappa2 = kappa1 / theta
|
|
311
|
-
kappa = kappa1 + kappa2 * theta
|
|
312
|
-
return kappa - 1.5*vartheta2
|
|
313
|
-
|
|
314
334
|
else:
|
|
315
335
|
raise NotImplementedError(f"{model_calibration_type}")
|
|
316
336
|
|
|
@@ -349,26 +369,7 @@ class LogSVPricer(ModelPricer):
|
|
|
349
369
|
else:
|
|
350
370
|
res = minimize(objective, p0, args=None, method='SLSQP', bounds=bounds, options=options)
|
|
351
371
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if model_calibration_type == LogsvModelCalibrationType.PARAMS4:
|
|
355
|
-
fit_params = LogSvParams(sigma0=popt[0],
|
|
356
|
-
theta=popt[1],
|
|
357
|
-
kappa1=params0.kappa1,
|
|
358
|
-
kappa2=params0.kappa2,
|
|
359
|
-
beta=popt[2],
|
|
360
|
-
volvol=popt[3])
|
|
361
|
-
|
|
362
|
-
elif model_calibration_type == LogsvModelCalibrationType.PARAMS5:
|
|
363
|
-
fit_params = LogSvParams(sigma0=popt[0],
|
|
364
|
-
theta=popt[1],
|
|
365
|
-
kappa1=popt[2],
|
|
366
|
-
kappa2=None,
|
|
367
|
-
beta=popt[3],
|
|
368
|
-
volvol=popt[4])
|
|
369
|
-
|
|
370
|
-
else:
|
|
371
|
-
raise NotImplementedError(f"{model_calibration_type}")
|
|
372
|
+
fit_params = parse_model_params(pars=res.x)
|
|
372
373
|
|
|
373
374
|
return fit_params
|
|
374
375
|
|
|
@@ -379,12 +380,14 @@ class LogSVPricer(ModelPricer):
|
|
|
379
380
|
ttm: float = 1.0,
|
|
380
381
|
nb_path: int = 100000,
|
|
381
382
|
is_spot_measure: bool = True,
|
|
382
|
-
nb_steps: int =
|
|
383
|
+
nb_steps: int = None,
|
|
384
|
+
year_days: int = 360,
|
|
383
385
|
**kwargs
|
|
384
386
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
385
387
|
"""
|
|
386
388
|
simulate vols in dt_path grid
|
|
387
389
|
"""
|
|
390
|
+
nb_steps = nb_steps or int(np.ceil(year_days * ttm))
|
|
388
391
|
sigma_t, grid_t = simulate_vol_paths(ttm=ttm,
|
|
389
392
|
v0=params.sigma0,
|
|
390
393
|
theta=params.theta,
|
|
@@ -487,7 +490,8 @@ def logsv_chain_pricer(params: LogSvParams,
|
|
|
487
490
|
is_spot_measure: bool = True,
|
|
488
491
|
expansion_order: ExpansionOrder = ExpansionOrder.SECOND,
|
|
489
492
|
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
490
|
-
vol_scaler: float = None
|
|
493
|
+
vol_scaler: float = None,
|
|
494
|
+
**kwargs
|
|
491
495
|
) -> List[np.ndarray]:
|
|
492
496
|
"""
|
|
493
497
|
wrapper to price option chain on variable_type
|
|
@@ -590,19 +594,24 @@ def logsv_pdfs(params: LogSvParams,
|
|
|
590
594
|
if variable_type == VariableType.LOG_RETURN:
|
|
591
595
|
transform_var_grid = phi_grid
|
|
592
596
|
shift = 0.0
|
|
593
|
-
|
|
597
|
+
scale = 1.0
|
|
598
|
+
elif variable_type == VariableType.Q_VAR: # scaled by ttm
|
|
594
599
|
transform_var_grid = psi_grid
|
|
595
600
|
shift = 0.0
|
|
601
|
+
scale = 1.0 / ttm
|
|
596
602
|
elif variable_type == VariableType.SIGMA:
|
|
597
603
|
transform_var_grid = theta_grid
|
|
598
604
|
shift = params.theta
|
|
605
|
+
scale = 1.0
|
|
599
606
|
else:
|
|
600
607
|
raise NotImplementedError
|
|
601
608
|
|
|
602
609
|
pdf = mgfp.pdf_with_mgf_grid(log_mgf_grid=log_mgf_grid,
|
|
603
610
|
transform_var_grid=transform_var_grid,
|
|
604
611
|
space_grid=space_grid,
|
|
605
|
-
shift=shift
|
|
612
|
+
shift=shift,
|
|
613
|
+
scale=scale)
|
|
614
|
+
pdf = pdf / scale
|
|
606
615
|
return pdf
|
|
607
616
|
|
|
608
617
|
|
|
@@ -620,9 +629,9 @@ def logsv_mc_chain_pricer(ttms: np.ndarray,
|
|
|
620
629
|
volvol: float,
|
|
621
630
|
is_spot_measure: bool = True,
|
|
622
631
|
nb_path: int = 100000,
|
|
632
|
+
nb_steps: int = 360,
|
|
623
633
|
variable_type: VariableType = VariableType.LOG_RETURN
|
|
624
634
|
) -> Tuple[List[np.ndarray], List[np.ndarray]]:
|
|
625
|
-
|
|
626
635
|
# starting values
|
|
627
636
|
x0 = np.zeros(nb_path)
|
|
628
637
|
qvar0 = np.zeros(nb_path)
|
|
@@ -643,6 +652,7 @@ def logsv_mc_chain_pricer(ttms: np.ndarray,
|
|
|
643
652
|
beta=beta,
|
|
644
653
|
volvol=volvol,
|
|
645
654
|
nb_path=nb_path,
|
|
655
|
+
nb_steps=nb_steps,
|
|
646
656
|
is_spot_measure=is_spot_measure)
|
|
647
657
|
ttm0 = ttm
|
|
648
658
|
option_prices, option_std = compute_mc_vars_payoff(x0=x0, sigma0=sigma0, qvar0=qvar0,
|
|
@@ -714,7 +724,9 @@ def simulate_logsv_x_vol_terminal(ttm: float,
|
|
|
714
724
|
nb_path: int = 100000,
|
|
715
725
|
nb_steps: int = 360
|
|
716
726
|
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
717
|
-
|
|
727
|
+
"""
|
|
728
|
+
mc simulator for terminal values of log-return, vol sigma0, and qvar for log sv model
|
|
729
|
+
"""
|
|
718
730
|
if x0.shape[0] == 1: # initial value
|
|
719
731
|
x0 = x0*np.zeros(nb_path)
|
|
720
732
|
else:
|
|
@@ -744,9 +756,10 @@ def simulate_logsv_x_vol_terminal(ttm: float,
|
|
|
744
756
|
for t_, (w0, w1) in enumerate(zip(W0, W1)):
|
|
745
757
|
sigma0_2dt = sigma0 * sigma0 * dt
|
|
746
758
|
x0 = x0 + alpha * 0.5 * sigma0_2dt + sigma0 * w0
|
|
747
|
-
qvar0 = qvar0 + sigma0_2dt
|
|
748
759
|
vol_var = vol_var + ((kappa1 * theta / sigma0 - kappa1) + kappa2*(theta-sigma0) + adj*sigma0 - 0.5*vartheta2) * dt + beta*w0+volvol*w1
|
|
749
760
|
sigma0 = np.exp(vol_var)
|
|
761
|
+
qvar0 = qvar0 + 0.5*(sigma0_2dt+sigma0 * sigma0 * dt)
|
|
762
|
+
|
|
750
763
|
|
|
751
764
|
return x0, sigma0, qvar0
|
|
752
765
|
|
|
@@ -9,6 +9,7 @@ market options data is passed using data container ChainData
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import string
|
|
12
13
|
import pandas as pd
|
|
13
14
|
import numpy as np
|
|
14
15
|
import matplotlib.pyplot as plt
|
|
@@ -18,6 +19,7 @@ from abc import ABC, abstractmethod
|
|
|
18
19
|
from scipy import stats
|
|
19
20
|
from dataclasses import dataclass, asdict
|
|
20
21
|
from typing import Tuple, Optional, Dict
|
|
22
|
+
import qis as qis
|
|
21
23
|
|
|
22
24
|
from stochvolmodels.pricers.core.config import VariableType
|
|
23
25
|
from stochvolmodels.data.option_chain import OptionChain, OptionSlice
|
|
@@ -58,12 +60,13 @@ class ModelPricer(ABC):
|
|
|
58
60
|
def compute_chain_prices_with_vols(self,
|
|
59
61
|
option_chain: OptionChain,
|
|
60
62
|
params: ModelParams,
|
|
63
|
+
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
61
64
|
**kwargs
|
|
62
65
|
) -> Tuple[List[np.ndarray], List[np.ndarray]]:
|
|
63
66
|
"""
|
|
64
67
|
price chain and compute model vols
|
|
65
68
|
"""
|
|
66
|
-
model_prices = self.price_chain(option_chain=option_chain, params=params, **kwargs)
|
|
69
|
+
model_prices = self.price_chain(option_chain=option_chain, params=params, variable_type=variable_type, **kwargs)
|
|
67
70
|
model_ivols = option_chain.compute_model_ivols_from_chain_data(model_prices=model_prices)
|
|
68
71
|
return model_prices, model_ivols
|
|
69
72
|
|
|
@@ -80,7 +83,10 @@ class ModelPricer(ABC):
|
|
|
80
83
|
**kwargs)
|
|
81
84
|
return model_ivols
|
|
82
85
|
|
|
83
|
-
def model_mc_price_chain(self,
|
|
86
|
+
def model_mc_price_chain(self,
|
|
87
|
+
option_chain: OptionChain, params: ModelParams,
|
|
88
|
+
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
89
|
+
**kwargs
|
|
84
90
|
) -> Tuple[List[np.ndarray], List[np.ndarray]]:
|
|
85
91
|
"""
|
|
86
92
|
abstract method for pricing chain data using simulation of model dynamics
|
|
@@ -161,6 +167,7 @@ class ModelPricer(ABC):
|
|
|
161
167
|
def compute_mc_chain_implied_vols(self,
|
|
162
168
|
option_chain: OptionChain,
|
|
163
169
|
params: ModelParams,
|
|
170
|
+
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
164
171
|
nb_path: int = 100000,
|
|
165
172
|
**kwargs
|
|
166
173
|
) -> Tuple[List[np.ndarray], ...]:
|
|
@@ -169,6 +176,7 @@ class ModelPricer(ABC):
|
|
|
169
176
|
"""
|
|
170
177
|
model_prices_ttms, option_std_ttms = self.model_mc_price_chain(option_chain=option_chain,
|
|
171
178
|
params=params,
|
|
179
|
+
variable_type=variable_type,
|
|
172
180
|
nb_path=nb_path,
|
|
173
181
|
**kwargs)
|
|
174
182
|
std_factor = 1.96
|
|
@@ -307,6 +315,7 @@ class ModelPricer(ABC):
|
|
|
307
315
|
is_log_strike_xaxis: bool = False,
|
|
308
316
|
headers: Optional[List[str]] = None,
|
|
309
317
|
xvar_format: str = None,
|
|
318
|
+
figsize: Tuple[float, float] = plot.FIGSIZE,
|
|
310
319
|
**kwargs
|
|
311
320
|
) -> plt.Figure:
|
|
312
321
|
"""
|
|
@@ -316,19 +325,19 @@ class ModelPricer(ABC):
|
|
|
316
325
|
model_ivols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, **kwargs)
|
|
317
326
|
|
|
318
327
|
num_slices = len(option_chain.ttms)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
fig, ax = plt.subplots(1, 1, figsize=
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
328
|
+
with sns.axes_style('darkgrid'):
|
|
329
|
+
if num_slices == 1:
|
|
330
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
|
|
331
|
+
axs = [ax]
|
|
332
|
+
elif num_slices == 2:
|
|
333
|
+
fig, axs = plt.subplots(1, 2, figsize=figsize, tight_layout=True)
|
|
334
|
+
elif num_slices == 3:
|
|
335
|
+
fig, axs = plt.subplots(1, 3, figsize=figsize, tight_layout=True)
|
|
336
|
+
elif num_slices == 4:
|
|
337
|
+
fig, axs = plt.subplots(2, 2, figsize=figsize, tight_layout=True)
|
|
338
|
+
axs = qis.to_flat_list(axs)
|
|
339
|
+
else:
|
|
340
|
+
raise NotImplementedError
|
|
332
341
|
|
|
333
342
|
atm_vols = option_chain.get_chain_atm_vols()
|
|
334
343
|
for idx, ttm in enumerate(option_chain.ttms):
|
|
@@ -367,7 +376,7 @@ class ModelPricer(ABC):
|
|
|
367
376
|
strike_name=strike_name,
|
|
368
377
|
xvar_format=xvar_format,
|
|
369
378
|
x_rotation=0,
|
|
370
|
-
ax=axs[idx
|
|
379
|
+
ax=axs[idx],
|
|
371
380
|
**kwargs)
|
|
372
381
|
return fig
|
|
373
382
|
|
|
@@ -377,8 +386,9 @@ class ModelPricer(ABC):
|
|
|
377
386
|
is_log_strike_xaxis: bool = False,
|
|
378
387
|
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
379
388
|
nb_path: int = 100000,
|
|
389
|
+
figsize: Tuple[float, float] = plot.FIGSIZE,
|
|
380
390
|
**kwargs
|
|
381
|
-
) ->
|
|
391
|
+
) -> plt.Figure:
|
|
382
392
|
"""
|
|
383
393
|
comparision of model implied vols computed old_analytics vs mc pricer
|
|
384
394
|
optimized for 2*2 figure
|
|
@@ -391,11 +401,20 @@ class ModelPricer(ABC):
|
|
|
391
401
|
nb_path=nb_path,
|
|
392
402
|
**kwargs)
|
|
393
403
|
|
|
404
|
+
num_slices = len(option_chain.ttms)
|
|
394
405
|
with sns.axes_style('darkgrid'):
|
|
395
|
-
if
|
|
396
|
-
fig,
|
|
406
|
+
if num_slices == 1:
|
|
407
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
|
|
408
|
+
axs = [ax]
|
|
409
|
+
elif num_slices == 2:
|
|
410
|
+
fig, axs = plt.subplots(1, 2, figsize=figsize, tight_layout=True)
|
|
411
|
+
elif num_slices == 3:
|
|
412
|
+
fig, axs = plt.subplots(1, 3, figsize=figsize, tight_layout=True)
|
|
413
|
+
elif num_slices == 4:
|
|
414
|
+
fig, axs = plt.subplots(2, 2, figsize=figsize, tight_layout=True)
|
|
415
|
+
axs = qis.to_flat_list(axs)
|
|
397
416
|
else:
|
|
398
|
-
|
|
417
|
+
raise NotImplementedError
|
|
399
418
|
|
|
400
419
|
for idx, ttm in enumerate(option_chain.ttms):
|
|
401
420
|
if is_log_strike_xaxis:
|
|
@@ -419,10 +438,6 @@ class ModelPricer(ABC):
|
|
|
419
438
|
else:
|
|
420
439
|
title = f"{ttm=:0.2f}"
|
|
421
440
|
|
|
422
|
-
if len(option_chain.ttms) > 1:
|
|
423
|
-
ax = axs[idx % 2][idx // 2]
|
|
424
|
-
else:
|
|
425
|
-
ax = axs
|
|
426
441
|
plot.vol_slice_fit(bid_vol=pd.Series(mc_ivols_down[idx], index=strikes),
|
|
427
442
|
ask_vol=pd.Series(mc_ivols_up[idx], index=strikes),
|
|
428
443
|
model_vols=model_vol_t,
|
|
@@ -432,18 +447,19 @@ class ModelPricer(ABC):
|
|
|
432
447
|
strike_name=strike_name,
|
|
433
448
|
xvar_format=xvar_format,
|
|
434
449
|
x_rotation=0,
|
|
435
|
-
ax=
|
|
450
|
+
ax=axs[idx],
|
|
436
451
|
**kwargs)
|
|
452
|
+
return fig
|
|
437
453
|
|
|
438
454
|
def plot_comp_mma_inverse_options_with_mc(self,
|
|
439
455
|
option_chain: OptionChain,
|
|
440
456
|
params: ModelParams,
|
|
441
457
|
variable_type: VariableType = VariableType.LOG_RETURN,
|
|
442
458
|
nb_path: int = 100000,
|
|
443
|
-
headers: Optional[List[str]] = ('(A)', '(B)', '(C)', '(D)'), # optimized for 2*2 figure
|
|
444
459
|
is_log_strike_xaxis: bool = False,
|
|
445
460
|
is_plot_vols: bool = True,
|
|
446
461
|
figsize: Tuple[float, float] = plot.FIGSIZE,
|
|
462
|
+
xvar_format: str = '{:0,.2f}',
|
|
447
463
|
**kwargs
|
|
448
464
|
) -> plt.Figure:
|
|
449
465
|
"""
|
|
@@ -458,18 +474,19 @@ class ModelPricer(ABC):
|
|
|
458
474
|
variable_type=variable_type,
|
|
459
475
|
**kwargs)
|
|
460
476
|
|
|
461
|
-
model_prices_inv, model_ivols_inv= self.compute_chain_prices_with_vols(option_chain=option_chain, params=params,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
477
|
+
model_prices_inv, model_ivols_inv = self.compute_chain_prices_with_vols(option_chain=option_chain, params=params,
|
|
478
|
+
is_spot_measure=False,
|
|
479
|
+
variable_type=variable_type,
|
|
480
|
+
**kwargs)
|
|
465
481
|
|
|
466
482
|
# we perform MC simulation in MMA measure
|
|
467
|
-
mc_kwargs = update_kwargs(kwargs, dict(is_spot_measure=True, variable_type=variable_type))
|
|
468
483
|
model_prices_ttms, model_prices_ttms_ups, model_prices_ttms_downs, \
|
|
469
484
|
mc_ivols, mc_ivols_up, mc_ivols_down, mc_stdev_ttms = self.compute_mc_chain_implied_vols(option_chain=option_chain,
|
|
470
485
|
params=params,
|
|
471
486
|
nb_path=nb_path,
|
|
472
|
-
|
|
487
|
+
variable_type=variable_type,
|
|
488
|
+
is_spot_measure=True,
|
|
489
|
+
**kwargs)
|
|
473
490
|
|
|
474
491
|
if is_plot_vols:
|
|
475
492
|
model_datas = {mma_label: model_ivols_mma, inverse_lable: model_ivols_inv}
|
|
@@ -480,27 +497,34 @@ class ModelPricer(ABC):
|
|
|
480
497
|
mc_data = model_prices_ttms
|
|
481
498
|
mc_data_lower, mc_data_upper = model_prices_ttms_downs, model_prices_ttms_ups
|
|
482
499
|
|
|
483
|
-
|
|
484
|
-
nrows, ncols = 1, option_chain.ttms.size
|
|
485
|
-
else:
|
|
486
|
-
nrows, ncols = 2, option_chain.ttms.size//2
|
|
487
|
-
|
|
500
|
+
num_slices = len(option_chain.ttms)
|
|
488
501
|
with sns.axes_style('darkgrid'):
|
|
489
|
-
|
|
502
|
+
if num_slices == 1:
|
|
503
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
|
|
504
|
+
axs = [ax]
|
|
505
|
+
elif num_slices == 2:
|
|
506
|
+
fig, axs = plt.subplots(1, 2, figsize=figsize, tight_layout=True)
|
|
507
|
+
elif num_slices == 3:
|
|
508
|
+
fig, axs = plt.subplots(1, 3, figsize=figsize, tight_layout=True)
|
|
509
|
+
elif num_slices == 4:
|
|
510
|
+
fig, axs = plt.subplots(2, 2, figsize=figsize, tight_layout=True)
|
|
511
|
+
axs = qis.to_flat_list(axs)
|
|
512
|
+
else:
|
|
513
|
+
raise NotImplementedError
|
|
490
514
|
|
|
491
515
|
for idx, ttm in enumerate(option_chain.ttms):
|
|
492
516
|
if is_log_strike_xaxis:
|
|
493
517
|
strikes = np.log(option_chain.strikes_ttms[idx] / option_chain.forwards[idx])
|
|
494
|
-
xvar_format = '{:0.2f}'
|
|
495
518
|
strike_name = 'log-strike'
|
|
496
519
|
else:
|
|
497
520
|
strikes = option_chain.strikes_ttms[idx]
|
|
498
521
|
if variable_type == VariableType.LOG_RETURN:
|
|
499
|
-
xvar_format = '{:0,.2f}'
|
|
500
522
|
strike_name = 'strike'
|
|
523
|
+
elif variable_type == VariableType.Q_VAR:
|
|
524
|
+
strikes = option_chain.strikes_ttms[idx] / option_chain.forwards[idx]
|
|
525
|
+
strike_name = 'QVAR strike %'
|
|
501
526
|
else:
|
|
502
|
-
|
|
503
|
-
strike_name = 'QVAR strike'
|
|
527
|
+
raise NotImplementedError
|
|
504
528
|
|
|
505
529
|
model_vols = {}
|
|
506
530
|
for key, model_data in model_datas.items():
|
|
@@ -509,10 +533,8 @@ class ModelPricer(ABC):
|
|
|
509
533
|
model_vols = pd.DataFrame.from_dict(model_vols, orient='columns')
|
|
510
534
|
|
|
511
535
|
if option_chain.ids is not None:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
else:
|
|
515
|
-
title = f"slice - {option_chain.ids[idx]}"
|
|
536
|
+
title = f"{string.ascii_uppercase[idx]}) slice - {option_chain.ids[idx]}"
|
|
537
|
+
|
|
516
538
|
else:
|
|
517
539
|
title = f"{ttm=:0.2f}"
|
|
518
540
|
|
|
@@ -520,6 +542,8 @@ class ModelPricer(ABC):
|
|
|
520
542
|
fp=0.5 * (mc_data_lower[idx] + mc_data_upper[idx]))
|
|
521
543
|
if is_log_strike_xaxis:
|
|
522
544
|
atm_points = {'ATM': (0.0, atm_vol)}
|
|
545
|
+
elif variable_type == VariableType.Q_VAR:
|
|
546
|
+
atm_points = {'ATM': (1.0, atm_vol)}
|
|
523
547
|
else:
|
|
524
548
|
atm_points = {'ATM': (option_chain.forwards[idx], atm_vol)}
|
|
525
549
|
|
|
@@ -535,6 +559,6 @@ class ModelPricer(ABC):
|
|
|
535
559
|
atm_points=atm_points,
|
|
536
560
|
ylabel='Implied vols' if is_plot_vols else 'Model prices',
|
|
537
561
|
yvar_format='{:.0%}' if is_plot_vols else '{:.2f}',
|
|
538
|
-
ax=axs[idx]
|
|
562
|
+
ax=axs[idx],
|
|
539
563
|
**kwargs)
|
|
540
564
|
return fig
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stochvolmodels-1.0.9 → stochvolmodels-1.0.11}/stochvolmodels/pricers/logsv/vol_moments_ode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|