Ranking and Position Sizing

In this notebook, we will learn about the features of PyBroker that enable you to rank ticker symbols and set position sizes for a group of symbols in your trading strategy. With these features, you can easily optimize your strategy and manage risk more effectively.

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

pybroker.enable_data_source_cache('ranking_and_pos_sizing')
[1]:
<diskcache.core.Cache at 0x7fd17427dd60>

Ranking Ticker Symbols

In this section, we will learn about how to rank ticker symbols when placing buy orders. Let’s begin with an example of how to rank ticker symbols based on volume when placing buy orders.

[2]:
def buy_highest_volume(ctx):
    # If there are no long positions across all tickers being traded:
    if not tuple(ctx.long_positions()):
        ctx.buy_shares = ctx.calc_target_shares(1)
        ctx.hold_bars = 2
        ctx.score = ctx.volume[-1]

The buy_highest_volume function ranks ticker symbols by their most recent trading volume and allocates 100% of the portfolio for 2 bars. The ctx.score is set to ctx.volume[-1], which is the most recent trading volume.

[3]:
config = StrategyConfig(max_long_positions=1)
strategy = Strategy(YFinance(), '6/1/2021', '6/1/2022', config)
strategy.add_execution(buy_highest_volume, ['T', 'F', 'GM', 'PFE'])

To limit the number of long positions that can be held at any time to 1, we set max_long_positions to 1 in the StrategyConfig. In this example, we add the buy_highest_volume function to the Strategy object and specify the ticker symbols to trade: ['T', 'F', 'GM', 'PFE'].

[4]:
result = strategy.backtest()
result.trades
Backtesting: 2021-06-01 00:00:00 to 2022-06-01 00:00:00

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

Test split: 2021-06-01 00:00:00 to 2022-05-31 00:00:00
100% (253 of 253) |######################| Elapsed Time: 0:00:00 Time:  0:00:00

Finished backtest: 0:00:02
[4]:
type symbol entry_date exit_date entry exit shares pnl return_pct agg_pnl bars pnl_per_bar stop
id
1 long F 2021-06-02 2021-06-04 14.85 16.13 6734 8619.52 8.62 8619.52 2 4309.76 bar
2 long F 2021-06-07 2021-06-09 15.93 15.51 6801 -2856.42 -2.64 5763.10 2 -1428.21 bar
3 long F 2021-06-10 2021-06-14 15.43 15.06 6832 -2527.84 -2.40 3235.26 2 -1263.92 bar
4 long F 2021-06-15 2021-06-17 14.96 14.99 6900 207.00 0.20 3442.26 2 103.50 bar
5 long F 2021-06-18 2021-06-22 14.61 14.96 7003 2451.05 2.40 5893.31 2 1225.53 bar
... ... ... ... ... ... ... ... ... ... ... ... ... ...
80 long F 2022-05-10 2022-05-12 13.43 12.47 7263 -6972.48 -7.15 -9423.13 2 -3486.24 bar
81 long F 2022-05-13 2022-05-17 13.25 13.34 6835 615.15 0.68 -8807.98 2 307.58 bar
82 long F 2022-05-18 2022-05-20 13.03 12.59 6739 -2965.16 -3.38 -11773.14 2 -1482.58 bar
83 long F 2022-05-23 2022-05-25 12.72 12.57 6936 -1040.40 -1.18 -12813.54 2 -520.20 bar
84 long F 2022-05-26 2022-05-31 12.99 13.59 6711 4026.60 4.62 -8786.94 2 2013.30 bar

84 rows × 13 columns

Setting Position Sizes

In PyBroker, you can set position sizes based on multiple tickers. To illustrate this, let’s take a simple buy and hold strategy that starts trading after 100 days and holds positions for 30 days:

[5]:
def buy_and_hold(ctx):
    if not ctx.long_pos() and ctx.bars >= 100:
        ctx.buy_shares = 100
        ctx.hold_bars = 30

strategy = Strategy(YFinance(), '6/1/2021', '6/1/2022')
strategy.add_execution(buy_and_hold, ['T', 'F', 'GM', 'PFE'])

