stochvolmodels 1.0.26__tar.gz → 1.0.28__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.
Files changed (48) hide show
  1. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/PKG-INFO +1 -1
  2. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/pyproject.toml +1 -1
  3. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/__init__.py +6 -1
  4. stochvolmodels-1.0.28/stochvolmodels/data/fetch_option_chain.py +174 -0
  5. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/examples/run_lognormal_sv_pricer.py +48 -1
  6. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/rate_logsv_pricer.py +2 -2
  7. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/hawkes_jd_pricer.py +1 -1
  8. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/heston_pricer.py +2 -2
  9. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/logsv_pricer.py +152 -15
  10. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/utils/funcs.py +4 -2
  11. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/LICENSE.txt +0 -0
  12. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/README.md +0 -0
  13. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/data/__init__.py +0 -0
  14. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/data/option_chain.py +0 -0
  15. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/data/test_option_chain.py +0 -0
  16. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/examples/quick_run_lognormal_sv_pricer.py +0 -0
  17. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/examples/run_heston.py +0 -0
  18. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/examples/run_heston_sv_pricer.py +0 -0
  19. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/examples/run_pricing_options_on_qvar.py +0 -0
  20. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/__init__.py +0 -0
  21. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/analytic/__init__.py +0 -0
  22. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/analytic/bachelier.py +0 -0
  23. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/analytic/bsm.py +0 -0
  24. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/analytic/tdist.py +0 -0
  25. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/double_exp_pricer.py +0 -0
  26. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/factor_hjm_pricer.py +0 -0
  27. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/rate_affine_expansion.py +0 -0
  28. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/rate_core.py +0 -0
  29. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/rate_evaluate.py +0 -0
  30. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/rate_factor_basis.py +0 -0
  31. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/rate_logsv_ivols.py +0 -0
  32. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/factor_hjm/rate_logsv_params.py +0 -0
  33. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/gmm_pricer.py +0 -0
  34. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/logsv/__init__.py +0 -0
  35. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/logsv/affine_expansion.py +0 -0
  36. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/logsv/logsv_params.py +0 -0
  37. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/logsv/vol_moments_ode.py +0 -0
  38. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/model_pricer.py +0 -0
  39. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/pricers/tdist_pricer.py +0 -0
  40. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/tests/__init__.py +0 -0
  41. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/tests/bsm_mgf_pricer.py +0 -0
  42. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/tests/qv_pricer.py +0 -0
  43. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/utils/__init__.py +0 -0
  44. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/utils/config.py +0 -0
  45. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/utils/mc_payoffs.py +0 -0
  46. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/utils/mgf_pricer.py +0 -0
  47. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/utils/plots.py +0 -0
  48. {stochvolmodels-1.0.26 → stochvolmodels-1.0.28}/stochvolmodels/utils/var_swap_pricer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: stochvolmodels
3
- Version: 1.0.26
3
+ Version: 1.0.28
4
4
  Summary: Implementation of stochastic volatility models for option pricing
5
5
  License: LICENSE.txt
6
6
  Keywords: volatility,options,Black-Scholes,Heston,Monte-Carlo
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "stochvolmodels"
3
- version = "1.0.26"
3
+ version = "1.0.28"
4
4
  description = "Implementation of stochastic volatility models for option pricing"
5
5
  license = "LICENSE.txt"
6
6
  authors = ["Artur Sepp <artursepp@gmail.com>"]
@@ -106,7 +106,10 @@ from stochvolmodels.pricers.logsv_pricer import (
106
106
  LOGSV_BTC_PARAMS,
107
107
  LogSVPricer,
108
108
  LogsvModelCalibrationType,
109
- ConstraintsType
109
+ ConstraintsType,
110
+ CalibrationEngine,
111
+ get_randoms_for_chain_valuation,
112
+ logsv_mc_chain_pricer_fixed_randoms
110
113
  )
111
114
  from stochvolmodels.pricers.logsv.logsv_params import LogSvParams
112
115
 
@@ -156,3 +159,5 @@ from stochvolmodels.utils.plots import (
156
159
 
157
160
 
158
161
  from stochvolmodels.pricers.logsv.vol_moments_ode import compute_analytic_qvar
162
+
163
+ from stochvolmodels.data.option_chain import generate_vol_chain_np
@@ -0,0 +1,174 @@
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
+ from qis import TimePeriod
11
+ from typing import Dict, Tuple, Optional, Literal
12
+ from numba.typed import List
13
+ from enum import Enum
14
+ import qis as qis
15
+ # chain
16
+ from option_chain_analytics import OptionsDataDFs, create_chain_from_from_options_dfs
17
+ from option_chain_analytics.option_chain import SliceColumn, SlicesChain
18
+ # analytics
19
+ from stochvolmodels.data.option_chain import OptionChain
20
+
21
+
22
+ def generate_vol_chain_np(chain: SlicesChain,
23
+ value_time: pd.Timestamp,
24
+ days_map: Dict[str, int] = {'1w': 7, '1m': 21},
25
+ delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
26
+ is_filtered: bool = True
27
+ ) -> OptionChain:
28
+ """
29
+ given SlicesChain generate OptionChain for calibration inputs
30
+ """
31
+
32
+ ttms, future_prices, discfactors = List(), List(), List()
33
+ optiontypes_ttms, strikes_ttms = List(), List()
34
+ bid_ivs, ask_ivs = List(), List()
35
+ bid_prices, ask_prices = List(), List()
36
+ slice_ids = []
37
+ for label, day in days_map.items():
38
+ next_date = value_time + pd.DateOffset(days=day) # if overlapping next date will be last avilable maturity
39
+ slice_date = chain.get_next_slice_after_date(mat_date=next_date)
40
+ slice_t = chain.expiry_slices[slice_date]
41
+ df = slice_t.get_joint_slice(delta_bounds=delta_bounds, is_filtered=is_filtered)
42
+ if not df.empty:
43
+ slice_ids.append(f"{label}: {slice_t.expiry_id}")
44
+ ttms.append(slice_t.get_ttm())
45
+ future_prices.append(slice_t.get_future_price())
46
+ discfactors.append(1.0)
47
+ strikes_ttms.append(df.index.to_numpy())
48
+ optiontypes_ttms.append(df[SliceColumn.OPTION_TYPE].to_numpy(dtype=str))
49
+ bid_ivs.append(df[SliceColumn.BID_IV].to_numpy())
50
+ ask_ivs.append(df[SliceColumn.ASK_IV].to_numpy())
51
+ bid_prices.append(df[SliceColumn.BID_PRICE].to_numpy())
52
+ ask_prices.append(df[SliceColumn.ASK_PRICE].to_numpy())
53
+
54
+ out = OptionChain(ttms=np.array(ttms),
55
+ forwards=np.array(future_prices),
56
+ discfactors=np.array(discfactors),
57
+ ids=np.array(slice_ids),
58
+ strikes_ttms=strikes_ttms,
59
+ optiontypes_ttms=optiontypes_ttms,
60
+ bid_ivs=bid_ivs,
61
+ ask_ivs=ask_ivs,
62
+ bid_prices=bid_prices,
63
+ ask_prices=ask_prices)
64
+ return out
65
+
66
+
67
+ def load_option_chain(options_data_dfs: OptionsDataDFs,
68
+ value_time: pd.Timestamp = pd.Timestamp('2023-02-06 08:00:00+00:00'),
69
+ days_map: Dict[str, int] = {'1w': 7, '1m': 21},
70
+ delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
71
+ is_filtered: bool = True
72
+ ) -> Optional[OptionChain]:
73
+ chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time)
74
+ if chain is not None:
75
+ option_chain = generate_vol_chain_np(chain=chain,
76
+ value_time=value_time,
77
+ days_map=days_map,
78
+ delta_bounds=delta_bounds,
79
+ is_filtered=is_filtered)
80
+ else:
81
+ option_chain = None
82
+
83
+ return option_chain
84
+
85
+
86
+ def sample_option_chain_at_times(options_data_dfs: OptionsDataDFs,
87
+ time_period: TimePeriod,
88
+ freq: str = 'W-FRI',
89
+ days_map: Dict[str, int] = {'1w': 7, '1m': 21},
90
+ delta_bounds: Tuple[Optional[float], Optional[float]] = (-0.1, 0.1),
91
+ hour_offset: int = 8
92
+ ) -> Dict[pd.Timestamp, OptionChain]:
93
+ value_times = qis.generate_dates_schedule(time_period=time_period,
94
+ freq=freq,
95
+ hour_offset=hour_offset)
96
+ option_chains = {}
97
+ for value_time in value_times:
98
+ option_chains[value_time] = load_option_chain(options_data_dfs=options_data_dfs,
99
+ value_time=value_time,
100
+ days_map=days_map,
101
+ delta_bounds=delta_bounds,
102
+ is_filtered=True)
103
+ return option_chains
104
+
105
+
106
+ def load_price_data(options_data_dfs: OptionsDataDFs,
107
+ time_period: TimePeriod = None,
108
+ data: Literal['spot', 'perp', 'funding_rate'] = 'spot',
109
+ freq: Optional[str] = 'D' # to do
110
+ ) -> pd.Series:
111
+ #options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker, freq='D', hour_offset=8))
112
+ spot_price = options_data_dfs.get_spot_data()[data]
113
+ if freq is not None:
114
+ spot_price = spot_price.resample(freq).last()
115
+ if time_period is not None:
116
+ spot_price = time_period.locate(spot_price)
117
+ return spot_price
118
+
119
+
120
+ class UnitTests(Enum):
121
+ PRINT_CHAIN_DATA = 1
122
+ GENERATE_VOL_CHAIN_NP = 2
123
+ SAMPLE_CHAIN_AT_TIMES = 3
124
+
125
+
126
+ def run_unit_test(unit_test: UnitTests):
127
+
128
+ ticker = 'BTC' # BTC, ETH
129
+ value_time = pd.Timestamp('2021-10-21 08:00:00+00:00')
130
+ value_time = pd.Timestamp('2023-10-06 08:00:00+00:00')
131
+
132
+ from option_chain_analytics.ts_loaders import ts_data_loader_wrapper
133
+ options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker))
134
+ options_data_dfs.get_start_end_date().print()
135
+ chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=value_time)
136
+
137
+ if unit_test == UnitTests.PRINT_CHAIN_DATA:
138
+ for expiry, eslice in chain.expiry_slices.items():
139
+ eslice.print()
140
+
141
+ elif unit_test == UnitTests.GENERATE_VOL_CHAIN_NP:
142
+ option_chain = generate_vol_chain_np(chain=chain,
143
+ value_time=value_time,
144
+ days_map={'1w': 7},
145
+ delta_bounds=(-0.1, 0.1),
146
+ is_filtered=True)
147
+ option_chain.print()
148
+ skews = option_chain.get_chain_skews(delta=0.35)
149
+ print(skews)
150
+
151
+ elif unit_test == UnitTests.SAMPLE_CHAIN_AT_TIMES:
152
+ time_period = qis.TimePeriod('01Jan2023', '31Jan2023', tz='UTC')
153
+ option_chains = sample_option_chain_at_times(options_data_dfs=options_data_dfs,
154
+ time_period=time_period,
155
+ freq='W-FRI',
156
+ hour_offset=9
157
+ )
158
+ for key, chain in option_chains.items():
159
+ print(f"{key}")
160
+ print(chain)
161
+
162
+ plt.show()
163
+
164
+
165
+ if __name__ == '__main__':
166
+
167
+ unit_test = UnitTests.SAMPLE_CHAIN_AT_TIMES
168
+
169
+ is_run_all_tests = False
170
+ if is_run_all_tests:
171
+ for unit_test in UnitTests:
172
+ run_unit_test(unit_test=unit_test)
173
+ else:
174
+ run_unit_test(unit_test=unit_test)
@@ -17,6 +17,8 @@ class UnitTests(Enum):
17
17
  COMPARE_MODEL_VOLS_TO_MC = 4
18
18
  PLOT_FIT_TO_BITCOIN_OPTION_CHAIN = 5
19
19
  CALIBRATE_MODEL_TO_BTC_OPTIONS = 6
20
+ MC_WITH_FIXED_RANDOMS = 7
21
+ CALIBRATE_MODEL_TO_BTC_OPTIONS_WITH_MC = 8
20
22
 
21
23
 
22
24
  def run_unit_test(unit_test: UnitTests):
@@ -95,6 +97,36 @@ def run_unit_test(unit_test: UnitTests):
95
97
  logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
96
98
  params=btc_calibrated_params)
97
99
 
100
+ elif unit_test == UnitTests.MC_WITH_FIXED_RANDOMS:
101
+ btc_option_chain = sv.get_btc_test_chain_data()
102
+ W0s, W1s, dts = sv.get_randoms_for_chain_valuation(ttms=btc_option_chain.ttms,
103
+ nb_path=10000,
104
+ nb_steps_per_year=360,
105
+ seed=10)
106
+ print(dts)
107
+ params0 = LogSvParams(sigma0=0.8, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=2.0)
108
+ vol_backbone_etas = params.get_vol_backbone_etas(ttms=btc_option_chain.ttms)
109
+ args = dict(ttms=btc_option_chain.ttms,
110
+ forwards=btc_option_chain.forwards,
111
+ discfactors=btc_option_chain.discfactors,
112
+ strikes_ttms=btc_option_chain.strikes_ttms,
113
+ optiontypes_ttms=btc_option_chain.optiontypes_ttms,
114
+ W0s=W0s,
115
+ W1s=W1s,
116
+ dts=dts,
117
+ v0=params0.sigma0,
118
+ theta=params0.theta,
119
+ kappa1=params0.kappa1,
120
+ kappa2=params0.kappa2,
121
+ beta=params0.beta,
122
+ volvol=params0.volvol,
123
+ vol_backbone_etas=vol_backbone_etas)
124
+ option_prices_ttm, option_std_ttm = sv.logsv_mc_chain_pricer_fixed_randoms(**args)
125
+ print(option_prices_ttm)
126
+
127
+ option_prices_ttm, option_std_ttm = sv.logsv_mc_chain_pricer_fixed_randoms(**args)
128
+ print(option_prices_ttm)
129
+
98
130
  elif unit_test == UnitTests.CALIBRATE_MODEL_TO_BTC_OPTIONS:
99
131
  btc_option_chain = sv.get_btc_test_chain_data()
100
132
  params0 = LogSvParams(sigma0=0.8, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=2.0)
@@ -105,12 +137,27 @@ def run_unit_test(unit_test: UnitTests):
105
137
  print(btc_calibrated_params)
106
138
  logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
107
139
  params=btc_calibrated_params)
140
+
141
+ elif unit_test == UnitTests.CALIBRATE_MODEL_TO_BTC_OPTIONS_WITH_MC:
142
+ btc_option_chain = sv.get_btc_test_chain_data()
143
+ params0 = LogSvParams(sigma0=0.8, theta=1.0, kappa1=2.21, kappa2=2.18, beta=0.15, volvol=2.0)
144
+ btc_calibrated_params = logsv_pricer.calibrate_model_params_to_chain(option_chain=btc_option_chain,
145
+ params0=params0,
146
+ model_calibration_type=LogsvModelCalibrationType.PARAMS4,
147
+ constraints_type=sv.ConstraintsType.INVERSE_MARTINGALE,
148
+ calibration_engine=sv.CalibrationEngine.MC,
149
+ nb_path=100000,
150
+ seed=7)
151
+ print(btc_calibrated_params)
152
+ logsv_pricer.plot_model_ivols_vs_bid_ask(option_chain=btc_option_chain,
153
+ params=btc_calibrated_params)
154
+
108
155
  plt.show()
109
156
 
110
157
 
111
158
  if __name__ == '__main__':
112
159
 
113
- unit_test = UnitTests.COMPUTE_MODEL_PRICES
160
+ unit_test = UnitTests.CALIBRATE_MODEL_TO_BTC_OPTIONS
114
161
 
115
162
  is_run_all_tests = False
116
163
  if is_run_all_tests:
@@ -877,7 +877,7 @@ def simulate_logsv_MF(ttms: np.ndarray,
877
877
  if seed is None:
878
878
  seed = 16
879
879
  np.random.seed(seed) # fix seed
880
- nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps=int(np.ceil(year_days*ttm)))
880
+ nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps_per_year=360)
881
881
  if W is None:
882
882
  W0 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path, basis.get_nb_factors())) # TODO: undo
883
883
  W1 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path)) # TODO: undo
@@ -1017,7 +1017,7 @@ def simulate_logsv_futures_MF2(params: MultiFactRateLogSvParams,
1017
1017
  if seed is None:
1018
1018
  seed = 16
1019
1019
  np.random.seed(seed) # fix seed
1020
- nb_steps, dt, grid_t = set_time_grid(ttm=ttm, year_days=720)
1020
+ nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps_per_year=720)
1021
1021
  if W is None:
1022
1022
  W0 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path, basis.get_nb_factors())) # TODO: undo
1023
1023
  W1 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path)) # TODO: undo
@@ -696,7 +696,7 @@ def simulate_hawkesjd_terminal(ttm: float,
696
696
  assert lambda_m0.shape[0] == nb_path
697
697
 
698
698
  # vars
699
- nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps=int(5*360)) # need small dt step for large intensities
699
+ nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps_per_year=5*360) # need small dt step for large intensities
700
700
  W0 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path))
