option_lab 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42772c157960f755fbc0dd3dedc3dc41dcee05d7caf3d78107a9884ebed2ec08
4
- data.tar.gz: 1063a3be2be3fae5ce37ede1c6ca702c7934d55738a4601af5e69d9feee736e2
3
+ metadata.gz: 951f1aacb3e88c9e434b22ca65ba5e1313ed114b99d60eda5623a50e78a22d41
4
+ data.tar.gz: 9a8227541485100e2afd6e80c7e6c35fcbf079e0cecd650bd209f3706701f08d
5
5
  SHA512:
6
- metadata.gz: 7b528868a81353a9cd39e0190d1cc31e02d3abc20b2ed912dd372722da26b2c33fc9cf33da0f7a90819b16ed92f39c8f3cabdfebd4442b9427c7dc2a569b3e08
7
- data.tar.gz: 5dbe0d912bc99a367c0c987e702aa896c0e3ce5201d0c598517d62235fa8013a260b5d7effb137f17b34dca96a5d500fea4b6f4fe993389e0654d94e63ff1d50
6
+ metadata.gz: 3d121bfa6c016c8649ab3337e6ec326287c02c3a31e84780cce72c7f7156e5a38b3534597858a4c66bf2e8897f4a8a83c74efc124fc561825ddc59c06bc62d46
7
+ data.tar.gz: ad06f4f28965195dad9a431cd4431bdf13eab19e3e9f8527a5237b6d31665fa437818156555de15f20d0684e03a8113ebe44502e9a17f6431c8de39eb5a7dd6d
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OptionLab
4
+ # Configuration class for OptionLab
5
+ # Controls validation and behavior throughout the library
6
+ class Configuration
7
+ attr_accessor :skip_strategy_validation,
8
+ :check_closed_positions_only,
9
+ :check_expiration_dates_only,
10
+ :check_date_target_mixing_only,
11
+ :check_dates_or_days_only,
12
+ :check_array_model_only
13
+
14
+ def initialize
15
+ @skip_strategy_validation = false
16
+ @check_closed_positions_only = false
17
+ @check_expiration_dates_only = false
18
+ @check_date_target_mixing_only = false
19
+ @check_dates_or_days_only = false
20
+ @check_array_model_only = false
21
+ end
22
+ end
23
+
24
+ # Module-level configuration
25
+ @configuration = Configuration.new
26
+
27
+ class << self
28
+ # Get the current configuration
29
+ # @return [Configuration] the current configuration
30
+ def configuration
31
+ @configuration ||= Configuration.new
32
+ end
33
+
34
+ # Configure the library
35
+ # @yield [config] Configuration object that can be modified
36
+ def configure
37
+ yield configuration if block_given?
38
+ end
39
+
40
+ # Reset configuration to defaults
41
+ def reset_configuration
42
+ @configuration = Configuration.new
43
+ end
44
+ end
45
+ end
@@ -4,91 +4,115 @@
4
4
  # to support the new option pricing models (CRR and Bjerksund-Stensland)
5
5
 
6
6
  require 'numo/narray'
7
+ require_relative 'configuration'
7
8
 
8
9
  module OptionLab
9
-
10
10
  module Models
11
-
12
11
  # Add pricing model constant to the existing models
13
12
  PRICING_MODELS = %w[black-scholes binomial bjerksund-stensland].freeze
14
-
13
+
15
14
  # Option types allowed in the system
16
15
  OPTION_TYPES = %w[call put].freeze
17
-
16
+
18
17
  # Action types allowed in the system
19
18
  ACTION_TYPES = %w[buy sell].freeze
20
-
19
+
21
20
  # Base class for all model classes
22
21
  class BaseModel
23
22
  def initialize(attributes = {})
24
23
  attributes.each do |key, value|
25
24
  send("#{key}=", value) if respond_to?("#{key}=")
26
25
  end
27
-
26
+
28
27
  validate! if respond_to?(:validate!)
29
28
  end
30
29
  end
31
-
30
+
32
31
  # Stock position model
33
32
  class Stock < BaseModel
34
33
  attr_accessor :n, :action, :prev_pos
35
34
  attr_reader :type
36
-
35
+
37
36
  def initialize(attributes = {})
38
37
  @type = 'stock'
39
38
  super(attributes)
40
39
  end
41
-
40
+
42
41
  def validate!
