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.
- checksums.yaml +7 -0
- data/.copilotignore +4 -0
- data/.rspec +2 -0
- data/.rspec_status +292 -0
- data/.rubocop.yml +41 -0
- data/.rubocop_todo.yml +105 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +23 -0
- data/README.md +271 -0
- data/Rakefile +12 -0
- data/doc/notes/data_objects_analysis.md +223 -0
- data/doc/notes/data_objects_refactoring_plan.md +82 -0
- data/examples/fetch_account_numbers.rb +49 -0
- data/examples/fetch_user_preferences.rb +49 -0
- data/lib/schwab_rb/account.rb +9 -0
- data/lib/schwab_rb/auth/auth_context.rb +23 -0
- data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
- data/lib/schwab_rb/auth/init_client_login.rb +201 -0
- data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
- data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
- data/lib/schwab_rb/auth/token.rb +24 -0
- data/lib/schwab_rb/auth/token_manager.rb +105 -0
- data/lib/schwab_rb/clients/async_client.rb +122 -0
- data/lib/schwab_rb/clients/base_client.rb +887 -0
- data/lib/schwab_rb/clients/client.rb +97 -0
- data/lib/schwab_rb/configuration.rb +39 -0
- data/lib/schwab_rb/constants.rb +7 -0
- data/lib/schwab_rb/data_objects/account.rb +281 -0
- data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
- data/lib/schwab_rb/data_objects/instrument.rb +156 -0
- data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
- data/lib/schwab_rb/data_objects/option.rb +147 -0
- data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
- data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
- data/lib/schwab_rb/data_objects/order.rb +186 -0
- data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
- data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
- data/lib/schwab_rb/data_objects/position.rb +100 -0
- data/lib/schwab_rb/data_objects/price_history.rb +187 -0
- data/lib/schwab_rb/data_objects/quote.rb +276 -0
- data/lib/schwab_rb/data_objects/transaction.rb +132 -0
- data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
- data/lib/schwab_rb/market_hours.rb +13 -0
- data/lib/schwab_rb/movers.rb +35 -0
- data/lib/schwab_rb/option.rb +64 -0
- data/lib/schwab_rb/orders/builder.rb +202 -0
- data/lib/schwab_rb/orders/destination.rb +19 -0
- data/lib/schwab_rb/orders/duration.rb +9 -0
- data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
- data/lib/schwab_rb/orders/errors.rb +5 -0
- data/lib/schwab_rb/orders/instruments.rb +35 -0
- data/lib/schwab_rb/orders/option_instructions.rb +10 -0
- data/lib/schwab_rb/orders/order.rb +77 -0
- data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
- data/lib/schwab_rb/orders/price_link_type.rb +9 -0
- data/lib/schwab_rb/orders/session.rb +14 -0
- data/lib/schwab_rb/orders/special_instruction.rb +10 -0
- data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
- data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
- data/lib/schwab_rb/orders/stop_type.rb +11 -0
- data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
- data/lib/schwab_rb/price_history.rb +55 -0
- data/lib/schwab_rb/quote.rb +13 -0
- data/lib/schwab_rb/transaction.rb +23 -0
- data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
- data/lib/schwab_rb/utils/logger.rb +70 -0
- data/lib/schwab_rb/utils/redactor.rb +104 -0
- data/lib/schwab_rb/version.rb +5 -0
- data/lib/schwab_rb.rb +48 -0
- data/sig/schwab_rb.rbs +4 -0
- 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
|