refactor(archive): move unused modules to archive/

Archive legacy framework and utility modules that are no longer
referenced by the active core (datasource/ and rotation/):

- framework/ -> archive/framework/
- framework_v2/ -> archive/framework_v2/
- strategies/ -> archive/strategies/
- config/ -> archive/config/
- visualization/ -> archive/visualization/
- scripts/ -> archive/scripts/
- tests/ -> archive/tests/
- run_rotation.py, run_us_rotation.py -> archive/single_files/
- compare_*.py, test_api_dates.py -> archive/single_files/
This commit is contained in:
2026-06-03 23:41:46 +08:00
parent d700bc1dfd
commit c905230a40
98 changed files with 0 additions and 714 deletions

View File

@@ -0,0 +1,127 @@
"""
全球资产大类轮动策略回测脚本V2 正式版)
支持功能:
- 信号-交易分离(指数信号 → ETF收益
- 强制分散化选股(每个 group 只选 1 个)
- 动态短债阈值(标的动量 < 短债动量 → 不持有)
- 溢价过滤(避免买入高溢价 ETF
- 调仓控制rebalance_days + rebalance_threshold
- 交易成本计算trade_cost: 0.1%
用法:
python framework_v2/scripts/backtest_global_rotation.py
"""
import sys
from pathlib import Path
# 添加项目根目录到 Python 路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from framework_v2.config import load_config
from framework_v2.strategies.rotation.rotation import GlobalRotationStrategy
def run_backtest():
"""运行回测"""
print("=" * 70)
print(" 全球资产大类轮动策略回测V2 正式版)")
print(" 场景:指数信号 → ETF收益完整功能")
print("=" * 70)
# 加载配置
config_file = project_root / "framework_v2" / "strategies" / "rotation" / "config_simple.yaml"
print(f"\n配置文件: {config_file}")
config = load_config(str(config_file))
# 打印配置摘要
print("\n" + "=" * 70)
print(" 配置摘要")
print("=" * 70)
print(f"策略名称: {config.metadata.strategy}")
print(f"回测区间: {config.backtest.start_date} ~ {config.backtest.end_date or '至今'}")
print(f"因子类型: {config.factor.type.value}")
print(f"动量窗口: {config.factor.n_days}")
print(f"选股数量: {config.rotation.select_num}")
print(f"强制分散: {config.rotation.diversified}")
# 打印策略参数
rotation_config = config.rotation
print(f"\n策略参数:")
print(f" 动态阈值: {'启用' if rotation_config and rotation_config.threshold and rotation_config.threshold.mode == 'dynamic' else '禁用'}")
print(f" 调仓控制: rebalance_days={getattr(rotation_config, 'rebalance_days', 1)}, threshold={getattr(rotation_config, 'rebalance_threshold', 0.0)}")
print(f" 交易成本: {getattr(config.backtest, 'trade_cost', 0.001):.2%}")
print(f" 溢价控制: {'启用' if hasattr(config, 'premium_control') and config.premium_control.enabled else '禁用'}")
# 打印资产池
print(f"\n资产池 ({config.asset_pools.count()} 个标的):")
groups = config.asset_pools.by_group
for group_name, assets in groups.items():
print(f" [{group_name}] {len(assets)} 个标的:")
for code, asset in assets.items():
print(f" {code}: {asset.name}")
print(f" 信号: {asset.signal_source}, 交易: {asset.trade_source}")
print(f" 跨市场: {'' if asset.is_cross_market else ''}")
# 创建策略
print("\n" + "=" * 70)
print(" 运行回测...")
print("=" * 70)
strategy = GlobalRotationStrategy(config)
# 运行回测并导出 detail
output_dir = project_root / "framework_v2" / "results"
output_dir.mkdir(exist_ok=True)
detail_path = output_dir / "backtest_detail_v2.json"
print(f"\n导出 detail JSON: {detail_path}")
result = strategy.run(
export_detail=True,
detail_path=str(detail_path)
)
# 打印结果
print("\n" + "=" * 70)
print(" 回测结果")
print("=" * 70)
metrics = result['metrics']
print(f"总收益: {metrics['total_return']:.2%}")
print(f"年化收益: {metrics['annual_return']:.2%}")
print(f"最大回撤: {metrics['max_drawdown']:.2%}")
print(f"夏普比率: {metrics['sharpe_ratio']:.2f}")
print(f"交易天数: {metrics['n_days']}")
print(f"调仓次数: {metrics['rebalance_count']}")
# 打印净值曲线
equity_curve = result['equity_curve']
print(f"\n净值曲线:")
print(f" 起始净值: {equity_curve.iloc[0]:.4f}")
print(f" 结束净值: {equity_curve.iloc[-1]:.4f}")
print(f" 数据点数: {len(equity_curve)}")
# 保存结果
output_dir = project_root / "framework_v2" / "results"
output_dir.mkdir(exist_ok=True)
# 保存净值曲线
equity_curve.to_csv(output_dir / "global_rotation_equity.csv")
print(f"\n净值曲线已保存: {output_dir / 'global_rotation_equity.csv'}")
# 保存持仓记录
positions = result['positions']
positions.to_csv(output_dir / "global_rotation_positions.csv")
print(f"持仓记录已保存: {output_dir / 'global_rotation_positions.csv'}")
print("\n" + "=" * 70)
print(" 回测完成!")
print("=" * 70)
if __name__ == "__main__":
run_backtest()

