ib-extensions 1.1 → 1.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ib/eod.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  module IB
2
2
  require 'active_support/core_ext/date/calculations'
3
+ require 'csv'
4
+
5
+ module Eod
3
6
  module BuisinesDays
4
- # 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
5
8
 
6
9
  # Calculates the number of business days in range (start_date, end_date]
7
10
  #
@@ -13,136 +16,262 @@ require 'active_support/core_ext/date/calculations'
13
16
  days_between = (end_date - start_date).to_i
14
17
  return 0 unless days_between > 0
15
18
 
16
- # Assuming we need to calculate days from 9th to 25th, 10-23 are covered
17
- # by whole weeks, and 24-25 are extra days.
18
- #
19
- # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
20
- # 1 2 3 4 5 # 1 2 3 4 5
21
- # 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww
22
- # 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww
23
- # 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26
24
- # 27 28 29 30 31 # 27 28 29 30 31
25
- whole_weeks, extra_days = days_between.divmod(7)
26
-
27
- unless extra_days.zero?
28
- # Extra days start from the week day next to start_day,
29
- # and end on end_date's week date. The position of the
30
- # start date in a week can be either before (the left calendar)
31
- # or after (the right one) the end date.
32
- #
33
- # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa
34
- # 1 2 3 4 5 # 1 2 3 4 5
35
- # 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12
36
- # ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ##
37
- # 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26
38
- # 27 28 29 30 31 # 27 28 29 30 31
39
- #
40
- # If some of the extra_days fall on a weekend, they need to be subtracted.
41
- # In the first case only corner days can be days off,
42
- # and in the second case there are indeed two such days.
43
- extra_days -= if start_date.tomorrow.wday <= end_date.wday
44
- [start_date.tomorrow.sunday?, end_date.saturday?].count(true)
45
- else
46
- 2
47
- end
48
- end
49
-
50
- (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
51
54
  end
52
55
  end
53
- class Contract
54
- # Receive EOD-Data
55
- #
56
- # The Enddate has to be specified (as Date Object), t
57
- #
58
- # The Duration can either be specified as Sting " yx D" or as Integer.
59
- # Altenative a start date can be specified with the :start parameter.
60
- #
61
- # The parameter :what specified the kind of received data:
62
- # Valid values:
63
- # :trades, :midpoint, :bid, :ask, :bid_ask,
64
- # :historical_volatility, :option_implied_volatility,
65
- # :option_volume, :option_open_interest
66
- #
67
- # The results can be preprocessed through a block, thus
68
- #
69
- # puts IB::Symbols::Index::stoxx.eod( duration: '10 d')){|r| r.to_human}
70
- # <Bar: 2019-04-01 wap 0.0 OHLC 3353.67 3390.98 3353.67 3385.38 trades 1750 vol 0>
71
- # <Bar: 2019-04-02 wap 0.0 OHLC 3386.18 3402.77 3382.84 3395.7 trades 1729 vol 0>
72
- # <Bar: 2019-04-03 wap 0.0 OHLC 3399.93 3435.9 3399.93 3435.56 trades 1733 vol 0>
73
- # <Bar: 2019-04-04 wap 0.0 OHLC 3434.34 3449.44 3425.19 3441.93 trades 1680 vol 0>
74
- # <Bar: 2019-04-05 wap 0.0 OHLC 3445.05 3453.01 3437.92 3447.47 trades 1677 vol 0>
75
- # <Bar: 2019-04-08 wap 0.0 OHLC 3446.15 3447.08 3433.47 3438.06 trades 1648 vol 0>
76
- # <Bar: 2019-04-09 wap 0.0 OHLC 3437.07 3450.69 3416.67 3417.22 trades 1710 vol 0>
77
- # <Bar: 2019-04-10 wap 0.0 OHLC 3418.36 3435.32 3418.36 3424.65 trades 1670 vol 0>
78
- # <Bar: 2019-04-11 wap 0.0 OHLC 3430.73 3442.25 3412.15 3435.34 trades 1773 vol 0>
79
- # <Bar: 2019-04-12 wap 0.0 OHLC 3432.16 3454.77 3425.84 3447.83 trades 1715 vol 0>
80
- #
81
- # «to_human« is not needed here because ist aliased with `to_s`
82
- #
83
- # puts Symbols::Stocks.wfc.eod( start: Date.new(2019,10,9), duration: 3 )
84
- # <Bar: 2020-10-23 wap 23.3675 OHLC 23.55 23.55 23.12 23.28 trades 5778 vol 50096>
85
- # <Bar: 2020-10-26 wap 22.7445 OHLC 22.98 22.99 22.6 22.7 trades 6873 vol 79560>
86
- # <Bar: 2020-10-27 wap 22.086 OHLC 22.55 22.58 21.82 21.82 trades 7503 vol 97691>
87
-
88
- # puts Symbols::Stocks.wfc.eod( to: Date.new(2019,10,9), duration: 3 )
89
- # <Bar: 2019-10-04 wap 48.964 OHLC 48.61 49.25 48.54 49.21 trades 9899 vol 50561>
90
- # <Bar: 2019-10-07 wap 48.9445 OHLC 48.91 49.29 48.75 48.81 trades 10317 vol 50189>
91
- # <Bar: 2019-10-08 wap 47.9165 OHLC 48.25 48.34 47.55 47.82 trades 12607 vol 53577>
92
- #
93
- 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}
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
+ # 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)
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')
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')
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
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
155
+
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
164
+
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
176
+
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
94
203
 
