schwab_rb 0.2.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.copilotignore +4 -0
  3. data/.rspec +2 -0
  4. data/.rspec_status +292 -0
  5. data/.rubocop.yml +41 -0
  6. data/.rubocop_todo.yml +105 -0
  7. data/CHANGELOG.md +28 -0
  8. data/LICENSE.txt +23 -0
  9. data/README.md +271 -0
  10. data/Rakefile +12 -0
  11. data/doc/notes/data_objects_analysis.md +223 -0
  12. data/doc/notes/data_objects_refactoring_plan.md +82 -0
  13. data/examples/fetch_account_numbers.rb +49 -0
  14. data/examples/fetch_user_preferences.rb +49 -0
  15. data/lib/schwab_rb/account.rb +9 -0
  16. data/lib/schwab_rb/auth/auth_context.rb +23 -0
  17. data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
  18. data/lib/schwab_rb/auth/init_client_login.rb +201 -0
  19. data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
  20. data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
  21. data/lib/schwab_rb/auth/token.rb +24 -0
  22. data/lib/schwab_rb/auth/token_manager.rb +105 -0
  23. data/lib/schwab_rb/clients/async_client.rb +122 -0
  24. data/lib/schwab_rb/clients/base_client.rb +887 -0
  25. data/lib/schwab_rb/clients/client.rb +97 -0
  26. data/lib/schwab_rb/configuration.rb +39 -0
  27. data/lib/schwab_rb/constants.rb +7 -0
  28. data/lib/schwab_rb/data_objects/account.rb +281 -0
  29. data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
  30. data/lib/schwab_rb/data_objects/instrument.rb +156 -0
  31. data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
  32. data/lib/schwab_rb/data_objects/option.rb +147 -0
  33. data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
  34. data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
  35. data/lib/schwab_rb/data_objects/order.rb +186 -0
  36. data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
  37. data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
  38. data/lib/schwab_rb/data_objects/position.rb +100 -0
  39. data/lib/schwab_rb/data_objects/price_history.rb +187 -0
  40. data/lib/schwab_rb/data_objects/quote.rb +276 -0
  41. data/lib/schwab_rb/data_objects/transaction.rb +132 -0
  42. data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
  43. data/lib/schwab_rb/market_hours.rb +13 -0
  44. data/lib/schwab_rb/movers.rb +35 -0
  45. data/lib/schwab_rb/option.rb +64 -0
  46. data/lib/schwab_rb/orders/builder.rb +202 -0
  47. data/lib/schwab_rb/orders/destination.rb +19 -0
  48. data/lib/schwab_rb/orders/duration.rb +9 -0
  49. data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
  50. data/lib/schwab_rb/orders/errors.rb +5 -0
  51. data/lib/schwab_rb/orders/instruments.rb +35 -0
  52. data/lib/schwab_rb/orders/option_instructions.rb +10 -0
  53. data/lib/schwab_rb/orders/order.rb +77 -0
  54. data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
  55. data/lib/schwab_rb/orders/price_link_type.rb +9 -0
  56. data/lib/schwab_rb/orders/session.rb +14 -0
  57. data/lib/schwab_rb/orders/special_instruction.rb +10 -0
  58. data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
  59. data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
  60. data/lib/schwab_rb/orders/stop_type.rb +11 -0
  61. data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
  62. data/lib/schwab_rb/price_history.rb +55 -0
  63. data/lib/schwab_rb/quote.rb +13 -0
  64. data/lib/schwab_rb/transaction.rb +23 -0
  65. data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
  66. data/lib/schwab_rb/utils/logger.rb +70 -0
  67. data/lib/schwab_rb/utils/redactor.rb +104 -0
  68. data/lib/schwab_rb/version.rb +5 -0
  69. data/lib/schwab_rb.rb +48 -0
  70. data/sig/schwab_rb.rbs +4 -0
  71. metadata +289 -0
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ module DataObjects
5
+ class MarketHours
6
+ attr_reader :markets
7
+
8
+ class << self
9
+ def build(data)
10
+ new(data)
11
+ end
12
+ end
13
+
14
+ def initialize(data)
15
+ @markets = {}
16
+ data.each do |market_type, market_data|
17
+ @markets[market_type] = {}
18
+ market_data.each do |product_key, product_data|
19
+ @markets[market_type][product_key] = MarketInfo.new(product_data)
20
+ end
21
+ end
22
+ end
23
+
24
+ def to_h
25
+ result = {}
26
+ @markets.each do |market_type, market_data|
27
+ result[market_type] = {}
28
+ market_data.each do |product_key, market_info|
29
+ result[market_type][product_key] = market_info.to_h
30
+ end
31
+ end
32
+ result
33
+ end
34
+
35
+ def equity
36
+ @markets['equity'] || {}
37
+ end
38
+
39
+ def option
40
+ @markets['option'] || {}
41
+ end
42
+
43
+ def future
44
+ @markets['future'] || {}
45
+ end
46
+
47
+ def forex
48
+ @markets['forex'] || {}
49
+ end
50
+
51
+ def bond
52
+ @markets['bond'] || {}
53
+ end
54
+
55
+ def market_types
56
+ @markets.keys
57
+ end
58
+
59
+ def find_by_market_type(market_type)
60
+ @markets[market_type.to_s]
61
+ end
62
+
63
+ def find_market_info(market_type, product_key)
64
+ market_data = find_by_market_type(market_type)
65
+ return nil unless market_data
66
+ market_data[product_key.to_s]
67
+ end
68
+
69
+ def open_markets
70
+ result = {}
71
+ @markets.each do |market_type, market_data|
72
+ market_data.each do |product_key, market_info|
73
+ if market_info.open?
74
+ result[market_type] ||= {}
75
+ result[market_type][product_key] = market_info
76
+ end
77
+ end
78
+ end
79
+ result
80
+ end
81
+
82
+ def closed_markets
83
+ result = {}
84
+ @markets.each do |market_type, market_data|
85
+ market_data.each do |product_key, market_info|
86
+ unless market_info.open?
87
+ result[market_type] ||= {}
88
+ result[market_type][product_key] = market_info
89
+ end
90
+ end
91
+ end
92
+ result
93
+ end
94
+
95
+ def any_open?
96
+ @markets.any? do |_, market_data|
97
+ market_data.any? { |_, market_info| market_info.open? }
98
+ end
99
+ end
100
+
101
+ def all_closed?
102
+ !any_open?
103
+ end
104
+
105
+ def each_market(&block)
106
+ return enum_for(:each_market) unless block_given?
107
+ @markets.each do |market_type, market_data|
108
+ market_data.each do |product_key, market_info|
109
+ yield(market_type, product_key, market_info)
110
+ end
111
+ end
112
+ end
113
+
114
+ include Enumerable
115
+
116
+ def each(&block)
117
+ each_market(&block)
118
+ end
119
+
120
+ class MarketInfo
121
+ attr_reader :date, :market_type, :product, :product_name, :is_open, :session_hours
122
+
123
+ def initialize(data)
124
+ @date = data['date']
125
+ @market_type = data['marketType']
126
+ @product = data['product']
127
+ @product_name = data['productName']
128
+ @is_open = data['isOpen']
129
+ @session_hours = data['sessionHours'] ? SessionHours.new(data['sessionHours']) : nil
130
+ end
131
+
132
+ def to_h
133
+ result = {
134
+ 'date' => @date,
135
+ 'marketType' => @market_type,
136
+ 'product' => @product,
137
+ 'isOpen' => @is_open
138
+ }
139
+ result['productName'] = @product_name if @product_name
140
+ result['sessionHours'] = @session_hours.to_h if @session_hours
141
+ result
142
+ end
143
+
144
+ def open?
145
+ @is_open == true
146
+ end
147
+
148
+ def closed?
149
+ !open?
150
+ end
151
+
152
+ def date_object
153
+ Date.parse(@date) if @date
154
+ end
155
+
156
+ def has_session_hours?
157
+ !@session_hours.nil?
158
+ end
159
+
160
+ def regular_market_hours
161
+ return nil unless @session_hours
162
+ @session_hours.regular_market
163
+ end
164
+
165
+ def pre_market_hours
166
+ return nil unless @session_hours
167
+ @session_hours.pre_market
168
+ end
169
+
170
+ def post_market_hours
171
+ return nil unless @session_hours
172
+ @session_hours.post_market
173
+ end
174
+
175
+ def equity?
176
+ @market_type == 'EQUITY'
177
+ end
178
+
179
+ def option?
180
+ @market_type == 'OPTION'
181
+ end
182
+
183
+ def future?
184
+ @market_type == 'FUTURE'
185
+ end
186
+
187
+ def forex?
188
+ @market_type == 'FOREX'
189
+ end
190
+
191
+ def bond?
192
+ @market_type == 'BOND'
193
+ end
194
+ end
195
+
196
+ class SessionHours
197
+ attr_reader :regular_market, :pre_market, :post_market
198
+
199
+ def initialize(data)
200
+ @regular_market = parse_session_periods(data['regularMarket'])
201
+ @pre_market = parse_session_periods(data['preMarket'])
202
+ @post_market = parse_session_periods(data['postMarket'])
203
+ end
204
+
205
+ def to_h
206
+ result = {}
207
+ result['regularMarket'] = @regular_market.map(&:to_h) if @regular_market
208
+ result['preMarket'] = @pre_market.map(&:to_h) if @pre_market
209
+ result['postMarket'] = @post_market.map(&:to_h) if @post_market
210
+ result
211
+ end
212
+
213
+ def has_regular_market?
214
+ @regular_market && !@regular_market.empty?
215
+ end
216
+
217
+ def has_pre_market?
218
+ @pre_market && !@pre_market.empty?
219
+ end
220
+
221
+ def has_post_market?
222
+ @post_market && !@post_market.empty?
223
+ end
224
+
225
+ private
226
+
227
+ def parse_session_periods(periods_data)
228
+ return nil unless periods_data && periods_data.is_a?(Array)
229
+ periods_data.map { |period_data| SessionPeriod.new(period_data) }
230
+ end
231
+ end
232
+
233
+ class SessionPeriod
234
+ attr_reader :start_time, :end_time
235
+
236
+ def initialize(data)
237
+ @start_time = data['start']
238
+ @end_time = data['end']
239
+ end
240
+
241
+ def to_h
242
+ {
243
+ 'start' => @start_time,
244
+ 'end' => @end_time
245
+ }
246
+ end
247
+
248
+ def start_time_object
249
+ Time.parse(@start_time) if @start_time
250
+ end
251
+
252
+ def end_time_object
253
+ Time.parse(@end_time) if @end_time
254
+ end
255
+
256
+ def duration_minutes
257
+ return nil unless @start_time && @end_time
258
+ start_obj = start_time_object
259
+ end_obj = end_time_object
260
+ return nil unless start_obj && end_obj
261
+ ((end_obj - start_obj) / 60).to_i
262
+ end
263
+
264
+ def active_now?
265
+ return false unless @start_time && @end_time
266
+ now = Time.now
267
+ start_obj = start_time_object
268
+ end_obj = end_time_object
269
+ return false unless start_obj && end_obj
270
+ now >= start_obj && now <= end_obj
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'date'
5
+
6
+ module SchwabRb
7
+ module DataObjects
8
+ class Option
9
+ class << self
10
+ def build(underyling_symbol, data)
11
+ Option.new(
12
+ symbol: data.fetch(:symbol),
13
+ underlying_symbol: underyling_symbol,
14
+ description: data.fetch(:description),
15
+ strike: data.fetch(:strikePrice),
16
+ put_call: data.fetch(:putCall),
17
+ exchange_name: data.fetch(:exchangeName, nil),
18
+ bid: data.fetch(:bid),
19
+ ask: data.fetch(:ask),
20
+ last: data.fetch(:last),
21
+ mark: data.fetch(:mark),
22
+ bid_size: data.fetch(:bidSize, nil),
23
+ ask_size: data.fetch(:askSize, nil),
24
+ bid_ask_size: data.fetch(:bidAskSize, nil),
25
+ last_size: data.fetch(:lastSize, nil),
26
+ high_price: data.fetch(:highPrice, nil),
27
+ low_price: data.fetch(:lowPrice, nil),
28
+ open_price: data.fetch(:openPrice, nil),
29
+ close_price: data.fetch(:closePrice, nil),
30
+ total_volume: data.fetch(:totalVolume, nil),
31
+ trade_time_in_long: data.fetch(:tradeTimeInLong, nil),
32
+ quote_time_in_long: data.fetch(:quoteTimeInLong, nil),
33
+ net_change: data.fetch(:netChange, nil),
34
+ volatility: data.fetch(:volatility, nil),
35
+ delta: data.fetch(:delta, nil),
36
+ gamma: data.fetch(:gamma, nil),
37
+ theta: data.fetch(:theta, nil),
38
+ vega: data.fetch(:vega, nil),
39
+ rho: data.fetch(:rho, nil),
40
+ open_interest: data.fetch(:openInterest, nil),
41
+ time_value: data.fetch(:timeValue, nil),
42
+ theoretical_option_value: data.fetch(:theoreticalOptionValue, nil),
43
+ theoretical_volatility: data.fetch(:theoreticalVolatility, nil),
44
+ option_deliverables_list: data.fetch(:optionDeliverablesList, nil),
45
+ strike_price: data.fetch(:strikePrice, nil),
46
+ expiration_date: Date.parse(data.fetch(:expirationDate)),
47
+ days_to_expiration: data.fetch(:daysToExpiration, nil),
48
+ expiration_type: data.fetch(:expirationType, nil),
49
+ last_trading_day: data.fetch(:lastTradingDay, nil),
50
+ multiplier: data.fetch(:multiplier, nil),
51
+ settlement_type: data.fetch(:settlementType, nil),
52
+ deliverable_note: data.fetch(:deliverableNote, nil),
53
+ percent_change: data.fetch(:percentChange, nil),
54
+ mark_change: data.fetch(:markChange, nil),
55
+ mark_percent_change: data.fetch(:markPercentChange, nil),
56
+ intrinsic_value: data.fetch(:intrinsicValue, nil),
57
+ extrinsic_value: data.fetch(:extrinsicValue, nil),
58
+ option_root: data.fetch(:optionRoot, nil),
59
+ exercise_type: data.fetch(:exerciseType, nil),
60
+ high_52_week: data.fetch(:high52Week, nil),
61
+ low_52_week: data.fetch(:low52Week, nil),
62
+ non_standard: data.fetch(:nonStandard, nil),
63
+ in_the_money: data.fetch(:inTheMoney, nil)
64
+ )
65
+ end
66
+ end
67
+
68
+ def initialize(
69
+ symbol:, underlying_symbol:, description:, strike:, put_call:,
70
+ exchange_name:, bid:, ask:, last:, mark:, bid_size:, ask_size:,
71
+ bid_ask_size:, last_size:, high_price:, low_price:, open_price:,
72
+ close_price:, total_volume:, trade_time_in_long:,
73
+ quote_time_in_long:, net_change:, volatility:, delta:,
74
+ gamma:, theta:, vega:, rho:, open_interest:, time_value:,
75
+ theoretical_option_value:, theoretical_volatility:, option_deliverables_list:, strike_price:,
76
+ expiration_date:, days_to_expiration:, expiration_type:, last_trading_day:, multiplier:,
77
+ settlement_type:, deliverable_note:, percent_change:, mark_change:, mark_percent_change:, intrinsic_value:, extrinsic_value:, option_root:, exercise_type:, high_52_week:, low_52_week:, non_standard:, in_the_money:
78
+ )
79
+ @symbol = symbol
80
+ @underlying_symbol = underlying_symbol
81
+ @description = description
82
+ @strike = strike
83
+ @put_call = put_call
84
+ @exchange_name = exchange_name
85
+ @bid = bid
86
+ @ask = ask
87
+ @last = last
88
+ @mark = mark
89
+ @bid_size = bid_size
90
+ @ask_size = ask_size
91
+ @bid_ask_size = bid_ask_size
92
+ @last_size = last_size
93
+ @high_price = high_price
94
+ @low_price = low_price
95
+ @open_price = open_price
96
+ @close_price = close_price
97
+ @total_volume = total_volume
98
+ @trade_time_in_long = trade_time_in_long
99
+ @quote_time_in_long = quote_time_in_long
100
+ @net_change = net_change
101
+ @volatility = volatility
102
+ @delta = delta
103
+ @gamma = gamma
104
+ @theta = theta
105
+ @vega = vega
106
+ @rho = rho
107
+ @open_interest = open_interest
108
+ @time_value = time_value
109
+ @theoretical_option_value = theoretical_option_value
110
+ @theoretical_volatility = theoretical_volatility
111
+ @option_deliverables_list = option_deliverables_list
112
+ @strike_price = strike_price
113
+ @expiration_date = expiration_date
114
+ @days_to_expiration = days_to_expiration
115
+ @expiration_type = expiration_type
116
+ @last_trading_day = last_trading_day
117
+ @multiplier = multiplier
118
+ @settlement_type = settlement_type
119
+ @deliverable_note = deliverable_note
120
+ @percent_change = percent_change
121
+ @mark_change = mark_change
122
+ @mark_percent_change = mark_percent_change
123
+ @intrinsic_value = intrinsic_value
124
+ @extrinsic_value = extrinsic_value
125
+ @option_root = option_root
126
+ @exercise_type = exercise_type
127
+ @high_52_week = high_52_week
128
+ @low_52_week = low_52_week
129
+ @non_standard = non_standard
130
+ @in_the_money = in_the_money
131
+ end
132
+
133
+ attr_reader :symbol, :underlying_symbol, :description, :strike, :put_call,
134
+ :exchange_name, :bid, :ask, :last, :mark, :bid_size, :ask_size,
135
+ :bid_ask_size, :last_size, :high_price, :low_price, :open_price,
136
+ :close_price, :total_volume, :trade_time_in_long, :quote_time_in_long,
137
+ :net_change, :volatility, :delta, :gamma, :theta, :vega, :rho,
138
+ :open_interest, :time_value, :theoretical_option_value,
139
+ :theoretical_volatility, :option_deliverables_list, :strike_price,
140
+ :expiration_date, :days_to_expiration, :expiration_type, :last_trading_day,
141
+ :multiplier, :settlement_type, :deliverable_note, :percent_change,
142
+ :mark_change, :mark_percent_change, :intrinsic_value, :extrinsic_value,
143
+ :option_root, :exercise_type, :high_52_week, :low_52_week, :non_standard,
144
+ :in_the_money
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'date'
5
+ require_relative 'option'
6
+
7
+ module SchwabRb
8
+ module DataObjects
9
+ class OptionChain
10
+ class << self
11
+ def build(data)
12
+ underlying_symbol = data.fetch(:symbol)
13
+
14
+ call_dates = []
15
+ call_opts = []
16
+ data.fetch(:callExpDateMap).each do |exp_date, options|
17
+ call_dates << Date.strptime(exp_date.to_s.split(':').first, '%Y-%m-%d')
18
+ options.each_value do |opts|
19
+ opts.each do |option_data|
20
+ call_opts << Option.build(underlying_symbol, option_data)
21
+ end
22
+ end
23
+ end
24
+
25
+ put_dates = []
26
+ put_opts = []
27
+ data.fetch(:putExpDateMap).each do |exp_date, options|
28
+ put_dates << Date.strptime(exp_date.to_s.split(':').first, '%Y-%m-%d')
29
+
30
+ options.each_value do |opts|
31
+ opts.each do |option_data|
32
+ put_opts << Option.build(underlying_symbol, option_data)
33
+ end
34
+ end
35
+ end
36
+
37
+ new(
38
+ symbol: data.fetch(:symbol),
39
+ status: data.fetch(:status),
40
+ strategy: data.fetch(:strategy),
41
+ interval: data.fetch(:interval, nil),
42
+ is_delayed: data.fetch(:isDelayed, nil),
43
+ is_index: data.fetch(:isIndex, nil),
44
+ interest_rate: data.fetch(:interestRate, nil),
45
+ underlying_price: data.fetch(:underlyingPrice),
46
+ volatility: data.fetch(:volatility, nil),
47
+ days_to_expiration: data.fetch(:daysToExpiration),
48
+ asset_main_type: data.fetch(:assetMainType, nil),
49
+ asset_sub_type: data.fetch(:assetSubType, nil),
50
+ is_chain_truncated: data.fetch(:isChainTruncated, false),
51
+ call_dates: call_dates,
52
+ call_opts: call_opts,
53
+ put_dates: put_dates,
54
+ put_opts: put_opts
55
+ )
56
+ end
57
+ end
58
+
59
+ def initialize(
60
+ symbol:, status:, strategy:, interval:, is_delayed:, is_index:, interest_rate:, underlying_price:, volatility:, days_to_expiration:, asset_main_type:, asset_sub_type:, is_chain_truncated:, call_dates: [], call_opts: [], put_dates: [], put_opts: []
61
+ )
62
+ @symbol = symbol
63
+ @status = status
64
+ @strategy = strategy
65
+ @interval = interval
66
+ @is_delayed = is_delayed
67
+ @is_index = is_index
68
+ @interest_rate = interest_rate
69
+ @underlying_price = underlying_price
70
+ @volatility = volatility
71
+ @days_to_expiration = days_to_expiration
72
+ @asset_main_type = asset_main_type
73
+ @asset_sub_type = asset_sub_type
74
+ @is_chain_truncated = is_chain_truncated
75
+ @call_dates = call_dates
76
+ @call_opts = call_opts
77
+ @put_dates = put_dates
78
+ @put_opts = put_opts
79
+ end
80
+
81
+ attr_reader :symbol, :status, :strategy, :interval, :is_delayed, :is_index, :interest_rate, :underlying_price,
82
+ :volatility, :days_to_expiration, :asset_main_type, :asset_sub_type, :is_chain_truncated, :call_dates, :call_opts, :put_dates, :put_opts
83
+
84
+ def to_a(_date = nil)
85
+ call_opts.map do |copt|
86
+ [copt.expiration_date.strftime('%Y-%m-%d'), copt.put_call, copt.strike, copt.delta, copt.bid, copt.ask,
87
+ copt.mark]
88
+ end + put_opts.map do |popt|
89
+ [popt.expiration_date.strftime('%Y-%m-%d'), popt.put_call, popt.strike, popt.delta, popt.bid, popt.ask,
90
+ popt.mark]
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ module DataObjects
5
+ class OptionExpirationChain
6
+ attr_reader :expiration_list, :status
7
+
8
+ class << self
9
+ def build(data)
10
+ new(data)
11
+ end
12
+ end
13
+
14
+ def initialize(data)
15
+ @expiration_list = data['expirationList']&.map { |expiration_data| Expiration.new(expiration_data) } || []
16
+ @status = data['status']
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ 'expirationList' => @expiration_list.map(&:to_h),
22
+ 'status' => @status
23
+ }
24
+ end
25
+
26
+ def find_by_date(date)
27
+ date_str = date.is_a?(Date) ? date.strftime('%Y-%m-%d') : date.to_s
28
+ @expiration_list.find { |exp| exp.expiration_date == date_str }
29
+ end
30
+
31
+ def find_by_days_to_expiration(days)
32
+ @expiration_list.select { |exp| exp.days_to_expiration == days }
33
+ end
34
+
35
+ def weekly_expirations
36
+ @expiration_list.select { |exp| exp.expiration_type == 'W' }
37
+ end
38
+
39
+ def monthly_expirations
40
+ @expiration_list.select { |exp| exp.expiration_type == 'M' }
41
+ end
42
+
43
+ def quarterly_expirations
44
+ @expiration_list.select { |exp| exp.expiration_type == 'Q' }
45
+ end
46
+
47
+ def standard_expirations
48
+ @expiration_list.select(&:standard?)
49
+ end
50
+
51
+ def non_standard_expirations
52
+ @expiration_list.reject(&:standard?)
53
+ end
54
+
55
+ def count
56
+ @expiration_list.length
57
+ end
58
+ alias size count
59
+ alias length count
60
+
61
+ def empty?
62
+ @expiration_list.empty?
63
+ end
64
+
65
+ def each(&block)
66
+ return enum_for(:each) unless block_given?
67
+ @expiration_list.each(&block)
68
+ end
69
+
70
+ include Enumerable
71
+
72
+ class Expiration
73
+ attr_reader :expiration_date, :days_to_expiration, :expiration_type,
74
+ :settlement_type, :option_roots, :standard
75
+
76
+ def initialize(data)
77
+ @expiration_date = data['expirationDate']
78
+ @days_to_expiration = data['daysToExpiration']
79
+ @expiration_type = data['expirationType']
80
+ @settlement_type = data['settlementType']
81
+ @option_roots = data['optionRoots']
82
+ @standard = data['standard']
83
+ end
84
+
85
+ def to_h
86
+ {
87
+ 'expirationDate' => @expiration_date,
88
+ 'daysToExpiration' => @days_to_expiration,
89
+ 'expirationType' => @expiration_type,
90
+ 'settlementType' => @settlement_type,
91
+ 'optionRoots' => @option_roots,
92
+ 'standard' => @standard
93
+ }
94
+ end
95
+
96
+ def standard?
97
+ @standard == true
98
+ end
99
+
100
+ def weekly?
101
+ @expiration_type == 'W'
102
+ end
103
+
104
+ def monthly?
105
+ @expiration_type == 'M'
106
+ end
107
+
108
+ def quarterly?
109
+ @expiration_type == 'Q'
110
+ end
111
+
112
+ def special?
113
+ @expiration_type == 'S'
114
+ end
115
+
116
+ def date_object
117
+ Date.parse(@expiration_date) if @expiration_date
118
+ end
119
+
120
+ def expires_in_days?(days)
121
+ @days_to_expiration == days
122
+ end
123
+
124
+ def expires_today?
125
+ @days_to_expiration == 0
126
+ end
127
+
128
+ def expires_tomorrow?
129
+ @days_to_expiration == 1
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end