166 lines
6.6 KiB
Python
166 lines
6.6 KiB
Python
"""
|
|
akshare news adapter for TradingAgents.
|
|
Replaces yfinance_news for company and global news sourcing.
|
|
|
|
Uses:
|
|
- stock_news_em → individual stock news (East Money)
|
|
- news_cctv → CCTV daily news (macro/global)
|
|
- news_economic_baidu → economic calendar/events
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
import os
|
|
import pandas as pd
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
# Suppress akshare's internal tqdm progress bars
|
|
os.environ.setdefault('TQDM_DISABLE', '1')
|
|
|
|
import akshare as ak
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Symbol detection
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
def _normalize_ticker(ticker: str) -> str:
|
|
"""Normalize ticker for akshare news API.
|
|
|
|
stock_news_em accepts: A-share 6-digit codes, or US tickers as-is.
|
|
"""
|
|
sym = ticker.strip().upper()
|
|
if sym.isdigit() and len(sym) == 6:
|
|
return sym
|
|
return sym
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Company news
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
def get_news(
|
|
ticker: str,
|
|
start_date: str,
|
|
end_date: str,
|
|
) -> str:
|
|
"""Get company-specific news from East Money.
|
|
|
|
Args:
|
|
ticker: Stock symbol (A-share code or US ticker).
|
|
start_date: YYYY-MM-DD format.
|
|
end_date: YYYY-MM-DD format.
|
|
|
|
Returns:
|
|
Markdown string containing news items.
|
|
"""
|
|
try:
|
|
sym = _normalize_ticker(ticker)
|
|
df = ak.stock_news_em(symbol=sym)
|
|
|
|
# Filter by date range
|
|
df["发布时间"] = pd.to_datetime(df["发布时间"])
|
|
mask = (df["发布时间"] >= pd.Timestamp(start_date)) & (
|
|
df["发布时间"] <= pd.Timestamp(end_date) + pd.Timedelta(days=1)
|
|
)
|
|
df = df[mask]
|
|
|
|
if df.empty:
|
|
return f"No news found for {ticker} between {start_date} and {end_date}."
|
|
|
|
# Select and format key columns
|
|
cols = [c for c in ["发布时间", "新闻标题", "新闻内容", "文章来源"] if c in df.columns]
|
|
result = df[cols].head(30) # Limit to 30 most recent
|
|
result["发布时间"] = result["发布时间"].astype(str)
|
|
return result.to_markdown(index=False)
|
|
|
|
except Exception as e:
|
|
return f"[akshare] Error fetching news for {ticker}: {e}"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Global / macro news
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
def get_global_news(
|
|
curr_date: str,
|
|
look_back_days: int = 7,
|
|
limit: int = 20,
|
|
) -> str:
|
|
"""Get global macroeconomic news from multiple Chinese news sources.
|
|
|
|
Aggregates:
|
|
- CCTV daily news (political/economic headlines)
|
|
- Baidu economic calendar (economic events/data releases)
|
|
|
|
Args:
|
|
curr_date: Current reference date (YYYY-MM-DD).
|
|
look_back_days: Days to look back.
|
|
limit: Maximum total news items to return.
|
|
|
|
Returns:
|
|
Markdown string with global news and economic events.
|
|
"""
|
|
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
|
sections = []
|
|
|
|
# — CCTV news —
|
|
try:
|
|
cctv_parts = []
|
|
for i in range(look_back_days + 1):
|
|
day = curr_dt - relativedelta(days=i)
|
|
day_str = day.strftime("%Y%m%d")
|
|
try:
|
|
df_day = ak.news_cctv(date=day_str)
|
|
if df_day is not None and not df_day.empty:
|
|
for _, row in df_day.head(10).iterrows():
|
|
if len(cctv_parts) >= limit:
|
|
break
|
|
title = str(row.get("title", "")) if "title" in df_day.columns else ""
|
|
content = str(row.get("content", "")) if "content" in df_day.columns else ""
|
|
brief = content[:120] if content else ""
|
|
cctv_parts.append(f"- [{day_str}] **{title}** — {brief}")
|
|
if len(cctv_parts) >= limit:
|
|
break
|
|
except Exception:
|
|
continue # skip days with no data
|
|
|
|
if cctv_parts:
|
|
sections.append("## CCTV News (央视新闻)\n\n" + "\n".join(cctv_parts[:limit]))
|
|
except Exception as e:
|
|
sections.append(f"## CCTV News\n\n*Error fetching CCTV news: {e}*")
|
|
|
|
# — Economic calendar —
|
|
try:
|
|
econ_parts = []
|
|
for i in range(look_back_days + 1):
|
|
day = curr_dt - relativedelta(days=i)
|
|
day_str = day.strftime("%Y%m%d")
|
|
try:
|
|
df_econ = ak.news_economic_baidu(date=day_str)
|
|
if df_econ is not None and not df_econ.empty:
|
|
for _, row in df_econ.head(10).iterrows():
|
|
if len(econ_parts) >= limit:
|
|
break
|
|
time_val = str(row.get("time", ""))
|
|
country = str(row.get("country", "")) if "country" in df_econ.columns else ""
|
|
event = str(row.get("event", "")) if "event" in df_econ.columns else ""
|
|
importance = str(row.get("importance", "")) if "importance" in df_econ.columns else ""
|
|
line = f"- **{country}** {importance} — {event} ({time_val})"
|
|
econ_parts.append(line)
|
|
if len(econ_parts) >= limit:
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if econ_parts:
|
|
sections.append("## Economic Calendar (经济日历)\n\n" + "\n".join(econ_parts[:limit]))
|
|
except Exception as e:
|
|
sections.append(f"## Economic Calendar\n\n*Error fetching economic calendar: {e}*")
|
|
|
|
if not sections:
|
|
return f"No global news found for the period ending {curr_date}."
|
|
|
|
header = f"**Global news and economic events from {curr_dt - relativedelta(days=look_back_days):%Y-%m-%d} to {curr_date}**\n\n"
|
|
return header + "\n\n".join(sections)
|