基于LightGBM的股指期货CTA策略开发例子 包括使用贝叶斯优化优化LightGBM参数

文末代码总结

该Python代码实现了一个基于LightGBM机器学习模型的股指期货(以模拟的中证1000为例)日内CTA策略。其核心流程包括:

  1. 数据模拟/加载: 生成或加载模拟的1分钟OHLCV期货数据。
  2. 特征工程: 从OHLCV数据中提取技术指标(如移动平均线、收益率滞后项、波动率、量价关系等)作为模型的输入特征。
  3. 目标定义: 将未来N分钟的价格方向(上涨/下跌)定义为二分类预测目标。
  4. 参数优化: 使用hyperopt库进行贝叶斯优化,在每个(或每隔几个)训练窗口开始时,自动搜索LightGBM模型的最佳超参数组合(如学习率、树的数量、深度等)。优化过程基于在当前训练窗口内部划分的验证集上的准确率。
  5. 滚动训练: 采用滚动时间窗口的方法。模型在最近一段时间(训练窗口)的数据上训练(使用优化得到的参数),然后预测接下来一小段时间(预测窗口)的目标。之后,整个窗口向前滑动,重复优化(可选)、训练和预测过程。
  6. 信号生成: 将模型的预测结果(上涨概率或类别)转换为交易信号(做多/做空)。应用了信号延迟处理,确保信号基于过去信息生成。
  7. 简单回测: 基于生成的交易信号,在忽略交易成本和滑点的情况下,模拟交易并计算策略的累计收益率,与市场基准进行比较,并绘制收益曲线。

优点 (Pros)

  1. 模型适应性 (Adaptivity): 滚动训练机制使得模型能够适应变化的市场环境,用较新的数据不断重新训练,理论上比使用全历史数据训练的模型更能捕捉近期的市场模式。
  2. 自动化调参 (Automated Tuning): 集成了hyperopt进行贝叶斯优化,避免了繁琐且低效的手动调参或网格搜索,能更智能地找到适用于当前数据窗口的较优模型超参数。
  3. 模型性能 (Model Power): LightGBM是业界领先的梯度提升框架之一,以其训练速度快、内存占用低和预测精度高而闻名,适合处理大规模数据集和复杂的非线性关系。
  4. 结构清晰 (Clear Structure): 代码按照功能模块(数据、特征、目标、优化、训练、回测)进行组织,相对易于理解和修改。
  5. 考虑延迟 (Signal Lag): 回测部分正确地处理了信号生成与实际交易之间的时间延迟 (signal.shift(1)),这是避免未来函数、进行有效回测的关键一步。

缺点 (Cons)

  1. 依赖模拟数据 (Reliance on Simulated Data): 代码的核心是基于模拟数据运行的。模拟数据无法完全复制真实市场的复杂性(如跳空、异常波动、微观结构、事件驱动等),因此在该数据上表现良好的策略在实盘中可能效果迥异。
  2. 回测过于简化 (Oversimplified Backtest): 回测没有考虑交易成本(手续费、印花税)、滑点(实际成交价与预期价的差异)、保证金要求、资金管理、市场冲击成本等现实因素。这会导致回测结果极其乐观,远超实盘可能达到的效果。
  3. 过拟合风险 (Overfitting Risk):
    • 参数优化过拟合: 即使在滚动窗口内优化,也可能对该窗口内的特定噪声或模式过拟合,导致在新数据上表现不佳。
    • 特征过拟合: 如果特征工程过于复杂或特征数量过多,模型可能学到虚假的关联。
    • 整体流程过拟合: 整个策略(包括特征选择、模型、优化过程)可能无意中针对了模拟数据的特定统计特性。
  4. 计算资源消耗 (Computational Cost): 滚动训练,特别是频繁进行参数优化(每次fmin需要多次训练模型),计算成本较高,对长周期、高频率数据的回测可能非常耗时。
  5. 目标定义简单 (Simple Target Definition): 仅预测未来固定N分钟的方向,可能忽略了波动性、风险或不同幅度变动的重要性。
  6. 特征工程局限 (Limited Feature Engineering): 示例中的特征相对基础,可能未包含更具预测性的因子(如订单簿特征、因子库因子、另类数据等)。

扩展方向 (Extensions)

  1. 使用真实数据 (Use Real Data): 将数据模拟部分替换为加载真实、高质量的历史期货高频数据(如Tick或1分钟数据),并进行严格的数据清洗。
  2. 构建专业回测引擎 (Build Realistic Backtester):
    • 引入交易成本(按比例或固定费用)。
    • 模拟滑点(固定值、按比例、或基于成交量/波动率的动态模型)。
    • 考虑保证金占用和杠杆效应。
    • 实现资金管理和头寸规模控制(如固定分数、波动率目标等)。
    • 使用如Backtrader, Zipline-Reloaded, vnpy等专业回测框架。
  3. 丰富特征工程 (Enhance Feature Engineering):
    • 探索更多技术指标和模式(如KDJ, MACD, 布林带宽度,形态识别)。
    • 引入不同时间周期的特征(如5分钟、15分钟、日线级别的指标)。
    • 考虑价差、基差、跨品种相关性等特征。
    • 如果可能,整合订单簿深度、大单流等微观结构特征。
    • 进行特征重要性分析和选择,移除冗余或无效特征。
  4. 改进模型与目标 (Improve Model & Target):
    • 尝试其他机器学习模型(XGBoost, CatBoost, 神经网络如LSTM)或模型集成方法。
    • 预测收益率的具体值(回归问题)而非仅仅方向。
    • 预测未来波动率或风险。
    • 定义多分类目标(如大涨、小涨、盘整、小跌、大跌)。
    • 将模型输出的概率值用于更精细的信号生成(如根据概率大小调整仓位)。
  5. 优化策略与流程 (Optimize Strategy & Workflow):
    • 调整滚动窗口大小和重训练频率,找到适应性和稳定性的平衡点。
    • 探索扩展窗口(Expanding Window)而非纯滚动窗口。
    • 实现更严格的样本外测试(Walk-Forward Optimization)。
    • 在信号生成后加入过滤条件(如波动率过滤、交易量过滤、趋势过滤)。
    • 加入止盈止损逻辑。
  6. 鲁棒性分析 (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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值