Commit Graph

20 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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