trading_formulas 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +34 -0
- data/Rakefile +10 -0
- data/lib/trading_formulas/bermudan_options.rb +124 -0
- data/lib/trading_formulas/binomial_options.rb +753 -0
- data/lib/trading_formulas/black_scholes.rb +369 -0
- data/lib/trading_formulas/version.rb +3 -0
- data/lib/trading_formulas.rb +8 -0
- data/test/test_bermudan_options.rb +65 -0
- data/test/test_binomial_options.rb +393 -0
- data/test/test_black_scholes.rb +329 -0
- data/trading_formulas.gemspec +18 -0
- metadata +62 -0
@@ -0,0 +1,369 @@
|
|
1
|
+
module TradingFormulas
|
2
|
+
##
|
3
|
+
# Author:: Matt.Osentoski (matt.osentoski@gmail.com)
|
4
|
+
#
|
5
|
+
# This module contains formulas based on the Black Scholes model
|
6
|
+
# Converted to Python from "Financial Numerical Recipes in C" by:
|
7
|
+
# Bernt Arne Odegaard
|
8
|
+
# http://finance.bi.no/~bernt/gcc_prog/index.html
|
9
|
+
#
|
10
|
+
class BlackScholes
|
11
|
+
|
12
|
+
##
|
13
|
+
# Normal distribution
|
14
|
+
#
|
15
|
+
# +z+: Value to apply to a Normal distribution
|
16
|
+
# *Returns* Normal distribution
|
17
|
+
#
|
18
|
+
def self.n(z)
|
19
|
+
return (1.0/Math.sqrt(2.0*Math::PI))*Math.exp(-0.5*z*z)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Cumulative normal distribution
|
24
|
+
#
|
25
|
+
# +z+: Value to apply to a Cumulative normal distribution
|
26
|
+
# *Returns* Cumulative normal distribution
|
27
|
+
#
|
28
|
+
def self.N(z)
|
29
|
+
if (z > 6.0) # this guards against overflow
|
30
|
+
return 1.0
|
31
|
+
end
|
32
|
+
if (z < -6.0)
|
33
|
+
return 0.0
|
34
|
+
end
|
35
|
+
|
36
|
+
b1 = 0.31938153
|
37
|
+
b2 = -0.356563782
|
38
|
+
b3 = 1.781477937
|
39
|
+
b4 = -1.821255978
|
40
|
+
b5 = 1.330274429
|
41
|
+
p = 0.2316419
|
42
|
+
c2 = 0.3989423
|
43
|
+
|
44
|
+
a = z.abs
|
45
|
+
t = 1.0/(1.0+a*p)
|
46
|
+
b = c2*Math.exp((-z)*(z/2.0))
|
47
|
+
n = ((((b5*t+b4)*t+b3)*t+b2)*t+b1)*t
|
48
|
+
n = 1.0-b*n
|
49
|
+
if ( z < 0.0 )
|
50
|
+
n = 1.0 - n
|
51
|
+
end
|
52
|
+
return n
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Black Scholes formula (Call)
|
57
|
+
# Black and Scholes (1973) and Merton (1973)
|
58
|
+
#
|
59
|
+
# +s+: spot (underlying) price
|
60
|
+
# +k+: strike (exercise) price,
|
61
|
+
# +r+: interest rate
|
62
|
+
# +sigma+: volatility
|
63
|
+
# +time+: time to maturity
|
64
|
+
# *Returns* Option price
|
65
|
+
#
|
66
|
+
def self.call(s, k, r, sigma, time)
|
67
|
+
time_sqrt = Math.sqrt(time)
|
68
|
+
d1 = (Math.log(s/k)+r*time)/(sigma*time_sqrt)+0.5*sigma*time_sqrt
|
69
|
+
d2 = d1-(sigma*time_sqrt)
|
70
|
+
return s*N(d1) - k*Math.exp(-r*time)*N(d2)
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Black Scholes formula (Put)
|
75
|
+
# Black and Scholes (1973) and Merton (1973)
|
76
|
+
#
|
77
|
+
# +s+: spot (underlying) price
|
78
|
+
# +k+: strike (exercise) price,
|
79
|
+
# +r+: interest rate
|
80
|
+
# +sigma+: volatility
|
81
|
+
# +time+: time to maturity
|
82
|
+
# *Returns* Option price
|
83
|
+
#
|
84
|
+
def self.put(s, k, r, sigma, time)
|
85
|
+
time_sqrt = Math.sqrt(time)
|
86
|
+
d1 = (Math.log(s/k)+r*time)/(sigma*time_sqrt) + 0.5*sigma*time_sqrt
|
87
|
+
d2 = d1-(sigma*time_sqrt)
|
88
|
+
return k*Math.exp(-r*time)*N(-d2) - s*N(-d1)
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Delta of the Black Scholes formula (Call)
|
93
|
+
# +s+: spot (underlying) price
|
94
|
+
# +k+: strike (exercise) price,
|
95
|
+
# +r+: interest rate
|
96
|
+
# +sigma+: volatility
|
97
|
+
# +time+: time to maturity
|
98
|
+
# *Returns* Delta of the option
|
99
|
+
#
|
100
|
+
def self.delta_call(s, k, r, sigma, time)
|
101
|
+
time_sqrt = Math.sqrt(time)
|
102
|
+
d1 = (Math.log(s/k)+r*time)/(sigma*time_sqrt) + 0.5*sigma*time_sqrt
|
103
|
+
delta = N(d1)
|
104
|
+
return delta
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Delta of the Black Scholes formula (Put)
|
109
|
+
# +s+: spot (underlying) price
|
110
|
+
# +k+: strike (exercise) price,
|
111
|
+
# +r+: interest rate
|
112
|
+
# +sigma+: volatility
|
113
|
+
# +time+: time to maturity
|
114
|
+
# *Returns* Delta of the option
|
115
|
+
#
|
116
|
+
def self.delta_put(s, k, r, sigma, time)
|
117
|
+
time_sqrt = Math.sqrt(time)
|
118
|
+
d1 = (Math.log(s/k)+r*time)/(sigma*time_sqrt) + 0.5*sigma*time_sqrt
|
119
|
+
delta = -N(-d1)
|
120
|
+
return delta
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Calculates implied volatility for the Black Scholes formula using
|
125
|
+
# binomial search algorithm
|
126
|
+
# (NOTE: In the original code a large negative number was used as an
|
127
|
+
# exception handling mechanism. This has been replace with a generic
|
128
|
+
# 'Exception' that is thrown. The original code is in place and commented
|
129
|
+
# if you want to use the pure version of this code)
|
130
|
+
#
|
131
|
+
# +s+: spot (underlying) price
|
132
|
+
# +k+: strike (exercise) price,
|
133
|
+
# +r+: interest rate
|
134
|
+
# +time+: time to maturity
|
135
|
+
# +option_price+: The price of the option
|
136
|
+
# *Returns* Sigma (implied volatility)
|
137
|
+
# *Raises* Exception if there is a problem with the binomial search
|
138
|
+
#
|
139
|
+
def self.implied_volatility_call_bisections(s,k,r,time,option_price)
|
140
|
+
if (option_price<0.99*(s-k*Math.exp(-time*r))) # check for arbitrage violations.
|
141
|
+
return 0.0 # Option price is too low if this happens
|
142
|
+
end
|
143
|
+
|
144
|
+
# simple binomial search for the implied volatility.
|
145
|
+
# relies on the value of the option increasing in volatility
|
146
|
+
accuracy = 1.0e-5 # make this smaller for higher accuracy
|
147
|
+
max_iterations = 100
|
148
|
+
high_value = 1e10
|
149
|
+
#ERROR = -1e40 // <--- original code
|
150
|
+
|
151
|
+
# want to bracket sigma. first find a maximum sigma by finding a sigma
|
152
|
+
# with a estimated price higher than the actual price.
|
153
|
+
sigma_low=1e-5
|
154
|
+
sigma_high=0.3
|
155
|
+
price = call(s,k,r,sigma_high,time)
|
156
|
+
while (price < option_price)
|
157
|
+
sigma_high = 2.0 * sigma_high # keep doubling.
|
158
|
+
price = call(s,k,r,sigma_high,time)
|
159
|
+
if (sigma_high>high_value)
|
160
|
+
#return ERROR # panic, something wrong. // <--- original code
|
161
|
+
raise "panic, something wrong." # Comment this line if you uncomment the line above
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
(0..max_iterations).each do |i|
|
166
|
+
sigma = (sigma_low+sigma_high)*0.5
|
167
|
+
price = call(s,k,r,sigma,time)
|
168
|
+
test = (price-option_price)
|
169
|
+
if (test.abs<accuracy)
|
170
|
+
return sigma
|
171
|
+
end
|
172
|
+
if (test < 0.0)
|
173
|
+
sigma_low = sigma
|
174
|
+
else
|
175
|
+
sigma_high = sigma
|
176
|
+
end
|
177
|
+
end
|
178
|
+
#return ERROR // <--- original code
|
179
|
+
raise "An error occurred" # Comment this line if you uncomment the line above
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Calculates implied volatility for the Black Scholes formula using
|
184
|
+
# the Newton-Raphson formula
|
185
|
+
# (NOTE: In the original code a large negative number was used as an
|
186
|
+
# exception handling mechanism. This has been replace with a generic
|
187
|
+
# 'Exception' that is thrown. The original code is in place and commented
|
188
|
+
# if you want to use the pure version of this code)
|
189
|
+
#
|
190
|
+
# +s+: spot (underlying) price
|
191
|
+
# +k+: strike (exercise) price,
|
192
|
+
# +r+: interest rate
|
193
|
+
# +time+: time to maturity
|
194
|
+
# +option_price+: The price of the option
|
195
|
+
# *Returns* Sigma (implied volatility)
|
196
|
+
# *Raises* Exception if there is a problem with the newton formula
|
197
|
+
#
|
198
|
+
def self.implied_volatility_call_newton(s, k, r, time, option_price)
|
199
|
+
if (option_price<0.99*(s-k*Math.exp(-time*r))) # check for arbitrage violations. Option price is too low if this happens
|
200
|
+
return 0.0
|
201
|
+
end
|
202
|
+
|
203
|
+
max_iterations = 100
|
204
|
+
accuracy = 1.0e-5
|
205
|
+
t_sqrt = Math.sqrt(time)
|
206
|
+
|
207
|
+
sigma = (option_price/s)/(0.398*t_sqrt) # find initial value
|
208
|
+
|
209
|
+
(0..max_iterations).each do |i|
|
210
|
+
price = call(s,k,r,sigma,time)
|
211
|
+
diff = option_price -price
|
212
|
+
if (diff.abs<accuracy)
|
213
|
+
return sigma
|
214
|
+
end
|
215
|
+
d1 = (Math.log(s/k)+r*time)/(sigma*t_sqrt) + 0.5*sigma*t_sqrt
|
216
|
+
vega = s * t_sqrt * n(d1)
|
217
|
+
sigma = sigma + diff/vega
|
218
|
+
end
|
219
|
+
#return -99e10 # something screwy happened, should throw exception // <--- original code
|
220
|
+
raise "An error occurred" # Comment this line if you uncomment the line above
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Calculate partial derivatives for a Black Scholes Option (Call)
|
225
|
+
# (NOTE: Originally, this method used argument pointer references as a
|
226
|
+
# way of returning the partial derivatives in C++. I've removed these
|
227
|
+
# references from the method signature and chose to return a tuple instead.)
|
228
|
+
# +s+: spot (underlying) price
|
229
|
+
# +k+: strike (exercise) price,
|
230
|
+
# +r+: interest rate
|
231
|
+
# +sigma+: volatility
|
232
|
+
# +time+: time to maturity
|
233
|
+
# *Returns* Tuple of partial derivatives: (Delta, Gamma, Theta, Vega, Rho)
|
234
|
+
# delta: partial wrt S
|
235
|
+
# gamma: second partial wrt S
|
236
|
+
# theta: partial wrt time
|
237
|
+
# vega: partial wrt sigma
|
238
|
+
# rho: partial wrt r
|
239
|
+
#
|
240
|
+
def self.partials_call(s, k, r, sigma, time)
|
241
|
+
time_sqrt = Math.sqrt(time)
|
242
|
+
d1 = (Math.log(s/k)+r*time)/(sigma*time_sqrt) + 0.5*sigma*time_sqrt
|
243
|
+
d2 = d1-(sigma*time_sqrt)
|
244
|
+
delta = N(d1)
|
245
|
+
gamma = n(d1)/(s*sigma*time_sqrt)
|
246
|
+
theta =- (s*sigma*n(d1))/(2*time_sqrt) - r*k*Math.exp( -r*time)*N(d2)
|
247
|
+
vega = s * time_sqrt*n(d1)
|
248
|
+
rho = k*time*Math.exp(-r*time)*N(d2)
|
249
|
+
return delta, gamma, theta, vega, rho
|
250
|
+
end
|
251
|
+
|
252
|
+
##
|
253
|
+
# Calculate partial derivatives for a Black Scholes Option (Put)
|
254
|
+
# (NOTE: Originally, this method used argument pointer references as a
|
255
|
+
# way of returning the partial derivatives in C++. I've removed these
|
256
|
+
# references from the method signature and chose to return a tuple instead.)
|
257
|
+
# +s+: spot (underlying) price
|
258
|
+
# +k+: strike (exercise) price,
|
259
|
+
# +r+: interest rate
|
260
|
+
# +sigma+: volatility
|
261
|
+
# +time+: time to maturity
|
262
|
+
# *Returns* Tuple of partial derivatives: (Delta, Gamma, Theta, Vega, Rho)
|
263
|
+
# Delta: partial wrt S
|
264
|
+
# Gamma: second partial wrt S
|
265
|
+
# Theta: partial wrt time
|
266
|
+
# Vega: partial wrt sigma
|
267
|
+
# Rho: partial wrt r
|
268
|
+
#
|
269
|
+
def self.partials_put(s, k, r, sigma, time)
|
270
|
+
time_sqrt = Math.sqrt(time)
|
271
|
+
d1 = (Math.log(s/k)+r*time)/(sigma*time_sqrt) + 0.5*sigma*time_sqrt
|
272
|
+
d2 = d1-(sigma*time_sqrt)
|
273
|
+
delta = -N(-d1)
|
274
|
+
gamma = n(d1)/(s*sigma*time_sqrt)
|
275
|
+
theta = -(s*sigma*n(d1)) / (2*time_sqrt)+ r*k * Math.exp(-r*time) * N(-d2)
|
276
|
+
vega = s * time_sqrt * n(d1)
|
277
|
+
rho = -k*time*Math.exp(-r*time) * N(-d2)
|
278
|
+
return delta, gamma, theta, vega, rho
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# European option (Call) with a continuous payout.
|
283
|
+
# The continuous payout would be for fees associated with the asset.
|
284
|
+
# For example, storage costs.
|
285
|
+
# +s+: spot (underlying) price
|
286
|
+
# +x+: strike (exercise) price,
|
287
|
+
# +r+: interest rate
|
288
|
+
# +q+: yield on underlying
|
289
|
+
# +sigma+: volatility
|
290
|
+
# +time+: time to maturity
|
291
|
+
# *Returns* Option price
|
292
|
+
#
|
293
|
+
def self.european_call_payout(s, x, r, q, sigma, time)
|
294
|
+
sigma_sqr = sigma**2
|
295
|
+
time_sqrt = Math.sqrt(time)
|
296
|
+
d1 = (Math.log(s/x) + (r-q + 0.5*sigma_sqr)*time)/(sigma*time_sqrt)
|
297
|
+
d2 = d1-(sigma*time_sqrt)
|
298
|
+
call_price = s * Math.exp(-q*time)* N(d1) - x * Math.exp(-r*time) * N(d2)
|
299
|
+
return call_price
|
300
|
+
end
|
301
|
+
|
302
|
+
##
|
303
|
+
# European option (Put) with a continuous payout.
|
304
|
+
# The continuous payout would be for fees associated with the asset.
|
305
|
+
# For example, storage costs.
|
306
|
+
# +s+: spot (underlying) price
|
307
|
+
# +x+: strike (exercise) price,
|
308
|
+
# +r+: interest rate
|
309
|
+
# +q+: yield on underlying
|
310
|
+
# +sigma+: volatility
|
311
|
+
# +time+: time to maturity
|
312
|
+
# *Returns* Option price
|
313
|
+
#
|
314
|
+
def self.european_put_payout(s, k, r, q, sigma, time)
|
315
|
+
sigma_sqr = sigma**2
|
316
|
+
time_sqrt = Math.sqrt(time)
|
317
|
+
d1 = (Math.log(s/k) + (r-q + 0.5*sigma_sqr)*time)/(sigma*time_sqrt)
|
318
|
+
d2 = d1-(sigma*time_sqrt)
|
319
|
+
put_price = k * Math.exp(-r*time)*N(-d2)-s*Math.exp(-q*time)*N(-d1)
|
320
|
+
return put_price
|
321
|
+
end
|
322
|
+
|
323
|
+
##
|
324
|
+
# European option for known dividends (Call)
|
325
|
+
# +s+: spot (underlying) price
|
326
|
+
# +k+: strike (exercise) price,
|
327
|
+
# +r+: interest rate
|
328
|
+
# +sigma+: volatility
|
329
|
+
# +time_to_maturity+: time to maturity
|
330
|
+
# +dividend_times+: Array of dividend times. (Ex: [0.25, 0.75] for 1/4 and 3/4 of a year)
|
331
|
+
# +dividend_amounts+: Array of dividend amounts for the 'dividend_times'
|
332
|
+
# *Returns* Option price
|
333
|
+
#
|
334
|
+
def self.european_call_dividends(s, k, r, sigma, time_to_maturity,
|
335
|
+
dividend_times, dividend_amounts )
|
336
|
+
adjusted_s = s
|
337
|
+
dividend_times.each_index do |i|
|
338
|
+
if (dividend_times[i]<=time_to_maturity)
|
339
|
+
adjusted_s = adjusted_s - dividend_amounts[i] * Math.exp(-r*dividend_times[i])
|
340
|
+
end
|
341
|
+
end
|
342
|
+
return call(adjusted_s,k,r,sigma,time_to_maturity)
|
343
|
+
end
|
344
|
+
|
345
|
+
##
|
346
|
+
# European option for known dividends (Put)
|
347
|
+
# +s+: spot (underlying) price
|
348
|
+
# +k+: strike (exercise) price,
|
349
|
+
# +r+: interest rate
|
350
|
+
# +sigma+: volatility
|
351
|
+
# +time_to_maturity+: time to maturity
|
352
|
+
# +dividend_times+: Array of dividend times. (Ex: [0.25, 0.75] for 1/4 and 3/4 of a year)
|
353
|
+
# +dividend_amounts+: Array of dividend amounts for the 'dividend_times'
|
354
|
+
# *Returns*: Option price
|
355
|
+
#
|
356
|
+
def self.european_put_dividends(s, k, r, sigma, time_to_maturity,
|
357
|
+
dividend_times, dividend_amounts )
|
358
|
+
# reduce the current stock price by the amount of dividends.
|
359
|
+
adjusted_s=s
|
360
|
+
dividend_times.each_index do |i|
|
361
|
+
if (dividend_times[i]<=time_to_maturity)
|
362
|
+
adjusted_s = adjusted_s - dividend_amounts[i] * Math.exp(-r*dividend_times[i])
|
363
|
+
end
|
364
|
+
end
|
365
|
+
return put(adjusted_s,k,r,sigma,time_to_maturity)
|
366
|
+
end
|
367
|
+
|
368
|
+
end
|
369
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'trading_formulas'
|
3
|
+
|
4
|
+
class BermudanOptionsTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_call
|
7
|
+
s = 80
|
8
|
+
k = 100
|
9
|
+
r = 0.20
|
10
|
+
q = 0.0
|
11
|
+
sigma = 0.25
|
12
|
+
time = 1.0
|
13
|
+
steps = 500
|
14
|
+
potential_exercise_times = [0.25, 0.5, 0.75]
|
15
|
+
test_val = TradingFormulas::BermudanOptions.call(s, k, r, q, sigma, time,
|
16
|
+
potential_exercise_times, steps)
|
17
|
+
assert_equal(7.14016, test_val.round(5))
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Negative test case
|
22
|
+
def test_invalid_call
|
23
|
+
s = 80
|
24
|
+
k = 100
|
25
|
+
r = 0.20
|
26
|
+
q = 0.0
|
27
|
+
sigma = 0.25
|
28
|
+
time = 1.0
|
29
|
+
steps = 500
|
30
|
+
potential_exercise_times = [0.25, 0.5, 0.75]
|
31
|
+
test_val = TradingFormulas::BermudanOptions.call(s, k, r, q, sigma, time,
|
32
|
+
potential_exercise_times, steps)
|
33
|
+
assert_not_equal(7.14019, test_val.round(5))
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_put
|
37
|
+
s = 80
|
38
|
+
k = 100
|
39
|
+
r = 0.20
|
40
|
+
q = 0.0
|
41
|
+
sigma = 0.25
|
42
|
+
time = 1.0
|
43
|
+
steps = 500
|
44
|
+
potential_exercise_times = [0.25, 0.5, 0.75]
|
45
|
+
test_val = TradingFormulas::BermudanOptions.put(s, k, r, q, sigma, time,
|
46
|
+
potential_exercise_times, steps)
|
47
|
+
assert_equal(15.8869, test_val.round(4))
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Negative test case
|
52
|
+
def test_invalid_put
|
53
|
+
s = 80
|
54
|
+
k = 100
|
55
|
+
r = 0.20
|
56
|
+
q = 0.0
|
57
|
+
sigma = 0.25
|
58
|
+
time = 1.0
|
59
|
+
steps = 500
|
60
|
+
potential_exercise_times = [0.25, 0.5, 0.75]
|
61
|
+
test_val = TradingFormulas::BermudanOptions.put(s, k, r, q, sigma, time,
|
62
|
+
potential_exercise_times, steps)
|
63
|
+
assert_not_equal(15.8861, test_val.round(4))
|
64
|
+
end
|
65
|
+
end
|