ib-ruby 0.5.15 → 0.5.16

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY CHANGED
@@ -105,3 +105,7 @@
105
105
  == 0.5.15 / 2012-02-01
106
106
 
107
107
  * Docs improvement
108
+
109
+ == 0.5.16 / 2012-02-08
110
+
111
+ * Message specs refactored
data/README.md CHANGED
@@ -41,6 +41,9 @@ localhost if you're running ib-ruby on the same machine as TWS.
41
41
 
42
42
  First, start up Interactive Broker's Trader Work Station or Gateway.
43
43
  Make sure it is configured to allow API connections on localhost.
44
+ 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.
44
47
 
45
48
  >> require 'ib-ruby'
46
49
  >> ib = IB::Connection.new
data/TODO ADDED
@@ -0,0 +1,14 @@
1
+ 1. IB Connection uses delays to prevent hitting 50 msgs/sec limit:
2
+ http://finance.groups.yahoo.com/group/TWSAPI/message/25413
3
+
4
+ 2. Decouple Broker-specific Adapter from universal high-level messaging layer
5
+ (potentially adding other broker adapters)
6
+
7
+ 3. Tweak IB::Message API for speed (use class methods)
8
+
9
+ 4. Create integration tests (Brokerton?)
10
+
11
+ 5. IB#send_message method should accept block, thus compressing subscribe/send_message
12
+ pair into a single call - to simplify DSL.
13
+
14
+ 6. Compatibility check for new TWS v.966
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.15
1
+ 0.5.16
data/bin/cancel_orders CHANGED
@@ -1,29 +1,29 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
- # This script allows you to cancel ALL Orders opened via IB API at once.
4
- # Useful if your robot goes crazy and opens gazillions of wrong limit orders.
3
+ # This script allows you to cancel either a set of open Orders by their ids,
4
+ # or ALL Orders opened via IB API at once. The latter is useful when your
5
+ # robot goes crazy and opens gazillions of wrong limit orders.
5
6
 
7
+ require 'rubygems'
6
8
  require 'pathname'
7
9
  LIB_DIR = (Pathname.new(__FILE__).dirname + '../lib/').realpath.to_s
8
10
  $LOAD_PATH.unshift LIB_DIR unless $LOAD_PATH.include?(LIB_DIR)
9
11
 
10
- require 'rubygems'
11
12
  require 'bundler/setup'
12
13
  require 'ib-ruby'
13
14
 
14
- p ARGV
15
-
16
15
  # First, connect to IB TWS.
17
16
  ib = IB::Connection.new
18
17
 
19
18
  # Subscribe to TWS alerts/errors and order-related messages
20
- ib.subscribe(:Alert, :OpenOrder, :OrderStatus) { |msg| puts msg.to_human }
21
-
22
- ib.send_message :RequestGlobalCancel if ARGV.empty?
19
+ ib.subscribe(:Alert, :OpenOrder, :OrderStatus, :OpenOrderEnd) { |msg| puts msg.to_human }
23
20
 
24
- ARGV.each { |order_id| ib.send_message :CancelOrder, :id => order_id.to_i }
21
+ if ARGV.empty?
22
+ ib.send_message :RequestGlobalCancel
23
+ else
24
+ ARGV.each { |order_id| ib.send_message :CancelOrder, :id => order_id.to_i }
25
+ end
25
26
 
26
27
  ib.send_message :RequestAllOpenOrders
27
28
 
28
- puts "\n******** Press <Enter> to cancel... *********\n\n"
29
- STDIN.gets
29
+ sleep 3
@@ -24,7 +24,8 @@ module IB
24
24
  #:port => '7496', # TWS connection, with annoying pop-ups
25
25
  :client_id => nil, # Will be randomly assigned
26
26
  :connect => true,
27
- :reader => true
27
+ :reader => true,
28
+ :logger => nil
28
29
  }
29
30
 
30
31
  # Singleton to make active Connection universally accessible as IB::Connection.current
@@ -38,6 +39,7 @@ module IB
38
39
  def initialize(opts = {})
39
40
  @options = DEFAULT_OPTIONS.merge(opts)
40
41
 
42
+ self.default_logger = @options[:logger] if @options[:logger]
41
43
  @connected = false
42
44
  @next_order_id = nil
43
45
  @server = Hash.new
@@ -82,8 +84,8 @@ module IB
82
84
 
83
85
  @connected = true
84
86
  log.info "Connected to server, version: #{@server[:version]}, connection time: " +
85
- "#{@server[:local_connect_time]} local, " +
86
- "#{@server[:remote_connect_time]} remote."
87
+ "#{@server[:local_connect_time]} local, " +
88
+ "#{@server[:remote_connect_time]} remote."
87
89
  end
88
90
 
89
91
  alias open connect # Legacy alias
@@ -193,12 +195,6 @@ module IB
193
195
  @next_order_id
194
196
  end
195
197
 
196
- protected
197
-
198
- def random_id
199
- rand 999999999
200
- end
201
-
202
198
  # Start reader thread that continuously reads messages from server in background.
203
199
  # If you don't start reader, you should manually poll @server[:socket] for messages
204
200
  # or use #process_messages(msec) API.
@@ -210,6 +206,12 @@ module IB
210
206
  end
