第19章:Optuna实战:安装配置、目标函数与搜索空间

说实话,做量化交易这几年,我调参调得最多的就是基差策略。以前用网格搜索,那叫一个痛苦——参数一多,组合爆炸,跑一宿都出不来结果。后来接触到Optuna,嗯,这玩意儿确实香。今天我就把Optuna这套东西掰开揉碎了讲给你听。

19.1 安装与配置:别踩这些坑

安装Optuna其实很简单,一行命令搞定:

pip install optuna

但我建议你用conda环境,尤其是做回测的时候。我曾经在系统Python里直接装,结果跟numpy版本冲突,折腾了俩小时。后来学乖了,专门建个虚拟环境:

conda create -n optuna_env python=3.9
conda activate optuna_env
pip install optuna pandas numpy matplotlib

这里有个小细节——如果你要用TPE算法(后面会讲),建议装个额外的依赖:

pip install optuna[plotly]

这样可视化效果会好很多。我个人习惯把Optuna跟Jupyter Notebook配合使用,边调边看结果,效率高。

注意:Windows用户如果遇到安装报错,多半是C++编译环境问题。去装个Microsoft Visual C++ Redistributable就能解决。

19.2 定义目标函数:核心中的核心

目标函数是什么?说白了,就是你要优化的那个东西。在基差交易里,我们通常优化夏普比率或年化收益率。

来看个实际例子。假设我们有个简单的基差策略,参数就两个:zscore_threshold(开仓阈值)和lookback_window(回看窗口)。目标函数长这样:

import optuna
import numpy as np

def objective(trial):
    # 定义参数
    zscore_threshold = trial.suggest_float('zscore_threshold', 1.0, 3.0)
    lookback_window = trial.suggest_int('lookback_window', 10, 60)
    
    # 模拟回测(这里简化了)
    sharpe = backtest_strategy(zscore_threshold, lookback_window)
    
    return sharpe  # Optuna默认最大化

你可能会问:为什么返回值是夏普比率而不是收益率?嗯,我在项目里吃过亏——单纯优化收益率,容易过拟合出极端参数。夏普比率考虑了风险调整,结果更稳健。

小技巧:如果想让Optuna最小化某个指标(比如最大回撤),在返回值前面加个负号就行:return -max_drawdown

19.3 定义搜索空间:别太贪心

搜索空间就是参数的范围。Optuna支持好几种类型:

参数类型 函数 适用场景
连续型 suggest_float 阈值、比例等
离散型 suggest_int 窗口长度、次数等
类别型 suggest_categorical 策略模式、合约选择等

举个例子,我有个策略需要选择是近月还是远月合约:

contract_type = trial.suggest_categorical('contract_type', ['near', 'far'])

这里有个经验之谈——搜索空间别设太大。我曾经把lookback_window设成1到200,结果Optuna花了大量时间在无效区域探索。后来我根据经验缩到10到60,效果反而更好。你想想看,参数范围越合理,找到最优解的速度就越快。

19.4 优化器选择:TPE vs CMA-ES

Optuna内置了好几种优化算法,最常用的就两个:TPE和CMA-ES。

TPE(Tree-structured Parzen Estimator):这是Optuna的默认算法。它的思路是——先随机采样一批参数,然后根据结果好坏,把参数空间分成"好区域"和"差区域",接下来重点在好区域里采样。

我个人特别喜欢TPE,原因很简单:它处理离散参数和条件参数特别顺手。比如你的策略有时候用均线,有时候用布林带,TPE能自动处理这种"如果A则B"的逻辑。

CMA-ES(Covariance Matrix Adaptation Evolution Strategy):这个算法更"暴力"一些。它通过不断调整参数分布的协方差矩阵来逼近最优解。适合连续型参数多、参数之间关联性强的情况。

怎么选?我一般这样判断:

  • 参数少于10个,且包含离散参数 → 用TPE
  • 参数全是连续型,且超过10个 → 用CMA-ES
  • 不确定?先用TPE跑100次看看结果

代码上切换很简单:

# 默认TPE
study = optuna.create_study(direction='maximize')

# 指定CMA-ES
study = optuna.create_study(sampler=optuna.samplers.CmaEsSampler(), direction='maximize')
避坑指南:我曾经在参数维度很高(20+)的时候用了TPE,结果收敛特别慢。换成CMA-ES后,虽然每次迭代计算量大了点,但整体收敛速度反而更快。

19.5 可视化结果:一眼看出好坏

跑完优化,光看数字可不行。Optuna的可视化功能很强大,我常用的就这几个:

1. 优化历史图:看看每次迭代的目标值变化

optuna.visualization.plot_optimization_history(study)

2. 参数重要性图:哪个参数对结果影响最大?一目了然

optuna.visualization.plot_param_importances(study)

3. 平行坐标图:看参数组合与目标值的关系

optuna.visualization.plot_parallel_coordinate(study)

我一般先看参数重要性图。如果某个参数重要性接近0,说明这个参数设成啥都差不多,下次优化可以直接固定住,减少搜索维度。

下面这张图展示了Optuna调参的核心流程,我画了个简图帮你理解:

Optuna调参核心流程 定义目标函数 返回夏普比率等指标 定义搜索空间 参数范围与类型 选择优化器 TPE / CMA-ES 迭代优化循环 采样 → 评估 → 更新模型 重复N次 可视化结果 优化历史 / 参数重要性 / 平行坐标

跑完优化后,记得用study.best_params查看最优参数组合:

print("最佳参数:", study.best_params)
print("最佳夏普:", study.best_value)
我的习惯:每次优化完,我会把study对象保存下来,方便以后复盘:joblib.dump(study, 'optuna_study.pkl')

好了,Optuna这套东西其实不复杂。核心就三步:定义目标函数、划好搜索空间、选对优化器。剩下的交给算法去跑就行。你只要记住——参数范围别太贪心,可视化结果多看看,基本不会出大问题。

公众号:蓝海资料掘金营,微信deep3321