schwab_rb 0.4.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9855a77823d8f9039e402281cec2efba85bd1bdba9b7ff7404a8ad136a276631
4
- data.tar.gz: fd0c1e0cc43e9c52c82e8f45fee14c919ea307b40c82195d8272ce921ca5defd
3
+ metadata.gz: 123cd13fd78778da2f5f2d08f9dc8f7594a07b8fcd93659bd49df3b1cd28ddb7
4
+ data.tar.gz: 0760e50de57a1a2d207cf1cc4744441f4a08fc2fc983859317feda22df701b15
5
5
  SHA512:
6
- metadata.gz: 1cd4e54dc2eace5a02ce328762f3ffaa4f23356684716d8d4987268c63d463b4cacbd51c8f278ec42cee8a73005bacd11a150db8e78beba3e34ae84c35e9e86e
7
- data.tar.gz: 0b3f8920a5fd5c6256e5a3d69da6a65d54a781e8773b0a1a8981f88418444d2ee1243060f73b5c57dc4f63f61eea14aa420fcae57a70c98d5a0c0ed02929c7dd
6
+ metadata.gz: bc5b094add1c5002f1a799ef41ca023b18e3857c39476c91282174640728fd7be0fe8f7de59a265cdf7c86a893a0c04dce91f7fb7551ee2818a5e371f9b1f792
7
+ data.tar.gz: f29fc656583df33e6ff787715713075fb0ffc094ca5ae81915fb37fca4c0dbbefa362488a687f6dcc9dd957dd65c65917b66ba98fc4401467139adf68ea03f4a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2025-12-11
4
+
5
+ ### Breaking Changes
6
+ - **OrderPreview**: Replaced `projected_commission` with `commission_and_fee` structure to match current Schwab API
7
+ - **OrderPreview**: Commission/fee data now uses nested `commissionLegs`/`feeLegs` instead of scalar values
8
+ - **OrderValidationResult**: Changed from `warningMessage` string to `warns` array of objects with `activity_message` and `original_severity`
9
+ - **OrderValidationResult**: Updated `Reject` structure to match `Warn` structure
10
+ - **BaseClient**: Methods with `return_data_objects: false` now return parsed Hash/Array instead of raw `OAuth2::Response` objects
11
+
12
+ ### Changed
13
+ - Removed redundant `strike_price` attribute from option instruments
14
+ - Removed account numbers from log messages for improved security
15
+
16
+ ## [0.5.0] - 2025-11-03
17
+
18
+ ### Added
19
+ - Vertical Roll Orders: Support for vertical roll orders that close an existing vertical spread and open a new one in a single order
20
+ - `VerticalRollOrder` class with 4-leg order structure for rolling positions
21
+ - Comprehensive tests for vertical roll orders including credit rolls, debit rolls, and stop limit orders
22
+
23
+ ### Fixed
24
+ - Fixed typo in `OrderFactory` (SchwabRbL → SchwabRb)
25
+
3
26
  ## [0.4.0] - 2025-10-19
4
27
 
5
28
  ### Added
@@ -162,7 +162,7 @@ Buy 10 shares of XYZ at a Limit price of $34.97 good for the Day. If filled, imm
162
162
 
163
163
  ## Conditional Order: One Cancels Another
164
164
 
165
- Sell 2 shares of XYZ at a Limit price of $45.97 and Sell 2 shares of XYZ with a Stop Limit order where the stop price is $37.03 and limit is $37.00. Both orders are sent at the same time. If one order fills, the other order is immediately cancelled. Both orders are good for the Day. Also known as an OCO order.
165
+ Sell 2 shares of XYZ at a Limit price of $45.97 and Sell 2 shares of XYZ with a Stop Limit order where the stop price is $37.03 and limit is $37.00. Both orders are sent at the same time. If one order fills, the other order is immediately cancelled. Both orders are good for the Day. Also known as an OCO order.
166
166
 
