寻参优化#

示例策略#

策略参数#

我们继续使用前面教程中的双均线策略,并加入更加两个可优化指标参数:

  • min_stock_price: 最低股价。低价股交易时滑点可能更严重,但价格下限过高,会减少开仓机会。

  • min_volatility: 最低波动率。波动率太低的股票,可能长期占用仓位,降低资金利用率;波动率过高的股票,可能日内同时触发止盈/止损,降低回测结果的可靠性,更可能导致当日买入即出现较大亏损。

[1]:
import pandas as pd
from tradepy.strategy.base import BacktestStrategy, BuyOption
from tradepy.strategy.factors import FactorsMixin
from tradepy.decorators import tag


class MovingAverageCrossoverStrategy(BacktestStrategy, FactorsMixin):

    @tag(outputs=["ema10_ref1", "sma30_ref1"], notna=True)
    def moving_averages_ref1(self, ema10, sma30) -> pd.Series:
        return ema10.shift(1), sma30.shift(1)

    def should_buy(self, orig_open, sma120, ema10, sma30, typical_price, atr,
                   ema10_ref1, sma30_ref1, close, company) -> BuyOption | None:
        if "ST" in company:
            return

        if orig_open < self.min_stock_price:
            return

        volatility = 100 * atr / typical_price
        if volatility < self.min_volatility:
            return

        if (ema10 > sma120) and (ema10_ref1 < sma30_ref1) and (ema10 > sma30):
            return close, 1

    def should_sell(self, ema10, sma30, ema10_ref1, sma30_ref1):
        return (ema10_ref1 > sma30_ref1) and (ema10 < sma30)

    def pre_process(self, df: pd.DataFrame) -> pd.DataFrame:
        return df.query('market != "科创板"').copy()
[ ]:

参数空间#

由于该策略使用静态止盈/止损,止盈/止损点位也应加入可调参数。我们可以设计如下的参数搜寻空间:

  • min_stock_price: [3, 5, 10]

  • min_volatility: [2, 4, 7]

  • 止盈: [4.5, 8]

  • 止损: [3, 5]

使用网格搜索时,这一共有 3 x 3 x 2 x 2 = 36组参数。另外,如果策略带有一定随机性(比如当日触发买入信号标的数量,大于最大开仓数量时,需要随机选择标的),每组参数还应回测多轮,再以平均收益评估。假设每组跑20轮,最终需要运行 36 * 20 = 720 次回测。

网格空间会随着参数数量而呈指数增长,只适合参数数量和搜索范围都较少的情况下使用。TradePy未来将支持贝叶斯优化和梯度优化等更加高效的超参数搜索算法。
[ ]:

寻参计算#

详见如下代码。要特别注意的是, 策略代码需放到Python文件,在这个例子中,我们将其放在当前工作目录下的 ma_cross.py

[3]:
from tradepy.optimization.schedulers import OptimizationScheduler
from tradepy.optimization.result import OptimizationResult
from tradepy.core.conf import BacktestConf, StrategyConf, OptimizationConf, SlippageConf


conf = OptimizationConf(
    repetition=20,
    backtest=BacktestConf(
        cash_amount=1e6,
        broker_commission_rate=0.01,
        min_broker_commission_fee=0,
        strategy=StrategyConf(
            strategy_class="ma_cross.MovingAverageCrossoverStrategy",
            take_profit_slip=SlippageConf(
                method='max_jump',
                params=1
            ),
            stop_loss_slip=SlippageConf(
                method='max_pct',
                params=0.1
            ),
            max_position_opens=10,
            max_position_size=0.25,
            min_trade_amount=8000,
        )
    )
)

param_search_ranges = [
    { "name": "min_stock_price", "range": [3, 5, 10] },
    { "name": "min_volatility", "range": [2, 4, 7] },
    { "name": "stop_loss", "range": [3, 5] },
    { "name": "take_profit", "range": [4.5, 8] },
]

scheduler = OptimizationScheduler(conf, param_search_ranges)
result: OptimizationResult = scheduler.run(data_df=df)
2023-08-23 20:27:03.539 | INFO     | tradepy.optimization.schedulers:__init__:41 - 任务工作目录: /Users/dilu/.tradepy/optimizer/2023-08-23/20:27:03
  1. 止损和止盈是两个特殊的可搜参数,名字必须为 stop_losstake_profit
  2. OptimizationScheduler.rundata_df 可以是通过 StocksDailyBarsDepot 加载的原始日K数据,也可以是已经包含了策略指标的。如果是原始数据, OptimizationScheduler 会先调用策略类预生成含指标的回测数据,并保存到本次寻参的工作目录中。

部分运行日志如下。默认设置下,计算并行数为 CPU核数 * 0.75 (向下取整),运行时可访问 http://localhost:8787 查看Dask worker的状态等监控信息。

  1. 请根据可用内存来调整并行数,可通过设置 OptimizationScheduler.rundask_args 的worker数量来控制。
  2. 每次运行寻参都会创建一些工作目录,位置是 ~/.tradepy/optimizer,时间久了请注意自行清理。
