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,887 @@
|
|
1
|
+
require "date"
|
2
|
+
require "json"
|
3
|
+
require_relative "../utils/enum_enforcer"
|
4
|
+
require_relative "../data_objects/account"
|
5
|
+
require_relative "../data_objects/account_numbers"
|
6
|
+
require_relative "../data_objects/user_preferences"
|
7
|
+
require_relative "../data_objects/option_expiration_chain"
|
8
|
+
require_relative "../data_objects/price_history"
|
9
|
+
require_relative "../data_objects/market_hours"
|
10
|
+
require_relative "../data_objects/quote"
|
11
|
+
require_relative "../data_objects/transaction"
|
12
|
+
require_relative "../data_objects/order"
|
13
|
+
|
14
|
+
module SchwabRb
|
15
|
+
class BaseClient
|
16
|
+
include EnumEnforcer
|
17
|
+
|
18
|
+
attr_reader :api_key, :app_secret, :session, :token_manager, :enforce_enums
|
19
|
+
|
20
|
+
def initialize(api_key, app_secret, session, token_manager:, enforce_enums: true)
|
21
|
+
@api_key = api_key
|
22
|
+
@app_secret = app_secret
|
23
|
+
@session = session
|
24
|
+
@token_manager = token_manager
|
25
|
+
@enforce_enums = enforce_enums
|
26
|
+
end
|
27
|
+
|
28
|
+
def refresh!
|
29
|
+
refresh_token_if_needed
|
30
|
+
end
|
31
|
+
|
32
|
+
def timeout
|
33
|
+
@session.options[:connection_opts][:request][:timeout]
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_timeout(timeout)
|
37
|
+
# Sets the timeout for the client session.
|
38
|
+
#
|
39
|
+
# @param timeout [Integer] The timeout value in seconds.
|
40
|
+
@session.options[:connection_opts] ||= {}
|
41
|
+
@session.options[:connection_opts][:request] ||= {}
|
42
|
+
@session.options[:connection_opts][:request][:timeout] = timeout
|
43
|
+
end
|
44
|
+
|
45
|
+
def token_age
|
46
|
+
@token_manager.token_age
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_account(account_hash, fields: nil, return_data_objects: true)
|
50
|
+
# Account balances, positions, and orders for a given account hash.
|
51
|
+
#
|
52
|
+
# @param fields [Array] Balances displayed by default, additional fields can be
|
53
|
+
# added here by adding values from Account.fields.
|
54
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
55
|
+
refresh_token_if_needed
|
56
|
+
|
57
|
+
fields = convert_enum_iterable(fields, SchwabRb::Account::Statuses) if fields
|
58
|
+
|
59
|
+
params = {}
|
60
|
+
params[:fields] = fields.join(",") if fields
|
61
|
+
|
62
|
+
path = "/trader/v1/accounts/#{account_hash}"
|
63
|
+
response = get(path, params)
|
64
|
+
|
65
|
+
if return_data_objects
|
66
|
+
account_data = JSON.parse(response.body, symbolize_names: true)
|
67
|
+
SchwabRb::DataObjects::Account.build(account_data)
|
68
|
+
else
|
69
|
+
response
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_accounts(fields: nil, return_data_objects: true)
|
74
|
+
# Account balances, positions, and orders for all linked accounts.
|
75
|
+
#
|
76
|
+
# Note: This method does not return account hashes.
|
77
|
+
#
|
78
|
+
# @param fields [Array] Balances displayed by default, additional fields can be
|
79
|
+
# added here by adding values from Account.fields.
|
80
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
81
|
+
refresh_token_if_needed
|
82
|
+
|
83
|
+
fields = convert_enum_iterable(fields, SchwabRb::Account::Statuses) if fields
|
84
|
+
|
85
|
+
params = {}
|
86
|
+
params[:fields] = fields.join(",") if fields
|
87
|
+
|
88
|
+
path = "/trader/v1/accounts"
|
89
|
+
response = get(path, params)
|
90
|
+
|
91
|
+
if return_data_objects
|
92
|
+
accounts_data = JSON.parse(response.body, symbolize_names: true)
|
93
|
+
accounts_data.map { |account_data| SchwabRb::DataObjects::Account.build(account_data) }
|
94
|
+
else
|
95
|
+
response
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_account_numbers(return_data_objects: true)
|
100
|
+
# Returns a mapping from account IDs available to this token to the
|
101
|
+
# account hash that should be passed whenever referring to that account
|
102
|
+
# in API calls.
|
103
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
104
|
+
refresh_token_if_needed
|
105
|
+
|
106
|
+
path = "/trader/v1/accounts/accountNumbers"
|
107
|
+
response = get(path, {})
|
108
|
+
|
109
|
+
if return_data_objects
|
110
|
+
account_numbers_data = JSON.parse(response.body, symbolize_names: true)
|
111
|
+
SchwabRb::DataObjects::AccountNumbers.build(account_numbers_data)
|
112
|
+
else
|
113
|
+
response
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_order(order_id, account_hash, return_data_objects: true)
|
118
|
+
# Get a specific order for a specific account by its order ID.
|
119
|
+
#
|
120
|
+
# @param order_id [String] The order ID.
|
121
|
+
# @param account_hash [String] The account hash.
|
122
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
123
|
+
refresh_token_if_needed
|
124
|
+
|
125
|
+
path = "/trader/v1/accounts/#{account_hash}/orders/#{order_id}"
|
126
|
+
response = get(path, {})
|
127
|
+
|
128
|
+
if return_data_objects
|
129
|
+
order_data = JSON.parse(response.body, symbolize_names: true)
|
130
|
+
SchwabRb::DataObjects::Order.build(order_data)
|
131
|
+
else
|
132
|
+
response
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def cancel_order(order_id, account_hash)
|
137
|
+
# Cancel a specific order for a specific account.
|
138
|
+
#
|
139
|
+
# @param order_id [String] The order ID.
|
140
|
+
# @param account_hash [String] The account hash.
|
141
|
+
refresh_token_if_needed
|
142
|
+
|
143
|
+
path = "/trader/v1/accounts/#{account_hash}/orders/#{order_id}"
|
144
|
+
delete(path)
|
145
|
+
end
|
146
|
+
|
147
|
+
def get_account_orders(
|
148
|
+
account_hash,
|
149
|
+
max_results: nil,
|
150
|
+
from_entered_datetime: nil,
|
151
|
+
to_entered_datetime: nil,
|
152
|
+
status: nil,
|
153
|
+
return_data_objects: true
|
154
|
+
)
|
155
|
+
# Orders for a specific account. Optionally specify a single status on which to filter.
|
156
|
+
#
|
157
|
+
# @param max_results [Integer] The maximum number of orders to retrieve.
|
158
|
+
# @param from_entered_datetime [DateTime] Start of the query date range (default: 60 days ago).
|
159
|
+
# @param to_entered_datetime [DateTime] End of the query date range (default: now).
|
160
|
+
# @param status [String] Restrict query to orders with this status.
|
161
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
162
|
+
refresh_token_if_needed
|
163
|
+
|
164
|
+
if from_entered_datetime.nil?
|
165
|
+
from_entered_datetime = DateTime.now.new_offset(0) - 60
|
166
|
+
end
|
167
|
+
|
168
|
+
if to_entered_datetime.nil?
|
169
|
+
to_entered_datetime = DateTime.now
|
170
|
+
end
|
171
|
+
|
172
|
+
status = convert_enum(status, SchwabRb::Order::Statuses) if status
|
173
|
+
|
174
|
+
path = "/trader/v1/accounts/#{account_hash}/orders"
|
175
|
+
params = make_order_query(
|
176
|
+
max_results: max_results,
|
177
|
+
from_entered_datetime: from_entered_datetime,
|
178
|
+
to_entered_datetime: to_entered_datetime,
|
179
|
+
status: status
|
180
|
+
)
|
181
|
+
|
182
|
+
response = get(path, params)
|
183
|
+
|
184
|
+
if return_data_objects
|
185
|
+
orders_data = JSON.parse(response.body, symbolize_names: true)
|
186
|
+
orders_data.map { |order_data| SchwabRb::DataObjects::Order.build(order_data) }
|
187
|
+
else
|
188
|
+
response
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def get_all_linked_account_orders(
|
193
|
+
max_results: nil,
|
194
|
+
from_entered_datetime: nil,
|
195
|
+
to_entered_datetime: nil,
|
196
|
+
status: nil,
|
197
|
+
return_data_objects: true
|
198
|
+
)
|
199
|
+
# Orders for all linked accounts. Optionally specify a single status on which to filter.
|
200
|
+
#
|
201
|
+
# @param max_results [Integer] The maximum number of orders to retrieve.
|
202
|
+
# @param from_entered_datetime [DateTime] Start of the query date range (default: 60 days ago).
|
203
|
+
# @param to_entered_datetime [DateTime] End of the query date range (default: now).
|
204
|
+
# @param status [String] Restrict query to orders with this status.
|
205
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
206
|
+
refresh_token_if_needed
|
207
|
+
|
208
|
+
path = "/trader/v1/orders"
|
209
|
+
params = make_order_query(
|
210
|
+
max_results: max_results,
|
211
|
+
from_entered_datetime: from_entered_datetime,
|
212
|
+
to_entered_datetime: to_entered_datetime,
|
213
|
+
status: status
|
214
|
+
)
|
215
|
+
|
216
|
+
response = get(path, params)
|
217
|
+
|
218
|
+
if return_data_objects
|
219
|
+
orders_data = JSON.parse(response.body, symbolize_names: true)
|
220
|
+
orders_data.map { |order_data| SchwabRb::DataObjects::Order.build(order_data) }
|
221
|
+
else
|
222
|
+
response
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def place_order(account_hash, order_spec)
|
227
|
+
# Place an order for a specific account. If order creation is successful,
|
228
|
+
# the response will contain the ID of the generated order.
|
229
|
+
#
|
230
|
+
# Note: Unlike most methods in this library, successful responses typically
|
231
|
+
# do not contain JSON data, and attempting to extract it may raise an exception.
|
232
|
+
refresh_token_if_needed
|
233
|
+
|
234
|
+
order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
|
235
|
+
|
236
|
+
path = "/trader/v1/accounts/#{account_hash}/orders"
|
237
|
+
post(path, order_spec)
|
238
|
+
end
|
239
|
+
|
240
|
+
def replace_order(account_hash, order_id, order_spec)
|
241
|
+
# Replace an existing order for an account.
|
242
|
+
# The existing order will be replaced by the new order.
|
243
|
+
# Once replaced, the old order will be canceled and a new order will be created.
|
244
|
+
refresh_token_if_needed
|
245
|
+
|
246
|
+
order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
|
247
|
+
|
248
|
+
path = "/trader/v1/accounts/#{account_hash}/orders/#{order_id}"
|
249
|
+
put(path, order_spec)
|
250
|
+
end
|
251
|
+
|
252
|
+
def preview_order(account_hash, order_spec, return_data_objects: true)
|
253
|
+
# Preview an order, i.e., test whether an order would be accepted by the
|
254
|
+
# API and see the structure it would result in.
|
255
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
256
|
+
refresh_token_if_needed
|
257
|
+
|
258
|
+
order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
|
259
|
+
|
260
|
+
path = "/trader/v1/accounts/#{account_hash}/previewOrder"
|
261
|
+
response = post(path, order_spec)
|
262
|
+
|
263
|
+
if return_data_objects
|
264
|
+
preview_data = JSON.parse(response.body, symbolize_names: true)
|
265
|
+
SchwabRb::DataObjects::OrderPreview.build(preview_data)
|
266
|
+
else
|
267
|
+
response
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def get_transactions(
|
272
|
+
account_hash,
|
273
|
+
start_date: nil,
|
274
|
+
end_date: nil,
|
275
|
+
transaction_types: nil,
|
276
|
+
symbol: nil,
|
277
|
+
return_data_objects: true
|
278
|
+
)
|
279
|
+
# Transactions for a specific account.
|
280
|
+
#
|
281
|
+
# @param account_hash [String] Account hash corresponding to the account.
|
282
|
+
# @param start_date [Date, DateTime] Start date for transactions (default: 60 days ago).
|
283
|
+
# @param end_date [Date, DateTime] End date for transactions (default: now).
|
284
|
+
# @param transaction_types [Array] List of transaction types to filter by.
|
285
|
+
# @param symbol [String] Filter transactions by the specified symbol.
|
286
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
287
|
+
refresh_token_if_needed
|
288
|
+
|
289
|
+
transaction_types = if transaction_types
|
290
|
+
convert_enum_iterable(transaction_types, SchwabRb::Transaction::Types)
|
291
|
+
else
|
292
|
+
get_valid_enum_values(SchwabRb::Transaction::Types)
|
293
|
+
end
|
294
|
+
|
295
|
+
start_date = if start_date.nil?
|
296
|
+
format_date_as_iso("start_date", DateTime.now.new_offset(0) - 60)
|
297
|
+
else
|
298
|
+
format_date_as_iso("start_date", start_date)
|
299
|
+
end
|
300
|
+
|
301
|
+
end_date = if end_date.nil?
|
302
|
+
format_date_as_iso("end_date", DateTime.now.new_offset(0))
|
303
|
+
else
|
304
|
+
format_date_as_iso("end_date", end_date)
|
305
|
+
end
|
306
|
+
|
307
|
+
params = {
|
308
|
+
"types" => transaction_types.sort.join(","),
|
309
|
+
"startDate" => start_date,
|
310
|
+
"endDate" => end_date
|
311
|
+
}
|
312
|
+
params["symbol"] = symbol unless symbol.nil?
|
313
|
+
|
314
|
+
path = "/trader/v1/accounts/#{account_hash}/transactions"
|
315
|
+
response = get(path, params)
|
316
|
+
|
317
|
+
if return_data_objects
|
318
|
+
transactions_data = JSON.parse(response.body, symbolize_names: true)
|
319
|
+
transactions_data.map do |transaction_data|
|
320
|
+
SchwabRb::DataObjects::Transaction.build(transaction_data)
|
321
|
+
end
|
322
|
+
else
|
323
|
+
response
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def get_transaction(account_hash, activity_id, return_data_objects: true)
|
328
|
+
# Transaction for a specific account.
|
329
|
+
#
|
330
|
+
# @param account_hash [String] Account hash corresponding to the account whose
|
331
|
+
# transactions should be returned.
|
332
|
+
# @param activity_id [String] ID of the order for which to return data.
|
333
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
334
|
+
refresh_token_if_needed
|
335
|
+
|
336
|
+
path = "/trader/v1/accounts/#{account_hash}/transactions/#{activity_id}"
|
337
|
+
response = get(path, {})
|
338
|
+
|
339
|
+
if return_data_objects
|
340
|
+
transaction_data = JSON.parse(response.body, symbolize_names: true)
|
341
|
+
SchwabRb::DataObjects::Transaction.build(transaction_data)
|
342
|
+
else
|
343
|
+
response
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def get_user_preferences(return_data_objects: true)
|
348
|
+
# Get user preferences for the authenticated user.
|
349
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
350
|
+
refresh_token_if_needed
|
351
|
+
path = "/trader/v1/userPreference"
|
352
|
+
response = get(path, {})
|
353
|
+
|
354
|
+
if return_data_objects
|
355
|
+
preferences_data = JSON.parse(response.body, symbolize_names: true)
|
356
|
+
SchwabRb::DataObjects::UserPreferences.build(preferences_data)
|
357
|
+
else
|
358
|
+
response
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def get_quote(symbol, fields: nil, return_data_objects: true)
|
363
|
+
# Get quote for a symbol.
|
364
|
+
#
|
365
|
+
# @param symbol [String] Single symbol to fetch.
|
366
|
+
# @param fields [Array] Fields to request. If unset, return all available
|
367
|
+
# data (i.e., all fields). See `GetQuote::Field` for options.
|
368
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
369
|
+
refresh_token_if_needed
|
370
|
+
|
371
|
+
fields = convert_enum_iterable(fields, SchwabRb::Quote::Types) if fields
|
372
|
+
params = fields ? { "fields" => fields.join(",") } : {}
|
373
|
+
path = "/marketdata/v1/#{symbol}/quotes"
|
374
|
+
response = get(path, params)
|
375
|
+
|
376
|
+
if return_data_objects
|
377
|
+
quote_data = JSON.parse(response.body, symbolize_names: true)
|
378
|
+
SchwabRb::DataObjects::QuoteFactory.build(quote_data)
|
379
|
+
else
|
380
|
+
response
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def get_quotes(symbols, fields: nil, indicative: nil, return_data_objects: true)
|
385
|
+
# Get quotes for symbols. This method supports all symbols, including those
|
386
|
+
# containing non-alphanumeric characters like `/ES`.
|
387
|
+
#
|
388
|
+
# @param symbols [Array, String] Symbols to fetch. Can be a single symbol or an array of symbols.
|
389
|
+
# @param fields [Array] Fields to request. If unset, return all available data.
|
390
|
+
# See `GetQuote::Field` for options.
|
391
|
+
# @param indicative [Boolean] If set, fetch indicative quotes. Must be true or false.
|
392
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
393
|
+
refresh_token_if_needed
|
394
|
+
|
395
|
+
symbols = [symbols] if symbols.is_a?(String)
|
396
|
+
params = { "symbols" => symbols.join(",") }
|
397
|
+
fields = convert_enum_iterable(fields, SchwabRb::Quote::Types) if fields
|
398
|
+
params["fields"] = fields.join(",") if fields
|
399
|
+
|
400
|
+
unless indicative.nil?
|
401
|
+
unless [true, false].include?(indicative)
|
402
|
+
raise ArgumentError, "value of 'indicative' must be either true or false"
|
403
|
+
end
|
404
|
+
|
405
|
+
params["indicative"] = indicative ? "true" : "false"
|
406
|
+
end
|
407
|
+
|
408
|
+
path = "/marketdata/v1/quotes"
|
409
|
+
response = get(path, params)
|
410
|
+
|
411
|
+
if return_data_objects
|
412
|
+
quotes_data = JSON.parse(response.body, symbolize_names: true)
|
413
|
+
quotes_data.map do |symbol, quote_data|
|
414
|
+
SchwabRb::DataObjects::QuoteFactory.build({symbol => quote_data})
|
415
|
+
end
|
416
|
+
else
|
417
|
+
response
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def get_option_chain(
|
422
|
+
symbol,
|
423
|
+
contract_type: nil,
|
424
|
+
strike_count: nil,
|
425
|
+
include_underlying_quote: nil,
|
426
|
+
strategy: nil,
|
427
|
+
interval: nil,
|
428
|
+
strike: nil,
|
429
|
+
strike_range: nil,
|
430
|
+
from_date: nil,
|
431
|
+
to_date: nil,
|
432
|
+
volatility: nil,
|
433
|
+
underlying_price: nil,
|
434
|
+
interest_rate: nil,
|
435
|
+
days_to_expiration: nil,
|
436
|
+
exp_month: nil,
|
437
|
+
option_type: nil,
|
438
|
+
entitlement: nil,
|
439
|
+
return_data_objects: true
|
440
|
+
)
|
441
|
+
# Get option chain for an optionable symbol.
|
442
|
+
#
|
443
|
+
# @param symbol [String] The symbol for the option chain.
|
444
|
+
# @param contract_type [String] Type of contracts to return in the chain.
|
445
|
+
# @param strike_count [Integer] Number of strikes above and below the ATM price.
|
446
|
+
# @param include_underlying_quote [Boolean] Include a quote for the underlying.
|
447
|
+
# @param strategy [String] Strategy type for the option chain.
|
448
|
+
# @param interval [Float] Strike interval for spread strategy chains.
|
449
|
+
# @param strike [Float] Specific strike price for the option chain.
|
450
|
+
# @param strike_range [String] Range of strikes to include.
|
451
|
+
# @param from_date [Date] Filter expirations after this date.
|
452
|
+
# @param to_date [Date] Filter expirations before this date.
|
453
|
+
# @param volatility [Float] Volatility for analytical calculations.
|
454
|
+
# @param underlying_price [Float] Underlying price for analytical calculations.
|
455
|
+
# @param interest_rate [Float] Interest rate for analytical calculations.
|
456
|
+
# @param days_to_expiration [Integer] Days to expiration for analytical calculations.
|
457
|
+
# @param exp_month [String] Filter options by expiration month.
|
458
|
+
# @param option_type [String] Type of options to include in the chain.
|
459
|
+
# @param entitlement [String] Client entitlement.
|
460
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
461
|
+
|
462
|
+
refresh_token_if_needed
|
463
|
+
|
464
|
+
contract_type = convert_enum(contract_type, SchwabRb::Option::ContractTypes)
|
465
|
+
strategy = convert_enum(strategy, SchwabRb::Option::Strategies)
|
466
|
+
strike_range = convert_enum(strike_range, SchwabRb::Option::StrikeRanges)
|
467
|
+
option_type = convert_enum(option_type, SchwabRb::Option::Types)
|
468
|
+
exp_month = convert_enum(exp_month, SchwabRb::Option::ExpirationMonths)
|
469
|
+
entitlement = convert_enum(entitlement, SchwabRb::Option::Entitlements)
|
470
|
+
|
471
|
+
params = { "symbol" => symbol }
|
472
|
+
params["contractType"] = contract_type if contract_type
|
473
|
+
params["strikeCount"] = strike_count if strike_count
|
474
|
+
params["includeUnderlyingQuote"] = include_underlying_quote if include_underlying_quote
|
475
|
+
params["strategy"] = strategy if strategy
|
476
|
+
params["interval"] = interval if interval
|
477
|
+
params["strike"] = strike if strike
|
478
|
+
params["range"] = strike_range if strike_range
|
479
|
+
params["fromDate"] = format_date_as_day("from_date", from_date) if from_date
|
480
|
+
params["toDate"] = format_date_as_day("to_date", to_date) if to_date
|
481
|
+
params["volatility"] = volatility if volatility
|
482
|
+
params["underlyingPrice"] = underlying_price if underlying_price
|
483
|
+
params["interestRate"] = interest_rate if interest_rate
|
484
|
+
params["daysToExpiration"] = days_to_expiration if days_to_expiration
|
485
|
+
params["expMonth"] = exp_month if exp_month
|
486
|
+
params["optionType"] = option_type if option_type
|
487
|
+
params["entitlement"] = entitlement if entitlement
|
488
|
+
|
489
|
+
path = "/marketdata/v1/chains"
|
490
|
+
response = get(path, params)
|
491
|
+
|
492
|
+
if return_data_objects
|
493
|
+
option_chain_data = JSON.parse(response.body, symbolize_names: true)
|
494
|
+
SchwabRb::DataObjects::OptionChain.build(option_chain_data)
|
495
|
+
else
|
496
|
+
response
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
def get_option_expiration_chain(symbol, return_data_objects: true)
|
501
|
+
# Get option expiration chain for a symbol.
|
502
|
+
# @param symbol [String] The symbol for which to get option expiration dates.
|
503
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
504
|
+
refresh_token_if_needed
|
505
|
+
path = "/marketdata/v1/expirationchain"
|
506
|
+
response = get(path, { symbol: symbol })
|
507
|
+
|
508
|
+
if return_data_objects
|
509
|
+
expiration_data = JSON.parse(response.body, symbolize_names: true)
|
510
|
+
SchwabRb::DataObjects::OptionExpirationChain.build(expiration_data)
|
511
|
+
else
|
512
|
+
response
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
def get_price_history(
|
517
|
+
symbol,
|
518
|
+
period_type: nil,
|
519
|
+
period: nil,
|
520
|
+
frequency_type: nil,
|
521
|
+
frequency: nil,
|
522
|
+
start_datetime: nil,
|
523
|
+
end_datetime: nil,
|
524
|
+
need_extended_hours_data: nil,
|
525
|
+
need_previous_close: nil,
|
526
|
+
return_data_objects: true
|
527
|
+
)
|
528
|
+
# Get price history for a symbol.
|
529
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
530
|
+
refresh_token_if_needed
|
531
|
+
|
532
|
+
period_type = convert_enum(period_type, SchwabRb::PriceHistory::PeriodTypes) if period_type
|
533
|
+
period = convert_enum(period, SchwabRb::PriceHistory::Periods) if period
|
534
|
+
frequency_type = convert_enum(frequency_type, SchwabRb::PriceHistory::FrequencyTypes) if frequency_type
|
535
|
+
frequency = convert_enum(frequency, SchwabRb::PriceHistory::Frequencies) if frequency
|
536
|
+
|
537
|
+
params = { "symbol" => symbol }
|
538
|
+
|
539
|
+
params["periodType"] = period_type if period_type
|
540
|
+
params["period"] = period if period
|
541
|
+
params["frequencyType"] = frequency_type if frequency_type
|
542
|
+
params["frequency"] = frequency if frequency
|
543
|
+
params["startDate"] = format_date_as_millis("start_datetime", start_datetime) if start_datetime
|
544
|
+
params["endDate"] = format_date_as_millis("end_datetime", end_datetime) if end_datetime
|
545
|
+
params["needExtendedHoursData"] = need_extended_hours_data unless need_extended_hours_data.nil?
|
546
|
+
params["needPreviousClose"] = need_previous_close unless need_previous_close.nil?
|
547
|
+
path = "/marketdata/v1/pricehistory"
|
548
|
+
|
549
|
+
response = get(path, params)
|
550
|
+
|
551
|
+
if return_data_objects
|
552
|
+
price_history_data = JSON.parse(response.body, symbolize_names: true)
|
553
|
+
SchwabRb::DataObjects::PriceHistory.build(price_history_data)
|
554
|
+
else
|
555
|
+
response
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def get_price_history_every_minute(symbol,
|
560
|
+
start_datetime: nil,
|
561
|
+
end_datetime: nil,
|
562
|
+
need_extended_hours_data: nil,
|
563
|
+
need_previous_close: nil)
|
564
|
+
refresh_token_if_needed
|
565
|
+
|
566
|
+
start_datetime, end_datetime = normalize_start_and_end_datetimes(
|
567
|
+
start_datetime, end_datetime
|
568
|
+
)
|
569
|
+
|
570
|
+
get_price_history(
|
571
|
+
symbol,
|
572
|
+
period_type: SchwabRb::PriceHistory::PeriodType::DAY,
|
573
|
+
period: SchwabRb::PriceHistory::Period::ONE_DAY,
|
574
|
+
frequency_type: SchwabRb::PriceHistory::FrequencyType::MINUTE,
|
575
|
+
frequency: SchwabRb::PriceHistory::Frequency::EVERY_MINUTE,
|
576
|
+
start_datetime: start_datetime,
|
577
|
+
end_datetime: end_datetime,
|
578
|
+
need_extended_hours_data: need_extended_hours_data,
|
579
|
+
need_previous_close: need_previous_close
|
580
|
+
)
|
581
|
+
end
|
582
|
+
|
583
|
+
def get_price_history_every_five_minutes(symbol,
|
584
|
+
start_datetime: nil,
|
585
|
+
end_datetime: nil,
|
586
|
+
need_extended_hours_data: nil,
|
587
|
+
need_previous_close: nil)
|
588
|
+
refresh_token_if_needed
|
589
|
+
|
590
|
+
start_datetime, end_datetime = normalize_start_and_end_datetimes(
|
591
|
+
start_datetime, end_datetime
|
592
|
+
)
|
593
|
+
|
594
|
+
get_price_history(
|
595
|
+
symbol,
|
596
|
+
period_type: SchwabRb::PriceHistory::PeriodType::DAY,
|
597
|
+
period: SchwabRb::PriceHistory::Period::ONE_DAY,
|
598
|
+
frequency_type: SchwabRb::PriceHistory::FrequencyType::MINUTE,
|
599
|
+
frequency: SchwabRb::PriceHistory::Frequency::EVERY_FIVE_MINUTES,
|
600
|
+
start_datetime: start_datetime,
|
601
|
+
end_datetime: end_datetime,
|
602
|
+
need_extended_hours_data: need_extended_hours_data,
|
603
|
+
need_previous_close: need_previous_close
|
604
|
+
)
|
605
|
+
end
|
606
|
+
|
607
|
+
def get_price_history_every_ten_minutes(symbol,
|
608
|
+
start_datetime: nil,
|
609
|
+
end_datetime: nil,
|
610
|
+
need_extended_hours_data: nil,
|
611
|
+
need_previous_close: nil)
|
612
|
+
refresh_token_if_needed
|
613
|
+
|
614
|
+
start_datetime, end_datetime = normalize_start_and_end_datetimes(
|
615
|
+
start_datetime, end_datetime
|
616
|
+
)
|
617
|
+
|
618
|
+
get_price_history(
|
619
|
+
symbol,
|
620
|
+
period_type: SchwabRb::PriceHistory::PeriodType::DAY,
|
621
|
+
period: SchwabRb::PriceHistory::Period::ONE_DAY,
|
622
|
+
frequency_type: SchwabRb::PriceHistory::FrequencyType::MINUTE,
|
623
|
+
frequency: SchwabRb::PriceHistory::Frequency::EVERY_TEN_MINUTES,
|
624
|
+
start_datetime: start_datetime,
|
625
|
+
end_datetime: end_datetime,
|
626
|
+
need_extended_hours_data: need_extended_hours_data,
|
627
|
+
need_previous_close: need_previous_close
|
628
|
+
)
|
629
|
+
end
|
630
|
+
|
631
|
+
def get_price_history_every_fifteen_minutes(symbol,
|
632
|
+
start_datetime: nil,
|
633
|
+
end_datetime: nil,
|
634
|
+
need_extended_hours_data: nil,
|
635
|
+
need_previous_close: nil)
|
636
|
+
refresh_token_if_needed
|
637
|
+
|
638
|
+
start_datetime, end_datetime = normalize_start_and_end_datetimes(
|
639
|
+
start_datetime, end_datetime
|
640
|
+
)
|
641
|
+
|
642
|
+
get_price_history(
|
643
|
+
symbol,
|
644
|
+
period_type: SchwabRb::PriceHistory::PeriodType::DAY,
|
645
|
+
period: SchwabRb::PriceHistory::Period::ONE_DAY,
|
646
|
+
frequency_type: SchwabRb::PriceHistory::FrequencyType::MINUTE,
|
647
|
+
frequency: SchwabRb::PriceHistory::Frequency::EVERY_FIFTEEN_MINUTES,
|
648
|
+
start_datetime: start_datetime,
|
649
|
+
end_datetime: end_datetime,
|
650
|
+
need_extended_hours_data: need_extended_hours_data,
|
651
|
+
need_previous_close: need_previous_close
|
652
|
+
)
|
653
|
+
end
|
654
|
+
|
655
|
+
def get_price_history_every_thirty_minutes(symbol,
|
656
|
+
start_datetime: nil,
|
657
|
+
end_datetime: nil,
|
658
|
+
need_extended_hours_data: nil,
|
659
|
+
need_previous_close: nil)
|
660
|
+
refresh_token_if_needed
|
661
|
+
|
662
|
+
start_datetime, end_datetime = normalize_start_and_end_datetimes(
|
663
|
+
start_datetime, end_datetime
|
664
|
+
)
|
665
|
+
|
666
|
+
get_price_history(
|
667
|
+
symbol,
|
668
|
+
period_type: SchwabRb::PriceHistory::PeriodType::DAY,
|
669
|
+
period: SchwabRb::PriceHistory::Period::ONE_DAY,
|
670
|
+
frequency_type: SchwabRb::PriceHistory::FrequencyType::MINUTE,
|
671
|
+
frequency: SchwabRb::PriceHistory::Frequency::EVERY_THIRTY_MINUTES,
|
672
|
+
start_datetime: start_datetime,
|
673
|
+
end_datetime: end_datetime,
|
674
|
+
need_extended_hours_data: need_extended_hours_data,
|
675
|
+
need_previous_close: need_previous_close
|
676
|
+
)
|
677
|
+
end
|
678
|
+
|
679
|
+
def get_price_history_every_day(symbol,
|
680
|
+
start_datetime: nil,
|
681
|
+
end_datetime: nil,
|
682
|
+
need_extended_hours_data: nil,
|
683
|
+
need_previous_close: nil)
|
684
|
+
refresh_token_if_needed
|
685
|
+
|
686
|
+
start_datetime, end_datetime = normalize_start_and_end_datetimes(
|
687
|
+
start_datetime, end_datetime
|
688
|
+
)
|
689
|
+
|
690
|
+
get_price_history(
|
691
|
+
symbol,
|
692
|
+
period_type: SchwabRb::PriceHistory::PeriodType::YEAR,
|
693
|
+
period: SchwabRb::PriceHistory::Period::TWENTY_YEARS,
|
694
|
+
frequency_type: SchwabRb::PriceHistory::FrequencyType::DAILY,
|
695
|
+
frequency: SchwabRb::PriceHistory::Frequency::EVERY_MINUTE,
|
696
|
+
start_datetime: start_datetime,
|
697
|
+
end_datetime: end_datetime,
|
698
|
+
need_extended_hours_data: need_extended_hours_data,
|
699
|
+
need_previous_close: need_previous_close
|
700
|
+
)
|
701
|
+
end
|
702
|
+
|
703
|
+
def get_price_history_every_week(symbol,
|
704
|
+
start_datetime: nil,
|
705
|
+
end_datetime: nil,
|
706
|
+
need_extended_hours_data: nil,
|
707
|
+
need_previous_close: nil)
|
708
|
+
refresh_token_if_needed
|
709
|
+
|
710
|
+
start_datetime, end_datetime = normalize_start_and_end_datetimes(
|
711
|
+
start_datetime, end_datetime
|
712
|
+
)
|
713
|
+
|
714
|
+
get_price_history(
|
715
|
+
symbol,
|
716
|
+
period_type: SchwabRb::PriceHistory::PeriodType::YEAR,
|
717
|
+
period: SchwabRb::PriceHistory::Period::TWENTY_YEARS,
|
718
|
+
frequency_type: SchwabRb::PriceHistory::FrequencyType::WEEKLY,
|
719
|
+
frequency: SchwabRb::PriceHistory::Frequency::EVERY_MINUTE,
|
720
|
+
start_datetime: start_datetime,
|
721
|
+
end_datetime: end_datetime,
|
722
|
+
need_extended_hours_data: need_extended_hours_data,
|
723
|
+
need_previous_close: need_previous_close
|
724
|
+
)
|
725
|
+
end
|
726
|
+
|
727
|
+
def get_movers(index, sort_order: nil, frequency: nil)
|
728
|
+
# Get a list of the top ten movers for a given index.
|
729
|
+
#
|
730
|
+
# @param index [String] Category of mover. See Movers::Index for valid values.
|
731
|
+
# @param sort_order [String] Order in which to return values. See Movers::SortOrder for valid values.
|
732
|
+
# @param frequency [String] Only return movers that saw this magnitude or greater. See Movers::Frequency for valid values.
|
733
|
+
refresh_token_if_needed
|
734
|
+
|
735
|
+
index = convert_enum(index, SchwabRb::Movers::Indexes)
|
736
|
+
sort_order = convert_enum(sort_order, SchwabRb::Movers::SortOrders) if sort_order
|
737
|
+
frequency = convert_enum(frequency, SchwabRb::Movers::Frequencies) if frequency
|
738
|
+
|
739
|
+
path = "/marketdata/v1/movers/#{index}"
|
740
|
+
|
741
|
+
params = {}
|
742
|
+
params["sort"] = sort_order if sort_order
|
743
|
+
params["frequency"] = frequency.to_s if frequency
|
744
|
+
|
745
|
+
get(path, params)
|
746
|
+
end
|
747
|
+
|
748
|
+
def get_market_hours(markets, date: nil, return_data_objects: true)
|
749
|
+
# Retrieve market hours for specified markets.
|
750
|
+
#
|
751
|
+
# @param markets [Array, String] Markets for which to return trading hours.
|
752
|
+
# @param date [Date] Date for which to return market hours. Accepts values up to
|
753
|
+
# one year from today.
|
754
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
755
|
+
refresh_token_if_needed
|
756
|
+
|
757
|
+
markets = convert_enum_iterable(markets, SchwabRb::MarketHours::Markets)
|
758
|
+
|
759
|
+
params = { "markets" => markets.join(",") }
|
760
|
+
params["date"] = format_date_as_day("date", date) if date
|
761
|
+
|
762
|
+
response = get("/marketdata/v1/markets", params)
|
763
|
+
|
764
|
+
if return_data_objects
|
765
|
+
market_hours_data = JSON.parse(response.body, symbolize_names: true)
|
766
|
+
SchwabRb::DataObjects::MarketHours.build(market_hours_data)
|
767
|
+
else
|
768
|
+
response
|
769
|
+
end
|
770
|
+
end
|
771
|
+
|
772
|
+
def get_instruments(symbols, projection, return_data_objects: true)
|
773
|
+
# Get instrument details by using different search methods. Also used
|
774
|
+
# to get fundamental instrument data using the "FUNDAMENTAL" projection.
|
775
|
+
#
|
776
|
+
# @param symbols [String, Array] For "FUNDAMENTAL" projection, the symbols to fetch.
|
777
|
+
# For other projections, a search term.
|
778
|
+
# @param projection [String] Search mode or "FUNDAMENTAL" for instrument fundamentals.
|
779
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
780
|
+
refresh_token_if_needed
|
781
|
+
|
782
|
+
symbols = [symbols] unless symbols.is_a?(Array)
|
783
|
+
projection = convert_enum(projection, SchwabRb::Orders::Instrument::Projections)
|
784
|
+
params = {
|
785
|
+
"symbol" => symbols.join(","),
|
786
|
+
"projection" => projection
|
787
|
+
}
|
788
|
+
|
789
|
+
response = get("/marketdata/v1/instruments", params)
|
790
|
+
|
791
|
+
if return_data_objects
|
792
|
+
instruments_data = JSON.parse(response.body, symbolize_names: true)
|
793
|
+
instruments_data.map do |instrument_data|
|
794
|
+
SchwabRb::DataObjects::Instrument.build(instrument_data)
|
795
|
+
end
|
796
|
+
else
|
797
|
+
response
|
798
|
+
end
|
799
|
+
end
|
800
|
+
|
801
|
+
def get_instrument_by_cusip(cusip, return_data_objects: true)
|
802
|
+
# Get instrument information for a single instrument by CUSIP.
|
803
|
+
#
|
804
|
+
# @param cusip [String] CUSIP of the instrument to fetch. Leading zeroes must be preserved.
|
805
|
+
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
806
|
+
refresh_token_if_needed
|
807
|
+
|
808
|
+
raise ArgumentError, "cusip must be passed as a string" unless cusip.is_a?(String)
|
809
|
+
|
810
|
+
response = get("/marketdata/v1/instruments/#{cusip}", {})
|
811
|
+
|
812
|
+
if return_data_objects
|
813
|
+
instrument_data = JSON.parse(response.body, symbolize_names: true)
|
814
|
+
SchwabRb::DataObjects::Instrument.build(instrument_data)
|
815
|
+
else
|
816
|
+
response
|
817
|
+
end
|
818
|
+
end
|
819
|
+
|
820
|
+
private
|
821
|
+
|
822
|
+
def make_order_query(
|
823
|
+
max_results: nil,
|
824
|
+
from_entered_datetime: nil,
|
825
|
+
to_entered_datetime: nil,
|
826
|
+
status: nil
|
827
|
+
)
|
828
|
+
status = convert_enum(status, SchwabRb::Order::Statuses) if status
|
829
|
+
|
830
|
+
from_entered_datetime ||= (DateTime.now.new_offset(0) - 60) # 60 days ago (UTC)
|
831
|
+
to_entered_datetime ||= DateTime.now.new_offset(0) # Current UTC time
|
832
|
+
|
833
|
+
params = {
|
834
|
+
"fromEnteredTime" => format_date_as_iso("from_entered_datetime", from_entered_datetime),
|
835
|
+
"toEnteredTime" => format_date_as_iso("to_entered_datetime", to_entered_datetime)
|
836
|
+
}
|
837
|
+
|
838
|
+
params["maxResults"] = max_results if max_results
|
839
|
+
params["status"] = status if status
|
840
|
+
|
841
|
+
params
|
842
|
+
end
|
843
|
+
|
844
|
+
def format_date_as_iso(var_name, dt)
|
845
|
+
assert_type(var_name, dt, [Date, DateTime])
|
846
|
+
dt = DateTime.new(dt.year, dt.month, dt.day) unless dt.is_a?(DateTime)
|
847
|
+
dt.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
|
848
|
+
end
|
849
|
+
|
850
|
+
def format_date_as_day(var_name, date)
|
851
|
+
assert_type(var_name, date, [Date, DateTime])
|
852
|
+
date = Date.new(date.year, date.month, date.day) unless date.is_a?(Date)
|
853
|
+
date.strftime("%Y-%m-%d")
|
854
|
+
end
|
855
|
+
|
856
|
+
def format_date_as_millis(var_name, dt)
|
857
|
+
assert_type(var_name, dt, [Date, DateTime])
|
858
|
+
dt = DateTime.new(dt.year, dt.month, dt.day) unless dt.is_a?(DateTime)
|
859
|
+
(dt.to_time.to_f * 1000).to_i
|
860
|
+
end
|
861
|
+
|
862
|
+
def normalize_start_and_end_datetimes(start_datetime, end_datetime)
|
863
|
+
start_datetime ||= DateTime.new(1971, 1, 1)
|
864
|
+
end_datetime ||= DateTime.now + 7
|
865
|
+
|
866
|
+
[start_datetime, end_datetime]
|
867
|
+
end
|
868
|
+
|
869
|
+
def authorize_request(request)
|
870
|
+
request["Authorization"] = "Bearer #{@session.token}"
|
871
|
+
request
|
872
|
+
end
|
873
|
+
|
874
|
+
def refresh_token_if_needed
|
875
|
+
return unless session.expired?
|
876
|
+
|
877
|
+
new_session = token_manager.refresh_token(self)
|
878
|
+
@session = new_session
|
879
|
+
end
|
880
|
+
|
881
|
+
def assert_type(var_name, value, types)
|
882
|
+
return if types.any? { |type| value.is_a?(type) }
|
883
|
+
|
884
|
+
raise ArgumentError, "#{var_name} must be one of #{types.join(', ')}"
|
885
|
+
end
|
886
|
+
end
|
887
|
+
end
|