mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-22 15:18:04 +00:00
251 lines
9.8 KiB
Python
251 lines
9.8 KiB
Python
"""Earnings router -- earnings history, upcoming dates, and transcripts (FMP)."""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _safe_float(val, default=None):
|
|
if val is None:
|
|
return default
|
|
try:
|
|
import math
|
|
f = float(val)
|
|
return default if math.isnan(f) or math.isinf(f) else f
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
@router.get("/{ticker}/history", summary="Earnings history (EPS actual vs estimate)")
|
|
async def earnings_history(ticker: str) -> Dict[str, Any]:
|
|
try:
|
|
import yfinance as yf
|
|
t = yf.Ticker(ticker.upper())
|
|
earnings = t.earnings_history
|
|
if earnings is None or (hasattr(earnings, 'empty') and earnings.empty):
|
|
return {"ticker": ticker.upper(), "history": []}
|
|
|
|
history: List[Dict[str, Any]] = []
|
|
if hasattr(earnings, 'iterrows'):
|
|
for idx, row in earnings.iterrows():
|
|
history.append({
|
|
"date": str(idx)[:10],
|
|
"eps_actual": _safe_float(row.get("epsActual", row.get("Reported EPS"))),
|
|
"eps_estimate": _safe_float(row.get("epsEstimate", row.get("EPS Estimate"))),
|
|
"surprise": round(_safe_float(row.get("surprisePercent", row.get("Surprise(%)")), 0) * 100, 2),
|
|
})
|
|
return {"ticker": ticker.upper(), "history": history[-12:]}
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Earnings history failed: {exc}") from exc
|
|
|
|
|
|
@router.get("/{ticker}/calendar", summary="Upcoming earnings date")
|
|
async def earnings_calendar(ticker: str) -> Dict[str, Any]:
|
|
try:
|
|
import yfinance as yf
|
|
t = yf.Ticker(ticker.upper())
|
|
cal = t.calendar
|
|
if cal is None:
|
|
return {"ticker": ticker.upper(), "next_earnings": None}
|
|
|
|
if isinstance(cal, dict):
|
|
earnings_date = cal.get("Earnings Date")
|
|
if isinstance(earnings_date, list) and earnings_date:
|
|
earnings_date = str(earnings_date[0])[:10]
|
|
elif earnings_date:
|
|
earnings_date = str(earnings_date)[:10]
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"next_earnings": earnings_date,
|
|
"revenue_estimate": _safe_float(cal.get("Revenue Average")),
|
|
"eps_estimate": _safe_float(cal.get("Earnings Average")),
|
|
}
|
|
return {"ticker": ticker.upper(), "next_earnings": None}
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Earnings calendar failed: {exc}") from exc
|
|
|
|
|
|
@router.get("/{ticker}/transcript", summary="Earnings call transcript (FMP)")
|
|
async def earnings_transcript(
|
|
ticker: str,
|
|
year: int = Query(..., ge=1990, le=2035),
|
|
quarter: int = Query(..., ge=1, le=4),
|
|
) -> Dict[str, Any]:
|
|
"""Return one quarter of earnings call text from FMP when ``FMP_API_KEY`` is set."""
|
|
from server.services.fmp_client import fetch_earning_call_transcript, fmp_is_configured
|
|
|
|
if not fmp_is_configured():
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"year": year,
|
|
"quarter": quarter,
|
|
"available": False,
|
|
"message": "Set FMP_API_KEY for earnings call transcripts.",
|
|
"content": None,
|
|
}
|
|
rows: Optional[List[Dict[str, Any]]] = await fetch_earning_call_transcript(
|
|
ticker, year, quarter
|
|
)
|
|
if not rows:
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"year": year,
|
|
"quarter": quarter,
|
|
"available": False,
|
|
"message": "No transcript returned (symbol/quarter or API limit).",
|
|
"content": None,
|
|
}
|
|
# FMP returns list of dicts with 'content' or similar
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"year": year,
|
|
"quarter": quarter,
|
|
"available": True,
|
|
"data": rows,
|
|
}
|
|
|
|
|
|
@router.get("/{ticker}/delta", summary="What changed vs last quarter")
|
|
async def earnings_delta(ticker: str) -> Dict[str, Any]:
|
|
"""Compare the two most recent quarters: revenue/earnings delta + AI summary."""
|
|
try:
|
|
import yfinance as yf
|
|
|
|
t = yf.Ticker(ticker.upper())
|
|
quarterly = t.quarterly_earnings
|
|
if quarterly is None or (hasattr(quarterly, "empty") and quarterly.empty) or len(quarterly) < 2:
|
|
return {"ticker": ticker.upper(), "available": False, "message": "Not enough quarterly data"}
|
|
|
|
rows = []
|
|
for idx, row in quarterly.iterrows():
|
|
rows.append({
|
|
"period": str(idx),
|
|
"revenue": _safe_float(row.get("Revenue")),
|
|
"earnings": _safe_float(row.get("Earnings")),
|
|
})
|
|
if len(rows) < 2:
|
|
return {"ticker": ticker.upper(), "available": False, "message": "Not enough quarterly data"}
|
|
|
|
latest, prev = rows[0], rows[1]
|
|
rev_delta = None
|
|
earn_delta = None
|
|
if latest["revenue"] and prev["revenue"] and prev["revenue"] != 0:
|
|
rev_delta = round((latest["revenue"] - prev["revenue"]) / abs(prev["revenue"]) * 100, 2)
|
|
if latest["earnings"] and prev["earnings"] and prev["earnings"] != 0:
|
|
earn_delta = round((latest["earnings"] - prev["earnings"]) / abs(prev["earnings"]) * 100, 2)
|
|
|
|
# EPS surprise trend from earnings_history
|
|
eh = t.earnings_history
|
|
eps_trend: List[Dict[str, Any]] = []
|
|
if eh is not None and hasattr(eh, "iterrows"):
|
|
for idx2, row2 in eh.iterrows():
|
|
eps_trend.append({
|
|
"date": str(idx2)[:10],
|
|
"surprise_pct": round(_safe_float(row2.get("surprisePercent", 0), 0) * 100, 2),
|
|
})
|
|
eps_trend = eps_trend[-4:]
|
|
|
|
# AI summary via Gemini (best-effort)
|
|
ai_summary: Optional[str] = None
|
|
try:
|
|
from server.services.gemini_service import generate_text
|
|
prompt = (
|
|
f"Compare {ticker.upper()} most recent two quarters.\n"
|
|
f"Latest quarter ({latest['period']}): Revenue ${latest['revenue']}, Earnings ${latest['earnings']}.\n"
|
|
f"Previous quarter ({prev['period']}): Revenue ${prev['revenue']}, Earnings ${prev['earnings']}.\n"
|
|
f"Revenue changed {rev_delta}%, Earnings changed {earn_delta}%.\n"
|
|
"In 2-3 sentences, explain what changed and why. Be concise and specific."
|
|
)
|
|
ai_summary = await generate_text(prompt)
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"available": True,
|
|
"latest_quarter": latest["period"],
|
|
"prev_quarter": prev["period"],
|
|
"latest_revenue": latest["revenue"],
|
|
"prev_revenue": prev["revenue"],
|
|
"latest_earnings": latest["earnings"],
|
|
"prev_earnings": prev["earnings"],
|
|
"revenue_delta_pct": rev_delta,
|
|
"earnings_delta_pct": earn_delta,
|
|
"eps_trend": eps_trend,
|
|
"ai_summary": ai_summary,
|
|
}
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Earnings delta failed: {exc}") from exc
|
|
|
|
|
|
@router.get("/{ticker}/transcript-delta", summary="Earnings call transcript phrase delta")
|
|
async def earnings_transcript_delta(
|
|
ticker: str,
|
|
year: Optional[int] = Query(None, ge=1990, le=2035),
|
|
quarter: Optional[int] = Query(None, ge=1, le=4),
|
|
prev_year: Optional[int] = Query(None, ge=1990, le=2035),
|
|
prev_quarter: Optional[int] = Query(None, ge=1, le=4),
|
|
) -> Dict[str, Any]:
|
|
"""Compare management language between two earnings-call transcripts."""
|
|
|
|
from server.services.earnings_transcripts import (
|
|
compute_delta,
|
|
default_quarter_pair,
|
|
fetch_transcript,
|
|
generate_delta_narrative,
|
|
)
|
|
from server.services.fmp_client import fmp_is_configured
|
|
|
|
if not fmp_is_configured():
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"available": False,
|
|
"message": "Set FMP_API_KEY for earnings call transcript delta.",
|
|
}
|
|
|
|
if year is None or quarter is None:
|
|
(year, quarter), (default_prev_year, default_prev_quarter) = default_quarter_pair()
|
|
prev_year = prev_year or default_prev_year
|
|
prev_quarter = prev_quarter or default_prev_quarter
|
|
if prev_year is None or prev_quarter is None:
|
|
prev_year = year if quarter > 1 else year - 1
|
|
prev_quarter = quarter - 1 if quarter > 1 else 4
|
|
|
|
current = await fetch_transcript(ticker, year, quarter)
|
|
previous = await fetch_transcript(ticker, prev_year, prev_quarter)
|
|
if current is None or previous is None:
|
|
return {
|
|
"ticker": ticker.upper(),
|
|
"available": False,
|
|
"message": "Transcript pair not available for the requested quarters.",
|
|
"current": {"year": year, "quarter": quarter},
|
|
"previous": {"year": prev_year, "quarter": prev_quarter},
|
|
}
|
|
|
|
delta = compute_delta(current, previous)
|
|
delta["narrative"] = await generate_delta_narrative(delta, ticker)
|
|
return delta
|
|
|
|
|
|
@router.get("/{ticker}/quarterly", summary="Quarterly earnings data")
|
|
async def quarterly_earnings(ticker: str) -> Dict[str, Any]:
|
|
try:
|
|
import yfinance as yf
|
|
t = yf.Ticker(ticker.upper())
|
|
|
|
quarterly = t.quarterly_earnings
|
|
data: List[Dict[str, Any]] = []
|
|
if quarterly is not None and hasattr(quarterly, 'iterrows'):
|
|
for idx, row in quarterly.iterrows():
|
|
data.append({
|
|
"period": str(idx),
|
|
"revenue": _safe_float(row.get("Revenue")),
|
|
"earnings": _safe_float(row.get("Earnings")),
|
|
})
|
|
|
|
return {"ticker": ticker.upper(), "quarterly": data}
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Quarterly earnings failed: {exc}") from exc
|