refactor: 整理rotation目录结构

将分析/测试/实验脚本从核心目录移出:
- enrich_etf_data.py → scripts/
- oil_tracking.py → analysis/
- tracking_error_full.py → analysis/
- tracking_error_validation.py → analysis/
- test_start_year_analysis.py → experiments/
- experiment_select_num.py → experiments/

rotation/ 目录现在只保留核心策略代码:
- simple_rotation.py (策略主逻辑)
- config_loader.py (配置加载)
- config_simple.yaml (配置文件)
- daily_scheduler.py (调度器)
This commit is contained in:
2026-06-21 13:38:15 +08:00
parent 0da0306894
commit ac022020c7
6 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
"""计算3只原油ETF跟踪WTI原油价格的准确率使用Flask API数据源"""
import os, sys
sys.path.insert(0, '/app')
from dotenv import load_dotenv
load_dotenv('/app/.env')
import pandas as pd
import numpy as np
import requests
import time
# 绕过系统代理避免SSL EOF
_session = requests.Session()
_session.trust_env = False
BASE_URL = os.getenv('FLASK_API_URL', 'https://k3s.tokenpluse.xyz')
def fetch_ohlcv(code, start='2020-01-01', end='2026-06-20'):
"""通过Flask API获取OHLCV数据"""
url = f"{BASE_URL}/api/v1/ohlcv"
params = {'code': code, 'start': start, 'end': end}
for attempt in range(3):
try:
resp = _session.get(url, params=params, timeout=120)
if resp.status_code == 200:
data = resp.json()
if 'error' in data or not data.get('data'):
print(f" {code}: 无数据 - {data.get('error', 'empty')}")
return None
df = pd.DataFrame(data['data'])
df['date'] = pd.to_datetime(df['date'])
df = df.set_index('date').sort_index()
df['ret'] = df['close'].pct_change()
cnt = data.get('count', len(df))
dr = data.get('date_range', {})
print(f" {code}: {cnt} 条 ({dr.get('start','?')} ~ {dr.get('end','?')})")
return df
else:
print(f" {code}: HTTP {resp.status_code}")
return None
except Exception as e:
if attempt < 2:
time.sleep(2)
continue
print(f" {code}: 失败 - {e}")
return None
return None
# 1. 获取WTI原油价格
print("=" * 90)
print("获取WTI原油价格 (CL=F)")
print("=" * 90)
cl_df = fetch_ohlcv('CL=F')
if cl_df is None or len(cl_df) < 10:
print("WTI数据获取失败退出")
sys.exit(1)
# 2. 获取3只原油ETF价格
etf_codes = {
'160723.SZ': '嘉实原油(WTI*100%)',
'161129.SZ': '易方达原油(标普高盛原油*100%)',
'501018.SH': '南方原油(WTI*60%+BRENT*40%)',
}
print("\n" + "=" * 90)
print("获取原油ETF价格")
print("=" * 90)
etf_dfs = {}
for code, name in etf_codes.items():
df = fetch_ohlcv(code)
if df is not None:
etf_dfs[code] = name
# 3. 计算跟踪准确率
print("\n" + "=" * 90)
print("跟踪准确率基于ETF收盘价收益率 vs WTI收盘价日收益率")
print("=" * 90)
for code, name in etf_dfs.items():
etf = etf_dfs[code][['ret']].copy()
etf.columns = ['etf_ret']
etf.index = etf.index.normalize()
cl = cl_df[['ret']].copy()
cl.columns = ['cl_ret']
cl.index = cl.index.normalize()
m = pd.merge(etf.reset_index(), cl.reset_index(), on='date', how='inner', suffixes=('', '_cl'))
if 'ret' in m.columns and 'ret_cl' in m.columns:
m = m.rename(columns={'ret': 'etf_ret', 'ret_cl': 'cl_ret'})
m = m[['date', 'etf_ret', 'cl_ret']].dropna()
if len(m) < 10:
print(f"\n{code} {name}: 数据不足 ({len(m)} 天)")
continue
corr = m['etf_ret'].corr(m['cl_ret'])
r2 = corr ** 2
diff = m['etf_ret'] - m['cl_ret']
te_annual = diff.std() * np.sqrt(252)
cum_etf = (1 + m['etf_ret']).prod() - 1
cum_cl = (1 + m['cl_ret']).prod() - 1
bias = diff.mean()
# 分段: 2024至今
recent = m[m['date'] >= '2024-01-01']
if len(recent) > 20:
r2_recent = recent['etf_ret'].corr(recent['cl_ret']) ** 2
else:
r2_recent = np.nan
print(f"\n{code} {name}")
print(f" 重叠交易日: {len(m)}")
print(f" 全区间 R²: {r2:.4f} ({r2*100:.1f}%)")
if not np.isnan(r2_recent):
print(f" 2024至今 R²: {r2_recent:.4f} ({r2_recent*100:.1f}%)")
else:
print(f" 2024至今 R²: 数据不足")
print(f" 年化跟踪误差: {te_annual*100:.2f}%")
print(f" 日均偏差: {bias*100:.4f}%")
print(f" ETF累计收益: {cum_etf*100:.1f}%")
print(f" WTI累计收益: {cum_cl*100:.1f}%")
print(f" 累计收益差: {(cum_etf-cum_cl)*100:.1f}%")
print("\n" + "=" * 90)
print("注: 原油ETF为QDII-LOF净值披露有T+1~T+2延迟")
print(" 且需通过期货合约展期与WTI现货价格存在结构性偏差。")
print("=" * 90)

