文末代码总结
该Python代码实现了一个基于LightGBM机器学习模型的股指期货(以模拟的中证1000为例)日内CTA策略。其核心流程包括:
- 数据模拟/加载: 生成或加载模拟的1分钟OHLCV期货数据。
- 特征工程: 从OHLCV数据中提取技术指标(如移动平均线、收益率滞后项、波动率、量价关系等)作为模型的输入特征。
- 目标定义: 将未来N分钟的价格方向(上涨/下跌)定义为二分类预测目标。
- 参数优化: 使用
hyperopt
库进行贝叶斯优化,在每个(或每隔几个)训练窗口开始时,自动搜索LightGBM模型的最佳超参数组合(如学习率、树的数量、深度等)。优化过程基于在当前训练窗口内部划分的验证集上的准确率。 - 滚动训练: 采用滚动时间窗口的方法。模型在最近一段时间(训练窗口)的数据上训练(使用优化得到的参数),然后预测接下来一小段时间(预测窗口)的目标。之后,整个窗口向前滑动,重复优化(可选)、训练和预测过程。
- 信号生成: 将模型的预测结果(上涨概率或类别)转换为交易信号(做多/做空)。应用了信号延迟处理,确保信号基于过去信息生成。
- 简单回测: 基于生成的交易信号,在忽略交易成本和滑点的情况下,模拟交易并计算策略的累计收益率,与市场基准进行比较,并绘制收益曲线。
优点 (Pros)
- 模型适应性 (Adaptivity): 滚动训练机制使得模型能够适应变化的市场环境,用较新的数据不断重新训练,理论上比使用全历史数据训练的模型更能捕捉近期的市场模式。
- 自动化调参 (Automated Tuning): 集成了
hyperopt
进行贝叶斯优化,避免了繁琐且低效的手动调参或网格搜索,能更智能地找到适用于当前数据窗口的较优模型超参数。 - 模型性能 (Model Power): LightGBM是业界领先的梯度提升框架之一,以其训练速度快、内存占用低和预测精度高而闻名,适合处理大规模数据集和复杂的非线性关系。
- 结构清晰 (Clear Structure): 代码按照功能模块(数据、特征、目标、优化、训练、回测)进行组织,相对易于理解和修改。
- 考虑延迟 (Signal Lag): 回测部分正确地处理了信号生成与实际交易之间的时间延迟 (
signal.shift(1)
),这是避免未来函数、进行有效回测的关键一步。
缺点 (Cons)
- 依赖模拟数据 (Reliance on Simulated Data): 代码的核心是基于模拟数据运行的。模拟数据无法完全复制真实市场的复杂性(如跳空、异常波动、微观结构、事件驱动等),因此在该数据上表现良好的策略在实盘中可能效果迥异。
- 回测过于简化 (Oversimplified Backtest): 回测没有考虑交易成本(手续费、印花税)、滑点(实际成交价与预期价的差异)、保证金要求、资金管理、市场冲击成本等现实因素。这会导致回测结果极其乐观,远超实盘可能达到的效果。
- 过拟合风险 (Overfitting Risk):
- 参数优化过拟合: 即使在滚动窗口内优化,也可能对该窗口内的特定噪声或模式过拟合,导致在新数据上表现不佳。
- 特征过拟合: 如果特征工程过于复杂或特征数量过多,模型可能学到虚假的关联。
- 整体流程过拟合: 整个策略(包括特征选择、模型、优化过程)可能无意中针对了模拟数据的特定统计特性。
- 计算资源消耗 (Computational Cost): 滚动训练,特别是频繁进行参数优化(每次
fmin
需要多次训练模型),计算成本较高,对长周期、高频率数据的回测可能非常耗时。 - 目标定义简单 (Simple Target Definition): 仅预测未来固定N分钟的方向,可能忽略了波动性、风险或不同幅度变动的重要性。
- 特征工程局限 (Limited Feature Engineering): 示例中的特征相对基础,可能未包含更具预测性的因子(如订单簿特征、因子库因子、另类数据等)。
扩展方向 (Extensions)
- 使用真实数据 (Use Real Data): 将数据模拟部分替换为加载真实、高质量的历史期货高频数据(如Tick或1分钟数据),并进行严格的数据清洗。
- 构建专业回测引擎 (Build Realistic Backtester):
- 引入交易成本(按比例或固定费用)。
- 模拟滑点(固定值、按比例、或基于成交量/波动率的动态模型)。
- 考虑保证金占用和杠杆效应。
- 实现资金管理和头寸规模控制(如固定分数、波动率目标等)。
- 使用如
Backtrader
,Zipline-Reloaded
,vnpy
等专业回测框架。
- 丰富特征工程 (Enhance Feature Engineering):
- 探索更多技术指标和模式(如KDJ, MACD, 布林带宽度,形态识别)。
- 引入不同时间周期的特征(如5分钟、15分钟、日线级别的指标)。
- 考虑价差、基差、跨品种相关性等特征。
- 如果可能,整合订单簿深度、大单流等微观结构特征。
- 进行特征重要性分析和选择,移除冗余或无效特征。
- 改进模型与目标 (Improve Model & Target):
- 尝试其他机器学习模型(XGBoost, CatBoost, 神经网络如LSTM)或模型集成方法。
- 预测收益率的具体值(回归问题)而非仅仅方向。
- 预测未来波动率或风险。
- 定义多分类目标(如大涨、小涨、盘整、小跌、大跌)。
- 将模型输出的概率值用于更精细的信号生成(如根据概率大小调整仓位)。
- 优化策略与流程 (Optimize Strategy & Workflow):
- 调整滚动窗口大小和重训练频率,找到适应性和稳定性的平衡点。
- 探索扩展窗口(Expanding Window)而非纯滚动窗口。
- 实现更严格的样本外测试(Walk-Forward Optimization)。
- 在信号生成后加入过滤条件(如波动率过滤、交易量过滤、趋势过滤)。
- 加入止盈止损逻辑。
- 鲁棒性分析 (Robustness Checks):
- 在不同的市场行情(牛市、熊市、震荡市)下测试策略表现。
- 对关键参数(如
look_forward
、成本、滑点假设)进行敏感性分析。 - 使用蒙特卡洛模拟等方法评估策略的风险。
# -*- coding: utf-8 -*-
# 导入必要的库
import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import train_test_split # 用于优化时的内部验证
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
import random
from tqdm import tqdm # 用于显示进度条
from hyperopt import fmin, tpe, hp, STATUS_OK, Trials # 贝叶斯优化库
from functools import partial # 用于向目标函数传递额外参数
import warnings
warnings.filterwarnings('ignore') # 忽略一些不必要的警告
# --------------------------------------------------------------------------
# 1. 模拟数据生成 (中证1000股指期货 1分钟 K线)
# --------------------------------------------------------------------------
def generate_futures_data(start_date='2019-01-01', end_date='2023-12-31', initial_price=6000, volatility=0.0005, daily_drift_mean=0.0001, daily_drift_std=0.001, filename="simulated_csi1000_futures_1min.csv"):
"""
模拟生成股指期货1分钟OHLCV数据,并保存到CSV文件。
如果文件已存在,则直接加载。
Args:
start_date (str): 开始日期
end_date (str): 结束日期
initial_price (float): 初始价格
volatility (float): 分钟级别价格波动率(标准差)
daily_drift_mean (float): 日级别的平均漂移率
daily_drift_std (float): 日级别漂移率的标准差
filename (str): 保存/加载数据的文件名
Returns:
pd.DataFrame: 包含 'datetime', 'open', 'high', 'low', 'close', 'volume' 的DataFrame
如果生成或加载失败,返回None
"""
try:
# 尝试加载已有的数据文件
df = pd.read_csv(filename, index_col='datetime', parse_dates=True)
print(f"已从 {filename} 加载本地模拟数据.")
return df
except FileNotFoundError:
print(f"本地模拟数据文件 {filename} 未找到,开始生成...")
# 生成交易日的时间序列 (跳过周末)
trading_days = pd.bdate_range(start=start_date, end=end_date)
if trading_days.empty:
print("错误:指定的日期范围内没有交易日。")
return None
all_minutes_data = []
current_price = initial_price
# 使用tqdm显示生成进度
for day in tqdm(trading_days, desc="生成模拟数据"):
# 模拟每日漂移
daily_drift = np.random.normal(daily_drift_mean, daily_drift_std)
minute_drift = daily_drift / 240 # 假设每天240分钟交易时间 (简化)
# 模拟一天内的分钟时间点 (简化版,实际应更精确)
day_minutes = []
# 上午 9:30 - 11:30
for hour in [9, 10]:
minute_start = 30 if hour == 9 else 0
for minute in range(minute_start, 60):
day_minutes.append(pd.Timestamp(f"{day.date()} {hour:02d}:{minute:02d}:00"))
for minute in range(0, 31): # 到11:30
day_minutes.append(pd.Timestamp(f"{day.date()} 11:{minute:02d}:00"))
# 下午 13:00 - 15:00
for hour in [13, 14]:
minute_start = 0 # 13点从0分开始
for minute in range(minute_start, 60):
day_minutes.append(pd.Timestamp(f"{day.date()} {hour:02d}:{minute:02d}:00"))
# 15:00 最后一分钟
day_minutes.append(pd.Timestamp(f"{day.date()} 15:00:00"))
day_minutes = sorted(list(set(day_minutes))) # 去重并排序
# 如果当天没有生成时间点(例如数据生成逻辑错误),跳过
if not day_minutes:
continue
open_price = current_price # 当日开盘价等于前一日收盘价(简化)
for i, dt in enumerate(day_minutes):
# 模拟价格变动
price_change = np.random.normal(minute_drift, volatility)
# 防止价格变为负数(虽然期货可能性小,但模拟中可能出现极端值)
close_price = max(open_price * (1 + price_change), 0.01)
# 模拟 High 和 Low
high_price = max(open_price, close_price) * (1 + np.random.uniform(0, volatility * 0.5))
low_price = min(open_price, close_price) * (1 - np.random.uniform(0, volatility * 0.5))
# 保证 OHLC 关系正确
high_price = max(high_price, open_price, close_price)
low_price = min(low_price, open_price, close_price)
low_price = max(low_price, 0.01) # 防止low为负
# 模拟交易量 (随机生成,可根据价格波动幅度调整)
base_volume = 1000
volume_multiplier = 1 + abs(price_change) / volatility * 2 # 波动大时交易量可能更大
volume = max(0, int(np.random.poisson(base_volume * volume_multiplier))) # 确保成交量非负
all_minutes_data.append({
'datetime': dt,
'open': open_price,
'high': high_price,
'low': low_price,
'close': close_price,
'volume': volume
})
open_price = close_price # 下一分钟的开盘价是当前收盘价
# 确保当天有数据才更新current_price
if all_minutes_data and all_minutes_data[-1]['datetime'].date() == day.date():
current_price = all_minutes_data[-1]['close'] # 更新到下一天
if not all_minutes_data:
print("错误:未能生成任何模拟数据点。")
return None
df = pd.DataFrame(all_minutes_data)
df['datetime'] = pd.to_datetime(df['datetime'])
df = df.set_index('datetime')
# 保存数据到文件
try:
df.to_csv(filename)
print(f"模拟数据已保存到 {filename}")
except Exception as e:
print(f"保存模拟数据到 {filename} 失败: {e}")
print("模拟数据生成完毕.")
return df
# --------------------------------------------------------------------------
# 2. 特征工程
# --------------------------------------------------------------------------
def create_features(df, lags=[1, 3, 5, 10, 20], sma_windows=[5, 10, 20, 60], vol_window=20):
"""
基于OHLCV数据创建特征因子。
Args:
df (pd.DataFrame): 包含OHLCV数据的DataFrame,索引为datetime
lags (list): 计算收益率滞后期数
sma_windows (list): 计算移动平均线的窗口大小
vol_window (int): 计算波动率的窗口大小
Returns:
pd.DataFrame: 包含特征的DataFrame (已去除因计算产生的NaN行)
"""
print("开始进行特征工程...")
df_feat = df.copy()
# 计算对数收益率 (避免价格为0或负数导致的错误)
df_feat['log_return'] = np.log(df_feat['close'].clip(lower=1e-9) / df_feat['close'].shift(1).clip(lower=1e-9))
# 滞后收益率特征
for lag in lags:
df_feat[f'log_return_lag{lag}'] = df_feat['log_return'].shift(lag)
# 移动平均线 (SMA) 特征
for window in sma_windows:
df_feat[f'sma_{window}'] = df_feat['close'].rolling(window=window, min_periods=1).mean()
# 与当前价格的偏离度 (防止除以0)
df_feat[f'price_sma_{window}_ratio'] = df_feat['close'] / df_feat[f'sma_{window}'].replace(0, 1e-9)
# 移动平均线之间的关系 (例子:短周期/长周期)
if len(sma_windows) >= 2:
sma_short = f'sma_{sma_windows[0]}'
sma_long = f'sma_{sma_windows[-1]}'
df_feat[f'sma_ratio'] = df_feat[sma_short] / df_feat[sma_long].replace(0, 1e-9)
# 波动率特征 (基于对数收益率的标准差)
df_feat[f'volatility_{vol_window}'] = df_feat['log_return'].rolling(window=vol_window, min_periods=1).std()
# 量价特征 (简单例子:成交量变化率,处理分母为0的情况)
vol_mean = df_feat['volume'].rolling(window=vol_window, min_periods=1).mean()
df_feat['volume_change_ratio'] = df_feat['volume'] / vol_mean.replace(0, 1) # 如果均值为0,变化率设为原始值
# 移除包含NaN的行 (通常是数据开头因计算滞后项或滚动窗口造成的)
df_feat = df_feat.dropna()
print(f"特征工程完成,生成特征数量: {len(df_feat.columns) - len(df.columns)}, 剩余数据点: {len(df_feat)}")
return df_feat
# --------------------------------------------------------------------------
# 3. 目标变量定义
# --------------------------------------------------------------------------
def create_target(df, look_forward=5):
"""
定义目标变量: 未来 N 分钟的价格方向 (上涨为1, 下跌/持平为0)。
Args:
df (pd.DataFrame): 包含价格数据的DataFrame (通常是特征工程后的df)
look_forward (int): 向前看多少分钟来确定未来方向
Returns:
pd.Series: 目标变量序列 (1或0),索引与输入df对齐
"""
print(f"开始创建目标变量 (预测未来 {look_forward} 分钟方向)...")
# 计算未来N分钟后的收盘价与当前收盘价的差值
# 使用 .shift(-look_forward) 将未来价格对齐到当前行
future_close = df['close'].shift(-look_forward)
delta = future_close - df['close']
# 定义目标:如果未来价格上涨,则为1,否则为0
# 使用 np.where 处理NaN值(最后几行没有未来价格)
target = pd.Series(np.where(delta > 0, 1, 0), index=df.index)
# 移除最后 look_forward 行的目标值 (因为它们没有对应的未来价格)
target = target.iloc[:-look_forward]
print("目标变量创建完成.")
return target
# --------------------------------------------------------------------------
# 4. Hyperopt 优化相关定义
# --------------------------------------------------------------------------
# --- 4.1 定义参数搜索空间 ---
# 定义 LightGBM 超参数的搜索范围,供贝叶斯优化使用
# hp.loguniform: 对数均匀分布,适合学习率、正则化项
# hp.quniform: 均匀分布中按指定步长取整,适合树数量、叶子数、深度
# hp.uniform: 均匀分布,适合比例参数
lgb_search_space = {
'learning_rate': hp.loguniform('learning_rate', np.log(0.01), np.log(0.2)),
'n_estimators': hp.quniform('n_estimators', 100, 500, 50), # 100到500,步长50
'num_leaves': hp.quniform('num_leaves', 20, 60, 5), # 20到60,步长5
'max_depth': hp.quniform('max_depth', 3, 10, 1), # 3到10,步长1
'subsample': hp.uniform('subsample', 0.6, 1.0), # 行采样比例
'colsample_bytree': hp.uniform('colsample_bytree', 0.6, 1.0), # 特征采样比例
'reg_alpha': hp.loguniform('reg_alpha', np.log(0.001), np.log(1.0)), # L1正则化
'reg_lambda': hp.loguniform('reg_lambda', np.log(0.001), np.log(1.0)), # L2正则化
# --- 固定参数 (不参与优化,但模型训练时需要) ---
'objective': 'binary', # 目标:二分类
'metric': 'accuracy', # 内部评估指标(优化时也用这个)
'boosting_type': 'gbdt', # 使用梯度提升决策树
'seed': 42, # 随机种子,保证结果可复现
'n_jobs': -1, # 使用所有可用的CPU核心
'verbose': -1, # 控制训练过程信息输出 (-1为不输出)
}
# --- 4.2 定义优化目标函数 ---
def lgb_objective_function(params, X_train_window, y_train_window, validation_split=0.2):
"""
Hyperopt的目标函数,用于评估一组给定参数的效果。
Args:
params (dict): 由Hyperopt提供的一组参数组合。
X_train_window (pd.DataFrame): 当前滚动窗口的特征数据。
y_train_window (pd.Series): 当前滚动窗口的目标数据。
validation_split (float): 从训练窗口中划分多少比例作为内部验证集。
Returns:
dict: 包含 'loss' (需要最小化的值) 和 'status' (优化状态) 的字典。
"""
# --- 重要:类型转换 ---
# Hyperopt提供的参数可能是浮点数,需要转换为LGBM要求的整数类型
params['n_estimators'] = int(params['n_estimators'])
params['num_leaves'] = int(params['num_leaves'])
params['max_depth'] = int(params['max_depth'])
# --- 内部训练/验证划分 ---
# 在当前滚动训练窗口内部划分出验证集,仅用于本次参数评估
# 使用 stratify 保证划分后训练集和验证集中类别比例与原数据相似
try:
X_train_opt, X_val_opt, y_train_opt, y_val_opt = train_test_split(
X_train_window, y_train_window,
test_size=validation_split,
random_state=42, # 固定随机种子,保证划分可复现
stratify=y_train_window
)
except ValueError: # 如果某个类别样本过少,无法进行分层抽样
print("警告:无法进行分层抽样,将使用普通随机抽样划分内部验证集。")
X_train_opt, X_val_opt, y_train_opt, y_val_opt = train_test_split(
X_train_window, y_train_window,
test_size=validation_split,
random_state=42
)
# --- 训练模型 ---
model = lgb.LGBMClassifier(**params)
model.fit(X_train_opt, y_train_opt,
eval_set=[(X_val_opt, y_val_opt)], # 在验证集上评估
# 使用早停可以加速优化过程:如果在验证集上性能连续多轮没有提升,则停止训练
callbacks=[lgb.early_stopping(stopping_rounds=15, verbose=False)]
)
# --- 评估模型 ---
y_pred_val = model.predict(X_val_opt)
accuracy = accuracy_score(y_val_opt, y_pred_val)
# --- 返回结果 ---
# Hyperopt 需要最小化目标 'loss',所以我们返回 1 - accuracy
# accuracy 越高,loss 越小
return {'loss': 1.0 - accuracy, 'status': STATUS_OK, 'params': params}
# --------------------------------------------------------------------------
# 5. 滚动训练与预测 (集成Hyperopt优化)
# --------------------------------------------------------------------------
def rolling_train_predict_optimized(df_features, target, train_window_size, retrain_frequency,
search_space, max_evals=50, optimize_every_n_retrains=1):
"""
执行滚动训练和预测,并在每个(或每隔N个)重训练周期使用Hyperopt进行参数优化。
Args:
df_features (pd.DataFrame): 包含特征的DataFrame。
target (pd.Series): 目标变量序列。
train_window_size (int): 训练窗口大小 (数据点数量)。
retrain_frequency (int): 重新训练的频率 (数据点数量/每次预测的步长)。
search_space (dict): Hyperopt定义的参数搜索空间。
max_evals (int): 每次运行Hyperopt优化时的最大评估次数。
optimize_every_n_retrains (int): 每隔多少次重训练进行一次参数优化 (1表示每次都优化)。
Returns:
tuple: (pd.Series, dict)
- pd.Series: 包含所有预测结果的序列 (索引与df_features对齐)。
- dict: 记录每次优化找到的最佳参数 (键为预测开始的时间点)。
"""
all_predictions = pd.Series(index=df_features.index, dtype=float) # 初始化用于存储预测结果的Series
best_params_history = {} # 初始化字典,用于记录每次优化找到的最佳参数
# 确保特征和目标的时间索引对齐
aligned_index = df_features.index.intersection(target.index)
df_features = df_features.loc[aligned_index]
target = target.loc[aligned_index]
# 初始化滚动窗口的起始索引
start_index = 0
retrain_counter = 0 # 计数器,用于控制优化频率
current_best_params = None # 保存当前使用的最佳参数
print("开始带优化的滚动训练和预测...")
# 创建进度条
pbar = tqdm(total=len(df_features) - train_window_size, desc="滚动训练进度")
# 循环执行滚动训练和预测
while start_index + train_window_size < len(df_features):
# 定义当前训练窗口的结束索引
train_end_index = start_index + train_window_size
# 定义当前预测窗口的结束索引,确保不超过数据末尾
predict_end_index = min(train_end_index + retrain_frequency, len(df_features))
# 如果预测窗口的起始点已经等于或超过终点,说明没有数据可预测,结束循环
if train_end_index >= predict_end_index:
break
# --- 获取当前窗口的数据 ---
X_train_window = df_features.iloc[start_index : train_end_index]
y_train_window = target.iloc[start_index : train_end_index]
X_predict = df_features.iloc[train_end_index : predict_end_index]
# 如果待预测的数据为空,跳过本次循环(可能发生在数据末尾)
if X_predict.empty:
break
# --- 参数优化步骤 ---
# 判断是否到达进行参数优化的时机
if retrain_counter % optimize_every_n_retrains == 0:
print(f"\n窗口 {retrain_counter // optimize_every_n_retrains + 1} (数据点 {start_index}-{train_end_index}): 开始进行参数优化 (最大评估次数: {max_evals})...")
# 创建一个部分应用的函数:固定目标函数中的训练数据参数
# 这样fmin调用时只需要传入变化的params
objective_fn = partial(lgb_objective_function, X_train_window=X_train_window, y_train_window=y_train_window)
# 创建Trials对象,用于记录优化过程
trials = Trials()
# 运行贝叶斯优化 (fmin)
# fn: 目标函数
# space: 参数搜索空间
# algo: 优化算法 (tpe.suggest 是常用的高效算法)
# max_evals: 最大评估次数
# trials: 用于记录过程的Trials对象
# rstate: 随机数生成器状态,用于保证优化过程可复现(如果需要)
# show_progressbar: 是否显示 hyperopt 内部的进度条
try:
best_trial = fmin(fn=objective_fn,
space=search_space,
algo=tpe.suggest,
max_evals=max_evals,
trials=trials,
rstate=np.random.default_rng(seed=start_index), # 使用变化的种子增加探索性
show_progressbar=False # 可以设为True看详细进度
)
# --- 获取优化找到的最佳参数 ---
# trials.best_trial 包含了损失最小的那次运行的所有信息
if trials.best_trial and 'result' in trials.best_trial and 'params' in trials.best_trial['result']:
best_run = trials.best_trial['result']
current_best_params = best_run['params']
# --- 重要:再次进行类型转换,确保用于最终模型的参数类型正确 ---
current_best_params['n_estimators'] = int(current_best_params['n_estimators'])
current_best_params['num_leaves'] = int(current_best_params['num_leaves'])
current_best_params['max_depth'] = int(current_best_params['max_depth'])
print(f"窗口 {retrain_counter // optimize_every_n_retrains + 1}: 优化完成。最佳内部验证集损失 (1-Accuracy): {best_run['loss']:.4f}")
print(f"找到的最佳参数: {current_best_params}")
# 记录这次找到的最佳参数,使用预测窗口的起始时间作为键
best_params_history[df_features.index[train_end_index]] = current_best_params
else:
print("警告:Hyperopt 优化未能找到有效的最佳试验结果,将使用上一次的参数或默认参数。")
# 如果优化失败,且之前有参数,则继续使用;否则触发下面的默认参数逻辑
except Exception as e:
print(f"错误:Hyperopt 优化过程中发生异常: {e}")
print("将尝试使用上一次的参数或默认参数。")
# 同样,如果优化失败,则尝试使用旧参数或默认参数
# 如果 current_best_params 仍然是 None (可能是第一次运行且optimize_every_n_retrains>1,或者优化失败)
if current_best_params is None:
print("警告:未使用优化参数,将使用搜索空间中的固定值或预定义默认值。")
# 从搜索空间提取固定值作为基础
current_best_params = {k: v for k, v in search_space.items() if not hasattr(v, 'pos_args')}
# 补充必要的默认值(如果未在固定值中定义)
current_best_params.setdefault('learning_rate', 0.05)
current_best_params.setdefault('n_estimators', 150)
current_best_params.setdefault('num_leaves', 31)
current_best_params.setdefault('max_depth', -1)
current_best_params.setdefault('subsample', 0.8)
current_best_params.setdefault('colsample_bytree', 0.8)
current_best_params.setdefault('reg_alpha', 0.0)
current_best_params.setdefault('reg_lambda', 0.0)
# 确保类型正确
current_best_params['n_estimators'] = int(current_best_params['n_estimators'])
current_best_params['num_leaves'] = int(current_best_params['num_leaves'])
current_best_params['max_depth'] = int(current_best_params['max_depth'])
print(f"使用的启动/默认参数: {current_best_params}")
# --- 使用找到的最佳参数(或上次优化的/默认的参数)训练最终模型 ---
# print(f"窗口 {retrain_counter // optimize_every_n_retrains + 1}: 使用参数 {current_best_params} 在数据点 {start_index}-{train_end_index} 上训练模型...")
model = lgb.LGBMClassifier(**current_best_params)
# 在完整的当前训练窗口数据上训练模型
model.fit(X_train_window, y_train_window)
# --- 进行预测 ---
# 使用训练好的模型对下一个窗口的数据进行预测
predictions = model.predict(X_predict)
# 将预测结果存储到 all_predictions Series 中,索引保持一致
all_predictions.iloc[train_end_index : predict_end_index] = predictions
# --- 更新进度和索引 ---
update_amount = predict_end_index - train_end_index # 本次循环处理的数据点数量
pbar.update(update_amount) # 更新进度条
start_index += retrain_frequency # 将训练窗口向前滚动指定的频率
retrain_counter += 1 # 增加重训练计数器
pbar.close() # 关闭进度条
print("带优化的滚动训练和预测完成.")
# 返回所有预测结果 (去除开头因窗口未满而产生的NaN) 和 优化参数历史记录
return all_predictions.dropna(), best_params_history
# --------------------------------------------------------------------------
# 6. 信号生成与简单回测
# --------------------------------------------------------------------------
def backtest_strategy(df_raw, predictions, look_forward=5):
"""
基于模型预测结果进行简单的向量化回测 (忽略交易成本和滑点)。
Args:
df_raw (pd.DataFrame): 包含原始OHLCV数据的DataFrame,用于获取价格进行收益计算。
predictions (pd.Series): 模型预测结果 (1: 预测上涨, 0: 预测下跌/持平)。
look_forward (int): 预测时向前看的时间窗口(用于对齐)。
Returns:
pd.DataFrame: 包含回测结果(信号、收益率、累计净值)的DataFrame。
如果无法生成回测结果,返回空的DataFrame。
"""
print("开始进行回测...")
# 创建一个用于回测的DataFrame副本,并将预测结果合并进去
# 确保使用原始数据的索引,以便获取正确的价格信息
backtest_df = df_raw[['open', 'close']].copy()
backtest_df['prediction'] = predictions
# --- 生成交易信号 ---
# 规则: 预测为1 (上涨),则信号为 1 (做多)
# 预测为0 (下跌/持平),则信号为 -1 (做空)
backtest_df['signal'] = np.where(backtest_df['prediction'] == 1, 1, -1)
# --- 关键:信号延迟 ---
# t 时刻的预测是基于 t 时刻收盘时的信息得到的,只能指导 t+1 时刻开盘后的交易。
# 因此,t 时刻产生的信号需要应用在 t+1 时刻的收益计算上。
# 我们将信号向下移动一行 (shift(1)),使得 t 时刻的信号对齐 t+1 时刻的收益率。
backtest_df['signal'] = backtest_df['signal'].shift(1)
# --- 计算策略收益 ---
# 计算每个分钟的对数收益率 (用于计算持有收益)
# 使用原始数据的收盘价计算
backtest_df['next_log_return'] = np.log(backtest_df['close'].clip(lower=1e-9) / backtest_df['close'].shift(1).clip(lower=1e-9))
# 策略的分钟对数收益率 = t+1 时刻的信号 * t+1 时刻的对数收益率
backtest_df['strategy_log_return'] = backtest_df['signal'] * backtest_df['next_log_return']
# --- 清理数据 ---
# 移除因 shift(1) 和收益率计算产生的NaN值
backtest_df = backtest_df.dropna(subset=['signal', 'strategy_log_return'])
# 如果清理后没有数据,则无法进行后续计算
if backtest_df.empty:
print("回测结果为空,可能数据不足或信号未能生成有效收益。")
# 返回空的DataFrame,避免后续计算出错
return pd.DataFrame(columns=['cumulative_market_return', 'cumulative_strategy_return', 'strategy_log_return'])
# --- 计算累计收益率 ---
# 市场基准收益 (买入持有策略的对数收益率,这里简单用分钟收益累加)
backtest_df['cumulative_market_log_return'] = backtest_df['next_log_return'].cumsum()
# 策略累计对数收益率
backtest_df['cumulative_strategy_log_return'] = backtest_df['strategy_log_return'].cumsum()
# 将累计对数收益率转换为累计净值 (指数化,起点为1)
backtest_df['cumulative_market_return'] = np.exp(backtest_df['cumulative_market_log_return'])
backtest_df['cumulative_strategy_return'] = np.exp(backtest_df['cumulative_strategy_log_return'])
print("回测计算完成.")
return backtest_df[['signal', 'next_log_return', 'strategy_log_return', 'cumulative_market_return', 'cumulative_strategy_return']]
# --------------------------------------------------------------------------
# 主程序入口
# --------------------------------------------------------------------------
if __name__ == "__main__":
# --- 参数设置 ---
DATA_START_DATE = '2022-01-01' # 数据开始日期 (模拟数据用)
DATA_END_DATE = '2022-12-31' # 数据结束日期 (模拟数据用, 使用1年数据演示)
DATA_FILENAME = "simulated_csi1000_futures_1min_2022.csv" # 数据文件名
LOOK_FORWARD_MINUTES = 5 # 预测未来多少分钟的方向
# 滚动窗口参数
MINUTES_PER_DAY_APPROX = 240 # 每日大致交易分钟数 (用于估算天数对应的点数)
TRAIN_DAYS = 45 # 训练窗口天数 (约2个月多)
RETRAIN_DAYS = 5 # 每隔多少天(的分钟数)重新训练一次 (约1周)
# Hyperopt 优化参数
OPTIMIZE_N_RETRAINS = 4 # 每隔多少次重训练进行一次参数优化 (这里是每 4*5 = 20 个交易日左右优化一次)
MAX_OPTIMIZATION_EVALS = 35 # 每次优化运行的最大评估次数 (演示用,实际可设更高如50-100)
# --- 1. 数据准备 ---
# 生成或加载模拟数据
df_raw = generate_futures_data(start_date=DATA_START_DATE, end_date=DATA_END_DATE, filename=DATA_FILENAME)
# 检查数据是否成功加载/生成
if df_raw is None or df_raw.empty:
print("错误:数据加载或生成失败,程序退出。")
exit()
# --- 2. 特征工程 ---
# 创建特征因子
df_features = create_features(df_raw, lags=[1, 2, 3, 5, 8, 13], sma_windows=[5, 10, 20, 30, 60], vol_window=30)
# 检查特征工程后是否有数据
if df_features.empty:
print("错误:特征工程后数据为空,请检查数据或特征参数。程序退出。")
exit()
# --- 3. 目标变量 ---
# 创建预测目标
target = create_target(df_features, look_forward=LOOK_FORWARD_MINUTES)
# --- 4/5. 滚动训练 (带优化) 与预测 ---
# 计算滚动窗口对应的点数
train_window_points = TRAIN_DAYS * MINUTES_PER_DAY_APPROX
retrain_frequency_points = RETRAIN_DAYS * MINUTES_PER_DAY_APPROX
# 确保数据量足够进行至少一次完整的训练和预测
if len(df_features) < train_window_points + retrain_frequency_points:
print(f"错误:数据量不足 ({len(df_features)} points)。")
print(f"需要至少 {train_window_points} 点用于初始训练,以及 {retrain_frequency_points} 点用于第一次预测。")
print("请增加数据量或减小训练窗口/重训练频率。程序退出。")
exit()
# 执行带优化的滚动训练和预测
predictions, params_log = rolling_train_predict_optimized(
df_features, # 特征数据
target, # 目标数据
train_window_points, # 训练窗口大小(点数)
retrain_frequency_points, # 重训练频率(点数)
lgb_search_space, # Hyperopt 搜索空间
max_evals=MAX_OPTIMIZATION_EVALS, # 每次优化评估次数
optimize_every_n_retrains=OPTIMIZE_N_RETRAINS # 优化频率
)
# 打印记录的最佳参数 (可选)
# print("\n--- 优化找到的最佳参数记录 ---")
# for date, params in params_log.items():
# print(f"预测开始于 {date}, 使用参数: {params}")
# --- 6. 简单回测与评估 ---
# 检查是否有预测结果
if predictions.empty:
print("错误:滚动训练未能生成任何预测结果。无法进行回测。")
exit()
print("\n开始进行回测...")
# 使用原始数据 df_raw 和预测结果 predictions 进行回测
# 注意:传入 df_raw 是为了使用其 'close' 价格计算真实收益
backtest_results = backtest_strategy(df_raw, predictions, look_forward=LOOK_FORWARD_MINUTES)
# 检查回测结果是否为空
if backtest_results.empty:
print("回测未能生成有效结果。")
else:
# --- 打印基础性能指标 ---
final_strategy_return = backtest_results['cumulative_strategy_return'].iloc[-1]
final_market_return = backtest_results['cumulative_market_return'].iloc[-1]
print(f"\n--- 回测结果 (简化版, 带优化) ---")
print(f"回测时间范围: {backtest_results.index.min()} 到 {backtest_results.index.max()}")
print(f"最终策略累计净值: {final_strategy_return:.4f}")
print(f"最终市场累计净值 (买入持有基准): {final_market_return:.4f}")
# 计算年化收益率(近似)
total_duration_years = (backtest_results.index.max() - backtest_results.index.min()).days / 365.25
if total_duration_years > 0:
# 避免净值为0或负数导致开方错误
if final_strategy_return > 0:
annualized_strategy_return = (final_strategy_return**(1/total_duration_years) - 1) * 100
print(f"策略年化收益率 (近似): {annualized_strategy_return:.2f}%")
else:
print("策略年化收益率 (近似): N/A (最终净值非正)")
annualized_strategy_return = -100.0 # 赋一个无效值
if final_market_return > 0:
annualized_market_return = (final_market_return**(1/total_duration_years) - 1) * 100
print(f"市场年化收益率 (近似): {annualized_market_return:.2f}%")
else:
print("市场年化收益率 (近似): N/A (最终净值非正)")
# 计算夏普比率(简化,假设无风险利率为0)
# 年化波动率 = 分钟收益率标准差 * sqrt(每年交易分钟数)
# 每年交易分钟数 ≈ 252 (年交易日) * 240 (每日分钟数)
annual_trading_minutes = 252 * MINUTES_PER_DAY_APPROX
strategy_returns_std_annual = backtest_results['strategy_log_return'].std() * np.sqrt(annual_trading_minutes)
if strategy_returns_std_annual > 1e-9: # 避免除以极小的波动率
sharpe_ratio = (annualized_strategy_return / 100) / strategy_returns_std_annual
print(f"策略夏普比率 (简化): {sharpe_ratio:.2f}")
else:
print("策略夏普比率 (简化): N/A (年化波动率接近0)")
else:
print("回测期不足一年,无法计算年化指标。")
# --- 绘制累计收益曲线 ---
print("绘制累计收益曲线...")
plt.figure(figsize=(14, 7))
plt.plot(backtest_results.index, backtest_results['cumulative_strategy_return'], label=f'Strategy (Optimized LGBM, Look Forward={LOOK_FORWARD_MINUTES}min)')
plt.plot(backtest_results.index, backtest_results['cumulative_market_return'], label='Market Benchmark (Buy & Hold)', alpha=0.7)
plt.title('Strategy vs Market Cumulative Returns (Simplified Backtest)')
plt.xlabel('Date')
plt.ylabel('Cumulative Return (Normalized to 1)')
plt.yscale('log') # 使用对数坐标轴,更好地观察长期趋势和回撤
plt.legend()
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.tight_layout() # 调整布局防止标签重叠
plt.show()