ib-ruby 0.5.21 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/HISTORY +8 -0
  2. data/README.md +46 -27
  3. data/TODO +13 -2
  4. data/VERSION +1 -1
  5. data/bin/generic_data.rb +26 -0
  6. data/bin/place_order +1 -1
  7. data/lib/ib-ruby/connection.rb +126 -65
  8. data/lib/ib-ruby/messages/incoming.rb +3 -3
  9. data/lib/ib-ruby/models/bar.rb +11 -11
  10. data/lib/ib-ruby/models/combo_leg.rb +23 -29
  11. data/lib/ib-ruby/models/contract/bag.rb +34 -2
  12. data/lib/ib-ruby/models/contract/option.rb +2 -2
  13. data/lib/ib-ruby/models/contract.rb +151 -197
  14. data/lib/ib-ruby/models/execution.rb +27 -45
  15. data/lib/ib-ruby/models/model.rb +10 -4
  16. data/lib/ib-ruby/models/model_properties.rb +63 -0
  17. data/lib/ib-ruby/models/order.rb +274 -320
  18. data/lib/ib-ruby/symbols/stocks.rb +11 -5
  19. data/spec/account_helper.rb +80 -0
  20. data/spec/ib-ruby/connection_spec.rb +195 -52
  21. data/spec/ib-ruby/messages/incoming_spec.rb +4 -4
  22. data/spec/ib-ruby/models/combo_leg_spec.rb +1 -0
  23. data/spec/ib-ruby/models/contract_spec.rb +1 -1
  24. data/spec/ib-ruby/models/execution_spec.rb +73 -0
  25. data/spec/integration/account_info_spec.rb +12 -59
  26. data/spec/integration/contract_info_spec.rb +23 -37
  27. data/spec/integration/depth_data_spec.rb +4 -4
  28. data/spec/integration/historic_data_spec.rb +15 -27
  29. data/spec/integration/market_data_spec.rb +74 -61
  30. data/spec/integration/option_data_spec.rb +5 -48
  31. data/spec/integration/orders/execution_spec.rb +26 -31
  32. data/spec/integration/orders/open_order +2 -0
  33. data/spec/integration/orders/placement_spec.rb +28 -28
  34. data/spec/integration/orders/valid_ids_spec.rb +11 -11
  35. data/spec/integration_helper.rb +46 -32
  36. data/spec/message_helper.rb +4 -38
  37. data/spec/spec_helper.rb +2 -3
  38. metadata +9 -2
data/HISTORY CHANGED
@@ -125,3 +125,11 @@
125
125
  == 0.5.21 / 2012-03-06
126
126
 
127
127
  * Fully tested with integration spec suite
128
+
129
+ == 0.6.0 / 2012-03-09
130
+
131
+ * Connection wait_for/received/received? API added
132
+
133
+ == 0.6.1 / 2012-03-14
134
+
135
+ * Version bump
data/README.md CHANGED
@@ -11,12 +11,9 @@ implied. Your use of this software is at your own risk. It may contain
11
11
  any number of bugs, known or unknown, which might cause you to lose
12
12
  money if you use it. You've been warned.
13
13
 
14
- __It is specifically NOT RECOMMENDED that this code be used for live trading.__
15
-
16
14
  This code is not sanctioned or supported by Interactive Brokers
17
15
  This software is available under the LGPL. See the file LICENSE for full licensing details.
18
16
 
19
-
20
17
  ## REQUIREMENTS:
21
18
 
22
19
  Either the Interactive Brokers
@@ -42,30 +39,52 @@ localhost if you're running ib-ruby on the same machine as TWS.
42
39
  First, start up Interactive Broker's Trader Work Station or Gateway.
43
40
  Make sure it is configured to allow API connections on localhost.
44
41
  Note that TWS and Gateway listen to different ports, this library assumes
