ib-extensions 1.2 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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
- # https://stackoverflow.com/questions/4027768/calculate-number-of-business-days-between-two-days
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
- # Assuming we need to calculate days from 9th to 25th, 10-23 are covered
18
- # by whole weeks, and 24-25 are extra days.
19
- #
20
- # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
21
- # 1 2 3 4 5 # 1 2 3 4 5
22
- # 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww
23
- # 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww
24
- # 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26
25
- # 27 28 29 30 31 # 27 28 29 30 31
26
- whole_weeks, extra_days = days_between.divmod(7)
27
-
28
- unless extra_days.zero?
29
- # Extra days start from the week day next to start_day,
30
- # and end on end_date's week date. The position of the
31
- # start date in a week can be either before (the left calendar)
32
- # or after (the right one) the end date.
33
- #
34
- # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
35
- # 1 2 3 4 5 # 1 2 3 4 5
36
- # 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12
37
- # ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ##
38
- # 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26
39
- # 27 28 29 30 31 # 27 28 29 30 31
40
- #
41
- # If some of the extra_days fall on a weekend, they need to be subtracted.
42
- # In the first case only corner days can be days off,
43
- # and in the second case there are indeed two such days.
44
- extra_days -= if start_date.tomorrow.wday <= end_date.wday
45
- [start_date.tomorrow.sunday?, end_date.saturday?].count(true)
46
- else
47
- 2
48
- end
49
- end
50
-
51
- (whole_weeks * 5) + extra_days
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
- class Contract
55
- # Receive EOD-Data
56
- #
57
- # The Enddate has to be specified (as Date Object), t
58
- #
59
- # The Duration can either be specified as Sting " yx D" or as Integer.
60
- # Altenative a start date can be specified with the :start parameter.
61
- #
62
- # The parameter :what specified the kind of received data:
63
- # Valid values:
64
- # :trades, :midpoint, :bid, :ask, :bid_ask,
65
- # :historical_volatility, :option_implied_volatility,
66
- # :option_volume, :option_open_interest
67
- #
68
- # The results can be preprocessed through a block, thus
69
- #
70
- # puts IB::Symbols::Index::stoxx.eod( duration: '10 d')){|r| r.to_human}
71
- # <Bar: 2019-04-01 wap 0.0 OHLC 3353.67 3390.98 3353.67 3385.38 trades 1750 vol 0>
72
- # <Bar: 2019-04-02 wap 0.0 OHLC 3386.18 3402.77 3382.84 3395.7 trades 1729 vol 0>
73
- # <Bar: 2019-04-03 wap 0.0 OHLC 3399.93 3435.9 3399.93 3435.56 trades 1733 vol 0>
74
- # <Bar: 2019-04-04 wap 0.0 OHLC 3434.34 3449.44 3425.19 3441.93 trades 1680 vol 0>
75
- # <Bar: 2019-04-05 wap 0.0 OHLC 3445.05 3453.01 3437.92 3447.47 trades 1677 vol 0>
76
- # <Bar: 2019-04-08 wap 0.0 OHLC 3446.15 3447.08 3433.47 3438.06 trades 1648 vol 0>
77
- # <Bar: 2019-04-09 wap 0.0 OHLC 3437.07 3450.69 3416.67 3417.22 trades 1710 vol 0>
78
- # <Bar: 2019-04-10 wap 0.0 OHLC 3418.36 3435.32 3418.36 3424.65 trades 1670 vol 0>
79
- # <Bar: 2019-04-11 wap 0.0 OHLC 3430.73 3442.25 3412.15 3435.34 trades 1773 vol 0>
80
- # <Bar: 2019-04-12 wap 0.0 OHLC 3432.16 3454.77 3425.84 3447.83 trades 1715 vol 0>
81
- #
82
- # «to_human« is not needed here because ist aliased with `to_s`
83
- #
84
- # puts Symbols::Stocks.wfc.eod( start: Date.new(2019,10,9), duration: 3 )
85
- # <Bar: 2020-10-23 wap 23.3675 OHLC 23.55 23.55 23.12 23.28 trades 5778 vol 50096>
86
- # <Bar: 2020-10-26 wap 22.7445 OHLC 22.98 22.99 22.6 22.7 trades 6873 vol 79560>
87
- # <Bar: 2020-10-27 wap 22.086 OHLC 22.55 22.58 21.82 21.82 trades 7503 vol 97691>
88
-
89
- # puts Symbols::Stocks.wfc.eod( to: Date.new(2019,10,9), duration: 3 )
90
- # <Bar: 2019-10-04 wap 48.964 OHLC 48.61 49.25 48.54 49.21 trades 9899 vol 50561>
91
- # <Bar: 2019-10-07 wap 48.9445 OHLC 48.91 49.29 48.75 48.81 trades 10317 vol 50189>
92
- # <Bar: 2019-10-08 wap 47.9165 OHLC 48.25 48.34 47.55 47.82 trades 12607 vol 53577>
93
- #
94
- def eod start:nil, to: Date.today, duration: nil , what: :trades
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
- tws = IB::Connection.current
97
- recieved = Queue.new
98
- r = nil
99
- # the hole response is transmitted at once!
100
- a = tws.subscribe(IB::Messages::Incoming::HistoricalData) do |msg|
101
- if msg.request_id == con_id
102
- # msg.results.each { |entry| puts " #{entry}" }
103
- self.bars = msg.results
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
- block_given? ? bars.map{|y| yield y} : bars # return bars or result of block
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
- end # def
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.2"
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
- logger.error {msg.message}
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
- raises an IB::TransmissionError if the account-data are not transmitted in time (1 sec)
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
- raises an IB::Error if less then 100 items are recieved-
31
+ Raises an IB::Error if less then 100 items are received.
30
32
  =end
