schwab_rb 0.3.12 → 0.4.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 +4 -4
- data/CHANGELOG.md +22 -0
- data/doc/ACCOUNT_MANAGEMENT.md +297 -0
- data/doc/PLACE_ORDER_SAMPLES.md +301 -0
- data/doc/QUICK_START.md +239 -0
- data/examples/place_oco_order.rb +75 -0
- data/lib/schwab_rb/account_hash_manager.rb +119 -0
- data/lib/schwab_rb/clients/base_client.rb +231 -115
- data/lib/schwab_rb/configuration.rb +7 -1
- data/lib/schwab_rb/orders/iron_condor_order.rb +14 -26
- data/lib/schwab_rb/orders/oco_order.rb +69 -0
- data/lib/schwab_rb/orders/order.rb +45 -2
- data/lib/schwab_rb/orders/order_factory.rb +21 -10
- data/lib/schwab_rb/orders/single_order.rb +16 -7
- data/lib/schwab_rb/orders/vertical_order.rb +23 -26
- data/lib/schwab_rb/version.rb +1 -1
- data/lib/schwab_rb.rb +2 -0
- metadata +8 -2
@@ -49,27 +49,33 @@ module SchwabRb
|
|
49
49
|
@token_manager.token_age
|
50
50
|
end
|
51
51
|
|
52
|
-
def get_account(account_hash, fields: nil, return_data_objects: true)
|
53
|
-
# Account balances, positions, and orders for a given account
|
52
|
+
def get_account(account_hash = nil, account_name: nil, fields: nil, return_data_objects: true)
|
53
|
+
# Account balances, positions, and orders for a given account.
|
54
54
|
#
|
55
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
56
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
55
57
|
# @param fields [Array] Balances displayed by default, additional fields can be
|
56
58
|
# added here by adding values from Account.fields.
|
57
59
|
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
58
|
-
|
60
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
59
61
|
|
60
|
-
|
62
|
+
with_account_hash_retry(resolved_hash) do
|
63
|
+
refresh_token_if_needed
|
61
64
|
|
62
|
-
|
63
|
-
params[:fields] = fields.join(",") if fields
|
65
|
+
fields = convert_enum_iterable(fields, SchwabRb::Account::Statuses) if fields
|
64
66
|
|
65
|
-
|
66
|
-
|
67
|
+
params = {}
|
68
|
+
params[:fields] = fields.join(",") if fields
|
67
69
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
path = "/trader/v1/accounts/#{resolved_hash}"
|
71
|
+
response = get(path, params)
|
72
|
+
|
73
|
+
if return_data_objects
|
74
|
+
account_data = JSON.parse(response.body, symbolize_names: true)
|
75
|
+
SchwabRb::DataObjects::Account.build(account_data)
|
76
|
+
else
|
77
|
+
response
|
78
|
+
end
|
73
79
|
end
|
74
80
|
end
|
75
81
|
|
@@ -109,46 +115,74 @@ module SchwabRb
|
|
109
115
|
path = "/trader/v1/accounts/accountNumbers"
|
110
116
|
response = get(path, {})
|
111
117
|
|
118
|
+
account_numbers_data = JSON.parse(response.body, symbolize_names: true)
|
119
|
+
|
120
|
+
begin
|
121
|
+
hash_manager = SchwabRb::AccountHashManager.new
|
122
|
+
hash_manager.update_hashes_from_api_response(account_numbers_data)
|
123
|
+
rescue SchwabRb::AccountHashManager::AccountNamesFileNotFoundError
|
124
|
+
# Silently skip if account names file doesn't exist - not all users will use this feature
|
125
|
+
end
|
126
|
+
|
112
127
|
if return_data_objects
|
113
|
-
account_numbers_data = JSON.parse(response.body, symbolize_names: true)
|
114
128
|
SchwabRb::DataObjects::AccountNumbers.build(account_numbers_data)
|
115
129
|
else
|
116
130
|
response
|
117
131
|
end
|
118
132
|
end
|
119
133
|
|
120
|
-
def
|
134
|
+
def available_account_names
|
135
|
+
# Returns a list of available account names from account_names.json
|
136
|
+
# Returns empty array if account_names.json doesn't exist
|
137
|
+
#
|
138
|
+
# @return [Array<String>] List of account names
|
139
|
+
hash_manager = SchwabRb::AccountHashManager.new
|
140
|
+
hash_manager.available_account_names
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_order(order_id, account_hash = nil, account_name: nil, return_data_objects: true)
|
121
144
|
# Get a specific order for a specific account by its order ID.
|
122
145
|
#
|
123
146
|
# @param order_id [String] The order ID.
|
124
|
-
# @param account_hash [String] The account hash
|
147
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
148
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
125
149
|
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
126
|
-
|
150
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
127
151
|
|
128
|
-
|
129
|
-
|
152
|
+
with_account_hash_retry(resolved_hash) do
|
153
|
+
refresh_token_if_needed
|
130
154
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
155
|
+
path = "/trader/v1/accounts/#{resolved_hash}/orders/#{order_id}"
|
156
|
+
response = get(path, {})
|
157
|
+
|
158
|
+
if return_data_objects
|
159
|
+
order_data = JSON.parse(response.body, symbolize_names: true)
|
160
|
+
SchwabRb::DataObjects::Order.build(order_data)
|
161
|
+
else
|
162
|
+
response
|
163
|
+
end
|
136
164
|
end
|
137
165
|
end
|
138
166
|
|
139
|
-
def cancel_order(order_id, account_hash)
|
167
|
+
def cancel_order(order_id, account_hash = nil, account_name: nil)
|
140
168
|
# Cancel a specific order for a specific account.
|
141
169
|
#
|
142
170
|
# @param order_id [String] The order ID.
|
143
|
-
# @param account_hash [String] The account hash
|
144
|
-
|
171
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
172
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
173
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
174
|
+
|
175
|
+
with_account_hash_retry(resolved_hash) do
|
176
|
+
refresh_token_if_needed
|
145
177
|
|
146
|
-
|
147
|
-
|
178
|
+
path = "/trader/v1/accounts/#{resolved_hash}/orders/#{order_id}"
|
179
|
+
delete(path)
|
180
|
+
end
|
148
181
|
end
|
149
182
|
|
150
183
|
def get_account_orders(
|
151
|
-
account_hash,
|
184
|
+
account_hash = nil,
|
185
|
+
account_name: nil,
|
152
186
|
max_results: nil,
|
153
187
|
from_entered_datetime: nil,
|
154
188
|
to_entered_datetime: nil,
|
@@ -157,34 +191,40 @@ module SchwabRb
|
|
157
191
|
)
|
158
192
|
# Orders for a specific account. Optionally specify a single status on which to filter.
|
159
193
|
#
|
194
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
195
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
160
196
|
# @param max_results [Integer] The maximum number of orders to retrieve.
|
161
197
|
# @param from_entered_datetime [DateTime] Start of the query date range (default: 60 days ago).
|
162
198
|
# @param to_entered_datetime [DateTime] End of the query date range (default: now).
|
163
199
|
# @param status [String] Restrict query to orders with this status.
|
164
200
|
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
165
|
-
|
201
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
166
202
|
|
167
|
-
|
203
|
+
with_account_hash_retry(resolved_hash) do
|
204
|
+
refresh_token_if_needed
|
168
205
|
|
169
|
-
|
206
|
+
from_entered_datetime = DateTime.now.new_offset(0) - 60 if from_entered_datetime.nil?
|
170
207
|
|
171
|
-
|
208
|
+
to_entered_datetime = DateTime.now if to_entered_datetime.nil?
|
172
209
|
|
173
|
-
|
174
|
-
params = make_order_query(
|
175
|
-
max_results: max_results,
|
176
|
-
from_entered_datetime: from_entered_datetime,
|
177
|
-
to_entered_datetime: to_entered_datetime,
|
178
|
-
status: status
|
179
|
-
)
|
210
|
+
status = convert_enum(status, SchwabRb::Order::Statuses) if status
|
180
211
|
|
181
|
-
|
212
|
+
path = "/trader/v1/accounts/#{resolved_hash}/orders"
|
213
|
+
params = make_order_query(
|
214
|
+
max_results: max_results,
|
215
|
+
from_entered_datetime: from_entered_datetime,
|
216
|
+
to_entered_datetime: to_entered_datetime,
|
217
|
+
status: status
|
218
|
+
)
|
182
219
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
220
|
+
response = get(path, params)
|
221
|
+
|
222
|
+
if return_data_objects
|
223
|
+
orders_data = JSON.parse(response.body, symbolize_names: true)
|
224
|
+
orders_data.map { |order_data| SchwabRb::DataObjects::Order.build(order_data) }
|
225
|
+
else
|
226
|
+
response
|
227
|
+
end
|
188
228
|
end
|
189
229
|
end
|
190
230
|
|
@@ -222,53 +262,79 @@ module SchwabRb
|
|
222
262
|
end
|
223
263
|
end
|
224
264
|
|
225
|
-
def place_order(account_hash,
|
265
|
+
def place_order(order_spec, account_hash = nil, account_name: nil)
|
226
266
|
# Place an order for a specific account. If order creation is successful,
|
227
267
|
# the response will contain the ID of the generated order.
|
228
268
|
#
|
269
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
270
|
+
# @param order_spec [Hash, SchwabRb::Orders::Builder] The order specification
|
271
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
272
|
+
#
|
229
273
|
# Note: Unlike most methods in this library, successful responses typically
|
230
274
|
# do not contain JSON data, and attempting to extract it may raise an exception.
|
231
|
-
|
275
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
232
276
|
|
233
|
-
|
277
|
+
with_account_hash_retry(resolved_hash) do
|
278
|
+
refresh_token_if_needed
|
234
279
|
|
235
|
-
|
236
|
-
|
280
|
+
order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
|
281
|
+
|
282
|
+
path = "/trader/v1/accounts/#{resolved_hash}/orders"
|
283
|
+
post(path, order_spec)
|
284
|
+
end
|
237
285
|
end
|
238
286
|
|
239
|
-
def replace_order(
|
287
|
+
def replace_order(order_id, order_spec, account_hash = nil, account_name: nil)
|
240
288
|
# Replace an existing order for an account.
|
241
289
|
# The existing order will be replaced by the new order.
|
242
290
|
# Once replaced, the old order will be canceled and a new order will be created.
|
243
|
-
|
291
|
+
#
|
292
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
293
|
+
# @param order_id [String] The order ID to replace
|
294
|
+
# @param order_spec [Hash, SchwabRb::Orders::Builder] The new order specification
|
295
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
296
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
297
|
+
|
298
|
+
with_account_hash_retry(resolved_hash) do
|
299
|
+
refresh_token_if_needed
|
244
300
|
|
245
|
-
|
301
|
+
order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
|
246
302
|
|
247
|
-
|
248
|
-
|
303
|
+
path = "/trader/v1/accounts/#{resolved_hash}/orders/#{order_id}"
|
304
|
+
put(path, order_spec)
|
305
|
+
end
|
249
306
|
end
|
250
307
|
|
251
|
-
def preview_order(account_hash,
|
308
|
+
def preview_order(order_spec, account_hash = nil, account_name: nil, return_data_objects: true)
|
252
309
|
# Preview an order, i.e., test whether an order would be accepted by the
|
253
310
|
# API and see the structure it would result in.
|
311
|
+
#
|
312
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
313
|
+
# @param order_spec [Hash, SchwabRb::Orders::Builder] The order specification to preview
|
314
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
254
315
|
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
255
|
-
|
316
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
256
317
|
|
257
|
-
|
318
|
+
with_account_hash_retry(resolved_hash) do
|
319
|
+
refresh_token_if_needed
|
258
320
|
|
259
|
-
|
260
|
-
response = post(path, order_spec)
|
321
|
+
order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
|
261
322
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
323
|
+
path = "/trader/v1/accounts/#{resolved_hash}/previewOrder"
|
324
|
+
response = post(path, order_spec)
|
325
|
+
|
326
|
+
if return_data_objects
|
327
|
+
preview_data = JSON.parse(response.body, symbolize_names: true)
|
328
|
+
SchwabRb::DataObjects::OrderPreview.build(preview_data)
|
329
|
+
else
|
330
|
+
response
|
331
|
+
end
|
267
332
|
end
|
268
333
|
end
|
269
334
|
|
270
335
|
def get_transactions(
|
271
|
-
account_hash,
|
336
|
+
account_hash = nil,
|
337
|
+
account_name: nil,
|
272
338
|
start_date: nil,
|
273
339
|
end_date: nil,
|
274
340
|
transaction_types: nil,
|
@@ -277,69 +343,78 @@ module SchwabRb
|
|
277
343
|
)
|
278
344
|
# Transactions for a specific account.
|
279
345
|
#
|
280
|
-
# @param account_hash [String]
|
346
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
347
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
281
348
|
# @param start_date [Date, DateTime] Start date for transactions (default: 60 days ago).
|
282
349
|
# @param end_date [Date, DateTime] End date for transactions (default: now).
|
283
350
|
# @param transaction_types [Array] List of transaction types to filter by.
|
284
351
|
# @param symbol [String] Filter transactions by the specified symbol.
|
285
352
|
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
353
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
354
|
+
|
355
|
+
with_account_hash_retry(resolved_hash) do
|
356
|
+
refresh_token_if_needed
|
357
|
+
|
358
|
+
transaction_types = if transaction_types
|
359
|
+
convert_enum_iterable(transaction_types, SchwabRb::Transaction::Types)
|
360
|
+
else
|
361
|
+
get_valid_enum_values(SchwabRb::Transaction::Types)
|
362
|
+
end
|
363
|
+
|
364
|
+
start_date = if start_date.nil?
|
365
|
+
format_date_as_iso("start_date", DateTime.now.new_offset(0) - 60)
|
366
|
+
else
|
367
|
+
format_date_as_iso("start_date", start_date)
|
368
|
+
end
|
369
|
+
|
370
|
+
end_date = if end_date.nil?
|
371
|
+
format_date_as_iso("end_date", DateTime.now.new_offset(0))
|
372
|
+
else
|
373
|
+
format_date_as_iso("end_date", end_date)
|
298
374
|
end
|
299
375
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
transactions_data = JSON.parse(response.body, symbolize_names: true)
|
318
|
-
transactions_data.map do |transaction_data|
|
319
|
-
SchwabRb::DataObjects::Transaction.build(transaction_data)
|
376
|
+
params = {
|
377
|
+
"types" => transaction_types.sort.join(","),
|
378
|
+
"startDate" => start_date,
|
379
|
+
"endDate" => end_date
|
380
|
+
}
|
381
|
+
params["symbol"] = symbol unless symbol.nil?
|
382
|
+
|
383
|
+
path = "/trader/v1/accounts/#{resolved_hash}/transactions"
|
384
|
+
response = get(path, params)
|
385
|
+
|
386
|
+
if return_data_objects
|
387
|
+
transactions_data = JSON.parse(response.body, symbolize_names: true)
|
388
|
+
transactions_data.map do |transaction_data|
|
389
|
+
SchwabRb::DataObjects::Transaction.build(transaction_data)
|
390
|
+
end
|
391
|
+
else
|
392
|
+
response
|
320
393
|
end
|
321
|
-
else
|
322
|
-
response
|
323
394
|
end
|
324
395
|
end
|
325
396
|
|
326
|
-
def get_transaction(account_hash,
|
397
|
+
def get_transaction(activity_id, account_hash = nil, account_name: nil, return_data_objects: true)
|
327
398
|
# Transaction for a specific account.
|
328
399
|
#
|
329
|
-
# @param account_hash [String]
|
330
|
-
#
|
331
|
-
# @param
|
400
|
+
# @param account_hash [String] The account hash (optional if account_name provided)
|
401
|
+
# @param activity_id [String] ID of the transaction to retrieve
|
402
|
+
# @param account_name [String] The account name from account_names.json (takes priority)
|
332
403
|
# @param return_data_objects [Boolean] Whether to return data objects or raw JSON
|
333
|
-
|
404
|
+
resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
|
334
405
|
|
335
|
-
|
336
|
-
|
406
|
+
with_account_hash_retry(resolved_hash) do
|
407
|
+
refresh_token_if_needed
|
337
408
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
409
|
+
path = "/trader/v1/accounts/#{resolved_hash}/transactions/#{activity_id}"
|
410
|
+
response = get(path, {})
|
411
|
+
|
412
|
+
if return_data_objects
|
413
|
+
transaction_data = JSON.parse(response.body, symbolize_names: true)
|
414
|
+
SchwabRb::DataObjects::Transaction.build(transaction_data)
|
415
|
+
else
|
416
|
+
response
|
417
|
+
end
|
343
418
|
end
|
344
419
|
end
|
345
420
|
|
@@ -855,6 +930,47 @@ module SchwabRb
|
|
855
930
|
|
856
931
|
private
|
857
932
|
|
933
|
+
# Resolves account identifier to actual account hash
|
934
|
+
# Accepts either account name (looked up in account_hashes.json) or direct hash
|
935
|
+
# Priority: account_name > account_hash
|
936
|
+
def resolve_account_hash(account_name: nil, account_hash: nil)
|
937
|
+
# If account_name is provided, look it up
|
938
|
+
if account_name
|
939
|
+
hash_manager = SchwabRb::AccountHashManager.new
|
940
|
+
resolved_hash = hash_manager.get_hash_by_name(account_name)
|
941
|
+
|
942
|
+
unless resolved_hash
|
943
|
+
raise ArgumentError,
|
944
|
+
"Account name '#{account_name}' not found in account hashes. " \
|
945
|
+
"Make sure get_account_numbers has been called to populate hashes."
|
946
|
+
end
|
947
|
+
|
948
|
+
return resolved_hash
|
949
|
+
end
|
950
|
+
|
951
|
+
# Fall back to account_hash if provided
|
952
|
+
if account_hash
|
953
|
+
return account_hash
|
954
|
+
end
|
955
|
+
|
956
|
+
# Neither was provided
|
957
|
+
raise ArgumentError, "Either account_name or account_hash must be provided"
|
958
|
+
end
|
959
|
+
|
960
|
+
# Wraps API calls that use account_hash with retry logic for stale hashes
|
961
|
+
def with_account_hash_retry(account_hash)
|
962
|
+
yield
|
963
|
+
rescue StandardError => e
|
964
|
+
# Try refreshing account hashes and retry once
|
965
|
+
begin
|
966
|
+
get_account_numbers
|
967
|
+
yield
|
968
|
+
rescue StandardError => retry_error
|
969
|
+
# If it fails again, raise the retry error
|
970
|
+
raise retry_error
|
971
|
+
end
|
972
|
+
end
|
973
|
+
|
858
974
|
def make_order_query(
|
859
975
|
max_results: nil,
|
860
976
|
from_entered_datetime: nil,
|
@@ -2,13 +2,19 @@
|
|
2
2
|
|
3
3
|
module SchwabRb
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :logger, :log_file, :log_level, :silence_output
|
5
|
+
attr_accessor :logger, :log_file, :log_level, :silence_output,
|
6
|
+
:schwab_home, :account_hashes_path, :account_names_path
|
6
7
|
|
7
8
|
def initialize
|
8
9
|
@logger = nil
|
9
10
|
@log_file = ENV.fetch("SCHWAB_LOGFILE", nil)
|
10
11
|
@log_level = ENV.fetch("SCHWAB_LOG_LEVEL", "WARN").upcase
|
11
12
|
@silence_output = ENV.fetch("SCHWAB_SILENCE_OUTPUT", "false").downcase == "true"
|
13
|
+
|
14
|
+
default_home = File.expand_path("~/.schwab_rb")
|
15
|
+
@schwab_home = ENV.fetch("SCHWAB_HOME", default_home)
|
16
|
+
@account_hashes_path = ENV.fetch("SCHWAB_ACCOUNT_HASHES_PATH", File.join(@schwab_home, "account_hashes.json"))
|
17
|
+
@account_names_path = ENV.fetch("SCHWAB_ACCOUNT_NAMES_PATH", File.join(@schwab_home, "account_names.json"))
|
12
18
|
end
|
13
19
|
|
14
20
|
def has_external_logger?
|
@@ -1,33 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "schwab_rb"
|
4
4
|
|
5
5
|
module SchwabRb
|
6
6
|
module Orders
|
7
7
|
class IronCondorOrder
|
8
8
|
class << self
|
9
9
|
def build(
|
10
|
-
account_number:,
|
11
10
|
put_short_symbol:,
|
12
11
|
put_long_symbol:,
|
13
12
|
call_short_symbol:,
|
14
13
|
call_long_symbol:,
|
15
14
|
price:,
|
15
|
+
stop_price: nil,
|
16
|
+
order_type: nil,
|
17
|
+
duration: SchwabRb::Orders::Duration::DAY,
|
16
18
|
credit_debit: :credit,
|
17
19
|
order_instruction: :open,
|
18
20
|
quantity: 1
|
19
21
|
)
|
20
22
|
schwab_order_builder.new.tap do |builder|
|
21
|
-
builder.
|
22
|
-
builder.set_order_strategy_type('SINGLE')
|
23
|
+
builder.set_order_strategy_type(SchwabRb::Order::OrderStrategyTypes::SINGLE)
|
23
24
|
builder.set_session(SchwabRb::Orders::Session::NORMAL)
|
24
|
-
builder.set_duration(
|
25
|
-
builder.set_order_type(order_type(credit_debit))
|
25
|
+
builder.set_duration(duration)
|
26
|
+
builder.set_order_type(order_type || determine_order_type(credit_debit))
|
26
27
|
builder.set_complex_order_strategy_type(SchwabRb::Order::ComplexOrderStrategyTypes::IRON_CONDOR)
|
27
28
|
builder.set_quantity(quantity)
|
28
29
|
builder.set_price(price)
|
30
|
+
builder.set_stop_price(stop_price) if stop_price && order_type == SchwabRb::Order::Types::STOP_LIMIT
|
29
31
|
|
30
|
-
instructions = leg_instructions_for_position(order_instruction
|
32
|
+
instructions = leg_instructions_for_position(order_instruction)
|
31
33
|
|
32
34
|
builder.add_option_leg(
|
33
35
|
instructions[:put_short],
|
@@ -52,7 +54,7 @@ module SchwabRb
|
|
52
54
|
end
|
53
55
|
end
|
54
56
|
|
55
|
-
def
|
57
|
+
def determine_order_type(credit_debit)
|
56
58
|
if credit_debit == :credit
|
57
59
|
SchwabRb::Order::Types::NET_CREDIT
|
58
60
|
else
|
@@ -60,37 +62,23 @@ module SchwabRb
|
|
60
62
|
end
|
61
63
|
end
|
62
64
|
|
63
|
-
def leg_instructions_for_position(order_instruction
|
64
|
-
if order_instruction == :open
|
65
|
+
def leg_instructions_for_position(order_instruction)
|
66
|
+
if order_instruction == :open
|
65
67
|
{
|
66
68
|
put_short: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
|
67
69
|
put_long: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN,
|
68
70
|
call_short: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
|
69
71
|
call_long: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN
|
70
72
|
}
|
71
|
-
elsif order_instruction == :
|
72
|
-
{
|
73
|
-
put_short: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN,
|
74
|
-
put_long: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
|
75
|
-
call_short: SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN,
|
76
|
-
call_long: SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN
|
77
|
-
}
|
78
|
-
elsif order_instruction == :close && credit_debit == :credit
|
73
|
+
elsif order_instruction == :close
|
79
74
|
{
|
80
75
|
put_short: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
|
81
76
|
put_long: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE,
|
82
77
|
call_short: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
|
83
78
|
call_long: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE
|
84
79
|
}
|
85
|
-
elsif order_instruction == :close && credit_debit == :debit
|
86
|
-
{
|
87
|
-
put_short: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE,
|
88
|
-
put_long: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
|
89
|
-
call_short: SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE,
|
90
|
-
call_long: SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE
|
91
|
-
}
|
92
80
|
else
|
93
|
-
raise "Unsupported order instruction: #{order_instruction}
|
81
|
+
raise "Unsupported order instruction: #{order_instruction}"
|
94
82
|
end
|
95
83
|
end
|
96
84
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'order_factory'
|
4
|
+
|
5
|
+
module SchwabRb
|
6
|
+
module Orders
|
7
|
+
class OcoOrder
|
8
|
+
class << self
|
9
|
+
# Build an OCO (One Cancels Another) order with 2 or more child orders
|
10
|
+
#
|
11
|
+
# @param child_order_specs [Array<Hash>] Array of order specifications for child orders.
|
12
|
+
# Each spec should include all parameters needed for OrderFactory.build
|
13
|
+
# @return [SchwabRb::Orders::Builder] A builder configured with OCO order structure
|
14
|
+
#
|
15
|
+
# @example Simple OCO with two equity orders
|
16
|
+
# OcoOrder.build(
|
17
|
+
# child_order_specs: [
|
18
|
+
# {
|
19
|
+
# strategy_type: 'single',
|
20
|
+
# symbol: 'XYZ 240315C00500000',
|
21
|
+
# price: 45.97,
|
22
|
+
# account_number: '12345',
|
23
|
+
# order_instruction: :close,
|
24
|
+
# credit_debit: :credit,
|
25
|
+
# quantity: 2
|
26
|
+
# },
|
27
|
+
# {
|
28
|
+
# strategy_type: 'single',
|
29
|
+
# symbol: 'XYZ 240315C00500000',
|
30
|
+
# price: 37.00,
|
31
|
+
# stop_price: 37.03,
|
32
|
+
# account_number: '12345',
|
33
|
+
# order_instruction: :close,
|
34
|
+
# credit_debit: :credit,
|
35
|
+
# quantity: 2
|
36
|
+
# }
|
37
|
+
# ]
|
38
|
+
# )
|
39
|
+
def build(child_order_specs:)
|
40
|
+
raise ArgumentError, "OCO orders require at least 2 child orders" if child_order_specs.length < 2
|
41
|
+
|
42
|
+
builder = schwab_order_builder.new
|
43
|
+
builder.set_order_strategy_type(SchwabRb::Order::OrderStrategyTypes::OCO)
|
44
|
+
|
45
|
+
# Build each child order using OrderFactory
|
46
|
+
child_order_specs.each do |child_spec|
|
47
|
+
child_order = build_child_order(child_spec)
|
48
|
+
builder.add_child_order_strategy(child_order)
|
49
|
+
end
|
50
|
+
|
51
|
+
builder
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def build_child_order(child_spec)
|
57
|
+
# Use OrderFactory to recursively build child orders
|
58
|
+
# This allows OCO orders to contain any type of order (single, vertical, iron condor, etc.)
|
59
|
+
|
60
|
+
OrderFactory.build(**child_spec)
|
61
|
+
end
|
62
|
+
|
63
|
+
def schwab_order_builder
|
64
|
+
SchwabRb::Orders::Builder
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|