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