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,274 @@
1
+ module IB
2
+
3
+ =begin
4
+
5
+ Plugin for Managed Accounts
6
+
7
+ Provides `clients` and `advisor` objects (Type: IB::Account) that contain account-specific data.
8
+
9
+ Public Api
10
+ ==========
11
+
12
+ * InitializeManagedAccounts
13
+ * populates @accounts through RequestFA
14
+ * should be called instead of `connect`
15
+
16
+
17
+ * GetAccountData
18
+ * requests account- and portfolio-data and associates them to the clients
19
+
20
+ * provides
21
+ * client.account_values
22
+ * client.portfolio_values
23
+ * client.contracts
24
+
25
+
26
+ The plugin should be activated **before** the connection attempt.
27
+
28
+ **IB::Connection.current.initialize_manage_acounts performs a `connect` to the tws-server**`
29
+
30
+
31
+ Standard usage
32
+
33
+ ib = IB::Connection.new
34
+ ib.activate_plugin 'managed-accounts'
35
+ ib.initialize_managed_accounts! # connects to the tws
36
+ ib.get_account_data # populates c.clients
37
+ account = ib.clients.first
38
+ puts account.portfolio_values.as_table
39
+
40
+ =end
41
+
42
+ module ManagedAccounts
43
+
44
+
45
+ =begin
46
+ clients returns a list of Account-Objects
47
+
48
+ If only one Account is present, Client and Advisor are identical.
49
+ =end
50
+ def clients
51
+ @accounts.find_all &:user?
52
+ end
53
+
54
+ # is the account a financial advisor
55
+ def fa?
56
+ !(advisor == clients.first)
57
+ end
58
+
59
+
60
+ =begin
61
+ The Advisor is always the first account
62
+ =end
63
+ def advisor
64
+ @accounts.first
65
+ end
66
+
67
+
68
+ =begin
69
+ --------------------------- GetAccountData --------------------------------------------
70
+ Queries for Account- and PortfolioValues
71
+ The parameter can either be the account_id, the IB::Account-Object or
72
+ an Array of account_id and IB::Account-Objects.
73
+
74
+ Resets Account#portfolio_values and -account_values
75
+
76
+ Raises an IB::TransmissionError if the account-data are not transmitted in time (1 sec)
77
+
78
+ Raises an IB::Error if less then 100 items are received.
79
+ =end
80
+ def get_account_data *accounts, **compatibily_argument
81
+
82
+ subscription = subscribe_account_updates( continuously: false )
83
+ download_end = nil # declare variable
84
+ received_array_status = received
85
+ self.received = false
86
+
87
+ accounts = clients if accounts.empty?
88
+ logger.warn{ "No active account present. AccountData are NOT requested" } if accounts.empty?
89
+ # Account-infos have to be requested sequentially.
90
+ # subsequent (parallel) calls kill the former on the tws-server-side
91
+ # In addition, there is no need to cancel the subscription of an request, as a new
92
+ # one overwrites the active one.
93
+ accounts.each do | ac |
94
+ account = ac.is_a?( IB::Account ) ? ac : clients.find{|x| x.account == ac }
95
+ error( "No Account detected " ) unless account.is_a? IB::Account
96
+ # don't repeat the query until 170 sec. have passed since the previous update
97
+ if account.last_updated.nil? || ( Time.now - account.last_updated ) > 170 # sec
98
+ logger.debug{ "#{account.account} :: Erasing Account- and Portfolio Data " }
99
+ logger.debug{ "#{account.account} :: Requesting AccountData " }
100
+
101
+ q = Queue.new
102
+ download_end = subscribe( :AccountDownloadEnd ) do | msg |
103
+ q.push true if msg.account_name == account.account
104
+ end
105
+ # reset account and portfolio-values
106
+ account.portfolio_values = []
107
+ account.account_values = []
108
+ # Data are gathered asynchron through the active subscription defined in `subscribe_account_updates`
109
+ send_message :RequestAccountData, subscribe: true, account_code: account.account
110
+
111
+ th = Thread.new{ sleep 10 ; q.close } # close the queue after 10 seconds
112
+ q.pop # wait for the data (or the closing event)
113
+
114
+ if q.closed?
115
+ error "No AccountData received", :reader
116
+ else
117
+ q.close
118
+ unsubscribe download_end
119
+ end
120
+
121
+ # account.organize_portfolio_positions unless IB::Gateway.current.active_watchlists.empty?
122
+ else
123
+ logger.info{ "#{account.account} :: Using stored AccountData " }
124
+ end
125
+ end
126
+ send_message :RequestAccountData, subscribe: false ## do this only once
127
+ unsubscribe subscription
128
+
129
+ self.received = received_array_status
130
+ rescue IB::TransmissionError => e
131
+ unsubscribe download_end unless download_end.nil?
132
+ unsubscribe subscription
133
+ raise
134
+ end
135
+
136
+
137
+ def all_contracts
138
+ clients.map(&:contracts).flat_map(&:itself).uniq(&:con_id)
139
+ end
140
+
141
+
142
+ =begin
143
+ --------------------------- InitializeManageAccounts ----------------------------------
144
+
145
+ If initiated with the parameter `force: true`, any active connection is terminated.
146
+ All subscriptiona are lost. The connection ist then re-established to initiate the
147
+ transmission of available managed-accounts by the tws.
148
+
149
+ =end
150
+ protected
151
+ def initialize_managed_accounts( force: false )
152
+ queue = Queue.new
153
+ if connected?
154
+ disconnect!
155
+ sleep(0.1)
156
+ end
157
+ try_connection!
158
+ @accounts = []
159
+ # Ensure reader thread is running to process messages
160
+ start_reader unless reader_running?
161
+ # in case of advisor-accounts: proper initialiastion of account records
162
+ rec_id = subscribe( :ReceiveFA ) do |msg|
163
+ msg.accounts.each do |a|
164
+ account_data( a.account ){| the_account | the_account.update_attribute :alias, a.alias } unless a.alias.blank?
165
+ end
166
+ logger.info { "Accounts initialized \n #{@accounts.map( &:to_human ).join " \n " }" }
167
+ queue.push(true)
168
+ end
169
+
170
+ # initialisation of Account after a successful connection
171
+ man_id = subscribe( :ManagedAccounts ) do |msg|
172
+ @accounts = msg.accounts
173
+ send_message( :RequestFA, fa_data_type: 3)
174
+ end
175
+
176
+ # single accounts return an alert message
177
+ error_id = subscribe( :Alert ){|x| queue.push(false) if x.code == 321 }
178
+
179
+ # Add timeout to prevent deadlock - wait up to 10 seconds for response
180
+ timeout_thread = Thread.new { sleep 10; queue.push(:timeout) unless queue.closed? }
181
+ result = queue.pop
182
+ timeout_thread.kill if timeout_thread.alive?
183
+ timeout_thread.join rescue nil
184
+
185
+ if result == :timeout
186
+ error "Timeout waiting for account initialization", :reader
187
+ unsubscribe man_id, rec_id, error_id
188
+ raise IB::TransmissionError, "Timeout waiting for ManagedAccounts/ReceiveFA messages"
189
+ end
190
+
191
+ unsubscribe man_id, rec_id, error_id
192
+
193
+ @accounts
194
+
195
+ end # def
196
+
197
+ # The subscription method should called only once per session.
198
+ # It places subscribers to AccountValue and PortfolioValue Messages, which should remain
199
+ # active through the session.
200
+ #
201
+ # The method returns the subscription-number.
202
+ #
203
+ # thus
204
+ # subscription = subscribe_account_updates
205
+ # # some code
206
+ # IB::Connection.current.unsubscribe subscription
207
+ #
208
+ # clears the subscription
209
+ #
210
+
211
+ private
212
+ def subscribe_account_updates continuously: true
213
+
214
+ add_or_update = ->(apv, new_hash) do
215
+ existing_index = apv.index { |h| h.contract.con_id == new_hash.contract.con_id }
216
+ if existing_index
217
+ apv[existing_index] = new_hash
218
+ else
219
+ apv << new_hash
220
+ end
221
+ end
222
+ subscribe( :AccountValue, :PortfolioValue,:AccountDownloadEnd ) do | msg |
223
+ account_data( msg.account_name ) do | account | # enter mutex controlled zone
224
+ case msg
225
+ when IB::Messages::Incoming::AccountValue
226
+ account.account_values << msg.account_value
227
+ account.update_attribute :last_updated, Time.now
228
+ IB::Connection.logger.debug { "#{account.account} :: #{msg.account_value.to_human }"}
229
+ when IB::Messages::Incoming::AccountDownloadEnd
230
+ if account.account_values.size > 10
231
+ account.update_attribute :connected, true ## flag: Account is completely initialized
232
+ IB::Connection.logger.info { "#{account.account} => Count of AccountValues: #{account.account_values.size}" }
233
+ else # unreasonable account_data received - request is still active
234
+ error "#{account.account} => Count of AccountValues too small: #{account.account_values.size}" , :reader
235
+ end
236
+ when IB::Messages::Incoming::PortfolioValue
237
+ account.contracts << msg.contract unless account.contracts.detect{|y| y.con_id == msg.contract.con_id }
238
+ add_or_update[account.portfolio_values,msg.portfolio_value]
239
+ IB::Connection.logger.debug { "#{ account.account } :: #{ msg.contract.to_human }" }
240
+ end # case
241
+ end # account_data
242
+ end # subscribe
243
+ end # def
244
+ # safe access to account-data
245
+ def account_data account_or_id=nil
246
+
247
+ if account_or_id.present?
248
+ account = account_or_id.is_a?(IB::Account) ? account_or_id : @accounts.detect{|x| x.account == account_or_id }
249
+ yield account
250
+ else
251
+ @accounts.map{|a| yield a}
252
+ end
253
+
254
+ end
255
+
256
+ alias activate_managed_accounts subscribe_account_updates
257
+
258
+
259
+ end
260
+
261
+ class Connection
262
+ include ManagedAccounts
263
+ # Note: Automatic initialization may need to be removed if the program becomes unstable.
264
+ # This automatic call to activate_managed_accounts! can cause race conditions and deadlocks,
265
+ # particularly when the plugin is loaded mid-session. Consider removing this automatic
266
+ # initialization and requiring users to explicitly call initialize_managed_accounts! when needed.
267
+ current.activate_managed_accounts!
268
+ rescue Workflow::NoTransitionAllowed => e
269
+ if current.workflow_state == :ready
270
+ current.disconnect!
271
+ resume
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,150 @@
1
+ module IB
2
+
3
+ module MarketPrice
4
+ # Ask for the Market-Price
5
+ #
6
+ # For valid contracts, either bid/ask or last_price and close_price are transmitted.
7
+ #
8
+ # If last_price is received, its returned.
9
+ # If not, midpoint (bid+ask/2) is used. Else the closing price will be returned.
10
+ #
11
+ # Any value (even 0.0) which is stored in IB::Contract.misc indicates that the contract is
12
+ # accepted by `request_market_data` and will be accepted by `place_order`, too.
13
+ #
14
+ # The result can be customized by a provided block.
15
+ #
16
+ # ```ruby
17
+ # IB::Symbols::Stocks.sie.market_price{ |x| x }
18
+ # -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3}
19
+ # ```
20
+ #
21
+ #
22
+ # Raw-data are stored in the _bars_-property of IB::Contract
23
+ # (volatile, ie. data are not preserved when the Object is reused via Contract#merge)
24
+ #
25
+ # ```ruby
26
+ # u= (z1=IB::Stock.new(symbol: :ge)).market_price
27
+ # A: Requested market data is not subscribed. Displaying delayed market data.
28
+ # > u => 0.16975e3
29
+ # > z1 => #<IB::Stock:0x00007f91037f0e18
30
+ # @attributes= { :symbol =>"ge", (...)
31
+ # :currency => "USD",
32
+ # :exchange => "SMART" },
33
+ # @bars = [ { last: -0.1e1, close: 0.16975e3, bid: -0.1e1, ask: -0.1e1 } ],
34
+ # @misc = { delayed: 0.16975e3 }
35
+ #
36
+ # ```
37
+ #
38
+ # Fetching of market-data is a time consuming process. A threaded approach is suitable
39
+ # to get a bunch of market-data in time
40
+ #
41
+ # ```ruby
42
+ # th = (z2 = IB::Stock.new(symbol: :ge)).market_price(thread: true)
43
+ # th.join
44
+ # ```
45
+ # assigns z2.misc with the value of the :last (or delayed_last) TickPrice-Message
46
+ # and returns the thread.
47
+ #
48
+
49
+ def market_price delayed: true, thread: false, no_error: false
50
+
51
+ tws= Connection.current # get the initialized ib-ruby instance
52
+ the_id , the_price = nil, nil
53
+ tickdata = Hash.new
54
+ q = Queue.new
55
+ # define requested tick-attributes
56
+ last, close, bid, ask = [ [ :delayed_last , :last_price ] , [:delayed_close , :close_price ],
57
+ [ :delayed_bid , :bid_price ], [ :delayed_ask , :ask_price ]]
58
+ request_data_type = delayed ? :frozen_delayed : :frozen
59
+
60
+ # From the tws-documentation (https://interactivebrokers.github.io/tws-api/market_data_type.html)
61
+ # Beginning in TWS v970, a IBApi.EClient.reqMarketDataType callback of 1 will occur automatically
62
+ # after invoking reqMktData if the user has live data permissions for the instrument.
63
+ #
64
+ # so - even if "delayed" is specified, realtime-data are returned if RT-permissions are present
65
+ #
66
+
67
+ # method returns the (running) thread
68
+ th = Thread.new do
69
+ # about 11 sec after the request, the TWS returns :TickSnapshotEnd if no ticks are transmitted
70
+ # we don't have to implement our own timeout-criteria
71
+ s_id = tws.subscribe(:TickSnapshotEnd){|x| q.push(true) if x.ticker_id == the_id }
72
+ a_id = tws.subscribe(:Alert){|x| q.push(x) if [200, 354, 10167, 10168].include?( x.code ) && x.error_id == the_id }
73
+ # TWS Error 354: Requested market data is not subscribed.
74
+
75
+ # subscribe to TickPrices
76
+ sub_id = tws.subscribe(:TickPrice ) do |msg| #, :TickSize, :TickGeneric, :TickOption) do |msg|
77
+ [last,close,bid,ask].each do |x|
78
+ tickdata[x] = msg.the_data[:price] if x.include?( IB::TICK_TYPES[ msg.the_data[:tick_type]])
79
+ # fast exit condition
80
+ q.push(true) if tickdata.size >= 4
81
+ end if msg.ticker_id == the_id
82
+ end
83
+ # initialize »the_id« that is used to identify the received tick messages
84
+ # by firing the market data request
85
+ the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
86
+
87
+ while !q.closed? do
88
+ result = q.pop
89
+ if result.is_a? IB::Messages::Incoming::Alert
90
+ tws.logger.debug result.message
91
+ case result.code
92
+ when 200
93
+ q.close
94
+ error "#{to_human} --> #{result.message}" unless no_error
95
+ when 354, # not subscribed to market data
96
+ 10167,
97
+ 10168
98
+ if delayed && !(result.message =~ /market data is not available/)
99
+ tws.logger.debug "#{to_human} --> requesting delayed data"
100
+ tws.send_message :RequestMarketDataType, :market_data_type => 3
101
+ self.misc = :delayed
102
+ sleep 0.1
103
+ the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
104
+ else
105
+ q.close
106
+ tws.logger.error "#{to_human} --> No marketdata permissions" unless no_error
107
+ end
108
+ end
109
+ elsif result.present?
110
+ q.close
111
+ tz = -> (z){ z.map{|y| y.to_s.split('_')}.flatten.count_duplicates.max_by{|k,v| v}.first.to_sym}
112
+ data = tickdata.map{|x,y| [tz[x],y]}.to_h
113
+ valid_data = ->(d){ !(d.to_i.zero? || d.to_i == -1) }
114
+ self.bars << data # store raw data in bars
115
+ the_price = if block_given?
116
+ yield data
117
+ # yields {:bid=>0.10142e3, :ask=>0.10144e3, :last=>0.10142e3, :close=>0.10172e3}
118
+ else # behavior if no block is provided
119
+ if valid_data[data[:last]]
120
+ data[:last]
121
+ elsif valid_data[data[:bid]]
122
+ (data[:bid]+data[:ask])/2
123
+ elsif data[:close].present?
124
+ data[:close]
125
+ else
126
+ nil
127
+ end
128
+ end
129
+
130
+ self.misc = misc == :delayed ? { :delayed => the_price } : { realtime: the_price }
131
+ else
132
+ q.close
133
+ error "#{to_human} --> No Marketdata received "
134
+ end
135
+ end
136
+
137
+ tws.unsubscribe sub_id, s_id, a_id
138
+ end
139
+ if thread
140
+ th # return thread
141
+ else
142
+ th.join
143
+ the_price # return
144
+ end
145
+ end #
146
+ end
147
+ class Contract
148
+ include MarketPrice
149
+ end
150
+ end
@@ -0,0 +1,167 @@
1
+ module IB
2
+
3
+ module OptionChain
4
+
5
+ # returns the Option Chain (monthly options, expiry: third friday)
6
+ # of the contract (if available)
7
+ #
8
+ #
9
+ ## parameters
10
+ ### right:: :call, :put, :straddle ( default: :put )
11
+ ### ref_price:: :request or a numeric value ( default: :request )
12
+ ### sort:: :strike, :expiry
13
+ ### exchange:: List of Exchanges to be queried ( default: SMART)
14
+ ### trading_class ( optional )
15
+ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', trading_class: nil
16
+
17
+ ib = Connection.current
18
+
19
+ # binary interthread communication
20
+ finalize = Queue.new
21
+
22
+ ## Enable Cashing of Definition-Matrix
23
+ @option_chain_definition ||= []
24
+
25
+ my_req = nil
26
+
27
+ # -----------------------------------------------------------------------------------------------------
28
+ # get OptionChainDefinition from IB ( instantiate cashed Hash )
29
+ if @option_chain_definition.blank?
30
+ sub_sdop = ib.subscribe( :SecurityDefinitionOptionParameterEnd ) { |msg| finalize.push(true) if msg.request_id == my_req }
31
+ sub_ocd = ib.subscribe( :OptionChainDefinition ) do | msg |
32
+ if msg.request_id == my_req
33
+ message = msg.data
34
+ # transfer the first record to @option_chain_definition
35
+ if @option_chain_definition.blank?
36
+ @option_chain_definition = msg.data
37
+ end
38
+ # override @option_chain_definition if a decent combination of attributes is met
39
+ # us- options: use the smart dataset
40
+ # other options: prefer options of the default trading class
41
+ # if message[:exchange] == 'SMART'
42
+ # @option_chain_definition = msg.data
43
+ # finalize.push(true)
44
+ # end
45
+ if message[:trading_class] == symbol
46
+ @option_chain_definition = msg.data
47
+ finalize.push(true)
48
+ end
49
+ end
50
+ end
51
+
52
+ c = verify.first # ensure a complete set of attributes
53
+ my_req = ib.send_message :RequestOptionChainDefinition, con_id: c.con_id,
54
+ symbol: c.symbol,
55
+ exchange: c.sec_type == :future ? c.exchange : "", # BOX,CBOE',
56
+ sec_type: c[:sec_type]
57
+
58
+ finalize.pop # wait until data appeared
59
+
60
+ ib.unsubscribe sub_sdop, sub_ocd
61
+ else
62
+ Connection.logger.info { "#{to_human} : using cached data" }
63
+ end
64
+
65
+ # -----------------------------------------------------------------------------------------------------
66
+ # select values and assign to options
67
+ #
68
+ unless @option_chain_definition.blank?
69
+ requested_strikes = if block_given?
70
+ ref_price = market_price if ref_price == :request
71
+ if ref_price.nil?
72
+ ref_price = @option_chain_definition[:strikes].min +
73
+ ( @option_chain_definition[:strikes].max -
74
+ @option_chain_definition[:strikes].min ) / 2
75
+ Connection.logger.warn { "#{to_human} :: market price not set – using midpoint of available strikes instead: #{ref_price.to_f}" }
76
+ end
77
+ atm_strike = @option_chain_definition[:strikes].min_by { |x| (x - ref_price).abs }
78
+ the_grouped_strikes = @option_chain_definition[:strikes].group_by{|e| e <=> atm_strike}
79
+ begin
80
+ the_strikes = yield the_grouped_strikes
81
+ the_strikes.unshift atm_strike unless the_strikes.first == atm_strike # the first item is the atm-strike
82
+ the_strikes
83
+ rescue
84
+ Connection.logger.error "#{to_human} :: not enough strikes :#{@option_chain_definition[:strikes].map(&:to_f).join(',')} "
85
+ []
86
+ end
87
+ else
88
+ @option_chain_definition[:strikes]
89
+ end
90
+
91
+ # third Friday of a month
92
+ monthly_expirations = @option_chain_definition[:expirations].find_all {|y| (15..21).include? y.day }
93
+ # puts @option_chain_definition.inspect
94
+ option_prototype = -> ( ltd, strike ) do
95
+ IB::Option.new( symbol: symbol,
96
+ exchange: @option_chain_definition[:exchange],
97
+ trading_class: @option_chain_definition[:trading_class],
98
+ multiplier: @option_chain_definition[:multiplier],
99
+ currency: currency,
100
+ last_trading_day: ltd,
101
+ strike: strike,
102
+ right: right).verify &.first
103
+ end
104
+ options_by_expiry = -> ( schema ) do
105
+ # Array: [ yymm -> Options] prepares for the correct conversion to a Hash
106
+ Hash[ monthly_expirations.map do | l_t_d |
107
+ [ l_t_d.strftime('%y%m').to_i , schema.map { | strike | option_prototype[ l_t_d, strike ]}.compact ]
108
+ end ] # by Hash[ ]
109
+ end
110
+ options_by_strike = -> ( schema ) do
111
+ Hash[ schema.map do | strike |
112
+ [ strike , monthly_expirations.map { | l_t_d | option_prototype[ l_t_d, strike ]}.compact ]
113
+ end ] # by Hash[ ]
114
+ end
115
+
116
+ if sort == :strike
117
+ options_by_strike[ requested_strikes ]
118
+ else
119
+ options_by_expiry[ requested_strikes ]
120
+ end
121
+ else
122
+ Connection.logger.error "#{to_human} ::No Options available"
123
+ nil # return_value
124
+ end
125
+ end # def
126
+
127
+ # return a set of AtTheMoneyOptions
128
+ def atm_options ref_price: :request, right: :put, **params
129
+ option_chain( right: right, ref_price: ref_price, sort: :expiry, **params) do | chain |
130
+ chain[0]
131
+ end
132
+
133
+
134
+ end
135
+
136
+ # return InTheMoneyOptions
137
+ def itm_options count: 5, right: :put, ref_price: :request, sort: :strike, exchange: ''
138
+ option_chain( right: right, ref_price: ref_price, sort: sort, exchange: exchange ) do | chain |
139
+ if right == :put
140
+ above_market_price_strikes = chain[1][0..count-1]
141
+ else
142
+ below_market_price_strikes = chain[-1][-count..-1].reverse
143
+ end # branch
144
+ end
145
+ end # def
146
+
147
+ # return OutOfTheMoneyOptions
148
+ def otm_options count: 5, right: :put, ref_price: :request, sort: :strike, exchange: ''
149
+ option_chain( right: right, ref_price: ref_price, sort: sort, exchange: exchange ) do | chain |
150
+ if right == :put
151
+ # puts "Chain: #{chain}"
152
+ below_market_price_strikes = chain[-1][-count..-1].reverse
153
+ else
154
+ above_market_price_strikes = chain[1][0..count-1]
155
+ end
156
+ end
157
+ end
158
+ end # module
159
+
160
+ Connection.current.activate_plugin 'verify'
161
+ Connection.current.activate_plugin 'market-price'
162
+
163
+ class Contract
164
+ include OptionChain
165
+ end
166
+
167
+ end # module