Signed-off-by: TsssIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
TIANHE
2026-02-05 00:25:38 +08:00
parent c0738c09fe
commit af8e46011e
17 changed files with 2868 additions and 288 deletions
@@ -890,15 +890,16 @@ class MarketDataCollector:
"""
获取新闻和情绪数据
策略:
1. 使用结构化API (Finnhub) - 无需深度阅读
2. 只获取标题和摘要 - 不读取全文
3. 多来源聚合 - Finnhub + 市场特定来源
策略(按优先级)
1. 结构化API (Finnhub) - 美股首选
2. akshare 多源 - A股首选(东方财富/新浪/同花顺/雪球)
3. 搜索引擎 (Bocha/Tavily) - 补充搜索
4. 情绪分析 - Finnhub 社交媒体情绪
"""
news_list = []
sentiment = {}
# 1) Finnhub 新闻 (最可靠)
# === 1) Finnhub 新闻 (美股首选) ===
if self._finnhub_client:
try:
end_date = datetime.now().strftime('%Y-%m-%d')
@@ -908,29 +909,32 @@ class MarketDataCollector:
if market == 'USStock':
raw_news = self._finnhub_client.company_news(symbol, _from=start_date, to=end_date)
else:
# 通用新闻
elif market == 'Crypto':
# 加密货币通用新闻
raw_news = self._finnhub_client.general_news('crypto', min_id=0)
elif market not in ('AShare', 'HShare'):
# 其他市场通用新闻
raw_news = self._finnhub_client.general_news('general', min_id=0)
if raw_news:
for item in raw_news[:10]: # 最多10条
for item in raw_news[:10]:
if not item.get('headline'):
continue
news_list.append({
"datetime": datetime.fromtimestamp(item.get('datetime', 0)).strftime('%Y-%m-%d %H:%M'),
"headline": item.get('headline', ''),
"summary": item.get('summary', '')[:300] if item.get('summary') else '', # 截断摘要
"summary": item.get('summary', '')[:300] if item.get('summary') else '',
"source": item.get('source', 'Finnhub'),
"url": item.get('url', ''),
"sentiment": item.get('sentiment', 'neutral'), # Finnhub有时提供情绪
"sentiment": item.get('sentiment', 'neutral'),
})
logger.info(f"Finnhub 新闻获取成功: {len(news_list)}")
except Exception as e:
logger.debug(f"Finnhub news fetch failed: {e}")
# 2) Finnhub 情绪分数 (如果可用)
# === 2) Finnhub 情绪分数 (美股社交媒体情绪) ===
if self._finnhub_client and market == 'USStock':
try:
# Finnhub 提供社交媒体情绪
social = self._finnhub_client.stock_social_sentiment(symbol)
if social:
sentiment['reddit'] = social.get('reddit', {})
@@ -938,31 +942,183 @@ class MarketDataCollector:
except Exception as e:
logger.debug(f"Finnhub sentiment fetch failed: {e}")
# 3) A股特定新闻 (akshare)
# === 3) A股多源新闻 (akshare) ===
if market == 'AShare' and self._ak:
try:
# 个股新闻
df = self._ak.stock_news_em(symbol=symbol)
if df is not None and not df.empty:
for _, row in df.head(10).iterrows():
news_list.append({
"datetime": str(row.get('发布时间', ''))[:16],
"headline": row.get('新闻标题', ''),
"summary": row.get('新闻内容', '')[:200] if row.get('新闻内容') else '',
"source": row.get('文章来源', 'eastmoney'),
"url": row.get('新闻链接', ''),
"sentiment": 'neutral',
})
except Exception as e:
logger.debug(f"akshare news fetch failed: {e}")
ashare_news = self._get_ashare_news_multi_source(symbol)
news_list.extend(ashare_news)
# === 4) 港股新闻 (akshare) ===
if market == 'HShare' and self._ak:
hshare_news = self._get_hshare_news(symbol)
news_list.extend(hshare_news)
# === 5) 搜索引擎补充 (如果新闻太少) ===
if len(news_list) < 5:
search_news = self._get_news_from_search(market, symbol, company_name)
news_list.extend(search_news)
# 去重(按标题)
seen_titles = set()
unique_news = []
for item in news_list:
title = item.get('headline', '')
if title and title not in seen_titles:
seen_titles.add(title)
unique_news.append(item)
# 按时间排序
news_list.sort(key=lambda x: x.get('datetime', ''), reverse=True)
unique_news.sort(key=lambda x: x.get('datetime', ''), reverse=True)
return {
"news": news_list[:15], # 最多15条
"news": unique_news[:15], # 最多15条
"sentiment": sentiment,
}
def _get_ashare_news_multi_source(self, symbol: str) -> List[Dict[str, Any]]:
"""
A股多源新闻获取
来源(按优先级):
1. 东方财富个股新闻 (stock_news_em)
2. 新浪财经滚动新闻 (stock_news_sina)
3. 同花顺个股新闻 (stock_news_ths)
4. 雪球热帖 (stock_xuqiu)
"""
news_list = []
# 1) 东方财富个股新闻
try:
df = self._ak.stock_news_em(symbol=symbol)
if df is not None and not df.empty:
for _, row in df.head(8).iterrows():
news_list.append({
"datetime": str(row.get('发布时间', ''))[:16],
"headline": row.get('新闻标题', ''),
"summary": row.get('新闻内容', '')[:200] if row.get('新闻内容') else '',
"source": "东方财富",
"url": row.get('新闻链接', ''),
"sentiment": 'neutral',
})
logger.debug(f"东方财富新闻: {len(df)}")
except Exception as e:
logger.debug(f"东方财富新闻获取失败: {e}")
# 2) 新浪财经个股新闻
try:
# 注意:akshare 的新浪新闻接口可能需要股票名称而非代码
df = self._ak.stock_news_sina(symbol=symbol)
if df is not None and not df.empty:
for _, row in df.head(5).iterrows():
title = row.get('title', '') or row.get('新闻标题', '')
if title and title not in [n.get('headline') for n in news_list]:
news_list.append({
"datetime": str(row.get('time', row.get('发布时间', '')))[:16],
"headline": title,
"summary": row.get('content', row.get('新闻内容', ''))[:200] if row.get('content') or row.get('新闻内容') else '',
"source": "新浪财经",
"url": row.get('url', row.get('新闻链接', '')),
"sentiment": 'neutral',
})
logger.debug(f"新浪财经新闻: {len(df)}")
except Exception as e:
logger.debug(f"新浪财经新闻获取失败: {e}")
# 3) 同花顺个股新闻
try:
df = self._ak.stock_news_ths(symbol=symbol)
if df is not None and not df.empty:
for _, row in df.head(5).iterrows():
title = row.get('标题', '') or row.get('title', '')
if title and title not in [n.get('headline') for n in news_list]:
news_list.append({
"datetime": str(row.get('发布时间', row.get('time', '')))[:16],
"headline": title,
"summary": row.get('内容', row.get('content', ''))[:200] if row.get('内容') or row.get('content') else '',
"source": "同花顺",
"url": row.get('链接', row.get('url', '')),
"sentiment": 'neutral',
})
logger.debug(f"同花顺新闻: {len(df)}")
except Exception as e:
logger.debug(f"同花顺新闻获取失败: {e}")
# 4) 雪球热帖(社区讨论)
try:
df = self._ak.stock_xuqiu(symbol=symbol)
if df is not None and not df.empty:
for _, row in df.head(3).iterrows():
title = row.get('标题', '') or row.get('title', '')
if title and title not in [n.get('headline') for n in news_list]:
news_list.append({
"datetime": str(row.get('发布时间', row.get('time', '')))[:16],
"headline": title,
"summary": row.get('内容摘要', row.get('content', ''))[:200] if row.get('内容摘要') or row.get('content') else '',
"source": "雪球",
"url": row.get('链接', row.get('url', '')),
"sentiment": 'neutral',
})
logger.debug(f"雪球热帖: {len(df)}")
except Exception as e:
logger.debug(f"雪球热帖获取失败: {e}")
return news_list
def _get_hshare_news(self, symbol: str) -> List[Dict[str, Any]]:
"""港股新闻获取"""
news_list = []
try:
# 港股新闻 (如果 akshare 支持)
df = self._ak.stock_hk_spot_em()
# 港股一般没有专门的新闻接口,可以通过搜索补充
except Exception as e:
logger.debug(f"港股新闻获取失败: {e}")
return news_list
def _get_news_from_search(
self, market: str, symbol: str, company_name: str = None
) -> List[Dict[str, Any]]:
"""
从搜索引擎获取新闻
使用增强的搜索服务 (Bocha/Tavily/SerpAPI)
"""
news_list = []
try:
from app.services.search import get_search_service
search_service = get_search_service()
if not search_service.is_available:
return news_list
# 构建搜索名称
search_name = company_name or symbol
# 搜索股票新闻
response = search_service.search_stock_news(
stock_code=symbol,
stock_name=search_name,
market=market,
max_results=5
)
if response.success and response.results:
for result in response.results:
news_list.append({
"datetime": result.published_date or datetime.now().strftime('%Y-%m-%d'),
"headline": result.title,
"summary": result.snippet[:200] if result.snippet else '',
"source": f"搜索:{result.source}",
"url": result.url,
"sentiment": result.sentiment,
})
logger.info(f"搜索引擎新闻补充: {len(news_list)} 条 (来源: {response.provider})")
except Exception as e:
logger.debug(f"搜索引擎新闻获取失败: {e}")
return news_list
# 全局实例
File diff suppressed because it is too large Load Diff