Files
myquant/finance/factors/sentiment/sentiment_factor.py
T
simonandClaude Opus 4.7 271a9343a5 Initial commit: cc-cursor 全链路量化研究平台
7 Sprints 全部完成:
  Sprint 0: 基础设施 (DataManager + MariaDB)
  Sprint 1: 因子引擎 (34因子/12分类)
  Sprint 2: VectorBT 回测 (5策略+截面)
  Sprint 3: Optuna 优化 (+Walk-Forward)
  Sprint 4: ML 模型 (LightGBM+CatBoost)
  Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐)
  Sprint 6: Agent 系统 (4Agent+日报.md/.html)

生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping,
  save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复,
  RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4,
  CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化,
  indexDatas API修正

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 15:59:05 +08:00

169 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
情绪因子。
将 Qwen 输出的情绪分数转换为量化因子值。
"""
import numpy as np
import pandas as pd
from factors.base import BaseFactor
class NewsSentimentFactor(BaseFactor):
"""
新闻情绪因子。
将多条新闻的情绪分数按时间加权聚合到每个交易日。
参数:
window: 滚动窗口(交易日)
decay: 指数衰减系数(越大衰减越快),0 表示等权
sentiment_df: 情绪分析结果 DataFrame
(date, sentiment_score, confidence, title, content)
"""
category = "sentiment"
def __init__(self, window: int = 5, decay: float = 0.3, sentiment_df: pd.DataFrame | None = None):
self.window = window
self.decay = decay
self.sentiment_df = sentiment_df
self.name = f"news_sent_{window}"
def calculate(self, df: pd.DataFrame) -> pd.Series:
if self.sentiment_df is None or self.sentiment_df.empty:
return pd.Series(float("nan"), index=df.index, name=self.name)
return _aggregate_sentiment(
df, self.sentiment_df, self.window, self.decay
)
def get_required_columns(self) -> list[str]:
return []
class SentimentMomentumFactor(BaseFactor):
"""
情绪动量因子。
当前情绪 - N 日前情绪,衡量情绪变化方向。
"""
category = "sentiment"
def __init__(self, period: int = 5, sentiment_df: pd.DataFrame | None = None):
self.period = period
self.sentiment_df = sentiment_df
self.name = f"sent_delta_{period}"
def calculate(self, df: pd.DataFrame) -> pd.Series:
base = NewsSentimentFactor(window=1, sentiment_df=self.sentiment_df).calculate(df)
return base.diff(self.period)
def get_required_columns(self) -> list[str]:
return []
class SentimentConfidenceFactor(BaseFactor):
"""
情绪置信度因子。
新闻情绪分析的置信度越高,因子绝对值越大(方向同 sentiment_score)。
sentiment_score × confidence → 高置信利好=正值大,高置信利空=负值大。
"""
category = "sentiment"
def __init__(self, window: int = 5, sentiment_df: pd.DataFrame | None = None):
self.window = window
self.sentiment_df = sentiment_df
self.name = f"news_conf_{window}"
def calculate(self, df: pd.DataFrame) -> pd.Series:
if self.sentiment_df is None or self.sentiment_df.empty:
return pd.Series(float("nan"), index=df.index, name=self.name)
sdf = self.sentiment_df.copy()
# 设置加权分数
if "confidence" in sdf.columns and "sentiment_score" in sdf.columns:
sdf["weighted_score"] = sdf["sentiment_score"] * sdf["confidence"]
else:
return pd.Series(float("nan"), index=df.index, name=self.name)
return _aggregate_sentiment(df, sdf, self.window, decay=0.3, score_col="weighted_score")
def get_required_columns(self) -> list[str]:
return []
# ── 情绪聚合工具函数 ──────────────────────────────────────
def _aggregate_sentiment(
daily_df: pd.DataFrame,
sentiment_df: pd.DataFrame,
window: int,
decay: float,
score_col: str = "sentiment_score",
) -> pd.Series:
"""
将情绪分数按时间加权聚合到交易日。
逻辑:
1. 对每个交易日 t,找到 [t - window + 1, t] 范围内的所有新闻
2. 按 time_decay = exp(-decay * days_from_t) 加权
3. 按 confidence(如有)加权
4. 返回加权平均情绪分数
参数:
daily_df: 日线 DataFrame(提供 index 和日期对齐)
sentiment_df: 情绪 DataFramedate 列 + score_col
window: 窗口大小
decay: 衰减系数
score_col: 情绪分数列名
"""
if sentiment_df.empty:
return pd.Series(float("nan"), index=daily_df.index, name=score_col)
# 统一日期格式
sdf = sentiment_df.copy()
sdf["date"] = pd.to_datetime(sdf["date"], format="%Y%m%d", errors="coerce")
sdf = sdf.dropna(subset=["date"])
sdf = sdf.sort_values("date")
daily_idx = pd.to_datetime(daily_df.index, format="%Y%m%d", errors="coerce")
if daily_idx.isna().all():
daily_idx = pd.to_datetime(daily_df.index)
result = pd.Series(float("nan"), index=daily_df.index)
# 对 news 日期建立搜索索引
news_dates = sdf["date"].values
for i, dt in enumerate(daily_idx):
if pd.isna(dt):
continue
# 窗口起始
window_start = dt - pd.Timedelta(days=window * 2) # 宽窗覆盖非交易日
mask = (news_dates >= window_start) & (news_dates <= dt)
candidates = sdf[mask]
if candidates.empty:
continue
# 时间衰减权重
days_diff = (dt - candidates["date"]).dt.days
time_weights = np.exp(-decay * days_diff)
# 置信度权重(如有)
conf_weights = candidates.get("confidence", pd.Series(1.0, index=candidates.index)).fillna(0.5)
scores = candidates[score_col].fillna(0.0)
total_weight = (time_weights * conf_weights).sum()
if total_weight > 0:
result.iloc[i] = (scores * time_weights * conf_weights).sum() / total_weight
result.name = score_col
return result.astype("float64")