Fix Yahoo minor unit handling (#335)

Some security prices get returned in non-standard minor units (eg. `GBp` for Great British Pence, instead of `GBP` for Great British Pounds), leading to incorrect handling of the returned price data.

This commit adds conversion logic for the two currencies I've been able to find examples of this affecting.
This commit is contained in:
Matthew Kilpatrick
2025-11-15 17:34:22 +00:00
committed by GitHub
parent 4475d85ab2
commit ff43e71dec
2 changed files with 59 additions and 3 deletions

View File

@@ -248,18 +248,21 @@ class Provider::YahooFinance < Provider
closes = quotes["close"] || []
# Get currency from metadata
currency = chart_data.dig("meta", "currency") || "USD"
raw_currency = chart_data.dig("meta", "currency") || "USD"
prices = []
timestamps.each_with_index do |timestamp, index|
close_price = closes[index]
next if close_price.nil? # Skip days with no data (weekends, holidays)
# Normalize currency and price to handle minor units
normalized_currency, normalized_price = normalize_currency_and_price(raw_currency, close_price.to_f)
prices << Price.new(
symbol: symbol,
date: Time.at(timestamp).to_date,
price: close_price.to_f,
currency: currency,
price: normalized_price,
currency: normalized_currency,
exchange_operating_mic: exchange_operating_mic
)
end
@@ -277,6 +280,29 @@ class Provider::YahooFinance < Provider
ENV["YAHOO_FINANCE_URL"] || "https://query1.finance.yahoo.com"
end
# ================================
# Currency Normalization
# ================================
# Yahoo Finance sometimes returns currencies in minor units (pence, cents)
# This is not part of ISO 4217 but is a convention used by financial data providers
# Mapping of Yahoo Finance minor unit codes to standard currency codes and conversion multipliers
MINOR_CURRENCY_CONVERSIONS = {
"GBp" => { currency: "GBP", multiplier: 0.01 }, # British pence to pounds (eg. https://finance.yahoo.com/quote/IITU.L/)
"ZAc" => { currency: "ZAR", multiplier: 0.01 } # South African cents to rand (eg. https://finance.yahoo.com/quote/JSE.JO)
}.freeze
# Normalizes Yahoo Finance currency codes and prices
# Returns [currency_code, price] with currency converted to standard ISO code
# and price converted from minor units to major units if applicable
def normalize_currency_and_price(currency, price)
if conversion = MINOR_CURRENCY_CONVERSIONS[currency]
[ conversion[:currency], price * conversion[:multiplier] ]
else
[ currency, price ]
end
end
# ================================
# Validation
# ================================

View File

@@ -215,4 +215,34 @@ class Provider::YahooFinanceTest < ActiveSupport::TestCase
@provider.send(:validate_date_range!, Date.current - 5.years, Date.current)
end
end
# ================================
# Currency Normalization Tests
# ================================
test "normalize_currency_and_price converts GBp to GBP" do
currency, price = @provider.send(:normalize_currency_and_price, "GBp", 1234.56)
assert_equal "GBP", currency
assert_equal 12.3456, price
end
test "normalize_currency_and_price converts ZAc to ZAR" do
currency, price = @provider.send(:normalize_currency_and_price, "ZAc", 5000.0)
assert_equal "ZAR", currency
assert_equal 50.0, price
end
test "normalize_currency_and_price leaves standard currencies unchanged" do
currency, price = @provider.send(:normalize_currency_and_price, "USD", 100.50)
assert_equal "USD", currency
assert_equal 100.50, price
currency, price = @provider.send(:normalize_currency_and_price, "GBP", 50.25)
assert_equal "GBP", currency
assert_equal 50.25, price
currency, price = @provider.send(:normalize_currency_and_price, "EUR", 75.75)
assert_equal "EUR", currency
assert_equal 75.75, price
end
end