ib-extensions 1.2 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +3 -4
- data/Gemfile.lock +52 -42
- data/README.md +56 -10
- data/bin/console +10 -3
- data/bin/gateway +14 -9
- data/changelog.md +54 -0
- data/ib-extensions.gemspec +7 -5
- data/lib/ib/alerts/base-alert.rb +10 -13
- data/lib/ib/eod.rb +243 -127
- data/lib/ib/extensions/version.rb +14 -1
- data/lib/ib/extensions.rb +5 -0
- data/lib/ib/gateway/account-infos.rb +74 -47
- data/lib/ib/gateway/order-handling.rb +57 -25
- data/lib/ib/gateway.rb +45 -31
- data/lib/ib/market-price.rb +34 -34
- data/lib/ib/models/account.rb +177 -144
- data/lib/ib/models/bag.rb +19 -0
- data/lib/ib/models/contract.rb +16 -0
- data/lib/ib/models/future.rb +20 -0
- data/lib/ib/models/option.rb +20 -13
- data/lib/ib/option-chain.rb +4 -6
- data/lib/ib/option-greeks.rb +27 -21
- data/lib/ib/order_prototypes/all-in-one.rb +46 -0
- data/lib/ib/plot-poec.rb +60 -0
- data/lib/ib/probability_of_expiring.rb +109 -0
- data/lib/ib/spread-prototypes.rb +1 -0
- data/lib/ib/spread_prototypes/butterfly.rb +2 -4
- data/lib/ib/spread_prototypes/calendar.rb +25 -23
- data/lib/ib/spread_prototypes/straddle.rb +3 -3
- data/lib/ib/spread_prototypes/strangle.rb +8 -9
- data/lib/ib/spread_prototypes/vertical.rb +6 -7
- data/lib/ib/verify.rb +34 -39
- data/lib/ib-gateway.rb +12 -0
- metadata +53 -5
data/lib/ib/eod.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
module IB
|
2
2
|
require 'active_support/core_ext/date/calculations'
|
3
3
|
require 'csv'
|
4
|
+
|
5
|
+
module Eod
|
4
6
|
module BuisinesDays
|
5
|
-
|
7
|
+
# https://stackoverflow.com/questions/4027768/calculate-number-of-business-days-between-two-days
|
6
8
|
|
7
9
|
# Calculates the number of business days in range (start_date, end_date]
|
8
10
|
#
|
@@ -14,138 +16,198 @@ require 'csv'
|
|
14
16
|
days_between = (end_date - start_date).to_i
|
15
17
|
return 0 unless days_between > 0
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
19
|
+
# Assuming we need to calculate days from 9th to 25th, 10-23 are covered
|
20
|
+
# by whole weeks, and 24-25 are extra days.
|
21
|
+
#
|
22
|
+
# Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
|
23
|
+
# 1 2 3 4 5 # 1 2 3 4 5
|
24
|
+
# 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww
|
25
|
+
# 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww
|
26
|
+
# 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26
|
27
|
+
# 27 28 29 30 31 # 27 28 29 30 31
|
28
|
+
whole_weeks, extra_days = days_between.divmod(7)
|
29
|
+
|
30
|
+
unless extra_days.zero?
|
31
|
+
# Extra days start from the week day next to start_day,
|
32
|
+
# and end on end_date's week date. The position of the
|
33
|
+
# start date in a week can be either before (the left calendar)
|
34
|
+
# or after (the right one) the end date.
|
35
|
+
#
|
36
|
+
# Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
|
37
|
+
# 1 2 3 4 5 # 1 2 3 4 5
|
38
|
+
# 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12
|
39
|
+
# ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ##
|
40
|
+
# 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26
|
41
|
+
# 27 28 29 30 31 # 27 28 29 30 31
|
42
|
+
#
|
43
|
+
# If some of the extra_days fall on a weekend, they need to be subtracted.
|
44
|
+
# In the first case only corner days can be days off,
|
45
|
+
# and in the second case there are indeed two such days.
|
46
|
+
extra_days -= if start_date.tomorrow.wday <= end_date.wday
|
47
|
+
[start_date.tomorrow.sunday?, end_date.saturday?].count(true)
|
48
|
+
else
|
49
|
+
2
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
(whole_weeks * 5) + extra_days
|
52
54
|
end
|
53
55
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
56
|
+
# Receive EOD-Data and store the data in the `:bars`-property of IB::Contract
|
57
|
+
#
|
58
|
+
# contract.eod duration: {String or Integer}, start: {Date}, to: {Date}, what: {see below}, polars: {true|false}
|
59
|
+
#
|
60
|
+
#
|
61
|
+
#
|
62
|
+
# The Enddate has to be specified (as Date Object), `:to`, default: Date.today
|
63
|
+
#
|
64
|
+
# The Duration can either be a String "yx D", "yd W", "yx M" or an Integer ( implies "D").
|
65
|
+
# *notice* "W" fetchtes weekly and "M" monthly bars
|
66
|
+
#
|
67
|
+
# A start date can be given with the `:start` parameter.
|
68
|
+
#
|
69
|
+
# The parameter `:what` specifies the kind of received data.
|
70
|
+
#
|
71
|
+
# Valid values:
|
72
|
+
# :trades, :midpoint, :bid, :ask, :bid_ask,
|
73
|
+
# :historical_volatility, :option_implied_volatility,
|
74
|
+
# :option_volume, :option_open_interest
|
75
|
+
#
|
76
|
+
# Polars DataFrames
|
77
|
+
# -----------------
|
78
|
+
# If »polars: true« is specified the response is stored as PolarsDataframe.
|
79
|
+
# For further processing: https://github.com/ankane/polars-ruby
|
80
|
+
# https://pola-rs.github.io/polars/py-polars/html/index.html
|
81
|
+
#
|
82
|
+
# Error-handling
|
83
|
+
# --------------
|
84
|
+
# * Basically all Errors simply lead to log-entries:
|
85
|
+
# * the contract is not valid,
|
86
|
+
# * no market data subscriptions
|
87
|
+
# * other servers-side errors
|
88
|
+
#
|
89
|
+
# If the duration is longer then the maximum range, the response is
|
90
|
+
# cut to the maximum allowed range
|
91
|
+
#
|
92
|
+
# Customize the result
|
93
|
+
# --------------------
|
94
|
+
# The results are stored in the `:bars` property of the contract
|
95
|
+
#
|
96
|
+
#
|
97
|
+
# Limitations
|
98
|
+
# -----------
|
99
|
+
# To identify a request, the con_id of the asset is used
|
100
|
+
# Thus, parallel requests of a single asset with different time-frames will fail
|
101
|
+
#
|
102
|
+
# Examples
|
103
|
+
# --------
|
104
|
+
#
|
105
|
+
# puts Stock.new( symbol: :iwm).eod( start: Date.new(2019,10,9), duration: 3, polars: true)
|
106
|
+
# shape: (3, 8)
|
107
|
+
# ┌────────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐
|
108
|
+
# │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │
|
109
|
+
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
|
110
|
+
# │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │
|
111
|
+
# ╞════════════╪════════╪════════╪════════╪════════╪════════╪═════════╪════════╡
|
112
|
+
# │ 2019-10-08 ┆ 148.62 ┆ 149.37 ┆ 146.11 ┆ 146.45 ┆ 156625 ┆ 146.831 ┆ 88252 │
|
113
|
+
# │ 2019-10-09 ┆ 147.18 ┆ 148.0 ┆ 145.38 ┆ 145.85 ┆ 94337 ┆ 147.201 ┆ 51294 │
|
114
|
+
# │ 2019-10-10 ┆ 146.9 ┆ 148.74 ┆ 146.87 ┆ 148.24 ┆ 134549 ┆ 147.792 ┆ 71084 │
|
115
|
+
# └────────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘
|
116
|
+
#
|
117
|
+
# puts Stock.new( symbol: :iwm).eod( start: Date.new(2021,10,9), duration: '3W', polars: true)
|
118
|
+
# shape: (3, 8)
|
119
|
+
# ┌────────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬────────┐
|
120
|
+
# │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │
|
121
|
+
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
|
122
|
+
# │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │
|
123
|
+
# ╞════════════╪════════╪════════╪════════╪════════╪═════════╪═════════╪════════╡
|
124
|
+
# │ 2021-10-01 ┆ 223.99 ┆ 227.68 ┆ 216.12 ┆ 222.8 ┆ 1295495 ┆ 222.226 ┆ 792711 │
|
125
|
+
# │ 2021-10-08 ┆ 221.4 ┆ 224.95 ┆ 216.76 ┆ 221.65 ┆ 1044233 ┆ 220.855 ┆ 621984 │
|
126
|
+
# │ 2021-10-15 ┆ 220.69 ┆ 228.41 ┆ 218.94 ┆ 225.05 ┆ 768065 ┆ 223.626 ┆ 437817 │
|
127
|
+
# └────────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴────────┘
|
128
|
+
#
|
129
|
+
# puts Stock.new( symbol: :iwm).eod( start: Date.new(2022,10,1), duration: '3M', polars: true)
|
130
|
+
# shape: (3, 8)
|
131
|
+
# ┌────────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬─────────┐
|
132
|
+
# │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │
|
133
|
+
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
|
134
|
+
# │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │
|
135
|
+
# ╞════════════╪════════╪════════╪════════╪════════╪═════════╪═════════╪═════════╡
|
136
|
+
# │ 2022-09-30 ┆ 181.17 ┆ 191.37 ┆ 162.77 ┆ 165.16 ┆ 4298969 ┆ 175.37 ┆ 2202407 │
|
137
|
+
# │ 2022-10-31 ┆ 165.5 ┆ 184.24 ┆ 162.5 ┆ 183.5 ┆ 4740014 ┆ 173.369 ┆ 2474286 │
|
138
|
+
# │ 2022-11-30 ┆ 184.51 ┆ 189.56 ┆ 174.11 ┆ 188.19 ┆ 3793861 ┆ 182.594 ┆ 1945674 │
|
139
|
+
# └────────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴─────────┘
|
140
|
+
#
|
141
|
+
# puts Stock.new( symbol: :iwm).eod( start: Date.new(2020,1,1), duration: '3M', what: :option_implied_vol, polars: true
|
142
|
+
# atility )
|
143
|
+
# shape: (3, 8)
|
144
|
+
# ┌────────────┬──────────┬──────────┬──────────┬──────────┬────────┬──────────┬────────┐
|
145
|
+
# │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │
|
146
|
+
# │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
|
147
|
+
# │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │
|
148
|
+
# ╞════════════╪══════════╪══════════╪══════════╪══════════╪════════╪══════════╪════════╡
|
149
|
+
# │ 2019-12-31 ┆ 0.134933 ┆ 0.177794 ┆ 0.115884 ┆ 0.138108 ┆ 0 ┆ 0.178318 ┆ 0 │
|
150
|
+
# │ 2020-01-31 ┆ 0.139696 ┆ 0.190494 ┆ 0.120646 ┆ 0.185732 ┆ 0 ┆ 0.19097 ┆ 0 │
|
151
|
+
# │ 2020-02-28 ┆ 0.185732 ┆ 0.436549 ┆ 0.134933 ┆ 0.39845 ┆ 0 ┆ 0.435866 ┆ 0 │
|
152
|
+
# └────────────┴──────────┴──────────┴──────────┴──────────┴────────┴──────────┴────────┘
|
153
|
+
#
|
154
|
+
def eod start: nil, to: nil, duration: nil , what: :trades, polars: false
|
95
155
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
105
|
-
recieved.push Time.now
|
106
|
-
end
|
107
|
-
b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg|
|
108
|
-
if [321,162,200].include? msg.code
|
109
|
-
tws.logger.info msg.message
|
110
|
-
# TWS Error 200: No security definition has been found for the request
|
111
|
-
# TWS Error 354: Requested market data is not subscribed.
|
112
|
-
# TWS Error 162 # Historical Market Data Service error
|
113
|
-
recieved.close
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
duration = if duration.present?
|
118
|
-
duration.is_a?(String) ? duration : duration.to_s + " D"
|
119
|
-
elsif start.present?
|
120
|
-
BuisinesDays.business_days_between(start, to).to_s + " D"
|
121
|
-
else
|
122
|
-
"1 D"
|
123
|
-
end
|
124
|
-
|
125
|
-
tws.send_message IB::Messages::Outgoing::RequestHistoricalData.new(
|
126
|
-
:request_id => con_id,
|
127
|
-
:contract => self,
|
128
|
-
:end_date_time => to.to_time.to_ib, # Time.now.to_ib,
|
129
|
-
:duration => duration, # ?
|
130
|
-
:bar_size => :day1, # IB::BAR_SIZES.key(:hour)?
|
131
|
-
:what_to_show => what,
|
132
|
-
:use_rth => 0,
|
133
|
-
:format_date => 2,
|
134
|
-
:keep_up_todate => 0)
|
135
|
-
|
136
|
-
recieved.pop # blocks until a message is ready on the queue or the queue is closed
|
137
|
-
|
138
|
-
tws.unsubscribe a
|
139
|
-
tws.unsubscribe b
|
156
|
+
# error "EOD:: Start-Date (parameter: to) must be a Date-Object" unless to.is_a? Date
|
157
|
+
normalize_duration = ->(d) do
|
158
|
+
if d.is_a?(Integer) || !["D","M","W","Y"].include?( d[-1].upcase )
|
159
|
+
d.to_i.to_s + "D"
|
160
|
+
else
|
161
|
+
d.gsub(" ","")
|
162
|
+
end.insert(-2, " ")
|
163
|
+
end
|
140
164
|
|
141
|
-
|
165
|
+
get_end_date = -> do
|
166
|
+
d = normalize_duration.call(duration)
|
167
|
+
case d[-1]
|
168
|
+
when "D"
|
169
|
+
start + d.to_i - 1
|
170
|
+
when 'W'
|
171
|
+
Date.commercial( start.year, start.cweek + d.to_i - 1, 1)
|
172
|
+
when 'M'
|
173
|
+
Date.new( start.year, start.month + d.to_i - 1 , start.day )
|
174
|
+
end
|
175
|
+
end
|
142
176
|
|
143
|
-
|
177
|
+
if to.nil?
|
178
|
+
# case eod start= Date.new ...
|
179
|
+
to = if start.present? && duration.nil?
|
180
|
+
# case eod start= Date.new
|
181
|
+
duration = BuisinesDays.business_days_between(start, to).to_s + "D"
|
182
|
+
Date.today # assign to var: to
|
183
|
+
elsif start.present? && duration.present?
|
184
|
+
# case eod start= Date.new , duration: 'nN'
|
185
|
+
get_end_date.call # assign to var: to
|
186
|
+
elsif duration.present?
|
187
|
+
# case start is not present, we are collecting until the present day
|
188
|
+
Date.today # assign to var: to
|
189
|
+
else
|
190
|
+
duration = "1D"
|
191
|
+
Date.today
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
barsize = case normalize_duration.call(duration)[-1].upcase
|
196
|
+
when "W"
|
197
|
+
:week1
|
198
|
+
when "M"
|
199
|
+
:month1
|
200
|
+
else
|
201
|
+
:day1
|
202
|
+
end
|
144
203
|
|
145
|
-
# creates (or overwrites) the specified file (or symbol.csv) and saves bar-data
|
146
|
-
def to_csv file:nil
|
147
|
-
file ||= "#{symbol}.csv"
|
148
204
|
|
205
|
+
get_bars(to.to_time.to_ib , normalize_duration[duration], barsize, what, polars)
|
206
|
+
|
207
|
+
end # def
|
208
|
+
|
209
|
+
# creates (or overwrites) the specified file (or symbol.csv) and saves bar-data
|
210
|
+
def to_csv file: "#{symbol}.csv"
|
149
211
|
if bars.present?
|
150
212
|
headers = bars.first.invariant_attributes.keys
|
151
213
|
CSV.open( file, 'w' ) {|f| f << headers ; bars.each {|y| f << y.invariant_attributes.values } }
|
@@ -160,6 +222,60 @@ require 'csv'
|
|
160
222
|
self.bars << IB::Bar.new( **row.to_h )
|
161
223
|
end
|
162
224
|
end
|
225
|
+
|
226
|
+
def get_bars(end_date_time, duration, bar_size, what_to_show, polars)
|
227
|
+
|
228
|
+
tws = IB::Connection.current
|
229
|
+
received = Queue.new
|
230
|
+
r = nil
|
231
|
+
# the hole response is transmitted at once!
|
232
|
+
a = tws.subscribe(IB::Messages::Incoming::HistoricalData) do |msg|
|
233
|
+
if msg.request_id == con_id
|
234
|
+
self.bars = if polars
|
235
|
+
# msg.results.each { |entry| puts " #{entry}" }
|
236
|
+
Polars::DataFrame.new msg.results.map( &:invariant_attributes )
|
237
|
+
else
|
238
|
+
msg.results
|
239
|
+
end
|
240
|
+
end
|
241
|
+
received.push Time.now
|
242
|
+
end
|
243
|
+
b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg|
|
244
|
+
if [321,162,200].include? msg.code
|
245
|
+
tws.logger.info msg.message
|
246
|
+
# TWS Error 200: No security definition has been found for the request
|
247
|
+
# TWS Error 354: Requested market data is not subscribed.
|
248
|
+
# TWS Error 162 # Historical Market Data Service error
|
249
|
+
received.close
|
250
|
+
elsif msg.code.to_i == 2174
|
251
|
+
tws.logger.info "Please switch to the \"10-19\"-Branch of the git-repository"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
tws.send_message IB::Messages::Outgoing::RequestHistoricalData.new(
|
257
|
+
:request_id => con_id,
|
258
|
+
:contract => self,
|
259
|
+
:end_date_time => end_date_time,
|
260
|
+
:duration => duration, # see ib/messages/outgoing/bar_request.rb => max duration for 5sec bar lookback is 10 000 - i.e. will yield 2000 bars
|
261
|
+
:bar_size => bar_size, # IB::BAR_SIZES.key(:hour)
|
262
|
+
:what_to_show => what_to_show,
|
263
|
+
:use_rth => 0,
|
264
|
+
:format_date => 2,
|
265
|
+
:keep_up_todate => 0)
|
266
|
+
|
267
|
+
received.pop # blocks until a message is ready on the queue or the queue is closed
|
268
|
+
|
269
|
+
tws.unsubscribe a
|
270
|
+
tws.unsubscribe b
|
271
|
+
|
272
|
+
block_given? ? bars.map{|y| yield y} : bars # return bars or result of block
|
273
|
+
|
274
|
+
end # def
|
275
|
+
end # module eod
|
276
|
+
|
277
|
+
class Contract
|
278
|
+
include Eod
|
163
279
|
end # class
|
164
|
-
end # module
|
280
|
+
end # module IB
|
165
281
|
|
@@ -1,5 +1,18 @@
|
|
1
1
|
module IB
|
2
2
|
module Extensions
|
3
|
-
VERSION = "1.
|
3
|
+
VERSION = "1.3.1"
|
4
4
|
end
|
5
5
|
end
|
6
|
+
|
7
|
+
|
8
|
+
__END__
|
9
|
+
|
10
|
+
Changelog V. 1.2 -> 1.3
|
11
|
+
|
12
|
+
* added probability_of_expiring to IB::Option
|
13
|
+
* IB::Contract.eod returns a Polars DataFrame
|
14
|
+
* improved IB::Option.request_greeks
|
15
|
+
* improved IB::Contract.verify
|
16
|
+
|
17
|
+
1.3.1: Contract.eod: Parameter polars: true|false
|
18
|
+
|
data/lib/ib/extensions.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
## require any file, except gateway and related
|
2
|
+
require "distribution"
|
3
|
+
require "polars"
|
2
4
|
require "ib/extensions/version"
|
3
5
|
require "ib/verify"
|
4
6
|
require "ib/eod"
|
@@ -8,3 +10,6 @@ require "ib/option-greeks"
|
|
8
10
|
require "ib/order-prototypes"
|
9
11
|
require "ib/spread-prototypes"
|
10
12
|
require "ib/models/option"
|
13
|
+
require "ib/models/future"
|
14
|
+
require "ib/models/contract"
|
15
|
+
require "ib/models/bag"
|
@@ -5,64 +5,82 @@ module IB
|
|
5
5
|
class Alert
|
6
6
|
class << self
|
7
7
|
def alert_2101 msg
|
8
|
-
|
9
|
-
@status_2101 = msg.dup
|
8
|
+
IB::Connection.logger.error {msg.message}
|
9
|
+
@status_2101 = msg.dup
|
10
10
|
end
|
11
11
|
|
12
12
|
def status_2101 account # resets status and raises IB::TransmissionError
|
13
13
|
error account.account + ": " +@status_2101.message, :reader unless @status_2101.nil?
|
14
|
-
@status_2101 = nil # always returns nil
|
14
|
+
@status_2101 = nil # always returns nil
|
15
15
|
end
|
16
16
|
end
|
17
|
-
end
|
17
|
+
end
|
18
18
|
end # module
|
19
19
|
|
20
20
|
module AccountInfos
|
21
21
|
|
22
22
|
=begin
|
23
23
|
Queries the tws for Account- and PortfolioValues
|
24
|
-
The parameter can either be the account_id, the IB::Account-Object or
|
24
|
+
The parameter can either be the account_id, the IB::Account-Object or
|
25
25
|
an Array of account_id and IB::Account-Objects.
|
26
26
|
|
27
|
-
|
27
|
+
Resets Account#portfolio_values and -account_values
|
28
|
+
|
29
|
+
Raises an IB::TransmissionError if the account-data are not transmitted in time (1 sec)
|
28
30
|
|
29
|
-
|
31
|
+
Raises an IB::Error if less then 100 items are received.
|
30
32
|
=end
|
31
|
-
|
33
|
+
def get_account_data *accounts, **compatibily_argument
|
32
34
|
|
33
35
|
|
34
|
-
|
36
|
+
subscription = subscribe_account_updates( continuously: false )
|
37
|
+
download_end = nil # declare variable
|
35
38
|
|
36
|
-
accounts =
|
37
|
-
|
38
|
-
# Account-infos have to be requested
|
39
|
-
# subsequent (parallel) calls kill the former
|
39
|
+
accounts = clients if accounts.empty?
|
40
|
+
IB::Connection.logger.warn{ "No active account present. AccountData are NOT requested" } if accounts.empty?
|
41
|
+
# Account-infos have to be requested sequentially.
|
42
|
+
# subsequent (parallel) calls kill the former on the tws-server-side
|
40
43
|
# In addition, there is no need to cancel the subscription of an request, as a new
|
41
44
|
# one overwrites the active one.
|
42
45
|
accounts.each do | ac |
|
43
|
-
account = ac.is_a?( IB::Account ) ? ac : clients.find{|x| x.account == ac }
|
46
|
+
account = ac.is_a?( IB::Account ) ? ac : clients.find{|x| x.account == ac }
|
44
47
|
error( "No Account detected " ) unless account.is_a? IB::Account
|
45
48
|
# don't repeat the query until 170 sec. have passed since the previous update
|
46
|
-
if account.last_updated.nil? || ( Time.now - account.last_updated ) > 170 # sec
|
47
|
-
|
48
|
-
|
49
|
+
if account.last_updated.nil? || ( Time.now - account.last_updated ) > 170 # sec
|
50
|
+
IB::Connection.logger.debug{ "#{account.account} :: Erasing Account- and Portfolio Data " }
|
51
|
+
IB::Connection.logger.debug{ "#{account.account} :: Requesting AccountData " }
|
52
|
+
|
53
|
+
q = Queue.new
|
54
|
+
download_end = tws.subscribe( :AccountDownloadEnd ) do | msg |
|
55
|
+
q.push true if msg.account_name == account.account
|
56
|
+
end
|
49
57
|
# reset account and portfolio-values
|
50
|
-
account.portfolio_values =
|
51
|
-
account.account_values =
|
58
|
+
account.portfolio_values = []
|
59
|
+
account.account_values = []
|
60
|
+
# Data are gathered asynchron through the active subscription defined in `subscribe_account_updates`
|
52
61
|
send_message :RequestAccountData, subscribe: true, account_code: account.account
|
53
|
-
|
54
|
-
#
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
+
|
63
|
+
th = Thread.new{ sleep 10 ; q.close } # close the queue after 10 seconds
|
64
|
+
q.pop # wait for the data (or the closing event)
|
65
|
+
|
66
|
+
if q.closed?
|
67
|
+
error "No AccountData received", :reader
|
68
|
+
else
|
69
|
+
q.close
|
70
|
+
tws.unsubscribe download_end
|
71
|
+
end
|
72
|
+
|
73
|
+
account.organize_portfolio_positions unless IB::Gateway.current.active_watchlists.empty?
|
62
74
|
else
|
63
|
-
|
75
|
+
IB::Connection.logger.info{ "#{account.account} :: Using stored AccountData " }
|
64
76
|
end
|
65
77
|
end
|
78
|
+
tws.send_message :RequestAccountData, subscribe: false ## do this only once
|
79
|
+
tws.unsubscribe subscription
|
80
|
+
rescue IB::TransmissionError => e
|
81
|
+
tws.unsubscribe download_end unless download_end.nil?
|
82
|
+
tws.unsubscribe subscription
|
83
|
+
raise
|
66
84
|
end
|
67
85
|
|
68
86
|
|
@@ -75,40 +93,49 @@ raises an IB::Error if less then 100 items are recieved-
|
|
75
93
|
|
76
94
|
# The subscription method should called only once per session.
|
77
95
|
# It places subscribers to AccountValue and PortfolioValue Messages, which should remain
|
78
|
-
# active through
|
79
|
-
|
80
|
-
|
81
|
-
|
96
|
+
# active through the session.
|
97
|
+
#
|
98
|
+
# The method returns the subscription-number.
|
99
|
+
#
|
100
|
+
# thus
|
101
|
+
# subscription = subscribe_account_updates
|
102
|
+
# # some code
|
103
|
+
# IB::Connection.current.unsubscribe subscription
|
104
|
+
#
|
105
|
+
# clears the subscription
|
106
|
+
#
|
107
|
+
|
108
|
+
def subscribe_account_updates continuously: true
|
82
109
|
tws.subscribe( :AccountValue, :PortfolioValue,:AccountDownloadEnd ) do | msg |
|
83
110
|
account_data( msg.account_name ) do | account | # enter mutex controlled zone
|
84
111
|
case msg
|
85
112
|
when IB::Messages::Incoming::AccountValue
|
86
113
|
account.account_values << msg.account_value
|
87
114
|
account.update_attribute :last_updated, Time.now
|
88
|
-
|
89
|
-
when IB::Messages::Incoming::AccountDownloadEnd
|
115
|
+
IB::Connection.logger.debug { "#{account.account} :: #{msg.account_value.to_human }"}
|
116
|
+
when IB::Messages::Incoming::AccountDownloadEnd
|
90
117
|
if account.account_values.size > 10
|
91
|
-
# simply don't cancel the
|
118
|
+
# simply don't cancel the subscription if continuously is specified
|
92
119
|
# the connected flag is set in any case, indicating that valid data are present
|
93
|
-
|
120
|
+
# tws.send_message :RequestAccountData, subscribe: false, account_code: account.account unless continuously
|
94
121
|
account.update_attribute :connected, true ## flag: Account is completely initialized
|
95
|
-
|
96
|
-
else # unreasonable account_data
|
97
|
-
error "#{account.account} => Count of AccountValues too small: #{account.account_values.size}" , :reader
|
122
|
+
IB::Connection.logger.info { "#{account.account} => Count of AccountValues: #{account.account_values.size}" }
|
123
|
+
else # unreasonable account_data received - request is still active
|
124
|
+
error "#{account.account} => Count of AccountValues too small: #{account.account_values.size}" , :reader
|
98
125
|
end
|
99
126
|
when IB::Messages::Incoming::PortfolioValue
|
100
|
-
|
101
|
-
|
127
|
+
account.contracts << msg.contract unless account.contracts.detect{|y| y.con_id == msg.contract.con_id }
|
128
|
+
account.portfolio_values << msg.portfolio_value
|
102
129
|
# msg.portfolio_value.account = account
|
103
|
-
|
130
|
+
# # link contract -> portfolio value
|
104
131
|
# account.contracts.find{ |x| x.con_id == msg.contract.con_id }
|
105
132
|
# .portfolio_values
|
106
|
-
# .update_or_create( msg.portfolio_value ) { :account }
|
107
|
-
|
133
|
+
# .update_or_create( msg.portfolio_value ) { :account }
|
134
|
+
IB::Connection.logger.debug { "#{ account.account } :: #{ msg.contract.to_human }" }
|
108
135
|
end # case
|
109
|
-
end # account_data
|
136
|
+
end # account_data
|
110
137
|
end # subscribe
|
111
|
-
end # def
|
138
|
+
end # def
|
112
139
|
|
113
140
|
|
114
141
|
end # module
|