TradingAgents/tradingagents/dataflows/finnhub_stock.py

144 lines
4.8 KiB
Python

"""Finnhub stock price data functions.
Provides OHLCV candle data and real-time quotes using the Finnhub REST API.
Output formats mirror the Alpha Vantage equivalents so LLM agents receive
consistent data regardless of the active vendor.
"""
from datetime import datetime
import pandas as pd
from .finnhub_common import (
FinnhubError,
ThirdPartyParseError,
_make_api_request,
_now_str,
_to_unix_timestamp,
)
# Finnhub resolution codes for the /stock/candle endpoint
_RESOLUTION_DAILY = "D"
def get_stock_candles(symbol: str, start_date: str, end_date: str) -> str:
"""Fetch daily OHLCV data for a symbol via Finnhub /stock/candle.
Returns a CSV-formatted string with columns matching the Alpha Vantage
TIME_SERIES_DAILY_ADJUSTED output (Date, Open, High, Low, Close, Volume)
so that downstream agents see a consistent format regardless of vendor.
Args:
symbol: Equity ticker symbol (e.g. "AAPL").
start_date: Inclusive start date in YYYY-MM-DD format.
end_date: Inclusive end date in YYYY-MM-DD format.
Returns:
CSV string with header row: ``timestamp,open,high,low,close,volume``
Raises:
FinnhubError: On API-level errors or when the symbol returns no data.
ThirdPartyParseError: When the response cannot be interpreted.
"""
params = {
"symbol": symbol,
"resolution": _RESOLUTION_DAILY,
"from": _to_unix_timestamp(start_date),
"to": _to_unix_timestamp(end_date) + 86400, # include end date (end of day)
}
data = _make_api_request("stock/candle", params)
status = data.get("s")
if status == "no_data":
raise FinnhubError(
f"No candle data returned for symbol={symbol}, "
f"start={start_date}, end={end_date}"
)
if status != "ok":
raise FinnhubError(
f"Unexpected candle response status '{status}' for symbol={symbol}"
)
# Finnhub returns parallel lists: t (timestamps), o, h, l, c, v
timestamps: list[int] = data.get("t", [])
opens: list[float] = data.get("o", [])
highs: list[float] = data.get("h", [])
lows: list[float] = data.get("l", [])
closes: list[float] = data.get("c", [])
volumes: list[int] = data.get("v", [])
if not timestamps:
raise FinnhubError(
f"Empty candle data for symbol={symbol}, "
f"start={start_date}, end={end_date}"
)
rows: list[str] = ["timestamp,open,high,low,close,volume"]
for ts, o, h, lo, c, v in zip(timestamps, opens, highs, lows, closes, volumes):
date_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
rows.append(f"{date_str},{o},{h},{lo},{c},{v}")
return "\n".join(rows)
def get_quote(symbol: str) -> dict:
"""Fetch the latest real-time quote for a symbol via Finnhub /quote.
Returns a normalised dict with human-readable keys so callers do not need
to map Finnhub's single-letter field names.
Args:
symbol: Equity ticker symbol (e.g. "AAPL").
Returns:
Dict with keys:
- ``symbol`` (str)
- ``current_price`` (float)
- ``change`` (float): Absolute change from previous close.
- ``change_percent`` (float): Percentage change from previous close.
- ``high`` (float): Day high.
- ``low`` (float): Day low.
- ``open`` (float): Day open.
- ``prev_close`` (float): Previous close price.
- ``timestamp`` (str): ISO datetime of the quote.
Raises:
FinnhubError: When the API returns an error or the symbol is invalid.
ThirdPartyParseError: When the response cannot be parsed.
"""
data = _make_api_request("quote", {"symbol": symbol})
current_price: float = data.get("c", 0.0)
prev_close: float = data.get("pc", 0.0)
# Finnhub returns d (change) and dp (change percent) directly
change: float = data.get("d", 0.0)
change_percent: float = data.get("dp", 0.0)
# Validate that we received a real quote (current_price == 0 means unknown symbol)
if current_price == 0 and prev_close == 0:
raise FinnhubError(
f"Quote returned all-zero values for symbol={symbol}. "
"Symbol may be invalid or market data unavailable."
)
timestamp_unix: int = data.get("t", 0)
if timestamp_unix:
timestamp_str = datetime.fromtimestamp(timestamp_unix).strftime("%Y-%m-%d %H:%M:%S")
else:
timestamp_str = _now_str()
return {
"symbol": symbol,
"current_price": current_price,
"change": change,
"change_percent": change_percent,
"high": data.get("h", 0.0),
"low": data.get("l", 0.0),
"open": data.get("o", 0.0),
"prev_close": prev_close,
"timestamp": timestamp_str,
}