sdevpy 1.0.2__tar.gz → 1.0.3__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 (55) hide show
  1. {sdevpy-1.0.2/src/sdevpy.egg-info → sdevpy-1.0.3}/PKG-INFO +2 -1
  2. {sdevpy-1.0.2 → sdevpy-1.0.3}/pyproject.toml +2 -2
  3. sdevpy-1.0.3/src/sdevpy/__init__.py +1 -0
  4. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/machinelearning/learningmodel.py +6 -2
  5. sdevpy-1.0.3/src/sdevpy/machinelearning/learningschedules.py +78 -0
  6. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/machinelearning/topology.py +1 -1
  7. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovolinverse/stovolinvgen.py +4 -3
  8. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovolinverse/stovolinvtrain.py +48 -68
  9. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/sabrgenerator.py +33 -24
  10. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/smilegenerator.py +36 -0
  11. {sdevpy-1.0.2 → sdevpy-1.0.3/src/sdevpy.egg-info}/PKG-INFO +2 -1
  12. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/requires.txt +1 -0
  13. sdevpy-1.0.2/src/sdevpy/__init__.py +0 -1
  14. sdevpy-1.0.2/src/sdevpy/machinelearning/learningschedules.py +0 -54
  15. {sdevpy-1.0.2 → sdevpy-1.0.3}/LICENSE +0 -0
  16. {sdevpy-1.0.2 → sdevpy-1.0.3}/README.md +0 -0
  17. {sdevpy-1.0.2 → sdevpy-1.0.3}/setup.cfg +0 -0
  18. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/bachelier.py +0 -0
  19. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/black.py +0 -0
  20. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/fbsabr.py +0 -0
  21. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/mcheston.py +0 -0
  22. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/mcsabr.py +0 -0
  23. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/mczabr.py +0 -0
  24. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/sabr.py +0 -0
  25. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/machinelearning/callbacks.py +0 -0
  26. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/machinelearning/datasets.py +0 -0
  27. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/interpolations.py +0 -0
  28. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/metrics.py +0 -0
  29. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/optimization.py +0 -0
  30. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/rand.py +0 -0
  31. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/montecarlo/smoothers.py +0 -0
  32. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/aad/aad_mc.py +0 -0
  33. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/aad/aad_mc_nd.py +0 -0
  34. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/datafiles.py +0 -0
  35. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovolgen.py +0 -0
  36. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovolplot.py +0 -0
  37. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovoltrain.py +0 -0
  38. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/settings.py +0 -0
  39. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/test.py +0 -0
  40. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/clipboard.py +0 -0
  41. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/constants.py +0 -0
  42. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/filemanager.py +0 -0
  43. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/jsonmanager.py +0 -0
  44. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/timegrids.py +0 -0
  45. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/timer.py +0 -0
  46. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/utils.py +0 -0
  47. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/fbsabrgenerator.py +0 -0
  48. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mchestongenerator.py +0 -0
  49. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mcsabrgenerator.py +0 -0
  50. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mczabrgenerator.py +0 -0
  51. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/stovolfactory.py +0 -0
  52. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/SOURCES.txt +0 -0
  53. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/dependency_links.txt +0 -0
  54. {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/top_level.txt +0 -0
  55. {sdevpy-1.0.2 → sdevpy-1.0.3}/tests/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sdevpy
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Python package for Machine Learning in Finance
5
5
  Author-email: Sebastien Gurrieri <sebgur@gmail.com>
6
6
  Project-URL: Git page, https://github.com/sebgur/SDev.Python
@@ -18,6 +18,7 @@ Requires-Dist: numpy
18
18
  Requires-Dist: tensorflow
19
19
  Requires-Dist: scikit-learn
20
20
  Requires-Dist: tensorflow_probability
21
+ Requires-Dist: silence_tensorflow
21
22
 
22
23
  # SDev.Python
23
24
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdevpy"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  authors = [
9
9
  { name="Sebastien Gurrieri", email="sebgur@gmail.com" },
10
10
  ]
@@ -18,7 +18,7 @@ classifiers = [
18
18
  ]
19
19
  dependencies = [
20
20
  "pandas","pyperclip","py_vollib","numpy","tensorflow",
21
- "scikit-learn", "tensorflow_probability"
21
+ "scikit-learn", "tensorflow_probability", "silence_tensorflow"
22
22
  ]
23
23
 
24
24
  [project.urls]
@@ -0,0 +1 @@
1
+ __version__ = '1.0.3'
@@ -7,6 +7,7 @@ import tensorflow as tf
7
7
  import joblib
8
8
  import absl.logging
9
9
  from sdevpy.tools import jsonmanager
10
+ from sdevpy.tools import filemanager
10
11
 
11
12
  class LearningModel:
