schwab_rb 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.copilotignore +4 -0
  3. data/.rspec +2 -0
  4. data/.rspec_status +292 -0
  5. data/.rubocop.yml +41 -0
  6. data/.rubocop_todo.yml +105 -0
  7. data/CHANGELOG.md +28 -0
  8. data/LICENSE.txt +23 -0
  9. data/README.md +271 -0
  10. data/Rakefile +12 -0
  11. data/doc/notes/data_objects_analysis.md +223 -0
  12. data/doc/notes/data_objects_refactoring_plan.md +82 -0
  13. data/examples/fetch_account_numbers.rb +49 -0
  14. data/examples/fetch_user_preferences.rb +49 -0
  15. data/lib/schwab_rb/account.rb +9 -0
  16. data/lib/schwab_rb/auth/auth_context.rb +23 -0
  17. data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
  18. data/lib/schwab_rb/auth/init_client_login.rb +201 -0
  19. data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
  20. data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
  21. data/lib/schwab_rb/auth/token.rb +24 -0
  22. data/lib/schwab_rb/auth/token_manager.rb +105 -0
  23. data/lib/schwab_rb/clients/async_client.rb +122 -0
  24. data/lib/schwab_rb/clients/base_client.rb +887 -0
  25. data/lib/schwab_rb/clients/client.rb +97 -0
  26. data/lib/schwab_rb/configuration.rb +39 -0
  27. data/lib/schwab_rb/constants.rb +7 -0
  28. data/lib/schwab_rb/data_objects/account.rb +281 -0
  29. data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
  30. data/lib/schwab_rb/data_objects/instrument.rb +156 -0
  31. data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
  32. data/lib/schwab_rb/data_objects/option.rb +147 -0
  33. data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
  34. data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
  35. data/lib/schwab_rb/data_objects/order.rb +186 -0
  36. data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
  37. data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
  38. data/lib/schwab_rb/data_objects/position.rb +100 -0
  39. data/lib/schwab_rb/data_objects/price_history.rb +187 -0
  40. data/lib/schwab_rb/data_objects/quote.rb +276 -0
  41. data/lib/schwab_rb/data_objects/transaction.rb +132 -0
  42. data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
  43. data/lib/schwab_rb/market_hours.rb +13 -0
  44. data/lib/schwab_rb/movers.rb +35 -0
  45. data/lib/schwab_rb/option.rb +64 -0
  46. data/lib/schwab_rb/orders/builder.rb +202 -0
  47. data/lib/schwab_rb/orders/destination.rb +19 -0
  48. data/lib/schwab_rb/orders/duration.rb +9 -0
  49. data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
  50. data/lib/schwab_rb/orders/errors.rb +5 -0
  51. data/lib/schwab_rb/orders/instruments.rb +35 -0
  52. data/lib/schwab_rb/orders/option_instructions.rb +10 -0
  53. data/lib/schwab_rb/orders/order.rb +77 -0
  54. data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
  55. data/lib/schwab_rb/orders/price_link_type.rb +9 -0
  56. data/lib/schwab_rb/orders/session.rb +14 -0
  57. data/lib/schwab_rb/orders/special_instruction.rb +10 -0
  58. data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
  59. data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
  60. data/lib/schwab_rb/orders/stop_type.rb +11 -0
  61. data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
  62. data/lib/schwab_rb/price_history.rb +55 -0
  63. data/lib/schwab_rb/quote.rb +13 -0
  64. data/lib/schwab_rb/transaction.rb +23 -0
  65. data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
  66. data/lib/schwab_rb/utils/logger.rb +70 -0
  67. data/lib/schwab_rb/utils/redactor.rb +104 -0
  68. data/lib/schwab_rb/version.rb +5 -0
  69. data/lib/schwab_rb.rb +48 -0
  70. data/sig/schwab_rb.rbs +4 -0
  71. metadata +289 -0
data/README.md ADDED
@@ -0,0 +1,271 @@
1
+ ****# schwab_rb: Schwab API Ruby Client
2
+
3
+ The `schwab_rb` gem is a Ruby client for interacting with the Schwab API. It provides a simple and flexible interface for accessing Schwab account data, placing orders, retrieving quotes, and more.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'schwab_rb'
11
+ ```
12
+
13
+ Or install it manually:
14
+
15
+ ```bash
16
+ gem install schwab_rb
17
+ ```
18
+
19
+ Note: The gem requires Ruby 3.0.0 or higher.
20
+
21
+ ## Prerequisites
22
+
23
+ Before using this gem, you'll need:
24
+
25
+ 1. A Charles Schwab Developer Account
26
+ 2. A registered application with Schwab to get your API key and secret
27
+ 3. Your Schwab trading account number
28
+
29
+ ## Dependencies
30
+
31
+ The gem depends on several key libraries:
32
+ - `async` and `async-http` for asynchronous HTTP operations
33
+ - `oauth2` for OAuth authentication
34
+ - `sinatra` and `puma` for the authentication callback server
35
+ - `dotenv` for environment variable management
36
+
37
+ ## Usage
38
+
39
+ ### Setting Up Environment Variables
40
+
41
+ Before using the gem, ensure you have the following environment variables set:
42
+
43
+ - `SCHWAB_API_KEY`: Your Schwab API key.
44
+ - `SCHWAB_APP_SECRET`: Your Schwab application secret.
45
+ - `APP_CALLBACK_URL`: The callback URL for your application.
46
+ - `TOKEN_PATH`: Path to store the authentication token.
47
+ - `SCHWAB_ACCOUNT_NUMBER`: Your Schwab account number.
48
+ - `SCHWAB_LOGFILE`: (Optional) Path to the log file. Defaults to `STDOUT`.
49
+ - `SCHWAB_LOG_LEVEL`: (Optional) Log level for the logger. Defaults to `WARN`. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`.
50
+ - `SCHWAB_SILENCE_OUTPUT`: (Optional) Set to `true` to disable logging output. Defaults to `false`.
51
+
52
+ You can also configure logging programmatically:
53
+
54
+ ```ruby
55
+ SchwabRb.configure do |config|
56
+ config.logger = Logger.new(STDOUT)
57
+ config.log_level = 'INFO'
58
+ config.silence_output = false
59
+ end
60
+ ```
61
+
62
+ ### Example Usage
63
+
64
+ Here is an example of how to use the `schwab_rb` gem:
65
+
66
+ ```ruby
67
+ require 'schwab_rb'
68
+
69
+ # Initialize the client
70
+ client = SchwabRb::Auth.init_client_easy(
71
+ ENV['SCHWAB_API_KEY'],
72
+ ENV['SCHWAB_APP_SECRET'],
73
+ ENV['APP_CALLBACK_URL'],
74
+ ENV['TOKEN_PATH']
75
+ )
76
+
77
+ # Fetch a quote
78
+ quote = client.get_quote('AAPL')
79
+ puts quote.body
80
+
81
+ # Fetch multiple quotes
82
+ quotes = client.get_quotes(['AAPL', 'MSFT', 'GOOGL'])
83
+ puts quotes.body
84
+
85
+ # Fetch account details
86
+ account = client.get_account('account_hash')
87
+ puts account.body
88
+
89
+ # Get option chain
90
+ option_chain = client.get_option_chain(
91
+ symbol: 'AAPL',
92
+ strike_count: 10,
93
+ include_non_standard: true
94
+ )
95
+
96
+ # Preview an order before placing
97
+ order = {
98
+ orderType: 'MARKET',
99
+ session: 'NORMAL',
100
+ duration: 'DAY',
101
+ orderLegCollection: [
102
+ {
103
+ instruction: 'BUY',
104
+ quantity: 100,
105
+ instrument: {
106
+ symbol: 'AAPL',
107
+ assetType: 'EQUITY'
108
+ }
109
+ }
110
+ ]
111
+ }
112
+
113
+ preview = client.preview_order('account_hash', order)
114
+ puts preview.body
115
+
116
+ # Place the order
117
+ response = client.place_order('account_hash', order)
118
+ puts response.body
119
+
120
+ # Get price history
121
+ price_history = client.get_price_history(
122
+ symbol: 'AAPL',
123
+ period_type: :month,
124
+ period: 3,
125
+ frequency_type: :daily
126
+ )
127
+ ```
128
+
129
+ ## Data Objects
130
+
131
+ The gem includes structured data objects for better handling of API responses. Most API methods support a `return_data_objects` parameter (defaults to `true`) which returns parsed Ruby objects instead of raw JSON responses:
132
+
133
+ ```ruby
134
+ # Returns structured data objects
135
+ quote = client.get_quote('AAPL') # Returns Quote object
136
+ account = client.get_account('hash') # Returns Account object
137
+
138
+ # Returns raw JSON response
139
+ quote_raw = client.get_quote('AAPL', return_data_objects: false)
140
+ ```
141
+
142
+ Available data object types include:
143
+ - `Quote` - Stock and option quotes
144
+ - `Account` - Account information and balances
145
+ - `Order` - Order details and status
146
+ - `Transaction` - Transaction history
147
+ - `OptionChain` - Option chain data
148
+ - `PriceHistory` - Historical price data
149
+ - And more...
150
+
151
+ For more detailed examples, refer to the `examples/schwab.rb` file in the repository.
152
+
153
+ ## Development
154
+
155
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
156
+
157
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
158
+
159
+ ## API Features
160
+
161
+ The gem provides comprehensive access to the Schwab API, including:
162
+
163
+ ### Account Information
164
+ - Get account details and balances
165
+ - Retrieve account numbers
166
+ - Get user preferences
167
+
168
+ ### Orders
169
+ - Place orders (equity and options)
170
+ - Cancel orders
171
+ - Replace existing orders
172
+ - Preview orders before placing
173
+ - Get order details and history
174
+
175
+ ### Market Data
176
+ - Real-time and delayed quotes for stocks and options
177
+ - Option chains with filtering capabilities
178
+ - Option expiration chains
179
+ - Price history with various time intervals
180
+ - Market movers
181
+ - Market hours information
182
+
183
+ ### Transactions
184
+ - Get transaction history
185
+ - Retrieve individual transaction details
186
+
187
+ ### Order Building
188
+ The gem includes a flexible order builder for creating complex orders:
189
+
190
+ ```ruby
191
+ # Using the order builder for a simple equity buy
192
+ order = SchwabRb::Orders::Builder.new
193
+ .set_session(:normal)
194
+ .set_duration(:day)
195
+ .set_order_type(:market)
196
+ .add_equity_leg(:buy, 'AAPL', 100)
197
+ .build
198
+
199
+ response = client.place_order('account_hash', order)
200
+ ```
201
+
202
+ ## Authentication Methods
203
+
204
+ The gem supports multiple authentication approaches:
205
+
206
+ 1. **Easy Initialization** (Recommended): Automatically handles token storage and refresh
207
+ 2. **Token File**: Initialize from a saved token file
208
+ 3. **Login Flow**: Interactive browser-based authentication
209
+
210
+ ### Easy Authentication Setup
211
+
212
+ ```ruby
213
+ client = SchwabRb::Auth.init_client_easy(
214
+ ENV['SCHWAB_API_KEY'],
215
+ ENV['SCHWAB_APP_SECRET'],
216
+ ENV['APP_CALLBACK_URL'],
217
+ ENV['TOKEN_PATH']
218
+ )
219
+ ```
220
+
221
+ This method will:
222
+ - Try to load an existing token from the specified path
223
+ - Automatically refresh the token if it's expired
224
+ - Fall back to the interactive login flow if no valid token exists
225
+
226
+ ## Async Support
227
+
228
+ The gem supports both synchronous and asynchronous operations. For async usage:
229
+
230
+ ```ruby
231
+ # Initialize async client
232
+ client = SchwabRb::Auth.init_client_easy(
233
+ ENV['SCHWAB_API_KEY'],
234
+ ENV['SCHWAB_APP_SECRET'],
235
+ ENV['APP_CALLBACK_URL'],
236
+ ENV['TOKEN_PATH'],
237
+ asyncio: true
238
+ )
239
+
240
+ # Use async methods
241
+ client.get_quote('AAPL').then do |response|
242
+ puts response.body
243
+ end
244
+ ```
245
+
246
+ ## Troubleshooting
247
+
248
+ ### Token Issues
249
+ - Ensure your `TOKEN_PATH` environment variable points to a writable location
250
+ - Delete the token file and re-authenticate if you encounter persistent token errors
251
+ - Check that your API key and secret are correctly set in environment variables
252
+
253
+ ### SSL/TLS Issues
254
+ - The gem uses SSL for all API communications
255
+ - If you encounter SSL errors, ensure your system's certificate store is up to date
256
+
257
+ ### Rate Limiting
258
+ - The Schwab API has rate limits; implement appropriate delays between requests
259
+ - Use the gem's logging features to monitor API request/response patterns
260
+
261
+ For more detailed examples, refer to the `examples/schwab.rb` file in the repository.
262
+
263
+ ## Contributing
264
+
265
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jwplatta/schwab_rb.
266
+
267
+ ## License
268
+
269
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
270
+
271
+ The gem is inspired by [schwab-py](https://pypi.org/project/schwab-py). The original implementation can be found [here](https://github.com/alexgolec/schwab-py).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,223 @@
1
+ # Data Objects Analysis for BaseClient
2
+
3
+ ## Analysis Date: July 20, 2025
4
+
5
+ This document analyzes all API methods in `base_client.rb` to identify:
6
+ 1. ### - [x] - [x]- [x] **UserPreferences** data object class
7
+ - [x] Create `lib/schwab_rb/data_objects/user_preferences.rb`
8
+ - [x] Create `spec/data_objects/user_preferences_spec.rb`
9
+ - [x] Use fixture: `spec/fixtures/user_preferences.json`
10
+ - [x] **OptionExpirationChain** data object class
11
+ - [x] Create `lib/schwab_rb/data_objects/option_expiration_chain.rb`
12
+ - [x] Create `spec/data_objects/option_expiration_chain_spec.rb`
13
+ - [x] Use fixture: `spec/fixtures/option_expiration_chain.json`spec/fixtures/user_preferences.json`
14
+ - [x] **OptionExpirationChain** data object class
15
+ - [x] Create `lib/schwab_rb/data_objects/option_expiration_chain.rb`
16
+ - [x] Create `spec/data_objects/option_expiration_chain_spec.rb`
17
+ - [x] Use fixture: `spec/fixtures/option_expiration_chain.json`
18
+ - [x] **PriceHistory** data object class
19
+ - [x] Create `lib/schwab_rb/data_objects/price_history.rb`
20
+ - [x] Create `spec/data_objects/price_history_spec.rb`
21
+ - [x] Use fixtures: `spec/fixtures/price_history_*.json`
22
+
23
+ ### 📋 Todoec/fixtures/user_preferences.json`
24
+ - [x] **OptionExpirationChain** data object class
25
+ - [x] Create `lib/schwab_rb/data_objects/option_expiration_chain.rb`
26
+ - [x] Create `spec/data_objects/option_expiration_chain_spec.rb`
27
+ - [x] Use fixture: `spec/fixtures/option_expiration_chain.json`
28
+
29
+ ### 📋 Todos
30
+ - [ ] **OptionExpirationChain** data object class
31
+ - [ ] Create `lib/schwab_rb/data_objects/option_expiration_chain.rb`
32
+ - [ ] Create `spec/data_objects/option_expiration_chain_spec.rb`
33
+ - [ ] Use fixture: `spec/fixtures/option_expiration_chain.json`
34
+
35
+ ### 📋 Todo
36
+ - [ ] **PriceHistory** data object class don't return data objects (only raw JSON)
37
+ 2. Methods that need data object classes to be created
38
+
39
+ ## Methods NOT Currently Returning Data Objects
40
+
41
+ ### Account-Related Methods
42
+ 1. **`get_account_numbers`** - Returns raw JSON mapping of account IDs to hashes
43
+ - Status: No `return_data_objects` parameter, always returns raw JSON
44
+ - Needs: `AccountNumbers` data object class
45
+
46
+ 2. **`get_order`** - Returns raw JSON for a specific order
47
+ - Status: No `return_data_objects` parameter, always returns raw JSON
48
+ - Note: Should probably return `Order` data object (which exists)
49
+
50
+ 3. **`cancel_order`** - Returns raw response from order cancellation
51
+ - Status: No `return_data_objects` parameter, always returns raw JSON
52
+ - Note: This is a mutation operation, may not need data object
53
+
54
+ 4. **`get_all_linked_account_orders`** - Returns raw JSON for orders across all accounts
55
+ - Status: No `return_data_objects` parameter, always returns raw JSON
56
+ - Note: Should probably return array of `Order` data objects (which exists)
57
+
58
+ 5. **`place_order`** - Returns raw response from order placement
59
+ - Status: No `return_data_objects` parameter, always returns raw JSON
60
+ - Note: This is a mutation operation, may not need data object
61
+
62
+ 6. **`replace_order`** - Returns raw response from order replacement
63
+ - Status: No `return_data_objects` parameter, always returns raw JSON
64
+ - Note: This is a mutation operation, may not need data object
65
+
66
+ 7. **`get_user_preferences`** - Returns raw JSON for user preferences
67
+ - Status: No `return_data_objects` parameter, always returns raw JSON
68
+ - Needs: `UserPreferences` data object class
69
+
70
+ ### Market Data Methods
71
+ 8. **`get_option_expiration_chain`** - Returns raw JSON for option expiration chain
72
+ - Status: No `return_data_objects` parameter, always returns raw JSON
73
+ - Needs: `OptionExpirationChain` data object class
74
+
75
+ 9. **`get_price_history`** - Returns raw JSON for price history data
76
+ - Status: No `return_data_objects` parameter, always returns raw JSON
77
+ - Needs: `PriceHistory` data object class
78
+
79
+ 10. **All price history convenience methods** - Return raw JSON (via `get_price_history`)
80
+ - `get_price_history_every_minute`
81
+ - `get_price_history_every_five_minutes`
82
+ - `get_price_history_every_ten_minutes`
83
+ - `get_price_history_every_fifteen_minutes`
84
+ - `get_price_history_every_thirty_minutes`
85
+ - `get_price_history_every_day`
86
+ - `get_price_history_every_week`
87
+ - Status: No `return_data_objects` parameter, always return raw JSON
88
+ - Note: These delegate to `get_price_history`, so fixing that method would fix all
89
+
90
+ 11. **`get_movers`** - Returns raw JSON for market movers
91
+ - Status: No `return_data_objects` parameter, always returns raw JSON
92
+ - Needs: `Movers` data object class
93
+
94
+ 12. **`get_market_hours`** - Returns raw JSON for market hours
95
+ - Status: No `return_data_objects` parameter, always returns raw JSON
96
+ - Needs: `MarketHours` data object class
97
+
98
+ 13. **`get_instruments`** - Returns raw JSON for instrument search results
99
+ - Status: No `return_data_objects` parameter, always returns raw JSON
100
+ - Note: `Instrument` data object exists but method doesn't use it
101
+
102
+ 14. **`get_instrument_by_cusip`** - Returns raw JSON for a specific instrument
103
+ - Status: No `return_data_objects` parameter, always returns raw JSON
104
+ - Note: `Instrument` data object exists but method doesn't use it
105
+
106
+ ## Methods Currently Returning Data Objects ✅
107
+
108
+ ### Account-Related Methods
109
+ - `get_account` - Returns `Account` data object ✅
110
+ - `get_accounts` - Returns array of `Account` data objects ✅
111
+ - `preview_order` - Returns `OrderPreview` data object ✅
112
+ - `get_account_orders` - Returns array of `Order` data objects ✅
113
+ - `get_transactions` - Returns array of `Transaction` data objects ✅
114
+ - `get_transaction` - Returns `Transaction` data object ✅
115
+
116
+ ### Market Data Methods
117
+ - `get_quote` - Returns data object via `QuoteFactory.build` ✅
118
+ - `get_quotes` - Returns array of data objects via `QuoteFactory.build` ✅
119
+ - `get_option_chain` - Returns `OptionChain` data object ✅
120
+
121
+ ## Data Object Classes That Need to Be Created
122
+
123
+ 1. **`AccountNumbers`** - For `get_account_numbers` method
124
+ 2. **`UserPreferences`** - For `get_user_preferences` method
125
+ 3. **`OptionExpirationChain`** - For `get_option_expiration_chain` method
126
+ 4. **`PriceHistory`** - For all price history methods
127
+ 5. **`Movers`** - For `get_movers` method
128
+ 6. **`MarketHours`** - For `get_market_hours` method
129
+
130
+ ## Existing Data Object Classes
131
+
132
+ The following data object classes already exist and are being used:
133
+ - `Account` ✅
134
+ - `Order` ✅
135
+ - `OrderPreview` ✅
136
+ - `Transaction` ✅
137
+ - `Quote` (via QuoteFactory) ✅
138
+ - `OptionChain` ✅
139
+ - `Instrument` ✅ (exists but not used by `get_instruments` methods)
140
+ - `OrderLeg` ✅
141
+ - `Position` ✅
142
+ - `Option` ✅
143
+
144
+ ## Recommendations
145
+
146
+ ### High Priority (Methods that should return data objects)
147
+ 1. Add `return_data_objects` parameter to `get_order` and use existing `Order` class
148
+ 2. Add `return_data_objects` parameter to `get_all_linked_account_orders` and use existing `Order` class
149
+ 3. Add `return_data_objects` parameter to `get_instruments` and use existing `Instrument` class
150
+ 4. Add `return_data_objects` parameter to `get_instrument_by_cusip` and use existing `Instrument` class
151
+
152
+ ### Medium Priority (Need new data object classes)
153
+ 1. Create `PriceHistory` class and add `return_data_objects` to `get_price_history`
154
+ 2. Create `Movers` class and add `return_data_objects` to `get_movers`
155
+ 3. Create `MarketHours` class and add `return_data_objects` to `get_market_hours`
156
+ 4. Create `AccountNumbers` class and add `return_data_objects` to `get_account_numbers`
157
+
158
+ ### Low Priority
159
+ 1. Create `UserPreferences` class and add `return_data_objects` to `get_user_preferences`
160
+ 2. Create `OptionExpirationChain` class and add `return_data_objects` to `get_option_expiration_chain`
161
+
162
+ ### Mutation Operations (May not need data objects)
163
+ - `place_order`, `replace_order`, `cancel_order` - These are write operations that typically return success/failure status rather than structured data, so they may not need data objects.
164
+
165
+ ## TODO List - Data Object Implementation
166
+
167
+ ### ✅ Completed
168
+ - [x] Collect fixture data from Schwab API
169
+ - [x] Analyze fixture data structures
170
+ - [x] **AccountNumbers** data object class
171
+ - [x] Create `lib/schwab_rb/data_objects/account_numbers.rb`
172
+ - [x] Create `spec/data_objects/account_numbers_spec.rb`
173
+ - [x] Use fixture: `spec/fixtures/account_numbers.json`
174
+ - [x] **UserPreferences** data object class
175
+ - [x] Create `lib/schwab_rb/data_objects/user_preferences.rb`
176
+ - [x] Create `spec/data_objects/user_preferences_spec.rb`
177
+ - [x] Use fixture: `spec/fixtures/user_preferences.json`
178
+
179
+ ### � In Progress
180
+ - [x] **UserPreferences** data object class
181
+ - [x] Create `lib/schwab_rb/data_objects/user_preferences.rb`
182
+ - [x] Create `spec/data_objects/user_preferences_spec.rb`
183
+ - [x] Use fixture: `spec/fixtures/user_preferences.json`
184
+
185
+ - [x] **OptionExpirationChain** data object class
186
+ - [x] Create `lib/schwab_rb/data_objects/option_expiration_chain.rb`
187
+ - [x] Create `spec/data_objects/option_expiration_chain_spec.rb`
188
+ - [x] Use fixture: `spec/fixtures/option_expiration_chain.json`
189
+
190
+ - [x] **PriceHistory** data object class
191
+ - [x] Create `lib/schwab_rb/data_objects/price_history.rb`
192
+ - [x] Create `spec/data_objects/price_history_spec.rb`
193
+ - [x] Use fixtures: `spec/fixtures/price_history_*.json`
194
+ - [x] **MarketHours** data object class
195
+ - [x] Create `lib/schwab_rb/data_objects/market_hours.rb`
196
+ - [x] Create `spec/data_objects/market_hours_spec.rb`
197
+ - [x] Use fixtures: `spec/fixtures/market_hours_*.json`
198
+
199
+ ### 📋 Todo
200
+ - [ ] **Movers** data object class
201
+ - [ ] Create `lib/schwab_rb/data_objects/movers.rb`
202
+ - [ ] Create `spec/data_objects/movers_spec.rb`
203
+ - [ ] Use fixture: `spec/fixtures/movers_basic.json`
204
+
205
+ ### 🔄 BaseClient Updates
206
+ - [x] Update `get_account_numbers` to support `return_data_objects: true`
207
+ - [x] Update `get_order` to support `return_data_objects: true`
208
+ - [x] Update `get_all_linked_account_orders` to support `return_data_objects: true`
209
+ - [x] Update `get_user_preferences` to support `return_data_objects: true`
210
+ - [x] Update `get_option_expiration_chain` to support `return_data_objects: true`
211
+ - [x] Update `get_price_history` to support `return_data_objects: true`
212
+ - [x] Update `get_market_hours` to support `return_data_objects: true`
213
+ - [x] Update `get_instruments` to support `return_data_objects: true`
214
+ - [x] Update `get_instrument_by_cusip` to support `return_data_objects: true`
215
+ - [ ] Update `get_movers` to support `return_data_objects: true` (skipped for now)
216
+
217
+ ## Summary
218
+
219
+ - **Total API methods analyzed**: 27
220
+ - **Methods currently returning data objects**: 9 (33%)
221
+ - **Methods not returning data objects**: 18 (67%)
222
+ - **New data object classes needed**: 6
223
+ - **Existing classes that could be reused**: 2 (`Order`, `Instrument`)
@@ -0,0 +1,82 @@
1
+ # Data Objects Refactoring Plan
2
+
3
+ ## Overview
4
+ Moving all data object classes from `options_trader` gem to `schwab_rb` gem, along with their specs, and updating the architecture to support both raw JSON and data object returns.
5
+
6
+ ## Phase 1: Analysis ✅
7
+
8
+ ### Data Objects to Move (from options_trader to schwab_rb):
9
+ - `lib/options_trader/schwab/data_objects/account.rb` → `lib/schwab_rb/data_objects/account.rb`
10
+ - `lib/options_trader/schwab/data_objects/instrument.rb` → `lib/schwab_rb/data_objects/instrument.rb`
11
+ - `lib/options_trader/schwab/data_objects/option.rb` → `lib/schwab_rb/data_objects/option.rb`
12
+ - `lib/options_trader/schwab/data_objects/option_chain.rb` → `lib/schwab_rb/data_objects/option_chain.rb`
13
+ - `lib/options_trader/schwab/data_objects/order.rb` → `lib/schwab_rb/data_objects/order.rb`
14
+ - `lib/options_trader/schwab/data_objects/order_leg.rb` → `lib/schwab_rb/data_objects/order_leg.rb`
15
+ - `lib/options_trader/schwab/data_objects/order_preview.rb` → `lib/schwab_rb/data_objects/order_preview.rb`
16
+ - `lib/options_trader/schwab/data_objects/position.rb` → `lib/schwab_rb/data_objects/position.rb`
17
+ - `lib/options_trader/schwab/data_objects/quote.rb` → `lib/schwab_rb/data_objects/quote.rb`
18
+ - `lib/options_trader/schwab/data_objects/transaction.rb` → `lib/schwab_rb/data_objects/transaction.rb`
19
+
20
+ ### Specs to Move:
21
+ - All files from `options_trader/spec/schwab/data_objects/` → `schwab_rb/spec/data_objects/`
22
+
23
+ ### Namespace Changes:
24
+ - `OptionsTrader::Schwab::DataObjects` → `SchwabRb::DataObjects`
25
+
26
+ ## Phase 2: Move Data Objects ✅ COMPLETED
27
+ - [x] Move each data object class
28
+ - [x] Update namespaces from OptionsTrader::Schwab::DataObjects to SchwabRb::DataObjects
29
+ - [x] Update internal requires and dependencies
30
+
31
+ ## Phase 3: Move and Update Specs ✅ COMPLETED
32
+ - [x] Move spec files
33
+ - [x] Update spec namespaces and requires
34
+ - [x] Copy fixture files from options_trader to schwab_rb
35
+ - [x] Ensure all tests pass
36
+
37
+ ## Phase 4: Update Base Client ✅ COMPLETED
38
+ - [x] Add `return_data_objects: true` parameter to get_account method
39
+ - [x] Add `return_data_objects: true` parameter to get_accounts method
40
+ - [x] Add data object imports to base client
41
+ - [x] Maintain backward compatibility with raw JSON responses
42
+ - [x] Update method signatures and documentation
43
+
44
+ ## Phase 5: Update Options Trader Schwab Mixin ✅ COMPLETED
45
+ - [x] Remove data object instantiation from schwab.rb mixin
46
+ - [x] Update account() method to use return_data_objects parameter
47
+ - [x] Simplify other methods to remove DataObjects references
48
+ - [x] Clean up require statements
49
+ - [x] Ensure mixin works with new architecture
50
+
51
+ ## Phase 6: Testing and Cleanup ✅ COMPLETED
52
+ - [x] Run schwab_rb tests - all data object tests passing
53
+ - [x] Verify options_trader integration works
54
+ - [x] Clean commit history with only relevant files
55
+ - [x] Add return_data_objects parameter to remaining base client methods
56
+ - [x] Update options_trader mixin to use new parameters
57
+ - [x] Eliminate all manual JSON parsing from mixin
58
+ - [x] Update documentation
59
+
60
+ ## ✅ **REFACTORING COMPLETE**
61
+
62
+ ### **Final Status:**
63
+ - **All 6 phases completed successfully**
64
+ - **10 data object classes** moved from options_trader to schwab_rb
65
+ - **37 comprehensive tests** - all passing
66
+ - **Dual architecture**: Support for both data objects and raw JSON
67
+ - **Full backward compatibility** maintained
68
+ - **Clean git history** with 17 systematic commits
69
+
70
+ ### **Architecture Achievement:**
71
+ - **schwab_rb**: Now the authoritative home for all Schwab data objects
72
+ - **options_trader**: Simplified mixin that leverages schwab_rb's capabilities
73
+ - **Base client**: Full support for `return_data_objects` parameter across all methods
74
+ - **Zero manual JSON parsing** in options_trader - all handled by schwab_rb
75
+
76
+ ## Notes
77
+ - Started: 2025-07-19
78
+ - Completed: 2025-07-19
79
+ - Duration: Single day systematic refactoring
80
+ - All 37 data object tests passing in schwab_rb
81
+ - 17 commits created with systematic refactoring approach
82
+ - Clean commits with only relevant files for each phase
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Script to fetch account numbers data and save as fixture
5
+ # Usage: ruby examples/fetch_account_numbers.rb
6
+
7
+ require_relative '../lib/schwab_rb'
8
+ require 'dotenv'
9
+ require 'json'
10
+ require 'fileutils'
11
+
12
+ Dotenv.load
13
+
14
+ def create_client
15
+ token_path = ENV['TOKEN_PATH'] || 'schwab_token.json'
16
+ SchwabRb::Auth.init_client_easy(
17
+ ENV['SCHWAB_API_KEY'],
18
+ ENV['SCHWAB_APP_SECRET'],
19
+ ENV['APP_CALLBACK_URL'],
20
+ token_path
21
+ )
22
+ end
23
+
24
+ def fetch_account_numbers
25
+ client = create_client
26
+ puts "Fetching account numbers..."
27
+
28
+ response = client.get_account_numbers
29
+ parsed_data = JSON.parse(response.body, symbolize_names: true)
30
+
31
+ # Create fixtures directory if it doesn't exist
32
+ fixtures_dir = File.join(__dir__, '..', 'spec', 'fixtures')
33
+ FileUtils.mkdir_p(fixtures_dir)
34
+
35
+ # Save the raw response
36
+ fixture_file = File.join(fixtures_dir, 'account_numbers.json')
37
+ File.write(fixture_file, JSON.pretty_generate(parsed_data))
38
+
39
+ puts "Account numbers data saved to: #{fixture_file}"
40
+ puts "Sample data: #{parsed_data.first(2)}"
41
+
42
+ rescue => e
43
+ puts "Error fetching account numbers: #{e.message}"
44
+ puts e.backtrace.first(3)
45
+ end
46
+
47
+ if __FILE__ == $0
48
+ fetch_account_numbers
49
+ end
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Script to fetch user preferences data and save as fixture
5
+ # Usage: ruby examples/fetch_user_preferences.rb
6
+
7
+ require_relative '../lib/schwab_rb'
8
+ require 'dotenv'
9
+ require 'json'
10
+ require 'fileutils'
11
+
12
+ Dotenv.load
13
+
14
+ def create_client
15
+ token_path = ENV['TOKEN_PATH'] || 'schwab_token.json'
16
+ SchwabRb::Auth.init_client_easy(
17
+ ENV['SCHWAB_API_KEY'],
18
+ ENV['SCHWAB_APP_SECRET'],
19
+ ENV['APP_CALLBACK_URL'],
20
+ token_path
21
+ )
22
+ end
23
+
24
+ def fetch_user_preferences
25
+ client = create_client
26
+ puts "Fetching user preferences..."
27
+
28
+ response = client.get_user_preferences
29
+ parsed_data = JSON.parse(response.body, symbolize_names: true)
30
+
31
+ # Create fixtures directory if it doesn't exist
32
+ fixtures_dir = File.join(__dir__, '..', 'spec', 'fixtures')
33
+ FileUtils.mkdir_p(fixtures_dir)
34
+
35
+ # Save the raw response
36
+ fixture_file = File.join(fixtures_dir, 'user_preferences.json')
37
+ File.write(fixture_file, JSON.pretty_generate(parsed_data))
38
+
39
+ puts "User preferences data saved to: #{fixture_file}"
40
+ puts "Sample data keys: #{parsed_data.keys.first(5)}"
41
+
42
+ rescue => e
43
+ puts "Error fetching user preferences: #{e.message}"
44
+ puts e.backtrace.first(3)
45
+ end
46
+
47
+ if __FILE__ == $0
48
+ fetch_user_preferences
49
+ end