701
701
  U_P = -np.log(np.random.uniform(low=1e-16, high=1.0, size=(nb_steps, nb_path)))/dt
702
702
  U_M = -np.log(np.random.uniform(low=1e-16, high=1.0, size=(nb_steps, nb_path)))/dt
@@ -298,7 +298,7 @@ def simulate_heston_x_vol_terminal(ttm: float,
298
298
  rho: float,
299
299
  volvol: float,
300
300
  nb_path: int = 100000,
301
- nb_steps: int = 360
301
+ nb_steps_per_year: int = 360
302
302
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
303
303
 
304
304
  if x0.shape[0] == 1: # initial value
@@ -316,7 +316,7 @@ def simulate_heston_x_vol_terminal(ttm: float,
316
316
  else:
317
317
  assert qvar0.shape[0] == nb_path
318
318
 
319
- nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps=nb_steps)
319
+ nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps_per_year=nb_steps_per_year)
320
320
  w0 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path))
321
321
  w1 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path))
322
322
 
@@ -15,7 +15,7 @@ from enum import Enum
15
15
  from stochvolmodels.utils.config import VariableType
16
16
  import stochvolmodels.utils.mgf_pricer as mgfp
17
17
  from stochvolmodels.utils.mc_payoffs import compute_mc_vars_payoff
18
- from stochvolmodels.utils.funcs import to_flat_np_array, set_time_grid, timer, compute_histogram_data
18
+ from stochvolmodels.utils.funcs import to_flat_np_array, set_time_grid, timer, compute_histogram_data, set_seed
19
19
 
20
20
  # stochvolmodels pricers
21
21
  from stochvolmodels.pricers.logsv.logsv_params import LogSvParams
@@ -44,6 +44,11 @@ class ConstraintsType(Enum):
44
44
  INVERSE_MARTINGALE_MOMENT4 = 5 # kappa_2 >= 2.0*beta
45
45
 
46
46
 
47
+ class CalibrationEngine(Enum):
48
+ ANALYTIC = 1
49
+ MC = 2
50
+
51
+
47
52
  LOGSV_BTC_PARAMS = LogSvParams(sigma0=0.8376, theta=1.0413, kappa1=3.1844, kappa2=3.058, beta=0.1514, volvol=1.8458)
48
53
 
49
54
 
@@ -95,7 +100,7 @@ class LogSVPricer(ModelPricer):
95
100
  is_spot_measure=is_spot_measure,
96
101
  variable_type=variable_type,
97
102
  nb_path=nb_path,
98
- nb_steps=nb_steps or int(360*np.max(option_chain.ttms))+1)
103
+ nb_steps_per_year=nb_steps or int(360 * np.max(option_chain.ttms)) + 1)
99
104
 
100
105
  def set_vol_scaler(self, option_chain: OptionChain) -> float:
101
106
  """
@@ -115,6 +120,10 @@ class LogSVPricer(ModelPricer):
115
120
  is_unit_ttm_vega: bool = False,
116
121
  model_calibration_type: LogsvModelCalibrationType = LogsvModelCalibrationType.PARAMS5,
117
122
  constraints_type: ConstraintsType = ConstraintsType.UNCONSTRAINT,
123
+ calibration_engine: CalibrationEngine = CalibrationEngine.ANALYTIC,
124
+ nb_path: int = 100000,
125
+ nb_steps: int = 360,
126
+ seed: int = 10,
118
127
  **kwargs
119
128
  ) -> LogSvParams:
120
129
  """
@@ -169,9 +178,38 @@ class LogSVPricer(ModelPricer):
169
178
  raise NotImplementedError(f"{model_calibration_type}")
170
179
  return fit_params
171
180
 
181
+ if calibration_engine == CalibrationEngine.MC:
182
+ W0s, W1s, dts = get_randoms_for_chain_valuation(ttms=option_chain.ttms, nb_path=nb_path, nb_steps_per_year=nb_steps, seed=seed)
183
+
172
184
  def objective(pars: np.ndarray, args: np.ndarray) -> float:
173
185
  params = parse_model_params(pars=pars)
