轮动交易
轮动交易涉及购买表现最好的资产,同时出售表现不佳的资产。PyBroker 可用于回测此类策略。
[1]:
import pybroker
from pybroker import ExecContext, Strategy, YFinance
我们的策略将涉及对 价格涨幅(ROC) 最高的股票进行排名和购买。首先,我们将使用 TA-Lib 定义一个 20 天的 ROC 指标:
[2]:
import talib as ta
roc_20 = pybroker.indicator(
"roc_20", lambda data: ta.ROC(data.adj_close, timeperiod=20)
)
接下来,让我们定义策略的规则:
购买 20 天涨幅(ROC)最高的两只股票。
将我们的资本的 50% 分配给每只股票。
如果其中一只股票不再位于前五名的 20 天涨幅(ROC)中,则我们将清盘该股票。
每天交易这些规则。
为了实现该策略,我们编写一个 rotate 函数,将每只股票的 long_score 设置为其 20 天涨幅(ROC)。然后 PyBroker 会根据 long_score 对这些股票进行降序排名。
[3]:
def rotate(ctx: ExecContext):
ctx.long_score = ctx.indicator("roc_20")[-1]
既然我们已经有了一种根据 ROC 对股票评分的方法,就可以使用 enable_rotation 方法来启用轮动交易。
我们通过 set_max_long_positions 将多头仓位数量上限设置为 2。将 worst_rank_held 设置为 5 会清盘任何跌出前五名 20 天涨幅(ROC)排名之外的当前持仓股票。否则,PyBroker 会买入排名前两位的股票,将 50% 的资金分配给每只股票。这次回测将使用由 10 只股票组成的股票池:
[4]:
strategy = Strategy(YFinance(), start_date="1/1/2018", end_date="1/1/2023")
strategy.set_max_long_positions(2)
strategy.enable_rotation(worst_rank_held=5)
strategy.add_execution(
rotate,
[
"TSLA",
"NFLX",
"AAPL",
"NVDA",
"AMZN",
"MSFT",
"GOOG",
"AMD",
"INTC",
"META",
],
indicators=roc_20,
)
result = strategy.backtest(warmup=20)
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 10 of 10 completed
Loaded bar data: 0:00:01
Computing indicators...
100% (10 of 10) |########################| Elapsed Time: 0:00:00 Time: 0:00:00
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:01
[5]:
result.orders
[5]:
| type | symbol | date | created | order_type | intent | shares | limit_price | market_price | fill_price | fees | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| id | |||||||||||
| 1 | buy | NFLX | 2018-02-01 | 2018-01-31 | market | buy_to_open | 1849 | NaN | 26.77 | 26.77 | 0.0 |
| 2 | buy | AMD | 2018-02-01 | 2018-01-31 | market | buy_to_open | 3639 | NaN | 13.53 | 13.53 | 0.0 |
| 3 | sell | AMD | 2018-02-05 | 2018-02-02 | market | sell_to_close | 3639 | NaN | 11.56 | 11.56 | 0.0 |
| 4 | buy | AMZN | 2018-02-05 | 2018-02-02 | market | buy_to_open | 623 | NaN | 69.49 | 69.49 | 0.0 |
| 5 | sell | AMZN | 2018-04-03 | 2018-04-02 | market | sell_to_close | 623 | NaN | 69.23 | 69.23 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 256 | buy | AMD | 2022-11-21 | 2022-11-18 | market | buy_to_open | 3591 | NaN | 72.28 | 72.28 | 0.0 |
| 257 | sell | AMD | 2022-12-14 | 2022-12-13 | market | sell_to_close | 3591 | NaN | 70.16 | 70.16 | 0.0 |
| 258 | buy | NFLX | 2022-12-14 | 2022-12-13 | market | buy_to_open | 8822 | NaN | 31.96 | 31.96 | 0.0 |
| 259 | sell | NVDA | 2022-12-28 | 2022-12-27 | market | sell_to_close | 14918 | NaN | 14.07 | 14.07 | 0.0 |
| 260 | buy | META | 2022-12-28 | 2022-12-27 | market | buy_to_open | 1868 | NaN | 116.83 | 116.83 | 0.0 |
260 rows × 11 columns
自定义仓位配置
默认情况下,PyBroker 会将我们的资金平均分配到各个仓位。若要自定义此行为,我们可以向 enable_rotation 传入一个 sizer 函数。在轮动决定要买入哪些股票之后,会使用 RotationContext 调用 sizer,使我们能够覆盖每笔入场的仓位规模。long_ranks 属性包含每只股票的排名,其中 1 表示排名最高。
让我们重用相同的策略,但这次将 70% 的资金分配给排名第一的股票,30% 分配给排名第二的股票:
[6]:
from pybroker import RotationContext
def size_by_rank(rotation: RotationContext):
for symbol, ctx in rotation.ctxs.items():
if ctx.buy_shares is not None:
rank = rotation.long_ranks[symbol]
match rank:
case 1:
ctx.buy_shares = ctx.calc_target_shares(0.7)
case 2:
ctx.buy_shares = ctx.calc_target_shares(0.3)
strategy.enable_rotation(worst_rank_held=5, sizer=size_by_rank)
result = strategy.backtest(warmup=20)
result.orders
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 10 of 10 completed
Loaded bar data: 0:00:00
Computing indicators...
100% (10 of 10) |########################| Elapsed Time: 0:00:00 Time: 0:00:00
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[6]:
| type | symbol | date | created | order_type | intent | shares | limit_price | market_price | fill_price | fees | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| id | |||||||||||
| 1 | buy | NFLX | 2018-02-01 | 2018-01-31 | market | buy_to_open | 2589 | NaN | 26.77 | 26.77 | 0.0 |
| 2 | buy | AMD | 2018-02-01 | 2018-01-31 | market | buy_to_open | 2183 | NaN | 13.53 | 13.53 | 0.0 |
| 3 | sell | AMD | 2018-02-05 | 2018-02-02 | market | sell_to_close | 2183 | NaN | 11.56 | 11.56 | 0.0 |
| 4 | buy | AMZN | 2018-02-05 | 2018-02-02 | market | buy_to_open | 379 | NaN | 69.49 | 69.49 | 0.0 |
| 5 | sell | AMZN | 2018-04-03 | 2018-04-02 | market | sell_to_close | 379 | NaN | 69.23 | 69.23 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 256 | buy | AMD | 2022-11-21 | 2022-11-18 | market | buy_to_open | 4701 | NaN | 72.28 | 72.28 | 0.0 |
| 257 | sell | AMD | 2022-12-14 | 2022-12-13 | market | sell_to_close | 4701 | NaN | 70.16 | 70.16 | 0.0 |
| 258 | buy | NFLX | 2022-12-14 | 2022-12-13 | market | buy_to_open | 4790 | NaN | 31.96 | 31.96 | 0.0 |
| 259 | sell | NVDA | 2022-12-28 | 2022-12-27 | market | sell_to_close | 7975 | NaN | 14.07 | 14.07 | 0.0 |
| 260 | buy | META | 2022-12-28 | 2022-12-27 | market | buy_to_open | 2731 | NaN | 116.83 | 116.83 | 0.0 |
260 rows × 11 columns