Module: SchwabRb::PriceHistory::Downloader
- Defined in:
- lib/schwab_rb/price_history/downloader.rb
Constant Summary collapse
- INDEX_API_SYMBOLS =
%w[ COMPX DJX MID NDX OEX RUT SPX VIX VIX9D VIX1D XSP ].freeze
- SUPPORTED_FORMATS =
%w[csv json].freeze
- FREQUENCY_ALIASES =
{ "1min" => { label: "1min", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::MINUTE, frequency: SchwabRb::PriceHistory::Frequencies::EVERY_MINUTE, period_type: SchwabRb::PriceHistory::PeriodTypes::DAY, period: SchwabRb::PriceHistory::Periods::ONE_DAY }, "5min" => { label: "5min", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::MINUTE, frequency: SchwabRb::PriceHistory::Frequencies::EVERY_FIVE_MINUTES, period_type: SchwabRb::PriceHistory::PeriodTypes::DAY, period: SchwabRb::PriceHistory::Periods::ONE_DAY }, "10min" => { label: "10min", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::MINUTE, frequency: SchwabRb::PriceHistory::Frequencies::EVERY_TEN_MINUTES, period_type: SchwabRb::PriceHistory::PeriodTypes::DAY, period: SchwabRb::PriceHistory::Periods::ONE_DAY }, "15min" => { label: "15min", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::MINUTE, frequency: SchwabRb::PriceHistory::Frequencies::EVERY_FIFTEEN_MINUTES, period_type: SchwabRb::PriceHistory::PeriodTypes::DAY, period: SchwabRb::PriceHistory::Periods::ONE_DAY }, "30min" => { label: "30min", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::MINUTE, frequency: SchwabRb::PriceHistory::Frequencies::EVERY_THIRTY_MINUTES, period_type: SchwabRb::PriceHistory::PeriodTypes::DAY, period: SchwabRb::PriceHistory::Periods::ONE_DAY }, "day" => { label: "day", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::DAILY, frequency: SchwabRb::PriceHistory::Frequencies::DAILY, period_type: SchwabRb::PriceHistory::PeriodTypes::YEAR, period: SchwabRb::PriceHistory::Periods::TWENTY_YEARS }, "week" => { label: "week", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::WEEKLY, frequency: SchwabRb::PriceHistory::Frequencies::WEEKLY, period_type: SchwabRb::PriceHistory::PeriodTypes::YEAR, period: SchwabRb::PriceHistory::Periods::TWENTY_YEARS }, "month" => { label: "month", frequency_type: SchwabRb::PriceHistory::FrequencyTypes::MONTHLY, frequency: SchwabRb::PriceHistory::Frequencies::MONTHLY, period_type: SchwabRb::PriceHistory::PeriodTypes::YEAR, period: SchwabRb::PriceHistory::Periods::TWENTY_YEARS } }.freeze
Class Method Summary collapse
- .api_symbol(symbol) ⇒ Object
- .business_dates_in_range(start_date, end_date) ⇒ Object
- .cached_range_covers?(response, start_date, end_date, frequency) ⇒ Boolean
- .candle_dates(response) ⇒ Object
- .canonical_output_path(directory:, symbol:, frequency:, format:) ⇒ Object
- .daily_range_covered?(cached_dates, start_date, end_date) ⇒ Boolean
- .load_cached_price_history(path, format) ⇒ Object
- .merge_price_history_responses(left, right, fallback_symbol) ⇒ Object
- .normalize_price_history_response(response, fallback_symbol) ⇒ Object
- .parse_price_history_csv(path) ⇒ Object
- .parse_price_history_file(path, format) ⇒ Object
- .requested_candle_dates(response, start_date, end_date) ⇒ Object
- .resolve(client:, symbol:, start_date:, end_date:, directory:, frequency:, format:, need_extended_hours_data:, need_previous_close:, period_type: nil, period: nil) ⇒ Object
- .sanitize_symbol(symbol) ⇒ Object
- .serialized_payload(response, format) ⇒ Object
- .symbolize_price_history_payload(payload) ⇒ Object
Class Method Details
.api_symbol(symbol) ⇒ Object
112 113 114 115 116 117 118 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 112 def api_symbol(symbol) raw_symbol = symbol.to_s.strip return raw_symbol if raw_symbol.start_with?("$", "/") return "$#{raw_symbol}" if INDEX_API_SYMBOLS.include?(raw_symbol.upcase) raw_symbol end |
.business_dates_in_range(start_date, end_date) ⇒ Object
215 216 217 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 215 def business_dates_in_range(start_date, end_date) (start_date..end_date).select { |date| (1..5).cover?(date.wday) } end |
.cached_range_covers?(response, start_date, end_date, frequency) ⇒ Boolean
191 192 193 194 195 196 197 198 199 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 191 def cached_range_covers?(response, start_date, end_date, frequency) return false unless response dates = requested_candle_dates(response, start_date, end_date) return false if dates.empty? return daily_range_covered?(dates, start_date, end_date) if frequency == "day" start_date >= dates.first && end_date <= dates.last end |
.candle_dates(response) ⇒ Object
201 202 203 204 205 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 201 def candle_dates(response) Array(response[:candles]).map do |candle| Time.at(candle.fetch(:datetime) / 1000.0).utc.to_date end.sort end |
.canonical_output_path(directory:, symbol:, frequency:, format:) ⇒ Object
103 104 105 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 103 def canonical_output_path(directory:, symbol:, frequency:, format:) File.join(directory, "#{sanitize_symbol(symbol)}_#{frequency}.#{format}") end |
.daily_range_covered?(cached_dates, start_date, end_date) ⇒ Boolean
211 212 213 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 211 def daily_range_covered?(cached_dates, start_date, end_date) business_dates_in_range(start_date, end_date).all? { |date| cached_dates.include?(date) } end |
.load_cached_price_history(path, format) ⇒ Object
143 144 145 146 147 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 143 def load_cached_price_history(path, format) return unless File.exist?(path) parse_price_history_file(path, format) end |
.merge_price_history_responses(left, right, fallback_symbol) ⇒ Object
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 219 def merge_price_history_responses(left, right, fallback_symbol) return normalize_price_history_response(right, fallback_symbol) unless left return normalize_price_history_response(left, fallback_symbol) unless right merged_candles = Array(left[:candles]) + Array(right[:candles]) deduped_candles = merged_candles.each_with_object({}) do |candle, by_datetime| by_datetime[candle.fetch(:datetime)] = candle.transform_keys(&:to_sym) end { symbol: left[:symbol] || right[:symbol] || fallback_symbol, empty: deduped_candles.empty?, candles: deduped_candles.values.sort_by { |candle| candle.fetch(:datetime) } } end |
.normalize_price_history_response(response, fallback_symbol) ⇒ Object
235 236 237 238 239 240 241 242 243 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 235 def normalize_price_history_response(response, fallback_symbol) { symbol: response[:symbol] || fallback_symbol, empty: Array(response[:candles]).empty?, candles: Array(response[:candles]).map { |candle| candle.transform_keys(&:to_sym) }.sort_by do |candle| candle.fetch(:datetime) end } end |
.parse_price_history_csv(path) ⇒ Object
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 162 def parse_price_history_csv(path) candles = CSV.read(path, headers: true).map do |row| { datetime: Time.iso8601(row.fetch("datetime")).to_i * 1000, open: row.fetch("open").to_f, high: row.fetch("high").to_f, low: row.fetch("low").to_f, close: row.fetch("close").to_f, volume: row.fetch("volume").to_i } end { symbol: File.basename(path).split("_").first, empty: candles.empty?, candles: candles } end |
.parse_price_history_file(path, format) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 149 def parse_price_history_file(path, format) case format when "json" symbolize_price_history_payload(JSON.parse(File.read(path))) when "csv" parse_price_history_csv(path) else raise ArgumentError, "Unsupported format `#{format}`." end rescue JSON::ParserError => e raise ArgumentError, "Unable to parse cached price history at #{path}: #{e.}" end |
.requested_candle_dates(response, start_date, end_date) ⇒ Object
207 208 209 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 207 def requested_candle_dates(response, start_date, end_date) candle_dates(response).select { |date| date >= start_date && date <= end_date } end |
.resolve(client:, symbol:, start_date:, end_date:, directory:, frequency:, format:, need_extended_hours_data:, need_previous_close:, period_type: nil, period: nil) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 76 def resolve(client:, symbol:, start_date:, end_date:, directory:, frequency:, format:, need_extended_hours_data:, need_previous_close:, period_type: nil, period: nil) config = FREQUENCY_ALIASES.fetch(frequency) path = canonical_output_path(directory: directory, symbol: symbol, frequency: frequency, format: format) existing_response = load_cached_price_history(path, format) response = existing_response unless cached_range_covers?(existing_response, start_date, end_date, frequency) FileUtils.mkdir_p(directory) downloaded = client.get_price_history( api_symbol(symbol), period_type: period_type || config.fetch(:period_type), period: period || config.fetch(:period), frequency_type: config.fetch(:frequency_type), frequency: config.fetch(:frequency), start_datetime: start_date, end_datetime: end_date, need_extended_hours_data: need_extended_hours_data, need_previous_close: need_previous_close, return_data_objects: false ) response = merge_price_history_responses(response, downloaded, symbol) end File.write(path, serialized_payload(response, format)) [response, path] end |
.sanitize_symbol(symbol) ⇒ Object
107 108 109 110 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 107 def sanitize_symbol(symbol) sanitized_symbol = symbol.to_s.gsub(/[^a-zA-Z0-9]+/, "_").gsub(/\A_+|_+\z/, "") sanitized_symbol.empty? ? "symbol" : sanitized_symbol end |
.serialized_payload(response, format) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 120 def serialized_payload(response, format) case format when "json" JSON.pretty_generate(response) when "csv" CSV.generate do |csv| csv << %w[datetime open high low close volume] Array(response[:candles]).each do |candle| csv << [ Time.at(candle.fetch(:datetime) / 1000.0).utc.iso8601, candle[:open], candle[:high], candle[:low], candle[:close], candle[:volume] ] end end else raise ArgumentError, "Unsupported format `#{format}`." end end |
.symbolize_price_history_payload(payload) ⇒ Object
181 182 183 184 185 186 187 188 189 |
# File 'lib/schwab_rb/price_history/downloader.rb', line 181 def symbolize_price_history_payload(payload) { symbol: payload["symbol"] || payload[:symbol], empty: payload["empty"].nil? ? payload[:empty] : payload["empty"], candles: Array(payload["candles"] || payload[:candles]).map do |candle| candle.transform_keys(&:to_sym) end } end |