95
- tws = IB::Connection.current
96
- recieved = Queue.new
97
- r = nil
204
+
205
+ get_bars(to.to_time.to_ib , normalize_duration[duration], barsize, what)
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"
211
+ if bars.present?
212
+ headers = bars.first.invariant_attributes.keys
213
+ CSV.open( file, 'w' ) {|f| f << headers ; bars.each {|y| f << y.invariant_attributes.values } }
214
+ end
215
+ end
216
+
217
+ # read csv-data into bars
218
+ def from_csv file: nil
219
+ file ||= "#{symbol}.csv"
220
+ self.bars = []
221
+ CSV.foreach( file, headers: true, header_converters: :symbol) do |row|
222
+ self.bars << IB::Bar.new( **row.to_h )
223
+ end
224
+ end
225
+
226
+ def get_bars(end_date_time, duration, bar_size, what_to_show)
227
+
228
+ tws = IB::Connection.current
229
+ received = Queue.new
230
+ r = nil
98
231
  # the hole response is transmitted at once!
99
- a = tws.subscribe(IB::Messages::Incoming::HistoricalData) do |msg|
100
- if msg.request_id == con_id
101
- # msg.results.each { |entry| puts " #{entry}" }
102
- r = block_given? ? msg.results.map{|y| yield y} : msg.results
103
- end
104
- recieved.push Time.now
105
- end
106
- b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg|
107
- if [321,162,200].include? msg.code
108
- tws.logger.info msg.message
109
- # TWS Error 200: No security definition has been found for the request
110
- # TWS Error 354: Requested market data is not subscribed.
111
- # TWS Error 162 # Historical Market Data Service error
112
- recieved.close
113
- end
114
- end
115
-
116
- duration = if duration.present?
117
- duration.is_a?(String) ? duration : duration.to_s + " D"
118
- elsif start.present?
119
- BuisinesDays.business_days_between(start, to).to_s + " D"
120
- else
121
- "1 D"
122
- end
123
-
124
- tws.send_message IB::Messages::Outgoing::RequestHistoricalData.new(
125
- :request_id => con_id,
126
- :contract => self,
127
- :end_date_time => to.to_time.to_ib, # Time.now.to_ib,
128
- :duration => duration, # ?
129
- :bar_size => :day1, # IB::BAR_SIZES.key(:hour)?
130
- :what_to_show => what,
131
- :use_rth => 0,
132
- :format_date => 2,
133
- :keep_up_todate => 0)
134
-
135
- Timeout::timeout(5) do # max 5 sec.
136
- sleep 0.1
137
- recieved.pop # blocks until a message is ready on the queue
138
- break if recieved.closed? || recieved.empty? # finish if data received
232
+ a = tws.subscribe(IB::Messages::Incoming::HistoricalData) do |msg|
233
+ if msg.request_id == con_id
234
+ # msg.results.each { |entry| puts " #{entry}" }
235
+ self.bars = Polars::DataFrame.new msg.results.map( &:invariant_attributes )
236
+ end
237
+ received.push Time.now
238
+ end
239
+ b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg|
240
+ if [321,162,200].include? msg.code
241
+ tws.logger.info msg.message
242
+ # TWS Error 200: No security definition has been found for the request
243
+ # TWS Error 354: Requested market data is not subscribed.
244
+ # TWS Error 162 # Historical Market Data Service error
245
+ received.close
246
+ elsif msg.code.to_i == 2174
247
+ tws.logger.info "Please switch to the \"10-19\"-Branch of the git-repository"
248
+ end
139
249
  end
