mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 06:56:39 +03:00
- vendor functions and date_window take as_of_date, the date data is served as of; the model-facing tool arguments are unchanged - build_instrument_context and resolve_instrument_context take trade_date, which is what they receive
305 lines
12 KiB
Python
305 lines
12 KiB
Python
import logging
|
|
from datetime import datetime
|
|
from typing import Annotated
|
|
|
|
import pandas as pd
|
|
import yfinance as yf
|
|
from dateutil.relativedelta import relativedelta
|
|
from stockstats import wrap
|
|
|
|
from tradingagents.dataflows.errors import NoMarketDataError, VendorError
|
|
from tradingagents.dataflows.symbols import normalize_symbol
|
|
from tradingagents.dataflows.vendors.yahoo.ohlcv import (
|
|
_assert_ohlcv_not_stale,
|
|
load_ohlcv,
|
|
raise_for_empty,
|
|
yf_retry,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_YFin_data_online(
|
|
symbol: Annotated[str, "ticker symbol of the company"],
|
|
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
|
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
|
):
|
|
|
|
datetime.strptime(start_date, "%Y-%m-%d")
|
|
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
|
|
|
# Resolve broker/forex symbols to Yahoo's convention (XAUUSD+ -> GC=F).
|
|
canonical = normalize_symbol(symbol)
|
|
ticker = yf.Ticker(canonical)
|
|
|
|
# yfinance treats ``end`` as EXCLUSIVE, so it would drop the requested
|
|
# end_date row (and the current day when end_date is today). Request one day
|
|
# past end_date so the requested range is actually inclusive (#986/#987).
|
|
end_inclusive = (end_dt + relativedelta(days=1)).strftime("%Y-%m-%d")
|
|
data = yf_retry(lambda: ticker.history(start=start_date, end=end_inclusive))
|
|
|
|
# Empty result means the symbol is unknown/delisted. Raise a typed error
|
|
# instead of returning prose: the routing layer turns it into a single
|
|
# unambiguous "no data" signal so the agent never fabricates a price.
|
|
if data.empty:
|
|
raise_for_empty(symbol, canonical, f"rows between {start_date} and {end_date}")
|
|
|
|
# Remove timezone info from index for cleaner output
|
|
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:
|
|
if col in data.columns:
|
|
data[col] = data[col].round(2)
|
|
|
|
csv_string = data.to_csv()
|
|
|
|
# Name the resolved symbol when it differs, so the reader sees which
|
|
# instrument was priced.
|
|
label = canonical if canonical == symbol.upper() else f"{canonical} (from {symbol})"
|
|
header = f"# Stock data for {label} from {start_date} to {end_date}\n"
|
|
header += f"# Total records: {len(data)}\n\n"
|
|
|
|
return header + csv_string
|
|
|
|
|
|
def get_stock_stats_indicators_window(
|
|
symbol: Annotated[str, "ticker symbol of the company"],
|
|
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
|
as_of_date: Annotated[
|
|
str, "The current trading date you are trading on, YYYY-mm-dd"
|
|
],
|
|
look_back_days: Annotated[int, "how many days to look back"],
|
|
) -> str:
|
|
|
|
best_ind_params = {
|
|
# Moving Averages
|
|
"close_50_sma": (
|
|
"50 SMA: A medium-term trend indicator. "
|
|
"Usage: Identify trend direction and serve as dynamic support/resistance. "
|
|
"Tips: It lags price; combine with faster indicators for timely signals."
|
|
),
|
|
"close_200_sma": (
|
|
"200 SMA: A long-term trend benchmark. "
|
|
"Usage: Confirm overall market trend and identify golden/death cross setups. "
|
|
"Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries."
|
|
),
|
|
"close_10_ema": (
|
|
"10 EMA: A responsive short-term average. "
|
|
"Usage: Capture quick shifts in momentum and potential entry points. "
|
|
"Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals."
|
|
),
|
|
# MACD Related
|
|
"macd": (
|
|
"MACD: Computes momentum via differences of EMAs. "
|
|
"Usage: Look for crossovers and divergence as signals of trend changes. "
|
|
"Tips: Confirm with other indicators in low-volatility or sideways markets."
|
|
),
|
|
"macds": (
|
|
"MACD Signal: An EMA smoothing of the MACD line. "
|
|
"Usage: Use crossovers with the MACD line to trigger trades. "
|
|
"Tips: Should be part of a broader strategy to avoid false positives."
|
|
),
|
|
"macdh": (
|
|
"MACD Histogram: Shows the gap between the MACD line and its signal. "
|
|
"Usage: Visualize momentum strength and spot divergence early. "
|
|
"Tips: Can be volatile; complement with additional filters in fast-moving markets."
|
|
),
|
|
# Momentum Indicators
|
|
"rsi": (
|
|
"RSI: Measures momentum to flag overbought/oversold conditions. "
|
|
"Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. "
|
|
"Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis."
|
|
),
|
|
# Volatility Indicators
|
|
"boll": (
|
|
"Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. "
|
|
"Usage: Acts as a dynamic benchmark for price movement. "
|
|
"Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals."
|
|
),
|
|
"boll_ub": (
|
|
"Bollinger Upper Band: Typically 2 standard deviations above the middle line. "
|
|
"Usage: Signals potential overbought conditions and breakout zones. "
|
|
"Tips: Confirm signals with other tools; prices may ride the band in strong trends."
|
|
),
|
|
"boll_lb": (
|
|
"Bollinger Lower Band: Typically 2 standard deviations below the middle line. "
|
|
"Usage: Indicates potential oversold conditions. "
|
|
"Tips: Use additional analysis to avoid false reversal signals."
|
|
),
|
|
"atr": (
|
|
"ATR: Averages true range to measure volatility. "
|
|
"Usage: Set stop-loss levels and adjust position sizes based on current market volatility. "
|
|
"Tips: It's a reactive measure, so use it as part of a broader risk management strategy."
|
|
),
|
|
# Volume-Based Indicators
|
|
"vwma": (
|
|
"VWMA: A moving average weighted by volume. "
|
|
"Usage: Confirm trends by integrating price action with volume data. "
|
|
"Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses."
|
|
),
|
|
"mfi": (
|
|
"MFI: The Money Flow Index is a momentum indicator that uses both price and volume to measure buying and selling pressure. "
|
|
"Usage: Identify overbought (>80) or oversold (<20) conditions and confirm the strength of trends or reversals. "
|
|
"Tips: Use alongside RSI or MACD to confirm signals; divergence between price and MFI can indicate potential reversals."
|
|
),
|
|
}
|
|
|
|
if indicator not in best_ind_params:
|
|
raise ValueError(
|
|
f"Indicator {indicator} is not supported. Please choose from: {list(best_ind_params.keys())}"
|
|
)
|
|
|
|
end_date = as_of_date
|
|
as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
|
before = as_of_dt - relativedelta(days=look_back_days)
|
|
|
|
# Optimized: Get stock data once and calculate indicators for all dates
|
|
try:
|
|
indicator_data = _get_stock_stats_bulk(symbol, indicator, as_of_date)
|
|
|
|
# Generate the date range we need
|
|
current_dt = as_of_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)
|
|
|
|
ind_string = ""
|
|
for date_str, value in date_values:
|
|
ind_string += f"{date_str}: {value}\n"
|
|
|
|
except VendorError:
|
|
raise # Unknown/delisted symbol — let the router emit the sentinel
|
|
except Exception as e:
|
|
logger.warning("Bulk stockstats fetch failed, falling back per-day: %s", e)
|
|
# Fallback to original implementation if bulk method fails
|
|
ind_string = ""
|
|
as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
|
while as_of_dt >= before:
|
|
indicator_value = get_stockstats_indicator(
|
|
symbol, indicator, as_of_dt.strftime("%Y-%m-%d")
|
|
)
|
|
ind_string += f"{as_of_dt.strftime('%Y-%m-%d')}: {indicator_value}\n"
|
|
as_of_dt = as_of_dt - relativedelta(days=1)
|
|
|
|
result_str = (
|
|
f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n"
|
|
+ ind_string
|
|
+ "\n\n"
|
|
+ best_ind_params.get(indicator, "No description available.")
|
|
)
|
|
|
|
return result_str
|
|
|
|
|
|
def _get_stock_stats_bulk(
|
|
symbol: Annotated[str, "ticker symbol of the company"],
|
|
indicator: Annotated[str, "technical indicator to calculate"],
|
|
as_of_date: Annotated[str, "current date for reference"]
|
|
) -> dict:
|
|
"""
|
|
Optimized bulk calculation of stock stats indicators.
|
|
Fetches data once and calculates indicator for all available dates.
|
|
Returns dict mapping date strings to indicator values.
|
|
"""
|
|
from stockstats import wrap
|
|
|
|
data = load_ohlcv(symbol, as_of_date)
|
|
df = wrap(data)
|
|
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
|
|
|
df[indicator] # This triggers stockstats to calculate the indicator
|
|
|
|
result_dict = {}
|
|
for _, row in df.iterrows():
|
|
date_str = row["Date"]
|
|
indicator_value = row[indicator]
|
|
|
|
if pd.isna(indicator_value):
|
|
result_dict[date_str] = "N/A"
|
|
else:
|
|
result_dict[date_str] = str(indicator_value)
|
|
|
|
return result_dict
|
|
|
|
|
|
def get_stockstats_indicator(
|
|
symbol: Annotated[str, "ticker symbol of the company"],
|
|
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
|
as_of_date: Annotated[
|
|
str, "The current trading date you are trading on, YYYY-mm-dd"
|
|
],
|
|
) -> str:
|
|
|
|
as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
|
as_of_date = as_of_dt.strftime("%Y-%m-%d")
|
|
|
|
try:
|
|
indicator_value = get_stock_stats(
|
|
symbol,
|
|
indicator,
|
|
as_of_date,
|
|
)
|
|
except VendorError:
|
|
raise # Unknown/delisted symbol — let the router emit the sentinel
|
|
except Exception as e:
|
|
# An empty string renders as "2026-05-08: " in the indicator table, which
|
|
# reads as no value that day rather than a read that failed. Raise so the
|
|
# router can try the next vendor or report the series unavailable.
|
|
raise NoMarketDataError(
|
|
symbol, symbol, f"{indicator} could not be read for {as_of_date}: {e}"
|
|
) from e
|
|
|
|
return str(indicator_value)
|
|
|
|
|
|
def get_closes(symbol: str, start_date: str, end_date: str) -> pd.Series:
|
|
"""Daily closes from ``start_date`` up to, not including, ``end_date``."""
|
|
canonical = normalize_symbol(symbol)
|
|
try:
|
|
history = yf_retry(lambda: yf.Ticker(canonical).history(start=start_date, end=end_date))
|
|
except Exception as e:
|
|
raise NoMarketDataError(symbol, canonical, f"prices unavailable: {e}") from e
|
|
return history["Close"] if "Close" in history else pd.Series(dtype=float)
|
|
|
|
|
|
def get_stock_stats(
|
|
symbol: Annotated[str, "ticker symbol for the company"],
|
|
indicator: Annotated[
|
|
str, "quantitative indicators based off of the stock data for the company"
|
|
],
|
|
as_of_date: Annotated[
|
|
str, "curr date for retrieving stock price data, YYYY-mm-dd"
|
|
],
|
|
):
|
|
data = load_ohlcv(symbol, as_of_date)
|
|
df = wrap(data)
|
|
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
|
as_of_str = pd.to_datetime(as_of_date).strftime("%Y-%m-%d")
|
|
|
|
df[indicator] # trigger stockstats to calculate the indicator
|
|
matching_rows = df[df["Date"].str.startswith(as_of_str)]
|
|
|
|
if not matching_rows.empty:
|
|
indicator_value = matching_rows[indicator].values[0]
|
|
return indicator_value
|
|
else:
|
|
return "N/A: Not a trading day (weekend or holiday)"
|