Strategic and Equal Weighted ETF Portfolios in QSTrader

Strategic and Equal Weighted ETF Portfolios in QSTrader

In a previous article the monthly rebalance feature of the open-source backtesting library QSTrader was demonstrated on a simplistic equities/bonds ETF mix portfolio.

In this article new streamlined code will be presented to allow straightforward modification of the portfolio weightings. In particular two new portfolios of ETFs will be presented, influenced by by a recent post[1] at The Capital Spectator.

The first will contain a "strategic" weighting of ETFs mixing US equities (large and small-cap), US bonds (investment grade and high-yield), emerging markets equities and "alternative assets" such as commodities and REITs. The second contains the same set of ETFs albeit with an equal dollar weighting as opposed to a strategic mix.

The performance between the two portfolios since late 2007 will be analysed and discussed.

The Trading Strategies

The trading strategies are extremely similar and only vary in portfolio weightings and starting dates.

At the end of every month the strategy fully liquidates the portfolio and then rebalances each asset to be dollar-weighted according to the current account equity.

The first strategy simply carries a 60%/40% weighting of SPY and AGG, representing large-cap US equities and investment grade bonds respectively.

The second strategy provides a 30% allocation to US equities via SPY and IJS, 25% to emerging markets equities via EFA and EEM, 25% to US bonds (investment grade and high-yield) via AGG and JNK, 10% allocation to commodities via DJP and finally 10% allocation to real estate investment trusts (REIT).

The third strategy uses the same set of ETFs as the second strategy but utilises equal weighting at 12.5% for all eight.

The starting dates are varied solely on the availability of data. Strategy #1 begins on the 29th September 2003, while strategies #2 and #3 begin on the 4th December 2007. All strategies end on the 12th October 2016.

Ticker #1 - 60/40 US Equities/Bonds #2 - "Strategic" Weight #3 - Equal Weight
SPY 60.0% 25.0% 12.5%
IJS 0.0% 5.0% 12.5%
EFA 0.0% 20.0% 12.5%
EEM 0.0% 5.0% 12.5%
AGG 40.0% 20.0% 12.5%
JNK 0.0% 5.0% 12.5%
DJP 0.0% 10.0% 12.5%
RWR 0.0% 10.0% 12.5%

Data

In order to carry out this strategy it is necessary to have OHLCV pricing data for the ETFs in the period covered by this backtest:

Ticker Name Period Link
SPY SPDR S&P 500 ETF 29th September 2003 - 12th October 2016 Yahoo Finance
IJS iShares S&P Small-Cap 600 Value ETF 4th December 2007 - 12th October 2016 Yahoo Finance
EFA iShares MSCI EAFE ETF 4th December 2007 - 12th October 2016 Yahoo Finance
EEM iShares MSCI Emerging Markets ETF 4th December 2007 - 12th October 2016 Yahoo Finance
AGG iShares Core US Aggregate Bond ETF 29th September 2003 - 12th October 2016 Yahoo Finance
JNK SPDR Barclays Capital High Yield Bond ETF 4th December 2007 - 12th October 2016 Yahoo Finance
DJP iPath Bloomberg Commodity Index Total Return ETN 4th December 2007 - 12th October 2016 Yahoo Finance
RWR SPDR Dow Jones REIT ETF 4th December 2007 - 12th October 2016 Yahoo Finance

This data will need to placed in the directory specified by the QSTrader settings file if you wish to replicate the results.

Python QSTrader Implementation

The procedure for carrying out a backtest with a monthly full liquidation and rebalance is outlined in the previous article.

In order to streamline the generation of multiple separate portfolios without excessive code duplication a new file called monthly_rebalance_run.py has been created. This contains the backtesting "boilerplate" code necessary to carry out a monthly full liquidation and rebalanced backtest.

To generate the separate portfolio backtests for this article the function run_monthly_rebalance is imported from monthly_rebalance_run.py. It is then called with the benchmark ticker (SPY), the ticker_weights dictionary containing the ETF proportions, the tearsheet title text, start/end dates and account equity.

This makes it extremely straightforward to modify portfolio composition assuming availability of the pricing data. As an example the code for the 60/40 US equities/bond mix is given by:

# equities_bonds_60_40_etf_portfolio_backtest.py

import datetime

from qstrader import settings
from monthly_rebalance_run import run_monthly_rebalance


if __name__ == "__main__":
    ticker_weights = {
        "SPY": 0.6,
        "AGG": 0.4,
    }
    run_monthly_rebalance(
        settings.DEFAULT_CONFIG_FILENAME, False, "",
        "SPY", ticker_weights, "US Equities/Bonds 60/40 Mix ETF Strategy",
        datetime.datetime(2003, 9, 29), datetime.datetime(2016, 10, 12),
        500000.00
    )

