- 修复分散化选股逻辑:每个 group 选 Top 1 后,需要再按动量排序选 Top select_num - 之前:所有 group 的 Top 1 都标记为信号(忽略 select_num) - 现在:先从每个 group 选 Top 1,再从中按动量选 Top select_num 个 - 影响:配置 select_num=3 时,实际持仓 3 只而不是 4 只(group 数量)
159 lines
4.8 KiB
Python
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()
|