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.
- checksums.yaml +7 -0
- data/.gitignore +52 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CLAUDE.md +131 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +120 -0
- data/Guardfile +24 -0
- data/LICENSE +674 -0
- data/LLM_GUIDE.md +388 -0
- data/README.md +114 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/api.gemspec +50 -0
- data/bin/console +96 -0
- data/bin/console.yml +3 -0
- data/bin/setup +8 -0
- data/bin/simple +91 -0
- data/changelog.md +32 -0
- data/conditions/ib/execution_condition.rb +31 -0
- data/conditions/ib/margin_condition.rb +28 -0
- data/conditions/ib/order_condition.rb +29 -0
- data/conditions/ib/percent_change_condition.rb +34 -0
- data/conditions/ib/price_condition.rb +44 -0
- data/conditions/ib/time_condition.rb +42 -0
- data/conditions/ib/volume_condition.rb +36 -0
- data/lib/class_extensions.rb +167 -0
- data/lib/ib/base.rb +109 -0
- data/lib/ib/base_properties.rb +178 -0
- data/lib/ib/connection.rb +573 -0
- data/lib/ib/constants.rb +402 -0
- data/lib/ib/contract.rb +30 -0
- data/lib/ib/errors.rb +52 -0
- data/lib/ib/messages/abstract_message.rb +68 -0
- data/lib/ib/messages/incoming/abstract_message.rb +116 -0
- data/lib/ib/messages/incoming/abstract_tick.rb +25 -0
- data/lib/ib/messages/incoming/account_message.rb +26 -0
- data/lib/ib/messages/incoming/alert.rb +34 -0
- data/lib/ib/messages/incoming/contract_data.rb +105 -0
- data/lib/ib/messages/incoming/contract_message.rb +13 -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/histogram_data.rb +30 -0
- data/lib/ib/messages/incoming/historical_data.rb +65 -0
- data/lib/ib/messages/incoming/historical_data_update.rb +50 -0
- data/lib/ib/messages/incoming/managed_accounts.rb +21 -0
- data/lib/ib/messages/incoming/market_depth.rb +34 -0
- data/lib/ib/messages/incoming/market_depth_l2.rb +15 -0
- data/lib/ib/messages/incoming/next_valid_id.rb +19 -0
- data/lib/ib/messages/incoming/open_order.rb +290 -0
- data/lib/ib/messages/incoming/order_status.rb +85 -0
- data/lib/ib/messages/incoming/portfolio_value.rb +47 -0
- data/lib/ib/messages/incoming/position_data.rb +21 -0
- data/lib/ib/messages/incoming/positions_multi.rb +15 -0
- data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
- data/lib/ib/messages/incoming/receive_fa.rb +30 -0
- data/lib/ib/messages/incoming/scanner_data.rb +54 -0
- data/lib/ib/messages/incoming/tick_by_tick.rb +77 -0
- data/lib/ib/messages/incoming/tick_efp.rb +18 -0
- data/lib/ib/messages/incoming/tick_generic.rb +12 -0
- data/lib/ib/messages/incoming/tick_option.rb +60 -0
- data/lib/ib/messages/incoming/tick_price.rb +60 -0
- data/lib/ib/messages/incoming/tick_size.rb +55 -0
- data/lib/ib/messages/incoming/tick_string.rb +13 -0
- data/lib/ib/messages/incoming.rb +292 -0
- data/lib/ib/messages/outgoing/abstract_message.rb +84 -0
- data/lib/ib/messages/outgoing/bar_request_message.rb +247 -0
- data/lib/ib/messages/outgoing/new-place-order.rb +193 -0
- data/lib/ib/messages/outgoing/old-place-order.rb +147 -0
- data/lib/ib/messages/outgoing/place_order.rb +149 -0
- data/lib/ib/messages/outgoing/request_account_summary.rb +79 -0
- data/lib/ib/messages/outgoing/request_historical_data.rb +182 -0
- data/lib/ib/messages/outgoing/request_market_data.rb +102 -0
- data/lib/ib/messages/outgoing/request_market_depth.rb +57 -0
- data/lib/ib/messages/outgoing/request_real_time_bars.rb +48 -0
- data/lib/ib/messages/outgoing/request_scanner_subscription.rb +73 -0
- data/lib/ib/messages/outgoing/request_tick_by_tick_data.rb +21 -0
- data/lib/ib/messages/outgoing.rb +410 -0
- data/lib/ib/messages.rb +139 -0
- data/lib/ib/order_condition.rb +26 -0
- data/lib/ib/plugins.rb +27 -0
- data/lib/ib/prepare_data.rb +61 -0
- data/lib/ib/raw_message_parser.rb +99 -0
- data/lib/ib/socket.rb +83 -0
- data/lib/ib/support.rb +236 -0
- data/lib/ib/version.rb +6 -0
- data/lib/ib-api.rb +44 -0
- data/lib/server_versions.rb +145 -0
- data/lib/support/array_function.rb +28 -0
- data/lib/support/logging.rb +45 -0
- data/models/ib/account.rb +72 -0
- data/models/ib/account_value.rb +33 -0
- data/models/ib/bag.rb +55 -0
- data/models/ib/bar.rb +31 -0
- data/models/ib/combo_leg.rb +127 -0
- data/models/ib/contract.rb +411 -0
- data/models/ib/contract_detail.rb +118 -0
- data/models/ib/execution.rb +67 -0
- data/models/ib/forex.rb +12 -0
- data/models/ib/future.rb +64 -0
- data/models/ib/index.rb +14 -0
- data/models/ib/option.rb +149 -0
- data/models/ib/option_detail.rb +84 -0
- data/models/ib/order.rb +720 -0
- data/models/ib/order_state.rb +155 -0
- data/models/ib/portfolio_value.rb +86 -0
- data/models/ib/spread.rb +176 -0
- data/models/ib/stock.rb +25 -0
- data/models/ib/underlying.rb +32 -0
- data/plugins/ib/advanced-account.rb +442 -0
- data/plugins/ib/alerts/base-alert.rb +125 -0
- data/plugins/ib/alerts/gateway-alerts.rb +15 -0
- data/plugins/ib/alerts/order-alerts.rb +73 -0
- data/plugins/ib/auto-adjust.rb +0 -0
- data/plugins/ib/connection-tools.rb +122 -0
- data/plugins/ib/eod.rb +326 -0
- data/plugins/ib/greeks.rb +102 -0
- data/plugins/ib/managed-accounts.rb +274 -0
- data/plugins/ib/market-price.rb +150 -0
- data/plugins/ib/option-chain.rb +167 -0
- data/plugins/ib/order-flow.rb +157 -0
- data/plugins/ib/order-prototypes/abstract.rb +67 -0
- data/plugins/ib/order-prototypes/adaptive.rb +40 -0
- data/plugins/ib/order-prototypes/all-in-one.rb +46 -0
- data/plugins/ib/order-prototypes/combo.rb +46 -0
- data/plugins/ib/order-prototypes/forex.rb +40 -0
- data/plugins/ib/order-prototypes/limit.rb +193 -0
- data/plugins/ib/order-prototypes/market.rb +116 -0
- data/plugins/ib/order-prototypes/pegged.rb +169 -0
- data/plugins/ib/order-prototypes/premarket.rb +31 -0
- data/plugins/ib/order-prototypes/stop.rb +202 -0
- data/plugins/ib/order-prototypes/volatility.rb +39 -0
- data/plugins/ib/order-prototypes.rb +118 -0
- data/plugins/ib/probability-of-expiring.rb +109 -0
- data/plugins/ib/process-orders.rb +155 -0
- data/plugins/ib/roll.rb +86 -0
- data/plugins/ib/spread-prototypes/butterfly.rb +77 -0
- data/plugins/ib/spread-prototypes/calendar.rb +97 -0
- data/plugins/ib/spread-prototypes/stock-spread.rb +56 -0
- data/plugins/ib/spread-prototypes/straddle.rb +70 -0
- data/plugins/ib/spread-prototypes/strangle.rb +93 -0
- data/plugins/ib/spread-prototypes/vertical.rb +83 -0
- data/plugins/ib/spread-prototypes.rb +70 -0
- data/plugins/ib/symbols/abstract.rb +136 -0
- data/plugins/ib/symbols/bonds.rb +28 -0
- data/plugins/ib/symbols/cfd.rb +19 -0
- data/plugins/ib/symbols/combo.rb +46 -0
- data/plugins/ib/symbols/commodity.rb +17 -0
- data/plugins/ib/symbols/forex.rb +41 -0
- data/plugins/ib/symbols/futures.rb +127 -0
- data/plugins/ib/symbols/index.rb +43 -0
- data/plugins/ib/symbols/options.rb +99 -0
- data/plugins/ib/symbols/stocks.rb +44 -0
- data/plugins/ib/symbols/version.rb +5 -0
- data/plugins/ib/symbols.rb +118 -0
- data/plugins/ib/verify.rb +226 -0
- data/symbols/w20.yml +210 -0
- data/t.txt +20 -0
- data/update.md +71 -0
- 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
|