Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add historical prices api endpoint #94

Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* [#91](https://github.com/dblock/iex-ruby-client/pull/91): Added support for Balance Sheet API endpoint - [@tylerhaugen-stanley](https://github.com/tylerhaugen-stanley).
* [#92](https://github.com/dblock/iex-ruby-client/pull/92): Added support for Cash Flow Statements API endpoint - [@tylerhaugen-stanley](https://github.com/tylerhaugen-stanley).
* [#93](https://github.com/dblock/iex-ruby-client/pull/93): Added `fiscal_date` and `currency` to income statements - [@tylerhaugen-stanley](https://github.com/tylerhaugen-stanley).
* [#94](https://github.com/dblock/iex-ruby-client/pull/94): Added `historical_prices` API endpoint - [@tylerhaugen-stanley](https://github.com/tylerhaugen-stanley).

### 1.3.0 (2020/10/31)

Expand Down
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A Ruby client for the [The IEX Cloud API](https://iexcloud.io/docs/api/).
- [Get a Quote](#get-a-quote)
- [Get a OHLC (Open, High, Low, Close) price](#get-a-ohlc-open-high-low-close-price)
- [Get a Market OHLC (Open, High, Low, Close) prices](#get-a-market-ohlc-open-high-low-close-prices)
- [Get Historical Prices](#get-historical-prices)
- [Get Company Information](#get-company-information)
- [Get a Company Logo](#get-a-company-logo)
- [Get Recent News](#get-recent-news)
Expand Down Expand Up @@ -133,6 +134,57 @@ market['SPY'].high #
market['SPY'].low #
```

### Get Historical Prices

Fetches a list of historical prices.

There are currently a few limitations of this endpoint compared to the official IEX one.

Options for `range` include:
`max, ytd, 5y, 2y, 1y, 6m, 3m, 1m, 5d, date`

NOTE: If you use the `date` value for the `range` parameter:
* The options _must_ include a date entry, `{date: ...}`
* The date value _must_ be either a Date object, or a string formatted as `YYYYMMDD`. Anything else will result in an `IEX::Errors::ClientError`.
* The options _must_ include `chartByDay: 'true'` or an `ArgumentError` will be raised.
* See below for examples.

`Query params` supported include:
`chartByDay`

This is a complicated endpoint as there is a lot of granularity over the time period of data returned. See below for a variety of ways to request data, NOTE: this is _NOT_ as exhaustive list.
```ruby
historial_prices = client.historical_prices('MSFT') # One month of data
historial_prices = client.historical_prices('MSFT', {range: 'max'}) # All data up to 15 years
historial_prices = client.historical_prices('MSFT', {range: 'ytd'}) # Year to date data
historial_prices = client.historical_prices('MSFT', {range: '5y'}) # 5 years of data
historial_prices = client.historical_prices('MSFT', {range: '6m'}) # 6 months of data
historial_prices = client.historical_prices('MSFT', {range: '5d'}) # 5 days of data
historial_prices = client.historical_prices('MSFT', {range: 'date', date: '20200930', chartByDay: 'true'}) # One day of data
historial_prices = client.historical_prices('MSFT', {range: 'date', date: Date.parse('2020-09-30)', chartByDay: 'true'}) # One day of data
...
```

Once you have the data over the preferred time period, you can access the following fields
```ruby
historial_prices = client.historical_prices('MSFT') # One month of data

historial_price = historial_prices.first
historical_price.date # 2020-10-07
historical_price.open #207.06
historical_price.open_dollar # '$207.06'
historical_price.close # 209.83
historical_price.close_dollar # '$209.83'
historical_price.high # 210.11
historical_price.high_dollar # '$210.11'
historical_price.low # 206.72
historical_price.low_dollar # '$206.72'
historical_price.volume # 25681054
...
```

There are a lot of options here so I would recommend viewing the official IEX documentation [#historical-prices](https://iexcloud.io/docs/api/#historical-prices) or [historical_prices.rb](lib/iex/resources/historical_prices.rb) for returned fields.

### Get Company Information

Fetches company information for a symbol.
Expand Down
1 change: 1 addition & 0 deletions lib/iex/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative 'endpoints/company'
require_relative 'endpoints/dividends'
require_relative 'endpoints/earnings'
require_relative 'endpoints/historial_prices'
require_relative 'endpoints/income'
require_relative 'endpoints/largest_trades'
require_relative 'endpoints/logo'
Expand Down
1 change: 1 addition & 0 deletions lib/iex/api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Client
include Endpoints::Crypto
include Endpoints::Dividends
include Endpoints::Earnings
include Endpoints::HistoricalPrices
include Endpoints::Income
include Endpoints::KeyStats
include Endpoints::LargestTrades
Expand Down
30 changes: 30 additions & 0 deletions lib/iex/endpoints/historial_prices.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module IEX
module Endpoints
module HistoricalPrices
def historical_prices(symbol, options = {})
if options[:range] == 'date'
raise ArgumentError unless options[:date].present?
raise ArgumentError unless options[:chartByDay].present?
end

options = options.dup
# Historical prices IEX endpoint expects dates passed in a specific format - YYYYMMDD
options[:date] = options[:date].strftime('%Y%m%d') if options[:date].is_a?(Date)

path = "stock/#{symbol}/chart"
path += "/#{options[:range]}" if options.key?(:range)
path += "/#{options[:date]}" if options[:range] == 'date'

# We only want options to include query params at this point, remove :range and :date
options.delete(:range)
options.delete(:date)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're modifying the hash passed in, so either options = options.dup, or use .except or similar to get a copy without those keys. Let's make sure there are no other places where we did this, I may have missed that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see any downside to modifying it but happy to dup it if you prefer. This was the only place I did this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The downside is that code like this doesn't do what you'd expect it to do:

options = { range: ... }
prices_for_msft = historical_prices('msft', options)
prices_for_goog = historical_prices('goog', options)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also use options.key?(:range), nil is a value and while it doesn't make sense you don't want the caller to rely on you clearing the option, this is more by convention

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup that's a downside. Thanks


(get(path, { token: publishable_token }.merge(options)) || []).map do |data|
IEX::Resources::HistorialPrices.new(data)
end
rescue Faraday::ResourceNotFound => e
raise IEX::Errors::SymbolNotFoundError.new(symbol, e.response[:body])
end
end
end
end
1 change: 1 addition & 0 deletions lib/iex/resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require_relative 'resources/company'
require_relative 'resources/dividends'
require_relative 'resources/earnings'
require_relative 'resources/historical_prices'
require_relative 'resources/income'
require_relative 'resources/key_stats'
require_relative 'resources/largest_trades'
Expand Down
31 changes: 31 additions & 0 deletions lib/iex/resources/historical_prices.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module IEX
module Resources
class HistorialPrices < Resource
property 'date'
property 'open'
property 'open_dollar', from: 'open', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'close'
property 'close_dollar', from: 'close', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'high'
property 'high_dollar', from: 'high', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'low'
property 'low_dollar', from: 'low', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'volume'
property 'u_open', from: 'uOpen'
property 'u_open_dollar', from: 'uOpen', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'u_close', from: 'uClose'
property 'u_close_dollar', from: 'uClose', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'u_low', from: 'uLow'
property 'u_low_dollar', from: 'uLow', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'u_high', from: 'uHigh'
property 'u_high_dollar', from: 'uHigh', with: ->(v) { to_dollar(amount: v, ignore_cents: false) }
property 'u_volume', from: 'uVolume'
property 'change'
property 'change_percent', from: 'changePercent'
property 'change_percent_s', from: 'changePercent', with: ->(v) { percentage_to_string(v) }
property 'label'
property 'change_over_time', from: 'changeOverTime'
property 'change_over_time_s', from: 'changeOverTime', with: ->(v) { percentage_to_string(v) }
end
end
end
12 changes: 12 additions & 0 deletions lib/iex/resources/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ def self.float_to_percentage(float_number)
].join
end

# Useful for values that are already a percent but we want to convert into a 2 decimal place string
def self.percentage_to_string(float_percent)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is essentially a direct copy of float_to_percentage but the the values being returned from IEX for historical prices were already a percent just in float format, so the string representation ended up being an incorrect value. Happy to try and combine these two methods with an optional parameter or something different? Don't love the copy paste nature of this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think copy paste is fine I wouldn't worry about it.

return unless float_percent.is_a? Numeric
return '+0.00%' if float_percent.zero?

[
float_percent.positive? ? '+' : '',
format('%.2f', float_percent),
'%'
].join
end

def self.to_dollar(amount:, ignore_cents: true)
MoneyHelper.money_to_text(amount, 'USD', nil, no_cents: ignore_cents)
end
Expand Down
56 changes: 56 additions & 0 deletions spec/fixtures/iex/historical_prices/abcd.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions spec/fixtures/iex/historical_prices/invalid.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions spec/fixtures/iex/historical_prices/invalid_date.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions spec/fixtures/iex/historical_prices/invalid_range.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading