ib-ruby 0.5.0 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY CHANGED
@@ -49,3 +49,11 @@
49
49
  == 0.5.0 / 2011-10-29
50
50
 
51
51
  * Rake versioning bug resolved
52
+
53
+ == 0.5.1 / 2011-10-29
54
+
55
+ * Connection specs extended
56
+
57
+ == 0.5.2 / 2011-10-30
58
+
59
+ * Add nonblocking Connection#process_messages API
@@ -12,7 +12,7 @@ Ruby Implementation of the Interactive Broker' Trader Work Station (TWS) API v.9
12
12
  == FEATURES/PROBLEMS:
13
13
 
14
14
  * This is a BETA release, and should not be used for live trading.
15
- Any features contained with are AS-IS and may not work in all conditions
15
+ Any features contained within are AS-IS and may not work in all conditions
16
16
  * This code is not sanctioned or supported by Interactive Brokers
17
17
 
18
18
  == REQUIREMENTS:
@@ -33,17 +33,17 @@ connections on localhost.
33
33
 
34
34
  == SYNOPSIS:
35
35
 
36
- First, start up Interactive Broker's Trader Work Station.
36
+ First, start up Interactive Broker's Trader Work Station or Gateway.
37
37
  Make sure it is configured to allow API connections on localhost.
38
38
 
39
39
  >> require 'ib-ruby'
40
40
  >> ib = IB::Connection.new
41
41
  >> ib.subscribe(:Alert, :AccountValue) { |msg| puts msg.to_human }
42
- >> ib.send :RequestAccountData, :subscribe => true
42
+ >> ib.send_message :RequestAccountData, :subscribe => true
43
43
 
44
- Essentially, all interaction of your code and TWS can be described as an exchange
45
- of messages. You subscribe to message types you're interested in using
46
- IB::Connection#subscribe and request data from TWS using IB::Connection#send.
44
+ Essentially, all interaction of your code and TWS can be described as exchange
45
+ of messages. You subscribe to message type(s) you're interested in using
46
+ IB::Connection#subscribe and request data from TWS using IB::Connection#send_message.
47
47
  The code blocks (or procs) given to #subscribe will be executed when a message of
48
48
  requested type is received, with the received message as its argument.
49
49
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.5.2
@@ -28,10 +28,10 @@ ib.subscribe(IB::Messages::Incoming::Alert) { |msg| puts msg.to_human }
28
28
  # message as its argument. In this case, we just print out the data.
29
29
  ib.subscribe(:ContractData) { |msg| puts msg.contract.inspect }
30
30
 
31
- # Now we actually request historical data for the symbols we're interested in. TWS will
32
- # respond with a HistoricalData message, which will be processed by the code above.
31
+ # Now we actually request Contract details for the symbols we're interested in. TWS will
32
+ # respond with ContractData messages, which will be processed by the code above.
33
33
  @market.each_pair do |id, contract|
34
34
  ib.send_message :RequestContractData, :id => id, :contract => contract
35
35
  end
36
36
 
37
- sleep 3 # Wait for IB to respond to our request
37
+ sleep 2 # Wait for IB to respond to our request
@@ -30,9 +30,12 @@ ib.subscribe(:MarketDepth) do |msg|
30
30
  puts @market[msg.data[:id]].description + ": " + msg.to_human
31
31
  end
32
32
 
33
- # Now we actually request market data for the symbols we're interested in.
33
+ # Now we actually request L2 market data for the symbols we're interested in.
34
34
  @market.each_pair do |id, contract|
35
- ib.send_message :RequestMarketDepth, :id => id, :contract => contract, :num_rows => 5
35
+ ib.send_message :RequestMarketDepth,
36
+ :id => id,
37
+ :contract => contract,
38
+ :num_rows => 5
36
39
  end
37
40
 
38
41
  puts "\nSubscribed to market data"
@@ -40,4 +43,4 @@ puts "\n******** Press <Enter> to cancel... *********\n\n"
40
43
  gets
41
44
  puts "Cancelling market data subscription.."
42
45
 
43
- @market.each_pair { |id, contract| ib.send :CancelMarketDepth, :id => id }
46
+ @market.each_pair { |id, contract| ib.send_message :CancelMarketDepth, :id => id }
@@ -11,15 +11,14 @@ require 'bundler/setup'
11
11
  require 'ib-ruby'
12
12
 
13
13
  ### Configurable Options
14
- Quiet = false # if Quiet == false, status data will be printed to STDERR
15
- Timeout = 10 # How long to wait for messages from TWS before exiting, sec
14
+ QUIET = false # if Quiet == false, status data will be printed to STDERR
16
15
 
17
16
  # Definition of what we want data for. We have to keep track of what ticker id
18
17
  # corresponds to what symbol ourselves, because the ticks don't include any other
19
18
  # identifying information. The choice of ticker ids is, as far as I can tell, arbitrary.
20
19
  @market = {123 => IB::Symbols::Stocks[:wfc],
21
20
  456 => IB::Symbols::Futures[:ym],
22
- 789 => IB::Symbols::Forex[:gbpusd] # No historical market data for GBP/CASH@IDEALPRO Last 60
21
+ 789 => IB::Symbols::Forex[:gbpusd] # No historical data for GBP/CASH@IDEALPRO
23
22
  }
24
23
 
25
24
  # Connect to IB TWS.
@@ -28,19 +27,20 @@ ib = IB::Connection.new
28
27
  # Subscribe to TWS alerts/errors
29
28
  ib.subscribe(:Alert) { |msg| puts msg.to_human }
30
29
 
31
- # Now, subscribe to HistoricalData incoming events. The code passed in the block
30
+ # Subscribe to HistoricalData incoming events. The code passed in the block
32
31
  # will be executed when a message of that type is received, with the received
33
32
  # message as its argument. In this case, we just print out the data.
34
33
  #
35
34
  # Note that we have to look the ticker id of each incoming message
36
35
  # up in local memory to figure out what it's for.
37
36
  ib.subscribe(IB::Messages::Incoming::HistoricalData) do |msg|
38
- STDERR.puts @market[msg.data[:id]].description + ": " + msg.data[:count].to_s + " items:" unless Quiet
37
+ STDERR.puts @market[msg.data[:id]].description + ": " +
38
+ msg.data[:count].to_s + " items:" unless QUIET
39
39
 
40
40
  msg.data[:results].each do |datum|
41
41
  @last_msg_time = Time.now.to_i
42
42
 
43
- STDERR.puts " " + datum.to_s unless Quiet
43
+ STDERR.puts " " + datum.to_s unless QUIET
44
44
  end
45
45
  end
46
46
 
@@ -51,8 +51,8 @@ end
51
51
  :id => id,
52
52
  :contract => contract,
53
53
  :end_date_time => Time.now.to_ib,
54
- :duration => '2 D', # ?
55
- :bar_size => '1 min', #IB::OutgoingMessages::BAR_SIZES.key(:hour),
54
+ :duration => '2 D', # ?
55
+ :bar_size => '1 min', # IB::BAR_SIZES.key(:hour)?
56
56
  :what_to_show => :trades,
57
57
  :use_rth => 1,
58
58
  :format_date => 1)
@@ -26,8 +26,6 @@ opt = Getopt::Long.getopts(
26
26
  ["--header", BOOLEAN],
27
27
  ["--dateformat", REQUIRED],
28
28
  ["--nonregularhours", BOOLEAN],
29
- ["--verbose", BOOLEAN],
30
- ["--veryverbose", BOOLEAN]
31
29
  )
32
30
 
33
31
  if opt["help"] || opt["security"].nil? || opt["security"].empty?
@@ -107,12 +105,8 @@ Possible values (from the IB documentation):
107
105
 
108
106
  --header : if present, prints a 1 line CSV header describing the fields in the CSV.
109
107
 
110
- --veryverbose : if present, prints very verbose debugging info.
111
- --verbose : if present, prints all messages received from IB, and print the data in human-readable
112
- format.
113
-
114
- Otherwise, in the default mode, prints only the historic data (and any errors), and prints the
115
- data in CSV format.
108
+ Otherwise, in the default mode, prints only the historic data (and any errors),
109
+ and prints the data in CSV format.
116
110
 
117
111
  ENDHELP
118
112
 
@@ -171,9 +165,6 @@ REGULAR_HOURS_ONLY = opt["nonregularhours"] ? 0 : 1
171
165
 
172
166
  DATE_FORMAT = (opt["dateformat"] && opt["dateformat"].to_i) || 1
