Back to Community
ETF TAA

I have been working on improving my ETF allocation thought I would share and see if anyone has any simple improvements...

  • Rebalance Once A Year
  • Vanguard 4 Fund in 60/40 config
  • Combine with All Weather because Gold/Commodities can lower drawdown without hurting returns
  • Try and add some simple momentum to do better

ToDo:

  • I am unsure if DBC allocation is worth it? I suspect back testing doesn't go back far enough to show when this saved the day.
  • Maybe add some growth and value depending on slope of yield curve.
  • Is momentum going to fail spectacularly when interest rates start going up?
Clone Algorithm
13
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import StaticAssets
from quantopian.pipeline.factors import SimpleMovingAverage as SMA

def initialize(context):
    context.etfs = [
        symbol('VTI'),    # Vanguard Total Stock Market (2001-06-01)
        symbol('VXUS'),    # Total International Stock ETF (2011-01-28)
        symbol('BND'),    # Vanguard Total Bond Market (2007-04-13)
        symbol('VNQ'),    # Vanguard REIT
        symbol('IAU')    # iShares Gold Trust
        #symbol('DBC')     # PowerShares DB Commodity Tracking
    ]
    context.default_weights = [0.40, 0.20, 0.30, 0.05, 0.05]
    
    context.MA_FAST = 20
    context.MA_SLOW = 120
    
    set_commission(commission.PerTrade(cost=0.00))
    set_slippage(slippage.FixedSlippage(spread=0.0))
    schedule_function(rebalance, date_rules.month_start(), time_rules.market_open(minutes=30))
    attach_pipeline(make_pipeline(context), 'pipeline')
    
    
def make_pipeline(context):
    universe = StaticAssets(context.etfs)
    pipe = Pipeline(screen = universe)
    
    ma_fast = SMA(inputs=[USEquityPricing.close], window_length=context.MA_FAST)
    ma_slow = SMA(inputs=[USEquityPricing.close], window_length=context.MA_SLOW)
    signal = ma_fast > ma_slow

    pipe.add(signal, 'signal')
    
    return pipe

    
def before_trading_start(context, data):
    context.output = pipeline_output('pipeline')
    
    
def rebalance(context, data):
    now = get_datetime()
    if now.month != 1 or get_open_orders(): return
    
    weights = calc_weights(context, data)
    
    log.info("Rebalance: {0:%Y-%m-%d %H:%M:%S}".format(now))
    for etf in context.etfs:
        if data.can_trade(etf):
            log.info("{} = {:.2%}".format(etf.symbol, weights[etf]))
            order_target_percent(etf, weights[etf])

        
def calc_weights(context, data):
    weights ={}
    excess = 0
    
    for i, etf in enumerate(context.etfs):
        weight = context.default_weights[i]
        if data.can_trade(etf) and context.output.get_value(etf, 'signal'):
            weights[etf] = weight
        else:
            weights[etf] = 0.0
            excess += weight
    
    # Distribute Excess Weight In Porportion
    for (etf, weight) in weights.items():
        if weight > 0:
            weights[etf] = weight + ((excess * weight) / (1.0 - excess))

    return weights
There was a runtime error.
10 responses

@Jacob,
FYI:
VNQ, started trading on 2004-09-29
IAU, started trading on 2005-01-28
BND, started trading on 2007-04-10
VXUS, started trading on 2011-01-28

Is it appropriate to use them in backtest 2004-01-01 to 2019-12-13 ?

@Vladimir

Agreed. I just was trying to make sure I got as much of the housing crisis as possible. Starting at 2007-01-01 doesn't change much, other than take away a few years of tracking the benchmark, but agreed all etfs should be available during back test.

Also just wanted to say I think you write the cleanest code on Quantopian. I don't know how many times I have seen something you have done and gone "that is so much more eloquent than what I was doing".

@Jacob,

Thanks for the compliments.
Here is my version of what you are trying to do.
First I replaced 4 Vanguard ETFs with similar ones with a longer history.
Changed the start date to 01-01-2005.
Added BOND = symbol('IEF') to be in position when all ASSETS are in downtrend.
For ALL WHETER portfolio may be sufficient annual rebalancing, but as you added a tactical element (momentum) to my mind,
it should be rebalanced more often.
Therefore, I set up a quarterly rebalancing.

# Tactical rebalancing of All Weather Portfolio by Vladimir  
# --------------------------------------------------------------------------------------------  
ASSETS = symbols('VTI', 'EEM', 'TLT', 'IYR', 'GLD'); WEIGHTS = [0.40, 0.20, 0.30, 0.05, 0.05]  
BOND = symbol('IEF'); MA_F = 20; MA_S = 120; LEV = 1.0; MONTHES = [1, 4, 7, 10]  
# --------------------------------------------------------------------------------------------  
def initialize(context):  
    schedule_function(rebalance, date_rules.month_start(), time_rules.market_open(minutes = 65))

