schwab_rb 0.9.3 → 1.0.1

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.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ module Stream
5
+ module Services
6
+ LEVELONE_EQUITIES = "LEVELONE_EQUITIES"
7
+ LEVELONE_OPTIONS = "LEVELONE_OPTIONS"
8
+ LEVELONE_FUTURES = "LEVELONE_FUTURES"
9
+ LEVELONE_FUTURES_OPTIONS = "LEVELONE_FUTURES_OPTIONS"
10
+ LEVELONE_FOREX = "LEVELONE_FOREX"
11
+ NYSE_BOOK = "NYSE_BOOK"
12
+ NASDAQ_BOOK = "NASDAQ_BOOK"
13
+ OPTIONS_BOOK = "OPTIONS_BOOK"
14
+ CHART_EQUITY = "CHART_EQUITY"
15
+ CHART_FUTURES = "CHART_FUTURES"
16
+ SCREENER_EQUITY = "SCREENER_EQUITY"
17
+ SCREENER_OPTION = "SCREENER_OPTION"
18
+ ACCT_ACTIVITY = "ACCT_ACTIVITY"
19
+
20
+ ALL = constants.map { |c| const_get(c) }.freeze
21
+
22
+ SYMBOL_TO_SERVICE = {
23
+ level_one_equities: LEVELONE_EQUITIES,
24
+ level_one_options: LEVELONE_OPTIONS,
25
+ level_one_futures: LEVELONE_FUTURES,
26
+ level_one_futures_options: LEVELONE_FUTURES_OPTIONS,
27
+ level_one_forex: LEVELONE_FOREX,
28
+ nyse_book: NYSE_BOOK,
29
+ nasdaq_book: NASDAQ_BOOK,
30
+ options_book: OPTIONS_BOOK,
31
+ chart_equity: CHART_EQUITY,
32
+ chart_futures: CHART_FUTURES,
33
+ screener_equity: SCREENER_EQUITY,
34
+ screener_option: SCREENER_OPTION,
35
+ acct_activity: ACCT_ACTIVITY
36
+ }.freeze
37
+
38
+ def self.lookup(symbol)
39
+ SYMBOL_TO_SERVICE[symbol] || raise(ArgumentError, "Unknown service: #{symbol.inspect}")
40
+ end
41
+ end
42
+
43
+ module Commands
44
+ LOGIN = "LOGIN"
45
+ LOGOUT = "LOGOUT"
46
+ SUBS = "SUBS"
47
+ ADD = "ADD"
48
+ UNSUBS = "UNSUBS"
49
+ VIEW = "VIEW"
50
+ end
51
+ end
52
+ end
@@ -31,7 +31,11 @@ module SchwabRb
31
31
 
32
32
  return null_logger if [:null, "/dev/null"].include?(log_destination)
33
33
 
34
- setup_log_file(log_destination) if log_destination.is_a?(String) && log_destination != "STDOUT"
34
+ if log_destination.is_a?(String) && log_destination.upcase == "STDOUT"
35
+ log_destination = $stdout
36
+ else
37
+ setup_log_file(log_destination)
38
+ end
35
39
 
36
40
  ::Logger.new(log_destination, "weekly").tap do |log|
37
41
  log.level = parse_log_level(config.log_level)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwabRb
4
- VERSION = "0.9.3"
4
+ VERSION = "1.0.1"
5
5
  end
data/lib/schwab_rb.rb CHANGED
@@ -2,12 +2,11 @@
2
2
 
3
3
  require_relative "schwab_rb/version"
4
4
  require_relative "schwab_rb/configuration"
5
- require_relative "schwab_rb/account_hash_manager"
6
5
  require_relative "schwab_rb/path_support"
7
6
  require_relative "schwab_rb/auth/token_manager"
8
7
  require_relative "schwab_rb/auth/token"
9
8
  require_relative "schwab_rb/auth/init_client_login"
10
- require_relative "schwab_rb/auth/init_client_token_file"
9
+ require_relative "schwab_rb/auth/init_client_from_database"
11
10
  require_relative "schwab_rb/auth/init_client_easy"
12
11
  require_relative "schwab_rb/auth/auth_context"
13
12
  require_relative "schwab_rb/clients/client"
@@ -54,6 +53,13 @@ require_relative "schwab_rb/orders/vertical_order"
54
53
  require_relative "schwab_rb/orders/vertical_roll_order"
55
54
  require_relative "schwab_rb/orders/single_order"
