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 +4 -0
- data/README.md +3 -0
- data/TODO +14 -0
- data/VERSION +1 -1
- data/bin/cancel_orders +11 -11
- data/lib/ib-ruby/connection.rb +11 -9
- data/lib/ib-ruby/constants.rb +37 -33
- data/lib/ib-ruby/logger.rb +11 -4
- data/lib/ib-ruby/messages/incoming.rb +33 -17
- data/lib/ib-ruby/messages/outgoing.rb +9 -6
- data/lib/ib-ruby/socket.rb +9 -1
- data/lib/ib-ruby/symbols/forex.rb +10 -0
- data/spec/ib-ruby/connection_spec.rb +16 -13
- data/spec/ib-ruby/messages/README.md +16 -0
- data/spec/ib-ruby/messages/account_info_spec.rb +84 -0
- data/spec/ib-ruby/messages/just_connect_spec.rb +31 -0
- data/spec/ib-ruby/messages/market_data_spec.rb +92 -0
- data/spec/ib-ruby/models/combo_leg_spec.rb +8 -8
- data/spec/ib-ruby/models/contract_spec.rb +20 -20
- data/spec/ib-ruby/models/order_spec.rb +12 -12
- data/spec/message_helper.rb +71 -0
- data/spec/spec_helper.rb +11 -2
- metadata +14 -3
data/HISTORY
CHANGED
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.
|
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
|
4
|
-
#
|
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.
|
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
|
-
|
29
|
-
STDIN.gets
|
29
|
+
sleep 3
|
data/lib/ib-ruby/connection.rb
CHANGED
@@ -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
|
-
|
86
|
-
|
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
|
|
data/lib/ib-ruby/constants.rb
CHANGED
@@ -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 =
|
13
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
19
|
-
:
|
20
|
-
:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
117
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
129
|
-
|
132
|
+
0 => :ask,
|
133
|
+
1 => :bid
|
130
134
|
}
|
131
135
|
end # module IB
|
data/lib/ib-ruby/logger.rb
CHANGED
@@ -1,15 +1,22 @@
|
|
1
1
|
require "logger"
|
2
2
|
|
3
|
-
# Add
|
4
|
-
def
|
5
|
-
@@
|
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
|
-
|
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
|
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
|
217
|
+
ContractDataEnd = def_message(52, [:id, :int]) { "<ContractDataEnd: #{@data[:id]}>" } # request_id
|
218
|
+
|
219
|
+
OpenOrderEnd = def_message(53) { "<OpenOrderEnd>" }
|
197
220
|
|
198
|
-
|
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 #{
|
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],
|
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} (#{
|
561
|
-
" price #{
|
562
|
-
" #{
|
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
|
-
|
337
|
-
|
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
|
-
|
341
|
-
|
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
|
-
|
352
|
+
bar_size,
|
350
353
|
@data[:duration],
|
351
354
|
@data[:use_rth],
|
352
|
-
|
355
|
+
data_type.to_s.upcase,
|
353
356
|
@data[:format_date],
|
354
357
|
contract.serialize_legs]
|
355
358
|
end
|
data/lib/ib-ruby/socket.rb
CHANGED
@@ -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
|
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)
|
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
|
16
|
-
its
|
17
|
-
its
|
18
|
-
its
|
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
|
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
|
78
|
-
its
|
79
|
-
its
|
80
|
-
its
|
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
|
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
|
22
|
-
its
|
23
|
-
its
|
24
|
-
its
|
25
|
-
its
|
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
|
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
|
40
|
+
its(:created_at) {should be_a Time}
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
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
|
23
|
-
its
|
24
|
-
its
|
25
|
-
its
|
26
|
-
its
|
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
|
40
|
-
its
|
41
|
-
its
|
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
|
60
|
-
its
|
61
|
-
its
|
62
|
-
its
|
63
|
-
its
|
64
|
-
its
|
65
|
-
its
|
66
|
-
its
|
67
|
-
its
|
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
|
74
|
-
its
|
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
|
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
|
23
|
-
its
|
24
|
-
its
|
25
|
-
its
|
26
|
-
its
|
27
|
-
its
|
28
|
-
its
|
29
|
-
its
|
30
|
-
its
|
31
|
-
its
|
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
|
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
|
2
|
-
|
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.
|
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-
|
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.
|
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
|