173
167
 
174
- VERYVERBOSE = !opt["veryverbose"].nil?
175
- VERBOSE = !opt["verbose"].nil?
176
-
177
168
  #
178
169
  # Definition of what we want market data for. We have to keep track
179
170
  # of what ticker id corresponds to what symbol ourselves, because the
@@ -186,16 +177,7 @@ VERBOSE = !opt["verbose"].nil?
186
177
  @market = {123 => opt["security"]}
187
178
 
188
179
  # First, connect to IB TWS.
189
- ib = IB::IB.new
190
-
191
- # Default level is quiet, only warnings printed.
192
- # IB::IBLogger.level = Logger::Severity::ERROR
193
-
194
- # For verbose printing of each message:
195
- # IB::IBLogger.level = Logger::Severity::INFO if VERBOSE
196
-
197
- # For very verbose debug messages:
198
- # IB::IBLogger.level = Logger::Severity::DEBUG if VERYVERBOSE
180
+ ib = IB::Connection.new
199
181
 
200
182
  puts "datetime,open,high,low,close,volume,wap,has_gaps" if !opt["header"].nil?
201
183
 
@@ -211,43 +193,38 @@ lastMessageTime = Queue.new # for communicating with the reader thread.
211
193
  # up in local memory to figure out what security it relates to.
212
194
  # The incoming message packet from TWS just identifies it by ticker id.
213
195
  #
214
- ib.subscribe(IB::IncomingMessages::HistoricalData, lambda { |msg|
196
+ ib.subscribe(:HistoricalData) do |msg|
215
197
  STDERR.puts @market[msg.data[:req_id]].description + ": " + msg.data[:item_count].to_s("F") + " items:" if VERBOSE
216
198
 
217
- msg.data[:history].each { |datum|
218
- puts(if VERBOSE
219
- datum.to_s
220
- else
221
- "#{datum.date},#{datum.open.to_s("F")},#{datum.high.to_s("F")},#{datum.low.to_s("F")}," +
222
- "#{datum.close.to_s("F")},#{datum.volume},#{datum.wap.to_s("F")},#{datum.has_gaps}"
223
- end
224
- )
225
- }
199
+ msg.data[:results].each do |datum|
200
+ if VERBOSE
201
+ puts datum.to_s
202
+ else
203
+ puts "#{datum.date},#{datum.open},#{datum.high},#{datum.low}," +
204
+ "#{datum.close},#{datum.volume},#{datum.wap},#{datum.has_gaps}"
205
+ end
206
+ end
226
207
  lastMessageTime.push(Time.now)
227
- })
208
+ end
228
209
 
229
210
  # Now we actually request historical data for the symbols we're
230
211
  # interested in. TWS will respond with a HistoricalData message,
231
212
  # which will be received by the code above.
232
-
233
213
  @market.each_pair { |id, contract|
234
- msg = IB::OutgoingMessages::RequestHistoricalData.new({
235
- :ticker_id => id,
236
- :contract => contract,
237
- :end_date_time => END_DATE_TIME,
238
- :duration => DURATION, # seconds == 1 hour
239
- :bar_size => BAR_SIZE, # 1 minute bars
240
- :what_to_show => WHAT,
241
- :use_RTH => REGULAR_HOURS_ONLY,
242
- :format_date => DATE_FORMAT
243
- })
244
- ib.send_message(msg)
245
- }
214
+ ib.send_message :RequestHistoricalData,
215
+ :id => id,
216
+ :contract => contract,
217
+ :end_date_time => END_DATE_TIME,
218
+ :duration => DURATION, # seconds == 1 hour
219
+ :bar_size => BAR_SIZE, # 1 minute bars
220
+ :what_to_show => WHAT,
221
+ :use_RTH => REGULAR_HOURS_ONLY,
222
+ :format_date => DATE_FORMAT
246
223
 
224
+ }
247
225
 
248
226
  # A complication here is that IB does not send any indication when all historic data is done being delivered.
249
227
  # So we have to guess - when there is no more new data for some period, we interpret that as "end of data" and exit.
250
-
251
228
  while true
252
229
  lastTime = lastMessageTime.pop # blocks until a message is ready on the queue
253
230
  sleep 2 # .. wait ..