56
55
  require_relative "schwab_rb/orders/oco_order"
56
+ require_relative "schwab_rb/storage/database"
57
+ require_relative "schwab_rb/stream/services"
58
+ require_relative "schwab_rb/stream/fields"
59
+ require_relative "schwab_rb/stream/message_builder"
60
+ require_relative "schwab_rb/stream/base"
61
+ require_relative "schwab_rb/stream/client"
62
+ require_relative "schwab_rb/stream/async_client"
57
63
  require_relative "schwab_rb/cli"
58
64
 
59
65
  module SchwabRb
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.9.3
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Platta
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-23 00:00:00.000000000 Z
10
+ date: 2026-09-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async
@@ -127,6 +127,34 @@ dependencies:
127
127
  - - ">="
128
128
  - !ruby/object:Gem::Version
129
129
  version: '0'
130
+ - !ruby/object:Gem::Dependency
131
+ name: sqlite3
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '2.0'
137
+ type: :runtime
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '2.0'
144
+ - !ruby/object:Gem::Dependency
145
+ name: async-websocket
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '0.26'
151
+ type: :runtime
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '0.26'
130
158
  - !ruby/object:Gem::Dependency
131
159
  name: rspec
132
160
  requirement: !ruby/object:Gem::Requirement
@@ -209,7 +237,6 @@ files:
209
237
  - LICENSE.txt
210
238
  - README.md
211
239
  - Rakefile
212
- - doc/ACCOUNT_MANAGEMENT.md
213
240
  - doc/LOGGING.md
214
241
  - doc/PLACE_ORDER_SAMPLES.md
215
242
  - doc/QUICK_START.md
@@ -219,14 +246,15 @@ files:
219
246
  - examples/fetch_price_history.rb
220
247
  - examples/fetch_user_preferences.rb
221
248
  - examples/place_oco_order.rb
249
+ - examples/stream_level_one.rb
250
+ - examples/stream_order_book.rb
222
251
  - exe/schwab_rb
223
252
  - lib/schwab_rb.rb
224
253
  - lib/schwab_rb/account.rb
225
- - lib/schwab_rb/account_hash_manager.rb
226
254
  - lib/schwab_rb/auth/auth_context.rb
227
255
  - lib/schwab_rb/auth/init_client_easy.rb
256
+ - lib/schwab_rb/auth/init_client_from_database.rb
228
257
  - lib/schwab_rb/auth/init_client_login.rb
229
- - lib/schwab_rb/auth/init_client_token_file.rb
230
258
  - lib/schwab_rb/auth/login_flow_server.rb
231
259
  - lib/schwab_rb/auth/token.rb
232
260
  - lib/schwab_rb/auth/token_manager.rb
@@ -284,6 +312,13 @@ files:
284
312
  - lib/schwab_rb/price_history.rb
285
313
  - lib/schwab_rb/price_history/downloader.rb
286
314
  - lib/schwab_rb/quote.rb
315
+ - lib/schwab_rb/storage/database.rb
316
+ - lib/schwab_rb/stream/async_client.rb
317
+ - lib/schwab_rb/stream/base.rb
318
+ - lib/schwab_rb/stream/client.rb
319
+ - lib/schwab_rb/stream/fields.rb
320
+ - lib/schwab_rb/stream/message_builder.rb
321
+ - lib/schwab_rb/stream/services.rb
287
322
  - lib/schwab_rb/transaction.rb
288
323
  - lib/schwab_rb/utils/enum_enforcer.rb
289
324
  - lib/schwab_rb/utils/logger.rb
