fix(data): reject stale yfinance OHLCV instead of reporting wrong prices

yfinance intermittently returns a year-old partial frame (e.g. June 2025 rows
for a June 2026 request) that still has rows and a Close, so it passed the
empty-check and silently fed a wrong close price and indicators into the report
(#1021). Add a freshness guard that rejects a frame whose latest row is far
older than the requested date, on both the raw OHLCV path and the indicator
path. It raises the existing NoMarketDataError with a stale-specific detail, so
the vendor router's try-next-vendor and single unavailable-signal handling apply
unchanged; the sentinel now surfaces that detail so the agent reports the
specific reason rather than fabricating a value.
This commit is contained in:
Yijia-Xiao committed 2026-06-14 07:10:15 +00:00
1 parent 7df18fc912
commit 9fd54f8368
3 files changed
+212 -22

No files matched your search

+30 -17
View File
@@ -1,11 +1,19 @@
from typing import Annotated
from datetime import datetime
from dateutil.relativedelta import relativedelta
from typing import Annotated
import pandas as pd
import yfinance as yf
import os
from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry, load_ohlcv, filter_financials_by_date
from .symbol_utils import normalize_symbol, NoMarketDataError
from dateutil.relativedelta import relativedelta
from .stockstats_utils import (
StockstatsUtils,
_assert_ohlcv_not_stale,
filter_financials_by_date,
load_ohlcv,
yf_retry,
)
from .symbol_utils import NoMarketDataError, normalize_symbol
def get_YFin_data_online(
symbol: Annotated[str, "ticker symbol of the company"],
@@ -38,6 +46,11 @@ def get_YFin_data_online(
if data.index.tz is not None:
data.index = data.index.tz_localize(None)
# Reject a stale frame (e.g. a year-old partial response) before it is
# formatted into the report. Raises NoMarketDataError, which the router
# turns into one clear unavailable signal (#1021).
_assert_ohlcv_not_stale(data, end_date, symbol, canonical)
# Round numerical values to 2 decimal places for cleaner display
numeric_columns = ["Open", "High", "Low", "Close", "Adj Close"]
for col in numeric_columns:
@@ -150,23 +163,23 @@ def get_stock_stats_indicators_window(
# Optimized: Get stock data once and calculate indicators for all dates
try:
indicator_data = _get_stock_stats_bulk(symbol, indicator, curr_date)
# Generate the date range we need
current_dt = curr_date_dt
date_values = []
while current_dt >= before:
date_str = current_dt.strftime('%Y-%m-%d')
# Look up the indicator value for this date
if date_str in indicator_data:
indicator_value = indicator_data[date_str]
else:
indicator_value = "N/A: Not a trading day (weekend or holiday)"
date_values.append((date_str, indicator_value))
current_dt = current_dt - relativedelta(days=1)
# Build the result string
ind_string = ""
for date_str, value in date_values:
@@ -211,22 +224,22 @@ def _get_stock_stats_bulk(
data = load_ohlcv(symbol, curr_date)
df = wrap(data)
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
# Calculate the indicator for all rows at once
df[indicator] # This triggers stockstats to calculate the indicator
# Create a dictionary mapping date strings to indicator values
result_dict = {}
for _, row in df.iterrows():
date_str = row["Date"]
indicator_value = row[indicator]
# Handle NaN/None values
if pd.isna(indicator_value):
result_dict[date_str] = "N/A"
else:
result_dict[date_str] = str(indicator_value)
return result_dict
@@ -450,8 +463,8 @@ def get_insider_transactions(
# Add header information
header = f"# Insider Transactions data for {canonical}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
return header + csv_string
except Exception as e:
return f"Error retrieving insider transactions for {ticker}: {str(e)}"
return f"Error retrieving insider transactions for {ticker}: {str(e)}"