ib-ruby 0.4.3

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