211
207
  end
212
208
 
209
+ protected
210
+
211
+ def random_id
212
+ rand 999999999
213
+ end
214
+
213
215
  end # class Connection
214
216
  #IB = Connection # Legacy alias
215
217
 
@@ -3,32 +3,33 @@ module IB
3
3
 
4
4
  EOL = "\0"
5
5
 
6
- FA_TYPES = {1 => "GROUPS", # FaMsgTypeName
7
- 2 => "PROFILES",
8
- 3 =>"ALIASES"}
9
-
10
6
  # Enumeration of bar size types for convenience.
11
7
  # Bar sizes less than 30 seconds do not work for some securities.
12
- BAR_SIZES = ['1 sec', '5 secs', '15 secs', '30 secs',
13
- '1 min', '2 mins', '3 mins', '5 mins',
14
- '15 mins', '30 mins', '1 hour', '1 day']
8
+ BAR_SIZES = {:sec1 => '1 sec',
9
+ :sec5 => '5 secs',
10
+ :sec15 => '15 secs',
11
+ :sec30 => '30 secs',
12
+ :min1 => '1 min',
13
+ :min2 => '2 mins',
14
+ :min3 => '3 mins',
15
+ :min5 => '5 mins',
16
+ :min15 => '15 mins',
17
+ :min30 => '30 mins',
18
+ :hour1 => '1 hour',
19
+ :day1 => '1 day'}
15
20
 
16
- # Enumeration of data types
17
- DATA_TYPES = [:trades,
18
- :midpoint,
19
- :bid,
20
- :ask
21
- #Determines the nature of data being extracted. Valid values:
22
- # TRADES
23
- #� MIDPOINT
24
- #� BID
25
- #� ASK
26
- #� BID_ASK
27
- #� HISTORICAL_VOLATILITY
28
- #� OPTION_IMPLIED_VOLATILITY
29
- #� OPTION_VOLUME
30
- #� OPTION_OPEN_INTEREST
31
- ]
21
+ # Enumeration of data types.
22
+ # Determines the nature of data being extracted. Valid values:
23
+ DATA_TYPES = {:trades => 'TRADES',
24
+ :midpoint => 'MIDPOINT',
25
+ :bid => 'BID',
26
+ :ask => 'ASK',
27
+ :bid_ask => 'BID_ASK',
28
+ :historical_volatility => 'HISTORICAL_VOLATILITY',
29
+ :option_implied_volatility => 'OPTION_IMPLIED_VOLATILITY',
30
+ :option_volume => 'OPTION_VOLUME',
31
+ :option_open_interest => 'OPTION_OPEN_INTEREST',
32
+ }
32
33
 
33
34
  # Valid security types (sec_type attribute of IB::Contract)
34
35
  SECURITY_TYPES = {:stock => "STK",
@@ -39,6 +40,8 @@ module IB
39
40
  :forex => "CASH",
40
41
  :bag => "BAG"}
41
42
 
43
+ ### These values are typically received from TWS in incoming messages
44
+
42
45
  # Tick types as received in TickPrice and TickSize messages (enumeration)
43
46
  TICK_TYPES = {
44
47
  # int id => :Description # Corresponding API Event/Function/Method
@@ -112,20 +115,21 @@ module IB
112
115
  # Note 3: Applies to bond contracts only.
113
116
  }
114
117
 
115
- #
116
- # Market depth messages contain these "operation" codes to tell you
117
- # what to do with the data.
118
- #
118
+ # Financial Advisor types (FaMsgTypeName)
119
+ FA_TYPES = {1 => 'GROUPS',
120
+ 2 => 'PROFILES',
121
+ 3 =>'ALIASES'}
122
+
123
+ # Market depth messages contain these "operation" codes to tell you what to do with the data.
119
124
  # See also http://www.interactivebrokers.com/php/apiUsersGuide/apiguide/java/updatemktdepth.htm
120
- #
121
125
  MARKET_DEPTH_OPERATIONS = {
122
- 0 => :insert, # New order, insert into the row identified by :position
123
- 1 => :update, # Update the existing order at the row identified by :position
124
- 2 => :delete # Delete the existing order at the row identified by :position
126
+ 0 => :insert, # New order, insert into the row identified by :position
127
+ 1 => :update, # Update the existing order at the row identified by :position
128
+ 2 => :delete # Delete the existing order at the row identified by :position
125
129
  }
126
130
 
127
131
  MARKET_DEPTH_SIDES = {
128
- 0 => :ask,
129
- 1 => :bid
132
+ 0 => :ask,
133
+ 1 => :bid
130
134
  }
131
135
  end # module IB
@@ -1,15 +1,22 @@
1
1
  require "logger"
2
2
 
3
- # Add universally accessible log method/accessor into Object
4
- def log *args
5
- @@logger ||= Logger.new(STDOUT).tap do |logger|
3
+ # Add default_logger accessor into Object
4
+ def default_logger
5
+ @@default_logger ||= Logger.new(STDOUT).tap do |logger|
6
6
  logger.formatter = proc do |level, time, prog, msg|
7
7
  "#{time.strftime('%H:%M:%S.%N')} #{msg}\n"
8
8
  end
