- 为避免不同设备、不同 agent 把
Rotation与博主完整体系混为一谈, 已将统一口径显式写入:experiments/rotation-next-phase.mdexperiments/target-strategy-evolution.mdproject-status.md
- 当前统一约定:
Rotation = 候选子策略- “对标”默认指向博主早期公开的日频截面基线
- 博主当前多策略
rule-based体系属于系统级长期目标
experiments/rotation-next-phase.md已进一步整理为 agent 入口页风格:- 新增
当前活跃路线 - 新增
已收口 / 后置路线 - 新增
历史归档说明 - 历史结论统一降级为按日期归档, 避免新 agent 把旧实验误判成当前主线
- 新增
project-status.md已同步补充:- 当前活跃路线
- 已收口路线
Rotationnotebook 已按“分析层 / 训练层 / 清单层”开始拆分:notebooks/rotation_factor_lab.py: 独立分析 notebook, 专门负责- 因子 IC
- 分组汇总
- Alpha decay
- Alpha158 top1 / 强子集筛选
manifests/rotation_feature_sets.py: Python manifest, 作为训练层唯一稳定特征集来源notebooks/cross_section_rotation.py: 收敛为训练入口 notebook
cross_section_rotation.py当前改动:- 不再依赖
Cell 3现场产出的alpha158_top1_factor_cols / core_factors / factors_keep - 训练入口改为直接读取
FEATURE_SET - 当前默认主线改为
core_plus_alpha158_kbar_shape
- 不再依赖
- manifest 当前已显式区分:
active:core_12,core_plus_alpha158_kbar_shapearchived / experimental:all_rotation,alpha158_kbar_shape,all_plus_alpha158_kbar_shape等analysis-only:core_plus_alpha158_top1,pruned_rotation
- 元数据兼容约束保持不变:
rotation_train_meta结构未改键名Cell 6b仍通过export_meta = {**rotation_train_meta, ...}导出utils/signal_export.py未改 artifact metadata 消费契约
- 重新审视小红书博主公开帖、评论区与私信截图后确认:
- 她并非一直在做截面多因子排序
- 早期存在明确的
128日日 K 截面多因子阶段 - 后期已转向
rule-based / trigger-based的多策略组合 - 当前公开可见体系更接近
12个子策略 + 市场状态切换 + 1 分钟级 T+0
- 额外确认:
- 纯量价策略大约只有
3个 - 其他策略混合基本面 / 另类数据
- “不是不做动量, 是不做截面排序”, 仍有策略保留动量内核, 只是
trigger改变
- 纯量价策略大约只有
- 新建
experiments/target-strategy-evolution.md:- 保留旧 benchmark 的全部关键信息
- 按“早期日频基线 / 后期多策略体系”重新整理
- 明确
Rotation现在只应对标其一个子策略层面的能力
- 原
experiments/rotation-benchmark.md废弃:- 原问题不是数据失真, 而是把不同阶段的信息混写成了一个静态 benchmark
- 后续统一以
target-strategy-evolution.md为准
- 验证组合:
LABEL = fwd_ret_1dFEATURE_MODE = core_plus_alpha158_top1ALPHA158_ANALYSIS_GROUP_MODE = all- 总特征数 =
21(core_12 + 9个 Alpha158 各组top1)
Cell 7诊断结果:Target IC Mean = +0.0322ICIR = +0.2987L/S Sharpe = 1.36Top-20日均双边换手约121.4%
- Rust 回测结果:
Gross Return = +10.40%Total Return = -49.33%Max Drawdown = 50.38%Avg Trades/Day = 7.0- 总成本
298,619
- 阶段结论:
- “每组保留 1 个”不适合作为主训练入口的默认规则
- 弱组
top1会稀释kbar_shape这类强增量组, 同时放大高换手噪声 core_plus_alpha158_top1暂时退出主线, 当前 Alpha158 主线仍是core_plus_alpha158(kbar_shape)
notebooks/cross_section_rotation.py新增FEATURE_MODE = "core_plus_alpha158_top1":- 训练特征 = 冻结
core_12+Cell 3产出的alpha158_top1_factor_cols - 不再手工拷贝各组
top1因子名到训练面板
- 训练特征 = 冻结
- 新模式设计为显式依赖
Cell 3:- 若
alpha158_top1_factor_cols为空,Cell 6会直接报错并提示先运行Cell 3 marimo现在会自动保证Alpha158 top1结果先于训练入口准备完成
- 若
- 训练元数据已补充:
alpha158_analysis_group_modealpha158_top1_factors
- 新增
utils/factor_analysis.py,下沉 notebook 原先散落的公共分析逻辑:IC summary汇总表- 分组汇总表
Alpha158每组top1因子提取- 通用
Alpha Decay计算
notebooks/cross_section_rotation.py的Cell 3系列已开始按“thin notebook”方向重构:Cell 3现在统一产出:rotation_ic_summary / rotation_ic_resultsalpha158_ic_summary / alpha158_ic_resultsdf_alpha158_group_summarydf_alpha158_top1alpha158_top1_factor_cols
- 原
Cell 3aa的 Alpha158 分组汇总已并回主分析面板, 不再单独散落 Cell 3b不再写死依赖 RotationTop-15, 现支持:rotationalpha158_top1custom_list
- 新增配置口径:
ALPHA158_ANALYSIS_GROUP_MODEALPHA_DECAY_SOURCEALPHA_DECAY_CUSTOM_FACTORS
- 训练与分析已开始解耦:
ALPHA158_GROUP_MODE继续服务训练特征选择ALPHA158_ANALYSIS_GROUP_MODE可单独控制分析侧要覆盖哪些分组
Cell 3c / 3d已降级:3c变为可选诊断工具, 默认不跑3d退出主流程, 默认直接回落到冻结core_12
- 当前冻结
core_12已显式写入 notebook 配置, 不再依赖每次运行时现场重筛
- 新增
utils/alpha158_factors.py:- 按
Qlib Alpha158默认配置复刻全部158个因子 - 默认包含:
kbar9 因子OPEN/HIGH/LOW/VWAP的window=0价格因子29类 rolling 算子 ×5/10/20/30/60窗口
- 按
- 当前实现口径:
- 基于本地
open_adj/high_adj/low_adj/close_adj/vwap_adj/volume - 价格类统一使用复权价
- 成交量类沿用本地
volume序列
- 基于本地
- 已完成冒烟验证:
- 因子总数确认 =
158 - 可与
Rotation原有46因子合并后一起走cross_section_normalize
- 因子总数确认 =
- 后续性能优化已完成:
RANK / BETA / RSQR / RESI / IMAX / IMIN / IMXD已全部从rolling_map改为 Polars 原生表达式- 现改用:
rolling_rankrolling_cov / rolling_varshift + max_horizontal
- 数值校验:
- 新旧实现对比后, 上述重因子最大误差仅为浮点噪声级别
- 不再依赖 Python
numpy回调逐窗口执行
notebooks/cross_section_rotation.py现已在 Cell 2 同时计算:- 原
Rotation因子 Alpha158因子
- 原
- Cell 6 新增
FEATURE_MODE选项:alpha158core_plus_alpha158all_plus_alpha158
- 当前设计选择:
- 因子分析 Cell 3/3d 仍以原
Rotation因子为主 Alpha158先作为训练特征集接入, 先验证是否提升最终回测
- 因子分析 Cell 3/3d 仍以原
FEATURE_MODE已前移到 Cell 1, 数据构建阶段即可感知当前实验模式- Cell 2 现按
FEATURE_MODE懒计算因子:alpha158模式: 跳过全部Rotation因子计算all / pruned / core模式: 跳过Alpha158因子计算core_plus_alpha158 / all_plus_alpha158模式: 同时计算两套因子
- Cell 2 最终仅
collect当前模式实际需要的特征列, 不再无差别落盘全部因子 Cell 3/3a/3b/3c/3d在alpha158模式下会自动跳过Rotation因子分析, 避免无意义计算
- 首轮实验组合:
LABEL = fwd_ret_1dFEATURE_MODE = core_plus_alpha158Alpha158 分组 = kbar_shape- 总特征数 =
21(core_12 + 9个kbar_shape因子) NORMALIZE_MODE = zscoreEXPORT_EMA_ALPHA = 0.30
Cell 7信号质量结果:Target IC Mean = +0.0441ICIR = +0.3827t-stat = +10.89- 经济分层
L/S Sharpe = 1.47 Top-20日均双边换手约106.6%
- Rust 组合回测结果:
Gross Return = +59.79%Total Return = +18.61%Max Drawdown = 14.45%Avg Trades/Day = 3.0- 总成本
205,925
- 相比当前冻结研究基线 (
core_12 + fwd_ret_1d + EMA=0.30), 本轮gross / net均继续抬升, 且回撤进一步收敛 - 阶段结论:
Alpha158不应按“全部 158 因子一次性灌入”推进kbar_shape这条小而强的增量分组已值得进入主线候选- 下一步优先隔离验证:
alpha158(kbar_shape)单跑core_12vscore_12 + kbar_shape的组合层成本兑现差异
- 单跑实验组合:
LABEL = fwd_ret_1dFEATURE_MODE = alpha158Alpha158 分组 = kbar_shape- 总特征数 =
9 NORMALIZE_MODE = zscoreEXPORT_EMA_ALPHA = 0.30
Cell 7结果:Target IC Mean = +0.0438ICIR = +0.4250t-stat = +12.10L/S Sharpe = 1.45Top-20日均双边换手约124.0%
- Rust 组合回测结果:
Gross Return = +7.65%Total Return = -21.70%Max Drawdown = 28.68%Avg Trades/Day = 2.8- 总成本
146,736
- 结论更新:
kbar_shape本身不是可直接单跑的强组合 alpha- 但它与
core_12组合时能显著抬升gross / net并改善回撤 - 因此当前更合理的判断是:
kbar_shape主要是高价值的交互增强器- 而不是应替代
core_12的独立主特征集
- 对研究优先级的影响:
- 暂不再推进
alpha158(kbar_shape)单跑方向 - 下一步主线转为围绕
core_12 + kbar_shape做组合层兑现优化
- 暂不再推进
- 复盘
notebooks/cross_section_rotation.py后确认:- 当前
LABEL = fwd_ret_1d+LGBMRegressor没有根本性错误 - 但训练目标是“收益幅度回归”, 实际使用是“当日截面 Top-N 排名”, 存在目标错配
- 当前
- 当前共识更新:
fwd_ret_1d继续保留为真实基线- 不回到
fwd_ret_1d_excess主线 (此前已验证失败) - 下一步优先做“排序化标签”实验, 再决定是否值得切到
LGBMRanker
- 修复一处明显 bug:
- Cell 6 训练样本过滤原先写死为
fwd_ret_1d - 现改为跟随
LABEL动态过滤, 避免切换标签时样本集与训练目标不一致
- Cell 6 训练样本过滤原先写死为
utils/signal_export.py为Rotation导出新增 artifact 追踪能力:- 训练阶段固定
train_run_id - 保存
artifacts/rotation/<train_run_id>/train.meta.json - 保存
artifacts/rotation/<train_run_id>/raw_scores.parquet - 保存
artifacts/rotation/<train_run_id>/signals/<signal_timestamp_ms>/signal.parquet - 保存对应
signal.meta.json - 在
artifacts/rotation/<train_run_id>/signals.jsonl记录该 train run 派生过哪些 signal
- 训练阶段固定
- sidecar 元数据已记录:
LABELfeature_modefeature_hash- 完整因子列表
LightGBM参数EXPORT_EMA_ALPHAgit_commit
bt-rotation现在会自动读取信号文件旁的signal.meta.json- 每次回测输出固定文件:
report.txtreport.json
- 输出目录改为 signal 目录下:
artifacts/rotation/<train_run_id>/signals/<signal_timestamp_ms>/backtests/<backtest_timestamp_ms>/
- 回测记录统一写入:
artifacts/rotation/<train_run_id>/backtest.jsonl
- 报告中区分:
Input Signal(Rust 实际读取路径)Canonical Signal(artifact 真正归档路径)
- 默认不再导出
data/signals/rotation_scores.parquet artifacts/rotation/.../signal.parquet成为唯一真实 signal 文件signals.jsonl改为下沉到每个train_run_id目录backtest-engine/run_rotation.bat现在是轻量包装器:- 不带参数时, 进入交互式选择
- 传入
signal.parquet / signal.meta.json / signal目录时直接回测
- 新增
scripts/rotation_backtest.py:- 交互式选择
train run -> signal - 自动创建
backtests/<backtest_timestamp_ms>/ - 调用
bt-rotation并把结果写回对应 signal 目录
- 交互式选择
- 这样可以安全支持:
- 历史 signal 回测
- 批量导出多个 signal
- 后续参数扫描
scripts/rotation_backtest.py新增 CLI 覆盖参数:--hold-buffer--min-score--max-hold-days--top-n
- 每次回测都会在对应
backtests/<backtest_timestamp_ms>/下生成:effective.config.toml
- 设计约定:
- Git 追踪的
backtest-engine/crates/rotation/config.toml继续作为稳定基线 - 临时实验参数不再要求手改基线 config
- 实际生效参数通过
effective.config.toml与backtest.jsonl / report.json一起追踪
- Git 追踪的
- 基于
core_12 + fwd_ret_1d对EXPORT_EMA_ALPHA做两轮扫描:- 粗扫:
1.0 / 0.4 / 0.3 / 0.2 / 0.15 / 0.1 / 0.05 - 细扫:
0.25 / 0.28 / 0.30 / 0.32 / 0.35
- 粗扫:
- 结论:
EXPORT_EMA_ALPHA = 0.30给出当前最佳净收益 (Gross +51.19% / Net +16.16%)0.28为峰值附近次优平衡点1.0 / 0.4平滑不足, 交易成本显著失控0.1 / 0.05平滑过强, 会压缩真实 alpha
- 已将
notebooks/cross_section_rotation.py的导出默认值更新为新的临时研究基线:EXPORT_EMA_ALPHA = 0.30
notebooks/cross_section_rotation.py的 Cell 6 训练样本过滤已改为使用LABEL- 进一步补齐 Cell 7 信号质量分析:
- join 的标签列不再写死
fwd_ret_1d - 过滤条件不再写死
fwd_ret_1d - IC / Quintile / Turnover 分析读取的收益列改为跟随
LABEL
- join 的标签列不再写死
- 这样后续切换到
fwd_ret_2d / fwd_ret_5d / excess / 排序化标签时, 训练层与分析层不会再出现静默口径错位
- 在
core_12 + fwd_ret_1d + EXPORT_EMA_ALPHA=0.30下完成两轮组合参数扫描:hold_buffer = 35 / 50 / 70 / 90 / 120max_hold_days = 5 / 7 / 10 / 15
- 结论:
hold_buffer = 50仍是当前最优退出阈值max_hold_days = 15给出当前最高net return(+17.00%)- 但为了保持博主早期公开日频基线“平均持仓约 2.8 天”的节奏特征, 当前不将
15升格为早期日频对标锚点 - 当前冻结的早期日频对标锚点为:
Feature Set = core_12LABEL = fwd_ret_1dEXPORT_EMA_ALPHA = 0.30hold_buffer = 50max_hold_days = 10
- 下一步转向:
- 先验证“排序化标签”是否能更贴近最终
Top-N排名目标 - 暂缓继续深挖
top_n / min_score,待训练目标方向确认后再回头微调
- 先验证“排序化标签”是否能更贴近最终
notebooks/cross_section_rotation.py现已新增一组最小排序化标签:fwd_ret_1d_rank_pctfwd_ret_2d_rank_pctfwd_ret_3d_rank_pctfwd_ret_5d_rank_pct
- 定义方式:
- 在每日截面内, 将未来收益映射为
[0, 1]分位数标签 - 保留现有
LGBMRegressor训练链路不变, 先验证“标签语义更贴近排序”本身是否有效
- 在每日截面内, 将未来收益映射为
- 设计目的:
- 避免一开始就同时改
LABEL + 模型类型 + Ranker,把实验变量拆开 - 若
rank_pct标签已能显著改善IC / Quintile / Rust Top-N,再决定是否值得进入LGBMRanker
- 避免一开始就同时改
notebooks/cross_section_rotation.py的 Cell 7 现已拆成两套口径:7a Target IC: 跟随LABEL,诊断模型是否学到训练目标7b / 7d Economic Evaluation: 固定使用fwd_ret_1d,统一比较不同训练目标的真实经济效果
- 这样切到
fwd_ret_1d_rank_pct / fwd_ret_2d / excess时:- 不会再把标签值本身误读成“真实收益”
- Quintile / L-S / 分年统计可以横向可比
- 首轮
fwd_ret_1d_rank_pct手动复核结论:Target IC仍然较高, 这本身不构成优势证明- 因为
rank_pct与原始收益在单日截面上是单调映射,Spearman IC天然可能接近 - 更关键的
fwd_ret_1d经济分层与回测暂未显示优于当前fwd_ret_1d基线
utils/rotation_factors.py的cross_section_normalize()现已支持:zscorerank_pctrank_gauss
notebooks/cross_section_rotation.py新增NORMALIZE_MODE配置项:- 目前可直接切换因子输入的截面归一化方式, 无需改训练主链路
- 设计目的:
- 快速验证“特征截面 z-score”是否值得替换为更偏排序化的输入表达
- 保持
LABEL / LightGBM / Rust 回测链路不变, 隔离单一实验变量
- 训练元数据现已额外记录:
normalize_mode
utils/signal_export.py现已同步把normalize_mode写入:train.meta.jsonsignal.meta.jsonsignals.jsonl
- 首轮实验结论:
NORMALIZE_MODE = rank_pct明显失败:Gross +8.66% / Net -12.76% / Avg Trades 2.0
NORMALIZE_MODE = rank_gauss同样失败, 且未优于rank_pct:Gross +9.13% / Net -14.26% / Avg Trades 2.1
- 两者都显著弱于当前
zscore基线:Gross +51.19% / Net +16.16% / Avg Trades 2.6
- 当前结论:
- 对
core_12 + LightGBM + rotation主线, 因子输入不能简单替换为纯 rank 系截面归一化 rank_pct / rank_gauss让交易频率略降, 但 gross alpha 基本被打穿- 因此“特征归一化改成 rank 系”这条线暂时收口, 主线继续保留
zscore
- 对
utils/duckdb_utils.py的日线 / 60 分钟加载现已新增:vwap_rawvwap_adj
- 当前约定:
stock_daily.volume单位确认为“手”,不是“股”vwap_raw = amount / (volume * 100)vwap_adj = vwap_raw * adj_ratio
notebooks/cross_section_rotation.py的df_all现已保留vwap_adj / vwap_rawnotebooks/cross_section_rotation.py新增Cell 2b:- 直接比较
amount / volume与amount / (volume * 100)哪个更贴近close_raw - 若
vwap_raw与close_raw数量级失配会直接报错 - 固定写明:
vwap_raw = amount / (volume * 100)turnover_rate(%) = volume * 100 / circulating_capital * 100
- 直接比较
- 同步修正:
utils/rotation_factors.py的turnover_rateutils/b1_factors_opt.py的turnover_rate
- 目的:
- 为后续接入
Qlib Alpha158准备价格字段 - 避免未来再回头改一次底层数据链路
- 为后续接入
- 将与策略无关的 artifact 追踪 I/O 从
bt-rotation抽到bt-core:SignalArtifactMetaload_signal_meta()build_report_stem()write_report_bundle()resolve_registry_path()append_jsonl_record()
Rotation仅保留策略专属部分:- 配置序列化
- 额外统计 (
limit_up_blocked) - registry 记录字段选择
- 这次重构目标是为后续
B1 / Renko / 更多策略复用同一套 artifact 追踪能力, 不强行统一各策略的个性化逻辑
- 新增
experiments/rotation-next-phase.md - 将下一阶段目标明确为:
- 导出侧独立
EXPORT_EMA_ALPHA - 因子治理与核心因子收敛
LightGBM之外的模型基线对照- 固定研究基线后的组合参数收敛
- 导出侧独立
- 明确修正共识:
Rotation当前标的池已经是 80~500 亿, 不再作为下一阶段主任务
notebooks/cross_section_rotation.py的训练 Cell 现在只输出df_scores_raw- 新增独立导出 Cell, 本地控制
EXPORT_EMA_ALPHA - 修改导出平滑参数时, 只需重跑导出 Cell, 无需重新训练
LightGBM Rotation与Renko现已统一为“raw score → export EMA”的导出模式
utils/rotation_factors.py新增:FACTOR_GROUPSFACTOR_GROUP_LABELSFACTOR_TO_GROUP
- 分组覆盖当前全部
Rotation因子, 并在模块加载时校验完整性 notebooks/cross_section_rotation.py新增分组概览 Cell, 可直接查看每组因子数量、平均|ICIR|与组内最佳因子
notebooks/cross_section_rotation.py新增Cell 3d- 基于:
- 因子分组
- 单因子
|ICIR| - 全局相关性剪枝结果 (
factors_keep)
- 自动给出一版建议
core feature set FEATURE_MODE现支持"core", 且参数位于Cell 6本地, 可直接训练核心因子版本与"all"/"pruned"对照
notebooks/renko_ml_explore.py统一为: T 日收盘确认信号 / 计算特征 → T+1 日开盘买入- 本次只改研究 notebook 的时间线, 暂不修改 Rust 导出 / 回测格式
- Renko 专属
rk_*因子不再混用T-1数据:- 删除
_c1,_o1,_v1临时 shift 列 rk_bias_wl,rk_wl_yl_spread,rk_shape,rk_rw_dif_pct,rk_vol_shrink全部改为 T 日收盘可得
- 删除
- 标签改为以
T+1 open为基准:- 新增
buy_open_t1 = open_adj.shift(-1) fwd_mfe_5d = max(high[T+1:T+5]) / buy_open_t1 - 1fwd_ret_1d = close[T+1] / buy_open_t1 - 1
- 新增
- 保留
renko_signal[T]作为信号确认时点, 不再与T-1特征混搭
- 旧版 notebook 内部同时混用了:
rotation通用因子: T 日- 部分
rk_*因子: T-1 - 标签分母:
close[T] - 单笔交易分析:
open[T+1]
- 现已统一为单一时间线, 便于后续重新评估 Renko ML 是否真实有效
- Cell 1 新增
LABEL配置, 当前默认fwd_ret_open_2d - Cell 2 预先计算以下标签, 后续只改一行即可重训:
fwd_ret_open_2d = open[T+2] / open[T+1] - 1fwd_ret_close_2d = close[T+2] / open[T+1] - 1fwd_ret_close_3d = close[T+3] / open[T+1] - 1
- Cell 3 / 4 / 5 全部改为自动引用
LABEL
- Cell 5b 改为专门验证高换手短脉冲问题, 提供三组 notebook 内实验:
- EMA 平滑实验:
ANALYSIS_EMA_ALPHAS = [1.0, 0.2, 0.1, 0.05] - Top-N 扩大实验:
ANALYSIS_TOP_NS = [20, 50, 100] - 高分阈值过滤实验:
ANALYSIS_SCORE_QUANTILES = [0.99, 0.97, 0.95, 0.90]
- EMA 平滑实验:
- 设计目标: 先在 notebook 内验证
open_2d / close_3d的 alpha 是否能通过平滑、扩容或高分过滤保留下来, 再决定是否值得继续改回测引擎
- 之前 +400% 收益中, 大量 alpha 来自"买入涨停股" — 实操中根本买不进
- 统计: Top-20 中日均 4.8 只是涨停股 (88% 的交易日有过滤), 占候选的 ~24%
- 过滤涨停后, 旧模型 Gross 从 +586% 暴跌至 +5.7%, 证实 alpha 几乎全是幻觉
bt-core/src/lib.rs: 新增price_limit_pct(),is_limit_up(),is_limit_down()共享函数- 主板 (60/00) → ±10%, 创业板 (300/301) / 科创板 (688/689) → ±20%
- 容差 0.1% (覆盖复权价四舍五入精度)
bt-rotation/main.rs: 候选股过滤 — 先选 Top-N 再剔除涨停, 并统计被过滤数量bt-rotation/systems.rs: 跌停锁仓 — 持仓跌停时跳过卖出, 打印 [LOCKED] 日志
utils/duckdb_utils.py:load_daily_data_full()新增pre_close_adj输出列- 新增
add_price_limit_cols()共享函数 (与 Rust 判定逻辑一致)
utils/__init__.py: 导出add_price_limit_colsnotebooks/cross_section_rotation.py:- Cell 2: 调用
add_price_limit_cols()打标记, 统计涨跌停样本数 - Cell 6: 训练时
valid = np.isfinite(y_tr) & ~is_limit_up_np[ts:te] - 打分: 全量打分 (不过滤), Rust 兜底
- Cell 2: 调用
| 指标 | 修复前 (含涨停幻觉) | 修复后 (排除涨停) |
|---|---|---|
| Gross Return | +586% (幻觉) | +48% |
| Total Return | +64% | +10.5% |
| Win Rate | 41.9% | 45.6% |
| Max Drawdown | 27.5% | 21.3% |
| 涨停过滤 (日均) | 4.8 只 | 2.0 只 |
- Cell 1 配置区新增
LABEL参数, Cell 3 (IC) 和 Cell 6 (训练) 自动引用 - 可选:
fwd_ret_{1/2/3/5}d或fwd_ret_{1/2/3/5}d_excess
- 标签:
fwd_ret_1d - mean(fwd_ret_1d).over("date"), 截面去均值 - 结果: 五分位单调性崩塌 (Q4 ≈ Q1), Top-20 选股退化为随机, Gross ≈ 0%
- 原因: 去均值后上半区信号区分度丢失
- Gross +54% (高于 1d 的 +48%), 但换手反而增加 (3406 vs 2202 笔)
- 额外成本吞掉了额外 alpha, 净效果不如 fwd_ret_1d
utils/rotation_factors.py重写: 去掉shift(1), 所有因子直接用 T 日 OHLCVnotebooks/b1_ml_explore.py,b1_ml_dedicated.py,renko_ml_explore.py: 自行计算_c1等 shift 列- Rust config.toml: slippage 调整为 0.3% (含 14:45~15:00 快照误差)
- 新建 notebook, 56 特征 (42 rotation + 14 B1 专属), 标签 MFE-10
- 全市场训练 LightGBM, 对 B1 候选排序
- 信号质量: IC +0.137, L/S +3.95%, t-stat +32.38 — 极显著
- Rust 回测: 近期 +36.63% (手搓 +30.49%), 长周期 +78.36% (手搓 +81.05%)
- 近期跑赢手搓 +6pp, 长周期持平, 但回撤更大
- 仅用 B1 信号日样本 (14,242 条) 训练, 38 特征 (IC 筛选)
- 发现: B1 子集 IC 排序与全市场完全不同 —
amihud_illiq_20d全市场 #1 但 B1 无效,vol_60d在 B1 中最强 - 信号质量: IC +0.009, t-stat +0.54 (不显著) — 每天仅 10~17 只 B1 候选, 截面太窄
- Rust 回测: 近期 +21.38%, 长周期 +43.83% — 全面跑输
- 结论: B1 专属模型不可行, 最佳方案仍是全市场 ML 排序
- 修复
b1_ml_explore.pyQuintile 标签方向 (Q1/Q5 含义反了) - 修复 Top-N Overlap 计算 (对比数组索引改为对比股票代码)
- 添加 LightGBM 训练进度打印 (对齐 rotation notebook)
- 新建
experiments/目录, 每个实验独立 markdown project-status.md按策略分节 (Rotation / B1 / 共享基础设施)- 迁移旧
experiments.md内容到experiments/rotation-benchmark.md+rotation-factors.md- 注:
rotation-benchmark.md后续已被target-strategy-evolution.md取代
- 注:
- 尝试从 42 因子扩展至 55 因子, 新增 128 天动量/波动率/回撤/均线偏离/价格位置等
- 结果: IC 下降, 五分位单调性破坏, Rust 回测净收益从 +82% 降至 +30%
- 尝试根据单因子 IC 剪枝 (55→50), 反而更差 — 树模型的非线性交互使单因子 IC 不适合做删减依据
- 结论: 128 天方向无效, 回退至 42 因子基线 (40 通用 + 2 处置效应)
- 发现 α=1.0 (无平滑) 导致 Rust 回测灾难性结果: 日均 14.1 笔交易, 成本 55 万 > 本金 50 万, 净收益 -45%
- α=0.1 (旧默认): IC +0.0227, L/S t-stat 1.78 (不显著)
- α=0.2 (新最优): IC +0.0234, L/S t-stat 2.08 (首次统计显著), Sharpe 1.14, 五分位完美单调
- 信号平滑是必需的, hold_buffer 单独无法控制换手
- 问题: Cell 6 (训练导出) 和 Cell 7 (信号分析) 各自做一次 EMA, 导致双重平滑
- 修复: Cell 6 新增
df_scores_raw输出 (EMA 前的原始分数) - Cell 7 改为依赖
df_scores_raw, 独立控制 EMA_ALPHA - 效果: 调整分析侧 α 只需重跑 Cell 7, 无需重新训练模型
notebooks/renko_ml_explore.py的 Cell 6 新增EXPORT_EMA_ALPHA- 导出 parquet 时改为基于
df_scores_raw现场做 EMA, 不再依赖训练 Cell 内部平滑 EXPORT_EMA_ALPHA已下沉到 Cell 6 本地配置, 避免 marimo 修改 Cell 1 时触发上游重跑- 效果: 切换导出用 α 只需重跑 Cell 6, 无需重新训练 LightGBM
- 复用价值: 这套“训练输出 raw score, 导出侧单独做平滑”的模式后续可迁移到
cross_section_rotation.py, 避免每次只改导出 EMA 也要重训模型
- 基于
fwd_ret_open_2d做了 Rust 组合回测, 发现结果对导出侧EXPORT_EMA_ALPHA高度敏感 EMA=1.0 / 0.1 / 0.2下净值表现都很差, 且 gross alpha 不稳定EMA=0.05虽然能把 Gross Return 提升到正值, 但净收益依旧明显为负, 成本无法覆盖- 结论: 当前 Renko ML 信号在 notebook 统计上有一定信息量, 但组合层可兑现性不足, 暂不作为优先探索方向
bt_core新增format_results()+write_report()共享函数- rotation 和 b1 两个 crate 均支持
--output-dir参数, 自动保存带时间戳的回测报告到results/ - 报告包含完整配置参数 + 回测结果, 便于跨实验对比
| 指标 | 值 |
|---|---|
| 净收益 | +82.57% |
| 毛收益 | +164.83% |
| 最大回撤 | 27.35% |
| 胜率 | 42.1% |
| 总交易 | 3,617 笔 (803 天) |
| 日均交易 | 4.5 笔 |
| 总成本 | 411,298 (Gross PnL 的 50%) |
- 核心决策: Python 模型只负责截面打分 (1d/1d), 回测/风控/持仓管理全部交给 Rust ECS 引擎
- 依据:
- LightGBM 1d/1d 模型偏度已为正 (+0.28), 天然适合日频信号
- Python 固定持仓 N 天 (3d/3d, 1d/3d) 效果均一般, 无法灵活止损止盈
- Rust 回测框架已支持 B1 策略的 Parquet 导入, 可复用架构
utils/signal_export.py: 新增export_rotation_scores()函数- 输入:
df_scores(date, code, score + OHLCV + market_cap) - 输出: Parquet 含 score, rank, is_top_n, pre_close_adj 等列
- Rust 端可直接读取, 每日选 Top-N 候选, 自行决策买卖
- 输入:
notebooks/cross_section_rotation.py:- Cell 6: 从"LightGBM Walk-Forward 回测"重构为"打分 → Parquet 导出"
- 移除: 所有 Python 侧回测逻辑 (HOLD_BUFFER/COST/HOLD_DAYS/portfolio 模拟/净值曲线/年度拆解)
- 保留: Walk-Forward 训练循环、特征重要性输出
- 新增: 每日全 universe 打分 → join 价格 → export_rotation_scores
- Cell 4/5: 清空 (线性排名回测 + 旧可视化, 已被 LightGBM + Rust 替代)
- Cell 6: 从"LightGBM Walk-Forward 回测"重构为"打分 → Parquet 导出"
- 改造原因: 原引擎为 B1 单策略设计, PriceBar/信号/退出逻辑全部硬编码 B1 语义, 无法复用于轮动策略
- 新结构:
backtest-engine/改为 Cargo workspace, 三个 crate:bt-core: 共享类型 — Portfolio, BacktestStats, CostModel, 工具函数bt-b1: B1 超跌反转策略 (从旧代码迁移, 功能不变)bt-rotation: 截面轮动策略 (新建)
- 轮动策略回测逻辑:
- 读取
rotation_scores.parquet(Python LightGBM 打分结果) - 每日系统: check_exit_conditions → fill_positions → update_stats
- 退出条件: 排名跌出 hold_buffer / 固定止损 / 移动止损 / 最大持仓天数
- 入场条件: Top-N 买入 (尾盘收盘价), 等权仓位
- TOML 配置: top_n, hold_buffer, stop_loss, trailing_stop, costs
- 读取
- 运行方式:
cargo run -p bt-rotation --release(从 backtest-engine/ 目录)
- 新增 2 个行为金融因子至
utils/rotation_factors.py,因子库扩展为 7 类 42 个disp_bias_20: 20日 EWM 估算持仓成本偏离度 (短期处置效应)disp_bias_60: 60日 EWM 估算持仓成本偏离度 (中期处置效应)
- 底层算法: EHC = EWM(TypicalPrice × Volume) / EWM(Volume),用 EWM 指数衰减近似换手率驱动的筹码替换
- 无需额外数据源,仅依赖现有 OHLCV + 换手率
- 完全嵌入现有 Polars lazy chain,零额外 collect 开销
- 毛收益年化: 41.0% → 50.3% (+9.3pp), Sharpe 1.32 → 1.52
- 净收益年化: 1.2% → 8.2% (+7.0pp), Sharpe 0.19 → 0.41
- 最大回撤: 51.1% → 44.2% (-6.9pp)
- 偏度: -0.01 → +0.28 (从负偏转正偏)
- 2025 年超额(净)从 -9.6% 翻正至 +19.8%
- 因子本身未进 Top 15 特征重要性,通过 GBM 交互效应提升整体表达力
- 详见
results/disposition_effect_ab_test.md
- 实现
utils/rotation_factors.py,共 40 个日线截面因子(6 大类) - Universe: 流通市值 80-500 亿,非 ST,上市 > 60 天,2020-09 起
- IC 分析结论: 25 个因子 |ICIR| > 0.1,12 个 > 0.35
- Top IC 因子: vol_std_20d (-0.58), ret_max_5d (-0.53), turnover_rate (-0.52)
- 新增 A 股 T+1 专用因子效果显著: high_open_pct (ICIR -0.46), amihud_illiq_20d (+0.40)
- 线性排名 Top-20 回测失败 (-46% 年化),证实简单排名无法利用弱 IC
- 480 天训练窗口,每 20 天重训,200 棵树,depth=6
- 毛收益: 43% 年化, Sharpe 1.34, 正偏度 — alpha 确认存在
- Hold buffer 优化路径:
- 无缓冲: 换手 87.8%, 净 -9.9%
- Top-50: 换手 79.8%, 净 -1.7%
- Top-150: 换手 68.2%, 净 +2.8%, Sharpe +0.24
- 特征重要性 Top 5: vol_ratio, vol_compress, turnover_ma_ratio, turnover_accel, abnormal_vol
- 核心瓶颈: 日均毛收益 0.166% vs 日均成本 0.136%,净利润空间薄
- 并非单模型, 而是 12 个子策略组合 — 灰线是 12 条子策略净值, 蓝线是组合
- 平均持仓 2.8 天, 非严格 T+1 → 日换手率 ~31% (6.2 笔/天 ÷ 20 只)
- 7475 笔交易 / 5 年, 平均仓位 ~50%, 最大同时持仓 20 只
- 年化 50.42%, 最大回撤 9.13%, 胜率 54.01%, 盈亏比 1.5:1
- Alpha 0.60, Beta 1.78 (R²=0.28), 日收益偏度 0.90
- 日内还有独立的 1 分钟 T+0 择时系统, 实盘比回测多 10-20% 年化
- 对我们的启示:
- 多策略架构 > 单一模型 (分散化 + 降回撤)
- 持仓 2.8 天 → 真实成本仅 0.062%/日, 是我们假设的一半
- 50% 仓位是为日内 T+0 留空间, 非风控约束
- 修复 Polars panic:
is_in(st_blacklist)改为anti-join - 修复嵌套
.over("code")问题: 拆分为多步.with_columns()物化中间列 - 修复 marimo 变量重定义: Cell 内逻辑包裹在函数中
- 修复 numpy datetime64 → Polars Date 类型转换
- 移除重复因子
gap(与overnight_ret公式完全相同)