Files
etf/framework_v2/scripts/convert_to_viewer_csv.py
aszerW e8e4e9c3ac fix: GlobalRotationStrategy select_num 未生效
- 修复分散化选股逻辑:每个 group 选 Top 1 后,需要再按动量排序选 Top select_num
- 之前:所有 group 的 Top 1 都标记为信号(忽略 select_num)
- 现在:先从每个 group 选 Top 1,再从中按动量选 Top select_num 个
- 影响:配置 select_num=3 时,实际持仓 3 只而不是 4 只(group 数量)
2026-05-25 01:23:00 +08:00

159 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
将 V2 回测细节 JSON 转换为 HTML viewer 需要的 CSV 格式。
用途:
- 适配 backtest_viewer.html 的 CSV 格式要求
- 生成 _detail.csv、_close.csv、_holdings.csv、_code_info.csv
用法:
python framework_v2/scripts/convert_to_viewer_csv.py
"""
import sys
import json
from pathlib import Path
import pandas as pd
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
def safe_float(val, decimals=4):
"""安全转换浮点数"""
if val is None:
return None
try:
return round(float(val), decimals)
except (ValueError, TypeError):
return None
def convert_v2_to_viewer_csv(json_path=None):
"""将 V2 JSON 转换为 V1 CSV 格式"""
# 1. 加载 JSON
if json_path is None:
json_path = project_root / 'framework_v2' / 'results' / 'backtest_detail_v2.json'
print(f"加载 V2 JSON: {json_path}")
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
meta = data['meta']
days = data['days']
print(f" 天数: {len(days)}")
print(f" 标的: {len(meta['codes'])}")
# 2. 生成 _detail.csv
print("\n生成 _detail.csv...")
detail_rows = []
for day in days:
row = {
'日期': day['date'],
'净值': day['nav'],
'日收益': day['daily_return'],
'持仓标的': ','.join(day['holdings']) if day['holdings'] else ''
}
# 添加每个标的的动量
for code, asset in day['assets'].items():
row[f'动量_{code}'] = asset.get('momentum')
detail_rows.append(row)
detail_df = pd.DataFrame(detail_rows)
detail_path = json_path.parent / 'backtest_detail_v2_detail.csv'
detail_df.to_csv(detail_path, index=False, encoding='utf-8-sig')
print(f"{detail_path.name} ({len(detail_df)} 行)")
# 3. 生成 _close.csv
print("\n生成 _close.csv...")
close_rows = []
for day in days:
row = {'日期': day['date']}
for code, asset in day['assets'].items():
# ETF 收盘价
etf_close = asset.get('etf_close')
row[f'收盘价_{code}'] = etf_close
close_rows.append(row)
close_df = pd.DataFrame(close_rows)
close_path = json_path.parent / 'backtest_detail_v2_close.csv'
close_df.to_csv(close_path, index=False, encoding='utf-8-sig')
print(f"{close_path.name} ({len(close_df)} 行)")
# 4. 生成 _holdings.csv
print("\n生成 _holdings.csv...")
holdings_rows = []
for day in days:
if not day['holdings']:
continue
for code in day['holdings']:
asset = day['assets'].get(code, {})
code_meta = meta['codes'].get(code, {})
row = {
'日期': day['date'],
'标的代码': code,
'标的名称': code_meta.get('name', code),
'大类': code_meta.get('group', ''),
'进场日期': asset.get('entry_date'),
'进场价': asset.get('entry_price'),
'当前价': asset.get('etf_close'),
'持仓天数': asset.get('holding_days', 0),
'持仓比例': asset.get('weight', 0),
'累计收益': asset.get('cum_return')
}
holdings_rows.append(row)
holdings_df = pd.DataFrame(holdings_rows)
holdings_path = json_path.parent / 'backtest_detail_v2_holdings.csv'
holdings_df.to_csv(holdings_path, index=False, encoding='utf-8-sig')
print(f"{holdings_path.name} ({len(holdings_df)} 行)")
# 5. 生成 _code_info.csv
print("\n生成 _code_info.csv...")
code_info_rows = []
for code, meta_info in meta['codes'].items():
row = {
'代码': code,
'名称': meta_info.get('name', code),
'大类': meta_info.get('group', '')
}
code_info_rows.append(row)
code_info_df = pd.DataFrame(code_info_rows)
code_info_path = json_path.parent / 'backtest_detail_v2_code_info.csv'
code_info_df.to_csv(code_info_path, index=False, encoding='utf-8-sig')
print(f"{code_info_path.name} ({len(code_info_df)} 行)")
# 6. 汇总
print("\n" + "=" * 80)
print("转换完成!")
print("=" * 80)
print(f"\n输出文件:")
print(f" {detail_path.name}")
print(f" {close_path.name}")
print(f" {holdings_path.name}")
print(f" {code_info_path.name}")
print(f"\n使用方法:")
print(f" 1. 打开 visualization/backtest_viewer.html")
print(f" 2. 选择 'V2 GlobalRotationStrategy (最新)'")
print(f" 3. 自动加载 CSV 文件")
if __name__ == '__main__':
convert_v2_to_viewer_csv()