174
- model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, vol_scaler=vol_scaler)
186
+
187
+ if calibration_engine == CalibrationEngine.ANALYTIC:
188
+ model_vols = self.compute_model_ivols_for_chain(option_chain=option_chain, params=params, vol_scaler=vol_scaler)
189
+
190
+ elif calibration_engine == CalibrationEngine.MC:
191
+ option_prices_ttm, option_std_ttm = logsv_mc_chain_pricer_fixed_randoms(ttms=option_chain.ttms,
192
+ forwards=option_chain.forwards,
193
+ discfactors=option_chain.discfactors,
194
+ strikes_ttms=option_chain.strikes_ttms,
195
+ optiontypes_ttms=option_chain.optiontypes_ttms,
196
+ W0s=W0s,
197
+ W1s=W1s,
198
+ dts=dts,
199
+ v0=params.sigma0,
200
+ theta=params.theta,
201
+ kappa1=params.kappa1,
202
+ kappa2=params.kappa2,
203
+ beta=params.beta,
204
+ volvol=params.volvol,
205
+ vol_backbone_etas=params.get_vol_backbone_etas(ttms=option_chain.ttms))
206
+ model_vols = option_chain.compute_model_ivols_from_chain_data(model_prices=option_prices_ttm)
207
+ # print(f"option_prices_ttm\n{option_prices_ttm}")
208
+ # print(f"model_vols\n{model_vols}")
209
+
210
+ else:
211
+ raise NotImplementedError(f"{calibration_engine}")
212
+
175
213
  resid = np.nansum(weights * np.square(to_flat_np_array(model_vols) - market_vols))
176
214
  return resid
177
215
 
@@ -278,7 +316,7 @@ class LogSVPricer(ModelPricer):
278
316
  volvol=params.volvol,
279
317
  nb_path=nb_path,
280
318
  is_spot_measure=is_spot_measure,
281
- nb_steps=nb_steps,
319
+ nb_steps_per_year=nb_steps,
282
320
  brownians=brownians,
283
321
  **kwargs)
284
322
  return sigma_t, grid_t
@@ -512,7 +550,7 @@ def logsv_mc_chain_pricer(ttms: np.ndarray,
512
550
  vol_backbone_etas: np.ndarray,
513
551
  is_spot_measure: bool = True,
514
552
  nb_path: int = 100000,
515
- nb_steps: int = 360,
553
+ nb_steps_per_year: int = 360,
516
554
  variable_type: VariableType = VariableType.LOG_RETURN
517
555
  ) -> Tuple[List[np.ndarray], List[np.ndarray]]:
518
556
  # starting values
@@ -538,7 +576,7 @@ def logsv_mc_chain_pricer(ttms: np.ndarray,
538
576
  volvol=volvol,
539
577
  vol_backbone_eta=vol_backbone_eta,
540
578
  nb_path=nb_path,
541
- nb_steps=nb_steps,
579
+ nb_steps_per_year=nb_steps_per_year,
542
580
  is_spot_measure=is_spot_measure)
543
581
  ttm0 = ttm
