ib-api 10.33.1

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.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +52 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/CLAUDE.md +131 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +17 -0
  8. data/Gemfile.lock +120 -0
  9. data/Guardfile +24 -0
  10. data/LICENSE +674 -0
  11. data/LLM_GUIDE.md +388 -0
  12. data/README.md +114 -0
  13. data/Rakefile +11 -0
  14. data/VERSION +1 -0
  15. data/api.gemspec +50 -0
  16. data/bin/console +96 -0
  17. data/bin/console.yml +3 -0
  18. data/bin/setup +8 -0
  19. data/bin/simple +91 -0
  20. data/changelog.md +32 -0
  21. data/conditions/ib/execution_condition.rb +31 -0
  22. data/conditions/ib/margin_condition.rb +28 -0
  23. data/conditions/ib/order_condition.rb +29 -0
  24. data/conditions/ib/percent_change_condition.rb +34 -0
  25. data/conditions/ib/price_condition.rb +44 -0
  26. data/conditions/ib/time_condition.rb +42 -0
  27. data/conditions/ib/volume_condition.rb +36 -0
  28. data/lib/class_extensions.rb +167 -0
  29. data/lib/ib/base.rb +109 -0
  30. data/lib/ib/base_properties.rb +178 -0
  31. data/lib/ib/connection.rb +573 -0
  32. data/lib/ib/constants.rb +402 -0
  33. data/lib/ib/contract.rb +30 -0
  34. data/lib/ib/errors.rb +52 -0
  35. data/lib/ib/messages/abstract_message.rb +68 -0
  36. data/lib/ib/messages/incoming/abstract_message.rb +116 -0
  37. data/lib/ib/messages/incoming/abstract_tick.rb +25 -0
  38. data/lib/ib/messages/incoming/account_message.rb +26 -0
  39. data/lib/ib/messages/incoming/alert.rb +34 -0
  40. data/lib/ib/messages/incoming/contract_data.rb +105 -0
  41. data/lib/ib/messages/incoming/contract_message.rb +13 -0
  42. data/lib/ib/messages/incoming/delta_neutral_validation.rb +23 -0
  43. data/lib/ib/messages/incoming/execution_data.rb +50 -0
  44. data/lib/ib/messages/incoming/histogram_data.rb +30 -0
  45. data/lib/ib/messages/incoming/historical_data.rb +65 -0
  46. data/lib/ib/messages/incoming/historical_data_update.rb +50 -0
  47. data/lib/ib/messages/incoming/managed_accounts.rb +21 -0
  48. data/lib/ib/messages/incoming/market_depth.rb +34 -0
  49. data/lib/ib/messages/incoming/market_depth_l2.rb +15 -0
  50. data/lib/ib/messages/incoming/next_valid_id.rb +19 -0
  51. data/lib/ib/messages/incoming/open_order.rb +290 -0
  52. data/lib/ib/messages/incoming/order_status.rb +85 -0
  53. data/lib/ib/messages/incoming/portfolio_value.rb +47 -0
  54. data/lib/ib/messages/incoming/position_data.rb +21 -0
  55. data/lib/ib/messages/incoming/positions_multi.rb +15 -0
  56. data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
  57. data/lib/ib/messages/incoming/receive_fa.rb +30 -0
  58. data/lib/ib/messages/incoming/scanner_data.rb +54 -0
  59. data/lib/ib/messages/incoming/tick_by_tick.rb +77 -0
  60. data/lib/ib/messages/incoming/tick_efp.rb +18 -0
  61. data/lib/ib/messages/incoming/tick_generic.rb +12 -0
  62. data/lib/ib/messages/incoming/tick_option.rb +60 -0
  63. data/lib/ib/messages/incoming/tick_price.rb +60 -0
  64. data/lib/ib/messages/incoming/tick_size.rb +55 -0
  65. data/lib/ib/messages/incoming/tick_string.rb +13 -0
  66. data/lib/ib/messages/incoming.rb +292 -0
  67. data/lib/ib/messages/outgoing/abstract_message.rb +84 -0
  68. data/lib/ib/messages/outgoing/bar_request_message.rb +247 -0
  69. data/lib/ib/messages/outgoing/new-place-order.rb +193 -0
  70. data/lib/ib/messages/outgoing/old-place-order.rb +147 -0
  71. data/lib/ib/messages/outgoing/place_order.rb +149 -0
  72. data/lib/ib/messages/outgoing/request_account_summary.rb +79 -0
  73. data/lib/ib/messages/outgoing/request_historical_data.rb +182 -0
  74. data/lib/ib/messages/outgoing/request_market_data.rb +102 -0
  75. data/lib/ib/messages/outgoing/request_market_depth.rb +57 -0
  76. data/lib/ib/messages/outgoing/request_real_time_bars.rb +48 -0
  77. data/lib/ib/messages/outgoing/request_scanner_subscription.rb +73 -0
  78. data/lib/ib/messages/outgoing/request_tick_by_tick_data.rb +21 -0
  79. data/lib/ib/messages/outgoing.rb +410 -0
  80. data/lib/ib/messages.rb +139 -0
  81. data/lib/ib/order_condition.rb +26 -0
  82. data/lib/ib/plugins.rb +27 -0
  83. data/lib/ib/prepare_data.rb +61 -0
  84. data/lib/ib/raw_message_parser.rb +99 -0
  85. data/lib/ib/socket.rb +83 -0
  86. data/lib/ib/support.rb +236 -0
  87. data/lib/ib/version.rb +6 -0
  88. data/lib/ib-api.rb +44 -0
  89. data/lib/server_versions.rb +145 -0
  90. data/lib/support/array_function.rb +28 -0
  91. data/lib/support/logging.rb +45 -0
  92. data/models/ib/account.rb +72 -0
  93. data/models/ib/account_value.rb +33 -0
  94. data/models/ib/bag.rb +55 -0
  95. data/models/ib/bar.rb +31 -0
  96. data/models/ib/combo_leg.rb +127 -0
  97. data/models/ib/contract.rb +411 -0
  98. data/models/ib/contract_detail.rb +118 -0
  99. data/models/ib/execution.rb +67 -0
  100. data/models/ib/forex.rb +12 -0
  101. data/models/ib/future.rb +64 -0
  102. data/models/ib/index.rb +14 -0
  103. data/models/ib/option.rb +149 -0
  104. data/models/ib/option_detail.rb +84 -0
  105. data/models/ib/order.rb +720 -0
  106. data/models/ib/order_state.rb +155 -0
  107. data/models/ib/portfolio_value.rb +86 -0
  108. data/models/ib/spread.rb +176 -0
  109. data/models/ib/stock.rb +25 -0
  110. data/models/ib/underlying.rb +32 -0
  111. data/plugins/ib/advanced-account.rb +442 -0
  112. data/plugins/ib/alerts/base-alert.rb +125 -0
  113. data/plugins/ib/alerts/gateway-alerts.rb +15 -0
  114. data/plugins/ib/alerts/order-alerts.rb +73 -0
  115. data/plugins/ib/auto-adjust.rb +0 -0
  116. data/plugins/ib/connection-tools.rb +122 -0
  117. data/plugins/ib/eod.rb +326 -0
  118. data/plugins/ib/greeks.rb +102 -0
  119. data/plugins/ib/managed-accounts.rb +274 -0
  120. data/plugins/ib/market-price.rb +150 -0
  121. data/plugins/ib/option-chain.rb +167 -0
  122. data/plugins/ib/order-flow.rb +157 -0
  123. data/plugins/ib/order-prototypes/abstract.rb +67 -0
  124. data/plugins/ib/order-prototypes/adaptive.rb +40 -0
  125. data/plugins/ib/order-prototypes/all-in-one.rb +46 -0
  126. data/plugins/ib/order-prototypes/combo.rb +46 -0
  127. data/plugins/ib/order-prototypes/forex.rb +40 -0
  128. data/plugins/ib/order-prototypes/limit.rb +193 -0
  129. data/plugins/ib/order-prototypes/market.rb +116 -0
  130. data/plugins/ib/order-prototypes/pegged.rb +169 -0
  131. data/plugins/ib/order-prototypes/premarket.rb +31 -0
  132. data/plugins/ib/order-prototypes/stop.rb +202 -0
  133. data/plugins/ib/order-prototypes/volatility.rb +39 -0
  134. data/plugins/ib/order-prototypes.rb +118 -0
  135. data/plugins/ib/probability-of-expiring.rb +109 -0
  136. data/plugins/ib/process-orders.rb +155 -0
  137. data/plugins/ib/roll.rb +86 -0
  138. data/plugins/ib/spread-prototypes/butterfly.rb +77 -0
  139. data/plugins/ib/spread-prototypes/calendar.rb +97 -0
  140. data/plugins/ib/spread-prototypes/stock-spread.rb +56 -0
  141. data/plugins/ib/spread-prototypes/straddle.rb +70 -0
  142. data/plugins/ib/spread-prototypes/strangle.rb +93 -0
  143. data/plugins/ib/spread-prototypes/vertical.rb +83 -0
  144. data/plugins/ib/spread-prototypes.rb +70 -0
  145. data/plugins/ib/symbols/abstract.rb +136 -0
  146. data/plugins/ib/symbols/bonds.rb +28 -0
  147. data/plugins/ib/symbols/cfd.rb +19 -0
  148. data/plugins/ib/symbols/combo.rb +46 -0
  149. data/plugins/ib/symbols/commodity.rb +17 -0
  150. data/plugins/ib/symbols/forex.rb +41 -0
  151. data/plugins/ib/symbols/futures.rb +127 -0
  152. data/plugins/ib/symbols/index.rb +43 -0
  153. data/plugins/ib/symbols/options.rb +99 -0
  154. data/plugins/ib/symbols/stocks.rb +44 -0
  155. data/plugins/ib/symbols/version.rb +5 -0
  156. data/plugins/ib/symbols.rb +118 -0
  157. data/plugins/ib/verify.rb +226 -0
  158. data/symbols/w20.yml +210 -0
  159. data/t.txt +20 -0
  160. data/update.md +71 -0
  161. metadata +327 -0