@@ -25,13 +25,8 @@ ib.subscribe(:Alert) { |msg| puts msg.to_human }
25
25
 
26
26
  # Subscribe to TickerPrice and TickerSize events. The code passed in the block will
27
27
  # be executed when a message of that type is received, with the received message as its
28
- # argument. In this case, we just print out the tick.
29
- #
30
- # Note that we have to look the ticker id of each incoming message
31
- # up in local memory to figure out what it's for.
32
- #
33
- # (N.B. The description field is not from IB TWS. It is defined
34
- # locally in forex.rb, and is just arbitrary text.)
28
+ # argument. In this case, we just print out the tick. NB: The description field is not
29
+ # from IB TWS. It is defined locally in forex.rb, and is just arbitrary text.
35
30
  ib.subscribe(:TickPrice, :TickSize) do |msg|
36
31
  puts @market[msg.data[:id]].description + ": " + msg.to_human
37
32
  end
@@ -46,7 +46,6 @@ end
46
46
  ib.subscribe(:TickPrice, :TickSize, :TickString) { |msg| show_sales_and_size(msg) }
47
47
 
48
48
  # Now we actually request market data for the symbols we're interested in.
49
-
50
49
  @market.each_pair do |id, contract|
51
50
  ib.send_message :RequestMarketData, :id => id, :contract => contract
52
51
  end
@@ -56,4 +55,4 @@ puts "\n******** Press <Enter> to cancel... *********\n\n"
56
55
  gets
57
56
  puts "Unsubscribing from TWS market data.."
58
57
 
59
- @market.each_pair { |id, contract| ib.send :CancelMarketData, :id => id }
58
+ @market.each_pair { |id, contract| ib.send_message :CancelMarketData, :id => id }
@@ -20,12 +20,14 @@ module IB
20
20
  CLIENT_VERSION = 48 # Was 27 in original Ruby code
21
21
  SERVER_VERSION = 53 # Minimal server version. Latest, was 38 in current Java code.
22
22
  DEFAULT_OPTIONS = {:host =>"127.0.0.1",
23
- :port => '4001', # Gateway, TWS: '7496'
24
- :open => true
23
+ :port => '4001', # IB Gateway connection (default)
24
+ #:port => '7496', # TWS connection, with annoying pop-ups
25
+ :connect => true,
26
+ :reader => true
25
27
  }
26
28
 
27
- attr_reader :next_order_id, # Next valid order id
28
- :server # Info about server and server connection state
29
+ attr_reader :server, # Info about IB server and server connection state
30
+ :next_order_id # Next valid order id
29
31
 
30
32
  def initialize(opts = {})
31
33
  @options = DEFAULT_OPTIONS.merge(opts)
@@ -34,7 +36,8 @@ module IB
34
36
  @next_order_id = nil
35
37
  @server = Hash.new
36
38
 
37
- self.open(@options) if @options[:open]
39
+ connect if @options[:connect]
40
+ start_reader if @options[:reader]
38
41
  end
39
42
 
40
43
  # Message subscribers. Key is the message class to listen for.
@@ -45,18 +48,16 @@ module IB
45
48
  @subscribers ||= Hash.new { |hash, key| hash[key] = Hash.new }
46
49
  end
47
50
 
48
- def open(opts = {})
51
+ def connect
49
52
  raise Exception.new("Already connected!") if @connected
50
53
 
51
- opts = @options.merge(opts)
52
-
53
54
  # TWS always sends NextValidID message at connect - save this id
54
55
  self.subscribe(:NextValidID) do |msg|
55
56
  @next_order_id = msg.data[:id]
56
57
  puts "Got next valid order id: #{@next_order_id}."
57
58
  end
58
59
 
59
- @server[:socket] = IBSocket.open(opts[:host], opts[:port])
60
+ @server[:socket] = IBSocket.open(@options[:host], @options[:port])
60
61
 
61
62
  # Secret handshake
62
63
  @server[:socket].send(CLIENT_VERSION)
@@ -70,24 +71,26 @@ module IB
70
71
  @server[:client_id] = random_id
71
72
  @server[:socket].send(@server[:client_id])
72
73
 
73
- # Starting reader thread
74
- Thread.abort_on_exception = true
75
- @server[:reader_thread] = Thread.new { self.reader }
76
-
77
74
  @connected = true
