#!/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()