tradepy.optimization.schedulers:__init__:40 - 任务工作目录: /Users/dilu/.tradepy/optimizer/2023-08-21/13:56:47
>>> 获取待计算因子
- 待计算: [sma30, sma120, ema10, typical_price, atr, moving_averages_ref1, ema10_ref1, sma30_ref1]
>>> 计算每支个股的后复权价格以及技术因子
100%|█████████████████████████████████| 5042/5042 [01:03<00:00, 79.60it/s]
tradepy.optimization.schedulers:_output_indicators_df:67 - 回测数据已保存至: /Users/dilu/.tradepy/optimizer/2023-08-21/13:56:47/dataset.pkl
tradepy.optimization.schedulers:run:148 - 启动Dask集群: id=Scheduler-ca6ee5e8-8b84-4bad-bf21-e4b2ccb7c383, dashboard port=8787, <Client: 'tcp://127.0.0.1:62136' processes=6 threads=6, memory=16.00 GiB>
tradepy.optimization.schedulers:_run:210 - 第1次执行
tradepy.optimization.schedulers:__run_once:189 - 获取第1个参数批, 批数量 = 36
tradepy.optimization.schedulers:submit_tasks_and_patch_results:100 - 提交36个任务
tradepy.optimization.worker:run:62 - 开始执行任务: 8d9daaa5-8b60-4f0a-9426-22ffe747ff72
tradepy.optimization.worker:run:62 - 开始执行任务: 4e5d4149-b0df-47b3-bc88-496f1481db7b
tradepy.optimization.worker:run:62 - 开始执行任务: 138f81e3-7cf7-49c3-83c1-e65a1bbc9bc8
tradepy.optimization.worker:run:62 - 开始执行任务: cb12e919-8633-4ee1-8141-91657d8cc637
tradepy.optimization.worker:run:62 - 开始执行任务: c3b4de0c-4cc4-462a-8c13-7da35f5313e0
tradepy.optimization.worker:run:62 - 开始执行任务: d3037570-fc75-44c3-b7bf-5507242500d9
[ ]:

分析结果#

如果以收益率为排序,则最佳一组参数为 { min_stock_price: 3, min_volatility: 2, stop_loss: 5, take_profit: 8 },按平均值计,1850%,胜率40.15%,最大回测-19.09%,夏普比率0.88。另外,我们也可以观察到到盈亏比和胜率呈负相关,这也是交易策略的一个普遍规律。

[6]:
metrics_df = result.get_total_metrics()
metrics_df[:10]
[6]:
收益率 胜率 最大回撤 开仓数 夏普比率
mean std mean std mean std mean std mean std
min_stock_price min_volatility stop_loss take_profit
3.0 2.0 5.0 8.0 185.43 21.16 40.63 0.38 -19.09 1.61 8015.25 90.13 0.88 0.15
5.0 2.0 5.0 8.0 161.03 15.55 40.30 0.31 -19.35 1.60 7703.05 78.94 0.70 0.12
4.5 152.95 14.40 53.67 0.32 -21.51 1.89 8321.35 44.74 0.65 0.13
3.0 2.0 5.0 4.5 152.84 13.04 53.58 0.25 -19.16 1.69 8523.65 33.41 0.65 0.11
10.0 2.0 5.0 4.5 122.50 11.18 53.48 0.30 -30.24 3.86 7369.90 46.04 0.34 0.12
8.0 119.58 11.58 39.84 0.33 -28.54 2.33 6684.10 77.82 0.31 0.12
3.0 4.0 5.0 8.0 107.82 9.43 39.75 0.31 -38.16 1.82 6770.10 41.68 0.19 0.09
10.0 2.0 3.0 8.0 102.53 9.06 29.20 0.28 -32.13 3.22 7086.15 41.32 0.12 0.11
3.0 2.0 3.0 8.0 98.56 11.87 29.00 0.39 -28.42 3.93 8180.95 87.92 0.06 0.16
5.0 4.0 5.0 8.0 97.76 7.94 39.47 0.19 -39.02 1.96 6506.75 40.97 0.09 0.08
[ ]:

最后,我们还可通过热力图,来大致评估参数组表现的有效性。 然而问题是,只有当可调参数数量(维度数)2或3时,才能直接生成直观可视的热力图,而在本教程中有4个可调参数。此时,我们可以利用一些降维算法(比如PCA、t-SNE算法),将4个维度的参数数据映射到2维空间,即可在2维热力图上进行分析。 OptimizationResult.plot_performance_metric 集成了t-SNE算法实现,当参数组数量 >= 3,会先将参数集映射到2D再生成热力图,并用给定的指标值着色。

在做映射时,t-SNE算法会在低维空间中保留原高维空间中的数据相似度,这样我们可以很直观地评估“相似的参数组,是否表现也类似”。如果一组最佳参数是有效的,在热力图上它的周围也应该是一片“热力高原”,意味该策略确实存在一个符合其盈利能力的参数空间。

结果如下:虽然存在表现显然不行的参数类型(普遍为波动率较大或最低股价太高),但似乎并没有显著优势的参数类型。

[5]:
result.plot_performance_metric("收益率")
[ ]:

自定义优化算法#

TODO

[ ]: