回测一个策略
我们现在已经准备好使用 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 中实现一个基本的交易策略:
如果某只股票的最新收盘价低于前一根 K 线的最低价,且该股票没有未平仓的多头头寸,则买入该股票。
将买单的限价设置为比最新收盘价低
0.01。持有头寸 3 天后以市价平仓。
在 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 函数接收一个 ExecContext (ctx),其中包含当前股票代码(AAPL 或 MSFT)的历史数据。你可以通过 ctx.close[-1] 获取最新的收盘价。
要下单,请使用 ctx.calc_target_shares 计算 25% 的投资组合分配,并将其赋值给 ctx.buy_shares,然后使用 buy_limit_price 设置限价。
默认情况下,买单会在下一根 K 线(buy_delay=1)以 该 K 线的中点价格 成交。你可以通过 StrategyConfig.buy_delay 和 ExecContext.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>]
你还可以访问每次进出场的成交交易记录,以及所有已下达的订单:
[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)筛选数据。
尽管之前的指标表明我们拥有一个盈利的策略,但我们也可能被随机性误导。在下一篇文档中,我们将讨论如何使用自助法进一步评估我们的交易策略。