option_lab 0.1.0

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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +139 -0
  4. data/.yard/hooks/before_generate.rb +7 -0
  5. data/.yardopts +11 -0
  6. data/Gemfile +26 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +180 -0
  9. data/Rakefile +44 -0
  10. data/docs/OptionLab/BinomialTree.html +1271 -0
  11. data/docs/OptionLab/BjerksundStensland.html +2022 -0
  12. data/docs/OptionLab/BlackScholes.html +2388 -0
  13. data/docs/OptionLab/Engine.html +1716 -0
  14. data/docs/OptionLab/Models/AmericanModelInputs.html +937 -0
  15. data/docs/OptionLab/Models/ArrayInputs.html +463 -0
  16. data/docs/OptionLab/Models/BaseModel.html +223 -0
  17. data/docs/OptionLab/Models/BinomialModelInputs.html +1161 -0
  18. data/docs/OptionLab/Models/BlackScholesInfo.html +967 -0
  19. data/docs/OptionLab/Models/BlackScholesModelInputs.html +851 -0
  20. data/docs/OptionLab/Models/ClosedPosition.html +445 -0
  21. data/docs/OptionLab/Models/EngineData.html +2523 -0
  22. data/docs/OptionLab/Models/EngineDataResults.html +435 -0
  23. data/docs/OptionLab/Models/Inputs.html +2241 -0
  24. data/docs/OptionLab/Models/LaplaceInputs.html +777 -0
  25. data/docs/OptionLab/Models/Option.html +736 -0
  26. data/docs/OptionLab/Models/Outputs.html +1753 -0
  27. data/docs/OptionLab/Models/PoPOutputs.html +645 -0
  28. data/docs/OptionLab/Models/PricingResult.html +848 -0
  29. data/docs/OptionLab/Models/Stock.html +583 -0
  30. data/docs/OptionLab/Models/TreeVisualization.html +688 -0
  31. data/docs/OptionLab/Models.html +251 -0
  32. data/docs/OptionLab/Plotting.html +548 -0
  33. data/docs/OptionLab/Support.html +2884 -0
  34. data/docs/OptionLab/Utils.html +619 -0
  35. data/docs/OptionLab.html +133 -0
  36. data/docs/_index.html +376 -0
  37. data/docs/class_list.html +54 -0
  38. data/docs/css/common.css +1 -0
  39. data/docs/css/full_list.css +58 -0
  40. data/docs/css/style.css +503 -0
  41. data/docs/file.LICENSE.html +70 -0
  42. data/docs/file.README.html +263 -0
  43. data/docs/file_list.html +64 -0
  44. data/docs/frames.html +22 -0
  45. data/docs/index.html +263 -0
  46. data/docs/js/app.js +344 -0
  47. data/docs/js/full_list.js +242 -0
  48. data/docs/js/jquery.js +4 -0
  49. data/docs/method_list.html +1974 -0
  50. data/docs/top-level-namespace.html +110 -0
  51. data/examples/american_options.rb +163 -0
  52. data/examples/covered_call.rb +76 -0
  53. data/lib/option_lab/binomial_tree.rb +238 -0
  54. data/lib/option_lab/bjerksund_stensland.rb +276 -0
  55. data/lib/option_lab/black_scholes.rb +323 -0
  56. data/lib/option_lab/engine.rb +492 -0
  57. data/lib/option_lab/models.rb +768 -0
  58. data/lib/option_lab/plotting.rb +182 -0
  59. data/lib/option_lab/support.rb +471 -0
  60. data/lib/option_lab/utils.rb +107 -0
  61. data/lib/option_lab/version.rb +5 -0
  62. data/lib/option_lab.rb +134 -0
  63. data/option_lab.gemspec +43 -0
  64. metadata +207 -0
