How to establish a strategy with really strong 57% returns

July 8, 2023
Facebook logo.
Twitter logo.
LinkedIn logo.

How to establish a strategy with really strong 57% returns

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.

1import warnings
2warnings.filterwarnings("ignore")
3
4import pandas as pd
5
6from zipline import run_algorithm
7from zipline.api import (
8    attach_pipeline,
9    date_rules,
10    order_target_percent,
11    pipeline_output,
12    record,
13    schedule_function,
14    symbol,
15    time_rules,
16    get_open_orders,
17)
18from zipline.finance import commission, slippage
19from zipline.pipeline import Pipeline
20from zipline.pipeline.factors import SimpleMovingAverage
21from zipline.pipeline.data import USEquityPricing
22
23import 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.

1def initialize(context):
2    context.symbols = [
3        symbol("SPY"),
4        symbol("EFA"),
5        symbol("IEF"),
6        symbol("VNQ"),
7        symbol("GSG"),
8    ]
9    context.sma = {}
10    context.period = 10 * 21
11
12    for asset in context.symbols: 
13        context.sma[asset] = SimpleMovingAverage(
14            inputs=[USEquityPricing.close],
15            window_length=context.period
16        )
17
18    schedule_function(
19        func=rebalance,
20        date_rule=date_rules.month_start(),
21        time_rule=time_rules.market_open(minutes=1),
22    )
23
24    context.set_commission(
25        commission.PerShare(cost=0.01, min_trade_cost=1.00)
26    )
27    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.

1def rebalance(context, data):
2    
3    longs = [
4        asset
5        for asset in context.symbols
6        if data.current(asset, "price") > context.sma[asset].mean()
7    ]
8
9    for asset in context.portfolio.positions:
10        if asset not in longs and data.can_trade(asset):
11            order_target_percent(asset, 0)
12
13    for asset in longs:
14        if data.can_trade(asset):
15            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.

1start = pd.Timestamp("2010")
2end = pd.Timestamp("2023-06-30")
3
4perf = run_algorithm(
5    start=start,
6    end=end,
7    initialize=initialize,
8    capital_base=100000,
9    bundle="quandl-eod"
10)

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.

1returns, positions, transactions = \
2    pf.utils.extract_rets_pos_txn_from_zipline(perf)
3
4
5pf.create_full_tear_sheet(
6    returns,
7    positions=positions,
8    transactions=transactions,
9    round_trips=True,
10)
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.

Man with glasses and a wristwatch, wearing a white shirt, looking thoughtfully at a laptop with a data screen in the background.