13c69c2a0b
feat(report): 全标的动量排名表替代原调仓信号表
...
Panel 0 从仅展示持仓调仓扩展为全标的排名表:
- 新增未入选标的行,按动量降序展示
- 新增排名、市场两列
- 表格按调入→维持→调出→未入选顺序排列
- 调出标的也展示真实得分(便于分析调出原因)
- 标题显示当前动态阈值
- 未入选标的浅灰背景区分
2026-06-08 00:12:17 +08:00
6a5ae8efbf
fix: generate_report now uses actual position_weights from daily_records
...
Previously hardcoded equal weight (1/select_num), ignoring config weight type.
Now reads position_weights from last daily_record, correctly showing rank-based weights.
2026-06-07 23:29:27 +08:00
d898ba0fd5
Revert "feat: add HTML report screenshot generation via Playwright"
...
This reverts commit f370caeff9 .
2026-06-07 23:12:21 +08:00
f370caeff9
feat: add HTML report screenshot generation via Playwright
...
- Add html_report.py module for Playwright-based screenshot generation
- Add generate_html_report() method to SimpleRotationStrategy
- Modify backtest_viewer.html to use window-scoped variables for external injection
- Inject monthly/yearly returns table into screenshot
- Auto-generate HTML report in __main__ after export_results()
Output: simple_rotation_html_report.png with ranking table + monthly returns
2026-06-07 22:43:12 +08:00
06df8767b9
docs: add select_num=1 strategy deep analysis report
...
- Asset contribution attribution (CL=F 59.1%, N225 -11.8%)
- IC analysis across lookback periods (only CL=F and ChiNext have robust positive IC)
- Hurst exponent analysis and asset classification
- Multi-factor direction recommendations
2026-06-07 12:26:13 +08:00
7b229ced14
docs: add strategy summary snapshot (2026-06-06, ca933e4)
...
First stage summary documenting core strategy logic, key design
decisions, and select_num/weight backtest comparison results.
Stored in dedicated docs/strategy_summaries/ directory with
date + commit hash naming for reproducibility.
2026-06-06 23:59:41 +08:00
ca933e43e4
fix: lock position weights on rebalance only, not daily ranking changes
...
Previously, position weights were recalculated every day in _generate_signals,
causing weights to change even when holdings didn't change (only ranking order
shifted). This was incorrect - weights should be locked at rebalance and remain
stable until the next rebalance.
Changes:
- _generate_signals now computes _pending_weights (for signal generation only)
- run() maintains active_weights, updated only on is_rebalance or first day
- _calculate_daily_return uses the locked active_weights
- daily_records stores active_weights in position_weights field
Result: 391 → 318 rebalances, 25.63% → 26.38% CAGR
2026-06-06 23:16:51 +08:00
8d8fd71149
feat(viewer): sort holdings cards by position weight descending
...
Display largest position first in the holdings panel for better readability
2026-06-06 22:48:40 +08:00
4d9e12886f
chore: remove *.html from gitignore to track all HTML files
2026-06-06 22:43:08 +08:00
eb3c82f05b
feat(rotation): add position weight to detail JSON and viewer
...
- Record position_weights in daily_records during backtest run
- Export weight field per held asset in detail JSON
- Display weight percentage in backtest_viewer holdings cards
- Force-add backtest_viewer.html (previously ignored by *.html rule)
2026-06-06 22:39:23 +08:00
4973a9a2a5
feat(rotation): componentize position weighting + fix bond threshold consistency
...
- Extract compute_position_weights() as pluggable pure function
- Add WeightType enum (equal/rank) and RotationConfig.weight field
- Fix bond threshold dimension mismatch: use configured factor function
for all assets instead of hardcoded weighted_momentum_score
- Default weight: equal in config, active: rank in config_simple.yaml
2026-06-06 22:28:08 +08:00
44588d5026
refactor(rotation): clean up experimental factor code
...
Remove slope_snr, slope_snr_r2, james_stein score functions and r2_alpha parameter.
slope_r2_score reverts to simple slope*R² with no alpha parameter.
Minor docstring fix: R^2 → R².
2026-06-06 18:45:11 +08:00
921f84cb6a
feat: 新增 standardized_slope (t-statistic) 因子并实验验证
...
- simple_rotation.py: 新增 standardized_slope_score 函数 (slope/SE)
- config_loader.py: FactorType 枚举新增 STANDARDIZED_SLOPE
- 对比实验结果: standardized_slope 年化 13.73% vs slope_r2 19.84%
- 结论: t-statistic 过度惩罚高波动资产的有效趋势信号,不适合本场景
- 文档更新: 动量因子对比调研报告新增 3.3 节详细分析
2026-06-06 16:40:01 +08:00
aff04318b1
chore: 动量因子对比调研报告移至 docs 目录
2026-06-06 16:19:10 +08:00
40853745c6
docs: 添加动量因子对比调研报告
...
包含4种因子公式对比、回测结果、slope_r2胜出原因分析、
业界学界方法调研(TSMOM/Baltas&Kosowski/AQR)、
负价格处理机制分析及改进建议
2026-06-06 16:16:42 +08:00
b564a47a1b
feat: 新增slope_r2因子并切换为默认因子(年化19.84%, 夏普1.14)
...
- simple_rotation.py: 新增3种score函数(vol_adjusted_momentum, slope_r2, momentum)
- config_loader.py: FactorType枚举新增VOL_ADJUSTED_MOMENTUM
- config_simple.yaml: factor.type 切换为 slope_r2
- experiments/factor_comparison.py: 4种因子对比实验脚本
- experiments/output: 实验结果(slope_r2全面胜出)
2026-06-06 15:49:22 +08:00
04b858ff09
feat: 添加ETF轮动策略诊断分析实验
...
新增6维度策略诊断实验脚本和报告:
- task1: 信号产生分析 (调仓频率、无效调仓率)
- task2: 收益计算分析 (T+1执行偏差、溢价问题)
- task3: 调仓逻辑分析 (最小持仓期模拟)
- task4: 资金管理分析 (止损、波动率适配)
- task5: 收益归因分析 (集中度、静态vs轮动)
- task6: 回撤诊断分析 (最大回撤复盘、尾部风险)
输出报告:
- diagnosis_report.md: 完整策略诊断报告
- rebalancing_optimization_experiment.md: 调仓频率优化实验报告
实验结论:
- 发现调仓过于频繁 (405次/1549天)
- No-Trade Region方案可提升年化3%、夏普0.11
- 但改善幅度有限,信号质量是根本瓶颈
2026-06-06 15:00:28 +08:00
f3ba6eb799
docs: add momentum time window research report
2026-06-03 23:56:05 +08:00
55e4cbf108
refactor(archive): move reports/ to archive/reports/
2026-06-03 23:43:31 +08:00
c905230a40
refactor(archive): move unused modules to archive/
...
Archive legacy framework and utility modules that are no longer
referenced by the active core (datasource/ and rotation/):
- framework/ -> archive/framework/
- framework_v2/ -> archive/framework_v2/
- strategies/ -> archive/strategies/
- config/ -> archive/config/
- visualization/ -> archive/visualization/
- scripts/ -> archive/scripts/
- tests/ -> archive/tests/
- run_rotation.py, run_us_rotation.py -> archive/single_files/
- compare_*.py, test_api_dates.py -> archive/single_files/
2026-06-03 23:41:46 +08:00
d700bc1dfd
fix(rotation): 回测导出JSON序列化NaN/Inf清洗
...
- simple_rotation.py: 新增 _sanitize_json() 递归替换 NaN/Inf 为 None,
确保 json.dump 生成合法 JSON(避免前端解析失败)
- .env: 注释掉群2钉钉配置(暂不使用)
2026-06-03 09:14:53 +08:00
4f9e0231bd
fix(datasource): yfinance时区标准化与NaN过滤修复
...
- yfinance_source.py: 用 tz_localize(None) 替代 pd.to_datetime(utc=True),
避免亚洲/欧洲市场因UTC转换导致日期回退一天(如日经225 5/25→5/24)
- yfinance_source.py: 新增 _normalize_index() 静态方法统一处理时区剥除
- yfinance_source.py: fetch() 增加 close=NaN 行过滤(yfinance未收盘日返回不完整数据)
- flask_api_source.py: 客户端同步增加 close=NaN 过滤防御
验证结果:N225 5/25-6/3 返回7个交易日数据,日期无偏移
2026-06-03 09:14:39 +08:00
972bbbe706
fix(rotation): signal_date改用日历日前一天以捕获外盘假期数据
...
- 将 signal_date = trading_calendar[i-1] 改为 date - timedelta(days=1)
- 解决A股长假期间美股继续交易但动量计算丢失外盘数据的问题
- 同步修复 export_results 中的 signal_date 映射逻辑
2026-06-03 01:25:09 +08:00
524fa5f513
refactor(rotation): 移除数据缓存 + 修复空值和pct_change警告
...
- 移除CSV本地缓存(cache_dir、_cache_path、_premium_cache_path、_save_premium_cache)
- 每次运行直接从API获取数据,简化DataCache类
- 修复_get_etf_prices中open/close为None时的空值处理(中证指数API不提供OHLC)
- 修复pct_change的FutureWarning(显式传fill_method=None)
- 更新trade_cost注释
2026-06-03 00:54:48 +08:00
d1139a9ee9
fix(http): 用requests+trust_env=False修复SSL EOF问题
...
根因:Clash代理(127.0.0.1:7890)在处理TLS 1.3+后量子密钥交换时
不兼容,导致SSL EOF错误。requests默认trust_env=True会读取系统
代理配置,通过代理转发HTTPS请求时触发问题。
修复:使用requests.Session(trust_env=False)绕过系统代理,
直连目标服务器。无需降级urllib3版本。
影响文件:
- rotation/simple_rotation.py
- datasource/flask_api_source.py
2026-06-03 00:35:49 +08:00
a2b4289080
revert(http): 改回串行数据获取
...
回退并行获取逻辑,恢复简单的串行循环:
- 移除 ThreadPoolExecutor 并行代码
- 移除 concurrent.futures 导入
- 保持简单的 for 循环串行获取
2026-06-03 00:09:29 +08:00
e29f57749d
perf(http): 并行获取数据加速数据加载
...
使用 ThreadPoolExecutor 并行获取多个标的的数据:
- 信号源 (index): 11个标的并行获取
- 交易源 (ETF): 4个标的并行获取
- 溢价率数据: 4个标的并行获取
性能提升:5个标的从 ~15s 串行 → ~4.6s 并行(约 3x 加速)
修改:
- 增大 urllib3 连接池 maxsize=16 支持并行连接
- 使用 concurrent.futures.ThreadPoolExecutor
2026-06-02 22:29:59 +08:00
81045f9d85
fix(http): 用urllib3替代requests修复SSL EOF错误
...
问题根因:
- Python OpenSSL 3.5.4 + requests 2.32.4 + urllib3 2.5.0 版本不兼容
- requests 2.32.4 内部使用 urllib3 的方式与 urllib3 2.5.0 API 不兼容
- curl(SecureTransport)正常工作,但 Python requests(OpenSSL)失败
- 服务器(Caddy)使用 TLS 1.3 + X25519MLKEM768(后量子密钥交换)
修复方案:
- 用 urllib3.PoolManager 直接发起 HTTP 请求(已验证可正常工作)
- 封装 _http_get() 函数替代 requests.get()
- 替换所有 requests 相关异常类型为 urllib3 异常
修改文件:
- datasource/flask_api_source.py: 核心数据源层
- rotation/simple_rotation.py: 简单轮动策略层
2026-06-02 22:22:36 +08:00
74f0eebef0
docs(experiment): add 1-day holding deep attribution analysis (006)
...
- Rank decline: 70.3% (45/64), mostly rank=3 entry → rank=4/5 exit
- Threshold breach: 25.0% (16/64), all actual momentum drops
- 38% cases: own momentum rises but outranked by others
- N225/GDAXI highest 1-day rate (22-26%), A-shares lowest (3-6%)
- Optimization: min holding period, confidence filter, rank smoothing
2026-06-02 21:41:34 +08:00
361b82fa4a
docs(experiment): add holding duration distribution analysis (006)
...
- Analyze holding period distribution from simple_rotation_detail.json
- 391 complete holding episodes across 11 assets
- Median holding: 7 days, mean: 10.9 days
- 75% of holdings within 16 days, 16.4% are 1-day switches
- NDX has longest avg holding (17.4d), HSI shortest (6.5d)
- Insight: consider minimum holding period to reduce noise trades
2026-06-02 21:36:46 +08:00
a47af0f0eb
docs(experiment): add select_num A/B/C comparison report (005)
...
- Experiment: select_num = 1, 2, 3 comparison
- Period: 2020-01-10 ~ 2026-06-02 (1546 trading days)
- Key findings:
- Top-1: highest return (600%), highest drawdown (-25.5%)
- Top-3: best risk-adjusted return (Calmar 1.73, Sharpe 1.35)
- Top-2: balanced middle ground (Calmar 1.69)
- Add rotation/experiment_select_num.py experiment script
- Save report to docs/experiments/005_select_num_comparison.md
2026-06-02 01:32:43 +08:00
07d6f1451c
fix(rotation): raise RuntimeError on held asset data failure
...
- Add data integrity check: if any currently held asset is missing
from factors, raise RuntimeError immediately to prevent false rebalance
- Previously missing data would silently cause incorrect sell signals
- Now fails fast with clear error message identifying the missing assets
and the date of failure
2026-06-02 01:16:44 +08:00
4791d3cf40
refactor(scheduler): move daily_scheduler.py to rotation/ and add simple_rotation support
...
- Move scripts/daily_scheduler.py -> rotation/daily_scheduler.py
- Add run_simple_rotation() to execute simple_rotation.py via subprocess
- Add --strategy flag (simple/legacy/all) for flexible strategy selection
- Add --simple-config flag for custom simple rotation config path
- Update Dockerfile and docker-compose.yml path references
- Add configurable title to send_report_to_dingtalk()
2026-06-02 01:16:34 +08:00
5e11b6b690
fix(rotation): 溢价率缓存增加增量更新逻辑
...
- preload_premium: 检查缓存日期范围,不足时增量拉取
- 新增 _fetch_premium_api: 拉取并合并新溢价率数据
- 调用时传入 end_date 触发增量检查
修复前: premium CSV存在即返回旧数据,明天9点运行时拿不到最新
修复后: 检测 latest_cached < end_date 时自动拉取增量
2026-06-01 23:56:18 +08:00
19f1c63981
fix(rotation): 修复溢价率计算,改用Flask API真实premium_series数据
...
- _fetch_api: 提取premium_series并存入df.attrs和CSV缓存
- DataCache: 新增premium_data字典、preload_premium方法
- preload_premium: 无缓存时主动请求API获取全量历史溢价率
- _preload_data: 加载ETF后同步调用preload_premium
- _compute_premium(trade_code, date): 从内存缓存按日期查找真实溢价率
- 新增trade_code_to_group映射,确保BOND资产正确识别
修复前: 溢价率 = (ETF价格 - 指数点位) / 指数点位 → -99.9%
修复后: 使用API返回的(ETF价格 - NAV) / NAV → 合理范围
2026-06-01 23:31:36 +08:00
6d0b928894
fix(rotation): 消除前视偏差 + V2兼容detail导出
...
时序对齐修复:
- 信号生成改用 T-1 收盘数据(9AM信号时T日未开盘)
- entry_price_etf 改用 T 日 open(实际买入价)
- 年化收益: 52.66% → 25.12%(去除约4倍虚高)
V2兼容detail JSON:
- _generate_signals 返回 (holdings, factors, bond_momentum)
- 6个helper方法: build_meta_codes, get_index/etf_close, daily_returns, premium, day_assets
- 每日11资产×16字段完整记录(momentum/rank/holding_days/cum_return等)
- export_results 同步修复 entry_info 时序逻辑
Backtest (2020-01-10 ~ 2026-06-01, 1545天):
- 总收益 295.14%, 年化 25.12%
- 最大回撤 -14.74%, 夏普 1.33, 卡尔马 1.70
2026-06-01 23:13:43 +08:00
451ffa33d2
clean(rotation): add simple rotation strategy and remove unused files
...
New:
- rotation/simple_rotation.py: daily-iteration rotation strategy (584 lines)
- rotation/config_loader.py: standalone config loader
- rotation/config_simple.yaml: 11 assets, 7 groups
- rotation/README_SIMPLE.md: usage guide
- scripts/get_trading_calendar.py: trading calendar fetcher
Removed:
- rotation/example_usage.py, run_strategy.py (replaced by simple_rotation.py)
- rotation/results/ output files (gitignored)
- scripts/verify_*.py, calculate_returns_from_detail.py (one-off scripts)
- scripts/README_TRADING_CALENDAR.md
Backtest result (2020-01-10 ~ 2026-06-01):
- Total return: 1237.6%, Annual: 52.66%
- Max drawdown: -11.71%, Sharpe: 2.50
2026-06-01 22:28:26 +08:00
3b0208d7d3
docs(viewer): 添加 backtest_viewer.html 到 git 追踪
...
- 修改 .gitignore 添加 HTML 文件例外规则
- 将 visualization/backtest_viewer.html 纳入版本控制
- 保留回测可视化查看器供团队使用
2026-05-26 23:33:06 +08:00
ee2453f65e
fix(rotation): 修复 backtest detail 中指数和 ETF 累计收益计算 bug
...
- 问题:cum_return_idx 和 cum_return_etf 使用相同的 ETF 价格计算
- 修复:分别使用指数价格(raw)和 ETF 价格(hfq)独立计算
- 验证:72.6% 的持仓记录显示差异(0.06%~0.48%),符合预期
- 新增验证脚本:verify_cum_return_fix.py
2026-05-26 23:22:26 +08:00
6a86a27108
test(scripts): 新增ETF数据获取验证脚本
...
新增脚本:
- verify_etf_hfq_fix.py: 验证指数使用raw、ETF使用hfq
- compare_index_vs_etf_returns.py: 对比指数收益vs ETF收益的KPI指标
验证内容:
- 指数数据完整性检查
- ETF数据完整性检查
- ETF是否正确使用hfq后复权价格(抽样对比raw和hfq)
- 验证510300.SH等ETF的hfq/raw比值(应>1.0)
2026-05-26 19:55:01 +08:00
2ff48e8d56
refactor(flask_api_fetcher): 暴露adj参数,增强接口透明度和灵活性
...
改进:
- fetch_indices()添加adj参数,默认'raw',可自定义
- fetch_etf()添加adj参数,默认'hfq',可自定义
- 改进日志输出,显示实际使用的adj参数
- 保持向后兼容,默认值保持原有行为
优势:
- 透明性:调用者清楚知道使用的复权方式
- 灵活性:可按需获取raw/qfq/hfq数据
- 一致性:两个方法接口统一
- 向后兼容:不影响现有代码
2026-05-26 19:54:41 +08:00
d404ddee17
fix(rotation): 修复ETF数据获取逻辑,分别获取指数raw和ETF hfq数据
...
问题:之前统一使用fetch_indices(adj='raw')导致ETF未使用后复权价格
修复:
- 在GlobalRotationStrategy中覆盖get_data()方法
- 指数数据:调用fetch_indices(adj='raw')获取原始价格
- ETF数据:调用fetch_etf(adj='hfq')获取后复权价格
- 确保指数信号计算和ETF收益计算使用正确的数据源
影响:回测将正确反映ETF的真实收益(包含分红再投资)
2026-05-26 19:54:21 +08:00
7fc1170964
feat(v2): 修复跨市场因子对齐 + 添加当日收益率字段
...
核心修复:
- 因子对齐到 A 股交易日历(ffill 填充休市日)
- 修复美股休市日 NDX 信号丢失问题(Memorial Day)
- BOND 参与大类竞争,作为阈值过滤其他组
- 添加 index_return 和 etf_return_ctc 字段
性能提升:
- 总收益: 356% → 686% (+92.7%)
- 年化收益: 28% → 40% (+12%)
- 夏普比率: 1.61 → 2.04 (+26.7%)
- 调仓次数: 747 → 399 (-46.6%)
- 最大回撤: -14.75% → -10.66% (改善)
2026-05-26 01:04:39 +08:00
537e7ccc45
feat(v2): 将导出功能内建到策略 run() 方法
...
- 修改 StrategyBase.run() 支持 export_detail 参数
- 保存 self._data 供导出方法复用
- 简化 export_backtest_detail.py 从 441 行到 62 行
- 消除策略重复执行,提升运行效率 40%
- API 请求减少 50%(溢价率数据复用)
2026-05-26 01:04:20 +08:00
b9543f0669
chore(env): 更新 Tushare API Token
2026-05-25 23:24:08 +08:00
3d9929904b
config(rotation): 更新回测配置 - 关闭溢价过滤并使用最新数据
...
- 注释掉 end_date,使用最新数据进行回测
- 关闭溢价率过滤 (premium_control.enabled: false)
- 溢价过滤逻辑未实现 (TODO),配置无效
- 避免误导,显式关闭该功能
2026-05-25 23:22:40 +08:00
7844b1ebf0
fix(tushare): QDII基金溢价率计算修复 - ETF类型识别+反向偏移T+2+周末填充
...
实现完整QDII基金溢价率计算修复方案:
1. 新增_get_etf_type()方法:
- 使用Tushare etf_basic接口查询etf_type字段
- 自动识别境内/QDII基金类型
- 缓存机制避免重复查询
2. 修改fetch_etf()方法:
- QDII基金净值范围向前/向后扩大2天
- 确保有足够净值数据进行T+2匹配
3. 修改_calculate_premium_series()方法:
- QDII基金使用反向偏移:价格日期-2天=净值日期
- 周末/节假日填充:使用ffill填充缺失净值
- 境内ETF保持T+0匹配逻辑
验证结果:
- 纳指ETF 513100.SH 5月18日溢价率:3.84%
- 同花顺溢价率:3.84%
- 偏差:0.00% ✅ 完美对齐
修复与同花顺等主流平台一致的QDII基金溢价率计算逻辑。
2026-05-25 22:46:08 +08:00
c79cde5d7f
fix(tushare): 修复ETF复权qfq支持和溢价率获取
...
- 移除fetch()方法中ETF复权路由的qfq硬编码限制
- 修改ETF复权调用从fetch_etf_adj改为fetch_etf
- 确保qfq/hfq模式也能获取净值和溢价率数据
- 溢价率始终基于原始价格计算,不受复权影响
修复前:
- qfq返回404错误
- hfq无溢价率数据
修复后:
- raw/qfq/hfq三种模式均正常
- 所有模式都返回溢价率数据
- 本地测试全部通过
2026-05-25 20:25:29 +08:00
c0195c5bca
refactor(tushare): 合并ETF复权方法,消除冗余设计
...
- 合并 fetch_etf_adj 和 _fetch_etf_adj 为单一方法
- 删除 _fetch_etf_qfq 转发方法
- 减少~26行代码,优化代码结构(从3个方法→1个方法)
- 保持公共接口签名不变,完全向后兼容
- 全面测试通过:raw/qfq/hfq三种模式数据正确
- 更新 VALID_ADJ_BY_TYPE 配置,ETF支持前复权/后复权
2026-05-25 19:59:49 +08:00
a62cfb4cd5
fix: 修复因子前向填充不生效的 bug(清理调试代码)
...
问题根因:
- pandas reindex(method='ffill') 只填充新增行的 NaN,不填充已存在的 NaN
- 当 factor_df 中已有境外市场放假日期的 NaN 值时,reindex 无法填充
修复方案:
- 改为两步操作:reindex() 然后 ffill()
- ffill() 会填充所有 NaN,包括已存在的
验证结果:
- 2026-04-30 HSI: None → 0.2388 ✅
- 2026-04-30 GDAXI: None → 0.5647 ✅
- 2026-05-08 HSI: None → 0.1144 ✅
2026-05-25 19:16:14 +08:00