9
9
  logger.level = Logger::INFO
10
10
  end
11
+ end
11
12
 
12
- @@logger.tap do |logger|
13
+ def default_logger= logger
14
+ @@default_logger = logger
15
+ end
16
+
17
+ # Add universally accessible log method/accessor into Object
18
+ def log *args
19
+ default_logger.tap do |logger|
13
20
  logger.fatal *args unless args.empty?
14
21
  end
15
22
  end
@@ -50,8 +50,29 @@ module IB
50
50
  self.inspect
51
51
  end
52
52
 
53
+ # Object#id is always defined, we cannot rely on method_missing
54
+ def id
55
+ @data.has_key?(:id) ? @data[:id] : super
56
+ end
57
+
53
58
  protected
54
59
 
60
+ def method_missing method, *args
61
+ getter = method.to_s.sub(/=$/, '').to_sym
62
+ if @data.has_key? method
63
+ @data[method]
64
+ elsif @data.has_key? getter
65
+ @data[getter] = *args
66
+ else
67
+ super method, *args
68
+ end
69
+ end
70
+
71
+ def respond_to? method
72
+ getter = method.to_s.sub(/=$/, '').to_sym
73
+ @data.has_key?(method) || @data.has_key?(getter) || super
74
+ end
75
+
55
76
  # Every message loads received message version first
56
77
  def load
57
78
  @data[:version] = @socket.read_int
@@ -157,7 +178,7 @@ module IB
157
178
  # This message is always sent by TWS automatically at connect.
158
179
  # The IB::Connection class subscribes to it automatically and stores
159
180
  # the order id in its @next_order_id attribute.
160
- NextValidID = def_message 9, [:id, :int]
181
+ NextValidID = def_message(9, [:id, :int]) { "<NextValidID: #{@data[:id]}>" }
161
182
 
162
183
  NewsBulletins =
163
184
  def_message 14, [:id, :int], # unique incrementing bulletin ID.
@@ -193,11 +214,14 @@ module IB
193
214
  FundamentalData = def_message 50, [:id, :int], # request_id
194
215
  [:data, :string]
195
216
 
196
- ContractDataEnd = def_message 52, [:id, :int] # request_id
217
+ ContractDataEnd = def_message(52, [:id, :int]) { "<ContractDataEnd: #{@data[:id]}>" } # request_id
218
+
219
+ OpenOrderEnd = def_message(53) { "<OpenOrderEnd>" }
197
220
 
198
- OpenOrderEnd = def_message 53
221
+ AccountDownloadEnd = def_message(54, [:account_name, :string]) do
222
+ "<AccountDownloadEnd: #{@data[:account_name]}}>"
223
+ end # request_id
199
224
 
200
- AccountDownloadEnd = def_message 54, [:account_name, :string]
201
225
 
202
226
  ExecutionDataEnd = def_message 55, [:id, :int] # request_id
203
227
 
@@ -372,14 +396,6 @@ module IB
372
396
  # It has additional accessors: #code and #message, derived from @data
373
397
  Alert = def_message 4, [:id, :int], [:code, :int], [:message, :string]
374
398
  class Alert
375
- def code
376
- @data && @data[:code]
377
- end
378
-
379
- def message
380
- @data && @data[:message]
381
- end
382
-
383
399
  # Is it an Error message?
384
400
  def error?
385
401
  code < 1000
@@ -397,7 +413,7 @@ module IB
397
413
 
398
414
  def to_human
399
415
  "TWS #{ error? ? 'Error' : system? ? 'System' : 'Warning'
400
- } Message #{@data[:code]}: #{@data[:message]}"
416
+ } Message #{code}: #{message}"
401
417
  end
402
418
  end # class ErrorMessage
403
419
  Error = Alert
@@ -552,14 +568,14 @@ module IB
552
568
  [:market_value, :decimal],
553
569
  [:average_cost, :decimal],
554
570
  [:unrealized_pnl, :decimal], # TODO: Check for Double.MAX_VALUE
555
- [:realized_pnl, :decimal], # TODO: Check for Double.MAX_VALUE
571
+ [:realized_pnl, :decimal], # TODO: Check for Double.MAX_VALUE
556
572
  [:account_name, :string]
557
573
  end
558
574
 
559
575
  def to_human
560
- "<PortfolioValue: #{@contract.to_human} (#{@data[:position]}): Market #{@data[:market_price]}" +
561
- " price #{@data[:market_value]} value; PnL: #{@data[:unrealized_pnl]} unrealized," +
562
- " #{@data[:realized_pnl]} realized; account #{@data[:account_name]}>"
576
+ "<PortfolioValue: #{@contract.to_human} (#{position}): Market #{market_price}" +
577
+ " price #{market_value} value; PnL: #{unrealized_pnl} unrealized," +
578
+ " #{realized_pnl} realized; account #{account_name}>"
563
579
  end
564
580
 
565
581
  end # PortfolioValue
@@ -333,12 +333,15 @@ module IB
333
333
  @version = 4
334
334
 
335
335
  def encode
