/~learn/s/af7e0961
本文档用于设计一套 截面 ETF 选基策略引擎,将现有量化系统从"纵向择时"扩展到"横向选基"能力,每日全市场扫描场内 ETF,通过多因子打分 + 截面排名选出最优品种并分配权重。
构建 ETFSelectionEngine,以"日频全市场截面扫描 → 多因子评分 → TopN 精选 → 权重分配 → 叠加择时信号"为完整链路,实现风格轮动自适应,解决牛市行业热点切换频繁、人工选基跟不上节奏的问题。
当前系统在 StrategyAllocationManager 中固定配置了约 15 个品种的权重:
code_to_weight = {
'399102.SZ': 0.05,
'980092.CNI': 0.12,
'510880': 0.05,
'513180': 0.08,
'GLD': 0.15,
...
}
| 维度 | 原有方式(纵向) | 新方式(横向) |
| -- | ------------------- | ------------------- |
| 视角 | 单一品种的历史时序 | 全市场品种的截面比较 |
| 问题 | 何时买/卖这个品种 | 买哪个品种最好 |
| 数据 | 单个 DF 的时间序列 | 多品种同日期快照 |
| 策略 | generate\_signals() | rank\_and\_select() |
| 输出 | position (0/1) | portfolio (权重列表) |
Mermaid Live Edit: https://mermaid.live/edit#pako:eNp1U91u2jAYfRXk6_YFuJi0FugPP6UC7WJOL9xgIFISo9TRhqpK00YLlFLQqo5JUzuYNHXdtO5HYtPKVl4GO8lbzHHYGrXgK-ec830-Pp-zC1RSwCAKijp5opaRRSP5mGJGxNqxt0sWqpQjMUQRVAA__crbV-zbcwlEUqiKLQVsBWJ_PYTxfII1eu7gg3v8k3VebUUWFx9EliDvvU8612Pe_OXVO7zV9KnbsiWpWoa88dE7e8fGn5z9C35eY6PrqQibhTuOEkilxBKe2Js--9z1PQXQDFfLsn0MpomBTWobQck9QRw-Irpt4Dl0Am7aSNdodQ6_ArMWNrQZ7WOSX4UB4by99F4PQ3Q8oG-BxF1gJQzczyKnEguLKNwvNdY48KPwEc0szchiVbZam3rh_TqrH7Cj8DDWpGIdssM-P3vhjH6zbiPoHBKtS1FyOjF-_JJ123NnlcM6Vqkw6D1r8talNCghjZgzLCZl7xTMk0rG-X4jikJkSpJpKKx59bYw5e23Q3Ra0hnID0_dYc0Z1YT5ub7iT7FqUz853rxwB0e-sQCbbSwjm29AcQfe-zEZD1hHvPG-CCok2pCiLJyMTiZ_2s7VwOmGg8tKelPkf86HLffmhNVDjxwsAANbBtIK4n_c9WEF0DI2sAKiYlvARWTrVAGKuSekyKYkVzVVEKWWjReAXSkgimMaEtczQLSI9B2BWsQulf9_VZD5mBDjX0nJ8o_y93t_AQnShtA)
架构分为五个层面,每层职责清晰、接口明确:
Mermaid Live Edit: https://mermaid.live/edit#pako:eNp1UttqGkEYfpVlC72KcU26pVlKbmzNRW1Qd0mgay6mOqPCHmSzS5EQ0LaJmnqihpiShMY2oRJoAj1YayB9GWfcfYuOu2uxF87VP_Md_n--mR02pachK7BI0V-lssAwmWgiqTF0SaJMDvvjk2PcajAhXuC4LSYQWGXWYrLd-IWbR3ivhwev8enwqRTBlY7d7T1-aQRXQRqYYBFZWnoxpyF9y3Nbi7niSEzGv_t2t0beVakF6Vw-Gw__uDpy0xwNrqgX85ALLPEc3YyGF-S4TUm-ScQziYdl-0fPKTfJ5yL5eOmqcfXQ-XCBS6fjai1IfpbwQY8Uv4wGdbv_3VfHw94Iomxfd8fXHVK5cs4-4ZNz_LXlejzXVaiZlhrc0BVLhcG4BZScWZg2F125GJbJeRmX93HtiLnP2DdvcWXfp4heh8S67HmTxnvcqvtgYt3TR2VJz4foFTmGXnb87c4pVqf6qEvZlGRy9sYp16mzszfVb0ouKIlLsn3XxuXh6PaARkNqpdFtm3I81rZZUCAlMSinKMI9hNAy5GYh-hAeBEOIR3AWisyHaHa-4TLkEf-fSpwLieG5Y9A05kE0BX-MR4iHK7MQzWAeRHOZOqbgA5hiF1gVGirIpenv3pkwk6yZhSpMsgIt0xABSzGTbFLbpVRgmbpY0FKsYBoWXGCtPP3F8EkOZAygsgICyjY9NXQrk_23ywPtha6rU0nGmLSa1Lt_AcZESvU)
每日流程约需 3-5 分钟完成全市场扫描,关键路径说明:
Mermaid Live Edit: https://mermaid.live/edit#pako:eNqtVduK2zAQ_ZWgpywb_0AIgV523xZaUvpQAmIsj22xsiZIo4awzb9XjpJFbuylG9ZPks7ozGE8Z_QiFFUolkIZ8P6rhsZBt7Wz-J1OZg8_HjdoULEm-2AbbXH2kvD-u6-RVSvBGIlcS6M9z-8yuAzaVNJb2PmWeF4BYw67YKW_sA8uNshyR45rMpouyHFUGLlc0WoFpWcHitfrjC9lmQerf6PzuJhlUga0T9Sh5dBN0BeG6LkE9Sz32la09zflCIb1I_TcU2nqE5qxF3vUTcs35Uup_qNKFjrMtgqMCiaSvqNu17mKVKkbeX-SCR1-NOv3AEbz4aNpvzns9FgN3sXyxZH3m2QKMJ9jszF6xqFOf-6a7OjCLNtoQ3KHodHmnsHxYoa2uhsTJjtkp5UfNdsm9gpjc_hkDCnohT2BhWao6d6nKI1elgdZErgqQ6GKk-DMI5kSfmV6eM3gZU1uGBU1pcXIVCqKdT4T3oybqvCblyZr8M-t-OdXf4pibJJMRY7OgxQ8MSt6Qek0hZ1bbpB4Cs8tdY0OrHEND1pcLESHrgNdxRfk1AlbwS3GISKWcVlhDVH-VmztMYZCYNocrBJLdgEXIux6B5zfHLGswURXCEehaV93O7C_iLrLlcb1qfr18S_ReinW)
核心类说明:
| 类名 | 职责 |
| ------------------------- | -------------------------------------- |
| ETFSelectionEngine | 顶层入口,编排全流程(获取列表->拉数据->选基->输出) |
| ETFSelector | 选基策略抽象基类,定义 select(universe, date) 接口 |
| MultiFactorETFSelector | 多因子选基实现,持有 Factor 列表和权重 |
| Factor | 因子抽象基类,定义 calculate(universe, date) 接口 |
| CrossSectionalBacktester | 截面回测框架,逐日模拟选基 + 调仓 |
| StrategyAllocationManager | 已有类,扩展支持动态品种配置 |
| 数据源 | 接口 | 用途 |
| ------------------------------------------------ | ----------- | -------------- |
| adata.fund.info.all\_etf\_exchange\_traded\_info | 东方财富接口 | 获取全市场场内 ETF 列表 |
| stock\_today\_client.fund\_daily | tushare 代理 | 获取单 ETF 日 K 线 |
| adata.fund.market.get\_market\_etf | adata(降级备选) | 同花顺数据源日 K 线 |
| 基金ETF列表-场内.csv | 本地 CSV 静态备份 | 离线参考/快速测试 |
从全市场约 1500 只场内 ETF 中筛选出有效候选池:
| 筛选条件 | 标准 | 目的 |
| ----- | ---------------- | ------------------ |
| 上市天数 | >= 60 个交易日 | 剔除次新 ETF,确保有足够历史数据 |
| 日均成交额 | >= 500 万(近 20 日) | 剔除流动性不足的品种 |
| 基金类型 | 股票型 | 排除债券、货币、REITs、商品 |
| 投资类型 | 被动指数型 | 排除主动管理 ETF |
| 重复标的 | 同指数保留规模最大的一只 | 减少同质化竞争 |
经过筛选后,预计保留 200-300 只有效 ETF,覆盖以下主要板块:
| 参数 | 默认值 | 说明 |
| ------------- | --- | ------------- |
| window\_short | 20 | 短期收益率(约 1 个月) |
| window\_mid | 60 | 中期收益率(约 3 个月) |
| window\_long | 120 | 长期收益率(约 6 个月) |
| window\_250 | 250 | 年化收益率 |
计算方式:
def calculate(self, universe, date):
for etf in universe:
df = etf.get_history(end_date=date, window=max_windows)
etf['ret_20'] = df['close'].pct_change(20).iloc[-1]
etf['ret_60'] = df['close'].pct_change(60).iloc[-1]
etf['ret_120'] = df['close'].pct_change(120).iloc[-1]
etf['ret_250'] = (df['close'].iloc[-1] / df['close'].iloc[-250] - 1)
return universe
| 参数 | 默认值 | 说明 |
| ---------------- | --- | ------- |
| vol\_window | 20 | 量比计算窗口 |
| turnover\_window | 20 | 换手率计算窗口 |
核心指标:
| 参数 | 默认值 | 说明 |
| ----------- | --- | ------- |
| vol\_window | 60 | 波动率计算窗口 |
| dd\_window | 252 | 回撤计算窗口 |
核心指标:
| 数据源 | 说明 |
| -------------------- | ---------- |
| ETFPremiumCalculator | 已实现的折溢价计算器 |
核心指标:
每日输出一个 (N_etfs, N_factors) 的因子矩阵 DataFrame:fund_code
premium ret_20 ret_60 ret_120 ret_250 vol_ratio turnover volatility max_dd sharpe calmar
每行代表一只 ETF 在当前日期的所有因子值。
在截面(横截面)上对每个因子做标准化处理,消除量纲差异:
| 方法 | 公式 | 适用场景 |
| -------- | ---------------- | ------------ |
| Z-Score | (x - mean) / std | 因子值近似正态分布 |
| Rank Pct | rank(x) / N | 所有因子通用,鲁棒性好 |
| Sigmoid | 1 / (1 + e^-z) | 需要压缩到 0-1 区间 |
推荐默认使用 Rank Pct 标准化,不依赖分布假设,对极端值不敏感。
multi_factor_score = (
w1 * momentum_score +
w2 * volume_score +
w3 * quality_score +
w4 * premium_score
)
其中 momentum\_score 由多个窗口的动量因子合成:
momentum_score = (
0.4 * ret_20_rank +
0.3 * ret_60_rank +
0.2 * ret_120_rank +
0.1 * ret_250_rank
)
| 大类因子 | 默认权重 | 说明 |
| -------- | ---- | ----------- |
| Momentum | 0.50 | 动量是最核心的选基因子 |
| Volume | 0.20 | 成交量验证动量有效性 |
| Quality | 0.20 | 控制风险 |
| Premium | 0.10 | 折溢价辅助信号 |
按综合评分降序排列,输出全市场排名。
| 策略 | 说明 |
| ---------------- | ----------------------- |
| Top10 等权 | 选取排名前 10 的 ETF,均分权重 |
| Top10 评分加权 | 按评分比例分配权重 |
| Top5 + Top5 不同权重 | 前 5 名重仓,6-10 名轻仓 |
| 阈值截断 | 评分 > 某个阈值则入选(可能多于 10 只) |
| 规则 | 说明 |
| ---- | ---------------------- |
| 固定周期 | 每日/每周/每月调仓,简单可控 |
| 阈值触发 | 组合偏离度 > 5% 时调仓 |
| 成分变动 | 排名跌出前 15 或被新 ETF 取代时调仓 |
ETFSelectionEngine 输出:
[('510880', 0.20), ('513180', 0.15), ('159941', 0.15), ...]
动态注入 StrategyAllocationManager:
allocation_manager.add_strategy_to_board(
TimingStrategy(code='510880'),
['510880'],
weight=0.20
)
两种模式:
| 维度 | 传统择时回测 | 截面选基回测 |
| -- | ----------------- | --------------------- |
| 数据 | 单品种时间序列 | 多品种每日截面 |
| 信号 | signal / position | rank / selected\_list |
| 交易 | 买卖点 | 调仓(买入新入选 + 卖出落选) |
| 费用 | 单边手续费 | 双边手续费 + 滑点 |
| 基准 | 买入持有 | 等权全市场 / 基准指数 |
class CrossSectionalBacktester:
def __init__(self, selector, universe_history, rebalance_freq='weekly'):
self.selector = selector # ETFSelector 实例
self.universe = universe_history # 多品种历史数据字典
self.freq = rebalance_freq
def run(self, start_date, end_date):
# 逐日推进,在调仓日执行 select()
# 记录持仓变化,计算每日组合收益
# 输出:每日持仓明细 + 组合净值曲线
def calculate_metrics(self):
# 年化收益、最大回撤、夏普、换手率
| 指标 | 说明 |
| ----- | --------------------- |
| 年化收益率 | 组合 vs 基准 |
| 最大回撤 | 组合 vs 基准 |
| 信息比率 | 超额收益 / 跟踪误差 |
| 换手率 | 平均每期调仓比例(越低越好) |
| 胜率 | 调仓后跑赢基准的比例 |
| 平均持仓数 | 每期持有的 ETF 数量 |
| 行业集中度 | 持仓的行业分布 Herfindahl 指数 |
adata/aohan/quant/
├── selection/ # 新增:选基引擎
│ ├── __init__.py
│ ├── engine.py # ETFSelectionEngine 主入口
│ ├── selector/
│ │ ├── base.py # ETFSelector 抽象基类
│ │ ├── momentum_selector.py # 纯动量选基
│ │ └── multi_factor_selector.py # 多因子选基
│ ├── factor/
│ │ ├── base.py # Factor 抽象基类
│ │ ├── momentum_factor.py # 动量因子
│ │ ├── volume_factor.py # 量价因子
│ │ ├── quality_factor.py # 质量因子
│ │ └── premium_factor.py # 折溢价因子
│ ├── universe/
│ │ └── etf_pool.py # ETF 池管理 + 筛选
│ └── backtest/
│ └── cross_section_backtester.py # 截面回测框架
├── backtest/
│ └── strategy/ # 已有:策略(无变化)
├── chart_test/
│ ├── show_marker.py # 已有:扩展支持选基模式
│ └── allocation_utils.py # 已有:扩展动态品种
└── utils/
└── df_loader.py # 已有:无变化
show_marker.py
└── MarketAnalysis
└── ETFSelectionEngine (new)
├── etf_pool.py (new) → adata.fund.info
├── multi_factor_selector.py (new)
│ └── Factor 子类 (new)
└── cross_section_backtester.py (new)
StrategyAllocationManager (existing)
└── 新增: update_board(code, weight) 动态更新品种配置
| 任务 | 预估工时 |
| ------------------------------------------------- | ----- |
| etf\_pool.py:ETF 列表获取 + 筛选逻辑 | 1 天 |
| base.py:ETFSelector 抽象基类 + MultiFactorETFSelector | 0.5 天 |
| momentum\_factor.py + volume\_factor.py:动量因子 | 1 天 |
| engine.py:ETFSelectionEngine 主流程 | 1 天 |
| 纯选基模式回测:固定调仓频率 | 1 天 |
可交付物:每日输出 Top10 精选 ETF 列表 + 权重
| 任务 | 预估工时 |
| --------------------------------------- | ----- |
| cross\_section\_backtester.py:完整回测框架 | 2 天 |
| quality\_factor.py + premium\_factor.py | 1 天 |
| 因子权重调优(网格搜索或随机搜索) | 1 天 |
| 集成到 StrategyAllocationManager | 0.5 天 |
可交付物:选基策略回测报告 + 参数调优
| 任务 | 预估工时 |
| ------------------------------ | ----- |
| show\_marker.py 选基入口 | 0.5 天 |
| 定时任务集成 | 0.5 天 |
| 消息推送(Server酱) | 0.5 天 |
| 并行拉取性能优化(asyncio / ThreadPool) | 1 天 |
可交付物:每日自动推送选基结果
***
adata/aohan/quant/selection/ # 选基引擎根目录
├── __init__.py
├── engine.py # ETFSelectionEngine 主引擎
├── run_selection.py # 命令行运行入口
├── quick_test.py # 快速验证脚本
├── universe/
│ └── etf_pool.py # 精选 ETF 池(73只 × 7大类)
├── factor/
│ ├── __init__.py
│ ├── base.py # Factor 抽象基类(含 normalize)
│ ├── momentum_factor.py # 动量因子(20/60/120日收益率)
│ ├── volume_factor.py # 量价因子(量比 + 量价确认)
│ ├── quality_factor.py # 质量因子(夏普比率)
│ ├── mean_reversion_factor.py # 反追高因子(RSI超买回避 + 波动率稳定)
│ └── regime.py # 市场状态识别(牛/熊/震荡/高波动)
├── selector/
│ ├── __init__.py
│ ├── base.py # ETFSelector 抽象基类
│ └── multi_factor_selector.py # 多因子选基 + 纯动量 + 全池等权
└── backtest/
├── __init__.py
├── cross_section_backtester.py # 截面回测框架(净值/回撤/夏普)
└── historical_backtest.py # 三策略对比回测入口
| 模式 | 因子 | 权重 | 默认 | 适用场景 |
| ------------------------------------ | ---------------------------------------------------------- | ----------- | :----: | ------------ |
MultiFactorETFSelector.defensive() | Momentum(60/120) + Sharpe + OverboughtAvoid + VolStability | 25:15:35:25 | 默认 | 防御型,反追高,穿越牛熊 |
MultiFactorETFSelector.default() | Momentum(20/60/120) + Sharpe + VolPriceConfirm | 50:30:20 | - | 纯趋势跟踪,单边牛市 |
AllETFSelector() | - | - | - | 等权持有全池,对比基准 |
defensive 模式特点:
# 拉数据 + 选基 + 评分排名
./run_selection.sh
# 指定日期
./run_selection.sh -d 2025-01-15
# 回测模式
./run_selection.sh -b
# 跳过数据拉取(使用缓存)
./run_selection.sh -s
# 选基模式
PYTHONPATH=$(pwd) .venv/bin/python -m adata.aohan.quant.selection.run_selection select
# 回测模式
PYTHONPATH=$(pwd) .venv/bin/python -m adata.aohan.quant.selection.run_selection backtest --start 2024-01-01
# 历史回测对比(防御型 vs 纯动量 vs 等权持有 vs 沪深300)
PYTHONPATH=$(pwd) .venv/bin/python adata/aohan/quant/selection/backtest/historical_backtest.py
# 快速验证
PYTHONPATH=$(pwd) .venv/bin/python adata/aohan/quant/selection/quick_test.py
from adata.aohan.quant.selection.engine import ETFSelectionEngine
from adata.aohan.quant.selection.selector.multi_factor_selector import MultiFactorETFSelector
from adata.aohan.quant.selection.backtest.cross_section_backtester import CrossSectionalBacktester
# 默认防御型
engine = ETFSelectionEngine()
# 或用纯动量模式
engine = ETFSelectionEngine(selector=MultiFactorETFSelector.default())
# 拉取数据
engine.fetch_all_data(years_back=2)
# 选基评分
top_codes, ranked, result_df = engine.run_selection(top_n=10)
# 获取权重配置
portfolio = engine.get_portfolio(top_n=10, weight_method='score_weighted')
# 生成推送消息
msg = engine.generate_push_message()
# 回测
bt = CrossSectionalBacktester(engine, rebalance_freq='weekly')
results, metrics = bt.run(start_date='2024-01-01', end_date='2026-05-12', top_n=8)
运行 ./run_push.sh(不加 -c)时,截面引擎自动运行并插入推送消息最顶部。
消息推送.txt 结构:
📊 截面选基策略引擎 ← 新增!全市场扫描
📈 市场状态(牛/熊/震荡)
🔥 TOP 10 推荐 + 仓位分级
🧊 BOTTOM 10 回避品种
📋 评分加权配置方案(含缩舱后的实际比例)
📈 风格信号
💡 操作建议
──────────────────────────
📈 原有择时策略结果 ← 保持不变
各品种的多策略回测...
使用 ./run_push.sh -c 399102.SZ 指定代码时,截面引擎自动跳过,只跑择时策略。
📈 市场状态: 🟢 强势牛市 ← 市场处于什么阶段
价格vs年线: +8.5% ← 价格相对200日均线的偏离度
波动率比: 0.85x ← 当前波动率/历史波动率比,>1.5为高波动预警
建议总仓位上限: 95% ← RegimeDetector计算的安全仓位上限
🔥 TOP 10 最强品种
#1 512480.SH 半导体ETF 评分:0.911 行业主题 ● 重仓
#2 588000.SH 科创50ETF 评分:0.880 宽基指数 ● 重仓
...
评分 ≥ 0.85 = ● 重仓
评分 ≥ 0.70 = ○ 中仓
评分 < 0.70 = ◑ 轻仓
熊市/高波动时评分<0.6 显示"观望"
📋 Top 10 配置方案 (总仓位上限 95%) ← RegimeDetector算出的总仓位上限
代码 名称 评分 配置比例 缩减后
512480.SH 半导体ETF 0.911 11.1% 10.5% (中仓)
↑ ↑ 配置比例 × 仓位上限 = 实际建议
💡 操作建议
▶ 当前强势牛市,仓位上限 95%,积极参与强势板块
▶ 注意回避:REITs 3只垫底 Bottom 10
市场状态识别器,基于沪深300指数的三个指标:
输出:BULL / MILD_BULL / RANGING / HIGH_VOL / BEAR / SEVERE_BEAR 六种状态
RSI 超买回避因子:
波动率稳定性因子:60日波动窗口内计算 Sharpe Ratio 和稳定度,低波动高夏普的品种评分更高。
等权持有全池的 selector,用作对比基准,调仓时维持全池等权。
回测框架 CrossSectionalBacktester 按固定频率调仓,模拟真实交易执行。
| 频率 | 参数 | 触发日 | 说明 |
| --------- | -- | ------- | ------------------------------ |
weekly | 默认 | 每个周五收盘后 | 每周最后一个交易日执行一次 All in / All out |
daily | - | 每个交易日 | 高频调仓,但换手率高 |
monthly | - | 每月第一个周五 | 低频但反应慢 |
第 N 个周五收盘后:
1. build_snapshot(date) → 取当日所有 ETF 的最近 250 日数据
2. RegimeDetector.detect(沪深300数据) → 判断 BULL/MILD_BULL/RANGING/BEAR/...
3. selector.select(snapshot, top_n, regime) → 算出当日 TopN 品种
4. 计算调仓区间收益:
持仓组合收益 = mean(每只持仓ETF (本周五close - 上周五close) / 上周五close)
基准收益 = (沪深300本周五close - 上周五close) / 上周五close
等权池收益 = mean(全池每只ETF同样公式)
5. 更新净值: NAV = NAV * (1 + 持仓组合收益)
6. 卖出旧持仓 + 买入新持仓(交易成本暂忽略)
7. 记录: turnover = (新入选+被移除) / (2×top_n)
从你看到的输出:
2026-04-10: turnover=0.3 → 3只换出, 3只换入, 10只持仓, (3+3)/(2×10)=0.3
2026-05-08: turnover=0.1 → 1只调出, 1只调入, 很稳定
2026-04-24: turnover=0.4 → 4只换出, 4只换入, 风格切换较大
防御型平均换手率 28.86%,大概每周换 3 只;纯动量 36.79%,每周换 \~4 只。
1/N 仓位
运行回测时加 verbose=True 会输出每期调仓明细:
日期 Regime ↕换手率 组合NAV 基准NAV 持仓
------------------------------------------------------------------------------------------
2021-01-08 BULL 0.0% 1026450 1053717 [510300.SH, 511260.SH, 512100.SH ... +1]
2021-06-04 MILD_BULL 43.8% 1032592 1001513 [159915.SZ, 159928.SZ, 511260.SH ... +3]
2021-07-02 RANGING 12.5% 852891 967089 [...]
2022-10-28 SEVERE_BEAR 6.2% 760700 783276 [511260.SH, 518880.SH, 159941.SZ ...]
Regime 列表示当期市场状态,换手率表示调仓比例,NAV 是策略净值,基准NAV 是沪深300净值。通过这个日志可以观察到熊市中 Regime 从 BULL → RANGING → BEAR 的转换过程,以及策略如何降低换手率和调整持仓方向。
使用 tushare 真实数据接口,基于 73 只场内 ETF 池,每周五收盘后调仓,等权持有 Top8 品种。
| 指标 | 防御型选基 | 纯动量选基 | 等权全池 | 沪深300(买入持有) |
| ---------- | :---------: | :---------: | :-----: | :---------: |
| 总收益率 | -20.82% | -25.63% | -24.05% | -33.82% |
| 年化收益率 | -7.49% | -9.53% | -8.85% | -13.27% |
| 最大回撤 | -37.15% | -37.17% | -35.47% | -37.17% |
| 超额收益(vs基准) | +13.00% | +8.18% | +9.77% | - |
| 夏普比率 | -0.09 | -0.14 | -0.05 | - |
| 调仓期胜率 | 47.03% | 47.03% | 50.00% | - |
| 平均换手率 | 19.87% | 25.98% | - | - |
| 指标 | 防御型选基 | 纯动量选基 | 等权全池 | 沪深300(买入持有) |
| ---------- | :-----: | :---------: | :-----: | :---------: |
| 总收益率 | +38.85% | +56.33% | +52.25% | +41.84% |
| 年化收益率 | +14.91% | +20.83% | +19.48% | +15.95% |
| 最大回撤 | -13.17% | -11.27% | -9.55% | -12.19% |
| 超额收益(vs基准) | -2.99% | +14.49% | +10.41% | - |
| 夏普比率 | 0.30 | 0.34 | 1.21 | - |
| 调仓期胜率 | 56.56% | 57.38% | 57.14% | - |
| 平均换手率 | 29.88% | 35.77% | - | - |
| 指标 | 熊市(2021-23) | 牛市(2024-26) |
| ------- | :---------: | :---------: |
| 防御型总收益率 | -20.82% | +38.85% |
| 防御型年化 | -7.49% | +14.91% |
| 防御型最大回撤 | -37.15% | -13.17% |
| 防御型超额收益 | +13.00% | -2.99% |
| 沪深300基准 | -33.82% | +41.84% |
| 纯动量总收益率 | -25.63% | +56.33% |
| 纯动量超额收益 | +8.18% | +14.49% |
┌────────────────────────────────────────────┐
│ 策略选择 = f(市场状态) │
│ │
│ 熊市(2021) 防御型 -> -20% (基准则 -34%) │
│ 牛市(2024) 动量型 -> +56% (基准则 +42%) │
│ 震荡市 防御型 -> 动量衰减时抗跌 │
└────────────────────────────────────────────┘
***
选基引擎的"强势"不是简单的「谁涨得多谁排前面」,而是四个独立因子做 Rank 标准化后加权求和。
综合评分 = 0.25 × momentum_score
+ 0.15 × sharpe_score
+ 0.35 × overbought_avoid_score
+ 0.25 × volatility_stability_score
| 因子 | 权重 | 计算方式 | 倾向 |
| ------------------ | :-----: | ----------------------------- | ------------- |
momentum | 25% | 最近60日+120日的绝对收益率,Rank Pct 标准化 | 涨幅大的分高 |
sharpe | 15% | 近60日年化收益率 / 年化波动率 | 性价比高的分高 |
overbought_avoid | 35% | RSI超买 + 短期暴涨 → 低分;跌幅大 → 高分 | 惩罚上涨,奖励超跌 |
vol_stability | 25% | 近60日夏普+趋势稳定性 | 低波动稳定品种分高 |
以你看的那一期为例,以 511010.SH(国债ETF) 排第6名为例,逐因子解构:
511010.SH(国债ETF):
momentum (25%) = 近半年涨了约 +2~3%,Rank 约 0.35 → 大部分股票涨幅比它大
sharpe (15%) = 涨幅虽小但标准差极低,Sharpe ≈ 1.8 → Rank 约 0.95
overbought (35%) = RSI~50,绝对没超买 → 得分 0.6(中性)
vol_stability (25%)= 波动率接近0,稳定性极高 → 得分 0.95
综合 = 0.25×0.35 + 0.15×0.95 + 0.35×0.6 + 0.25×0.95 = 0.694
与此同时,512480.SH(半导体ETF) 可能:
512480.SH(半导体ETF):
momentum (25%) = 近半年涨了 +30%+ → Rank 0.95
sharpe (15%) = 高收益但高波动,Sharpe 约 0.6 → Rank 0.6
overbought (35%) = RSI~72 + 涨幅>25% → 得分 0.0(强烈回避!)
vol_stability (25%)= 波动率是国债50倍 → 得分 0.3
综合 = 0.25×0.95 + 0.15×0.6 + 0.35×0.0 + 0.25×0.3 = 0.403
结论:半导体在 OverboughtAvoid 上被完全打成了 0 分,因为这个因子占权重的 35%,它专门把涨太多的品种踢下去。国债反而因为"没超买"获得了高分。
这其实是设计上的一个平衡问题。当前防御型策略里,有两个因子各占超过 20%——OverboughtAvoid(35%) 和 VolStability(25%),合起来 60% 的权重都在"防御"上而不是"选强"上。
在牛市中,这就产生了矛盾——防御型策略看到涨多了的品种就降分,看到低波动的防御品种就加分,结果就是把国债推到了排行榜前面。
这个行为在熊市里是对的(熊市确实该持国债),但在当前温和牛市里属于"过度防御"。 RegimeDetector 已经识别出当前是 MILD\_BULL 并切换到中性权重,但 OverboughtAvoid 的权重仍然是 25%,对牛市品种的杀伤力依然不小。
现状不是 Bug,是防御型在当前市场状态下的自然表现——它认为追高风险 > 踏空风险,所以选择了更安全的品种。
> 已修复:_get_regime_weights 新增 BULL 分支。当前市场为牛市/温和牛市时,权重自动切换为 45:25:10:20(动量45%, Sharpe25%, 反追高10%, 稳定性20%),OverboughtAvoid 权重从 35% 降至 10%,极大减少国债上榜概率。震荡市保持 30:20:25:25,熊市/高波为 15:5:35:45。
Momentum、Sharpe、RSI、Volatility——所有这些因子的输入都来自于已经发生的历史价格数据。引擎并没有任何"预测"能力,它做的事情是:
观察过去60~180天的价格走势 → 归类哪些品种"历史表现好" → 下注这些品种会继续好
这个逻辑叫"动量延续"假设,在学术界有充分支撑(Jegadeesh and Titman, 1993; Moskowitz et al., 2012),但问题也很清楚:
用 2021 年的数据来模拟:2021 年 2 月(核心资产见顶前),动量因子会把消费、医药推到最前面。但 A 股从 2021 年 2 月到 2022 年 10 月跌了 2 年,期间动量策略会不断在"高点买入 → 新低止损 → 再买下一个反弹高点 → 再止损"中反复亏损。
这就是为什么防御型策略必需存在——OverboughtAvoid 和 VolStability 的存在不是为了选强,而是为了在动量崩溃前降低"追高"品种的仓位。
但这也带来了代价:防御型对真正的牛市突破反应迟钝。2024 年 9 月的 A 股暴力反弹中,防御型的反追高因子把初涨品种全部降权,错过了最早的一波收益。
高位站岗的根因不是「动量因子本身有问题」,而是我们用来衡量动量的维度太单一了。当前的 momentum 因子只看一件事:
momentum = (当前价格 - 60天前价格) / 60天前价格 ← 只问了"涨了多少"
学术研究和实战经验表明,应该同时问三个问题:
| 维度 | 问题 | 高位站岗时的表现 | 真牛市突破时的表现 |
| ------ | ------ | :---: | :---: |
| 涨了多少 (magnitude) | 过去N天涨了多少? | 涨幅大 | 涨幅大 |
| 涨得稳不稳 (quality) | 上涨过程中回撤小吗?涨跌日比例如何? | 波动大,经常大阴线 | 稳步推升,阴线小 |
| 涨了多久 (age) | 这波趋势已经走了多久?是否接近尾声? | 趋势已经走了6个月+ | 刚开始2-4周 |
高位站岗的品种通常是「涨幅大 + 波动剧烈 + 趋势已老」,而真正值得追的品种是「涨幅开始放大 + 波动收敛 + 趋势年轻」。
以下五种解法按实现成本从低到高排列,各自独立可叠加:
##### 解法 A:趋势年龄衰减(最轻量,0.3天)
原理:动量因子按趋势持续时间做衰减。趋势越老,动量得分越低。
young_trend_bonus = 1.0 # 趋势 < 4 周:全额动量
middle_trend = 0.8 # 趋势 4~8 周:八折
old_trend_penalty = 0.5 # 趋势 > 12 周:五折
final_momentum = raw_momentum × age_factor
判断趋势年龄的方式:记录该 ETF 的收盘价首次突破 60 日均线的时间,计算距今的周数。年龄越大 → 衰减越重 → 高位站岗的概率越低。
##### 解法 B:趋势质量过滤(推荐首实现)
原理:用 R²(线性拟合度)衡量过去N天的上涨是否为「稳步推升」而非「剧烈震荡后偶然收高」。
trend_quality = R²(close[t-N:t] 对 时间轴做线性回归)
smoothed_momentum = raw_momentum × max(trend_quality, 0.3)
| 场景 | raw_momentum | trend_quality | 最终得分 |
| ------ | :---: | :---: | :---: |
| 稳步推升型 | 0.8 | 0.95 | 0.76 |
| 高位剧烈震荡型 | 0.8 | 0.35 | 0.28 ← 被大幅降权 |
实现简单——numpy 的 np.polyfit 一行即可计算 R²。半导体 ETF 在 2021 年 7 月(高位剧烈震荡)的 R² 约为 0.3~0.4,而 2024 年 2 月(牛市初期稳步推升)的 R² 约为 0.85+。
##### 解法 C:距高点回撤惩罚
原理:如果该 ETF 当前价格比 120 日最高点回撤超过 15%,说明动量信号是「反弹」而非「趋势」,应当降权。
drawdown_from_peak = (当前价 - 120日最高价) / 120日最高价
dd_penalty = 1.0 if drawdown_from_peak > -5% else # 接近新高,正常
0.7 if drawdown_from_peak > -15% else # 回撤中,谨慎
0.3 # 深度回撤,忽略动量
final_momentum = raw_momentum × dd_penalty
这解决了「追反弹高位站岗」的问题——2022 年熊市中反复反弹的品种,距高点回撤都超过 -15%,会被大幅降权。
##### 解法 D:涨跌日比例确认
原理:计算过去 N 个交易日中阳线占比。真正的牛市趋势中阳线占比通常 > 55%,而高位震荡中阳线占比接近 50%。
up_day_ratio = count(close > open) / N
up_day_bonus = clip(up_day_ratio / 0.55, 0.5, 1.2) # 低于 55% 阳线率降权
这是最简单的趋势质量 proxy,不依赖任何统计模型。
##### 解法 E:多周期动量背离检测(最深度)
原理:当短期动量(20日)极高但中期动量(60日)已经在减速时,这是经典的动量背离信号——往往是趋势末期的特征。
M20 = 20日动量
M60 = 60日动量
M120 = 120日动量
if M20 > 0.95 and M60 < 0.5: # 短期暴涨但中期已经在减速
danger_signal = True
momentum_score *= 0.3 # 大幅降权,因为这是"末路狂奔"
这对应的是 2021 年春节前核心资产冲顶的特征:20 日动量排名极高(最后一波冲刺),但 60 日动量已经从前 3 名滑到前 10 名。
第一阶段(立即,约 1 天):
实现 解法 B(趋势质量 R²) + 解法 C(距高点回撤惩罚)
→ 两种解法互补:B 看走势形态,C 看绝对位置
→ 预期效果:高位站岗概率降低 50%+
第二阶段(后续):
实现 解法 A(趋势年龄衰减)
→ 配合 SignalTracker 的 streak 数据,天然适配
第三阶段(研究级):
实现 解法 E(多周期动量背离)
→ 需要历史回测验证参数,作为长期优化方向
> 实现状态:解法 B(趋势质量 R²)+ 解法 C(距高点回撤惩罚)已合入 MomentumFactor,通过 use_trend_quality=True 和 use_drawdown_penalty=True 启用。defensive/adaptive 模式默认开启。详见下方「实现记录」。
已完成:MomentumFactor 新增两个参数,在计算原始动量收益率后自动乘以趋势质量乘数和回撤惩罚因子。
final_momentum = raw_momentum × max(trend_r_squared, 0.3) × drawdown_penalty
> 通俗解释:动量乘数永远 ≤ 1.0,越接近 1.0 说明趋势越健康、动量被保留得越多。×0.8 =「稳步上涨,动量留八成」;×0.3 =「走势震荡,扣掉七成」;×0.09 =「高位烂趋势,几乎全扣」。这个乘数只打折不加杠杆——它防止追高,不会错杀真趋势。
_compute_trend_quality:对近 60 日归一化收盘价做线性回归,计算 R²。若斜率为负(下跌趋势),R² 取负值。截断到 [-1, 1]。_compute_drawdown_penalty:当前价 vs 120 日最高点。回撤 < 5% → 全额(1.0),5%~15% → 七折(0.7),> 15% → 三折(0.3)。实测效果(2026-05-24,73 只 ETF 截面):
| 品种 | 旧 rank | 新 rank | 变化 | 原因 |
| ------ | :---: | :---: | :---: | ------ |
| 508000.SH 张江REIT | 0.222 | 0.067 | ↓ 0.156 | 震荡盘整,R² 接近 0 |
| 515060.SH 房地产 | 0.156 | 0.022 | ↓ 0.133 | 距高点回撤 > 15% |
| 511010.SH 国债 | 0.533 | 0.622 | ↑ 0.089 | 趋势极其稳定,R² ≈ 0.95 |
| 563000.SH A500 | 0.756 | 0.844 | ↑ 0.089 | 稳步推升,R² 高 |
> 正常现象说明:国债因 R² 极高会在动量排名中上升,但其综合评分仍会被 OverboughtAvoid(反追高)拉回——趋势质量过滤主要影响的是「震荡型品种被错误归类为动量强势」的场景,而非「低波动品种排名过高」的问题(后者由因子权重和 RegimeDetector 负责)。
趋势质量过滤不是一个新的独立因子,而是在 MomentumFactor 内部对原始动量值做乘数修正,发生在因子标准化之前。
完整选基流水线 (每个调仓日执行一次)
═══════════════════════════════════════════════════════════════
45只ETF日K数据
│
├─→ MomentumFactor.calculate()
│ │
│ ├─ 1. 计算60日+120日加权收益率 = raw_momentum
│ ├─ 2. ★ use_trend_quality? → R² = 60日线性回归拟合度
│ │ raw_momentum ×= max(R², 0.3)
│ │ (R²<0.3 的震荡品种被打三折)
│ └─ 3. ★ use_drawdown_penalty? → dd_penalty
│ raw_momentum ×= dd_penalty
│ (距高点回撤<-15% 打三折,-5%~-15%打七折)
│ └─→ 输出: filtered_momentum ← 已经是过滤后的值
│
├─→ SharpeFactor.calculate() → 夏普比率
├─→ OverboughtAvoidFactor.calculate() → RSI超买回避
└─→ VolatilityStabilityFactor.calculate() → 波动稳定性
│
▼
4因子 Rank 标准化 (0~1)
│
▼
RegimeDetector 检测市场状态 (BULL/MILD_BULL/RANGING/BEAR...)
│
▼
AdaptiveWeightEngine 计算各因子权重
(BULL→动量45% 熊市→动量15%)
│
▼
综合评分 = Σ(因子分 × 权重)
│
▼
Top N 截面选基 + SignalTracker 时效标记
│
▼
推送消息输出
纯动量策略 防御型策略
┌──────────┐ ┌──────────┐
牛市(2024) │ +56% │ │ +39% │
│ 追得猛 │ │ 部分踏空 │
└──────────┘ └──────────┘
│ │
熊市(2021) ┌──┴───────┐ ┌──┴───────┐
│ -26% │ │ -21% │
│ 追高挨打 │ │ 回撤更小 │
└──────────┘ └──────────┘
调仓规则非常简单:每周五收盘后,根据当日全部 ETF 的截面评分重新排名,取 Top N,全部替换。
上周五持仓: [半导体, 科创50, 创业板, 纳指, 煤炭, 红利, 国债, A500]
本周五评分: [半导体 0.95, 纳指 0.85, 创新药 0.78, ... 国债 0.69, ... 红利 0.32]
→ 卖出: 科创50(掉出Top8) + 创业板(掉出) + 煤炭(掉出) + 红利(掉出) + A500(掉出)
→ 买入: 创新药(新进) + 新能源(新进) + ...
本周五新持仓: [半导体, 纳指, 创新药, 新能源, 碳中和, 黄金, 国债, 军工]
这意味着:
| 任务 | 说明 | 预估 |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---- |
| ✅ 因子动态权重 | ~~牛市自动切换为动量主导权重(50%+),熊市保持防御权重~~ 已实现 _get_regime_weights 离散切换 + AdaptiveWeightEngine 自适应融合 | ✅ |
| ✅ OverboughtAvoid 牛市减重 | ~~牛市应将 OverboughtAvoid 降至 10%,动量提到 40%~~ 已实现 BULL:45:25:10:20 | ✅ |
| 类债券品种排除开关 | 给 defensive() 增加 exclude\_bonds 参数,牛市模式自动排除国债/国开债,让选基在权益品种内做排名 | 0.5天 |
| ✅ 品种相关性约束 | ~~Herfindahl 集中度 + 同指数去重~~ 已实现 DiversificationFilter(同指数去重 + 类别上限 ≤4只/类),自动集成到 engine/backtester | ✅ |
| 交易成本 & 滑点模型 | 回测引入 ETF 手续费(万 0.5)+ 冲击成本(日均成交额的 1% 作为上限),让牛熊对比更真实 | 1天 |
| 分阶段回测 | 除 2021-2023 / 2024-2026 外,增加更多时间切片:2018熊市、2020疫情暴跌-反弹、2022单边下跌,验证策略稳定性 | 0.5天 |
| 🆕 SharpeFactor 计算公式修正 | 当前 total_return / (daily_std * sqrt(n/252)) 不标准,应改为标准公式 mean_return / std * sqrt(252)。这会系统性改变夏普因子排序 | 0.5天 |
| ✅ DataFetcher 并行化 | ~~串行改并行~~ 已实现 ThreadPoolExecutor(max_workers=6),16 只 ETF 从 6.7s→1.3s,5.3x 加速 | ✅ |
| 🆕 补齐动量因子 250 日窗口 | defensive/adaptive 模式只有 60/120 日动量,缺少 250 日(年化)窗口。长周期动量对区分「短期反弹」和「趋势反转」至关重要 | 0.5天 |
| 🆕 本地数据缓存层 | 引入 DataCache 类,API 拉取结果存为 Parquet。回测直接读本地,只在需要最新交易日数据时拉增量。避免每次重跑回测都从 tushare 拉数 | 1天 |
| 🆕 weight\_engine as\_factor\_weights 映射修复 | 当前 volume→momentum、vol_price_conf→overbought_avoid 为错误映射。应改为从 factor.name 精确匹配 | 0.5天 |
| 方向 | 说明 |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| 引入领先指标 | 用资金流(北向资金、融资余额变化)代替价格动量做先行选基,捕捉爆发前迹象而非爆发后追入 |
| 事件驱动截面 | 政策、业绩预告、行业轮动日历效应——如每年四季度倾向买消费,一季度买科技 |
| 多周期动量合成 | 同时考察 5日/10日/20日/60日/120日动量,短周期权重高时表示"动能强劲初期",长周期权重高时表示"趋势确立后期" |
| 行业中性化 | 对每个 ETF 的因子值做行业(大类)去均值处理,消除"当前热的是整个行业而非该 ETF"的噪声 |
| 极致中性 vs 极致动量 双翼策略 | 同时持有两个子组合:50%仓位用动量选 Top 5 科技成长类,50%仓位用低波动选 Top 5 防御类,按 Regime 动态调节比例 |
| 引入 XGBoost/LightGBM | 用机器学习做因子合成——当前是人工定权重,ML 可以从历史数据中学到最优因子权重和交互效应 |
| 🆕 ETF 池全市场动态化 | 从当前 48 只静态手选池升级为从 adata.fund.info.all_etf_exchange_traded_info 动态拉取全市场 1000+ ETF,经流动性筛选(上市>60天、日均成交>500万)后自动生成候选池。确保新上市 ETF 不会被遗漏 |
| 🆕 PremiumFactor 折溢价因子 | 设计方案中规划但未实现的因子。跨境 ETF 在 A 股交易时段折溢价可达 10%+,缺失该因子会在极端行情下推荐高溢价品种。需要先接入 IOPV 数据源(akshare 可能有) |
| 🆕 VolatilityStabilityFactor 改用 Sortino/Calmar | 当前公式 (stability×2 + sharpe×0.5)/3 中两个子项高度相关(分母相同),缺乏理论依据。改用 Sortino Ratio(只惩罚下行波动)或 Calmar Ratio(年化收益/最大回撤)更合理 |
| 🆕 RegimeDetector 多资产基准 | 当前固定用沪深300(510300.SH)判断全市场状态。但债券 ETF 的状态应由国债收益率曲线判断,商品 ETF 由美元/商品指数判断,跨境 ETF 由对应海外指数判断。不同大类资产应有各自的 Regime 基准 |
| 🆕 MomentumFactor 日期对齐 | 当前用 close[-w-1] 整数索引而非 trade\_date 对齐计算收益率。如数据中存在非交易日缺口,会导致窗口长度不准确。应改为按实际日期窗口计算 |
***
上一节分析了防御型策略在牛市中"过度防御"的问题——OverboughtAvoid(35%)和 VolStability(25%) 合占 60% 权重,导致国债等低波品种霸榜。解决思路不只是调一两个数字,而是构建一套能感知市场状态、平滑调节各因子权重的系统。
当前 _get_regime_weights 的实现:
| 状态 | momentum | sharpe | overbought | vol\_stability |
| ---------- | :------: | :----: | :--------: | :------------: |
| BULL | 45% | 25% | 10% | 20% |
| MILD\_BULL | 30% | 20% | 25% | 25% |
| RANGING | 30% | 20% | 25% | 25% |
| BEAR | 15% | 5% | 35% | 45% |
纯连续调节,不依赖离散状态:
momentum_weight = base_momentum / max(vol_ratio, 0.3)
defense_weight = 1.0 - momentum_weight - sharpe_weight
用趋势强度作为唯一的连续调节器:
trend_factor = clip(trend_strength × 3, 0.5, 1.5)
momentum_weight = base_momentum × trend_factor
三层连续融合,取三者之长:
Layer 1 (状态锚点): base_momentum = regime_lookup[regime]
Layer 2 (波动调节): vol_factor = clip(1.0 / max(vol_ratio, 0.3), 0.3, 1.0)
Layer 3 (趋势微调): trend_factor = clip(trend_strength × 3.0, 0.5, 1.5)
adjustment = vol_factor × trend_factor (取值范围 0.15 ~ 1.5)
momentum_weight = base_momentum × adjustment
BULL, vol_ratio=0.8, trend=0.25 → adjustment=1.0×0.75=0.75 → momentum=0.55×0.75=0.412 ← 牛市低波强趋势: 激进
BULL, vol_ratio=1.5, trend=0.10 → adjustment=0.667×0.5=0.333 → momentum=0.55×0.333=0.183 ← 牛市高波弱趋势: 保守(防御)
BEAR, vol_ratio=1.0, trend=0.05 → adjustment=1.0×0.5=0.5 → momentum=0.15×0.5=0.075 ← 熊市: 几乎不动量
BEAR, vol_ratio=2.0, trend=0.30 → adjustment=0.5×0.9=0.45 → momentum=0.15×0.45=0.068 ← 高波熊市: 极度保守
| 市场场景 | 离散切换(A) | 自适应融合(D) |
| ---------------- | :--------------: | :---------------: |
| BULL + 波动率正常 | 动量 45% | 动量 41% |
| BULL + 波动率 1.5x | 动量 45%(不感知波动) | 动量 18%(自动缩仓) |
| MILD\_BULL + 趋势强 | 动量 30%(和震荡共用一个档) | 动量 30%\~40%(趋势微调) |
| RANGING + 趋势强 | 动量 30% | 动量 27%\~40%(趋势推动) |
核心优势:方案 D 在同状态内可根据波动和趋势做更精细的调节,避免方案 A 的"一刀切"。
from adata.aohan.quant.selection.factor.weight_engine import AdaptiveWeightEngine
from adata.aohan.quant.selection.selector.multi_factor_selector import MultiFactorETFSelector
# 使用自适应权重
selector = MultiFactorETFSelector.adaptive()
engine = ETFSelectionEngine(selector=selector)
# 回测时自动启用自适应权重(run 内部调用 selector.select 时传入 regime_info)
bt = CrossSectionalBacktester(engine, rebalance_freq='weekly')
results, metrics = bt.run(start_date='2024-01-01', end_date='2026-05-12', top_n=8)
也可以通过 use_adaptive_weights=True 参数直接从构造函数启用:
selector = MultiFactorETFSelector(
factors=[MomentumFactor(windows=[60,120], weights=[0.5,0.5]),
SharpeFactor(), OverboughtAvoidFactor(), VolatilityStabilityFactor()],
weights=[0.25, 0.15, 0.35, 0.25],
use_adaptive_weights=True # 启用自适应
)
| 参数 | 当前值(已调优) | 作用 | 调优方向 |
| -------------------- | :------------------: | -------------------- | ------------------- |
vol_ratio 下限 | 0.2 | 波动率低于0.2倍历史时不再继续放大动量 | 调优后从0.3降至0.2 |
trend_factor 上下界 | 0.6 \~ 1.3 | 趋势强度的映射范围 | 调优后收窄到0.6~1.3 |
各状态的 base_momentum | BULL:0.60, BEAR:0.10 | 状态锚点的绝对动量权重 | 调优后BULL更激进、BEAR更防御 |
MultiFactorETFSelector.adaptive()generate_push_message() 输出 TOP 10 和 BOTTOM 5 的因子分解明细exclude_bonds 参数,BULL/MILD\_BULL 状态自动排除国债/国开债,防止低波品种在牛市中上榜np.mean(daily_returns) / np.std(daily_returns) * np.sqrt(252)volume→momentum、vol_price_conf→overbought_avoid 映射改为从 factor.name 精确匹配***
纯截面选基只看当天的因子排名,无法区分「一只 ETF 是因为真有资金持续加仓所以连续上榜」和「一只 ETF 因为短线爆冲刚好排进前十、下周就掉出」。没有历史维度的追踪,用户面对 TOP 10 榜单时无法判断哪些品种是真正在走趋势、哪些只是偶发性的假突破。
| streak | 图标 | 状态名 | 含义 |
| :-------: | :-: | ------ | ------------------------ |
| 1 | 🆕 | 新晋 | 本周首次进入 Top 10,可能是假突破,需观察 |
| 2 | 📈 | 第2周在榜 | 连续两周在榜,初步确认不是一日游 |
| 3 | 📈 | 第3周在榜 | 接近确认,再持续一周即为强势 |
| ≥4 | 🔥 | 连续N周强势 | 趋势确立,主力持续加仓,置信度高 |
| 曾上榜→掉出→再进 | ⚡ | 回榜 | 轮动快,不够稳定 |
| 上周在榜本周掉出 | 🔻 | 掉出榜单 | 信号衰减,建议评估减仓 |
SignalTracker 不只在内存中运行——每次 record() 后自动保存到本地小数据库文件 signal_streaks_db.json(与 signal\_tracker.py 同目录),下次创建 ETFSelectionEngine 时自动加载。
第1次 push_preview.py → streak=1 (🆕) → save → signal_streaks_db.json
第2次 push_preview.py → load → streak=2 (📈) → save
第3次 push_preview.py → load → streak=3 (📈) → save
第4次 push_preview.py → load → streak=4 (🔥) → save
> 重要:持久化文件按「日期」去重,同一日期多次调用 run_selection() 只会记录一次,避免重复累加。
📊 信号时效性
------------------------------------------------
📈 趋势确认(2-3周): 588000(2周), 159941(2周), 563000(2周)
🔻 本周掉出: 510880, 513050
🔥 TOP 10 最强品种
① 159949.SZ 创业板50ETF [0.804] 📈 第2周在榜
宽基指数 · 配置 10.6%
② 512480.SH 半导体ETF [0.752] 🆕 新晋
行业主题 · 配置 9.9%
③ 512100.SH 中证1000ETF [0.687] 🔥 连续4周强势
宽基指数 · 配置 9.1%
ETFSelectionEngine.__init__() 自动创建 SignalTracker()(默认 load_persisted=True),run_selection() 每次选基后自动调用 signal_tracker.record()。用户无需额外操作,每次跑推送脚本自动累积。
# 查看当前状态
engine.signal_tracker.summary()
# {'streaks': {'512480': 4, '159915': 2}, 'dropouts': ['510880'], ...}
# 手动清空历史重新开始
os.remove(os.path.join(os.path.dirname(__file__), 'signal_streaks_db.json'))
***
推送消息中除了 TOP 10 / BOTTOM 10 外,还会固定打印 10 个用户关注的 ETF 的综合评分和操作建议,不论这些 ETF 排名高低。这 10 个板块与择时策略 show_marker.py 中的 codes_to_process 保持一致。
| 代码 | 简称 | 说明 |
| ------ | ------ | ------ |
| 513180.SH | 恒生科技 | 跨境-港股科技 |
| 513330.SH | 恒生互联网 | 跨境-港股互联网 |
| 159908.SZ | 创业综指 | 宽基-创业板综 |
| 159915.SZ | 创业板 | 宽基-创业板指 |
| 159941.SZ | 纳指 | 跨境-纳斯达克100 |
| 159937.SZ | 黄金 | 商品-黄金 |
| 510210.SH | 上证指数 | 宽基-上证综指 |
| 159903.SZ | 深证成指 | 宽基-深证成指 |
| 512890.SH | 红利低波 | 策略-红利低波 |
| 159201.SZ | 自由现金流 | 策略-自由现金流 |
> 配置位置:ETFSelectionEngine.__init__() → self.WATCHLIST,修改列表即可增减关注板块。51 前缀的 ETF(如上证/恒生/红利低波)使用 DataFetcher 通过 tushare fund_daily 接口拉取,与 15/16 开头的 ETF 走同一数据通道。
AdaptiveWeightEngine 有 4 个连续超参数和 6 组状态 base weights,在方案 D(自适应融合)中共同决定了最终因子权重分配:
vol_ratio_lower — 波动率比的裁剪下限(默认 0.3)
vol_factor_lower — 波动率调节因子的裁剪下限(默认 0.3)
trend_factor_lower — 趋势强度映射下限(默认 0.5)
trend_factor_upper — 趋势强度映射上限(默认 1.5)
base_weights — 6 种市场状态下 momentum/sharpe/overbought/vol_stability 的基础配比
这些超参数当前基于经验设定(参考了学术论文和 A 股历史特征),但并不保证在回测数据上最优。需要通过双区间回测 + 网格搜索来量化评估,找到 Pareto 最优解。
目标:在宽范围内确定 vol\_ratio\_lower、vol\_factor\_lower、trend\_factor 上下界的大致最优区间。
vol_ratio_lower ∈ {0.2, 0.3, 0.4, 0.5}
vol_factor_lower ∈ {0.2, 0.3, 0.4}
trend_factor_lower ∈ {0.3, 0.4, 0.5, 0.6}
trend_factor_upper ∈ {1.3, 1.5, 1.7, 2.0}
每组参数同时跑熊市(2021-01 \~ 2023-12)和牛市(2024-01 \~ 2026-05)双区间回测,算出综合评分,选 Top 5。
目标:在 Phase 1 最佳连续参数基础上,调优三组状态的动量/防御配比。
| 状态组 | 变量 | 含义 |
| ----------------------------------- | ------------------- | ----------- |
| BULL(牛市) | momentum 0.45\~0.60 | 牛市应该给动量多大胆量 |
| MILD\_BULL + RANGING(震荡) | momentum 0.25\~0.35 | 震荡市均衡点在哪 |
| BEAR + HIGH\_VOL + SEVERE\_BEAR(熊市) | momentum 0.10\~0.20 | 熊市防御要多极致 |
其余三个因子权重(sharpe、overbought\_avoid、vol\_stability)按比例分配剩余权重。
将最佳参数组合与三个基准策略做最终对比:
| 基准 | 含义 |
| ----------------------- | ------------------------------------------------------------- |
| 离散切换(Discrete) | 原始 \_get\_regime\_weights 离散查表 |
| 默认自适应(Adaptive Default) | AdaptiveWeightEngine 当前默认值 |
| 纯动量(Momentum) | MomentumFactor + SharpeFactor + VolumePriceConfirmationFactor |
| 最佳参数(Best) | Phase 1 + 2 找到的最优组合 |
单一个指标无法全面衡量策略质量——熊市抗跌和牛市跟随是矛盾目标。使用加权评分:
综合评分 =
熊市超额收益 × 25% ← 熊市跑赢基准的能力(最核心)
+ 牛市总收益率 × 25% ← 牛市跟得上行情的能力
+ (熊市收益 - 熊市回撤) × 10% ← 熊市的收益-回撤性价比
+ (牛市收益 - 牛市回撤) × 15% ← 牛市的收益-回撤性价比
+ 熊市夏普 × 10% ← 熊市风险调整后收益
+ 牛市夏普 × 10% ← 牛市风险调整后收益
- 平均换手率 × 2.5% ← 惩罚过度交易(两区间平均)
权重设计的核心思想:
cd /Users/aohan/PycharmProjects/adata
PYTHONPATH=$(pwd) .venv/bin/python adata/aohan/quant/selection/backtest/weight_tuning.py
运行流程:
总耗时:首次运行约 10\~15 分钟(含数据拉取),缓存后约 3\~5 分钟。
不固定。 调优结果存在两个层面的时效性:
| 层面 | 频率 | 说明 |
| :----------------------- | :---------- | :----------------------------------------------------------------------------------------------------------- |
| Phase 1 连续参数 | 稳定,季度/半年度重检 | vol_ratio_lower 和 trend_factor 边界反映的是市场波动率的长期统计特征,变化慢 |
| Phase 2 base weights | 需随市场结构变化重检 | 各状态的权重配比取决于因子有效性——如果动量因子在某段时期的有效性大幅下降(如长期震荡市),BULL 状态的 momentum 基础权重可能需要下调。建议每 3\~6 个月或市场结构发生重大变化后重新跑一次调优脚本。 |
实践中建议:
***
> 以下条目来源于代码审查中发现的具体问题,与上一节 V2/V3 中规划的功能性需求不同。这些是当前代码中真实存在的 Bug、设计缺陷和优化机会。
当前代码:
sharpe = total_return / (daily_std * np.sqrt(len(daily_returns) / 252))
问题:
total_return 而非 mean_return——Total Return 是区间累计收益,不是单期平均收益np.sqrt(n / 252) 是降频因子,反向操作会导致数值错误np.mean(daily_returns) / np.std(daily_returns) * np.sqrt(252)影响:所有使用 SharpeFactor 的选基排名都会系统性偏离标准 Sharpe Ratio。由于当前 Sharpe 只占 15-25% 权重,影响可控但需修正。
***
构造函数声明了 max_workers=5,但 fetch_batch() 中从未实例化 ThreadPoolExecutor。实际是串行循环:
for i, code in enumerate(codes):
df = self.fetch_single(code, start_date, end_date) # 阻塞等待
if (i + 1) % 5 == 0:
time.sleep(0.5)
应改为:
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = {executor.submit(self.fetch_single, code, start, end): code for code in codes}
for future in as_completed(futures):
...
48 只 ETF 串行约 5 分钟,并行可压缩到 1 分钟以内。
***
文件: weight\_engine.py:L103-L112
key_map = {
'volume': 'momentum', # ❌ 量价确认 ≠ 动量
'vol_price_conf': 'overbought_avoid', # ❌ 量价确认 ≠ 反追高
}
当调用 as_factor_weights(result, list(factor_df.columns)) 时,如果因子名为 volume 或 vol_price_conf,会被错误映射。虽然当前 adaptive 模式只用 defensive 因子集(暂无 volume/vol\_price\_conf),但这是未来的炸弹。应改为精确匹配或抛出明确异常。
***
文件: momentum\_factor.py:L28-L31
ret = (close[-1] - close[-w - 1]) / close[-w - 1]
这里假设 DataFrame 按日期连续排列、无缺失。如果源数据中存在非交易日缺口(如长假、停牌),close[-w-1] 对应的实际日历天数可能远超 w 个交易日。应改为按 trade_date 列过滤后再计算。
***
| 底层指数 | 重复 ETF |
| ------- | -------------------- |
| 纳斯达克100 | 159941.SZ, 513100.SH |
| 黄金 | 518880.SH, 159937.SZ |
| 中证1000 | 512100.SH, 159845.SZ |
| 自由现金流 | 159399.SZ, 159201.SZ |
| 农业 | 159825.SZ (出现两次) |
当这些品种同时动量好时,会在 TopN 中占用多名额却不分散风险。需要增加 underlying_index 字段并在 select 后去重。
***
文件: mean\_reversion\_factor.py:L127-L129
stability = abs(avg_return) / vol # = |mean| / σ
score = min(1.0, max(0.0, (stability * 2 + sharpe * 0.5) / 3))
stability 和 sharpe 都除以 vol(标准差),高度相关,组合权重 2:0.5 ad-hoc。建议调研 Sortino Ratio(只惩罚下行波动率)或 Calmar Ratio(收益/最大回撤)作为替代。
***
设计方案规划的集成路径是 ETFSelectionEngine → StrategyAllocationManager,但实际实现中选基引擎完全独立运行。当前需要通过 run_push.sh 将选基结果和择时结果分别生成后拼接。未实现:
StrategyAllocationManager***
整个 selection/ 包(18 个 Python 文件)没有任何单元测试。关键逻辑(因子计算、权重调节、回测净值)缺乏回归保护。建议至少覆盖:
***
设计方案中规划了但从未在代码中引用或预留接口的模块:
PremiumFactor —— 折溢价因子类不存在premium_factor.py —— 文件不存在momentum_selector.py —— 只实现了 MomentumETFSelector(纯动量单因子),而非多窗口动量选基StrategyAllocationManager.update_board() —— 不存在动态更新方法建议定期对比设计文档与代码,清理已废弃的路径描述或新增实现说明。
***
> 背景:当前系统存在两条独立信号链——截面选基引擎(中长期配置,输出仓位上限和品种选择)和择时策略层(中短期交易,33+子策略加权合成每日建仓建议)。但两个信号链之间没有互锁机制,且策略输出到人工执行之间存在未定义的地带——操作者需要凭感觉解读信号并决定买卖。这导致两个典型错误模式:(1) 把择时仓位的短期下降误解成悲观信号而恐慌清仓;(2) 不信任新开发的截面引擎而不敢按上限配置。本节解决"系统有信号,人不知道怎么执行"的断层问题。
择时策略输出的百分比(如 59.77%)的含义是:
> 该品种下 N 个子策略中,加权平均后有 59.77% 的子策略当前看多。
格式说明(以创业板 33 策略为例):
[399102.SZ 创业板 (5.0%) :] ← 品种级权重 5%
策略总数: 33
2026-05-25 59.77% 仓位: 2.99%(预测值-量价平开) ← 择时仓位 59.77% × 5% = 2.99%
2026-05-22 82.76% 仓位: 4.14% [当日操作建议,加仓: 0.11%]
仓位-信号映射表(替代凭感觉看数字):
| 择时仓位区间 | 含义 | 正确操作 |
| ------------ | ------ | --------- |
| >80% | 极强共识做多 | 正常满配(按截面上限) |
| 60-80% | 多数策略看多,分歧可控 | 正常配置,关注分歧加大的子策略 |
| 40-60% | 多空分歧加大 | 配置但不加仓,检查分歧来源 |
| 20-40% | 看多策略已成少数 | 减仓至该比例附近,保留底仓观察 |
| <20% | 极少数策略看多 | 大幅减仓,但不清零(防踏空) |
核心原则:
两条信号链的时间维度和职责不同:
| 维度 | 截面选基引擎 | 择时策略层 |
| ------ | ----------- | ---------- |
| 时间尺度 | 周度(下次调仓日) | 每日 |
| 职责 | 判断能不能买(仓位上限)+ 买什么(选品种) | 判断现在该不该在场(每日建仓建议) |
| 信号性质 | 中长期趋势判断 | 短期多空共识度量 |
互锁规则:
最终执行仓位 = min(截面上限, 择时仓位建议) × 品种权重
截面「BULL 96%上限」+ 择时「59.77%」
→ 执行仓位 = min(96%, 59.77%) × 5% = 2.99% ← 正确,已自动执行
截面「BULL 96%上限」+ 择时「<20%」
→ 执行仓位 = min(96%, <20%) × 5% = <1%
→ 大幅减仓但保留底仓,等择时回暖
不要在截面上限很高时绕过择时大幅加仓,也不要在择时短期波动时忽略截面的牛熊判断而清仓。
当前问题:操作者看到回撤 → 恐慌 → 直接清零。需要用事前约定的分级减仓规则替代情绪决策。
当前仓位 X%
│
├─ 触发条件 A:择时仓位跨区间下降 ≥ 两档(如 80%+ → 40-60%)
│ → 减至 max(X% × 0.5, 截面上限 × 0.5)
│ → 冷却期:至少等 2 个交易日才能再次减仓
│
├─ 触发条件 B:截面牛熊降级(如 BULL → RANGING)
│ → 仓位重置为新的截面上限
│ → 例:96%上限 → 60%上限,立即执行
│
├─ 触发条件 C:单日回撤 > 预设最大回撤容忍度(推荐 3-5%)
│ → 减 20% 仓位(非清零)
│ → 次日择时回暖则补回,否则继续观察
│
└─ 触发条件 D:截面降级 + 择时 < 20%(双重确认)
→ 可清仓至 10% 底仓
→ 除非截面 = SEVERE_BEAR,否则永不归零
关键设计原则:
截面选基引擎仅运行 1-2 周,不信任是理性的。应设计验证流程让系统自己证明自己:
阶段 1:影子跟踪期(2-4 周)
- 生成信号但只记录不执行
- 每日对比「截面选出的 Top ETF」vs「基准收益率」
- 验证指标:选出品种是否跑赢等权基准
阶段 2:小仓位验证期(4-8 周)
- 按截面上限的 30% 执行(如 96% → 实际配 ~29%)
- 继续追踪信号准确性
- 验证指标:实际收益是否在回测预期区间内
阶段 3:正常执行期
- 按截面上限的 70-80% 执行
- 永远不打满 100%(留 20% 安全边际 = 有弹药应对黑天鹅 + 心理舒适区)
- 每月复盘截面预测 vs 实际表现
为什么永远留 20% 安全边际?
当前截面选出的品种(科技/半导体/创业板/纳指)在特定市场环境下可能高度相关,导致单日集中回撤。
待实现:
ETFSelectionEngine 中增加 correlation_matrix 计算(日收益滚动 60 日相关性矩阵)diversification.py 中实现基于相关性的仓位分散化调整
当前推送消息缺少一个给操作者看的简明行动指南。建议在推送末尾增加:
📋 今日执行建议
--------------------------------------------------------------------------------
截面状态: 🟢 强势牛市 | 仓位上限于 96%
择时共识: 加权平均仓位 65%(跨品种均值)
相关性: 待计算
→ 建议执行仓位: min(96%, 65%) = 65%(不高于截面,跟随择时)
今日操作:
✅ 维持现有仓位,无需调整
⚠️ 若择时单日下降 > 20 个百分点,触发条件 A,减仓 50%
🚫 禁止操作: 盘中恐慌清仓、因单一品种下跌而全盘抛售
| 陷阱 | 表现 | 纠正 |
| ------ | ------ | ------ |
| 锚定近期收益高点 | 收益新高后患得患失,任何回撤都难以接受 | 回撤是持有风险资产的「门票」,没有门票就没有收益 |
| 用短期噪音覆盖长期信号 | 看到一日择时下降就否定截面的牛市判断 | 截面看周线,择时看日线,日线波动不应推翻周线趋势 |
| 亏损厌恶 | 宁愿错过反弹也不愿承受回撤 | 但「错过反弹」也是亏损——机会成本亏得更隐蔽 |
| 不信任新系统 | 截面只跑了 1-2 周就不敢用 | 正确,但不该因此回到情绪决策。应进入「阶段 1:影子跟踪期」——让系统用数据自证,而非用情绪投票 |
持续承担风险获取收益的唯一方法:不是变得「更勇敢」,而是让系统替你承担决策压力。当回撤发生时,你看的不是账户数字,而是规则表——「条件 C 触发了吗?触发了,减 20%。没触发,不动。」规则执行不需要勇气,只需要纪律。
***
> 以下条目为系统待实现项,与本节分析直接相关。
ETFSelectionEngine 输出动态注入 StrategyAllocationManager,实现「选基 × 择时」双轮驱动execution_safety_margin 参数(默认 0.8),所有截面上限 × 此系数后才是实际执行上限