sdevpy 1.0.1__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.
- {sdevpy-1.0.1/src/sdevpy.egg-info → sdevpy-1.0.3}/PKG-INFO +2 -1
- {sdevpy-1.0.1 → sdevpy-1.0.3}/pyproject.toml +2 -2
- sdevpy-1.0.3/src/sdevpy/__init__.py +1 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/machinelearning/callbacks.py +36 -14
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/machinelearning/learningmodel.py +6 -2
- sdevpy-1.0.3/src/sdevpy/machinelearning/learningschedules.py +78 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/machinelearning/topology.py +1 -1
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/maths/metrics.py +11 -0
- sdevpy-1.0.3/src/sdevpy/maths/optimization.py +158 -0
- sdevpy-1.0.3/src/sdevpy/projects/stovolinverse/stovolinvgen.py +75 -0
- sdevpy-1.0.3/src/sdevpy/projects/stovolinverse/stovolinvtrain.py +369 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/settings.py +4 -2
- sdevpy-1.0.3/src/sdevpy/test.py +477 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/tools/constants.py +3 -0
- sdevpy-1.0.3/src/sdevpy/tools/utils.py +7 -0
- sdevpy-1.0.3/src/sdevpy/volsurfacegen/sabrgenerator.py +481 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/smilegenerator.py +67 -1
- {sdevpy-1.0.1 → sdevpy-1.0.3/src/sdevpy.egg-info}/PKG-INFO +2 -1
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy.egg-info/SOURCES.txt +3 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy.egg-info/requires.txt +1 -0
- sdevpy-1.0.1/src/sdevpy/__init__.py +0 -1
- sdevpy-1.0.1/src/sdevpy/machinelearning/learningschedules.py +0 -23
- sdevpy-1.0.1/src/sdevpy/maths/optimization.py +0 -1
- sdevpy-1.0.1/src/sdevpy/test.py +0 -301
- sdevpy-1.0.1/src/sdevpy/volsurfacegen/sabrgenerator.py +0 -227
- {sdevpy-1.0.1 → sdevpy-1.0.3}/LICENSE +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/README.md +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/setup.cfg +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/analytics/bachelier.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/analytics/black.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/analytics/fbsabr.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/analytics/mcheston.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/analytics/mcsabr.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/analytics/mczabr.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/analytics/sabr.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/machinelearning/datasets.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/maths/interpolations.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/maths/rand.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/montecarlo/smoothers.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/projects/aad/aad_mc.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/projects/aad/aad_mc_nd.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/projects/datafiles.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovolgen.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovolplot.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovoltrain.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/tools/clipboard.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/tools/filemanager.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/tools/jsonmanager.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/tools/timegrids.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/tools/timer.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/fbsabrgenerator.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mchestongenerator.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mcsabrgenerator.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mczabrgenerator.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/stovolfactory.py +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy.egg-info/dependency_links.txt +0 -0
- {sdevpy-1.0.1 → sdevpy-1.0.3}/src/sdevpy.egg-info/top_level.txt +0 -0
- {sdevpy-1.0.1 → 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.
|
|
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.
|
|
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'
|
|
@@ -8,7 +8,7 @@ from sklearn.preprocessing import MinMaxScaler
|
|
|
8
8
|
class SDevPyCallback(keras.callbacks.Callback):
|
|
9
9
|
""" SDevPy's base class for callbacks. Compared to Keras's base, it also keeps track of
|
|
10
10
|
the learning rate and displays it at each period """
|
|
11
|
-
def __init__(self, epoch_sampling=1, optimizer=None):
|
|
11
|
+
def __init__(self, epoch_sampling=1, optimizer=None, loss_function=None, x_train=None, y_train=None):
|
|
12
12
|
keras.callbacks.Callback.__init__(self)
|
|
13
13
|
self.x_scaler = MinMaxScaler()
|
|
14
14
|
self.y_scaler = MinMaxScaler()
|
|
@@ -22,6 +22,12 @@ class SDevPyCallback(keras.callbacks.Callback):
|
|
|
22
22
|
self.losses = []
|
|
23
23
|
self.learning_rates = []
|
|
24
24
|
self.sampled_epochs = []
|
|
25
|
+
# Used to calculate the loss periodically, as in reality the loss function
|
|
26
|
+
# is used on the transformed coordinates, not, as the user might expect,
|
|
27
|
+
# on the real world ones.
|
|
28
|
+
self.loss_function = loss_function
|
|
29
|
+
self.x_train = x_train
|
|
30
|
+
self.y_train = y_train
|
|
25
31
|
|
|
26
32
|
def on_train_begin(self, logs=None):
|
|
27
33
|
""" Display settings, reset training variables """
|
|
@@ -46,10 +52,15 @@ class SDevPyCallback(keras.callbacks.Callback):
|
|
|
46
52
|
|
|
47
53
|
def on_epoch_end(self, epoch, logs=None):
|
|
48
54
|
self.epochs.append(epoch)
|
|
55
|
+
# if self.x_train is None and self.y_train is None:
|
|
49
56
|
loss = logs['loss']
|
|
50
|
-
scale = self.y_scaler.scale_[0]
|
|
51
|
-
loss = loss * scale
|
|
57
|
+
# scale = self.y_scaler.scale_[0] # This is a trick only working in 1D
|
|
58
|
+
# loss = loss * scale
|
|
59
|
+
# else:
|
|
60
|
+
# loss = self.estimate_loss(self.x_train, self.y_train)
|
|
61
|
+
|
|
52
62
|
self.losses.append(loss)
|
|
63
|
+
|
|
53
64
|
if self.optimizer is not None:
|
|
54
65
|
lr = self.optimizer.learning_rate.numpy()
|
|
55
66
|
self.learning_rates.append(lr)
|
|
@@ -67,6 +78,17 @@ class SDevPyCallback(keras.callbacks.Callback):
|
|
|
67
78
|
self.x_scaler = x_scaler
|
|
68
79
|
self.y_scaler = y_scaler
|
|
69
80
|
|
|
81
|
+
def estimate_loss(self, x_est, y_est):
|
|
82
|
+
""" Estimate loss on test set """
|
|
83
|
+
x_scaled = self.x_scaler.transform(x_est)
|
|
84
|
+
y_scaled = self.model.predict(x_scaled, verbose=0)
|
|
85
|
+
# y_pred = self.y_scaler.inverse_transform(y_scaled)
|
|
86
|
+
y_pred = y_scaled
|
|
87
|
+
y_est_sc = self.y_scaler.transform(y_est)
|
|
88
|
+
est_loss = self.loss_function(y_est_sc, y_pred)
|
|
89
|
+
# est_loss = rmse(y_est, y_pred) * 10000
|
|
90
|
+
return est_loss
|
|
91
|
+
|
|
70
92
|
# def convergence(self):
|
|
71
93
|
# """ Retrieve sampled epochs, losses and learning rates """
|
|
72
94
|
# return self.epochs, self.losses, self.learning_rates, self.sampled_epochs
|
|
@@ -75,11 +97,11 @@ class SDevPyCallback(keras.callbacks.Callback):
|
|
|
75
97
|
class RefCallback(SDevPyCallback):
|
|
76
98
|
""" Callback class that compares model predictions vs reference periodically """
|
|
77
99
|
def __init__(self, x_test, y_test, loss_function, epoch_sampling=1,
|
|
78
|
-
optimizer=None):
|
|
79
|
-
SDevPyCallback.__init__(self, epoch_sampling, optimizer)
|
|
100
|
+
optimizer=None, x_train=None, y_train=None):
|
|
101
|
+
SDevPyCallback.__init__(self, epoch_sampling, optimizer, loss_function, x_train, y_train)
|
|
80
102
|
self.x_test = x_test
|
|
81
103
|
self.y_test = y_test
|
|
82
|
-
self.loss_function = loss_function
|
|
104
|
+
# self.loss_function = loss_function
|
|
83
105
|
self.test_losses = []
|
|
84
106
|
|
|
85
107
|
def on_train_begin(self, logs=None):
|
|
@@ -98,14 +120,14 @@ class RefCallback(SDevPyCallback):
|
|
|
98
120
|
self.test_losses.append(test_loss)
|
|
99
121
|
print(f", Test loss: {test_loss:,.2f}", end="")
|
|
100
122
|
|
|
101
|
-
def estimate_loss(self, x_est, y_est):
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
# def estimate_loss(self, x_est, y_est):
|
|
124
|
+
# """ Estimate loss on test set """
|
|
125
|
+
# x_scaled = self.x_scaler.transform(x_est)
|
|
126
|
+
# y_scaled = self.model.predict(x_scaled, verbose=0)
|
|
127
|
+
# y_pred = self.y_scaler.inverse_transform(y_scaled)
|
|
128
|
+
# est_loss = self.loss_function(y_est, y_pred)
|
|
129
|
+
# # est_loss = rmse(y_est, y_pred) * 10000
|
|
130
|
+
# return est_loss
|
|
109
131
|
|
|
110
132
|
def test_set_loss(self):
|
|
111
133
|
""" Retrieve loss on test set """
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -3,6 +3,13 @@ import numpy as np
|
|
|
3
3
|
from sklearn.metrics import mean_squared_error
|
|
4
4
|
import tensorflow as tf
|
|
5
5
|
|
|
6
|
+
def mse(set1, set2):
|
|
7
|
+
""" Mean Squared Error """
|
|
8
|
+
return mean_squared_error(set1, set2)
|
|
9
|
+
|
|
10
|
+
def rmsew(set1, set2, weights):
|
|
11
|
+
""" Root Mean Squared Error with weights """
|
|
12
|
+
return np.sqrt(mean_squared_error(set1, set2, sample_weight=weights))
|
|
6
13
|
|
|
7
14
|
def rmse(set1, set2):
|
|
8
15
|
""" Root Mean Squared Error """
|
|
@@ -13,6 +20,10 @@ def bps_rmse(y_true, y_ref):
|
|
|
13
20
|
return 10000.0 * rmse(y_true, y_ref)
|
|
14
21
|
|
|
15
22
|
# Tensorflow versions
|
|
23
|
+
def tf_mse(y_true, y_pred):
|
|
24
|
+
""" Mean Squared Error in tensorflow """
|
|
25
|
+
return tf.math.reduce_mean(tf.square(y_true - y_pred))
|
|
26
|
+
|
|
16
27
|
def tf_rmse(y_true, y_pred):
|
|
17
28
|
""" Root Mean Squared Error in tensorflow """
|
|
18
29
|
return tf.sqrt(tf.math.reduce_mean(tf.square(y_true - y_pred)))
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
""" Optimization """
|
|
2
|
+
# https://machinelearningmastery.com/how-to-use-nelder-mead-optimization-in-python/
|
|
3
|
+
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html
|
|
4
|
+
# For DE: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html
|
|
5
|
+
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
import numpy as np
|
|
8
|
+
import scipy.optimize as opt
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SCIPY_OPTIMIZERS = ['Nelder-Mead', 'Powell', 'CG', 'BFGS', 'L-BFGS-B', 'TNC', 'COBYLA',
|
|
13
|
+
'SLSQP', 'trust-constr', 'Newton-CG', 'dogleg', 'trust-ncg',
|
|
14
|
+
'trust-exact', 'trust-krylov', 'DE']
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Available strategies for DE
|
|
18
|
+
# best1bin, best1exp, best2exp, best2bin, rand1bin, rand1exp, rand2bin, rand2exp,
|
|
19
|
+
# randtobest1bin, randtobest1exp, currenttobest1bin, currenttobest1exp
|
|
20
|
+
|
|
21
|
+
def create_optimizer(method, **kwargs):
|
|
22
|
+
""" Create an optimizer. Currently only supporting SciPy """
|
|
23
|
+
optimizer = None
|
|
24
|
+
if method in SCIPY_OPTIMIZERS:
|
|
25
|
+
optimizer = SciPyOptimizer(method, **kwargs)
|
|
26
|
+
else:
|
|
27
|
+
raise RuntimeError("Optimizer type not supported: " + method)
|
|
28
|
+
|
|
29
|
+
return optimizer
|
|
30
|
+
|
|
31
|
+
def create_bounds(lw_bounds, up_bounds):
|
|
32
|
+
return opt.Bounds(lw_bounds, up_bounds, keep_feasible=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Optimizer:
|
|
36
|
+
# def __init__(self):
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def minimize(self, f, x0, args, bounds):
|
|
40
|
+
""" Minimization """
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SciPyOptimizer(Optimizer):
|
|
44
|
+
""" Wrapper for SciPy optimizers, including differential_evolution """
|
|
45
|
+
def __init__(self, method = 'Powell', **kwargs):
|
|
46
|
+
self.method_ = method
|
|
47
|
+
self.std_minimizers = ['Nelder-Mead', 'Powell', 'CG', 'BFGS', 'L-BFGS-B', 'TNC', 'COBYLA',
|
|
48
|
+
'SLSQP', 'trust-constr', 'Newton-CG', 'dogleg', 'trust-ncg',
|
|
49
|
+
'trust-exact', 'trust-krylov']
|
|
50
|
+
|
|
51
|
+
self.other_minimizers = ['DE']
|
|
52
|
+
|
|
53
|
+
self.kwargs = kwargs
|
|
54
|
+
|
|
55
|
+
if self.method_ not in self.std_minimizers and self.method_ not in self.other_minimizers:
|
|
56
|
+
raise RuntimeError("Method " + self.method_ + " not found in SciPy")
|
|
57
|
+
|
|
58
|
+
def minimize(self, f, x0=None, args=(), bounds=None):
|
|
59
|
+
result = None
|
|
60
|
+
if self.method_ in self.std_minimizers:
|
|
61
|
+
tol = self.kwargs.get('tol', None)
|
|
62
|
+
result = opt.minimize(f, x0, args, method=self.method_, bounds=bounds,
|
|
63
|
+
tol=tol)
|
|
64
|
+
elif self.method_== 'DE':
|
|
65
|
+
atol = self.kwargs.get('atol', 0)
|
|
66
|
+
popsize = self.kwargs.get('popsize', 15)
|
|
67
|
+
strategy = self.kwargs.get('strategy', 'best1bin')
|
|
68
|
+
recombination = self.kwargs.get('recombination', 0.7)
|
|
69
|
+
mutation = self.kwargs.get('mutation', (0.5, 1.0))
|
|
70
|
+
result = opt.differential_evolution(f, x0=x0, args=args, bounds=bounds, atol=atol,
|
|
71
|
+
popsize=popsize, strategy=strategy,
|
|
72
|
+
recombination=recombination)
|
|
73
|
+
else:
|
|
74
|
+
raise RuntimeError("Method " + self.method_ + " not recognized")
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
class MultiOptimizer(Optimizer):
|
|
79
|
+
""" Wrapper for SciPy optimizers, including differential_evolution """
|
|
80
|
+
def __init__(self, methods = ['L-BFGS-B', 'DE'], mtol=1e-4, **kwargs):
|
|
81
|
+
self.methods_ = methods
|
|
82
|
+
self.mtol_ = mtol
|
|
83
|
+
self.kwargs = kwargs
|
|
84
|
+
|
|
85
|
+
self.optimizers_ = []
|
|
86
|
+
for method in self.methods_:
|
|
87
|
+
self.optimizers_.append(create_optimizer(method, **kwargs))
|
|
88
|
+
|
|
89
|
+
def minimize(self, f, x0=None, args=(), bounds=None):
|
|
90
|
+
result = None
|
|
91
|
+
for i, optimizer in enumerate(self.optimizers_):
|
|
92
|
+
print("Trying optimization using " + self.methods_[i] + ": ", end='')
|
|
93
|
+
result = optimizer.minimize(f, x0, args, bounds)
|
|
94
|
+
if result.fun < self.mtol_:
|
|
95
|
+
print("SUCCESS!")
|
|
96
|
+
break
|
|
97
|
+
else:
|
|
98
|
+
print("FAILURE")
|
|
99
|
+
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
# Objective function
|
|
105
|
+
def f(x, *args):
|
|
106
|
+
x_ = x[0]
|
|
107
|
+
a = args[0]
|
|
108
|
+
b = args[1]
|
|
109
|
+
c = args[2]
|
|
110
|
+
prod = a * b * c
|
|
111
|
+
bi = a * b + b * c + a * c
|
|
112
|
+
sum = a + b + c
|
|
113
|
+
return 0.25 * np.power(x_, 4) - sum / 3.0 * np.power(x_, 3) + 0.5 * bi * x_**2 - prod * x_ + 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Choose method
|
|
117
|
+
method = 'Nelder-Mead'
|
|
118
|
+
# method = "Powell" # Success x^4
|
|
119
|
+
# method = "CG"
|
|
120
|
+
# method = "BFGS"
|
|
121
|
+
# method = "L-BFGS-B"
|
|
122
|
+
# method = "TNC"
|
|
123
|
+
# method = "COBYLA" # Success x^4
|
|
124
|
+
# method = "SLSQP"
|
|
125
|
+
# method = "trust-constr"
|
|
126
|
+
# method = "Newton-CG" # Requires Jacobian
|
|
127
|
+
# method = "dogleg" # Requires Jacobian
|
|
128
|
+
# method = "trust-ncg" # Requires Jacobian
|
|
129
|
+
# method = "trust-exact" # Requires Jacobian
|
|
130
|
+
# method = "trust-krylov" # Requires Jacobian
|
|
131
|
+
# method = "DE" # Success x^4
|
|
132
|
+
|
|
133
|
+
# Create the optimizer
|
|
134
|
+
optimizer = create_optimizer(method)
|
|
135
|
+
|
|
136
|
+
# Define the bounds
|
|
137
|
+
bounds = opt.Bounds([0], [4], keep_feasible=False)
|
|
138
|
+
|
|
139
|
+
# Optimize
|
|
140
|
+
result = optimizer.minimize(f, x0=[1.5], args=(1, 2, 3.2), bounds=bounds)
|
|
141
|
+
|
|
142
|
+
for key in result.keys():
|
|
143
|
+
if key in result:
|
|
144
|
+
print(key + "\n", result[key])
|
|
145
|
+
|
|
146
|
+
x = result.x
|
|
147
|
+
fun = result.fun
|
|
148
|
+
# print("Keys\n", result.keys())
|
|
149
|
+
|
|
150
|
+
# Plot
|
|
151
|
+
points = np.linspace(0, 4, 100).reshape(-1, 1)
|
|
152
|
+
y = []
|
|
153
|
+
for p in points:
|
|
154
|
+
y.append(f(p, 1, 2, 3.2))
|
|
155
|
+
|
|
156
|
+
plt.plot(points, y, color='blue', alpha=0.8, label='Objective')
|
|
157
|
+
plt.scatter(x[0], fun, color='red', alpha=1.0, label='Solution')
|
|
158
|
+
plt.show()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
""" Generate training data for Stochastic Local Vol models. We implement the inverse map here.
|
|
2
|
+
Datasets of parameters (outputs) vs prices/implied vols (inputs) are generated to later train
|
|
3
|
+
a network that learns the so-called 'inverse' calculation, i.e. parameters from prices. """
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from sdevpy.volsurfacegen import stovolfactory
|
|
7
|
+
from sdevpy import settings
|
|
8
|
+
from sdevpy.tools import filemanager
|
|
9
|
+
from sdevpy.tools.timer import Stopwatch
|
|
10
|
+
|
|
11
|
+
# ################ Runtime configuration ##########################################################
|
|
12
|
+
MODEL_TYPE = "SABR"
|
|
13
|
+
# MODEL_TYPE = "McSABR"
|
|
14
|
+
# MODEL_TYPE = "FbSABR"
|
|
15
|
+
# MODEL_TYPE = "McZABR"
|
|
16
|
+
# MODEL_TYPE = "McHeston"
|
|
17
|
+
SHIFT = 0.03
|
|
18
|
+
NUM_SAMPLES = 1000 * 1000
|
|
19
|
+
# The 4 parameters below are only relevant for models whose reference is calculated by MC
|
|
20
|
+
NUM_EXPIRIES = 15
|
|
21
|
+
NUM_MC = 100 * 1000 # 100 * 1000
|
|
22
|
+
POINTS_PER_YEAR = 25 # 25
|
|
23
|
+
SEED = 1234 # [1357, 8642, 1000, 8888, 4444, 2222, 1111, 4321, 1234, 42]
|
|
24
|
+
SPREADS = [-200, -100, -75, -50, -25, -10, 0, 10, 25, 50, 75, 100, 200]
|
|
25
|
+
USE_NVOL = True
|
|
26
|
+
|
|
27
|
+
print(">> Set up runtime configuration")
|
|
28
|
+
project_folder = os.path.join(settings.WORKFOLDER, "stovolinv")
|
|
29
|
+
print("> Project folder: " + project_folder)
|
|
30
|
+
dataset_folder = os.path.join(project_folder, "datasets")
|
|
31
|
+
print("> Data folder: " + dataset_folder)
|
|
32
|
+
filemanager.check_directory(dataset_folder)
|
|
33
|
+
print("> Chosen model: " + MODEL_TYPE)
|
|
34
|
+
|
|
35
|
+
# ################ Select model ###################################################################
|
|
36
|
+
generator = stovolfactory.set_generator(MODEL_TYPE, SHIFT, NUM_EXPIRIES, num_mc=NUM_MC,
|
|
37
|
+
points_per_year=POINTS_PER_YEAR, seed=SEED)
|
|
38
|
+
|
|
39
|
+
# ################ Select training ranges #########################################################
|
|
40
|
+
# SABR
|
|
41
|
+
RANGES = {'Ttm': [1.0 / 12.0, 35.0], 'F': [-0.009, 0.041], 'LnVol': [0.05, 0.5],
|
|
42
|
+
'Beta': [0.1, 0.9], 'Nu': [0.1, 1.0], 'Rho': [-0.6, 0.6]}
|
|
43
|
+
# # FBSABR
|
|
44
|
+
# RANGES = {'Ttm': [1.0 / 12.0, 5.0], 'F': [-0.009, 0.041], 'LnVol': [0.05, 0.5],
|
|
45
|
+
# 'Beta': [0.25, 0.75], 'Nu': [0.1, 1.0], 'Rho': [-0.6, 0.6]}
|
|
46
|
+
# # ZABR
|
|
47
|
+
# RANGES = {'Ttm': [1.0 / 12.0, 35.0], 'F': [-0.009, 0.041], 'LnVol': [0.05, 0.5],
|
|
48
|
+
# 'Beta': [0.1, 0.9], 'Nu': [0.10, 1.0], 'Rho': [-0.6, 0.6],
|
|
49
|
+
# 'Gamma': [0.1, 0.9]}
|
|
50
|
+
# Heston
|
|
51
|
+
# RANGES = {'Ttm': [1.0 / 12.0, 35.0], 'F': [-0.009, 0.041], 'LnVol': [0.05, 0.25],
|
|
52
|
+
# 'Kappa': [0.25, 4.00], 'Theta': [0.05**2, 0.25**2], 'Xi': [0.10, 0.50],
|
|
53
|
+
# 'Rho': [-0.40, 0.40]}
|
|
54
|
+
|
|
55
|
+
# ################ Generate dataset ###############################################################
|
|
56
|
+
print(">> Generate dataset")
|
|
57
|
+
|
|
58
|
+
print(f"> Generate {NUM_SAMPLES:,} price samples")
|
|
59
|
+
timer_gen = Stopwatch("Generating Samples")
|
|
60
|
+
timer_gen.trigger()
|
|
61
|
+
data_df = generator.generate_samples_inverse(NUM_SAMPLES, RANGES, SPREADS, USE_NVOL)
|
|
62
|
+
timer_gen.stop()
|
|
63
|
+
|
|
64
|
+
timer_out = Stopwatch("File Output")
|
|
65
|
+
timer_out.trigger()
|
|
66
|
+
now = datetime.now()
|
|
67
|
+
dt_string = now.strftime("%Y%m%d-%H_%M_%S")
|
|
68
|
+
data_file = os.path.join(dataset_folder, MODEL_TYPE + "_data_" + dt_string + ".tsv")
|
|
69
|
+
print("> Output to file: " + data_file)
|
|
70
|
+
generator.to_file(data_df, data_file)
|
|
71
|
+
timer_out.stop()
|
|
72
|
+
|
|
73
|
+
# Show timers
|
|
74
|
+
timer_gen.print()
|
|
75
|
+
timer_out.print()
|