336
- if @data.has_key?(:what_to_show) && @data[:what_to_show].is_a?(String)
337
- @data[:what_to_show] = @data[:what_to_show].downcase.to_sym
336
+ data_type = DATA_TYPES[@data[:what_to_show]] || @data[:what_to_show]
337
+ unless DATA_TYPES.values.include?(data_type)
338
+ raise ArgumentError(":what_to_show must be one of #{DATA_TYPES}.")
338
339
  end
339
340
 
340
- raise ArgumentError(":what_to_show must be one of #{DATA_TYPES}.") unless DATA_TYPES.include?(@data[:what_to_show])
341
- raise ArgumentError(":bar_size must be one of #{BAR_SIZES}.") unless BAR_SIZES.include?(@data[:bar_size])
341
+ bar_size = BAR_SIZES[@data[:bar_size]] || @data[:bar_size]
342
+ unless BAR_SIZES.values.include?(bar_size)
343
+ raise ArgumentError(":bar_size must be one of #{BAR_SIZES}.")
344
+ end
342
345
 
343
346
  contract = @data[:contract].is_a?(Models::Contract) ?
344
347
  @data[:contract] : Models::Contract.from_ib_ruby(@data[:contract])
@@ -346,10 +349,10 @@ module IB
346
349
  [super,
347
350
  contract.serialize_long(:include_expired),
348
351
  @data[:end_date_time],
349
- @data[:bar_size],
352
+ bar_size,
350
353
  @data[:duration],
351
354
  @data[:use_rth],
352
- @data[:what_to_show].to_s.upcase,
355
+ data_type.to_s.upcase,
353
356
  @data[:format_date],
354
357
  contract.serialize_legs]
355
358
  end
@@ -9,7 +9,15 @@ module IB
9
9
  end
10
10
 
11
11
  def read_string
12
- self.gets(EOL).chop
12
+ string = self.gets(EOL)
13
+
14
+ until string
15
+ # Silently ignores nils
16
+ string = self.gets(EOL)
17
+ sleep 0.1
18
+ end
19
+
20
+ string.chop
13
21
  end
14
22
 
15
23
  def read_int
@@ -64,5 +64,15 @@ module IB
64
64
  :sec_type => SECURITY_TYPES[:forex],
65
65
  :description => "USDJPY")
66
66
  }
67
+
68
+ #.symbol = "EUR"
69
+ #.currency = "USD"
70
+ #.exchange = "IDEALPRO"
71
+ #.secType = "CASH"
72
+ # This is all that is required for an FX contract object.
73
+ # IDEALPRO is for orders over 25,000 and routes to the interbank quote stream.
74
+ # IDEAL is for smaller orders, and has wider spreads/slower execution... generally
75
+ # used for smaller currency conversions.
76
+
67
77
  end # Contracts
68
78
  end
@@ -1,10 +1,14 @@
1
- require File.join(File.dirname(__FILE__), %w[.. spec_helper])
1
+ require 'spec_helper'
2
2
 
3
3
  describe IB::Connection do
4
4
 
5
5
  context 'when connected to IB Gateway', :connected => true do
6
6
  # THIS depends on TWS|Gateway connectivity
7
- before(:all) { @ib = IB::Connection.new }
7
+ before(:all) do
8
+ @ib = IB::Connection.new CONNECTION_OPTS
9
+ @ib.subscribe(:OpenOrderEnd) {}
10
+ end
11
+
8
12
  after(:all) { @ib.close if @ib }
9
13
 
10
14
  context 'instantiation with default options' do
@@ -12,10 +16,10 @@ describe IB::Connection do
12
16
 
13
17
  it { should_not be_nil }
14
18
  it { should be_connected }
15
- its (:server) {should be_a Hash}
16
- its (:server) {should have_key :reader}
17
- its (:subscribers) {should have_at_least(1).item} # :NextValidID and empty Hashes
18
- its (:next_order_id) {should be_a Fixnum} # Not before :NextValidID arrives
19
+ its(:server) {should be_a Hash}
20
+ its(:server) {should have_key :reader}
21
+ its(:subscribers) {should have_at_least(1).item} # :NextValidID and empty Hashes
22
+ its(:next_order_id) {should be_a Fixnum} # Not before :NextValidID arrives
19
23
  end
20
24
 
21
25
  describe '#send_message', 'sending messages' do
@@ -41,9 +45,8 @@ describe IB::Connection do
41
45
 
42
46
  context "subscriptions" do
43
47
 
44
- it '#subscribe, adds (multiple) subscribers' do
48
+ it '#subscribe, adds(multiple) subscribers' do
45
49
  @subscriber_id = @ib.subscribe(IB::Messages::Incoming::Alert, :AccountValue) do
46
- puts "oooooooooo"
47
50
  end
48
51
 
49
52
  @subscriber_id.should be_a Fixnum
@@ -66,7 +69,7 @@ describe IB::Connection do
66
69
  end # subscriptions
67
70
  end # connected
68
71
 
69
- context 'not connected to IB Gateway' do
72
+ context 'when not connected to IB Gateway' do
70
73
  before(:all) { @ib = IB::Connection.new :connect => false, :reader => false }
71
74
 
72
75
  context 'instantiation passing :connect => false' do
@@ -74,10 +77,10 @@ describe IB::Connection do
74
77
 
75
78
  it { should_not be_nil }
76
79
  it { should_not be_connected }