12
13
  """ Wrapper class for machine learning models, including scalers, and simplifying
@@ -54,10 +55,12 @@ class LearningModel:
54
55
 
55
56
  def save(self, path):
56
57
  """ Save model and its scalers to files """
58
+ filemanager.check_directory(path)
57
59
  # Save keras model first. Turn dummy warning off temporarily.
58
60
  verbosity = absl.logging.get_verbosity()
59
61
  absl.logging.set_verbosity(absl.logging.ERROR)
60
- self.model.save(path)
62
+ model_file = os.path.join(path, "model.keras")
63
+ self.model.save(model_file)
61
64
  absl.logging.set_verbosity(verbosity)
62
65
 
63
66
  # Save scalers
@@ -132,7 +135,8 @@ def load_learning_model(path, compile_=False):
132
135
  if os.path.exists(path) is False:
133
136
  raise RuntimeError("Model folder does not exist: " + path)
134
137
 
135
- keras_model = tf.keras.models.load_model(path, compile=compile_)
138
+ model_file = os.path.join(path, "model.keras")
139
+ keras_model = tf.keras.models.load_model(model_file, compile=compile_)
136
140
 
137
141
  x_scaler_file, y_scaler_file = scaler_files(path)
138
142
  if os.path.exists(x_scaler_file) and os.path.exists(y_scaler_file):
@@ -0,0 +1,78 @@
1
+ """ Custom learning schedules """
2
+ import tensorflow as tf
3
+ import math
4
+ from sdevpy.tools.constants import TWO_PI
5
+
6
+ # Custom learning rate scheduler, exponentially decreases between given values
7
+ class FlooredExponentialDecay(tf.keras.optimizers.schedules.LearningRateSchedule):
8
+ """ Custom learning rate scheduler, exponentially decreases between given values """
9
+ def __init__(self, num_samples, batch_size, target_epoch, initial_lr=1e-1, final_lr=1e-4):
10
+ self.initial_lr = initial_lr
11
+ self.final_lr = final_lr
12
+ # self.decay = decay
13
+ # self.decay_steps = decay_steps
14
+ # A step is the usage of one gradient, i.e. for one batch. As we go through the whole sample
15
+ # in 1 epoch, the number of steps per epoch is given by the number of batches per epoch
16
+ # i.e. the formula below.
17
+ steps_per_epoch = num_samples / batch_size
18
+ percent_reached = 0.10 # Percentage of the final LR reached by the chosen epoch
19
+ self.decay = final_lr * percent_reached / (initial_lr - final_lr)
20
+ self.steps_to_target = np.float32(steps_per_epoch * target_epoch)
21
+
22
+ def __call__(self, step):
23
+ ratio = tf.cast(step / self.steps_to_target, tf.float32)
24
+ coeff = tf.pow(self.decay, ratio)
25
+ ampl = self.initial_lr - self.final_lr
26
+ return self.final_lr + ampl * coeff
27
+
28
+ # def __call__(self, step):
29
+ # ratio = tf.cast(step / self.decay_steps, tf.float32)
30
+ # coeff = tf.pow(self.decay, ratio)
31
+ # return self.initial_lr * coeff + self.final_lr * (1.0 - coeff)
32
+
33
+ def get_config(self):
34
+ config = { 'initial_lr': self.initial_lr,
35
+ 'final_lr': self.final_lr,
36
+ 'decay': self.decay,
37
+ 'decay_steps': self.steps_to_target }
38
+ return config
39
+
40
+ import numpy as np
41
+
42
+ # Custom learning rate scheduler, cyclically exponentially decreases between given values
43
+ class CyclicalExponentialDecay(tf.keras.optimizers.schedules.LearningRateSchedule):
44
+ """ Custom learning rate scheduler, cyclically exponentially decreases between given values """
45
+ def __init__(self, num_samples, batch_size, target_epoch, initial_lr=1e-1, final_lr=1e-4,
46
+ periods=10.0):
47
+ # Amplitude decay
48
+ self.initial_lr = initial_lr
49
+ self.final_lr = final_lr
50
+ # self.period = periods
51
+ # A step is the usage of one gradient, i.e. for one batch. As we go through the whole sample
52
+ # in 1 epoch, the number of steps per epoch is given by the number of batches per epoch
53
+ # i.e. the formula below.
54
+ steps_per_epoch = num_samples / batch_size
55
+ percent_reached = 0.10 # Percentage of the final LR reached by the chosen epoch
56
+ self.decay = final_lr * percent_reached / (initial_lr - final_lr)
57
+ self.steps_to_target = np.float32(steps_per_epoch * target_epoch)
58
+
59
+ # Oscillations
60
+ self.steps_per_period = np.float32(target_epoch * steps_per_epoch / periods)
61
+
62
+ def __call__(self, step):
63
+ ratio = tf.cast(step / self.steps_to_target, tf.float32)
64
+ coeff = tf.pow(self.decay, ratio)
65
+ ampl = self.initial_lr - self.final_lr
66
+ two_pi = tf.cast(TWO_PI, tf.float32)
67
+ arg = tf.cast(step / self.steps_per_period, tf.float32)
68
+ oscillation = (2.0 + tf.math.cos(arg * two_pi)) / 2.0 # Between 0.5 and 1.5
69
+ ampl = ampl * oscillation
70
+ return self.final_lr + ampl * coeff
71
+
72
+ def get_config(self):
73
+ config = { 'initial_lr': self.initial_lr,
74
+ 'final_lr': self.final_lr,
75
+ 'decay': self.decay,
76
+ 'steps_to_target': self.steps_to_target,
77
+ 'steps_per_period': self.steps_per_period }
78
+ return config
@@ -20,7 +20,7 @@ def compose_model(num_inputs, num_outputs, hidden_layers, neurons, dropout=0.2):
20
20
  model = tf.keras.Sequential()
21
21
 
22
22
  # Input layer
23
- model.add(tf.keras.Input(num_inputs))
23
+ model.add(tf.keras.Input(shape=(num_inputs,)))
24
24
 
25
25
  # Hidden layers
26
26
  for layer in hidden_layers:
@@ -15,13 +15,14 @@ MODEL_TYPE = "SABR"
15
15
  # MODEL_TYPE = "McZABR"
16
16
  # MODEL_TYPE = "McHeston"
17
17
  SHIFT = 0.03
18
- NUM_SAMPLES = 100 * 1000
18
+ NUM_SAMPLES = 1000 * 1000
19
19
  # The 4 parameters below are only relevant for models whose reference is calculated by MC
20
20
  NUM_EXPIRIES = 15
21
21
  NUM_MC = 100 * 1000 # 100 * 1000
22
22
  POINTS_PER_YEAR = 25 # 25
23
- SEED = 1357 # [1357, 8642, 1000, 8888, 4444, 2222, 1111, 4321, 1234, 42]
23
+ SEED = 1234 # [1357, 8642, 1000, 8888, 4444, 2222, 1111, 4321, 1234, 42]
24
24
  SPREADS = [-200, -100, -75, -50, -25, -10, 0, 10, 25, 50, 75, 100, 200]
25
+ USE_NVOL = True
25
26
 
26
27
  print(">> Set up runtime configuration")
27
28
  project_folder = os.path.join(settings.WORKFOLDER, "stovolinv")
@@ -57,7 +58,7 @@ print(">> Generate dataset")
57
58
  print(f"> Generate {NUM_SAMPLES:,} price samples")
58
59
  timer_gen = Stopwatch("Generating Samples")
59
60
  timer_gen.trigger()
60
- data_df = generator.generate_samples_inverse(NUM_SAMPLES, RANGES, SPREADS)
61
+ data_df = generator.generate_samples_inverse(NUM_SAMPLES, RANGES, SPREADS, USE_NVOL)
61
62
  timer_gen.stop()
62
63
 
63
64
  timer_out = Stopwatch("File Output")
@@ -24,6 +24,9 @@ from sdevpy.projects.stovol import stovolplot as xplt
24
24
 
25
25
 
26
26
  # ################ ToDo ###########################################################################
27
+ # Start additional script with simplified function starting from trained model only, to test
28
+ # against classic calibration, shifting parameters, sensies, etc.
29
+
27
30
  # We could generate market points from SABR, then generate a new set from a slightly different
28
31
  # set of SABR parameters such as a rho move, a nu move, etc. Then re-calibrate by optimization
29
32
  # vs using the network and see if they produce expected move. For instance if the second
@@ -55,18 +58,19 @@ SHIFT = 0.03
55
58
  USE_TRAINED = True
56
59
  DOWNLOAD_MODELS = False # Only used when USE_TRAINED is True
57
60
  DOWNLOAD_DATASETS = False # Use when already created/downloaded
58
- TRAIN = False
61
+ TRAIN = True
59
62
  if USE_TRAINED is False and TRAIN is False:
60
63
  raise RuntimeError("When not using pre-trained models, a new model must be trained")
61
64
 
62
- NUM_SAMPLES = 10 * 1000#2 * 1000 * 1000 # Number of samples to read from sample files
65
+ NUM_SAMPLES = 200 * 1000#2 * 1000 * 1000 # Number of samples to read from sample files
63
66
  TRAIN_PERCENT = 0.90 # Proportion of dataset used for training (rest used for test)
64
- EPOCHS = 200
67
+ EPOCHS = 50
65
68
  BATCH_SIZE = 1000
66
69
  SHOW_VOL_CHARTS = True # Show smile section charts
67
70
  # For comparison to reference values (accuracy of reference)
68
71
  NUM_MC = 100 * 1000 # 100 * 1000
69
72
  POINTS_PER_YEAR = 25# 25
73
+ USE_NVOL = True
70
74
  project_folder = os.path.join(settings.WORKFOLDER, "stovolinv")
71
75
 
72
76
  print(">> Set up runtime configuration")
@@ -146,6 +150,7 @@ else:
146
150
  print(">> Composing new model")
147
151
  # Initialize the model
148
152
  HIDDEN_LAYERS = ['softplus', 'softplus', 'softplus']
153
+ # NUM_NEURONS = 128
149
154
  NUM_NEURONS = 64
150
155
  DROP_OUT = 0.0
151
156
  keras_model = compose_model(input_dim, output_dim, HIDDEN_LAYERS, NUM_NEURONS, DROP_OUT)
@@ -162,13 +167,14 @@ print(f"> Drop-out rate: {DROP_OUT:.2f}")
162
167
  # ################ Train the model ################################################################
163
168
  if TRAIN:
164
169
  # Learning rate scheduler
165
- INIT_LR = 1.0e-2
166
- FINAL_LR = 1.0e-4
167
- DECAY = 0.99 # 0.97
168
- STEPS = 250
169
- PERIOD = 500
170
- lr_schedule = CyclicalExponentialDecay(INIT_LR, FINAL_LR, DECAY, STEPS, PERIOD)
171
- # lr_schedule = FlooredExponentialDecay(INIT_LR, FINAL_LR, DECAY, STEPS)
170
+ INIT_LR = 1.0e-2#1.0e-2
171
+ FINAL_LR = 1.0e-3#1.0e-4
172
+ TARGET_EPOCH = EPOCHS * 0.90 # Epoch by which we plan to be down to 110% of final LR
173
+ PERIODS = 10 # Number of oscillation periods until target epoch
174
+
175
+ # lr_schedule = CyclicalExponentialDecay(NUM_SAMPLES, BATCH_SIZE, TARGET_EPOCH, INIT_LR, FINAL_LR,
176
+ # PERIODS)
177
+ lr_schedule = FlooredExponentialDecay(NUM_SAMPLES, BATCH_SIZE, TARGET_EPOCH, INIT_LR, FINAL_LR)
172
178
 
173
179
  # Optimizer
174
180
  optimizer = tf.keras.optimizers.Adam(learning_rate=lr_schedule)
@@ -183,7 +189,7 @@ if TRAIN:
183
189
  keras_model.compile(loss=tf_bps_rmse, optimizer=optimizer)
184
190
 
185
191
  # Callbacks
186
- EPOCH_SAMPLING = 5
192
+ EPOCH_SAMPLING = 1
187
193
  callback = RefCallback(x_test, y_test, bps_rmse, optimizer=optimizer,
188
194
  epoch_sampling=EPOCH_SAMPLING, x_train=x_train, y_train=y_train)
189
195
 
@@ -239,20 +245,25 @@ if SHOW_VOL_CHARTS:
239
245
  TRAINING_SPREADS = np.asarray(TRAINING_SPREADS)
240
246
  TRAINING_SPREADS = np.tile(TRAINING_SPREADS, (NUM_EXPIRIES, 1))
241
247
  mkt_strikes = TRAINING_SPREADS / 10000.0 + FWD
242
- # print(mkt_strikes)
243
- mkt_prices = generator.price_straddles_ref(EXPIRIES, mkt_strikes, FWD, PARAMS)
244
- # print(mkt_prices)
248
+
249
+ # Calculate market prices and vols
250
+ mkt_vols = generator.price_straddles_ref(EXPIRIES, mkt_strikes, FWD, PARAMS, True)
251
+ mkt_prices = generator.price_straddles_ref(EXPIRIES, mkt_strikes, FWD, PARAMS, False)
245
252
 
246
253
  # Use model to get parameters at each expiry, then calculate parameters and then prices
247
- mod_params, mod_prices = generator.price_straddles_mod(model, EXPIRIES, mkt_strikes, FWD,
248
- mkt_prices)
249
- rmse_mkt_mod = bps_rmse(mkt_prices, mod_prices)
254
+ mod_params, mod_vols = generator.price_straddles_mod(model, EXPIRIES, mkt_strikes, FWD,
255
+ mkt_vols, True)
256
+ # mod_vols = generator.price_straddles_ref(EXPIRIES, mkt_strikes, FWD, mod_params, True)
257
+
258
+ # mkt_prices = generator.price_straddles_ref(EXPIRIES, mkt_strikes, FWD, PARAMS, False)
259
+ rmse_mkt_mod = bps_rmse(mkt_vols, mod_vols)
250
260
  # print(mod_prices)
251
261
 
252
262
  # Calibrate prices by optimization
253
263
  weights = np.asarray([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
254
- cal_params, cal_prices = generator.calibrate(EXPIRIES, mkt_strikes, FWD, mkt_prices, weights)
255
- rmse_mkt_cal = bps_rmse(mkt_prices, cal_prices)
264
+ cal_params, cal_vols = generator.calibrate(EXPIRIES, mkt_strikes, FWD, mkt_prices, weights, True)
265
+ # cal_vols = generator.price_straddles_ref(EXPIRIES, mkt_strikes, FWD, cal_params, True)
266
+ rmse_mkt_cal = bps_rmse(mkt_vols, cal_vols)
256
267
  print(f"RMSE market-model: {rmse_mkt_mod:,.2f}")
257
268
  print(f"RMSE market-calibration: {rmse_mkt_cal:,.2f}")
258
269
  # print(cal_params)
@@ -288,81 +299,50 @@ if SHOW_VOL_CHARTS:
288
299
 
289
300
  # PV
290
301
  plot_spreads = TRAINING_SPREADS[0]
291
- axs[0, 0].plot(plot_spreads, mkt_prices[0], color='red', label='Target')
292
- axs[0, 0].plot(plot_spreads, mod_prices[0], color='blue', label='Model')
293
- axs[0, 0].plot(plot_spreads, cal_prices[0], 'g--', alpha=0.8, label='Calibration')
302
+ axs[0, 0].plot(plot_spreads, mkt_vols[0], color='red', label='Target')
303
+ axs[0, 0].plot(plot_spreads, mod_vols[0], color='blue', label='Model')
304
+ axs[0, 0].plot(plot_spreads, cal_vols[0], 'g--', alpha=0.8, label='Calibration')
294
305
  axs[0, 0].set_xlabel('Spread')
295
306
  axs[0, 0].set_title(f"Fit vs Target at T={EXPIRIES[0]}")
296
307
  axs[0, 0].legend(loc='upper right')
297
308
 
298
- axs[0, 1].plot(plot_spreads, mkt_prices[1], color='red', label='Target')
299
- axs[0, 1].plot(plot_spreads, mod_prices[1], color='blue', label='Model')
300
- axs[0, 1].plot(plot_spreads, cal_prices[1], 'g--', alpha=0.8, label='Calibration')
309
+ axs[0, 1].plot(plot_spreads, mkt_vols[1], color='red', label='Target')
310
+ axs[0, 1].plot(plot_spreads, mod_vols[1], color='blue', label='Model')
311
+ axs[0, 1].plot(plot_spreads, cal_vols[1], 'g--', alpha=0.8, label='Calibration')
301
312
  axs[0, 1].set_xlabel('Spread')
302
313
  axs[0, 1].set_title(f"Fit vs Target at T={EXPIRIES[1]}")
303
314
  axs[0, 1].legend(loc='upper right')
304
315
 
305
- axs[1, 0].plot(plot_spreads, mkt_prices[2], color='red', label='Target')
306
- axs[1, 0].plot(plot_spreads, mod_prices[2], color='blue', label='Model')
307
- axs[1, 0].plot(plot_spreads, cal_prices[2], 'g--', alpha=0.8, label='Calibration')
316
+ axs[1, 0].plot(plot_spreads, mkt_vols[2], color='red', label='Target')
317
+ axs[1, 0].plot(plot_spreads, mod_vols[2], color='blue', label='Model')
318
+ axs[1, 0].plot(plot_spreads, cal_vols[2], 'g--', alpha=0.8, label='Calibration')
308
319
  axs[1, 0].set_xlabel('Spread')
309
320
  axs[1, 0].set_title(f"Fit vs Target at T={EXPIRIES[2]}")
310
321
  axs[1, 0].legend(loc='upper right')
311
322
 
312
- axs[1, 1].plot(plot_spreads, mkt_prices[3], color='red', label='Target')
313
- axs[1, 1].plot(plot_spreads, mod_prices[3], color='blue', label='Model')
314
- axs[1, 1].plot(plot_spreads, cal_prices[3], 'g--', alpha=0.8, label='Calibration')
323
+ axs[1, 1].plot(plot_spreads, mkt_vols[3], color='red', label='Target')
324
+ axs[1, 1].plot(plot_spreads, mod_vols[3], color='blue', label='Model')
325
+ axs[1, 1].plot(plot_spreads, cal_vols[3], 'g--', alpha=0.8, label='Calibration')
315
326
  axs[1, 1].set_xlabel('Spread')
316
327
  axs[1, 1].set_title(f"Fit vs Target at T={EXPIRIES[3]}")
317
328
  axs[1, 1].legend(loc='upper right')
318
329
 
319
- axs[2, 0].plot(plot_spreads, mkt_prices[4], color='red', label='Target')
320
- axs[2, 0].plot(plot_spreads, mod_prices[4], color='blue', label='Model')
321
- axs[2, 0].plot(plot_spreads, cal_prices[4], 'g--', alpha=0.8, label='Calibration')
330
+ axs[2, 0].plot(plot_spreads, mkt_vols[4], color='red', label='Target')
331
+ axs[2, 0].plot(plot_spreads, mod_vols[4], color='blue', label='Model')
332
+ axs[2, 0].plot(plot_spreads, cal_vols[4], 'g--', alpha=0.8, label='Calibration')
322
333
  axs[2, 0].set_xlabel('Spread')
323
334
  axs[2, 0].set_title(f"Fit vs Target at T={EXPIRIES[4]}")
324
335
  axs[2, 0].legend(loc='upper right')
325
336
 
326
- axs[2, 1].plot(plot_spreads, mkt_prices[5], color='red', label='Target')
327
- axs[2, 1].plot(plot_spreads, mod_prices[5], color='blue', label='Model')
328
- axs[2, 1].plot(plot_spreads, cal_prices[5], 'g--', alpha=0.8, label='Calibration')
337
+ axs[2, 1].plot(plot_spreads, mkt_vols[5], color='red', label='Target')
338
+ axs[2, 1].plot(plot_spreads, mod_vols[5], color='blue', label='Model')
339
+ axs[2, 1].plot(plot_spreads, cal_vols[5], 'g--', alpha=0.8, label='Calibration')
329
340
  axs[2, 1].set_xlabel('Spread')
330
341
  axs[2, 1].set_title(f"Fit vs Target at T={EXPIRIES[5]}")
331
342
  axs[2, 1].legend(loc='upper right')
332
343
 
333
344
  plt.show()
334
345
 
335
- # METHOD = 'Percentiles'
336
- # PERCENTS = np.linspace(0.01, 0.99, num=NUM_STRIKES)
337
- # PERCENTS = np.asarray([PERCENTS] * NUM_EXPIRIES)
338
-
339
- # strikes = generator.convert_strikes(EXPIRIES, PERCENTS, FWD, PARAMS, METHOD)
340
-
341
- # print("> Calculating chart surface with reference model")
342
- # timer_ref = Stopwatch("Reference surface calculation")
343
- # timer_ref.trigger()
344
- # ref_prices = generator.price_straddle(EXPIRIES, strikes, FWD, PARAMS)
345
- # timer_ref.stop()
346
- # clipboard.export2d(ref_prices)
347
- # print("> Calculating chart surface with trained model")
348
- # timer_mod = Stopwatch("Model surface calculation")
349
- # timer_mod.trigger()
350
- # mod_prices = generator.price_surface_mod(model, EXPIRIES, strikes, ARE_CALLS, FWD, PARAMS)
351
- # timer_mod.stop()
352
- # print(f"> Ref-Mod RMSE(price): {bps_rmse(ref_prices, mod_prices):.2f}")
353
-
354
- # Display timers
355
- # timer_ref.print()
356
- # timer_mod.print()
357
-
358
- # Available tranforms: Price, ShiftedBlackScholes, Bachelier
359
- # TITLE = f"{MODEL_TYPE} smile sections, forward={FWD*100:.2f}"#,%\n parameters={PARAMS}"
360
- # TRANSFORM = "Bachelier"
361
- # TRANSFORM = "Price"
362
- # TRANSFORM = "ShiftedBlackScholes"
363
- # xplt.plot_transform_surface(EXPIRIES, strikes, ARE_CALLS, FWD, ref_prices, mod_prices,
364
- # TITLE, transform=TRANSFORM)
365
-
366
346
  # Show training history
367
347
  if TRAIN:
368
348
  hist_epochs = callback.epochs
@@ -97,7 +97,9 @@ class SabrGenerator(SmileGenerator):
97
97
 
98
98
  return df
99
99
 
100
- def generate_samples_inverse(self, num_samples, rg, spreads):
100
+ def generate_samples_inverse(self, num_samples, rg, spreads, use_nvol=False,
101
+ min_vol=0.0001, max_vol=0.2):
102
+ np.seterr(divide='raise') # To catch errors and warnings
101
103
  shift = self.shift
102
104
 
103
105
  min_fwd = -shift - spreads[0] / 10000 + constants.BPS10
@@ -154,7 +156,7 @@ class SabrGenerator(SmileGenerator):
154
156
  params = {'LnVol': lnvol[j], 'Beta': beta[j], 'Nu': nu[j], 'Rho': rho[j]}
155
157
 
156
158
  # Calculate prices
157
- price = self.price_straddles_ref(expiries, ks, fwd, params)
159
+ price = self.price_straddles_ref(expiries, ks, fwd, params, use_nvol)
158
160
 
159
161
  # Flatten the results
160
162
  for exp_idx, expiry in enumerate(expiries):
@@ -166,6 +168,8 @@ class SabrGenerator(SmileGenerator):
166
168
  rhos.append(rho[j])
167
169
  prices.append(price[exp_idx])
168
170
 
171
+ np.seterr(divide='warn') # Set back to warning
172
+
169
173
  # Create strike headers
170
174
  strike_headers = ['K' + str(j) for j in range(num_strikes)]
171
175
 
@@ -184,6 +188,12 @@ class SabrGenerator(SmileGenerator):
184
188
  df = pd.DataFrame(data_dic)
185
189
  df.columns = columns
186
190
 
191
+ # Cleanse
192
+ if use_nvol:
193
+ for j in range(num_strikes):
194
+ df = df.drop(df[df[strike_headers[j]] > max_vol].index)
195
+ df = df.drop(df[df[strike_headers[j]] < min_vol].index)
196
+
187
197
  return df
188
198
 
189
199
  def price(self, expiries, strikes, are_calls, fwd, parameters):
@@ -201,35 +211,39 @@ class SabrGenerator(SmileGenerator):
201
211
 
202
212
  return np.asarray(prices)
203
213
 
204
- def price_straddles_ref(self, expiries, strikes, fwd, parameters):
214
+ def price_straddles_ref(self, expiries, strikes, fwd, parameters, output_nvol=False):
205
215
  expiries_ = np.asarray(expiries).reshape(-1, 1)
206
216
  shifted_k = strikes + self.shift
207
217
  shifted_f = fwd + self.shift
208
218
  prices = []
209
219
  for i, expiry in enumerate(expiries_):
210
220
  k_prices = []
211
- for j, sk in enumerate(shifted_k[i]):
212
- iv = sabr.implied_vol_vec(expiry, sk, shifted_f, parameters)
213
- call_price = black.price(expiry, sk, True, shifted_f, iv)
214
- put_price = black.price(expiry, sk, False, shifted_f, iv)
215
- k_prices.append(call_price[0] + put_price[0])
221
+ if output_nvol:
222
+ for (k, sk) in zip(strikes[i], shifted_k[i]): # Normal vols
223
+ iv = sabr.implied_vol_vec(expiry, sk, shifted_f, parameters)
224
+ is_call = True
225
+ price = black.price(expiry, sk, is_call, shifted_f, iv)
226
+ try:
227
+ n_vol = bachelier.implied_vol(expiry, k, is_call, fwd, price)[0]
228
+ except (Exception,):
229
+ n_vol = -9999
230
+ k_prices.append(n_vol)
231
+ else:
232
+ for sk in shifted_k[i]: # Straddle prices
233
+ iv = sabr.implied_vol_vec(expiry, sk, shifted_f, parameters)
234
+ call_price = black.price(expiry, sk, True, shifted_f, iv)
235
+ put_price = black.price(expiry, sk, False, shifted_f, iv)
236
+ k_prices.append(call_price[0] + put_price[0])
216
237
  prices.append(k_prices)
217
238
 
218
239
  return np.asarray(prices)
219
240
 
220
- def price_straddles_mod(self, model, expiries, strikes, fwd, mkt_prices):
241
+ def price_straddles_mod(self, model, expiries, strikes, fwd, mkt_prices, output_nvol=False):
221
242
  """ Calculate straddle prices for given parameters using the learning model """
222
- # print("Expiries ", expiries.shape, "\n", expiries)
223
- # print("Strikes ", strikes.shape, "\n", strikes)
224
- # print("FWD ", fwd)
225
- # print("Mkt Prices ", mkt_prices.shape, "\n", mkt_prices)
226
-
227
243
  # Prepare input points for the model
228
244
  n_expiries = expiries.shape[0]
229
245
  points = np.c_[expiries, np.ones(n_expiries) * fwd]
230
- # print(points)
231
246
  points = np.c_[points, mkt_prices]
232
- # print(points)
233
247
 
234
248
  # Evaluation model
235
249
  params = model.predict(points)
@@ -243,13 +257,8 @@ class SabrGenerator(SmileGenerator):
243
257
  mod_prices = []
244
258
  for i, expiry in enumerate(expiries):
245
259
  expiry_ = np.asarray([expiry])
246
- # print(expiry)
247
260
  strikes_ = np.asarray([strikes[i]])
248
- # print(strikes_)
249
- # print(mod_params[i])
250
- # mod_prices_ = self.price_straddles_ref(expiry_, strikes_, fwd, src_params)
251
- mod_prices_ = self.price_straddles_ref(expiry_, strikes_, fwd, mod_params[i])
252
- # print(mod_prices)
261
+ mod_prices_ = self.price_straddles_ref(expiry_, strikes_, fwd, mod_params[i], output_nvol)
253
262
  mod_prices.append(mod_prices_[0])
254
263
 
255
264
  return mod_params, np.asarray(mod_prices)
@@ -334,7 +343,7 @@ class SabrGenerator(SmileGenerator):
334
343
 
335
344
  return x_set, y_set
336
345
 
337
- def calibrate(self, expiries, strikes, fwd, mkt_prices, weights):
346
+ def calibrate(self, expiries, strikes, fwd, mkt_prices, weights, output_nvol=False):
338
347
  # method = 'Nelder-Mead'
339
348
  # method = "Powell"
340
349
  # method = "L-BFGS-B"
@@ -381,7 +390,7 @@ class SabrGenerator(SmileGenerator):
381
390
  for i in range(num_expiries):
382
391
  expiries_ = np.asarray([expiries[i]])
383
392
  strikes_ = np.asarray([strikes[i]])
384
- cal_prices_ = self.price_straddles_ref(expiries_, strikes_, fwd, cal_params[i])
393
+ cal_prices_ = self.price_straddles_ref(expiries_, strikes_, fwd, cal_params[i], output_nvol)
385
394
  cal_prices.append(cal_prices_[0])
386
395
 
387
396
  return cal_params, cal_prices
@@ -137,6 +137,42 @@ class SmileGenerator(ABC):
137
137
 
138
138
  return data_df
139
139
 
140
+ # def to_straddle_nvol(self, data_df, cleanse=True, min_vol=0.0001, max_vol=0.1):
141
+ # """ Calculate normal implied vol and remove errors. Further remove points that are not
142
+ # in the given min/max range """
143
+ # # Calculate normal vols
144
+ # np.seterr(divide='raise') # To catch errors and warnings
145
+ # n_strikes = data_df.shape[1] - 6
146
+ # t = data_df.Ttm
147
+ # fwd = data_df.F
148
+ # # strike = data_df.K
149
+ # # price = data_df.Price
150
+ # nvol = []
151
+ # num_samples = t.shape[0]
152
+ # num_print = 10000
153
+ # num_batches = int(num_samples / num_print) + 1
154
+ # batch_id = 0
155
+ # for i in range(num_samples):
156
+ # if i % num_print == 0:
157
+ # batch_id = batch_id + 1
158
+ # print(f"Converting to normal vol, batch {batch_id:,} out of {num_batches:,}")
159
+ # try:
160
+ # nvol.append(bachelier.implied_vol(t[i], strike[i], self.is_call, fwd[i], price[i]))
161
+ # except (Exception,):
162
+ # nvol.append(-9999)
163
+
164
+ # np.seterr(divide='warn') # Set back to warning
165
+
166
+ # data_df['NVol'] = nvol
167
+ # # data_df['BSVol'] = bsvol
168
+
169
+ # # Remove out of range
170
+ # if cleanse:
171
+ # data_df = data_df.drop(data_df[data_df.NVol > max_vol].index)
172
+ # data_df = data_df.drop(data_df[data_df.NVol < min_vol].index)
173
+
174
+ # return data_df
175
+
140
176
  def target_is_call(self):