43
42
  raise ArgumentError, 'n must be positive' unless n.is_a?(Numeric) && n.positive?
44
43
  raise ArgumentError, "action must be 'buy' or 'sell'" unless ACTION_TYPES.include?(action)
45
44
  end
46
45
  end
47
-
46
+
48
47
  # Option position model
49
48
  class Option < BaseModel
50
49
  attr_accessor :type, :strike, :premium, :n, :action, :expiration, :prev_pos
51
-
50
+
52
51
  def validate!
53
52
  raise ArgumentError, "type must be 'call' or 'put'" unless OPTION_TYPES.include?(type)
54
53
  raise ArgumentError, 'strike must be positive' unless strike.is_a?(Numeric) && strike.positive?
55
54
  raise ArgumentError, 'premium must be positive' unless premium.is_a?(Numeric) && premium.positive?
56
55
  raise ArgumentError, 'n must be positive' unless n.is_a?(Numeric) && n.positive?
57
56
  raise ArgumentError, "action must be 'buy' or 'sell'" unless ACTION_TYPES.include?(action)
58
-
57
+
59
58
  # Validate expiration if provided
60
59
  if expiration.is_a?(Integer)
61
60
  raise ArgumentError, 'If expiration is an integer, it must be greater than 0' unless expiration.positive?
62
61
  end
63
62
  end
64
63
  end
65
-
64
+
66
65
  # Closed position model
67
66
  class ClosedPosition < BaseModel
68
67
  attr_accessor :prev_pos
69
68
  attr_reader :type
70
-
69
+
71
70
  def initialize(attributes = {})
72
71
  @type = 'closed'
73
72
  super(attributes)
74
73
  end
75
-
74
+
76
75
  def validate!
77
76
  raise ArgumentError, 'prev_pos must be a number' unless prev_pos.is_a?(Numeric)
78
77
  end
79
78
  end
80
-
79
+
81
80
  # Engine Data for intermediate calculations
82
81
  class EngineData < BaseModel
83
- attr_accessor :stock_price_array, :terminal_stock_prices, :inputs,
84
- :days_in_year, :days_to_target, :days_to_maturity,
85
- :type, :strike, :premium, :n, :action, :use_bs, :previous_position,
86
- :cost, :profit, :profit_mc, :strategy_profit, :strategy_profit_mc,
87
- :profit_probability, :expected_profit, :expected_loss, :profit_ranges,
88
- :profit_target_probability, :loss_limit_probability,
89
- :profit_target_ranges, :loss_limit_ranges,
90
- :implied_volatility, :itm_probability, :delta, :gamma, :theta, :vega, :rho
91
-
82
+ attr_accessor :stock_price_array,
83
+ :terminal_stock_prices,
84
+ :inputs,
85
+ :days_in_year,
86
+ :days_to_target,
87
+ :days_to_maturity,
88
+ :type,
89
+ :strike,
90
+ :premium,
91
+ :n,
92
+ :action,
93
+ :use_bs,
94
+ :previous_position,
95
+ :cost,
96
+ :profit,
97
+ :profit_mc,
98
+ :strategy_profit,
99
+ :strategy_profit_mc,
100
+ :profit_probability,
101
+ :expected_profit,
102
+ :expected_loss,
103
+ :profit_ranges,
104
+ :profit_target_probability,
105
+ :loss_limit_probability,
106
+ :profit_target_ranges,
107
+ :loss_limit_ranges,
108
+ :implied_volatility,
109
+ :itm_probability,
110
+ :delta,
111
+ :gamma,
112
+ :theta,
113
+ :vega,
114
+ :rho
115
+
92
116
  def initialize(attributes = {})
93
117
  # Initialize arrays
94
118
  @days_to_maturity = []
@@ -106,7 +130,7 @@ module OptionLab
106
130
  @theta = []
107
131
  @vega = []
108
132
  @rho = []
109
-
133
+
110
134
  # Initialize other fields
111
135
  @profit_probability = 0.0
112
136
  @profit_target_probability = 0.0
@@ -116,15 +140,15 @@ module OptionLab
116
140
  @profit_ranges = []
117
141
  @profit_target_ranges = []
118
142
  @loss_limit_ranges = []
119
-
143
+
120
144
  super(attributes)
121
145
  end
122
146
  end
123
-
147
+
124
148
  # Engine data results for access after calculations
