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