好的,这是带有详细中文注释的完整代码,以及对整个框架(包含参数优化)的最终详细总结。
一、 最终详细总结
本 Python 框架旨在为基于技术指标和量价分析的CTA(商品交易顾问)类策略提供一个模块化、可配置、可扩展的开发、回测与参数优化环境。其核心设计理念是将策略的不同部分解耦,方便独立开发、测试和组合。
核心模块与功能:
- 数据层 (模拟):
generate_sample_data
函数用于生成模拟的OHLCV(开高低收量)时间序列数据,方便快速演示和测试。实际使用中应替换为真实历史数据的加载模块。 - 信号因子库 (
SIGNAL_FACTOR_REGISTRY
,signal_from_*
,filter_signal_from_*
):- 职责: 每个函数代表一个具体的交易信号生成逻辑或过滤条件逻辑。它接收市场数据 (
data
) 和参数 (params
),直接输出一个标准的信号序列(主信号为 1 买入 / -1 卖出 / 0 无操作;过滤信号为 1 满足 / 0 不满足)。 - 实现: 包含了基于移动平均线交叉、RSI阈值穿越、MACD交叉、布林带突破、唐奇安通道突破等生成主信号的因子,以及基于ADX强度、成交量变化的过滤信号因子。
- 管理:
SIGNAL_FACTOR_REGISTRY
字典方便按名称动态调用这些因子函数。
- 职责: 每个函数代表一个具体的交易信号生成逻辑或过滤条件逻辑。它接收市场数据 (
- 信号组合与过滤层 (
generate_combined_signals
):- 职责: 根据策略配置,选择一个主信号因子,并可选地应用一个或多个过滤信号因子(如ADX强度过滤、成交量过滤)。
- 逻辑: 通常采用“与”逻辑,即只有当所有过滤条件都满足时(过滤信号为1),主信号才会被保留;否则,该时间点的最终信号置为0。
- 回测引擎 (
run_backtest
):- 核心功能: 模拟实际交易过程,按日(或其他周期)进行。它接收最终的交易信号序列。
- 风险管理集成: 内置了止盈、止损(百分比、ATR、追踪)和时间止损逻辑。这些规则在每个交易周期优先于策略信号进行检查。
- 交易模拟: 考虑了手续费 (
commission_rate
) 和滑点 (slippage
),模拟了基本的交易成本。入场/出场通常假设在信号产生的下一个周期的开盘价执行(可配置)。 - 状态管理: 跟踪当前现金、持仓数量、入场价格、入场时间、持仓期最高价、入场时ATR等状态。
- 输出: 返回详细的资金曲线序列 (
portfolio_value
) 和交易记录列表 (trades_df
)。
- 绩效评估层 (
evaluate_performance
):- 职责: 基于回测产生的资金曲线和交易记录,计算一系列标准的量化绩效指标。
- 指标: 包括总回报率、年化回报率、夏普比率、最大回撤、总交易次数、胜率、盈亏比、最终组合价值等。
- 编排层 (
run_single_strategy
):- 职责: 作为一个协调者,将数据准备、信号生成、回测执行、绩效评估这几个步骤串联起来,完成对单个具体策略配置的回测。
- 参数优化层 (
batch_optimize_strategy
,generate_config_combinations
):- 核心功能: 实现了参数网格搜索优化。用户可以定义一个基础策略配置 (
base_config
) 和一个参数网格 (param_grid
),其中包含要优化的参数及其候选值列表。 - 参数组合生成: 利用
itertools.product
生成所有可能的参数组合。 - 自动化回测: 自动遍历每一种参数组合,调用
run_single_strategy
进行回测。 - 结果聚合与排序: 将所有参数组合的回测结果(参数+绩效)汇总到 DataFrame 中,并根据用户指定的绩效指标 (
optimize_metric
) 进行排序,方便找出最优参数集。
- 核心功能: 实现了参数网格搜索优化。用户可以定义一个基础策略配置 (
框架特点:
- 模块化: 各组件职责清晰,低耦合,易于理解、修改和扩展。
- 配置驱动: 策略逻辑、参数、风险管理规则均通过配置字典定义,灵活性高。
- 可扩展性: 添加新指标信号、过滤条件或风险管理方法,只需编写相应函数并注册/集成即可。
- 参数优化: 内置了系统的参数遍历优化功能,是策略研发的关键步骤。
- 易用性: 提供了清晰的函数接口和示例用法。
潜在改进方向:
- 更专业的回测引擎: 使用如
backtrader
,zipline-reloaded
,vectorbt
等成熟库,以支持更复杂的订单类型、事件驱动、保证金模型等。 - 数据管理: 集成更强大的数据加载、清洗、存储方案。
- 高级优化算法: 引入如贝叶斯优化、遗传算法等更高效的参数搜索方法。
- 因子分析: 增加独立评估因子有效性(如IC分析)的模块。
- 可视化: 添加绘图功能,如资金曲线、指标、交易点位、参数热力图等。
- 并行计算: 对参数优化过程进行并行化,加速回测。
总而言之,该框架提供了一个从策略逻辑定义、信号生成、风险管理集成、回测执行到参数优化的完整解决方案,特别适合进行基于技术分析的量化策略研究与开发。
二、 带有详细中文注释的完整代码
# 导入所需库
import pandas as pd
import numpy as np
import itertools # 用于生成参数组合的笛卡尔积
import copy # 用于深度复制字典,避免修改原始配置
import traceback # 用于打印详细错误信息
# --- 0. 模拟数据生成 ---
def generate_sample_data(days=750):
"""
生成模拟的 OHLCV(开高低收量)时间序列数据。
用于快速演示和测试框架功能。
实际应用中应替换为真实数据加载逻辑。
Args:
days (int): 需要生成的数据天数。
Returns:
pd.DataFrame: 包含 'Open', 'High', 'Low', 'Close', 'Volume' 列和 DatetimeIndex 的 DataFrame。
"""
# 创建日期序列(交易日)
dates = pd.date_range(end=pd.Timestamp.today(), periods=days, freq='B')
data = pd.DataFrame(index=dates)
# 生成随机价格,并添加一个简单的线性趋势
data['Open'] = np.random.rand(days) * 50 + 100 + np.linspace(0, 40, days)
# 基于开盘价生成高低价
data['High'] = data['Open'] + np.random.rand(days) * 12
data['Low'] = data['Open'] - np.random.rand(days) * 12
# 基于高低价生成收盘价
data['Close'] = data['Low'] + (data['High'] - data['Low']) * np.random.rand(days)
# 生成随机成交量,并模拟价量关系(价格波动大时成交量倾向于放大)
data['Volume'] = np.random.randint(10000, 60000, size=days) * (1 + (data['Close'] - data['Open']) / data['Open'] * 5).abs()
# 保证 H >= O/C, L <= O/C
data['High'] = data[['High', 'Open', 'Close']].max(axis=1)
data['Low'] = data[['Low', 'Open', 'Close']].min(axis=1)
# 对价格数据进行简单平滑,使其更像真实市场数据
for col in ['Open', 'High', 'Low', 'Close']:
data[col] = data[col].rolling(window=3, min_periods=1).mean()
# 删除因滚动计算产生的初始 NaN 值
data.dropna(inplace=True)
return data
# --- 1. 信号因子库 ---
# 每个函数代表一个信号生成逻辑,输入数据和参数,输出信号序列 (1:买, -1:卖, 0:无)
# 或者过滤信号序列 (1:满足, 0:不满足)
def signal_from_sma_cross(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于价格穿越简单移动平均线 (SMA) 生成交易信号。
金叉 (价格上穿均线) 为买入信号 (1),死叉 (价格下穿均线) 为卖出信号 (-1)。
Args:
data (pd.DataFrame): 包含 'Close' 列的市场数据。
params (dict): 参数字典,应包含 'window' (SMA 的计算窗口期)。
Returns:
pd.Series: 交易信号序列。
"""
window = params.get('window', 20) # 获取窗口期参数,默认20
# 计算 SMA
indicator = data['Close'].rolling(window=window, min_periods=window).mean()
# 初始化信号序列,默认为 0 (无信号)
signal = pd.Series(index=data.index, data=0.0)
# 判断金叉:昨日收盘价 < 昨日SMA 且 今日收盘价 > 今日SMA
signal[ (data['Close'].shift(1) < indicator.shift(1)) & (data['Close'] > indicator) ] = 1
# 判断死叉:昨日收盘价 > 昨日SMA 且 今日收盘价 < 今日SMA
signal[ (data['Close'].shift(1) > indicator.shift(1)) & (data['Close'] < indicator) ] = -1
# 填充可能因 shift 产生的 NaN 为 0
return signal.fillna(0)
def signal_from_ema_cross(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于价格穿越指数移动平均线 (EMA) 生成交易信号。
逻辑与 SMA 交叉类似。
Args:
data (pd.DataFrame): 包含 'Close' 列的市场数据。
params (dict): 参数字典,应包含 'window' (EMA 的计算窗口期/span)。
Returns:
pd.Series: 交易信号序列。
"""
window = params.get('window', 20) # 获取窗口期参数,默认20
# 计算 EMA
indicator = data['Close'].ewm(span=window, adjust=False, min_periods=window).mean()
signal = pd.Series(index=data.index, data=0.0)
# 金叉
signal[ (data['Close'].shift(1) < indicator.shift(1)) & (data['Close'] > indicator) ] = 1
# 死叉
signal[ (data['Close'].shift(1) > indicator.shift(1)) & (data['Close'] < indicator) ] = -1
return signal.fillna(0)
def signal_from_rsi_threshold(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于相对强弱指数 (RSI) 的阈值穿越生成交易信号。
Args:
data (pd.DataFrame): 包含 'Close' 列的市场数据。
params (dict): 参数字典,包含:
'window' (int): RSI 计算窗口期。
'upper_threshold' (int): 超买阈值。
'lower_threshold' (int): 超卖阈值。
'logic' (str): 'cross_from_bound' (从区域外穿越阈值) 或 'enter_zone' (进入区域)。
Returns:
pd.Series: 交易信号序列。
"""
window = params.get('window', 14)
upper_threshold = params.get('upper_threshold', 70)
lower_threshold = params.get('lower_threshold', 30)
logic = params.get('logic', 'cross_from_bound') # 默认使用穿越逻辑
# --- RSI 计算 ---
delta = data['Close'].diff()
gain = (delta.where(delta > 0, 0.0)).rolling(window=window, min_periods=window).mean()
loss = (-delta.where(delta < 0, 0.0)).rolling(window=window, min_periods=window).mean()
loss = loss.replace(0, 1e-10) # 避免除以零错误
rs = gain / loss
indicator = 100.0 - (100.0 / (1.0 + rs))
# --- End RSI 计算 ---
signal = pd.Series(index=data.index, data=0.0)
if logic == 'cross_from_bound':
# 从超卖区下方上穿阈值 -> 买入信号
signal[ (indicator.shift(1) <= lower_threshold) & (indicator > lower_threshold) ] = 1
# 从超买区上方下穿阈值 -> 卖出信号
signal[ (indicator.shift(1) >= upper_threshold) & (indicator < upper_threshold) ] = -1
elif logic == 'enter_zone': # 进入超卖/超买区即给信号 (通常需要额外平仓逻辑)
signal[ indicator < lower_threshold ] = 1
signal[ indicator > upper_threshold ] = -1
else:
raise ValueError(f"未知的RSI信号逻辑: {logic}")
return signal.fillna(0)
def signal_from_macd_cross(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于 MACD 指标的快线 (MACD Line) 与慢线 (Signal Line) 的交叉生成交易信号。
金叉 (快线上穿慢线) 为买入信号 (1),死叉 (快线下穿慢线) 为卖出信号 (-1)。
Args:
data (pd.DataFrame): 包含 'Close' 列的市场数据。
params (dict): 参数字典,包含:
'fast_window' (int): 快线 EMA 周期。
'slow_window' (int): 慢线 EMA 周期。
'signal_window' (int): 信号线 EMA 周期。
Returns:
pd.Series: 交易信号序列。
"""
fast_window = params.get('fast_window', 12)
slow_window = params.get('slow_window', 26)
signal_window = params.get('signal_window', 9)
# --- MACD 计算 ---
ema_fast = data['Close'].ewm(span=fast_window, adjust=False).mean()
ema_slow = data['Close'].ewm(span=slow_window, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=signal_window, adjust=False).mean()
# --- End MACD 计算 ---
signal = pd.Series(index=data.index, data=0.0)
# 金叉: 昨日 MACD < 昨日 Signal 且 今日 MACD > 今日 Signal
signal[ (macd_line.shift(1) < signal_line.shift(1)) & (macd_line > signal_line) ] = 1
# 死叉: 昨日 MACD > 昨日 Signal 且 今日 MACD < 今日 Signal
signal[ (macd_line.shift(1) > signal_line.shift(1)) & (macd_line < signal_line) ] = -1
return signal.fillna(0)
def signal_from_bbands_breakout(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于价格突破布林带 (Bollinger Bands) 上下轨生成信号。
突破上轨为买入信号 (1),跌破下轨为卖出信号 (-1)。
注意:布林带突破常用于不同策略,如趋势跟踪(突破买入)或均值回归(突破后反向操作)。此实现为趋势跟踪用法。
Args:
data (pd.DataFrame): 包含 'Close' 列的市场数据。
params (dict): 参数字典,包含:
'window' (int): 计算均线和标准差的窗口期。
'num_std_dev' (float): 标准差倍数。
Returns:
pd.Series: 交易信号序列。
"""
window = params.get('window', 20)
num_std_dev = params.get('num_std_dev', 2.0)
# --- 布林带计算 ---
close_sma = data['Close'].rolling(window=window, min_periods=window).mean()
close_std = data['Close'].rolling(window=window, min_periods=window).std()
upper_band = close_sma + (close_std * num_std_dev)
lower_band = close_sma - (close_std * num_std_dev)
# --- End 布林带计算 ---
signal = pd.Series(index=data.index, data=0.0)
# 突破上轨: 昨日收盘价 <= 昨日上轨 且 今日收盘价 > 今日上轨
signal[ (data['Close'].shift(1) <= upper_band.shift(1)) & (data['Close'] > upper_band) ] = 1
# 跌破下轨: 昨日收盘价 >= 昨日下轨 且 今日收盘价 < 今日下轨
signal[ (data['Close'].shift(1) >= lower_band.shift(1)) & (data['Close'] < lower_band) ] = -1
return signal.fillna(0)
def signal_from_donchian_breakout(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于价格突破唐奇安通道 (Donchian Channel) 生成信号(类似海龟交易法则入场)。
价格创 N 日新高 (突破上轨) 为买入信号 (1),价格创 N 日新低 (跌破下轨) 为卖出信号 (-1)。
通常使用前一日的通道进行判断。
Args:
data (pd.DataFrame): 包含 'High', 'Low' 列的市场数据。
params (dict): 参数字典,包含 'window' (int): 通道计算窗口期。
Returns:
pd.Series: 交易信号序列。
"""
window = params.get('window', 20)
# 计算前一日的 N 日最高价 (上轨)
upper_band = data['High'].shift(1).rolling(window=window, min_periods=window).max()
# 计算前一日的 N 日最低价 (下轨)
lower_band = data['Low'].shift(1).rolling(window=window, min_periods=window).min()
signal = pd.Series(index=data.index, data=0.0)
# 突破上轨: 今日最高价 > 昨日 N 日最高价
signal[ data['High'] > upper_band ] = 1
# 跌破下轨: 今日最低价 < 昨日 N 日最低价
signal[ data['Low'] < lower_band ] = -1
# 处理可能的信号冲突(例如日内同时突破上下轨),这里简单处理,优先买入(信号1)
# 更复杂的处理可能需要日内逻辑或延迟判断
return signal.fillna(0)
# --- 过滤器信号因子 (返回 1 表示条件满足, 0 表示不满足) ---
def filter_signal_from_adx_strength(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于 ADX 指标判断趋势强度,生成过滤信号。
当 ADX 大于指定阈值时,表示趋势明显,过滤器信号为 1;否则为 0。
Args:
data (pd.DataFrame): 包含 'High', 'Low', 'Close' 列的市场数据。
params (dict): 参数字典,包含:
'window' (int): ADX 计算窗口期。
'threshold' (float): ADX 强度阈值。
Returns:
pd.Series: 过滤信号序列 (1 或 0)。
"""
window = params.get('window', 14)
threshold = params.get('threshold', 25.0)
# --- ADX 计算 (简化版 Wilder's Smoothing) ---
# 参考 TA-Lib 或其他可靠库获取更精确的实现
move_up = data['High'].diff()
move_down = -data['Low'].diff()
plus_dm = np.where((move_up > move_down) & (move_up > 0), move_up, 0.0)
minus_dm = np.where((move_down > move_up) & (move_down > 0), move_down, 0.0)
# TR (True Range)
high_low = data['High'] - data['Low']
high_close_prev = np.abs(data['High'] - data['Close'].shift(1))
low_close_prev = np.abs(data['Low'] - data['Close'].shift(1))
tr = pd.concat([high_low, high_close_prev, low_close_prev], axis=1).max(axis=1, skipna=False)
# ATR (Average True Range) using Wilder's smoothing (SMA for initial value, then EMA with alpha=1/N)
atr = tr.ewm(alpha=1/window, adjust=False, min_periods=window).mean()
atr = atr.replace(0, 1e-10) # 防御除零错误
# +DI / -DI
plus_di = 100.0 * (pd.Series(plus_dm).ewm(alpha=1/window, adjust=False, min_periods=window).mean() / atr)
minus_di = 100.0 * (pd.Series(minus_dm).ewm(alpha=1/window, adjust=False, min_periods=window).mean() / atr)
# DX
di_sum = (plus_di + minus_di).replace(0, 1e-10) # 防御除零错误
dx = 100.0 * (np.abs(plus_di - minus_di) / di_sum)
# ADX (Smoothed DX)
adx = dx.ewm(alpha=1/window, adjust=False, min_periods=window * 2 - 1).mean() # ADX通常需要更长的平滑期
# --- End ADX Calculation ---
filter_signal = pd.Series(index=data.index, data=0.0)
# 当 ADX 大于阈值时,过滤器信号为 1
filter_signal[adx > threshold] = 1
return filter_signal.fillna(0)
def filter_signal_from_volume_increase(data: pd.DataFrame, params: dict) -> pd.Series:
"""
基于当前成交量是否高于其移动平均值生成过滤信号。
当成交量高于均线时,过滤器信号为 1;否则为 0。
Args:
data (pd.DataFrame): 包含 'Volume' 列的市场数据。
params (dict): 参数字典,包含 'window' (int): 成交量均线计算窗口期。
Returns:
pd.Series: 过滤信号序列 (1 或 0)。
"""
window = params.get('window', 20)
# 计算成交量的简单移动平均
volume_sma = data['Volume'].rolling(window=window, min_periods=window).mean()
filter_signal = pd.Series(index=data.index, data=0.0)
# 当今日成交量 > 今日成交量均线时,信号为 1
filter_signal[data['Volume'] > volume_sma] = 1
return filter_signal.fillna(0)
# --- 信号因子注册表 ---
# 将所有信号因子函数映射到名称,方便动态调用
SIGNAL_FACTOR_REGISTRY = {
# 主信号因子
'sma_cross': signal_from_sma_cross,
'ema_cross': signal_from_ema_cross,
'rsi_threshold': signal_from_rsi_threshold,
'macd_cross': signal_from_macd_cross,
'bbands_breakout': signal_from_bbands_breakout,
'donchian_breakout': signal_from_donchian_breakout,
# 过滤信号因子
'adx_strength_filter': filter_signal_from_adx_strength,
'volume_increase_filter': filter_signal_from_volume_increase,
}
# --- 2. 信号组合与过滤层 ---
def generate_combined_signals(data: pd.DataFrame,
main_signal_config: dict,
filter_signal_configs: list = None):
"""
组合主信号和过滤信号,生成最终的交易信号。
Args:
data (pd.DataFrame): 原始市场数据 (OHLCV)。
main_signal_config (dict): 主信号的配置,格式如 {'name': '因子名称', 'params': {参数字典}}。
filter_signal_configs (list, optional): 过滤信号的配置列表,每个元素格式同 main_signal_config。
Returns:
pd.Series: 最终合并后的交易信号序列 (1: 买, -1: 卖, 0: 无)。
"""
# 获取主信号配置
main_signal_name = main_signal_config['name']
main_params = main_signal_config.get('params', {})
# 检查主信号因子是否已注册
if main_signal_name not in SIGNAL_FACTOR_REGISTRY:
raise ValueError(f"未注册的主信号因子: {main_signal_name}")
# --- 1. 生成主信号 ---
main_signal_func = SIGNAL_FACTOR_REGISTRY[main_signal_name]
# 调用因子函数,传入数据的副本以防被修改(虽然良好实践是不修改输入)
main_signal = main_signal_func(data.copy(), main_params)
# 检查返回值是否有效
if main_signal is None or not isinstance(main_signal, pd.Series):
raise ValueError(f"主信号因子 {main_signal_name} 未返回有效的 Pandas Series")
# --- 2. 应用过滤信号 ---
final_signal = main_signal.copy() # 从主信号开始
if filter_signal_configs: # 如果配置了过滤器
for filter_config in filter_signal_configs:
filter_name = filter_config['name']
filter_params = filter_config.get('params', {})
# 检查过滤器因子是否已注册
if filter_name not in SIGNAL_FACTOR_REGISTRY:
print(f"警告: 未注册的过滤信号因子: {filter_name},将跳过此过滤器。")
continue
filter_func = SIGNAL_FACTOR_REGISTRY[filter_name]
# 调用过滤器因子函数获取过滤信号 (1 或 0)
filter_signal = filter_func(data.copy(), filter_params) # 同样使用副本
# 检查返回值
if filter_signal is None or not isinstance(filter_signal, pd.Series):
print(f"警告: 过滤信号因子 {filter_name} 未返回有效的 Pandas Series,将跳过此过滤器。")
continue
# 应用过滤:只有当过滤信号为 1 时,主信号才有效,否则最终信号置为 0
# 这里假设所有过滤器都必须满足 (AND 逻辑)
final_signal[filter_signal == 0] = 0
# 填充可能产生的 NaN 为 0
final_signal = final_signal.fillna(0)
return final_signal
# --- 3. 回测引擎 (包含止盈止损) ---
def run_backtest(data: pd.DataFrame, signals: pd.Series,
initial_capital: float = 100000.0,
commission_rate: float = 0.0005, # 单边手续费率 (万分之五)
slippage: float = 0.0002, # 单边滑点率 (万分之二)
stop_loss_config: dict = None, # 止损配置字典
take_profit_config: dict = None, # 止盈配置字典
time_stop_bars: int = None): # 时间止损 K 线数量
"""
执行基于信号的回测,并集成止盈、止损、时间止损等风险管理规则。
Args:
data (pd.DataFrame): 包含 OHLCV 数据的 DataFrame。
signals (pd.Series): 最终的交易信号序列 (1: 买, -1: 卖, 0: 无)。
initial_capital (float): 初始资金。
commission_rate (float): 单边手续费率。
slippage (float): 单边滑点率 (模拟成交价的不利变动)。
stop_loss_config (dict, optional): 止损配置, e.g.,
{'type': 'percentage', 'value': 0.05} # 百分比止损
{'type': 'atr', 'multiplier': 2.0, 'atr_params': {'window': 14}} # ATR止损
{'type': 'trailing', 'value': 0.10} # 追踪止损
take_profit_config (dict, optional): 止盈配置, e.g.,
{'type': 'percentage', 'value': 0.10} # 百分比止盈
time_stop_bars (int, optional): 持仓达到指定 K 线数后强制平仓。
Returns:
tuple: 包含:
pd.Series: 每日投资组合净值序列。
pd.DataFrame: 交易记录 DataFrame。
dict: 回测绩效总结字典。
"""
# --- 初始化状态变量 ---
capital = initial_capital # 当前现金
position = 0 # 当前持仓数量 (股/手, >0 多头, <0 空头(暂未支持), 0 空仓)
entry_price = 0.0 # 当前持仓的入场均价
entry_date_index = -1 # 入场时的 K 线索引 (用于时间止损)
highest_price_since_entry = 0.0 # 入场后的最高价 (用于追踪止损)
current_atr = 0.0 # 入场时的 ATR 值 (用于 ATR 止损)
portfolio_value = pd.Series(index=data.index, dtype=float) # 记录每日组合净值
trades = [] # 记录交易详情列表
# --- 数据准备 ---
# 复制数据以防修改原始 DataFrame
df_for_backtest = data.copy()
# 检查是否需要 ATR 止损,如果需要且数据中没有 'atr' 列,则计算
use_atr_stop = (stop_loss_config and stop_loss_config.get('type') == 'atr')
if use_atr_stop and 'atr' not in df_for_backtest.columns:
print("回测引擎: 检测到 ATR 止损配置,但数据缺少 'atr' 列,正在计算...")
# --- ATR 计算 (如果需要) ---
atr_params = stop_loss_config.get('atr_params', {})
atr_window = atr_params.get('window', 14)
high_low = df_for_backtest['High'] - df_for_backtest['Low']
high_close_prev = np.abs(df_for_backtest['High'] - df_for_backtest['Close'].shift(1))
low_close_prev = np.abs(df_for_backtest['Low'] - df_for_backtest['Close'].shift(1))
tr = pd.concat([high_low, high_close_prev, low_close_prev], axis=1).max(axis=1)
df_for_backtest['atr'] = tr.ewm(alpha=1/atr_window, adjust=False, min_periods=atr_window).mean()
# 向前填充初始的 NaN 值,确保早期 K 线也有 ATR 参考
df_for_backtest['atr'] = df_for_backtest['atr'].fillna(method='bfill')
print("回测引擎: ATR 计算完成。")
# --- End ATR Calculation ---
# --- 逐 K 线回测循环 ---
for i in range(len(df_for_backtest)):
current_date = df_for_backtest.index[i]
# 获取当前 K 线的价格信息
current_open = df_for_backtest['Open'].iloc[i]
current_high = df_for_backtest['High'].iloc[i]
current_low = df_for_backtest['Low'].iloc[i]
current_close = df_for_backtest['Close'].iloc[i]
# 获取当天的最终交易信号
current_signal = signals.iloc[i]
# --- 0. 开盘前计算当日初始组合价值 ---
# 如果持有仓位,价值 = 现金 + 持仓市值 (按开盘价计)
if position > 0:
portfolio_value.iloc[i] = capital + position * current_open
# 如果空仓,价值 = 现金
else:
portfolio_value.iloc[i] = capital
# --- 1. 检查是否需要平仓 (止盈/止损/时间/策略信号) ---
# 仅当持有仓位时检查
exit_signal_triggered = False # 标记是否触发了任何平仓条件
exit_reason = "" # 记录平仓原因
exit_price = 0.0 # 记录平仓成交价
if position > 0: # 假设只做多
# 更新持仓期间遇到的最高价
highest_price_since_entry = max(highest_price_since_entry, current_high)
# a) 检查止损
if stop_loss_config:
sl_type = stop_loss_config.get('type')
sl_value = stop_loss_config.get('value')
sl_multiplier = stop_loss_config.get('multiplier')
stop_price = -np.inf # 初始化止损价
if sl_type == 'percentage' and sl_value:
stop_price = entry_price * (1.0 - sl_value)
exit_reason = f"止损(百分比 {sl_value*100:.1f}%)"
elif sl_type == 'atr' and sl_multiplier and current_atr > 0:
# 使用入场时的 ATR 计算固定止损位
stop_price = entry_price - (current_atr * sl_multiplier)
exit_reason = f"止损(ATR {sl_multiplier:.1f}x)"
elif sl_type == 'trailing' and sl_value:
# 追踪止损价 = 入场后最高价 * (1 - 追踪比例)
stop_price = highest_price_since_entry * (1.0 - sl_value)
exit_reason = f"追踪止损({sl_value*100:.1f}%)"
# 如果当日最低价触及或跌破止损价
if current_low <= stop_price:
exit_signal_triggered = True
# 模拟以止损价成交,考虑滑点 (对卖出不利)
# 实际成交价可能是止损价或开盘价中对我们更不利的那个(如果开盘就跳空低于止损价)
exit_price = min(stop_price, current_open) * (1.0 - slippage)
# b) 检查止盈 (如果未触发止损)
if not exit_signal_triggered and take_profit_config:
tp_type = take_profit_config.get('type')
tp_value = take_profit_config.get('value')
target_price = np.inf # 初始化止盈目标价
if tp_type == 'percentage' and tp_value:
target_price = entry_price * (1.0 + tp_value)
exit_reason = f"止盈(百分比 {tp_value*100:.1f}%)"
# 可以扩展其他止盈类型,如固定点数、ATR倍数等
# 如果当日最高价触及或超过止盈价
if current_high >= target_price:
exit_signal_triggered = True
# 模拟以止盈价成交,考虑滑点 (对卖出不利)
# 实际成交价可能是止盈价或开盘价中对我们更有利的那个(如果开盘就跳空高于目标价)
exit_price = max(target_price, current_open) * (1.0 - slippage)
# c) 检查时间止损 (如果都未触发)
# i 是当前 K 线索引,entry_date_index 是入场 K 线索引
if not exit_signal_triggered and time_stop_bars and (i - entry_date_index) >= time_stop_bars:
exit_signal_triggered = True
exit_reason = f"时间止损({time_stop_bars} K线)"
# 时间止损通常在满足条件的下一根 K 线开盘执行
if i + 1 < len(df_for_backtest):
exit_price = df_for_backtest['Open'].iloc[i+1] * (1.0 - slippage)
else: # 如果是最后一天
exit_price = current_close * (1.0 - slippage) # 以当日收盘价退出
# d) 检查策略本身的卖出信号 (如果都未触发)
# 策略信号通常也在下一根 K 线开盘执行
if not exit_signal_triggered and current_signal == -1:
# 检查是否不是最后一天,避免索引越界
if i + 1 < len(df_for_backtest):
exit_signal_triggered = True
exit_reason = "策略信号"
exit_price = df_for_backtest['Open'].iloc[i+1] * (1.0 - slippage)
# 如果是最后一天产生的卖出信号,也需要处理
elif i == len(df_for_backtest) - 1:
exit_signal_triggered = True
exit_reason = "策略信号(最后一天)"
exit_price = current_close * (1.0 - slippage)
# --- 2. 执行平仓交易 ---
if exit_signal_triggered and position > 0:
# 确保退出价格有效 (例如时间止损在最后一天时 exit_price 可能未设置)
if exit_price <= 0:
exit_price = current_close * (1.0 - slippage) # 保底用收盘价
# 计算卖出收入 (扣除手续费)
revenue = position * exit_price * (1.0 - commission_rate)
capital += revenue # 更新现金
# 记录交易详情
# 交易日期通常记录为实际执行的日期 (下一天开盘或当天触发)
trade_exec_date = df_for_backtest.index[i+1] if exit_reason not in ["策略信号(最后一天)"] and i + 1 < len(df_for_backtest) else current_date
trades.append((trade_exec_date, 'SELL', exit_price, position, revenue, exit_reason))
# print(f"{trade_exec_date.date()} SELL ({exit_reason}) @ {exit_price:.2f}, Qty: {position}, Rev: {revenue:.2f}, Cap: {capital:.2f}")
# 重置持仓状态
position = 0
entry_price = 0.0
entry_date_index = -1
highest_price_since_entry = 0.0
current_atr = 0.0
# --- 3. 检查并执行入场交易 (仅当当前无持仓时) ---
# 策略的买入信号通常也在下一根 K 线开盘执行
if position == 0 and current_signal == 1:
# 检查是否不是最后一天
if i + 1 < len(df_for_backtest):
# 获取下一根 K 线的开盘价作为理论入场价
trade_price = df_for_backtest['Open'].iloc[i+1]
# 计算实际买入价 (考虑滑点,对买入不利)
buy_price = trade_price * (1.0 + slippage)
# 简化:全仓买入 (实际应考虑资金管理和仓位控制)
# 计算理论上能买多少股/手
quantity_to_buy = int(capital / buy_price)
# 计算买入成本 (包含手续费)
cost = quantity_to_buy * buy_price * (1.0 + commission_rate)
# 确保资金充足且能买入至少1单位
if capital >= cost and quantity_to_buy > 0:
# 更新状态
position = quantity_to_buy
capital -= cost
entry_price = buy_price # 记录入场成本价
entry_date_index = i + 1 # 记录入场 K 线的索引
# 用入场当天的最高价初始化追踪止损用的最高价
highest_price_since_entry = df_for_backtest['High'].iloc[i+1]
# 如果使用 ATR 止损,记录入场时的 ATR 值
if use_atr_stop and 'atr' in df_for_backtest.columns:
entry_atr = df_for_backtest['atr'].iloc[i+1]
# 处理可能的 NaN 值
current_atr = entry_atr if pd.notna(entry_atr) else 0
# 记录交易
trades.append((df_for_backtest.index[i+1], 'BUY', buy_price, quantity_to_buy, cost, "策略信号"))
# print(f"{df_for_backtest.index[i+1].date()} BUY @ {buy_price:.2f}, Qty: {quantity_to_buy}, Cost: {cost:.2f}, Cap Left: {capital:.2f}")
# --- 4. 收盘后更新当日最终组合价值 ---
# 如果持有仓位,价值 = 现金 + 持仓市值 (按收盘价计)
if position > 0:
portfolio_value.iloc[i] = capital + position * df_for_backtest['Close'].iloc[i]
# 如果空仓,价值 = 现金
else:
portfolio_value.iloc[i] = capital
# --- 5. 处理 NaN (主要发生在回测初期) ---
# 如果当前价值是 NaN,用前一天的价值填充
if i > 0 and pd.isna(portfolio_value.iloc[i]):
portfolio_value.iloc[i] = portfolio_value.iloc[i-1]
# 如果第一天就是 NaN,用初始资金填充
elif i == 0 and pd.isna(portfolio_value.iloc[i]):
portfolio_value.iloc[i] = initial_capital
# --- 回测循环结束 ---
# --- 整理交易记录 ---
trades_df = pd.DataFrame(trades, columns=['Date', 'Type', 'Price', 'Quantity', 'Amount', 'Reason'])
if not trades_df.empty:
# 将日期设为索引
trades_df.set_index('Date', inplace=True)
# --- 填充资金曲线开头的 NaN (如果还有) ---
# 向前填充,确保资金曲线连续
portfolio_value.ffill(inplace=True)
# 如果最开始仍有 NaN,用初始资金填充
portfolio_value.fillna(initial_capital, inplace=True)
# --- 计算绩效指标 ---
performance_summary = evaluate_performance(portfolio_value, trades_df, initial_capital)
# 返回结果
return portfolio_value, trades_df, performance_summary
# --- 4. 绩效评估层 ---
def evaluate_performance(portfolio_value: pd.Series, trades_df: pd.DataFrame, initial_capital: float) -> dict:
"""
根据资金曲线和交易记录计算常用的回测绩效指标。
Args:
portfolio_value (pd.Series): 每日组合净值序列。
trades_df (pd.DataFrame): 交易记录 DataFrame。
initial_capital (float): 初始资金。
Returns:
dict: 包含各项绩效指标的字典。
"""
# 防御性检查:如果资金曲线为空或无效,返回默认值
if portfolio_value.empty or portfolio_value.isnull().all() or len(portfolio_value) < 2:
return {
'总回报率': 0.0, '年化回报率': 0.0, '夏普比率': 0.0,
'最大回撤': 0.0, '胜率': 0.0, '盈亏比': 'N/A',
'总交易次数': 0, '最终组合价值': initial_capital
}
# --- 计算年化相关的交易日数 ---
try:
# 尝试推断数据频率
trading_days_per_year = 252 # 默认按中国股市交易日数
time_diff = portfolio_value.index.to_series().diff().median()
# 如果数据是日历日,使用 365
if time_diff == pd.Timedelta(days=1):
trading_days_per_year = 365
# 这里可以根据需要添加对其他频率(周、月、小时)的判断
except Exception:
trading_days_per_year = 252 # 推断失败,使用默认值
# --- 计算核心指标 ---
# 总回报率 = (最终价值 / 初始价值) - 1
final_value = portfolio_value.iloc[-1]
total_return = (final_value / initial_capital) - 1.0
# 年化回报率 = (1 + 总回报率)^(1 / 年数) - 1
start_date = portfolio_value.index[0]
end_date = portfolio_value.index[-1]
years = (end_date - start_date).days / 365.25 # 计算回测覆盖的总年数
# 避免年数为0或负数
if years <= 0:
annualized_return = total_return # 如果时间太短,年化无意义,显示总回报
else:
annualized_return = ((1.0 + total_return) ** (1.0 / years)) - 1.0
# 夏普比率 = (年化(平均日收益率 - 无风险利率)) / 年化(日收益率标准差)
# 假设无风险利率为 0
daily_returns = portfolio_value.pct_change().dropna()
# 检查是否有有效的日收益率数据且标准差不为0
if not daily_returns.empty and daily_returns.std() > 1e-8:
# 年化波动率 = 日标准差 * sqrt(年交易日数)
# 年化收益率 = 日均收益率 * 年交易日数 (近似)
# 夏普 = (日均收益 / 日标准差) * sqrt(年交易日数)
sharpe_ratio = (daily_returns.mean() / daily_returns.std()) * np.sqrt(trading_days_per_year)
else:
sharpe_ratio = 0.0 # 无法计算夏普比率
# 最大回撤
# 计算累积最高价值
cumulative_max = portfolio_value.cummax()
# 计算回撤 = (当前价值 - 累积最高价值) / 累积最高价值
drawdown = (portfolio_value - cumulative_max) / cumulative_max.replace(0, 1e-10) # 防止除以零
# 最大回撤是回撤序列的最小值
max_drawdown = drawdown.min() if not drawdown.empty else 0.0
# --- 计算交易相关指标 ---
total_trades = 0 # 总交易次数 (一买一卖算一次)
winning_trades = 0 # 盈利交易次数
total_profit = 0.0 # 总盈利金额
total_loss = 0.0 # 总亏损金额 (取绝对值)
if not trades_df.empty:
# 筛选出买入和卖出交易
buy_trades = trades_df[trades_df['Type'] == 'BUY']
sell_trades = trades_df[trades_df['Type'] == 'SELL']
# 简化计算:假设交易严格配对 (一买对应一卖)
num_pairs = min(len(buy_trades), len(sell_trades))
total_trades = num_pairs
if num_pairs > 0:
# 遍历所有完整交易对
for i in range(num_pairs):
# 获取买入成本和卖出收入
# 注意:这里的 Amount 列记录的是成本(负)或收入(正)可能更好,但当前实现是成本和收入绝对值
# 需要根据 run_backtest 中 trades.append 的逻辑调整
# 假设 Amount 对 BUY 是 cost,对 SELL 是 revenue
buy_cost = buy_trades['Amount'].iloc[i] # 买入的总花费
sell_revenue = sell_trades['Amount'].iloc[i] # 卖出的总收入
# 计算单次交易盈亏
profit = sell_revenue - buy_cost
if profit > 0:
winning_trades += 1
total_profit += profit
else:
# total_loss 记录亏损总额(正值)
total_loss += abs(profit)
# 胜率 = 盈利次数 / 总次数
win_rate = winning_trades / total_trades if total_trades > 0 else 0.0
# 盈亏比 = 总盈利 / 总亏损
# 如果没有亏损,盈亏比为无穷大
profit_factor = total_profit / total_loss if total_loss > 0 else np.inf
# 返回包含所有指标的字典
return {
'总回报率': total_return,
'年化回报率': annualized_return,
'夏普比率': sharpe_ratio,
'最大回撤': max_drawdown,
'总交易次数': total_trades,
'胜率': win_rate,
# 将无穷大的盈亏比表示为字符串 'inf'
'盈亏比': f"{profit_factor:.2f}" if profit_factor != np.inf else 'inf',
'最终组合价值': final_value
}
# --- 5. 编排与批量优化层 ---
# --- 辅助函数 ---
def set_nested_value(d: dict, keys: list, value):
"""
根据键的列表 (路径) 安全地设置嵌套字典的值。
如果中间的键不存在,会自动创建空字典。
Args:
d (dict): 目标字典。
keys (list): 字符串列表,表示从外到内的键。
value: 要设置的值。
"""
# 遍历到倒数第二个键
for key in keys[:-1]:
# 如果键不存在,或对应的值不是字典,则创建一个新字典
if key not in d or not isinstance(d[key], dict):
d[key] = {}
d = d[key] # 进入下一层
# 设置最后一个键的值
d[keys[-1]] = value
def generate_config_combinations(base_config: dict, param_grid: dict) -> list:
"""
根据基础配置模板和参数网格,生成所有可能的具体配置组合。
Args:
base_config (dict): 策略的基础配置模板字典。
param_grid (dict): 参数网格字典。
键 (str): 参数在 base_config 中的路径,用 '.' 分隔嵌套层级 (e.g., 'main_signal.params.window')。
对于列表中的字典,使用索引 (e.g., 'filters.0.params.threshold')。
值 (list): 该参数的所有候选值列表 (e.g., [10, 20, 30])。
Returns:
list: 包含所有具体配置字典的列表。每个字典都是一个可用于回测的完整配置。
"""
# 如果参数网格为空,直接返回基础配置(不需要优化)
if not param_grid:
return [base_config]
# 获取参数网格中的所有参数名称和对应的候选值列表
param_names = list(param_grid.keys())
param_values_lists = list(param_grid.values())
config_list = [] # 用于存储所有生成的具体配置
# 使用 itertools.product 计算所有参数候选值列表的笛卡尔积
# 例如,如果 param_values_lists = [[10, 20], [0.05, 0.07]]
# 则 product 会生成 (10, 0.05), (10, 0.07), (20, 0.05), (20, 0.07)
for current_combination_values in itertools.product(*param_values_lists):
# 对每个参数值的组合:
# 1. 创建基础配置的深层副本,确保每个组合的配置是独立的
specific_config = copy.deepcopy(base_config)
# 2. 将当前组合中的值,根据对应的参数名路径,设置到副本配置中
for i, param_name in enumerate(param_names):
keys = param_name.split('.') # 将路径字符串按 '.' 分割成键的列表
value_to_set = current_combination_values[i] # 获取当前组合中该参数的值
set_nested_value(specific_config, keys, value_to_set) # 设置到配置副本中
# 3. 将这个具体的配置字典添加到列表中
config_list.append(specific_config)
return config_list
# --- 核心执行函数 ---
def run_single_strategy(data: pd.DataFrame, config: dict, initial_capital: float = 100000.0):
"""
执行单次完整的策略回测。
接收一个具体的、完整的策略配置字典。
Args:
data (pd.DataFrame): 原始市场数据 (OHLCV)。
config (dict): 具体的策略配置字典,包含信号、过滤器、风险管理等所有参数。
initial_capital (float): 初始资金。
Returns:
dict: 包含该配置的回测结果,格式为 {'config': 配置字符串, 'performance': 绩效字典}。
可选地,可以修改此函数返回 portfolio_value 和 trades_df 用于详细分析。
"""
# 复制数据,以防信号生成或回测修改原始数据
df = data.copy()
# 1. 根据配置生成最终交易信号
signals = generate_combined_signals(
data=df, # 传递副本,因子函数可能会在内部计算指标
main_signal_config=config['main_signal'],
filter_signal_configs=config.get('filters') # filters 是可选的
)
# 2. 执行回测
portfolio_value, trades_df, performance_summary = run_backtest(
data=df, # 传递原始数据副本以获取价格
signals=signals, # 使用组合后的信号
initial_capital=initial_capital,
# 从配置中获取交易成本和风险管理参数,如果缺失则使用默认值
commission_rate=config.get('commission_rate', 0.0005),
slippage=config.get('slippage', 0.0002),
stop_loss_config=config.get('stop_loss'),
take_profit_config=config.get('take_profit'),
time_stop_bars=config.get('time_stop')
)
# 3. 准备返回结果
# 将配置字典转换为字符串,方便在结果 DataFrame 中存储和显示
clean_config = {k: str(v) for k, v in config.items()}
result = {
'config': clean_config, # 字符串化的配置信息
'performance': performance_summary # 绩效指标字典
# --- 可选返回项 ---
# 'portfolio_curve': portfolio_value, # 资金曲线序列
# 'trades': trades_df # 交易记录 DataFrame
}
return result
def batch_optimize_strategy(data: pd.DataFrame,
base_config: dict,
param_grid: dict,
initial_capital: float = 100000.0,
optimize_metric: str = '夏普比率'):
"""
执行参数优化:批量回测给定策略基础配置下的所有参数组合。
Args:
data (pd.DataFrame): 原始市场数据 (OHLCV)。
base_config (dict): 策略的基础配置模板。
param_grid (dict): 参数网格字典,定义要优化的参数及其候选值。
initial_capital (float): 初始资金。
optimize_metric (str): 用于对结果进行排序的绩效指标名称 (应与 evaluate_performance 返回的键匹配)。
Returns:
pd.DataFrame: 包含每个参数组合的配置信息和绩效结果的 DataFrame,
按指定的 optimize_metric 降序排列。
"""
# 1. 使用辅助函数生成所有待测试的具体配置列表
specific_configs = generate_config_combinations(base_config, param_grid)
total_configs = len(specific_configs)
print(f"参数优化开始,共需测试 {total_configs} 种参数组合...")
all_results = [] # 用于存储每次回测的结果
# 2. 遍历所有生成的具体配置
for i, config in enumerate(specific_configs):
print(f"\n--- 正在测试组合 {i+1}/{total_configs} ---")
# 打印当前测试组合中变化的参数值,方便跟踪进度
current_params_desc = {}
for param_name in param_grid.keys():
keys = param_name.split('.')
value = config
try: # 安全地获取嵌套字典的值
for key in keys: value = value[key]
current_params_desc[param_name] = value
except (KeyError, TypeError): # 处理路径错误或中间不是字典的情况
current_params_desc[param_name] = "获取失败"
print(f"当前参数: {current_params_desc}")
try:
# 对当前配置执行单次回测
# 传入数据的副本,确保每次回测基于原始数据
result = run_single_strategy(data.copy(), config, initial_capital)
# 准备将结果存入 DataFrame 的行数据
# flat_result = {}
# 将当前变化的参数作为列(可选,如果参数路径不复杂)
# for k, v in current_params_desc.items():
# flat_result[f"param_{k.replace('.', '_')}"] = v # 清理参数名作为列名
# 或者,将完整的配置字符串作为列(更通用,但可能较长)
flat_result = {f"config_{k}": v for k, v in result['config'].items()}
# 合并绩效指标
flat_result.update(result['performance'])
all_results.append(flat_result)
# 打印关键绩效指标
perf = result['performance']
print("测试完成。绩效:")
# 优先显示优化目标指标
print(f" {optimize_metric}: {perf.get(optimize_metric, 'N/A')}")
# 可以根据需要打印更多指标
# print(f" 年化回报率: {perf.get('年化回报率', 0):.4f}")
# print(f" 最大回撤: {perf.get('最大回撤', 0):.4f}")
except Exception as e:
# 如果当前配置的回测失败,记录错误信息
print(f"组合 {i+1} 测试失败: {e}")
traceback.print_exc() # 打印详细的错误堆栈信息
# 记录失败信息和对应的参数
flat_result = {f"config_{k}": str(v) for k, v in config.items()} # 记录失败时的配置
flat_result['Error'] = str(e) # 添加错误信息列
all_results.append(flat_result)
# --- 3. 结果汇总与排序 ---
# 将所有结果合并成一个 DataFrame
results_df = pd.DataFrame(all_results)
# 清理列名,去除可能影响后续操作的特殊字符(例如 evaluate_performance 返回的键可能包含中文)
# 这里简单替换非字母、数字、下划线的字符为空字符串
results_df.columns = results_df.columns.str.replace('[^A-Za-z0-9_]+', '', regex=True)
# 根据指定的优化指标对结果进行排序
# 清理优化指标名称以匹配清理后的列名
sort_col = optimize_metric.replace('[^A-Za-z0-9_]+', '', regex=True)
if sort_col in results_df.columns:
print(f"\n按指标 '{optimize_metric}' (列名: '{sort_col}') 对结果进行排序...")
# 在排序前,将指标列转换为数值类型,无法转换的设为 NaN
# 将 NaN 填充为负无穷,这样在降序排序时它们会排在最后
results_df[sort_col] = pd.to_numeric(results_df[sort_col], errors='coerce').fillna(-np.inf)
# 按指标列降序排序
results_df = results_df.sort_values(by=sort_col, ascending=False, na_position='last')
else:
print(f"警告: 找不到用于排序的指标列 '{sort_col}'。结果将不会排序。")
# 检查是否存在 'Error' 列,如果存在,可以考虑将其排在前面或后面
if 'Error' in results_df.columns:
print("检测到错误列,可能影响排序。")
return results_df
# --- 6. 示例用法 ---
if __name__ == "__main__":
# 设置 Pandas 显示选项,方便查看 DataFrame 结果
pd.set_option('display.max_columns', None) # 显示所有列
pd.set_option('display.width', 2000) # 设置显示宽度
pd.set_option('display.max_colwidth', 100) # 设置列的最大宽度
# 1. 加载/生成数据
print("="*20 + " 1. 生成模拟数据 " + "="*20)
historical_data = generate_sample_data(days=750) # 生成约 3 年的日线数据
print(f"数据已生成,共 {len(historical_data)} 条记录。")
print("数据预览 (前3行):")
print(historical_data.head(3))
print("\n")
# 2. 定义要进行参数优化的策略的基础配置
# 假设我们要优化一个基于 EMA 交叉,并带有 ADX 强度过滤和 ATR 止损的策略
print("="*20 + " 2. 定义基础策略与参数网格 " + "="*20)
base_strategy_config_to_optimize = {
'main_signal': {
'name': 'ema_cross', # 主信号使用 EMA 交叉
'params': {
'window': 20 # EMA 窗口期,将被优化
}
},
'filters': [
{
'name': 'adx_strength_filter', # 使用 ADX 强度过滤
'params': {
'window': 14, # ADX 计算窗口期,将被优化
'threshold': 20 # ADX 强度阈值,将被优化
}
}
],
'stop_loss': {
'type': 'atr', # 使用 ATR 止损
'multiplier': 2.0, # ATR 倍数,将被优化
'atr_params': {'window': 14} # 计算 ATR 时的窗口期,也将被优化
},
# 可以添加其他固定或待优化的配置项
# 'take_profit': {'type': 'percentage', 'value': 0.15}, # 例如,固定的止盈
'commission_rate': 0.0005,
'slippage': 0.0002
}
print("基础策略配置模板:")
import json
print(json.dumps(base_strategy_config_to_optimize, indent=2)) # 打印基础配置
# 3. 定义参数网格 (指定要遍历的参数及其候选值)
parameter_optimization_grid = {
# EMA 窗口期
'main_signal.params.window': [15, 20, 30],
# ADX 过滤器的阈值
'filters.0.params.threshold': [18, 22, 25], # filters 是列表,用索引 .0 访问第一个过滤器
# ADX 计算窗口期
'filters.0.params.window': [10, 14],
# ATR 止损的乘数
'stop_loss.multiplier': [1.5, 2.0, 2.5],
# ATR 计算窗口期
'stop_loss.atr_params.window': [10, 14]
}
print("\n参数优化网格:")
print(json.dumps(parameter_optimization_grid, indent=2))
print("\n")
# 4. 执行参数优化
print("="*20 + " 3. 开始执行参数优化回测 " + "="*20)
optimization_results = batch_optimize_strategy(
data=historical_data,
base_config=base_strategy_config_to_optimize,
param_grid=parameter_optimization_grid,
initial_capital=100000.0,
optimize_metric='夏普比率' # 设置优化目标为夏普比率
)
print("\n" + "="*20 + " 4. 参数优化完成 " + "="*20)
# 5. 显示优化结果
print("参数优化结果总结 (按夏普比率降序排列):")
#