@@ -1,297 +0,0 @@
1
- # Account Management Guide
2
-
3
- The schwab_rb gem provides a secure and convenient way to manage multiple Schwab accounts using friendly account names instead of exposing account numbers or hashes in your code.
4
-
5
- ## Table of Contents
6
-
7
- - [Overview](#overview)
8
- - [Quick Start](#quick-start)
9
- - [Configuration](#configuration)
10
- - [Account Names Setup](#account-names-setup)
11
- - [Using Account Names](#using-account-names)
12
- - [Security Best Practices](#security-best-practices)
13
- - [Troubleshooting](#troubleshooting)
14
- - [API Reference](#api-reference)
15
-
16
- ## Overview
17
-
18
- Schwab's API requires account hashes for most operations, which:
19
- - Change periodically (accounts get "rehashed")
20
- - Are inconvenient to look up and copy
21
- - Expose sensitive account numbers in code
22
-
23
- schwab_rb provides an account name mapping system that:
24
- - **Increases Security**: Account numbers never appear in your code
25
- - **Improves Usability**: Reference accounts by friendly names like "my_trading_account"
26
- - **Auto-Updates**: Account hashes refresh automatically
27
- - **Handles Staleness**: Retries failed requests with fresh hashes
28
-
29
- ## Quick Start
30
-
31
- ### 1. Create Account Names File
32
-
33
- Create `~/.schwab_rb/account_names.json` with your account mappings:
34
-
35
- ```json
36
- {
37
- "my_trading_account": "12345678",
38
- "my_ira": "87654321",
39
- "my_roth_ira": "11223344"
40
- }
41
- ```
42
-
43
- ### 2. Initialize Client and Fetch Hashes
44
-
45
- ```ruby
46
- require "schwab_rb"
47
-
48
- # Initialize client
49
- client = SchwabRb::Auth.init_client_easy(
50
- ENV['SCHWAB_API_KEY'],
51
- ENV['SCHWAB_APP_SECRET'],
52
- ENV['SCHWAB_APP_CALLBACK_URL'],
53
- ENV['SCHWAB_TOKEN_PATH']
54
- )
55
-
56
- # Fetch account numbers and populate hashes
57
- client.get_account_numbers
58
- ```
59
-
60
- This automatically creates `~/.schwab_rb/account_hashes.json` with the name-to-hash mappings.
61
-
62
- ### 3. Use Account Names in API Calls
63
-
64
- ```ruby
65
- # Get account by name
66
- account = client.get_account(account_name: "my_trading_account")
67
-
68
- # Place order using account name
69
- order = client.place_order(order_spec, account_name: "my_ira")
70
-
71
- # Get transactions
72
- transactions = client.get_transactions(account_name: "my_roth_ira")
73
- ```
74
-
75
- ## Configuration
76
-
77
- ### Default Paths
78
-
79
- By default, schwab_rb uses:
80
- - `schwab_home`: `~/.schwab_rb`
81
- - `account_hashes_path`: `~/.schwab_rb/account_hashes.json`
82
- - `account_names_path`: `~/.schwab_rb/account_names.json`
83
-
84
- ### Custom Paths
85
-
86
- #### Via Environment Variables
87
-
88
- ```bash
89
- export SCHWAB_HOME="/custom/path"
90
- export SCHWAB_ACCOUNT_HASHES_PATH="/custom/path/hashes.json"
91
- export SCHWAB_ACCOUNT_NAMES_PATH="/custom/path/names.json"
92
- ```
93
-
94
- #### Via Ruby Configuration
95
-
96
- ```ruby
97
- SchwabRb.configure do |config|
98
- config.schwab_home = "/custom/path"
99
- config.account_hashes_path = "/custom/path/hashes.json"
100
- config.account_names_path = "/custom/path/names.json"
101
- end
102
- ```
103
-
104
- ## Account Names Setup
105
-
106
- ### File Format
107
-
108
- `account_names.json` is a simple JSON object mapping friendly names to 8-digit account numbers:
109
-
110
- ```json
111
- {
112
- "descriptive_name": "account_number"
113
- }
114
- ```
115
-
116
- ## Using Account Names
117
-
118
- ### Methods Supporting Account Names
119
-
120
- All account-specific methods support both `account_hash` and `account_name`:
121
-
122
- - `get_account(account_name:)` or `get_account(account_hash)`
123
- - `get_order(order_id, account_name:)`
124
- - `cancel_order(order_id, account_name:)`
125
- - `get_account_orders(account_name:)`
126
- - `place_order(order_spec, account_name:)`
127
- - `replace_order(order_id, order_spec, account_name:)`
128
- - `preview_order(order_spec, account_name:)`
129
- - `get_transactions(account_name:)`
130
- - `get_transaction(activity_id, account_name:)`
131
-
132
- ### Calling Patterns
133
-
134
- ```ruby
135
- # Using account name (keyword argument) - RECOMMENDED
136
- client.get_account(account_name: "my_trading_account")
137
-
138
- # Using account hash (positional argument) - still works
139
- client.get_account("ABC123HASH")
140
-
141
- # Using account hash (keyword argument)
142
- client.get_account(account_hash: "ABC123HASH")
143
-
144
- # Priority: account_name always takes precedence
145
- client.get_account("HASH1", account_name: "my_ira") # Uses "my_ira"
146
- ```
147
-
148
- ### List Available Accounts
149
-
150
- ```ruby
151
- # Get list of configured account names
152
- names = client.available_account_names
153
- # => ["my_trading_account", "my_ira", "my_roth_ira"]
154
-
155
- puts "Available accounts:"
156
- names.each { |name| puts " - #{name}" }
157
- ```
158
-
159
- ## Security
160
-
161
- ### 1. Protect Your Files
162
-
163
- The account files contain sensitive information:
164
-
165
- ```bash
166
- # Set restrictive permissions
167
- chmod 600 ~/.schwab_rb/account_names.json
168
- chmod 600 ~/.schwab_rb/account_hashes.json
169
-
170
- # Verify permissions
171
- ls -l ~/.schwab_rb/
172
- ```
173
-
174
- ### 2. Don't Commit to Version Control
175
-
176
- Add to `.gitignore`:
177
-
178
- ```
179
- .schwab_rb/
180
- **/account_names.json
181
- **/account_hashes.json
182
- ```
183
-
184
- ### 3. Environment-Specific Configs
185
-
186
- Use different configs per environment:
187
-
188
- ```ruby
189
- # In development
190
- SchwabRb.configure do |config|
191
- config.schwab_home = "~/.schwab_rb/development"
192
- end
193
-
194
- # In production
195
- SchwabRb.configure do |config|
196
- config.schwab_home = ENV['SCHWAB_CONFIG_PATH']
197
- end
198
- ```
199
-
200
- ## Troubleshooting
201
-
202
- ### Account Name Not Found
203
-
204
- **Error**: `Account name 'my_account' not found in account hashes.`
205
-
206
- **Solutions**:
207
- 1. Run `client.get_account_numbers` to populate hashes
208
- 2. Check spelling in `account_names.json`
209
- 3. Verify account number is correct
210
-
211
- ### Account Numbers File Missing
212
-
213
- **Error**: `Account names file not found at ~/.schwab_rb/account_names.json`
214
-
215
- **Solutions**:
216
- 1. Create the file with your account mappings (see [Quick Start](#quick-start))
217
- 2. Set custom path via config if file is elsewhere
218
-
219
- ### Account Not in API Response
220
-
221
- **Warning**: `Account 'old_account' (98765432) not found in API response.`
222
-
223
- This warning means an account in your `account_names.json` wasn't returned by the API.
224
-
225
- **Possible Causes**:
226
- - Account was closed
227
- - Wrong account number in `account_names.json`
228
- - Account not accessible with current API credentials
229
-
230
- **Actions**:
231
- 1. Verify account status on Schwab website
232
- 2. Check account number in `account_names.json`
233
- 3. Remove closed accounts from config
234
-
235
- ### Stale Account Hashes
236
-
237
- The gem automatically handles stale hashes:
238
- 1. Request fails with stale hash
239
- 2. Calls `get_account_numbers` to refresh
240
- 3. Retries request with new hash
241
- 4. If still fails, raises original error
242
-
243
- ## API Reference
244
-
245
- ### Client Methods
246
-
247
- #### `available_account_names`
248
-
249
- Returns list of configured account names.
250
-
251
- ```ruby
252
- client.available_account_names
253
- # => ["my_trading_account", "my_ira"]
254
- ```
255
-
256
- **Returns**: `Array<String>` - List of account names from `account_names.json`
257
- **Returns**: `[]` - Empty array if file doesn't exist
258
-
259
- #### Account-Specific Methods
260
-
261
- All methods accept either `account_name:` or `account_hash`:
262
-
263
- ```ruby
264
- # Get account
265
- client.get_account(account_name: "my_trading_account")
266
- client.get_account("ABC123HASH")
267
-
268
- # Get orders
269
- client.get_account_orders(account_name: "my_ira", max_results: 10)
270
-
271
- # Place order
272
- client.place_order(order_spec, account_name: "my_trading_account")
273
- ```
274
-
275
- ### AccountHashManager
276
-
277
- Direct access to the account hash manager:
278
-
279
- ```ruby
280
- manager = SchwabRb::AccountHashManager.new
281
-
282
- # Get available names
283
- manager.available_account_names
284
- # => ["my_trading_account", "my_ira"]
285
-
286
- # Get hash by name
287
- manager.get_hash_by_name("my_trading_account")
288
- # => "ABC123HASH"
289
-
290
- # Get all hashes
291
- manager.get_all_hashes
292
- # => {"my_trading_account" => "ABC123HASH", "my_ira" => "XYZ789HASH"}
293
- ```
294
-
295
- ---
296
-
297
- For more information, see the [main README](../README.md) or visit [github.com/jwplatta/schwab_rb](https://github.com/jwplatta/schwab_rb).
@@ -1,119 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fileutils"
5
-
6
- module SchwabRb
7
- class AccountHashManager
8
- class AccountNamesFileNotFoundError < StandardError; end
9
- class InvalidAccountNamesFileError < StandardError; end
10
-
11
- attr_reader :account_names_path, :account_hashes_path, :account_names
12
-
13
- def initialize(account_names_path = nil, account_hashes_path = nil)
14
- @account_names_path = account_names_path || SchwabRb.configuration.account_names_path
15
- @account_hashes_path = account_hashes_path || SchwabRb.configuration.account_hashes_path
16
- @account_names_path = File.expand_path(@account_names_path)
17
- @account_hashes_path = File.expand_path(@account_hashes_path)
18
- @account_names = []
19
- end
20
-
21
- def update_hashes_from_api_response(account_numbers_response)
22
- account_names = load_account_names
23
- current_hashes = load_account_hashes
24
-
25
- number_to_hash = {}
26
- account_numbers_response.each do |account_data|
27
- account_number = account_data[:accountNumber]
28
- hash_value = account_data[:hashValue]
29
- number_to_hash[account_number] = hash_value
30
- end
31
-
32
- updated_hashes = {}
33
- missing_accounts = []
34
-
35
- account_names.each do |name, account_number|
36
- if number_to_hash.key?(account_number)
37
- updated_hashes[name] = number_to_hash[account_number]
38
- elsif current_hashes.key?(name)
39
- # Keep existing hash but warn that account wasn't in API response
40
- updated_hashes[name] = current_hashes[name]
41
- missing_accounts << { name: name, number: account_number }
42
- else
43
- # Account name exists but no hash found (new or invalid account)
44
- missing_accounts << { name: name, number: account_number }
45
- end
46
- end
47
-
48
- # Log warnings for accounts that weren't found in API response
49
- if missing_accounts.any?
50
- missing_accounts.each do |account|
51
- SchwabRb::Logger.logger.warn(
52
- "Account '#{account[:name]}' not found in API response. " \
53
- "This may indicate a closed account or incorrect account number in account_names.json"
54
- )
55
- end
56
- end
57
-
58
- save_account_hashes(updated_hashes)
59
- updated_hashes
60
- end
61
-
62
- def get_hash_by_name(account_name)
63
- hashes = load_account_hashes
64
- hashes[account_name]
65
- end
66
-
67
- def get_all_hashes
68
- load_account_hashes
69
- end
70
-
71
- def available_account_names
72
- begin
73
- load_account_names.keys
74
- rescue AccountNamesFileNotFoundError
75
- []
76
- end
77
- end
78
-
79
- private
80
-
81
- def load_account_hashes
82
- return {} unless File.exist?(@account_hashes_path)
83
-
84
- begin
85
- json_content = File.read(@account_hashes_path)
86
- JSON.parse(json_content)
87
- rescue JSON::ParserError
88
- {}
89
- end
90
- end
91
-
92
- def save_account_hashes(hashes_map)
93
- FileUtils.mkdir_p(File.dirname(@account_hashes_path))
94
-
95
- File.write(@account_hashes_path, JSON.pretty_generate(hashes_map))
96
- end
97
-
98
- def load_account_names
99
- unless File.exist?(@account_names_path)
100
- raise AccountNamesFileNotFoundError,
101
- "Account names file not found at #{@account_names_path}. " \
102
- "Please create a JSON file mapping account names to account numbers. " \
103
- "Example: {\"my_trading_account\": \"12345678\", \"my_ira\": \"87654321\"}"
104
- end
105
-
106
- begin
107
- json_content = File.read(@account_names_path)
108
- return {} if json_content.strip.empty?
109
-
110
- account_names_hash = JSON.parse(json_content)
111
- @account_names = account_names_hash.keys
112
- account_names_hash
113
- rescue JSON::ParserError => e
114
- raise InvalidAccountNamesFileError,
115
- "Invalid JSON in account names file at #{@account_names_path}: #{e.message}"
116
- end
117
- end
118
- end
119
- end