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.
- {sdevpy-1.0.2/src/sdevpy.egg-info → sdevpy-1.0.3}/PKG-INFO +2 -1
- {sdevpy-1.0.2 → sdevpy-1.0.3}/pyproject.toml +2 -2
- sdevpy-1.0.3/src/sdevpy/__init__.py +1 -0
- {sdevpy-1.0.2 → 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.2 → sdevpy-1.0.3}/src/sdevpy/machinelearning/topology.py +1 -1
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovolinverse/stovolinvgen.py +4 -3
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovolinverse/stovolinvtrain.py +48 -68
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/sabrgenerator.py +33 -24
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/smilegenerator.py +36 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3/src/sdevpy.egg-info}/PKG-INFO +2 -1
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/requires.txt +1 -0
- sdevpy-1.0.2/src/sdevpy/__init__.py +0 -1
- sdevpy-1.0.2/src/sdevpy/machinelearning/learningschedules.py +0 -54
- {sdevpy-1.0.2 → sdevpy-1.0.3}/LICENSE +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/README.md +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/setup.cfg +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/bachelier.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/black.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/fbsabr.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/mcheston.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/mcsabr.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/mczabr.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/analytics/sabr.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/machinelearning/callbacks.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/machinelearning/datasets.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/interpolations.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/metrics.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/optimization.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/maths/rand.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/montecarlo/smoothers.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/aad/aad_mc.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/aad/aad_mc_nd.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/datafiles.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovolgen.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovolplot.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/projects/stovol/stovoltrain.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/settings.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/test.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/clipboard.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/constants.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/filemanager.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/jsonmanager.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/timegrids.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/timer.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/tools/utils.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/fbsabrgenerator.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mchestongenerator.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mcsabrgenerator.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/mczabrgenerator.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy/volsurfacegen/stovolfactory.py +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/SOURCES.txt +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/dependency_links.txt +0 -0
- {sdevpy-1.0.2 → sdevpy-1.0.3}/src/sdevpy.egg-info/top_level.txt +0 -0
- {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.
|
|
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'
|
|
@@ -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:
|
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
lr_schedule = CyclicalExponentialDecay(
|
|
171
|
-
#
|
|
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 =
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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,
|
|
248
|
-
|
|
249
|
-
|
|
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,
|
|
255
|
-
|
|
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,
|
|
292
|
-
axs[0, 0].plot(plot_spreads,
|
|
293
|
-
axs[0, 0].plot(plot_spreads,
|
|
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,
|
|
299
|
-
axs[0, 1].plot(plot_spreads,
|
|
300
|
-
axs[0, 1].plot(plot_spreads,
|
|
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,
|
|
306
|
-
axs[1, 0].plot(plot_spreads,
|
|
307
|
-
axs[1, 0].plot(plot_spreads,
|
|
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,
|
|
313
|
-
axs[1, 1].plot(plot_spreads,
|
|
314
|
-
axs[1, 1].plot(plot_spreads,
|
|
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,
|
|
320
|
-
axs[2, 0].plot(plot_spreads,
|
|
321
|
-
axs[2, 0].plot(plot_spreads,
|
|
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,
|
|
327
|
-
axs[2, 1].plot(plot_spreads,
|
|
328
|
-
axs[2, 1].plot(plot_spreads,
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|