77
- its (:server) {should be_a Hash}
78
- its (:server) {should_not have_key :reader}
79
- its (:subscribers) {should be_empty}
80
- its (:next_order_id) {should be_nil}
80
+ its(:server) {should be_a Hash}
81
+ its(:server) {should_not have_key :reader}
82
+ its(:subscribers) {should be_empty}
83
+ its(:next_order_id) {should be_nil}
81
84
  end
82
85
 
83
86
  end # not connected
@@ -0,0 +1,16 @@
1
+ # WRITING MESSAGE SPECS
2
+
3
+ Pattern for writing message specs is like this:
4
+
5
+ 1. You indicate your interest in some message types by calling 'connect_and_receive'
6
+ in a top-level before(:all) block. All messages of given types will be caught
7
+ and placed into @received Hash, keyed by message type
8
+
9
+ 2. You send request messages to IB and then wait for specific conditions (or timeout)
10
+ by calling 'wait_for' in a context before(:all) block.
11
+
12
+ 3. Once the condition is satisfied, you can test the content of @received Hash
13
+ to see what messages were received, or log_entries Array to see what was logged
14
+
15
+ 4. When done, you call 'close_connection' in a top-level after(:all) block.
16
+
@@ -0,0 +1,84 @@
1
+ require 'message_helper'
2
+
3
+ describe IB::Messages do
4
+
5
+ # Pattern for writing message specs is like this:
6
+ #
7
+ # 1. You indicate your interest in some message types by calling 'connect_and_receive'
8
+ # in a top-level before(:all) block. All messages of given types will be caught
9
+ # and placed into @received Hash, keyed by message type
10
+ #
11
+ # 2. You send request messages to IB and then wait for specific conditions (or timeout)
12
+ # by calling 'wait_for' in a context before(:all) block.
13
+ #
14
+ # 3. Once the condition is satisfied, you can test the content of @received Hash
15
+ # to see what messages were received, or log_entries Array to see what was logged
16
+ #
17
+ # 4. When done, you call 'close_connection' in a top-level after(:all) block.
18
+
19
+ context "Request Account Data", :connected => true do
20
+
21
+ before(:all) do
22
+ connect_and_receive(:Alert, :AccountValue, :AccountDownloadEnd,
23
+ :PortfolioValue, :AccountUpdateTime)
24
+
25
+ @ib.send_message :RequestAccountData, :subscribe => true
26
+
27
+ wait_for(5) { not @received[:AccountDownloadEnd].empty? }
28
+ end
29
+
30
+ after(:all) do
31
+ @ib.send_message :RequestAccountData, :subscribe => false
32
+ close_connection
33
+ end
34
+
35
+ context "received :Alert message " do
36
+ subject { @received[:Alert].first }
37
+
38
+ it { should_not be_nil }
39
+ it { should be_warning }
40
+ it { should_not be_error }
41
+ its(:code) { should be_a Integer }
42
+ its(:message) { should =~ /Market data farm connection is OK/ }
43
+ its(:to_human) { should =~ /TWS Warning Message/ }
44
+ end
45
+
46
+ context "received :AccountValue message" do
47
+ subject { @received[:AccountValue].first }
48
+
49
+ #ps
50
+ it { should_not be_nil }
51
+ its(:data) { should be_a Hash }
52
+ its(:account_name) { should =~ /\w\d/ }
53
+ its(:key) { should be_a String }
54
+ its(:value) { should be_a String }
55
+ its(:currency) { should be_a String }
56
+ its(:to_human) { should =~ /AccountValue/ }
57
+ end
58
+
59
+ context "received :AccountDownloadEnd message" do
60
+ subject { @received[:AccountDownloadEnd].first }
61
+
62
+ it { should_not be_nil }
63
+ its(:data) { should be_a Hash }
64
+ its(:account_name) { should =~ /\w\d/ }
65
+ its(:to_human) { should =~ /AccountDownloadEnd/ }
66
+ end
67
+
68
+ context "received :PortfolioValue message" do
69
+ subject { @received[:PortfolioValue].first }
70
+
71
+ it { should_not be_nil }
72
+ its(:contract) { should be_a IB::Models::Contract }
73
+ its(:data) { should be_a Hash }
74
+ its(:position) { should be_a Integer }
75
+ its(:market_price) { should be_a Float }
76
+ its(:market_value) { should be_a Float }
77
+ its(:average_cost) { should be_a Float }
78
+ its(:unrealized_pnl) { should be_a Float }
79
+ its(:realized_pnl) { should be_a Float }
80
+ its(:account_name) { should =~ /\w\d/ }
81
+ its(:to_human) { should =~ /PortfolioValue/ }
82
+ end
83
+ end # Request Account Data
84
+ end # describe IB::Messages::Incomming
@@ -0,0 +1,31 @@
1
+ require 'message_helper'
2
+
3
+ describe IB::Messages do
4
+
5
+ context 'Normal message exchange at any connection', :connected => true do
6
+
7
+ before(:all) do
8
+ connect_and_receive :NextValidID, :OpenOrderEnd, :Alert
9
+ wait_for(2) { not @received[:OpenOrderEnd].empty? }
10
+ end
11
+
12
+ after(:all) { close_connection }
13
+
14
+ it 'receives :NextValidID message' do
15
+ @received[:NextValidID].should_not be_empty
16
+ end
17
+
18
+ it 'receives :OpenOrderEnd message' do
19
+ @received[:OpenOrderEnd].should_not be_empty
20
+ end
21
+
22
+ it 'logs connection notification' do
23
+ should_log /Connected to server, version: 53, connection time/
24
+ end
25
+
26
+ it 'logs next valid order id' do
27
+ should_log /Got next valid order id/
28
+ end
29
+
30
+ end # Normal message exchange at any connection
31
+ end # describe IB::Messages
@@ -0,0 +1,92 @@
1
+ require 'message_helper'
2
+
3
+ describe IB::Messages do
4
+
5
+ context 'Request Market Data', :connected => true do
6
+
7
+ context 'when subscribed to :Tick... messages' do
8
+
9
+ before(:all) do
10
+ connect_and_receive :Alert, :TickPrice, :TickSize
11
+
12
+ ##TODO consider a follow the sun market lookup for windening the types tested
13
+ @ib.send_message :RequestMarketData, :id => 456,
14
+ :contract => IB::Symbols::Forex[:eurusd]
15
+ wait_for(5) { @received[:TickPrice].size > 3 && @received[:TickSize].size > 1 }
16
+ end
17
+
18
+ after(:all) do
19
+ @ib.send_message :CancelMarketData, :id => 456
20
+ close_connection
21
+ end
22
+
23
+ context "received :Alert message " do
24
+ subject { @received[:Alert].first }
25
+
26
+ it { should_not be_nil }
27
+ it { should be_warning }
28
+ it { should_not be_error }
29
+ its(:code) { should be_an Integer }
30
+ its(:message) { should =~ /Market data farm connection is OK/ }
31
+ its(:to_human) { should =~ /TWS Warning Message/ }
32
+ end
33
+
34
+ context "received :TickPrice message" do
35
+ subject { @received[:TickPrice].first }
36
+
37
+ it { should_not be_nil }
38
+ its(:tick_type) { should be_an Integer }
39
+ its(:type) { should be_a Symbol }
40
+ its(:price) { should be_a Float }
41
+ its(:size) { should be_an Integer }
42
+ its(:data) { should be_a Hash }
43
+ its(:id) { should == 456 } # ticker_id
44
+ its(:to_human) { should =~ /TickPrice/ }
45
+ end
46
+
47
+ context "received :TickSize message" do
48
+ subject { @received[:TickSize].first }
49
+
50
+ it { should_not be_nil }
51
+ its(:type) { should_not be_nil }
52
+ its(:data) { should be_a Hash }
53
+ its(:tick_type) { should be_an Integer }
54
+ its(:type) { should be_a Symbol }
55
+ its(:size) { should be_an Integer }
56
+ its(:id) { should == 456 }
57
+ its(:to_human) { should =~ /TickSize/ }
58
+ end
59
+ end # when subscribed to :Tick... messages
60
+
61
+ context 'when NOT subscribed to :Tick... messages' do
62
+
63
+ before(:all) do
64
+ connect_and_receive :NextValidID
65
+
66
+ @ib.send_message :RequestMarketData, :id => 456,
67
+ :contract => IB::Symbols::Forex[:eurusd]
68
+ wait_for(2)
69
+ end
70
+
71
+ after(:all) do
72
+ @ib.send_message :CancelMarketData, :id => 456
73
+ close_connection
74
+ end
75
+
76
+ it "logs warning about unhandled :OpenOrderEnd message" do
77
+ should_log /No subscribers for message IB::Messages::Incoming::OpenOrderEnd/
78
+ end
79
+
80
+ it "logs warning about unhandled :Alert message" do
81
+ should_log /No subscribers for message IB::Messages::Incoming::Alert/
82
+ end
83
+
84
+ it "logs warning about unhandled :Tick... messages" do
85
+ should_log /No subscribers for message IB::Messages::Incoming::TickPrice/,
86
+ /No subscribers for message IB::Messages::Incoming::TickSize/
87
+ end
88
+
89
+ end # NOT subscribed to :Tick... messages
90
+
91
+ end # Request Market Data
92
+ end # describe IB::Messages
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
1
+ require 'spec_helper'
2
2
 
