ib-api 972.0
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.
- 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
@@ -0,0 +1,450 @@
|
|
1
|
+
require 'thread'
|
2
|
+
#require 'active_support'
|
3
|
+
require 'ib/socket'
|
4
|
+
require 'ib/logger'
|
5
|
+
require 'ib/messages'
|
6
|
+
|
7
|
+
module IB
|
8
|
+
# Encapsulates API connection to TWS or Gateway
|
9
|
+
class Connection
|
10
|
+
|
11
|
+
|
12
|
+
## -------------------------------------------- Interface ---------------------------------
|
13
|
+
## public attributes: socket, next_local_id ( alias next_order_id)
|
14
|
+
## public methods: connect (alias open), disconnect, connected?
|
15
|
+
## subscribe, unsubscribe
|
16
|
+
## send_message (alias dispatch)
|
17
|
+
## place_order, modify_order, cancel_order
|
18
|
+
## public data-queue: received, received?, wait_for, clear_received
|
19
|
+
## misc: reader_running?
|
20
|
+
|
21
|
+
include LogDev # provides default_logger
|
22
|
+
|
23
|
+
mattr_accessor :current
|
24
|
+
mattr_accessor :logger ## borrowed from active_support
|
25
|
+
# Please note, we are realizing only the most current TWS protocol versions,
|
26
|
+
# thus improving performance at the expense of backwards compatibility.
|
27
|
+
# Older protocol versions support can be found in older gem versions.
|
28
|
+
|
29
|
+
attr_accessor :socket # Socket to IB server (TWS or Gateway)
|
30
|
+
attr_accessor :next_local_id # Next valid order id
|
31
|
+
attr_accessor :client_id
|
32
|
+
attr_accessor :server_version
|
33
|
+
attr_accessor :client_version
|
34
|
+
alias next_order_id next_local_id
|
35
|
+
alias next_order_id= next_local_id=
|
36
|
+
|
37
|
+
def initialize host: '127.0.0.1',
|
38
|
+
port: '4002', # IB Gateway connection (default --> demo) 4001: production
|
39
|
+
#:port => '7497', # TWS connection --> demo 7496: production
|
40
|
+
connect: true, # Connect at initialization
|
41
|
+
received: true, # Keep all received messages in a @received Hash
|
42
|
+
# redis: false, # future plans
|
43
|
+
logger: default_logger,
|
44
|
+
client_id: rand( 1001 .. 9999 ) ,
|
45
|
+
client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb
|
46
|
+
optional_capacities: "", # TWS-Version 974: "+PACEAPI"
|
47
|
+
#server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb
|
48
|
+
**any_other_parameters_which_are_ignored
|
49
|
+
# V 974 release motes
|
50
|
+
# API messages sent at a higher rate than 50/second can now be paced by TWS at the 50/second rate instead of potentially causing a disconnection. This is now done automatically by the RTD Server API and can be done with other API technologies by invoking SetConnectOptions("+PACEAPI") prior to eConnect.
|
51
|
+
|
52
|
+
|
53
|
+
# convert parameters into instance-variables and assign them
|
54
|
+
method(__method__).parameters.each do |type, k|
|
55
|
+
next unless type == :key
|
56
|
+
case k
|
57
|
+
when :logger
|
58
|
+
self.logger = logger
|
59
|
+
else
|
60
|
+
v = eval(k.to_s)
|
61
|
+
instance_variable_set("@#{k}", v) unless v.nil?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# A couple of locks to avoid race conditions in JRuby
|
66
|
+
@subscribe_lock = Mutex.new
|
67
|
+
@receive_lock = Mutex.new
|
68
|
+
@message_lock = Mutex.new
|
69
|
+
|
70
|
+
@connected = false
|
71
|
+
self.next_local_id = nil
|
72
|
+
|
73
|
+
# self.subscribe(:Alert) do |msg|
|
74
|
+
# puts msg.to_human
|
75
|
+
# end
|
76
|
+
|
77
|
+
# TWS always sends NextValidId message at connect -subscribe save this id
|
78
|
+
## this block is executed before tws-communication is established
|
79
|
+
yield self if block_given?
|
80
|
+
|
81
|
+
self.subscribe(:NextValidId) do |msg|
|
82
|
+
logger.progname = "Connection#connect"
|
83
|
+
self.next_local_id = msg.local_id
|
84
|
+
logger.info { "Got next valid order id: #{next_local_id}." }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Ensure the transmission of NextValidId.
|
88
|
+
# works even if no reader_thread is established
|
89
|
+
if connect
|
90
|
+
disconnect if connected?
|
91
|
+
update_next_order_id
|
92
|
+
Kernel.exit if self.next_local_id.nil?
|
93
|
+
end
|
94
|
+
#start_reader if @received && connected?
|
95
|
+
Connection.current = self
|
96
|
+
end
|
97
|
+
|
98
|
+
# read actual order_id and
|
99
|
+
# connect if not connected
|
100
|
+
def update_next_order_id
|
101
|
+
i,finish = 0, false
|
102
|
+
sub = self.subscribe(:NextValidID) { finish = true }
|
103
|
+
connected? ? self.send_message( :RequestIds ) : open()
|
104
|
+
Timeout::timeout(1, IB::TransmissionError,"Could not get NextValidId" ) do
|
105
|
+
loop { sleep 0.1; break if finish }
|
106
|
+
end
|
107
|
+
self.unsubscribe sub
|
108
|
+
end
|
109
|
+
|
110
|
+
### Working with connection
|
111
|
+
|
112
|
+
def connect
|
113
|
+
logger.progname='IB::Connection#connect'
|
114
|
+
if connected?
|
115
|
+
error "Already connected!"
|
116
|
+
return
|
117
|
+
end
|
118
|
+
|
119
|
+
self.socket = IBSocket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible
|
120
|
+
socket.initialising_handshake
|
121
|
+
socket.decode_message( socket.recieve_messages ) do | the_message |
|
122
|
+
# logger.info{ "TheMessage :: #{the_message.inspect}" }
|
123
|
+
@server_version = the_message.shift.to_i
|
124
|
+
error "ServerVersion does not match #{@server_version} <--> #{MAX_CLIENT_VER}" if @server_version != MAX_CLIENT_VER
|
125
|
+
|
126
|
+
@remote_connect_time = DateTime.parse the_message.shift
|
127
|
+
@local_connect_time = Time.now
|
128
|
+
end
|
129
|
+
|
130
|
+
# Sending (arbitrary) client ID to identify subsequent communications.
|
131
|
+
# The client with a client_id of 0 can manage the TWS-owned open orders.
|
132
|
+
# Other clients can only manage their own open orders.
|
133
|
+
|
134
|
+
# V100 initial handshake
|
135
|
+
# Parameters borrowed from the python client
|
136
|
+
start_api = 71
|
137
|
+
version = 2
|
138
|
+
# optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities
|
139
|
+
socket.send_messages start_api, version, @client_id , @optional_capacities
|
140
|
+
@connected = true
|
141
|
+
logger.info { "Connected to server, version: #{@server_version},\n connection time: " +
|
142
|
+
"#{@local_connect_time} local, " +
|
143
|
+
"#{@remote_connect_time} remote."}
|
144
|
+
|
145
|
+
# if the client_id is wrong or the port is not accessible the first read attempt fails
|
146
|
+
# get the first message and proceed if something reasonable is recieved
|
147
|
+
the_message = process_message # recieve next_order_id
|
148
|
+
error "Check Port/Client_id ", :reader if the_message == " "
|
149
|
+
start_reader
|
150
|
+
end
|
151
|
+
|
152
|
+
alias open connect # Legacy alias
|
153
|
+
|
154
|
+
def disconnect
|
155
|
+
if reader_running?
|
156
|
+
@reader_running = false
|
157
|
+
@reader_thread.join
|
158
|
+
end
|
159
|
+
if connected?
|
160
|
+
socket.close
|
161
|
+
@connected = false
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
alias close disconnect # Legacy alias
|
166
|
+
|
167
|
+
def connected?
|
168
|
+
@connected
|
169
|
+
end
|
170
|
+
|
171
|
+
### Working with message subscribers
|
172
|
+
|
173
|
+
# Subscribe Proc or block to specific type(s) of incoming message events.
|
174
|
+
# Listener will be called later with received message instance as its argument.
|
175
|
+
# Returns subscriber id to allow unsubscribing
|
176
|
+
def subscribe *args, &block
|
177
|
+
@subscribe_lock.synchronize do
|
178
|
+
subscriber = args.last.respond_to?(:call) ? args.pop : block
|
179
|
+
id = random_id
|
180
|
+
|
181
|
+
error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc
|
182
|
+
|
183
|
+
args.each do |what|
|
184
|
+
message_classes =
|
185
|
+
case
|
186
|
+
when what.is_a?(Class) && what < Messages::Incoming::AbstractMessage
|
187
|
+
[what]
|
188
|
+
when what.is_a?(Symbol)
|
189
|
+
[Messages::Incoming.const_get(what)]
|
190
|
+
when what.is_a?(Regexp)
|
191
|
+
Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what }
|
192
|
+
else
|
193
|
+
error "#{what} must represent incoming IB message class", :args
|
194
|
+
end
|
195
|
+
# @subscribers_lock.synchronize do
|
196
|
+
message_classes.flatten.each do |message_class|
|
197
|
+
# TODO: Fix: RuntimeError: can't add a new key into hash during iteration
|
198
|
+
subscribers[message_class][id] = subscriber
|
199
|
+
end
|
200
|
+
# end # lock
|
201
|
+
end
|
202
|
+
|
203
|
+
id
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Remove all subscribers with specific subscriber id
|
208
|
+
def unsubscribe *ids
|
209
|
+
@subscribe_lock.synchronize do
|
210
|
+
ids.collect do |id|
|
211
|
+
removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact
|
212
|
+
logger.error "No subscribers with id #{id}" if removed_at_id.empty?
|
213
|
+
removed_at_id # return_value
|
214
|
+
end.flatten
|
215
|
+
end
|
216
|
+
end
|
217
|
+
### Working with received messages Hash
|
218
|
+
|
219
|
+
# Clear received messages Hash
|
220
|
+
def clear_received *message_types
|
221
|
+
@receive_lock.synchronize do
|
222
|
+
if message_types.empty?
|
223
|
+
received.each { |message_type, container| container.clear }
|
224
|
+
else
|
225
|
+
message_types.each { |message_type| received[message_type].clear }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Hash of received messages, keyed by message type
|
231
|
+
def received
|
232
|
+
@received_hash ||= Hash.new do |hash, message_type|
|
233
|
+
# enable access to the hash via
|
234
|
+
# ib.received[:MessageType].attribute
|
235
|
+
the_array = Array.new
|
236
|
+
def the_array.method_missing(method, *key)
|
237
|
+
unless method == :to_hash || method == :to_str #|| method == :to_int
|
238
|
+
return self.map{|x| x.public_send(method, *key)}
|
239
|
+
end
|
240
|
+
end
|
241
|
+
hash[message_type] = the_array
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Check if messages of given type were received at_least n times
|
246
|
+
def received? message_type, times=1
|
247
|
+
@receive_lock.synchronize do
|
248
|
+
received[message_type].size >= times
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# Wait for specific condition(s) - given as callable/block, or
|
254
|
+
# message type(s) - given as Symbol or [Symbol, times] pair.
|
255
|
+
# Timeout after given time or 1 second.
|
256
|
+
#
|
257
|
+
# wait_for depends heavyly on Connection#received. If collection of messages through recieved
|
258
|
+
# is turned off, wait_for loses most of its functionality
|
259
|
+
def wait_for *args, &block
|
260
|
+
timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
|
261
|
+
end_time = Time.now + (timeout || 1) # default timeout 1 sec
|
262
|
+
conditions = args.delete_if { |arg| arg.is_a? Numeric }.push(block).compact
|
263
|
+
|
264
|
+
until end_time < Time.now || satisfied?(*conditions)
|
265
|
+
if reader_running?
|
266
|
+
sleep 0.05
|
267
|
+
else
|
268
|
+
process_messages 50
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
### Working with Incoming messages from IB
|
274
|
+
|
275
|
+
|
276
|
+
def reader_running?
|
277
|
+
@reader_running && @reader_thread && @reader_thread.alive?
|
278
|
+
end
|
279
|
+
|
280
|
+
# Process incoming messages during *poll_time* (200) msecs, nonblocking
|
281
|
+
def process_messages poll_time = 50 # in msec
|
282
|
+
time_out = Time.now + poll_time/1000.0
|
283
|
+
while (time_left = time_out - Time.now) > 0
|
284
|
+
# If socket is readable, process single incoming message
|
285
|
+
#process_message if select [socket], nil, nil, time_left
|
286
|
+
# the following checks for shutdown of TWS side; ensures we don't run in a spin loop.
|
287
|
+
# unfortunately, it raises Errors in windows environment
|
288
|
+
# disabled for now
|
289
|
+
if select [socket], nil, nil, time_left
|
290
|
+
# # Peek at the message from the socket; if it's blank then the
|
291
|
+
# # server side of connection (TWS) has likely shut down.
|
292
|
+
socket_likely_shutdown = socket.recvmsg(100, Socket::MSG_PEEK)[0] == ""
|
293
|
+
#
|
294
|
+
# # We go ahead process messages regardless (a no-op if socket_likely_shutdown).
|
295
|
+
process_message
|
296
|
+
#
|
297
|
+
# # After processing, if socket has shut down we sleep for 100ms
|
298
|
+
# # to avoid spinning in a tight loop. If the server side somehow
|
299
|
+
# # comes back up (gets reconnedted), normal processing
|
300
|
+
# # (without the 100ms wait) should happen.
|
301
|
+
sleep(0.1) if socket_likely_shutdown
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
### Sending Outgoing messages to IB
|
307
|
+
|
308
|
+
# Send an outgoing message.
|
309
|
+
# returns the used request_id if appropiate, otherwise "true"
|
310
|
+
def send_message what, *args
|
311
|
+
message =
|
312
|
+
case
|
313
|
+
when what.is_a?(Messages::Outgoing::AbstractMessage)
|
314
|
+
what
|
315
|
+
when what.is_a?(Class) && what < Messages::Outgoing::AbstractMessage
|
316
|
+
what.new *args
|
317
|
+
when what.is_a?(Symbol)
|
318
|
+
Messages::Outgoing.const_get(what).new *args
|
319
|
+
else
|
320
|
+
error "Only able to send outgoing IB messages", :args
|
321
|
+
end
|
322
|
+
error "Not able to send messages, IB not connected!" unless connected?
|
323
|
+
begin
|
324
|
+
@message_lock.synchronize do
|
325
|
+
message.send_to socket
|
326
|
+
end
|
327
|
+
rescue Errno::EPIPE
|
328
|
+
logger.error{ "Broken Pipe, trying to reconnect" }
|
329
|
+
disconnect
|
330
|
+
connect
|
331
|
+
retry
|
332
|
+
end
|
333
|
+
## return the transmitted message
|
334
|
+
message.data[:request_id].presence || true
|
335
|
+
end
|
336
|
+
|
337
|
+
alias dispatch send_message # Legacy alias
|
338
|
+
|
339
|
+
# Place Order (convenience wrapper for send_message :PlaceOrder).
|
340
|
+
# Assigns client_id and order_id fields to placed order. Returns assigned order_id.
|
341
|
+
def place_order order, contract
|
342
|
+
# order.place contract, self ## old
|
343
|
+
error "Unable to place order, next_local_id not known" unless next_local_id
|
344
|
+
error "local_id present. Order is already placed. Do might use modify insteed" unless order.local_id.nil?
|
345
|
+
order.client_id = client_id
|
346
|
+
order.local_id = next_local_id
|
347
|
+
self.next_local_id += 1
|
348
|
+
order.placed_at = Time.now
|
349
|
+
modify_order order, contract
|
350
|
+
end
|
351
|
+
|
352
|
+
# Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id.
|
353
|
+
def modify_order order, contract
|
354
|
+
# order.modify contract, self ## old
|
355
|
+
error "Unable to modify order; local_id not specified" if order.local_id.nil?
|
356
|
+
order.modified_at = Time.now
|
357
|
+
send_message :PlaceOrder,
|
358
|
+
:order => order,
|
359
|
+
:contract => contract,
|
360
|
+
:local_id => order.local_id
|
361
|
+
order.local_id # return value
|
362
|
+
end
|
363
|
+
|
364
|
+
# Cancel Orders by their local ids (convenience wrapper for send_message :CancelOrder).
|
365
|
+
def cancel_order *local_ids
|
366
|
+
local_ids.each do |local_id|
|
367
|
+
send_message :CancelOrder, :local_id => local_id.to_i
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Start reader thread that continuously reads messages from @socket in background.
|
372
|
+
# If you don't start reader, you should manually poll @socket for messages
|
373
|
+
# or use #process_messages(msec) API.
|
374
|
+
def start_reader
|
375
|
+
return(@reader_thread) if @reader_running
|
376
|
+
if connected?
|
377
|
+
Thread.abort_on_exception = true
|
378
|
+
@reader_running = true
|
379
|
+
@reader_thread = Thread.new { process_messages while @reader_running }
|
380
|
+
else
|
381
|
+
logger.fatal {"Could not start reader, not connected!"}
|
382
|
+
nil # return_value
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
protected
|
387
|
+
# Message subscribers. Key is the message class to listen for.
|
388
|
+
# Value is a Hash of subscriber Procs, keyed by their subscription id.
|
389
|
+
# All subscriber Procs will be called with the message instance
|
390
|
+
# as an argument when a message of that type is received.
|
391
|
+
def subscribers
|
392
|
+
@subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new }
|
393
|
+
end
|
394
|
+
|
395
|
+
# Process single incoming message (blocking!)
|
396
|
+
def process_message
|
397
|
+
logger.progname='IB::Connection#process_message' if logger.is_a?(Logger)
|
398
|
+
|
399
|
+
socket.decode_message( socket.recieve_messages ) do | the_decoded_message |
|
400
|
+
# puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}"
|
401
|
+
msg_id = the_decoded_message.shift.to_i
|
402
|
+
|
403
|
+
# Debug:
|
404
|
+
logger.debug { "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"}
|
405
|
+
|
406
|
+
# Create new instance of the appropriate message type,
|
407
|
+
# and have it read the message from socket.
|
408
|
+
# NB: Failure here usually means unsupported message type received
|
409
|
+
logger.error { "Got unsupported message #{msg_id}" } unless Messages::Incoming::Classes[msg_id]
|
410
|
+
error "Something strange happened - Reader has to be restarted" , :reader if msg_id.to_i.zero?
|
411
|
+
msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message)
|
412
|
+
|
413
|
+
# Deliver message to all registered subscribers, alert if no subscribers
|
414
|
+
# Ruby 2.0 and above: Hashes are ordered.
|
415
|
+
# Thus first declared subscribers of a class are executed first
|
416
|
+
@subscribe_lock.synchronize do
|
417
|
+
subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
|
418
|
+
end
|
419
|
+
logger.warn { "No subscribers for message #{msg.class}!" } if subscribers[msg.class].empty?
|
420
|
+
|
421
|
+
# Collect all received messages into a @received Hash
|
422
|
+
if @received
|
423
|
+
@receive_lock.synchronize do
|
424
|
+
received[msg.message_type] << msg
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def random_id
|
431
|
+
rand 999999
|
432
|
+
end
|
433
|
+
|
434
|
+
# Check if all given conditions are satisfied
|
435
|
+
def satisfied? *conditions
|
436
|
+
!conditions.empty? &&
|
437
|
+
conditions.inject(true) do |result, condition|
|
438
|
+
result && if condition.is_a?(Symbol)
|
439
|
+
received?(condition)
|
440
|
+
elsif condition.is_a?(Array)
|
441
|
+
received?(*condition)
|
442
|
+
elsif condition.respond_to?(:call)
|
443
|
+
condition.call
|
444
|
+
else
|
445
|
+
logger.error { "Unknown wait condition #{condition}" }
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end # class Connection
|
450
|
+
end # module IB
|
data/lib/ib/constants.rb
ADDED
@@ -0,0 +1,393 @@
|
|
1
|
+
module IB
|
2
|
+
### Widely used TWS constants:
|
3
|
+
|
4
|
+
EOL = "\0"
|
5
|
+
# TWS_MAX is TWSMAX (transmitted from the TWS) minus the first digit (1)
|
6
|
+
# Anything bigger then TWS_MAX is considered as nil (in read_decimal @ messages/incomming/abstract_message.rb)
|
7
|
+
TWS_MAX = 79769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0
|
8
|
+
TWSMAX = 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.0
|
9
|
+
|
10
|
+
|
11
|
+
# Enumeration of bar size types for convenience.
|
12
|
+
# Bar sizes less than 30 seconds do not work for some securities.
|
13
|
+
BAR_SIZES = {'1 sec' => :sec1,
|
14
|
+
'5 secs' => :sec5,
|
15
|
+
'15 secs' =>:sec15,
|
16
|
+
'30 secs' =>:sec30,
|
17
|
+
'1 min' => :min1,
|
18
|
+
'2 mins' => :min2,
|
19
|
+
'3 mins' => :min3,
|
20
|
+
'5 mins' => :min5,
|
21
|
+
'15 mins' =>:min15,
|
22
|
+
'30 mins' =>:min30,
|
23
|
+
'1 hour' =>:hour1,
|
24
|
+
'1 day' => :day1
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
# Enumeration of data types.
|
28
|
+
# Determines the nature of data being extracted. Valid values:
|
29
|
+
DATA_TYPES = {'TRADES' => :trades,
|
30
|
+
'MIDPOINT' => :midpoint,
|
31
|
+
'BID' => :bid,
|
32
|
+
'ASK' => :ask,
|
33
|
+
'BID_ASK' => :bid_ask,
|
34
|
+
'HISTORICAL_VOLATILITY' => :historical_volatility,
|
35
|
+
'OPTION_IMPLIED_VOLATILITY' => :option_implied_volatility,
|
36
|
+
'OPTION_VOLUME' => :option_volume,
|
37
|
+
'OPTION_OPEN_INTEREST' => :option_open_interest
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
### These values are typically received from TWS in incoming messages
|
41
|
+
|
42
|
+
# Tick types as received in TickPrice and TickSize messages (enumeration)
|
43
|
+
TICK_TYPES = {
|
44
|
+
# int id => :Description # Corresponding API Event/Function/Method
|
45
|
+
0 => :bid_size, # tickSize()
|
46
|
+
1 => :bid_price, # tickPrice()
|
47
|
+
2 => :ask_price, # tickPrice()
|
48
|
+
3 => :ask_size, # tickSize()
|
49
|
+
4 => :last_price, # tickPrice()
|
50
|
+
5 => :last_size, # tickSize()
|
51
|
+
6 => :high, # tickPrice()
|
52
|
+
7 => :low, # tickPrice()
|
53
|
+
8 => :volume, # tickSize()
|
54
|
+
9 => :close_price, # tickPrice()
|
55
|
+
10 => :bid_option, # tickOptionComputation() See Note 1 below
|
56
|
+
11 => :ask_option, # tickOptionComputation() See => :Note 1 below
|
57
|
+
12 => :last_option, # tickOptionComputation() See Note 1 below
|
58
|
+
13 => :model_option, # tickOptionComputation() See Note 1 below
|
59
|
+
14 => :open_tick, # tickPrice()
|
60
|
+
15 => :low_13_week, # tickPrice()
|
61
|
+
16 => :high_13_week, # tickPrice()
|
62
|
+
17 => :low_26_week, # tickPrice()
|
63
|
+
18 => :high_26_week, # tickPrice()
|
64
|
+
19 => :low_52_week, # tickPrice()
|
65
|
+
20 => :high_52_week, # tickPrice()
|
66
|
+
21 => :avg_volume, # tickSize()
|
67
|
+
22 => :open_interest, # tickSize()
|
68
|
+
23 => :option_historical_vol, # tickGeneric()
|
69
|
+
24 => :option_implied_vol, # tickGeneric()
|
70
|
+
25 => :option_bid_exch, # not USED
|
71
|
+
26 => :option_ask_exch, # not USED
|
72
|
+
27 => :option_call_open_interest, # tickSize()
|
73
|
+
28 => :option_put_open_interest, # tickSize()
|
74
|
+
29 => :option_call_volume, # tickSize()
|
75
|
+
30 => :option_put_volume, # tickSize()
|
76
|
+
31 => :index_future_premium, # tickGeneric()
|
77
|
+
32 => :bid_exch, # tickString()
|
78
|
+
33 => :ask_exch, # tickString()
|
79
|
+
34 => :auction_volume, # not USED
|
80
|
+
35 => :auction_price, # not USED
|
81
|
+
36 => :auction_imbalance, # not USED
|
82
|
+
37 => :mark_price, # tickPrice()
|
83
|
+
38 => :bid_efp_computation, # tickEFP()
|
84
|
+
39 => :ask_efp_computation, # tickEFP()
|
85
|
+
40 => :last_efp_computation, # tickEFP()
|
86
|
+
41 => :open_efp_computation, # tickEFP()
|
87
|
+
42 => :high_efp_computation, # tickEFP()
|
88
|
+
43 => :low_efp_computation, # tickEFP()
|
89
|
+
44 => :close_efp_computation, # tickEFP()
|
90
|
+
45 => :last_timestamp, # tickString()
|
91
|
+
46 => :shortable, # tickGeneric()
|
92
|
+
47 => :fundamental_ratios, # tickString()
|
93
|
+
48 => :rt_volume, # tickGeneric()
|
94
|
+
49 => :halted, # see Note 2 below.
|
95
|
+
50 => :bid_yield, # tickPrice() See Note 3 below
|
96
|
+
51 => :ask_yield, # tickPrice() See Note 3 below
|
97
|
+
52 => :last_yield, # tickPrice() See Note 3 below
|
98
|
+
53 => :cust_option_computation, # tickOptionComputation()
|
99
|
+
54 => :trade_count, # tickGeneric()
|
100
|
+
55 => :trade_rate, # tickGeneric()
|
101
|
+
56 => :volume_rate, # tickGeneric()
|
102
|
+
57 => :last_rth_trade, #
|
103
|
+
58 => :rt_historical_vol,
|
104
|
+
59 => :ib_dividends,
|
105
|
+
60 => :bond_factor_multiplier,
|
106
|
+
61 => :regulatory_imbalance,
|
107
|
+
62 => :news_tick,
|
108
|
+
63 => :short_term_volume_3_min,
|
109
|
+
64 => :short_term_volume_5_min,
|
110
|
+
65 => :short_term_volume_10_min,
|
111
|
+
66 => :delayed_bid,
|
112
|
+
67 => :delayed_ask,
|
113
|
+
68 => :delayed_last,
|
114
|
+
69 => :delayed_bid_size,
|
115
|
+
70 => :delayed_ask_size,
|
116
|
+
71 => :delayed_last_size,
|
117
|
+
72 => :delayed_high,
|
118
|
+
73 => :delayed_low,
|
119
|
+
74 => :delayed_volume,
|
120
|
+
75 => :delayed_close,
|
121
|
+
76 => :delayed_open,
|
122
|
+
77 => :rt_trd_volume,
|
123
|
+
78 => :creditman_mark_price,
|
124
|
+
79 => :creditman_slow_mark_price,
|
125
|
+
80 => :delayed_bid_option,
|
126
|
+
81 => :delayed_ask_option,
|
127
|
+
82 => :delayed_last_option,
|
128
|
+
83 => :delayed_model_option,
|
129
|
+
84 => :last_exch,
|
130
|
+
85 => :last_reg_time,
|
131
|
+
86 => :futures_open_interest,
|
132
|
+
87 => :avg_opt_volume,
|
133
|
+
88 => :not_set,
|
134
|
+
105 => :average_option_volume #(for Stocks) tickGeneric()
|
135
|
+
|
136
|
+
|
137
|
+
# Note 1: Tick types BID_OPTION, ASK_OPTION, LAST_OPTION, and MODEL_OPTION return
|
138
|
+
# all Greeks (delta, gamma, vega, theta), the underlying price and the
|
139
|
+
# stock and option reference price when requested.
|
140
|
+
# MODEL_OPTION also returns model implied volatility.
|
141
|
+
# Note 2: When trading is halted for a contract, TWS receives a special tick:
|
142
|
+
# haltedLast=1. When trading is resumed, TWS receives haltedLast=0.
|
143
|
+
# A tick type, HALTED, tick ID= 49, is now available in regular market
|
144
|
+
# data via the API to indicate this halted state. Possible values for
|
145
|
+
# this new tick type are: 0 = Not halted, 1 = Halted.
|
146
|
+
# Note 3: Applies to bond contracts only.
|
147
|
+
}
|
148
|
+
|
149
|
+
# Financial Advisor types (FaMsgTypeName)
|
150
|
+
FA_TYPES = {
|
151
|
+
1 => :groups,
|
152
|
+
2 => :profiles,
|
153
|
+
3 => :aliases}.freeze
|
154
|
+
|
155
|
+
# Received in new MarketDataType (58 incoming) message
|
156
|
+
MARKET_DATA_TYPES = {
|
157
|
+
0 => :unknown,
|
158
|
+
1 => :real_time,
|
159
|
+
2 => :frozen,
|
160
|
+
3 => :delayed,
|
161
|
+
4 => :frozen_delayed }.freeze
|
162
|
+
|
163
|
+
# Market depth messages contain these "operation" codes to tell you what to do with the data.
|
164
|
+
# See also http://www.interactivebrokers.com/php/apiUsersGuide/apiguide/java/updatemktdepth.htm
|
165
|
+
MARKET_DEPTH_OPERATIONS = {
|
166
|
+
0 => :insert, # New order, insert into the row identified by :position
|
167
|
+
1 => :update, # Update the existing order at the row identified by :position
|
168
|
+
2 => :delete # Delete the existing order at the row identified by :position
|
169
|
+
}.freeze
|
170
|
+
|
171
|
+
MARKET_DEPTH_SIDES = {
|
172
|
+
0 => :ask,
|
173
|
+
1 => :bid
|
174
|
+
}.freeze
|
175
|
+
|
176
|
+
ORDER_TYPES =
|
177
|
+
{'LMT' => :limit, # Limit Order
|
178
|
+
'LIT' => :limit_if_touched, # Limit if Touched
|
179
|
+
'LOC' => :limit_on_close, # Limit-on-Close LMTCLS ?
|
180
|
+
'LOO' => :limit_on_open, # Limit-on-Open
|
181
|
+
'MKT' => :market, # Market
|
182
|
+
'MIT' => :market_if_touched, # Market-if-Touched
|
183
|
+
'MOC' => :market_on_close, # Market-on-Close MKTCLSL ?
|
184
|
+
'MOO' => :market_on_open, # Market-on-Open
|
185
|
+
'MTL' => :market_to_limit, # Market-to-Limit
|
186
|
+
'MKT PRT' => :market_protected, # Market with Protection
|
187
|
+
'QUOTE' => :request_for_quote, # Request for Quote
|
188
|
+
'STP' => :stop, # Stop
|
189
|
+
'STP LMT' => :stop_limit, # Stop Limit
|
190
|
+
'STP PRT' => :stop_protected, # Stop with Protection
|
191
|
+
'TRAIL' => :trailing_stop, # Trailing Stop
|
192
|
+
'TRAIL LIMIT' => :trailing_limit, # Trailing Stop Limit
|
193
|
+
'TRAIL LIT' => :trailing_limit_if_touched, # Trailing Limit if Touched
|
194
|
+
'TRAIL MIT' => :trailing_market_if_touched, # Trailing Market If Touched
|
195
|
+
'REL' => :relative, # Relative
|
196
|
+
'BOX TOP' => :box_top, # Box Top
|
197
|
+
'PEG MKT' => :pegged_to_market, # Pegged-to-Market
|
198
|
+
'PEG STK' => :pegged_to_market, # Pegged-to-Stock
|
199
|
+
'PEG MID' => :pegged_to_midpoint, # Pegged-to-Midpoint
|
200
|
+
'PEG BENCH' => :pegged_to_benchmark, # Pegged-to-Benmchmark # Vers. 102
|
201
|
+
'VWAP' => :vwap, # VWAP-Guaranted
|
202
|
+
'OCA' => :one_cancels_all, # One-Cancels-All
|
203
|
+
'VOL' => :volatility, # Volatility
|
204
|
+
'SCALE' => :scale, # Scale
|
205
|
+
'NONE' => :none, # Used to indicate no hedge in :delta_neutral_order_type
|
206
|
+
'None' => :none, # Used to indicate no hedge in :delta_neutral_order_type
|
207
|
+
}.freeze
|
208
|
+
# Valid security types (sec_type attribute of IB::Contract)
|
209
|
+
SECURITY_TYPES =
|
210
|
+
{ 'BAG' => :bag,
|
211
|
+
'BOND' => :bond,
|
212
|
+
'CASH' => :forex,
|
213
|
+
'CMDTY'=> :commodity,
|
214
|
+
'CFD' => :cfd,
|
215
|
+
'FUT' => :future,
|
216
|
+
'CONTFUT' => :continous_future,
|
217
|
+
'FUT+CONTFUT' => :all_futures,
|
218
|
+
'FOP' => :futures_option,
|
219
|
+
'FUND' => :fund, # ETF?
|
220
|
+
'IND' => :index,
|
221
|
+
'NEWS' => :news,
|
222
|
+
'OPT' => :option,
|
223
|
+
'IOPT' => :dutch_option,
|
224
|
+
'STK' => :stock,
|
225
|
+
'WAR' => :warrant,
|
226
|
+
'ICU' => :icu,
|
227
|
+
'ICS' => :ics,
|
228
|
+
'BILL' => :bill,
|
229
|
+
'BSK' => :basket,
|
230
|
+
'FWD' => :forward,
|
231
|
+
'FIXED' => :fixed }.freeze
|
232
|
+
|
233
|
+
# Obtain symbolic value from given property code:
|
234
|
+
# VALUES[:side]['B'] -> :buy
|
235
|
+
VALUES = {
|
236
|
+
:sec_type => SECURITY_TYPES,
|
237
|
+
:order_type => ORDER_TYPES,
|
238
|
+
:delta_neutral_order_type => ORDER_TYPES,
|
239
|
+
|
240
|
+
:origin => {0 => :customer, 1 => :firm},
|
241
|
+
:volatility_type => {1 => :daily, 2 => :annual},
|
242
|
+
:reference_price_type => {1 => :average, 2 => :bid_or_ask},
|
243
|
+
|
244
|
+
# This property encodes differently for ComboLeg and Order objects,
|
245
|
+
# we use ComboLeg codes and transcode for Order codes as needed
|
246
|
+
:open_close =>
|
247
|
+
{0 => :same, # Default for Legs, same as the parent (combo) security.
|
248
|
+
1 => :open, # Open. For Legs, this value is only used by institutions.
|
249
|
+
2 => :close, # Close. For Legs, this value is only used by institutions.
|
250
|
+
3 => :unknown}, # WTF
|
251
|
+
|
252
|
+
:right =>
|
253
|
+
{'' => :none, # Not an option
|
254
|
+
'P' => :put,
|
255
|
+
'C' => :call},
|
256
|
+
|
257
|
+
:side => # AKA action
|
258
|
+
{'B' => :buy, # or BOT
|
259
|
+
'S' => :sell, # or SLD
|
260
|
+
'T' => :short, # short
|
261
|
+
'X' => :short_exempt # Short Sale Exempt action. This allows some orders
|
262
|
+
# to be exempt from the SEC recent changes to Regulation SHO, which
|
263
|
+
# eliminated the old uptick rule and replaced it with a new "circuit breaker"
|
264
|
+
# rule, and allows some orders to be exempt from the new rule.
|
265
|
+
},
|
266
|
+
|
267
|
+
:short_sale_slot =>
|
268
|
+
{0 => :default, # The only valid option for retail customers
|
269
|
+
1 => :broker, # Shares are at your clearing broker, institutions
|
270
|
+
2 => :third_party}, # Shares will be delivered from elsewhere, institutions
|
271
|
+
|
272
|
+
:oca_type =>
|
273
|
+
{0 => :none, # Not a member of OCA group
|
274
|
+
1 => :cancel_with_block, # Cancel all remaining orders with block
|
275
|
+
2 => :reduce_with_block, # Remaining orders are reduced in size with block
|
276
|
+
3 => :reduce_no_block}, # Remaining orders are reduced in size with no block
|
277
|
+
|
278
|
+
:auction_strategy =>
|
279
|
+
{0 => :none, # Not a BOX order
|
280
|
+
1 => :match,
|
281
|
+
2 => :improvement,
|
282
|
+
3 => :transparent},
|
283
|
+
|
284
|
+
:trigger_method =>
|
285
|
+
{0 => :default, # "double bid/ask" used for OTC/US options, "last" otherswise.
|
286
|
+
1 => :double_bid_ask, # stops are triggered by 2 consecutive bid or ask prices.
|
287
|
+
2 => :last, # stops are triggered based on the last price.
|
288
|
+
3 => :double_last,
|
289
|
+
4 => :bid_ask, # bid >= trigger price for buy orders, ask <= trigger for sell orders
|
290
|
+
7 => :last_or_bid_ask, # bid OR last price >= trigger price for buy orders
|
291
|
+
8 => :mid_point}, # midpoint >= trigger price for buy orders and the
|
292
|
+
# spread between the bid and ask must be less than 0.1% of the midpoint
|
293
|
+
|
294
|
+
:hedge_type =>
|
295
|
+
{'D' => :delta, # parent order is an option and the child order is a stock
|
296
|
+
'B' => :beta, # offset market risk by entering into a position with
|
297
|
+
# another contract based on the system or user-defined beta
|
298
|
+
'F' => :forex, # offset risk with currency different from your base currency
|
299
|
+
'P' => :pair}, # trade a mis-valued pair of contracts and provide the
|
300
|
+
# ratio between the parent and hedging child order
|
301
|
+
|
302
|
+
:clearing_intent =>
|
303
|
+
{'' => :none,
|
304
|
+
'IB' => :ib,
|
305
|
+
'AWAY' => :away,
|
306
|
+
'PTA' => :post_trade_allocation},
|
307
|
+
|
308
|
+
:delta_neutral_clearing_intent =>
|
309
|
+
{'' => :none,
|
310
|
+
'IB' => :ib,
|
311
|
+
'AWAY' => :away,
|
312
|
+
'PTA' => :post_trade_allocation},
|
313
|
+
|
314
|
+
:tif =>
|
315
|
+
{'DAY' => :day,
|
316
|
+
'GAT' => :good_after_time,
|
317
|
+
'GTD' => :good_till_date,
|
318
|
+
'GTC' => :good_till_cancelled,
|
319
|
+
'IOC' => :immediate_or_cancel,
|
320
|
+
'OPG' => :opening_price,
|
321
|
+
'AUC' => :at_auction},
|
322
|
+
|
323
|
+
:rule_80a =>
|
324
|
+
{'I' => :individual,
|
325
|
+
'A' => :agency,
|
326
|
+
'W' => :agent_other_member,
|
327
|
+
'J' => :individual_ptia,
|
328
|
+
'U' => :agency_ptia,
|
329
|
+
'M' => :agent_other_member_ptia,
|
330
|
+
'K' => :individual_pt,
|
331
|
+
'Y' => :agency_pt,
|
332
|
+
'N' => :agent_other_member_pt},
|
333
|
+
|
334
|
+
:opt? => # TODO: unknown Order property, like OPT_BROKER_DEALER... in Order.java
|
335
|
+
{'?' => :unknown,
|
336
|
+
'b' => :broker_dealer,
|
337
|
+
'c' => :customer,
|
338
|
+
'f' => :firm,
|
339
|
+
'm' => :isemm,
|
340
|
+
'n' => :farmm,
|
341
|
+
'y' => :specialist},
|
342
|
+
# conditions
|
343
|
+
:conjunction_connection => { 'o' => :or, 'a' => :and },
|
344
|
+
:operator => { 1 => '>=' , 0 => '<=' }
|
345
|
+
|
346
|
+
}.freeze
|
347
|
+
|
348
|
+
# Obtain property code from given symbolic value:
|
349
|
+
# CODES[:side][:buy] -> 'B'
|
350
|
+
CODES = Hash[VALUES.map { |property, hash| [property, hash.invert] }].freeze
|
351
|
+
|
352
|
+
# Most common property processors
|
353
|
+
PROPS = {:side =>
|
354
|
+
{:set => proc { |val| # BUY(BOT)/SELL(SLD)/SSHORT/SSHORTX
|
355
|
+
self[:side] = case val.to_s.upcase
|
356
|
+
when /SHORT.*X|\AX\z/
|
357
|
+
'X'
|
358
|
+
when /SHORT|\AT\z/
|
359
|
+
'T'
|
360
|
+
when /\AB/
|
361
|
+
'B'
|
362
|
+
when /\AS/
|
363
|
+
'S'
|
364
|
+
end },
|
365
|
+
:validate =>
|
366
|
+
{:format =>
|
367
|
+
{:with => /\Abuy\z|\Asell\z|\Ashort\z|\Ashort_exempt\z/,
|
368
|
+
:message => "should be buy/sell/short"}
|
369
|
+
}
|
370
|
+
},
|
371
|
+
|
372
|
+
:open_close =>
|
373
|
+
{:set => proc { |val|
|
374
|
+
self[:open_close] = case val.to_s.upcase[0..0]
|
375
|
+
when 'S', '0' # SAME
|
376
|
+
0
|
377
|
+
when 'O', '1' # OPEN
|
378
|
+
1
|
379
|
+
when 'C', '2' # CLOSE
|
380
|
+
2
|
381
|
+
when 'U', '3' # Unknown
|
382
|
+
3
|
383
|
+
end
|
384
|
+
},
|
385
|
+
:validate =>
|
386
|
+
{:format =>
|
387
|
+
{:with => /\Asame\z|\Aopen\z|\Aclose\z|\Aunknown\z/,
|
388
|
+
:message => "should be same/open/close/unknown"}
|
389
|
+
},
|
390
|
+
}
|
391
|
+
}.freeze
|
392
|
+
|
393
|
+
end # module IB
|