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.
@@ -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 hash.
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
- refresh_token_if_needed
60
+ resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
59
61
 
60
- fields = convert_enum_iterable(fields, SchwabRb::Account::Statuses) if fields
62
+ with_account_hash_retry(resolved_hash) do
63
+ refresh_token_if_needed
61
64
 
62
- params = {}
63
- params[:fields] = fields.join(",") if fields
65
+ fields = convert_enum_iterable(fields, SchwabRb::Account::Statuses) if fields
64
66
 
65
- path = "/trader/v1/accounts/#{account_hash}"
66
- response = get(path, params)
67
+ params = {}
68
+ params[:fields] = fields.join(",") if fields
67
69
 
68
- if return_data_objects
69
- account_data = JSON.parse(response.body, symbolize_names: true)
70
- SchwabRb::DataObjects::Account.build(account_data)
71
- else
72
- response
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 get_order(order_id, account_hash, return_data_objects: true)
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
- refresh_token_if_needed
150
+ resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
127
151
 
128
- path = "/trader/v1/accounts/#{account_hash}/orders/#{order_id}"
129
- response = get(path, {})
152
+ with_account_hash_retry(resolved_hash) do
153
+ refresh_token_if_needed
130
154
 
131
- if return_data_objects
132
- order_data = JSON.parse(response.body, symbolize_names: true)
133
- SchwabRb::DataObjects::Order.build(order_data)
134
- else
135
- response
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
- refresh_token_if_needed
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
- path = "/trader/v1/accounts/#{account_hash}/orders/#{order_id}"
147
- delete(path)
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
- refresh_token_if_needed
201
+ resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
166
202
 
167
- from_entered_datetime = DateTime.now.new_offset(0) - 60 if from_entered_datetime.nil?
203
+ with_account_hash_retry(resolved_hash) do
204
+ refresh_token_if_needed
168
205
 
169
- to_entered_datetime = DateTime.now if to_entered_datetime.nil?
206
+ from_entered_datetime = DateTime.now.new_offset(0) - 60 if from_entered_datetime.nil?
170
207
 
171
- status = convert_enum(status, SchwabRb::Order::Statuses) if status
208
+ to_entered_datetime = DateTime.now if to_entered_datetime.nil?
172
209
 
173
- path = "/trader/v1/accounts/#{account_hash}/orders"
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
- response = get(path, params)
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
- if return_data_objects
184
- orders_data = JSON.parse(response.body, symbolize_names: true)
185
- orders_data.map { |order_data| SchwabRb::DataObjects::Order.build(order_data) }
186
- else
187
- response
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, order_spec)
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
- refresh_token_if_needed
275
+ resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
232
276
 
233
- order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
277
+ with_account_hash_retry(resolved_hash) do
278
+ refresh_token_if_needed
234
279
 
235
- path = "/trader/v1/accounts/#{account_hash}/orders"
236
- post(path, order_spec)
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(account_hash, order_id, order_spec)
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
- refresh_token_if_needed
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
- order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
301
+ order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
246
302
 
247
- path = "/trader/v1/accounts/#{account_hash}/orders/#{order_id}"
248
- put(path, order_spec)
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, order_spec, return_data_objects: true)
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
- refresh_token_if_needed
316
+ resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
256
317
 
257
- order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
318
+ with_account_hash_retry(resolved_hash) do
319
+ refresh_token_if_needed
258
320
 
259
- path = "/trader/v1/accounts/#{account_hash}/previewOrder"
260
- response = post(path, order_spec)
321
+ order_spec = order_spec.build if order_spec.is_a?(SchwabRb::Orders::Builder)
261
322
 
262
- if return_data_objects
263
- preview_data = JSON.parse(response.body, symbolize_names: true)
264
- SchwabRb::DataObjects::OrderPreview.build(preview_data)
265
- else
266
- response
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] Account hash corresponding to the account.
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
- refresh_token_if_needed
287
-
288
- transaction_types = if transaction_types
289
- convert_enum_iterable(transaction_types, SchwabRb::Transaction::Types)
290
- else
291
- get_valid_enum_values(SchwabRb::Transaction::Types)
292
- end
293
-
294
- start_date = if start_date.nil?
295
- format_date_as_iso("start_date", DateTime.now.new_offset(0) - 60)
296
- else
297
- format_date_as_iso("start_date", start_date)
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
- end_date = if end_date.nil?
301
- format_date_as_iso("end_date", DateTime.now.new_offset(0))
302
- else
303
- format_date_as_iso("end_date", end_date)
304
- end
305
-
306
- params = {
307
- "types" => transaction_types.sort.join(","),
308
- "startDate" => start_date,
309
- "endDate" => end_date
310
- }
311
- params["symbol"] = symbol unless symbol.nil?
312
-
313
- path = "/trader/v1/accounts/#{account_hash}/transactions"
314
- response = get(path, params)
315
-
316
- if return_data_objects
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, activity_id, return_data_objects: true)
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] Account hash corresponding to the account whose
330
- # transactions should be returned.
331
- # @param activity_id [String] ID of the order for which to return data.
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
- refresh_token_if_needed
404
+ resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
334
405
 
335
- path = "/trader/v1/accounts/#{account_hash}/transactions/#{activity_id}"
336
- response = get(path, {})
406
+ with_account_hash_retry(resolved_hash) do
407
+ refresh_token_if_needed
337
408
 
338
- if return_data_objects
339
- transaction_data = JSON.parse(response.body, symbolize_names: true)
340
- SchwabRb::DataObjects::Transaction.build(transaction_data)
341
- else
342
- response
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 'schwab_rb'
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.set_account_number(account_number)
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(SchwabRb::Orders::Duration::DAY)
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, credit_debit)
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 order_type(credit_debit)
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, credit_debit)
64
- if order_instruction == :open && credit_debit == :credit
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 == :open && credit_debit == :debit
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} with credit/debit: #{credit_debit}"
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