ib-ruby 0.4.3 → 0.4.20
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/ib-ruby/datatypes.rb
DELETED
@@ -1,400 +0,0 @@
|
|
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
|
-
#
|
21
|
-
# TODO: Implement equals() according to the criteria in IB's Java client.
|
22
|
-
#
|
23
|
-
|
24
|
-
module IB
|
25
|
-
|
26
|
-
module Datatypes
|
27
|
-
attr_reader :created_at
|
28
|
-
|
29
|
-
class AbstractDatum
|
30
|
-
def init
|
31
|
-
@created_at = Time.now
|
32
|
-
end
|
33
|
-
|
34
|
-
# If a hash is given, keys are taken as attribute names, values as data.
|
35
|
-
# The attrs of the instance are set automatically from the attributeHash.
|
36
|
-
#
|
37
|
-
# If no hash is given, #init is called in the instance. #init
|
38
|
-
# should set the datum up in a generic state.
|
39
|
-
#
|
40
|
-
def initialize(attributeHash=nil)
|
41
|
-
if attributeHash.nil?
|
42
|
-
init
|
43
|
-
else
|
44
|
-
raise(ArgumentError.new("Argument must be a Hash")) unless attributeHash.is_a?(Hash)
|
45
|
-
attributeHash.keys.each {|key|
|
46
|
-
self.send((key.to_s + "=").to_sym, attributeHash[key])
|
47
|
-
}
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end # AbstractDatum
|
51
|
-
|
52
|
-
|
53
|
-
# This is used within HistoricData messages.
|
54
|
-
# Instantiate with a Hash of attributes, to be auto-set via initialize in AbstractDatum.
|
55
|
-
class Bar < AbstractDatum
|
56
|
-
attr_accessor :date, :open, :high, :low, :close, :volume, :wap, :has_gaps
|
57
|
-
|
58
|
-
def to_s
|
59
|
-
"<Bar: #{@date}; OHLC: #{@open.to_s}, #{@high.to_s}, #{@low.to_s}, #{@close.to_s}; volume: #{@volume}; wap: #{@wap.to_s}; has_gaps: #{@has_gaps}>"
|
60
|
-
end
|
61
|
-
|
62
|
-
end # Bar
|
63
|
-
|
64
|
-
|
65
|
-
class Order < AbstractDatum
|
66
|
-
# Constants used in Order objects. Drawn from Order.java
|
67
|
-
Origin_Customer = 0
|
68
|
-
Origin_Firm = 1
|
69
|
-
|
70
|
-
Opt_Unknown = '?'
|
71
|
-
Opt_Broker_Dealer = 'b'
|
72
|
-
Opt_Customer = 'c'
|
73
|
-
Opt_Firm = 'f'
|
74
|
-
Opt_Isemm = 'm'
|
75
|
-
Opt_Farmm = 'n'
|
76
|
-
Opt_Specialist = 'y'
|
77
|
-
|
78
|
-
# Main order fields
|
79
|
-
attr_accessor(:id, :client_id, :perm_id, :action, :total_quantity, :order_type, :limit_price,
|
80
|
-
:aux_price, :shares_allocation)
|
81
|
-
|
82
|
-
# Extended order fields
|
83
|
-
attr_accessor(:tif, :oca_group, :account, :open_close, :origin, :order_ref,
|
84
|
-
:transmit, # if false, order will be created but not transmitted.
|
85
|
-
:parent_id, # Parent order id, to associate auto STP or TRAIL orders with the original order.
|
86
|
-
:block_order,
|
87
|
-
:sweep_to_fill,
|
88
|
-
:display_size,
|
89
|
-
:trigger_method,
|
90
|
-
:ignore_rth,
|
91
|
-
:hidden,
|
92
|
-
:discretionary_amount,
|
93
|
-
:good_after_time,
|
94
|
-
:good_till_date)
|
95
|
-
|
96
|
-
OCA_Cancel_with_block = 1
|
97
|
-
OCA_Reduce_with_block = 2
|
98
|
-
OCA_Reduce_non_block = 3
|
99
|
-
|
100
|
-
# No idea what the fa_* attributes are for, nor many of the others.
|
101
|
-
attr_accessor(:fa_group, :fa_profile, :fa_method, :fa_profile, :fa_method, :fa_percentage, :primary_exchange,
|
102
|
-
:short_sale_slot, # 1 or 2, says Order.java. (No idea what the difference is.)
|
103
|
-
:designated_location, # "when slot=2 only"
|
104
|
-
:oca_type, # 1 = CANCEL_WITH_BLOCK, 2 = REDUCE_WITH_BLOCK, 3 = REDUCE_NON_BLOCK
|
105
|
-
:rth_only, :override_percentage_constraints, :rule_80a, :settling_firm, :all_or_none,
|
106
|
-
:min_quantity, :percent_offset, :etrade_only, :firm_quote_only, :nbbo_price_cap)
|
107
|
-
|
108
|
-
# Box orders only:
|
109
|
-
Box_Auction_Match = 1
|
110
|
-
Box_Auction_Improvement = 2
|
111
|
-
Box_Auction_Transparent = 3
|
112
|
-
attr_accessor(:auction_strategy, # Box_* constants above
|
113
|
-
:starting_price, :stock_ref_price, :delta, :stock_range_lower, :stock_range_upper)
|
114
|
-
|
115
|
-
# Volatility orders only:
|
116
|
-
Volatility_Type_Daily = 1
|
117
|
-
Volatility_Type_Annual = 2
|
118
|
-
|
119
|
-
Volatility_Ref_Price_Average = 1
|
120
|
-
Volatility_Ref_Price_BidOrAsk = 2
|
121
|
-
|
122
|
-
attr_accessor(:volatility,
|
123
|
-
:volatility_type, # 1 = daily, 2 = annual, as above
|
124
|
-
:continuous_update,
|
125
|
-
:reference_price_type, # 1 = average, 2 = BidOrAsk
|
126
|
-
:delta_neutral_order_type,
|
127
|
-
:delta_neutral_aux_price)
|
128
|
-
|
129
|
-
Max_value = 99999999 # I don't know why IB uses a very large number as the default for certain fields
|
130
|
-
def init
|
131
|
-
super
|
132
|
-
|
133
|
-
@open_close = "0"
|
134
|
-
@origin = Origin_Customer
|
135
|
-
@transmit = true
|
136
|
-
@primary_exchange = ''
|
137
|
-
@designated_location = ''
|
138
|
-
@min_quantity = Max_value
|
139
|
-
@percent_offset = Max_value
|
140
|
-
@nbba_price_cap = Max_value
|
141
|
-
@starting_price = Max_value
|
142
|
-
@stock_ref_price = Max_value
|
143
|
-
@delta = Max_value
|
144
|
-
@delta_neutral_order_type = ''
|
145
|
-
@delta_neutral_aux_price = Max_value
|
146
|
-
@reference_price_type = Max_value
|
147
|
-
end # init
|
148
|
-
|
149
|
-
end # class Order
|
150
|
-
|
151
|
-
|
152
|
-
class Contract < AbstractDatum
|
153
|
-
|
154
|
-
# Valid security types (sec_type attribute)
|
155
|
-
SECURITY_TYPES =
|
156
|
-
{
|
157
|
-
:stock => "STK",
|
158
|
-
:option => "OPT",
|
159
|
-
:future => "FUT",
|
160
|
-
:index => "IND",
|
161
|
-
:futures_option => "FOP",
|
162
|
-
:forex => "CASH",
|
163
|
-
:bag => "BAG"
|
164
|
-
}
|
165
|
-
|
166
|
-
# note that the :description field is entirely local to ib-ruby, and not part of TWS.
|
167
|
-
# You can use it to store whatever arbitrary data you want.
|
168
|
-
|
169
|
-
attr_accessor(:symbol, :strike, :multiplier, :exchange, :currency,
|
170
|
-
:local_symbol, :combo_legs, :description)
|
171
|
-
|
172
|
-
# Bond values
|
173
|
-
attr_accessor(:cusip, :ratings, :desc_append, :bond_type, :coupon_type, :callable, :puttable,
|
174
|
-
:coupon, :convertible, :maturity, :issue_date)
|
175
|
-
|
176
|
-
attr_reader :sec_type, :expiry, :right, :primary_exchange
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
# some protective filters
|
181
|
-
|
182
|
-
def primary_exchange=(x)
|
183
|
-
x.upcase! if x.is_a?(String)
|
184
|
-
|
185
|
-
# per http://chuckcaplan.com/twsapi/index.php/Class%20Contract
|
186
|
-
raise(ArgumentError.new("Don't set primary_exchange to smart")) if x == "SMART"
|
187
|
-
|
188
|
-
@primary_exchange = x
|
189
|
-
end
|
190
|
-
|
191
|
-
def right=(x)
|
192
|
-
x.upcase! if x.is_a?(String)
|
193
|
-
x = nil if !x.nil? && x.empty?
|
194
|
-
raise(ArgumentError.new("Invalid right \"#{x}\" (must be one of PUT, CALL, P, C)")) unless x.nil? || [ "PUT", "CALL", "P", "C", "0"].include?(x)
|
195
|
-
@right = x
|
196
|
-
end
|
197
|
-
|
198
|
-
def expiry=(x)
|
199
|
-
x = x.to_s
|
200
|
-
if (x.nil? || ! (x =~ /\d{6,8}/)) and !x.empty? then
|
201
|
-
raise ArgumentError.new("Invalid expiry \"#{x}\" (must be in format YYYYMM or YYYYMMDD)")
|
202
|
-
end
|
203
|
-
@expiry = x
|
204
|
-
end
|
205
|
-
|
206
|
-
def sec_type=(x)
|
207
|
-
x = nil if !x.nil? && x.empty?
|
208
|
-
raise(ArgumentError.new("Invalid security type \"#{x}\" (see SECURITY_TYPES constant in Contract class for valid types)")) unless x.nil? || SECURITY_TYPES.values.include?(x)
|
209
|
-
@sec_type = x
|
210
|
-
end
|
211
|
-
|
212
|
-
def reset
|
213
|
-
@combo_legs = Array.new
|
214
|
-
@strike = 0
|
215
|
-
end
|
216
|
-
|
217
|
-
# Different messages serialize contracts differently. Go figure.
|
218
|
-
def serialize_short(version)
|
219
|
-
q = [ self.symbol,
|
220
|
-
self.sec_type,
|
221
|
-
self.expiry,
|
222
|
-
self.strike,
|
223
|
-
self.right ]
|
224
|
-
|
225
|
-
q.push(self.multiplier) if version >= 15
|
226
|
-
q.concat([
|
227
|
-
self.exchange,
|
228
|
-
self.currency,
|
229
|
-
self.local_symbol
|
230
|
-
])
|
231
|
-
|
232
|
-
q
|
233
|
-
end # serialize
|
234
|
-
|
235
|
-
# This returns an Array of data from the given contract, in standard format.
|
236
|
-
# Note that it does not include the combo legs.
|
237
|
-
def serialize_long(version)
|
238
|
-
queue = [
|
239
|
-
self.symbol,
|
240
|
-
self.sec_type,
|
241
|
-
self.expiry,
|
242
|
-
self.strike,
|
243
|
-
self.right
|
244
|
-
]
|
245
|
-
|
246
|
-
queue.push(self.multiplier) if version >= 15
|
247
|
-
queue.push(self.exchange)
|
248
|
-
queue.push(self.primary_exchange) if version >= 14
|
249
|
-
queue.push(self.currency)
|
250
|
-
queue.push(self.local_symbol) if version >= 2
|
251
|
-
|
252
|
-
queue
|
253
|
-
end # serialize_long
|
254
|
-
|
255
|
-
#
|
256
|
-
# This produces a string uniquely identifying this contract, in the format used
|
257
|
-
# for command line arguments in the IB-Ruby examples. The format is:
|
258
|
-
#
|
259
|
-
# symbol:security_type:expiry:strike:right:multiplier:exchange:primary_exchange:currency:local_symbol
|
260
|
-
#
|
261
|
-
# Fields not needed for a particular security should be left blank (e.g. strike and right are only relevant for options.)
|
262
|
-
#
|
263
|
-
# For example, to query the British pound futures contract trading on Globex expiring in September, 2008,
|
264
|
-
# the string is:
|
265
|
-
#
|
266
|
-
# GBP:FUT:200809:::62500:GLOBEX::USD:
|
267
|
-
#
|
268
|
-
|
269
|
-
def serialize_ib_ruby(version)
|
270
|
-
serialize_long(version).join(":")
|
271
|
-
end
|
272
|
-
|
273
|
-
# This returns a Contract initialized from the serialize_ib_ruby format string.
|
274
|
-
def self.from_ib_ruby(string)
|
275
|
-
c = Contract.new
|
276
|
-
c.symbol, c.sec_type, c.expiry, c.strike, c.right, c.multiplier, c.exchange, c.primary_exchange, c.currency, c.local_symbol = string.split(":")
|
277
|
-
|
278
|
-
c
|
279
|
-
end
|
280
|
-
|
281
|
-
# Some messages send open_close too, some don't. WTF.
|
282
|
-
def serialize_combo_legs(include_open_close = false)
|
283
|
-
if self.combo_legs.nil?
|
284
|
-
[0]
|
285
|
-
else
|
286
|
-
[ self.combo_legs.size ].concat(self.combo_legs.serialize(include_open_close))
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
def init
|
291
|
-
super
|
292
|
-
|
293
|
-
@combo_legs = Array.new
|
294
|
-
@strike = 0
|
295
|
-
@sec_type = ''
|
296
|
-
end
|
297
|
-
|
298
|
-
def to_human
|
299
|
-
"<IB-Contract: " + [symbol, expiry, sec_type, strike, right, exchange, currency].join("-") + "}>"
|
300
|
-
end
|
301
|
-
|
302
|
-
def to_short
|
303
|
-
"#{symbol}#{expiry}#{strike}#{right}#{exchange}#{currency}"
|
304
|
-
end
|
305
|
-
|
306
|
-
def to_s
|
307
|
-
to_human
|
308
|
-
end
|
309
|
-
|
310
|
-
end # class Contract
|
311
|
-
|
312
|
-
|
313
|
-
class ContractDetails < AbstractDatum
|
314
|
-
attr_accessor :summary, :market_name, :trading_class, :con_id, :min_tick, :multiplier, :price_magnifier, :order_types, :valid_exchanges
|
315
|
-
|
316
|
-
def init
|
317
|
-
super
|
318
|
-
|
319
|
-
@summary = Contract.new
|
320
|
-
@con_id = 0
|
321
|
-
@min_tick = 0
|
322
|
-
end
|
323
|
-
end # class ContractDetails
|
324
|
-
|
325
|
-
|
326
|
-
class Execution < AbstractDatum
|
327
|
-
attr_accessor :order_id, :client_id, :exec_id, :time, :account_number, :exchange, :side, :shares, :price, :perm_id, :liquidation
|
328
|
-
|
329
|
-
def init
|
330
|
-
super
|
331
|
-
|
332
|
-
@order_id = 0
|
333
|
-
@client_id = 0
|
334
|
-
@shares = 0
|
335
|
-
@price = 0
|
336
|
-
@perm_id = 0
|
337
|
-
@liquidation =0
|
338
|
-
end
|
339
|
-
end # Execution
|
340
|
-
|
341
|
-
# EClientSocket.java tells us: 'Note that the valid format for m_time is "yyyymmdd-hh:mm:ss"'
|
342
|
-
class ExecutionFilter < AbstractDatum
|
343
|
-
attr_accessor :client_id, :acct_code, :time, :symbol, :sec_type, :exchange, :side
|
344
|
-
|
345
|
-
def init
|
346
|
-
super
|
347
|
-
|
348
|
-
@client_id = 0
|
349
|
-
end
|
350
|
-
|
351
|
-
end # ExecutionFilter
|
352
|
-
|
353
|
-
|
354
|
-
class ComboLeg < AbstractDatum
|
355
|
-
attr_accessor :con_id, :ratio, :action, :exchange, :open_close
|
356
|
-
|
357
|
-
def init
|
358
|
-
super
|
359
|
-
|
360
|
-
@con_id = 0
|
361
|
-
@ratio = 0
|
362
|
-
@open_close = 0
|
363
|
-
end
|
364
|
-
|
365
|
-
# Some messages include open_close, some don't. wtf.
|
366
|
-
def serialize(include_open_close = false)
|
367
|
-
self.collect { |leg|
|
368
|
-
[ leg.con_id, leg.ratio, leg.action, leg.exchange, (include_open_close ? leg.open_close : [] )]
|
369
|
-
}.flatten
|
370
|
-
end
|
371
|
-
end # ComboLeg
|
372
|
-
|
373
|
-
|
374
|
-
class ScannerSubscription < AbstractDatum
|
375
|
-
attr_accessor :number_of_rows, :instrument, :location_code, :scan_code, :above_price, :below_price,
|
376
|
-
:above_volume, :average_option_volume_above, :market_cap_above, :market_cap_below, :moody_rating_above,
|
377
|
-
:moody_rating_below, :sp_rating_above, :sp_rating_below, :maturity_date_above, :maturity_date_below,
|
378
|
-
:coupon_rate_above, :coupon_rate_below, :exclude_convertible, :scanner_setting_pairs, :stock_type_filter
|
379
|
-
|
380
|
-
def init
|
381
|
-
super
|
382
|
-
|
383
|
-
@coupon_rate_above = @coupon_rate_below = @market_cap_below = @market_cap_above = @average_option_volume_above =
|
384
|
-
@above_volume = @below_price = @above_price = nil
|
385
|
-
@number_of_rows = -1 # none specified, per ScannerSubscription.java
|
386
|
-
end
|
387
|
-
end # ScannerSubscription
|
388
|
-
|
389
|
-
|
390
|
-
# Just like a Hash, but throws an exception if you try to access a key that doesn't exist.
|
391
|
-
class StringentHash < Hash
|
392
|
-
def initialize(hash)
|
393
|
-
super() {|hash,key| raise Exception.new("key #{key.inspect} not found!") }
|
394
|
-
self.merge!(hash) unless hash.nil?
|
395
|
-
end
|
396
|
-
end
|
397
|
-
|
398
|
-
end # module Datatypes
|
399
|
-
|
400
|
-
end # module
|
data/lib/ib-ruby/ib.rb
DELETED
@@ -1,242 +0,0 @@
|
|
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
|
-
require 'sha1'
|
21
|
-
require 'socket'
|
22
|
-
require 'logger'
|
23
|
-
require 'bigdecimal'
|
24
|
-
require 'bigdecimal/util'
|
25
|
-
|
26
|
-
# Add method to_ib to render datetime in IB format (zero padded "yyyymmdd HH:mm:ss")
|
27
|
-
class Time
|
28
|
-
def to_ib
|
29
|
-
"#{self.year}#{sprintf("%02d", self.month)}#{sprintf("%02d", self.day)} " +
|
30
|
-
"#{sprintf("%02d", self.hour)}:#{sprintf("%02d", self.min)}:#{sprintf("%02d", self.sec)}"
|
31
|
-
end
|
32
|
-
end # Time
|
33
|
-
|
34
|
-
|
35
|
-
module IB
|
36
|
-
|
37
|
-
TWS_IP_ADDRESS = "127.0.0.1"
|
38
|
-
TWS_PORT = "7496"
|
39
|
-
|
40
|
-
#logger = Logger.new(STDERR)
|
41
|
-
|
42
|
-
class IBSocket < TCPSocket
|
43
|
-
|
44
|
-
# send nice null terminated binary data
|
45
|
-
def send(data)
|
46
|
-
self.syswrite(data.to_s + "\0")
|
47
|
-
end
|
48
|
-
|
49
|
-
def read_string
|
50
|
-
self.gets("\0").chop
|
51
|
-
end
|
52
|
-
|
53
|
-
def read_int
|
54
|
-
self.read_string.to_i
|
55
|
-
end
|
56
|
-
|
57
|
-
def read_boolean
|
58
|
-
self.read_string.to_i != 0
|
59
|
-
end
|
60
|
-
|
61
|
-
# Floating-point numbers shouldn't be used to store money.
|
62
|
-
def read_decimal
|
63
|
-
self.read_string.to_d
|
64
|
-
end
|
65
|
-
|
66
|
-
end # class IBSocket
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
class IB
|
71
|
-
Tws_client_version = 27
|
72
|
-
|
73
|
-
attr_reader :next_order_id
|
74
|
-
|
75
|
-
def initialize(options_in = {})
|
76
|
-
@options = {
|
77
|
-
:ip => TWS_IP_ADDRESS,
|
78
|
-
:port => TWS_PORT,
|
79
|
-
}.merge(options_in)
|
80
|
-
|
81
|
-
@connected = false
|
82
|
-
@next_order_id = nil
|
83
|
-
@server = Hash.new # information about server and server connection state
|
84
|
-
|
85
|
-
# Message listeners.
|
86
|
-
# Key is the message class to listen for.
|
87
|
-
# Value is an Array of Procs. The proc will be called with the populated message instance as its argument when
|
88
|
-
# a message of that type is received.
|
89
|
-
@listeners = Hash.new { |hash, key|
|
90
|
-
hash[key] = Array.new
|
91
|
-
}
|
92
|
-
|
93
|
-
|
94
|
-
#logger.debug("IB#init: Initializing...")
|
95
|
-
|
96
|
-
self.open(@options)
|
97
|
-
|
98
|
-
end # init
|
99
|
-
|
100
|
-
def server_version
|
101
|
-
@server[:version]
|
102
|
-
end
|
103
|
-
|
104
|
-
|
105
|
-
def open(options_in = {})
|
106
|
-
raise Exception.new("Already connected!") if @connected
|
107
|
-
|
108
|
-
opts = {
|
109
|
-
:ip => "127.0.0.1",
|
110
|
-
:port => "7496"
|
111
|
-
}.merge(options_in)
|
112
|
-
|
113
|
-
|
114
|
-
# Subscribe to the NextValidID message from TWS that is always
|
115
|
-
# sent at connect, and save the id.
|
116
|
-
self.subscribe(IncomingMessages::NextValidID, lambda {|msg|
|
117
|
-
@next_order_id = msg.data[:order_id]
|
118
|
-
#logger.info { "Got next valid order id #{@next_order_id}." }
|
119
|
-
})
|
120
|
-
|
121
|
-
@server[:socket] = IBSocket.open(@options[:ip], @options[:port])
|
122
|
-
#logger.info("* TWS socket connected to #{@options[:ip]}:#{@options[:port]}.")
|
123
|
-
|
124
|
-
# Sekrit handshake.
|
125
|
-
#logger.debug("\tSending client version #{Tws_client_version}..")
|
126
|
-
|
127
|
-
@server[:socket].send(Tws_client_version)
|
128
|
-
@server[:version] = @server[:socket].read_int
|
129
|
-
@@server_version = @server[:version]
|
130
|
-
@server[:local_connect_time] = Time.now()
|
131
|
-
|
132
|
-
#logger.debug("\tGot server version: #{@server[:version]}.")
|
133
|
-
|
134
|
-
# Server version >= 20 sends the server time back.
|
135
|
-
if @server[:version] >= 20
|
136
|
-
@server[:remote_connect_time] = @server[:socket].read_string
|
137
|
-
#logger.debug("\tServer connect time: #{@server[:remote_connect_time]}.")
|
138
|
-
end
|
139
|
-
|
140
|
-
# Server version >= 3 wants an arbitrary client ID at this point. This can be used
|
141
|
-
# to identify subsequent communications.
|
142
|
-
if @server[:version] >= 3
|
143
|
-
@server[:client_id] = SHA1.digest(Time.now.to_s + $$.to_s).unpack("C*").join.to_i % 999999999
|
144
|
-
@server[:socket].send(@server[:client_id])
|
145
|
-
#logger.debug("\tSent client id # #{@server[:client_id]}.")
|
146
|
-
end
|
147
|
-
|
148
|
-
#logger.debug("Starting reader thread..")
|
149
|
-
Thread.abort_on_exception = true
|
150
|
-
@server[:reader_thread] = Thread.new {
|
151
|
-
self.reader
|
152
|
-
}
|
153
|
-
|
154
|
-
@connected = true
|
155
|
-
end
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
def close
|
160
|
-
@server[:reader_thread].kill # Thread uses blocking I/O, so join is useless.
|
161
|
-
@server[:socket].close()
|
162
|
-
@server = Hash.new
|
163
|
-
@@server_version = nil
|
164
|
-
@connected = false
|
165
|
-
#logger.debug("Disconnected.")
|
166
|
-
end # close
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
def to_s
|
171
|
-
"IB Connector: #{ @connected ? "connected." : "disconnected."}"
|
172
|
-
end
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
# Subscribe to incoming message events of type messageClass.
|
177
|
-
# code is a Proc that will be called with the message instance as its argument.
|
178
|
-
def subscribe(messageClass, code)
|
179
|
-
raise(Exception.new("Invalid argument type (#{messageClass}, #{code.class}) - " +
|
180
|
-
" must be (IncomingMessages::AbstractMessage, Proc)")) unless
|
181
|
-
messageClass <= IncomingMessages::AbstractMessage && code.is_a?(Proc)
|
182
|
-
|
183
|
-
@listeners[messageClass].push(code)
|
184
|
-
end
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
# Send an outgoing message.
|
189
|
-
def dispatch(message)
|
190
|
-
raise Exception.new("dispatch() must be given an OutgoingMessages::AbstractMessage subclass") unless
|
191
|
-
message.is_a?(OutgoingMessages::AbstractMessage)
|
192
|
-
|
193
|
-
#logger.info("Sending message " + message.inspect)
|
194
|
-
message.send(@server)
|
195
|
-
end
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
protected
|
200
|
-
|
201
|
-
def reader
|
202
|
-
#logger.debug("Reader started.")
|
203
|
-
|
204
|
-
while true
|
205
|
-
msg_id = @server[:socket].read_int # this blocks, so Thread#join is useless.
|
206
|
-
#logger.debug { "Reader: got message id #{msg_id}.\n" }
|
207
|
-
|
208
|
-
# create a new instance of the appropriate message type, and have it read the message.
|
209
|
-
msg = IncomingMessages::Table[msg_id].new(@server[:socket], @server[:version])
|
210
|
-
|
211
|
-
@listeners[msg.class].each { |listener|
|
212
|
-
listener.call(msg)
|
213
|
-
}
|
214
|
-
|
215
|
-
#logger.debug { " Listeners: " + @listeners.inspect + " inclusion: #{ @listeners.include?(msg.class)}" }
|
216
|
-
|
217
|
-
# Log the message if it's an error.
|
218
|
-
# Make an exception for the "successfully connected" messages, which, for some reason, come back from IB as errors.
|
219
|
-
if msg.is_a?(IncomingMessages::Error)
|
220
|
-
if msg.code == 2104 || msg.code == 2106 # connect strings
|
221
|
-
#logger.info(msg.to_human)
|
222
|
-
else
|
223
|
-
#logger.error(msg.to_human)
|
224
|
-
end
|
225
|
-
else
|
226
|
-
# Warn if nobody listened to a non-error incoming message.
|
227
|
-
unless @listeners[msg.class].size > 0
|
228
|
-
#logger.warn { " WARNING: Nobody listened to incoming message #{msg.class}" }
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
|
233
|
-
# #logger.debug("Reader done with message id #{msg_id}.")
|
234
|
-
|
235
|
-
|
236
|
-
end # while
|
237
|
-
|
238
|
-
#logger.debug("Reader done.")
|
239
|
-
end # reader
|
240
|
-
|
241
|
-
end # class IB
|
242
|
-
end # module IB
|