package com.rods.backtestingstrategies.service; import org.springframework.stereotype.Service; import yahoofinance.Stock; import yahoofinance.YahooFinance; import yahoofinance.histquotes.HistoricalQuote; import yahoofinance.histquotes.Interval; import yahoofinance.quotes.stock.StockQuote; import yahoofinance.quotes.stock.StockStats; import java.io.IOException; import java.util.Calendar; import java.util.List; import java.util.Map; /** * Service wrapping the Yahoo Finance API. * No API key required. No enforced rate limits. */ @Service public class YahooFinanceService { static { // Set standard user-agent so Yahoo doesn't reject as bot with 429 Error System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"); } /** * Fetch full stock info (quote, stats, dividend) */ public Stock getStock(String symbol) throws IOException { return YahooFinance.get(symbol); } /** * Fetch stock with full historical data (default: 1 year, daily) */ public Stock getStockWithHistory(String symbol) throws IOException { return YahooFinance.get(symbol, true); } /** * Fetch historical daily data for a custom date range using v8 API (Bypasses * 429) */ public List getHistoricalData(String symbol, Calendar from, Calendar to) throws IOException { String urlString = String.format( "https://query1.finance.yahoo.com/v8/finance/chart/%s?period1=%d&period2=%d&interval=1d", symbol, from.getTimeInMillis() / 1000, to.getTimeInMillis() / 1000); return fetchHistoryFromV8(symbol, urlString); } /** * Fetch historical daily data with default lookback (1 year) */ public List getHistoricalData(String symbol) throws IOException { Calendar from = Calendar.getInstance(); from.add(Calendar.YEAR, -1); Calendar to = Calendar.getInstance(); return getHistoricalData(symbol, from, to); } private List fetchHistoryFromV8(String symbol, String urlString) throws IOException { java.util.List history = new java.util.ArrayList<>(); try { java.net.URL url = new java.net.URL(urlString); java.net.HttpURLConnection request = (java.net.HttpURLConnection) url.openConnection(); request.setRequestMethod("GET"); request.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); request.connect(); int responseCode = request.getResponseCode(); if (responseCode == 404) return history; // Stock not found com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(request.getInputStream()); com.fasterxml.jackson.databind.JsonNode result = root.path("chart").path("result").get(0); if (result == null || result.isMissingNode()) return history; com.fasterxml.jackson.databind.JsonNode timestampNode = result.path("timestamp"); com.fasterxml.jackson.databind.JsonNode quoteNode = result.path("indicators").path("quote").get(0); com.fasterxml.jackson.databind.JsonNode adjCloseNode = result.path("indicators").path("adjclose").get(0); if (timestampNode.isMissingNode() || quoteNode.isMissingNode()) return history; for (int i = 0; i < timestampNode.size(); i++) { long timestamp = timestampNode.get(i).asLong(); java.util.Calendar date = java.util.Calendar.getInstance(); date.setTimeInMillis(timestamp * 1000); java.math.BigDecimal open = getBigDecimal(quoteNode.path("open").get(i)); java.math.BigDecimal high = getBigDecimal(quoteNode.path("high").get(i)); java.math.BigDecimal low = getBigDecimal(quoteNode.path("low").get(i)); java.math.BigDecimal close = getBigDecimal(quoteNode.path("close").get(i)); java.math.BigDecimal adjClose = adjCloseNode != null && !adjCloseNode.isMissingNode() ? getBigDecimal(adjCloseNode.path("adjclose").get(i)) : close; long volume = quoteNode.path("volume").get(i) != null ? quoteNode.path("volume").get(i).asLong() : 0L; if (close != null) { history.add(new HistoricalQuote(symbol, date, open, low, high, close, adjClose, volume)); } } } catch (Exception e) { throw new IOException("Failed to fetch custom Yahoo v8 API for " + symbol, e); } return history; } private java.math.BigDecimal getBigDecimal(com.fasterxml.jackson.databind.JsonNode node) { if (node == null || node.isNull() || node.isMissingNode()) return null; return new java.math.BigDecimal(node.asText()); } /** * Fetch multiple stocks at once (single batch request) */ public Map getMultipleStocks(String[] symbols) throws IOException { return YahooFinance.get(symbols); } /** * Validate if a symbol exists on Yahoo Finance */ public boolean isValidSymbol(String symbol) { try { Stock stock = YahooFinance.get(symbol); return stock != null && stock.getQuote() != null && stock.getQuote().getPrice() != null; } catch (IOException e) { return false; } } /** * Get real-time quote data */ public StockQuote getQuote(String symbol) throws IOException { Stock stock = YahooFinance.get(symbol); return stock.getQuote(); } /** * Get stock statistics (PE, EPS, market cap, etc.) */ public StockStats getStats(String symbol) throws IOException { Stock stock = YahooFinance.get(symbol); return stock.getStats(); } }