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,400 @@
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 ADDED
@@ -0,0 +1,242 @@
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