回测一个策略

我们现在已经准备好使用 PyBroker 测试一个基本的交易策略。首先,导入以下类:

[1]:
import pybroker
from pybroker import Strategy, StrategyConfig, YFinance

pybroker.enable_data_source_cache("my_strategy")
[1]:
<pybroker.cache._L1Cache at 0x7f4c7b673290>

在本次回测中,我们将使用 Yahoo Finance 作为 数据源。我们还将启用数据缓存,以确保只下载一次所需的数据。

首先,创建一个 StrategyConfig 对象来配置 Strategy。在本例中,我们将初始资金设置为 500,000

[2]:
config = StrategyConfig(initial_cash=500_000)

接下来,你可以通过传入以下参数来创建一个 Strategy 类的新实例:

  • 数据源:本示例使用 Yahoo Finance。

  • 开始日期:回测的起始日期。

  • 结束日期:回测的结束日期。

  • 配置:之前创建的配置对象。

[3]:
strategy = Strategy(YFinance(), "3/1/2017", "3/1/2022", config)

Strategy 实例现在已准备好从 Yahoo Finance 下载 2017 年 3 月 1 日至 2022 年 3 月 1 日期间的数据。如需修改其他设置,请参阅 StrategyConfig 参考文档

定义策略规则

在本节中,你将使用以下规则在 PyBroker 中实现一个基本的交易策略:

  1. 如果某只股票的最新收盘价低于前一根 K 线的最低价,且该股票没有未平仓的多头头寸,则买入该股票。

  2. 将买单的限价设置为比最新收盘价低 0.01

  3. 持有头寸 3 天后以市价平仓。

  4. 在 AAPL 和 MSFT 上执行这些规则,为每只股票分配最多 25% 的投资组合。

为此,定义一个 buy_low 函数。PyBroker 会在每根 K 线上分别为 AAPL 和 MSFT 调用该函数(每根 K 线代表一个交易日):

[4]:
def buy_low(ctx):
    # If shares were already purchased and are currently being held, then return.
    if ctx.long_pos():
        return
    # If the latest close price is less than the previous day's low price,
    # then place a buy order.
    if ctx.bars >= 2 and ctx.close[-1] < ctx.low[-2]:
        # Buy a number of shares that is equal to 25% the portfolio.
        ctx.buy_shares = ctx.calc_target_shares(0.25)
        # Set the limit price of the order.
        ctx.buy_limit_price = ctx.close[-1] - 0.01
        # Hold the position for 3 bars before liquidating (in this case, 3 days).
        ctx.hold_bars = 3

buy_low 函数接收一个 ExecContextctx),其中包含当前股票代码(AAPL 或 MSFT)的历史数据。你可以通过 ctx.close[-1] 获取最新的收盘价。

要下单,请使用 ctx.calc_target_shares 计算 25% 的投资组合分配,并将其赋值给 ctx.buy_shares,然后使用 buy_limit_price 设置限价。

默认情况下,买单会在下一根 K 线(buy_delay=1)以 该 K 线的中点价格 成交。你可以通过 StrategyConfig.buy_delayExecContext.buy_fill_price 自定义此行为。

接下来,使用 ctx.hold_bars 指定持仓周期。平仓时,股票将以 ExecContext.sell_fill_price 价格卖出(该价格默认同样为该 K 线的中点价)。

要将这些 buy_low 规则应用于 AAPL 和 MSFT,请使用 add_execution 方法:

[5]:
strategy.add_execution(buy_low, ["AAPL", "MSFT"])

添加第二个执行逻辑

你可以在同一个 Strategy 实例中为不同的股票代码应用不同的交易规则。为了说明这一点,我们在一个名为 short_high 的函数中定义一组新的做空策略规则,其工作方式与之前的规则类似:

[6]:
def short_high(ctx):
    # If shares were already shorted then return.
    if ctx.short_pos():
        return
    # If the latest close price is more than the previous day's high price,
    # then place a sell order.
    if ctx.bars >= 2 and ctx.close[-1] > ctx.high[-2]:
        # Short 100 shares.
        ctx.sell_shares = 100
        # Cover the shares after 2 bars (in this case, 2 days).
        ctx.hold_bars = 2

