Rotational Trading
Rotational trading involves purchasing the best-performing assets while selling underperforming ones. As you may have guessed, PyBroker is an excellent tool for backtesting such strategies. So, let’s dive in and get started with testing our rotational trading strategy!
[1]:
import pybroker as pyb
from pybroker import ExecContext, Strategy, StrategyConfig, YFinance
Our strategy will involve ranking and buying stocks with the highest price rate-of-change (ROC). To start, we’ll define a 20-day ROC indicator using TA-Lib:
[2]:
import talib as ta
roc_20 = pyb.indicator(
'roc_20', lambda data: ta.ROC(data.adj_close, timeperiod=20))
Next, let’s define the rules of our strategy:
Buy the two stocks with the highest 20-day ROC.
Allocate 50% of our capital to each stock.
If either of the stocks is no longer ranked among the top five 20-day ROCs, then we will liquidate that stock.
Trade these rules daily.
Let’s set up our config and some parameters for the above rules:
[3]:
config = StrategyConfig(max_long_positions=2)
pyb.param('target_size', 1 / config.max_long_positions)
pyb.param('rank_threshold', 5)
[3]:
5
To proceed with our strategy, we will implement a rank
function that ranks each stock by their 20-day ROC in descending order, from highest to lowest.
[4]:
def rank(ctxs: dict[str, ExecContext]):
scores = {
symbol: ctx.indicator('roc_20')[-1]
for symbol, ctx in ctxs.items()
}
sorted_scores = sorted(
scores.items(),
key=lambda score: score[1],
reverse=True
)
threshold = pyb.param('rank_threshold')
top_scores = sorted_scores[:threshold]
top_symbols = [score[0] for score in top_scores]
pyb.param('top_symbols', top_symbols)
The top_symbols
global parameter contains the symbols of the stocks with the top five highest 20-day ROCs.
Now that we have a method for ranking stocks by their ROC, we can proceed with implementing a rotate
function to manage the rotational trading.
[5]:
def rotate(ctx: ExecContext):
if ctx.long_pos():
if ctx.symbol not in pyb.param('top_symbols'):
ctx.sell_all_shares()
else:
target_size = pyb.param('target_size')
ctx.buy_shares = ctx.calc_target_shares(target_size)
ctx.score = ctx.indicator('roc_20')[-1]
We liquidate the currently held stock if it is no longer ranked among the top five 20-day ROCs. Otherwise, we rank all stocks by their 20-day ROC and buy up to the top two ranked. For more information on ranking when placing buy orders, see the Ranking and Position Sizing notebook.
We will use the set_before_exec method to execute our ranking with rank
before running the rotate
function. For this backtest, we will use a universe of 10 stocks:
[6]:
strategy = Strategy(
YFinance(),
start_date='1/1/2018',
end_date='1/1/2023',
config=config
)
strategy.set_before_exec(rank)
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:03
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:06
[7]:
result.orders
[7]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | NFLX | 2018-02-01 | 184 | NaN | 267.67 | 0.0 |
2 | buy | AMD | 2018-02-01 | 3639 | NaN | 13.53 | 0.0 |
3 | sell | AMD | 2018-02-05 | 3639 | NaN | 11.56 | 0.0 |
4 | buy | AMZN | 2018-02-05 | 627 | NaN | 69.49 | 0.0 |
5 | sell | AMZN | 2018-04-03 | 627 | NaN | 69.23 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... |
256 | buy | AMD | 2022-11-21 | 3589 | NaN | 72.28 | 0.0 |
257 | sell | AMD | 2022-12-14 | 3589 | NaN | 70.16 | 0.0 |
258 | buy | NFLX | 2022-12-14 | 881 | NaN | 319.57 | 0.0 |
259 | sell | NVDA | 2022-12-28 | 1491 | NaN | 140.73 | 0.0 |
260 | buy | META | 2022-12-28 | 1869 | NaN | 116.83 | 0.0 |
260 rows × 7 columns