schwab_rb 1.0.2 → 1.0.4
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/CHANGELOG.md +25 -0
- data/README.md +41 -0
- data/lib/schwab_rb/storage/database.rb +1 -7
- data/lib/schwab_rb/stream/base.rb +41 -6
- data/lib/schwab_rb/stream/client.rb +7 -0
- data/lib/schwab_rb/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80e57e5f4da3062980fd00cfbce3661185d36a6ccd34a87d51c3d6cd8c3400f8
|
|
4
|
+
data.tar.gz: 1f2ec23196743009009fda3942719b680e474a25dbf54cb5d412f25203140abf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd23d7763c0ee77ec39a45bb653d4c65f4a19ae1962b12c9c3c51cd1b7035114158fb77f36e090a3909d2d08dd88f32b6626d92fa5f038b3a563ff369dbfce49
|
|
7
|
+
data.tar.gz: 17bab9ddddca12ed8e69b580936c4f166088cd8e919d530ad690375d0bf674fc4eab539cfd5b7c241f56358c2182cca59e9d3f4b4b3e6db15dcfa8efbe67ae52
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.0.4] - 2026-09-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Dynamic symbol unsubscription (`stream.unsub`) using Schwab `UNSUBS` command to remove symbols as market windows close without disconnecting.
|
|
7
|
+
- `stream.add` method (alias for `on`) for explicitly adding subscriptions to active streams.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Treat `nil` socket read in `Base#run_receive_loop` as disconnect error instead of clean exit to prevent silent reconnect spin loops.
|
|
11
|
+
- Outbound WebSocket write synchronization using a Mutex to prevent framing issues when subscriptions change dynamically.
|
|
12
|
+
|
|
13
|
+
## [1.0.3] - 2026-09-19
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Prevent `load_token` from deleting database row on expired access token (#33).
|
|
17
|
+
|
|
18
|
+
## [1.0.2] - 2026-09-18
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Fix WebSocket framing to resolve stream login bad command formatting (#32).
|
|
22
|
+
|
|
23
|
+
## [1.0.1] - 2026-09-17
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Fix stream login authorization header formatting and STDOUT logger routing (#30).
|
|
27
|
+
|
|
3
28
|
## [1.0.0] - 2026-09-05
|
|
4
29
|
|
|
5
30
|
### Breaking Changes
|
data/README.md
CHANGED
|
@@ -164,6 +164,47 @@ price_history = client.get_price_history(
|
|
|
164
164
|
)
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
### Real-Time WebSocket Streaming
|
|
168
|
+
|
|
169
|
+
Stream Level 1 quotes, Level 2 order books, chart candles, and account activity over a single WebSocket connection:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
client = SchwabRb::Auth.init_client_easy(
|
|
173
|
+
ENV['SCHWAB_API_KEY'],
|
|
174
|
+
ENV['SCHWAB_APP_SECRET'],
|
|
175
|
+
ENV['APP_CALLBACK_URL']
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
stream = SchwabRb::Stream::Client.new(client)
|
|
179
|
+
|
|
180
|
+
# Subscribe to Level 1 Equities
|
|
181
|
+
stream.on(:level_one_equities, symbols: %w[SPY QQQ], fields: :all) do |event|
|
|
182
|
+
puts "Equity quote: #{event}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Subscribe to Level 1 Futures
|
|
186
|
+
stream.on(:level_one_futures, symbols: ["/ES"], fields: :all) do |event|
|
|
187
|
+
puts "Futures quote: #{event}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Subscribe to Level 2 Order Book
|
|
191
|
+
stream.on(:nyse_book, symbols: ["SPY"], fields: :all) do |event|
|
|
192
|
+
puts "Order book: #{event}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Start async in a background thread
|
|
196
|
+
stream.start_async
|
|
197
|
+
|
|
198
|
+
# Dynamically add symbols while streaming
|
|
199
|
+
stream.add(:level_one_equities, symbols: ["IWM"])
|
|
200
|
+
|
|
201
|
+
# Dynamically remove symbols when window closes without dropping stream
|
|
202
|
+
stream.unsub(:level_one_equities, symbols: ["SPY", "QQQ"])
|
|
203
|
+
|
|
204
|
+
# Stop streaming when finished
|
|
205
|
+
stream.stop
|
|
206
|
+
```
|
|
207
|
+
|
|
167
208
|
## Data Objects
|
|
168
209
|
|
|
169
210
|
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:
|
|
@@ -30,18 +30,12 @@ module SchwabRb
|
|
|
30
30
|
end
|
|
31
31
|
return nil unless row
|
|
32
32
|
|
|
33
|
-
expires_at = row["expires_at"]
|
|
34
|
-
if expires_at && Time.now.to_i > expires_at
|
|
35
|
-
synchronize { db.execute("DELETE FROM tokens WHERE api_key = ?", [api_key]) }
|
|
36
|
-
return nil
|
|
37
|
-
end
|
|
38
|
-
|
|
39
33
|
{
|
|
40
34
|
"timestamp" => row["timestamp"],
|
|
41
35
|
"token" => {
|
|
42
36
|
"access_token" => row["access_token"],
|
|
43
37
|
"refresh_token" => row["refresh_token"],
|
|
44
|
-
"expires_at" => expires_at,
|
|
38
|
+
"expires_at" => row["expires_at"],
|
|
45
39
|
"expires_in" => row["expires_in"],
|
|
46
40
|
"token_type" => row["token_type"],
|
|
47
41
|
"scope" => row["scope"],
|
|
@@ -27,13 +27,14 @@ module SchwabRb
|
|
|
27
27
|
@connect_time = nil
|
|
28
28
|
@backoff_time = INITIAL_BACKOFF
|
|
29
29
|
@should_run = false
|
|
30
|
+
@write_mutex = Mutex.new
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def on(service_symbol, symbols: nil, fields: nil, &block)
|
|
33
34
|
service_name = Services.lookup(service_symbol)
|
|
34
35
|
resolved_fields = fields ? Fields.resolve(service_name, fields) : nil
|
|
35
36
|
|
|
36
|
-
handler = { symbols: Array(symbols), fields: resolved_fields, callback: block }
|
|
37
|
+
handler = { symbols: Array(symbols).map(&:to_s), fields: resolved_fields, callback: block }
|
|
37
38
|
@handlers[service_name] ||= []
|
|
38
39
|
@handlers[service_name] << handler
|
|
39
40
|
|
|
@@ -46,6 +47,36 @@ module SchwabRb
|
|
|
46
47
|
|
|
47
48
|
self
|
|
48
49
|
end
|
|
50
|
+
alias add on
|
|
51
|
+
|
|
52
|
+
def unsub(service_symbol, symbols: nil)
|
|
53
|
+
service_name = Services.lookup(service_symbol)
|
|
54
|
+
symbols_to_remove = Array(symbols).map(&:to_s)
|
|
55
|
+
return self if symbols_to_remove.empty?
|
|
56
|
+
|
|
57
|
+
if @subscriptions[service_name]
|
|
58
|
+
@subscriptions[service_name][:symbols] -= symbols_to_remove
|
|
59
|
+
@subscriptions.delete(service_name) if @subscriptions[service_name][:symbols].empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if @handlers[service_name]
|
|
63
|
+
@handlers[service_name].each do |h|
|
|
64
|
+
h[:symbols] -= symbols_to_remove
|
|
65
|
+
end
|
|
66
|
+
@handlers[service_name].reject! { |h| h[:symbols].empty? }
|
|
67
|
+
@handlers.delete(service_name) if @handlers[service_name].empty?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if @connected
|
|
71
|
+
request = @message_builder.build_request(
|
|
72
|
+
service_name, Commands::UNSUBS,
|
|
73
|
+
keys: symbols_to_remove
|
|
74
|
+
)
|
|
75
|
+
send_message(@message_builder.wrap_requests(request))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
self
|
|
79
|
+
end
|
|
49
80
|
|
|
50
81
|
def start
|
|
51
82
|
@should_run = true
|
|
@@ -64,7 +95,7 @@ module SchwabRb
|
|
|
64
95
|
|
|
65
96
|
def stop
|
|
66
97
|
@should_run = false
|
|
67
|
-
|
|
98
|
+
@connected = false
|
|
68
99
|
end
|
|
69
100
|
|
|
70
101
|
def connected?
|
|
@@ -161,7 +192,7 @@ module SchwabRb
|
|
|
161
192
|
def run_receive_loop
|
|
162
193
|
while @connected && @should_run
|
|
163
194
|
message = @connection.read
|
|
164
|
-
|
|
195
|
+
raise "Connection closed by remote host" if message.nil?
|
|
165
196
|
|
|
166
197
|
parsed = JSON.parse(message.to_str)
|
|
167
198
|
dispatch_message(parsed)
|
|
@@ -221,9 +252,13 @@ module SchwabRb
|
|
|
221
252
|
end
|
|
222
253
|
|
|
223
254
|
def send_message(json_string)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
255
|
+
@write_mutex.synchronize do
|
|
256
|
+
return unless @connection
|
|
257
|
+
|
|
258
|
+
SchwabRb::Logger.logger.debug("Stream sending: #{json_string}")
|
|
259
|
+
@connection.write(json_string)
|
|
260
|
+
@connection.flush
|
|
261
|
+
end
|
|
227
262
|
end
|
|
228
263
|
|
|
229
264
|
def merge_subscription(existing, handler)
|
|
@@ -15,6 +15,12 @@ module SchwabRb
|
|
|
15
15
|
@stream.on(service_symbol, symbols: symbols, fields: fields, &block)
|
|
16
16
|
self
|
|
17
17
|
end
|
|
18
|
+
alias add on
|
|
19
|
+
|
|
20
|
+
def unsub(service_symbol, symbols: nil)
|
|
21
|
+
@stream.unsub(service_symbol, symbols: symbols)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
18
24
|
|
|
19
25
|
def start
|
|
20
26
|
@thread = Thread.new do
|
|
@@ -32,6 +38,7 @@ module SchwabRb
|
|
|
32
38
|
|
|
33
39
|
def stop
|
|
34
40
|
@stream.stop
|
|
41
|
+
@thread&.raise(Interrupt)
|
|
35
42
|
@thread&.join(5)
|
|
36
43
|
@thread = nil
|
|
37
44
|
end
|
data/lib/schwab_rb/version.rb
CHANGED
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: 1.0.
|
|
4
|
+
version: 1.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joseph Platta
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|