31
- def get_account_data *accounts, watchlists: []
33
+ def get_account_data *accounts, **compatibily_argument
32
34
 
33
35
 
34
- @account_data_subscription ||= subscribe_account_updates
36
+ subscription = subscribe_account_updates( continuously: false )
37
+ download_end = nil # declare variable
35
38
 
36
- accounts = clients if accounts.empty?
37
- logger.warn{ "No active account present. AccountData are NOT requested" } if accounts.empty?
38
- # Account-infos have to be requested sequencially.
39
- # subsequent (parallel) calls kill the former once on the tws-server-side
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
- logger.debug{ "#{account.account} :: Requesting AccountData " }
48
- account.update_attribute :connected, false # indicates: AccountUpdate in Progress
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
- Timeout::timeout(3, IB::TransmissionError, "RequestAccountData failed (#{account.account})") do
54
- # initialize requests sequencially
55
- loop{ sleep 0.1; break if account.connected }
56
- end
57
- if watchlists.present?
58
- watchlists.each{|w| error "Watchlists must be IB::Symbols--Classes :.#{w.inspect}" unless w.is_a? IB::Symbols }
59
- account.organize_portfolio_positions watchlists
60
- end
61
- send_message :RequestAccountData, subscribe: false ## do this only once
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
- logger.info{ "#{account.account} :: Using stored AccountData " }
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 its session.
79
- #
80
-
81
- def subscribe_account_updates continously: true
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
- logger.debug { "#{account.account} :: #{msg.account_value.to_human }"}
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 subscripton if continously is specified
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
- send_message :RequestAccountData, subscribe: false, account_code: account.account unless continously
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
- logger.info { "#{account.account} => Count of AccountValues: #{account.account_values.size}" }
96
- else # unreasonable account_data recieved - request is still active
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
- account.contracts.update_or_create msg.contract
101
- account.portfolio_values << msg.portfolio_value
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
- # link contract -> portfolio value
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
- logger.debug { "#{ account.account } :: #{ msg.contract.to_human }" }
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