45
- connection to Gateway (localhost:4001) by default, this can changed via :host and :port
46
- options given to IB::Connection.new.
47
-
48
- >> require 'ib-ruby'
49
- >> ib = IB::Connection.new
50
- >> ib.subscribe(:Alert, :AccountValue) { |msg| puts msg.to_human }
51
- >> ib.send_message :RequestAccountData, :subscribe => true
52
-
53
- Your code and TWS interact via an exchange of messages. You
54
- subscribe to message types you're interested in using
55
- `IB::Connection#subscribe` and request data from TWS using
56
- `IB::Connection#send_message`.
57
-
58
- The code blocks (or procs) given to `#subscribe` will be executed when
59
- a message of the requested type is received, with the received message as
60
- its argument.
61
-
62
- See `lib/ib-ruby/messages` for a full list of supported incoming/outgoing messages and
63
- their attributes. The original TWS docs and code samples can be found
64
- in the `misc/` folder.
65
-
66
- The sample scripts in the `bin/` directory provide examples of how
67
- common tasks can be achieved using ib-ruby.
68
-
42
+ connection to Gateway (localhost:4001) by default, this can changed via :host
43
+ and :port options given to IB::Connection.new.
44
+
45
+ require 'ib-ruby'
46
+
47
+ ib = IB::Connection.new
48
+ ib.subscribe(:Alert, :AccountValue) { |msg| puts msg.to_human }
49
+ ib.send_message :RequestAccountData
50
+ ib.wait_for :AccountDownloadEnd
51
+
52
+ ib.subscribe(:OpenOrder) { |msg| puts "Placed: #{msg.order}!" }
53
+ ib.subscribe(:ExecutionData) { |msg| puts "Filled: #{msg.execution}!" }
54
+ contract = IB::Models::Contract.new :symbol => 'WFC',
55
+ :exchange => 'NYSE'
56
+ :currency => 'USD',
57
+ :sec_type => IB::SECURITY_TYPES[:stock]
58
+ buy_order = IB::Models::Order.new :total_quantity => 100,
59
+ :limit_price => 21.00,
60
+ :action => 'BUY',
61
+ :order_type => 'LMT'
62
+ ib.place_order buy_order, contract
63
+ ib.wait_for :ExecutionData
64
+
65
+ Your code interacts with TWS via exchange of messages. Messages that you send to
66
+ TWS are called 'Outgoing', messages your code receives from TWS - 'Incoming'.
67
+
68
+ First, you need to subscribe to incoming message types you're interested in
69
+ using `Connection#subscribe`. The code block (or proc) given to `#subscribe`
70
+ will be executed when an incoming message of the requested type is received
71
+ from TWS, with the received message as its argument.
72
+
73
+ Then, you request specific data from TWS using `Connection#send_message` or place
74
+ your order using `Connection#place_order`. TWS will respond with messages that you
75
+ should have subscribed for, and these messages are be processed in a code block
76
+ given to `#subscribe`.
77
+
78
+ In order to give TWS time to respond, you either run a message processing loop or
79
+ just wait until Connection receives the messages type you requested.
80
+
81
+ See `lib/ib-ruby/messages` for a full list of supported incoming/outgoing messages
82
+ and their attributes. The original TWS docs and code samples can be found
83
+ in `misc` directory.
84
+
85
+ The sample scripts in `bin` directory provide examples of how common tasks
86
+ can be achieved using ib-ruby. You may also want to look into `spec/integration`
87
+ directory for more scenarios and examples of handling IB messages.
69
88
 
70
89
  ## LICENSE:
71
90
 
data/TODO CHANGED
@@ -1,7 +1,7 @@
1
- 1. Create integration tests for basic use cases
2
-
3
1
  Plan:
4
2
 
3
+ 1. Detailed message documentation
4
+
5
5
  2. Make ActiveModel-like attributes Hash for easy attributes updating
6
6
 
7
7
  3. IB#send_message method should accept block, thus compressing subscribe/send_message
@@ -14,8 +14,19 @@ http://finance.groups.yahoo.com/group/TWSAPI/message/25413
14
14
 
15
15
  6. Compatibility check for new TWS v.966
16
16
 
17
+ 7. @received_at timestamp in messages
18
+
19
+ 8. Collect all messages in Connection#received_messages
20
+
21
+ 9. Flow handlers: Connection#wait_for / Connection#received?
22
+
23
+ 10. Create integration tests for more use cases (spec/README)
24
+
25
+
17
26
  Done:
18
27
 
28
+ 1. Create integration tests for basic use cases
29
+
19
30
  2. IB#subscribe should accept regexes.
