ib-ruby 0.5.0 → 0.5.2

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