140
- tws.unsubscribe a
141
- tws.unsubscribe b
142
250
 
143
- r # the collected result
144
251
 
145
- end # def
252
+ tws.send_message IB::Messages::Outgoing::RequestHistoricalData.new(
253
+ :request_id => con_id,
254
+ :contract => self,
255
+ :end_date_time => end_date_time,
256
+ :duration => duration, # see ib/messages/outgoing/bar_request.rb => max duration for 5sec bar lookback is 10 000 - i.e. will yield 2000 bars
257
+ :bar_size => bar_size, # IB::BAR_SIZES.key(:hour)
258
+ :what_to_show => what_to_show,
259
+ :use_rth => 0,
260
+ :format_date => 2,
261
+ :keep_up_todate => 0)
262
+
263
+ received.pop # blocks until a message is ready on the queue or the queue is closed
264
+
265
+ tws.unsubscribe a
266
+ tws.unsubscribe b
267
+
268
+ block_given? ? bars.map{|y| yield y} : bars # return bars or result of block
269
+
270
+ end # def
271
+ end # module eod
272
+
273
+ class Contract
274
+ include Eod
146
275
  end # class
147
- end # module
276
+ end # module IB
148
277
 
@@ -1,34 +1,6 @@
1
+ module IB
1
2
 
2
- def associate_ticdata
3
-
4
- tws= IB::Gateway.tws # get the initialized ib-ruby instance
5
- the_id = nil
6
- finalize= false
7
- # switch to delayed data
8
- tws.send_message :RequestMarketDataType, :market_data_type => :delayed
9
-
10
- s_id = tws.subscribe(:TickSnapshotEnd) { |msg| finalize = true if msg.ticker_id == the_id }
11
-
12
- sub_id = tws.subscribe(:TickPrice, :TickSize, :TickGeneric, :TickOption) do |msg|
13
- self.bars << msg.the_data if msg.ticker_id == the_id
14
- end
15
-
16
- # initialize »the_id« that is used to identify the received tick messages
17
- # by firing the market data request
18
- the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
19
-
20
- #keep the method-call running until the request finished
21
- #and cancel subscriptions to the message handler.
22
- Thread.new do
23
- i=0; loop{ i+=1; sleep 0.1; break if finalize || i > 1000 }
24
- tws.unsubscribe sub_id
25
- tws.unsubscribe s_id
26
- puts "#{symbol} data gathered"
27
- end # method returns the (running) thread
28
-
29
- end # def
30
- ###################### private methods
31
-
3
+ class Contract
32
4
  end # class
33
5
 
34
6
 
@@ -1,5 +1,5 @@
1
1
  module IB
2
2
  module Extensions
3
- VERSION = "1.1"
3
+ VERSION = "1.3"
4
4
  end
5
5
  end
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