ib-ruby 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +4 -0
- data/README.md +25 -17
- data/VERSION +1 -1
- data/bin/account_info +1 -1
- data/bin/cancel_orders +2 -1
- data/bin/contract_details +3 -2
- data/bin/depth_of_market +1 -1
- data/bin/historic_data +1 -1
- data/bin/historic_data_cli +57 -104
- data/bin/list_orders +4 -3
- data/bin/market_data +1 -1
- data/bin/option_data +1 -1
- data/bin/place_combo_order +63 -0
- data/bin/place_order +2 -4
- data/bin/template +1 -1
- data/bin/{generic_data.rb → tick_data} +3 -1
- data/bin/time_and_sales +1 -1
- data/lib/ib-ruby.rb +1 -0
- data/lib/ib-ruby/connection.rb +68 -68
- data/lib/ib-ruby/errors.rb +28 -0
- data/lib/ib-ruby/extensions.rb +7 -0
- data/lib/ib-ruby/messages.rb +1 -0
- data/lib/ib-ruby/messages/abstract_message.rb +16 -11
- data/lib/ib-ruby/messages/incoming.rb +125 -329
- data/lib/ib-ruby/messages/incoming/open_order.rb +193 -0
- data/lib/ib-ruby/messages/incoming/ticks.rb +131 -0
- data/lib/ib-ruby/messages/outgoing.rb +44 -45
- data/lib/ib-ruby/models/combo_leg.rb +16 -1
- data/lib/ib-ruby/models/contract.rb +18 -10
- data/lib/ib-ruby/models/contract/bag.rb +1 -7
- data/lib/ib-ruby/models/execution.rb +2 -1
- data/lib/ib-ruby/models/model.rb +1 -1
- data/lib/ib-ruby/models/order.rb +116 -56
- data/lib/ib-ruby/socket.rb +24 -3
- data/spec/account_helper.rb +2 -1
- data/spec/ib-ruby/messages/outgoing_spec.rb +1 -1
- data/spec/ib-ruby/models/combo_leg_spec.rb +0 -1
- data/spec/integration/account_info_spec.rb +2 -2
- data/spec/integration/contract_info_spec.rb +4 -4
- data/spec/integration/depth_data_spec.rb +3 -3
- data/spec/integration/historic_data_spec.rb +1 -1
- data/spec/integration/market_data_spec.rb +4 -4
- data/spec/integration/option_data_spec.rb +1 -1
- data/spec/integration/orders/combo_spec.rb +51 -0
- data/spec/integration/orders/execution_spec.rb +15 -8
- data/spec/integration/orders/placement_spec.rb +46 -72
- data/spec/integration/orders/valid_ids_spec.rb +6 -6
- data/spec/integration_helper.rb +0 -79
- data/spec/order_helper.rb +153 -0
- metadata +13 -4
data/bin/option_data
CHANGED
@@ -19,7 +19,7 @@ require 'ib-ruby'
|
|
19
19
|
19 => IB::Symbols::Options[:spy100]}
|
20
20
|
|
21
21
|
# First, connect to IB TWS.
|
22
|
-
ib = IB::Connection.new
|
22
|
+
ib = IB::Connection.new :client_id => 1112 # Arbitrary id to identify your script
|
23
23
|
|
24
24
|
## Subscribe to TWS alerts/errors
|
25
25
|
ib.subscribe(:Alert) { |msg| puts msg.to_human }
|
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This script connects to IB API, subscribes to account info and prints out
|
4
|
+
# messages received from IB (update every 3 minute or so)
|
5
|
+
|
6
|
+
require 'pathname'
|
7
|
+
LIB_DIR = (Pathname.new(__FILE__).dirname + '../lib/').realpath.to_s
|
8
|
+
$LOAD_PATH.unshift LIB_DIR unless $LOAD_PATH.include?(LIB_DIR)
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'bundler/setup'
|
12
|
+
require 'ib-ruby'
|
13
|
+
|
14
|
+
# Utility method that helps us build multi-legged (BAG) Orders
|
15
|
+
def butterfly symbol, expiry, right, *strikes
|
16
|
+
raise 'No Connection!' unless @ib && @ib.connected?
|
17
|
+
|
18
|
+
legs = strikes.zip([1, -2, 1]).map do |strike, weight|
|
19
|
+
# Create contract
|
20
|
+
contract = IB::Models::Contract::Option.new :symbol => symbol,
|
21
|
+
:expiry => expiry,
|
22
|
+
:right => right,
|
23
|
+
:strike => strike
|
24
|
+
# Find out contract's con_id
|
25
|
+
@ib.clear_received :ContractData, :ContractDataEnd
|
26
|
+
@ib.send_message :RequestContractData, :id => strike, :contract => contract
|
27
|
+
@ib.wait_for :ContractDataEnd, 3
|
28
|
+
con_id = @ib.received[:ContractData].last.contract.con_id
|
29
|
+
|
30
|
+
# Create Comboleg from con_id and weight
|
31
|
+
IB::Models::ComboLeg.new :con_id => con_id, :weight => weight
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create new Combo contract
|
35
|
+
IB::Models::Contract::Bag.new :symbol => symbol,
|
36
|
+
:currency => "USD", # Only US options in combo Contracts
|
37
|
+
:exchange => "SMART",
|
38
|
+
:legs => legs
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# First, connect to IB TWS.
|
43
|
+
@ib = IB::Connection.new :client_id => 1112 # Arbitrary id to identify your script
|
44
|
+
@ib.wait_for :NextValidId
|
45
|
+
|
46
|
+
# Subscribe to TWS alerts/errors and order-related messages
|
47
|
+
@ib.subscribe(:Alert, :OpenOrder, :OrderStatus) { |msg| puts msg.to_human }
|
48
|
+
|
49
|
+
# Create multi-legged option Combo using utility method above
|
50
|
+
combo = butterfly 'GOOG', '201301', 'CALL', 500, 510, 520
|
51
|
+
|
52
|
+
# Create Order stub
|
53
|
+
order = IB::Models::Order.new :total_quantity => 10, # 10 butterflies
|
54
|
+
:limit_price => 0.01, # at 0.01 x 100 USD per contract
|
55
|
+
:action => 'BUY',
|
56
|
+
:order_type => 'LMT'
|
57
|
+
|
58
|
+
@ib.place_order order, combo
|
59
|
+
|
60
|
+
@ib.wait_for [:OpenOrder, 3], [:OrderStatus, 2]
|
61
|
+
|
62
|
+
puts "\n******** Press <Enter> to cancel... *********\n\n"
|
63
|
+
STDIN.gets
|
data/bin/place_order
CHANGED
@@ -12,7 +12,7 @@ require 'bundler/setup'
|
|
12
12
|
require 'ib-ruby'
|
13
13
|
|
14
14
|
# First, connect to IB TWS.
|
15
|
-
ib = IB::Connection.new
|
15
|
+
ib = IB::Connection.new :client_id => 1112 # Arbitrary id to identify your script
|
16
16
|
|
17
17
|
# Subscribe to TWS alerts/errors and order-related messages
|
18
18
|
ib.subscribe(:Alert, :OpenOrder, :OrderStatus) { |msg| puts msg.to_human }
|
@@ -22,9 +22,7 @@ buy_order = IB::Models::Order.new :total_quantity => 100,
|
|
22
22
|
:limit_price => 1 + rand().round(2),
|
23
23
|
:action => 'BUY',
|
24
24
|
:order_type => 'LMT'
|
25
|
-
|
26
|
-
sleep 0.5 # waiting for :NextValidId
|
27
|
-
|
25
|
+
ib.wait_for :NextValidId
|
28
26
|
ib.place_order buy_order, wfc
|
29
27
|
|
30
28
|
ib.send_message :RequestAllOpenOrders
|
data/bin/template
CHANGED
@@ -11,7 +11,7 @@ require 'bundler/setup'
|
|
11
11
|
require 'ib-ruby'
|
12
12
|
|
13
13
|
# Connect to IB TWS.
|
14
|
-
ib = IB::Connection.new
|
14
|
+
ib = IB::Connection.new :client_id => 1112 # Arbitrary id to identify your script
|
15
15
|
|
16
16
|
# Subscribe to TWS alerts/errors
|
17
17
|
ib.subscribe(:Alert) { |msg| puts msg.to_human }
|
@@ -15,7 +15,9 @@ contract = IB::Models::Contract.new :symbol=> 'AAPL',
|
|
15
15
|
:currency=> "USD",
|
16
16
|
:sec_type=> IB::SECURITY_TYPES[:stock],
|
17
17
|
:description=> "Some stock"
|
18
|
-
|
18
|
+
|
19
|
+
ib = IB::Connection.new :client_id => 1112 # Arbitrary id to identify your script
|
20
|
+
|
19
21
|
ib.subscribe(:Alert) { |msg| puts msg.to_human }
|
20
22
|
ib.subscribe(:TickGeneric, :TickString, :TickPrice, :TickSize) { |msg| puts msg.inspect }
|
21
23
|
ib.send_message :RequestMarketData, :id => 123, :contract => contract
|
data/bin/time_and_sales
CHANGED
@@ -21,7 +21,7 @@ require 'ib-ruby'
|
|
21
21
|
@last_msg_time = Time.now.to_i + 2
|
22
22
|
|
23
23
|
# Connect to IB TWS.
|
24
|
-
ib = IB::Connection.new
|
24
|
+
ib = IB::Connection.new :client_id => 1112 # Arbitrary id to identify your script
|
25
25
|
|
26
26
|
# Subscribe to TWS alerts/errors
|
27
27
|
ib.subscribe(:Alert) { |msg| puts msg.to_human }
|
data/lib/ib-ruby.rb
CHANGED
data/lib/ib-ruby/connection.rb
CHANGED
@@ -9,16 +9,16 @@ module IB
|
|
9
9
|
# thus improving performance at the expense of backwards compatibility.
|
10
10
|
# Older protocol versions support can be found in older gem versions.
|
11
11
|
|
12
|
-
CLIENT_VERSION = 48 # Was 27 in original Ruby code
|
13
|
-
SERVER_VERSION = 53 # Minimal server version. Latest, was 38 in current Java code.
|
14
12
|
DEFAULT_OPTIONS = {:host =>'127.0.0.1',
|
15
13
|
:port => '4001', # IB Gateway connection (default)
|
16
14
|
#:port => '7496', # TWS connection, with annoying pop-ups
|
17
|
-
:client_id => nil, # Will be randomly assigned
|
18
15
|
:connect => true, # Connect at initialization
|
19
16
|
:reader => true, # Start a separate reader Thread
|
20
17
|
:received => true, # Keep all received messages in a Hash
|
21
18
|
:logger => nil,
|
19
|
+
:client_id => nil, # Will be randomly assigned
|
20
|
+
:client_version => 57, # 48, # 57 = can receive commissionReport message
|
21
|
+
:server_version => 60 # 53? Minimal server version required
|
22
22
|
}
|
23
23
|
|
24
24
|
# Singleton to make active Connection universally accessible as IB::Connection.current
|
@@ -26,54 +26,57 @@ module IB
|
|
26
26
|
attr_accessor :current
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
attr_accessor :server, # Info about IB server and server connection state
|
30
|
+
:options, # Connection options
|
31
|
+
:next_order_id # Next valid order id
|
31
32
|
|
32
33
|
def initialize opts = {}
|
33
34
|
@options = DEFAULT_OPTIONS.merge(opts)
|
34
35
|
|
35
|
-
self.default_logger =
|
36
|
+
self.default_logger = options[:logger] if options[:logger]
|
36
37
|
@connected = false
|
37
38
|
@next_order_id = nil
|
38
39
|
@server = Hash.new
|
39
40
|
|
40
|
-
connect if
|
41
|
+
connect if options[:connect]
|
41
42
|
Connection.current = self
|
42
43
|
end
|
43
44
|
|
44
45
|
### Working with connection
|
45
46
|
|
46
47
|
def connect
|
47
|
-
|
48
|
+
error "Already connected!" if connected?
|
48
49
|
|
49
50
|
# TWS always sends NextValidId message at connect - save this id
|
50
51
|
self.subscribe(:NextValidId) do |msg|
|
51
52
|
@next_order_id = msg.order_id
|
52
|
-
log.info "Got next valid order id: #{
|
53
|
+
log.info "Got next valid order id: #{next_order_id}."
|
53
54
|
end
|
54
55
|
|
55
|
-
|
56
|
+
server[:socket] = IBSocket.open(options[:host], options[:port])
|
56
57
|
|
57
58
|
# Secret handshake
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
socket.write_data options[:client_version]
|
60
|
+
server[:client_version] = options[:client_version]
|
61
|
+
server[:server_version] = socket.read_int
|
62
|
+
if server[:server_version] < options[:server_version]
|
63
|
+
error "TWS version #{server[:server_version]}, #{options[:server_version]} required."
|
64
|
+
end
|
65
|
+
server[:remote_connect_time] = socket.read_string
|
66
|
+
server[:local_connect_time] = Time.now()
|
64
67
|
|
65
68
|
# Sending (arbitrary) client ID to identify subsequent communications.
|
66
69
|
# The client with a client_id of 0 can manage the TWS-owned open orders.
|
67
70
|
# Other clients can only manage their own open orders.
|
68
|
-
|
69
|
-
|
71
|
+
server[:client_id] = options[:client_id] || random_id
|
72
|
+
socket.write_data server[:client_id]
|
70
73
|
|
71
74
|
@connected = true
|
72
|
-
log.info "Connected to server, version: #{
|
73
|
-
"#{
|
74
|
-
"#{
|
75
|
+
log.info "Connected to server, version: #{server[:server_version]}, connection time: " +
|
76
|
+
"#{server[:local_connect_time]} local, " +
|
77
|
+
"#{server[:remote_connect_time]} remote."
|
75
78
|
|
76
|
-
start_reader if
|
79
|
+
start_reader if options[:reader] # Allows reconnect
|
77
80
|
end
|
78
81
|
|
79
82
|
alias open connect # Legacy alias
|
@@ -81,11 +84,11 @@ module IB
|
|
81
84
|
def disconnect
|
82
85
|
if reader_running?
|
83
86
|
@reader_running = false
|
84
|
-
|
87
|
+
server[:reader].join
|
85
88
|
end
|
86
89
|
if connected?
|
87
|
-
|
88
|
-
|
90
|
+
socket.close
|
91
|
+
server = Hash.new
|
89
92
|
@connected = false
|
90
93
|
end
|
91
94
|
end
|
@@ -96,6 +99,10 @@ module IB
|
|
96
99
|
@connected
|
97
100
|
end
|
98
101
|
|
102
|
+
def socket
|
103
|
+
server[:socket]
|
104
|
+
end
|
105
|
+
|
99
106
|
### Working with message subscribers
|
100
107
|
|
101
108
|
# Subscribe Proc or block to specific type(s) of incoming message events.
|
@@ -105,7 +112,7 @@ module IB
|
|
105
112
|
subscriber = args.last.respond_to?(:call) ? args.pop : block
|
106
113
|
id = random_id
|
107
114
|
|
108
|
-
|
115
|
+
error "Need subscriber proc or block", :args unless subscriber.is_a? Proc
|
109
116
|
|
110
117
|
args.each do |what|
|
111
118
|
message_classes =
|
@@ -115,9 +122,9 @@ module IB
|
|
115
122
|
when what.is_a?(Symbol)
|
116
123
|
[Messages::Incoming.const_get(what)]
|
117
124
|
when what.is_a?(Regexp)
|
118
|
-
Messages::Incoming::
|
125
|
+
Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what }
|
119
126
|
else
|
120
|
-
|
127
|
+
error "#{what} must represent incoming IB message class", :args
|
121
128
|
end
|
122
129
|
message_classes.flatten.each do |message_class|
|
123
130
|
# TODO: Fix: RuntimeError: can't add a new key into hash during iteration
|
@@ -132,7 +139,7 @@ module IB
|
|
132
139
|
removed = []
|
133
140
|
ids.each do |id|
|
134
141
|
removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact
|
135
|
-
|
142
|
+
error "No subscribers with id #{id}" if removed_at_id.empty?
|
136
143
|
removed << removed_at_id
|
137
144
|
end
|
138
145
|
removed.flatten
|
@@ -146,14 +153,6 @@ module IB
|
|
146
153
|
@subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new }
|
147
154
|
end
|
148
155
|
|
149
|
-
## Check if subscribers for given type exists
|
150
|
-
#def subscribed? message_type
|
151
|
-
# message_type
|
152
|
-
# (subscribers[message_type.class] ||
|
153
|
-
# subscribers[message_type.class] ||
|
154
|
-
# subscribers[message_type]).empty?
|
155
|
-
#end
|
156
|
-
|
157
156
|
### Working with received messages Hash
|
158
157
|
|
159
158
|
# Hash of received messages, keyed by message type
|
@@ -167,32 +166,32 @@ module IB
|
|
167
166
|
end
|
168
167
|
|
169
168
|
# Clear received messages Hash
|
170
|
-
def clear_received
|
171
|
-
if
|
172
|
-
received
|
169
|
+
def clear_received *message_types
|
170
|
+
if message_types.empty?
|
171
|
+
received.each { |message_type, container| container.clear }
|
173
172
|
else
|
174
|
-
|
173
|
+
message_types.each { |message_type| received[message_type].clear }
|
175
174
|
end
|
176
175
|
end
|
177
176
|
|
178
177
|
# Wait for specific condition(s) - given as callable/block, or
|
179
178
|
# message type(s) - given as Symbol or [Symbol, times] pair.
|
180
|
-
# Timeout after given time or
|
179
|
+
# Timeout after given time or 1 second.
|
181
180
|
def wait_for *args, &block
|
182
|
-
|
183
|
-
|
184
|
-
args.push(block)
|
185
|
-
|
186
|
-
sleep 0.1 until
|
187
|
-
|
188
|
-
result && if
|
189
|
-
received?(
|
190
|
-
elsif
|
191
|
-
received?(*
|
192
|
-
elsif
|
193
|
-
|
181
|
+
timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
|
182
|
+
end_time = Time.now + (timeout || 1) # default timeout 1 sec
|
183
|
+
conditions = args.delete_if { |arg| arg.is_a? Numeric }.push(block).compact
|
184
|
+
|
185
|
+
sleep 0.1 until end_time < Time.now || !conditions.empty? &&
|
186
|
+
conditions.inject(true) do |result, condition|
|
187
|
+
result && if condition.is_a?(Symbol)
|
188
|
+
received?(condition)
|
189
|
+
elsif condition.is_a?(Array)
|
190
|
+
received?(*condition)
|
191
|
+
elsif condition.respond_to?(:call)
|
192
|
+
condition.call
|
194
193
|
else
|
195
|
-
|
194
|
+
error "Unknown wait condition #{condition}"
|
196
195
|
end
|
197
196
|
end
|
198
197
|
end
|
@@ -200,18 +199,18 @@ module IB
|
|
200
199
|
### Working with Incoming messages from IB
|
201
200
|
|
202
201
|
# Start reader thread that continuously reads messages from server in background.
|
203
|
-
# If you don't start reader, you should manually poll @
|
202
|
+
# If you don't start reader, you should manually poll @socket for messages
|
204
203
|
# or use #process_messages(msec) API.
|
205
204
|
def start_reader
|
206
205
|
Thread.abort_on_exception = true
|
207
206
|
@reader_running = true
|
208
|
-
|
207
|
+
server[:reader] = Thread.new do
|
209
208
|
process_messages while @reader_running
|
210
209
|
end
|
211
210
|
end
|
212
211
|
|
213
212
|
def reader_running?
|
214
|
-
@reader_running &&
|
213
|
+
@reader_running && server[:reader] && server[:reader].alive?
|
215
214
|
end
|
216
215
|
|
217
216
|
# Process incoming messages during *poll_time* (200) msecs, nonblocking
|
@@ -219,27 +218,28 @@ module IB
|
|
219
218
|
time_out = Time.now + poll_time/1000.0
|
220
219
|
while (time_left = time_out - Time.now) > 0
|
221
220
|
# If server socket is readable, process single incoming message
|
222
|
-
process_message if select [
|
221
|
+
process_message if select [socket], nil, nil, time_left
|
223
222
|
end
|
224
223
|
end
|
225
224
|
|
226
225
|
# Process single incoming message (blocking!)
|
227
226
|
def process_message
|
228
|
-
msg_id =
|
227
|
+
msg_id = socket.read_int # This read blocks!
|
229
228
|
|
230
229
|
# Debug:
|
231
|
-
log.debug "Got message #{msg_id} (#{Messages::Incoming::
|
230
|
+
log.debug "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"
|
232
231
|
|
233
|
-
# Create new instance of the appropriate message type,
|
232
|
+
# Create new instance of the appropriate message type,
|
233
|
+
# and have it read the message from server.
|
234
234
|
# NB: Failure here usually means unsupported message type received
|
235
|
-
msg = Messages::Incoming::
|
235
|
+
msg = Messages::Incoming::Classes[msg_id].new(server)
|
236
236
|
|
237
237
|
# Deliver message to all registered subscribers, alert if no subscribers
|
238
238
|
subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
|
239
239
|
log.warn "No subscribers for message #{msg.class}!" if subscribers[msg.class].empty?
|
240
240
|
|
241
241
|
# Collect all received messages into a @received Hash
|
242
|
-
received[msg.message_type] << msg if
|
242
|
+
received[msg.message_type] << msg if options[:received]
|
243
243
|
end
|
244
244
|
|
245
245
|
### Sending Outgoing messages to IB
|
@@ -255,10 +255,10 @@ module IB
|
|
255
255
|
when what.is_a?(Symbol)
|
256
256
|
Messages::Outgoing.const_get(what).new *args
|
257
257
|
else
|
258
|
-
|
258
|
+
error "Only able to send outgoing IB messages", :args
|
259
259
|
end
|
260
|
-
|
261
|
-
message.send_to
|
260
|
+
error "Not able to send messages, IB not connected!" unless connected?
|
261
|
+
message.send_to server
|
262
262
|
end
|
263
263
|
|
264
264
|
alias dispatch send_message # Legacy alias
|
@@ -270,7 +270,7 @@ module IB
|
|
270
270
|
:order => order,
|
271
271
|
:contract => contract,
|
272
272
|
:id => @next_order_id
|
273
|
-
order.client_id =
|
273
|
+
order.client_id = server[:client_id]
|
274
274
|
order.order_id = @next_order_id
|
275
275
|
@next_order_id += 1
|
276
276
|
order.order_id
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module IB
|
2
|
+
|
3
|
+
# Error handling
|
4
|
+
class Error < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
class ArgumentError < ArgumentError
|
8
|
+
end
|
9
|
+
|
10
|
+
class LoadError < LoadError
|
11
|
+
end
|
12
|
+
|
13
|
+
end # module IB
|
14
|
+
|
15
|
+
### Patching Object with universally accessible top level error method
|
16
|
+
def error message, type=:standard, backtrace=nil
|
17
|
+
e = case type
|
18
|
+
when :standard
|
19
|
+
IB::Error.new message
|
20
|
+
when :args
|
21
|
+
IB::ArgumentError.new message
|
22
|
+
when :load
|
23
|
+
IB::LoadError.new message
|
24
|
+
end
|
25
|
+
e.set_backtrace(backtrace) if backtrace
|
26
|
+
raise e
|
27
|
+
end
|
28
|
+
|