From ff43e71dec5e0d450b97afa35c21c40092fafb74 Mon Sep 17 00:00:00 2001 From: Matthew Kilpatrick <16194494+Matthew-Kilpatrick@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:34:22 +0000 Subject: [PATCH] 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. --- app/models/provider/yahoo_finance.rb | 32 ++++++++++++++++++++-- test/models/provider/yahoo_finance_test.rb | 30 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/app/models/provider/yahoo_finance.rb b/app/models/provider/yahoo_finance.rb index 2f934f9ba..bb008b5f6 100644 --- a/app/models/provider/yahoo_finance.rb +++ b/app/models/provider/yahoo_finance.rb @@ -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 # ================================ diff --git a/test/models/provider/yahoo_finance_test.rb b/test/models/provider/yahoo_finance_test.rb index 0bf9bb974..2c01fde3f 100644 --- a/test/models/provider/yahoo_finance_test.rb +++ b/test/models/provider/yahoo_finance_test.rb @@ -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