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 +4 -4
- data/lib/option_lab/configuration.rb +45 -0
- data/lib/option_lab/models.rb +280 -214
- data/lib/option_lab/version.rb +3 -1
- data/lib/option_lab.rb +2 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 951f1aacb3e88c9e434b22ca65ba5e1313ed114b99d60eda5623a50e78a22d41
|
4
|
+
data.tar.gz: 9a8227541485100e2afd6e80c7e6c35fcbf079e0cecd650bd209f3706701f08d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/option_lab/models.rb
CHANGED
@@ -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,
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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,
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
#
|
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
|
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
|
-
#
|
244
|
-
#
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
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
|
-
|
255
|
-
elsif
|
256
|
-
#
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
-
#
|
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
|
-
|
311
|
-
|
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
|
-
#
|
316
|
-
def
|
317
|
-
|
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
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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
|
-
#
|
346
|
-
def
|
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
|
-
|
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,
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
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,
|
418
|
-
|
419
|
-
|
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,
|
435
|
-
|
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
|
-
|
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
|
-
|
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,
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
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
|
data/lib/option_lab/version.rb
CHANGED
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.
|
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-
|
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
|