View File

@@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""
对比指数收益 vs ETF 收益的 KPI 指标
从 backtest_detail_v2.json 中提取:
1. 策略实际持仓
2. 各标的当日指数收益率
3. 各标的当日 ETF 收益率
分别计算两种收益模式下的 KPI
- 指数收益模式:使用 index_return 计算策略净值
- ETF 收益模式:使用 etf_return_ctc 计算策略净值
"""
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
def load_detail_json(json_path: str) -> dict:
"""加载 detail JSON"""
print(f"[1] 加载 JSON: {json_path}")
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f" 天数: {len(data['days'])}")
print(f" 标的: {len(data['meta']['codes'])}")
return data
def calculate_returns_from_detail(data: dict, return_field: str, trade_cost: float = 0.001) -> tuple:
"""
从 detail JSON 计算策略收益率(与 rotation.py 完全对齐)
逻辑(与 rotation.py 第 354-362 行一致):
1. T+1 执行:今天的持仓信号,明天才产生收益
2. 等权分配仓位
3. 扣除交易成本0.1%
Args:
data: detail JSON 数据
return_field: 收益率字段名 ('index_return''etf_return_ctc')
trade_cost: 交易成本(默认 0.1%
Returns:
(策略收益率序列, 调仓次数)
"""
dates = []
strategy_returns = []
positions = [] # 记录每日仓位,用于计算调仓次数
for i, day in enumerate(data['days']):
date = day['date']
holdings = day['holdings']
dates.append(pd.Timestamp(date))
if not holdings:
# 空仓
positions.append({})
if i == 0:
strategy_returns.append(0.0)
else:
# T+1昨天空仓今天收益为 0
strategy_returns.append(0.0)
else:
# 记录仓位(等权)
n_holdings = len(holdings)
pos = {code: 1.0 / n_holdings for code in holdings}
positions.append(pos)
if i == 0:
# 第一天T+1 执行,收益为 0
strategy_returns.append(0.0)
else:
# T+1 执行:用昨天的仓位 × 今天的收益率
daily_return = 0.0
for code, weight in positions[i-1].items():
if code in day['assets']:
asset = day['assets'][code]
ret = asset.get(return_field, 0.0)
if ret is None:
ret = 0.0
daily_return += weight * ret
strategy_returns.append(daily_return)
# 转换为 Series
returns_series = pd.Series(strategy_returns, index=dates, name='strategy_returns')
# 计算调仓次数(与 rotation.py 第 425 行一致)
# 检测持仓变化
rebalance_count = 0
for i in range(1, len(positions)):
if positions[i] != positions[i-1]:
rebalance_count += 1
# 扣除交易成本(与 rotation.py 第 429 行一致)
if trade_cost > 0 and rebalance_count > 0:
# 检测调仓日
position_changes = []
for i in range(1, len(positions)):
position_changes.append(positions[i] != positions[i-1])
position_changes.insert(0, False) # 第一天
# 在调仓日扣除成本
for i, is_change in enumerate(position_changes):
if is_change:
returns_series.iloc[i] -= trade_cost
return returns_series, rebalance_count
def calculate_kpi(strategy_returns: pd.Series, mode_name: str, rebalance_count: int) -> dict:
"""
计算 KPI 指标(与 rotation.py 第 383-394 行完全一致)
Args:
strategy_returns: 策略收益率序列
mode_name: 模式名称(用于打印)
rebalance_count: 调仓次数
Returns:
KPI 字典
"""
# 净值曲线(与 rotation.py 第 365 行一致)
equity_curve = (1 + strategy_returns).cumprod()
# 总收益(与 rotation.py 第 384 行一致)
total_return = equity_curve.iloc[-1] / equity_curve.iloc[0] - 1
# 年化收益(与 rotation.py 第 385-386 行一致,使用 252 天)
n_days = len(strategy_returns)
annual_return = (1 + total_return) ** (252 / n_days) - 1 if n_days > 0 else 0
# 最大回撤(与 rotation.py 第 388-391 行一致)
cumulative_max = equity_curve.cummax()
drawdown = (equity_curve - cumulative_max) / cumulative_max
max_drawdown = drawdown.min()
# 夏普比率(与 rotation.py 第 394 行一致,使用 252 天)
sharpe = (strategy_returns.mean() / strategy_returns.std() * np.sqrt(252)
if strategy_returns.std() > 0 else 0)
kpi = {
'mode': mode_name,
'total_return': total_return,
'annual_return': annual_return,
'max_drawdown': max_drawdown,
'sharpe_ratio': sharpe,
'n_days': n_days,
'rebalance_count': rebalance_count,
'final_nav': equity_curve.iloc[-1],
}
return kpi
def print_kpi_comparison(index_kpi: dict, etf_kpi: dict):
"""打印 KPI 对比表"""
print("\n" + "=" * 80)
print(" KPI 指标对比:指数收益 vs ETF 收益")
print("=" * 80)
# 表头
print(f"\n{'指标':<20} {'指数收益':>15} {'ETF 收益':>15} {'差异':>15}")
print("-" * 80)
# 数据行
metrics = [
('总收益', 'total_return', '{:.2%}'),
('年化收益', 'annual_return', '{:.2%}'),
('最大回撤', 'max_drawdown', '{:.2%}'),
('夏普比率', 'sharpe_ratio', '{:.2f}'),
('最终净值', 'final_nav', '{:.4f}'),
('交易天数', 'n_days', '{:.0f}'),
('调仓次数', 'rebalance_count', '{:.0f}'),
]
for label, key, fmt in metrics:
idx_val = index_kpi[key]
etf_val = etf_kpi[key]
diff = etf_val - idx_val
# 特殊处理百分比格式的差异
if 'return' in key or 'drawdown' in key:
diff_fmt = '{:+.2%}'.format(diff)
else:
diff_fmt = '{:+.2f}'.format(diff) if key != 'n_days' and key != 'rebalance_count' else '{:+.0f}'.format(diff)
print(f"{label:<20} {fmt.format(idx_val):>15} {fmt.format(etf_val):>15} {diff_fmt:>15}")
print("=" * 80)
def save_comparison_csv(index_returns: pd.Series, etf_returns: pd.Series, output_path: str):
"""保存对比数据到 CSV"""
df = pd.DataFrame({
'date': index_returns.index,
'index_return': index_returns.values,
'etf_return': etf_returns.values,
'index_nav': (1 + index_returns).cumprod().values,
'etf_nav': (1 + etf_returns).cumprod().values,
})
df['nav_diff'] = df['etf_nav'] - df['index_nav']
df['return_diff'] = df['etf_return'] - df['index_return']
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(output_path, index=False)
print(f"\n[3] 对比数据已保存: {output_path}")
print(f" 行数: {len(df)}")
def main():
print("=" * 80)
print(" 指数收益 vs ETF 收益 KPI 对比分析")
print("=" * 80)
# 1. 加载数据
json_path = project_root / 'framework_v2' / 'results' / 'backtest_detail_v2.json'
if not json_path.exists():
print(f"错误: JSON 文件不存在: {json_path}")
sys.exit(1)
data = load_detail_json(str(json_path))
# 2. 计算两种模式的收益率
print("\n[2] 计算收益率(与 rotation.py 完全对齐)...")
print(" - 指数收益模式 (index_return)")
print(" T+1 执行 + 等权仓位 + 交易成本 0.1%")
index_returns, index_rebalance = calculate_returns_from_detail(data, 'index_return', trade_cost=0.001)
print(f" 调仓次数: {index_rebalance}")
print(" - ETF 收益模式 (etf_return_ctc)")
print(" T+1 执行 + 等权仓位 + 交易成本 0.1%")
etf_returns, etf_rebalance = calculate_returns_from_detail(data, 'etf_return_ctc', trade_cost=0.001)
print(f" 调仓次数: {etf_rebalance}")
# 3. 计算 KPI
print("\n计算 KPI 指标(使用 252 天/年,与 rotation.py 一致)...")
index_kpi = calculate_kpi(index_returns, '指数收益', index_rebalance)
etf_kpi = calculate_kpi(etf_returns, 'ETF 收益', etf_rebalance)
# 4. 打印对比
print_kpi_comparison(index_kpi, etf_kpi)
# 5. 保存 CSV
csv_path = project_root / 'framework_v2' / 'results' / 'kpi_comparison_index_vs_etf.csv'
save_comparison_csv(index_returns, etf_returns, str(csv_path))
# 6. 分析差异来源
print("\n[4] 差异分析...")
daily_diff = (etf_returns - index_returns).abs()
large_diff_days = (daily_diff > 0.001).sum() # 差异 > 0.1%
print(f" 差异 > 0.1% 的天数: {large_diff_days} / {len(index_returns)}")
print(f" 平均日差异: {daily_diff.mean():.6f} ({daily_diff.mean()*100:.4f}%)")
print(f" 最大日差异: {daily_diff.max():.6f} ({daily_diff.max()*100:.4f}%)")
print("\n" + "=" * 80)
print(" 分析完成!")
print("=" * 80)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,158 @@
#!/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()

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
导出 V2 框架回测逐日明细到 JSON简化版
现在直接调用 strategy.run(export_detail=True)
不再重复执行策略逻辑
用法:
python framework_v2/scripts/export_backtest_detail.py
"""
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from dotenv import load_dotenv
load_dotenv()
from framework_v2.config import load_config
from framework_v2.strategies.rotation.rotation import GlobalRotationStrategy
def main():
print("=" * 80)
print(" V2 回测逐日明细导出GlobalRotationStrategy")
print("=" * 80)
# 1. 加载配置
config_file = project_root / 'framework_v2' / 'strategies' / 'rotation' / 'config_simple.yaml'
print(f"\n[1] 加载配置: {config_file}")
config = load_config(str(config_file))
# 2. 初始化策略
print("[2] 初始化策略...")
strategy = GlobalRotationStrategy(config)
# 3. 运行策略并导出明细
output_path = project_root / 'framework_v2' / 'results' / 'backtest_detail_v2.json'
print("[3] 运行策略并导出明细...")
result = strategy.run(
export_detail=True,
detail_path=str(output_path)
)
# 4. 打印汇总
print("\n" + "=" * 80)
print(" 回测汇总")
print("=" * 80)
print(f" 总收益: {result['metrics']['total_return'] * 100:.2f}%")
print(f" 年化收益: {result['metrics']['annual_return'] * 100:.2f}%")
print(f" 最大回撤: {result['metrics']['max_drawdown'] * 100:.2f}%")
print(f" 夏普比率: {result['metrics']['sharpe_ratio']:.2f}")
print(f" 调仓次数: {result['metrics']['rebalance_count']}")
print(f" 交易天数: {result['metrics']['n_days']}")
print(f" 输出文件: {output_path}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
测算 ETF 跳空收益Gap Return对策略的影响
测算目标:
1. 量化各 ETF 的跳空特征(幅度、频率、波动率)
2. 分析跳空对策略收益的实际影响
3. 判断是否需要修改收益计算逻辑
用法:
python framework_v2/scripts/measure_gap_impact.py
"""
import sys
from pathlib import Path
import numpy as np
import pandas as pd
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from dotenv import load_dotenv
load_dotenv()
from framework_v2.config import load_config
from framework_v2.strategies.rotation.rotation import GlobalRotationStrategy
from framework_v2.shared.data import FlaskAPIFetcher
def fetch_etf_data_with_ohlc(codes, start, end):
"""获取 ETF 的 OHLC 数据hfq"""
fetcher = FlaskAPIFetcher()
print(f"\n[数据获取] 获取 {len(codes)} 只 ETF 的 OHLC 数据hfq...")
data = {}
for i, code in enumerate(codes, 1):
print(f" [{i}/{len(codes)}] {code}...")
df = fetcher._source.fetch(
code=code,
start_date=start,
end_date=end,
adj='hfq',
asset_type='china_etf'
)
if df is not None:
data[code] = df
print(f"{len(df)}")
else:
print(f" ✗ 获取失败")
return data
def calculate_gap_statistics(etf_data):
"""计算各 ETF 的跳空统计"""
print("\n" + "=" * 80)
print(" 跳空收益统计分析")
print("=" * 80)
stats_list = []
for code, df in etf_data.items():
# 确保按日期排序
df = df.sort_index()
# 计算收益率
prev_close = df['close'].shift(1)
# 跳空收益率:(T_open - T-1_close) / T-1_close
gap_return = (df['open'] - prev_close) / prev_close
# 日内收益率:(T_close - T_open) / T_open
intraday_return = (df['close'] - df['open']) / df['open']
# 验证:总收益率 ≈ 跳空 + 日内
total_return = df['close'].pct_change()
# 统计指标
stats = {
'ETF': code,
'数据天数': len(df),
'平均跳空(%)': gap_return.mean() * 100,
'跳空波动率(%)': gap_return.std() * 100,
'向上跳空比例(%)': (gap_return > 0.0001).sum() / len(gap_return) * 100,
'向下跳空比例(%)': (gap_return < -0.0001).sum() / len(gap_return) * 100,
'最大向上跳空(%)': gap_return.max() * 100,
'最大向下跳空(%)': gap_return.min() * 100,
'平均日内收益(%)': intraday_return.mean() * 100,
'日内波动率(%)': intraday_return.std() * 100,
'跳空>1%天数': (gap_return.abs() > 0.01).sum(),
'跳空>2%天数': (gap_return.abs() > 0.02).sum(),
}
stats_list.append(stats)
# 转换为 DataFrame
stats_df = pd.DataFrame(stats_list)
# 打印统计表格
print("\n各 ETF 跳空收益统计:")
print("-" * 80)
for _, row in stats_df.iterrows():
print(f"\n{row['ETF']}:")
print(f" 数据天数: {row['数据天数']}")
print(f" 平均跳空: {row['平均跳空(%)']:+.3f}% (波动率: {row['跳空波动率(%)']:.2f}%)")
print(f" 向上跳空: {row['向上跳空比例(%)']:.1f}% 向下: {row['向下跳空比例(%)']:.1f}%")
print(f" 最大跳空: +{row['最大向上跳空(%)']:.2f}% / {row['最大向下跳空(%)']:.2f}%")
print(f" 跳空>1%: {row['跳空>1%天数']}天 >2%: {row['跳空>2%天数']}")
print(f" 平均日内收益: {row['平均日内收益(%)']:+.3f}%")
return stats_df
def analyze_strategy_gap_impact(strategy, etf_data):
"""分析跳空对策略的实际影响"""
print("\n" + "=" * 80)
print(" 策略跳空影响分析")
print("=" * 80)
# 1. 获取策略持仓数据
print("\n[1] 获取策略持仓数据...")
# 运行策略获取信号和仓位
from datetime import date
config = strategy.config
start = config.backtest.start_date
end = config.backtest.end_date
if end is None:
end = date.today().strftime('%Y-%m-%d')
# 运行策略(不导出 JSON
result = strategy.run(export_detail=False)
positions = result['positions']
trading_calendar = positions.index
# 2. 计算新旧两种收益
print("\n[2] 计算两种收益方法...")
signal_to_trade = config.asset_pools.get_signal_to_trade_mapping()
# 准备数据
close_dict = {}
open_dict = {}
for signal_code, trade_code in signal_to_trade.items():
if trade_code in etf_data:
df = etf_data[trade_code]
# 对齐到 A 股日历
close_dict[signal_code] = df['close'].reindex(trading_calendar, method='ffill')
open_dict[signal_code] = df['open'].reindex(trading_calendar, method='ffill')
close_df = pd.DataFrame(close_dict)
open_df = pd.DataFrame(open_dict)
# 方法 1旧方法close-to-close
positions_delayed = positions.shift(1).fillna(0)
old_returns_df = close_df.pct_change()
old_strategy_returns = (positions_delayed * old_returns_df).sum(axis=1)
# 方法 2新方法分段计算
prev_positions = positions_delayed.shift(1).fillna(0)
curr_positions = positions_delayed
# 检测状态
is_buying = (prev_positions == 0) & (curr_positions > 0)
is_holding = (prev_positions > 0) & (curr_positions > 0)
is_selling = (prev_positions > 0) & (curr_positions == 0)
# 计算各类收益率
buy_returns = (close_df - open_df) / open_df # open-to-close
hold_returns = close_df.pct_change() # close-to-close
sell_returns = (open_df - close_df.shift(1)) / close_df.shift(1) # close-to-open
# 组合收益率
new_returns_df = pd.DataFrame(0.0, index=close_df.index, columns=close_df.columns)
new_returns_df[is_buying] = buy_returns[is_buying]
new_returns_df[is_holding] = hold_returns[is_holding]
new_returns_df[is_selling] = sell_returns[is_selling]
new_strategy_returns = (curr_positions * new_returns_df).sum(axis=1)
# 3. 计算净值曲线和 KPI
print("\n[3] 计算净值曲线和 KPI 对比...")
old_equity = (1 + old_strategy_returns).cumprod()
new_equity = (1 + new_strategy_returns).cumprod()
def calc_kpi(returns, equity, name):
total_return = equity.iloc[-1] / equity.iloc[0] - 1
n_days = len(returns)
annual_return = (1 + total_return) ** (252 / n_days) - 1
cummax = equity.cummax()
drawdown = (equity - cummax) / cummax
max_drawdown = drawdown.min()
sharpe = returns.mean() / returns.std() * np.sqrt(252) if returns.std() > 0 else 0
print(f"\n {name}:")
print(f" 总收益: {total_return * 100:.2f}%")
print(f" 年化收益: {annual_return * 100:.2f}%")
print(f" 最大回撤: {max_drawdown * 100:.2f}%")
print(f" 夏普比率: {sharpe:.2f}")
print(f" 交易天数: {n_days}")
return {
'总收益': total_return,
'年化收益': annual_return,
'最大回撤': max_drawdown,
'夏普比率': sharpe,
}
old_kpi = calc_kpi(old_strategy_returns, old_equity, "旧方法close-to-close")
new_kpi = calc_kpi(new_strategy_returns, new_equity, "新方法(分段计算)")
# 4. 差异分析
print("\n" + "=" * 80)
print(" 差异对比")
print("=" * 80)
print(f"\n {'指标':<12} {'旧方法':>12} {'新方法':>12} {'差异':>12}")
print(f" {'-'*12} {'-'*12} {'-'*12} {'-'*12}")
for key in ['总收益', '年化收益', '最大回撤', '夏普比率']:
old_val = old_kpi[key]
new_val = new_kpi[key]
diff = new_val - old_val
if key == '夏普比率':
print(f" {key:<12} {old_val:>12.2f} {new_val:>12.2f} {diff:>+12.2f}")
else:
print(f" {key:<12} {old_val*100:>11.2f}% {new_val*100:>11.2f}% {diff*100:>+11.2f}%")
# 5. 调仓日分析
print("\n" + "=" * 80)
print(" 调仓日跳空分析")
print("=" * 80)
# 识别调仓日
position_changes = (positions != positions.shift(1)).any(axis=1)
rebalance_dates = positions[position_changes].index
print(f"\n 总调仓次数: {len(rebalance_dates)}")
# 分析调仓日的跳空
gap_returns_all = []
for date in rebalance_dates:
if date in close_df.index:
# 计算该日的平均跳空(所有持仓 ETF
pos = positions.loc[date]
held_codes = pos[pos > 0].index
if len(held_codes) > 0:
# 过滤掉不在 open_df 中的代码(如指数)
held_codes = [c for c in held_codes if c in open_df.columns]
if len(held_codes) == 0:
continue
day_gap = open_df.loc[date][held_codes]
prev_close = close_df.shift(1).loc[date][held_codes]
gap = (day_gap - prev_close) / prev_close
gap_returns_all.append(gap.mean())
if gap_returns_all:
gap_series = pd.Series(gap_returns_all)
print(f"\n 调仓日跳空统计:")
print(f" 平均跳空: {gap_series.mean() * 100:+.3f}%")
print(f" 跳空标准差: {gap_series.std() * 100:.2f}%")
print(f" 最大向上跳空: {gap_series.max() * 100:+.2f}%")
print(f" 最大向下跳空: {gap_series.min() * 100:+.2f}%")
print(f" 向上跳空天数: {(gap_series > 0).sum()} ({(gap_series > 0).sum() / len(gap_series) * 100:.1f}%)")
print(f" 向下跳空天数: {(gap_series < 0).sum()} ({(gap_series < 0).sum() / len(gap_series) * 100:.1f}%)")
else:
print(f"\n ⚠ 无法计算调仓日跳空(数据缺失)")
return old_kpi, new_kpi
def main():
print("=" * 80)
print(" ETF 跳空收益影响测算")
print("=" * 80)
# 1. 加载配置
config_file = project_root / 'framework_v2' / 'strategies' / 'rotation' / 'config_simple.yaml'
print(f"\n[1] 加载配置: {config_file}")
config = load_config(str(config_file))
# 2. 获取 ETF 列表
signal_to_trade = config.asset_pools.get_signal_to_trade_mapping()
trade_codes = list(set(signal_to_trade.values()))
# 过滤掉不是 ETF 的代码(如 931862.CSI
trade_codes = [c for c in trade_codes if not c.endswith('.CSI')]
print(f" ETF 数量: {len(trade_codes)}")
# 3. 获取数据
from datetime import date
start = config.backtest.start_date
end = config.backtest.end_date
if end is None:
end = date.today().strftime('%Y-%m-%d')
etf_data = fetch_etf_data_with_ohlc(trade_codes, start, end)
# 4. 计算跳空统计
stats_df = calculate_gap_statistics(etf_data)
# 5. 分析策略影响
strategy = GlobalRotationStrategy(config)
old_kpi, new_kpi = analyze_strategy_gap_impact(strategy, etf_data)
# 6. 结论
print("\n" + "=" * 80)
print(" 结论与建议")
print("=" * 80)
annual_diff = new_kpi['年化收益'] - old_kpi['年化收益']
if abs(annual_diff) < 0.01: # 差异 < 1%
print("\n ✓ 跳空影响较小(< 1%),可以继续使用 close-to-close 简化计算")
elif abs(annual_diff) < 0.03: # 差异 1-3%
print("\n ⚠ 跳空影响中等1-3%),建议考虑使用分段计算提高精度")
else: # 差异 > 3%
print("\n ✗ 跳空影响显著(> 3%),强烈建议使用分段计算")
print(f"\n 当前年化: {old_kpi['年化收益'] * 100:.2f}%")
print(f" 修正后年化: {new_kpi['年化收益'] * 100:.2f}%")
print(f" 差异: {annual_diff * 100:+.2f}%")
print("=" * 80)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
验证 cum_return_idx 和 cum_return_etf 是否独立计算
"""
import sys
from pathlib import Path
import json
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# 读取已有的 backtest_detail_v2.json
detail_path = project_root / 'framework_v2' / 'results' / 'backtest_detail_v2.json'
if not detail_path.exists():
print(f"❌ 文件不存在: {detail_path}")
print("请先运行: python framework_v2/scripts/export_backtest_detail.py")
sys.exit(1)
print("=" * 80)
print(" 验证指数和 ETF 累计收益是否独立计算")
print("=" * 80)
with open(detail_path, 'r') as f:
data = json.load(f)
# 检查每日数据
days = data['days']
print(f"\n总天数: {len(days)}")
# 统计有差异的天数
diff_count = 0
same_count = 0
total_checked = 0
for day in days[:100]: # 检查前 100 天
date = day['date']
assets = day.get('assets', {})
for code, asset in assets.items():
if not asset.get('is_held'):
continue
cum_etf = asset.get('cum_return_etf')
cum_idx = asset.get('cum_return_idx')
if cum_etf is not None and cum_idx is not None:
total_checked += 1
if abs(cum_etf - cum_idx) > 0.0001: # 差异超过 0.01%
diff_count += 1
if diff_count <= 5: # 只显示前 5 个示例
print(f"\n{date} - {code}:")
print(f" ETF 累计收益: {cum_etf:.4f} ({cum_etf*100:.2f}%)")
print(f" 指数累计收益: {cum_idx:.4f} ({cum_idx*100:.2f}%)")
print(f" 差异: {abs(cum_etf - cum_idx)*100:.2f}%")
else:
same_count += 1
print(f"\n{'=' * 80}")
print(f"统计结果(前 100 天,持仓标的):")
print(f" 总检查次数: {total_checked}")
print(f" 有差异: {diff_count} ({diff_count/total_checked*100:.1f}%)")
print(f" 相同: {same_count} ({same_count/total_checked*100:.1f}%)")
if diff_count > 0:
print(f"\n✅ 修复成功!指数和 ETF 累计收益已独立计算")
else:
print(f"\n❌ 仍有问题:指数和 ETF 累计收益完全相同")
print(" 需要重新生成 backtest_detail_v2.json")

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
验证 ETF 数据获取修复
测试点:
1. 指数数据使用 adj='raw'
2. ETF 数据使用 adj='hfq'
3. 数据字典中同时包含指数和 ETF
"""
import sys
from pathlib import Path
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from framework_v2.config import load_config
from framework_v2.strategies.rotation.rotation import GlobalRotationStrategy
def main():
print("=" * 70)
print(" 验证 ETF 数据获取修复")
print("=" * 70)
# 加载配置
config_path = project_root / 'framework_v2' / 'config' / 'rotation_global.yaml'
print(f"\n加载配置: {config_path}")
config = load_config(str(config_path))
# 初始化策略
strategy = GlobalRotationStrategy(config)
# 获取数据
print("\n" + "=" * 70)
print("获取数据...")
print("=" * 70)
data = strategy.get_data()
# 分析数据结构
print("\n" + "=" * 70)
print("数据结构分析")
print("=" * 70)
# 获取映射关系
signal_to_trade = config.asset_pools.get_signal_to_trade_mapping()
signal_codes = config.asset_pools.get_signal_codes()
trade_codes = set(signal_to_trade.values())
print(f"\n信号标的(指数): {len(signal_codes)}")
for code in sorted(signal_codes):
if code in data:
df = data[code]
has_hfq = 'close_hfq' in df.columns if 'close' in df.columns else False
print(f"{code}: {len(df)} 条, 有 close_hfq: {has_hfq}")
else:
print(f"{code}: 数据缺失")
print(f"\n交易标的ETF: {len(trade_codes)}")
for code in sorted(trade_codes):
if code in data:
df = data[code]
has_nav = 'nav' in df.attrs
has_premium = 'premium_series' in df.attrs
print(f"{code}: {len(df)}")
print(f" close (最新): {df['close'].iloc[-1]:.4f}")
print(f" 有 nav: {has_nav}")
print(f" 有 premium: {has_premium}")
else:
print(f"{code}: 数据缺失")
# 验证关键指标
print("\n" + "=" * 70)
print("验证结果")
print("=" * 70)
# 检查指数数据
index_ok = all(code in data for code in signal_codes)
print(f"\n指数数据完整性: {'✓ 全部获取' if index_ok else '✗ 部分缺失'}")
# 检查 ETF 数据
etf_ok = all(code in data for code in trade_codes)
print(f"ETF 数据完整性: {'✓ 全部获取' if etf_ok else '✗ 部分缺失'}")
# 检查 ETF 是否使用 hfq对比 raw 和 hfq 的价格差异)
print("\n验证 ETF 是否使用 hfq抽样检查...")
from framework_v2.shared.data import FlaskAPIFetcher
fetcher = FlaskAPIFetcher()
etf_hfq_verified = 0
sample_codes = list(trade_codes)[:3] # 抽样前3个
# 获取日期范围
from datetime import date
start = config.backtest.start_date
end = config.backtest.end_date
if end is None:
end = date.today().strftime('%Y-%m-%d')
for code in sample_codes:
if code in data:
hfq_close = data[code]['close'].iloc[-1]
# 获取 raw 数据对比
raw_df = fetcher._source.fetch(code, start, end, adj='raw', asset_type='china_etf')
if raw_df is not None:
raw_close = raw_df['close'].iloc[-1]
ratio = hfq_close / raw_close if raw_close > 0 else 1
if ratio > 1.01: # 差异超过1%说明使用了 hfq
print(f"{code}: raw={raw_close:.4f}, hfq={hfq_close:.4f}, 倍数={ratio:.4f} (正确)")
etf_hfq_verified += 1
else:
print(f"{code}: raw={raw_close:.4f}, hfq={hfq_close:.4f}, 倍数={ratio:.4f} (错误)")
print(f"ETF 使用 hfq: {etf_hfq_verified}/{len(sample_codes)} {'✓ 正确' if etf_hfq_verified == len(sample_codes) else '✗ 错误'}")
# 总结
print("\n" + "=" * 70)
if index_ok and etf_ok and etf_hfq_verified == len(sample_codes):
print("✓ 验证通过:数据获取逻辑正确")
print(" - 指数使用 raw原始价格")
print(" - ETF 使用 hfq后复权价格")
else:
print("✗ 验证失败:数据获取存在问题")
print("=" * 70)
if __name__ == '__main__':
main()