The code for other portfolio compositions can be found in the "Full Code" section at the end of the article, although they are very similar to the above snippet.

To run any of the backtests it is necessary to change directory to the location of the monthly_rebalance_run.py file and then type the following into the console:

$ python equities_bonds_60_40_etf_portfolio_backtest.py

Remember to change the filename depending upon which backtest you wish to run.

Strategy Results

Transaction Costs

The strategy results presented here are given net of transaction costs. The costs are simulated using Interactive Brokers US equities fixed pricing for shares in North America. They do not take into account commission differences for ETFs, but they are reasonably representative of what could be achieved in a real trading strategy.

US Equities/Bonds 60/40 ETF Portfolio

The strategy itself is identical to that in the previous post and has been republished here due to the use of additional historic data stretching back to 2003.

The tearsheet for the strategy is given below:

Click the image for a larger view.

The benchmark is provided by a buy-and-hold portfolio (i.e. no monthly rebalancing) solely of the SPY ETF.

The Sharpe Ratio of the benchmark and this portfolio are identical at 0.4. Hence we are accepting a higher maximum drawdown by only investing in SPY. The CAGR of the strategy is 4.43%, which is lower than the 5.92% of SPY. This is due to transaction costs of carrying out the rebalancing as well as the underperformance of AGG with respect to SPY.

AGG did somewhat cushion the vast drawdown of 2008 for the portfolio but not by a significant amount. Because of the 2008/2009 crash the portfolio is underwater for 1242 days—almost 3 1/2 years—compared to the benchmark's 1365 days. Be aware however that this period has had a dramatic effect on nearly any ETF portfolio.

"Strategic" Weight ETF Portfolio

The tearsheet for the strategy is given below:

Click the image for a larger view.

The benchmark is provided by a buy-and-hold portfolio (i.e. no monthly rebalancing) solely of the SPY ETF.

In the strategic weight portfolio an attempt was made to replicate the portfolio over at this post[1]. However, suitable instruments could not be found to replicate the specific indices of VWEXH, RPIBX, PREMX and VGSIX. These represent US junk bonds, foreign developed markets bonds, emerging markets bonds and US REITs respectively.

In addition it was difficult to locate sufficient data for ETFs intended to represent RPIBX and PREMX, namely VWOB (Vanguard Emerging Markets Govt Bd ETF) and BNDX (Vanguard Total International Bond ETF). The latter two only began trading in late 2013, which meant only a 36-month period backtest. Such a duration is not sufficiently long to produce representative performance for a monthly rebalance strategy.

Hence the universe was reduced to eight ETFs compared to ten in the aforementioned post. They are outlined above in the "Data" section. Instead the portfolio is weighted 30% to US equities, 25% to emerging markets equities, 25% to US bonds, 10% to commodities and 10% to real estate.

The performance of this strategy is far worse than the 60/40 portfolio. It has a negative CAGR. Some of the pressure on the returns are a consequence of trading in eight separate securities every month. However nearly all other ETFs in the portfolio underperformed SPY by a wide margin, dragging the returns down significantly.

Commodities and fixed income in particular have not had good performance over the last five years relative to US equities.

This suggests that an equal weighting will likely make things far worse. In the next section the strategy for equal weighting is carried out.

Equal Weight ETF Portfolio

The tearsheet for the strategy is given below:

Click the image for a larger view.

The benchmark is provided by a buy-and-hold portfolio (i.e. no monthly rebalancing) solely of the SPY ETF.

As expected the performance of this portfolio is indeed significantly worse than either the strategic weighting or the 60/40 mix. The portfolio remains underwater from 2008 onwards and posts a CAGR of almost -5%. The maximum daily drawdown of this strategy is just under 73%.

However it is also clear that nearly all of the drawdown is a consequence of the 2008 financial crisis. Had the backtest been carried out a year later the results would likely have been quite different, albeit still underperforming with regards to SPY.

In summary it is clearly relatively tricky to construct an equal-weighted or dollar-weighted portfolio that can weather significant market events such as the 2008 crash. In future posts we will investigate ways of mitigating against such problems.

Next Steps

The unequal volatility, as measured by historic standard deviations of returns, of the various asset classes motivates the concept of risk parity, whereby capital is allocated on the basis of "risk" rather than dollar-weighting.

Risk parity requires historical calculations over streams of returns and thus has a fundamentally different structure to the strategies outlined above. A future version of QSTrader will enable calculations of this sort, such that a wide variety of risk parity portfolios can be constructed in an attempt to increase Sharpe Ratios.

References

Full Code

# monthly_rebalance_run.py

import datetime

import click

