.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deep Hedging Demo
2 | ## Pricing Derivatives using Machine Learning
3 |
4 | 
5 |
6 | ``` 1) Jupyter version: Run ./colab/deep_hedging_colab.ipynb on Colab. ```
7 |
8 | ``` 2) Gui version: Run python ./pyqt5/main.py Check ./requirements.txt for main dependencies.```
9 |
10 | The Black-Scholes (BS) model – developed in 1973 and based on Nobel Prize winning works – has been the de-facto standard for pricing options and other financial derivatives for nearly half a century. The model can be used, under the assumption of a perfect financial market, to calculate an options price and the associated risk sensitivities. These risk sensitivities can then be theoretically used by a trader to create a perfect hedging strategy that eliminates all risks in a portfolio of options. However, the necessary conditions for a perfect financial market, such as zero transaction cost and the possibility of continuous trading, are difficult to meet in the real world. Therefore, in practice, banks have to rely on their traders’ intuition and experience to augment the BS model hedges with manual adjustments to account for these market imperfections.
11 | The derivative desks of every bank all hedge their positions, and their PnL and risk exposure depend crucially on the quality of their hedges. If their hedges does not properly account for market imperfections, banks might underestimate the true risk exposure of their portfolios. On the other hand, if their hedges overestimate the cost of market imperfections, banks might overprice their positions (relative to their competitors) and hence risk losing trades and/or customers. Over the last few decades, the financial market has become increasingly sophisticated. Intuition and experience of traders might not be sufficiently fast and accurate to compute the impact of market imperfections on their portfolios and to come up with good manual adjustments to their BS model hedges.
12 |
13 | These limitations of the BS model are well-known, but neither academics nor practitioners have managed to develop alternatives to properly and systematically account for market frictions – at least not successful enough to be widely adopted by banks. Could machine learning (ML) be the cure? Last year, the Risk magazine reported that JP Morgan has begun to use machine learning to hedge (a.k.a. Deep Hedging) a portion of its vanilla index options flow book and plan to roll out the similar technology for single stocks, baskets and light exotics. According to Risk.net (2019), the technology can create hedging strategies that “automatically factor in market fictions, such as transaction costs, liquidity constraints and risk limits”. More amazingly, the ML algorithm “far outperformed” hedging strategies derived from the BS model, and it could reduce the cost of hedging (in certain asset class) by “as much as 80%”. The technology has been heralded by some as “a breakthrough in quantitative finance, one that could mark the end of the Black-Scholes era.” Hence, it is not surprising that firms, such as Bank of America, Societe Generale and IBM, are reportedly developing their own ML-based system for derivative hedging.
14 |
15 | Machine learning algorithms are often referred to as “black boxes” because of the inherent opaqueness and difficulties to inspect how an algorithm is able to accomplishing what is accomplishing. Buhler et al (2019) recently published a paper outlining the mechanism of this ground-breaking technology. We follow their outlined methodology to implement and replicate the “deep hedging” algorithm under different simulated market conditions. Given a distribution of the underlying assets and trader preference, the “deep hedging” algorithm attempts to identify the optimal hedge strategy (as a function of over 10k model parameters) that minimizes the residual risk of a hedged portfolio. We implement the “deep hedging” algorithm to demonstrate its potential benefit in a simplified yet sufficiently realistic setting. We first benchmark the deep hedging strategy against the classic Black-Scholes hedging strategy in a perfect world with no transaction cost, in which case the performance of both strategies should be similar. Then, we benchmark again in a world with market friction (i.e. non-zero transaction costs), in which case the deep hedging strategy should outperform the classic Black-Scholes hedging strategy.
16 |
17 | **References:**
18 |
19 | Risk.net, (2019). “Deep hedging and the end of the Black-Scholes era.”
20 |
21 | Hans Buhler et al, (2019). “Deep Hedging.” Quantitative Finance, 19(8).
22 |
--------------------------------------------------------------------------------
/deep_hedging/__init__.py:
--------------------------------------------------------------------------------
1 | from .deep_hedging import Deep_Hedging_Model
2 | from .deep_hedging import Delta_SubModel
3 |
--------------------------------------------------------------------------------
/deep_hedging/deep_hedging.py:
--------------------------------------------------------------------------------
1 | from tensorflow.keras.layers import Input, Dense, Concatenate, Subtract, \
2 | Lambda, Add, Dot, BatchNormalization, Activation, LeakyReLU
3 | from tensorflow.keras.models import Model
4 | from tensorflow.keras.initializers import he_normal, Zeros, he_uniform, TruncatedNormal
5 | import tensorflow.keras.backend as K
6 | import tensorflow as tf
7 | import numpy as np
8 |
9 | intitalizer_dict = {
10 | "he_normal": he_normal(),
11 | "zeros": Zeros(),
12 | "he_uniform": he_uniform(),
13 | "truncated_normal": TruncatedNormal()
14 | }
15 |
16 | bias_initializer=he_uniform()
17 |
18 | class Strategy_Layer(tf.keras.layers.Layer):
19 | def __init__(self, d = None, m = None, use_batch_norm = None, \
20 | kernel_initializer = "he_uniform", \
21 | activation_dense = "relu", activation_output = "linear",
22 | delta_constraint = None, day = None):
23 | super().__init__(name = "delta_" + str(day))
24 | self.d = d
25 | self.m = m
26 | self.use_batch_norm = use_batch_norm
27 | self.activation_dense = activation_dense
28 | self.activation_output = activation_output
29 | self.delta_constraint = delta_constraint
30 | self.kernel_initializer = kernel_initializer
31 |
32 | self.intermediate_dense = [None for _ in range(d)]
33 | self.intermediate_BN = [None for _ in range(d)]
34 |
35 | for i in range(d):
36 | self.intermediate_dense[i] = Dense(self.m,
37 | kernel_initializer=self.kernel_initializer,
38 | bias_initializer=bias_initializer,
39 | use_bias=(not self.use_batch_norm))
40 | if self.use_batch_norm:
41 | self.intermediate_BN[i] = BatchNormalization(momentum = 0.99, trainable=True)
42 |
43 | self.output_dense = Dense(1,
44 | kernel_initializer=self.kernel_initializer,
45 | bias_initializer = bias_initializer,
46 | use_bias=True)
47 |
48 | def call(self, input):
49 | for i in range(self.d):
50 | if i == 0:
51 | output = self.intermediate_dense[i](input)
52 | else:
53 | output = self.intermediate_dense[i](output)
54 |
55 | if self.use_batch_norm:
56 | # Batch normalization.
57 | output = self.intermediate_BN[i](output, training=True)
58 |
59 | if self.activation_dense == "leaky_relu":
60 | output = LeakyReLU()(output)
61 | else:
62 | output = Activation(self.activation_dense)(output)
63 |
64 | output = self.output_dense(output)
65 |
66 | if self.activation_output == "leaky_relu":
67 | output = LeakyReLU()(output)
68 | elif self.activation_output == "sigmoid" or self.activation_output == "tanh" or \
69 | self.activation_output == "hard_sigmoid":
70 | # Enforcing hedge constraints
71 | if self.delta_constraint is not None:
72 | output = Activation(self.activation_output)(output)
73 | delta_min, delta_max = self.delta_constraint
74 | output = Lambda(lambda x : (delta_max-delta_min)*x + delta_min)(output)
75 | else:
76 | output = Activation(self.activation_output)(output)
77 |
78 | return output
79 |
80 | def Deep_Hedging_Model(N = None, d = None, m = None, \
81 | risk_free = None, dt = None, initial_wealth = 0.0, epsilon = 0.0, \
82 | final_period_cost = False, strategy_type = None, use_batch_norm = None, \
83 | kernel_initializer = "he_uniform", \
84 | activation_dense = "relu", activation_output = "linear",
85 | delta_constraint = None, share_stretegy_across_time = False,
86 | cost_structure = "proportional"):
87 |
88 | # State variables.
89 | prc = Input(shape=(1,), name = "prc_0")
90 | information_set = Input(shape=(1,), name = "information_set_0")
91 |
92 | inputs = [prc, information_set]
93 |
94 | for j in range(N+1):
95 | if j < N:
96 | # Define the inputs for the strategy layers here.
97 | if strategy_type == "simple":
98 | helper1 = information_set
99 | elif strategy_type == "recurrent":
100 | if j ==0:
101 | # Tensorflow hack to deal with the dimension problem.
102 | # Strategy at t = -1 should be 0.
103 | # There is probably a better way but this works.
104 | # Constant tensor doesn't work.
105 | strategy = Lambda(lambda x: x*0.0)(prc)
106 |
107 | helper1 = Concatenate()([information_set,strategy])
108 |
109 | # Determine if the strategy function depends on time t or not.
110 | if not share_stretegy_across_time:
111 | strategy_layer = Strategy_Layer(d = d, m = m,
112 | use_batch_norm = use_batch_norm, \
113 | kernel_initializer = kernel_initializer, \
114 | activation_dense = activation_dense, \
115 | activation_output = activation_output,
116 | delta_constraint = delta_constraint, \
117 | day = j)
118 | else:
119 | if j == 0:
120 | # Strategy does not depend on t so there's only a single
121 | # layer at t = 0
122 | strategy_layer = Strategy_Layer(d = d, m = m,
123 | use_batch_norm = use_batch_norm, \
124 | kernel_initializer = kernel_initializer, \
125 | activation_dense = activation_dense, \
126 | activation_output = activation_output,
127 | delta_constraint = delta_constraint, \
128 | day = j)
129 |
130 | strategyhelper = strategy_layer(helper1)
131 |
132 |
133 | # strategy_-1 is set to 0
134 | # delta_strategy = strategy_{t+1} - strategy_t
135 | if j == 0:
136 | delta_strategy = strategyhelper
137 | else:
138 | delta_strategy = Subtract(name = "diff_strategy_" + str(j))([strategyhelper, strategy])
139 |
140 | if cost_structure == "proportional":
141 | # Proportional transaction cost
142 | absolutechanges = Lambda(lambda x : K.abs(x), name = "absolutechanges_" + str(j))(delta_strategy)
143 | costs = Dot(axes=1)([absolutechanges,prc])
144 | costs = Lambda(lambda x : epsilon*x, name = "cost_" + str(j))(costs)
145 | elif cost_structure == "constant":
146 | # Tensorflow hack..
147 | costs = Lambda(lambda x : epsilon + x*0.0)(prc)
148 |
149 | if j == 0:
150 | wealth = Lambda(lambda x : initial_wealth - x, name = "costDot_" + str(j))(costs)
151 | else:
152 | wealth = Subtract(name = "costDot_" + str(j))([wealth, costs])
153 |
154 | # Wealth for the next period
155 | # w_{t+1} = w_t + (strategy_t-strategy_{t+1})*prc_t
156 | # = w_t - delta_strategy*prc_t
157 | mult = Dot(axes=1)([delta_strategy, prc])
158 | wealth = Subtract(name = "wealth_" + str(j))([wealth, mult])
159 |
160 | # Accumulate interest rate for next period.
161 | FV_factor = np.exp(risk_free*dt)
162 | wealth = Lambda(lambda x: x*FV_factor)(wealth)
163 |
164 | prc = Input(shape=(1,),name = "prc_" + str(j+1))
165 | information_set = Input(shape=(1,), name = "information_set_" + str(j+1))
166 |
167 | strategy = strategyhelper
168 |
169 | if j != N - 1:
170 | inputs += [prc, information_set]
171 | else:
172 | inputs += [prc]
173 | else:
174 | # The paper assumes no transaction costs for the final period
175 | # when the position is liquidated.
176 | if final_period_cost:
177 | if cost_structure == "proportional":
178 | # Proportional transaction cost
179 | absolutechanges = Lambda(lambda x : K.abs(x), name = "absolutechanges_" + str(j))(strategy)
180 | costs = Dot(axes=1)([absolutechanges,prc])
181 | costs = Lambda(lambda x : epsilon*x, name = "cost_" + str(j))(costs)
182 | elif cost_structure == "constant":
183 | # Tensorflow hack..
184 | costs = Lambda(lambda x : epsilon + x*0.0)(prc)
185 |
186 | wealth = Subtract(name = "costDot_" + str(j))([wealth, costs])
187 | # Wealth for the final period
188 | # -delta_strategy = strategy_t
189 | mult = Dot(axes=1)([strategy, prc])
190 | wealth = Add()([wealth, mult])
191 |
192 | # Add the terminal payoff of any derivatives.
193 | payoff = Input(shape=(1,), name = "payoff")
194 | inputs += [payoff]
195 |
196 | wealth = Add(name = "wealth_" + str(j))([wealth,payoff])
197 | return Model(inputs=inputs, outputs=wealth)
198 |
199 | def Delta_SubModel(model = None, days_from_today = None, share_stretegy_across_time = False, strategy_type = "simple"):
200 | if strategy_type == "simple":
201 | inputs = model.get_layer("delta_" + str(days_from_today)).input
202 | intermediate_inputs = inputs
203 | elif strategy_type == "recurrent":
204 | inputs = [Input(1,), Input(1,)]
205 | intermediate_inputs = Concatenate()(inputs)
206 |
207 | if not share_stretegy_across_time:
208 | outputs = model.get_layer("delta_" + str(days_from_today))(intermediate_inputs)
209 | else:
210 | outputs = model.get_layer("delta_0")(intermediate_inputs)
211 |
212 | return Model(inputs, outputs)
213 |
--------------------------------------------------------------------------------
/env.yml:
--------------------------------------------------------------------------------
1 | name: base
2 | channels:
3 | - conda-forge
4 | - defaults
5 | dependencies:
6 | - brotlipy=0.7.0=py38h94c058a_1001
7 | - ca-certificates=2020.11.8=h033912b_0
8 | - certifi=2020.11.8=py38h50d1736_0
9 | - cffi=1.14.3=py38h2125817_2
10 | - chardet=3.0.4=py38h5347e94_1008
11 | - conda=4.9.2=py38h50d1736_0
12 | - conda-package-handling=1.7.2=py38h94c058a_0
13 | - cryptography=3.2.1=py38h5c1d3f9_0
14 | - idna=2.10=pyh9f0ad1d_0
15 | - libcxx=11.0.0=h439d374_0
16 | - libffi=3.3=hb1e8313_2
17 | - ncurses=6.2=h2e338ed_4
18 | - openssl=1.1.1h=haf1e3a3_0
19 | - pycosat=0.6.3=py38h94c058a_1005
20 | - pycparser=2.20=pyh9f0ad1d_2
21 | - pyopenssl=19.1.0=py_1
22 | - pysocks=1.7.1=py38h5347e94_2
23 | - python=3.8.3=h26836e1_1
24 | - python.app=2=py38_10
25 | - python_abi=3.8=1_cp38
26 | - readline=8.0=h0678c8f_2
27 | - requests=2.25.0=pyhd3deb0d_0
28 | - ruamel_yaml=0.15.80=py38h94c058a_1003
29 | - setuptools=49.6.0=py38h5347e94_2
30 | - six=1.15.0=pyh9f0ad1d_0
31 | - sqlite=3.33.0=h960bd1c_1
32 | - tk=8.6.10=hb0a8c7a_1
33 | - tqdm=4.53.0=pyhd3deb0d_0
34 | - urllib3=1.25.11=py_0
35 | - xz=5.2.5=haf1e3a3_1
36 | - yaml=0.2.5=haf1e3a3_0
37 | - zlib=1.2.11=h7795811_1010
38 | prefix: /usr/local/Caskroom/miniconda/base
39 |
--------------------------------------------------------------------------------
/instruments/EuropeanCall.py:
--------------------------------------------------------------------------------
1 | import QuantLib as ql
2 | import numpy as np
3 | from scipy import stats
4 | from stochastic_processes import BlackScholesProcess
5 |
6 | # Assume continuous dividend with flat term-structure and flat dividend structure.
7 | class EuropeanCall:
8 | def __init__(self):
9 | pass
10 |
11 | def get_BS_price(self,S=None, sigma = None,risk_free = None, \
12 | dividend = None, K = None, exercise_date = None, calculation_date = None, \
13 | day_count = None, dt = None, evaluation_method = "Numpy"):
14 |
15 | if evaluation_method is "QuantLib":
16 | # For our purpose, assume all inputs are scalar.
17 | stochastic_process = BlackScholesProcess(s0 = S, sigma = sigma, \
18 | risk_free = risk_free, dividend = dividend, day_count=day_count)
19 |
20 | engine = ql.AnalyticEuropeanEngine(stochastic_process.get_process(calculation_date))
21 |
22 | ql_payoff = ql.PlainVanillaPayoff(ql.Option.Call, K)
23 | exercise_date = ql.EuropeanExercise(exercise_date)
24 | instrument = ql.VanillaOption(ql_payoff, exercise_date)
25 |
26 | if type(self.process).__name__ is "BlackScholesProcess":
27 | engine = ql.AnalyticEuropeanEngine(self.process.get_process(calculation_date))
28 |
29 | instrument.setPricingEngine(engine)
30 |
31 | return instrument.NPV()
32 | elif evaluation_method is "Numpy":
33 | # For our purpose, assume s0 is a NumPy array, other inputs are scalar.
34 | T = np.arange(0, (exercise_date - calculation_date + 1))*dt
35 | T = np.repeat(np.flip(T[None,:]), S.shape[0], 0)
36 |
37 | # Ignore division by 0 warning (expected behaviors as the limits of CDF is defined).
38 | with np.errstate(divide='ignore'):
39 | d1 = np.divide(np.log(S / K) + (risk_free - dividend + 0.5 * sigma ** 2) * T, sigma * np.sqrt(T))
40 | d2 = np.divide(np.log(S / K) + (risk_free - dividend - 0.5 * sigma ** 2) * T, sigma * np.sqrt(T))
41 |
42 | return (S * stats.norm.cdf(d1, 0.0, 1.0) - K * np.exp(-risk_free * T) * stats.norm.cdf(d2, 0.0, 1.0))
43 |
44 | def get_BS_delta(self,S=None, sigma = None,risk_free = None, \
45 | dividend = None, K = None, exercise_date = None, calculation_date = None, \
46 | day_count = None, dt = None, evaluation_method = "Numpy"):
47 |
48 | if evaluation_method is "QuantLib":
49 | # For our purpose, assume all inputs are scalar.
50 | stochastic_process = BlackScholesProcess(s0 = S, sigma = sigma, \
51 | risk_free = risk_free, dividend = dividend, day_count=day_count)
52 |
53 | engine = ql.AnalyticEuropeanEngine(stochastic_process.get_process(calculation_date))
54 |
55 | ql_payoff = ql.PlainVanillaPayoff(ql.Option.Call, K)
56 | exercise_date = ql.EuropeanExercise(exercise_date)
57 | instrument = ql.VanillaOption(ql_payoff, exercise_date)
58 |
59 | if type(self.process).__name__ is "BlackScholesProcess":
60 | engine = ql.AnalyticEuropeanEngine(self.process.get_process(calculation_date))
61 |
62 | instrument.setPricingEngine(engine)
63 |
64 | return instrument.delta()
65 | elif evaluation_method is "Numpy":
66 | # For our purpose, assume s0 is a NumPy array, other inputs are scalar.
67 | T = np.arange(0, (exercise_date - calculation_date + 1))*dt
68 | T = np.repeat(np.flip(T[None,:]), S.shape[0], 0)
69 |
70 | # Ignore division by 0 warning (expected behaviors as the limits of CDF is defined).
71 | with np.errstate(divide='ignore'):
72 | d1 = np.divide(np.log(S / K) + (risk_free - dividend + 0.5 * sigma ** 2) * T, sigma * np.sqrt(T))
73 |
74 | return stats.norm.cdf(d1, 0.0, 1.0)
75 |
76 | def get_BS_vega(self,S=None, sigma = None,risk_free = None, \
77 | dividend = None, K = None, exercise_date = None, calculation_date = None, \
78 | day_count = None, dt = None, evaluation_method = "Numpy"):
79 |
80 | if evaluation_method is "QuantLib":
81 | # For our purpose, assume all inputs are scalar.
82 | stochastic_process = BlackScholesProcess(s0 = S, sigma = sigma, \
83 | risk_free = risk_free, dividend = dividend, day_count=day_count)
84 |
85 | engine = ql.AnalyticEuropeanEngine(stochastic_process.get_process(calculation_date))
86 |
87 | ql_payoff = ql.PlainVanillaPayoff(ql.Option.Call, K)
88 | exercise_date = ql.EuropeanExercise(exercise_date)
89 | instrument = ql.VanillaOption(ql_payoff, exercise_date)
90 |
91 | if type(self.process).__name__ is "BlackScholesProcess":
92 | engine = ql.AnalyticEuropeanEngine(self.process.get_process(calculation_date))
93 |
94 | instrument.setPricingEngine(engine)
95 |
96 | return instrument.vega()
97 | elif evaluation_method is "Numpy":
98 | # For our purpose, assume s0 is a NumPy array, other inputs are scalar.
99 | T = np.arange(0, (exercise_date - calculation_date + 1))*dt
100 | T = np.repeat(np.flip(T[None,:]), S.shape[0], 0)
101 |
102 | # Ignore division by 0 warning (expected behaviors as the limits of CDF is defined).
103 | with np.errstate(divide='ignore'):
104 | d1 = np.divide(np.log(S / K) + (risk_free - dividend + 0.5 * sigma ** 2) * T, sigma * np.sqrt(T))
105 |
106 | return np.multiply(S, np.sqrt(T))*stats.norm.pdf(d1, 0.0, 1.0)
107 |
108 | def get_BS_PnL(self, S = None, payoff = None, delta = None, dt = None, risk_free = None, \
109 | final_period_cost = None, epsilon = None, cost_structure="proportional"):
110 | # Compute Black-Scholes PnL (for a short position, i.e. the Bank sells
111 | # a call option. The model delta from Quantlib is a long delta.
112 | N = S.shape[1]-1
113 |
114 | PnL_BS = np.multiply(S[:,0], -delta[:,0]) \
115 |
116 | if cost_structure == "proportional":
117 | PnL_BS -= np.abs(delta[:,0])*S[:,0]*epsilon
118 | elif cost_structure == "constant":
119 | PnL_BS -= epsilon
120 |
121 | PnL_BS = PnL_BS*np.exp(risk_free*dt)
122 |
123 | for t in range(1, N):
124 | PnL_BS += np.multiply(S[:,t], -delta[:,t] + delta[:,t-1])
125 |
126 | if cost_structure == "proportional":
127 | PnL_BS -= np.abs(delta[:,t] -delta[:,t-1])*S[:,t]*epsilon
128 | elif cost_structure == "constant":
129 | PnL_BS -= epsilon
130 |
131 | PnL_BS = PnL_BS*np.exp(risk_free*dt)
132 |
133 | PnL_BS += np.multiply(S[:,N],delta[:,N-1]) + payoff
134 |
135 | if final_period_cost:
136 | if cost_structure == "proportional":
137 | PnL_BS -= np.abs(delta[:,N-1])*S[:,N]*epsilon
138 | elif cost_structure == "constant":
139 | PnL_BS -= epsilon
140 |
141 | return PnL_BS
142 |
--------------------------------------------------------------------------------
/instruments/__init__.py:
--------------------------------------------------------------------------------
1 | from .EuropeanCall import EuropeanCall
2 |
--------------------------------------------------------------------------------
/loss_metrics/__init__.py:
--------------------------------------------------------------------------------
1 | from .entropy import Entropy
2 | from .cvar import CVaR
3 |
--------------------------------------------------------------------------------
/loss_metrics/cvar.py:
--------------------------------------------------------------------------------
1 | import tensorflow.keras.backend as K
2 |
3 |
4 | def CVaR(wealth = None, w = None, loss_param = None):
5 | alpha = loss_param
6 | # Expected shortfall risk measure
7 | return K.mean(w + (K.maximum(-wealth-w,0)/(1.0-alpha)))
8 |
--------------------------------------------------------------------------------
/loss_metrics/entropy.py:
--------------------------------------------------------------------------------
1 | import tensorflow.keras.backend as K
2 |
3 |
4 | def Entropy(wealth=None, w=None, loss_param=None):
5 | _lambda = loss_param
6 |
7 | # Entropy (exponential) risk measure
8 | return (1/_lambda)*K.log(K.mean(K.exp(-_lambda*wealth)))
9 |
--------------------------------------------------------------------------------
/presentation/data/target_PnL_0.015.npy:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YuMan-Tam/deep-hedging/b4c570a25f7134950579db37cc2cfe74ba4895ff/presentation/data/target_PnL_0.015.npy
--------------------------------------------------------------------------------
/presentation/default_params.py:
--------------------------------------------------------------------------------
1 | # Define the initial parameters for the deep hedging demo
2 | def DeepHedgingParams():
3 | params = [
4 | {'name': 'European Call Option', 'type': 'group', 'children': [
5 | {'name': 'S0', 'type': 'int', 'value': 100.0},
6 | {'name': 'Strike', 'type': 'float', 'value': 100.0},
7 | {'name': 'Implied Volatility', 'type': 'float', 'value': 0.2},
8 | {'name': 'Risk-Free Rate', 'type': 'float', 'value': 0.0},
9 | {'name': 'Dividend Yield', 'type': 'float', 'value': 0.0},
10 | {'name': 'Maturity (in days)', 'type': 'int', 'value': 30},
11 | {'name': 'Proportional Transaction Cost', 'type': 'group', 'children': [
12 | {'name': 'Cost', 'type': 'float', 'value': 0.0},
13 | ]},
14 | ]},
15 | {'name': 'Monte-Carlo Simulation', 'type': 'group', 'children': [
16 | {'name': 'Sample Size', 'type': 'group', 'children': [
17 | {'name': 'Training', 'type': 'int', 'value': 1*(10**5)},
18 | {'name': 'Testing (as fraction of Training)', 'type': 'float', 'value': 0.2}
19 | ]},
20 | ]},
21 | {'name': 'Deep Hedging Strategy', 'type': 'group', 'children': [
22 | {'name': 'Loss Function', 'type': 'group', 'children': [
23 | {'name': 'Loss Type', 'type': 'list', 'values': {"Entropy": "Entropy", "CVaR": "CVaR"}, "default": "Entropy"},
24 | {'name': 'Risk Aversion', 'type': 'float', 'value': 1.0}
25 | ]},
26 | {'name': 'Network Structure', 'type': 'group', 'children': [
27 | {'name': 'Network Type', 'type': 'list', 'values': {"Simple": "simple", "Recurrent": "recurrent"}, "default": "simple"},
28 | {'name': 'Number of Hidden Layers', 'type': 'int', 'value': 1},
29 | {'name': 'Number of Neurons', 'type': 'int', 'value': 15},
30 | ]},
31 | {'name': 'Learning Parameters', 'type': 'group', 'children': [
32 | {'name': 'Learning Rate', 'type': 'float', 'value': 5e-3},
33 | {'name': 'Mini-Batch Size', 'type': 'int', 'value': 256},
34 | {'name': 'Number of Epochs', 'type': 'int', 'value': 50},
35 | ]},
36 | ]},
37 | ]
38 | return params
39 |
--------------------------------------------------------------------------------
/presentation/dh_worker.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | # Add the parent directory to the search paths to import the libraries.
5 | dir_path = os.path.dirname(os.path.realpath(__file__))
6 | sys.path.insert(0, "/".join([dir_path, ".."]))
7 |
8 | import time
9 |
10 | import numpy as np
11 |
12 | import tensorflow as tf
13 | from tensorflow.keras.optimizers import Adam
14 |
15 | from pyqtgraph.Qt import QtCore
16 |
17 | from loss_metrics import Entropy
18 |
19 |
20 | # Reducing learning rate
21 | reduce_lr_param = {"patience": 2, "min_delta": 1e-3, "factor": 0.5}
22 |
23 | # Number of bins to plot for the PnL histograms.
24 | num_bins = 30
25 |
26 |
27 | # Put the deep-hedging algo in a separate thread than the plotting thread to
28 | # improve performance.
29 | class DHworker(QtCore.QThread):
30 | DH_outputs = QtCore.pyqtSignal(np.ndarray,
31 | np.ndarray,
32 | np.ndarray,
33 | np.float32,
34 | float,
35 | float,
36 | bool)
37 |
38 | def __init__(self):
39 | QtCore.QThread.__init__(self)
40 |
41 | def __del__(self):
42 | self.wait()
43 |
44 | def run_deep_hedge_algo(self,
45 | training_dataset=None,
46 | epochs=None,
47 | Ktrain=None,
48 | batch_size=None,
49 | model=None,
50 | submodel=None,
51 | strategy_type=None,
52 | loss_param=None,
53 | learning_rate=None,
54 | xtest=None,
55 | xtrain=None,
56 | initial_price_BS=None,
57 | width=None,
58 | I_range=None,
59 | x_range=None):
60 | self.training_dataset = training_dataset
61 | self.Ktrain = Ktrain
62 | self.batch_size = batch_size
63 | self.model = model
64 | self.submodel = submodel
65 | self.loss_param = loss_param
66 | self.initial_price_BS = initial_price_BS
67 | self.width = width
68 | self.epochs = epochs
69 | self.xtest = xtest
70 | self.xtrain = xtrain
71 | self.I_range = I_range
72 | self.x_range = x_range
73 | self.strategy_type = strategy_type
74 | self.learning_rate = learning_rate
75 |
76 | self.start()
77 |
78 | def pause(self):
79 | self._pause = True
80 |
81 | def cont(self):
82 | self._pause = False
83 |
84 | def stop(self):
85 | self._exit = True
86 | self.exit()
87 |
88 | def is_running(self):
89 | if self._pause or self._exit:
90 | return False
91 | else:
92 | return True
93 |
94 | def Reduce_Learning_Rate(self, num_epoch, loss):
95 | # Extract in-sample loss from the previous epoch. Comparison starts in
96 | # epoch 2 and the index for epoch 1 is 0 -> -2
97 | min_loss = self.loss_record[:, 1].min()
98 | if min_loss - loss < reduce_lr_param["min_delta"]:
99 | self.reduce_lr_counter += 1
100 |
101 | if self.reduce_lr_counter > reduce_lr_param["patience"]:
102 | self.learning_rate = self.learning_rate * reduce_lr_param["factor"]
103 | self.optimizer.learning_rate = self.learning_rate
104 | print(
105 | "The learning rate is reduced to {}.".format(
106 | self.learning_rate))
107 | self.reduce_lr_counter = 0
108 |
109 | def run(self):
110 | # Initialize pause and stop buttons.
111 | self._exit = False
112 | self._pause = False
113 |
114 | # Variables to control skipped frames. If the DH algo output much
115 | # faster than the graphic output, the plots can be jammed.
116 | self.Figure_IsUpdated = True
117 |
118 | self.reduce_lr_counter = 0
119 | self.early_stopping_counter = 0
120 |
121 | certainty_equiv = tf.Variable(0.0, name="certainty_equiv")
122 |
123 | # Accelerator Function.
124 | model_func = tf.function(self.model)
125 | submodel_func = tf.function(self.submodel)
126 |
127 | self.optimizer = Adam(learning_rate=self.learning_rate)
128 |
129 | oos_loss = None
130 | PnL_DH = None
131 | DH_delta = None
132 | DH_bins = None
133 | num_batch = None
134 |
135 | num_epoch = 0
136 | while num_epoch <= self.epochs:
137 | # Exit event loop if the exit flag is set to True.
138 | if self._exit:
139 | mini_batch_iter = None
140 | self._exit = False
141 | self._pause = False
142 | break
143 |
144 | if not self._pause:
145 | try:
146 | mini_batch = mini_batch_iter.next()
147 | except BaseException:
148 | # Reduce learning rates and Early Stopping are based on
149 | # in-sample losses calculated once per epoch.
150 | in_sample_wealth = model_func(self.xtrain)
151 | in_sample_loss = Entropy(
152 | in_sample_wealth, certainty_equiv, self.loss_param)
153 |
154 | if num_epoch >= 1:
155 | print(("The deep-hedging price is {:0.4f} after " +
156 | "{} epoch.").format(oos_loss, num_epoch))
157 |
158 | # Programming hack. The deep-hedging algo computes
159 | # faster than the computer can plot, so there could
160 | # be missing frames, i.e. there is no guarantee
161 | # that every batch is plotted. Here, I force a
162 | # signal to be emitted at the end of an epoch.
163 | time.sleep(1)
164 |
165 | self.DH_outputs.emit(
166 | PnL_DH,
167 | DH_delta,
168 | DH_bins,
169 | oos_loss.numpy().squeeze(),
170 | num_epoch,
171 | num_batch,
172 | True)
173 |
174 | # This is needed to prevent the output signals from
175 | # emitting faster than the system can plot a graph.
176 | #
177 | # The performance is much better than emitting at fixed
178 | # time intervals.
179 | self.Figure_IsUpdated = False
180 |
181 | if num_epoch == 1:
182 | self.loss_record = np.array(
183 | [num_epoch, in_sample_loss], ndmin=2)
184 | elif num_epoch > 1:
185 | self.Reduce_Learning_Rate(num_epoch, in_sample_loss)
186 | self.loss_record = np.vstack(
187 | [self.loss_record,
188 | np.array([num_epoch, in_sample_loss])])
189 |
190 | mini_batch_iter = self.training_dataset.shuffle(
191 | self.Ktrain).batch(self.batch_size).__iter__()
192 | mini_batch = mini_batch_iter.next()
193 |
194 | num_batch = 0
195 | num_epoch += 1
196 |
197 | num_batch += 1
198 |
199 | # Record gradient
200 | with tf.GradientTape() as tape:
201 | wealth = model_func(mini_batch)
202 | loss = Entropy(wealth, certainty_equiv, self.loss_param)
203 |
204 | oos_wealth = model_func(self.xtest)
205 | PnL_DH = oos_wealth.numpy().squeeze() # Out-of-sample
206 |
207 | submodel_delta_range = np.expand_dims(self.I_range, axis=1)
208 | if self.strategy_type == "simple":
209 | submodel_inputs = submodel_delta_range
210 | elif self.strategy_type == "recurrent":
211 | # Assume previous delta is ATM.
212 | submodel_inputs = [
213 | submodel_delta_range,
214 | np.ones_like(submodel_delta_range) * 0.5]
215 | DH_delta = submodel_func(submodel_inputs).numpy().squeeze()
216 | DH_bins, _ = np.histogram(
217 | PnL_DH + self.initial_price_BS,
218 | bins=num_bins,
219 | range=self.x_range)
220 |
221 | # Forward and backward passes
222 | grads = tape.gradient(loss, self.model.trainable_weights)
223 | self.optimizer.apply_gradients(
224 | zip(grads, self.model.trainable_weights))
225 |
226 | # Compute Out-of-Sample Loss
227 | oos_loss = Entropy(
228 | oos_wealth, certainty_equiv, self.loss_param)
229 |
230 | if self.Figure_IsUpdated:
231 | self.DH_outputs.emit(
232 | PnL_DH,
233 | DH_delta,
234 | DH_bins,
235 | oos_loss.numpy().squeeze(),
236 | num_epoch,
237 | num_batch,
238 | False)
239 |
240 | # This is needed to prevent the output signals from emitting
241 | # faster than the system can plot a graph.
242 | #
243 | # The performance is much better than emitting at fixed time
244 | # intervals.
245 | self.Figure_IsUpdated = False
246 |
247 | # Mandatory pause for the first iteration to explain demo.
248 | if num_epoch == 1 and num_batch == 1:
249 | self.pause()
250 | else:
251 | time.sleep(1)
252 |
--------------------------------------------------------------------------------
/presentation/main.py:
--------------------------------------------------------------------------------
1 | # Author: Yu-Man Tam
2 | # Email: yuman.tam@gmail.com
3 | #
4 | # Last updated: 5/22/2020
5 | #
6 | # Reference: Deep Hedging (2019, Quantitative Finance) by Buehler et al.
7 | # https://www.tandfonline.com/doi/abs/10.1080/14697688.2019.1571683
8 |
9 | import sys
10 | import os
11 |
12 | import tensorflow as tf
13 |
14 | # Add the parent directory to the search paths to import the libraries.
15 | dir_path = os.path.dirname(os.path.realpath(__file__))
16 | sys.path.insert(0, "/".join([dir_path, ".."]))
17 |
18 | from pyqtgraph.Qt import QtWidgets
19 | from main_window import MainWindow
20 |
21 | # Tensorflow settings
22 | tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
23 | tf.autograph.set_verbosity(0)
24 |
25 | if __name__ == '__main__':
26 | app = QtWidgets.QApplication(sys.argv)
27 | main = MainWindow()
28 | main.show()
29 | app.exec_()
30 |
--------------------------------------------------------------------------------
/presentation/main_window.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | # Add the parent directory to the search paths to import the libraries.
5 | dir_path = os.path.dirname(os.path.realpath(__file__))
6 | sys.path.insert(0, "/".join([dir_path, ".."]))
7 |
8 | import QuantLib as ql
9 | import numpy as np
10 | import tensorflow as tf
11 | import pyqtgraph as pg
12 |
13 | from tensorflow.keras.models import Model
14 | from tensorflow.keras.layers import Input, Concatenate
15 | from pyqtgraph.Qt import QtWidgets, QtGui, QtCore
16 | from pyqtgraph.parametertree import ParameterTree, Parameter
17 | from scipy.stats import norm
18 |
19 | from dh_worker import DHworker
20 | from default_params import DeepHedgingParams
21 | from loss_metrics import Entropy
22 | from deep_hedging import Deep_Hedging_Model
23 | from stochastic_processes import BlackScholesProcess
24 | from instruments import EuropeanCall
25 | from utilities import train_test_split
26 |
27 | # Specify the day (from today) for the delta plot.
28 | delta_plot_day = 15
29 |
30 | # European call option (short).
31 | calculation_date = ql.Date.todaysDate()
32 |
33 | # Day convention.
34 | day_count = ql.Actual365Fixed() # Actual/Actual (ISDA)
35 |
36 | # Information set (in string)
37 | # Choose from: S, log_S, normalized_log_S (by S0)
38 | information_set = "normalized_log_S"
39 |
40 | # Loss function
41 | # loss_type = "CVaR" (Expected Shortfall) -> loss_param = alpha
42 | # loss_type = "Entropy" -> loss_param = lambda
43 | loss_type = "Entropy"
44 |
45 | # Other NN parameters
46 | use_batch_norm = False
47 | kernel_initializer = "he_uniform"
48 |
49 | activation_dense = "leaky_relu"
50 | activation_output = "sigmoid"
51 | final_period_cost = False
52 |
53 | # Reducing learning rate
54 | reduce_lr_param = {"patience": 2, "min_delta": 1e-3, "factor": 0.5}
55 |
56 | # Number of bins to plot for the PnL histograms.
57 | num_bins = 30
58 |
59 |
60 | class MainWindow(QtWidgets.QMainWindow):
61 | def __init__(self):
62 | # Inheritance from the QMainWindow class
63 | # Reference: https://doc.qt.io/qt-5/qmainwindow.html
64 | super().__init__()
65 | self.days_from_today = delta_plot_day
66 | self.Thread_RunDH = DHworker()
67 |
68 | # The order of code is important here: Make sure the
69 | # emitted signals are connected before actually running
70 | # the Worker.
71 | self.Thread_RunDH.DH_outputs["PyQt_PyObject",
72 | "PyQt_PyObject",
73 | "PyQt_PyObject",
74 | "PyQt_PyObject",
75 | "double",
76 | "double",
77 | "bool"].connect(self.Update_Plots_Widget)
78 |
79 | # Define a top-level widget to hold everything
80 | self.w = QtGui.QWidget()
81 |
82 | # Create a grid layout to manage the widgets size and position
83 | self.layout = QtGui.QGridLayout()
84 | self.w.setLayout(self.layout)
85 |
86 | self.setCentralWidget(self.w)
87 |
88 | # Add the parameter menu.
89 | self.tree_height = 5 # Must be Odd number.
90 |
91 | self.tree = self.Deep_Hedging_Parameter_Widget()
92 |
93 | self.layout.addWidget(self.tree,
94 | 0, 0, self.tree_height, 2) # upper-left
95 |
96 | self.tree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
97 | self.tree.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
98 | self.tree.setMinimumSize(350, 650)
99 |
100 | # Add a run button
101 | self.run_btn = QtGui.QPushButton('Run')
102 | self.layout.addWidget(self.run_btn,
103 | self.tree_height + 1,
104 | 0, 1, 1) # button goes in upper-left
105 |
106 | # Add a pause button
107 | self.pause_btn = QtGui.QPushButton('Pause')
108 | self.layout.addWidget(self.pause_btn,
109 | self.tree_height + 1,
110 | 1, 1, 1) # button goes in upper-left
111 |
112 | # Run the deep hedging algo in a separate thread when the run
113 | # button is clicked.
114 | self.run_btn.clicked.connect(self.RunButton)
115 |
116 | # Pause button.
117 | self.pause_btn.clicked.connect(self.Pause)
118 |
119 | def Deep_Hedging_Parameter_Widget(self):
120 | tree = ParameterTree()
121 |
122 | # Create tree of Parameter objects
123 | self.params = Parameter.create(name='params', type='group',
124 | children=DeepHedgingParams())
125 | tree.setParameters(self.params, showTop=False)
126 |
127 | return tree
128 |
129 | # Define the event when the "Run" button is clicked.
130 | def RunButton(self):
131 | if self.run_btn.text() == "Stop":
132 | self.Thread_RunDH.stop()
133 | if self.pause_btn.text() == "Continue":
134 | self.pause_btn.setText("Pause")
135 | self.run_btn.setText("Run")
136 | elif self.run_btn.text() == "Run":
137 | self.run_btn.setText("Stop")
138 |
139 | # Set parameters
140 | self.Ktrain = self.params.param("Monte-Carlo Simulation",
141 | 'Sample Size', "Training").value()
142 | self.Ktest_ratio = \
143 | self.params.param("Monte-Carlo Simulation",
144 | 'Sample Size',
145 | "Testing (as fraction of Training)").value()
146 | self.N = self.params.param("European Call Option",
147 | "Maturity (in days)").value()
148 | self.S0 = self.params.param("European Call Option", "S0").value()
149 | self.strike = self.params.param("European Call Option",
150 | "Strike").value()
151 | self.sigma = self.params.param("European Call Option",
152 | "Implied Volatility").value()
153 | self.risk_free = self.params.param("European Call Option",
154 | "Risk-Free Rate").value()
155 | self.dividend = self.params.param("European Call Option",
156 | "Dividend Yield").value()
157 |
158 | self.loss_param = self.params.param("Deep Hedging Strategy",
159 | 'Loss Function',
160 | "Risk Aversion").value()
161 | self.epsilon = self.params.param("European Call Option",
162 | "Proportional Transaction Cost",
163 | "Cost").value()
164 | self.d = self.params.param("Deep Hedging Strategy",
165 | "Network Structure",
166 | "Number of Hidden Layers").value()
167 | self.m = self.params.param("Deep Hedging Strategy",
168 | "Network Structure",
169 | "Number of Neurons").value()
170 | self.strategy_type = self.params.param("Deep Hedging Strategy",
171 | "Network Structure",
172 | "Network Type").value()
173 | self.lr = self.params.param("Deep Hedging Strategy",
174 | "Learning Parameters",
175 | "Learning Rate").value()
176 | self.batch_size = self.params.param("Deep Hedging Strategy",
177 | "Learning Parameters",
178 | "Mini-Batch Size").value()
179 | self.epochs = self.params.param("Deep Hedging Strategy",
180 | "Learning Parameters",
181 | "Number of Epochs").value()
182 |
183 | self.maturity_date = calculation_date + self.N
184 | self.payoff_func = lambda x: -np.maximum(x - self.strike, 0.0)
185 |
186 | # Simulate the stock price process.
187 | self.S = self.simulate_stock_prices()
188 |
189 | # Assemble the dataset for training and testing.
190 | # Structure of data:
191 | # 1) Trade set: [S]
192 | # 2) Information set: [S]
193 | # 3) payoff (dim = 1)
194 | self.training_dataset = self.assemble_data()
195 |
196 | # Compute Black-Scholes prices for benchmarking.
197 | self.price_BS, self.delta_BS, self.PnL_BS = \
198 | self.get_Black_Scholes_Prices()
199 |
200 | # Compute the loss value for Black-Scholes PnL
201 | self.loss_BS = Entropy(
202 | self.PnL_BS,
203 | tf.Variable(0.0),
204 | self.loss_param).numpy()
205 |
206 | # Define model and sub-models
207 | self.model = self.Define_DH_model()
208 | self.submodel = self.Define_DH_Delta_Strategy_Model()
209 |
210 | plot_height_split = (self.tree_height + 1) / 2
211 |
212 | # For the presentation...
213 | self.flag_target = False
214 | if self.epsilon > 0:
215 | try:
216 | self.target_color = (0, 155, 0)
217 | self.target_PnL = np.load(
218 | "./data/target_PnL_" + str(self.epsilon) + ".npy")
219 | self.target_loss = Entropy(
220 | self.target_PnL,
221 | tf.Variable(0.0),
222 | self.loss_param).numpy()
223 | self.flag_target = True
224 | except BaseException:
225 | print("No saved file.")
226 | pass
227 | else:
228 | try:
229 | self.fig_loss.removeItem(self.DH_target_loss_textItem)
230 | except BaseException:
231 | pass
232 |
233 | # Add the PnL histogram (PlotWidget) - Black-Scholes vs Deep
234 | # Hedging.
235 | self.fig_PnL = self.PnL_Hist_Widget()
236 | self.layout.addWidget(self.fig_PnL, 0, 3, plot_height_split, 1)
237 | self.fig_PnL.setMinimumWidth(600)
238 |
239 | # Add the Delta line plot (PlotWidget) - Black-Scholes vs Deep
240 | # Hedging.
241 | self.fig_delta = self.Delta_Plot_Widget()
242 | self.layout.addWidget(self.fig_delta, 0, 4, plot_height_split, 1)
243 | self.fig_delta.setMinimumWidth(600)
244 |
245 | # Add the loss plot (PlotWidget) - Black-Scholes vs Deep Hedging.
246 | self.fig_loss = self.Loss_Plot_Widget()
247 | self.layout.addWidget(
248 | self.fig_loss,
249 | plot_height_split,
250 | 3,
251 | plot_height_split + 1,
252 | 2)
253 | self.fig_loss.setMinimumWidth(1200)
254 |
255 | # Run the deep hedging algo in a separate thread.
256 | self.Thread_RunDH.run_deep_hedge_algo(
257 | training_dataset=self.training_dataset,
258 | epochs=self.epochs,
259 | Ktrain=self.Ktrain,
260 | batch_size=self.batch_size,
261 | model=self.model,
262 | submodel=self.submodel,
263 | strategy_type=self.strategy_type,
264 | loss_param=self.loss_param,
265 | learning_rate=self.lr,
266 | xtest=self.xtest,
267 | xtrain=self.xtrain,
268 | initial_price_BS=self.price_BS[0][0],
269 | width=self.width,
270 | I_range=self.I_range,
271 | x_range=self.x_range)
272 |
273 | # Define action when the Pause button is clicked.
274 |
275 | def Pause(self):
276 | if self.pause_btn.text() == "Pause":
277 | self.Thread_RunDH.pause()
278 | self.pause_btn.setText("Continue")
279 | elif self.pause_btn.text() == "Continue":
280 | self.Thread_RunDH.cont()
281 | self.pause_btn.setText("Pause")
282 |
283 | # Define deep hedging model
284 |
285 | def Define_DH_model(self):
286 | # Setup and compile the model
287 | model = Deep_Hedging_Model(
288 | N=self.N,
289 | d=self.d,
290 | m=self.m,
291 | risk_free=self.risk_free,
292 | dt=self.dt,
293 | strategy_type=self.strategy_type,
294 | epsilon=self.epsilon,
295 | use_batch_norm=use_batch_norm,
296 | kernel_initializer=kernel_initializer,
297 | activation_dense=activation_dense,
298 | activation_output=activation_output,
299 | final_period_cost=final_period_cost,
300 | delta_constraint=None)
301 | return model
302 |
303 | def Define_DH_Delta_Strategy_Model(self):
304 | if self.strategy_type == "simple":
305 | # Set up the sub-model that outputs the delta.
306 | submodel = \
307 | Model(self.model.get_layer("delta_" +
308 | str(self.days_from_today)).input,
309 | self.model.get_layer("delta_" +
310 | str(self.days_from_today)).output)
311 | elif self.strategy_type == "recurrent":
312 | # For "recurrent", the information set is price as well as the past
313 | # delta.
314 | inputs = [Input(1,), Input(1,)]
315 |
316 | intermediate_inputs = Concatenate()(inputs)
317 |
318 | outputs = self.model.get_layer(
319 | "delta_" + str(self.days_from_today))(intermediate_inputs)
320 |
321 | submodel = Model(inputs=inputs, outputs=outputs)
322 | return submodel
323 |
324 | # Draw PnL histogram (PlotWidget) - Black-Scholes vs Deep Hedging.
325 |
326 | def PnL_Hist_Widget(self):
327 | # Initialize the PnL Histogram Widget.
328 | fig_PnL = pg.PlotWidget()
329 |
330 | x_min = np.minimum(self.PnL_BS.min() + self.price_BS[0, 0], -3)
331 | x_max = np.maximum(self.PnL_BS.max() + self.price_BS[0, 0], 3)
332 |
333 | self.x_range = (x_min, x_max)
334 | self.BS_bins, self.bin_edges = np.histogram(
335 | self.PnL_BS + self.price_BS[0, 0],
336 | bins=num_bins,
337 | range=self.x_range)
338 | if self.flag_target:
339 | self.width = (self.bin_edges[1] - self.bin_edges[0]) / 3.0
340 | else:
341 | self.width = (self.bin_edges[1] - self.bin_edges[0]) / 2.0
342 |
343 | self.BS_hist = pg.BarGraphItem(x=self.bin_edges[:-2],
344 | height=self.BS_bins,
345 | width=self.width,
346 | brush='r',
347 | name="Red - Black-Scholes",
348 | antialias=False)
349 |
350 | fig_PnL.setTitle(
351 | "Profit and Loss (PnL) Histogram")
352 | fig_PnL.setLabels(
353 | left="Frequency",
354 | bottom="Profit and Loss (PnL) ")
355 |
356 | # Fix the problem that Y-axes keep moving when transactioni cost is
357 | # greater than zero.
358 | fig_PnL.setYRange(0, self.BS_bins.max() * 1.1)
359 |
360 | if self.flag_target:
361 | fig_PnL.setXRange(self.bin_edges.min(), 2)
362 | else:
363 | fig_PnL.setXRange(self.bin_edges.min(), 2)
364 |
365 | fig_PnL.addItem(self.BS_hist)
366 |
367 | if self.flag_target:
368 | self.DH_target_bins, _ = np.histogram(
369 | self.target_PnL + self.price_BS[0, 0],
370 | bins=num_bins,
371 | range=self.x_range)
372 | self.DH_target_hist = pg.BarGraphItem(
373 | x=self.bin_edges[
374 | :-2] + 2 * self.width,
375 | height=self.DH_target_bins,
376 | width=self.width,
377 | brush=self.target_color,
378 | name="Green - Deep-Hedging PnL (Target)",
379 | antialias=False)
380 | fig_PnL.addItem(self.DH_target_hist)
381 | PnL_html = ("" +
382 | "" +
383 | "Black-Scholes PnL (Benchmark)
" +
384 | "" +
385 | "Deep-Hedging PnL (Target)" +
386 | "
").format(str(self.target_color)) + \
387 | "" + \
388 | "Deep-Hedging PnL
"
389 | else:
390 | PnL_html = "" + \
391 | "" + \
392 | "Black-Scholes PnL (Benchmark)
" + \
393 | "" + \
394 | "Deep-Hedging PnL
"
395 |
396 | fig_PnL_text = pg.TextItem(
397 | html=PnL_html, anchor=(
398 | 0, 0), angle=0, border='w', fill=(
399 | 225, 225, 200))
400 |
401 | fig_PnL_text.setPos(self.bin_edges.min(), self.BS_bins.max() * 1.05)
402 | fig_PnL.addItem(fig_PnL_text)
403 |
404 | return fig_PnL
405 |
406 | # Draw Delta plot (PlotWidget) - Black-Scholes vs Deep Hedging.
407 | # Assume the PnL_Hist_Widget ran first, so we don't need to run the model
408 | # again.
409 |
410 | def Delta_Plot_Widget(self):
411 | self.tau = (self.N - self.days_from_today) * self.dt
412 |
413 | self.min_S = self.S_test[0][:, self.days_from_today].min()
414 | self.max_S = self.S_test[0][:, self.days_from_today].max()
415 | self.S_range = np.linspace(self.min_S, self.max_S, 51)
416 |
417 | # Attention: Need to transform it to be consistent with the information
418 | # set.
419 | if information_set == "S":
420 | self.I_range = self.S_range # Information set
421 | elif information_set == "log_S":
422 | self.I_range = np.log(self.S_range)
423 | elif information_set == "normalized_log_S":
424 | self.I_range = np.log(self.S_range / self.S0)
425 |
426 | # Compute Black-Scholes delta for S_range.
427 | # Reference: https://en.wikipedia.org/wiki/Greeks_(finance)
428 | self.d1 = (np.log(self.S_range) - np.log(self.strike) +
429 | (self.risk_free - self.dividend + (self.sigma**2) / 2) *
430 | self.tau) / (self.sigma * np.sqrt(self.tau))
431 |
432 | self.model_delta = norm.cdf(
433 | self.d1) * np.exp(-self.dividend * self.tau)
434 |
435 | fig_delta = pg.PlotWidget()
436 |
437 | self.BS_delta_plot = pg.PlotCurveItem(
438 | pen=pg.mkPen(color="r", width=2.5), name="Black-Scholes")
439 | self.BS_delta_plot.setData(self.S_range, self.model_delta)
440 |
441 | fig_delta.setTitle(
442 | " Hedging Strategy: Delta (at t = 15 days)")
443 | fig_delta.setLabels(
444 | left="Delta",
445 | bottom="Stock Price")
446 |
447 | fig_delta_text = pg.TextItem(
448 | html="" +
449 | "Black-Scholes Delta (Benchmark)
" +
450 | "" +
451 | "Deep-Hedging Delta
",
452 | anchor=(
453 | 0,
454 | 0),
455 | angle=0,
456 | border='w',
457 | fill=(
458 | 255,
459 | 255,
460 | 200))
461 | fig_delta_text.setPos(self.S_range.min(), self.model_delta.max())
462 |
463 | fig_delta.addItem(self.BS_delta_plot)
464 | fig_delta.addItem(fig_delta_text)
465 |
466 | return fig_delta
467 |
468 | # Draw loss plot (PlotWidget) - Black-Scholes vs Deep Hedging.
469 |
470 | def Loss_Plot_Widget(self):
471 | fig_loss = pg.PlotWidget()
472 |
473 | self.DH_loss_plot = pg.PlotDataItem(
474 | pen=pg.mkPen(
475 | color="b", width=6), symbolBrush=(
476 | 0, 0, 255), symbolPen='y', symbol='+', symbolSize=8)
477 | fig_loss.addItem(self.DH_loss_plot)
478 |
479 | # Add a line for the Black-Scholes price.
480 | fig_loss.addLine(y=self.loss_BS, pen=pg.mkPen(color="r", width=1.5))
481 |
482 | self.BS_loss_html = ("" +
483 | "" +
484 | "Black-Scholes Loss (Benchmark)
" +
485 | "{:0.3f}
").format(
487 | self.loss_BS)
488 | self.BS_loss_textItem = pg.TextItem(
489 | html=self.BS_loss_html, anchor=(
490 | 1, 1), angle=0, border='w', fill=(
491 | 255, 255, 200))
492 |
493 | if self.flag_target:
494 | self.DH_target_loss_html = ("" +
495 | "Deep-Hedging Loss " +
498 | "(Target) " +
499 | "
{:0.3f}"
502 | "
").format(self.target_loss)
503 | self.DH_target_loss_textItem = pg.TextItem(
504 | html=self.DH_target_loss_html, anchor=(
505 | 1, 1), angle=0, border='w', fill=(
506 | 255, 255, 200))
507 |
508 | # Label the graph.
509 | fig_loss.setTitle(
510 | " Loss Function (Option Price) ")
511 | fig_loss.setLabels(
512 | left="Loss Value",
513 | bottom="Loss Function (Option Price) " +
514 | "- Number of Epochs")
515 |
516 | # Set appropriate xRange and yRange.
517 | fig_loss.setRange(xRange=(0, self.epochs))
518 |
519 | # For the presentation...
520 | if self.flag_target:
521 | fig_loss.addLine(
522 | y=self.target_loss, pen=pg.mkPen(
523 | color=self.target_color, width=1.5))
524 |
525 | return fig_loss
526 |
527 | # Update Plots - Black-Scholes vs Deep Hedging.
528 | def Update_Plots_Widget(
529 | self,
530 | PnL_DH=None,
531 | DH_delta=None,
532 | DH_bins=None,
533 | loss=None,
534 | num_epoch=None,
535 | num_batch=None,
536 | flag_last_batch_in_epoch=None):
537 |
538 | self.Update_PnL_Histogram(
539 | PnL_DH,
540 | DH_delta,
541 | DH_bins,
542 | loss,
543 | num_epoch,
544 | num_batch,
545 | flag_last_batch_in_epoch)
546 |
547 | self.Update_Delta_Plot(
548 | PnL_DH,
549 | DH_delta,
550 | DH_bins,
551 | loss,
552 | num_epoch,
553 | num_batch,
554 | flag_last_batch_in_epoch)
555 |
556 | self.Update_Loss_Plot(
557 | PnL_DH,
558 | DH_delta,
559 | DH_bins,
560 | loss,
561 | num_epoch,
562 | num_batch,
563 | flag_last_batch_in_epoch)
564 |
565 | self.Thread_RunDH.Figure_IsUpdated = True
566 |
567 | if num_epoch == self.epochs and \
568 | flag_last_batch_in_epoch is True and \
569 | self.epsilon > 0.0:
570 | np.save("../data/target_PnL_" + str(self.epsilon), PnL_DH)
571 |
572 | def Update_Loss_Plot(
573 | self,
574 | PnL_DH=None,
575 | DH_delta=None,
576 | DH_bins=None,
577 | loss=None,
578 | num_epoch=None,
579 | num_batch=None,
580 | flag_last_batch_in_epoch=None):
581 |
582 | DH_shift = 0.6
583 |
584 | # Get the latest viewRange
585 | yMin_View, yMax_View = self.fig_loss.viewRange()[1]
586 |
587 | # Update text position for Black-Scholes
588 | self.BS_loss_textItem.setPos(
589 | self.epochs * 0.8, self.loss_BS +
590 | (yMax_View - self.loss_BS) * 0.005)
591 |
592 | if self.flag_target:
593 | self.DH_target_loss_textItem.setPos(
594 | self.epochs * 0.6, self.target_loss +
595 | (yMax_View - self.target_loss) * 0.005)
596 |
597 | # Update text for Deep-Hedging.
598 | DH_loss_text_title = "Deep-Hedging Loss
"
600 | DH_loss_text_step = " Epoch: {} " + \
601 | "Batch: {}
"
602 | DH_loss_text_loss = "{:0.3f}
"
604 |
605 | DH_loss_text_str = (
606 | DH_loss_text_title +
607 | DH_loss_text_step +
608 | DH_loss_text_loss).format(
609 | int(num_epoch),
610 | int(num_batch),
611 | loss)
612 |
613 | if num_epoch == 1 and num_batch == 1:
614 | self.fig_loss.addItem(self.BS_loss_textItem)
615 |
616 | if self.flag_target:
617 | self.fig_loss.addItem(self.DH_target_loss_textItem)
618 |
619 | # Setup the textbox for the deep-hedging loss.
620 | self.DH_loss_textItem = pg.TextItem(
621 | html=DH_loss_text_str, anchor=(
622 | 0, 0), angle=0, border='w', fill=(
623 | 255, 255, 200))
624 | self.DH_loss_textItem.setPos((num_epoch - 1) + DH_shift, loss)
625 | self.fig_loss.addItem(self.DH_loss_textItem)
626 |
627 | self.fig_loss.enableAutoRange()
628 |
629 | # Mandatory pause to explain the demo. Remember to modify the
630 | # algo thread as well if one wants to remove the feature.
631 | # This part takes care the pause button.
632 | self.Pause()
633 | else:
634 | self.DH_loss_textItem.setHtml(DH_loss_text_str)
635 | if flag_last_batch_in_epoch:
636 | self.DH_loss_textItem.setPos(num_epoch + DH_shift, loss)
637 | if num_epoch == 1:
638 | # Establish the data for the out-of-sample loss at the end
639 | # of the first epoch.
640 | self.oos_loss_record = np.array([num_epoch, loss], ndmin=2)
641 | else:
642 | # Keep adding data at the end of each epoch.
643 | self.oos_loss_record = np.vstack(
644 | [self.oos_loss_record, np.array([num_epoch, loss])])
645 |
646 | self.DH_loss_plot.setData(self.oos_loss_record)
647 |
648 | # Move the Black-Scholes textbox to the left to avoid collision of the
649 | # deep-hedging textbox.
650 | if num_epoch > self.epochs * 0.5:
651 | if self.epsilon == 0:
652 | anchor = (0, 0)
653 | elif self.epsilon > 0:
654 | anchor = (0, 1)
655 |
656 | self.fig_loss.removeItem(self.BS_loss_textItem)
657 | self.BS_loss_textItem = pg.TextItem(
658 | html=self.BS_loss_html,
659 | anchor=anchor,
660 | angle=0,
661 | border='w',
662 | fill=(
663 | 255,
664 | 255,
665 | 200))
666 | self.BS_loss_textItem.setPos(
667 | 0, self.loss_BS + (yMax_View - self.loss_BS) * 0.005)
668 | self.fig_loss.addItem(self.BS_loss_textItem)
669 |
670 | def Update_PnL_Histogram(
671 | self,
672 | PnL_DH=None,
673 | DH_delta=None,
674 | DH_bins=None,
675 | loss=None,
676 | num_epoch=None,
677 | num_batch=None,
678 | flag_last_batch_in_epoch=None):
679 | if num_epoch == 1 and num_batch == 1:
680 | # Update PnL Histogram
681 | self.DH_hist = pg.BarGraphItem(x=self.bin_edges[:-2] + self.width,
682 | height=DH_bins,
683 | width=self.width,
684 | brush='b',
685 | name="Blue - Deep Hedging",
686 | antialias=False)
687 | self.fig_PnL.addItem(self.DH_hist)
688 | else:
689 | # Update PnL Histograms
690 | self.DH_hist.setOpts(height=DH_bins)
691 |
692 | def Update_Delta_Plot(
693 | self,
694 | PnL_DH=None,
695 | DH_delta=None,
696 | DH_bins=None,
697 | loss=None,
698 | num_epoch=None,
699 | num_batch=None,
700 | flag_last_batch_in_epoch=None):
701 | if num_epoch == 1 and num_batch == 1:
702 | # Update the Delta plot
703 | self.DH_delta_plot = pg.PlotDataItem(
704 | symbolBrush=(
705 | 0,
706 | 0,
707 | 255),
708 | symbolPen='b',
709 | symbol='+',
710 | symbolSize=10,
711 | name="Deep Hedging")
712 | self.DH_delta_plot.setData(self.S_range, DH_delta)
713 | self.fig_delta.addItem(self.DH_delta_plot)
714 | else:
715 | # Update the Delta plot
716 | self.DH_delta_plot.setData(self.S_range, DH_delta)
717 |
718 | def simulate_stock_prices(self):
719 | # Total obs = Training + Testing
720 | self.nobs = int(self.Ktrain * (1 + self.Ktest_ratio))
721 |
722 | # Length of one time-step (as fraction of a year).
723 | self.dt = day_count.yearFraction(
724 | calculation_date, calculation_date + 1)
725 | self.maturity = self.N * self.dt # Maturities (in the unit of a year)
726 |
727 | self.stochastic_process = BlackScholesProcess(
728 | s0=self.S0,
729 | sigma=self.sigma,
730 | risk_free=self.risk_free,
731 | dividend=self.dividend,
732 | day_count=day_count)
733 |
734 | print("\nRun Monte-Carlo Simulations for the Stock Price Process.\n")
735 | return self.stochastic_process.gen_path(
736 | self.maturity, self.N, self.nobs)
737 | print("\n")
738 |
739 | def assemble_data(self):
740 | self.payoff_T = self.payoff_func(
741 | self.S[:, -1]) # Payoff of the call option
742 |
743 | self.trade_set = np.stack((self.S), axis=1) # Trading set
744 |
745 | if information_set == "S":
746 | self.infoset = np.stack((self.S), axis=1) # Information set
747 | elif information_set == "log_S":
748 | self.infoset = np.stack((np.log(self.S)), axis=1)
749 | elif information_set == "normalized_log_S":
750 | self.infoset = np.stack((np.log(self.S / self.S0)), axis=1)
751 |
752 | # Structure of xtrain:
753 | # 1) Trade set: [S]
754 | # 2) Information set: [S]
755 | # 3) payoff (dim = 1)
756 | self.x_all = []
757 | for i in range(self.N + 1):
758 | self.x_all += [self.trade_set[i, :, None]]
759 | if i != self.N:
760 | self.x_all += [self.infoset[i, :, None]]
761 | self.x_all += [self.payoff_T[:, None]]
762 |
763 | # Split the entire sample into a training sample and a testing sample.
764 | self.test_size = int(self.Ktrain * self.Ktest_ratio)
765 | [self.xtrain, self.xtest] = train_test_split(
766 | self.x_all, test_size=self.test_size)
767 | [self.S_train, self.S_test] = train_test_split(
768 | [self.S], test_size=self.test_size)
769 | [self.option_payoff_train, self.option_payoff_test] = \
770 | train_test_split([self.x_all[-1]], test_size=self.test_size)
771 |
772 | # Convert the training sample into tf.Data format (same as xtrain).
773 | training_dataset = tf.data.Dataset.from_tensor_slices(
774 | tuple(self.xtrain))
775 | return training_dataset.cache()
776 |
777 | def get_Black_Scholes_Prices(self):
778 | # Obtain Black-Scholes price, delta, and PnL
779 | call = EuropeanCall()
780 | price_BS = call.get_BS_price(
781 | S=self.S_test[0],
782 | sigma=self.sigma,
783 | risk_free=self.risk_free,
784 | dividend=self.dividend,
785 | K=self.strike,
786 | exercise_date=self.maturity_date,
787 | calculation_date=calculation_date,
788 | day_count=day_count,
789 | dt=self.dt)
790 | delta_BS = call.get_BS_delta(
791 | S=self.S_test[0],
792 | sigma=self.sigma,
793 | risk_free=self.risk_free,
794 | dividend=self.dividend,
795 | K=self.strike,
796 | exercise_date=self.maturity_date,
797 | calculation_date=calculation_date,
798 | day_count=day_count,
799 | dt=self.dt)
800 | PnL_BS = call.get_BS_PnL(S=self.S_test[0],
801 | payoff=self.payoff_func(self.S_test[0][:,
802 | -1]),
803 | delta=delta_BS,
804 | dt=self.dt,
805 | risk_free=self.risk_free,
806 | final_period_cost=final_period_cost,
807 | epsilon=self.epsilon)
808 | return price_BS, delta_BS, PnL_BS
809 |
--------------------------------------------------------------------------------
/presentation/readme.txt:
--------------------------------------------------------------------------------
1 | Instruction for Running on the Grid:
2 |
3 | Run:
4 |
5 | bsub -Is -q "python" numactl --cpunodebind=0 zsh python3
6 |
7 | Then, within Python:
8 |
9 | exec(open("main.py").read())
10 |
--------------------------------------------------------------------------------
/pyqt5/default_params.py:
--------------------------------------------------------------------------------
1 | # Define the initial parameters for the deep hedging demo
2 | def DeepHedgingParams():
3 | params = [
4 | {'name': 'European Call Option', 'type': 'group', 'children': [
5 | {'name': 'S0', 'type': 'int', 'value': 100.0},
6 | {'name': 'Strike', 'type': 'float', 'value': 100.0},
7 | {'name': 'Implied Volatility', 'type': 'float', 'value': 0.2},
8 | {'name': 'Risk-Free Rate', 'type': 'float', 'value': 0.0},
9 | {'name': 'Dividend Yield', 'type': 'float', 'value': 0.0},
10 | {'name': 'Maturity (in days)', 'type': 'int', 'value': 30},
11 | {'name': 'Proportional Transaction Cost', 'type': 'group', 'children': [
12 | {'name': 'Cost', 'type': 'float', 'value': 0.0},
13 | ]},
14 | ]},
15 | {'name': 'Monte-Carlo Simulation', 'type': 'group', 'children': [
16 | {'name': 'Sample Size', 'type': 'group', 'children': [
17 | {'name': 'Training', 'type': 'int', 'value': 1*(10**5)},
18 | {'name': 'Testing (as fraction of Training)', 'type': 'float', 'value': 0.2}
19 | ]},
20 | ]},
21 | {'name': 'Deep Hedging Strategy', 'type': 'group', 'children': [
22 | {'name': 'Loss Function', 'type': 'group', 'children': [
23 | {'name': 'Loss Type', 'type': 'list', 'values': {"Entropy": "Entropy", "CVaR": "CVaR"}, "default": "Entropy"},
24 | {'name': 'Risk Aversion', 'type': 'float', 'value': 1.0}
25 | ]},
26 | {'name': 'Network Structure', 'type': 'group', 'children': [
27 | {'name': 'Network Type', 'type': 'list', 'values': {"Simple": "simple", "Recurrent": "recurrent"}, "default": "simple"},
28 | {'name': 'Number of Hidden Layers', 'type': 'int', 'value': 1},
29 | {'name': 'Number of Neurons', 'type': 'int', 'value': 15},
30 | ]},
31 | {'name': 'Learning Parameters', 'type': 'group', 'children': [
32 | {'name': 'Learning Rate', 'type': 'float', 'value': 5e-3},
33 | {'name': 'Mini-Batch Size', 'type': 'int', 'value': 256},
34 | {'name': 'Number of Epochs', 'type': 'int', 'value': 50},
35 | ]},
36 | ]},
37 | ]
38 | return params
39 |
--------------------------------------------------------------------------------
/pyqt5/dh_worker.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | # Add the parent directory to the search paths to import the libraries.
5 | dir_path = os.path.dirname(os.path.realpath(__file__))
6 | sys.path.insert(0, "/".join([dir_path, ".."]))
7 |
8 | import time
9 |
10 | import numpy as np
11 |
12 | import tensorflow as tf
13 | from tensorflow.keras.optimizers import Adam
14 |
15 | from pyqtgraph.Qt import QtCore
16 |
17 | from loss_metrics import Entropy
18 |
19 |
20 | # Reducing learning rate
21 | reduce_lr_param = {"patience": 2, "min_delta": 1e-3, "factor": 0.5}
22 |
23 | # Number of bins to plot for the PnL histograms.
24 | num_bins = 30
25 |
26 |
27 | # Put the deep-hedging algo in a separate thread than the plotting thread to
28 | # improve performance.
29 | class DHworker(QtCore.QThread):
30 | DH_outputs = QtCore.pyqtSignal(np.ndarray,
31 | np.ndarray,
32 | np.ndarray,
33 | np.float32,
34 | float,
35 | float,
36 | bool)
37 |
38 | def __init__(self):
39 | QtCore.QThread.__init__(self)
40 |
41 | def __del__(self):
42 | self.wait()
43 |
44 | def run_deep_hedge_algo(self,
45 | training_dataset=None,
46 | epochs=None,
47 | Ktrain=None,
48 | batch_size=None,
49 | model=None,
50 | submodel=None,
51 | strategy_type=None,
52 | loss_param=None,
53 | learning_rate=None,
54 | xtest=None,
55 | xtrain=None,
56 | initial_price_BS=None,
57 | width=None,
58 | I_range=None,
59 | x_range=None):
60 | self.training_dataset = training_dataset
61 | self.Ktrain = Ktrain
62 | self.batch_size = batch_size
63 | self.model = model
64 | self.submodel = submodel
65 | self.loss_param = loss_param
66 | self.initial_price_BS = initial_price_BS
67 | self.width = width
68 | self.epochs = epochs
69 | self.xtest = xtest
70 | self.xtrain = xtrain
71 | self.I_range = I_range
72 | self.x_range = x_range
73 | self.strategy_type = strategy_type
74 | self.learning_rate = learning_rate
75 |
76 | self.start()
77 |
78 | def pause(self):
79 | self._pause = True
80 |
81 | def cont(self):
82 | self._pause = False
83 |
84 | def stop(self):
85 | self._exit = True
86 | self.exit()
87 |
88 | def is_running(self):
89 | if self._pause or self._exit:
90 | return False
91 | else:
92 | return True
93 |
94 | def Reduce_Learning_Rate(self, num_epoch, loss):
95 | # Extract in-sample loss from the previous epoch. Comparison starts in
96 | # epoch 2 and the index for epoch 1 is 0 -> -2
97 | min_loss = self.loss_record[:, 1].min()
98 | if min_loss - loss < reduce_lr_param["min_delta"]:
99 | self.reduce_lr_counter += 1
100 |
101 | if self.reduce_lr_counter > reduce_lr_param["patience"]:
102 | self.learning_rate = self.learning_rate * reduce_lr_param["factor"]
103 | self.optimizer.learning_rate = self.learning_rate
104 | print(
105 | "The learning rate is reduced to {}.".format(
106 | self.learning_rate))
107 | self.reduce_lr_counter = 0
108 |
109 | def run(self):
110 | # Initialize pause and stop buttons.
111 | self._exit = False
112 | self._pause = False
113 |
114 | # Variables to control skipped frames. If the DH algo output much
115 | # faster than the graphic output, the plots can be jammed.
116 | self.Figure_IsUpdated = True
117 |
118 | self.reduce_lr_counter = 0
119 | self.early_stopping_counter = 0
120 |
121 | certainty_equiv = tf.Variable(0.0, name="certainty_equiv")
122 |
123 | # Accelerator Function.
124 | model_func = tf.function(self.model)
125 | submodel_func = tf.function(self.submodel)
126 |
127 | self.optimizer = Adam(learning_rate=self.learning_rate)
128 |
129 | oos_loss = None
130 | PnL_DH = None
131 | DH_delta = None
132 | DH_bins = None
133 | num_batch = None
134 |
135 | num_epoch = 0
136 | while num_epoch <= self.epochs:
137 | # Exit event loop if the exit flag is set to True.
138 | if self._exit:
139 | mini_batch_iter = None
140 | self._exit = False
141 | self._pause = False
142 | break
143 |
144 | if not self._pause:
145 | try:
146 | mini_batch = mini_batch_iter.next()
147 | except BaseException:
148 | # Reduce learning rates and Early Stopping are based on
149 | # in-sample losses calculated once per epoch.
150 | in_sample_wealth = model_func(self.xtrain)
151 | in_sample_loss = Entropy(
152 | in_sample_wealth, certainty_equiv, self.loss_param)
153 |
154 | if num_epoch >= 1:
155 | print(("The deep-hedging price is {:0.4f} after " +
156 | "{} epoch.").format(oos_loss, num_epoch))
157 |
158 | # Programming hack. The deep-hedging algo computes
159 | # faster than the computer can plot, so there could
160 | # be missing frames, i.e. there is no guarantee
161 | # that every batch is plotted. Here, I force a
162 | # signal to be emitted at the end of an epoch.
163 | time.sleep(1)
164 |
165 | self.DH_outputs.emit(
166 | PnL_DH,
167 | DH_delta,
168 | DH_bins,
169 | oos_loss.numpy().squeeze(),
170 | num_epoch,
171 | num_batch,
172 | True)
173 |
174 | # This is needed to prevent the output signals from
175 | # emitting faster than the system can plot a graph.
176 | #
177 | # The performance is much better than emitting at fixed
178 | # time intervals.
179 | self.Figure_IsUpdated = False
180 |
181 | if num_epoch == 1:
182 | self.loss_record = np.array(
183 | [num_epoch, in_sample_loss], ndmin=2)
184 | elif num_epoch > 1:
185 | self.Reduce_Learning_Rate(num_epoch, in_sample_loss)
186 | self.loss_record = np.vstack(
187 | [self.loss_record,
188 | np.array([num_epoch, in_sample_loss])])
189 |
190 | mini_batch_iter = self.training_dataset.shuffle(
191 | self.Ktrain).batch(self.batch_size).__iter__()
192 | mini_batch = mini_batch_iter.next()
193 |
194 | num_batch = 0
195 | num_epoch += 1
196 |
197 | num_batch += 1
198 |
199 | # Record gradient
200 | with tf.GradientTape() as tape:
201 | wealth = model_func(mini_batch)
202 | loss = Entropy(wealth, certainty_equiv, self.loss_param)
203 |
204 | oos_wealth = model_func(self.xtest)
205 | PnL_DH = oos_wealth.numpy().squeeze() # Out-of-sample
206 |
207 | submodel_delta_range = np.expand_dims(self.I_range, axis=1)
208 | if self.strategy_type == "simple":
209 | submodel_inputs = submodel_delta_range
210 | elif self.strategy_type == "recurrent":
211 | # Assume previous delta is ATM.
212 | submodel_inputs = [
213 | submodel_delta_range,
214 | np.ones_like(submodel_delta_range) * 0.5]
215 | DH_delta = submodel_func(submodel_inputs).numpy().squeeze()
216 | DH_bins, _ = np.histogram(
217 | PnL_DH + self.initial_price_BS,
218 | bins=num_bins,
219 | range=self.x_range)
220 |
221 | # Forward and backward passes
222 | grads = tape.gradient(loss, self.model.trainable_weights)
223 | self.optimizer.apply_gradients(
224 | zip(grads, self.model.trainable_weights))
225 |
226 | # Compute Out-of-Sample Loss
227 | oos_loss = Entropy(
228 | oos_wealth, certainty_equiv, self.loss_param)
229 |
230 | if self.Figure_IsUpdated:
231 | self.DH_outputs.emit(
232 | PnL_DH,
233 | DH_delta,
234 | DH_bins,
235 | oos_loss.numpy().squeeze(),
236 | num_epoch,
237 | num_batch,
238 | False)
239 |
240 | # This is needed to prevent the output signals from emitting
241 | # faster than the system can plot a graph.
242 | #
243 | # The performance is much better than emitting at fixed time
244 | # intervals.
245 | self.Figure_IsUpdated = False
246 |
247 | # Mandatory pause for the first iteration to explain demo.
248 | if num_epoch == 1 and num_batch == 1:
249 | self.pause()
250 | else:
251 | time.sleep(1)
252 |
--------------------------------------------------------------------------------
/pyqt5/main.py:
--------------------------------------------------------------------------------
1 | # Author: Yu-Man Tam
2 | # Email: yuman.tam@gmail.com
3 | #
4 | # Last updated: 5/22/2020
5 | #
6 | # Reference: Deep Hedging (2019, Quantitative Finance) by Buehler et al.
7 | # https://www.tandfonline.com/doi/abs/10.1080/14697688.2019.1571683
8 |
9 | import sys
10 | import os
11 |
12 | import tensorflow as tf
13 |
14 | # Add the parent directory to the search paths to import the libraries.
15 | dir_path = os.path.dirname(os.path.realpath(__file__))
16 | sys.path.insert(0, "/".join([dir_path, ".."]))
17 |
18 | from pyqtgraph.Qt import QtWidgets
19 | from main_window import MainWindow
20 |
21 | # Tensorflow settings
22 | tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
23 | tf.autograph.set_verbosity(0)
24 |
25 | if __name__ == '__main__':
26 | app = QtWidgets.QApplication(sys.argv)
27 | main = MainWindow()
28 | main.show()
29 | app.exec_()
30 |
--------------------------------------------------------------------------------
/pyqt5/main_window.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | # Add the parent directory to the search paths to import the libraries.
5 | dir_path = os.path.dirname(os.path.realpath(__file__))
6 | sys.path.insert(0, "/".join([dir_path, ".."]))
7 |
8 | import QuantLib as ql
9 | import numpy as np
10 | import tensorflow as tf
11 | import pyqtgraph as pg
12 |
13 | from tensorflow.keras.models import Model
14 | from tensorflow.keras.layers import Input, Concatenate
15 | from pyqtgraph.Qt import QtWidgets, QtGui, QtCore
16 | from pyqtgraph.parametertree import ParameterTree, Parameter
17 | from scipy.stats import norm
18 |
19 | from dh_worker import DHworker
20 | from default_params import DeepHedgingParams
21 | from loss_metrics import Entropy
22 | from deep_hedging import Deep_Hedging_Model
23 | from stochastic_processes import BlackScholesProcess
24 | from instruments import EuropeanCall
25 | from utilities import train_test_split
26 |
27 | # Specify the day (from today) for the delta plot.
28 | delta_plot_day = 15
29 |
30 | # European call option (short).
31 | calculation_date = ql.Date.todaysDate()
32 |
33 | # Day convention.
34 | day_count = ql.Actual365Fixed() # Actual/Actual (ISDA)
35 |
36 | # Information set (in string)
37 | # Choose from: S, log_S, normalized_log_S (by S0)
38 | information_set = "normalized_log_S"
39 |
40 | # Loss function
41 | # loss_type = "CVaR" (Expected Shortfall) -> loss_param = alpha
42 | # loss_type = "Entropy" -> loss_param = lambda
43 | loss_type = "Entropy"
44 |
45 | # Other NN parameters
46 | use_batch_norm = False
47 | kernel_initializer = "he_uniform"
48 |
49 | activation_dense = "leaky_relu"
50 | activation_output = "sigmoid"
51 | final_period_cost = False
52 |
53 | # Reducing learning rate
54 | reduce_lr_param = {"patience": 2, "min_delta": 1e-3, "factor": 0.5}
55 |
56 | # Number of bins to plot for the PnL histograms.
57 | num_bins = 30
58 |
59 |
60 | class MainWindow(QtWidgets.QMainWindow):
61 | def __init__(self):
62 | # Inheritance from the QMainWindow class
63 | # Reference: https://doc.qt.io/qt-5/qmainwindow.html
64 | super().__init__()
65 | self.days_from_today = delta_plot_day
66 | self.Thread_RunDH = DHworker()
67 |
68 | # The order of code is important here: Make sure the
69 | # emitted signals are connected before actually running
70 | # the Worker.
71 | self.Thread_RunDH.DH_outputs["PyQt_PyObject",
72 | "PyQt_PyObject",
73 | "PyQt_PyObject",
74 | "PyQt_PyObject",
75 | "double",
76 | "double",
77 | "bool"].connect(self.Update_Plots_Widget)
78 |
79 | # Define a top-level widget to hold everything
80 | self.w = QtGui.QWidget()
81 |
82 | # Create a grid layout to manage the widgets size and position
83 | self.layout = QtGui.QGridLayout()
84 | self.w.setLayout(self.layout)
85 |
86 | self.setCentralWidget(self.w)
87 |
88 | # Add the parameter menu.
89 | self.tree_height = 5 # Must be Odd number.
90 |
91 | self.tree = self.Deep_Hedging_Parameter_Widget()
92 |
93 | self.layout.addWidget(self.tree,
94 | 0, 0, self.tree_height, 2) # upper-left
95 |
96 | self.tree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
97 | self.tree.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
98 | self.tree.setMinimumSize(350, 650)
99 |
100 | # Add a run button
101 | self.run_btn = QtGui.QPushButton('Run')
102 | self.layout.addWidget(self.run_btn,
103 | self.tree_height + 1,
104 | 0, 1, 1) # button goes in upper-left
105 |
106 | # Add a pause button
107 | self.pause_btn = QtGui.QPushButton('Pause')
108 | self.layout.addWidget(self.pause_btn,
109 | self.tree_height + 1,
110 | 1, 1, 1) # button goes in upper-left
111 |
112 | # Run the deep hedging algo in a separate thread when the run
113 | # button is clicked.
114 | self.run_btn.clicked.connect(self.RunButton)
115 |
116 | # Pause button.
117 | self.pause_btn.clicked.connect(self.Pause)
118 |
119 | def Deep_Hedging_Parameter_Widget(self):
120 | tree = ParameterTree()
121 |
122 | # Create tree of Parameter objects
123 | self.params = Parameter.create(name='params', type='group',
124 | children=DeepHedgingParams())
125 | tree.setParameters(self.params, showTop=False)
126 |
127 | return tree
128 |
129 | # Define the event when the "Run" button is clicked.
130 | def RunButton(self):
131 | if self.run_btn.text() == "Stop":
132 | self.Thread_RunDH.stop()
133 | if self.pause_btn.text() == "Continue":
134 | self.pause_btn.setText("Pause")
135 | self.run_btn.setText("Run")
136 | elif self.run_btn.text() == "Run":
137 | self.run_btn.setText("Stop")
138 |
139 | # Set parameters
140 | self.Ktrain = self.params.param("Monte-Carlo Simulation",
141 | 'Sample Size', "Training").value()
142 | self.Ktest_ratio = \
143 | self.params.param("Monte-Carlo Simulation",
144 | 'Sample Size',
145 | "Testing (as fraction of Training)").value()
146 | self.N = self.params.param("European Call Option",
147 | "Maturity (in days)").value()
148 | self.S0 = self.params.param("European Call Option", "S0").value()
149 | self.strike = self.params.param("European Call Option",
150 | "Strike").value()
151 | self.sigma = self.params.param("European Call Option",
152 | "Implied Volatility").value()
153 | self.risk_free = self.params.param("European Call Option",
154 | "Risk-Free Rate").value()
155 | self.dividend = self.params.param("European Call Option",
156 | "Dividend Yield").value()
157 |
158 | self.loss_param = self.params.param("Deep Hedging Strategy",
159 | 'Loss Function',
160 | "Risk Aversion").value()
161 | self.epsilon = self.params.param("European Call Option",
162 | "Proportional Transaction Cost",
163 | "Cost").value()
164 | self.d = self.params.param("Deep Hedging Strategy",
165 | "Network Structure",
166 | "Number of Hidden Layers").value()
167 | self.m = self.params.param("Deep Hedging Strategy",
168 | "Network Structure",
169 | "Number of Neurons").value()
170 | self.strategy_type = self.params.param("Deep Hedging Strategy",
171 | "Network Structure",
172 | "Network Type").value()
173 | self.lr = self.params.param("Deep Hedging Strategy",
174 | "Learning Parameters",
175 | "Learning Rate").value()
176 | self.batch_size = self.params.param("Deep Hedging Strategy",
177 | "Learning Parameters",
178 | "Mini-Batch Size").value()
179 | self.epochs = self.params.param("Deep Hedging Strategy",
180 | "Learning Parameters",
181 | "Number of Epochs").value()
182 |
183 | self.maturity_date = calculation_date + self.N
184 | self.payoff_func = lambda x: -np.maximum(x - self.strike, 0.0)
185 |
186 | # Simulate the stock price process.
187 | self.S = self.simulate_stock_prices()
188 |
189 | # Assemble the dataset for training and testing.
190 | # Structure of data:
191 | # 1) Trade set: [S]
192 | # 2) Information set: [S]
193 | # 3) payoff (dim = 1)
194 | self.training_dataset = self.assemble_data()
195 |
196 | # Compute Black-Scholes prices for benchmarking.
197 | self.price_BS, self.delta_BS, self.PnL_BS = \
198 | self.get_Black_Scholes_Prices()
199 |
200 | # Compute the loss value for Black-Scholes PnL
201 | self.loss_BS = Entropy(
202 | self.PnL_BS,
203 | tf.Variable(0.0),
204 | self.loss_param).numpy()
205 |
206 | # Define model and sub-models
207 | self.model = self.Define_DH_model()
208 | self.submodel = self.Define_DH_Delta_Strategy_Model()
209 |
210 | plot_height_split = (self.tree_height + 1) / 2
211 |
212 | # For the presentation...
213 | self.flag_target = False
214 | if self.epsilon > 0:
215 | try:
216 | self.target_color = (0, 155, 0)
217 | self.target_PnL = np.load(
218 | "../data/target_PnL_" + str(self.epsilon) + ".npy")
219 | self.target_loss = Entropy(
220 | self.target_PnL,
221 | tf.Variable(0.0),
222 | self.loss_param).numpy()
223 | self.flag_target = True
224 | except BaseException:
225 | print("No saved file.")
226 | pass
227 | else:
228 | try:
229 | self.fig_loss.removeItem(self.DH_target_loss_textItem)
230 | except BaseException:
231 | pass
232 |
233 | # Add the PnL histogram (PlotWidget) - Black-Scholes vs Deep
234 | # Hedging.
235 | self.fig_PnL = self.PnL_Hist_Widget()
236 | self.layout.addWidget(self.fig_PnL, 0, 3, plot_height_split, 1)
237 | self.fig_PnL.setMinimumWidth(600)
238 |
239 | # Add the Delta line plot (PlotWidget) - Black-Scholes vs Deep
240 | # Hedging.
241 | self.fig_delta = self.Delta_Plot_Widget()
242 | self.layout.addWidget(self.fig_delta, 0, 4, plot_height_split, 1)
243 | self.fig_delta.setMinimumWidth(600)
244 |
245 | # Add the loss plot (PlotWidget) - Black-Scholes vs Deep Hedging.
246 | self.fig_loss = self.Loss_Plot_Widget()
247 | self.layout.addWidget(
248 | self.fig_loss,
249 | plot_height_split,
250 | 3,
251 | plot_height_split + 1,
252 | 2)
253 | self.fig_loss.setMinimumWidth(1200)
254 |
255 | # Run the deep hedging algo in a separate thread.
256 | self.Thread_RunDH.run_deep_hedge_algo(
257 | training_dataset=self.training_dataset,
258 | epochs=self.epochs,
259 | Ktrain=self.Ktrain,
260 | batch_size=self.batch_size,
261 | model=self.model,
262 | submodel=self.submodel,
263 | strategy_type=self.strategy_type,
264 | loss_param=self.loss_param,
265 | learning_rate=self.lr,
266 | xtest=self.xtest,
267 | xtrain=self.xtrain,
268 | initial_price_BS=self.price_BS[0][0],
269 | width=self.width,
270 | I_range=self.I_range,
271 | x_range=self.x_range)
272 |
273 | # Define action when the Pause button is clicked.
274 |
275 | def Pause(self):
276 | if self.pause_btn.text() == "Pause":
277 | self.Thread_RunDH.pause()
278 | self.pause_btn.setText("Continue")
279 | elif self.pause_btn.text() == "Continue":
280 | self.Thread_RunDH.cont()
281 | self.pause_btn.setText("Pause")
282 |
283 | # Define deep hedging model
284 |
285 | def Define_DH_model(self):
286 | # Setup and compile the model
287 | model = Deep_Hedging_Model(
288 | N=self.N,
289 | d=self.d,
290 | m=self.m,
291 | risk_free=self.risk_free,
292 | dt=self.dt,
293 | strategy_type=self.strategy_type,
294 | epsilon=self.epsilon,
295 | use_batch_norm=use_batch_norm,
296 | kernel_initializer=kernel_initializer,
297 | activation_dense=activation_dense,
298 | activation_output=activation_output,
299 | final_period_cost=final_period_cost,
300 | delta_constraint=None)
301 | return model
302 |
303 | def Define_DH_Delta_Strategy_Model(self):
304 | if self.strategy_type == "simple":
305 | # Set up the sub-model that outputs the delta.
306 | submodel = \
307 | Model(self.model.get_layer("delta_" +
308 | str(self.days_from_today)).input,
309 | self.model.get_layer("delta_" +
310 | str(self.days_from_today)).output)
311 | elif self.strategy_type == "recurrent":
312 | # For "recurrent", the information set is price as well as the past
313 | # delta.
314 | inputs = [Input(1,), Input(1,)]
315 |
316 | intermediate_inputs = Concatenate()(inputs)
317 |
318 | outputs = self.model.get_layer(
319 | "delta_" + str(self.days_from_today))(intermediate_inputs)
320 |
321 | submodel = Model(inputs=inputs, outputs=outputs)
322 | return submodel
323 |
324 | # Draw PnL histogram (PlotWidget) - Black-Scholes vs Deep Hedging.
325 |
326 | def PnL_Hist_Widget(self):
327 | # Initialize the PnL Histogram Widget.
328 | fig_PnL = pg.PlotWidget()
329 |
330 | x_min = np.minimum(self.PnL_BS.min() + self.price_BS[0, 0], -3)
331 | x_max = np.maximum(self.PnL_BS.max() + self.price_BS[0, 0], 3)
332 |
333 | self.x_range = (x_min, x_max)
334 | self.BS_bins, self.bin_edges = np.histogram(
335 | self.PnL_BS + self.price_BS[0, 0],
336 | bins=num_bins,
337 | range=self.x_range)
338 | if self.flag_target:
339 | self.width = (self.bin_edges[1] - self.bin_edges[0]) / 3.0
340 | else:
341 | self.width = (self.bin_edges[1] - self.bin_edges[0]) / 2.0
342 |
343 | self.BS_hist = pg.BarGraphItem(x=self.bin_edges[:-2],
344 | height=self.BS_bins,
345 | width=self.width,
346 | brush='r',
347 | name="Red - Black-Scholes",
348 | antialias=False)
349 |
350 | fig_PnL.setTitle(
351 | "Profit and Loss (PnL) Histogram")
352 | fig_PnL.setLabels(
353 | left="Frequency",
354 | bottom="Profit and Loss (PnL) ")
355 |
356 | # Fix the problem that Y-axes keep moving when transactioni cost is
357 | # greater than zero.
358 | fig_PnL.setYRange(0, self.BS_bins.max() * 1.1)
359 |
360 | if self.flag_target:
361 | fig_PnL.setXRange(self.bin_edges.min(), 2)
362 | else:
363 | fig_PnL.setXRange(self.bin_edges.min(), 2)
364 |
365 | fig_PnL.addItem(self.BS_hist)
366 |
367 | if self.flag_target:
368 | self.DH_target_bins, _ = np.histogram(
369 | self.target_PnL + self.price_BS[0, 0],
370 | bins=num_bins,
371 | range=self.x_range)
372 | self.DH_target_hist = pg.BarGraphItem(
373 | x=self.bin_edges[
374 | :-2] + 2 * self.width,
375 | height=self.DH_target_bins,
376 | width=self.width,
377 | brush=self.target_color,
378 | name="Green - Deep-Hedging PnL (Target)",
379 | antialias=False)
380 | fig_PnL.addItem(self.DH_target_hist)
381 | PnL_html = ("" +
382 | "" +
383 | "Black-Scholes PnL (Benchmark)
" +
384 | "" +
385 | "Deep-Hedging PnL (Target)" +
386 | "
").format(str(self.target_color)) + \
387 | "" + \
388 | "Deep-Hedging PnL
"
389 | else:
390 | PnL_html = "" + \
391 | "" + \
392 | "Black-Scholes PnL (Benchmark)
" + \
393 | "" + \
394 | "Deep-Hedging PnL
"
395 |
396 | fig_PnL_text = pg.TextItem(
397 | html=PnL_html, anchor=(
398 | 0, 0), angle=0, border='w', fill=(
399 | 225, 225, 200))
400 |
401 | fig_PnL_text.setPos(self.bin_edges.min(), self.BS_bins.max() * 1.05)
402 | fig_PnL.addItem(fig_PnL_text)
403 |
404 | return fig_PnL
405 |
406 | # Draw Delta plot (PlotWidget) - Black-Scholes vs Deep Hedging.
407 | # Assume the PnL_Hist_Widget ran first, so we don't need to run the model
408 | # again.
409 |
410 | def Delta_Plot_Widget(self):
411 | self.tau = (self.N - self.days_from_today) * self.dt
412 |
413 | self.min_S = self.S_test[0][:, self.days_from_today].min()
414 | self.max_S = self.S_test[0][:, self.days_from_today].max()
415 | self.S_range = np.linspace(self.min_S, self.max_S, 51)
416 |
417 | # Attention: Need to transform it to be consistent with the information
418 | # set.
419 | if information_set == "S":
420 | self.I_range = self.S_range # Information set
421 | elif information_set == "log_S":
422 | self.I_range = np.log(self.S_range)
423 | elif information_set == "normalized_log_S":
424 | self.I_range = np.log(self.S_range / self.S0)
425 |
426 | # Compute Black-Scholes delta for S_range.
427 | # Reference: https://en.wikipedia.org/wiki/Greeks_(finance)
428 | self.d1 = (np.log(self.S_range) - np.log(self.strike) +
429 | (self.risk_free - self.dividend + (self.sigma**2) / 2) *
430 | self.tau) / (self.sigma * np.sqrt(self.tau))
431 |
432 | self.model_delta = norm.cdf(
433 | self.d1) * np.exp(-self.dividend * self.tau)
434 |
435 | fig_delta = pg.PlotWidget()
436 |
437 | self.BS_delta_plot = pg.PlotCurveItem(
438 | pen=pg.mkPen(color="r", width=2.5), name="Black-Scholes")
439 | self.BS_delta_plot.setData(self.S_range, self.model_delta)
440 |
441 | fig_delta.setTitle(
442 | " Hedging Strategy: Delta (at t = 15 days)")
443 | fig_delta.setLabels(
444 | left="Delta",
445 | bottom="Stock Price")
446 |
447 | fig_delta_text = pg.TextItem(
448 | html="" +
449 | "Black-Scholes Delta (Benchmark)
" +
450 | "" +
451 | "Deep-Hedging Delta
",
452 | anchor=(
453 | 0,
454 | 0),
455 | angle=0,
456 | border='w',
457 | fill=(
458 | 255,
459 | 255,
460 | 200))
461 | fig_delta_text.setPos(self.S_range.min(), self.model_delta.max())
462 |
463 | fig_delta.addItem(self.BS_delta_plot)
464 | fig_delta.addItem(fig_delta_text)
465 |
466 | return fig_delta
467 |
468 | # Draw loss plot (PlotWidget) - Black-Scholes vs Deep Hedging.
469 |
470 | def Loss_Plot_Widget(self):
471 | fig_loss = pg.PlotWidget()
472 |
473 | self.DH_loss_plot = pg.PlotDataItem(
474 | pen=pg.mkPen(
475 | color="b", width=6), symbolBrush=(
476 | 0, 0, 255), symbolPen='y', symbol='+', symbolSize=8)
477 | fig_loss.addItem(self.DH_loss_plot)
478 |
479 | # Add a line for the Black-Scholes price.
480 | fig_loss.addLine(y=self.loss_BS, pen=pg.mkPen(color="r", width=1.5))
481 |
482 | self.BS_loss_html = ("" +
483 | "" +
484 | "Black-Scholes Loss (Benchmark)
" +
485 | "{:0.3f}
").format(
487 | self.loss_BS)
488 | self.BS_loss_textItem = pg.TextItem(
489 | html=self.BS_loss_html, anchor=(
490 | 1, 1), angle=0, border='w', fill=(
491 | 255, 255, 200))
492 |
493 | if self.flag_target:
494 | self.DH_target_loss_html = ("" +
495 | "Deep-Hedging Loss " +
498 | "(Target) " +
499 | "
{:0.3f}"
502 | "
").format(self.target_loss)
503 | self.DH_target_loss_textItem = pg.TextItem(
504 | html=self.DH_target_loss_html, anchor=(
505 | 1, 1), angle=0, border='w', fill=(
506 | 255, 255, 200))
507 |
508 | # Label the graph.
509 | fig_loss.setTitle(
510 | " Loss Function (Option Price) ")
511 | fig_loss.setLabels(
512 | left="Loss Value",
513 | bottom="Loss Function (Option Price) " +
514 | "- Number of Epochs")
515 |
516 | # Set appropriate xRange and yRange.
517 | fig_loss.setRange(xRange=(0, self.epochs))
518 |
519 | # For the presentation...
520 | if self.flag_target:
521 | fig_loss.addLine(
522 | y=self.target_loss, pen=pg.mkPen(
523 | color=self.target_color, width=1.5))
524 |
525 | return fig_loss
526 |
527 | # Update Plots - Black-Scholes vs Deep Hedging.
528 | def Update_Plots_Widget(
529 | self,
530 | PnL_DH=None,
531 | DH_delta=None,
532 | DH_bins=None,
533 | loss=None,
534 | num_epoch=None,
535 | num_batch=None,
536 | flag_last_batch_in_epoch=None):
537 |
538 | self.Update_PnL_Histogram(
539 | PnL_DH,
540 | DH_delta,
541 | DH_bins,
542 | loss,
543 | num_epoch,
544 | num_batch,
545 | flag_last_batch_in_epoch)
546 |
547 | self.Update_Delta_Plot(
548 | PnL_DH,
549 | DH_delta,
550 | DH_bins,
551 | loss,
552 | num_epoch,
553 | num_batch,
554 | flag_last_batch_in_epoch)
555 |
556 | self.Update_Loss_Plot(
557 | PnL_DH,
558 | DH_delta,
559 | DH_bins,
560 | loss,
561 | num_epoch,
562 | num_batch,
563 | flag_last_batch_in_epoch)
564 |
565 | self.Thread_RunDH.Figure_IsUpdated = True
566 |
567 | if num_epoch == self.epochs and \
568 | flag_last_batch_in_epoch is True and \
569 | self.epsilon > 0.0:
570 | np.save("../data/target_PnL_" + str(self.epsilon), PnL_DH)
571 |
572 | def Update_Loss_Plot(
573 | self,
574 | PnL_DH=None,
575 | DH_delta=None,
576 | DH_bins=None,
577 | loss=None,
578 | num_epoch=None,
579 | num_batch=None,
580 | flag_last_batch_in_epoch=None):
581 |
582 | DH_shift = 0.6
583 |
584 | # Get the latest viewRange
585 | yMin_View, yMax_View = self.fig_loss.viewRange()[1]
586 |
587 | # Update text position for Black-Scholes
588 | self.BS_loss_textItem.setPos(
589 | self.epochs * 0.8, self.loss_BS +
590 | (yMax_View - self.loss_BS) * 0.005)
591 |
592 | if self.flag_target:
593 | self.DH_target_loss_textItem.setPos(
594 | self.epochs * 0.6, self.target_loss +
595 | (yMax_View - self.target_loss) * 0.005)
596 |
597 | # Update text for Deep-Hedging.
598 | DH_loss_text_title = "Deep-Hedging Loss
"
600 | DH_loss_text_step = " Epoch: {} " + \
601 | "Batch: {}
"
602 | DH_loss_text_loss = "{:0.3f}
"
604 |
605 | DH_loss_text_str = (
606 | DH_loss_text_title +
607 | DH_loss_text_step +
608 | DH_loss_text_loss).format(
609 | int(num_epoch),
610 | int(num_batch),
611 | loss)
612 |
613 | if num_epoch == 1 and num_batch == 1:
614 | self.fig_loss.addItem(self.BS_loss_textItem)
615 |
616 | if self.flag_target:
617 | self.fig_loss.addItem(self.DH_target_loss_textItem)
618 |
619 | # Setup the textbox for the deep-hedging loss.
620 | self.DH_loss_textItem = pg.TextItem(
621 | html=DH_loss_text_str, anchor=(
622 | 0, 0), angle=0, border='w', fill=(
623 | 255, 255, 200))
624 | self.DH_loss_textItem.setPos((num_epoch - 1) + DH_shift, loss)
625 | self.fig_loss.addItem(self.DH_loss_textItem)
626 |
627 | self.fig_loss.enableAutoRange()
628 |
629 | # Mandatory pause to explain the demo. Remember to modify the
630 | # algo thread as well if one wants to remove the feature.
631 | # This part takes care the pause button.
632 | self.Pause()
633 | else:
634 | self.DH_loss_textItem.setHtml(DH_loss_text_str)
635 | if flag_last_batch_in_epoch:
636 | self.DH_loss_textItem.setPos(num_epoch + DH_shift, loss)
637 | if num_epoch == 1:
638 | # Establish the data for the out-of-sample loss at the end
639 | # of the first epoch.
640 | self.oos_loss_record = np.array([num_epoch, loss], ndmin=2)
641 | else:
642 | # Keep adding data at the end of each epoch.
643 | self.oos_loss_record = np.vstack(
644 | [self.oos_loss_record, np.array([num_epoch, loss])])
645 |
646 | self.DH_loss_plot.setData(self.oos_loss_record)
647 |
648 | # Move the Black-Scholes textbox to the left to avoid collision of the
649 | # deep-hedging textbox.
650 | if num_epoch > self.epochs * 0.5:
651 | if self.epsilon == 0:
652 | anchor = (0, 0)
653 | elif self.epsilon > 0:
654 | anchor = (0, 1)
655 |
656 | self.fig_loss.removeItem(self.BS_loss_textItem)
657 | self.BS_loss_textItem = pg.TextItem(
658 | html=self.BS_loss_html,
659 | anchor=anchor,
660 | angle=0,
661 | border='w',
662 | fill=(
663 | 255,
664 | 255,
665 | 200))
666 | self.BS_loss_textItem.setPos(
667 | 0, self.loss_BS + (yMax_View - self.loss_BS) * 0.005)
668 | self.fig_loss.addItem(self.BS_loss_textItem)
669 |
670 | def Update_PnL_Histogram(
671 | self,
672 | PnL_DH=None,
673 | DH_delta=None,
674 | DH_bins=None,
675 | loss=None,
676 | num_epoch=None,
677 | num_batch=None,
678 | flag_last_batch_in_epoch=None):
679 | if num_epoch == 1 and num_batch == 1:
680 | # Update PnL Histogram
681 | self.DH_hist = pg.BarGraphItem(x=self.bin_edges[:-2] + self.width,
682 | height=DH_bins,
683 | width=self.width,
684 | brush='b',
685 | name="Blue - Deep Hedging",
686 | antialias=False)
687 | self.fig_PnL.addItem(self.DH_hist)
688 | else:
689 | # Update PnL Histograms
690 | self.DH_hist.setOpts(height=DH_bins)
691 |
692 | def Update_Delta_Plot(
693 | self,
694 | PnL_DH=None,
695 | DH_delta=None,
696 | DH_bins=None,
697 | loss=None,
698 | num_epoch=None,
699 | num_batch=None,
700 | flag_last_batch_in_epoch=None):
701 | if num_epoch == 1 and num_batch == 1:
702 | # Update the Delta plot
703 | self.DH_delta_plot = pg.PlotDataItem(
704 | symbolBrush=(
705 | 0,
706 | 0,
707 | 255),
708 | symbolPen='b',
709 | symbol='+',
710 | symbolSize=10,
711 | name="Deep Hedging")
712 | self.DH_delta_plot.setData(self.S_range, DH_delta)
713 | self.fig_delta.addItem(self.DH_delta_plot)
714 | else:
715 | # Update the Delta plot
716 | self.DH_delta_plot.setData(self.S_range, DH_delta)
717 |
718 | def simulate_stock_prices(self):
719 | # Total obs = Training + Testing
720 | self.nobs = int(self.Ktrain * (1 + self.Ktest_ratio))
721 |
722 | # Length of one time-step (as fraction of a year).
723 | self.dt = day_count.yearFraction(
724 | calculation_date, calculation_date + 1)
725 | self.maturity = self.N * self.dt # Maturities (in the unit of a year)
726 |
727 | self.stochastic_process = BlackScholesProcess(
728 | s0=self.S0,
729 | sigma=self.sigma,
730 | risk_free=self.risk_free,
731 | dividend=self.dividend,
732 | day_count=day_count)
733 |
734 | print("\nRun Monte-Carlo Simulations for the Stock Price Process.\n")
735 | return self.stochastic_process.gen_path(
736 | self.maturity, self.N, self.nobs)
737 | print("\n")
738 |
739 | def assemble_data(self):
740 | self.payoff_T = self.payoff_func(
741 | self.S[:, -1]) # Payoff of the call option
742 |
743 | self.trade_set = np.stack((self.S), axis=1) # Trading set
744 |
745 | if information_set == "S":
746 | self.infoset = np.stack((self.S), axis=1) # Information set
747 | elif information_set == "log_S":
748 | self.infoset = np.stack((np.log(self.S)), axis=1)
749 | elif information_set == "normalized_log_S":
750 | self.infoset = np.stack((np.log(self.S / self.S0)), axis=1)
751 |
752 | # Structure of xtrain:
753 | # 1) Trade set: [S]
754 | # 2) Information set: [S]
755 | # 3) payoff (dim = 1)
756 | self.x_all = []
757 | for i in range(self.N + 1):
758 | self.x_all += [self.trade_set[i, :, None]]
759 | if i != self.N:
760 | self.x_all += [self.infoset[i, :, None]]
761 | self.x_all += [self.payoff_T[:, None]]
762 |
763 | # Split the entire sample into a training sample and a testing sample.
764 | self.test_size = int(self.Ktrain * self.Ktest_ratio)
765 | [self.xtrain, self.xtest] = train_test_split(
766 | self.x_all, test_size=self.test_size)
767 | [self.S_train, self.S_test] = train_test_split(
768 | [self.S], test_size=self.test_size)
769 | [self.option_payoff_train, self.option_payoff_test] = \
770 | train_test_split([self.x_all[-1]], test_size=self.test_size)
771 |
772 | # Convert the training sample into tf.Data format (same as xtrain).
773 | training_dataset = tf.data.Dataset.from_tensor_slices(
774 | tuple(self.xtrain))
775 | return training_dataset.cache()
776 |
777 | def get_Black_Scholes_Prices(self):
778 | # Obtain Black-Scholes price, delta, and PnL
779 | call = EuropeanCall()
780 | price_BS = call.get_BS_price(
781 | S=self.S_test[0],
782 | sigma=self.sigma,
783 | risk_free=self.risk_free,
784 | dividend=self.dividend,
785 | K=self.strike,
786 | exercise_date=self.maturity_date,
787 | calculation_date=calculation_date,
788 | day_count=day_count,
789 | dt=self.dt)
790 | delta_BS = call.get_BS_delta(
791 | S=self.S_test[0],
792 | sigma=self.sigma,
793 | risk_free=self.risk_free,
794 | dividend=self.dividend,
795 | K=self.strike,
796 | exercise_date=self.maturity_date,
797 | calculation_date=calculation_date,
798 | day_count=day_count,
799 | dt=self.dt)
800 | PnL_BS = call.get_BS_PnL(S=self.S_test[0],
801 | payoff=self.payoff_func(self.S_test[0][:,
802 | -1]),
803 | delta=delta_BS,
804 | dt=self.dt,
805 | risk_free=self.risk_free,
806 | final_period_cost=final_period_cost,
807 | epsilon=self.epsilon)
808 | return price_BS, delta_BS, PnL_BS
809 |
--------------------------------------------------------------------------------
/pyqt5/readme.txt:
--------------------------------------------------------------------------------
1 | Instruction for Running on the Grid:
2 |
3 | Run:
4 |
5 | bsub -Is -q "python" numactl --cpunodebind=0 zsh python3
6 |
7 | Then, within Python:
8 |
9 | exec(open("deep_hedging_gui.py").read())
10 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Package Version
2 | ----------------------------- -------------------
3 | pyqtgraph 0.11.0
4 | QtPy 1.9.0
5 | QuantLib 1.20
6 | scikit-learn 0.23.2
7 | scipy 1.5.2
8 | tensorflow 2.3.1
9 | tqdm 4.51.0
10 |
--------------------------------------------------------------------------------
/stochastic_processes/BlackScholesProcess.py:
--------------------------------------------------------------------------------
1 | import QuantLib as ql
2 | import numpy as np
3 | from tqdm import trange
4 |
5 | # References:
6 | # https://www.implementingquantlib.com/2014/06/chapter-6-part-5-of-8-path-generators.html
7 | # https://www.quantlib.org/reference/index.html
8 | # https://github.com/lballabio/QuantLib-SWIG
9 |
10 | # Assigned seed for testing. Set to 0 for random seeds.
11 |
12 | # Geometric Brownian Motion.
13 | class BlackScholesProcess:
14 | def __init__(self,s0 = None, sigma = None, risk_free = None, \
15 | dividend = None, day_count = None, seed=0):
16 | self.s0 = s0
17 | self.sigma = sigma
18 | self.risk_free = risk_free
19 | self.dividend = dividend
20 | self.day_count = day_count
21 | self.seed = seed
22 |
23 | def get_process(self, calculation_date = ql.Date.todaysDate()):
24 | spot_handle = ql.QuoteHandle(ql.SimpleQuote(self.s0))
25 | rf_handle = ql.QuoteHandle(ql.SimpleQuote(self.risk_free))
26 | dividend_handle = ql.QuoteHandle(ql.SimpleQuote(self.dividend))
27 |
28 | volatility = ql.BlackConstantVol(0, ql.NullCalendar(),self.sigma,self.day_count)
29 |
30 | # Assume flat term-structure.
31 | flat_ts = ql.YieldTermStructureHandle(ql.FlatForward(0, ql.NullCalendar(), rf_handle, self.day_count))
32 | dividend_yield = ql.YieldTermStructureHandle(ql.FlatForward(0, ql.NullCalendar(), dividend_handle, self.day_count))
33 |
34 | ql.Settings.instance().evaluationDate = calculation_date
35 |
36 | return ql.GeneralizedBlackScholesProcess(
37 | spot_handle,
38 | dividend_yield,
39 | flat_ts,
40 | ql.BlackVolTermStructureHandle(volatility))
41 |
42 | def gen_path(self, length = None, time_step = None, num_paths = None):
43 | # The variable length is in the unit of one year.
44 | rng = ql.GaussianRandomSequenceGenerator(ql.UniformRandomSequenceGenerator(time_step, ql.UniformRandomGenerator(self.seed)))
45 | seq = ql.GaussianMultiPathGenerator(self.get_process(), np.linspace(0,length,time_step+1), rng, False)
46 |
47 | value = np.zeros((num_paths, time_step+1))
48 |
49 | for i in trange(num_paths):
50 | sample_path = seq.next()
51 | path = sample_path.value()
52 | value[i, :] = np.array(path[0])
53 | return value
54 |
--------------------------------------------------------------------------------
/stochastic_processes/__init__.py:
--------------------------------------------------------------------------------
1 | from .BlackScholesProcess import BlackScholesProcess
2 |
--------------------------------------------------------------------------------
/utilities/__init__.py:
--------------------------------------------------------------------------------
1 | from .train_test_split import train_test_split
2 |
--------------------------------------------------------------------------------
/utilities/train_test_split.py:
--------------------------------------------------------------------------------
1 | from sklearn import model_selection
2 |
3 |
4 | def train_test_split(data=None, test_size=None):
5 | """Split simulated data into training and testing sample."""
6 | xtrain = []
7 | xtest = []
8 | for x in data:
9 | tmp_xtrain, tmp_xtest = model_selection.train_test_split(
10 | x, test_size=test_size, shuffle=False)
11 | xtrain += [tmp_xtrain]
12 | xtest += [tmp_xtest]
13 | return xtrain, xtest
14 |
--------------------------------------------------------------------------------