20
31
 
21
32
  Ideas for future:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.21
1
+ 0.6.1
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script reproduces https://github.com/ib-ruby/ib-ruby/issues/12
4
+
5
+ require 'pathname'
6
+ LIB_DIR = (Pathname.new(__FILE__).dirname + '../lib/').realpath.to_s
7
+ $LOAD_PATH.unshift LIB_DIR unless $LOAD_PATH.include?(LIB_DIR)
8
+
9
+ require 'rubygems'
10
+ require 'bundler/setup'
11
+ require 'ib-ruby'
12
+
13
+ contract = IB::Models::Contract.new :symbol=> 'AAPL',
14
+ :exchange=> "Smart",
15
+ :currency=> "USD",
16
+ :sec_type=> IB::SECURITY_TYPES[:stock],
17
+ :description=> "Some stock"
18
+ ib = IB::Connection.new
19
+ ib.subscribe(:Alert) { |msg| puts msg.to_human }
20
+ ib.subscribe(:TickGeneric, :TickString, :TickPrice, :TickSize) { |msg| puts msg.inspect }
21
+ ib.send_message :RequestMarketData, :id => 123, :contract => contract
22
+
23
+ puts "\nSubscribed to market data"
24
+ puts "\n******** Press <Enter> to cancel... *********\n\n"
25
+ gets
26
+ puts "Cancelling market data subscription.."
data/bin/place_order CHANGED
@@ -23,7 +23,7 @@ buy_order = IB::Models::Order.new :total_quantity => 100,
23
23
  :action => 'BUY',
24
24
  :order_type => 'LMT'
25
25
 
26
- sleep 0.5 # waiting for :NextValidID
26
+ sleep 0.5 # waiting for :NextValidId
27
27
 
28
28
  ib.place_order buy_order, wfc
29
29
 
@@ -15,9 +15,10 @@ module IB
15
15
  :port => '4001', # IB Gateway connection (default)
16
16
  #:port => '7496', # TWS connection, with annoying pop-ups
17
17
  :client_id => nil, # Will be randomly assigned
18
- :connect => true,
19
- :reader => true,
20
- :logger => nil
18
+ :connect => true, # Connect at initialization
19
+ :reader => true, # Start a separate reader Thread
20
+ :received => true, # Keep all received messages in a Hash
21
+ :logger => nil,
21
22
  }
22
23
 
23
24
  # Singleton to make active Connection universally accessible as IB::Connection.current
@@ -28,7 +29,7 @@ module IB
28
29
  attr_reader :server # Info about IB server and server connection state
29
30
  attr_accessor :next_order_id # Next valid order id
30
31
 
31
- def initialize(opts = {})
32
+ def initialize opts = {}
32
33
  @options = DEFAULT_OPTIONS.merge(opts)
33
34
 
34
35
  self.default_logger = @options[:logger] if @options[:logger]
@@ -40,19 +41,13 @@ module IB
40
41
  Connection.current = self
41
42
  end
42
43
 
43
- # Message subscribers. Key is the message class to listen for.
44
- # Value is a Hash of subscriber Procs, keyed by their subscription id.
45
- # All subscriber Procs will be called with the message instance
46
- # as an argument when a message of that type is received.
47
- def subscribers
48
- @subscribers ||= Hash.new { |hash, key| hash[key] = Hash.new }
49
- end
44
+ ### Working with connection
50
45
 
51
46
  def connect
52
47
  raise "Already connected!" if connected?
53
48
 
54
- # TWS always sends NextValidID message at connect - save this id
55
- self.subscribe(:NextValidID) do |msg|
49
+ # TWS always sends NextValidId message at connect - save this id
50
+ self.subscribe(:NextValidId) do |msg|
56
51
  @next_order_id = msg.order_id
57
52
  log.info "Got next valid order id: #{@next_order_id}."
58
53
  end
@@ -101,16 +96,14 @@ module IB
101
96
  @connected
102
97
  end
103
98
 
104
- def reader_running?
105
- @reader_running && @server[:reader] && @server[:reader].alive?
106
- end
99
+ ### Working with message subscribers
107
100
 
108
101
  # Subscribe Proc or block to specific type(s) of incoming message events.
