ib-ruby 0.4.3 → 0.4.20
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.
- data/.gitignore +32 -0
- data/HISTORY +68 -0
- data/README.rdoc +9 -6
- data/VERSION +1 -1
- data/bin/account_info +29 -0
- data/bin/contract_details +37 -0
- data/bin/depth_of_market +43 -0
- data/bin/historic_data +62 -0
- data/bin/{RequestHistoricData → historic_data_cli} +46 -91
- data/bin/market_data +49 -0
- data/bin/option_data +45 -0
- data/bin/template +21 -0
- data/bin/time_and_sales +63 -0
- data/lib/ib-ruby/connection.rb +166 -0
- data/lib/ib-ruby/constants.rb +91 -0
- data/lib/ib-ruby/messages/incoming.rb +807 -0
- data/lib/ib-ruby/messages/outgoing.rb +573 -0
- data/lib/ib-ruby/messages.rb +8 -1445
- data/lib/ib-ruby/models/bar.rb +26 -0
- data/lib/ib-ruby/models/contract.rb +335 -0
- data/lib/ib-ruby/models/execution.rb +55 -0
- data/lib/ib-ruby/models/model.rb +20 -0
- data/lib/ib-ruby/models/order.rb +262 -0
- data/lib/ib-ruby/models.rb +11 -0
- data/lib/ib-ruby/socket.rb +50 -0
- data/lib/ib-ruby/symbols/forex.rb +32 -72
- data/lib/ib-ruby/symbols/futures.rb +47 -68
- data/lib/ib-ruby/symbols/options.rb +30 -0
- data/lib/ib-ruby/symbols/stocks.rb +23 -0
- data/lib/ib-ruby/symbols.rb +9 -0
- data/lib/ib-ruby.rb +7 -8
- data/lib/legacy/bin/account_info_old +36 -0
- data/lib/legacy/bin/historic_data_old +81 -0
- data/lib/legacy/bin/market_data_old +68 -0
- data/lib/legacy/datatypes.rb +485 -0
- data/lib/legacy/ib-ruby.rb +10 -0
- data/lib/legacy/ib.rb +226 -0
- data/lib/legacy/messages.rb +1458 -0
- data/lib/version.rb +2 -3
- data/spec/ib-ruby/models/contract_spec.rb +261 -0
- data/spec/ib-ruby/models/order_spec.rb +64 -0
- data/spec/ib-ruby_spec.rb +0 -131
- metadata +106 -76
- data/bin/AccountInfo +0 -67
- data/bin/HistoricToCSV +0 -111
- data/bin/RequestMarketData +0 -78
- data/bin/SimpleTimeAndSales +0 -98
- data/bin/ib-ruby +0 -8
- data/lib/ib-ruby/datatypes.rb +0 -400
- data/lib/ib-ruby/ib.rb +0 -242
@@ -0,0 +1,1458 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2006 Blue Voodoo Magic LLC.
|
3
|
+
#
|
4
|
+
# This library is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU Lesser General Public License as
|
6
|
+
# published by the Free Software Foundation; either version 2.1 of the
|
7
|
+
# License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This library is distributed in the hope that it will be useful, but
|
10
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
12
|
+
# Lesser General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU Lesser General Public
|
15
|
+
# License along with this library; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
17
|
+
# 02110-1301 USA
|
18
|
+
|
19
|
+
|
20
|
+
# EClientSocket.java uses sendMax() rather than send() for a number of these.
|
21
|
+
# It sends an EOL rather than a number if the value == Integer.MAX_VALUE (or Double.MAX_VALUE).
|
22
|
+
# These fields are initialized to this MAX_VALUE.
|
23
|
+
# This has been implemented with nils in Ruby to represent the case where an EOL should be sent.
|
24
|
+
|
25
|
+
module IB
|
26
|
+
|
27
|
+
#logger = Logger.new(STDERR)
|
28
|
+
|
29
|
+
class ExtremelyAbstractMessage
|
30
|
+
attr_reader :created_at
|
31
|
+
|
32
|
+
def to_human
|
33
|
+
self.inspect
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
################################################################
|
39
|
+
#### Outgoing messages
|
40
|
+
################################################################
|
41
|
+
|
42
|
+
module OutgoingMessages
|
43
|
+
EOL = "\0"
|
44
|
+
|
45
|
+
class AbstractMessage < ExtremelyAbstractMessage
|
46
|
+
def self.message_id
|
47
|
+
raise Exception("AbstractMessage.message_id called - you need to override this in a subclass.")
|
48
|
+
end
|
49
|
+
|
50
|
+
# data is a Hash.
|
51
|
+
def initialize(data=nil)
|
52
|
+
@created_at = Time.now
|
53
|
+
@data = Datatypes::StringentHash.new(data)
|
54
|
+
end
|
55
|
+
|
56
|
+
# This causes the message to send itself over the server socket in server[:socket].
|
57
|
+
# "server" is the @server instance variable from the IB object.
|
58
|
+
# You can also use this to e.g. get the server version number.
|
59
|
+
#
|
60
|
+
# Subclasses can either override this method for precise control
|
61
|
+
# over how stuff gets sent to the server, or else define a
|
62
|
+
# method queue() that returns an Array of elements that ought to
|
63
|
+
# be sent to the server by calling to_s on each one and
|
64
|
+
# postpending a '\0'.
|
65
|
+
#
|
66
|
+
def send(server)
|
67
|
+
self.queue(server).each { |datum|
|
68
|
+
|
69
|
+
# TWS wants to receive booleans as 1 or 0... rewrite as necessary.
|
70
|
+
datum = "1" if datum == true
|
71
|
+
datum = "0" if datum == false
|
72
|
+
#
|
73
|
+
#print 'SENDING: '
|
74
|
+
#p datum
|
75
|
+
server[:socket].syswrite(datum.to_s + "\0")
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def queue
|
80
|
+
raise Exception("AbstractMessage.queue() called - you need to override this in a subclass.")
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def requireVersion(server, version)
|
87
|
+
raise(Exception.new("TWS version >= #{version} required.")) if server[:version] < version
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns EOL instead of datum if datum is nil, providing the same functionality
|
91
|
+
# as sendMax() in the Java version, which uses Double.MAX_VALUE to mean "item not set"
|
92
|
+
# in a variable, and replaces that with EOL on send.
|
93
|
+
def nilFilter(datum)
|
94
|
+
datum.nil? ? EOL : datum
|
95
|
+
end
|
96
|
+
|
97
|
+
end # AbstractMessage
|
98
|
+
|
99
|
+
|
100
|
+
# Data format is { :ticker_id => int, :contract => Datatypes::Contract }
|
101
|
+
class RequestMarketData < AbstractMessage
|
102
|
+
def self.message_id
|
103
|
+
1
|
104
|
+
end
|
105
|
+
|
106
|
+
def queue(server)
|
107
|
+
queue = [self.class.message_id,
|
108
|
+
5, # message version number
|
109
|
+
@data[:ticker_id]
|
110
|
+
].concat(@data[:contract].serialize_long(server[:version]))
|
111
|
+
|
112
|
+
# No idea what "BAG" means. Copied from the Java code.
|
113
|
+
queue.concat(@data[:contract].serialize_combo_legs
|
114
|
+
) if server[:version] >= 8 && @data[:contract].sec_type == "BAG"
|
115
|
+
|
116
|
+
queue
|
117
|
+
end # queue
|
118
|
+
end # RequestMarketData
|
119
|
+
|
120
|
+
# Data format is { :ticker_id => int }
|
121
|
+
class CancelMarketData < AbstractMessage
|
122
|
+
def self.message_id
|
123
|
+
2
|
124
|
+
end
|
125
|
+
|
126
|
+
def queue(server)
|
127
|
+
[self.class.message_id,
|
128
|
+
1, # message version number
|
129
|
+
@data[:ticker_id]]
|
130
|
+
end # queue
|
131
|
+
end # CancelMarketData
|
132
|
+
|
133
|
+
# Data format is { :order_id => int, :contract => Contract, :order => Order }
|
134
|
+
class PlaceOrder < AbstractMessage
|
135
|
+
def self.message_id
|
136
|
+
3
|
137
|
+
end
|
138
|
+
|
139
|
+
def queue(server)
|
140
|
+
queue = [self.class.message_id,
|
141
|
+
20, # version
|
142
|
+
@data[:order_id],
|
143
|
+
@data[:contract].symbol,
|
144
|
+
@data[:contract].sec_type,
|
145
|
+
@data[:contract].expiry,
|
146
|
+
@data[:contract].strike,
|
147
|
+
@data[:contract].right
|
148
|
+
]
|
149
|
+
queue.push(@data[:contract].multiplier) if server[:version] >= 15
|
150
|
+
queue.push(@data[:contract].exchange) if server[:version] >= 14
|
151
|
+
queue.push(@data[:contract].currency)
|
152
|
+
queue.push(@data[:contract].local_symbol) if server[:version] >= 2
|
153
|
+
|
154
|
+
queue.concat([
|
155
|
+
@data[:order].tif,
|
156
|
+
@data[:order].oca_group,
|
157
|
+
@data[:order].account,
|
158
|
+
@data[:order].open_close,
|
159
|
+
@data[:order].origin,
|
160
|
+
@data[:order].order_ref,
|
161
|
+
@data[:order].transmit
|
162
|
+
])
|
163
|
+
|
164
|
+
queue.push(@data[:contract].parent_id) if server[:version] >= 4
|
165
|
+
|
166
|
+
queue.concat([
|
167
|
+
@data[:order].block_order,
|
168
|
+
@data[:order].sweep_to_fill,
|
169
|
+
@data[:order].display_size,
|
170
|
+
@data[:order].trigger_method,
|
171
|
+
@data[:order].outside_rth
|
172
|
+
]) if server[:version] >= 5
|
173
|
+
|
174
|
+
queue.push(@data[:order].hidden) if server[:version] >= 7
|
175
|
+
|
176
|
+
|
177
|
+
queue.concat(@data[:contract].serialize_combo_legs(true)) if server[:version] >= 8 &&
|
178
|
+
@data[:contract].sec_type.upcase == "BAG" # "BAG" is defined as a constant in EClientSocket.java, line 45
|
179
|
+
|
180
|
+
queue.push(@data[:order].shares_allocation) if server[:version] >= 9 # EClientSocket.java says this is deprecated. No idea.
|
181
|
+
queue.push(@data[:order].discretionary_amount) if server[:version] >= 10
|
182
|
+
queue.push(@data[:order].good_after_time) if server[:version] >= 11
|
183
|
+
queue.push(@data[:order].good_till_date) if server[:version] >= 12
|
184
|
+
|
185
|
+
queue.concat([
|
186
|
+
@data[:order].fa_group,
|
187
|
+
@data[:order].fa_method,
|
188
|
+
@data[:order].fa_percentage,
|
189
|
+
@data[:order].fa_profile
|
190
|
+
]) if server[:version] >= 13
|
191
|
+
|
192
|
+
queue.concat([
|
193
|
+
@data[:order].short_sale_slot,
|
194
|
+
@data[:order].designated_location
|
195
|
+
]) if server[:version] >= 18
|
196
|
+
|
197
|
+
queue.concat([
|
198
|
+
@data[:order].oca_type,
|
199
|
+
@data[:order].rth_only,
|
200
|
+
@data[:order].rule_80a,
|
201
|
+
@data[:order].settling_firm,
|
202
|
+
@data[:order].all_or_none,
|
203
|
+
nilFilter(@data[:order].min_quantity),
|
204
|
+
nilFilter(@data[:order].percent_offset),
|
205
|
+
@data[:order].etrade_only,
|
206
|
+
@data[:order].firm_quote_only,
|
207
|
+
nilFilter(@data[:order].nbbo_price_cap),
|
208
|
+
nilFilter(@data[:order].auction_strategy),
|
209
|
+
nilFilter(@data[:order].starting_price),
|
210
|
+
nilFilter(@data[:order].stock_ref_price),
|
211
|
+
nilFilter(@data[:order].delta),
|
212
|
+
|
213
|
+
# Says the Java here:
|
214
|
+
# "// Volatility orders had specific watermark price attribs in server version 26"
|
215
|
+
# I have no idea what this means.
|
216
|
+
|
217
|
+
((server[:version] == 26 && @data[:order].order_type.upcase == "VOL") ? EOL : @data[:order].stock_range_lower),
|
218
|
+
((server[:version] == 26 && @data[:order].order_type.upcase == "VOL") ? EOL : @data[:order].stock_range_upper),
|
219
|
+
|
220
|
+
]) if server[:version] >= 19
|
221
|
+
|
222
|
+
queue.push(@data[:order].override_percentage_constraints) if server[:version] >= 22
|
223
|
+
|
224
|
+
# Volatility orders
|
225
|
+
if server[:version] >= 26
|
226
|
+
queue.concat([nilFilter(@data[:order].volatility),
|
227
|
+
nilFilter(@data[:order].volatility_type)])
|
228
|
+
|
229
|
+
if server[:version] < 28
|
230
|
+
queue.push(@data[:order].delta_neutral_order_type.upcase == "MKT")
|
231
|
+
else
|
232
|
+
queue.concat([@data[:order].delta_neutral_order_type,
|
233
|
+
nilFilter(@data[:order].delta_neutral_aux_price)
|
234
|
+
])
|
235
|
+
end
|
236
|
+
|
237
|
+
queue.push(@data[:order].continuous_update)
|
238
|
+
queue.concat([
|
239
|
+
(@data[:order].order_type.upcase == "VOL" ? @data[:order].stock_range_lower : EOL),
|
240
|
+
(@data[:order].order_type.upcase == "VOL" ? @data[:order].stock_range_upper : EOL)
|
241
|
+
]) if server[:version] == 26
|
242
|
+
|
243
|
+
queue.push(@data[:order].reference_price_type)
|
244
|
+
|
245
|
+
end # if version >= 26
|
246
|
+
|
247
|
+
queue
|
248
|
+
end # queue()
|
249
|
+
|
250
|
+
end # PlaceOrder
|
251
|
+
|
252
|
+
# Data format is { :id => id-to-cancel }
|
253
|
+
class CancelOrder < AbstractMessage
|
254
|
+
def self.message_id
|
255
|
+
4
|
256
|
+
end
|
257
|
+
|
258
|
+
def queue(server)
|
259
|
+
[
|
260
|
+
self.class.message_id,
|
261
|
+
1, # version
|
262
|
+
@data[:id]
|
263
|
+
]
|
264
|
+
end # queue
|
265
|
+
end # CancelOrder
|
266
|
+
|
267
|
+
class RequestOpenOrders < AbstractMessage
|
268
|
+
def self.message_id
|
269
|
+
5
|
270
|
+
end
|
271
|
+
|
272
|
+
def queue(server)
|
273
|
+
[self.class.message_id,
|
274
|
+
1 # version
|
275
|
+
]
|
276
|
+
end
|
277
|
+
end # RequestOpenOrders
|
278
|
+
|
279
|
+
# Data is { :subscribe => boolean, :account_code => string }
|
280
|
+
#
|
281
|
+
# :account_code is only necessary for advisor accounts. Set it to
|
282
|
+
# empty ('') for a standard account.
|
283
|
+
#
|
284
|
+
class RequestAccountData < AbstractMessage
|
285
|
+
def self.message_id
|
286
|
+
6
|
287
|
+
end
|
288
|
+
|
289
|
+
def queue(server)
|
290
|
+
queue = [self.class.message_id,
|
291
|
+
2, # version
|
292
|
+
@data[:subscribe]
|
293
|
+
]
|
294
|
+
queue.push(@data[:account_code]) if server[:version] >= 9
|
295
|
+
queue
|
296
|
+
end
|
297
|
+
end # RequestAccountData
|
298
|
+
|
299
|
+
|
300
|
+
# data = { :filter => ExecutionFilter ]
|
301
|
+
class RequestExecutions < AbstractMessage
|
302
|
+
def self.message_id
|
303
|
+
7
|
304
|
+
end
|
305
|
+
|
306
|
+
def queue(server)
|
307
|
+
queue = [self.class.message_id,
|
308
|
+
2 # version
|
309
|
+
]
|
310
|
+
|
311
|
+
queue.concat([
|
312
|
+
@data[:filter].client_id,
|
313
|
+
@data[:filter].acct_code,
|
314
|
+
|
315
|
+
# The Java says: 'Note that the valid format for m_time is "yyyymmdd-hh:mm:ss"'
|
316
|
+
@data[:filter].time,
|
317
|
+
@data[:filter].symbol,
|
318
|
+
@data[:filter].sec_type,
|
319
|
+
@data[:filter].exchange,
|
320
|
+
@data[:filter].side
|
321
|
+
]) if server[:version] >= 9
|
322
|
+
|
323
|
+
queue
|
324
|
+
end # queue
|
325
|
+
end # RequestExecutions
|
326
|
+
|
327
|
+
|
328
|
+
# data = { :number_of_ids => int }
|
329
|
+
class RequestIds < AbstractMessage
|
330
|
+
def self.message_id
|
331
|
+
8
|
332
|
+
end
|
333
|
+
|
334
|
+
def queue(server)
|
335
|
+
[self.class.message_id,
|
336
|
+
1, # version
|
337
|
+
@data[:number_of_ids]
|
338
|
+
]
|
339
|
+
end
|
340
|
+
end # RequestIds
|
341
|
+
|
342
|
+
|
343
|
+
# data => { :contract => Contract }
|
344
|
+
class RequestContractData < AbstractMessage
|
345
|
+
def self.message_id
|
346
|
+
9
|
347
|
+
end
|
348
|
+
|
349
|
+
def queue(server)
|
350
|
+
requireVersion(server, 4)
|
351
|
+
|
352
|
+
queue = [
|
353
|
+
self.class.message_id,
|
354
|
+
2, # version
|
355
|
+
@data[:contract].symbol,
|
356
|
+
@data[:contract].sec_type,
|
357
|
+
@data[:contract].expiry,
|
358
|
+
@data[:contract].strike,
|
359
|
+
@data[:contract].right
|
360
|
+
]
|
361
|
+
queue.push(@data[:contract].multiplier) if server[:version] >= 15
|
362
|
+
|
363
|
+
queue.concat([
|
364
|
+
@data[:contract].exchange,
|
365
|
+
@data[:contract].currency,
|
366
|
+
@data[:contract].local_symbol,
|
367
|
+
])
|
368
|
+
|
369
|
+
queue
|
370
|
+
end # send
|
371
|
+
end # RequestContractData
|
372
|
+
|
373
|
+
# data = { :ticker_id => int, :contract => Contract, :num_rows => int }
|
374
|
+
class RequestMarketDepth < AbstractMessage
|
375
|
+
def self.message_id
|
376
|
+
10
|
377
|
+
end
|
378
|
+
|
379
|
+
def queue(server)
|
380
|
+
requireVersion(server, 6)
|
381
|
+
|
382
|
+
queue = [self.class.message_id,
|
383
|
+
3, # version
|
384
|
+
@data[:ticker_id]
|
385
|
+
]
|
386
|
+
queue.concat(@data[:contract].serialize_short(server[:version]))
|
387
|
+
queue.push(@data[:num_rows]) if server[:version] >= 19
|
388
|
+
|
389
|
+
queue
|
390
|
+
|
391
|
+
end # queue
|
392
|
+
end # RequestMarketDepth
|
393
|
+
|
394
|
+
# data = { :ticker_id => int }
|
395
|
+
class CancelMarketDepth < AbstractMessage
|
396
|
+
def self.message_id
|
397
|
+
11
|
398
|
+
end
|
399
|
+
|
400
|
+
def queue(server)
|
401
|
+
requireVersion(self, 6)
|
402
|
+
|
403
|
+
[self.class.message_id,
|
404
|
+
1, # version
|
405
|
+
@data[:ticker_id]
|
406
|
+
]
|
407
|
+
end
|
408
|
+
end # CancelMarketDepth
|
409
|
+
|
410
|
+
|
411
|
+
# data = { :all_messages => boolean }
|
412
|
+
class RequestNewsBulletins < AbstractMessage
|
413
|
+
def self.message_id
|
414
|
+
12
|
415
|
+
end
|
416
|
+
|
417
|
+
def queue(server)
|
418
|
+
[self.class.message_id,
|
419
|
+
1, # version
|
420
|
+
@data[:all_messages]
|
421
|
+
]
|
422
|
+
end
|
423
|
+
end # RequestNewsBulletins
|
424
|
+
|
425
|
+
class CancelNewsBulletins < AbstractMessage
|
426
|
+
def self.message_id
|
427
|
+
13
|
428
|
+
end
|
429
|
+
|
430
|
+
def queue(server)
|
431
|
+
[self.class.message_id,
|
432
|
+
1 # version
|
433
|
+
]
|
434
|
+
end
|
435
|
+
end # CancelNewsBulletins
|
436
|
+
|
437
|
+
# data = { :loglevel => int }
|
438
|
+
class SetServerLoglevel < AbstractMessage
|
439
|
+
def self.message_id
|
440
|
+
14
|
441
|
+
end
|
442
|
+
|
443
|
+
def queue(server)
|
444
|
+
[self.class.message_id,
|
445
|
+
1, # version
|
446
|
+
@data[:loglevel]
|
447
|
+
]
|
448
|
+
end
|
449
|
+
end # SetServerLoglevel
|
450
|
+
|
451
|
+
# data = { :auto_bind => boolean }
|
452
|
+
class RequestAutoOpenOrders < AbstractMessage
|
453
|
+
def self.message_id
|
454
|
+
15
|
455
|
+
end
|
456
|
+
|
457
|
+
def queue(server)
|
458
|
+
[self.class.message_id,
|
459
|
+
1, # version
|
460
|
+
@data[:auto_bind]
|
461
|
+
]
|
462
|
+
end
|
463
|
+
end # RequestAutoOpenOrders
|
464
|
+
|
465
|
+
|
466
|
+
class RequestAllOpenOrders < AbstractMessage
|
467
|
+
def self.message_id
|
468
|
+
16
|
469
|
+
end
|
470
|
+
|
471
|
+
def queue(server)
|
472
|
+
[self.class.message_id,
|
473
|
+
1 # version
|
474
|
+
]
|
475
|
+
end
|
476
|
+
end # RequestAllOpenOrders
|
477
|
+
|
478
|
+
class RequestManagedAccounts < AbstractMessage
|
479
|
+
def self.message_id
|
480
|
+
17
|
481
|
+
end
|
482
|
+
|
483
|
+
def queue(server)
|
484
|
+
[self.class.message_id,
|
485
|
+
1 # version
|
486
|
+
]
|
487
|
+
end
|
488
|
+
end # RequestManagedAccounts
|
489
|
+
|
490
|
+
# No idea what this is.
|
491
|
+
# data = { :fa_data_type => int }
|
492
|
+
class RequestFA < AbstractMessage
|
493
|
+
def self.message_id
|
494
|
+
18
|
495
|
+
end
|
496
|
+
|
497
|
+
def queue(server)
|
498
|
+
requireVersion(server, 13)
|
499
|
+
|
500
|
+
[self.class.message_id,
|
501
|
+
1, # version
|
502
|
+
@data[:fa_data_type]
|
503
|
+
]
|
504
|
+
end
|
505
|
+
end # RequestFA
|
506
|
+
|
507
|
+
# No idea what this is.
|
508
|
+
# data = { :fa_data_type => int, :xml => string }
|
509
|
+
class ReplaceFA < AbstractMessage
|
510
|
+
def self.message_id
|
511
|
+
19
|
512
|
+
end
|
513
|
+
|
514
|
+
def queue(server)
|
515
|
+
requireVersion(server, 13)
|
516
|
+
|
517
|
+
[self.class.message_id,
|
518
|
+
1, # version
|
519
|
+
@data[:fa_data_type],
|
520
|
+
@data[:xml]
|
521
|
+
]
|
522
|
+
end
|
523
|
+
end # ReplaceFA
|
524
|
+
|
525
|
+
# data = { :ticker_id => int,
|
526
|
+
# :contract => Contract,
|
527
|
+
# :end_date_time => string,
|
528
|
+
# :duration => string, # this specifies an integer number of seconds
|
529
|
+
# :bar_size => int,
|
530
|
+
# :what_to_show => symbol, # one of :trades, :midpoint, :bid, or :ask
|
531
|
+
# :use_RTH => int,
|
532
|
+
# :format_date => int
|
533
|
+
# }
|
534
|
+
#
|
535
|
+
# Note that as of 4/07 there is no historical data available for forex spot.
|
536
|
+
#
|
537
|
+
# data[:contract] may either be a Contract object or a String. A String should be
|
538
|
+
# in serialize_ib_ruby format; that is, it should be a colon-delimited string in
|
539
|
+
# the format:
|
540
|
+
#
|
541
|
+
# symbol:security_type:expiry:strike:right:multiplier:exchange:primary_exchange:currency:local_symbol
|
542
|
+
#
|
543
|
+
# Fields not needed for a particular security should be left blank (e.g. strike
|
544
|
+
# and right are only relevant for options.)
|
545
|
+
#
|
546
|
+
# For example, to query the British pound futures contract trading on Globex expiring
|
547
|
+
# in September, 2008, the correct string is:
|
548
|
+
#
|
549
|
+
# GBP:FUT:200809:::62500:GLOBEX::USD:
|
550
|
+
#
|
551
|
+
# A Contract object will be automatically serialized into the required format.
|
552
|
+
#
|
553
|
+
# See also http://chuckcaplan.com/twsapi/index.php/void%20reqIntradayData%28%29
|
554
|
+
# for general information about how TWS handles historic data requests, whence
|
555
|
+
# the following has been adapted:
|
556
|
+
#
|
557
|
+
# The server providing historical prices appears to not always be
|
558
|
+
# available outside of market hours. If you call it outside of its
|
559
|
+
# supported time period, or if there is otherwise a problem with
|
560
|
+
# it, you will receive error #162 "Historical Market Data Service
|
561
|
+
# query failed.:HMDS query returned no data."
|
562
|
+
#
|
563
|
+
# The "endDateTime" parameter accepts a string in the form
|
564
|
+
# "yyyymmdd HH:mm:ss", with a time zone optionally allowed after a
|
565
|
+
# space at the end of the string; e.g. "20050701 18:26:44 GMT"
|
566
|
+
#
|
567
|
+
# The ticker id needs to be different than the reqMktData ticker
|
568
|
+
# id. If you use the same ticker ID you used for the symbol when
|
569
|
+
# you did ReqMktData, nothing comes back for the historical data call.
|
570
|
+
#
|
571
|
+
# Possible :bar_size values:
|
572
|
+
# 1 = 1 sec
|
573
|
+
# 2 = 5 sec
|
574
|
+
# 3 = 15 sec
|
575
|
+
# 4 = 30 sec
|
576
|
+
# 5 = 1 minute
|
577
|
+
# 6 = 2 minutes
|
578
|
+
# 7 = 5 minutes
|
579
|
+
# 8 = 15 minutes
|
580
|
+
# 9 = 30 minutes
|
581
|
+
# 10 = 1 hour
|
582
|
+
# 11 = 1 day
|
583
|
+
#
|
584
|
+
# Values less than 4 do not appear to work for certain securities.
|
585
|
+
#
|
586
|
+
# The nature of the data extracted is governed by sending a string
|
587
|
+
# having a value of "TRADES," "MIDPOINT," "BID," or "ASK." Here,
|
588
|
+
# we require a symbol argument of :trades, :midpoint, :bid, or
|
589
|
+
# :asked to be passed as data[:what_to_show].
|
590
|
+
#
|
591
|
+
# If data[:use_RTH] is set to 0, all data available during the time
|
592
|
+
# span requested is returned, even data bars covering time
|
593
|
+
# intervals where the market in question was illiquid. If useRTH
|
594
|
+
# has a non-zero value, only data within the "Regular Trading
|
595
|
+
# Hours" of the product in question is returned, even if the time
|
596
|
+
# span requested falls partially or completely outside of them.
|
597
|
+
#
|
598
|
+
# Using a :format_date of 1 will cause the dates in the returned
|
599
|
+
# messages with the historic data to be in a text format, like
|
600
|
+
# "20050307 11:32:16". If you set :format_date to 2 instead, you
|
601
|
+
# will get an offset in seconds from the beginning of 1970, which
|
602
|
+
# is the same format as the UNIX epoch time.
|
603
|
+
#
|
604
|
+
# For backfill on futures data, you may need to leave the Primary
|
605
|
+
# Exchange field of the Contract structure blank; see
|
606
|
+
# http://www.interactivebrokers.com/discus/messages/2/28477.html?1114646754
|
607
|
+
# [This message does not appear to exist anymore as of 4/07.]
|
608
|
+
|
609
|
+
ALLOWED_HISTORICAL_TYPES = [:trades, :midpoint, :bid, :ask]
|
610
|
+
|
611
|
+
class RequestHistoricalData < AbstractMessage
|
612
|
+
# Enumeration of bar size types for convenience. These are passed to TWS as the (one-based!) index into the array.
|
613
|
+
# Bar sizes less than 30 seconds do not work for some securities.
|
614
|
+
BarSizes = [
|
615
|
+
:invalid, # zero is not a valid barsize
|
616
|
+
:second,
|
617
|
+
:five_seconds,
|
618
|
+
:fifteen_seconds,
|
619
|
+
:thirty_seconds,
|
620
|
+
:minute,
|
621
|
+
:two_minutes,
|
622
|
+
:five_minutes,
|
623
|
+
:fifteen_minutes,
|
624
|
+
:thirty_minutes,
|
625
|
+
:hour,
|
626
|
+
:day,
|
627
|
+
]
|
628
|
+
|
629
|
+
|
630
|
+
def self.message_id
|
631
|
+
20
|
632
|
+
end
|
633
|
+
|
634
|
+
def queue(server)
|
635
|
+
requireVersion(server, 16)
|
636
|
+
|
637
|
+
if @data.has_key?(:what_to_show) && @data[:what_to_show].is_a?(String)
|
638
|
+
@data[:what_to_show].downcase!
|
639
|
+
@data[:what_to_show] = @data[:what_to_show].to_sym
|
640
|
+
end
|
641
|
+
|
642
|
+
raise ArgumentError("RequestHistoricalData: @data[:what_to_show] must be one of #{ALLOWED_HISTORICAL_TYPES.inspect}.") unless ALLOWED_HISTORICAL_TYPES.include?(@data[:what_to_show])
|
643
|
+
|
644
|
+
queue = [self.class.message_id,
|
645
|
+
3, # version
|
646
|
+
@data[:ticker_id]
|
647
|
+
]
|
648
|
+
|
649
|
+
contract = @data[:contract].is_a?(Datatypes::Contract) ? @data[:contract] : Datatypes::Contract.from_ib_ruby(@data[:contract])
|
650
|
+
queue.concat(contract.serialize_long(server[:version]))
|
651
|
+
|
652
|
+
queue.concat([
|
653
|
+
@data[:end_date_time],
|
654
|
+
@data[:bar_size]
|
655
|
+
]) if server[:version] > 20
|
656
|
+
|
657
|
+
|
658
|
+
queue.concat([
|
659
|
+
@data[:duration],
|
660
|
+
@data[:use_RTH],
|
661
|
+
@data[:what_to_show].to_s.upcase
|
662
|
+
])
|
663
|
+
|
664
|
+
queue.push(@data[:format_date]) if server[:version] > 16
|
665
|
+
|
666
|
+
if contract.sec_type.upcase == "BAG"
|
667
|
+
queue.concat(contract.serialize_combo_legs)
|
668
|
+
end
|
669
|
+
|
670
|
+
queue
|
671
|
+
end
|
672
|
+
end # RequestHistoricalData
|
673
|
+
|
674
|
+
# data = { :ticker_id => int,
|
675
|
+
# :contract => Contract,
|
676
|
+
# :exercise_action => int,
|
677
|
+
# :exercise_quantity => int,
|
678
|
+
# :account => string,
|
679
|
+
# :override => int } ## override? override what?
|
680
|
+
class ExerciseOptions < AbstractMessage
|
681
|
+
def self.message_id
|
682
|
+
21
|
683
|
+
end
|
684
|
+
|
685
|
+
def queue(server)
|
686
|
+
|
687
|
+
requireVersion(server, 21)
|
688
|
+
|
689
|
+
q = [self.class.message_id,
|
690
|
+
1, # version
|
691
|
+
@data[:ticker_id]
|
692
|
+
]
|
693
|
+
q.concat(@data[:contract].serialize_long(server[:version]))
|
694
|
+
q.concat([
|
695
|
+
@data[:exercise_action],
|
696
|
+
@data[:exercise_quantity],
|
697
|
+
@data[:account],
|
698
|
+
@data[:override]
|
699
|
+
])
|
700
|
+
q
|
701
|
+
end # queue
|
702
|
+
end # ExerciseOptions
|
703
|
+
|
704
|
+
# data = { :ticker_id => int,
|
705
|
+
# :scanner_subscription => ScannerSubscription
|
706
|
+
# }
|
707
|
+
class RequestScannerSubscription < AbstractMessage
|
708
|
+
def self.message_id
|
709
|
+
22
|
710
|
+
end
|
711
|
+
|
712
|
+
def queue(server)
|
713
|
+
requireVersion(server, 24)
|
714
|
+
|
715
|
+
[
|
716
|
+
self.class.message_id,
|
717
|
+
3, # version
|
718
|
+
@data[:ticker_id],
|
719
|
+
@data[:subscription].number_of_rows,
|
720
|
+
nilFilter(@data[:subscription].number_of_rows),
|
721
|
+
@data[:subscription].instrument,
|
722
|
+
@data[:subscription].location_code,
|
723
|
+
@data[:subscription].scan_code,
|
724
|
+
nilFilter(@data[:subscription].above_price),
|
725
|
+
nilFilter(@data[:subscription].below_price),
|
726
|
+
nilFilter(@data[:subscription].above_volume),
|
727
|
+
nilFilter(@data[:subscription].market_cap_above),
|
728
|
+
@data[:subscription].moody_rating_above,
|
729
|
+
@data[:subscription].moody_rating_below,
|
730
|
+
@data[:subscription].sp_rating_above,
|
731
|
+
@data[:subscription].sp_rating_below,
|
732
|
+
@data[:subscription].maturity_date_above,
|
733
|
+
@data[:subscription].maturity_date_below,
|
734
|
+
nilFilter(@data[:subscription].coupon_rate_above),
|
735
|
+
nilFilter(@data[:subscription].coupon_rate_below),
|
736
|
+
@data[:subscription].exclude_convertible,
|
737
|
+
(server[:version] >= 25 ? [@data[:subscription].average_option_volume_above,
|
738
|
+
@data[:subscription].scanner_setting_pairs] : []),
|
739
|
+
|
740
|
+
(server[:version] >= 27 ? [@data[:subscription].stock_type_filter] : []),
|
741
|
+
].flatten
|
742
|
+
|
743
|
+
end
|
744
|
+
end # RequestScannerSubscription
|
745
|
+
|
746
|
+
|
747
|
+
# data = { :ticker_id => int }
|
748
|
+
class CancelScannerSubscription
|
749
|
+
def self.message_id
|
750
|
+
23
|
751
|
+
end
|
752
|
+
|
753
|
+
def queue(server)
|
754
|
+
requireVersion(server, 24)
|
755
|
+
[self.class.message_id,
|
756
|
+
1, # version
|
757
|
+
@data[:ticker_id]
|
758
|
+
]
|
759
|
+
end
|
760
|
+
end # CancelScannerSubscription
|
761
|
+
|
762
|
+
|
763
|
+
class RequestScannerParameters
|
764
|
+
def self.message_id
|
765
|
+
24
|
766
|
+
end
|
767
|
+
|
768
|
+
def queue(server)
|
769
|
+
requireVersion(server, 24)
|
770
|
+
|
771
|
+
[self.class.message_id,
|
772
|
+
1 # version
|
773
|
+
]
|
774
|
+
end
|
775
|
+
end # RequestScannerParameters
|
776
|
+
|
777
|
+
|
778
|
+
# data = { :ticker_id => int }
|
779
|
+
class CancelHistoricalData
|
780
|
+
def self.message_id
|
781
|
+
25
|
782
|
+
end
|
783
|
+
|
784
|
+
def queue(server)
|
785
|
+
requireVersion(server, 24)
|
786
|
+
[self.class.message_id,
|
787
|
+
1, # version
|
788
|
+
@data[:ticker_id]
|
789
|
+
]
|
790
|
+
end
|
791
|
+
end # CancelHistoricalData
|
792
|
+
|
793
|
+
end # module OutgoingMessages
|
794
|
+
|
795
|
+
################################################################
|
796
|
+
#### end outgoing messages
|
797
|
+
################################################################
|
798
|
+
|
799
|
+
|
800
|
+
################################################################
|
801
|
+
#### Incoming messages
|
802
|
+
################################################################
|
803
|
+
|
804
|
+
module IncomingMessages
|
805
|
+
Classes = Array.new
|
806
|
+
|
807
|
+
#
|
808
|
+
# This is just a basic generic message from the server.
|
809
|
+
#
|
810
|
+
# Class variables:
|
811
|
+
# @@message_id - integer message id.
|
812
|
+
#
|
813
|
+
# Instance attributes:
|
814
|
+
# :data - Hash of actual data read from a stream.
|
815
|
+
#
|
816
|
+
# Override the load(socket) method in your subclass to do actual reading into @data.
|
817
|
+
#
|
818
|
+
class AbstractMessage < ExtremelyAbstractMessage
|
819
|
+
attr_accessor :data
|
820
|
+
|
821
|
+
def self.message_id
|
822
|
+
raise Exception("AbstractMessage.message_id called - you need to override this in a subclass.")
|
823
|
+
end
|
824
|
+
|
825
|
+
|
826
|
+
def initialize(socket, server_version)
|
827
|
+
raise Exception.new("Don't use AbstractMessage directly; use the subclass for your specific message type") if self.class.name == "AbstractMessage"
|
828
|
+
#logger.debug(" * loading #{self.class.name}")
|
829
|
+
@created_at = Time.now
|
830
|
+
|
831
|
+
@data = Hash.new
|
832
|
+
@socket = socket
|
833
|
+
@server_version = server_version
|
834
|
+
|
835
|
+
self.load()
|
836
|
+
|
837
|
+
@socket = nil
|
838
|
+
|
839
|
+
#logger.debug(" * New #{self.class.name}: #{ self.to_human }")
|
840
|
+
end
|
841
|
+
|
842
|
+
def AbstractMessage.inherited(by)
|
843
|
+
super(by)
|
844
|
+
Classes.push(by)
|
845
|
+
end
|
846
|
+
|
847
|
+
def load
|
848
|
+
raise Exception.new("Don't use AbstractMessage; override load() in a subclass.")
|
849
|
+
end
|
850
|
+
|
851
|
+
protected
|
852
|
+
|
853
|
+
#
|
854
|
+
# Load @data from the socket according to the given map.
|
855
|
+
#
|
856
|
+
# map is a series of Arrays in the format [ [ :name, :type ] ], e.g. autoload([:version, :int ], [:ticker_id, :int])
|
857
|
+
# type identifiers must have a corresponding read_type method on socket (read_int, etc.).
|
858
|
+
#
|
859
|
+
def autoload(*map)
|
860
|
+
##logger.debug("autoloading map: " + map.inspect)
|
861
|
+
map.each { |spec|
|
862
|
+
@data[spec[0]] = @socket.__send__(("read_" + spec[1].to_s).to_sym)
|
863
|
+
}
|
864
|
+
end
|
865
|
+
|
866
|
+
# version_load loads map only if @data[:version] is >= required_version.
|
867
|
+
def version_load(required_version, *map)
|
868
|
+
if @data[:version] >= required_version
|
869
|
+
map.each { |item|
|
870
|
+
autoload(item)
|
871
|
+
}
|
872
|
+
end
|
873
|
+
end
|
874
|
+
|
875
|
+
end # class AbstractMessage
|
876
|
+
|
877
|
+
|
878
|
+
### Actual message classes
|
879
|
+
|
880
|
+
# The IB code seems to dispatch up to two wrapped objects for this message, a tickPrice
|
881
|
+
# and sometimes a tickSize, which seems to be identical to the TICK_SIZE object.
|
882
|
+
#
|
883
|
+
# Important note from
|
884
|
+
# http://chuckcaplan.com/twsapi/index.php/void%20tickPrice%28%29 :
|
885
|
+
#
|
886
|
+
# "The low you get is NOT the low for the day as you'd expect it
|
887
|
+
# to be. It appears IB calculates the low based on all
|
888
|
+
# transactions after 4pm the previous day. The most inaccurate
|
889
|
+
# results occur when the stock moves up in the 4-6pm aftermarket
|
890
|
+
# on the previous day and then gaps open upward in the
|
891
|
+
# morning. The low you receive from TWS can be easily be several
|
892
|
+
# points different from the actual 9:30am-4pm low for the day in
|
893
|
+
# cases like this. If you require a correct traded low for the
|
894
|
+
# day, you can't get it from the TWS API. One possible source to
|
895
|
+
# help build the right data would be to compare against what Yahoo
|
896
|
+
# lists on finance.yahoo.com/q?s=ticker under the "Day's Range"
|
897
|
+
# statistics (be careful here, because Yahoo will use anti-Denial
|
898
|
+
# of Service techniques to hang your connection if you try to
|
899
|
+
# request too many bytes in a short period of time from them). For
|
900
|
+
# most purposes, a good enough approach would start by replacing
|
901
|
+
# the TWS low for the day with Yahoo's day low when you first
|
902
|
+
# start watching a stock ticker; let's call this time T. Then,
|
903
|
+
# update your internal low if the bid or ask tick you receive is
|
904
|
+
# lower than that for the remainder of the day. You should check
|
905
|
+
# against Yahoo again at time T+20min to handle the occasional
|
906
|
+
# case where the stock set a new low for the day in between
|
907
|
+
# T-20min (the real time your original quote was from, taking into
|
908
|
+
# account the delay) and time T. After that you should have a
|
909
|
+
# correct enough low for the rest of the day as long as you keep
|
910
|
+
# updating based on the bid/ask. It could still get slightly off
|
911
|
+
# in a case where a short transaction setting a new low appears in
|
912
|
+
# between ticks of data that TWS sends you. The high is probably
|
913
|
+
# distorted in the same way the low is, which would throw your
|
914
|
+
# results off if the stock traded after-hours and gapped down. It
|
915
|
+
# should be corrected in a similar way as described above if this
|
916
|
+
# is important to you."
|
917
|
+
#
|
918
|
+
|
919
|
+
class TickPrice < AbstractMessage
|
920
|
+
def self.message_id
|
921
|
+
1
|
922
|
+
end
|
923
|
+
|
924
|
+
def load
|
925
|
+
autoload([:version, :int], [:ticker_id, :int], [:tick_type, :int], [:price, :decimal])
|
926
|
+
|
927
|
+
version_load(2, [:size, :int])
|
928
|
+
version_load(3, [:can_auto_execute, :int])
|
929
|
+
|
930
|
+
if @data[:version] >= 2
|
931
|
+
# the IB code translates these into 0, 3, and 5, respectively, and wraps them in a TICK_SIZE-type wrapper.
|
932
|
+
@data[:type] = case @data[:tick_type]
|
933
|
+
when 1
|
934
|
+
:bid
|
935
|
+
when 2
|
936
|
+
:ask
|
937
|
+
when 4
|
938
|
+
:last
|
939
|
+
when 6
|
940
|
+
:high
|
941
|
+
when 7
|
942
|
+
:low
|
943
|
+
when 9
|
944
|
+
:close
|
945
|
+
else
|
946
|
+
nil
|
947
|
+
end
|
948
|
+
end
|
949
|
+
|
950
|
+
end
|
951
|
+
|
952
|
+
# load
|
953
|
+
|
954
|
+
def inspect
|
955
|
+
"Tick (" + @data[:type].to_s('F') + " at " + @data[:price].to_s('F') + ") " + super.inspect
|
956
|
+
end
|
957
|
+
|
958
|
+
def to_human
|
959
|
+
@data[:size].to_s + " " + @data[:type].to_s + " at " + @data[:price].to_s('F')
|
960
|
+
end
|
961
|
+
|
962
|
+
end # TickPrice
|
963
|
+
|
964
|
+
|
965
|
+
class TickSize < AbstractMessage
|
966
|
+
def self.message_id
|
967
|
+
2
|
968
|
+
end
|
969
|
+
|
970
|
+
def load
|
971
|
+
autoload([:version, :int], [:ticker_id, :int], [:tick_type, :int], [:size, :int])
|
972
|
+
@data[:type] = case @data[:tick_type]
|
973
|
+
when 0
|
974
|
+
:bid
|
975
|
+
when 3
|
976
|
+
:ask
|
977
|
+
when 5
|
978
|
+
:last
|
979
|
+
when 8
|
980
|
+
:volume
|
981
|
+
else
|
982
|
+
nil
|
983
|
+
end
|
984
|
+
end
|
985
|
+
|
986
|
+
def to_human
|
987
|
+
@data[:type].to_s + " size: " + @data[:size].to_s
|
988
|
+
end
|
989
|
+
end # TickSize
|
990
|
+
|
991
|
+
|
992
|
+
class OrderStatus < AbstractMessage
|
993
|
+
def self.message_id
|
994
|
+
3
|
995
|
+
end
|
996
|
+
|
997
|
+
def load
|
998
|
+
autoload([:version, :int], [:id, :int], [:status, :string], [:filled, :int], [:remaining, :int],
|
999
|
+
[:average_fill_price, :decimal])
|
1000
|
+
|
1001
|
+
version_load(2, [:perm_id, :int])
|
1002
|
+
version_load(3, [:parent_id, :int])
|
1003
|
+
version_load(4, [:last_fill_price, :decimal])
|
1004
|
+
version_load(5, [:client_id, :int])
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
|
1009
|
+
class Error < AbstractMessage
|
1010
|
+
def self.message_id
|
1011
|
+
4
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
def code
|
1015
|
+
@data && @data[:code]
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
def load
|
1019
|
+
@data[:version] = @socket.read_int
|
1020
|
+
|
1021
|
+
if @data[:version] < 2
|
1022
|
+
@data[:message] = @socket.read_string
|
1023
|
+
else
|
1024
|
+
autoload([:id, :int], [:code, :int], [:message, :string])
|
1025
|
+
end
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
def to_human
|
1029
|
+
"TWS #{@data[:code]}: #{@data[:message]}"
|
1030
|
+
end
|
1031
|
+
end # class ErrorMessage
|
1032
|
+
|
1033
|
+
class OpenOrder < AbstractMessage
|
1034
|
+
attr_accessor :order, :contract
|
1035
|
+
|
1036
|
+
def self.message_id
|
1037
|
+
5
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def load
|
1041
|
+
@order = Datatypes::Order.new
|
1042
|
+
@contract = Datatypes::Contract.new
|
1043
|
+
|
1044
|
+
autoload([:version, :int])
|
1045
|
+
|
1046
|
+
@order.id = @socket.read_int
|
1047
|
+
|
1048
|
+
@contract.symbol = @socket.read_string
|
1049
|
+
@contract.sec_type = @socket.read_string
|
1050
|
+
@contract.expiry = @socket.read_string
|
1051
|
+
@contract.strike = @socket.read_decimal
|
1052
|
+
@contract.right = @socket.read_string
|
1053
|
+
@contract.exchange = @socket.read_string
|
1054
|
+
@contract.currency = @socket.read_string
|
1055
|
+
|
1056
|
+
@contract.local_symbol = @socket.read_string if @data[:version] >= 2
|
1057
|
+
|
1058
|
+
@order.action = @socket.read_string
|
1059
|
+
@order.total_quantity = @socket.read_int
|
1060
|
+
@order.order_type = @socket.read_string
|
1061
|
+
@order.limit_price = @socket.read_decimal
|
1062
|
+
@order.aux_price = @socket.read_decimal
|
1063
|
+
@order.tif = @socket.read_string
|
1064
|
+
@order.oca_group = @socket.read_string
|
1065
|
+
@order.account = @socket.read_string
|
1066
|
+
@order.open_close = @socket.read_string
|
1067
|
+
@order.origin = @socket.read_int
|
1068
|
+
@order.order_ref = @socket.read_string
|
1069
|
+
|
1070
|
+
@order.client_id = @socket.read_int if @data[:version] >= 3
|
1071
|
+
|
1072
|
+
if @data[:version] >= 4
|
1073
|
+
@order.perm_id = @socket.read_int
|
1074
|
+
@order.outside_rth = (@socket.read_int == 1)
|
1075
|
+
@order.hidden = (@socket.read_int == 1)
|
1076
|
+
@order.discretionary_amount = @socket.read_decimal
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
@order.good_after_time = @socket.read_string if @data[:version] >= 5
|
1080
|
+
@order.shares_allocation = @socket.read_string if @data[:version] >= 6
|
1081
|
+
|
1082
|
+
if @data[:version] >= 7
|
1083
|
+
@order.fa_group = @socket.read_string
|
1084
|
+
@order.fa_method = @socket.read_string
|
1085
|
+
@order.fa_percentage = @socket.read_string
|
1086
|
+
@order.fa_profile = @socket.read_string
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
@order.good_till_date = @socket.read_string if @data[:version] >= 8
|
1090
|
+
|
1091
|
+
if @data[:version] >= 9
|
1092
|
+
@order.rule_80A = @socket.read_string
|
1093
|
+
@order.percent_offset = @socket.read_decimal
|
1094
|
+
@order.settling_firm = @socket.read_string
|
1095
|
+
@order.short_sale_slot = @socket.read_int
|
1096
|
+
@order.designated_location = @socket.read_string
|
1097
|
+
@order.auction_strategy = @socket.read_int
|
1098
|
+
@order.starting_price = @socket.read_decimal
|
1099
|
+
@order.stock_ref_price = @socket.read_decimal
|
1100
|
+
@order.delta = @socket.read_decimal
|
1101
|
+
@order.stock_range_lower = @socket.read_decimal
|
1102
|
+
@order.stock_range_upper = @socket.read_decimal
|
1103
|
+
@order.display_size = @socket.read_int
|
1104
|
+
@order.rth_only = @socket.read_boolean
|
1105
|
+
@order.block_order = @socket.read_boolean
|
1106
|
+
@order.sweep_to_fill = @socket.read_boolean
|
1107
|
+
@order.all_or_none = @socket.read_boolean
|
1108
|
+
@order.min_quantity = @socket.read_int
|
1109
|
+
@order.oca_type = @socket.read_int
|
1110
|
+
@order.eTrade_only = @socket.read_boolean
|
1111
|
+
@order.firm_quote_only = @socket.read_boolean
|
1112
|
+
@order.nbbo_price_cap = @socket.read_decimal
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
if @data[:version] >= 10
|
1116
|
+
@order.parent_id = @socket.read_int
|
1117
|
+
@order.trigger_method = @socket.read_int
|
1118
|
+
end
|
1119
|
+
|
1120
|
+
if @data[:version] >= 11
|
1121
|
+
@order.volatility = @socket.read_decimal
|
1122
|
+
@order.volatility_type = @socket.read_int
|
1123
|
+
|
1124
|
+
if @data[:version] == 11
|
1125
|
+
@order.delta_neutral_order_type = (@socket.read_int == 0 ? "NONE" : "MKT")
|
1126
|
+
else
|
1127
|
+
@order.delta_neutral_order_type = @socket.read_string
|
1128
|
+
@order.delta_neutral_aux_price = @socket.read_decimal
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
@order.continuous_update = @socket.read_int
|
1132
|
+
if @server_version == 26
|
1133
|
+
@order.stock_range_lower = @socket.read_decimal
|
1134
|
+
@order.stock_range_upper = @socket.read_decimal
|
1135
|
+
end
|
1136
|
+
|
1137
|
+
@order.reference_price_type = @socket.read_int
|
1138
|
+
end # if version >= 11
|
1139
|
+
|
1140
|
+
|
1141
|
+
end # load
|
1142
|
+
end # OpenOrder
|
1143
|
+
|
1144
|
+
class AccountValue < AbstractMessage
|
1145
|
+
def self.message_id
|
1146
|
+
6
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
def load
|
1150
|
+
autoload([:version, :int], [:key, :string], [:value, :string], [:currency, :string])
|
1151
|
+
version_load(2, [:account_name, :string])
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
def to_human
|
1155
|
+
"<AccountValue: acct ##{@data[:account_name]}; #{@data[:key]}=#{@data[:value]} (#{@data[:currency]})>"
|
1156
|
+
end
|
1157
|
+
end # AccountValue
|
1158
|
+
|
1159
|
+
class PortfolioValue < AbstractMessage
|
1160
|
+
attr_accessor :contract
|
1161
|
+
|
1162
|
+
def self.message_id
|
1163
|
+
7
|
1164
|
+
end
|
1165
|
+
|
1166
|
+
def load
|
1167
|
+
@contract = Datatypes::Contract.new
|
1168
|
+
|
1169
|
+
autoload([:version, :int])
|
1170
|
+
@contract.symbol = @socket.read_string
|
1171
|
+
@contract.sec_type = @socket.read_string
|
1172
|
+
@contract.expiry = @socket.read_string
|
1173
|
+
@contract.strike = @socket.read_decimal
|
1174
|
+
@contract.right = @socket.read_string
|
1175
|
+
@contract.currency = @socket.read_string
|
1176
|
+
@contract.local_symbol = @socket.read_string if @data[:version] >= 2
|
1177
|
+
|
1178
|
+
autoload([:position, :int], [:market_price, :decimal], [:market_value, :decimal])
|
1179
|
+
version_load(3, [:average_cost, :decimal], [:unrealized_pnl, :decimal], [:realized_pnl, :decimal])
|
1180
|
+
version_load(4, [:account_name, :string])
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
def to_human
|
1184
|
+
"<PortfolioValue: update for #{@contract.to_human}: market price #{@data[:market_price].to_s('F')}; market value " +
|
1185
|
+
"#{@data[:market_value].to_s('F')}; position #{@data[:position]}; unrealized PnL #{@data[:unrealized_pnl].to_s('F')}; " +
|
1186
|
+
"realized PnL #{@data[:realized_pnl].to_s('F')}; account #{@data[:account_name]}>"
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
end # PortfolioValue
|
1190
|
+
|
1191
|
+
class AccountUpdateTime < AbstractMessage
|
1192
|
+
def self.message_id
|
1193
|
+
8
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
def load
|
1197
|
+
autoload([:version, :int], [:time_stamp, :string])
|
1198
|
+
end
|
1199
|
+
end # AccountUpdateTime
|
1200
|
+
|
1201
|
+
|
1202
|
+
#
|
1203
|
+
# This message is always sent by TWS automatically at connect.
|
1204
|
+
# The IB class subscribes to it automatically and stores the order id in
|
1205
|
+
# its :next_order_id attribute.
|
1206
|
+
#
|
1207
|
+
class NextValidID < AbstractMessage
|
1208
|
+
def self.message_id
|
1209
|
+
9
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
def load
|
1213
|
+
autoload([:version, :int], [:order_id, :int])
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
end # NextValidIDMessage
|
1217
|
+
|
1218
|
+
|
1219
|
+
class ContractData < AbstractMessage
|
1220
|
+
attr_accessor :contract_details
|
1221
|
+
|
1222
|
+
def self.message_id
|
1223
|
+
10
|
1224
|
+
end
|
1225
|
+
|
1226
|
+
def load
|
1227
|
+
@contract_details = Datatypes::ContractDetails.new
|
1228
|
+
|
1229
|
+
autoload([:version, :int])
|
1230
|
+
|
1231
|
+
@contract_details.summary.symbol = @socket.read_string
|
1232
|
+
@contract_details.summary.sec_type = @socket.read_string
|
1233
|
+
@contract_details.summary.expiry = @socket.read_string
|
1234
|
+
@contract_details.summary.strike = @socket.read_decimal
|
1235
|
+
@contract_details.summary.right = @socket.read_string
|
1236
|
+
@contract_details.summary.exchange = @socket.read_string
|
1237
|
+
@contract_details.summary.currency = @socket.read_string
|
1238
|
+
@contract_details.summary.local_symbol = @socket.read_string
|
1239
|
+
|
1240
|
+
@contract_details.market_name = @socket.read_string
|
1241
|
+
@contract_details.trading_class = @socket.read_string
|
1242
|
+
@contract_details.con_id = @socket.read_int
|
1243
|
+
@contract_details.min_tick = @socket.read_decimal
|
1244
|
+
@contract_details.multiplier = @socket.read_string
|
1245
|
+
@contract_details.order_types = @socket.read_string
|
1246
|
+
@contract_details.valid_exchanges = @socket.read_string
|
1247
|
+
@contract_details.price_magnifier = @socket.read_int if @data[:version] >= 2
|
1248
|
+
|
1249
|
+
end
|
1250
|
+
end # ContractData
|
1251
|
+
|
1252
|
+
|
1253
|
+
class ExecutionData < AbstractMessage
|
1254
|
+
attr_accessor :contract, :execution
|
1255
|
+
|
1256
|
+
def self.message_id
|
1257
|
+
11
|
1258
|
+
end
|
1259
|
+
|
1260
|
+
def load
|
1261
|
+
@contract = Datatypes::Contract.new
|
1262
|
+
@execution = Datatypes::Execution.new
|
1263
|
+
|
1264
|
+
autoload([:version, :int], [:order_id, :int])
|
1265
|
+
|
1266
|
+
@contract.symbol = @socket.read_string
|
1267
|
+
@contract.sec_type = @socket.read_string
|
1268
|
+
@contract.expiry = @socket.read_string
|
1269
|
+
@contract.strike = @socket.read_decimal
|
1270
|
+
@contract.right = @socket.read_string
|
1271
|
+
@contract.currency = @socket.read_string
|
1272
|
+
@contract.local_symbol = @socket.read_string if @data[:version] >= 2
|
1273
|
+
|
1274
|
+
@execution.order_id = @data[:order_id]
|
1275
|
+
@execution.exec_id = @socket.read_string
|
1276
|
+
@execution.time = @socket.read_string
|
1277
|
+
@execution.account_number = @socket.read_string
|
1278
|
+
@execution.exchange = @socket.read_string
|
1279
|
+
@execution.side = @socket.read_string
|
1280
|
+
@execution.shares = @socket.read_int
|
1281
|
+
@execution.price = @socket.read_decimal
|
1282
|
+
|
1283
|
+
@execution.perm_id = @socket.read_int if @data[:version] >= 2
|
1284
|
+
@execution.client_id = @socket.read_int if @data[:version] >= 3
|
1285
|
+
@execution.liquidation = @socket.read_int if @data[:version] >= 4
|
1286
|
+
end
|
1287
|
+
end # ExecutionData
|
1288
|
+
|
1289
|
+
class MarketDepth < AbstractMessage
|
1290
|
+
def self.message_id
|
1291
|
+
12
|
1292
|
+
end
|
1293
|
+
|
1294
|
+
def load
|
1295
|
+
autoload([:version, :int], [:id, :int], [:position, :int], [:operation, :int], [:side, :int], [:price, :decimal], [:size, :int])
|
1296
|
+
end
|
1297
|
+
end # MarketDepth
|
1298
|
+
|
1299
|
+
class MarketDepthL2 < AbstractMessage
|
1300
|
+
def self.message_id
|
1301
|
+
13
|
1302
|
+
end
|
1303
|
+
|
1304
|
+
def load
|
1305
|
+
autoload([:version, :int], [:id, :int], [:position, :int], [:market_maker, :string], [:operation, :int], [:side, :int],
|
1306
|
+
[:price, :decimal], [:size, :int])
|
1307
|
+
end
|
1308
|
+
end # MarketDepthL2
|
1309
|
+
|
1310
|
+
|
1311
|
+
class NewsBulletins < AbstractMessage
|
1312
|
+
def self.message_id
|
1313
|
+
14
|
1314
|
+
end
|
1315
|
+
|
1316
|
+
def load
|
1317
|
+
autoload([:version, :int], [:news_message_id, :int], [:news_message_type, :int], [:news_message, :string], [:originating_exchange, :string])
|
1318
|
+
end
|
1319
|
+
end # NewsBulletins
|
1320
|
+
|
1321
|
+
class ManagedAccounts < AbstractMessage
|
1322
|
+
def self.message_id
|
1323
|
+
15
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
def load
|
1327
|
+
autoload([:version, :int], [:accounts_list, :string])
|
1328
|
+
end
|
1329
|
+
end # ManagedAccounts
|
1330
|
+
|
1331
|
+
# "Fa"?
|
1332
|
+
class ReceiveFa < AbstractMessage
|
1333
|
+
def self.message_id
|
1334
|
+
16
|
1335
|
+
end
|
1336
|
+
|
1337
|
+
def load
|
1338
|
+
autoload([:version, :int], [:fa_data_type, :int], [:xml, :string])
|
1339
|
+
end
|
1340
|
+
end # ReceiveFa
|
1341
|
+
|
1342
|
+
class HistoricalData < AbstractMessage
|
1343
|
+
def self.message_id
|
1344
|
+
17
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
def load
|
1348
|
+
autoload([:version, :int], [:req_id, :int])
|
1349
|
+
version_load(2, [:start_date_str, :string], [:end_date_str, :string])
|
1350
|
+
@data[:completed_indicator] = "finished-" + @data[:start_date_str] + "-" + @data[:end_date_str] if @data[:version] >= 2
|
1351
|
+
|
1352
|
+
autoload([:item_count, :int])
|
1353
|
+
@data[:history] = Array.new(@data[:item_count]) { |index|
|
1354
|
+
attrs = {
|
1355
|
+
:date => @socket.read_string,
|
1356
|
+
:open => @socket.read_decimal,
|
1357
|
+
:high => @socket.read_decimal,
|
1358
|
+
:low => @socket.read_decimal,
|
1359
|
+
:close => @socket.read_decimal,
|
1360
|
+
:volume => @socket.read_int,
|
1361
|
+
:wap => @socket.read_decimal,
|
1362
|
+
:has_gaps => @socket.read_string
|
1363
|
+
}
|
1364
|
+
|
1365
|
+
Datatypes::Bar.new(attrs)
|
1366
|
+
}
|
1367
|
+
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
def to_human
|
1371
|
+
"<HistoricalData: req id #{@data[:req_id]}, #{@data[:item_count]} items, from #{@data[:start_date_str]} to #{@data[:end_date_str]}>"
|
1372
|
+
end
|
1373
|
+
end # HistoricalData
|
1374
|
+
|
1375
|
+
class BondContractData < AbstractMessage
|
1376
|
+
attr_accessor :contract_details
|
1377
|
+
|
1378
|
+
def self.message_id
|
1379
|
+
18
|
1380
|
+
end
|
1381
|
+
|
1382
|
+
def load
|
1383
|
+
@contract_details = Datatypes::ContractDetails.new
|
1384
|
+
@contract_details.summary.symbol = @socket.read_string
|
1385
|
+
@contract_details.summary.sec_type = @socket.read_string
|
1386
|
+
@contract_details.summary.cusip = @socket.read_string
|
1387
|
+
@contract_details.summary.coupon = @socket.read_decimal
|
1388
|
+
@contract_details.summary.maturity = @socket.read_string
|
1389
|
+
@contract_details.summary.issue_date = @socket.read_string
|
1390
|
+
@contract_details.summary.ratings = @socket.read_string
|
1391
|
+
@contract_details.summary.bond_type = @socket.read_string
|
1392
|
+
@contract_details.summary.coupon_type = @socket.read_string
|
1393
|
+
@contract_details.summary.convertible = @socket.read_boolean
|
1394
|
+
@contract_details.summary.callable = @socket.read_boolean
|
1395
|
+
@contract_details.summary.puttable = @socket.read_boolean
|
1396
|
+
@contract_details.summary.desc_append = @socket.read_string
|
1397
|
+
@contract_details.summary.exchange = @socket.read_string
|
1398
|
+
@contract_details.summary.currency = @socket.read_string
|
1399
|
+
@contract_details.market_name = @socket.read_string
|
1400
|
+
@contract_details.trading_class = @socket.read_string
|
1401
|
+
@contract_details.con_id = @socket.read_int
|
1402
|
+
@contract_details.min_tick = @socket.read_decimal
|
1403
|
+
@contract_details.order_types = @socket.read_string
|
1404
|
+
@contract_details.valid_exchanges = @socket.read_string
|
1405
|
+
|
1406
|
+
end
|
1407
|
+
end # BondContractData
|
1408
|
+
|
1409
|
+
class ScannerParameters < AbstractMessage
|
1410
|
+
def self.message_id
|
1411
|
+
19
|
1412
|
+
end
|
1413
|
+
|
1414
|
+
def load
|
1415
|
+
autoload([:version, :int], [:xml, :string])
|
1416
|
+
end
|
1417
|
+
end # ScannerParamters
|
1418
|
+
|
1419
|
+
|
1420
|
+
class ScannerData < AbstractMessage
|
1421
|
+
attr_accessor :contract_details
|
1422
|
+
|
1423
|
+
def self.message_id
|
1424
|
+
20
|
1425
|
+
end
|
1426
|
+
|
1427
|
+
def load
|
1428
|
+
autoload([:version, :int], [:ticker_id, :int], [:number_of_elements, :int])
|
1429
|
+
@data[:results] = Array.new(@data[:number_of_elements]) { |index|
|
1430
|
+
{
|
1431
|
+
:rank => @socket.read_int
|
1432
|
+
## TODO: Pick up here.
|
1433
|
+
}
|
1434
|
+
}
|
1435
|
+
|
1436
|
+
end
|
1437
|
+
end # ScannerData
|
1438
|
+
|
1439
|
+
###########################################
|
1440
|
+
###########################################
|
1441
|
+
## End message classes
|
1442
|
+
###########################################
|
1443
|
+
###########################################
|
1444
|
+
|
1445
|
+
Table = Hash.new
|
1446
|
+
Classes.each { |msg_class|
|
1447
|
+
Table[msg_class.message_id] = msg_class
|
1448
|
+
}
|
1449
|
+
|
1450
|
+
#logger.debug("Incoming message class table is #{Table.inspect}")
|
1451
|
+
|
1452
|
+
end # module IncomingMessages
|
1453
|
+
################################################################
|
1454
|
+
#### End incoming messages
|
1455
|
+
################################################################
|
1456
|
+
|
1457
|
+
|
1458
|
+
end # module IB
|