167
167
  ```json
168
168
  {
@@ -4,19 +4,19 @@
4
4
  # Script to fetch account numbers data and save as fixture
5
5
  # Usage: ruby examples/fetch_account_numbers.rb
6
6
 
7
- require_relative "../lib/schwab_rb"
8
- require "dotenv"
9
- require "json"
10
- require "fileutils"
7
+ require_relative '../lib/schwab_rb'
8
+ require 'dotenv'
9
+ require 'json'
10
+ require 'fileutils'
11
11
 
12
12
  Dotenv.load
13
13
 
14
14
  def create_client
15
- token_path = ENV["TOKEN_PATH"] || "schwab_token.json"
15
+ token_path = ENV['TOKEN_PATH'] || 'schwab_token.json'
16
16
  SchwabRb::Auth.init_client_easy(
17
- ENV.fetch("SCHWAB_API_KEY", nil),
18
- ENV.fetch("SCHWAB_APP_SECRET", nil),
19
- ENV.fetch("APP_CALLBACK_URL", nil),
17
+ ENV['SCHWAB_API_KEY'],
18
+ ENV['SCHWAB_APP_SECRET'],
19
+ ENV['APP_CALLBACK_URL'],
20
20
  token_path
21
21
  )
22
22
  end
@@ -24,23 +24,26 @@ end
24
24
  def fetch_account_numbers
25
25
  client = create_client
26
26
  puts "Fetching account numbers..."
27
-
27
+
28
28
  response = client.get_account_numbers
29
29
  parsed_data = JSON.parse(response.body, symbolize_names: true)
30
-
30
+
31
31
  # Create fixtures directory if it doesn't exist
32
- fixtures_dir = File.join(__dir__, "..", "spec", "fixtures")
32
+ fixtures_dir = File.join(__dir__, '..', 'spec', 'fixtures')
33
33
  FileUtils.mkdir_p(fixtures_dir)
34
-
34
+
35
35
  # Save the raw response
36
- fixture_file = File.join(fixtures_dir, "account_numbers.json")
36
+ fixture_file = File.join(fixtures_dir, 'account_numbers.json')
37
37
  File.write(fixture_file, JSON.pretty_generate(parsed_data))
38
-
38
+
39
39
  puts "Account numbers data saved to: #{fixture_file}"
40
40
  puts "Sample data: #{parsed_data.first(2)}"
41
- rescue StandardError => e
41
+
42
+ rescue => e
42
43
  puts "Error fetching account numbers: #{e.message}"
43
44
  puts e.backtrace.first(3)
44
45
  end
45
46
 
46
- fetch_account_numbers if __FILE__ == $PROGRAM_NAME
47
+ if __FILE__ == $0
48
+ fetch_account_numbers
49
+ end
@@ -4,19 +4,19 @@
4
4
  # Script to fetch user preferences data and save as fixture
5
5
  # Usage: ruby examples/fetch_user_preferences.rb
6
6
 
7
- require_relative "../lib/schwab_rb"
8
- require "dotenv"
9
- require "json"
10
- require "fileutils"
7
+ require_relative '../lib/schwab_rb'
8
+ require 'dotenv'
9
+ require 'json'
10
+ require 'fileutils'
11
11
 
12
12
  Dotenv.load
13
13
 
14
14
  def create_client
15
- token_path = ENV["TOKEN_PATH"] || "schwab_token.json"
15
+ token_path = ENV['TOKEN_PATH'] || 'schwab_token.json'
16
16
  SchwabRb::Auth.init_client_easy(
17
- ENV.fetch("SCHWAB_API_KEY", nil),
18
- ENV.fetch("SCHWAB_APP_SECRET", nil),
19
- ENV.fetch("APP_CALLBACK_URL", nil),
17
+ ENV['SCHWAB_API_KEY'],
18
+ ENV['SCHWAB_APP_SECRET'],
19
+ ENV['APP_CALLBACK_URL'],
20
20
  token_path
21
21
  )
22
22
  end
@@ -24,23 +24,26 @@ end
24
24
  def fetch_user_preferences
25
25
  client = create_client
26
26
  puts "Fetching user preferences..."
27
-
27
+
28
28
  response = client.get_user_preferences
29
29
  parsed_data = JSON.parse(response.body, symbolize_names: true)
30
-
30
+
31
31
  # Create fixtures directory if it doesn't exist
32
- fixtures_dir = File.join(__dir__, "..", "spec", "fixtures")
32
+ fixtures_dir = File.join(__dir__, '..', 'spec', 'fixtures')
33
33
  FileUtils.mkdir_p(fixtures_dir)
34
-
34
+
35
35
  # Save the raw response
36
- fixture_file = File.join(fixtures_dir, "user_preferences.json")
36
+ fixture_file = File.join(fixtures_dir, 'user_preferences.json')
37
37
  File.write(fixture_file, JSON.pretty_generate(parsed_data))
38
-
38
+
39
39
  puts "User preferences data saved to: #{fixture_file}"
40
40
  puts "Sample data keys: #{parsed_data.keys.first(5)}"
41
- rescue StandardError => e
41
+
42
+ rescue => e
42
43
  puts "Error fetching user preferences: #{e.message}"
43
44
  puts e.backtrace.first(3)
44
45
  end
45
46
 
46
- fetch_user_preferences if __FILE__ == $PROGRAM_NAME
47
+ if __FILE__ == $0
48
+ fetch_user_preferences
49
+ end
@@ -46,30 +46,25 @@ oco_order = SchwabRb::Orders::OrderFactory.build(
46
46
  short_leg_symbol: "SPXW 251020P06530000",
47
47
  long_leg_symbol: "SPXW 251020P06510000",
48
48
  order_type: SchwabRb::Order::Types::STOP_LIMIT,
49
- price: 2.1,
50
- stop_price: 2.0,
49
+ stop:
50
+ price: 0.3,
51
51
  order_instruction: :close,
52
52
  credit_debit: :debit,
53
- quantity: 2
53
+ quantity: 1
54
54
  },
55
55
  {
56
56
  strategy_type: SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL,
57
57
  short_leg_symbol: "SPXW 251020C06770000",
58
58
  long_leg_symbol: "SPXW 251020C06790000",
59
59
  order_type: SchwabRb::Order::Types::STOP_LIMIT,
60
- price: 2.1,
61
- stop_price: 2.0,
60
+ price: 0.3,
62
61
  order_instruction: :close,
63
62
  credit_debit: :debit,
64
- quantity: 2
63
+ quantity: 1
65
64
  }
66
65
  ]
67
66
  )
68
67
 
69
68
  built_order = oco_order.build
70
69
 
71
- binding.pry
72
-
73
70
  response = client.place_order(built_order, account_name: CURRENT_ACCT)
74
-
75
- binding.pry
@@ -49,7 +49,7 @@ module SchwabRb
49
49
  if missing_accounts.any?
50
50
  missing_accounts.each do |account|
51
51
  SchwabRb::Logger.logger.warn(
52
- "Account '#{account[:name]}' (#{account[:number]}) not found in API response. " \
52
+ "Account '#{account[:name]}' not found in API response. " \
53
53
  "This may indicate a closed account or incorrect account number in account_names.json"
54
54
  )
55
55
  end
@@ -56,7 +56,7 @@ module SchwabRb
56
56
  # @param account_name [String] The account name from account_names.json (takes priority)
57
57
  # @param fields [Array] Balances displayed by default, additional fields can be
58
58
  # added here by adding values from Account.fields.
59
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
59
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
60
60
  resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
61
61
 
62
62
  with_account_hash_retry(resolved_hash) do
@@ -69,12 +69,12 @@ module SchwabRb
69
69
 
70
70
  path = "/trader/v1/accounts/#{resolved_hash}"
71
71
  response = get(path, params)
72
+ data = JSON.parse(response.body, symbolize_names: true)
72
73
 
73
74
  if return_data_objects
74
- account_data = JSON.parse(response.body, symbolize_names: true)
75
- SchwabRb::DataObjects::Account.build(account_data)
75
+ SchwabRb::DataObjects::Account.build(data)
76
76
  else
77
- response
77
+ data
78
78
  end
79
79
  end
80
80
  end
@@ -86,7 +86,7 @@ module SchwabRb
86
86
  #
87
87
  # @param fields [Array] Balances displayed by default, additional fields can be
88
88
  # added here by adding values from Account.fields.
89
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
89
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
90
90
  refresh_token_if_needed
91
91
 
92
92
  fields = convert_enum_iterable(fields, SchwabRb::Account::Statuses) if fields
@@ -96,12 +96,12 @@ module SchwabRb
96
96
 
97
97
  path = "/trader/v1/accounts"
98
98
  response = get(path, params)
99
+ data = JSON.parse(response.body, symbolize_names: true)
99
100
 
100
101
  if return_data_objects
101
- accounts_data = JSON.parse(response.body, symbolize_names: true)
102
- accounts_data.map { |account_data| SchwabRb::DataObjects::Account.build(account_data) }
102
+ data.map { |account_data| SchwabRb::DataObjects::Account.build(account_data) }
103
103
  else
104
- response
104
+ data
105
105
  end
106
106
  end
107
107
 
@@ -109,7 +109,7 @@ module SchwabRb
109
109
  # Returns a mapping from account IDs available to this token to the
110
110
  # account hash that should be passed whenever referring to that account
111
111
  # in API calls.
112
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
112
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
113
113
  refresh_token_if_needed
114
114
 
115
115
  path = "/trader/v1/accounts/accountNumbers"
@@ -127,7 +127,7 @@ module SchwabRb
127
127
  if return_data_objects
128
128
  SchwabRb::DataObjects::AccountNumbers.build(account_numbers_data)
129
129
  else
130
- response
130
+ account_numbers_data
131
131
  end
132
132
  end
133
133
 
@@ -146,7 +146,7 @@ module SchwabRb
146
146
  # @param order_id [String] The order ID.
147
147
  # @param account_hash [String] The account hash (optional if account_name provided)
148
148
  # @param account_name [String] The account name from account_names.json (takes priority)
149
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
149
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
150
150
  resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
151
151
 
152
152
  with_account_hash_retry(resolved_hash) do
@@ -154,12 +154,12 @@ module SchwabRb
154
154
 
155
155
  path = "/trader/v1/accounts/#{resolved_hash}/orders/#{order_id}"
156
156
  response = get(path, {})
157
+ data = JSON.parse(response.body, symbolize_names: true)
157
158
 
158
159
  if return_data_objects
159
- order_data = JSON.parse(response.body, symbolize_names: true)
160
- SchwabRb::DataObjects::Order.build(order_data)
160
+ SchwabRb::DataObjects::Order.build(data)
161
161
  else
162
- response
162
+ data
163
163
  end
164
164
  end
165
165
  end
@@ -197,7 +197,7 @@ module SchwabRb
197
197
  # @param from_entered_datetime [DateTime] Start of the query date range (default: 60 days ago).
198
198
  # @param to_entered_datetime [DateTime] End of the query date range (default: now).
199
199
  # @param status [String] Restrict query to orders with this status.
200
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
200
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
201
201
  resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
202
202
 
203
203
  with_account_hash_retry(resolved_hash) do
@@ -218,12 +218,12 @@ module SchwabRb
218
218
  )
219
219
 
220
220
  response = get(path, params)
221
+ data = JSON.parse(response.body, symbolize_names: true)
221
222
 
222
223
  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) }
224
+ data.map { |order_data| SchwabRb::DataObjects::Order.build(order_data) }
225
225
  else
226
- response
226
+ data
227
227
  end
228
228
  end
229
229
  end
@@ -241,7 +241,7 @@ module SchwabRb
241
241
  # @param from_entered_datetime [DateTime] Start of the query date range (default: 60 days ago).
242
242
  # @param to_entered_datetime [DateTime] End of the query date range (default: now).
243
243
  # @param status [String] Restrict query to orders with this status.
244
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
244
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
245
245
  refresh_token_if_needed
246
246
 
247
247
  path = "/trader/v1/orders"
@@ -253,12 +253,12 @@ module SchwabRb
253
253
  )
254
254
 
255
255
  response = get(path, params)
256
+ data = JSON.parse(response.body, symbolize_names: true)
256
257
 
257
258
  if return_data_objects
258
- orders_data = JSON.parse(response.body, symbolize_names: true)
259
- orders_data.map { |order_data| SchwabRb::DataObjects::Order.build(order_data) }
259
+ data.map { |order_data| SchwabRb::DataObjects::Order.build(order_data) }
260
260
  else
261
- response
261
+ data
262
262
  end
263
263
  end
264
264
 
@@ -312,7 +312,7 @@ module SchwabRb
312
312
  # @param account_hash [String] The account hash (optional if account_name provided)
313
313
  # @param order_spec [Hash, SchwabRb::Orders::Builder] The order specification to preview
314
314
  # @param account_name [String] The account name from account_names.json (takes priority)
315
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
315
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
316
316
  resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
317
317
 
318
318
  with_account_hash_retry(resolved_hash) do
@@ -322,12 +322,12 @@ module SchwabRb
322
322
 
323
323
  path = "/trader/v1/accounts/#{resolved_hash}/previewOrder"
324
324
  response = post(path, order_spec)
325
+ data = JSON.parse(response.body, symbolize_names: true)
325
326
 
326
327
  if return_data_objects
327
- preview_data = JSON.parse(response.body, symbolize_names: true)
328
- SchwabRb::DataObjects::OrderPreview.build(preview_data)
328
+ SchwabRb::DataObjects::OrderPreview.build(data)
329
329
  else
330
- response
330
+ data
331
331
  end
332
332
  end
333
333
  end
@@ -349,7 +349,7 @@ module SchwabRb
349
349
  # @param end_date [Date, DateTime] End date for transactions (default: now).
350
350
  # @param transaction_types [Array] List of transaction types to filter by.
351
351
  # @param symbol [String] Filter transactions by the specified symbol.
352
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
352
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
353
353
  resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
354
354
 
355
355
  with_account_hash_retry(resolved_hash) do
@@ -382,14 +382,14 @@ module SchwabRb
382
382
 
383
383
  path = "/trader/v1/accounts/#{resolved_hash}/transactions"
384
384
  response = get(path, params)
385
+ data = JSON.parse(response.body, symbolize_names: true)
385
386
 
386
387
  if return_data_objects
387
- transactions_data = JSON.parse(response.body, symbolize_names: true)
388
- transactions_data.map do |transaction_data|
388
+ data.map do |transaction_data|
389
389
  SchwabRb::DataObjects::Transaction.build(transaction_data)
390
390
  end
391
391
  else
392
- response
392
+ data
393
393
  end
394
394
  end
395
395
  end
@@ -400,7 +400,7 @@ module SchwabRb
400
400
  # @param account_hash [String] The account hash (optional if account_name provided)
401
401
  # @param activity_id [String] ID of the transaction to retrieve
402
402
  # @param account_name [String] The account name from account_names.json (takes priority)
403
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
403
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
404
404
  resolved_hash = resolve_account_hash(account_name: account_name, account_hash: account_hash)
405
405
 
406
406
  with_account_hash_retry(resolved_hash) do
@@ -408,28 +408,28 @@ module SchwabRb
408
408
 
409
409
  path = "/trader/v1/accounts/#{resolved_hash}/transactions/#{activity_id}"
410
410
  response = get(path, {})
411
+ data = JSON.parse(response.body, symbolize_names: true)
411
412
 
412
413
  if return_data_objects
413
- transaction_data = JSON.parse(response.body, symbolize_names: true)
414
- SchwabRb::DataObjects::Transaction.build(transaction_data)
414
+ SchwabRb::DataObjects::Transaction.build(data)
415
415
  else
416
- response
416
+ data
417
417
  end
418
418
  end
419
419
  end
420
420
 
421
421
  def get_user_preferences(return_data_objects: true)
422
422
  # Get user preferences for the authenticated user.
423
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
423
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
424
424
  refresh_token_if_needed
425
425
  path = "/trader/v1/userPreference"
426
426
  response = get(path, {})
427
+ data = JSON.parse(response.body, symbolize_names: true)
427
428
 
428
429
  if return_data_objects
429
- preferences_data = JSON.parse(response.body, symbolize_names: true)
430
- SchwabRb::DataObjects::UserPreferences.build(preferences_data)
430
+ SchwabRb::DataObjects::UserPreferences.build(data)
431
431
  else
432
- response
432
+ data
433
433
  end
434
434
  end
435
435
 
@@ -439,19 +439,19 @@ module SchwabRb
439
439
  # @param symbol [String] Single symbol to fetch.
440
440
  # @param fields [Array] Fields to request. If unset, return all available
441
441
  # data (i.e., all fields). See `GetQuote::Field` for options.
442
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
442
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
443
443
  refresh_token_if_needed
444
444
 
445
445
  fields = convert_enum_iterable(fields, SchwabRb::Quote::Types) if fields
446
446
  params = fields ? { "fields" => fields.join(",") } : {}
447
447
  path = "/marketdata/v1/#{symbol}/quotes"
448
448
  response = get(path, params)
449
+ data = JSON.parse(response.body, symbolize_names: true)
449
450
 
450
451
  if return_data_objects
451
- quote_data = JSON.parse(response.body, symbolize_names: true)
452
- SchwabRb::DataObjects::QuoteFactory.build(quote_data)
452
+ SchwabRb::DataObjects::QuoteFactory.build(data)
453
453
  else
454
- response
454
+ data
455
455
  end
456
456
  end
457
457
 
@@ -463,7 +463,7 @@ module SchwabRb
463
463
  # @param fields [Array] Fields to request. If unset, return all available data.
464
464
  # See `GetQuote::Field` for options.
465
465
  # @param indicative [Boolean] If set, fetch indicative quotes. Must be true or false.
466
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
466
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
467
467
  refresh_token_if_needed
468
468
 
469
469
  symbols = [symbols] if symbols.is_a?(String)
@@ -481,14 +481,14 @@ module SchwabRb
481
481
 
482
482
  path = "/marketdata/v1/quotes"
483
483
  response = get(path, params)
484
+ data = JSON.parse(response.body, symbolize_names: true)
484
485
 
485
486
  if return_data_objects
486
- quotes_data = JSON.parse(response.body, symbolize_names: true)
487
- quotes_data.map do |symbol, quote_data|
487
+ data.map do |symbol, quote_data|
488
488
  SchwabRb::DataObjects::QuoteFactory.build({ symbol => quote_data })
489
489
  end
490
490
  else
491
- response
491
+ data
492
492
  end
493
493
  end
494
494
 
@@ -531,7 +531,7 @@ module SchwabRb
531
531
  # @param exp_month [String] Filter options by expiration month.
532
532
  # @param option_type [String] Type of options to include in the chain.
533
533
  # @param entitlement [String] Client entitlement.
534
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
534
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
535
535
 
536
536
  refresh_token_if_needed
537
537
 
@@ -562,28 +562,28 @@ module SchwabRb
562
562
 
563
563
  path = "/marketdata/v1/chains"
564
564
  response = get(path, params)
565
+ data = JSON.parse(response.body, symbolize_names: true)
565
566
 
566
567
  if return_data_objects
567
- option_chain_data = JSON.parse(response.body, symbolize_names: true)
568
- SchwabRb::DataObjects::OptionChain.build(option_chain_data)
568
+ SchwabRb::DataObjects::OptionChain.build(data)
569
569
  else
570
- response
570
+ data
571
571
  end
572
572
  end
573
573
 
574
574
  def get_option_expiration_chain(symbol, return_data_objects: true)
575
575
  # Get option expiration chain for a symbol.
576
576
  # @param symbol [String] The symbol for which to get option expiration dates.
577
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
577
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
578
578
  refresh_token_if_needed
579
579
  path = "/marketdata/v1/expirationchain"
580
580
  response = get(path, { symbol: symbol })
581
+ data = JSON.parse(response.body, symbolize_names: true)
581
582
 
582
583
  if return_data_objects
583
- expiration_data = JSON.parse(response.body, symbolize_names: true)
584
- SchwabRb::DataObjects::OptionExpirationChain.build(expiration_data)
584
+ SchwabRb::DataObjects::OptionExpirationChain.build(data)
585
585
  else
586
- response
586
+ data
587
587
  end
588
588
  end
589
589
 
@@ -600,7 +600,7 @@ module SchwabRb
600
600
  return_data_objects: true
601
601
  )
602
602
  # Get price history for a symbol.
603
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
603
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
604
604
  refresh_token_if_needed
605
605
 
606
606
  period_type = convert_enum(period_type, SchwabRb::PriceHistory::PeriodTypes) if period_type
@@ -621,12 +621,12 @@ module SchwabRb
621
621
  path = "/marketdata/v1/pricehistory"
622
622
 
623
623
  response = get(path, params)
624
+ data = JSON.parse(response.body, symbolize_names: true)
624
625
 
625
626
  if return_data_objects
626
- price_history_data = JSON.parse(response.body, symbolize_names: true)
627
- SchwabRb::DataObjects::PriceHistory.build(price_history_data)
627
+ SchwabRb::DataObjects::PriceHistory.build(data)
628
628
  else
629
- response
629
+ data
630
630
  end
631
631
  end
632
632
 
@@ -847,12 +847,12 @@ module SchwabRb
847
847
  params["frequency"] = frequency.to_s if frequency
848
848
 
849
849
  response = get(path, params)
850
+ data = JSON.parse(response.body, symbolize_names: true)
850
851
 
851
852
  if return_data_objects
852
- movers_data = JSON.parse(response.body, symbolize_names: true)
853
- SchwabRb::DataObjects::MarketMoversFactory.build(movers_data)
853
+ SchwabRb::DataObjects::MarketMoversFactory.build(data)
854
854
  else
855
- response
855
+ data
856
856
  end
857
857
  end
858
858
 
@@ -862,7 +862,7 @@ module SchwabRb
862
862
  # @param markets [Array, String] Markets for which to return trading hours.
863
863
  # @param date [Date] Date for which to return market hours. Accepts values up to
864
864
  # one year from today.
865
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
865
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
866
866
  refresh_token_if_needed
867
867
 
868
868
  markets = convert_enum_iterable(markets, SchwabRb::MarketHours::Markets)
@@ -871,12 +871,12 @@ module SchwabRb
871
871
  params["date"] = format_date_as_day("date", date) if date
872
872
 
873
873
  response = get("/marketdata/v1/markets", params)
874
+ data = JSON.parse(response.body, symbolize_names: true)
874
875
 
875
876
  if return_data_objects
876
- market_hours_data = JSON.parse(response.body, symbolize_names: true)
877
- SchwabRb::DataObjects::MarketHours.build(market_hours_data)
877
+ SchwabRb::DataObjects::MarketHours.build(data)
878
878
  else
879
- response
879
+ data
880
880
  end
881
881
  end
882
882
 
@@ -887,7 +887,7 @@ module SchwabRb
887
887
  # @param symbols [String, Array] For "FUNDAMENTAL" projection, the symbols to fetch.
888
888
  # For other projections, a search term.
889
889
  # @param projection [String] Search mode or "FUNDAMENTAL" for instrument fundamentals.
890
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
890
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
891
891
  refresh_token_if_needed
892
892
 
893
893
  symbols = [symbols] unless symbols.is_a?(Array)
@@ -898,14 +898,14 @@ module SchwabRb
898
898
  }
899
899
 
900
900
  response = get("/marketdata/v1/instruments", params)
901
+ data = JSON.parse(response.body, symbolize_names: true)
901
902
 
902
903
  if return_data_objects
903
- instruments_data = JSON.parse(response.body, symbolize_names: true)
904
- instruments_data.map do |instrument_data|
904
+ data.map do |instrument_data|
905
905
  SchwabRb::DataObjects::Instrument.build(instrument_data)
906
906
  end
907
907
  else
908
- response
908
+ data
909
909
  end
910
910
  end
911
911
 
@@ -913,18 +913,18 @@ module SchwabRb
913
913
  # Get instrument information for a single instrument by CUSIP.
914
914
  #
915
915
  # @param cusip [String] CUSIP of the instrument to fetch. Leading zeroes must be preserved.
916
- # @param return_data_objects [Boolean] Whether to return data objects or raw JSON
916
+ # @param return_data_objects [Boolean] Whether to return data objects or Hash
917
917
  refresh_token_if_needed
918
918
 
919
919
  raise ArgumentError, "cusip must be passed as a string" unless cusip.is_a?(String)
920
920
 
921
921
  response = get("/marketdata/v1/instruments/#{cusip}", {})
922
+ data = JSON.parse(response.body, symbolize_names: true)
922
923
 
923
924
  if return_data_objects
924
- instrument_data = JSON.parse(response.body, symbolize_names: true)
925
- SchwabRb::DataObjects::Instrument.build(instrument_data)
925
+ SchwabRb::DataObjects::Instrument.build(data)
926
926
  else
927
- response
927
+ data
928
928
  end
929
929
  end
930
930
 
@@ -42,7 +42,6 @@ module SchwabRb
42
42
  theoretical_option_value: data.fetch(:theoreticalOptionValue, nil),
43
43
  theoretical_volatility: data.fetch(:theoreticalVolatility, nil),
44
44
  option_deliverables_list: data.fetch(:optionDeliverablesList, nil),
45
- strike_price: data.fetch(:strikePrice, nil),
46
45
  expiration_date: Date.parse(data.fetch(:expirationDate)),
47
46
  days_to_expiration: data.fetch(:daysToExpiration, nil),
48
47
  expiration_type: data.fetch(:expirationType, nil),
@@ -72,7 +71,7 @@ module SchwabRb
72
71
  close_price:, total_volume:, trade_time_in_long:,
73
72
  quote_time_in_long:, net_change:, volatility:, delta:,
74
73
  gamma:, theta:, vega:, rho:, open_interest:, time_value:,
75
- theoretical_option_value:, theoretical_volatility:, option_deliverables_list:, strike_price:,
74
+ theoretical_option_value:, theoretical_volatility:, option_deliverables_list:,
76
75
  expiration_date:, days_to_expiration:, expiration_type:, last_trading_day:, multiplier:,
77
76
  settlement_type:, deliverable_note:, percent_change:, mark_change:, mark_percent_change:, intrinsic_value:, extrinsic_value:, option_root:, exercise_type:, high_52_week:, low_52_week:, non_standard:, in_the_money:
78
77
  )
@@ -109,7 +108,6 @@ module SchwabRb
109
108
  @theoretical_option_value = theoretical_option_value
110
109
  @theoretical_volatility = theoretical_volatility
111
110
  @option_deliverables_list = option_deliverables_list
112
- @strike_price = strike_price
113
111
  @expiration_date = expiration_date
114
112
  @days_to_expiration = days_to_expiration
115
113
  @expiration_type = expiration_type
@@ -136,12 +134,16 @@ module SchwabRb
136
134
  :close_price, :total_volume, :trade_time_in_long, :quote_time_in_long,
137
135
  :net_change, :volatility, :delta, :gamma, :theta, :vega, :rho,
138
136
  :open_interest, :time_value, :theoretical_option_value,
139
- :theoretical_volatility, :option_deliverables_list, :strike_price,
137
+ :theoretical_volatility, :option_deliverables_list,
140
138
  :expiration_date, :days_to_expiration, :expiration_type, :last_trading_day,
141
139
  :multiplier, :settlement_type, :deliverable_note, :percent_change,
142
140
  :mark_change, :mark_percent_change, :intrinsic_value, :extrinsic_value,
143
141
  :option_root, :exercise_type, :high_52_week, :low_52_week, :non_standard,
144
142
  :in_the_money
143
+
144
+ def strike_price
145
+ strike
146
+ end
145
147
  end
146
148
  end
147
149
  end
@@ -6,7 +6,7 @@ module SchwabRb
6
6
  module DataObjects
7
7
  class OrderPreview
8
8
  attr_reader :order_id, :order_value, :order_strategy, :order_balance, :order_validation_result,
9
- :projected_commission
9
+ :commission_and_fee
10
10
 
11
11
  class << self
12
12
  def build(data)
@@ -20,7 +20,7 @@ module SchwabRb
20
20
  @order_strategy = attrs[:orderStrategy] ? OrderStrategy.new(attrs[:orderStrategy]) : nil
21
21
  @order_balance = attrs[:orderBalance] ? OrderBalance.new(attrs[:orderBalance]) : nil
22
22
  @order_validation_result = attrs[:orderValidationResult] ? OrderValidationResult.new(attrs[:orderValidationResult]) : nil
23
- @projected_commission = attrs[:projectedCommission] ? CommissionAndFee.new(attrs[:projectedCommission]) : nil
23
+ @commission_and_fee = attrs[:commissionAndFee] ? CommissionAndFee.new(attrs[:commissionAndFee]) : nil
24
24
  end
25
25
 
26
26
  def status
@@ -40,15 +40,15 @@ module SchwabRb
40
40
  end
41
41
 
42
42
  def commission
43
- return 0.0 unless @projected_commission
43
+ return 0.0 unless @commission_and_fee
44
44
 
45
- @projected_commission.commission_value
45
+ @commission_and_fee.commission
46
46
  end
47
47
 
48
48
  def fees
49
- return 0.0 unless @projected_commission
49
+ return 0.0 unless @commission_and_fee
50
50
 
51
- @projected_commission.fee_value
51
+ @commission_and_fee.fee
52
52
  end
53
53
 
54
54
  def to_h
@@ -58,15 +58,14 @@ module SchwabRb
58
58
  orderStrategy: @order_strategy&.to_h,
59
59
  orderBalance: @order_balance&.to_h,
60
60
  orderValidationResult: @order_validation_result&.to_h,
61
- projectedCommission: @projected_commission&.to_h
61
+ commissionAndFee: @commission_and_fee&.to_h
62
62
  }
63
63
  end
64
64
 
65
65
  class OrderStrategy
66
- attr_reader :account_number, :status, :price, :quantity, :order_type, :type, :strategy_id, :order_legs
66
+ attr_reader :status, :price, :quantity, :order_type, :type, :strategy_id, :order_legs
67
67
 
68
68
  def initialize(attrs)
69
- @account_number = attrs[:accountNumber]
70
69
  @status = attrs[:status]
71
70
  @price = attrs[:price]
72
71
  @quantity = attrs[:quantity]
@@ -78,7 +77,6 @@ module SchwabRb
78
77
 
79
78
  def to_h
80
79
  {
81
- accountNumber: @account_number,
82
80
  status: @status,
83
81
  price: @price,
84
82
  quantity: @quantity,
@@ -111,75 +109,81 @@ module SchwabRb
111
109
  end
112
110
 
113
111
  class OrderValidationResult
114
- attr_reader :is_valid, :warning_message, :rejects
112
+ attr_reader :is_valid, :warns, :rejects
115
113
 
116
114
  def initialize(attrs)
117
115
  @is_valid = attrs[:isValid]
118
- @warning_message = attrs[:warningMessage]
116
+ @warns = attrs[:warns]&.map { |warn| Warn.new(warn) } || []
119
117
  @rejects = attrs[:rejects]&.map { |reject| Reject.new(reject) } || []
120
118
  end
121
119
 
122
120
  def to_h
123
121
  {
124
122
  isValid: @is_valid,
125
- warningMessage: @warning_message,
123
+ warns: @warns.map(&:to_h),
126
124
  rejects: @rejects.map(&:to_h)
127
125
  }
128
126
  end
129
127
 
128
+ class Warn
129
+ attr_reader :activity_message, :original_severity
130
+
131
+ def initialize(attrs)
132
+ @activity_message = attrs[:activityMessage]
133
+ @original_severity = attrs[:originalSeverity]
134
+ end
135
+
136
+ def to_h
137
+ {
138
+ activityMessage: @activity_message,
139
+ originalSeverity: @original_severity
140
+ }
141
+ end
142
+ end
143
+
130
144
  class Reject
131
- attr_reader :reject_code, :reject_message
145
+ attr_reader :activity_message, :original_severity
132
146
 
133
147
  def initialize(attrs)
134
- @reject_code = attrs[:rejectCode]
135
- @reject_message = attrs[:rejectMessage]
148
+ @activity_message = attrs[:activityMessage]
149
+ @original_severity = attrs[:originalSeverity]
136
150
  end
137
151
 
138
152
  def to_h
139
153
  {
140
- rejectCode: @reject_code,
141
- rejectMessage: @reject_message
154
+ activityMessage: @activity_message,
155
+ originalSeverity: @original_severity
142
156
  }
143
157
  end
144
158
  end
145
159
  end
146
160
 
147
161
  class CommissionAndFee
148
- attr_reader :commission, :commissions, :fee, :fees, :true_commission
162
+ attr_reader :commissions, :fees, :true_commission_legs
149
163
 
150
164
  def initialize(attrs)
151
- @commission = attrs[:commission]&.to_f
152
- @commissions = attrs[:commissions] || []
153
- @fee = attrs[:fee]&.to_f
154
- @fees = attrs[:fees] || []
155
- @true_commission = attrs[:trueCommission]&.to_f
165
+ @commissions = attrs[:commission][:commissionLegs] || []
166
+ @fees = attrs[:fee][:feeLegs] || []
167
+ @true_commission_legs = attrs[:trueCommission][:commissionLegs] || []
156
168
  end
157
169
 
158
- def commission_total
170
+ def commission
159
171
  calculate_total_from_legs(@commissions, "COMMISSION")
160
172
  end
161
173
 
162
- def commission_value
163
- @commission || commission_total
164
- end
165
-
166
- def fee_total
174
+ def fee
167
175
  calculate_total_from_legs(@fees, %w[OPT_REG_FEE INDEX_OPTION_FEE])
168
176
  end
169
177
 
170
- def fee_value
171
- @fee || fee_total
172
- end
173
-
174
- def true_commission_value
175
- @true_commission || (commission_total * 2)
178
+ def true_commission
179
+ calculate_total_from_legs(@true_commission_legs, "COMMISSION")
176
180
  end
177
181
 
178
182
  def to_h
179
183
  {
180
- commission: @commission,
181
- fee: @fee,
182
- trueCommission: @true_commission,
184
+ commission: commission,
185
+ fee: fee,
186
+ trueCommission: true_commission,
183
187
  commissions: @commissions,
184
188
  fees: @fees
185
189
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "iron_condor_order"
4
4
  require_relative "vertical_order"
5
+ require_relative "vertical_roll_order"
5
6
  require_relative "single_order"
6
7
  require_relative "oco_order"
7
8
  require_relative "order"
@@ -37,6 +38,19 @@ module SchwabRb
37
38
  order_instruction: options[:order_instruction] || :open,
38
39
  quantity: options[:quantity] || 1
39
40
  )
41
+ when SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL_ROLL
42
+ VerticalRollOrder.build(
43
+ close_short_leg_symbol: options[:close_short_leg_symbol],
44
+ close_long_leg_symbol: options[:close_long_leg_symbol],
45
+ open_short_leg_symbol: options[:open_short_leg_symbol],
46
+ open_long_leg_symbol: options[:open_long_leg_symbol],
47
+ price: options[:price],
48
+ stop_price: options[:stop_price],
49
+ order_type: options[:order_type],
50
+ duration: options[:duration] || SchwabRb::Orders::Duration::DAY,
51
+ credit_debit: options[:credit_debit] || :credit,
52
+ quantity: options[:quantity] || 1
53
+ )
40
54
  when SchwabRb::Order::OrderStrategyTypes::SINGLE
41
55
  SingleOrder.build(
42
56
  symbol: options[:symbol],
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "schwab_rb"
4
+
5
+ module SchwabRb
6
+ module Orders
7
+ class VerticalRollOrder
8
+ class << self
9
+ def build(
10
+ close_short_leg_symbol:,
11
+ close_long_leg_symbol:,
12
+ open_short_leg_symbol:,
13
+ open_long_leg_symbol:,
14
+ price:,
15
+ stop_price: nil,
16
+ order_type: nil,
17
+ duration: SchwabRb::Orders::Duration::DAY,
18
+ credit_debit: :credit,
19
+ quantity: 1
20
+ )
21
+ schwab_order_builder.new.tap do |builder|
22
+ builder.set_order_strategy_type(SchwabRb::Order::OrderStrategyTypes::SINGLE)
23
+ builder.set_session(SchwabRb::Orders::Session::NORMAL)
24
+ builder.set_duration(duration)
25
+ builder.set_order_type(order_type || determine_order_type(credit_debit))
26
+ builder.set_complex_order_strategy_type(SchwabRb::Order::ComplexOrderStrategyTypes::VERTICAL_ROLL)
27
+ builder.set_quantity(quantity)
28
+ builder.set_price(price)
29
+ builder.set_stop_price(stop_price) if stop_price && order_type == SchwabRb::Order::Types::STOP_LIMIT
30
+
31
+ # Close the existing spread (opposite instructions)
32
+ builder.add_option_leg(
33
+ SchwabRb::Orders::OptionInstructions::BUY_TO_CLOSE,
34
+ close_short_leg_symbol,
35
+ quantity
36
+ )
37
+ builder.add_option_leg(
38
+ SchwabRb::Orders::OptionInstructions::SELL_TO_CLOSE,
39
+ close_long_leg_symbol,
40
+ quantity
41
+ )
42
+
43
+ # Open the new spread
44
+ builder.add_option_leg(
45
+ SchwabRb::Orders::OptionInstructions::SELL_TO_OPEN,
46
+ open_short_leg_symbol,
47
+ quantity
48
+ )
49
+ builder.add_option_leg(
50
+ SchwabRb::Orders::OptionInstructions::BUY_TO_OPEN,
51
+ open_long_leg_symbol,
52
+ quantity
53
+ )
54
+ end
55
+ end
56
+
57
+ def determine_order_type(credit_debit)
58
+ if credit_debit == :credit
59
+ SchwabRb::Order::Types::NET_CREDIT
60
+ else
61
+ SchwabRb::Order::Types::NET_DEBIT
62
+ end
63
+ end
64
+
65
+ def schwab_order_builder
66
+ SchwabRb::Orders::Builder
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwabRb
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/schwab_rb.rb CHANGED
@@ -47,6 +47,7 @@ require_relative "schwab_rb/data_objects/market_movers"
47
47
  require_relative "schwab_rb/orders/order_factory"
48
48
  require_relative "schwab_rb/orders/iron_condor_order"
49
49
  require_relative "schwab_rb/orders/vertical_order"
50
+ require_relative "schwab_rb/orders/vertical_roll_order"
50
51
  require_relative "schwab_rb/orders/single_order"
51
52
  require_relative "schwab_rb/orders/oco_order"
52
53
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schwab_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Platta
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-10-20 00:00:00.000000000 Z
10
+ date: 2025-12-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async
@@ -266,6 +266,7 @@ files:
266
266
  - lib/schwab_rb/orders/stop_type.rb
267
267
  - lib/schwab_rb/orders/tax_lot_method.rb
268
268
  - lib/schwab_rb/orders/vertical_order.rb
269
+ - lib/schwab_rb/orders/vertical_roll_order.rb
269
270
  - lib/schwab_rb/price_history.rb
270
271
  - lib/schwab_rb/quote.rb
271
272
  - lib/schwab_rb/transaction.rb