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:
3 files changed
+212
-22
No files matched your search
@@ -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)}"
|
||||
Reference in new issue
Block a user