141
177
  """ True if the fit target is call options, False if puts """
142
178
  return self.is_call
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sdevpy
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Python package for Machine Learning in Finance
5
5
  Author-email: Sebastien Gurrieri <sebgur@gmail.com>
6
6
  Project-URL: Git page, https://github.com/sebgur/SDev.Python
@@ -18,6 +18,7 @@ Requires-Dist: numpy
18
18
  Requires-Dist: tensorflow
19
19
  Requires-Dist: scikit-learn
20
20
  Requires-Dist: tensorflow_probability
21
+ Requires-Dist: silence_tensorflow
21
22
 
22
23
  # SDev.Python
23
24
 
@@ -5,3 +5,4 @@ numpy
5
5
  tensorflow
6
6
  scikit-learn
7
7
  tensorflow_probability
8
+ silence_tensorflow
@@ -1 +0,0 @@
1
- __version__ = '1.0.2'
@@ -1,54 +0,0 @@
1
- """ Custom learning schedules """
2
- import tensorflow as tf
3
- import math
4
- from sdevpy.tools.constants import TWO_PI
5
-
6
- # Custom learning rate scheduler, exponentially decreases between given values
7
- class FlooredExponentialDecay(tf.keras.optimizers.schedules.LearningRateSchedule):
8
- """ Custom learning rate scheduler, exponentially decreases between given values """
9
- def __init__(self, initial_lr=1e-1, final_lr=1e-4, decay=0.96, decay_steps=100):
10
- self.initial_lr = initial_lr
11
- self.final_lr = final_lr
12
- self.decay = decay
13
- self.decay_steps = decay_steps
14
-
15
- def __call__(self, step):
16
- ratio = tf.cast(step / self.decay_steps, tf.float32)
17
- coeff = tf.pow(self.decay, ratio)
18
- return self.initial_lr * coeff + self.final_lr * (1.0 - coeff)
19
-
20
- def get_config(self):
21
- config = { 'initial_lr': self.initial_lr,
22
- 'final_lr': self.final_lr,
23
- 'decay': self.decay,
24
- 'decay_steps': self.decay_steps }
25
- return config
26
-
27
- # Custom learning rate scheduler, cyclically exponentially decreases between given values
28
- class CyclicalExponentialDecay(tf.keras.optimizers.schedules.LearningRateSchedule):
29
- """ Custom learning rate scheduler, cyclically exponentially decreases between given values """
30
- def __init__(self, initial_lr=1e-1, final_lr=1e-4, decay=0.96, decay_steps=100, period=10.0):
31
- self.initial_lr = initial_lr
32
- self.final_lr = final_lr
33
- self.decay = decay
34
- self.decay_steps = decay_steps
35
- self.period = period
36
-
37
- def __call__(self, step):
38
- ratio = tf.cast(step / self.decay_steps, tf.float32)
39
- coeff = tf.pow(self.decay, ratio)
40
- ampl = self.initial_lr - self.final_lr
41
- two_pi = tf.cast(TWO_PI, tf.float32)
42
- arg = tf.cast(step / self.period, tf.float32)
43
- # arg = arg * two_pi
44
- ampl = ampl * (1.0 + tf.math.cos(arg * two_pi)) / 2.0
45
- return self.final_lr + ampl * coeff
46
- # return self.initial_lr * coeff + self.final_lr * (1.0 - coeff)
47
-
48
- def get_config(self):
49
- config = { 'initial_lr': self.initial_lr,
50
- 'final_lr': self.final_lr,
51
- 'decay': self.decay,
52
- 'decay_steps': self.decay_steps,
53
- 'period': self.period }
54
- return config
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