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>
116 lines
3.0 KiB
Python
116 lines
3.0 KiB
Python
"""
|
||
信号生成工具函数。
|
||
|
||
因子值 → 交易信号的桥梁,纯函数无副作用。
|
||
"""
|
||
|
||
import pandas as pd
|
||
|
||
|
||
def factor_to_threshold_signal(
|
||
factor_series: pd.Series,
|
||
buy_threshold: float,
|
||
sell_threshold: float | None = None,
|
||
cross_direction: str = "up",
|
||
) -> pd.Series:
|
||
"""
|
||
因子阈值交叉信号。
|
||
|
||
参数:
|
||
factor_series: 因子值 Series
|
||
buy_threshold: 买入阈值(如 RSI < 30 则买)
|
||
sell_threshold: 卖出阈值(如 RSI > 70 则卖),None 表示平所有仓
|
||
cross_direction: 'up'=因子向上穿越阈值时触发, 'down'=向下穿越
|
||
|
||
返回:
|
||
信号 Series:1=买入, 0=平仓
|
||
"""
|
||
signals = pd.Series(0, index=factor_series.index)
|
||
|
||
if cross_direction == "down":
|
||
buys = factor_series < buy_threshold
|
||
else:
|
||
buys = factor_series > buy_threshold
|
||
|
||
signals[buys] = 1
|
||
|
||
if sell_threshold is not None:
|
||
if cross_direction == "down":
|
||
sells = factor_series > sell_threshold
|
||
else:
|
||
sells = factor_series < sell_threshold
|
||
signals[sells] = 0
|
||
|
||
# 过滤连续信号
|
||
signals = _filter_consecutive(signals)
|
||
|
||
return signals
|
||
|
||
|
||
def factor_to_quantile_signal(
|
||
factor_series: pd.Series,
|
||
top_quantile: float = 0.8,
|
||
bottom_quantile: float = 0.2,
|
||
) -> pd.Series:
|
||
"""
|
||
因子分位数信号 — 按滚动分位数判断。
|
||
|
||
参数:
|
||
factor_series: 因子值
|
||
top_quantile: 高于此分位买入
|
||
bottom_quantile: 低于此分位平仓
|
||
|
||
返回:
|
||
信号 Series
|
||
"""
|
||
top = factor_series.quantile(top_quantile)
|
||
bottom = factor_series.quantile(bottom_quantile)
|
||
|
||
signals = pd.Series(0, index=factor_series.index)
|
||
signals[factor_series > top] = 1
|
||
signals[factor_series < bottom] = 0
|
||
|
||
return _filter_consecutive(signals)
|
||
|
||
|
||
def cross_signal(
|
||
fast: pd.Series,
|
||
slow: pd.Series,
|
||
) -> pd.Series:
|
||
"""
|
||
金叉/死叉信号。
|
||
|
||
fast 上穿 slow → 买入(1)
|
||
fast 下穿 slow → 平仓(0)
|
||
"""
|
||
fast = fast.dropna()
|
||
slow = slow.dropna()
|
||
common_idx = fast.index.intersection(slow.index)
|
||
fast, slow = fast[common_idx], slow[common_idx]
|
||
|
||
signals = pd.Series(-1, index=common_idx)
|
||
above = (fast > slow).fillna(False)
|
||
above = above.infer_objects(copy=False)
|
||
# 交叉点:今天 above=True 且昨天 above=False → 金叉
|
||
prev = above.shift(1).fillna(False)
|
||
prev = prev.infer_objects(copy=False)
|
||
cross_up = above & ~prev
|
||
cross_down = ~above & prev
|
||
|
||
signals[cross_up] = 1
|
||
signals[cross_down] = 0
|
||
|
||
return _filter_consecutive(signals)
|
||
|
||
|
||
def _filter_consecutive(signals: pd.Series) -> pd.Series:
|
||
"""过滤连续相同信号,只保留首次出现的信号。"""
|
||
result = signals.copy()
|
||
prev = None
|
||
for i in range(len(result)):
|
||
if result.iloc[i] == prev:
|
||
result.iloc[i] = -1 # 标记为不操作
|
||
else:
|
||
prev = result.iloc[i]
|
||
return result[result != -1].reindex(signals.index).fillna(-1)
|