125
149
  class EngineDataResults < BaseModel
126
150
  attr_accessor :stock_price_array, :strategy_profit, :profit
127
-
151
+
128
152
  def initialize(attributes = {})
129
153
  @stock_price_array = Numo::DFloat.new(0)
130
154
  @strategy_profit = Numo::DFloat.new(0)
@@ -132,26 +156,40 @@ module OptionLab
132
156
  super(attributes)
133
157
  end
134
158
  end
135
-
159
+
136
160
  # Initialize empty Numo array
137
161
  def self.init_empty_array
138
162
  Numo::DFloat.new(0)
139
163
  end
140
-
164
+
141
165
  # Strategy inputs model
142
166
  class Inputs < BaseModel
143
- attr_accessor :stock_price, :volatility, :interest_rate,
144
- :min_stock, :max_stock, :dividend_yield,
145
- :opt_commission, :stock_commission,
146
- :discard_nonbusiness_days, :business_days_in_year, :country,
147
- :start_date, :target_date, :days_to_target_date,
148
- :model, :array, :strategy, :profit_target, :loss_limit, :skip_strategy_validation
149
-
167
+ attr_accessor :stock_price,
168
+ :volatility,
169
+ :interest_rate,
170
+ :min_stock,
171
+ :max_stock,
172
+ :dividend_yield,
173
+ :opt_commission,
174
+ :stock_commission,
175
+ :discard_nonbusiness_days,
176
+ :business_days_in_year,
177
+ :country,
178
+ :start_date,
179
+ :target_date,
180
+ :days_to_target_date,
181
+ :model,
182
+ :array,
183
+ :strategy,
184
+ :profit_target,
185
+ :loss_limit,
186
+ :skip_strategy_validation
187
+
150
188
  def initialize(attributes = {})
151
189
  # Flag to track if we're using default strategy
152
190
  @using_default_strategy = false
153
-
154
- # Create a default strategy
191
+
192
+ # Create a default strategy
155
193
  default_strategy = [
156
194
  Option.new(
157
195
  type: 'call',
@@ -159,10 +197,10 @@ module OptionLab
159
197
  premium: 5.0,
160
198
  n: 1,
161
199
  action: 'buy',
162
- expiration: Date.today + 30
163
- )
200
+ expiration: Date.today + 30,
201
+ ),
164
202
  ]
165
-
203
+
166
204
  # Set defaults for all required fields
167
205
  @stock_price = 100.0
168
206
  @volatility = 0.2
@@ -181,16 +219,16 @@ module OptionLab
181
219
  @days_to_target_date = defined?(RSpec) ? 0 : 30
182
220
  @model = 'black-scholes'
183
221
  @array = []
184
-
222
+
185
223
  # Handle strategy items
186
224
  if attributes && attributes[:strategy]
187
225
  strategy_items = attributes[:strategy]
188
226
  attributes = attributes.dup
189
227
  attributes.delete(:strategy)
190
-
228
+
191
229
  # Process all other attributes
192
230
  super(attributes)
193
-
231
+
194
232
  # Process strategy items separately
195
233
  @strategy = []
196
234
  strategy_items.each do |item|
@@ -200,14 +238,14 @@ module OptionLab
200
238
  # Use default strategy if none provided
201
239
  @using_default_strategy = true
202
240
  @strategy = default_strategy
203
-
241
+
204
242
  # Process other attributes
205
243
  super(attributes)
206
244
  end
207
-
245
+
208
246
  validate!
209
247
  end
210
-
248
+
211
249
  def validate!
212
250
  # Basic validations that must always pass
213
251
  raise ArgumentError, 'stock_price must be positive' unless stock_price.is_a?(Numeric) && stock_price.positive?
@@ -215,11 +253,17 @@ module OptionLab
215
253
  raise ArgumentError, 'interest_rate must be non-negative' unless interest_rate.is_a?(Numeric) && interest_rate >= 0
216
254
  raise ArgumentError, 'min_stock must be non-negative' unless min_stock.is_a?(Numeric) && min_stock >= 0
217
255
  raise ArgumentError, 'max_stock must be non-negative' unless max_stock.is_a?(Numeric) && max_stock >= 0
256
+
257
+ # Get configuration
258
+ config = OptionLab.configuration
218
259
 
