ib-extensions 1.2 → 1.3.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.
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