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 +8 -0
- data/README.rdoc +6 -6
- data/VERSION +1 -1
- data/bin/contract_details +3 -3
- data/bin/depth_of_market +6 -3
- data/bin/historic_data +8 -8
- data/bin/historic_data_cli +23 -46
- data/bin/market_data +2 -7
- data/bin/time_and_sales +1 -2
- data/lib/ib-ruby/connection.rb +59 -38
- data/lib/ib-ruby/messages/incoming.rb +34 -18
- data/lib/ib-ruby/messages/outgoing.rb +83 -133
- data/lib/ib-ruby/models/contract.rb +17 -18
- data/lib/ib-ruby/models/order.rb +69 -1
- data/lib/ib-ruby/socket.rb +1 -1
- data/lib/ib-ruby/symbols/options.rb +4 -4
- data/spec/ib-ruby/connection_spec.rb +60 -33
- data/spec/ib-ruby/models/contract_spec.rb +2 -2
- metadata +101 -101
data/HISTORY
CHANGED
data/README.rdoc
CHANGED
@@ -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
|
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.
|
42
|
+
>> ib.send_message :RequestAccountData, :subscribe => true
|
43
43
|
|
44
|
-
Essentially, all interaction of your code and TWS can be described as
|
45
|
-
of messages. You subscribe to message
|
46
|
-
IB::Connection#subscribe and request data from TWS using IB::Connection#
|
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.
|
1
|
+
0.5.2
|
data/bin/contract_details
CHANGED
@@ -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
|
32
|
-
# respond with
|
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
|
37
|
+
sleep 2 # Wait for IB to respond to our request
|
data/bin/depth_of_market
CHANGED
@@ -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,
|
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.
|
46
|
+
@market.each_pair { |id, contract| ib.send_message :CancelMarketDepth, :id => id }
|
data/bin/historic_data
CHANGED
@@ -11,15 +11,14 @@ require 'bundler/setup'
|
|
11
11
|
require 'ib-ruby'
|
12
12
|
|
13
13
|
### Configurable Options
|
14
|
-
|
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
|
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
|
-
#
|
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 + ": " +
|
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
|
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::
|
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)
|
data/bin/historic_data_cli
CHANGED
@@ -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
|
-
|
111
|
-
|
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::
|
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(
|
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[:
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
"#{datum.close
|
223
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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 ..
|
data/bin/market_data
CHANGED
@@ -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
|
data/bin/time_and_sales
CHANGED
@@ -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.
|
58
|
+
@market.each_pair { |id, contract| ib.send_message :CancelMarketData, :id => id }
|
data/lib/ib-ruby/connection.rb
CHANGED
@@ -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
|
24
|
-
|
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 :
|
28
|
-
:
|
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
|
-
|
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
|
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(
|
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
|
-
|
84
|
-
|
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
|
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
|
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
|
-
|
147
|
+
alias dispatch send_message # Legacy alias
|
145
148
|
|
146
|
-
|
147
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
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
|
-
|
178
|
+
def random_id
|
179
|
+
rand 999999999
|
180
|
+
end
|
170
181
|
|
171
|
-
|
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
|