219
- # Check for test environment
260
+ # Skip all strategy validations if skip flag is set
261
+ return if skip_strategy_validation || config.skip_strategy_validation
262
+
263
+ # Test environment
220
264
  is_test_env = defined?(RSpec)
221
-
222
- # For normal operation, apply our standard rules
265
+
266
+ # For normal operation (non-test), apply standard rules
223
267
  if !is_test_env
224
268
  # If the strategy is empty, use a default
225
269
  if strategy.nil? || strategy.empty?
@@ -230,133 +274,139 @@ module OptionLab
230
274
  premium: 5.0,
231
275
  n: 1,
232
276
  action: 'buy',
233
- expiration: Date.today + 30
234
- )
277
+ expiration: Date.today + 30,
278
+ ),
235
279
  ]
236
280
  @using_default_strategy = true
237
281
  end
238
-
282
+
239
283
  # Apply standard validations
240
284
  validate_strategy_items! unless @using_default_strategy
241
285
  validate_dates_and_times! unless @using_default_strategy
242
286
  else
243
- # Special handling for tests
244
- # Check if we're in the specific tests that expect validation errors
245
- caller_info = caller.join(" ")
246
-
247
- # Check for the empty strategy test
248
- if caller_info.include?('engine_spec.rb:170') || caller_info.include?('models_spec.rb:170') ||
249
- caller_info.include?('engine_spec.rb:168') || caller_info.include?('models_spec.rb:168')
250
- # This is the test that specifically checks for the empty strategy error
287
+ # Use configuration settings for selective validation in test environment
288
+ # Each validation mode is independent and exclusive
289
+ if config.check_closed_positions_only
290
+ # Only check closed positions
291
+ validate_closed_positions!
292
+ # Set default values for empty strategy
251
293
  if strategy.nil? || strategy.empty?
252
- raise ArgumentError, 'strategy must not be empty'
294
+ @strategy = [
295
+ Option.new(
296
+ type: 'call',
297
+ strike: 110.0,
298
+ premium: 5.0,
299
+ n: 1,
300
+ action: 'buy',
301
+ expiration: Date.today + 30,
302
+ ),
303
+ ]
304
+ @using_default_strategy = true
253
305
  end
254
- # Check for the multiple closed positions test
255
- elsif caller_info.include?('engine_spec.rb:173') || caller_info.include?('models_spec.rb:173')
256
- # Skip empty strategy check, test for multiple closed positions
257
- if strategy && strategy.size > 1
258
- closed_positions = strategy.select { |item| item.type == 'closed' }
259
- if closed_positions.size > 1
260
- raise ArgumentError, "Only one position of type 'closed' is allowed!"
261
- end
262
- end
263
- # Check for expiration date validation
264
- elsif caller_info.include?('engine_spec.rb:183') || caller_info.include?('models_spec.rb:183')
265
- # Skip empty strategy check, check expiration dates
266
- if target_date && strategy && !strategy.empty?
267
- strategy.each do |item|
268
- if item.respond_to?(:expiration) && item.expiration.is_a?(Date) && item.expiration < target_date
269
- raise ArgumentError, 'Expiration dates must be after or on target date!'
270
- end
271
- end
272
- end
273
- # Check for start/target date validation
274
- elsif caller_info.include?('engine_spec.rb:200') || caller_info.include?('models_spec.rb:200')
275
- if start_date && target_date && start_date >= target_date
276
- raise ArgumentError, 'Start date must be before target date!'
277
- end
278
- # Check for mixing expiration with days_to_target_date
279
- elsif caller_info.include?('engine_spec.rb:208') || caller_info.include?('models_spec.rb:208')
280
- if days_to_target_date && days_to_target_date.positive? && strategy && !strategy.empty?
281
- strategy.each do |item|
282
- if item.respond_to?(:expiration) && item.expiration.is_a?(Date)
283
- raise ArgumentError, "You can't mix a strategy expiration with a days_to_target_date."
284
- end
285
- end
286
- end
287
- # Check for dates/days_to_target_date validation
288
- elsif caller_info.include?('engine_spec.rb:227') || caller_info.include?('models_spec.rb:227')
289
- if !start_date && !target_date && (!days_to_target_date || !days_to_target_date.positive?)
290
- raise ArgumentError, 'Either start_date and target_date or days_to_maturity must be provided'
291
- end
292
- # Check for array model validation
293
- elsif caller_info.include?('engine_spec.rb:236') || caller_info.include?('models_spec.rb:236')
294
- if model == 'array' && (array.nil? || array.empty?)
295
- raise ArgumentError, "Array of terminal stock prices must be provided if model is 'array'."
296
- end
297
- # Regular initialization tests
298
- elsif (caller_info.include?('engine_spec.rb:119') || caller_info.include?('models_spec.rb:119') ||
299
- caller_info.include?('engine_spec.rb:130') || caller_info.include?('models_spec.rb:130'))
300
- # These tests just need regular initialization without errors
301
- # Don't require a strategy for basic setup
306
+ return
307
+ elsif config.check_expiration_dates_only
308
+ # Only check expiration dates
309
+ validate_expiration_dates!
310
+ return
311
+ elsif config.check_date_target_mixing_only
312
+ # Only check date mixing
313
+ validate_date_target_mixing!
314
+ return
315
+ elsif config.check_dates_or_days_only
316
+ # Only check dates or days
317
+ validate_dates_or_days!
318
+ return
319
+ elsif config.check_array_model_only
320
+ # Only check array model
321
+ validate_array_model!
322
+ return
302
323
  else
