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.
- checksums.yaml +4 -4
- data/.claude/settings.local.json +18 -1
- data/CHANGELOG.md +20 -4
- data/examples/place_oco_order.rb +1 -4
- data/examples/stream_level_one.rb +65 -0
- data/examples/stream_order_book.rb +57 -0
- data/lib/schwab_rb/auth/init_client_easy.rb +19 -18
- data/lib/schwab_rb/auth/{init_client_token_file.rb → init_client_from_database.rb} +5 -6
- data/lib/schwab_rb/auth/init_client_login.rb +7 -8
- data/lib/schwab_rb/auth/token_manager.rb +25 -15
- data/lib/schwab_rb/cli/app.rb +11 -23
- data/lib/schwab_rb/clients/base_client.rb +51 -75
- data/lib/schwab_rb/configuration.rb +3 -3
- data/lib/schwab_rb/constants.rb +0 -1
- data/lib/schwab_rb/path_support.rb +1 -2
- data/lib/schwab_rb/storage/database.rb +148 -0
- data/lib/schwab_rb/stream/async_client.rb +30 -0
- data/lib/schwab_rb/stream/base.rb +240 -0
- data/lib/schwab_rb/stream/client.rb +44 -0
- data/lib/schwab_rb/stream/fields.rb +339 -0
- data/lib/schwab_rb/stream/message_builder.rb +66 -0
- data/lib/schwab_rb/stream/services.rb +52 -0
- data/lib/schwab_rb/utils/logger.rb +5 -1
- data/lib/schwab_rb/version.rb +1 -1
- data/lib/schwab_rb.rb +8 -2
- metadata +40 -5
- data/doc/ACCOUNT_MANAGEMENT.md +0 -297
- data/lib/schwab_rb/account_hash_manager.rb +0 -119
|
@@ -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
|
-
|
|
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)
|
data/lib/schwab_rb/version.rb
CHANGED
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/
|
|
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.
|
|
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-
|
|
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
|
data/doc/ACCOUNT_MANAGEMENT.md
DELETED
|
@@ -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
|