ib-api 972.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +105 -0
- data/Guardfile +24 -0
- data/LICENSE +674 -0
- data/README.md +65 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/api.gemspec +43 -0
- data/bin/console +95 -0
- data/bin/console.yml +3 -0
- data/bin/setup +8 -0
- data/changelog.md +7 -0
- data/example/README.md +76 -0
- data/example/account_info +54 -0
- data/example/account_positions +30 -0
- data/example/account_summary +88 -0
- data/example/cancel_orders +74 -0
- data/example/fa_accounts +25 -0
- data/example/fundamental_data +40 -0
- data/example/historic_data_cli +186 -0
- data/example/list_orders +45 -0
- data/example/portfolio_csv +81 -0
- data/example/scanner_data +62 -0
- data/example/template +19 -0
- data/example/tick_data +28 -0
- data/lib/extensions/class-extensions.rb +87 -0
- data/lib/ib-api.rb +7 -0
- data/lib/ib/base.rb +103 -0
- data/lib/ib/base_properties.rb +160 -0
- data/lib/ib/connection.rb +450 -0
- data/lib/ib/constants.rb +393 -0
- data/lib/ib/errors.rb +44 -0
- data/lib/ib/logger.rb +26 -0
- data/lib/ib/messages.rb +99 -0
- data/lib/ib/messages/abstract_message.rb +101 -0
- data/lib/ib/messages/incoming.rb +251 -0
- data/lib/ib/messages/incoming/abstract_message.rb +116 -0
- data/lib/ib/messages/incoming/account_value.rb +78 -0
- data/lib/ib/messages/incoming/alert.rb +34 -0
- data/lib/ib/messages/incoming/contract_data.rb +102 -0
- data/lib/ib/messages/incoming/delta_neutral_validation.rb +23 -0
- data/lib/ib/messages/incoming/execution_data.rb +50 -0
- data/lib/ib/messages/incoming/historical_data.rb +84 -0
- data/lib/ib/messages/incoming/market_depths.rb +44 -0
- data/lib/ib/messages/incoming/next_valid_id.rb +18 -0
- data/lib/ib/messages/incoming/open_order.rb +277 -0
- data/lib/ib/messages/incoming/order_status.rb +85 -0
- data/lib/ib/messages/incoming/portfolio_value.rb +78 -0
- data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
- data/lib/ib/messages/incoming/scanner_data.rb +54 -0
- data/lib/ib/messages/incoming/ticks.rb +268 -0
- data/lib/ib/messages/outgoing.rb +437 -0
- data/lib/ib/messages/outgoing/abstract_message.rb +88 -0
- data/lib/ib/messages/outgoing/account_requests.rb +112 -0
- data/lib/ib/messages/outgoing/bar_requests.rb +250 -0
- data/lib/ib/messages/outgoing/place_order.rb +209 -0
- data/lib/ib/messages/outgoing/request_marketdata.rb +99 -0
- data/lib/ib/messages/outgoing/request_tick_data.rb +21 -0
- data/lib/ib/model.rb +4 -0
- data/lib/ib/models.rb +14 -0
- data/lib/ib/server_versions.rb +114 -0
- data/lib/ib/socket.rb +185 -0
- data/lib/ib/support.rb +160 -0
- data/lib/ib/version.rb +6 -0
- data/lib/models/ib/account.rb +85 -0
- data/lib/models/ib/account_value.rb +33 -0
- data/lib/models/ib/bag.rb +55 -0
- data/lib/models/ib/bar.rb +31 -0
- data/lib/models/ib/combo_leg.rb +105 -0
- data/lib/models/ib/condition.rb +245 -0
- data/lib/models/ib/contract.rb +415 -0
- data/lib/models/ib/contract_detail.rb +108 -0
- data/lib/models/ib/execution.rb +67 -0
- data/lib/models/ib/forex.rb +13 -0
- data/lib/models/ib/future.rb +15 -0
- data/lib/models/ib/index.rb +15 -0
- data/lib/models/ib/option.rb +78 -0
- data/lib/models/ib/option_detail.rb +55 -0
- data/lib/models/ib/order.rb +519 -0
- data/lib/models/ib/order_state.rb +152 -0
- data/lib/models/ib/portfolio_value.rb +64 -0
- data/lib/models/ib/stock.rb +16 -0
- data/lib/models/ib/underlying.rb +34 -0
- data/lib/models/ib/vertical.rb +96 -0
- data/lib/requires.rb +12 -0
- metadata +203 -0
data/lib/ib/socket.rb
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'socket'
|
2
|
+
module IBSupport
|
3
|
+
refine Array do
|
4
|
+
def tws
|
5
|
+
if blank?
|
6
|
+
nil.tws
|
7
|
+
else
|
8
|
+
self.flatten.map( &:tws ).join # [ "", [] , nil].flatten -> ["", nil]
|
9
|
+
# elemets with empty array's are cut
|
10
|
+
# this is the desired behavior!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
refine Symbol do
|
15
|
+
def tws
|
16
|
+
self.to_s.tws
|
17
|
+
end
|
18
|
+
end
|
19
|
+
refine String do
|
20
|
+
def tws
|
21
|
+
if empty?
|
22
|
+
IB::EOL
|
23
|
+
else
|
24
|
+
self[-1] == IB::EOL ? self : self+IB::EOL
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
refine Numeric do
|
30
|
+
def tws
|
31
|
+
self.to_s.tws
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
refine TrueClass do
|
36
|
+
def tws
|
37
|
+
1.tws
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
refine FalseClass do
|
42
|
+
def tws
|
43
|
+
0.tws
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
refine NilClass do
|
48
|
+
def tws
|
49
|
+
IB::EOL
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
module IB
|
54
|
+
# includes methods from IBSupport
|
55
|
+
# which adds a tws-method to
|
56
|
+
# - Array
|
57
|
+
# - Symbol
|
58
|
+
# - String
|
59
|
+
# - Numeric
|
60
|
+
# - TrueClass, FalseClass and NilClass
|
61
|
+
#
|
62
|
+
module PrepareData
|
63
|
+
using IBSupport
|
64
|
+
# First call the method #tws on the data-object
|
65
|
+
#
|
66
|
+
# Then transfom into an Array using the #Pack-Method
|
67
|
+
#
|
68
|
+
# The optional Block introduces a user-defined pattern to pack the data.
|
69
|
+
#
|
70
|
+
# Default is "Na*"
|
71
|
+
def prepare_message data
|
72
|
+
data = data.tws unless data.is_a?(String) && data[-1]== EOL
|
73
|
+
matrize = [data.size,data]
|
74
|
+
if block_given? # A user defined decoding-sequence is accepted via block
|
75
|
+
matrize.pack yield
|
76
|
+
else
|
77
|
+
matrize.pack "Na*"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# The received package is decoded. The parameter (msg) is an Array
|
82
|
+
#
|
83
|
+
# The protocol is simple: Every Element is treated as Character.
|
84
|
+
# Exception: The first Element determines the expected length.
|
85
|
+
#
|
86
|
+
# The decoded raw-message can further modified by the optional block.
|
87
|
+
#
|
88
|
+
# The default is to instantiate a Hash: message_id becomes the key.
|
89
|
+
# The Hash is returned
|
90
|
+
#
|
91
|
+
# If a block is provided, no Hash is build and the modified raw-message is returned
|
92
|
+
def decode_message msg
|
93
|
+
m = Hash.new
|
94
|
+
while not msg.blank?
|
95
|
+
# the first item is the length
|
96
|
+
size= msg[0..4].unpack("N").first
|
97
|
+
msg = msg[4..-1]
|
98
|
+
# followed by a sequence of characters
|
99
|
+
message = msg.unpack("A#{size}").first.split("\0")
|
100
|
+
if block_given?
|
101
|
+
yield message
|
102
|
+
else
|
103
|
+
m[message.shift.to_i] = message
|
104
|
+
end
|
105
|
+
msg = msg[size..-1]
|
106
|
+
end
|
107
|
+
return m unless block_given?
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
class IBSocket < TCPSocket
|
113
|
+
include PrepareData
|
114
|
+
using IBSupport
|
115
|
+
|
116
|
+
def initialising_handshake
|
117
|
+
v100_prefix = "API".tws.encode 'ascii'
|
118
|
+
v100_version = self.prepare_message Messages::SERVER_VERSION
|
119
|
+
write_data v100_prefix+v100_version
|
120
|
+
## start tws-log
|
121
|
+
# [QO] INFO [JTS-SocketListener-49] - State: HEADER, IsAPI: UNKNOWN
|
122
|
+
# [QO] INFO [JTS-SocketListener-49] - State: STOP, IsAPI: YES
|
123
|
+
# [QO] INFO [JTS-SocketListener-49] - ArEServer: Adding 392382055 with id 2147483647
|
124
|
+
# [QO] INFO [JTS-SocketListener-49] - eServersChanged: 1
|
125
|
+
# [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Starting new conversation with client on 127.0.0.1
|
126
|
+
# [QO] INFO [JTS-EServerSocketNotifier-288] - Starting async queue thread
|
127
|
+
# [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Server version is 136
|
128
|
+
# [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Client version is 136
|
129
|
+
# [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] is 3rdParty true
|
130
|
+
## end tws-log
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def read_string
|
135
|
+
string = self.gets(EOL)
|
136
|
+
|
137
|
+
until string
|
138
|
+
# Silently ignores nils
|
139
|
+
string = self.gets(EOL)
|
140
|
+
sleep 0.1
|
141
|
+
end
|
142
|
+
|
143
|
+
string.chomp
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
# Sends null terminated data string into socket
|
148
|
+
def write_data data
|
149
|
+
self.syswrite data.tws
|
150
|
+
end
|
151
|
+
|
152
|
+
# send the message (containing several instructions) to the socket,
|
153
|
+
# calls prepare_message to convert data-elements into NULL-terminated strings
|
154
|
+
def send_messages *data
|
155
|
+
self.syswrite prepare_message(data)
|
156
|
+
rescue Errno::ECONNRESET => e
|
157
|
+
Connection.logger.error{ "Data not accepted by IB \n
|
158
|
+
#{data.inspect} \n
|
159
|
+
Backtrace:\n "}
|
160
|
+
Connection.logger.error e.backtrace
|
161
|
+
end
|
162
|
+
|
163
|
+
def recieve_messages
|
164
|
+
begin
|
165
|
+
complete_message_buffer = []
|
166
|
+
begin
|
167
|
+
# this is the blocking version of recv
|
168
|
+
buffer = self.recvfrom(4096)[0]
|
169
|
+
# STDOUT.puts "BUFFER:: #{buffer.inspect}"
|
170
|
+
complete_message_buffer << buffer
|
171
|
+
|
172
|
+
end while buffer.size == 4096
|
173
|
+
complete_message_buffer.join('')
|
174
|
+
rescue Errno::ECONNRESET => e
|
175
|
+
Connection.logger.error{ "Data Buffer is not filling \n
|
176
|
+
The Buffer: #{buffer.inspect} \n
|
177
|
+
Backtrace:\n
|
178
|
+
#{e.backtrace.join("\n") } " }
|
179
|
+
Kernel.exit
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
end # class IBSocket
|
184
|
+
|
185
|
+
end # module IB
|
data/lib/ib/support.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
|
2
|
+
module IBSupport
|
3
|
+
refine Array do
|
4
|
+
|
5
|
+
def zero?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
# Returns the integer.
|
9
|
+
# retuns nil otherwise or if no element is left on the stack
|
10
|
+
def read_int
|
11
|
+
i= self.shift rescue nil
|
12
|
+
i = i.to_i unless i.blank? # this includes conversion of string to zero(0)
|
13
|
+
i.is_a?( Integer ) ? i : nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_float
|
17
|
+
i= self.shift rescue nil
|
18
|
+
i = i.to_f unless i.blank?
|
19
|
+
|
20
|
+
end
|
21
|
+
def read_decimal
|
22
|
+
i= self.shift rescue nil
|
23
|
+
i = i.to_d unless i.blank?
|
24
|
+
i.is_a?(Numeric) && i < IB::TWS_MAX ? i : nil # return nil, if a very large number is transmitted
|
25
|
+
end
|
26
|
+
|
27
|
+
alias read_decimal_max read_decimal
|
28
|
+
|
29
|
+
## Values -1 and below indicate: Not computed (TickOptionComputation)
|
30
|
+
def read_decimal_limit_1
|
31
|
+
i= read_decimal
|
32
|
+
i <= -1 ? nil : i
|
33
|
+
end
|
34
|
+
|
35
|
+
## Values -2 and below indicate: Not computed (TickOptionComputation)
|
36
|
+
def read_decimal_limit_2
|
37
|
+
i= read_decimal
|
38
|
+
i <= -2 ? nil : i
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def read_string
|
43
|
+
self.shift rescue ""
|
44
|
+
end
|
45
|
+
## reads a string and proofs if NULL == IB::TWS_MAX is present.
|
46
|
+
## in that case: returns nil. otherwise: returns the string
|
47
|
+
def read_string_not_null
|
48
|
+
r = read_string
|
49
|
+
rd = r.to_d unless r.blank?
|
50
|
+
rd.is_a?(Numeric) && rd >= IB::TWS_MAX ? nil : r
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_symbol
|
54
|
+
read_string.to_sym
|
55
|
+
end
|
56
|
+
|
57
|
+
# convert xml into a hash
|
58
|
+
def read_xml
|
59
|
+
Ox.load( read_string(), mode: :hash_no_attrs)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def read_int_date
|
64
|
+
t= read_int
|
65
|
+
s= Time.at(t)
|
66
|
+
# s.year == 1970 --> data is most likely a date-string
|
67
|
+
s.year == 1970 ? Date.parse(t.to_s) : s
|
68
|
+
end
|
69
|
+
|
70
|
+
def read_parse_date
|
71
|
+
Time.parse read_string
|
72
|
+
end
|
73
|
+
|
74
|
+
def read_boolean
|
75
|
+
|
76
|
+
v = self.shift rescue nil
|
77
|
+
case v
|
78
|
+
when "1"
|
79
|
+
true
|
80
|
+
when "0"
|
81
|
+
false
|
82
|
+
else nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def read_datetime
|
88
|
+
the_string = read_string
|
89
|
+
the_string.blank? ? nil : DateTime.parse(the_string)
|
90
|
+
end
|
91
|
+
|
92
|
+
def read_date
|
93
|
+
the_string = read_string
|
94
|
+
the_string.blank? ? nil : Date.parse(the_string)
|
95
|
+
end
|
96
|
+
# def read_array
|
97
|
+
# count = read_int
|
98
|
+
# end
|
99
|
+
|
100
|
+
## originally provided in socket.rb
|
101
|
+
# # Returns loaded Array or [] if count was 0#
|
102
|
+
#
|
103
|
+
# Without providing a Block, the elements are treated as string
|
104
|
+
def read_array hashmode:false, &block
|
105
|
+
count = read_int
|
106
|
+
case count
|
107
|
+
when 0
|
108
|
+
[]
|
109
|
+
when nil
|
110
|
+
nil
|
111
|
+
else
|
112
|
+
count= count + count if hashmode
|
113
|
+
if block_given?
|
114
|
+
Array.new(count, &block)
|
115
|
+
else
|
116
|
+
Array.new( count ){ read_string }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
#
|
121
|
+
# Returns a hash
|
122
|
+
# Expected Buffer-Format:
|
123
|
+
# count (of Hash-elements)
|
124
|
+
# count* key|Value
|
125
|
+
# Key's are transformed to symbols, values are treated as string
|
126
|
+
def read_hash
|
127
|
+
tags = read_array( hashmode: true ) # { |_| [read_string, read_string] }
|
128
|
+
result = if tags.nil? || tags.flatten.empty?
|
129
|
+
tags
|
130
|
+
else
|
131
|
+
interim = if tags.size.modulo(2).zero?
|
132
|
+
Hash[*tags.flatten]
|
133
|
+
else
|
134
|
+
Hash[*tags[0..-2].flatten] # omit the last element
|
135
|
+
end
|
136
|
+
# symbolize Hash
|
137
|
+
Hash[interim.map { |k, v| [k.to_sym, v] unless k.nil? }.compact]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
#
|
141
|
+
|
142
|
+
def read_contract # read a standard contract and return als hash
|
143
|
+
{ con_id: read_int,
|
144
|
+
symbol: read_string,
|
145
|
+
sec_type: read_string,
|
146
|
+
expiry: read_string,
|
147
|
+
strike: read_decimal,
|
148
|
+
right: read_string,
|
149
|
+
multiplier: read_int,
|
150
|
+
exchange: read_string,
|
151
|
+
currency: read_string,
|
152
|
+
local_symbol: read_string,
|
153
|
+
trading_class: read_string } # new Version 8
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
alias read_bool read_boolean
|
159
|
+
end
|
160
|
+
end
|
data/lib/ib/version.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module IB
|
2
|
+
class Account < IB::Model
|
3
|
+
include BaseProperties
|
4
|
+
# include Redis::Objects
|
5
|
+
# attr_accessible :alias, :account, :connected
|
6
|
+
|
7
|
+
prop :account, # String
|
8
|
+
:alias, #
|
9
|
+
:type,
|
10
|
+
:last_updated,
|
11
|
+
:connected => :bool
|
12
|
+
|
13
|
+
# redis_id_field :account
|
14
|
+
# value :my_alias
|
15
|
+
# value :the_account
|
16
|
+
# value :active
|
17
|
+
|
18
|
+
|
19
|
+
validates_format_of :account, :with => /\A[D]?[UF]{1}\d{5,8}\z/ , :message => 'should be (X)X00000'
|
20
|
+
|
21
|
+
# in tableless mode the scope is ignored
|
22
|
+
|
23
|
+
has_many :account_values
|
24
|
+
has_many :portfolio_values
|
25
|
+
has_many :contracts
|
26
|
+
has_many :orders
|
27
|
+
has_many :focuses
|
28
|
+
|
29
|
+
def default_attributes
|
30
|
+
super.merge account: 'X000000'
|
31
|
+
super.merge alias: ''
|
32
|
+
super.merge type: 'Account'
|
33
|
+
super.merge connected: false
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger #nodoc#
|
37
|
+
Connection.logger
|
38
|
+
end
|
39
|
+
|
40
|
+
# Setze Account connect/disconnect und undate!
|
41
|
+
def connect!
|
42
|
+
update_attribute :connected , true
|
43
|
+
end
|
44
|
+
def disconnect!
|
45
|
+
update_attribute :connected , false
|
46
|
+
end
|
47
|
+
|
48
|
+
def print_type #nodoc#
|
49
|
+
(test_environment? ? "demo_" : "") + ( user? ? "user" : "advisor" )
|
50
|
+
end
|
51
|
+
|
52
|
+
def advisor?
|
53
|
+
!!(type =~ /Advisor/ || account =~ /\A[D]?[F]{1}/)
|
54
|
+
end
|
55
|
+
|
56
|
+
def user?
|
57
|
+
!!(type =~ /User/ || account =~ /\A[D]?[U]{1}/)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_environment?
|
61
|
+
!!(account =~ /^[D]{1}/)
|
62
|
+
end
|
63
|
+
|
64
|
+
def == other
|
65
|
+
super(other) ||
|
66
|
+
other.is_a?(self.class) && account == other.account
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_human
|
70
|
+
a = if self.alias.present? && self.alias != account
|
71
|
+
" alias: "+ self.alias
|
72
|
+
else
|
73
|
+
""
|
74
|
+
end
|
75
|
+
"<#{print_type} #{account}#{a}>"
|
76
|
+
end
|
77
|
+
|
78
|
+
def name #nodoc#
|
79
|
+
self.alias.present? ? self.alias : account
|
80
|
+
end
|
81
|
+
|
82
|
+
# alias :id :account
|
83
|
+
end # class
|
84
|
+
|
85
|
+
end # module
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module IB
|
2
|
+
# Instantiate with a Hash of attributes, to be auto-set via initialize in Model.
|
3
|
+
class AccountValue < IB::Model
|
4
|
+
include BaseProperties
|
5
|
+
|
6
|
+
belongs_to :account
|
7
|
+
|
8
|
+
prop :key,
|
9
|
+
:value,
|
10
|
+
:currency
|
11
|
+
|
12
|
+
|
13
|
+
# comparison
|
14
|
+
def == other
|
15
|
+
super(other) ||
|
16
|
+
other.is_a?(self.class) &&
|
17
|
+
key == other.key &&
|
18
|
+
currency == other.currency &&
|
19
|
+
value == other.value
|
20
|
+
end
|
21
|
+
def default_attributes
|
22
|
+
super.merge key: 'AccountValue',
|
23
|
+
value: 0,
|
24
|
+
currency: 'USD'
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_human
|
28
|
+
"<#{key}=#{value} #{currency}>"
|
29
|
+
end
|
30
|
+
|
31
|
+
alias to_s to_human
|
32
|
+
end # class
|
33
|
+
end # module IB
|