short_high 中的规则将应用于 TSLA

[7]:
strategy.add_execution(short_high, ["TSLA"])

(注意,你还可以通过调用 ExecContext#foreign 获取另一个股票代码的 K 线数据)

运行回测

要运行回测,请在 Strategy 实例上调用 backtest 方法。以下是一个例子:

[8]:
result = strategy.backtest()
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00

Loading bar data...
[*********************100%***********************]  3 of 3 completed
Loaded bar data: 0:00:00

Test split: 2017-03-01 00:00:00 to 2022-02-28 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time:  0:00:00

Finished backtest: 0:00:02

backtest 方法会返回一个 TestResult 对象,其中包含回测的详细指标。例如,你可以使用 Matplotlib 绘制投资组合每日市值的图表:

[9]:
import matplotlib.pyplot as plt

chart = plt.subplot2grid((3, 2), (0, 0), rowspan=3, colspan=2)
chart.plot(result.portfolio.index, result.portfolio["market_value"])
[9]:
[<matplotlib.lines.Line2D at 0x7f4ba410e780>]
../_images/notebooks_2._Backtesting_a_Strategy_18_1.png

你还可以访问每次进出场的成交交易记录,以及所有已下达的订单:

[10]:
result.trades
[10]:
type symbol entry_date exit_date entry exit shares pnl return_pct agg_pnl bars pnl_per_bar stop mae mfe
id
1 long MSFT 2017-03-03 2017-03-08 63.95 64.67 1952 1405.44 1.13 1405.44 3 468.48 bar -0.33 0.83
2 long MSFT 2017-03-14 2017-03-17 64.35 64.96 1937 1181.57 0.95 2587.01 3 393.86 bar -0.20 0.61
3 short TSLA 2017-03-15 2017-03-17 17.18 17.55 100 -37.00 -2.11 2550.01 2 -18.50 bar -0.54 0.23
4 short TSLA 2017-03-27 2017-03-29 17.68 18.50 100 -82.00 -4.43 2468.01 2 -41.00 bar -1.03 0.36
5 short TSLA 2017-04-04 2017-04-06 19.98 19.87 100 11.00 0.55 2479.01 2 5.50 bar -0.35 0.37
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
384 long AAPL 2022-02-11 2022-02-16 170.56 171.69 984 1111.92 0.66 180091.35 3 370.64 bar -4.00 2.52
385 long MSFT 2022-02-11 2022-02-16 299.26 297.27 560 -1114.40 -0.66 178976.95 3 -371.47 bar -7.91 5.03
386 short TSLA 2022-02-16 2022-02-18 304.61 287.41 100 1720.00 5.98 180696.95 2 860.00 bar -4.20 17.20
387 long AAPL 2022-02-18 2022-02-24 168.36 157.43 1005 -10984.65 -6.49 169712.30 3 -3661.55 bar -10.93 2.18
388 long MSFT 2022-02-18 2022-02-24 290.08 283.34 583 -3929.42 -2.32 165782.88 3 -1309.81 bar -9.98 3.78

388 rows × 15 columns

[11]:
result.orders
[11]:
type symbol date created order_type intent shares limit_price market_price fill_price fees
id
1 buy MSFT 2017-03-03 2017-03-02 limit buy_to_open 1952 64.00 63.95 63.95 0.0
2 sell MSFT 2017-03-08 NaT stop_bar sell_to_close 1952 NaN 64.67 64.67 0.0
3 buy MSFT 2017-03-14 2017-03-13 limit buy_to_open 1937 64.70 64.35 64.35 0.0
4 sell TSLA 2017-03-15 2017-03-14 market sell_to_open 100 NaN 17.18 17.18 0.0
5 sell MSFT 2017-03-17 NaT stop_bar sell_to_close 1937 NaN 64.96 64.96 0.0
... ... ... ... ... ... ... ... ... ... ... ...
773 buy AAPL 2022-02-18 2022-02-17 limit buy_to_open 1005 168.87 168.36 168.36 0.0
774 buy MSFT 2022-02-18 2022-02-17 limit buy_to_open 583 290.72 290.08 290.08 0.0
775 sell AAPL 2022-02-24 NaT stop_bar sell_to_close 1005 NaN 157.43 157.43 0.0
776 sell MSFT 2022-02-24 NaT stop_bar sell_to_close 583 NaN 283.34 283.34 0.0
777 sell TSLA 2022-02-28 2022-02-25 market sell_to_open 100 NaN 281.93 281.93 0.0

777 rows × 11 columns

此外,result.metrics_df 提供一个根据回测收益率计算得出的指标 DataFrame。你可以在 参考文档 中查看这些指标的详细说明。

[12]:
result.metrics_df
[12]:
name value
0 trade_count 388
1 initial_market_value 500000.0
2 end_market_value 664961.55
3 total_pnl 165782.88
4 unrealized_pnl -821.33
5 total_return_pct 33.156576
6 total_profit 401998.21
7 total_loss -236215.33
8 total_fees 0.0
9 max_drawdown -31619.46
10 max_drawdown_pct -4.723114
11 max_drawdown_date 2021-03-08 00:00:00
12 win_rate 52.57732
13 loss_rate 47.42268
14 winning_trades 204
15 losing_trades 184
16 avg_pnl 427.275464
17 avg_return_pct 0.279691
18 avg_trade_bars 2.414948
19 avg_profit 1970.579461
20 avg_profit_pct 3.168627
21 avg_winning_trade_bars 2.465686
22 avg_loss -1283.778967
23 avg_loss_pct -2.923261
24 avg_losing_trade_bars 2.358696
25 largest_win 20965.76
26 largest_win_pct 14.49
27 largest_win_bars 3
28 largest_loss -10984.65
29 largest_loss_pct -6.49
30 largest_loss_bars 3
31 max_wins 7
32 max_losses 7
33 sharpe 0.058274
34 sortino 0.098373
35 profit_factor 1.31741
36 ulcer_index 1.369395
37 upi 0.017141
38 equity_r2 0.902276
39 std_error 65819.995043

筛选回测数据

你可以筛选回测数据,仅保留特定的 K 线。例如,要将策略限制为仅在周一交易,只需筛选数据以仅保留周一的 K 线:

[13]:
result = strategy.backtest(days="mon")
result.orders
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00

Loaded cached bar data.

Test split: 2017-03-06 00:00:00 to 2022-02-28 00:00:00
100% (238 of 238) |######################| Elapsed Time: 0:00:00 Time:  0:00:00

Finished backtest: 0:00:00
[13]:
type symbol date created order_type intent shares limit_price market_price fill_price fees
id
1 sell TSLA 2017-03-27 2017-03-20 market sell_to_open 100 NaN 17.68 17.68 0.0
2 buy TSLA 2017-04-10 NaT stop_bar buy_to_close 100 NaN 20.75 20.75 0.0
3 sell TSLA 2017-04-17 2017-04-10 market sell_to_open 100 NaN 20.09 20.09 0.0
4 buy TSLA 2017-05-01 NaT stop_bar buy_to_close 100 NaN 21.40 21.40 0.0
5 sell TSLA 2017-05-08 2017-05-01 market sell_to_open 100 NaN 20.65 20.65 0.0
... ... ... ... ... ... ... ... ... ... ... ...
178 sell TSLA 2022-02-07 2022-01-31 market sell_to_open 100 NaN 308.41 308.41 0.0
179 sell AAPL 2022-02-14 NaT stop_bar sell_to_close 777 NaN 168.07 168.07 0.0
180 buy MSFT 2022-02-14 2022-02-07 limit buy_to_open 457 300.94 294.06 294.06 0.0
181 buy TSLA 2022-02-28 NaT stop_bar buy_to_close 100 NaN 281.93 281.93 0.0
182 buy AAPL 2022-02-28 2022-02-14 limit buy_to_open 811 168.87 163.92 163.92 0.0

182 rows × 11 columns

由于启用了数据缓存,PyBroker 直接筛选本地数据,而无需从 Yahoo Finance 重新下载。

你还可以使用 between_time 参数按特定时间范围(例如 9:30 至 10:30 AM)筛选数据。

尽管之前的指标表明我们拥有一个盈利的策略,但我们也可能被随机性误导。在下一篇文档中,我们将讨论如何使用自助法进一步评估我们的交易策略