@@ -0,0 +1,573 @@
1
+ module IB
2
+ # Encapsulates API connection to TWS or Gateway
3
+ class Connection
4
+
5
+
6
+ ## -------------------------------------------- Interface ---------------------------------
7
+ ## public attributes: socket, next_local_id ( alias next_order_id)
8
+ ## public methods: connect (alias open), disconnect, connected?
9
+ ## subscribe, unsubscribe
10
+ ## send_message (alias dispatch)
11
+ ## place_order, modify_order, cancel_order
12
+ ## public data-queue: received, received?, wait_for, clear_received
13
+ ## misc: reader_running?
14
+
15
+ include ::Support::Logging # provides default_logger
16
+ include Plugins
17
+ include Workflow
18
+
19
+ mattr_accessor :current
20
+
21
+ attr_accessor :socket # Socket to IB server (TWS or Gateway)
22
+ attr_accessor :next_local_id # Next valid order id
23
+ attr_accessor :client_id
24
+ attr_accessor :server_version
25
+ attr_accessor :client_version
26
+ attr_accessor :host
27
+ attr_accessor :received
28
+ attr_accessor :port
29
+ attr_accessor :plugins
30
+ alias next_order_id next_local_id
31
+ alias next_order_id= next_local_id=
32
+
33
+ def workflow_state
34
+ @workflow_state
35
+ end
36
+
37
+ workflow do
38
+ state :virgin do
39
+ event :try_connection, transitions_to: :ready
40
+ event :activate_managed_accounts, transitions_to: :gateway_mode
41
+ event :collect_data, transitions_to: :lean_mode
42
+ end
43
+
44
+ state :lean_mode do
45
+ event :try_connection, transitions_to: :ready
46
+ end
47
+
48
+ state :gateway_mode do
49
+ event :try_connection, transitions_to: :ready
50
+ event :initialize_managed_accounts, transitions_to: :account_based_operations
51
+ end
52
+ state :ready do
53
+ event :initialize_managed_accounts, transitions_to: :account_based_operations # plugin managed_account
54
+ event :disconnect, transitions_to: :disconnected
55
+ end
56
+ state :disconnected do
57
+ event :try_connection, transitions_to: :ready
58
+ event :activate_managed_accounts, transitions_to: :gateway_mode # plugin managed_account
59
+
60
+ end
61
+
62
+ state :account_based_operations do
63
+ event :disconnect, transitions_to: :disconnected
64
+ event :initialize_order_handling, transitions_to: :account_based_orderflow # plugin process-orders
65
+ end
66
+
67
+ state :account_based_orderflow do
68
+ event :disconnect, transitions_to: :disconnected
69
+ end
70
+
71
+ on_transition do |from, to, triggering_event, *event_args|
72
+ logger.warn{ "Workflow:: #{workflow_state} -> #{to}" }
73
+ end
74
+ end
75
+
76
+
77
+ def initialize host: '127.0.0.1:4002', # combination of host + port
78
+ port: nil,
79
+ #:port => '7497', # TWS connection --> demo 7496: production
80
+ # connect: true, # Connect at initialization ---> disabled in favour of Connection.new.try_connection!
81
+ # received: true, # Keep all received messages in a @received Hash ---> disabled; automatically disabled in lean- and
82
+ # gateway-modus
83
+ logger: nil,
84
+ client_id: rand( 1001 .. 9999 ) ,
85
+ client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb
86
+ optional_capacities: "", # TWS-Version 974: "+PACEAPI"
87
+ plugins: [],
88
+ #server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb
89
+ **any_other_parameters_which_are_ignored
90
+ # V 974 release motes
91
+ # 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.
92
+
93
+ Connection.current = self
94
+ self.class.configure_logger logger
95
+ # enable specification of host and port through host: 'localhost:4002' as parameter
96
+ host, port = (host+':'+port.to_s).split(':')
97
+ # convert parameters into instance-variables and assign them
98
+ method(__method__).parameters.each do |type, k|
99
+ next unless type == :key ## available: key , keyrest
100
+ next if k.to_s == 'logger'
101
+ v = eval(k.to_s)
102
+ instance_variable_set("@#{k}", v) unless v.nil?
103
+ end
104
+
105
+ # A couple of locks to avoid race conditions in JRuby
106
+ @subscribe_lock = Mutex.new
107
+ @receive_lock = Mutex.new
108
+ @message_lock = Mutex.new
109
+
110
+ @parser = nil
111
+ @connected = false
112
+
113
+ @plugins.each do |name|
114
+ activate_plugin name
115
+ end
116
+
117
+ @next_local_id = nil
118
+
119
+ #
120
+ # this block is executed before tws-communication is established
121
+ # Its intended for globally available subscriptions of tws-messages
122
+ yield self if block_given?
123
+
124
+ end
125
+
126
+ # read actual order_id and
127
+ # connect if not connected
128
+ def update_next_order_id
129
+ q = Queue.new
130
+ subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id }
131
+ try_connection! unless connected?
132
+ send_message :RequestIds
133
+ th = Thread.new { sleep 5; q.close }
134
+ @next_local_id = q.pop
135
+ if q.closed?
136
+ error "Could not get NextValidID", :reader
137
+ else
138
+ th.kill
139
+ end
140
+ unsubscribe subscription
141
+ @next_local_id # return next_id
142
+ end
143
+
144
+ ### Working with connection
145
+ def connected?
146
+ @connected
147
+ end
148
+ #
149
+ ### Event – call through Connection-object.try_connection!
150
+ protected
151
+ def try_connection
152
+ logger.progname='IB::Connection#Event:TryConnection'
153
+ if connected?
154
+ error "Already connected!"
155
+ return
156
+ end
157
+ # TWS always sends NextValidId message at connect -subscribe save this id
158
+ subscribe(:NextValidId) do |msg|
159
+ logger.progname = "Connection"
160
+ @next_local_id = msg.local_id
161
+ logger.info { "Got next valid order id: #{@next_local_id}." }
162
+ end
163
+
164
+ self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible
165
+ socket.initialising_handshake
166
+ @parser = RawMessageParser.new socket
167
+ @parser.each do | the_message |
168
+ # socket.decode_message( socket.receive_messages ) do | the_message |
169
+ #puts "TheMessage :: #{the_message.inspect}"
170
+ @server_version = the_message.shift.to_i.freeze
171
+ error "ServerVersion does not match #{@server_version} <--> #{MAX_CLIENT_VER}" if @server_version != MAX_CLIENT_VER
172
+
173
+ @remote_connect_time = DateTime.parse the_message.shift.freeze
174
+ @local_connect_time = Time.now.freeze
175
+ @connected = true
176
+ break # only receive one message
177
+ end
178
+
179
+ # V100 initial handshake
180
+ # Parameters borrowed from the python client
181
+ start_api = 71
182
+ version = 2
183
+ # optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities
184
+ socket.send_messages start_api, version, @client_id , @optional_capacities
185
+ logger.fatal{ "Connected to server, version: #{@server_version}, " +
186
+ "using client-id: #{client_id},\n connection time: " +
187
+ "#{@local_connect_time} local, " +
188
+ "#{@remote_connect_time} remote." }
189
+ start_reader
190
+ rescue IB::TransmissionError =>e
191
+ logger.fatal "Transmission Error: Retrying establishing the connection"
192
+ logger.fatal e.msg
193
+ disconnect!
194
+ try_connection!
195
+ # update_next_order_id
196
+ end
197
+
198
+
199
+ ### Event – call through Connection-object.disconnect!
200
+ def disconnect
201
+ if reader_running?
202
+ @reader_running = false
203
+ @reader_thread.join
204
+ end
205
+ socket.close
206
+ @connected = false
207
+ end
208
+
209
+
210
+ public
211
+
212
+ # disconnect and restart communication with the tws.
213
+ #
214
+ # cancels all subscriptions and reestablishes standard
215
+ # subscriptions for the current workflow state.
216
+ #
217
+ # connects if called in the disconnected state
218
+ #
219
+ #
220
+ # Usecase:
221
+ # 3.2.0 :015 > Symbols::Stocks.msft.verify
222
+ # A: Error reading request. Unable to parse data. java.lang.NumberFormatException: For input string: "MSFT"
223
+ #
224
+ # ^C/usr/share/rvm/rubies/ruby-3.2.0/lib/ruby/3.2.0/irb.rb:438:in `raise': abort then interrupt! (IRB::Abort)
225
+ # from <internal:thread_sync>:18:in `pop'
226
+ # from /home/ubuntu/labor/ib-api/plugins/ib/verify.rb:164:in `_verify'
227
+ # from /home/ubuntu/labor/ib-api/plugins/ib/verify.rb:78:in `verify'
228
+ # from (irb):15:in `<main>'
229
+ # from ./console:96:in `<main>'
230
+ # 3.2.0 :016 > C.reconnect
231
+ # F: Connected to server, version: 165, using client-id: 2000,
232
+ # connection time: 2024-11-21 20:57:57 +0100 local, 2024-11-21T19:57:57+00:00 remote.
233
+ # => 227896
234
+ # 3.2.0 :017 > Symbols::Stocks.msft.verify
235
+ # => [<Stock: MSFT USD NASDAQ>]
236
+ #
237
+ def reconnect
238
+ return if workflow_state == "virgin"
239
+ old_workflowstate = workflow_state.dup
240
+ disconnect! unless disconnected?
241
+
242
+ unsubscribe *@subscribers.map{|_,m| m.keys}.flatten.uniq
243
+
244
+ if ["ready", "lean_mode", "disconnected"].include? old_workflowstate
245
+ try_connection!
246
+ else
247
+ activate_managed_accounts!
248
+ unless old_workflowstate == 'gateway_mode'
249
+ initialize_managed_accounts!
250
+ initialize_order_handling! unless old_workflowstate != "account_based_orderflow"
251
+ end
252
+ end
253
+ end
254
+
255
+
256
+ ### Working with message subscribers
257
+
258
+ # Subscribe Proc or block to specific type(s) of incoming message events.
259
+ # Listener will be called later with received message instance as its argument.
260
+ # Returns subscriber id to allow unsubscribing
261
+ def subscribe *args, &block
262
+ @subscribe_lock.synchronize do
263
+ subscriber = args.last.respond_to?(:call) ? args.pop : block
264
+ id = random_id
265
+
266
+ error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc
267
+
268
+ args.each do |what|
269
+ message_classes =
270
+ case
271
+ when what.is_a?(Class) && what < IB::Messages::Incoming::AbstractMessage
272
+ [what]
273
+ when what.is_a?(Symbol)
274
+ if IB::Messages::Incoming.const_defined?(what)
275
+ [IB::Messages::Incoming.const_get(what)]
276
+ # elsif TechnicalAnalysis::Signals.const_defined?(what)
277
+ # [TechnicalAnalysis::Signals.const_get?(what)]
278
+ else
279
+ error "#{what} is no IB::Messages class"
280
+ end
281
+ when what.is_a?(Regexp)
282
+ Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what }
283
+ else
284
+ error "#{what} must represent incoming IB message class"
285
+ end
286
+ # @subscribers_lock.synchronize do
287
+ message_classes.flatten.each do |message_class|
288
+ # TODO: Fix: RuntimeError: can't add a new key into hash during iteration
289
+ subscribers[message_class][id] = subscriber
290
+ end
291
+ # end # lock
292
+ end
293
+
294
+ id
295
+ end
296
+ end
297
+
298
+ # Remove all subscribers with specific subscriber id
299
+ def unsubscribe *ids
300
+ @subscribe_lock.synchronize do
301
+ ids.collect do |id|
302
+ removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact
303
+ logger.error "No subscribers with id #{id}" if removed_at_id.empty?
304
+ removed_at_id # return_value
305
+ end.flatten
306
+ end
307
+ end
308
+
309
+
310
+ ### Working with received messages Hash
311
+ # Clear received messages Hash
312
+ def clear_received *message_types
313
+ @receive_lock.synchronize do
314
+ if message_types.empty?
315
+ received.each { |message_type, container| container.clear }
316
+ else
317
+ message_types.each { |message_type| received[message_type].clear }
318
+ end
319
+ end
320
+ end
321
+
322
+
323
+
324
+ # Hash of received messages, keyed by message type
325
+ def received
326
+ @received_hash ||= Hash.new do |hash, message_type|
327
+ # enable access to the hash via
328
+ # ib.received[:MessageType].attribute
329
+ the_array = Array.new
330
+ def the_array.method_missing(method, *key)
331
+ unless method == :to_hash || method == :to_str #|| method == :to_int
332
+ return self.map{|x| x.public_send(method, *key)}
333
+ end
334
+ end
335
+ hash[message_type] = the_array
336
+ end
337
+ end
338
+
339
+ # Check if messages of given type were received at_least n times
340
+ def received? message_type, times=1
341
+ @receive_lock.synchronize do
342
+ received[message_type].size >= times
343
+ end
344
+ end
345
+
346
+
347
+ # Wait for specific condition(s) - given as callable/block, or
348
+ # message type(s) - given as Symbol or [Symbol, times] pair.
349
+ # Timeout after given time or 1 second.
350
+ # wait_for depends on Connection#received. If collection of messages through recieved
351
+ # is turned off, wait_for loses most of its functionality
352
+
353
+ def wait_for *args, &block
354
+ timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
355
+ end_time = Time.now + (timeout || 1) # default timeout 1 sec
356
+ conditions = args.delete_if { |arg| arg.is_a? Numeric }.push(block).compact
357
+
358
+ until end_time < Time.now || satisfied?(*conditions)
359
+ if reader_running?
360
+ sleep 0.05
361
+ else
362
+ process_messages 50
363
+ end
364
+ end
365
+ end
366
+
367
+ ### Working with Incoming messages from IB
368
+
369
+
370
+ protected
371
+ def reader_running?
372
+ @reader_running && @reader_thread && @reader_thread.alive?
373
+ end
374
+
375
+ # Process incoming messages during *poll_time* (200) msecs, nonblocking
376
+ def process_messages poll_time = 50 # in msec
377
+ time_out = Time.now + poll_time/1000.0
378
+ begin
379
+ while (time_left = time_out - Time.now) > 0
380
+ # If socket is readable, process single incoming message
381
+ # windows environment: just read the socket
382
+ if RUBY_PLATFORM.match(/cygwin|mswin|mingw|bccwin|wince|emx/)
383
+ process_message if select [socket], nil, nil, time_left
384
+ else
385
+ # the following checks for shutdown of TWS side; ensures we don't run in a spin loop.
386
+ # unfortunately, it raises Errors in windows environment
387
+ if select [socket], nil, nil, time_left
388
+ # Peek at the message from the socket; if it's blank then the
389
+ # server side of connection (TWS) has likely shut down.
390
+ socket_likely_shutdown = socket.recvmsg(100, ::Socket::MSG_PEEK)[0] == ""
391
+ # We go ahead process messages regardless (a no-op if socket_likely_shutdown).
392
+ process_message
393
+ # After processing, if socket has shut down we sleep for 100ms
394
+ # to avoid spinning in a tight loop. If the server side somehow
395
+ # comes back up (gets reconnedted), normal processing
396
+ # (without the 100ms wait) should happen.
397
+ sleep(0.1) if socket_likely_shutdown
398
+ end # if
399
+ end # if
400
+ end # while
401
+ rescue Errno::ECONNRESET => e
402
+ logger.fatal e.message
403
+ if e.message =~ /Connection reset by peer/
404
+ logger.fatal "Is another client listening on the same port?"
405
+ error "try reconnecting with a different client-id", :reader
406
+ else
407
+ logger.fatal "Aborting"
408
+ Kernel.exit
409
+ end
410
+ end
411
+ end
412
+
413
+ ### Sending Outgoing messages to IB
414
+
415
+ # Send an outgoing message.
416
+ # returns the used request_id if appropiate, otherwise "true"
417
+ public
418
+ def send_message what, *args
419
+ message =
420
+ case
421
+ when what.is_a?(Messages::Outgoing::AbstractMessage)
422
+ what
423
+ when what.is_a?(Class) && what < Messages::Outgoing::AbstractMessage
424
+ what.new *args
425
+ when what.is_a?(Symbol)
426
+ Messages::Outgoing.const_get(what).new *args
427
+ else
428
+ error "Only able to send outgoing IB messages"
429
+ end
430
+ error "Not able to send messages, IB not connected!" unless connected?
431
+ begin
432
+ @message_lock.synchronize do
433
+ message.send_to socket
434
+ end
435
+ rescue Errno::EPIPE
436
+ logger.error{ "Broken Pipe, trying to reconnect" }
437
+ reconnect
438
+ retry
439
+ end
440
+ ## return the transmitted message
441
+ message.data[:request_id].presence || true
442
+ end
443
+
444
+ alias dispatch send_message # Legacy alias
445
+
446
+ # Place Order (convenience wrapper for send_message :PlaceOrder).
447
+ # Assigns client_id and order_id fields to placed order. Returns assigned order_id.
448
+ def place_order order, contract
449
+ # order.place contract, self ## old
450
+ error "Unable to place order, next_local_id not known" unless @next_local_id
451
+ error "local_id present. Order is already placed. Do might use modify insteed" unless order.local_id.nil?
452
+ order.client_id = client_id
453
+ order.local_id = @next_local_id
454
+ @next_local_id += 1
455
+ order.placed_at = Time.now
456
+ modify_order order, contract
457
+ end
458
+
459
+ # Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id.
460
+ def modify_order order, contract
461
+ error "Unable to modify order; local_id not specified" if order.local_id.nil?
462
+ order.modified_at = Time.now
463
+ # if con_id is present, to place an order use only con_id and exchange
464
+ send_message :PlaceOrder,
465
+ :order => order.then{|y| y.contract = nil; y},
466
+ :contract => contract.con_id.to_i > 0 ? Contract.new( con_id: contract.con_id, exchange: contract.exchange || 'SMART' ) : contract,
467
+ :local_id => order.local_id
468
+ order.local_id # return value
469
+ end
470
+
471
+ # Cancel Orders by their local ids (convenience wrapper for send_message :CancelOrder).
472
+ def cancel_order *local_ids
473
+ local_ids.each do |local_id|
474
+ send_message :CancelOrder, :local_id => local_id.to_i
475
+ end
476
+ end
477
+
478
+ # Start reader thread that continuously reads messages from @socket in background.
479
+ # If you don't start reader, you should manually poll @socket for messages
480
+ # or use #process_messages(msec) API.
481
+ protected
482
+ def start_reader
483
+ if @reader_running
484
+ @reader_thread
485
+ else # connected? # if called from try_connection, the connected state is not set
486
+ begin
487
+ Thread.abort_on_exception = true
488
+ @reader_running = true
489
+ @reader_thread = Thread.new { process_messages while @reader_running }
490
+ rescue Errno::ECONNRESET => e
491
+ logger.fatal e.message
492
+ reconnect
493
+ end
494
+ # else
495
+ # error "Could not start reader, not connected!", :reader, true
496
+ end
497
+ end
498
+
499
+ # Message subscribers. Key is the message class to listen for.
500
+ # Value is a Hash of subscriber Procs, keyed by their subscription id.
501
+ # All subscriber Procs will be called with the message instance
502
+ # as an argument when a message of that type is received.
503
+ def subscribers
504
+ @subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new }
505
+ end
506
+
507
+ # Process single incoming message (blocking!)
508
+ def process_message
509
+ logger.progname='IB::Connection#process_message'
510
+
511
+ ## decode mesage is included throught `prepare_data
512
+ # socket.decode_message( socket.receive_messages ) do | the_decoded_message |
513
+ @parser.each do | the_decoded_message |
514
+ # puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}"
515
+ msg_id = the_decoded_message.shift.to_i
516
+
517
+ # Debug:
518
+ # logger.debug { "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"}
519
+
520
+ # Create new instance of the appropriate message type,
521
+ # and have it read the message from socket.
522
+ # NB: Failure here usually means unsupported message type received
523
+
524
+ ## raising IB::TransmissionError if something went wrong.
525
+ ## the calling program has to initiate reconnection
526
+ logger.fatal { the_decoded_message } unless Messages::Incoming::Classes[msg_id]
527
+ error "Got unsupported message #{msg_id}", :reader unless Messages::Incoming::Classes[msg_id]
528
+ error "Something strange happened - Reader has to be restarted" , :reader, true if msg_id.to_i.zero?
529
+ begin
530
+ msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message)
531
+ rescue IB::TransmissionError
532
+ logger.fatal { the_decoded_message }
533
+ raise
534
+ end
535
+
536
+ # Deliver message to all registered subscribers, alert if no subscribers
537
+ # Ruby 2.0 and above: Hashes are ordered.
538
+ # Thus first declared subscribers of a class are executed first
539
+ @subscribe_lock.synchronize do
540
+ subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
541
+ end
542
+ logger.info { "No subscribers for message #{msg.class}!" } if subscribers[msg.class].empty?
543
+
544
+ # Collect all received messages into a @received Hash
545
+ if @received
546
+ @receive_lock.synchronize do
547
+ received[msg.message_type] << msg
548
+ end
549
+ end
550
+ end
551
+ end
552
+
553
+ def random_id
554
+ rand 999999
555
+ end
556
+
557
+ # Check if all given conditions are satisfied
558
+ def satisfied? *conditions
559
+ !conditions.empty? &&
560
+ conditions.inject(true) do |result, condition|
561
+ result && if condition.is_a?(Symbol)
562
+ received?(condition)
563
+ elsif condition.is_a?(Array)
564
+ received?(*condition)
565
+ elsif condition.respond_to?(:call)
566
+ condition.call
567
+ else
568
+ logger.error { "Unknown wait condition #{condition}" }
569
+ end
570
+ end
571
+ end
572
+ end # class Connection
573
+ end # module IB