ib-api 972.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +105 -0
- data/Guardfile +24 -0
- data/LICENSE +674 -0
- data/README.md +65 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/api.gemspec +43 -0
- data/bin/console +95 -0
- data/bin/console.yml +3 -0
- data/bin/setup +8 -0
- data/changelog.md +7 -0
- data/example/README.md +76 -0
- data/example/account_info +54 -0
- data/example/account_positions +30 -0
- data/example/account_summary +88 -0
- data/example/cancel_orders +74 -0
- data/example/fa_accounts +25 -0
- data/example/fundamental_data +40 -0
- data/example/historic_data_cli +186 -0
- data/example/list_orders +45 -0
- data/example/portfolio_csv +81 -0
- data/example/scanner_data +62 -0
- data/example/template +19 -0
- data/example/tick_data +28 -0
- data/lib/extensions/class-extensions.rb +87 -0
- data/lib/ib-api.rb +7 -0
- data/lib/ib/base.rb +103 -0
- data/lib/ib/base_properties.rb +160 -0
- data/lib/ib/connection.rb +450 -0
- data/lib/ib/constants.rb +393 -0
- data/lib/ib/errors.rb +44 -0
- data/lib/ib/logger.rb +26 -0
- data/lib/ib/messages.rb +99 -0
- data/lib/ib/messages/abstract_message.rb +101 -0
- data/lib/ib/messages/incoming.rb +251 -0
- data/lib/ib/messages/incoming/abstract_message.rb +116 -0
- data/lib/ib/messages/incoming/account_value.rb +78 -0
- data/lib/ib/messages/incoming/alert.rb +34 -0
- data/lib/ib/messages/incoming/contract_data.rb +102 -0
- data/lib/ib/messages/incoming/delta_neutral_validation.rb +23 -0
- data/lib/ib/messages/incoming/execution_data.rb +50 -0
- data/lib/ib/messages/incoming/historical_data.rb +84 -0
- data/lib/ib/messages/incoming/market_depths.rb +44 -0
- data/lib/ib/messages/incoming/next_valid_id.rb +18 -0
- data/lib/ib/messages/incoming/open_order.rb +277 -0
- data/lib/ib/messages/incoming/order_status.rb +85 -0
- data/lib/ib/messages/incoming/portfolio_value.rb +78 -0
- data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
- data/lib/ib/messages/incoming/scanner_data.rb +54 -0
- data/lib/ib/messages/incoming/ticks.rb +268 -0
- data/lib/ib/messages/outgoing.rb +437 -0
- data/lib/ib/messages/outgoing/abstract_message.rb +88 -0
- data/lib/ib/messages/outgoing/account_requests.rb +112 -0
- data/lib/ib/messages/outgoing/bar_requests.rb +250 -0
- data/lib/ib/messages/outgoing/place_order.rb +209 -0
- data/lib/ib/messages/outgoing/request_marketdata.rb +99 -0
- data/lib/ib/messages/outgoing/request_tick_data.rb +21 -0
- data/lib/ib/model.rb +4 -0
- data/lib/ib/models.rb +14 -0
- data/lib/ib/server_versions.rb +114 -0
- data/lib/ib/socket.rb +185 -0
- data/lib/ib/support.rb +160 -0
- data/lib/ib/version.rb +6 -0
- data/lib/models/ib/account.rb +85 -0
- data/lib/models/ib/account_value.rb +33 -0
- data/lib/models/ib/bag.rb +55 -0
- data/lib/models/ib/bar.rb +31 -0
- data/lib/models/ib/combo_leg.rb +105 -0
- data/lib/models/ib/condition.rb +245 -0
- data/lib/models/ib/contract.rb +415 -0
- data/lib/models/ib/contract_detail.rb +108 -0
- data/lib/models/ib/execution.rb +67 -0
- data/lib/models/ib/forex.rb +13 -0
- data/lib/models/ib/future.rb +15 -0
- data/lib/models/ib/index.rb +15 -0
- data/lib/models/ib/option.rb +78 -0
- data/lib/models/ib/option_detail.rb +55 -0
- data/lib/models/ib/order.rb +519 -0
- data/lib/models/ib/order_state.rb +152 -0
- data/lib/models/ib/portfolio_value.rb +64 -0
- data/lib/models/ib/stock.rb +16 -0
- data/lib/models/ib/underlying.rb +34 -0
- data/lib/models/ib/vertical.rb +96 -0
- data/lib/requires.rb +12 -0
- metadata +203 -0
@@ -0,0 +1,450 @@
|
|
1
|
+
require 'thread'
|
2
|
+
#require 'active_support'
|
3
|
+
require 'ib/socket'
|
4
|
+
require 'ib/logger'
|
5
|
+
require 'ib/messages'
|
6
|
+
|
7
|
+
module IB
|
8
|
+
# Encapsulates API connection to TWS or Gateway
|
9
|
+
class Connection
|
10
|
+
|
11
|
+
|
12
|
+
## -------------------------------------------- Interface ---------------------------------
|
13
|
+
## public attributes: socket, next_local_id ( alias next_order_id)
|
14
|
+
## public methods: connect (alias open), disconnect, connected?
|
15
|
+
## subscribe, unsubscribe
|
16
|
+
## send_message (alias dispatch)
|
17
|
+
## place_order, modify_order, cancel_order
|
18
|
+
## public data-queue: received, received?, wait_for, clear_received
|
19
|
+
## misc: reader_running?
|
20
|
+
|
21
|
+
include LogDev # provides default_logger
|
22
|
+
|
23
|
+
mattr_accessor :current
|
24
|
+
mattr_accessor :logger ## borrowed from active_support
|
25
|
+
# Please note, we are realizing only the most current TWS protocol versions,
|
26
|
+
# thus improving performance at the expense of backwards compatibility.
|
27
|
+
# Older protocol versions support can be found in older gem versions.
|
28
|
+
|
29
|
+
attr_accessor :socket # Socket to IB server (TWS or Gateway)
|
30
|
+
attr_accessor :next_local_id # Next valid order id
|
31
|
+
attr_accessor :client_id
|
32
|
+
attr_accessor :server_version
|
33
|
+
attr_accessor :client_version
|
34
|
+
alias next_order_id next_local_id
|
35
|
+
alias next_order_id= next_local_id=
|
36
|
+
|
37
|
+
def initialize host: '127.0.0.1',
|
38
|
+
port: '4002', # IB Gateway connection (default --> demo) 4001: production
|
39
|
+
#:port => '7497', # TWS connection --> demo 7496: production
|
40
|
+
connect: true, # Connect at initialization
|
41
|
+
received: true, # Keep all received messages in a @received Hash
|
42
|
+
# redis: false, # future plans
|
43
|
+
logger: default_logger,
|
44
|
+
client_id: rand( 1001 .. 9999 ) ,
|
45
|
+
client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb
|
46
|
+
optional_capacities: "", # TWS-Version 974: "+PACEAPI"
|
47
|
+
#server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb
|
48
|
+
**any_other_parameters_which_are_ignored
|
49
|
+
# V 974 release motes
|
50
|
+
# API messages sent at a higher rate than 50/second can now be paced by TWS at the 50/second rate instead of potentially causing a disconnection. This is now done automatically by the RTD Server API and can be done with other API technologies by invoking SetConnectOptions("+PACEAPI") prior to eConnect.
|
51
|
+
|
52
|
+
|
53
|
+
# convert parameters into instance-variables and assign them
|
54
|
+
method(__method__).parameters.each do |type, k|
|
55
|
+
next unless type == :key
|
56
|
+
case k
|
57
|
+
when :logger
|
58
|
+
self.logger = logger
|
59
|
+
else
|
60
|
+
v = eval(k.to_s)
|
61
|
+
instance_variable_set("@#{k}", v) unless v.nil?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# A couple of locks to avoid race conditions in JRuby
|
66
|
+
@subscribe_lock = Mutex.new
|
67
|
+
@receive_lock = Mutex.new
|
68
|
+
@message_lock = Mutex.new
|
69
|
+
|
70
|
+
@connected = false
|
71
|
+
self.next_local_id = nil
|
72
|
+
|
73
|
+
# self.subscribe(:Alert) do |msg|
|
74
|
+
# puts msg.to_human
|
75
|
+
# end
|
76
|
+
|
77
|
+
# TWS always sends NextValidId message at connect -subscribe save this id
|
78
|
+
## this block is executed before tws-communication is established
|
79
|
+
yield self if block_given?
|
80
|
+
|
81
|
+
self.subscribe(:NextValidId) do |msg|
|
82
|
+
logger.progname = "Connection#connect"
|
83
|
+
self.next_local_id = msg.local_id
|
84
|
+
logger.info { "Got next valid order id: #{next_local_id}." }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Ensure the transmission of NextValidId.
|
88
|
+
# works even if no reader_thread is established
|
89
|
+
if connect
|
90
|
+
disconnect if connected?
|
91
|
+
update_next_order_id
|
92
|
+
Kernel.exit if self.next_local_id.nil?
|
93
|
+
end
|
94
|
+
#start_reader if @received && connected?
|
95
|
+
Connection.current = self
|
96
|
+
end
|
97
|
+
|
98
|
+
# read actual order_id and
|
99
|
+
# connect if not connected
|
100
|
+
def update_next_order_id
|
101
|
+
i,finish = 0, false
|
102
|
+
sub = self.subscribe(:NextValidID) { finish = true }
|
103
|
+
connected? ? self.send_message( :RequestIds ) : open()
|
104
|
+
Timeout::timeout(1, IB::TransmissionError,"Could not get NextValidId" ) do
|
105
|
+
loop { sleep 0.1; break if finish }
|
106
|
+
end
|
107
|
+
self.unsubscribe sub
|
108
|
+
end
|
109
|
+
|
110
|
+
### Working with connection
|
111
|
+
|
112
|
+
def connect
|
113
|
+
logger.progname='IB::Connection#connect'
|
114
|
+
if connected?
|
115
|
+
error "Already connected!"
|
116
|
+
return
|
117
|
+
end
|
118
|
+
|
119
|
+
self.socket = IBSocket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible
|
120
|
+
socket.initialising_handshake
|
121
|
+
socket.decode_message( socket.recieve_messages ) do | the_message |
|
122
|
+
# logger.info{ "TheMessage :: #{the_message.inspect}" }
|
123
|
+
@server_version = the_message.shift.to_i
|
124
|
+
error "ServerVersion does not match #{@server_version} <--> #{MAX_CLIENT_VER}" if @server_version != MAX_CLIENT_VER
|
125
|
+
|
126
|
+
@remote_connect_time = DateTime.parse the_message.shift
|
127
|
+
@local_connect_time = Time.now
|
128
|
+
end
|
129
|
+
|
130
|
+
# Sending (arbitrary) client ID to identify subsequent communications.
|
131
|
+
# The client with a client_id of 0 can manage the TWS-owned open orders.
|
132
|
+
# Other clients can only manage their own open orders.
|
133
|
+
|
134
|
+
# V100 initial handshake
|
135
|
+
# Parameters borrowed from the python client
|
136
|
+
start_api = 71
|
137
|
+
version = 2
|
138
|
+
# optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities
|
139
|
+
socket.send_messages start_api, version, @client_id , @optional_capacities
|
140
|
+
@connected = true
|
141
|
+
logger.info { "Connected to server, version: #{@server_version},\n connection time: " +
|
142
|
+
"#{@local_connect_time} local, " +
|
143
|
+
"#{@remote_connect_time} remote."}
|
144
|
+
|
145
|
+
# if the client_id is wrong or the port is not accessible the first read attempt fails
|
146
|
+
# get the first message and proceed if something reasonable is recieved
|
147
|
+
the_message = process_message # recieve next_order_id
|
148
|
+
error "Check Port/Client_id ", :reader if the_message == " "
|
149
|
+
start_reader
|
150
|
+
end
|
151
|
+
|
152
|
+
alias open connect # Legacy alias
|
153
|
+
|
154
|
+
def disconnect
|
155
|
+
if reader_running?
|
156
|
+
@reader_running = false
|
157
|
+
@reader_thread.join
|
158
|
+
end
|
159
|
+
if connected?
|
160
|
+
socket.close
|
161
|
+
@connected = false
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
alias close disconnect # Legacy alias
|
166
|
+
|
167
|
+
def connected?
|
168
|
+
@connected
|
169
|
+
end
|
170
|
+
|
171
|
+
### Working with message subscribers
|
172
|
+
|
173
|
+
# Subscribe Proc or block to specific type(s) of incoming message events.
|
174
|
+
# Listener will be called later with received message instance as its argument.
|
175
|
+
# Returns subscriber id to allow unsubscribing
|
176
|
+
def subscribe *args, &block
|
177
|
+
@subscribe_lock.synchronize do
|
178
|
+
subscriber = args.last.respond_to?(:call) ? args.pop : block
|
179
|
+
id = random_id
|
180
|
+
|
181
|
+
error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc
|
182
|
+
|
183
|
+
args.each do |what|
|
184
|
+
message_classes =
|
185
|
+
case
|
186
|
+
when what.is_a?(Class) && what < Messages::Incoming::AbstractMessage
|
187
|
+
[what]
|
188
|
+
when what.is_a?(Symbol)
|
189
|
+
[Messages::Incoming.const_get(what)]
|
190
|
+
when what.is_a?(Regexp)
|
191
|
+
Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what }
|
192
|
+
else
|
193
|
+
error "#{what} must represent incoming IB message class", :args
|
194
|
+
end
|
195
|
+
# @subscribers_lock.synchronize do
|
196
|
+
message_classes.flatten.each do |message_class|
|
197
|
+
# TODO: Fix: RuntimeError: can't add a new key into hash during iteration
|
198
|
+
subscribers[message_class][id] = subscriber
|
199
|
+
end
|
200
|
+
# end # lock
|
201
|
+
end
|
202
|
+
|
203
|
+
id
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Remove all subscribers with specific subscriber id
|
208
|
+
def unsubscribe *ids
|
209
|
+
@subscribe_lock.synchronize do
|
210
|
+
ids.collect do |id|
|
211
|
+
removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact
|
212
|
+
logger.error "No subscribers with id #{id}" if removed_at_id.empty?
|
213
|
+
removed_at_id # return_value
|
214
|
+
end.flatten
|
215
|
+
end
|
216
|
+
end
|
217
|
+
### Working with received messages Hash
|
218
|
+
|
219
|
+
# Clear received messages Hash
|
220
|
+
def clear_received *message_types
|
221
|
+
@receive_lock.synchronize do
|
222
|
+
if message_types.empty?
|
223
|
+
received.each { |message_type, container| container.clear }
|
224
|
+
else
|
225
|
+
message_types.each { |message_type| received[message_type].clear }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Hash of received messages, keyed by message type
|
231
|
+
def received
|
232
|
+
@received_hash ||= Hash.new do |hash, message_type|
|
233
|
+
# enable access to the hash via
|
234
|
+
# ib.received[:MessageType].attribute
|
235
|
+
the_array = Array.new
|
236
|
+
def the_array.method_missing(method, *key)
|
237
|
+
unless method == :to_hash || method == :to_str #|| method == :to_int
|
238
|
+
return self.map{|x| x.public_send(method, *key)}
|
239
|
+
end
|
240
|
+
end
|
241
|
+
hash[message_type] = the_array
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Check if messages of given type were received at_least n times
|
246
|
+
def received? message_type, times=1
|
247
|
+
@receive_lock.synchronize do
|
248
|
+
received[message_type].size >= times
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# Wait for specific condition(s) - given as callable/block, or
|
254
|
+
# message type(s) - given as Symbol or [Symbol, times] pair.
|
255
|
+
# Timeout after given time or 1 second.
|
256
|
+
#
|
257
|
+
# wait_for depends heavyly on Connection#received. If collection of messages through recieved
|
258
|
+
# is turned off, wait_for loses most of its functionality
|
259
|
+
def wait_for *args, &block
|
260
|
+
timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
|
261
|
+
end_time = Time.now + (timeout || 1) # default timeout 1 sec
|
262
|
+
conditions = args.delete_if { |arg| arg.is_a? Numeric }.push(block).compact
|
263
|
+
|
264
|
+
until end_time < Time.now || satisfied?(*conditions)
|
265
|
+
if reader_running?
|
266
|
+
sleep 0.05
|
267
|
+
else
|
268
|
+
process_messages 50
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
### Working with Incoming messages from IB
|
274
|
+
|
275
|
+
|
276
|
+
def reader_running?
|
277
|
+
@reader_running && @reader_thread && @reader_thread.alive?
|
278
|
+
end
|
279
|
+
|
280
|
+
# Process incoming messages during *poll_time* (200) msecs, nonblocking
|
281
|
+
def process_messages poll_time = 50 # in msec
|
282
|
+
time_out = Time.now + poll_time/1000.0
|
283
|
+
while (time_left = time_out - Time.now) > 0
|
284
|
+
# If socket is readable, process single incoming message
|
285
|
+
#process_message if select [socket], nil, nil, time_left
|
286
|
+
# the following checks for shutdown of TWS side; ensures we don't run in a spin loop.
|
287
|
+
# unfortunately, it raises Errors in windows environment
|
288
|
+
# disabled for now
|
289
|
+
if select [socket], nil, nil, time_left
|
290
|
+
# # Peek at the message from the socket; if it's blank then the
|
291
|
+
# # server side of connection (TWS) has likely shut down.
|
292
|
+
socket_likely_shutdown = socket.recvmsg(100, Socket::MSG_PEEK)[0] == ""
|
293
|
+
#
|
294
|
+
# # We go ahead process messages regardless (a no-op if socket_likely_shutdown).
|
295
|
+
process_message
|
296
|
+
#
|
297
|
+
# # After processing, if socket has shut down we sleep for 100ms
|
298
|
+
# # to avoid spinning in a tight loop. If the server side somehow
|
299
|
+
# # comes back up (gets reconnedted), normal processing
|
300
|
+
# # (without the 100ms wait) should happen.
|
301
|
+
sleep(0.1) if socket_likely_shutdown
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
### Sending Outgoing messages to IB
|
307
|
+
|
308
|
+
# Send an outgoing message.
|
309
|
+
# returns the used request_id if appropiate, otherwise "true"
|
310
|
+
def send_message what, *args
|
311
|
+
message =
|
312
|
+
case
|
313
|
+
when what.is_a?(Messages::Outgoing::AbstractMessage)
|
314
|
+
what
|
315
|
+
when what.is_a?(Class) && what < Messages::Outgoing::AbstractMessage
|
316
|
+
what.new *args
|
317
|
+
when what.is_a?(Symbol)
|
318
|
+
Messages::Outgoing.const_get(what).new *args
|
319
|
+
else
|
320
|
+
error "Only able to send outgoing IB messages", :args
|
321
|
+
end
|
322
|
+
error "Not able to send messages, IB not connected!" unless connected?
|
323
|
+
begin
|
324
|
+
@message_lock.synchronize do
|
325
|
+
message.send_to socket
|
326
|
+
end
|
327
|
+
rescue Errno::EPIPE
|
328
|
+
logger.error{ "Broken Pipe, trying to reconnect" }
|
329
|
+
disconnect
|
330
|
+
connect
|
331
|
+
retry
|
332
|
+
end
|
333
|
+
## return the transmitted message
|
334
|
+
message.data[:request_id].presence || true
|
335
|
+
end
|
336
|
+
|
337
|
+
alias dispatch send_message # Legacy alias
|
338
|
+
|
339
|
+
# Place Order (convenience wrapper for send_message :PlaceOrder).
|
340
|
+
# Assigns client_id and order_id fields to placed order. Returns assigned order_id.
|
341
|
+
def place_order order, contract
|
342
|
+
# order.place contract, self ## old
|
343
|
+
error "Unable to place order, next_local_id not known" unless next_local_id
|
344
|
+
error "local_id present. Order is already placed. Do might use modify insteed" unless order.local_id.nil?
|
345
|
+
order.client_id = client_id
|
346
|
+
order.local_id = next_local_id
|
347
|
+
self.next_local_id += 1
|
348
|
+
order.placed_at = Time.now
|
349
|
+
modify_order order, contract
|
350
|
+
end
|
351
|
+
|
352
|
+
# Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id.
|
353
|
+
def modify_order order, contract
|
354
|
+
# order.modify contract, self ## old
|
355
|
+
error "Unable to modify order; local_id not specified" if order.local_id.nil?
|
356
|
+
order.modified_at = Time.now
|
357
|
+
send_message :PlaceOrder,
|
358
|
+
:order => order,
|
359
|
+
:contract => contract,
|
360
|
+
:local_id => order.local_id
|
361
|
+
order.local_id # return value
|
362
|
+
end
|
363
|
+
|
364
|
+
# Cancel Orders by their local ids (convenience wrapper for send_message :CancelOrder).
|
365
|
+
def cancel_order *local_ids
|
366
|
+
local_ids.each do |local_id|
|
367
|
+
send_message :CancelOrder, :local_id => local_id.to_i
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Start reader thread that continuously reads messages from @socket in background.
|
372
|
+
# If you don't start reader, you should manually poll @socket for messages
|
373
|
+
# or use #process_messages(msec) API.
|
374
|
+
def start_reader
|
375
|
+
return(@reader_thread) if @reader_running
|
376
|
+
if connected?
|
377
|
+
Thread.abort_on_exception = true
|
378
|
+
@reader_running = true
|
379
|
+
@reader_thread = Thread.new { process_messages while @reader_running }
|
380
|
+
else
|
381
|
+
logger.fatal {"Could not start reader, not connected!"}
|
382
|
+
nil # return_value
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
protected
|
387
|
+
# Message subscribers. Key is the message class to listen for.
|
388
|
+
# Value is a Hash of subscriber Procs, keyed by their subscription id.
|
389
|
+
# All subscriber Procs will be called with the message instance
|
390
|
+
# as an argument when a message of that type is received.
|
391
|
+
def subscribers
|
392
|
+
@subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new }
|
393
|
+
end
|
394
|
+
|
395
|
+
# Process single incoming message (blocking!)
|
396
|
+
def process_message
|
397
|
+
logger.progname='IB::Connection#process_message' if logger.is_a?(Logger)
|
398
|
+
|
399
|
+
socket.decode_message( socket.recieve_messages ) do | the_decoded_message |
|
400
|
+
# puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}"
|
401
|
+
msg_id = the_decoded_message.shift.to_i
|
402
|
+
|
403
|
+
# Debug:
|
404
|
+
logger.debug { "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"}
|
405
|
+
|
406
|
+
# Create new instance of the appropriate message type,
|
407
|
+
# and have it read the message from socket.
|
408
|
+
# NB: Failure here usually means unsupported message type received
|
409
|
+
logger.error { "Got unsupported message #{msg_id}" } unless Messages::Incoming::Classes[msg_id]
|
410
|
+
error "Something strange happened - Reader has to be restarted" , :reader if msg_id.to_i.zero?
|
411
|
+
msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message)
|
412
|
+
|
413
|
+
# Deliver message to all registered subscribers, alert if no subscribers
|
414
|
+
# Ruby 2.0 and above: Hashes are ordered.
|
415
|
+
# Thus first declared subscribers of a class are executed first
|
416
|
+
@subscribe_lock.synchronize do
|
417
|
+
subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
|
418
|
+
end
|
419
|
+
logger.warn { "No subscribers for message #{msg.class}!" } if subscribers[msg.class].empty?
|
420
|
+
|
421
|
+
# Collect all received messages into a @received Hash
|
422
|
+
if @received
|
423
|
+
@receive_lock.synchronize do
|
424
|
+
received[msg.message_type] << msg
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def random_id
|
431
|
+
rand 999999
|
432
|
+
end
|
433
|
+
|
434
|
+
# Check if all given conditions are satisfied
|
435
|
+
def satisfied? *conditions
|
436
|
+
!conditions.empty? &&
|
437
|
+
conditions.inject(true) do |result, condition|
|
438
|
+
result && if condition.is_a?(Symbol)
|
439
|
+
received?(condition)
|
440
|
+
elsif condition.is_a?(Array)
|
441
|
+
received?(*condition)
|
442
|
+
elsif condition.respond_to?(:call)
|
443
|
+
condition.call
|
444
|
+
else
|
445
|
+
logger.error { "Unknown wait condition #{condition}" }
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end # class Connection
|
450
|
+
end # module IB
|
data/lib/ib/constants.rb
ADDED
@@ -0,0 +1,393 @@
|
|
1
|
+
module IB
|
2
|
+
### Widely used TWS constants:
|
3
|
+
|
4
|
+
EOL = "\0"
|
5
|
+
# TWS_MAX is TWSMAX (transmitted from the TWS) minus the first digit (1)
|
6
|
+
# Anything bigger then TWS_MAX is considered as nil (in read_decimal @ messages/incomming/abstract_message.rb)
|
7
|
+
TWS_MAX = 79769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0
|
8
|
+
TWSMAX = 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0
|
9
|
+
|
10
|
+
|
11
|
+
# Enumeration of bar size types for convenience.
|
12
|
+
# Bar sizes less than 30 seconds do not work for some securities.
|
13
|
+
BAR_SIZES = {'1 sec' => :sec1,
|
14
|
+
'5 secs' => :sec5,
|
15
|
+
'15 secs' =>:sec15,
|
16
|
+
'30 secs' =>:sec30,
|
17
|
+
'1 min' => :min1,
|
18
|
+
'2 mins' => :min2,
|
19
|
+
'3 mins' => :min3,
|
20
|
+
'5 mins' => :min5,
|
21
|
+
'15 mins' =>:min15,
|
22
|
+
'30 mins' =>:min30,
|
23
|
+
'1 hour' =>:hour1,
|
24
|
+
'1 day' => :day1
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
# Enumeration of data types.
|
28
|
+
# Determines the nature of data being extracted. Valid values:
|
29
|
+
DATA_TYPES = {'TRADES' => :trades,
|
30
|
+
'MIDPOINT' => :midpoint,
|
31
|
+
'BID' => :bid,
|
32
|
+
'ASK' => :ask,
|
33
|
+
'BID_ASK' => :bid_ask,
|
34
|
+
'HISTORICAL_VOLATILITY' => :historical_volatility,
|
35
|
+
'OPTION_IMPLIED_VOLATILITY' => :option_implied_volatility,
|
36
|
+
'OPTION_VOLUME' => :option_volume,
|
37
|
+
'OPTION_OPEN_INTEREST' => :option_open_interest
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
### These values are typically received from TWS in incoming messages
|
41
|
+
|
42
|
+
# Tick types as received in TickPrice and TickSize messages (enumeration)
|
43
|
+
TICK_TYPES = {
|
44
|
+
# int id => :Description # Corresponding API Event/Function/Method
|
45
|
+
0 => :bid_size, # tickSize()
|
46
|
+
1 => :bid_price, # tickPrice()
|
47
|
+
2 => :ask_price, # tickPrice()
|
48
|
+
3 => :ask_size, # tickSize()
|
49
|
+
4 => :last_price, # tickPrice()
|
50
|
+
5 => :last_size, # tickSize()
|
51
|
+
6 => :high, # tickPrice()
|
52
|
+
7 => :low, # tickPrice()
|
53
|
+
8 => :volume, # tickSize()
|
54
|
+
9 => :close_price, # tickPrice()
|
55
|
+
10 => :bid_option, # tickOptionComputation() See Note 1 below
|
56
|
+
11 => :ask_option, # tickOptionComputation() See => :Note 1 below
|
57
|
+
12 => :last_option, # tickOptionComputation() See Note 1 below
|
58
|
+
13 => :model_option, # tickOptionComputation() See Note 1 below
|
59
|
+
14 => :open_tick, # tickPrice()
|
60
|
+
15 => :low_13_week, # tickPrice()
|
61
|
+
16 => :high_13_week, # tickPrice()
|
62
|
+
17 => :low_26_week, # tickPrice()
|
63
|
+
18 => :high_26_week, # tickPrice()
|
64
|
+
19 => :low_52_week, # tickPrice()
|
65
|
+
20 => :high_52_week, # tickPrice()
|
66
|
+
21 => :avg_volume, # tickSize()
|
67
|
+
22 => :open_interest, # tickSize()
|
68
|
+
23 => :option_historical_vol, # tickGeneric()
|
69
|
+
24 => :option_implied_vol, # tickGeneric()
|
70
|
+
25 => :option_bid_exch, # not USED
|
71
|
+
26 => :option_ask_exch, # not USED
|
72
|
+
27 => :option_call_open_interest, # tickSize()
|
73
|
+
28 => :option_put_open_interest, # tickSize()
|
74
|
+
29 => :option_call_volume, # tickSize()
|
75
|
+
30 => :option_put_volume, # tickSize()
|
76
|
+
31 => :index_future_premium, # tickGeneric()
|
77
|
+
32 => :bid_exch, # tickString()
|
78
|
+
33 => :ask_exch, # tickString()
|
79
|
+
34 => :auction_volume, # not USED
|
80
|
+
35 => :auction_price, # not USED
|
81
|
+
36 => :auction_imbalance, # not USED
|
82
|
+
37 => :mark_price, # tickPrice()
|
83
|
+
38 => :bid_efp_computation, # tickEFP()
|
84
|
+
39 => :ask_efp_computation, # tickEFP()
|
85
|
+
40 => :last_efp_computation, # tickEFP()
|
86
|
+
41 => :open_efp_computation, # tickEFP()
|
87
|
+
42 => :high_efp_computation, # tickEFP()
|
88
|
+
43 => :low_efp_computation, # tickEFP()
|
89
|
+
44 => :close_efp_computation, # tickEFP()
|
90
|
+
45 => :last_timestamp, # tickString()
|
91
|
+
46 => :shortable, # tickGeneric()
|
92
|
+
47 => :fundamental_ratios, # tickString()
|
93
|
+
48 => :rt_volume, # tickGeneric()
|
94
|
+
49 => :halted, # see Note 2 below.
|
95
|
+
50 => :bid_yield, # tickPrice() See Note 3 below
|
96
|
+
51 => :ask_yield, # tickPrice() See Note 3 below
|
97
|
+
52 => :last_yield, # tickPrice() See Note 3 below
|
98
|
+
53 => :cust_option_computation, # tickOptionComputation()
|
99
|
+
54 => :trade_count, # tickGeneric()
|
100
|
+
55 => :trade_rate, # tickGeneric()
|
101
|
+
56 => :volume_rate, # tickGeneric()
|
102
|
+
57 => :last_rth_trade, #
|
103
|
+
58 => :rt_historical_vol,
|
104
|
+
59 => :ib_dividends,
|
105
|
+
60 => :bond_factor_multiplier,
|
106
|
+
61 => :regulatory_imbalance,
|
107
|
+
62 => :news_tick,
|
108
|
+
63 => :short_term_volume_3_min,
|
109
|
+
64 => :short_term_volume_5_min,
|
110
|
+
65 => :short_term_volume_10_min,
|
111
|
+
66 => :delayed_bid,
|
112
|
+
67 => :delayed_ask,
|
113
|
+
68 => :delayed_last,
|
114
|
+
69 => :delayed_bid_size,
|
115
|
+
70 => :delayed_ask_size,
|
116
|
+
71 => :delayed_last_size,
|
117
|
+
72 => :delayed_high,
|
118
|
+
73 => :delayed_low,
|
119
|
+
74 => :delayed_volume,
|
120
|
+
75 => :delayed_close,
|
121
|
+
76 => :delayed_open,
|
122
|
+
77 => :rt_trd_volume,
|
123
|
+
78 => :creditman_mark_price,
|
124
|
+
79 => :creditman_slow_mark_price,
|
125
|
+
80 => :delayed_bid_option,
|
126
|
+
81 => :delayed_ask_option,
|
127
|
+
82 => :delayed_last_option,
|
128
|
+
83 => :delayed_model_option,
|
129
|
+
84 => :last_exch,
|
130
|
+
85 => :last_reg_time,
|
131
|
+
86 => :futures_open_interest,
|
132
|
+
87 => :avg_opt_volume,
|
133
|
+
88 => :not_set,
|
134
|
+
105 => :average_option_volume #(for Stocks) tickGeneric()
|
135
|
+
|
136
|
+
|
137
|
+
# Note 1: Tick types BID_OPTION, ASK_OPTION, LAST_OPTION, and MODEL_OPTION return
|
138
|
+
# all Greeks (delta, gamma, vega, theta), the underlying price and the
|
139
|
+
# stock and option reference price when requested.
|
140
|
+
# MODEL_OPTION also returns model implied volatility.
|
141
|
+
# Note 2: When trading is halted for a contract, TWS receives a special tick:
|
142
|
+
# haltedLast=1. When trading is resumed, TWS receives haltedLast=0.
|
143
|
+
# A tick type, HALTED, tick ID= 49, is now available in regular market
|
144
|
+
# data via the API to indicate this halted state. Possible values for
|
145
|
+
# this new tick type are: 0 = Not halted, 1 = Halted.
|
146
|
+
# Note 3: Applies to bond contracts only.
|
147
|
+
}
|
148
|
+
|
149
|
+
# Financial Advisor types (FaMsgTypeName)
|
150
|
+
FA_TYPES = {
|
151
|
+
1 => :groups,
|
152
|
+
2 => :profiles,
|
153
|
+
3 => :aliases}.freeze
|
154
|
+
|
155
|
+
# Received in new MarketDataType (58 incoming) message
|
156
|
+
MARKET_DATA_TYPES = {
|
157
|
+
0 => :unknown,
|
158
|
+
1 => :real_time,
|
159
|
+
2 => :frozen,
|
160
|
+
3 => :delayed,
|
161
|
+
4 => :frozen_delayed }.freeze
|
162
|
+
|
163
|
+
# Market depth messages contain these "operation" codes to tell you what to do with the data.
|
164
|
+
# See also http://www.interactivebrokers.com/php/apiUsersGuide/apiguide/java/updatemktdepth.htm
|
165
|
+
MARKET_DEPTH_OPERATIONS = {
|
166
|
+
0 => :insert, # New order, insert into the row identified by :position
|
167
|
+
1 => :update, # Update the existing order at the row identified by :position
|
168
|
+
2 => :delete # Delete the existing order at the row identified by :position
|
169
|
+
}.freeze
|
170
|
+
|
171
|
+
MARKET_DEPTH_SIDES = {
|
172
|
+
0 => :ask,
|
173
|
+
1 => :bid
|
174
|
+
}.freeze
|
175
|
+
|
176
|
+
ORDER_TYPES =
|
177
|
+
{'LMT' => :limit, # Limit Order
|
178
|
+
'LIT' => :limit_if_touched, # Limit if Touched
|
179
|
+
'LOC' => :limit_on_close, # Limit-on-Close LMTCLS ?
|
180
|
+
'LOO' => :limit_on_open, # Limit-on-Open
|
181
|
+
'MKT' => :market, # Market
|
182
|
+
'MIT' => :market_if_touched, # Market-if-Touched
|
183
|
+
'MOC' => :market_on_close, # Market-on-Close MKTCLSL ?
|
184
|
+
'MOO' => :market_on_open, # Market-on-Open
|
185
|
+
'MTL' => :market_to_limit, # Market-to-Limit
|
186
|
+
'MKT PRT' => :market_protected, # Market with Protection
|
187
|
+
'QUOTE' => :request_for_quote, # Request for Quote
|
188
|
+
'STP' => :stop, # Stop
|
189
|
+
'STP LMT' => :stop_limit, # Stop Limit
|
190
|
+
'STP PRT' => :stop_protected, # Stop with Protection
|
191
|
+
'TRAIL' => :trailing_stop, # Trailing Stop
|
192
|
+
'TRAIL LIMIT' => :trailing_limit, # Trailing Stop Limit
|
193
|
+
'TRAIL LIT' => :trailing_limit_if_touched, # Trailing Limit if Touched
|
194
|
+
'TRAIL MIT' => :trailing_market_if_touched, # Trailing Market If Touched
|
195
|
+
'REL' => :relative, # Relative
|
196
|
+
'BOX TOP' => :box_top, # Box Top
|
197
|
+
'PEG MKT' => :pegged_to_market, # Pegged-to-Market
|
198
|
+
'PEG STK' => :pegged_to_market, # Pegged-to-Stock
|
199
|
+
'PEG MID' => :pegged_to_midpoint, # Pegged-to-Midpoint
|
200
|
+
'PEG BENCH' => :pegged_to_benchmark, # Pegged-to-Benmchmark # Vers. 102
|
201
|
+
'VWAP' => :vwap, # VWAP-Guaranted
|
202
|
+
'OCA' => :one_cancels_all, # One-Cancels-All
|
203
|
+
'VOL' => :volatility, # Volatility
|
204
|
+
'SCALE' => :scale, # Scale
|
205
|
+
'NONE' => :none, # Used to indicate no hedge in :delta_neutral_order_type
|
206
|
+
'None' => :none, # Used to indicate no hedge in :delta_neutral_order_type
|
207
|
+
}.freeze
|
208
|
+
# Valid security types (sec_type attribute of IB::Contract)
|
209
|
+
SECURITY_TYPES =
|
210
|
+
{ 'BAG' => :bag,
|
211
|
+
'BOND' => :bond,
|
212
|
+
'CASH' => :forex,
|
213
|
+
'CMDTY'=> :commodity,
|
214
|
+
'CFD' => :cfd,
|
215
|
+
'FUT' => :future,
|
216
|
+
'CONTFUT' => :continous_future,
|
217
|
+
'FUT+CONTFUT' => :all_futures,
|
218
|
+
'FOP' => :futures_option,
|
219
|
+
'FUND' => :fund, # ETF?
|
220
|
+
'IND' => :index,
|
221
|
+
'NEWS' => :news,
|
222
|
+
'OPT' => :option,
|
223
|
+
'IOPT' => :dutch_option,
|
224
|
+
'STK' => :stock,
|
225
|
+
'WAR' => :warrant,
|
226
|
+
'ICU' => :icu,
|
227
|
+
'ICS' => :ics,
|
228
|
+
'BILL' => :bill,
|
229
|
+
'BSK' => :basket,
|
230
|
+
'FWD' => :forward,
|
231
|
+
'FIXED' => :fixed }.freeze
|
232
|
+
|
233
|
+
# Obtain symbolic value from given property code:
|
234
|
+
# VALUES[:side]['B'] -> :buy
|
235
|
+
VALUES = {
|
236
|
+
:sec_type => SECURITY_TYPES,
|
237
|
+
:order_type => ORDER_TYPES,
|
238
|
+
:delta_neutral_order_type => ORDER_TYPES,
|
239
|
+
|
240
|
+
:origin => {0 => :customer, 1 => :firm},
|
241
|
+
:volatility_type => {1 => :daily, 2 => :annual},
|
242
|
+
:reference_price_type => {1 => :average, 2 => :bid_or_ask},
|
243
|
+
|
244
|
+
# This property encodes differently for ComboLeg and Order objects,
|
245
|
+
# we use ComboLeg codes and transcode for Order codes as needed
|
246
|
+
:open_close =>
|
247
|
+
{0 => :same, # Default for Legs, same as the parent (combo) security.
|
248
|
+
1 => :open, # Open. For Legs, this value is only used by institutions.
|
249
|
+
2 => :close, # Close. For Legs, this value is only used by institutions.
|
250
|
+
3 => :unknown}, # WTF
|
251
|
+
|
252
|
+
:right =>
|
253
|
+
{'' => :none, # Not an option
|
254
|
+
'P' => :put,
|
255
|
+
'C' => :call},
|
256
|
+
|
257
|
+
:side => # AKA action
|
258
|
+
{'B' => :buy, # or BOT
|
259
|
+
'S' => :sell, # or SLD
|
260
|
+
'T' => :short, # short
|
261
|
+
'X' => :short_exempt # Short Sale Exempt action. This allows some orders
|
262
|
+
# to be exempt from the SEC recent changes to Regulation SHO, which
|
263
|
+
# eliminated the old uptick rule and replaced it with a new "circuit breaker"
|
264
|
+
# rule, and allows some orders to be exempt from the new rule.
|
265
|
+
},
|
266
|
+
|
267
|
+
:short_sale_slot =>
|
268
|
+
{0 => :default, # The only valid option for retail customers
|
269
|
+
1 => :broker, # Shares are at your clearing broker, institutions
|
270
|
+
2 => :third_party}, # Shares will be delivered from elsewhere, institutions
|
271
|
+
|
272
|
+
:oca_type =>
|
273
|
+
{0 => :none, # Not a member of OCA group
|
274
|
+
1 => :cancel_with_block, # Cancel all remaining orders with block
|
275
|
+
2 => :reduce_with_block, # Remaining orders are reduced in size with block
|
276
|
+
3 => :reduce_no_block}, # Remaining orders are reduced in size with no block
|
277
|
+
|
278
|
+
:auction_strategy =>
|
279
|
+
{0 => :none, # Not a BOX order
|
280
|
+
1 => :match,
|
281
|
+
2 => :improvement,
|
282
|
+
3 => :transparent},
|
283
|
+
|
284
|
+
:trigger_method =>
|
285
|
+
{0 => :default, # "double bid/ask" used for OTC/US options, "last" otherswise.
|
286
|
+
1 => :double_bid_ask, # stops are triggered by 2 consecutive bid or ask prices.
|
287
|
+
2 => :last, # stops are triggered based on the last price.
|
288
|
+
3 => :double_last,
|
289
|
+
4 => :bid_ask, # bid >= trigger price for buy orders, ask <= trigger for sell orders
|
290
|
+
7 => :last_or_bid_ask, # bid OR last price >= trigger price for buy orders
|
291
|
+
8 => :mid_point}, # midpoint >= trigger price for buy orders and the
|
292
|
+
# spread between the bid and ask must be less than 0.1% of the midpoint
|
293
|
+
|
294
|
+
:hedge_type =>
|
295
|
+
{'D' => :delta, # parent order is an option and the child order is a stock
|
296
|
+
'B' => :beta, # offset market risk by entering into a position with
|
297
|
+
# another contract based on the system or user-defined beta
|
298
|
+
'F' => :forex, # offset risk with currency different from your base currency
|
299
|
+
'P' => :pair}, # trade a mis-valued pair of contracts and provide the
|
300
|
+
# ratio between the parent and hedging child order
|
301
|
+
|
302
|
+
:clearing_intent =>
|
303
|
+
{'' => :none,
|
304
|
+
'IB' => :ib,
|
305
|
+
'AWAY' => :away,
|
306
|
+
'PTA' => :post_trade_allocation},
|
307
|
+
|
308
|
+
:delta_neutral_clearing_intent =>
|
309
|
+
{'' => :none,
|
310
|
+
'IB' => :ib,
|
311
|
+
'AWAY' => :away,
|
312
|
+
'PTA' => :post_trade_allocation},
|
313
|
+
|
314
|
+
:tif =>
|
315
|
+
{'DAY' => :day,
|
316
|
+
'GAT' => :good_after_time,
|
317
|
+
'GTD' => :good_till_date,
|
318
|
+
'GTC' => :good_till_cancelled,
|
319
|
+
'IOC' => :immediate_or_cancel,
|
320
|
+
'OPG' => :opening_price,
|
321
|
+
'AUC' => :at_auction},
|
322
|
+
|
323
|
+
:rule_80a =>
|
324
|
+
{'I' => :individual,
|
325
|
+
'A' => :agency,
|
326
|
+
'W' => :agent_other_member,
|
327
|
+
'J' => :individual_ptia,
|
328
|
+
'U' => :agency_ptia,
|
329
|
+
'M' => :agent_other_member_ptia,
|
330
|
+
'K' => :individual_pt,
|
331
|
+
'Y' => :agency_pt,
|
332
|
+
'N' => :agent_other_member_pt},
|
333
|
+
|
334
|
+
:opt? => # TODO: unknown Order property, like OPT_BROKER_DEALER... in Order.java
|
335
|
+
{'?' => :unknown,
|
336
|
+
'b' => :broker_dealer,
|
337
|
+
'c' => :customer,
|
338
|
+
'f' => :firm,
|
339
|
+
'm' => :isemm,
|
340
|
+
'n' => :farmm,
|
341
|
+
'y' => :specialist},
|
342
|
+
# conditions
|
343
|
+
:conjunction_connection => { 'o' => :or, 'a' => :and },
|
344
|
+
:operator => { 1 => '>=' , 0 => '<=' }
|
345
|
+
|
346
|
+
}.freeze
|
347
|
+
|
348
|
+
# Obtain property code from given symbolic value:
|
349
|
+
# CODES[:side][:buy] -> 'B'
|
350
|
+
CODES = Hash[VALUES.map { |property, hash| [property, hash.invert] }].freeze
|
351
|
+
|
352
|
+
# Most common property processors
|
353
|
+
PROPS = {:side =>
|
354
|
+
{:set => proc { |val| # BUY(BOT)/SELL(SLD)/SSHORT/SSHORTX
|
355
|
+
self[:side] = case val.to_s.upcase
|
356
|
+
when /SHORT.*X|\AX\z/
|
357
|
+
'X'
|
358
|
+
when /SHORT|\AT\z/
|
359
|
+
'T'
|
360
|
+
when /\AB/
|
361
|
+
'B'
|
362
|
+
when /\AS/
|
363
|
+
'S'
|
364
|
+
end },
|
365
|
+
:validate =>
|
366
|
+
{:format =>
|
367
|
+
{:with => /\Abuy\z|\Asell\z|\Ashort\z|\Ashort_exempt\z/,
|
368
|
+
:message => "should be buy/sell/short"}
|
369
|
+
}
|
370
|
+
},
|
371
|
+
|
372
|
+
:open_close =>
|
373
|
+
{:set => proc { |val|
|
374
|
+
self[:open_close] = case val.to_s.upcase[0..0]
|
375
|
+
when 'S', '0' # SAME
|
376
|
+
0
|
377
|
+
when 'O', '1' # OPEN
|
378
|
+
1
|
379
|
+
when 'C', '2' # CLOSE
|
380
|
+
2
|
381
|
+
when 'U', '3' # Unknown
|
382
|
+
3
|
383
|
+
end
|
384
|
+
},
|
385
|
+
:validate =>
|
386
|
+
{:format =>
|
387
|
+
{:with => /\Asame\z|\Aopen\z|\Aclose\z|\Aunknown\z/,
|
388
|
+
:message => "should be same/open/close/unknown"}
|
389
|
+
},
|
390
|
+
}
|
391
|
+
}.freeze
|
392
|
+
|
393
|
+
end # module IB
|