3
3
  describe IB::Models::ComboLeg do
4
4
 
@@ -18,13 +18,13 @@ describe IB::Models::ComboLeg do
18
18
  subject { IB::Models::ComboLeg.new }
19
19
 
20
20
  it { should_not be_nil }
21
- its (:con_id) {should == 0}
22
- its (:ratio) {should == 0}
23
- its (:open_close) {should == 0}
24
- its (:short_sale_slot) {should == 0}
25
- its (:exempt_code) {should == -1}
21
+ its(:con_id) {should == 0}
22
+ its(:ratio) {should == 0}
23
+ its(:open_close) {should == 0}
24
+ its(:short_sale_slot) {should == 0}
25
+ its(:exempt_code) {should == -1}
26
26
 
27
- its (:created_at) {should be_a Time}
27
+ its(:created_at) {should be_a Time}
28
28
  end
29
29
 
30
30
  context 'with properties' do
@@ -37,7 +37,7 @@ describe IB::Models::ComboLeg do
37
37
  end
38
38
 
39
39
  context 'essential properties are still set, even if not given explicitely' do
40
- its (:created_at) {should be_a Time}
40
+ its(:created_at) {should be_a Time}
41
41
  end
42
42
  end
43
43
 
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
1
+ require 'spec_helper'
2
2
 