109
102
  # Listener will be called later with received message instance as its argument.
110
103
  # Returns subscriber id to allow unsubscribing
111
- def subscribe(*args, &block)
104
+ def subscribe *args, &block
112
105
  subscriber = args.last.respond_to?(:call) ? args.pop : block
113
- subscriber_id = random_id
106
+ id = random_id
114
107
 
115
108
  raise ArgumentError.new "Need subscriber proc or block" unless subscriber.is_a? Proc
116
109
 
@@ -118,50 +111,110 @@ module IB
118
111
  message_classes =
119
112
  case
120
113
  when what.is_a?(Class) && what < Messages::Incoming::AbstractMessage
121
- what
114
+ [what]
122
115
  when what.is_a?(Symbol)
123
- Messages::Incoming.const_get(what)
116
+ [Messages::Incoming.const_get(what)]
124
117
  when what.is_a?(Regexp)
125
118
  Messages::Incoming::Table.values.find_all { |klass| klass.to_s =~ what }
126
119
  else
127
120
  raise ArgumentError.new "#{what} must represent incoming IB message class"
128
121
  end
129
- [message_classes].flatten.each do |message_class|
122
+ message_classes.flatten.each do |message_class|
130
123
  # TODO: Fix: RuntimeError: can't add a new key into hash during iteration
131
- subscribers[message_class][subscriber_id] = subscriber
124
+ subscribers[message_class][id] = subscriber
132
125
  end
133
126
  end
134
- subscriber_id
127
+ id
135
128
  end
136
129
 
137
130
  # Remove all subscribers with specific subscriber id (TODO: multiple ids)
138
- def unsubscribe(subscriber_id)
131
+ def unsubscribe *ids
132
+ removed = []
133
+ ids.each do |id|
134
+ removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact
135
+ raise "No subscribers with id #{id}" if removed_at_id.empty?
136
+ removed << removed_at_id
137
+ end
138
+ removed.flatten
139
+ end
140
+
141
+ # Message subscribers. Key is the message class to listen for.
142
+ # Value is a Hash of subscriber Procs, keyed by their subscription id.
143
+ # All subscriber Procs will be called with the message instance
144
+ # as an argument when a message of that type is received.
145
+ def subscribers
146
+ @subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new }
147
+ end
148
+
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
+ ### Working with received messages Hash
158
+
159
+ # Hash of received messages, keyed by message type
160
+ def received
161
+ @received ||= Hash.new { |hash, message_type| hash[message_type] = Array.new }
162
+ end
139
163
 
140
- subscribers.each do |message_class, message_subscribers|
141
- message_subscribers.delete subscriber_id
164
+ # Check if messages of given type were received at_least n times
165
+ def received? message_type, times=1
166
+ received[message_type].size >= times
167
+ end
168
+
169
+ # Clear received messages Hash
170
+ def clear_received message_type=nil
171
+ if message_type
172
+ received[message_type].clear
173
+ else
174
+ received.each { |_, message_type| message_type.clear }
142
175
  end
143
176
  end
144
177
 
145
- # Send an outgoing message.
146
- def send_message(what, *args)
147
- message =
148
- case
149
- when what.is_a?(Messages::Outgoing::AbstractMessage)
150
- what
151
- when what.is_a?(Class) && what < Messages::Outgoing::AbstractMessage
152
- what.new *args
153
- when what.is_a?(Symbol)
154
- Messages::Outgoing.const_get(what).new *args
155
- else
156
- raise ArgumentError.new "Only able to send outgoing IB messages"
178
+ # Wait for specific condition(s) - given as callable/block, or
179
+ # message type(s) - given as Symbol or [Symbol, times] pair.
180
+ # Timeout after given time or 2 seconds.
181
+ 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
194
+ else
195
+ true
196
+ end
157
197
  end
158
- raise "Not able to send messages, IB not connected!" unless connected?
159
- message.send_to(@server)
160
198
  end
161
199
 
162
- alias dispatch send_message # Legacy alias
200
+ ### Working with Incoming messages from IB
201
+
202
+ # 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
204
+ # or use #process_messages(msec) API.
205
+ def start_reader
206
+ Thread.abort_on_exception = true
207
+ @reader_running = true
208
+ @server[:reader] = Thread.new do
209
+ process_messages while @reader_running
210
+ end
211
+ end
163
212
 