@@ -0,0 +1,492 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'numo/narray'
5
+
6
+ module OptionLab
7
+
8
+ module Engine
9
+ class << self
10
+ # Run strategy calculation
11
+ # @param inputs_data [Hash, Models::Inputs] Input data for strategy calculation
12
+ # @return [Models::Outputs] Output data from strategy calculation
13
+ def run_strategy(inputs_data)
14
+ # Ensure inputs_data is not nil
15
+ inputs_data ||= {}
16
+
17
+ # Ensure strategy is present
18
+ if inputs_data.is_a?(Hash) && !inputs_data[:strategy]
19
+ # Create a default call option
20
+ inputs_data[:strategy] = [
21
+ {
22
+ type: 'call',
23
+ strike: 110.0,
24
+ premium: 5.0,
25
+ n: 1,
26
+ action: 'buy',
27
+ expiration: Date.today + 30
28
+ }
29
+ ]
30
+ end
31
+
32
+ # Convert hash to Inputs if needed
33
+ inputs = if inputs_data.is_a?(Models::Inputs)
34
+ inputs_data
35
+ else
36
+ Models::Inputs.new(inputs_data)
37
+ end
38
+
39
+ # Initialize data
40
+ data = _init_inputs(inputs)
41
+
42
+ # Run calculations
43
+ data = _run(data)
44
+
45
+ # Generate outputs
46
+ _generate_outputs(data)
47
+ end
48
+
49
+ # Initialize input data
50
+ # @param inputs [Models::Inputs] Input data
51
+ # @return [Models::EngineData] Initialized engine data
52
+ def _init_inputs(inputs)
53
+ # Create engine data
54
+ data = Models::EngineData.new(
55
+ stock_price_array: Support.create_price_seq(inputs.min_stock, inputs.max_stock),
56
+ terminal_stock_prices: inputs.model == 'array' ? inputs.array : Models.init_empty_array,
57
+ inputs: inputs,
58
+ )
59
+
60
+ # Set days in year
61
+ data.days_in_year = inputs.discard_nonbusiness_days ? inputs.business_days_in_year : 365
62
+
63
+ # Calculate days to target
64
+ if inputs.start_date && inputs.target_date
65
+ n_discarded_days = if inputs.discard_nonbusiness_days
66
+ Utils.get_nonbusiness_days(
67
+ inputs.start_date, inputs.target_date, inputs.country
68
+ )
69
+ else
70
+ 0
71
+ end
72
+
73
+ data.days_to_target = (inputs.target_date - inputs.start_date).to_i + 1 - n_discarded_days
74
+ else
75
+ data.days_to_target = inputs.days_to_target_date
76
+ end
77
+
78
+ # Process each strategy leg
79
+ inputs.strategy.each_with_index do |strategy, _i|
80
+ data.type << strategy.type
81
+
82
+ case strategy
83
+ when Models::Option
84
+ data.strike << strategy.strike
85
+ data.premium << strategy.premium
86
+ data.n << strategy.n
87
+ data.action << strategy.action
88
+ data.previous_position << strategy.prev_pos || 0.0
89
+
90
+ if !strategy.expiration
91
+ data.days_to_maturity << data.days_to_target
92
+ data.use_bs << false
93
+ elsif strategy.expiration.is_a?(Date) && inputs.start_date
94
+ n_discarded_days = if inputs.discard_nonbusiness_days
95
+ Utils.get_nonbusiness_days(
96
+ inputs.start_date, strategy.expiration, inputs.country
97
+ )
98
+ else
99
+ 0
100
+ end
101
+
102
+ data.days_to_maturity << (strategy.expiration - inputs.start_date).to_i + 1 - n_discarded_days
103
+ data.use_bs << (strategy.expiration != inputs.target_date)
104
+ elsif strategy.expiration.is_a?(Integer)
105
+ if strategy.expiration >= data.days_to_target
106
+ data.days_to_maturity << strategy.expiration
107
+ data.use_bs << (strategy.expiration != data.days_to_target)
108
+ else
109
+ raise ArgumentError, 'Days remaining to maturity must be greater than or equal to the number of days remaining to the target date!'
110
+ end
111
+ else
112
+ raise ArgumentError, 'Expiration must be a date, an int, or nil.'
113
+ end
114
+
115
+ when Models::Stock
116
+ data.n << strategy.n
117
+ data.action << strategy.action
118
+ data.previous_position << strategy.prev_pos || 0.0
119
+ data.strike << 0.0
120
+ data.premium << 0.0
121
+ data.use_bs << false
122
+ data.days_to_maturity << -1
123
+
124
+ when Models::ClosedPosition
125
+ data.previous_position << strategy.prev_pos
126
+ data.strike << 0.0
127
+ data.n << 0
128
+ data.premium << 0.0
129
+ data.action << 'n/a'
130
+ data.use_bs << false
131
+ data.days_to_maturity << -1
132
+
133
+ else
134
+ raise ArgumentError, "Type must be 'call', 'put', 'stock' or 'closed'!"
135
+ end
136
+ end
137
+
138
+ data
139
+ end
140
+
141
+ # Run calculations
142
+ # @param data [Models::EngineData] Engine data
143
+ # @return [Models::EngineData] Updated engine data
144
+ def _run(data)
145
+ inputs = data.inputs
146
+
147
+ # Calculate time to target
148
+ time_to_target = data.days_to_target.to_f / data.days_in_year
149
+
150
+ # Initialize arrays
151
+ data.cost = Array.new(data.type.size, 0.0)
152
+ data.profit = Numo::DFloat.zeros(data.type.size, data.stock_price_array.size)
153
+ data.strategy_profit = Numo::DFloat.zeros(data.stock_price_array.size)
154
+
155
+ if inputs.model == 'array'
156
+ data.profit_mc = Numo::DFloat.zeros(data.type.size, data.terminal_stock_prices.size)
157
+ data.strategy_profit_mc = Numo::DFloat.zeros(data.terminal_stock_prices.size)
158
+ end
159
+
160
+ # Process each strategy leg
161
+ data.type.each_with_index do |type, i|
162
+ case type
163
+ when 'call', 'put'
164
+ _run_option_calcs(data, i)
165
+ when 'stock'
166
+ _run_stock_calcs(data, i)
167
+ when 'closed'
168
+ _run_closed_position_calcs(data, i)
169
+ end
170
+
171
+ # Add to strategy profit
172
+ data.strategy_profit += data.profit[i, true]
173
+
174
+ if inputs.model == 'array'
175
+ data.strategy_profit_mc += data.profit_mc[i, true]
176
+ end
177
+ end
178
+
179
+ # Calculate probability of profit
180
+ pop_inputs = if inputs.model == 'array'
181
+ Models::ArrayInputs.new(
182
+ array: data.strategy_profit_mc,
183
+ )
184
+ else
185
+ Models::BlackScholesModelInputs.new(
186
+ stock_price: inputs.stock_price,
187
+ volatility: inputs.volatility,
188
+ years_to_target_date: time_to_target,
189
+ interest_rate: inputs.interest_rate,
190
+ dividend_yield: inputs.dividend_yield,
191
+ )
192
+ end
193
+
194
+ pop_out = Support.get_pop(data.stock_price_array, data.strategy_profit, pop_inputs)
195
+
196
+ # Store results
197
+ data.profit_probability = pop_out.probability_of_reaching_target
198
+ data.expected_profit = pop_out.expected_return_above_target
199
+ data.expected_loss = pop_out.expected_return_below_target
200
+ data.profit_ranges = pop_out.reaching_target_range
201
+
202
+ # Calculate profit target probability if needed
203
+ if inputs.profit_target && inputs.profit_target > 0.01
204
+ pop_out_prof_targ = Support.get_pop(
205
+ data.stock_price_array,
206
+ data.strategy_profit,
207
+ pop_inputs,
208
+ inputs.profit_target,
209
+ )
210
+
211
+ data.profit_target_probability = pop_out_prof_targ.probability_of_reaching_target
212
+ data.profit_target_ranges = pop_out_prof_targ.reaching_target_range
213
+ end
214
+
215
+ # Calculate loss limit probability if needed
216
+ if inputs.loss_limit && inputs.loss_limit < 0.0
217
+ pop_out_loss_lim = Support.get_pop(
218
+ data.stock_price_array,
219
+ data.strategy_profit,
220
+ pop_inputs,
221
+ inputs.loss_limit + 0.01,
222
+ )
223
+
224
+ data.loss_limit_probability = pop_out_loss_lim.probability_of_missing_target
225
+ data.loss_limit_ranges = pop_out_loss_lim.missing_target_range
226
+ end
227
+
228
+ data
229
+ end
230
+
231
+ # Run option calculations
232
+ # @param data [Models::EngineData] Engine data
233
+ # @param i [Integer] Index of strategy leg
234
+ # @return [Models::EngineData] Updated engine data
235
+ def _run_option_calcs(data, i)
236
+ inputs = data.inputs
237
+ action = data.action[i]
238
+ type = data.type[i]
239
+
240
+ if data.previous_position[i] && data.previous_position[i] < 0.0
241
+ # Previous position is closed
242
+ data.implied_volatility << 0.0
243
+ data.itm_probability << 0.0
244
+ data.delta << 0.0
245
+ data.gamma << 0.0
246
+ data.vega << 0.0
247
+ data.theta << 0.0
248
+ data.rho << 0.0
249
+
250
+ cost = (data.premium[i] + data.previous_position[i]) * data.n[i]
251
+ cost *= -1.0 if data.action[i] == 'buy'
252
+
253
+ data.cost[i] = cost
254
+ data.profit[i, true] += cost
255
+
256
+ if inputs.model == 'array'
257
+ data.profit_mc[i, true] += cost
258
+ end
259
+
260
+ return data
261
+ end
262
+
263
+ # Calculate option metrics
264
+ time_to_maturity = data.days_to_maturity[i].to_f / data.days_in_year
265
+
266
+ bs = BlackScholes.get_bs_info(
267
+ inputs.stock_price,
268
+ data.strike[i],
269
+ inputs.interest_rate,
270
+ inputs.volatility,
271
+ time_to_maturity,
272
+ inputs.dividend_yield,
273
+ )
274
+
275
+ # Store Greeks
276
+ data.gamma << bs.gamma
277
+ data.vega << bs.vega
278
+
279
+ data.implied_volatility << BlackScholes.get_implied_vol(
280
+ type,
281
+ data.premium[i],
282
+ inputs.stock_price,
283
+ data.strike[i],
284
+ inputs.interest_rate,
285
+ time_to_maturity,
286
+ inputs.dividend_yield,
287
+ )
288
+
289
+ # Set multiplier for buy/sell
290
+ negative_multiplier = data.action[i] == 'buy' ? 1 : -1
291
+
292
+ # Store type-specific metrics
293
+ if type == 'call'
294
+ data.itm_probability << bs.call_itm_prob
295
+ data.delta << bs.call_delta * negative_multiplier
296
+ data.theta << bs.call_theta / data.days_in_year * negative_multiplier
297
+ data.rho << bs.call_rho * negative_multiplier
298
+ else
299
+ data.itm_probability << bs.put_itm_prob
300
+ data.delta << bs.put_delta * negative_multiplier
301
+ data.theta << bs.put_theta / data.days_in_year * negative_multiplier
302
+ data.rho << bs.put_rho * negative_multiplier
303
+ end
304
+
305
+ # Use previous position premium if available
306
+ opt_value = (data.previous_position[i] && data.previous_position[i] > 0.0) ? data.previous_position[i] : data.premium[i]
307
+
308
+ # Calculate profit/loss profile
309
+ if data.use_bs[i]
310
+ target_to_maturity = (data.days_to_maturity[i] - data.days_to_target).to_f / data.days_in_year
311
+
312
+ profit, cost = Support.get_pl_profile_bs(
313
+ type,
314
+ action,
315
+ data.strike[i],
316
+ opt_value,
317
+ inputs.interest_rate,
318
+ target_to_maturity,
319
+ inputs.volatility,
320
+ data.n[i],
321
+ data.stock_price_array,
322
+ inputs.dividend_yield,
323
+ inputs.opt_commission,
324
+ )
325
+
326
+ data.profit[i, true] = profit
327
+ data.cost[i] = cost
328
+
329
+ if inputs.model == 'array'
330
+ data.profit_mc[i, true] = Support.get_pl_profile_bs(
331
+ type,
332
+ action,
333
+ data.strike[i],
334
+ opt_value,
335
+ inputs.interest_rate,
336
+ target_to_maturity,
337
+ inputs.volatility,
338
+ data.n[i],
339
+ data.terminal_stock_prices,
340
+ inputs.dividend_yield,
341
+ inputs.opt_commission,
342
+ )[0]
343
+ end
344
+ else
345
+ profit, cost = Support.get_pl_profile(
346
+ type,
347
+ action,
348
+ data.strike[i],
349
+ opt_value,
350
+ data.n[i],
351
+ data.stock_price_array,
352
+ inputs.opt_commission,
353
+ )
354
+
355
+ data.profit[i, true] = profit
356
+ data.cost[i] = cost
357
+
358
+ if inputs.model == 'array'
359
+ data.profit_mc[i, true] = Support.get_pl_profile(
360
+ type,
361
+ action,
362
+ data.strike[i],
363
+ opt_value,
364
+ data.n[i],
365
+ data.terminal_stock_prices,
366
+ inputs.opt_commission,
367
+ )[0]
368
+ end
369
+ end
370
+
371
+ data
372
+ end
373
+
374
+ # Run stock calculations
375
+ # @param data [Models::EngineData] Engine data
376
+ # @param i [Integer] Index of strategy leg
377
+ # @return [Models::EngineData] Updated engine data
378
+ def _run_stock_calcs(data, i)
379
+ inputs = data.inputs
380
+ action = data.action[i]
381
+
382
+ # Set delta based on action
383
+ data.delta << (action == 'buy' ? 1.0 : -1.0)
384
+
385
+ # Set other metrics
386
+ data.itm_probability << 1.0
387
+ data.implied_volatility << 0.0
388
+ data.gamma << 0.0
389
+ data.vega << 0.0
390
+ data.rho << 0.0
391
+ data.theta << 0.0
392
+
393
+ if data.previous_position[i] && data.previous_position[i] < 0.0
394
+ # Previous position is closed
395
+ costtmp = (inputs.stock_price + data.previous_position[i]) * data.n[i]
396
+ costtmp *= -1.0 if data.action[i] == 'buy'
397
+
398
+ data.cost[i] = costtmp
399
+ data.profit[i, true] += costtmp
400
+
401
+ if inputs.model == 'array'
402
+ data.profit_mc[i, true] += costtmp
403
+ end
404
+
405
+ return data
406
+ end
407
+
408
+ # Use previous position if available
409
+ stockpos = (data.previous_position[i] && data.previous_position[i] > 0.0) ? data.previous_position[i] : inputs.stock_price
410
+
411
+ # Calculate profit/loss profile
412
+ profit, cost = Support.get_pl_profile_stock(
413
+ stockpos,
414
+ action,
415
+ data.n[i],
416
+ data.stock_price_array,
417
+ inputs.stock_commission,
418
+ )
419
+
420
+ data.profit[i, true] = profit
421
+ data.cost[i] = cost
422
+
423
+ if inputs.model == 'array'
424
+ data.profit_mc[i, true] = Support.get_pl_profile_stock(
425
+ stockpos,
426
+ action,
427
+ data.n[i],
428
+ data.terminal_stock_prices,
429
+ inputs.stock_commission,
430
+ )[0]
431
+ end
432
+
433
+ data
434
+ end
435
+
436
+ # Run closed position calculations
437
+ # @param data [Models::EngineData] Engine data
438
+ # @param i [Integer] Index of strategy leg
439
+ # @return [Models::EngineData] Updated engine data
440
+ def _run_closed_position_calcs(data, i)
441
+ # Set metrics
442
+ data.implied_volatility << 0.0
443
+ data.itm_probability << 0.0
444
+ data.delta << 0.0
445
+ data.gamma << 0.0
446
+ data.vega << 0.0
447
+ data.rho << 0.0
448
+ data.theta << 0.0
449
+
450
+ # Set cost and profit
451
+ data.cost[i] = data.previous_position[i]
452
+ data.profit[i, true] += data.previous_position[i]
453
+
454
+ if data.inputs.model == 'array'
455
+ data.profit_mc[i, true] += data.previous_position[i]
456
+ end
457
+
458
+ data
459
+ end
460
+
461
+ # Generate outputs from engine data
462
+ # @param data [Models::EngineData] Engine data
463
+ # @return [Models::Outputs] Strategy outputs
464
+ def _generate_outputs(data)
465
+ Models::Outputs.new(
466
+ inputs: data.inputs,
467
+ data: data,
468
+ probability_of_profit: data.profit_probability,
469
+ expected_profit: data.expected_profit,
470
+ expected_loss: data.expected_loss,
471
+ strategy_cost: data.cost.sum,
472
+ per_leg_cost: data.cost,
473
+ profit_ranges: data.profit_ranges,
474
+ minimum_return_in_the_domain: data.strategy_profit.min,
475
+ maximum_return_in_the_domain: data.strategy_profit.max,
476
+ implied_volatility: data.implied_volatility,
477
+ in_the_money_probability: data.itm_probability,
478
+ delta: data.delta,
479
+ gamma: data.gamma,
480
+ theta: data.theta,
481
+ vega: data.vega,
482
+ rho: data.rho,
483
+ probability_of_profit_target: data.profit_target_probability,
484
+ probability_of_loss_limit: data.loss_limit_probability,
485
+ profit_target_ranges: data.profit_target_ranges,
486
+ loss_limit_ranges: data.loss_limit_ranges,
487
+ )
488
+ end
489
+ end
490
+ end
491
+
492
+ end