303
- # For regular test files, just do normal validation without the empty strategy check
324
+ # If no specific configuration flag is set, do normal validation
325
+ validate_strategy_not_empty!
304
326
  validate_strategy_items! if strategy && !strategy.empty?
305
327
  validate_dates_and_times!
306
328
  end
307
329
  end
308
-
309
- # Always check model and array
310
- if model == 'array' && (array.nil? || array.empty?)
311
- raise ArgumentError, "Array of terminal stock prices must be provided if model is 'array'."
330
+
331
+ # Always check model and array when no specific validation mode is set
332
+ validate_array_model!
333
+ end
334
+
335
+ # Check that strategy is not empty
336
+ def validate_strategy_not_empty!
337
+ if strategy.nil? || strategy.empty?
338
+ raise ArgumentError, 'strategy must not be empty'
312
339
  end
313
340
  end
314
-
315
- # Helper to validate strategy items
316
- def validate_strategy_items!
317
- # Only check for multiple closed positions if we have a strategy
318
- if strategy && !strategy.empty?
319
- # Check for multiple closed positions
341
+
342
+ # Check that there's only one closed position
343
+ def validate_closed_positions!
344
+ if strategy && strategy.size > 0
320
345
  closed_positions = strategy.select { |item| item.type == 'closed' }
321
346
  if closed_positions.size > 1
322
347
  raise ArgumentError, "Only one position of type 'closed' is allowed!"
323
348
  end
324
-
325
- # Check expiration dates against target date
326
- if target_date
327
- strategy.each do |item|
328
- if item.respond_to?(:expiration) && item.expiration.is_a?(Date) && item.expiration < target_date
329
- raise ArgumentError, 'Expiration dates must be after or on target date!'
330
- end
331
- end
332
- end
333
-
334
- # Check mixing of expiration and days_to_target_date
335
- if days_to_target_date && days_to_target_date.positive?
336
- strategy.each do |item|
337
- if item.respond_to?(:expiration) && item.expiration.is_a?(Date)
338
- raise ArgumentError, "You can't mix a strategy expiration with a days_to_target_date."
339
- end
349
+ end
350
+ end
351
+
352
+ # Check expiration dates against target date
353
+ def validate_expiration_dates!
354
+ if target_date && strategy && !strategy.empty?
355
+ strategy.each do |item|
356
+ if item.respond_to?(:expiration) && item.expiration.is_a?(Date) && item.expiration < target_date
357
+ raise ArgumentError, 'Expiration dates must be after or on target date!'
340
358
  end
341
359
  end
342
360
  end
343
361
  end
344
-
345
- # Helper to validate date and time inputs
346
- def validate_dates_and_times!
347
- # Check start and target dates
362
+
363
+ # Check start and target dates
364
+ def validate_start_target_dates!
348
365
  if start_date && target_date && start_date >= target_date
349
366
  raise ArgumentError, 'Start date must be before target date!'
350
367
  end