164
- # Process incoming messages during *poll_time* (200) msecs
213
+ def reader_running?
214
+ @reader_running && @server[:reader] && @server[:reader].alive?
215
+ end
216
+
217
+ # Process incoming messages during *poll_time* (200) msecs, nonblocking
165
218
  def process_messages poll_time = 200 # in msec
166
219
  time_out = Time.now + poll_time/1000.0
167
220
  while (time_left = time_out - Time.now) > 0
@@ -170,27 +223,48 @@ module IB
170
223
  end
171
224
  end
172
225
 
173
- # Process single incoming message (blocking)
226
+ # Process single incoming message (blocking!)
174
227
  def process_message
175
- # This read blocks!
176
- msg_id = @server[:socket].read_int
228
+ msg_id = @server[:socket].read_int # This read blocks!
177
229
 
178
230
  # Debug:
179
- unless [1, 2, 4, 6, 7, 8, 9, 12, 21, 53].include? msg_id
180
- log.debug "Got message #{msg_id} (#{Messages::Incoming::Table[msg_id]})"
181
- end
231
+ log.debug "Got message #{msg_id} (#{Messages::Incoming::Table[msg_id]})"
182
232
 
183
233
  # Create new instance of the appropriate message type, and have it read the message.
184
234
  # NB: Failure here usually means unsupported message type received
185
235
  msg = Messages::Incoming::Table[msg_id].new(@server[:socket])
186
236
 
237
+ # Deliver message to all registered subscribers, alert if no subscribers
187
238
  subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
188
239
  log.warn "No subscribers for message #{msg.class}!" if subscribers[msg.class].empty?
240
+
241
+ # Collect all received messages into a @received Hash
242
+ received[msg.message_type] << msg if @options[:received]
189
243
  end
190
244
 
191
- # Place Order (convenience wrapper for message :PlaceOrder).
192
- # Assigns client_id and order_id fields to placed order.
193
- # Returns order_id.
245
+ ### Sending Outgoing messages to IB
246
+
247
+ # Send an outgoing message.
248
+ def send_message what, *args
249
+ message =
250
+ case
251
+ when what.is_a?(Messages::Outgoing::AbstractMessage)
252
+ what
253
+ when what.is_a?(Class) && what < Messages::Outgoing::AbstractMessage
254
+ what.new *args
255
+ when what.is_a?(Symbol)
256
+ Messages::Outgoing.const_get(what).new *args
257
+ else
258
+ raise ArgumentError.new "Only able to send outgoing IB messages"
259
+ end
260
+ raise "Not able to send messages, IB not connected!" unless connected?
261
+ message.send_to(@server)
262
+ end
263
+
264
+ alias dispatch send_message # Legacy alias
265
+
266
+ # Place Order (convenience wrapper for send_message :PlaceOrder).
267
+ # Assigns client_id and order_id fields to placed order. Returns assigned order_id.
194
268
  def place_order order, contract
195
269
  send_message :PlaceOrder,
196
270
  :order => order,
@@ -202,24 +276,13 @@ module IB
202
276
  order.order_id
203
277
  end
204
278
 
205
- # Cancel Orders by their id (convenience wrapper for message :CancelOrder).
279
+ # Cancel Orders by their ids (convenience wrapper for send_message :CancelOrder).
206
280
  def cancel_order *order_ids
207
281
  order_ids.each do |order_id|
208
282
  send_message :CancelOrder, :id => order_id.to_i
209
283
  end
210
284
  end
211
285
 
212
- # Start reader thread that continuously reads messages from server in background.
213
- # If you don't start reader, you should manually poll @server[:socket] for messages
214
- # or use #process_messages(msec) API.
215
- def start_reader
216
- Thread.abort_on_exception = true
217
- @reader_running = true
218
- @server[:reader] = Thread.new do
219
- process_messages while @reader_running
220
- end
221
- end
222
-
223
286
  protected
224
287
 
225
288
  def random_id
@@ -227,6 +290,4 @@ module IB
227
290
  end
228
291
 
229
292
  end # class Connection
230
- #IB = Connection # Legacy alias
231
-
232
293
  end # module IB
