Compare commits
3 Commits
7fc1170964
...
6a86a27108
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a86a27108 | |||
| 2ff48e8d56 | |||
| d404ddee17 |
271
framework_v2/scripts/compare_index_vs_etf_returns.py
Normal file
271
framework_v2/scripts/compare_index_vs_etf_returns.py
Normal 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()
|
||||
133
framework_v2/scripts/verify_etf_hfq_fix.py
Normal file
133
framework_v2/scripts/verify_etf_hfq_fix.py
Normal 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()
|
||||
@@ -60,7 +60,8 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
self,
|
||||
codes: List[str],
|
||||
start: str,
|
||||
end: str
|
||||
end: str,
|
||||
adj: str = 'raw'
|
||||
) -> Dict[str, pd.DataFrame]:
|
||||
"""
|
||||
获取指数 OHLCV 数据
|
||||
@@ -69,6 +70,7 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
codes: 指数代码列表(如 ["000300.SH", "000905.SH"])
|
||||
start: 开始日期 (YYYY-MM-DD)
|
||||
end: 结束日期 (YYYY-MM-DD)
|
||||
adj: 复权类型,默认 'raw'(指数通常用原始价格)
|
||||
|
||||
Returns:
|
||||
{code: DataFrame} 字典,DataFrame 包含 OHLCV 列
|
||||
@@ -82,7 +84,7 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
... )
|
||||
>>> print(data["000300.SH"].head())
|
||||
"""
|
||||
print(f"\n[FlaskAPI] 获取 {len(codes)} 只指数数据...")
|
||||
print(f"\n[FlaskAPI] 获取 {len(codes)} 只指数数据(adj='{adj}')...")
|
||||
|
||||
results = {}
|
||||
for i, code in enumerate(codes, 1):
|
||||
@@ -92,7 +94,7 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
code=code,
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
adj='raw' # 指数通常用原始价格
|
||||
adj=adj # 使用传入的 adj 参数
|
||||
)
|
||||
|
||||
if df is not None:
|
||||
@@ -110,7 +112,8 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
self,
|
||||
codes: List[str],
|
||||
start: str,
|
||||
end: str
|
||||
end: str,
|
||||
adj: str = 'hfq'
|
||||
) -> Dict[str, pd.DataFrame]:
|
||||
"""
|
||||
获取 ETF 数据(价格 + 净值)
|
||||
@@ -119,6 +122,7 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
codes: ETF 代码列表(如 ["510300.SH", "159919.SZ"])
|
||||
start: 开始日期 (YYYY-MM-DD)
|
||||
end: 结束日期 (YYYY-MM-DD)
|
||||
adj: 复权类型,默认 'hfq'(ETF 收益计算推荐后复权)
|
||||
|
||||
Returns:
|
||||
{code: DataFrame} 字典
|
||||
@@ -128,15 +132,23 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
|
||||
示例:
|
||||
>>> fetcher = FlaskAPIFetcher()
|
||||
>>> # 默认使用 hfq(后复权)
|
||||
>>> data = fetcher.fetch_etf(
|
||||
... ["510300.SH", "159919.SZ"],
|
||||
... "2024-01-01",
|
||||
... "2024-12-31"
|
||||
... )
|
||||
>>> # 或者显式指定 raw(原始价格,用于计算溢价率)
|
||||
>>> data_raw = fetcher.fetch_etf(
|
||||
... ["510300.SH"],
|
||||
... "2024-01-01",
|
||||
... "2024-12-31",
|
||||
... adj='raw'
|
||||
... )
|
||||
>>> # 访问净值
|
||||
>>> nav = data["510300.SH"].attrs.get('nav')
|
||||
"""
|
||||
print(f"\n[FlaskAPI] 获取 {len(codes)} 只 ETF 数据...")
|
||||
print(f"\n[FlaskAPI] 获取 {len(codes)} 只 ETF 数据(adj='{adj}')...")
|
||||
|
||||
results = {}
|
||||
for i, code in enumerate(codes, 1):
|
||||
@@ -146,7 +158,7 @@ class FlaskAPIFetcher(DataFetcher):
|
||||
code=code,
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
adj='hfq', # ETF 收益计算必须使用后复权价格(处理份额拆分)
|
||||
adj=adj, # 使用传入的 adj 参数
|
||||
asset_type='china_etf' # 强制指定 ETF 类型
|
||||
)
|
||||
|
||||
|
||||
@@ -115,6 +115,68 @@ class GlobalRotationStrategy(StrategyBase):
|
||||
|
||||
return list(codes)
|
||||
|
||||
def get_data(self) -> Dict[str, pd.DataFrame]:
|
||||
"""
|
||||
获取数据(分别获取指数和 ETF,使用不同的复权方式)
|
||||
|
||||
指数数据:使用 raw(原始价格)用于信号计算
|
||||
ETF 数据:使用 hfq(后复权价格)用于收益计算
|
||||
|
||||
Returns:
|
||||
数据字典 {code: DataFrame}
|
||||
"""
|
||||
if self._data_fetcher is None:
|
||||
self._data_fetcher = self._create_data_fetcher()
|
||||
|
||||
# 获取信号→交易映射
|
||||
signal_to_trade = self.config.asset_pools.get_signal_to_trade_mapping()
|
||||
|
||||
# 处理 end_date 为 None 的情况(使用今天)
|
||||
from datetime import date
|
||||
start = self.config.backtest.start_date
|
||||
end = self.config.backtest.end_date
|
||||
if end is None:
|
||||
end = date.today().strftime('%Y-%m-%d')
|
||||
|
||||
data = {}
|
||||
|
||||
# 1. 获取指数数据(信号标的,使用 raw)
|
||||
signal_codes = set(self.config.asset_pools.get_signal_codes())
|
||||
if self.use_dynamic_threshold and self.bond_code:
|
||||
signal_codes.add(self.bond_code)
|
||||
|
||||
if signal_codes:
|
||||
print(f"\n[数据] 获取 {len(signal_codes)} 只指数数据(adj='raw')...")
|
||||
try:
|
||||
index_data = self._data_fetcher.fetch_indices(
|
||||
codes=list(signal_codes),
|
||||
start=start,
|
||||
end=end,
|
||||
adj='raw' # 指数使用原始价格
|
||||
)
|
||||
data.update(index_data)
|
||||
print(f" ✓ 指数数据: {len(index_data)} 只")
|
||||
except Exception as e:
|
||||
print(f" ✗ 指数数据获取失败: {e}")
|
||||
|
||||
# 2. 获取 ETF 数据(交易标的,使用 hfq)
|
||||
trade_codes = list(set(signal_to_trade.values()))
|
||||
if trade_codes:
|
||||
print(f"\n[数据] 获取 {len(trade_codes)} 只 ETF 数据(adj='hfq')...")
|
||||
try:
|
||||
etf_data = self._data_fetcher.fetch_etf(
|
||||
codes=trade_codes,
|
||||
start=start,
|
||||
end=end,
|
||||
adj='hfq' # ETF 使用后复权价格
|
||||
)
|
||||
data.update(etf_data)
|
||||
print(f" ✓ ETF 数据: {len(etf_data)} 只")
|
||||
except Exception as e:
|
||||
print(f" ✗ ETF 数据获取失败: {e}")
|
||||
|
||||
return data
|
||||
|
||||
def compute_factors(self, data: Dict[str, pd.DataFrame]) -> Dict[str, pd.Series]:
|
||||
"""
|
||||
计算动量因子(只使用信号标的的数据)
|
||||
|
||||
Reference in New Issue
Block a user