544
582
  option_prices, option_std = compute_mc_vars_payoff(x0=x0, sigma0=sigma0, qvar0=qvar0,
@@ -564,7 +602,7 @@ def simulate_vol_paths(ttm: float,
564
602
  volvol: float,
565
603
  is_spot_measure: bool = True,
566
604
  nb_path: int = 100000,
567
- nb_steps: int = 360,
605
+ nb_steps_per_year: int = 360,
568
606
  brownians: np.ndarray = None,
569
607
  **kwargs
570
608
  ) -> Tuple[np.ndarray, np.ndarray]:
@@ -573,7 +611,7 @@ def simulate_vol_paths(ttm: float,
573
611
  """
574
612
  sigma0 = v0 * np.ones(nb_path)
575
613
 
576
- nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps=nb_steps)
614
+ nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps_per_year=nb_steps_per_year)
577
615
 
578
616
  if brownians is None:
579
617
  brownians = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path))
@@ -586,7 +624,7 @@ def simulate_vol_paths(ttm: float,
586
624
  vartheta2 = beta*beta + volvol*volvol
587
625
  vartheta = np.sqrt(vartheta2)
588
626
  vol_var = np.log(sigma0)
589
- sigma_t = np.zeros((nb_steps+1, nb_path)) # sigma grid will increase to include the sigma_0 at t0 = 0
627
+ sigma_t = np.zeros((nb_steps_per_year + 1, nb_path)) # sigma grid will increase to include the sigma_0 at t0 = 0
590
628
  sigma_t[0, :] = sigma0 # keep first value
591
629
  for t_, w1_ in enumerate(brownians):
592
630
  vol_var = vol_var + ((kappa1 * theta / sigma0 - kappa1) + kappa2*(theta-sigma0) + adj*sigma0 - 0.5*vartheta2) * dt + vartheta*w1_
@@ -609,7 +647,10 @@ def simulate_logsv_x_vol_terminal(ttm: float,
609
647
  vol_backbone_eta: float = 1.0,
610
648
  is_spot_measure: bool = True,
611
649
  nb_path: int = 100000,
612
- nb_steps: int = 360
650
+ nb_steps_per_year: int = 360,
651
+ W0: Optional[np.ndarray] = None,
652
+ W1: Optional[np.ndarray] = None,
653
+ dt: Optional[float] = None
613
654
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
614
655
  """
615
656
  mc simulator for terminal values of log-return, vol sigma0, and qvar for log sv model
@@ -628,10 +669,16 @@ def simulate_logsv_x_vol_terminal(ttm: float,
628
669
  sigma0 = sigma0 * np.ones(nb_path)
629
670
  else:
630
671
  assert sigma0.shape[0] == nb_path
631
-
632
- nb_steps, dt, grid_t = set_time_grid(ttm=ttm, nb_steps=nb_steps)
633
- W0 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path))
634
- W1 = np.sqrt(dt) * np.random.normal(0, 1, size=(nb_steps, nb_path))
672
+ if W0 is None and W1 is None:
673
+ nb_steps1, dt, grid_t = set_time_grid(ttm=ttm, nb_steps_per_year=nb_steps_per_year)
674
+ print(f"nb_steps1={nb_steps1}, dt={dt}")
675
+ sdt = np.sqrt(dt)
676
+ W0_ = sdt * np.random.normal(0, 1, size=(nb_steps1, nb_path))
677
+ W1_ = sdt * np.random.normal(0, 1, size=(nb_steps1, nb_path))
678
+ else:
679
+ sdt = np.sqrt(dt)
680
+ W0_ = sdt * W0
681
+ W1_ = sdt * W1
635
682
 
636
683
  if is_spot_measure:
637
684
  alpha, adj = -1.0, 0.0
@@ -641,7 +688,7 @@ def simulate_logsv_x_vol_terminal(ttm: float,
641
688
  vartheta2 = beta*beta + volvol*volvol
642
689
  vol_backbone_eta2 = vol_backbone_eta * vol_backbone_eta
643
690
  vol_var = np.log(sigma0)
644
- for t_, (w0, w1) in enumerate(zip(W0, W1)):
691
+ for t_, (w0, w1) in enumerate(zip(W0_, W1_)):
645
692
  sigma0_2dt = vol_backbone_eta2 * sigma0 * sigma0 * dt
646
693
  x0 = x0 + alpha * 0.5 * sigma0_2dt + vol_backbone_eta * sigma0 * w0
647
694
  vol_var = vol_var + ((kappa1 * theta / sigma0 - kappa1) + kappa2*(theta-sigma0) + adj*sigma0 - 0.5*vartheta2) * dt + beta*w0+volvol*w1
@@ -651,6 +698,96 @@ def simulate_logsv_x_vol_terminal(ttm: float,
651
698
  return x0, sigma0, qvar0
652
699
 
653
700
 
701
+ def get_randoms_for_chain_valuation(ttms: np.ndarray,
702
+ nb_path: int = 100000,
703
+ nb_steps_per_year: int = 360,
704
+ seed: int = 10
705
+ ) -> Tuple[List[np.ndarray], List[np.ndarray], List[np.ndarray]]:
706
+ """
707
+ we need to fix random normals for suesequent evaluation using mc slices
708
+ outputs as numpy lists
709
+ """
710
+ #
711
+ set_seed(seed)
712
+ W0s = List()
713
+ W1s = List()
714
+ dts = List()
715
+ ttm0 = 0.0
716
+ for ttm in ttms:
717
+ # qqq
718
+ nb_steps_, dt, grid_t = set_time_grid(ttm=ttm - ttm0, nb_steps_per_year=nb_steps_per_year)
719
+ W0s.append(np.random.normal(0, 1, size=(nb_steps_, nb_path)))
720
+ W1s.append(np.random.normal(0, 1, size=(nb_steps_, nb_path)))
721
+ dts.append(dt)
722
+ ttm0 = ttm
723
+ return W0s, W1s, dts
724
+
725
+
726
+ @njit(cache=False, fastmath=True)
727
+ def logsv_mc_chain_pricer_fixed_randoms(ttms: np.ndarray,
728
+ forwards: np.ndarray,
729
+ discfactors: np.ndarray,
730
+ strikes_ttms: Tuple[np.ndarray,...],
731
+ optiontypes_ttms: Tuple[np.ndarray, ...],
732
+ W0s: Tuple[np.ndarray, ...],
733
+ W1s: Tuple[np.ndarray, ...],
734
+ dts: Tuple[np.ndarray, ...],
735
+ v0: float,
736
+ theta: float,
737
+ kappa1: float,
738
+ kappa2: float,
739
+ beta: float,
740
+ volvol: float,
741
+ vol_backbone_etas: np.ndarray,
742
+ is_spot_measure: bool = True,
743
+ variable_type: VariableType = VariableType.LOG_RETURN
744
+ ) -> Tuple[List[np.ndarray], List[np.ndarray]]:
745
+ """
746
+ chain valuation using fixed randoms
747
+ """
748
+ # starting values
749
+ nb_path = W0s[0].shape[1]
750
+ x0 = np.zeros(nb_path)
751
+ qvar0 = np.zeros(nb_path)
752
+ sigma0 = v0*np.ones(nb_path)
753
+ ttm0 = 0.0
754
+
755
+ # outputs as numpy lists
756
+ option_prices_ttm = List()
757
+ option_std_ttm = List()
758
+ for ttm, forward, discfactor, strikes_ttm, optiontypes_ttm, vol_backbone_eta, W0, W1, dt in zip(ttms, forwards, discfactors,
759
+ strikes_ttms, optiontypes_ttms,
760
+ vol_backbone_etas,
761
+ W0s, W1s, dts):
762
+ x0, sigma0, qvar0 = simulate_logsv_x_vol_terminal(ttm=ttm - ttm0,
763
+ x0=x0,
764
+ sigma0=sigma0,
765
+ qvar0=qvar0,
766
+ theta=theta,
767
+ kappa1=kappa1,
768
+ kappa2=kappa2,
769
+ beta=beta,
770
+ volvol=volvol,
771
+ vol_backbone_eta=vol_backbone_eta,
772
+ nb_path=nb_path,
773
+ dt=dt,
774
+ is_spot_measure=is_spot_measure,
775
+ W0=W0,
776
+ W1=W1)
777
+ ttm0 = ttm
778
+ option_prices, option_std = compute_mc_vars_payoff(x0=x0, sigma0=sigma0, qvar0=qvar0,
779
+ ttm=ttm,
780
+ forward=forward,
781
+ strikes_ttm=strikes_ttm,
782
+ optiontypes_ttm=optiontypes_ttm,
783
+ discfactor=discfactor,
784
+ variable_type=variable_type)
785
+ option_prices_ttm.append(option_prices)
786
+ option_std_ttm.append(option_std)
787
+
788
+ return option_prices_ttm, option_std_ttm
789
+
790
+
654
791
  class UnitTests(Enum):
655
792
  CHAIN_PRICER = 1
656
793
  SLICE_PRICER = 2
@@ -15,11 +15,13 @@ def to_flat_np_array(input_list: List[np.ndarray]) -> np.ndarray:
15
15
 
16
16
 
17
17
  @njit(cache=False, fastmath=False)
18
- def set_time_grid(ttm: float, nb_steps: int = 360) -> Tuple[int, float, np.ndarray]:
18
+ def set_time_grid(ttm: float, nb_steps_per_year: int = 360) -> Tuple[int, float, np.ndarray]:
19
19
  """
20
20
  set daily steps
21
21
  """
22
+ nb_steps = int(ttm * nb_steps_per_year) + 1
22
23
  grid_t = np.linspace(0.0, ttm, nb_steps + 1)
24
+ # dt = ttm / nb_steps
23
25
  dt = grid_t[1] - grid_t[0]
24
26
  return nb_steps, dt, grid_t
25
27
 
@@ -102,7 +104,7 @@ def find_nearest(a: np.ndarray,
102
104
  is_equal_or_largest: bool = False
103
105
  ) -> float:
104
106
  """
105
- find closes element
107
+ find closest element
106
108
  https://stackoverflow.com/questions/2566412/find-nearest-value-in-numpy-array
107
109
  """
108
110
  if is_sorted: