Back to Community
Meb Fabers' 3-Way system

How annoying to see a system that is incredibly simple and it works. Seems like my own systems are too complex, although most code is to catch exceptions....

Based on the post of Meb Faber on his blog on the 16th of June ( http://mebfaber.com/2015/06/16/three-way-model/ ) I made a quick implementation of his 3-way system.

Three asset classes: Stocks, bonds, gold.

Invest equally in whatever is going up (defined as 3 month SMA > 10
month SMA).

So, how can we improve this further? How can we isolate the Alpha for the fund without destroying the edge?

have fun,

Peter

Clone Algorithm
143
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
# Meb Faber 3 ways system

# Three asset classes: Stocks, bonds, gold.
# Invest equally in whatever is going up (defined as 3 month SMA > 10 month SMA).
# - See more at: http://mebfaber.com/2015/06/16/three-way-model/#sthash.nUERh7tS.dpuf
import pandas as pd
import talib as ta

def initialize(context):
    context.stocks = symbol('IVV')
    context.bonds = symbol('TLT')
    context.gold = symbol('GLD') # from 2005
    context.longterm  = 10
    context.shortterm = 3
    context.N = (context.longterm+ context.shortterm)*30
    
    schedule_function(handle_data_daily,
                      date_rule=date_rules.month_end(days_offset=5),
                      time_rule=time_rules.market_open(minutes=30))
    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    pass

def handle_data_daily(context, data):
    
    prices =  history(context.N, frequency="1d", field='price')
    prices = prices.resample('BM', how='last',closed ='left', label='left')
    pricesshort =  prices.apply(ta.SMA, timeperiod=context.shortterm)
    priceslong  =  prices.apply(ta.SMA, timeperiod=context.longterm)
    
    updown = pricesshort.iloc[-1] > priceslong.iloc[-1] 
    weight = 1.0/len(updown[updown==True]) if len(updown[updown==True])!=0 else 0.0
    
    for stock in updown.index:
        if stock in data and 'price' in data[stock]:
            if not updown[stock]: order_target_percent(stock,0)
            if updown[stock]:     order_target_percent(stock,(weight))
There was a runtime error.
9 responses

beta 0.06 and sharpe 1.35 it is already too good to be true.
This is what Q is really looking for.

These 3 assets are really very cool just holding them in equal amount over
long time gives +ve sharpe and very low beta.

Clone Algorithm
7
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
# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    context.security = symbol('SPY')
    context.bond = symbol('TLT')
    context.gold = symbol('GLD')
    pass

# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    record(pamount = context.portfolio.positions[context.security].amount)
    if context.portfolio.positions[context.bond].amount <= 0:
        order_percent(context.security, 0.33)
        order_percent(context.bond, 0.33)
        order_percent(context.gold, 0.33)
There was a runtime error.

Could you explain the rules? Specifically, is this system based on a monthly rebalancing period and, if, for example, only one asset class is showing a "buy," does one invest 100% in it, or only 33.3%?

100%

weight = 1.0/len(updown[updown==True]) if len(updown[updown==True])!=0 else 0.0

~ if the len of the updown array is unequal to zero then divide 1 by the count of the updows elements with True. If there are no elements in updown weight =0

and I made a more aggressive version of this algo:

Still lost 12.5% in 4 months so performs as abad as the market

Clone Algorithm
22
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
# Meb Faber 3 ways system

# Three asset classes: Stocks, bonds, gold.
# Invest equally in whatever is going up (defined as 3 month SMA > 10 month SMA).
# - See more at: http://mebfaber.com/2015/06/16/three-way-model/#sthash.nUERh7tS.dpuf
import pandas as pd
import numpy as np
import datetime
import math
import operator
from functools import partial
from scipy  import  polyfit, polyval
from datetime import timedelta
import time
import statsmodels.api as sm
import talib as ta

def initialize(context):
    set_symbol_lookup_date('2015-01-01')

    context.stocks = symbol('IVV')
    context.bonds = symbol('TLT')
    context.gold = symbol('GLD') # from 2005
    #context.vol = symbol('VXX')
    context.spycomponents = symbols(
                           'XLY',  # XLY Consumer Discrectionary SPDR Fund   
                           'XLF',  # XLF Financial SPDR Fund  
                           'XLK',  # XLK Technology SPDR Fund  
                           'XLE',  # XLE Energy SPDR Fund  
                           'XLV',  # XLV Health Care SPRD Fund  
                           'XLI',  # XLI Industrial SPDR Fund  
                           'XLP',  # XLP Consumer Staples SPDR Fund   
                           'XLB',  # XLB Materials SPDR Fund  
                           'XLU')  # XLU Utilities SPRD Fund
    
 
    context.longterm  = 10
    context.shortterm = 3
    context.firsttime = True
    context.N = (context.longterm+ context.shortterm)*30
    
    schedule_function(handle_data_daily,
                      date_rule=date_rules.week_start(days_offset=1),
                      time_rule=time_rules.market_open(minutes=30))
    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    if context.firsttime:
        handle_data_daily(context, data)
        context.firsttime = False
    pass

def handle_data_daily(context, data):
    c=context
    prices =  history(context.N, frequency="1d", field='price').fillna(0)
    prices = prices.resample('BM', how='last',closed ='left', label='left').fillna(0)
    pricesshort =  prices.apply(ta.SMA, timeperiod=context.shortterm)
    priceslong  =  prices.apply(ta.SMA, timeperiod=context.longterm)
    
    updown = pricesshort.iloc[-1] > priceslong.iloc[-1] 
    updown_sub = updown[[context.stocks, context.bonds, context.gold]]
    weight = 0.99/len(updown_sub[updown_sub==True]) \
                         if len(updown_sub[updown_sub==True])!=0 else 0.0
    
    updown_comp = updown[context.spycomponents]
    
    if updown[context.stocks]:
         weight_comp = weight/len(updown_comp[updown_comp==True]) \
                         if len(updown_comp[updown_comp==True])!=0 else 0.0
    else:            
        weight_comp = 0.0
     
    log.info('weight = '+str(weight)+' companent weights = '+str(weight_comp))
    
    for stock in updown_sub.index:
        if stock == context.stocks: continue
        if stock in data and 'price' in data[stock]:
            if not updown[stock]: 
                order_target_percent(stock,0)
                log.info('Selling ' +stock.symbol)
            if updown[stock]:# and data[stock].price > pricesshort[stock][-1]:
                order_target_percent(stock,(weight))
                log.info('Buying ' +stock.symbol)
  
    for stock in updown_comp.index:
        if stock in data and 'price' in data[stock]:
            if not updown[stock] or not updown[context.stocks]: 
                order_target_percent(stock,0)
                log.info('Selling ' +stock.symbol)
            elif updown[stock]:# and data[stock].price > pricesshort[stock][-1]:
                order_target_percent(stock,(weight_comp))
                log.info('Buying ' +stock.symbol)
                
    ps = context.portfolio.positions
    p= context.portfolio.positions_value
    cs= context.portfolio.cash
    cp = (cs/(cs+p))*100
    record(Cash=cp)
    record(Positions_Value=(p/(cs+p))*100)
    record(No_Pos=len(ps))
    record(Bond=ps[c.bonds].amount*ps[c.bonds].last_sale_price/p*100 if p<>0 else 0)
    record(Gold=ps[c.gold].amount*ps[c.gold].last_sale_price/p*100 if p<>0 else 0)
    
    
# HELPER FUNCTIONS

fst = lambda tup: tup[0]
snd = lambda tup: tup[1]

def dedupe(seq, idfun=None): 
   # order preserving
   if idfun is None:
       def idfun(x): return x
   seen = {}
   result = []
   for item in seq:
       marker = idfun(item)
       if marker in seen: continue
       seen[marker] = 1
       result.append(item)
   return result        


def get_all_open_orders():
    return reduce(operator.add, map(snd, get_open_orders().iteritems()), [])
    
def get_orders_for_stock(stock):
    orders = get_all_open_orders()
    return filter(lambda o: o.sid == stock and o.amount != 0, orders)

def check_if_no_conflicting_orders(stock):
    # Check that we are not already trying to move this stock
    return len(get_orders_for_stock(stock)) == 0

def check_invalid_positions(context, securities):
    # Check that the portfolio does not contain any broken positions
    # or external securities
    for sid, position in context.portfolio.positions.iteritems():
        if sid not in securities and position.amount != 0:
            errmsg = \
            "Invalid position found: {sid} amount = {amt} on {date}".format(sid=position.sid,
                                                                            amt=position.amount,
                                                                            date=get_datetime())
            raise Exception(errmsg)

def do_cancel_order(context, sym, do_delete, order):
    log.info("X CANCELED {0:s} with {1:,d} / {2:,d} filled"\
        .format(sym,
                order.filled,
                order.amount))
    cancel_order(order)
    if do_delete: del context.duration[order.id]

def close_orders_after_time(context, stock, currentTime):
    # Ensure that the orders are not open past its desired duration.
    #  For live trading you could replace this with VWAPBestEffort,
    #  https://www.quantopian.com/help#api-VWAPBestEffort

    f = lambda o: o.id in context.duration and context.duration[o.id] <= currentTime
    map(partial(do_cancel_order, context, stock.symbol, True), filter(f, get_orders_for_stock(stock)))

def end_of_day(context, data):
    # cancel any order at the end of day. Do it ourselves so we can see slow moving stocks.

    log.info("")
    log.info("* EOD: Stoping Orders & Printing Held *")
    log.info("")

    #Print what positions we are holding overnight
    for stock in data:
        if context.portfolio.positions[stock.sid].amount != 0:
            log.info("{0:s} has remaining {1:,d} Positions worth ${2:,.2f}"\
                     .format(stock.symbol,
                             context.portfolio.positions[stock.sid].amount,
                             context.portfolio.positions[stock.sid].cost_basis\
                             *context.portfolio.positions[stock.sid].amount))

    # Cancel any open orders ourselves(In live trading this would be done for us, soon in backtest too)
    open_orders = get_all_open_orders()
    map(partial(do_cancel_order, context, stock.symbol, False), open_orders) # stock here just happens to be last one in list (is that truly what is desired?)

def fire_sale(context, data):
    #Sell everything in the portfolio, at market price
    log.info("# Fire Sale #")
    for stock in data:
        if context.portfolio.positions[stock.sid].amount != 0:
            order_target(stock, 0.0)
            value_of_open_orders(context, data)
            availibleCash = context.portfolio.cash-context.cashCommitedToBuy-context.cashCommitedToSell
            log.info("- SELL {0:,d} of {1:s} at ${2:,.2f} for ${3:,.2f} / ${4:,.2f}"\
                         .format(context.portfolio.positions[stock.sid].amount,
                                 stock.symbol,
                                 data[stock]['price'],
                                 data[stock]['price']*context.portfolio.positions[stock.sid].amount,
                                 availibleCash))   
    
        
    
def percent_diff(val1,val2):
    return abs(val1-val2)/((val1+val2)/2.0)

def value_of_open_orders(context, data):
    #current cash commited to open orders
    #currentCash = context.portfolio.cash
    open_orders = get_open_orders()
    cashCommitedToBuy = 0.0
    cashCommitedToSell = 0.0
    if open_orders:
        for security, orders in open_orders.iteritems():
            for oo in orders:
                #estimate value of existing order with current price, best to use order conditons?
                #log.info(oo.amount * data[oo.sid]['price'])
                if(oo.amount>0):
                    cashCommitedToBuy += oo.amount * data[oo.sid]['price']
                elif(oo.amount<0):
                    cashCommitedToSell += oo.amount * data[oo.sid]['price']
    
    return cashCommitedToBuy+cashCommitedToSell
                    
# Check if a company is about to end in the next days
def is_company_about_to_end_next_days(stock, days_to_check=5):
    return ((stock.end_date - get_datetime()).days <= days_to_check)


# Sell companies who are about to finish trading (merges, bankruptcy)
def sell_companies_about_to_end(context, data):
    for stock in context.portfolio.positions:
        if context.portfolio.positions[stock].amount ==0: continue
        if is_company_about_to_end_next_days(stock, 5) and \
           check_if_no_conflicting_orders(stock):
                
            log.info("Selling company %s which is about to end at %s." % (stock, stock.end_date))
            order_target_value(stock, 0.0)

                
    
    
There was a runtime error.

out of sample performance (mine is a bit lower in real life as I suffer forex loss/profit)

Clone Algorithm
22
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
# Meb Faber 3 ways system

# Three asset classes: Stocks, bonds, gold.
# Invest equally in whatever is going up (defined as 3 month SMA > 10 month SMA).
# - See more at: http://mebfaber.com/2015/06/16/three-way-model/#sthash.nUERh7tS.dpuf
import pandas as pd
import numpy as np
import datetime
import math
import operator
from functools import partial
from scipy  import  polyfit, polyval
from datetime import timedelta
import time
import statsmodels.api as sm
import talib as ta

def initialize(context):
    set_symbol_lookup_date('2015-01-01')

    context.stocks = symbol('IVV')
    context.bonds = symbol('TLT')
    context.gold = symbol('GLD') # from 2005
    #context.vol = symbol('VXX')
    context.spycomponents = symbols(
                           'XLY',  # XLY Consumer Discrectionary SPDR Fund   
                           'XLF',  # XLF Financial SPDR Fund  
                           'XLK',  # XLK Technology SPDR Fund  
                           'XLE',  # XLE Energy SPDR Fund  
                           'XLV',  # XLV Health Care SPRD Fund  
                           'XLI',  # XLI Industrial SPDR Fund  
                           'XLP',  # XLP Consumer Staples SPDR Fund   
                           'XLB',  # XLB Materials SPDR Fund  
                           'XLU',  # XLU Utilities SPRD Fund
                           'XME',  # XME Metals and Mining
                           'XOP')  # XOP Oil & Gas Exploration
 
    context.longterm  = 10
    context.shortterm = 3
    context.firsttime = True
    context.N = (context.longterm+ context.shortterm)*30
    
    context.available_weight = 0.95
    
    schedule_function(handle_data_daily,
                      date_rule=date_rules.week_start(days_offset=1),
                      time_rule=time_rules.market_open(minutes=30))
    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    if context.firsttime:
        handle_data_daily(context, data)
        context.firsttime = False
    pass

def handle_data_daily(context, data):
    c=context
    prices =  history(context.N, frequency="1d", field='price').fillna(0)
    prices = prices.resample('BM', how='last',closed ='left', label='left').fillna(0)
    pricesshort =  prices.apply(ta.SMA, timeperiod=context.shortterm)
    priceslong  =  prices.apply(ta.SMA, timeperiod=context.longterm)
    
    updown = pricesshort.iloc[-1] > priceslong.iloc[-1] 
    updown_sub = updown[[context.stocks, context.bonds, context.gold]]
    weight = context.available_weight/len(updown_sub[updown_sub==True]) \
                         if len(updown_sub[updown_sub==True])!=0 else 0.0
    
    updown_comp = updown[context.spycomponents]
    
    if updown[context.stocks]:
         weight_comp = weight/len(updown_comp[updown_comp==True]) \
                         if len(updown_comp[updown_comp==True])!=0 else 0.0
    else:            
        weight_comp = 0.0
     
    log.info('weight = '+str(weight)+' companent weights = '+str(weight_comp))
    
    for stock in updown_sub.index:
        if stock == context.stocks: continue
        if stock in data and 'price' in data[stock]:
            if not updown[stock]: 
                order_target_percent(stock,0)
                log.info('Selling ' +stock.symbol)
            if updown[stock]:# and data[stock].price > pricesshort[stock][-1]:
                order_target_percent(stock,(weight))
                log.info('Buying ' +stock.symbol)
  
    for stock in updown_comp.index:
        if stock in data and 'price' in data[stock]:
            if not updown[stock] or not updown[context.stocks]: 
                order_target_percent(stock,0)
                log.info('Selling ' +stock.symbol)
            elif updown[stock]:# and data[stock].price > pricesshort[stock][-1]:
                order_target_percent(stock,(weight_comp))
                log.info('Buying ' +stock.symbol)
                
    ps = context.portfolio.positions
    p= context.portfolio.positions_value
    cs= context.portfolio.cash
    cp = (cs/(cs+p))*100
    record(Cash=cp)
    record(Positions_Value=(p/(cs+p))*100)
    record(No_Pos=len(ps))
    record(Bond=ps[c.bonds].amount*ps[c.bonds].last_sale_price/p*100 if p<>0 else 0)
    record(Gold=ps[c.gold].amount*ps[c.gold].last_sale_price/p*100 if p<>0 else 0)
    
    
# HELPER FUNCTIONS

fst = lambda tup: tup[0]
snd = lambda tup: tup[1]

def dedupe(seq, idfun=None): 
   # order preserving
   if idfun is None:
       def idfun(x): return x
   seen = {}
   result = []
   for item in seq:
       marker = idfun(item)
       if marker in seen: continue
       seen[marker] = 1
       result.append(item)
   return result        


def get_all_open_orders():
    return reduce(operator.add, map(snd, get_open_orders().iteritems()), [])
    
def get_orders_for_stock(stock):
    orders = get_all_open_orders()
    return filter(lambda o: o.sid == stock and o.amount != 0, orders)

def check_if_no_conflicting_orders(stock):
    # Check that we are not already trying to move this stock
    return len(get_orders_for_stock(stock)) == 0

def check_invalid_positions(context, securities):
    # Check that the portfolio does not contain any broken positions
    # or external securities
    for sid, position in context.portfolio.positions.iteritems():
        if sid not in securities and position.amount != 0:
            errmsg = \
            "Invalid position found: {sid} amount = {amt} on {date}".format(sid=position.sid,
                                                                            amt=position.amount,
                                                                            date=get_datetime())
            raise Exception(errmsg)

def do_cancel_order(context, sym, do_delete, order):
    log.info("X CANCELED {0:s} with {1:,d} / {2:,d} filled"\
        .format(sym,
                order.filled,
                order.amount))
    cancel_order(order)
    if do_delete: del context.duration[order.id]

def close_orders_after_time(context, stock, currentTime):
    # Ensure that the orders are not open past its desired duration.
    #  For live trading you could replace this with VWAPBestEffort,
    #  https://www.quantopian.com/help#api-VWAPBestEffort

    f = lambda o: o.id in context.duration and context.duration[o.id] <= currentTime
    map(partial(do_cancel_order, context, stock.symbol, True), filter(f, get_orders_for_stock(stock)))

def end_of_day(context, data):
    # cancel any order at the end of day. Do it ourselves so we can see slow moving stocks.

    log.info("")
    log.info("* EOD: Stoping Orders & Printing Held *")
    log.info("")

    #Print what positions we are holding overnight
    for stock in data:
        if context.portfolio.positions[stock.sid].amount != 0:
            log.info("{0:s} has remaining {1:,d} Positions worth ${2:,.2f}"\
                     .format(stock.symbol,
                             context.portfolio.positions[stock.sid].amount,
                             context.portfolio.positions[stock.sid].cost_basis\
                             *context.portfolio.positions[stock.sid].amount))

    # Cancel any open orders ourselves(In live trading this would be done for us, soon in backtest too)
    open_orders = get_all_open_orders()
    map(partial(do_cancel_order, context, stock.symbol, False), open_orders) # stock here just happens to be last one in list (is that truly what is desired?)

def fire_sale(context, data):
    #Sell everything in the portfolio, at market price
    log.info("# Fire Sale #")
    for stock in data:
        if context.portfolio.positions[stock.sid].amount != 0:
            order_target(stock, 0.0)
            value_of_open_orders(context, data)
            availibleCash = context.portfolio.cash-context.cashCommitedToBuy-context.cashCommitedToSell
            log.info("- SELL {0:,d} of {1:s} at ${2:,.2f} for ${3:,.2f} / ${4:,.2f}"\
                         .format(context.portfolio.positions[stock.sid].amount,
                                 stock.symbol,
                                 data[stock]['price'],
                                 data[stock]['price']*context.portfolio.positions[stock.sid].amount,
                                 availibleCash))   
    
        
    
def percent_diff(val1,val2):
    return abs(val1-val2)/((val1+val2)/2.0)

def value_of_open_orders(context, data):
    #current cash commited to open orders
    #currentCash = context.portfolio.cash
    open_orders = get_open_orders()
    cashCommitedToBuy = 0.0
    cashCommitedToSell = 0.0
    if open_orders:
        for security, orders in open_orders.iteritems():
            for oo in orders:
                #estimate value of existing order with current price, best to use order conditons?
                #log.info(oo.amount * data[oo.sid]['price'])
                if(oo.amount>0):
                    cashCommitedToBuy += oo.amount * data[oo.sid]['price']
                elif(oo.amount<0):
                    cashCommitedToSell += oo.amount * data[oo.sid]['price']
    
    return cashCommitedToBuy+cashCommitedToSell
                    
# Check if a company is about to end in the next days
def is_company_about_to_end_next_days(stock, days_to_check=5):
    return ((stock.end_date - get_datetime()).days <= days_to_check)


# Sell companies who are about to finish trading (merges, bankruptcy)
def sell_companies_about_to_end(context, data):
    for stock in context.portfolio.positions:
        if context.portfolio.positions[stock].amount ==0: continue
        if is_company_about_to_end_next_days(stock, 5) and \
           check_if_no_conflicting_orders(stock):
                
            log.info("Selling company %s which is about to end at %s." % (stock, stock.end_date))
            order_target_value(stock, 0.0)

                
    
    
There was a runtime error.

Seems like most of the gains come from the 2008 dip and afterwards it does as well as buy and hold SPY.

Is a non leveraged strategy that keeps up with the market during a bull run so bad? I don't know of many that do.

Not complaining. I'll probably try it in my vanguard account to hedge myself. Great that it's so simple.