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.
@@ -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,3 @@
1
+ module TradingFormulas
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "trading_formulas/version"
2
+ require "trading_formulas/bermudan_options"
3
+ require "trading_formulas/black_scholes"
4
+ require "trading_formulas/binomial_options"
5
+
6
+ module TradingFormulas
7
+
8
+ 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