ib-ruby 0.5.15 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|