3
3
  describe IB::Models::Contract do
4
4
 
@@ -19,11 +19,11 @@ describe IB::Models::Contract do
19
19
  subject { IB::Models::Contract.new }
20
20
 
21
21
  it { should_not be_nil }
22
- its (:con_id) {should == 0}
23
- its (:strike) {should == 0}
24
- its (:sec_type) {should == ''}
25
- its (:created_at) {should be_a Time}
26
- its (:include_expired) {should == false}
22
+ its(:con_id) {should == 0}
23
+ its(:strike) {should == 0}
24
+ its(:sec_type) {should == ''}
25
+ its(:created_at) {should be_a Time}
26
+ its(:include_expired) {should == false}
27
27
  end
28
28
 
29
29
  context 'with properties' do
@@ -36,9 +36,9 @@ describe IB::Models::Contract do
36
36
  end
37
37
 
38
38
  context 'essential properties are still set, even if not given explicitely' do
39
- its (:con_id) {should == 0}
40
- its (:created_at) {should be_a Time}
41
- its (:include_expired) {should == false}
39
+ its(:con_id) {should == 0}
40
+ its(:created_at) {should be_a Time}
41
+ its(:include_expired) {should == false}
42
42
  end
43
43
  end
44
44
 
@@ -56,22 +56,22 @@ describe IB::Models::Contract do
56
56
  context 'empty without properties' do
57
57
  subject { IB::Models::Contract.new }
58
58
 
59
- its (:summary) {should == subject}
60
- its (:under_con_id) {should == 0}
61
- its (:min_tick) {should == 0}
62
- its (:callable) {should == false}
63
- its (:puttable) {should == false}
64
- its (:coupon) {should == 0}
65
- its (:convertible) {should == false}
66
- its (:next_option_partial) {should == false}
67
- its (:created_at) {should be_a Time}
59
+ its(:summary) {should == subject}
60
+ its(:under_con_id) {should == 0}
61
+ its(:min_tick) {should == 0}
62
+ its(:callable) {should == false}
63
+ its(:puttable) {should == false}
64
+ its(:coupon) {should == 0}
65
+ its(:convertible) {should == false}
66
+ its(:next_option_partial) {should == false}
67
+ its(:created_at) {should be_a Time}
68
68
  end
69
69
 
70
70
  context 'with properties' do
71
71
  subject { IB::Models::Contract.new detailed_properties }
72
72
 
73
- its (:summary) {should == subject}
74
- its (:created_at) {should be_a Time}
73
+ its(:summary) {should == subject}
74
+ its(:created_at) {should be_a Time}
75
75
 
76
76
  it 'sets properties right' do
77
77
  detailed_properties.each do |name, value|
@@ -1,4 +1,4 @@
1
- require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
1
+ require 'spec_helper'
2
2
 
3
3
  describe IB::Models::Order do
4
4
 
@@ -19,16 +19,16 @@ describe IB::Models::Order do
19
19
  subject { IB::Models::Order.new }
20
20
 
21
21
  it { should_not be_nil }
22
- its (:outside_rth) {should == false}
23
- its (:open_close) {should == "O"}
24
- its (:origin) {should == IB::Models::Order::Origin_Customer}
25
- its (:transmit) {should == true}
26
- its (:designated_location) {should == ''}
27
- its (:exempt_code) {should == -1}
28
- its (:delta_neutral_order_type) {should == ''}
29
- its (:what_if) {should == false}
30
- its (:not_held) {should == false}
31
- its (:created_at) {should be_a Time}
22
+ its(:outside_rth) {should == false}
23
+ its(:open_close) {should == "O"}
24
+ its(:origin) {should == IB::Models::Order::Origin_Customer}
25
+ its(:transmit) {should == true}
26
+ its(:designated_location) {should == ''}
27
+ its(:exempt_code) {should == -1}
28
+ its(:delta_neutral_order_type) {should == ''}
29
+ its(:what_if) {should == false}
30
+ its(:not_held) {should == false}
31
+ its(:created_at) {should be_a Time}
32
32
  end
33
33
 
34
34
  context 'with properties' do
@@ -41,7 +41,7 @@ describe IB::Models::Order do
41
41
  end
42
42
 
43
43
  context 'essential properties are still set, even if not given explicitely' do
44
- its (:created_at) {should be_a Time}
44
+ its(:created_at) {should be_a Time}
45
45
  end
46
46
  end
