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