lib/ib/eod.rb in ib-extensions-1.2 vs lib/ib/eod.rb in ib-extensions-1.3

- old
+ new

@@ -1,10 +1,12 @@ module IB require 'active_support/core_ext/date/calculations' require 'csv' + + module Eod module BuisinesDays - # https://stackoverflow.com/questions/4027768/calculate-number-of-business-days-between-two-days + # https://stackoverflow.com/questions/4027768/calculate-number-of-business-days-between-two-days # Calculates the number of business days in range (start_date, end_date] # # @param start_date [Date] # @param end_date [Date] @@ -12,142 +14,202 @@ # @return [Fixnum] def self.business_days_between(start_date, end_date) days_between = (end_date - start_date).to_i return 0 unless days_between > 0 - # Assuming we need to calculate days from 9th to 25th, 10-23 are covered - # by whole weeks, and 24-25 are extra days. - # - # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa - # 1 2 3 4 5 # 1 2 3 4 5 - # 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww - # 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww - # 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26 - # 27 28 29 30 31 # 27 28 29 30 31 - whole_weeks, extra_days = days_between.divmod(7) + # Assuming we need to calculate days from 9th to 25th, 10-23 are covered + # by whole weeks, and 24-25 are extra days. + # + # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 # 1 2 3 4 5 + # 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww + # 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww + # 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26 + # 27 28 29 30 31 # 27 28 29 30 31 + whole_weeks, extra_days = days_between.divmod(7) - unless extra_days.zero? - # Extra days start from the week day next to start_day, - # and end on end_date's week date. The position of the - # start date in a week can be either before (the left calendar) - # or after (the right one) the end date. - # - # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa - # 1 2 3 4 5 # 1 2 3 4 5 - # 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12 - # ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ## - # 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26 - # 27 28 29 30 31 # 27 28 29 30 31 - # - # If some of the extra_days fall on a weekend, they need to be subtracted. - # In the first case only corner days can be days off, - # and in the second case there are indeed two such days. - extra_days -= if start_date.tomorrow.wday <= end_date.wday - [start_date.tomorrow.sunday?, end_date.saturday?].count(true) - else - 2 - end - end + unless extra_days.zero? + # Extra days start from the week day next to start_day, + # and end on end_date's week date. The position of the + # start date in a week can be either before (the left calendar) + # or after (the right one) the end date. + # + # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 # 1 2 3 4 5 + # 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12 + # ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ## + # 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26 + # 27 28 29 30 31 # 27 28 29 30 31 + # + # If some of the extra_days fall on a weekend, they need to be subtracted. + # In the first case only corner days can be days off, + # and in the second case there are indeed two such days. + extra_days -= if start_date.tomorrow.wday <= end_date.wday + [start_date.tomorrow.sunday?, end_date.saturday?].count(true) + else + 2 + end + end - (whole_weeks * 5) + extra_days + (whole_weeks * 5) + extra_days end end - class Contract - # Receive EOD-Data - # - # The Enddate has to be specified (as Date Object), t - # - # The Duration can either be specified as Sting " yx D" or as Integer. - # Altenative a start date can be specified with the :start parameter. - # - # The parameter :what specified the kind of received data: - # Valid values: - # :trades, :midpoint, :bid, :ask, :bid_ask, - # :historical_volatility, :option_implied_volatility, - # :option_volume, :option_open_interest - # - # The results can be preprocessed through a block, thus - # - # puts IB::Symbols::Index::stoxx.eod( duration: '10 d')){|r| r.to_human} - # <Bar: 2019-04-01 wap 0.0 OHLC 3353.67 3390.98 3353.67 3385.38 trades 1750 vol 0> - # <Bar: 2019-04-02 wap 0.0 OHLC 3386.18 3402.77 3382.84 3395.7 trades 1729 vol 0> - # <Bar: 2019-04-03 wap 0.0 OHLC 3399.93 3435.9 3399.93 3435.56 trades 1733 vol 0> - # <Bar: 2019-04-04 wap 0.0 OHLC 3434.34 3449.44 3425.19 3441.93 trades 1680 vol 0> - # <Bar: 2019-04-05 wap 0.0 OHLC 3445.05 3453.01 3437.92 3447.47 trades 1677 vol 0> - # <Bar: 2019-04-08 wap 0.0 OHLC 3446.15 3447.08 3433.47 3438.06 trades 1648 vol 0> - # <Bar: 2019-04-09 wap 0.0 OHLC 3437.07 3450.69 3416.67 3417.22 trades 1710 vol 0> - # <Bar: 2019-04-10 wap 0.0 OHLC 3418.36 3435.32 3418.36 3424.65 trades 1670 vol 0> - # <Bar: 2019-04-11 wap 0.0 OHLC 3430.73 3442.25 3412.15 3435.34 trades 1773 vol 0> - # <Bar: 2019-04-12 wap 0.0 OHLC 3432.16 3454.77 3425.84 3447.83 trades 1715 vol 0> - # - # «to_human« is not needed here because ist aliased with `to_s` - # - # puts Symbols::Stocks.wfc.eod( start: Date.new(2019,10,9), duration: 3 ) - # <Bar: 2020-10-23 wap 23.3675 OHLC 23.55 23.55 23.12 23.28 trades 5778 vol 50096> - # <Bar: 2020-10-26 wap 22.7445 OHLC 22.98 22.99 22.6 22.7 trades 6873 vol 79560> - # <Bar: 2020-10-27 wap 22.086 OHLC 22.55 22.58 21.82 21.82 trades 7503 vol 97691> + # Receive EOD-Data and store the data in the `:bars`-property of IB::Contract + # + # contract.eod duration: {String or Integer}, start: {Date}, to: {Date} what: {see below} + # + # + # + # The Enddate has to be specified (as Date Object), `:to`, default: Date.today + # + # The Duration can either be a String "yx D", "yd W", "yx M" or an Integer ( implies "D"). + # *notice* "W" fetchtes weekly and "M" monthly bars + # + # A start date can be given with the `:start` parameter. + # + # The parameter `:what` specifies the kind of received data. + # + # Valid values: + # :trades, :midpoint, :bid, :ask, :bid_ask, + # :historical_volatility, :option_implied_volatility, + # :option_volume, :option_open_interest + # + # Polars DataFrames + # ----------------- + # The response is stored as PolarsDataframe + # for further processing: https://github.com/ankane/polars-ruby + # https://pola-rs.github.io/polars/py-polars/html/index.html + # + # Error-handling + # -------------- + # * Basically all Errors simply lead to log-entries: + # * the contract is not valid, + # * no market data subscriptions + # * other servers-side errors + # + # If the duration is longer then the maximum range, the response is + # cut to the maximum allowed range + # + # Customize the result + # -------------------- + # The results are stored in the `:bars` property of the contract + # + # + # Limitations + # ----------- + # To identify a request, the con_id of the asset is used + # Thus, parallel requests of a single asset with different time-frames will fail + # + # Examples + # -------- + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2019,10,9), duration: 3) + # shape: (3, 8) + # ┌────────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪════════╪════════╪════════╪════════╪════════╪═════════╪════════╡ + # │ 2019-10-08 ┆ 148.62 ┆ 149.37 ┆ 146.11 ┆ 146.45 ┆ 156625 ┆ 146.831 ┆ 88252 │ + # │ 2019-10-09 ┆ 147.18 ┆ 148.0 ┆ 145.38 ┆ 145.85 ┆ 94337 ┆ 147.201 ┆ 51294 │ + # │ 2019-10-10 ┆ 146.9 ┆ 148.74 ┆ 146.87 ┆ 148.24 ┆ 134549 ┆ 147.792 ┆ 71084 │ + # └────────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2021,10,9), duration: '3W') + # shape: (3, 8) + # ┌────────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪════════╪════════╪════════╪════════╪═════════╪═════════╪════════╡ + # │ 2021-10-01 ┆ 223.99 ┆ 227.68 ┆ 216.12 ┆ 222.8 ┆ 1295495 ┆ 222.226 ┆ 792711 │ + # │ 2021-10-08 ┆ 221.4 ┆ 224.95 ┆ 216.76 ┆ 221.65 ┆ 1044233 ┆ 220.855 ┆ 621984 │ + # │ 2021-10-15 ┆ 220.69 ┆ 228.41 ┆ 218.94 ┆ 225.05 ┆ 768065 ┆ 223.626 ┆ 437817 │ + # └────────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴────────┘ + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2022,10,1), duration: '3M') + # shape: (3, 8) + # ┌────────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬─────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪════════╪════════╪════════╪════════╪═════════╪═════════╪═════════╡ + # │ 2022-09-30 ┆ 181.17 ┆ 191.37 ┆ 162.77 ┆ 165.16 ┆ 4298969 ┆ 175.37 ┆ 2202407 │ + # │ 2022-10-31 ┆ 165.5 ┆ 184.24 ┆ 162.5 ┆ 183.5 ┆ 4740014 ┆ 173.369 ┆ 2474286 │ + # │ 2022-11-30 ┆ 184.51 ┆ 189.56 ┆ 174.11 ┆ 188.19 ┆ 3793861 ┆ 182.594 ┆ 1945674 │ + # └────────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴─────────┘ + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2020,1,1), duration: '3M', what: :option_implied_vol + # atility ) + # shape: (3, 8) + # ┌────────────┬──────────┬──────────┬──────────┬──────────┬────────┬──────────┬────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪══════════╪══════════╪══════════╪══════════╪════════╪══════════╪════════╡ + # │ 2019-12-31 ┆ 0.134933 ┆ 0.177794 ┆ 0.115884 ┆ 0.138108 ┆ 0 ┆ 0.178318 ┆ 0 │ + # │ 2020-01-31 ┆ 0.139696 ┆ 0.190494 ┆ 0.120646 ┆ 0.185732 ┆ 0 ┆ 0.19097 ┆ 0 │ + # │ 2020-02-28 ┆ 0.185732 ┆ 0.436549 ┆ 0.134933 ┆ 0.39845 ┆ 0 ┆ 0.435866 ┆ 0 │ + # └────────────┴──────────┴──────────┴──────────┴──────────┴────────┴──────────┴────────┘ + # + def eod start: nil, to: nil, duration: nil , what: :trades - # puts Symbols::Stocks.wfc.eod( to: Date.new(2019,10,9), duration: 3 ) - # <Bar: 2019-10-04 wap 48.964 OHLC 48.61 49.25 48.54 49.21 trades 9899 vol 50561> - # <Bar: 2019-10-07 wap 48.9445 OHLC 48.91 49.29 48.75 48.81 trades 10317 vol 50189> - # <Bar: 2019-10-08 wap 47.9165 OHLC 48.25 48.34 47.55 47.82 trades 12607 vol 53577> - # - def eod start:nil, to: Date.today, duration: nil , what: :trades + # error "EOD:: Start-Date (parameter: to) must be a Date-Object" unless to.is_a? Date + normalize_duration = ->(d) do + if d.is_a?(Integer) || !["D","M","W","Y"].include?( d[-1].upcase ) + d.to_i.to_s + "D" + else + d.gsub(" ","") + end.insert(-2, " ") + end - tws = IB::Connection.current - recieved = Queue.new - r = nil - # the hole response is transmitted at once! - a = tws.subscribe(IB::Messages::Incoming::HistoricalData) do |msg| - if msg.request_id == con_id - # msg.results.each { |entry| puts " #{entry}" } - self.bars = msg.results - end - recieved.push Time.now - end - b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg| - if [321,162,200].include? msg.code - tws.logger.info msg.message - # TWS Error 200: No security definition has been found for the request - # TWS Error 354: Requested market data is not subscribed. - # TWS Error 162 # Historical Market Data Service error - recieved.close - end - end + get_end_date = -> do + d = normalize_duration.call(duration) + case d[-1] + when "D" + start + d.to_i - 1 + when 'W' + Date.commercial( start.year, start.cweek + d.to_i - 1, 1) + when 'M' + Date.new( start.year, start.month + d.to_i - 1 , start.day ) + end + end - duration = if duration.present? - duration.is_a?(String) ? duration : duration.to_s + " D" - elsif start.present? - BuisinesDays.business_days_between(start, to).to_s + " D" - else - "1 D" - end + if to.nil? + # case eod start= Date.new ... + to = if start.present? && duration.nil? + # case eod start= Date.new + duration = BuisinesDays.business_days_between(start, to).to_s + "D" + Date.today # assign to var: to + elsif start.present? && duration.present? + # case eod start= Date.new , duration: 'nN' + get_end_date.call # assign to var: to + elsif duration.present? + # case start is not present, we are collecting until the present day + Date.today # assign to var: to + else + duration = "1D" + Date.today + end + end - tws.send_message IB::Messages::Outgoing::RequestHistoricalData.new( - :request_id => con_id, - :contract => self, - :end_date_time => to.to_time.to_ib, # Time.now.to_ib, - :duration => duration, # ? - :bar_size => :day1, # IB::BAR_SIZES.key(:hour)? - :what_to_show => what, - :use_rth => 0, - :format_date => 2, - :keep_up_todate => 0) + barsize = case normalize_duration.call(duration)[-1].upcase + when "W" + :week1 + when "M" + :month1 + else + :day1 + end - recieved.pop # blocks until a message is ready on the queue or the queue is closed - tws.unsubscribe a - tws.unsubscribe b + get_bars(to.to_time.to_ib , normalize_duration[duration], barsize, what) - block_given? ? bars.map{|y| yield y} : bars # return bars or result of block + end # def - end # def - # creates (or overwrites) the specified file (or symbol.csv) and saves bar-data - def to_csv file:nil - file ||= "#{symbol}.csv" - + def to_csv file: "#{symbol}.csv" if bars.present? headers = bars.first.invariant_attributes.keys CSV.open( file, 'w' ) {|f| f << headers ; bars.each {|y| f << y.invariant_attributes.values } } end end @@ -158,8 +220,58 @@ self.bars = [] CSV.foreach( file, headers: true, header_converters: :symbol) do |row| self.bars << IB::Bar.new( **row.to_h ) end end + + def get_bars(end_date_time, duration, bar_size, what_to_show) + + tws = IB::Connection.current + received = Queue.new + r = nil + # the hole response is transmitted at once! + a = tws.subscribe(IB::Messages::Incoming::HistoricalData) do |msg| + if msg.request_id == con_id + # msg.results.each { |entry| puts " #{entry}" } + self.bars = Polars::DataFrame.new msg.results.map( &:invariant_attributes ) + end + received.push Time.now + end + b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg| + if [321,162,200].include? msg.code + tws.logger.info msg.message + # TWS Error 200: No security definition has been found for the request + # TWS Error 354: Requested market data is not subscribed. + # TWS Error 162 # Historical Market Data Service error + received.close + elsif msg.code.to_i == 2174 + tws.logger.info "Please switch to the \"10-19\"-Branch of the git-repository" + end + end + + + tws.send_message IB::Messages::Outgoing::RequestHistoricalData.new( + :request_id => con_id, + :contract => self, + :end_date_time => end_date_time, + :duration => duration, # see ib/messages/outgoing/bar_request.rb => max duration for 5sec bar lookback is 10 000 - i.e. will yield 2000 bars + :bar_size => bar_size, # IB::BAR_SIZES.key(:hour) + :what_to_show => what_to_show, + :use_rth => 0, + :format_date => 2, + :keep_up_todate => 0) + + received.pop # blocks until a message is ready on the queue or the queue is closed + + tws.unsubscribe a + tws.unsubscribe b + + block_given? ? bars.map{|y| yield y} : bars # return bars or result of block + + end # def + end # module eod + + class Contract + include Eod end # class -end # module +end # module IB