78
75
  puts "Connected to server, version: #{@server[:version]}, connection time: " +
79
76
  "#{@server[:local_connect_time]} local, " +
80
77
  "#{@server[:remote_connect_time]} remote."
81
78
  end
82
79
 
83
- def close
84
- @server[:reader_thread].kill # Thread uses blocking I/O, so join is useless.
80
+ alias open connect # Legacy alias
81
+
82
+ def disconnect
83
+ if @server[:reader]
84
+ @reader_running = false
85
+ @server[:reader].join
86
+ end
85
87
  @server[:socket].close
86
88
  @server = Hash.new
87
- @server[:version] = nil
88
89
  @connected = false
89
90
  end
90
91
 
92
+ alias close disconnect # Legacy alias
93
+
91
94
  def connected?
92
95
  @connected
93
96
  end
@@ -117,7 +120,7 @@ module IB
117
120
  subscriber_id
118
121
  end
119
122
 
120
- # Remove subscriber(s) with specific subscriber id
123
+ # Remove all subscribers with specific subscriber id
121
124
  def unsubscribe(subscriber_id)
122
125
 
123
126
  subscribers.each do |message_class, message_subscribers|
@@ -136,38 +139,56 @@ module IB
136
139
  when what.is_a?(Symbol)
137
140
  Messages::Outgoing.const_get(what).new *args
138
141
  else
139
- raise ArgumentError.new("Only able to send Messages::Outgoing")
142
+ raise ArgumentError.new "Only able to send outgoing IB messages"
140
143
  end
141
144
  message.send_to(@server)
142
145
  end
143
146
 
144
- protected
147
+ alias dispatch send_message # Legacy alias
145
148
 
146
- def random_id
147
- rand 999999999
149
+ # Process incoming messages during *poll_time* (200) msecs (nonblocking)
150
+ def process_messages poll_time = 200 # in msec
151
+ time_out = Time.now + poll_time/1000.0
152
+ while (time_left = time_out - Time.now) > 0
153
+ # If server socket is readable, process single incoming message
154
+ process_message if select [@server[:socket]], nil, nil, time_left
155
+ end
148
156
  end
149
157
 
150
- def reader
151
- loop do
152
- # This read blocks, so Thread#join is useless.
153
- msg_id = @server[:socket].read_int
158
+ # Process single incoming message (blocking)
159
+ def process_message
160
+ # This read blocks!
161
+ msg_id = @server[:socket].read_int
162
+
163
+ # Debug:
164
+ unless [1, 2, 4, 6, 7, 8, 9, 12, 21, 53].include? msg_id
165
+ puts "Got message #{msg_id} (#{Messages::Incoming::Table[msg_id]})"
166
+ end
154
167
 
155
- # Debug:
156
- unless [1, 2, 4, 6, 7, 8, 9, 21, 53].include? msg_id
157
- puts "Got message #{msg_id} (#{Messages::Incoming::Table[msg_id]})"
158
- end
168
+ # Create new instance of the appropriate message type, and have it read the message.
169
+ # NB: Failure here usually means unsupported message type received
170
+ msg = Messages::Incoming::Table[msg_id].new(@server[:socket])
159
171
 
160
- # Create new instance of the appropriate message type, and have it read the message.
161
- # NB: Failure here usually means unsupported message type received
162
- msg = Messages::Incoming::Table[msg_id].new(@server[:socket])
172
+ subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
173
+ puts "No subscribers for message #{msg.class}!" if subscribers[msg.class].empty?
174
+ end
163
175
 
164
- subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
165
- puts "No subscribers for incoming message #{msg.class}!" if subscribers[msg.class].empty?
166
- end # loop
167
- end # reader
176
+ protected
168
177
 
169
- end
178
+ def random_id
179
+ rand 999999999
180
+ end
170
181
 
171
- # class Connection
182
+ # Start reader thread that continuously reads messages from server in background.
183
+ # If you don't start reader, you should manually poll @server[:socket] for messages
184
+ # or use #process_messages(msec) API.
185
+ def start_reader
186
+ Thread.abort_on_exception = true
187
+ @reader_running = true
188
+ @server[:reader] = Thread.new { process_messages while @reader_running }
189
+ end
190
+
191
+ end # class Connection
172
192
  IB = Connection # Legacy alias
193
+
173
194
  end # module IB