-
Notifications
You must be signed in to change notification settings - Fork 0
/
which_vol.py
236 lines (195 loc) · 6.47 KB
/
which_vol.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Jul 9 14:59:23 2022
Demonstration for different ways of delta-hedging when the market estimation
of volatility (i.e. implied) doesn't match the actual volatility. Assumes
implied volatility stays the same until expiry, as well as a constant
actual vol.
Updates:
(30 July 2022)
added dividend yield
added bid-offer spread - transaction costs (30 July 2022)
changed the way asset is simulated.
The goal is to reproduce various plots in
* Ahmad, R. and Willmott, P. (2005) Which Free Lunch Would You Like Today Sir?
Delta-Hedging, Volatility Arbitrage and Optimal Portfolios.
Willmott Magazine, 2005, 64-79.
@author: emirg
"""
import numpy as np
import matplotlib.pyplot as plt
import random
from scipy.stats import norm
import warnings
#!!!
# d1 and d2 evaluate to infinity at expiry, but their CDF gives 1, so it
# doesn't cause any harm. For the moment, I'm turning off warnings.
warnings.filterwarnings("ignore", category=RuntimeWarning)
def simulate_asset(S0, T, mu, D, sig, Nstep):
"""
Generates log-normal distribution
Parameters
----------
S0 : float
Asset price today.
T : float
Expiry.
mu : float
Growth rate.
D : float
Dividend yield.
sig : float
Volatility.
Nstep : int
Number of timesteps.
Returns
-------
list of times
timeseries of asset prices
"""
dt = T / Nstep
S=np.empty(Nstep+1)
S[0] = S0
for i in range(Nstep):
S[i+1] = S[i] + (mu - D) * S[i] * dt \
+ sig * S[i] * random.normalvariate(0,np.sqrt(dt))
return np.linspace(0, T, Nstep+1), S
def d1(E, Dt, S, r, D, sig):
"""
Parameters
----------
E : strike
Dt : T - t
S : asset
r : interest rate
D : dividend yield
sig : volatility
"""
return (np.log(S/E) + (r - D + 0.5 * sig**2)*Dt)/ (sig * np.sqrt(Dt))
def d2(E, Dt, S, r, D, sig):
return d1(E, Dt, S, r, D, sig) - sig * np.sqrt(Dt)
def Delta(E, Dt, S, r, D, sig):
delt = np.exp(-D * Dt) * norm.cdf(d1(E, Dt, S, r, D, sig))
# But at Dt=0, Delta is always 1 but python assign nan. Let's fix it.
# delt[-1]=1.
return delt
def compute_Profit_and_Loss(E, S, r, D, s_i, s_h, t, kappa, last=True):
"""
Given a time-series of asset values, it produces a time-series
of net P&L resulting from delta-hedging with a given volatility.
Also output time series of hedging-errors defined as:
forward(portfolio_post(i-1), dt) -portfolio_pre(i)
Parameters
----------
E : float
Strike.
S : np.array(dtype=float)
Asset timeseries.
r : float
Interest rate.
D : float
Dividend yield.
s_i : float
Implied volatility.
s_h : float
Hedging volatility.
t : np.array(dtype=float)
array of discrete times
kappa : float
bid-ask spread parameter. bid - ask = 2 * kappa * (asset value)
last : bool
If true, only return the last PnL. Otherwise return the time series
Returns
-------
np.array(float)
M2M profit.
Hedging error.
"""
Nsteps = len(t)-1
T = t[-1]
deltat = T/Nsteps
# implied value of call option
V_i = S * np.exp(-D*(T-t)) * norm.cdf(d1(E, T-t, S, r, D, s_i)) - \
E0 * np.exp(-r*(T-t)) * norm.cdf(d2(E, T-t, S, r, D, s_i))
Delta_h = Delta(E, T-t, S, r, D, s_h)
# Initialise portfolio before and after hedging
portfolio_pre = np.empty(Nsteps+1)
portfolio_post = np.empty(Nsteps+1)
# Initialise cash balance
balance = np.empty(Nsteps+1)
# pre values have no meaning at t=0
portfolio_pre[0] = 0
# We start with the hedged portfolio:
portfolio_post[0] = V_i[0] - Delta_h[0] * S[0]
# At the first step, we sold some assets (if Delta>0), so added the
# transaction costs there too
balance[0] = S[0] * Delta_h[0] * (1 - np.abs(Delta_h[0]) * kappa) - V_i[0]
for i in range(Nsteps):
# Value of portfolio at step i+1, before rehedging
portfolio_pre[i+1] = V_i[i+1] - Delta_h[i]*S[i+1]
# New amount of asset to be hedged at step i+1
diff = Delta_h[i+1]-Delta_h[i]
# include effect of transaction costs.
diff -= np.abs(diff)*kappa
# Value of portfolio after re-hedging
portfolio_post[i+1] = portfolio_pre[i+1] - diff*S[i+1]
# Cashflow at this step
cashflow = portfolio_pre[i+1] - portfolio_post[i+1]
# add cashflows to the balance (with appropriate forward valuing)
balance[i+1] = balance[i] * np.exp(r * deltat) + cashflow
# Hedge error:
hedge_error = (portfolio_post[:-1]*np.exp(r*T/Nsteps)
-portfolio_pre[1:])
# Mark-to-market net position
M2Mnet = balance + portfolio_post
# Return mark-to-market net position and (mean,std) of hedge error
if last:
return M2Mnet[-1], [hedge_error.mean(), hedge_error.std()]
else:
return M2Mnet, [hedge_error.mean(), hedge_error.std()]
# strike of call option
E0 = 100
# value of asset at t=0
asset = 100
# volatilities (actual, implied, hedging)
sig_a, sig_i, sig_h = 0.30, 0.20, 0.20
# growth rate
mu0 = -0.9
# risk-free rate
r0 = 0.05
# Dividend yield
D0 = 0
# Kappa (transation cost fraction - Leland model)
kappa0 = 0.001
# expiry
T0 = 1
# total timesteps
timesteps = 365
tstep = T0/timesteps
# We first plot the evolution of profit for 10 different tries.
fig = plt.figure(figsize=(20,10))
ax = fig.add_subplot()
ax.set_xlabel('Time')
ax.set_ylabel('Mark-to-market P&L')
ax.set_xlim(0,1)
for _ in range(8):
t, S = simulate_asset(asset, T0, mu0, D0, sig_a, timesteps)
PnL = compute_Profit_and_Loss(E0, S, r0, D0, sig_i, sig_h, t, kappa0,
False)[0]
ax.plot(t, PnL)
plt.show()
# Now do a number of simulations and look at the distribution of net profit
# Number of trials:
Ntry = 1000
# Initialise array for net profit and loss
net_PnL = np.empty(Ntry)
hedge_err = np.empty((Ntry,2))
for i in range(Ntry):
t, S = simulate_asset(asset, T0, mu0, D0, sig_a, timesteps)
net_PnL[i], hedge_err[i] = \
compute_Profit_and_Loss(E0, S, r0, D0, sig_i, sig_h, t, kappa0)
print("After {} tries, with re-hedging every {:.2f} days,"\
" the distribution of the net mark-to-market P&L is:"
.format(Ntry, tstep*365))
print("mean = {}, std = {}".format(net_PnL.mean(), net_PnL.std(ddof=1)))