schwab_rb 0.9.2 → 1.0.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.
- checksums.yaml +4 -4
- data/.claude/settings.local.json +16 -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 +2 -1
- 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/version.rb +1 -1
- data/lib/schwab_rb.rb +8 -2
- metadata +40 -4
- data/lib/schwab_rb/account_hash_manager.rb +0 -119
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/websocket/client"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "services"
|
|
7
|
+
require_relative "fields"
|
|
8
|
+
require_relative "message_builder"
|
|
9
|
+
|
|
10
|
+
module SchwabRb
|
|
11
|
+
module Stream
|
|
12
|
+
class Base
|
|
13
|
+
INITIAL_BACKOFF = 2
|
|
14
|
+
MAX_BACKOFF = 120
|
|
15
|
+
MIN_CONNECTION_TIME = 90
|
|
16
|
+
|
|
17
|
+
attr_reader :client
|
|
18
|
+
|
|
19
|
+
def initialize(client)
|
|
20
|
+
@client = client
|
|
21
|
+
@handlers = {}
|
|
22
|
+
@subscriptions = {}
|
|
23
|
+
@connection = nil
|
|
24
|
+
@message_builder = nil
|
|
25
|
+
@streamer_info = nil
|
|
26
|
+
@connected = false
|
|
27
|
+
@connect_time = nil
|
|
28
|
+
@backoff_time = INITIAL_BACKOFF
|
|
29
|
+
@should_run = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on(service_symbol, symbols: nil, fields: nil, &block)
|
|
33
|
+
service_name = Services.lookup(service_symbol)
|
|
34
|
+
resolved_fields = fields ? Fields.resolve(service_name, fields) : nil
|
|
35
|
+
|
|
36
|
+
handler = { symbols: Array(symbols), fields: resolved_fields, callback: block }
|
|
37
|
+
@handlers[service_name] ||= []
|
|
38
|
+
@handlers[service_name] << handler
|
|
39
|
+
|
|
40
|
+
@subscriptions[service_name] = merge_subscription(
|
|
41
|
+
@subscriptions[service_name],
|
|
42
|
+
handler
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
send_subscription(service_name, handler) if @connected
|
|
46
|
+
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def start
|
|
51
|
+
@should_run = true
|
|
52
|
+
Async do |task|
|
|
53
|
+
loop do
|
|
54
|
+
connect
|
|
55
|
+
run_receive_loop
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
handle_disconnect(e)
|
|
58
|
+
break unless should_reconnect?
|
|
59
|
+
|
|
60
|
+
wait_and_reconnect(task)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def stop
|
|
66
|
+
@should_run = false
|
|
67
|
+
disconnect
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def connected?
|
|
71
|
+
@connected
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def connect
|
|
77
|
+
fetch_streamer_info
|
|
78
|
+
open_websocket
|
|
79
|
+
login
|
|
80
|
+
send_pending_subscriptions
|
|
81
|
+
@connected = true
|
|
82
|
+
@connect_time = Time.now
|
|
83
|
+
@backoff_time = INITIAL_BACKOFF
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def disconnect
|
|
87
|
+
return unless @connection
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
logout_request = @message_builder.build_logout
|
|
91
|
+
send_message(@message_builder.wrap_requests(logout_request))
|
|
92
|
+
rescue StandardError
|
|
93
|
+
# best-effort logout
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@connection.close
|
|
97
|
+
@connection = nil
|
|
98
|
+
@connected = false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def fetch_streamer_info
|
|
102
|
+
@client.refresh! if @client.session.expired?
|
|
103
|
+
prefs = @client.get_user_preferences
|
|
104
|
+
@streamer_info = prefs.streamer_info.first
|
|
105
|
+
|
|
106
|
+
raise "No streamer info available" unless @streamer_info
|
|
107
|
+
|
|
108
|
+
@message_builder = MessageBuilder.new(
|
|
109
|
+
@streamer_info.schwab_client_customer_id,
|
|
110
|
+
@streamer_info.schwab_client_correl_id
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def open_websocket
|
|
115
|
+
url = @streamer_info.streamer_socket_url
|
|
116
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
117
|
+
@connection = Async::WebSocket::Client.connect(endpoint)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def login
|
|
121
|
+
access_token = @client.token_manager.token.token
|
|
122
|
+
login_request = @message_builder.build_login(
|
|
123
|
+
access_token,
|
|
124
|
+
@streamer_info.schwab_client_channel,
|
|
125
|
+
@streamer_info.schwab_client_function_id
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
send_message(@message_builder.wrap_requests(login_request))
|
|
129
|
+
|
|
130
|
+
response = @connection.read
|
|
131
|
+
parsed = JSON.parse(response.to_str)
|
|
132
|
+
|
|
133
|
+
login_response = parsed.dig("response", 0) || parsed
|
|
134
|
+
code = login_response.dig("content", "code")
|
|
135
|
+
|
|
136
|
+
return if code.to_i.zero? || code == 0
|
|
137
|
+
|
|
138
|
+
raise "Login failed: #{login_response}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def send_pending_subscriptions
|
|
142
|
+
@subscriptions.each do |service_name, sub|
|
|
143
|
+
request = @message_builder.build_request(
|
|
144
|
+
service_name, Commands::SUBS,
|
|
145
|
+
keys: sub[:symbols],
|
|
146
|
+
fields: sub[:fields]
|
|
147
|
+
)
|
|
148
|
+
send_message(@message_builder.wrap_requests(request))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def send_subscription(service_name, handler)
|
|
153
|
+
request = @message_builder.build_request(
|
|
154
|
+
service_name, Commands::ADD,
|
|
155
|
+
keys: handler[:symbols],
|
|
156
|
+
fields: handler[:fields]
|
|
157
|
+
)
|
|
158
|
+
send_message(@message_builder.wrap_requests(request))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def run_receive_loop
|
|
162
|
+
while @connected && @should_run
|
|
163
|
+
message = @connection.read
|
|
164
|
+
break if message.nil?
|
|
165
|
+
|
|
166
|
+
parsed = JSON.parse(message.to_str)
|
|
167
|
+
dispatch_message(parsed)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def dispatch_message(parsed)
|
|
172
|
+
if parsed.key?("data")
|
|
173
|
+
parsed["data"].each do |data_entry|
|
|
174
|
+
service = data_entry["service"]
|
|
175
|
+
handlers = @handlers[service]
|
|
176
|
+
next unless handlers
|
|
177
|
+
|
|
178
|
+
handlers.each { |h| h[:callback]&.call(data_entry) }
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
return unless parsed.key?("response")
|
|
183
|
+
|
|
184
|
+
parsed["response"].each do |resp|
|
|
185
|
+
SchwabRb::Logger.logger.debug("Stream response: #{resp}")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def handle_disconnect(error)
|
|
190
|
+
@connected = false
|
|
191
|
+
begin
|
|
192
|
+
@connection&.close
|
|
193
|
+
rescue StandardError
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
@connection = nil
|
|
197
|
+
|
|
198
|
+
SchwabRb::Logger.logger.warn("Stream disconnected: #{error.message}")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def should_reconnect?
|
|
202
|
+
return false unless @should_run
|
|
203
|
+
return false unless @connect_time
|
|
204
|
+
|
|
205
|
+
elapsed = Time.now - @connect_time
|
|
206
|
+
if elapsed < MIN_CONNECTION_TIME
|
|
207
|
+
SchwabRb::Logger.logger.error(
|
|
208
|
+
"Stream connection failed after #{elapsed.round(1)}s (< #{MIN_CONNECTION_TIME}s). " \
|
|
209
|
+
"This likely indicates a configuration error. Not reconnecting."
|
|
210
|
+
)
|
|
211
|
+
return false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def wait_and_reconnect(task)
|
|
218
|
+
SchwabRb::Logger.logger.info("Reconnecting in #{@backoff_time}s...")
|
|
219
|
+
task.sleep(@backoff_time)
|
|
220
|
+
@backoff_time = [@backoff_time * 2, MAX_BACKOFF].min
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def send_message(json_string)
|
|
224
|
+
@connection.write(Protocol::WebSocket::TextMessage.generate(json_string))
|
|
225
|
+
@connection.flush
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def merge_subscription(existing, handler)
|
|
229
|
+
if existing
|
|
230
|
+
{
|
|
231
|
+
symbols: (existing[:symbols] + handler[:symbols]).uniq,
|
|
232
|
+
fields: existing[:fields] && handler[:fields] ? (existing[:fields] + handler[:fields]).uniq : nil
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
{ symbols: handler[:symbols].dup, fields: handler[:fields]&.dup }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module SchwabRb
|
|
7
|
+
module Stream
|
|
8
|
+
class Client
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@stream = Base.new(client)
|
|
11
|
+
@thread = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def on(service_symbol, symbols: nil, fields: nil, &block)
|
|
15
|
+
@stream.on(service_symbol, symbols: symbols, fields: fields, &block)
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
@thread = Thread.new do
|
|
21
|
+
@stream.start
|
|
22
|
+
end
|
|
23
|
+
@thread.join
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start_async
|
|
27
|
+
@thread = Thread.new do
|
|
28
|
+
@stream.start
|
|
29
|
+
end
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stop
|
|
34
|
+
@stream.stop
|
|
35
|
+
@thread&.join(5)
|
|
36
|
+
@thread = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def connected?
|
|
40
|
+
@stream.connected?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SchwabRb
|
|
4
|
+
module Stream
|
|
5
|
+
module Fields
|
|
6
|
+
module LevelOneEquity
|
|
7
|
+
SYMBOL = 0
|
|
8
|
+
BID_PRICE = 1
|
|
9
|
+
ASK_PRICE = 2
|
|
10
|
+
LAST_PRICE = 3
|
|
11
|
+
BID_SIZE = 4
|
|
12
|
+
ASK_SIZE = 5
|
|
13
|
+
ASK_ID = 6
|
|
14
|
+
BID_ID = 7
|
|
15
|
+
TOTAL_VOLUME = 8
|
|
16
|
+
LAST_SIZE = 9
|
|
17
|
+
HIGH_PRICE = 10
|
|
18
|
+
LOW_PRICE = 11
|
|
19
|
+
CLOSE_PRICE = 12
|
|
20
|
+
EXCHANGE_ID = 13
|
|
21
|
+
MARGINABLE = 14
|
|
22
|
+
DESCRIPTION = 15
|
|
23
|
+
LAST_ID = 16
|
|
24
|
+
OPEN_PRICE = 17
|
|
25
|
+
NET_CHANGE = 18
|
|
26
|
+
HIGH_PRICE_52_WEEK = 19
|
|
27
|
+
LOW_PRICE_52_WEEK = 20
|
|
28
|
+
PE_RATIO = 21
|
|
29
|
+
DIVIDEND_AMOUNT = 22
|
|
30
|
+
DIVIDEND_YIELD = 23
|
|
31
|
+
NAV = 24
|
|
32
|
+
EXCHANGE_NAME = 25
|
|
33
|
+
DIVIDEND_DATE = 26
|
|
34
|
+
REGULAR_MARKET_QUOTE = 27
|
|
35
|
+
REGULAR_MARKET_TRADE = 28
|
|
36
|
+
REGULAR_MARKET_LAST_PRICE = 29
|
|
37
|
+
REGULAR_MARKET_LAST_SIZE = 30
|
|
38
|
+
REGULAR_MARKET_NET_CHANGE = 31
|
|
39
|
+
SECURITY_STATUS = 32
|
|
40
|
+
MARK = 33
|
|
41
|
+
QUOTE_TIME_MILLIS = 34
|
|
42
|
+
TRADE_TIME_MILLIS = 35
|
|
43
|
+
REGULAR_MARKET_TRADE_MILLIS = 36
|
|
44
|
+
BID_TIME_MILLIS = 37
|
|
45
|
+
ASK_TIME_MILLIS = 38
|
|
46
|
+
ASK_MIC_ID = 39
|
|
47
|
+
BID_MIC_ID = 40
|
|
48
|
+
LAST_MIC_ID = 41
|
|
49
|
+
NET_CHANGE_PERCENT = 42
|
|
50
|
+
REGULAR_MARKET_CHANGE_PERCENT = 43
|
|
51
|
+
MARK_CHANGE = 44
|
|
52
|
+
MARK_CHANGE_PERCENT = 45
|
|
53
|
+
HTB_QUANTITY = 46
|
|
54
|
+
HTB_RATE = 47
|
|
55
|
+
HARD_TO_BORROW = 48
|
|
56
|
+
IS_SHORTABLE = 49
|
|
57
|
+
POST_MARKET_NET_CHANGE = 50
|
|
58
|
+
POST_MARKET_NET_CHANGE_PERCENT = 51
|
|
59
|
+
|
|
60
|
+
ALL = (0..51).to_a.freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module LevelOneOption
|
|
64
|
+
SYMBOL = 0
|
|
65
|
+
DESCRIPTION = 1
|
|
66
|
+
BID_PRICE = 2
|
|
67
|
+
ASK_PRICE = 3
|
|
68
|
+
LAST_PRICE = 4
|
|
69
|
+
HIGH_PRICE = 5
|
|
70
|
+
LOW_PRICE = 6
|
|
71
|
+
CLOSE_PRICE = 7
|
|
72
|
+
TOTAL_VOLUME = 8
|
|
73
|
+
OPEN_INTEREST = 9
|
|
74
|
+
VOLATILITY = 10
|
|
75
|
+
MONEY_INTRINSIC_VALUE = 11
|
|
76
|
+
EXPIRATION_YEAR = 12
|
|
77
|
+
MULTIPLIER = 13
|
|
78
|
+
DIGITS = 14
|
|
79
|
+
OPEN_PRICE = 15
|
|
80
|
+
BID_SIZE = 16
|
|
81
|
+
ASK_SIZE = 17
|
|
82
|
+
LAST_SIZE = 18
|
|
83
|
+
NET_CHANGE = 19
|
|
84
|
+
STRIKE_TYPE = 20
|
|
85
|
+
CONTRACT_TYPE = 21
|
|
86
|
+
UNDERLYING = 22
|
|
87
|
+
EXPIRATION_MONTH = 23
|
|
88
|
+
DELIVERABLES = 24
|
|
89
|
+
TIME_VALUE = 25
|
|
90
|
+
EXPIRATION_DAY = 26
|
|
91
|
+
DAYS_TO_EXPIRATION = 27
|
|
92
|
+
DELTA = 28
|
|
93
|
+
GAMMA = 29
|
|
94
|
+
THETA = 30
|
|
95
|
+
VEGA = 31
|
|
96
|
+
RHO = 32
|
|
97
|
+
SECURITY_STATUS = 33
|
|
98
|
+
THEORETICAL_OPTION_VALUE = 34
|
|
99
|
+
UNDERLYING_PRICE = 35
|
|
100
|
+
UV_EXPIRATION_TYPE = 36
|
|
101
|
+
MARK = 37
|
|
102
|
+
QUOTE_TIME_MILLIS = 38
|
|
103
|
+
TRADE_TIME_MILLIS = 39
|
|
104
|
+
EXCHANGE_ID = 40
|
|
105
|
+
EXCHANGE_NAME = 41
|
|
106
|
+
LAST_TRADING_DAY = 42
|
|
107
|
+
SETTLEMENT_TYPE = 43
|
|
108
|
+
NET_PERCENT_CHANGE = 44
|
|
109
|
+
MARK_CHANGE = 45
|
|
110
|
+
MARK_CHANGE_PERCENT = 46
|
|
111
|
+
IMPLIED_YIELD = 47
|
|
112
|
+
IS_PENNY = 48
|
|
113
|
+
OPTION_ROOT = 49
|
|
114
|
+
HIGH_PRICE_52_WEEK = 50
|
|
115
|
+
LOW_PRICE_52_WEEK = 51
|
|
116
|
+
INDICATIVE_ASKING_PRICE = 52
|
|
117
|
+
INDICATIVE_BID_PRICE = 53
|
|
118
|
+
INDICATIVE_QUOTE_TIME = 54
|
|
119
|
+
EXERCISE_TYPE = 55
|
|
120
|
+
|
|
121
|
+
ALL = (0..55).to_a.freeze
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
module LevelOneFutures
|
|
125
|
+
SYMBOL = 0
|
|
126
|
+
BID_PRICE = 1
|
|
127
|
+
ASK_PRICE = 2
|
|
128
|
+
LAST_PRICE = 3
|
|
129
|
+
BID_SIZE = 4
|
|
130
|
+
ASK_SIZE = 5
|
|
131
|
+
ASK_ID = 6
|
|
132
|
+
BID_ID = 7
|
|
133
|
+
TOTAL_VOLUME = 8
|
|
134
|
+
LAST_SIZE = 9
|
|
135
|
+
QUOTE_TIME_MILLIS = 10
|
|
136
|
+
TRADE_TIME_MILLIS = 11
|
|
137
|
+
HIGH_PRICE = 12
|
|
138
|
+
LOW_PRICE = 13
|
|
139
|
+
CLOSE_PRICE = 14
|
|
140
|
+
EXCHANGE_ID = 15
|
|
141
|
+
DESCRIPTION = 16
|
|
142
|
+
LAST_ID = 17
|
|
143
|
+
OPEN_PRICE = 18
|
|
144
|
+
NET_CHANGE = 19
|
|
145
|
+
FUTURE_PERCENT_CHANGE = 20
|
|
146
|
+
EXCHANGE_NAME = 21
|
|
147
|
+
SECURITY_STATUS = 22
|
|
148
|
+
OPEN_INTEREST = 23
|
|
149
|
+
MARK = 24
|
|
150
|
+
TICK = 25
|
|
151
|
+
TICK_AMOUNT = 26
|
|
152
|
+
PRODUCT = 27
|
|
153
|
+
FUTURE_PRICE_FORMAT = 28
|
|
154
|
+
FUTURE_TRADING_HOURS = 29
|
|
155
|
+
FUTURE_IS_TRADABLE = 30
|
|
156
|
+
FUTURE_MULTIPLIER = 31
|
|
157
|
+
FUTURE_IS_ACTIVE = 32
|
|
158
|
+
FUTURE_SETTLEMENT_PRICE = 33
|
|
159
|
+
FUTURE_ACTIVE_SYMBOL = 34
|
|
160
|
+
FUTURE_EXPIRATION_DATE = 35
|
|
161
|
+
EXPIRATION_STYLE = 36
|
|
162
|
+
ASK_MIC_ID = 37
|
|
163
|
+
BID_MIC_ID = 38
|
|
164
|
+
LAST_MIC_ID = 39
|
|
165
|
+
SETTLEMENT_DATE = 40
|
|
166
|
+
|
|
167
|
+
ALL = (0..40).to_a.freeze
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
module LevelOneFuturesOptions
|
|
171
|
+
SYMBOL = 0
|
|
172
|
+
BID_PRICE = 1
|
|
173
|
+
ASK_PRICE = 2
|
|
174
|
+
LAST_PRICE = 3
|
|
175
|
+
BID_SIZE = 4
|
|
176
|
+
ASK_SIZE = 5
|
|
177
|
+
BID_ID = 6
|
|
178
|
+
ASK_ID = 7
|
|
179
|
+
TOTAL_VOLUME = 8
|
|
180
|
+
LAST_SIZE = 9
|
|
181
|
+
QUOTE_TIME_MILLIS = 10
|
|
182
|
+
TRADE_TIME_MILLIS = 11
|
|
183
|
+
HIGH_PRICE = 12
|
|
184
|
+
LOW_PRICE = 13
|
|
185
|
+
CLOSE_PRICE = 14
|
|
186
|
+
LAST_ID = 15
|
|
187
|
+
DESCRIPTION = 16
|
|
188
|
+
OPEN_PRICE = 17
|
|
189
|
+
OPEN_INTEREST = 18
|
|
190
|
+
MARK = 19
|
|
191
|
+
TICK = 20
|
|
192
|
+
TICK_AMOUNT = 21
|
|
193
|
+
FUTURE_MULTIPLIER = 22
|
|
194
|
+
FUTURE_SETTLEMENT_PRICE = 23
|
|
195
|
+
UNDERLYING_SYMBOL = 24
|
|
196
|
+
STRIKE_PRICE = 25
|
|
197
|
+
FUTURE_EXPIRATION_DATE = 26
|
|
198
|
+
EXPIRATION_STYLE = 27
|
|
199
|
+
CONTRACT_TYPE = 28
|
|
200
|
+
SECURITY_STATUS = 29
|
|
201
|
+
EXCHANGE_ID = 30
|
|
202
|
+
EXCHANGE_NAME = 31
|
|
203
|
+
|
|
204
|
+
ALL = (0..31).to_a.freeze
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
module LevelOneForex
|
|
208
|
+
SYMBOL = 0
|
|
209
|
+
BID_PRICE = 1
|
|
210
|
+
ASK_PRICE = 2
|
|
211
|
+
LAST_PRICE = 3
|
|
212
|
+
BID_SIZE = 4
|
|
213
|
+
ASK_SIZE = 5
|
|
214
|
+
TOTAL_VOLUME = 6
|
|
215
|
+
LAST_SIZE = 7
|
|
216
|
+
QUOTE_TIME_MILLIS = 8
|
|
217
|
+
TRADE_TIME_MILLIS = 9
|
|
218
|
+
HIGH_PRICE = 10
|
|
219
|
+
LOW_PRICE = 11
|
|
220
|
+
CLOSE_PRICE = 12
|
|
221
|
+
EXCHANGE_ID = 13
|
|
222
|
+
DESCRIPTION = 14
|
|
223
|
+
OPEN_PRICE = 15
|
|
224
|
+
NET_CHANGE = 16
|
|
225
|
+
CHANGE_PERCENT = 17
|
|
226
|
+
EXCHANGE_NAME = 18
|
|
227
|
+
DIGITS = 19
|
|
228
|
+
SECURITY_STATUS = 20
|
|
229
|
+
TICK = 21
|
|
230
|
+
TICK_AMOUNT = 22
|
|
231
|
+
PRODUCT = 23
|
|
232
|
+
TRADING_HOURS = 24
|
|
233
|
+
IS_TRADABLE = 25
|
|
234
|
+
MARKET_MAKER = 26
|
|
235
|
+
HIGH_PRICE_52_WEEK = 27
|
|
236
|
+
LOW_PRICE_52_WEEK = 28
|
|
237
|
+
MARK = 29
|
|
238
|
+
|
|
239
|
+
ALL = (0..29).to_a.freeze
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
module Book
|
|
243
|
+
SYMBOL = 0
|
|
244
|
+
BOOK_TIME = 1
|
|
245
|
+
BIDS = 2
|
|
246
|
+
ASKS = 3
|
|
247
|
+
|
|
248
|
+
ALL = (0..3).to_a.freeze
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
module BookBid
|
|
252
|
+
BID_PRICE = 0
|
|
253
|
+
TOTAL_VOLUME = 1
|
|
254
|
+
NUM_BIDS = 2
|
|
255
|
+
BIDS = 3
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
module BookAsk
|
|
259
|
+
ASK_PRICE = 0
|
|
260
|
+
TOTAL_VOLUME = 1
|
|
261
|
+
NUM_ASKS = 2
|
|
262
|
+
ASKS = 3
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
module BookExchange
|
|
266
|
+
EXCHANGE = 0
|
|
267
|
+
VOLUME = 1
|
|
268
|
+
SEQUENCE = 2
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
module ChartEquity
|
|
272
|
+
SYMBOL = 0
|
|
273
|
+
SEQUENCE = 1
|
|
274
|
+
OPEN_PRICE = 2
|
|
275
|
+
HIGH_PRICE = 3
|
|
276
|
+
LOW_PRICE = 4
|
|
277
|
+
CLOSE_PRICE = 5
|
|
278
|
+
VOLUME = 6
|
|
279
|
+
CHART_TIME_MILLIS = 7
|
|
280
|
+
CHART_DAY = 8
|
|
281
|
+
|
|
282
|
+
ALL = (0..8).to_a.freeze
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
module ChartFutures
|
|
286
|
+
SYMBOL = 0
|
|
287
|
+
CHART_TIME_MILLIS = 1
|
|
288
|
+
OPEN_PRICE = 2
|
|
289
|
+
HIGH_PRICE = 3
|
|
290
|
+
LOW_PRICE = 4
|
|
291
|
+
CLOSE_PRICE = 5
|
|
292
|
+
VOLUME = 6
|
|
293
|
+
|
|
294
|
+
ALL = (0..6).to_a.freeze
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
module Screener
|
|
298
|
+
SYMBOL = 0
|
|
299
|
+
TIMESTAMP = 1
|
|
300
|
+
SORT_FIELD = 2
|
|
301
|
+
FREQUENCY = 3
|
|
302
|
+
ITEMS = 4
|
|
303
|
+
|
|
304
|
+
ALL = (0..4).to_a.freeze
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
module AccountActivity
|
|
308
|
+
SUBSCRIPTION_KEY = 0
|
|
309
|
+
ACCOUNT = 1
|
|
310
|
+
MESSAGE_TYPE = 2
|
|
311
|
+
MESSAGE_DATA = 3
|
|
312
|
+
|
|
313
|
+
ALL = (0..3).to_a.freeze
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
SERVICE_FIELDS = {
|
|
317
|
+
"LEVELONE_EQUITIES" => LevelOneEquity,
|
|
318
|
+
"LEVELONE_OPTIONS" => LevelOneOption,
|
|
319
|
+
"LEVELONE_FUTURES" => LevelOneFutures,
|
|
320
|
+
"LEVELONE_FUTURES_OPTIONS" => LevelOneFuturesOptions,
|
|
321
|
+
"LEVELONE_FOREX" => LevelOneForex,
|
|
322
|
+
"NYSE_BOOK" => Book,
|
|
323
|
+
"NASDAQ_BOOK" => Book,
|
|
324
|
+
"OPTIONS_BOOK" => Book,
|
|
325
|
+
"CHART_EQUITY" => ChartEquity,
|
|
326
|
+
"CHART_FUTURES" => ChartFutures,
|
|
327
|
+
"SCREENER_EQUITY" => Screener,
|
|
328
|
+
"SCREENER_OPTION" => Screener,
|
|
329
|
+
"ACCT_ACTIVITY" => AccountActivity
|
|
330
|
+
}.freeze
|
|
331
|
+
|
|
332
|
+
def self.resolve(service, fields)
|
|
333
|
+
return SERVICE_FIELDS[service]::ALL if fields == :all
|
|
334
|
+
|
|
335
|
+
Array(fields).map(&:to_i)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SchwabRb
|
|
6
|
+
module Stream
|
|
7
|
+
class MessageBuilder
|
|
8
|
+
def initialize(customer_id, correl_id)
|
|
9
|
+
@customer_id = customer_id
|
|
10
|
+
@correl_id = correl_id
|
|
11
|
+
@request_id = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build_login(access_token, channel, function_id)
|
|
15
|
+
{
|
|
16
|
+
"service" => "ADMIN",
|
|
17
|
+
"command" => Commands::LOGIN,
|
|
18
|
+
"requestid" => next_request_id,
|
|
19
|
+
"SchwabClientCustomerId" => @customer_id,
|
|
20
|
+
"SchwabClientCorrelId" => @correl_id,
|
|
21
|
+
"parameters" => {
|
|
22
|
+
"Authorization" => "Bearer #{access_token}",
|
|
23
|
+
"SchwabClientChannel" => channel,
|
|
24
|
+
"SchwabClientFunctionId" => function_id
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_logout
|
|
30
|
+
{
|
|
31
|
+
"service" => "ADMIN",
|
|
32
|
+
"command" => Commands::LOGOUT,
|
|
33
|
+
"requestid" => next_request_id,
|
|
34
|
+
"SchwabClientCustomerId" => @customer_id,
|
|
35
|
+
"SchwabClientCorrelId" => @correl_id
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_request(service, command, keys: nil, fields: nil)
|
|
40
|
+
request = {
|
|
41
|
+
"service" => service,
|
|
42
|
+
"command" => command,
|
|
43
|
+
"requestid" => next_request_id,
|
|
44
|
+
"SchwabClientCustomerId" => @customer_id,
|
|
45
|
+
"SchwabClientCorrelId" => @correl_id,
|
|
46
|
+
"parameters" => {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
request["parameters"]["keys"] = Array(keys).join(",") if keys
|
|
50
|
+
request["parameters"]["fields"] = Fields.resolve(service, fields).join(",") if fields
|
|
51
|
+
|
|
52
|
+
request
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def wrap_requests(*requests)
|
|
56
|
+
{ "requests" => requests.flatten }.to_json
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def next_request_id
|
|
62
|
+
@request_id += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|