ib-ruby 0.6.1 → 0.7.0
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 +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
|
+
|