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
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
|