Compare commits
2 Commits
7b229ced14
...
f370caeff9
| Author | SHA1 | Date | |
|---|---|---|---|
| f370caeff9 | |||
| 06df8767b9 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -199,3 +199,9 @@ zhihu-articles/
|
|||||||
|
|
||||||
# Results directory (test outputs, charts, etc.)
|
# Results directory (test outputs, charts, etc.)
|
||||||
results/
|
results/
|
||||||
|
|
||||||
|
# Node.js (local testing only)
|
||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
rotation/test_screenshot.mjs
|
||||||
|
|||||||
132
docs/select_num_1_策略深度分析报告.md
Normal file
132
docs/select_num_1_策略深度分析报告.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# select_num=1 策略深度分析报告
|
||||||
|
|
||||||
|
> 分析日期:2026-06-07
|
||||||
|
> 策略版本:基于 config_simple.yaml,select_num 覆盖为 1
|
||||||
|
> 回测区间:2020-01-10 至今(约 1549 个交易日)
|
||||||
|
|
||||||
|
## 1. 策略概述
|
||||||
|
|
||||||
|
基于 `slope_r2` 单因子的全球资产轮动策略,每次只持有排名第 1 的资产。
|
||||||
|
|
||||||
|
- **因子**:`slope_r2_score` — 归一化价格的线性回归斜率 × R²,回看 25 天
|
||||||
|
- **资产池**:11 个资产(原油、纳指100、日经225、黄金、DAX、有色金属、创业板指、恒生指数、恒生科技、红利低波、短债)
|
||||||
|
- **阈值**:动态阈值,以短债指数为参考
|
||||||
|
|
||||||
|
## 2. 回撤分析
|
||||||
|
|
||||||
|
### 2.1 整体回撤特征
|
||||||
|
|
||||||
|
- 显著回撤(>5%)共 34 次
|
||||||
|
- 水下时间占比 58.9%
|
||||||
|
- 最大回撤集中在原油高波动期间
|
||||||
|
|
||||||
|
### 2.2 主要回撤来源
|
||||||
|
|
||||||
|
策略在原油持仓期间的回撤最为剧烈,原因是原油波动率高(年化 ~45%)且事件驱动性强。频繁调仓在市场压力期进一步放大回撤。
|
||||||
|
|
||||||
|
## 3. 收益归因
|
||||||
|
|
||||||
|
### 3.1 资产贡献度
|
||||||
|
|
||||||
|
| 资产 | 收益贡献 | 持仓天数占比 | 贡献效率 |
|
||||||
|
|------|---------|------------|---------|
|
||||||
|
| 原油 (CL=F) | 62.8% | 18.9% | 极高 |
|
||||||
|
| 纳指100 (NDX) | 正贡献 | 中等 | 中等 |
|
||||||
|
| 创业板指 | 正贡献 | 中等 | 中等 |
|
||||||
|
| 日经225 (N225) | -15.4% | — | 最大拖累 |
|
||||||
|
|
||||||
|
### 3.2 原油收益的可持续性评估
|
||||||
|
|
||||||
|
- **正面**:择时 alpha 真实存在,策略收益约为随机择时的 2 倍
|
||||||
|
- **风险**:约 50% 的原油收益来自事件驱动(如俄乌战争),5 个交易日贡献了原油总收益的 50%
|
||||||
|
- **收益分布**:正偏度(+0.48)+ 厚尾特征
|
||||||
|
- **IC(信息系数)**:仅 0.063,信号质量并不高
|
||||||
|
- **结论**:回测年化 ~43% 中,现实可预期年化约 20-30%
|
||||||
|
|
||||||
|
## 4. 资产对比分析:为什么原油/纳指赚钱,日经赔钱
|
||||||
|
|
||||||
|
### 4.1 原油 — 长持获胜
|
||||||
|
|
||||||
|
- 持仓 >10 天的交易胜率 100%
|
||||||
|
- 正偏度 + 高波动 = 偶尔的大涨贡献绝大部分收益
|
||||||
|
- 趋势性较强,适合动量策略
|
||||||
|
|
||||||
|
### 4.2 纳指100 — 高盈亏比
|
||||||
|
|
||||||
|
- 盈亏比 2.14:1,靠"赚多赔少"取胜
|
||||||
|
- 资产本身具有正期望收益(长期上涨趋势)
|
||||||
|
- 即使因子预测能力为负,持有本身仍能获利
|
||||||
|
|
||||||
|
### 4.3 日经225 — 全面失败
|
||||||
|
|
||||||
|
- 在所有持仓周期(短/中/长)均亏损
|
||||||
|
- 在所有动量水平均表现不佳
|
||||||
|
- 均值回归特征明显,与动量因子方向相反
|
||||||
|
|
||||||
|
## 5. 回看周期敏感性测试
|
||||||
|
|
||||||
|
测试了 10 个回看周期(5d ~ 120d)× 3 个前瞻周期:
|
||||||
|
|
||||||
|
### 5.1 slope_r2 因子 IC 汇总
|
||||||
|
|
||||||
|
| 资产 | 最优回看 | 最优 IC | 典型 IC 范围 |
|
||||||
|
|------|---------|--------|-------------|
|
||||||
|
| 原油 (CL=F) | 20d | 正 | 正(稳健) |
|
||||||
|
| 创业板指 (399006.SZ) | 10d | 正 | 正(稳健) |
|
||||||
|
| 纳指100 (NDX) | — | 负 | 全部为负 |
|
||||||
|
| 日经225 (N225) | 90d | +0.059 | 大部分为负 |
|
||||||
|
| DAX (GDAXI) | — | 负 | 全部或大部分为负 |
|
||||||
|
| 恒生科技 (HSTECH.HK) | — | 负 | 全部为负 |
|
||||||
|
|
||||||
|
### 5.2 关键发现
|
||||||
|
|
||||||
|
- **只有原油和创业板指**对 slope_r2 因子有稳健的正 IC
|
||||||
|
- 纳指、日经、DAX、恒生科技在几乎所有回看周期下 IC 为负
|
||||||
|
- 日经即使在最优 90d 回看下,IC 也仅 +0.059,实用价值有限
|
||||||
|
- 纯收益率动量(不拟合直线)同样对日经/NDX/DAX 失效
|
||||||
|
|
||||||
|
### 5.3 结论
|
||||||
|
|
||||||
|
日经不是"回看周期不对"的问题,而是**资产本身的均值回归特性**与动量因子方向相反。调整回看周期无法根本解决。
|
||||||
|
|
||||||
|
## 6. 单因子局限性分析
|
||||||
|
|
||||||
|
### 6.1 核心问题
|
||||||
|
|
||||||
|
slope_r2 作为唯一切量因子,11 个资产中仅对 2 个有效(正 IC)。策略本质上在用一把只能开两把锁的钥匙去开 11 把锁。
|
||||||
|
|
||||||
|
### 6.2 资产行为分类
|
||||||
|
|
||||||
|
根据 Hurst 指数和 IC 分析,资产池可分为两类:
|
||||||
|
|
||||||
|
| 类型 | 特征 | 代表资产 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 趋势型 | H > 0.5,动量 IC 正 | 原油、创业板指 |
|
||||||
|
| 均值回归型 | H < 0.5,动量 IC 负 | 日经、DAX、恒生科技 |
|
||||||
|
| 混合型 | 特征不显著 | 纳指、黄金、恒生指数 |
|
||||||
|
|
||||||
|
### 6.3 可行的多因子方向
|
||||||
|
|
||||||
|
| 因子类型 | 适用场景 | 对应资产 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| 均值回归因子(短期反转) | IC 为负的资产 | 日经、DAX、恒生科技 |
|
||||||
|
| 波动率因子(低波动异象) | 防御期选股 | 全资产 |
|
||||||
|
| 趋势质量因子(ADX/均线排列) | 区分真假动量 | 原油、创业板 |
|
||||||
|
| 风险动量(收益/波动率) | 替代纯动量 | 纳指(高收益高波动) |
|
||||||
|
|
||||||
|
### 6.4 建议方案
|
||||||
|
|
||||||
|
**按资产特性分配因子**,而非统一因子:
|
||||||
|
- 趋势型资产(原油、创业板)→ 动量因子
|
||||||
|
- 均值回归型资产(日经、DAX)→ 反转因子
|
||||||
|
- 这比简单叠加多因子效果更精准
|
||||||
|
|
||||||
|
## 7. 总结与建议
|
||||||
|
|
||||||
|
1. **select_num=1 的收益高度集中于原油**(62.8%),且约半数为事件驱动,可持续性存疑
|
||||||
|
2. **日经225 是最大拖累**(-15.4% 贡献),根本原因是均值回归特性与动量因子矛盾
|
||||||
|
3. **单因子 slope_r2 覆盖面不足**,仅 2/11 资产有效
|
||||||
|
4. **下一步优化方向**:
|
||||||
|
- 短期:为均值回归类资产加入反转因子,或直接从池中移除不适合的资产
|
||||||
|
- 中期:实现资产级因子自适应选择(根据 Hurst 指数自动分配动量/反转)
|
||||||
|
- 长期:引入波动率因子作为风控层,在高波动期切换至防御资产
|
||||||
@@ -152,11 +152,15 @@ tr.below-threshold { color: #484f58; }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let DATA = null;
|
// Use window. to allow external access (for Playwright screenshot injection)
|
||||||
let currentIdx = 0;
|
window.DATA = null;
|
||||||
let playing = false;
|
window.currentIdx = 0;
|
||||||
let playTimer = null;
|
window.playing = false;
|
||||||
let rebalanceDays = [];
|
window.playTimer = null;
|
||||||
|
window.rebalanceDays = [];
|
||||||
|
|
||||||
|
// Convenience aliases for script-internal use
|
||||||
|
const DATA_PROXY = { get data() { return window.DATA; }, set data(v) { window.DATA = v; } };
|
||||||
|
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
|||||||
285
rotation/html_report.py
Normal file
285
rotation/html_report.py
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
"""
|
||||||
|
HTML Report Screenshot Generator
|
||||||
|
|
||||||
|
Uses Playwright to render backtest_viewer.html in headless Chromium
|
||||||
|
and capture a full-page screenshot as PNG.
|
||||||
|
|
||||||
|
Intended to be called after export_results() has produced simple_rotation_detail.json.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def compute_monthly_returns(daily_records: List[Dict]) -> List[Dict]:
|
||||||
|
"""Compute monthly cumulative returns from daily_records.
|
||||||
|
|
||||||
|
Returns list of {year, month, return_pct, trading_days} dicts.
|
||||||
|
"""
|
||||||
|
if not daily_records:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Group by (year, month)
|
||||||
|
monthly: Dict[str, List[float]] = {}
|
||||||
|
for rec in daily_records:
|
||||||
|
date_str = rec['date']
|
||||||
|
ym = date_str[:7] # "2024-01"
|
||||||
|
if ym not in monthly:
|
||||||
|
monthly[ym] = []
|
||||||
|
monthly[ym].append(rec['nav'])
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for ym in sorted(monthly.keys()):
|
||||||
|
navs = monthly[ym]
|
||||||
|
if len(navs) < 1:
|
||||||
|
continue
|
||||||
|
# Monthly return = last_nav / prev_last_nav - 1
|
||||||
|
# prev_last_nav = last nav of previous month, or first nav of this month's start
|
||||||
|
# More accurate: use the nav just before the first day of this month
|
||||||
|
results.append({
|
||||||
|
'year_month': ym,
|
||||||
|
'start_nav': navs[0],
|
||||||
|
'end_nav': navs[-1],
|
||||||
|
'trading_days': len(navs),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Compute returns sequentially
|
||||||
|
for i, r in enumerate(results):
|
||||||
|
if i == 0:
|
||||||
|
# First month: return relative to first NAV (which is usually ~1.0)
|
||||||
|
# Use previous day's nav if available, otherwise start_nav itself
|
||||||
|
r['return_pct'] = 0.0 # first month baseline
|
||||||
|
else:
|
||||||
|
r['return_pct'] = r['start_nav'] / results[i-1]['end_nav'] - 1
|
||||||
|
r['month_return'] = r['end_nav'] / r['start_nav'] - 1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def generate_html_screenshot(
|
||||||
|
detail_json_path: str,
|
||||||
|
output_path: str,
|
||||||
|
target_date: Optional[str] = None,
|
||||||
|
viewport_width: int = 1920,
|
||||||
|
viewport_height: int = 1080,
|
||||||
|
show_monthly_table: bool = True,
|
||||||
|
daily_records: Optional[List[Dict]] = None,
|
||||||
|
):
|
||||||
|
"""Render backtest_viewer.html and capture screenshot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detail_json_path: Path to simple_rotation_detail.json
|
||||||
|
output_path: Output PNG path
|
||||||
|
target_date: Date string to navigate to (YYYY-MM-DD). None = last day.
|
||||||
|
viewport_width: Browser viewport width
|
||||||
|
viewport_height: Browser viewport height (minimum, will expand for full page)
|
||||||
|
show_monthly_table: Whether to inject monthly returns table
|
||||||
|
daily_records: List of daily record dicts (needed for monthly table)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
except ImportError:
|
||||||
|
print(" x Playwright not installed. Run: pip install playwright && playwright install chromium")
|
||||||
|
return
|
||||||
|
|
||||||
|
detail_path = Path(detail_json_path)
|
||||||
|
if not detail_path.exists():
|
||||||
|
print(f" x Detail JSON not found: {detail_json_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
output = Path(output_path)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
html_path = Path(__file__).parent / 'backtest_viewer.html'
|
||||||
|
if not html_path.exists():
|
||||||
|
print(f" x backtest_viewer.html not found: {html_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(detail_path, 'r', encoding='utf-8') as f:
|
||||||
|
detail_data = json.load(f)
|
||||||
|
|
||||||
|
print(f" Launching headless Chromium...")
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=True)
|
||||||
|
page = browser.new_page(viewport={'width': viewport_width, 'height': viewport_height})
|
||||||
|
|
||||||
|
# Load the HTML file
|
||||||
|
page.goto(f'file://{html_path.absolute()}')
|
||||||
|
page.wait_for_selector('#loader')
|
||||||
|
|
||||||
|
# Inject data directly (skip file input)
|
||||||
|
page.evaluate(f"""
|
||||||
|
(data) => {{
|
||||||
|
DATA = data;
|
||||||
|
document.getElementById('loader').style.display = 'none';
|
||||||
|
document.getElementById('app').classList.add('active');
|
||||||
|
|
||||||
|
// Build rebalance day index
|
||||||
|
rebalanceDays = [];
|
||||||
|
DATA.days.forEach((d, i) => {{ if (d.is_rebalance) rebalanceDays.push(i); }});
|
||||||
|
|
||||||
|
// Set date picker range
|
||||||
|
document.getElementById('date-picker').min = DATA.days[0].date;
|
||||||
|
document.getElementById('date-picker').max = DATA.days[DATA.days.length - 1].date;
|
||||||
|
}}
|
||||||
|
""", detail_data)
|
||||||
|
|
||||||
|
# Navigate to target date
|
||||||
|
if target_date:
|
||||||
|
page.evaluate(f"""
|
||||||
|
(targetDate) => {{
|
||||||
|
const idx = DATA.days.findIndex(d => d.date === targetDate);
|
||||||
|
if (idx >= 0) {{
|
||||||
|
currentIdx = idx;
|
||||||
|
render(currentIdx);
|
||||||
|
drawChart();
|
||||||
|
drawChartMarker();
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
""", target_date)
|
||||||
|
else:
|
||||||
|
# Navigate to last day
|
||||||
|
page.evaluate("""
|
||||||
|
() => {
|
||||||
|
currentIdx = DATA.days.length - 1;
|
||||||
|
render(currentIdx);
|
||||||
|
drawChart();
|
||||||
|
drawChartMarker();
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Wait for rendering
|
||||||
|
page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# Inject monthly returns table if requested
|
||||||
|
if show_monthly_table and daily_records:
|
||||||
|
monthly = compute_monthly_returns(daily_records)
|
||||||
|
if monthly:
|
||||||
|
_inject_monthly_table(page, monthly)
|
||||||
|
page.wait_for_timeout(300)
|
||||||
|
|
||||||
|
# Take full page screenshot
|
||||||
|
page.screenshot(path=str(output), full_page=True)
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
print(f" + HTML Report Screenshot: {output}")
|
||||||
|
|
||||||
|
|
||||||
|
def _inject_monthly_table(page, monthly_data: List[Dict]):
|
||||||
|
"""Inject monthly returns table into the page."""
|
||||||
|
|
||||||
|
# Build HTML for monthly table
|
||||||
|
rows_html = ""
|
||||||
|
for r in monthly_data:
|
||||||
|
ret = r['month_return']
|
||||||
|
cls = 'positive' if ret >= 0 else 'negative'
|
||||||
|
rows_html += f"""
|
||||||
|
<tr>
|
||||||
|
<td>{r['year_month']}</td>
|
||||||
|
<td style="color:{'#3fb950' if ret >= 0 else '#da3633'};font-weight:600">
|
||||||
|
{(ret * 100):+.2f}%
|
||||||
|
</td>
|
||||||
|
<td>{r['end_nav']:.4f}</td>
|
||||||
|
<td>{r['trading_days']}</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Also compute yearly returns
|
||||||
|
yearly: Dict[str, float] = {}
|
||||||
|
yearly_navs: Dict[str, float] = {} # last nav of each year
|
||||||
|
for r in monthly_data:
|
||||||
|
year = r['year_month'][:4]
|
||||||
|
yearly[year] = r['end_nav']
|
||||||
|
yearly_list = sorted(yearly.keys())
|
||||||
|
yearly_rows = ""
|
||||||
|
for i, year in enumerate(yearly_list):
|
||||||
|
if i == 0:
|
||||||
|
yr_ret = 0.0
|
||||||
|
else:
|
||||||
|
prev_year = yearly_list[i-1]
|
||||||
|
yr_ret = yearly[year] / yearly[prev_year] - 1
|
||||||
|
cls_color = '#3fb950' if yr_ret >= 0 else '#da3633'
|
||||||
|
yearly_rows += f"""
|
||||||
|
<tr>
|
||||||
|
<td>{year}</td>
|
||||||
|
<td style="color:{cls_color};font-weight:600">{(yr_ret * 100):+.2f}%</td>
|
||||||
|
<td>{yearly[year]:.4f}</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
html = f"""
|
||||||
|
<div id="monthly-panel" style="
|
||||||
|
padding: 16px;
|
||||||
|
background: #161b22;
|
||||||
|
border-top: 1px solid #30363d;
|
||||||
|
">
|
||||||
|
<div style="display: flex; gap: 32px; align-items: flex-start;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<h3 style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">
|
||||||
|
月度累计收益
|
||||||
|
</h3>
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:12px;font-family:monospace;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0d1117;">
|
||||||
|
<th style="text-align:left;padding:6px 8px;color:#8b949e;border-bottom:1px solid #30363d;">月份</th>
|
||||||
|
<th style="text-align:right;padding:6px 8px;color:#8b949e;border-bottom:1px solid #30363d;">月收益</th>
|
||||||
|
<th style="text-align:right;padding:6px 8px;color:#8b949e;border-bottom:1px solid #30363d;">月末净值</th>
|
||||||
|
<th style="text-align:right;padding:6px 8px;color:#8b949e;border-bottom:1px solid #30363d;">交易天数</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows_html}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div style="min-width: 240px;">
|
||||||
|
<h3 style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">
|
||||||
|
年度收益
|
||||||
|
</h3>
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:12px;font-family:monospace;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0d1117;">
|
||||||
|
<th style="text-align:left;padding:6px 8px;color:#8b949e;border-bottom:1px solid #30363d;">年度</th>
|
||||||
|
<th style="text-align:right;padding:6px 8px;color:#8b949e;border-bottom:1px solid #30363d;">年收益</th>
|
||||||
|
<th style="text-align:right;padding:6px 8px;color:#8b949e;border-bottom:1px solid #30363d;">年末净值</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{yearly_rows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Append to app container
|
||||||
|
page.evaluate(f"""
|
||||||
|
(htmlContent) => {{
|
||||||
|
const app = document.getElementById('app');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerHTML = htmlContent;
|
||||||
|
app.appendChild(div);
|
||||||
|
}}
|
||||||
|
""", html)
|
||||||
|
|
||||||
|
# Apply row borders
|
||||||
|
page.evaluate("""
|
||||||
|
() => {
|
||||||
|
document.querySelectorAll('#monthly-panel td').forEach(td => {
|
||||||
|
td.style.borderBottom = '1px solid #21262d';
|
||||||
|
td.style.padding = '5px 8px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test: run with existing detail JSON
|
||||||
|
import sys
|
||||||
|
detail = sys.argv[1] if len(sys.argv) > 1 else "rotation/results/simple_rotation_detail.json"
|
||||||
|
output = sys.argv[2] if len(sys.argv) > 2 else "rotation/results/simple_rotation_html_report.png"
|
||||||
|
generate_html_screenshot(detail, output)
|
||||||
@@ -1418,6 +1418,37 @@ class SimpleRotationStrategy:
|
|||||||
plt.close()
|
plt.close()
|
||||||
print(f" + Report: {chart_path}")
|
print(f" + Report: {chart_path}")
|
||||||
|
|
||||||
|
def generate_html_report(self, output_dir: str = None, target_date: str = None):
|
||||||
|
"""Generate HTML-based report screenshot using Playwright.
|
||||||
|
|
||||||
|
Renders backtest_viewer.html in headless Chromium and captures
|
||||||
|
the ranking table + monthly returns as a PNG.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_dir: Directory to save the PNG (default: rotation/results)
|
||||||
|
target_date: Date to navigate to (YYYY-MM-DD). None = last day.
|
||||||
|
"""
|
||||||
|
from rotation.html_report import generate_html_screenshot
|
||||||
|
|
||||||
|
if output_dir is None:
|
||||||
|
output_dir = Path(__file__).parent / 'results'
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
detail_path = output_dir / 'simple_rotation_detail.json'
|
||||||
|
if not detail_path.exists():
|
||||||
|
print(f" x Detail JSON not found: {detail_path}")
|
||||||
|
print(" Run export_results() first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
output_path = output_dir / 'simple_rotation_html_report.png'
|
||||||
|
generate_html_screenshot(
|
||||||
|
detail_json_path=str(detail_path),
|
||||||
|
output_path=str(output_path),
|
||||||
|
target_date=target_date,
|
||||||
|
daily_records=self.daily_records,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Entry point
|
# Entry point
|
||||||
@@ -1433,3 +1464,4 @@ if __name__ == "__main__":
|
|||||||
if result:
|
if result:
|
||||||
strategy.export_results()
|
strategy.export_results()
|
||||||
strategy.generate_report()
|
strategy.generate_report()
|
||||||
|
strategy.generate_html_report()
|
||||||
|
|||||||
Reference in New Issue
Block a user