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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ded8507159f0890f4e0b0d2ad2d17bb009667423fd088eeb30f914acefa72bcf
4
- data.tar.gz: d2ad202eb8e83e7ce4aeee47f97f91e4a258200cee0b119b98fcdcc89912de7c
3
+ metadata.gz: 80e57e5f4da3062980fd00cfbce3661185d36a6ccd34a87d51c3d6cd8c3400f8
4
+ data.tar.gz: 1f2ec23196743009009fda3942719b680e474a25dbf54cb5d412f25203140abf
5
5
  SHA512:
6
- metadata.gz: d396924935ff3058b2a09a3266ab4ab7702c6897260b1584186967d282a928f292cf8a726f5c9f7bd93d9657e9c1fea7f3ac06c51f3e9885e77c0a7e54a428d0
7
- data.tar.gz: ebe03d4d4ac61df89d8d06fde06f9e098074a1bcf550adef79976291cbc271d6a82be8c36d2b7cbddd77b1f7e91181b982fc3dde3c45b18f7c695b10a73c46ef
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
- disconnect
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
- break if message.nil?
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
- SchwabRb::Logger.logger.debug("Stream sending: #{json_string}")
225
- @connection.write(json_string)
226
- @connection.flush
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwabRb
4
- VERSION = "1.0.2"
4
+ VERSION = "1.0.4"
5
5
  end
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.2
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-06 00:00:00.000000000 Z
10
+ date: 2026-09-21 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: async