This will buy 100 shares in each of ['T', 'F', 'GM', 'PFE']. But what if you don’t want to use equal position sizing? For example, you may want to size positions so that more shares are allocated to tickers with lower volatility to decrease the portfolio’s overall volatility.

To customize position sizing for each ticker, we can define a pos_size_handler function that calculates the position size for each ticker:

[6]:
import numpy as np

def pos_size_handler(ctx):
    # Fetch all buy signals.
    signals = tuple(ctx.signals("buy"))
    # Return if there are no buy signals (i.e. there are only sell signals).
    if not signals:
        return
    # Calculates the inverse volatility, where volatility is defined as the
    # standard deviation of close prices for the last 100 days.
    get_inverse_volatility = lambda signal: 1 / np.std(signal.bar_data.close[-100:])
    # Sums the inverse volatilities for all of the buy signals.
    total_inverse_volatility = sum(map(get_inverse_volatility, signals))
    for signal in signals:
        size = get_inverse_volatility(signal) / total_inverse_volatility
        # Calculate the number of shares given the latest close price.
        shares = ctx.calc_target_shares(size, signal.bar_data.close[-1], cash=95_000)
        ctx.set_shares(signal, shares)

strategy.set_pos_size_handler(pos_size_handler)

The handler runs on every bar that generates a buy or sell signal when buy_shares or sell_shares is set on the ExecContext:

[7]:
result = strategy.backtest()
Backtesting: 2021-06-01 00:00:00 to 2022-06-01 00:00:00

Loaded cached bar data.

Test split: 2021-06-01 00:00:00 to 2022-05-31 00:00:00
100% (253 of 253) |######################| Elapsed Time: 0:00:00 Time:  0:00:00

Finished backtest: 0:00:00
[8]:
result.trades
[8]:
type symbol entry_date exit_date entry exit shares pnl return_pct agg_pnl bars pnl_per_bar stop
id
1 long T 2021-10-21 2021-12-03 19.60 17.54 2248 -4630.88 -10.51 -4630.88 30 -154.36 bar
2 long F 2021-10-21 2021-12-03 16.41 19.66 2028 6591.00 19.80 1960.12 30 219.70 bar
3 long GM 2021-10-21 2021-12-03 58.20 60.28 129 268.32 3.57 2228.44 30 8.94 bar
4 long PFE 2021-10-21 2021-12-03 42.76 53.75 235 2582.65 25.70 4811.09 30 86.09 bar
5 long T 2021-12-06 2022-01-19 17.81 20.49 2665 7142.20 15.05 11953.29 30 238.07 bar
6 long F 2021-12-06 2022-01-19 19.05 23.66 1075 4955.75 24.20 16909.04 30 165.19 bar
7 long GM 2021-12-06 2022-01-19 59.72 57.99 209 -361.57 -2.90 16547.47 30 -12.05 bar
8 long PFE 2021-12-06 2022-01-19 52.57 53.97 290 406.00 2.66 16953.47 30 13.53 bar
9 long T 2022-01-20 2022-03-04 20.53 17.87 2625 -6982.50 -12.96 9970.97 30 -232.75 bar
10 long F 2022-01-20 2022-03-04 22.22 17.01 789 -4110.69 -23.45 5860.28 30 -137.02 bar
11 long GM 2022-01-20 2022-03-04 55.88 43.08 255 -3264.00 -22.91 2596.28 30 -108.80 bar
12 long PFE 2022-01-20 2022-03-04 53.80 48.09 192 -1096.32 -10.61 1499.96 30 -36.54 bar
13 long T 2022-03-07 2022-04-19 17.88 19.51 3132 5105.16 9.12 6605.12 30 170.17 bar
14 long F 2022-03-07 2022-04-19 16.43 15.98 1303 -586.35 -2.74 6018.77 30 -19.55 bar
15 long GM 2022-03-07 2022-04-19 41.09 41.50 220 90.20 1.00 6108.97 30 3.01 bar
16 long PFE 2022-03-07 2022-04-19 48.18 50.62 200 488.00 5.06 6596.97 30 16.27 bar

Using this method allows for a lot of possibilities, such as using Mean-Variance Optimization to determine portfolio allocations.

In the next notebook, we will discuss how to implement custom indicators in PyBroker.