May cohort is now open: How to secure your spot:

How to establish a strategy with really strong 57% returns

How to establish a strategy with really strong 57% returns. It’s an easy-to-establish, ETF-based strategy that rebalances monthly.

Today we’ll jump into an asset-class trend-following strategy.

I love covering trading strategies in the newsletter and I’m excited to share this one with you! It’s an easy-to-establish, ETF-based strategy that rebalances monthly. Over the backtesting period, it racks up 57% cumulative returns with only 12.7% volatility.

Let’s go!

How to establish a strategy with really strong 57% returns

Asset class trend-following is a strategy that takes uses momentum and moving averages. It identifies periods of potential outperformance to minimize volatility and drawdowns.

It was first introduced by Meb Faber and widely accepted in academia.

Professionals apply asset class trend-following by incorporating momentum filters for different asset classes. They use it to stay invested in asset classes that are performing well while avoiding those with higher risk.

By understanding and implementing this strategy, you can manage risk-adjusted returns. It helps achieve a balance of equity-like returns with bond-like volatility and drawdowns.

Here’s how to get started.

Imports and set up

To run the backtest, you need historic data for the 5 ETFs used in the strategy. Unfortunately, it’s tricky to get it set up. If you need help building algorithmic trading strategies, Getting Started With Python for Quant Finance has a library of 60+ pre-built strategies.

We’ll use the Zipline backtesting framework to assess the strategy. PyFolio is great for risk and performance analysis.

import warnings
warnings.filterwarnings("ignore")

import pandas as pd

from zipline import run_algorithm
from zipline.api import (
    attach_pipeline,
    date_rules,
    order_target_percent,
    pipeline_output,
    record,
    schedule_function,
    symbol,
    time_rules,
    get_open_orders,
)
from zipline.finance import commission, slippage
from zipline.pipeline import Pipeline
from zipline.pipeline.factors import SimpleMovingAverage
from zipline.pipeline.data import USEquityPricing

import pyfolio as pf

Set up the strategy code

Start with building the function that’s called at the beginning of the backtest. The strategy logic happens in the next function.

def initialize(context):
    context.symbols = [
        symbol("SPY"),
        symbol("EFA"),
        symbol("IEF"),
        symbol("VNQ"),
        symbol("GSG"),
    ]
    context.sma = {}
    context.period = 10 * 21

    for asset in context.symbols: 
        context.sma[asset] = SimpleMovingAverage(
            inputs=[USEquityPricing.close],
            window_length=context.period
        )

    schedule_function(
        func=rebalance,
        date_rule=date_rules.month_start(),
        time_rule=time_rules.market_open(minutes=1),
    )

    context.set_commission(
        commission.PerShare(cost=0.01, min_trade_cost=1.00)
    )
    context.set_slippage(slippage.VolumeShareSlippage())

The logic is straightforward: For each ETF representing an asset class, calculate the 10-month simple moving average using the closing price.

This is a long-term strategy that rebalances at the market open on the first day of the month. Zipline’s schedule function makes it simple to set this up. Note we schedule a function called rebalance to run on this schedule. You’ll create it next.

Finally, include realistic commission and slippage models.

Create the function that contains the strategy logic.

def rebalance(context, data):
    
    longs = [
        asset
        for asset in context.symbols
        if data.current(asset, "price") > context.sma[asset].mean()
    ]

    for asset in context.portfolio.positions:
        if asset not in longs and data.can_trade(asset):
            order_target_percent(asset, 0)

    for asset in longs:
        if data.can_trade(asset):
            order_target_percent(asset, 1.0 / len(longs))

On the first trading day of every month, the logic checks if the current price is greater than the 10-month simple moving average. If so, it adds the symbol to a list.

The next step sets a target of 0% for those assets with a price that does not exceed the 10-month simple moving average.

Finally, we equal-weight the portfolio with the ETFs that are trending.

Run the backtest and analyze the results

Running the backtest is a few lines of code.

start = pd.Timestamp("2010")
end = pd.Timestamp("2023-06-30")

perf = run_algorithm(
    start=start,
    end=end,
    initialize=initialize,
    capital_base=100000,
    bundle="quandl-eod"
)

Call the run_algorithm function with the start date, end date, and name of your initialize function. It takes about a minute to run.

Now the fun part.

returns, positions, transactions = \
    pf.utils.extract_rets_pos_txn_from_zipline(perf)


pf.create_full_tear_sheet(
    returns,
    positions=positions,
    transactions=transactions,
    round_trips=True,
)
How to establish a strategy with really strong 57% returns. It’s an easy-to-establish, ETF-based strategy that rebalances monthly.

PyFolio comes with a utility function to extract returns, positions, and transactions from the backtest result. From there, you can call a single function and get dozens of risk and performance metrics printed to the screen.

Here’s some highlights:

  • 57% cumulative return
  • 12.6% annual volatility
  • -28.2% drawdown

And the lowlights:

  • 3.4% annual return
  • 0.33 Sharpe
  • 0.77 average win to loss ratio

This is a stable, low volatility strategy you can easily incorporate into your portfolio. Add it alongside more aggressive strategies for diversification.

Action steps

Your action step this week is to modify the strategy to use different ETFs for different asset classes. Try out different rebalancing schedules, too.