View File

@@ -0,0 +1,399 @@
"""
ETF跟踪误差全量计算
- 覆盖轮动策略标的池全部10个标的
- 数据源分层:
- A股指数 → Tushare index_daily
- 商品 → Tushare fut_daily主力合约
- 海外指数 → Flask API (yfinance)
- 与天天基金数据对比校验
"""
import os
import sys
import time
import json
import pandas as pd
import numpy as np
from pathlib import Path
from datetime import datetime, timedelta
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from dotenv import load_dotenv
load_dotenv(PROJECT_ROOT / '.env')
import tushare as ts
from datasource.flask_api_source import FlaskAPIDataSource
# ============================================================
# 轮动策略标的池全部10个标的
# ============================================================
POOL_CONFIG = {
# --- A股指数Tushare index_daily---
'399006.SZ': {
'name': '创业板指', 'current_etf': '159915.SZ', 'group': 'A',
'benchmark_type': 'tushare_index', 'benchmark_code': '399006.SZ',
},
'H30269.CSI': {
'name': '红利低波', 'current_etf': '512890.SH', 'group': 'A',
'benchmark_type': 'tushare_index', 'benchmark_code': 'H30269.CSI',
},
# --- 商品Tushare fut_daily 主力合约)---
'GC=F': {
'name': '黄金', 'current_etf': '518880.SH', 'group': 'COMMODITY',
'benchmark_type': 'tushare_futures', 'benchmark_code': 'AU.SHF',
},
'HG=F': {
'name': '有色金属', 'current_etf': '159980.SZ', 'group': 'COMMODITY',
'benchmark_type': 'tushare_futures', 'benchmark_code': 'CU.SHF',
},
# --- 海外指数Flask API / yfinance---
'HSI': {
'name': '恒生指数', 'current_etf': '159920.SZ', 'group': 'HK',
'benchmark_type': 'flask_api', 'benchmark_code': '^HSI',
},
'HSTECH.HK': {
'name': '恒生科技', 'current_etf': '513130.SH', 'group': 'HK',
'benchmark_type': 'flask_api', 'benchmark_code': 'HSTECH.HK',
},
'NDX': {
'name': '纳指100', 'current_etf': '513100.SH', 'group': 'US',
'benchmark_type': 'flask_api', 'benchmark_code': '^NDX',
},
'N225': {
'name': '日经225', 'current_etf': '513520.SH', 'group': 'JP',
'benchmark_type': 'flask_api', 'benchmark_code': '^N225',
},
'GDAXI': {
'name': '德国DAX', 'current_etf': '513030.SH', 'group': 'EU',
'benchmark_type': 'flask_api', 'benchmark_code': '^GDAXI',
},
# --- 原油用最早ETF做基准无可靠数据源---
'CL=F': {
'name': '原油', 'current_etf': '160723.SZ', 'group': 'COMMODITY',
'benchmark_type': 'earliest_etf', 'benchmark_code': '159518.SZ',
},
}
# ============================================================
# 数据获取函数
# ============================================================
def get_etf_nav_tushare(pro, etf_code, start_date, end_date):
"""获取ETF累计净值Tushare fund_nav"""
try:
df = pro.fund_nav(
ts_code=etf_code,
start_date=start_date.replace('-', ''),
end_date=end_date.replace('-', '')
)
if df is not None and len(df) > 0:
df['date'] = pd.to_datetime(df['nav_date'])
df = df.set_index('date').sort_index()
return df['accum_nav'].astype(float)
except Exception as e:
pass
return None
def get_benchmark_tushare_index(pro, index_code, start_date, end_date):
"""获取A股指数收盘价Tushare index_daily"""
try:
df = pro.index_daily(
ts_code=index_code,
start_date=start_date.replace('-', ''),
end_date=end_date.replace('-', '')
)
if df is not None and len(df) > 0:
df['date'] = pd.to_datetime(df['trade_date'])
df = df.set_index('date').sort_index()
return df['close'].astype(float)
except Exception as e:
pass
return None
def get_benchmark_tushare_futures(pro, fut_code, start_date, end_date):
"""获取期货主力合约收盘价Tushare fut_daily"""
try:
df = pro.fut_daily(
ts_code=fut_code,
start_date=start_date.replace('-', ''),
end_date=end_date.replace('-', '')
)
if df is not None and len(df) > 0:
df['date'] = pd.to_datetime(df['trade_date'])
df = df.set_index('date').sort_index()
return df['close'].astype(float)
except Exception as e:
pass
return None
def get_benchmark_flask_api(flask_source, yf_code, start_date, end_date):
"""获取海外指数数据Flask API / yfinance"""
try:
df = flask_source.fetch(yf_code, start_date, end_date)
if df is not None and len(df) > 0:
return df['close'].astype(float)
except Exception as e:
pass
return None
def get_etf_close_tushare(pro, etf_code, start_date, end_date):
"""获取ETF收盘价用于原油等无基准数据的情况"""
try:
df = pro.fund_daily(
ts_code=etf_code,
start_date=start_date.replace('-', ''),
end_date=end_date.replace('-', '')
)
if df is not None and len(df) > 0:
df['date'] = pd.to_datetime(df['trade_date'])
df = df.set_index('date').sort_index()
return df['close'].astype(float)
except Exception as e:
pass
return None
# ============================================================
# 跟踪误差计算
# ============================================================
def calculate_tracking_error(etf_nav, benchmark_close):
"""
计算跟踪误差
公式STDEV(每日偏离度) × √252
每日偏离度 = ETF净值收益率 - 基准收益率
"""
if etf_nav is None or benchmark_close is None:
return None
etf_ret = etf_nav.pct_change().dropna()
bench_ret = benchmark_close.pct_change().dropna()
common = etf_ret.index.intersection(bench_ret.index)
if len(common) < 20:
return None
e = etf_ret.loc[common]
b = bench_ret.loc[common]
daily_deviation = e - b
tracking_error = daily_deviation.std() * np.sqrt(252)
correlation = e.corr(b)
r_squared = correlation ** 2
etf_cum = (1 + e).prod() - 1
bench_cum = (1 + b).prod() - 1
excess = etf_cum - bench_cum
return {
'annual_tracking_error': round(tracking_error * 100, 4),
'correlation': round(correlation, 6),
'r_squared': round(r_squared, 6),
'etf_cum_return': round(etf_cum * 100, 2),
'benchmark_cum_return': round(bench_cum * 100, 2),
'excess_return': round(excess * 100, 2),
'common_days': len(common),
}
# ============================================================
# 主流程
# ============================================================
def main():
print("=" * 80)
print("ETF跟踪误差全量计算10个标的")
print(f"分析日期: {datetime.now().strftime('%Y-%m-%d')}")
print("=" * 80)
# 初始化数据源
pro = ts.pro_api(os.getenv('TUSHARE_TOKEN'))
flask_source = FlaskAPIDataSource()
# 分析区间最近1年
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
print(f"计算区间: {start_date} ~ {end_date}")
# 加载天天基金数据(用于校验 + 获取ETF列表
eastmoney_path = PROJECT_ROOT / 'rotation' / 'results' / 'etf_competitor_analysis.json'
eastmoney_data = {}
if eastmoney_path.exists():
with open(eastmoney_path, 'r', encoding='utf-8') as f:
eastmoney_data = json.load(f)
print(f"已加载天天基金数据: {len(eastmoney_data)} 个标的")
# 按基准类型分组获取(减少重复请求)
benchmark_cache = {} # benchmark_key -> Series
results = {}
for key, info in POOL_CONFIG.items():
index_name = info['name']
current_etf = info['current_etf']
btype = info['benchmark_type']
bcode = info['benchmark_code']
print(f"\n{'='*60}")
print(f"=== {index_name} ({key}) | 基准类型: {btype} ===")
print(f"{'='*60}")
# Step 1: 获取基准数据(带缓存)
bench_key = f"{btype}:{bcode}"
if bench_key not in benchmark_cache:
print(f" 获取基准数据: {bcode} ({btype})")
if btype == 'tushare_index':
benchmark = get_benchmark_tushare_index(pro, bcode, start_date, end_date)
elif btype == 'tushare_futures':
benchmark = get_benchmark_tushare_futures(pro, bcode, start_date, end_date)
elif btype == 'flask_api':
benchmark = get_benchmark_flask_api(flask_source, bcode, start_date, end_date)
elif btype == 'earliest_etf':
benchmark = get_etf_close_tushare(pro, bcode, start_date, end_date)
else:
benchmark = None
if benchmark is not None:
benchmark_cache[bench_key] = benchmark
print(f" ✓ 基准数据: {len(benchmark)}")
else:
print(f" ✗ 基准数据获取失败")
benchmark_cache[bench_key] = None
else:
benchmark = benchmark_cache[bench_key]
print(f" (缓存) 基准数据: {len(benchmark)}")
if benchmark is None:
print(f" 跳过(无基准数据)")
continue
# Step 2: 获取该标的下所有ETF
etf_list = []
if key in eastmoney_data:
for etf in eastmoney_data[key]['etfs']:
etf_list.append({
'code': etf['ts_code'],
'name': etf['name'],
'eastmoney_te': etf.get('annual_tracking_error', 'N/A'),
})
print(f"{len(etf_list)} 只ETF需要计算")
# Step 3: 逐只计算跟踪误差
etf_results = []
for etf_info in etf_list:
etf_code = etf_info['code']
etf_name = etf_info['name']
# 获取ETF NAV或收盘价
if btype == 'earliest_etf':
# 原油:用收盘价对比收盘价
etf_data = get_etf_close_tushare(pro, etf_code, start_date, end_date)
else:
etf_data = get_etf_nav_tushare(pro, etf_code, start_date, end_date)
if etf_data is None or len(etf_data) < 20:
continue
tracking = calculate_tracking_error(etf_data, benchmark)
if tracking is None:
continue
result = {
'ts_code': etf_code,
'name': etf_name,
'tushare_te': tracking['annual_tracking_error'],
'tushare_r2': tracking['r_squared'],
'tushare_correlation': tracking['correlation'],
'tushare_excess_return': tracking['excess_return'],
'tushare_common_days': tracking['common_days'],
'eastmoney_te': etf_info['eastmoney_te'],
'is_current': etf_code == current_etf,
}
etf_results.append(result)
time.sleep(0.05)
# 按跟踪误差排序
etf_results.sort(key=lambda x: x['tushare_te'])
results[key] = {
'index_name': index_name,
'current_etf': current_etf,
'benchmark_type': btype,
'benchmark_code': bcode,
'group': info['group'],
'etf_count': len(etf_results),
'etfs': etf_results,
}
# 打印结果
print(f"\n 计算完成: {len(etf_results)} 只ETF")
print(f" {'代码':<12} {'名称':<20} {'TE':<10} {'天天基金TE':<12} {'':<8}")
print(f" {'-'*70}")
for etf in etf_results[:10]:
te_str = f"{etf['tushare_te']:.4f}%"
em_te = etf['eastmoney_te']
marker = "" if etf['is_current'] else ""
print(f" {etf['ts_code']:<12} {etf['name'][:20]:<20} {te_str:<10} {em_te:<12} {etf['tushare_r2']:<8}{marker}")
if len(etf_results) > 10:
print(f" ... 还有 {len(etf_results) - 10}")
# ============================================================
# 保存结果
# ============================================================
output_dir = PROJECT_ROOT / 'rotation' / 'results'
output_dir.mkdir(exist_ok=True)
output_path = output_dir / 'tracking_error_full.json'
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2, default=str)
print(f"\n{'='*80}")
print(f"结果已保存: {output_path}")
print(f"{'='*80}")
# ============================================================
# 汇总校验
# ============================================================
print(f"\n{'='*80}")
print("全量校验汇总")
print(f"{'='*80}")
for key, data in results.items():
matched = [e for e in data['etfs']
if e['eastmoney_te'] and e['eastmoney_te'] not in ['N/A', '--']]
print(f"\n--- {data['index_name']} ({data['benchmark_type']}) ---")
print(f" ETF总数: {data['etf_count']} | 天天基金有数据: {len(matched)}")
if matched:
diffs = []
for etf in matched:
try:
em_te = float(etf['eastmoney_te'].replace('%', ''))
diffs.append(etf['tushare_te'] - em_te)
except:
pass
if diffs:
print(f" 平均差异: {np.mean(diffs):+.4f}% | 最大差异: {max(diffs, key=abs):+.4f}%")
# 打印前3名
top3 = data['etfs'][:3]
print(f" Top3 (TE最低):")
for i, etf in enumerate(top3, 1):
marker = " ★当前" if etf['is_current'] else ""
print(f" {i}. {etf['ts_code']} {etf['name']} TE={etf['tushare_te']:.4f}%{marker}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,289 @@
"""
ETF跟踪误差计算与校验
- 使用Tushare数据计算ETF跟踪误差基于NAV
- 与天天基金数据对比校验
"""
import os
import sys
import time
import json
import pandas as pd
import numpy as np
from pathlib import Path
from datetime import datetime, timedelta
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from dotenv import load_dotenv
load_dotenv(PROJECT_ROOT / '.env')
import tushare as ts
# 轮动策略标的池
POOL_INDEX_MAP = {
'399006.SZ': {
'name': '创业板指', 'current_etf': '159915.SZ', 'group': 'A',
'index_code': '399006.SZ',
},
'H30269.CSI': {
'name': '红利低波', 'current_etf': '512890.SH', 'group': 'A',
'index_code': 'H30269.CSI',
},
}
def get_etf_nav_data(pro, etf_code, start_date, end_date):
"""
获取ETF净值数据使用fund_nav接口
注意ETF应使用accum_nav累计净值而非unit_nav单位净值
"""
try:
df = pro.fund_nav(
ts_code=etf_code,
start_date=start_date.replace('-', ''),
end_date=end_date.replace('-', '')
)
if df is not None and len(df) > 0:
df['date'] = pd.to_datetime(df['nav_date'])
df = df.set_index('date').sort_index()
# 使用累计净值
return df['accum_nav'].astype(float)
except Exception as e:
print(f" 获取 {etf_code} NAV失败: {e}")
return None
def get_index_data(pro, index_code, start_date, end_date):
"""获取指数日线数据"""
try:
df = pro.index_daily(
ts_code=index_code,
start_date=start_date.replace('-', ''),
end_date=end_date.replace('-', '')
)
if df is not None and len(df) > 0:
df['date'] = pd.to_datetime(df['trade_date'])
df = df.set_index('date').sort_index()
return df['close'].astype(float)
except Exception as e:
print(f" 获取指数 {index_code} 失败: {e}")
return None
def calculate_tracking_error(etf_nav, index_close):
"""
计算跟踪误差
公式STDEV(每日偏离度) × √252
每日偏离度 = ETF净值收益率 - 指数收益率
"""
if etf_nav is None or index_close is None:
return None
# 计算收益率
etf_ret = etf_nav.pct_change().dropna()
idx_ret = index_close.pct_change().dropna()
# 对齐日期
common = etf_ret.index.intersection(idx_ret.index)
if len(common) < 20:
return None
e = etf_ret.loc[common]
i = idx_ret.loc[common]
# 每日偏离度
daily_deviation = e - i
# 跟踪误差 = 标准差 × √252
tracking_error = daily_deviation.std() * np.sqrt(252)
# 其他指标
correlation = e.corr(i)
r_squared = correlation ** 2
# 累计收益
etf_cum = (1 + e).prod() - 1
idx_cum = (1 + i).prod() - 1
excess = etf_cum - idx_cum
return {
'annual_tracking_error': round(tracking_error * 100, 4), # %
'correlation': round(correlation, 6),
'r_squared': round(r_squared, 6),
'etf_cum_return': round(etf_cum * 100, 2), # %
'index_cum_return': round(idx_cum * 100, 2), # %
'excess_return': round(excess * 100, 2), # %
'common_days': len(common),
}
def main():
print("=" * 80)
print("ETF跟踪误差计算与校验")
print(f"分析日期: {datetime.now().strftime('%Y-%m-%d')}")
print("=" * 80)
# 初始化
pro = ts.pro_api(os.getenv('TUSHARE_TOKEN'))
# 分析时间范围最近1年
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
print(f"计算区间: {start_date} ~ {end_date}")
# 加载天天基金数据
eastmoney_path = PROJECT_ROOT / 'rotation' / 'results' / 'etf_competitor_analysis.json'
eastmoney_data = {}
if eastmoney_path.exists():
with open(eastmoney_path, 'r', encoding='utf-8') as f:
eastmoney_data = json.load(f)
print(f"已加载天天基金数据: {len(eastmoney_data)} 个指数")
# 对每个指数计算跟踪误差
print(f"\n开始计算跟踪误差...")
results = {}
for key, info in POOL_INDEX_MAP.items():
index_name = info['name']
index_code = info['index_code']
current_etf = info['current_etf']
print(f"\n{'='*60}")
print(f"=== {index_name} ({key}) ===")
print(f"{'='*60}")
# 获取指数数据
print(f" 获取指数数据: {index_code}")
index_data = get_index_data(pro, index_code, start_date, end_date)
if index_data is None:
print(f" ✗ 指数数据获取失败")
continue
print(f" ✓ 指数数据: {len(index_data)}")
# 获取该指数下所有ETF的NAV
etf_list = []
if key in eastmoney_data:
for etf in eastmoney_data[key]['etfs']:
etf_list.append({
'code': etf['ts_code'],
'name': etf['name'],
'eastmoney_te': etf.get('annual_tracking_error', 'N/A'),
})
print(f"{len(etf_list)} 只ETF需要计算")
etf_results = []
for etf_info in etf_list:
etf_code = etf_info['code']
etf_name = etf_info['name']
# 获取ETF NAV
etf_nav = get_etf_nav_data(pro, etf_code, start_date, end_date)
if etf_nav is None or len(etf_nav) < 20:
continue
# 计算跟踪误差
tracking = calculate_tracking_error(etf_nav, index_data)
if tracking is None:
continue
result = {
'ts_code': etf_code,
'name': etf_name,
'tushare_te': tracking['annual_tracking_error'],
'tushare_r2': tracking['r_squared'],
'tushare_correlation': tracking['correlation'],
'tushare_excess_return': tracking['excess_return'],
'tushare_common_days': tracking['common_days'],
'eastmoney_te': etf_info['eastmoney_te'],
'is_current': etf_code == current_etf,
}
etf_results.append(result)
time.sleep(0.1)
# 按跟踪误差排序
etf_results.sort(key=lambda x: x['tushare_te'])
results[key] = {
'index_name': index_name,
'index_code': index_code,
'current_etf': current_etf,
'etf_count': len(etf_results),
'etfs': etf_results,
}
# 打印结果
print(f"\n 计算完成: {len(etf_results)} 只ETF")
print(f" {'代码':<12} {'名称':<20} {'Tushare TE':<12} {'天天基金 TE':<12} {'差异':<10} {'':<8}")
print(f" {'-'*80}")
for etf in etf_results[:10]:
tushare_te = f"{etf['tushare_te']:.4f}%"
eastmoney_te = etf['eastmoney_te']
# 计算差异
diff = 'N/A'
if eastmoney_te and eastmoney_te != 'N/A' and eastmoney_te != '--':
try:
em_te = float(eastmoney_te.replace('%', ''))
diff_val = etf['tushare_te'] - em_te
diff = f"{diff_val:+.4f}%"
except:
pass
marker = "" if etf['is_current'] else ""
print(f" {etf['ts_code']:<12} {etf['name'][:20]:<20} {tushare_te:<12} {eastmoney_te:<12} {diff:<10} {etf['tushare_r2']:<8}{marker}")
if len(etf_results) > 10:
print(f" ... 还有 {len(etf_results) - 10}")
# 保存结果
output_dir = PROJECT_ROOT / 'rotation' / 'results'
output_dir.mkdir(exist_ok=True)
output_path = output_dir / 'tracking_error_validation.json'
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2, default=str)
print(f"\n{'='*80}")
print(f"结果已保存: {output_path}")
print(f"{'='*80}")
# 汇总统计
print(f"\n{'='*80}")
print("校验汇总")
print(f"{'='*80}")
for key, data in results.items():
print(f"\n--- {data['index_name']} ---")
print(f" 指数代码: {data['index_code']}")
print(f" 计算ETF数: {data['etf_count']}")
# 统计有天天基金数据的ETF
matched = [e for e in data['etfs'] if e['eastmoney_te'] and e['eastmoney_te'] not in ['N/A', '--']]
print(f" 天天基金有数据: {len(matched)}")
if matched:
# 计算平均差异
diffs = []
for etf in matched:
try:
em_te = float(etf['eastmoney_te'].replace('%', ''))
diff = etf['tushare_te'] - em_te
diffs.append(diff)
except:
pass
if diffs:
avg_diff = np.mean(diffs)
max_diff = max(diffs, key=abs)
print(f" 平均差异: {avg_diff:+.4f}%")
print(f" 最大差异: {max_diff:+.4f}%")
if __name__ == '__main__':
main()