@@ -146,7 +146,7 @@ module IB
146
146
  # This message is always sent by TWS automatically at connect.
147
147
  # The IB::Connection class subscribes to it automatically and stores
148
148
  # the order id in its @next_order_id attribute.
149
- NextValidID = def_message 9, [:order_id, :int]
149
+ NextValidID = NextValidId = def_message(9, [:order_id, :int])
150
150
 
151
151
  NewsBulletins =
152
152
  def_message 14, [:request_id, :int], # unique incrementing bulletin ID.
@@ -246,7 +246,7 @@ module IB
246
246
  [:tick_type, :int],
247
247
  [:size, :int]
248
248
 
249
- TickGeneric = def_message 45, AbstractTick,
249
+ TickGeneric = def_message [45, 6], AbstractTick,
250
250
  [:ticker_id, :int],
251
251
  [:tick_type, :int],
252
252
  [:value, :decimal]
@@ -256,7 +256,7 @@ module IB
256
256
  [:tick_type, :int],
257
257
  [:value, :string]
258
258
 
259
- TickEFP = def_message 47, AbstractTick,
259
+ TickEFP = def_message [47, 6], AbstractTick,
260
260
  [:ticker_id, :int],
261
261
  [:tick_type, :int],
262
262
  [:basis_points, :decimal],
@@ -5,17 +5,17 @@ module IB
5
5
  # This is a single data point delivered by HistoricData messages.
6
6
  # Instantiate with a Hash of attributes, to be auto-set via initialize in Model.
7
7
  class Bar < Model
8
- attr_accessor :time, # The date-time stamp of the start of the bar. The format is
9
- # determined by the reqHistoricalData() formatDate parameter.
10
- :open, # The bar opening price.
11
- :high, # The high price during the time covered by the bar.
12
- :low, # The low price during the time covered by the bar.
13
- :close, # The bar closing price.
14
- :volume, # The bar opening price.
15
- :wap, # Weighted average price during the time covered by the bar.
16
- :has_gaps, # Whether or not there are gaps in the data.
17
- :trades # int: When TRADES data history is returned, represents number
18
- # of trades that occurred during the time period the bar covers
8
+ prop :time, # The date-time stamp of the start of the bar. The format is
9
+ # determined by the reqHistoricalData() formatDate parameter.
10
+ :open, # The bar opening price.
11
+ :high, # The high price during the time covered by the bar.
12
+ :low, # The low price during the time covered by the bar.
13
+ :close, # The bar closing price.
14
+ :volume, # The bar opening price.
15
+ :wap, # Weighted average price during the time covered by the bar.
16
+ :has_gaps, # Whether or not there are gaps in the data.
17
+ :trades # int: When TRADES data history is returned, represents number
18
+ # of trades that occurred during the time period the bar covers
19
19
 
20
20
  def to_s
21
21
  "<Bar #{time}: wap: #{wap}, OHLC: #{open}, #{high}, #{low}, #{close}, " +
@@ -16,35 +16,29 @@ module IB
16
16
  CLOSE = 2 # Close. This value is only valid for institutional customers.
17
17
  UNKNOWN = 3
18
18
 
