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.
Files changed (50) hide show
  1. data/HISTORY +4 -0
  2. data/README.md +25 -17
  3. data/VERSION +1 -1
  4. data/bin/account_info +1 -1
  5. data/bin/cancel_orders +2 -1
  6. data/bin/contract_details +3 -2
  7. data/bin/depth_of_market +1 -1
  8. data/bin/historic_data +1 -1
  9. data/bin/historic_data_cli +57 -104
  10. data/bin/list_orders +4 -3
  11. data/bin/market_data +1 -1
  12. data/bin/option_data +1 -1
  13. data/bin/place_combo_order +63 -0
  14. data/bin/place_order +2 -4
  15. data/bin/template +1 -1
  16. data/bin/{generic_data.rb → tick_data} +3 -1
  17. data/bin/time_and_sales +1 -1
  18. data/lib/ib-ruby.rb +1 -0
  19. data/lib/ib-ruby/connection.rb +68 -68
  20. data/lib/ib-ruby/errors.rb +28 -0
  21. data/lib/ib-ruby/extensions.rb +7 -0
  22. data/lib/ib-ruby/messages.rb +1 -0
  23. data/lib/ib-ruby/messages/abstract_message.rb +16 -11
  24. data/lib/ib-ruby/messages/incoming.rb +125 -329
  25. data/lib/ib-ruby/messages/incoming/open_order.rb +193 -0
  26. data/lib/ib-ruby/messages/incoming/ticks.rb +131 -0
  27. data/lib/ib-ruby/messages/outgoing.rb +44 -45
  28. data/lib/ib-ruby/models/combo_leg.rb +16 -1
  29. data/lib/ib-ruby/models/contract.rb +18 -10
  30. data/lib/ib-ruby/models/contract/bag.rb +1 -7
  31. data/lib/ib-ruby/models/execution.rb +2 -1
  32. data/lib/ib-ruby/models/model.rb +1 -1
  33. data/lib/ib-ruby/models/order.rb +116 -56
  34. data/lib/ib-ruby/socket.rb +24 -3
  35. data/spec/account_helper.rb +2 -1
  36. data/spec/ib-ruby/messages/outgoing_spec.rb +1 -1
  37. data/spec/ib-ruby/models/combo_leg_spec.rb +0 -1
  38. data/spec/integration/account_info_spec.rb +2 -2
  39. data/spec/integration/contract_info_spec.rb +4 -4
  40. data/spec/integration/depth_data_spec.rb +3 -3
  41. data/spec/integration/historic_data_spec.rb +1 -1
  42. data/spec/integration/market_data_spec.rb +4 -4
  43. data/spec/integration/option_data_spec.rb +1 -1
  44. data/spec/integration/orders/combo_spec.rb +51 -0
  45. data/spec/integration/orders/execution_spec.rb +15 -8
  46. data/spec/integration/orders/placement_spec.rb +46 -72
  47. data/spec/integration/orders/valid_ids_spec.rb +6 -6
  48. data/spec/integration_helper.rb +0 -79
  49. data/spec/order_helper.rb +153 -0
  50. 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
- ib = IB::Connection.new
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
@@ -4,6 +4,7 @@ IbRuby = IB
4
4
 
5
5
  require 'ib-ruby/version'
6
6
  require 'ib-ruby/extensions'
7
+ require 'ib-ruby/errors'
7
8
  require 'ib-ruby/constants'
8
9
  require 'ib-ruby/connection'
9
10
  require 'ib-ruby/models'
@@ -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
- attr_reader :server # Info about IB server and server connection state
30
- attr_accessor :next_order_id # Next valid order id
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 = @options[:logger] if @options[: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 @options[:connect]
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
- raise "Already connected!" if connected?
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: #{@next_order_id}."
53
+ log.info "Got next valid order id: #{next_order_id}."
53
54
  end
54
55
 
55
- @server[:socket] = IBSocket.open(@options[:host], @options[:port])
56
+ server[:socket] = IBSocket.open(options[:host], options[:port])
56
57
 
57
58
  # Secret handshake
58
- @server[:socket].send(CLIENT_VERSION)
59
- @server[:version] = @server[:socket].read_int
60
- raise "TWS version >= #{SERVER_VERSION} required." if @server[:version] < SERVER_VERSION
61
-
62
- @server[:local_connect_time] = Time.now()
63
- @server[:remote_connect_time] = @server[:socket].read_string
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
- @server[:client_id] = @options[:client_id] || random_id
69
- @server[:socket].send(@server[:client_id])
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: #{@server[:version]}, connection time: " +
73
- "#{@server[:local_connect_time]} local, " +
74
- "#{@server[:remote_connect_time]} remote."
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 @options[:reader] # Allows reconnect
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
- @server[:reader].join
87
+ server[:reader].join
85
88
  end
86
89
  if connected?
87
- @server[:socket].close
88
- @server = Hash.new
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
- raise ArgumentError.new "Need subscriber proc or block" unless subscriber.is_a? Proc
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::Table.values.find_all { |klass| klass.to_s =~ what }
125
+ Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what }
119
126
  else
120
- raise ArgumentError.new "#{what} must represent incoming IB message class"
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
- raise "No subscribers with id #{id}" if removed_at_id.empty?
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 message_type=nil
171
- if message_type
172
- received[message_type].clear
169
+ def clear_received *message_types
170
+ if message_types.empty?
171
+ received.each { |message_type, container| container.clear }
173
172
  else
174
- received.each { |_, message_type| message_type.clear }
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 2 seconds.
179
+ # Timeout after given time or 1 second.
181
180
  def wait_for *args, &block
182
- time = args.find { |arg| arg.is_a? Numeric } || 2
183
- timeout = Time.now + time
184
- args.push(block) if block
185
-
186
- sleep 0.1 until timeout < Time.now ||
187
- args.inject(true) do |result, arg|
188
- result && if arg.is_a?(Symbol)
189
- received?(arg)
190
- elsif arg.is_a?(Array)
191
- received?(*arg)
192
- elsif arg.respond_to?(:call)
193
- arg.call
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
- true
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 @server[:socket] for messages
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
- @server[:reader] = Thread.new do
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 && @server[:reader] && @server[:reader].alive?
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 [@server[:socket]], nil, nil, time_left
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 = @server[:socket].read_int # This read blocks!
227
+ msg_id = socket.read_int # This read blocks!
229
228
 
230
229
  # Debug:
231
- log.debug "Got message #{msg_id} (#{Messages::Incoming::Table[msg_id]})"
230
+ log.debug "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"
232
231
 
233
- # Create new instance of the appropriate message type, and have it read the message.
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::Table[msg_id].new(@server[:socket])
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 @options[:received]
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
- raise ArgumentError.new "Only able to send outgoing IB messages"
258
+ error "Only able to send outgoing IB messages", :args
259
259
  end
260
- raise "Not able to send messages, IB not connected!" unless connected?
261
- message.send_to(@server)
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 = @server[: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
+