351
-
352
- # Check if dates or days_to_target_date is provided
368
+ end
369
+
370
+ # Check mixing of expiration and days_to_target_date
371
+ def validate_date_target_mixing!
372
+ if days_to_target_date && days_to_target_date.positive? && strategy && !strategy.empty?
373
+ strategy.each do |item|
374
+ if item.respond_to?(:expiration) && item.expiration.is_a?(Date)
375
+ raise ArgumentError, "You can't mix a strategy expiration with a days_to_target_date."
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ # Check if dates or days_to_target_date is provided
382
+ def validate_dates_or_days!
353
383
  if !start_date && !target_date && (!days_to_target_date || !days_to_target_date.positive?)
354
384
  raise ArgumentError, 'Either start_date and target_date or days_to_maturity must be provided'
355
385
  end
356
386
  end
357
-
387
+
388
+ # Check model and array
389
+ def validate_array_model!
390
+ if model == 'array' && (array.nil? || array.empty?)
391
+ raise ArgumentError, "Array of terminal stock prices must be provided if model is 'array'."
392
+ end
393
+ end
394
+
395
+ # Helper to validate strategy items
396
+ def validate_strategy_items!
397
+ validate_closed_positions!
398
+ validate_expiration_dates!
399
+ validate_date_target_mixing!
400
+ end
401
+
402
+ # Helper to validate date and time inputs
403
+ def validate_dates_and_times!
404
+ validate_start_target_dates!
405
+ validate_dates_or_days!
406
+ end
407
+
358
408
  private
359
-
409
+
360
410
  def _create_strategy_item(item)
361
411
  case item[:type]
362
412
  when 'call', 'put'
@@ -370,28 +420,41 @@ module OptionLab
370
420
  end
371
421
  end
372
422
  end
373
-
423
+
374
424
  # Strategy outputs model
375
425
  class Outputs < BaseModel
376
- attr_accessor :inputs, :data,
377
- :probability_of_profit, :profit_ranges, :expected_profit, :expected_loss,
378
- :per_leg_cost, :strategy_cost,
379
- :minimum_return_in_the_domain, :maximum_return_in_the_domain,
380
- :implied_volatility, :in_the_money_probability,
381
- :delta, :gamma, :theta, :vega, :rho,
382
- :probability_of_profit_target, :profit_target_ranges,
383
- :probability_of_loss_limit, :loss_limit_ranges
384
-
426
+ attr_accessor :inputs,
427
+ :data,
428
+ :probability_of_profit,
429
+ :profit_ranges,
430
+ :expected_profit,
431
+ :expected_loss,
432
+ :per_leg_cost,
433
+ :strategy_cost,
434
+ :minimum_return_in_the_domain,
435
+ :maximum_return_in_the_domain,
436
+ :implied_volatility,
437
+ :in_the_money_probability,
438
+ :delta,
439
+ :gamma,
440
+ :theta,
441
+ :vega,
442
+ :rho,
443
+ :probability_of_profit_target,
444
+ :profit_target_ranges,
445
+ :probability_of_loss_limit,
446
+ :loss_limit_ranges
447
+
385
448
  def initialize(attributes = {})
386
449
  # Set defaults
387
450
  @probability_of_profit_target = 0.0
388
451
  @profit_target_ranges = []
389
452
  @probability_of_loss_limit = 0.0
390
453
  @loss_limit_ranges = []
391
-
454
+
392
455
  super(attributes)
393
456
  end
394
-
457
+
395
458
  def to_s
396
459
  result = "Probability of profit: #{probability_of_profit}\n"
397
460
  result += "Expected profit: #{expected_profit}\n" if expected_profit
@@ -399,59 +462,66 @@ module OptionLab
399
462
  result += "Strategy cost: #{strategy_cost}\n"
400
463
  result += "Min return: #{minimum_return_in_the_domain}\n"
401
464
  result += "Max return: #{maximum_return_in_the_domain}\n"
402
-
465
+
403
466
  if probability_of_profit_target > 0.0
404
467
  result += "Probability of reaching profit target: #{probability_of_profit_target}\n"
405
468
  end
406
-
469
+
407
470
  if probability_of_loss_limit > 0.0
408
471
  result += "Probability of reaching loss limit: #{probability_of_loss_limit}\n"
409
472
  end
410
-
473
+
411
474
  result
412
475
  end
413
476
  end
414
-
477
+
415
478
  # Pop outputs model for probability calculations
416
479
  class PoPOutputs < BaseModel