19
- attr_accessor :con_id, # int: The unique contract identifier specifying the security.
20
- :ratio, # int: Select the relative number of contracts for the leg you
21
- # are constructing. To help determine the ratio for a
22
- # specific combination order, refer to the Interactive
23
- # Analytics section of the User's Guide.
24
-
25
- :action, # String: BUY/SELL/SSHORT/SSHORTX
26
- # The side (buy or sell) for the leg you are constructing.
27
- :exchange, # String: exchange to which the complete combination
28
- # order will be routed.
29
- :open_close, # int: Specifies whether the order is an open or close order.
30
- # Valid values: ComboLeg::SAME/OPEN/CLOSE/UNKNOWN
31
-
32
- # For institutional customers only! For stock legs when doing short sale
33
- :short_sale_slot, # int: 0 - retail, 1 = clearing broker, 2 = third party
34
- :designated_location, # String: Only for shortSaleSlot == 2.
35
- # Otherwise leave blank or orders will be rejected.
36
- :exempt_code # int: ?
37
-
38
- def initialize opts = {}
39
- @con_id = 0
40
- @ratio = 0
41
- @open_close = SAME
42
- @short_sale_slot = 0
43
- @designated_location = ''
44
- @exempt_code = -1
45
-
46
- super opts
47
- end
19
+ prop :con_id, # int: The unique contract identifier specifying the security.
20
+ :ratio, # int: Select the relative number of contracts for the leg you
21
+ # are constructing. To help determine the ratio for a
22
+ # specific combination order, refer to the Interactive
23
+ # Analytics section of the User's Guide.
24
+
25
+ :action, # String: BUY/SELL/SSHORT/SSHORTX The side (buy or sell) for the leg.
26
+ :exchange, # String: exchange to which the complete combo order will be routed.
27
+ :open_close, # int: Specifies whether the order is an open or close order.
28
+ # Valid values: ComboLeg::SAME/OPEN/CLOSE/UNKNOWN
29
+
30
+ # For institutional customers only! For stock legs when doing short sale
31
+ :short_sale_slot, # int: 0 - retail, 1 = clearing broker, 2 = third party
32
+ :designated_location, # String: Only for shortSaleSlot == 2.
33
+ # Otherwise leave blank or orders will be rejected.
34
+ :exempt_code # int: ?
35
+
36
+ DEFAULT_PROPS = {:con_id => 0,
37
+ :ratio => 0,
38
+ :open_close => SAME,
39
+ :short_sale_slot => 0,
40
+ :designated_location => '',
41
+ :exempt_code => -1, }
48
42
 
49
43
  # Some messages include open_close, some don't. wtf.
50
44
  def serialize *fields
@@ -13,19 +13,51 @@ module IB
13
13
  # The exception is for a STK legs, which must specify the SMART exchange.
14
14
  # 2. :symbol => "USD" For combo Contract, this is an arbitrary value (like �USD�)
15
15
 
16
+ attr_reader :legs # leg definitions for this contract.
17
+
18
+ alias combo_legs legs
19
+ alias combo_legs_description legs_description
20
+ alias combo_legs_description= legs_description=
21
+
16
22
  def initialize opts = {}
17
23
  super opts
18
- @sec_type = IB::SECURITY_TYPES[:bag]
24
+ @legs = Array.new
25
+ self[:sec_type] = IB::SECURITY_TYPES[:bag]
19
26
  end
20
27
 
21
28
  def description
22
- @description || to_human
29
+ self[:description] || to_human
23
30
  end
24
31
 
25
32
  def to_human
26
33
  "<Bag: #{[symbol, exchange, currency].join(' ')} legs: #{legs_description} >"
27
34
  end
28
35
 
36
+ ### Leg-related methods
37
+ # TODO: Rewrite with legs and legs_description being strictly in sync...
38
+
39
+ # TODO: Find a way to serialize legs without references...
40
+ # IB-equivalent leg description.
41
+ def legs_description
42
+ self[:legs_description] || legs.map { |leg| "#{leg.con_id}|#{leg.weight}" }.join(',')
43
+ end
44
+
45
+ def serialize_legs *fields
46
+ return [0] if legs.empty?
47
+ [legs.size, legs.map { |leg| leg.serialize *fields }]
48
+ end
49
+
50
+ # Check if two Contracts have same legs (maybe in different order)
51
+ def same_legs? other
52
+ legs == other.legs ||
53
+ legs_description.split(',').sort == other.legs_description.split(',').sort
54
+ end
55
+
56
+ # Contract comparison
57
+ def == other
58
+ super && same_legs?(other)
59
+ end
60
+
29
61
  end # class Bag
30
62
 
31
63
  TYPES[IB::SECURITY_TYPES[:bag]] = Bag
@@ -44,8 +44,8 @@ module IB
44
44
 
45
45
  def initialize opts = {}
46
46
  super opts
47
- @sec_type = IB::SECURITY_TYPES[:option]
48
- @description ||= osi ? osi : "#{symbol} #{strike} #{right} #{expiry}"
47
+ self[:sec_type] = IB::SECURITY_TYPES[:option]
48
+ self[:description] ||= osi ? osi : "#{symbol} #{strike} #{right} #{expiry}"
49
49
  end
50
50
 
51
51
  def to_human