From f6346f2070450f3e0d686b72b27168da3e3e7656 Mon Sep 17 00:00:00 2001 From: Francesc Leveque Date: Thu, 29 Jan 2026 07:34:50 +0100 Subject: [PATCH] Add extended stock information fields Include name, PE ratio, EPS, dividend, dividend yield (calculated), payout ratio (calculated), 50-day and 200-day moving averages in quote response. Handle edge cases for missing dividend data and negative EPS values. Co-Authored-By: Claude Opus 4.5 --- lib/yahoo_finance_client/stock.rb | 30 ++- spec/yahoo_finance_client/stock_spec.rb | 237 ++++++++++++++++++++---- 2 files changed, 228 insertions(+), 39 deletions(-) diff --git a/lib/yahoo_finance_client/stock.rb b/lib/yahoo_finance_client/stock.rb index 026117e..2780442 100644 --- a/lib/yahoo_finance_client/stock.rb +++ b/lib/yahoo_finance_client/stock.rb @@ -64,8 +64,34 @@ def parse_response(body, symbol) end def format_quote(quote) - { symbol: quote["symbol"], price: quote["regularMarketPrice"], change: quote["regularMarketChange"], - percent_change: quote["regularMarketChangePercent"], volume: quote["regularMarketVolume"] } + price = quote["regularMarketPrice"] + dividend = quote["dividendRate"] + eps = quote["epsTrailingTwelveMonths"] + + build_quote_hash(quote, price, dividend, eps) + end + + def build_quote_hash(quote, price, dividend, eps) + { + symbol: quote["symbol"], name: quote["shortName"], price: price, + change: quote["regularMarketChange"], percent_change: quote["regularMarketChangePercent"], + volume: quote["regularMarketVolume"], pe_ratio: quote["trailingPE"], eps: eps, + dividend: dividend, dividend_yield: calculate_yield(dividend, price), + payout_ratio: calculate_payout(dividend, eps), + ma50: quote["fiftyDayAverage"], ma200: quote["twoHundredDayAverage"] + } + end + + def calculate_yield(dividend, price) + return nil unless dividend && price&.positive? + + (dividend / price * 100).round(2) + end + + def calculate_payout(dividend, eps) + return nil unless dividend && eps&.positive? + + (dividend / eps * 100).round(2) end def fetch_from_cache(key) diff --git a/spec/yahoo_finance_client/stock_spec.rb b/spec/yahoo_finance_client/stock_spec.rb index 39d435f..509abaa 100644 --- a/spec/yahoo_finance_client/stock_spec.rb +++ b/spec/yahoo_finance_client/stock_spec.rb @@ -32,16 +32,40 @@ "result" => [ { "symbol" => "AAPL", + "shortName" => "Apple Inc.", "regularMarketPrice" => 150.0, "regularMarketChange" => 1.5, "regularMarketChangePercent" => 1.0, - "regularMarketVolume" => 100_000 + "regularMarketVolume" => 100_000, + "trailingPE" => 25.5, + "epsTrailingTwelveMonths" => 5.88, + "dividendRate" => 0.96, + "fiftyDayAverage" => 148.5, + "twoHundredDayAverage" => 145.0 } ] } }.to_json end + let(:expected_quote) do + { + symbol: "AAPL", + name: "Apple Inc.", + price: 150.0, + change: 1.5, + percent_change: 1.0, + volume: 100_000, + pe_ratio: 25.5, + eps: 5.88, + dividend: 0.96, + dividend_yield: 0.64, + payout_ratio: 16.33, + ma50: 148.5, + ma200: 145.0 + } + end + before do stub_request(:get, quote_url) .to_return(status: 200, body: response_body) @@ -49,25 +73,13 @@ it "returns the quote data" do result = described_class.get_quote(symbol) - expect(result).to eq( - symbol: "AAPL", - price: 150.0, - change: 1.5, - percent_change: 1.0, - volume: 100_000 - ) + expect(result).to eq(expected_quote) end it "caches the quote data" do described_class.get_quote(symbol) cache = described_class.instance_variable_get(:@cache) - expect(cache[cache_key][:data]).to eq( - symbol: "AAPL", - price: 150.0, - change: 1.5, - percent_change: 1.0, - volume: 100_000 - ) + expect(cache[cache_key][:data]).to eq(expected_quote) end end @@ -107,10 +119,18 @@ let(:cached_data) do { symbol: "AAPL", + name: "Apple Inc.", price: 150.0, change: 1.5, percent_change: 1.0, - volume: 100_000 + volume: 100_000, + pe_ratio: 25.5, + eps: 5.88, + dividend: 0.96, + dividend_yield: 0.64, + payout_ratio: 16.33, + ma50: 148.5, + ma200: 145.0 } end @@ -133,10 +153,18 @@ let(:cached_data) do { symbol: "AAPL", + name: "Apple Inc.", price: 150.0, change: 1.5, percent_change: 1.0, - volume: 100_000 + volume: 100_000, + pe_ratio: 25.5, + eps: 5.88, + dividend: 0.96, + dividend_yield: 0.64, + payout_ratio: 16.33, + ma50: 148.5, + ma200: 145.0 } end @@ -146,16 +174,40 @@ "result" => [ { "symbol" => "AAPL", - "regularMarketPrice" => 150.0, - "regularMarketChange" => 1.5, - "regularMarketChangePercent" => 1.0, - "regularMarketVolume" => 100_000 + "shortName" => "Apple Inc.", + "regularMarketPrice" => 155.0, + "regularMarketChange" => 2.0, + "regularMarketChangePercent" => 1.3, + "regularMarketVolume" => 120_000, + "trailingPE" => 26.0, + "epsTrailingTwelveMonths" => 5.96, + "dividendRate" => 0.96, + "fiftyDayAverage" => 150.0, + "twoHundredDayAverage" => 147.0 } ] } }.to_json end + let(:expected_new_quote) do + { + symbol: "AAPL", + name: "Apple Inc.", + price: 155.0, + change: 2.0, + percent_change: 1.3, + volume: 120_000, + pe_ratio: 26.0, + eps: 5.96, + dividend: 0.96, + dividend_yield: 0.62, + payout_ratio: 16.11, + ma50: 150.0, + ma200: 147.0 + } + end + before do described_class.instance_variable_set( :@cache, @@ -169,20 +221,103 @@ it "fetches new data and updates the cache" do result = described_class.get_quote(symbol) + expect(result).to eq(expected_new_quote) + cache = described_class.instance_variable_get(:@cache) + expect(cache[cache_key][:data]).to eq(expected_new_quote) + end + end + + context "when stock has no dividend data" do + let(:response_body) do + { + "quoteResponse" => { + "result" => [ + { + "symbol" => "GOOG", + "shortName" => "Alphabet Inc.", + "regularMarketPrice" => 140.0, + "regularMarketChange" => -0.5, + "regularMarketChangePercent" => -0.36, + "regularMarketVolume" => 50_000, + "trailingPE" => 22.0, + "epsTrailingTwelveMonths" => 6.36, + "fiftyDayAverage" => 138.0, + "twoHundredDayAverage" => 135.0 + } + ] + } + }.to_json + end + + before do + stub_request(:get, "#{base_url}/v7/finance/quote?symbols=GOOG&crumb=#{crumb}") + .to_return(status: 200, body: response_body) + end + + it "returns nil for dividend-related fields" do + result = described_class.get_quote("GOOG") expect(result).to eq( - symbol: "AAPL", - price: 150.0, - change: 1.5, - percent_change: 1.0, - volume: 100_000 + symbol: "GOOG", + name: "Alphabet Inc.", + price: 140.0, + change: -0.5, + percent_change: -0.36, + volume: 50_000, + pe_ratio: 22.0, + eps: 6.36, + dividend: nil, + dividend_yield: nil, + payout_ratio: nil, + ma50: 138.0, + ma200: 135.0 ) - cache = described_class.instance_variable_get(:@cache) - expect(cache[cache_key][:data]).to eq( - symbol: "AAPL", - price: 150.0, - change: 1.5, - percent_change: 1.0, - volume: 100_000 + end + end + + context "when stock has negative EPS" do + let(:response_body) do + { + "quoteResponse" => { + "result" => [ + { + "symbol" => "TSLA", + "shortName" => "Tesla Inc.", + "regularMarketPrice" => 200.0, + "regularMarketChange" => 5.0, + "regularMarketChangePercent" => 2.56, + "regularMarketVolume" => 80_000, + "trailingPE" => nil, + "epsTrailingTwelveMonths" => -1.5, + "dividendRate" => 0.0, + "fiftyDayAverage" => 195.0, + "twoHundredDayAverage" => 180.0 + } + ] + } + }.to_json + end + + before do + stub_request(:get, "#{base_url}/v7/finance/quote?symbols=TSLA&crumb=#{crumb}") + .to_return(status: 200, body: response_body) + end + + it "returns nil for payout ratio when EPS is negative" do + result = described_class.get_quote("TSLA") + expect(result).to eq( + symbol: "TSLA", + name: "Tesla Inc.", + price: 200.0, + change: 5.0, + percent_change: 2.56, + volume: 80_000, + pe_ratio: nil, + eps: -1.5, + dividend: 0.0, + dividend_yield: 0.0, + payout_ratio: nil, + ma50: 195.0, + ma200: 180.0 ) end end @@ -194,10 +329,16 @@ "result" => [ { "symbol" => "AAPL", + "shortName" => "Apple Inc.", "regularMarketPrice" => 150.0, "regularMarketChange" => 1.5, "regularMarketChangePercent" => 1.0, - "regularMarketVolume" => 100_000 + "regularMarketVolume" => 100_000, + "trailingPE" => 25.5, + "epsTrailingTwelveMonths" => 5.88, + "dividendRate" => 0.96, + "fiftyDayAverage" => 148.5, + "twoHundredDayAverage" => 145.0 } ] } @@ -217,10 +358,18 @@ result = described_class.get_quote(symbol) expect(result).to eq( symbol: "AAPL", + name: "Apple Inc.", price: 150.0, change: 1.5, percent_change: 1.0, - volume: 100_000 + volume: 100_000, + pe_ratio: 25.5, + eps: 5.88, + dividend: 0.96, + dividend_yield: 0.64, + payout_ratio: 16.33, + ma50: 148.5, + ma200: 145.0 ) end end @@ -244,10 +393,16 @@ "result" => [ { "symbol" => "AAPL", + "shortName" => "Apple Inc.", "regularMarketPrice" => 150.0, "regularMarketChange" => 1.5, "regularMarketChangePercent" => 1.0, - "regularMarketVolume" => 100_000 + "regularMarketVolume" => 100_000, + "trailingPE" => 25.5, + "epsTrailingTwelveMonths" => 5.88, + "dividendRate" => 0.96, + "fiftyDayAverage" => 148.5, + "twoHundredDayAverage" => 145.0 } ] } @@ -268,10 +423,18 @@ result = described_class.get_quote(symbol) expect(result).to eq( symbol: "AAPL", + name: "Apple Inc.", price: 150.0, change: 1.5, percent_change: 1.0, - volume: 100_000 + volume: 100_000, + pe_ratio: 25.5, + eps: 5.88, + dividend: 0.96, + dividend_yield: 0.64, + payout_ratio: 16.33, + ma50: 148.5, + ma200: 145.0 ) end end