Originally @: https://www.backtrader.com/blog/2019-07-19-rebalancing-conservative/rebalancing-conservative/

The Conservative Formula approach is presented in this paper: The Conservative Formula in Python: Quantitative Investing made Easy

It is one many possible rebalancing approaches, but one that is easy to grasp. A summary of the approach:

x stocks are selected from a universe of Y (100 of 1000)

The selection criteria are

Low volatility

High Net Payout Yield

High Momentum

Rebalancing every month

With this in mind let’s go and present a possible implementation in backtrader

The data

Even if one has a winning strategy, nothing will be actually won if no data is available for the strategy. Which means that it has to be considered how the data looks like and how to load it.

A set of CSV (“comma-separated-values”) files is assumed to be available, containing with the following features

ohlcv monthly data

monthly data With an extra field after the v containing the Net Payout Yield ( npy ), to have an ohlcvn data set.

The format of the CSV data will therefore look like this

I.e.: one row per month. The data loader engine can now be prepared for which simple extension of the generic built-in CSV loader delivered with backtrader will be created.

And that is. Notice how easy has been to add a point of fundamental data to the ohlcv data stream.

By using the expresion lines=('npy',) . The other usual fields ( open , high , ...) are already part of GenericCSVData By indicating the loading position with the params = dict(npy=6) . The other fields have a predefined position.

The timeframe has also been updated in the parameters to reflect the monthly nature of the data.

Note See Docs — Data Feeds Reference — GenericCSVDAta for the actual fields and loading positions (which can all be customized)

The data loader will have to be properly instantiated with a file name, but that’s something for later, when a standard boilerplate is presented below to have a complete script.

The Strategy

Let’s put the logic into a standard backtrader strategy. To make it as generic and customizable as possible, the same same params approach will be used, as it was used before with the data.

Before delving into the strategy, let’s consider one of the points from the quick summary

x stocks are selected from a universe of Y

The strategy itself is not in charge of adding stocks to the universe, but it is in charge of the selection. One could be in a situation in which only 50 stocks have been added and still try to select 100 if x and Y are fixed in the code. To cope with such situations, the following will be done:

Have a selperc parameter with a value of 0.10 (i.e.: 10% ), to indicate the amount of stocks to be selected from the universe.

parameter with a value of (i.e.: ), to indicate the amount of stocks to be selected from the universe. This means that if 1000 are present, only 100 will be selected and if the universe consist of 50 stocks, only 5 will be selected.

As for the formula ranking the stock, it looks like this:

(momentum * net payout) / volatility

Which means that those with higher momentum, higher payout and lower volatility will have a higher score.

For momentum the RateOfChange indicator (aka ROC ) will be used, which measures the ratio of change in prices over a period.

The net payout is already part of the data feed.

To calculate the volatility , the StandardDeviation of the n-periods return of the stock ( n-periods , because things will be kept as parameters) will be used.

With this information, the strategy can already be initialize with the right parameters and the setup of the indicators and calculations which will be later used in each monthly iteration.

First the declaration and the parameters

Notice that something not mentioned above has been added, and that is a parameter reserve=0.05 (i.e. 5%), which is used to calculated the percentage allocation per stock, keeping a reserve capital in the bank. Although for a simulation one could conceivable want to use 100% of the capital, one can hit the usual problems doing that, such as price gaps, floating point precision and end up missing some of the market entries.

Before anything else, a small logging method is created, which will allow to log how the portfolio is rebalanced.

At the beginning of the __init__ method, the number of stocks to rank is calculated and the reserve capital parameter is applied to determine the per stock percentage of the bank.

And finally the initialization is over with the calculation of the per stock indicators for volatility and momentum, which are then applied in the per stock ranking formula calculation.

It’s now time to iterate each month. The ranking is available in the self.ranks dictionary. The key/value pairs have to be sorted for each iteration, to get which items have to go and which ones have to be part of the portfolio (remain or be added)

The iterable is sorted in reverse order, because the ranking formula delivers higher scores for the highest ranked stocks.

Rebalancing is now due.

Rebalancing 1: Get Top Ranked and the stocks with open positions

A bit of Python trickery is happening here, because a dict is being used. The reason is that if the top ranked stocks were put in a list the operator == would be used internally by Python to check for presence with the operator in . And although improbable it would be possible for two stocks to have the same value on the same day. When using a dict a hash value is used when checking for presence of an item as part of the keys.

Note: For logging purposes rbot (ranked bottom) is also created with the stocks not present in rtop .

To later discriminate between stocks that have to leave the portfolio, those which simply have to be rebalanced and the newly top ranked, a current list of stocks in the portfolio is prepared.

Rebalancing 2: Sell those no longer top ranked

Just like in real world, in the backtrader ecosystem selling before buying is a must to ensure enough cash is there.

Stocks currently with an open position and no longer top ranked are sold (i.e. target=0.0 ).

Note A simple self.close(data) would have sufficed here, rather than explicitly stating the target percentage.

Rebalancing 3: Issue a target order for all top ranked stocks

The total portfolio value changes over time and those stocks already in the portfolio may have to slightly increase/reduce the current position to match the expected percentage. order_target_percent is an ideal method to enter the market, because it does automatically calculate whether a buy or a sell order is needed.

Rebalancing the stocks already with a position is done before adding the new ones to the portfolio, as the new one will only issue buy orders and consume cash. Having removed the existing stocks from with rtop[data].pop() after having re-balanced, the remaining stocks in rtop are those which will be newly added to the portfolio.

Running it all and Evaluating it!

Having a data loader class and the strategy is not enough. Just like with any other framework, some boilerplate is needed. The following code makes it possible.

Where the following is done:

Parsing arguments and have this available (this is obviously optional, as everything can be hardcoded, but good practices are good practices)

Creating a cerebro engine instance. Yes, this is Spanish for "brain" and is the part of the framework in charge of coordinating the orchestral maneuvers in the dark. Although it can accept several options, the defaults should suffice for most use cases.

engine instance. Yes, this is Spanish for "brain" and is the part of the framework in charge of coordinating the orchestral maneuvers in the dark. Although it can accept several options, the defaults should suffice for most use cases. Loading the data files, which is done with a simple directory scan of args.datadir is done and all files are loaded with NetPayOutData and added to the cerebro instance

is done and all files are loaded with and added to the instance Adding the strategy

Setting the cash, which defaults to 1,000,000 . Given that the use case is for 100 stocks in a universe of 500 , it seems fair to have some cash to spare. It is also an argument which can be changed.

. Given that the use case is for stocks in a universe of , it seems fair to have some cash to spare. It is also an argument which can be changed. And calling cerebro.run()

Finally the performance is evaluated

To make it possible to run things with different parameters straight from the command line, an argparse enabled boilerplate is presented below, with the entire code

Performance Evaluation

A naive performance evaluation added in the form of the final resulting value, i.e.: the final net asset value minus the starting cash.

The backtrader ecosystem offers a set of built-in performance analyzers which could also be used, like: SharpeRatio , Variability-Weighted Return , SQN and others. See Docs - Analyzers Reference

The complete script

And finally the bulk of the work presented as whole. Enjoy!