from qstrader import settings
from qstrader.compat import queue
from qstrader.price_parser import PriceParser
from qstrader.price_handler.yahoo_daily_csv_bar import YahooDailyCsvBarPriceHandler
from qstrader.strategy.monthly_liquidate_rebalance_strategy import MonthlyLiquidateRebalanceStrategy
from qstrader.strategy import Strategies, DisplayStrategy
from qstrader.position_sizer.rebalance import LiquidateRebalancePositionSizer
from qstrader.risk_manager.example import ExampleRiskManager
from qstrader.portfolio_handler import PortfolioHandler
from qstrader.compliance.example import ExampleCompliance
from qstrader.execution_handler.ib_simulated import IBSimulatedExecutionHandler
from qstrader.statistics.tearsheet import TearsheetStatistics
from qstrader.trading_session.backtest import Backtest


def run_monthly_rebalance(
    config, testing, filename, 
    benchmark, ticker_weights, title_str, 
    start_date, end_date, equity
):
    config = settings.from_file(config, testing)
    tickers = [t for t in ticker_weights.keys()]

    # Set up variables needed for backtest
    events_queue = queue.Queue()
    csv_dir = config.CSV_DATA_DIR
    initial_equity = PriceParser.parse(equity)

    # Use Yahoo Daily Price Handler
    price_handler = YahooDailyCsvBarPriceHandler(
        csv_dir, events_queue, tickers,
        start_date=start_date, end_date=end_date
    )

    # Use the monthly liquidate and rebalance strategy
    strategy = MonthlyLiquidateRebalanceStrategy(tickers, events_queue)
    strategy = Strategies(strategy, DisplayStrategy())

    # Use the liquidate and rebalance position sizer
    # with prespecified ticker weights
    position_sizer = LiquidateRebalancePositionSizer(ticker_weights)

    # Use an example Risk Manager
    risk_manager = ExampleRiskManager()

    # Use the default Portfolio Handler
    portfolio_handler = PortfolioHandler(
        initial_equity, events_queue, price_handler,
        position_sizer, risk_manager
    )

    # Use the ExampleCompliance component
    compliance = ExampleCompliance(config)

    # Use a simulated IB Execution Handler
    execution_handler = IBSimulatedExecutionHandler(
        events_queue, price_handler, compliance
    )

    # Use the default Statistics
    title = [title_str]
    statistics = TearsheetStatistics(
        config, portfolio_handler, title, benchmark
    )

    # Set up the backtest
    backtest = Backtest(
        price_handler, strategy,
        portfolio_handler, execution_handler,
        position_sizer, risk_manager,
        statistics, initial_equity
    )
    results = backtest.simulate_trading(testing=testing)
    statistics.save(filename)
    return results
# equities_bonds_60_40_etf_portfolio_backtest.py

import datetime

from qstrader import settings
from monthly_rebalance_run import run_monthly_rebalance


if __name__ == "__main__":
    ticker_weights = {
        "SPY": 0.6,
        "AGG": 0.4,
    }
    run_monthly_rebalance(
        settings.DEFAULT_CONFIG_FILENAME, False, "",
        "SPY", ticker_weights, "US Equities/Bonds 60/40 Mix ETF Strategy",
        datetime.datetime(2003, 9, 29), datetime.datetime(2016, 10, 12),
        500000.00
    )
# strategic_weight_etf_portfolio_backtest.py

import datetime

from qstrader import settings
from monthly_rebalance_run import run_monthly_rebalance


if __name__ == "__main__":
    ticker_weights = {
        "SPY": 0.25,
        "IJS": 0.05,
        "EFA": 0.20,
        "EEM": 0.05,
        "AGG": 0.20,
        "JNK": 0.05,
        "DJP": 0.10,
        "RWR": 0.10 
    }
    run_monthly_rebalance(
        settings.DEFAULT_CONFIG_FILENAME, False, "",
        "SPY", ticker_weights, "Strategic Weight ETF Strategy",
        datetime.datetime(2007, 12, 4), datetime.datetime(2016, 10, 12),
        500000.00
    )
# equal_weight_etf_portfolio_backtest.py

import datetime

from qstrader import settings
from monthly_rebalance_run import run_monthly_rebalance


if __name__ == "__main__":
    ticker_weights = {
        "SPY": 0.125,
        "IJS": 0.125,
        "EFA": 0.125,
        "EEM": 0.125,
        "AGG": 0.125,
        "JNK": 0.125,
        "DJP": 0.125,
        "RWR": 0.125 
    }
    run_monthly_rebalance(
        settings.DEFAULT_CONFIG_FILENAME, False, "",
        "SPY", ticker_weights, "Equal Weight ETF Strategy",
        datetime.datetime(2007, 12, 4), datetime.datetime(2016, 10, 12),
        500000.00
    )