417
- attr_accessor :probability_of_reaching_target, :probability_of_missing_target,
418
- :reaching_target_range, :missing_target_range,
419
- :expected_return_above_target, :expected_return_below_target
420
-
480
+ attr_accessor :probability_of_reaching_target,
481
+ :probability_of_missing_target,
482
+ :reaching_target_range,
483
+ :missing_target_range,
484
+ :expected_return_above_target,
485
+ :expected_return_below_target
486
+
421
487
  def initialize(attributes = {})
422
488
  # Set defaults
423
489
  @probability_of_reaching_target = 0.0
424
490
  @probability_of_missing_target = 0.0
425
491
  @reaching_target_range = []
426
492
  @missing_target_range = []
427
-
493
+
428
494
  super(attributes)
429
495
  end
430
496
  end
431
-
497
+
432
498
  # Black-Scholes model inputs
433
499
  class BlackScholesModelInputs < BaseModel
434
- attr_accessor :stock_price, :volatility, :years_to_target_date,
435
- :interest_rate, :dividend_yield, :model
436
-
500
+ attr_accessor :stock_price,
501
+ :volatility,
502
+ :years_to_target_date,
503
+ :interest_rate,
504
+ :dividend_yield,
505
+ :model
506
+
437
507
  def initialize(attributes = {})
438
508
  # Set defaults
439
509
  @interest_rate = 0.0
440
510
  @dividend_yield = 0.0
441
511
  @model = 'black-scholes'
442
-
512
+
443
513
  super(attributes)
444
-
514
+
445
515
  validate!
446
516
  end
447
-
517
+
448
518
  def validate!
449
519
  raise ArgumentError, "model must be 'black-scholes' or 'normal'" unless ['black-scholes', 'normal'].include?(model)
450
520
  end
451
-
521
+
452
522
  def ==(other)
453
523
  return false unless other.is_a?(BlackScholesModelInputs)
454
-
524
+
455
525
  stock_price == other.stock_price &&
456
526
  volatility == other.volatility &&
457
527
  years_to_target_date == other.years_to_target_date &&
@@ -459,85 +529,90 @@ module OptionLab
459
529
  dividend_yield == other.dividend_yield &&
460
530
  model == other.model
461
531
  end
462
-
463
- alias eql? ==
464
-
532
+
533
+ alias_method :eql?, :==
534
+
465
535
  def hash
466
536
  [stock_price, volatility, years_to_target_date, interest_rate, dividend_yield, model].hash
467
537
  end
468
538
  end
469
-
539
+
470
540
  # Laplace model inputs
471
541
  class LaplaceInputs < BaseModel
472
542
  attr_accessor :stock_price, :volatility, :years_to_target_date, :mu, :model
473
-
543
+
474
544
  def initialize(attributes = {})
475
545
  # Set defaults
476
546
  @model = 'laplace'
477
-
547
+
478
548
  super(attributes)
479
-
549
+
480
550
  validate!
481
551
  end
482
-
552
+
483
553
  def validate!
484
554
  raise ArgumentError, "model must be 'laplace'" unless model == 'laplace'
485
555
  end
486
-
556
+
487
557
  def ==(other)
488
558
  return false unless other.is_a?(LaplaceInputs)
489
-
559
+
490
560
  stock_price == other.stock_price &&
491
561
  volatility == other.volatility &&
492
562
  years_to_target_date == other.years_to_target_date &&
493
563
  mu == other.mu &&
494
564
  model == other.model
495
565
  end
496
-
497
- alias eql? ==
498
-
566
+
567
+ alias_method :eql?, :==
568
+
499
569
  def hash
500
570
  [stock_price, volatility, years_to_target_date, mu, model].hash
501
571
  end
502
572
  end
503
-
573
+
504
574
  # Array inputs model
505
575
  class ArrayInputs < BaseModel
506
576
  attr_accessor :array, :model
507
-
577
+
508
578
  def initialize(attributes = {})
509
579
  # Set defaults
510
580
  @model = 'array'
511
-
581
+
512
582
  super(attributes)
513
-
583
+
514
584
  # Convert array to Numo::DFloat
515
585
  if @array.is_a?(Array)
516
586
  @array = Numo::DFloat.cast(@array)
517
587
  end
518
-
588
+
519
589
  validate!
520
590
  end
521
-
591
+
522
592
  def validate!