47
47
 
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+ require 'thread'
3
+ require 'stringio'
4
+
5
+ # Given an IB message, retuns its type Symbol (e.g. :OpenOrderEnd)
6
+ def message_type msg
7
+ msg.class.to_s.split(/::/).last.to_sym
8
+ end
9
+
10
+ def print_subject
11
+ it 'prints out message' do
12
+ p subject
13
+ p subject.to_human
14
+ end
15
+ end
16
+
17
+ alias ps print_subject
18
+
19
+ ## Logger helpers
20
+
21
+ def mock_logger
22
+ @stdout = StringIO.new
23
+
24
+ @logger = Logger.new(@stdout).tap do |logger|
25
+ logger.formatter = proc do |level, time, prog, msg|
26
+ "#{time.strftime('%H:%M:%S.%N')} #{msg}\n"
27
+ end
28
+ logger.level = Logger::INFO
29
+ end
30
+ end
31
+
32
+ def log_entries
33
+ @stdout && @stdout.string.split(/\n/)
34
+ end
35
+
36
+ def should_log *patterns
37
+ patterns.each do |pattern|
38
+ log_entries.any? { |entry| entry =~ pattern }.should be_true
39
+ end
40
+ end
41
+
42
+ ## Connection helpers
43
+
44
+ def connect_and_receive *message_types
45
+
46
+ # Start disconnected (we need to set up catch-all subscriber first)
47
+ @ib = IB::Connection.new CONNECTION_OPTS.merge(:connect => false,
48
+ :reader => false,
49
+ :logger => mock_logger)
50
+
51
+ # Hash of received messages, keyed by message type
52
+ @received = Hash.new { |hash, key| hash[key] = Array.new }
53
+
54
+ # Catch all messages of given types and put them inside @received Hash
55
+ @ib.subscribe(*message_types) { |msg| @received[message_type(msg)] << msg }
56
+
57
+ @ib.connect
58
+ @ib.start_reader
59
+ end
60
+
61
+ def close_connection
62
+ @ib.close if @ib
63
+ puts log_entries
64
+ p @received.map { |type, msg| [type, msg.size] }
65
+ end
66
+
67
+ #noinspection RubyArgCount
68
+ def wait_for time = 1, &condition
69
+ timeout = Time.now + time
70
+ sleep 0.1 until timeout < Time.now || condition && condition.call
71
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,5 @@
1
- require File.expand_path(
2
- File.join(File.dirname(__FILE__), %w[.. lib ib-ruby]))
1
+ require 'rspec'
2
+ require 'ib-ruby'
3
3
 
4
4
  RSpec.configure do |config|
5
5
  # config.exclusion_filter = { :slow => true }
@@ -9,3 +9,12 @@ RSpec.configure do |config|
9
9
  # config.mock_with :flexmock
10
10
  # config.mock_with :rr
11
11
  end
12
+
13
+ BROKERTRON = false
14
+
15
+ CONNECTION_OPTS = BROKERTRON ?
16
+ {:client_id => 1111,
17
+ :host => 'free.brokertron.com',
18
+ :port=> 10501
19
+ } :
20
+ {:client_id => 1111}
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: ib-ruby
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.5.15
5
+ version: 0.5.16
6
6
  platform: ruby
7
7
  authors:
8
8
  - Paul Legato
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2012-02-01 00:00:00 Z
14
+ date: 2012-02-09 00:00:00 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -32,7 +32,7 @@ dependencies:
32
32
  requirements:
33
33
  - - ">="
34
34
  - !ruby/object:Gem::Version
35
- version: 2.5.0
35
+ version: 2.7.0
36
36
  type: :development
37
37
  version_requirements: *id002
38
38
  - !ruby/object:Gem::Dependency
@@ -104,8 +104,13 @@ files:
104
104
  - lib/ib-ruby/symbols/options.rb
105
105
  - lib/ib-ruby/symbols/stocks.rb
106
106
  - spec/ib-ruby_spec.rb
107
+ - spec/message_helper.rb
107
108
  - spec/spec_helper.rb
108
109
  - spec/ib-ruby/connection_spec.rb
110
+ - spec/ib-ruby/messages/account_info_spec.rb
111
+ - spec/ib-ruby/messages/just_connect_spec.rb
112
+ - spec/ib-ruby/messages/market_data_spec.rb
113
+ - spec/ib-ruby/messages/README.md
109
114
  - spec/ib-ruby/models/combo_leg_spec.rb
110
115
  - spec/ib-ruby/models/contract_spec.rb
111
116
  - spec/ib-ruby/models/order_spec.rb
@@ -120,6 +125,7 @@ files:
120
125
  - LICENSE
121
126
  - VERSION
122
127
  - HISTORY
128
+ - TODO
123
129
  - .gitignore
124
130
  homepage: https://github.com/pjlegato/ib-ruby
125
131
  licenses: []
@@ -150,8 +156,13 @@ specification_version: 3
150
156
  summary: Ruby Implementation of the Interactive Brokers TWS API
151
157
  test_files:
152
158
  - spec/ib-ruby_spec.rb
159
+ - spec/message_helper.rb
153
160
  - spec/spec_helper.rb
154
161
  - spec/ib-ruby/connection_spec.rb
162
+ - spec/ib-ruby/messages/account_info_spec.rb
163
+ - spec/ib-ruby/messages/just_connect_spec.rb
164
+ - spec/ib-ruby/messages/market_data_spec.rb
165
+ - spec/ib-ruby/messages/README.md
155
166
  - spec/ib-ruby/models/combo_leg_spec.rb
156
167
  - spec/ib-ruby/models/contract_spec.rb
157
168
  - spec/ib-ruby/models/order_spec.rb