feat: 实现贪心分配模式(greedy)

- config_loader.py: 添加 etf_pool 字段和 GREEDY 枚举
- config_simple.yaml: 每个资产添加 etf_pool 列表
- simple_rotation.py:
  - 添加 _compute_greedy_weights 方法
  - _calculate_daily_return 支持 greedy 模式
  - 向后兼容原有 rank/equal 模式

贪心算法:按 ETF 池容量分配仓位,装不下的顺延给下一名
- 有色金属(1 ETF): 吸收25%,顺延75%
- 原油(3 ETF): 吸收75%
- 黄金(4 ETF): 吸收100%

回测对比 (select_num=3):
- rank: 326.60% 累计收益, 1.24 夏普
- greedy: 421.35% 累计收益, 1.03 夏普
This commit is contained in:
2026-06-21 12:40:40 +08:00
parent b698857e49
commit adb83d8cd7
7 changed files with 1135 additions and 2 deletions

129
rotation/oil_tracking.py Normal file
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)