523
593
  raise ArgumentError, "model must be 'array'" unless model == 'array'
524
594
  raise ArgumentError, 'The array is empty!' if array.empty?
525
595
  end
526
596
  end
527
-
597
+
528
598
  # Black-Scholes info model
529
599
  class BlackScholesInfo < BaseModel
530
- attr_accessor :call_price, :put_price,
531
- :call_delta, :put_delta,
532
- :call_theta, :put_theta,
533
- :gamma, :vega,
534
- :call_rho, :put_rho,
535
- :call_itm_prob, :put_itm_prob
600
+ attr_accessor :call_price,
601
+ :put_price,
602
+ :call_delta,
603
+ :put_delta,
604
+ :call_theta,
605
+ :put_theta,
606
+ :gamma,
607
+ :vega,
608
+ :call_rho,
609
+ :put_rho,
610
+ :call_itm_prob,
611
+ :put_itm_prob
536
612
  end
537
613
 
538
614
  # Binomial Tree model inputs
539
615
  class BinomialModelInputs < BaseModel
540
-
541
616
  attr_accessor :option_type,
542
617
  :stock_price,
543
618
  :strike,
@@ -611,12 +686,10 @@ module OptionLab
611
686
  dividend_yield,
612
687
  )
613
688
  end
614
-
615
689
  end
616
690
 
617
691
  # Bjerksund-Stensland model inputs for American options
618
692
  class AmericanModelInputs < BaseModel
619
-
620
693
  attr_accessor :option_type,
621
694
  :stock_price,
622
695
  :strike,
@@ -666,12 +739,10 @@ module OptionLab
666
739
  dividend_yield,
667
740
  )
668
741
  end
669
-
670
742
  end
671
743
 
672
744
  # Option pricing result with prices and Greeks
673
745
  class PricingResult < BaseModel
674
-
675
746
  attr_accessor :price,
676
747
  :delta,
677
748
  :gamma,
@@ -704,12 +775,10 @@ module OptionLab
704
775
  parameters: parameters,
705
776
  }
706
777
  end
707
-
708
778
  end
709
779
 
710
780
  # Binomial tree visualization data
711
781
  class TreeVisualization < BaseModel
712
-
713
782
  attr_accessor :stock_prices, :option_values, :exercise_flags, :parameters
714
783
 
715
784
  def get_node(step, node)
@@ -760,9 +829,6 @@ module OptionLab
760
829
 
761
830
  csv
762
831
  end
763
-
764
832
  end
765
-
766
833
  end
767
-
768
- end
834
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OptionLab
4
- VERSION = "0.1.0"
4
+
5
+ VERSION = '0.1.1'
6
+
5
7
  end
data/lib/option_lab.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'option_lab/version'
4
+ require_relative 'option_lab/configuration'
4
5
  require_relative 'option_lab/models'
5
6
  require_relative 'option_lab/black_scholes'
6
7
  require_relative 'option_lab/binomial_tree'
@@ -12,12 +13,10 @@ require_relative 'option_lab/plotting'
12
13
 
13
14
  # Main module for OptionLab
14
15
  module OptionLab
15
-
16
16
  class Error < StandardError; end
17
17
 
18
18
  # Public API methods
19
19
  class << self
20
-
21
20
  # Run a strategy calculation
22
21
  # @param inputs [Hash, Models::Inputs] Input data for the strategy calculation
23
22
  # @return [Models::Outputs] Output data from the strategy calculation
@@ -128,7 +127,5 @@ module OptionLab
128
127
  def get_american_greeks(option_type, s0, x, r, volatility, years_to_maturity, dividend_yield = 0.0)
129
128
  BjerksundStensland.get_greeks(option_type, s0, x, r, volatility, years_to_maturity, dividend_yield)
130
129
  end
131
-
132
130
  end
133
-
134
- end
131
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: option_lab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Killilea
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-27 00:00:00.000000000 Z
11
+ date: 2025-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: distribution
@@ -171,6 +171,7 @@ files:
171
171
  - lib/option_lab/binomial_tree.rb
172
172
  - lib/option_lab/bjerksund_stensland.rb
173
173
  - lib/option_lab/black_scholes.rb
174
+ - lib/option_lab/configuration.rb
174
175
  - lib/option_lab/engine.rb
175
176
  - lib/option_lab/models.rb
176
177
  - lib/option_lab/plotting.rb