def rebalance(context, data):  
    if get_datetime().month not in MONTHES or get_open_orders(): return  
    sma_f = data.history(ASSETS, 'price', MA_F, '1d').mean()  
    sma_s = data.history(ASSETS, 'price', MA_S, '1d').mean()  
    picks = sma_f[ sma_f > sma_s ].index  
    weights = {}; adj_weights = {}; denom = 0;

    for i, sec in enumerate(ASSETS):  
        if data.can_trade(sec) and sec in picks: weights[sec] = WEIGHTS[i]; denom += WEIGHTS[i]  
        else: weights[sec] = 0.0   

    for sec in weights:  
        if sec in picks: adj_weights[sec] = weights[sec]/denom  
        else: adj_weights[sec] = 0.0

    adj_weights[BOND] = 0 if len(picks) > 0 else LEV  
    for sec, wt in adj_weights.items(): order_target_percent(sec, LEV*wt)  

def before_trading_start(context, data):  
    record(leverage = context.account.leverage, pos_count = len(context.portfolio.positions))  
Loading notebook preview...

@Vladimir

See that is what I mean... your code to distribute excess allocation to remaining funds is much more eloquent than my code :)

Question is is the extra alpha due to quarterly re-balancing, or getting out of everything and into bonds when everything is bad. Going to try and answer this now.

@Vladimir

1) Seems like quarterly rebalancing is everything. This is a shame because I would prefer to only rebalance once a year for effort and tax reasons.
2) Doesn't make much difference if you go all Bonds or all Cash when all assets are in downtrend.

i would try do the backtest using Jan/Feb/March... all the way to Dec. As it is only annual rebalancing, I wanted to know if the strategy is not data mining in terms of when the rebalancing occurs.

Moved rebalancing to the end of the quarter.

ASSETS = symbols('VTI', 'EEM', 'TLT', 'IYR', 'GLD'); WEIGHTS = [0.40, 0.20, 0.30, 0.05, 0.05]  
BOND = symbol('IEF'); MA_F = 20; MA_S = 120; LEV = 1.0; MONTHES = [3, 6, 9, 12]  
schedule_function(rebalance, date_rules.month_end(), time_rules.market_open(minutes = 65))  
Loading notebook preview...

@Xiaochen

Looks like @Vladimir answered your question before I could...

My biggest concern at this point is how much under performance this has during the "New Normal" period, which also happens to be the most recent. Also I am still testing for a way to re-balance once a year makes it so much easier on me handling family investments.

@Xiaochen

I think your right about re-balance once a year month. Using @Vladimir latest that re-balances EOM I got the following for each start month:

  • Jan: 202.16%
  • Feb: 212.82%
  • Mar: 506.75%
  • Apr: 282.44%
  • May: 132.91%
  • Jun: 226.42%
  • Jul: 181.83%
  • Aug: 232.32%
  • Sep: 181.83%
  • Oct: 251.13%
  • Nov: 150.14%
  • Dec: 154.84%

Interesting that Mar re-balance actually does better than quarterly re-balance.

I was playing with MaximizeAlpha and discovered it just goes all in on the highest weighted asset, and I was surprised by the results. IE go all in VTI, unless its out of trend, else all in TLT unless its out of trend, else ... Does this suggest trend following is better than asset diversification?! I don't like the potential short term capital gain generation, but it works pretty well re-balancing once a year in march too. It also just stays in VTI most of the time. This also opens a lot of questions about what is the optimum hierarchy of ETFs. Should one go IEF before GLD?

Clone Algorithm
11
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
'''
Trend Following All Into Heiarchy of ETFs
'''
# --------------------------------------------------------- 
ASSETS = symbols('VTI', 'TLT', 'EEM', 'IYR', 'GLD')  # Order Matters
BOND = symbol('IEF')
MA_F = 20
MA_S = 120
LEV = 1.0 
MONTHES = [3, 6, 9, 12]
# -------------------------------------------------------- 
def initialize(context):
    set_commission(commission.PerTrade(cost=0.00))
    set_slippage(slippage.FixedSlippage(spread=0.0))
    schedule_function(rebalance, date_rules.month_end(), time_rules.market_open(minutes = 65))

def rebalance(context, data):
    now = get_datetime()
    if now.month not in MONTHES or get_open_orders(): return
    
    sma_f = data.history(ASSETS, 'price', MA_F, '1d').mean()  
    sma_s = data.history(ASSETS, 'price', MA_S, '1d').mean()
    picks = sma_f[ sma_f > sma_s ].index

    weights = {}; found = False
    for sec in ASSETS:  
        if not found and data.can_trade(sec) and sec in picks: 
            weights[sec] = LEV
            found = True
        else:
            weights[sec] = 0.0

    weights[BOND] = 0 if found else LEV
            
    log.info("Rebalance: {0:%Y-%m-%d %H:%M:%S}".format(now))
    for sec, wt in weights.items():
        if data.can_trade(sec):
            log.info("{} = {:.2%}".format(sec.symbol, wt))
            order_target_percent(sec, wt) 
  
    record(
        leverage = context.account.leverage,
        pos_count = len(context.portfolio.positions)
    )
There was a runtime error.