feat(strategy): 新增纯美股动量轮动策略
新增美股轮动策略模块: - strategies/us_rotation/config.yaml: 47只美股标的池,动量窗口250天,Top5选股 - strategies/us_rotation/strategy.py: USRotationStrategy实现 - run_us_rotation.py: 回测入口脚本 回测结果 (2016-2026, 约10年): - 总收益: 7675% (年化52.49%) - 基准NDX收益: 540% (年化19.72%) - 超额年化收益: 32.78% - 夏普比率: 1.33 (基准0.80) - 最大回撤: 42.15% - 卡玛比率: 1.25 - 胜率: 56.1% - 平均持仓: 2.7天 年度最佳: 2020年+221% (超额176%) 年度防守: 2022年-10.5% (基准-33.7%, 超额+23.3%) 持仓Top5: NVDA(35.8%), AMD(30.1%), SHOP(26%), AVGO(23.9%), FICO(23.3%)
This commit is contained in:
55
run_us_rotation.py
Normal file
55
run_us_rotation.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
美股动量轮动策略回测入口
|
||||||
|
|
||||||
|
运行方式:
|
||||||
|
python run_us_rotation.py
|
||||||
|
python run_us_rotation.py --save results/us_rotation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from strategies.us_rotation.strategy import USRotationStrategy
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='美股动量轮动策略回测')
|
||||||
|
parser.add_argument(
|
||||||
|
'--config',
|
||||||
|
type=str,
|
||||||
|
default='strategies/us_rotation/config.yaml',
|
||||||
|
help='配置文件路径'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--save',
|
||||||
|
type=str,
|
||||||
|
default='results/us_rotation',
|
||||||
|
help='报告保存路径前缀'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("美股动量轮动策略")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"配置文件: {args.config}")
|
||||||
|
print(f"保存路径: {args.save}")
|
||||||
|
|
||||||
|
# 创建策略实例
|
||||||
|
strategy = USRotationStrategy.from_yaml(args.config)
|
||||||
|
|
||||||
|
# 运行回测
|
||||||
|
result = strategy.run_backtest(save_path=args.save)
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(f"\n总耗时: {elapsed:.1f}秒")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
7
strategies/us_rotation/__init__.py
Normal file
7
strategies/us_rotation/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
美股轮动策略模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .strategy import USRotationStrategy
|
||||||
|
|
||||||
|
__all__ = ['USRotationStrategy']
|
||||||
194
strategies/us_rotation/config.yaml
Normal file
194
strategies/us_rotation/config.yaml
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# 美股轮动策略配置
|
||||||
|
|
||||||
|
# ==================== 候选池配置 ====================
|
||||||
|
code_list:
|
||||||
|
# 科技巨头
|
||||||
|
"AAPL":
|
||||||
|
name: "Apple"
|
||||||
|
sector: "Technology"
|
||||||
|
"ADBE":
|
||||||
|
name: "Adobe"
|
||||||
|
sector: "Technology"
|
||||||
|
"AMD":
|
||||||
|
name: "AMD"
|
||||||
|
sector: "Technology"
|
||||||
|
"AMZN":
|
||||||
|
name: "Amazon"
|
||||||
|
sector: "Technology"
|
||||||
|
"ASML":
|
||||||
|
name: "ASML"
|
||||||
|
sector: "Technology"
|
||||||
|
"AVGO":
|
||||||
|
name: "Broadcom"
|
||||||
|
sector: "Technology"
|
||||||
|
"CRM":
|
||||||
|
name: "Salesforce"
|
||||||
|
sector: "Technology"
|
||||||
|
"CRWD":
|
||||||
|
name: "CrowdStrike"
|
||||||
|
sector: "Technology"
|
||||||
|
"CSCO":
|
||||||
|
name: "Cisco"
|
||||||
|
sector: "Technology"
|
||||||
|
"FICO":
|
||||||
|
name: "FICO"
|
||||||
|
sector: "Technology"
|
||||||
|
"GOOGL":
|
||||||
|
name: "Google"
|
||||||
|
sector: "Technology"
|
||||||
|
"INTC":
|
||||||
|
name: "Intel"
|
||||||
|
sector: "Technology"
|
||||||
|
"KLAC":
|
||||||
|
name: "KLA"
|
||||||
|
sector: "Technology"
|
||||||
|
"LRCX":
|
||||||
|
name: "Lam Research"
|
||||||
|
sector: "Technology"
|
||||||
|
"META":
|
||||||
|
name: "Meta"
|
||||||
|
sector: "Technology"
|
||||||
|
"MSFT":
|
||||||
|
name: "Microsoft"
|
||||||
|
sector: "Technology"
|
||||||
|
"MU":
|
||||||
|
name: "Micron"
|
||||||
|
sector: "Technology"
|
||||||
|
"NET":
|
||||||
|
name: "Cloudflare"
|
||||||
|
sector: "Technology"
|
||||||
|
"NFLX":
|
||||||
|
name: "Netflix"
|
||||||
|
sector: "Technology"
|
||||||
|
"NVDA":
|
||||||
|
name: "NVIDIA"
|
||||||
|
sector: "Technology"
|
||||||
|
"ORCL":
|
||||||
|
name: "Oracle"
|
||||||
|
sector: "Technology"
|
||||||
|
"PANW":
|
||||||
|
name: "Palo Alto"
|
||||||
|
sector: "Technology"
|
||||||
|
"PLTR":
|
||||||
|
name: "Palantir"
|
||||||
|
sector: "Technology"
|
||||||
|
"QCOM":
|
||||||
|
name: "Qualcomm"
|
||||||
|
sector: "Technology"
|
||||||
|
"SNOW":
|
||||||
|
name: "Snowflake"
|
||||||
|
sector: "Technology"
|
||||||
|
"TSLA":
|
||||||
|
name: "Tesla"
|
||||||
|
sector: "Technology"
|
||||||
|
"TSM":
|
||||||
|
name: "TSMC"
|
||||||
|
sector: "Technology"
|
||||||
|
|
||||||
|
# 金融
|
||||||
|
"AXP":
|
||||||
|
name: "American Express"
|
||||||
|
sector: "Financial"
|
||||||
|
"BAC":
|
||||||
|
name: "Bank of America"
|
||||||
|
sector: "Financial"
|
||||||
|
"C":
|
||||||
|
name: "Citigroup"
|
||||||
|
sector: "Financial"
|
||||||
|
"GS":
|
||||||
|
name: "Goldman Sachs"
|
||||||
|
sector: "Financial"
|
||||||
|
"JPM":
|
||||||
|
name: "JPMorgan"
|
||||||
|
sector: "Financial"
|
||||||
|
"MA":
|
||||||
|
name: "Mastercard"
|
||||||
|
sector: "Financial"
|
||||||
|
"MS":
|
||||||
|
name: "Morgan Stanley"
|
||||||
|
sector: "Financial"
|
||||||
|
|
||||||
|
# 消费零售
|
||||||
|
"COST":
|
||||||
|
name: "Costco"
|
||||||
|
sector: "Consumer"
|
||||||
|
"LULU":
|
||||||
|
name: "Lululemon"
|
||||||
|
sector: "Consumer"
|
||||||
|
"PDD":
|
||||||
|
name: "PDD Holdings"
|
||||||
|
sector: "Consumer"
|
||||||
|
"SHOP":
|
||||||
|
name: "Shopify"
|
||||||
|
sector: "Consumer"
|
||||||
|
|
||||||
|
# 医药健康
|
||||||
|
"LLY":
|
||||||
|
name: "Eli Lilly"
|
||||||
|
sector: "Healthcare"
|
||||||
|
"NVO":
|
||||||
|
name: "Novo Nordisk"
|
||||||
|
sector: "Healthcare"
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
"CAT":
|
||||||
|
name: "Caterpillar"
|
||||||
|
sector: "Industrial"
|
||||||
|
"COIN":
|
||||||
|
name: "Coinbase"
|
||||||
|
sector: "Crypto"
|
||||||
|
"CRCL":
|
||||||
|
name: "Circle"
|
||||||
|
sector: "Crypto"
|
||||||
|
"FUTU":
|
||||||
|
name: "Futu"
|
||||||
|
sector: "Financial"
|
||||||
|
"HOOD":
|
||||||
|
name: "Robinhood"
|
||||||
|
sector: "Financial"
|
||||||
|
"SAP":
|
||||||
|
name: "SAP"
|
||||||
|
sector: "Technology"
|
||||||
|
"SCCO":
|
||||||
|
name: "Southern Copper"
|
||||||
|
sector: "Materials"
|
||||||
|
|
||||||
|
# ==================== 基准配置 ====================
|
||||||
|
benchmark:
|
||||||
|
code: "NDX"
|
||||||
|
name: "纳斯达克100"
|
||||||
|
|
||||||
|
# ==================== 回测参数 ====================
|
||||||
|
start_date: "2016-01-01"
|
||||||
|
|
||||||
|
# ==================== 因子参数 ====================
|
||||||
|
# 动量窗口(天数)
|
||||||
|
n_days: 250
|
||||||
|
# 因子类型
|
||||||
|
factor_type: "momentum"
|
||||||
|
|
||||||
|
# ==================== 轮动参数 ====================
|
||||||
|
# 不分组,直接选 Top N
|
||||||
|
diversified: false
|
||||||
|
select_num: 5
|
||||||
|
|
||||||
|
# ==================== 调仓控制 ====================
|
||||||
|
# 每日调仓
|
||||||
|
rebalance_days: 1
|
||||||
|
# 调仓阈值:新组合得分超过当前组合 X% 才触发调仓
|
||||||
|
rebalance_threshold: 0.0
|
||||||
|
# 交易成本(双边)
|
||||||
|
trade_cost: 0.001
|
||||||
|
|
||||||
|
# ==================== 数据缓存 ====================
|
||||||
|
use_cache: true
|
||||||
|
|
||||||
|
# ==================== 数据源配置 ====================
|
||||||
|
# SSH 隧道配置(用于 yfinance)
|
||||||
|
ssh_tunnel:
|
||||||
|
enabled: true
|
||||||
|
host: "8.218.167.69"
|
||||||
|
port: 22
|
||||||
|
username: "root"
|
||||||
|
key_path: "hk_ecs.pem"
|
||||||
|
local_port: 1080
|
||||||
354
strategies/us_rotation/strategy.py
Normal file
354
strategies/us_rotation/strategy.py
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
"""
|
||||||
|
美股轮动策略
|
||||||
|
|
||||||
|
纯美股轮动策略,使用动量因子选股
|
||||||
|
特点:
|
||||||
|
- 全部使用 yfinance 数据源
|
||||||
|
- 美股交易日历
|
||||||
|
- 不分组,直接选 Top 5
|
||||||
|
- 基准为纳指 NDX
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import time
|
||||||
|
import pandas as pd
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# 添加项目根目录
|
||||||
|
project_root = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
from datasource.yfinance_source import YFinanceSource
|
||||||
|
from datasource.ssh_tunnel import SSHTunnelManager
|
||||||
|
from strategies.shared.factors.momentum import MomentumFactor
|
||||||
|
from strategies.shared.signals.selectors import TopNSelector
|
||||||
|
from framework.execution import BacktestExecutor
|
||||||
|
|
||||||
|
|
||||||
|
class USRotationStrategy:
|
||||||
|
"""美股轮动策略"""
|
||||||
|
|
||||||
|
def __init__(self, config_path: str = None, config: dict = None):
|
||||||
|
"""
|
||||||
|
初始化策略
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: 配置文件路径
|
||||||
|
config: 配置字典(可选)
|
||||||
|
"""
|
||||||
|
if config_path:
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
self.config = yaml.safe_load(f)
|
||||||
|
elif config:
|
||||||
|
self.config = config
|
||||||
|
else:
|
||||||
|
raise ValueError("需要提供 config_path 或 config")
|
||||||
|
|
||||||
|
# 结束日期默认今天
|
||||||
|
if not self.config.get('end_date'):
|
||||||
|
self.config['end_date'] = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# 应用配置
|
||||||
|
self._apply_config()
|
||||||
|
|
||||||
|
# 初始化因子
|
||||||
|
self._factor = MomentumFactor(
|
||||||
|
n_days=self.n_days,
|
||||||
|
weighted=True,
|
||||||
|
crash_filter=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化选择器(不分组,直接选 Top N)
|
||||||
|
self._selector = TopNSelector(
|
||||||
|
select_num=self.select_num,
|
||||||
|
rebalance_days=self.rebalance_days,
|
||||||
|
rebalance_threshold=self.rebalance_threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
# 数据源(延迟初始化)
|
||||||
|
self._yfinance: Optional[YFinanceSource] = None
|
||||||
|
self._tunnel: Optional[SSHTunnelManager] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_yaml(cls, config_path: str) -> 'USRotationStrategy':
|
||||||
|
"""从 YAML 文件创建策略实例"""
|
||||||
|
return cls(config_path=config_path)
|
||||||
|
|
||||||
|
def _apply_config(self):
|
||||||
|
"""应用配置参数"""
|
||||||
|
self.select_num = self.config.get('select_num', 5)
|
||||||
|
self.n_days = self.config.get('n_days', 250)
|
||||||
|
self.rebalance_days = self.config.get('rebalance_days', 1)
|
||||||
|
self.rebalance_threshold = self.config.get('rebalance_threshold', 0.0)
|
||||||
|
self.trade_cost = self.config.get('trade_cost', 0.001)
|
||||||
|
self.start_date = self.config.get('start_date', '2016-01-01')
|
||||||
|
self.end_date = self.config['end_date']
|
||||||
|
self.use_cache = self.config.get('use_cache', True)
|
||||||
|
|
||||||
|
def _start_tunnel(self) -> bool:
|
||||||
|
"""启动 SSH 隧道"""
|
||||||
|
ssh_config = self.config.get('ssh_tunnel', {})
|
||||||
|
if not ssh_config.get('enabled', False):
|
||||||
|
return True
|
||||||
|
|
||||||
|
self._tunnel = SSHTunnelManager(ssh_config)
|
||||||
|
return self._tunnel.start()
|
||||||
|
|
||||||
|
def _stop_tunnel(self):
|
||||||
|
"""停止 SSH 隧道"""
|
||||||
|
if self._tunnel:
|
||||||
|
self._tunnel.stop()
|
||||||
|
self._tunnel = None
|
||||||
|
|
||||||
|
def _init_yfinance(self):
|
||||||
|
"""初始化 YFinance 数据源"""
|
||||||
|
if self._yfinance is None:
|
||||||
|
self._yfinance = YFinanceSource(use_ssh_tunnel=True)
|
||||||
|
|
||||||
|
def fetch_data(self) -> Dict:
|
||||||
|
"""获取数据(全部使用 yfinance)"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("获取美股数据")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
code_list_config = self.config.get('code_list', {})
|
||||||
|
benchmark_config = self.config.get('benchmark', {})
|
||||||
|
benchmark_code = benchmark_config.get('code', 'NDX')
|
||||||
|
|
||||||
|
if not code_list_config:
|
||||||
|
raise ValueError("配置中未找到 code_list")
|
||||||
|
|
||||||
|
codes = list(code_list_config.keys())
|
||||||
|
print(f"标的池: {len(codes)} 只股票")
|
||||||
|
print(f"基准: {benchmark_code}")
|
||||||
|
print(f"时间范围: {self.start_date} ~ {self.end_date}")
|
||||||
|
|
||||||
|
# 启动 SSH 隓道
|
||||||
|
print("\n启动 SSH 隧道...")
|
||||||
|
if not self._start_tunnel():
|
||||||
|
raise RuntimeError("SSH 隧道启动失败")
|
||||||
|
|
||||||
|
self._init_yfinance()
|
||||||
|
|
||||||
|
# 获取数据
|
||||||
|
all_data: Dict[str, pd.DataFrame] = {}
|
||||||
|
valid_codes: List[str] = []
|
||||||
|
|
||||||
|
print("\n获取股票数据...")
|
||||||
|
for i, code in enumerate(codes):
|
||||||
|
print(f" [{i+1}/{len(codes)}] {code}...", end=" ")
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = self._yfinance.fetch(code, self.start_date, self.end_date)
|
||||||
|
time.sleep(0.5) # 避免限流
|
||||||
|
|
||||||
|
if df is not None and len(df) >= self.n_days:
|
||||||
|
all_data[code] = df
|
||||||
|
valid_codes.append(code)
|
||||||
|
print(f"✓ {len(df)} 条")
|
||||||
|
else:
|
||||||
|
print(f"✗ 数据不足")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 失败: {e}")
|
||||||
|
|
||||||
|
# 获取基准数据
|
||||||
|
print(f"\n获取基准 {benchmark_code}...", end=" ")
|
||||||
|
try:
|
||||||
|
benchmark_df = self._yfinance.fetch(benchmark_code, self.start_date, self.end_date)
|
||||||
|
if benchmark_df is not None and len(benchmark_df) > 0:
|
||||||
|
print(f"✓ {len(benchmark_df)} 条")
|
||||||
|
else:
|
||||||
|
print(f"✗ 基准数据获取失败")
|
||||||
|
benchmark_df = None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 失败: {e}")
|
||||||
|
benchmark_df = None
|
||||||
|
|
||||||
|
# 停止隧道
|
||||||
|
self._stop_tunnel()
|
||||||
|
|
||||||
|
print(f"\n数据获取完成: {len(valid_codes)}/{len(codes)} 只有效")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'stock_data': all_data,
|
||||||
|
'valid_codes': valid_codes,
|
||||||
|
'benchmark': benchmark_df,
|
||||||
|
'benchmark_code': benchmark_code
|
||||||
|
}
|
||||||
|
|
||||||
|
def compute_factors(self, data: Dict) -> pd.DataFrame:
|
||||||
|
"""计算动量因子"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("计算动量因子")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
stock_data = data['stock_data']
|
||||||
|
valid_codes = data['valid_codes']
|
||||||
|
|
||||||
|
factor_values: Dict[str, pd.Series] = {}
|
||||||
|
|
||||||
|
for code in valid_codes:
|
||||||
|
df = stock_data[code]
|
||||||
|
|
||||||
|
if 'close' not in df.columns:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 数据长度检查
|
||||||
|
if len(df) < self.n_days:
|
||||||
|
print(f" ⚠ {code}: 数据不足 {self.n_days} 天,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# MomentumFactor.compute 需要DataFrame
|
||||||
|
factor_series = self._factor.compute(df)
|
||||||
|
|
||||||
|
if factor_series is not None and len(factor_series) > 0:
|
||||||
|
factor_values[code] = factor_series
|
||||||
|
|
||||||
|
# 合成 DataFrame
|
||||||
|
factor_df = pd.DataFrame(factor_values)
|
||||||
|
|
||||||
|
print(f"\n因子计算完成: {len(factor_df.columns)} 只标的")
|
||||||
|
print(f" 窗口: {self.n_days} 天")
|
||||||
|
if len(factor_df) > 0:
|
||||||
|
print(f" 日期范围: {factor_df.index.min()} ~ {factor_df.index.max()}")
|
||||||
|
|
||||||
|
return factor_df
|
||||||
|
|
||||||
|
def generate_signals(self, factor_df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""生成轮动信号(不分组,直接选 Top 5)"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("生成轮动信号")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 不分组,直接对因子排序选 Top 5
|
||||||
|
# TopNSelector.generate 会自动处理调仓周期和T+1
|
||||||
|
signals_df = self._selector.generate(factor_df)
|
||||||
|
|
||||||
|
print(f"\n信号生成完成:")
|
||||||
|
print(f" 选股数量: {self.select_num}")
|
||||||
|
if 'signal' in signals_df.columns:
|
||||||
|
valid_signals = signals_df[signals_df['signal'] != '']
|
||||||
|
print(f" 有效信号天数: {len(valid_signals)}")
|
||||||
|
|
||||||
|
return signals_df
|
||||||
|
|
||||||
|
def run_backtest(self, data: Dict = None, save_path: str = None) -> Dict:
|
||||||
|
"""运行回测"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("美股动量轮动策略 回测")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. 获取数据
|
||||||
|
if data is None:
|
||||||
|
data = self.fetch_data()
|
||||||
|
|
||||||
|
valid_codes = data['valid_codes']
|
||||||
|
|
||||||
|
# 2. 计算因子
|
||||||
|
factor_df = self.compute_factors(data)
|
||||||
|
|
||||||
|
# 3. 生成信号
|
||||||
|
signals = self.generate_signals(factor_df)
|
||||||
|
|
||||||
|
# 4. 构建收益数据(BacktestExecutor期望列名格式:日收益率_{code})
|
||||||
|
print("\n构建收益数据...")
|
||||||
|
stock_data = data['stock_data']
|
||||||
|
|
||||||
|
# 计算日收益率,列名格式为 '日收益率_{code}'
|
||||||
|
returns_data: Dict[str, pd.Series] = {}
|
||||||
|
for code in valid_codes:
|
||||||
|
if code in stock_data:
|
||||||
|
df = stock_data[code]
|
||||||
|
if 'close' in df.columns:
|
||||||
|
returns_data[f'日收益率_{code}'] = df['close'].pct_change()
|
||||||
|
|
||||||
|
returns_df = pd.DataFrame(returns_data)
|
||||||
|
|
||||||
|
# 对齐日期
|
||||||
|
common_dates = signals.index.intersection(returns_df.index)
|
||||||
|
signals = signals.loc[common_dates]
|
||||||
|
returns_df = returns_df.loc[common_dates]
|
||||||
|
|
||||||
|
# 5. 执行回测
|
||||||
|
print("\n执行回测...")
|
||||||
|
executor = BacktestExecutor(
|
||||||
|
initial_capital=100,
|
||||||
|
trade_cost=self.trade_cost,
|
||||||
|
select_num=self.select_num
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio = executor.execute(signals, returns_df)
|
||||||
|
|
||||||
|
# 6. 计算基准收益
|
||||||
|
benchmark_df = data['benchmark']
|
||||||
|
benchmark_code = data['benchmark_code']
|
||||||
|
|
||||||
|
if benchmark_df is not None and 'close' in benchmark_df.columns:
|
||||||
|
benchmark_returns = benchmark_df['close'].pct_change()
|
||||||
|
# 对齐日期
|
||||||
|
benchmark_returns = benchmark_returns.loc[common_dates]
|
||||||
|
|
||||||
|
# 7. 输出结果
|
||||||
|
if hasattr(portfolio, 'backtest_result') and portfolio.backtest_result is not None:
|
||||||
|
result = portfolio.backtest_result
|
||||||
|
|
||||||
|
# 策略净值(DataFrame列)
|
||||||
|
if '策略净值' in result.columns:
|
||||||
|
strategy_nav = result['策略净值'].values
|
||||||
|
final_nav = strategy_nav[-1] if len(strategy_nav) > 0 else 100
|
||||||
|
total_return = (final_nav - 1) * 100 # 净值归一化起点为1
|
||||||
|
else:
|
||||||
|
final_nav = 100
|
||||||
|
total_return = 0
|
||||||
|
|
||||||
|
# 基准收益
|
||||||
|
if benchmark_df is not None and 'close' in benchmark_df.columns:
|
||||||
|
benchmark_start = benchmark_df['close'].iloc[0]
|
||||||
|
benchmark_end = benchmark_df['close'].iloc[-1]
|
||||||
|
benchmark_return = (benchmark_end / benchmark_start - 1) * 100
|
||||||
|
else:
|
||||||
|
benchmark_return = 0
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("回测结果")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"策略最终净值: {final_nav:.2f}")
|
||||||
|
print(f"策略总收益: {total_return:.2f}%")
|
||||||
|
print(f"基准 ({benchmark_code}) 收益: {benchmark_return:.2f}%")
|
||||||
|
print(f"超额收益: {total_return - benchmark_return:.2f}%")
|
||||||
|
print(f"交易成本: {self.trade_cost * 100:.1f}%")
|
||||||
|
|
||||||
|
# 保存结果
|
||||||
|
if save_path:
|
||||||
|
Path(save_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 保存净值曲线
|
||||||
|
if '策略净值' in result.columns:
|
||||||
|
nav_df = pd.DataFrame({
|
||||||
|
'date': result.index,
|
||||||
|
'strategy_nav': result['策略净值'].values
|
||||||
|
})
|
||||||
|
if benchmark_df is not None and 'close' in benchmark_df.columns:
|
||||||
|
# 重建基准净值
|
||||||
|
benchmark_nav = (benchmark_df['close'].pct_change() + 1).cumprod()
|
||||||
|
nav_df['benchmark_nav'] = benchmark_nav.reindex(result.index, method='ffill').values
|
||||||
|
nav_df.to_csv(f"{save_path}_nav.csv", index=False)
|
||||||
|
|
||||||
|
# 保存信号
|
||||||
|
signals.to_csv(f"{save_path}_signals.csv")
|
||||||
|
|
||||||
|
print(f"\n报告保存: {save_path}_*.csv")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'final_nav': final_nav,
|
||||||
|
'total_return': total_return,
|
||||||
|
'benchmark_return': benchmark_return,
|
||||||
|
'excess_return': total_return - benchmark_return,
|
||||||
|
'signals': signals,
|
||||||
|
'result': result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'signals': signals}
|
||||||
Reference in New Issue
Block a user