Valuation based market timing

Hello all,

I attempted to implement the market timing strategy discussed in this post from Alpha Architect. I took Schiller’s CAPE ratio data from his website and calculated the inflation rate directly from the CPI data listed in the spreadsheet.

I applied the data to an Acquirer’s Multiple strategy that rebalances yearly, but also sells when the market is ‘expensive’ and buys back when the market is ‘cheap’.

I tested the system from 2003-06-01 to 2015-12-04. The simple Acquirer’s Multiple strategy has the following returns,

Total Returns Benchmark Returns Alpha Beta Sharpe Sortino Information Ratio Volatility Max Drawdown
358.4% 173.2% .14 1.06 1.20 1.56 .67 .22 53.5%

The Acquirer’s Multiple strategy with market timing has the following returns,

Total Returns Benchmark Returns Alpha Beta Sharpe Sortino Information Ratio Volatility Max Drawdown
304.6% 173.2% .11 0.92 1.04 1.32 .49 .21 42.4%

Although there is a significant reduction in Max DD the results seam underwhelming given the reduction in overall volatility is negligible and there is a reduction in total return.

I also calculated the Real Yield Metric but only using CAPE and CPI data going back to 1945. The results using this data are more impressive,

Total Returns Benchmark Returns Alpha Beta Sharpe Sortino Information Ratio Volatility Max Drawdown
484.5% 173.2% .26 0.93 1.70 2.18 1.16 .21 40.4%

I’ll leave it to the reader to decide whether this is a case of datasnooping or if using post-war inflation and market data is a more realistic measure for estimating future valuations.

27
Loading...
Backtest from to with initial capital
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
'''
Implemented tactical asset allocation algorithm for value strategies discussed in this post from Alpha Architect,
http://blog.alphaarchitect.com/2015/07/21/eureka-a-valuation-based-asset-allocation-strategy-that-might-work/

To compute the Real Yield Spread Metric I took the Shiller CAPE ratio data directly from Robert Shiller's website. I computed an annualized inflation rate directly from the CPI data contained in the spreadsheet and used an expanding window for the percentile calculations as well as a 1-month lag on the inflation rate for the Real Yield calculation.

'''

from datetime import timedelta
import pandas as pd
import numpy as np

def initialize(context):
context.lower_mktcap = 500e6
context.limit = 500
context.num_positions = 100
context.base_weight = 1.0/context.num_positions
context.rebalance_month = 6 #June
context.is_rebalance_day = False
context.shy = symbol('IEF')
context.market_expensive = False
context.market_switched = False

#get Real Yield data
#fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1880.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield')
fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1945.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield')

schedule_function(set_rebalance_day,date_rules.month_start(days_offset=10),
time_rules.market_close(minutes=5))
schedule_function(rebalance_sell,date_rules.month_start(days_offset=11),
time_rules.market_open(minutes=240))
schedule_function(rebalance_buy,date_rules.month_start(days_offset=13),
time_rules.market_open(minutes=240))

def set_rebalance_day(context,data):
today = get_datetime()

if today.month == context.rebalance_month:
context.is_rebalance_day = True
else:
context.is_rebalance_day = False

def rebalance_sell(context,data):
if context.is_rebalance_day or (context.market_expensive and context.market_switched) :
for s in context.portfolio.positions:
order_target_percent(s,0)

if context.market_switched:
context.market_switched = False

def rebalance_buy(context,data):
if not(context.market_expensive) and (context.is_rebalance_day or  context.market_switched):
for s in context.longs:
if s in data and s.symbol != 'NP':
order_target_percent(s,context.base_weight)

context.is_rebalance_day = False
if context.market_switched:
context.market_switched = False

def before_trading_start(context,data):
if context.is_rebalance_day or  context.market_switched:
getstocks(context)
update_universe(context.longs)

def getstocks(context):
#103 = Financials, 207 = Utilities, 104 = Real Estate
excluded_sectors = [103, 207]
sector_code = fundamentals.asset_classification.morningstar_sector_code
fundamental_df = get_fundamentals(
query(
sector_code,
fundamentals.valuation.market_cap,
fundamentals.valuation.enterprise_value,
fundamentals.cash_flow_statement.capital_expenditure,
fundamentals.operation_ratios.roic,
fundamentals.income_statement.ebit,
fundamentals.income_statement.ebitda,
fundamentals.balance_sheet.total_assets,
fundamentals.valuation_ratios.ev_to_ebitda
)
.filter(fundamentals.share_class_reference.is_primary_share == True)
.filter(fundamentals.company_reference.primary_exchange_id != "OTCPK")
.filter(fundamentals.share_class_reference.is_depositary_receipt == False)
.filter(~sector_code.in_(excluded_sectors))
.filter(fundamentals.valuation.market_cap > context.lower_mktcap )
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
#.filter(fundamentals.valuation.market_cap < context.upper_mktcap )
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.limit)
)

context.longs = fundamental_df.columns[:context.num_positions]

def handle_data(context, data):
#log.info("in handle_data")

if 'Real Yield' in data and data['Real Yield']['Below 20th Percentile']:
if not(context.market_expensive):
context.market_switched = True
context.market_expensive = True
elif 'Real Yield' in data and not (data['Real Yield']['Below 20th Percentile']):
if context.market_expensive:
context.market_switched = True
context.market_expensive = False

record(leverage = context.account.leverage)

There was a runtime error.
12 responses

My apologies,

I noticed an error in the calculation of the Real Yield metric. I've updated the data,
here are the updated results.

For Real Yield calculated from 1800 the results are

Total Returns Benchmark Returns Alpha Beta Sharpe Sortino Information Ratio Volatility Max Drawdown
336.2% 173.2% .14 0.88 1.14 1.50 .63 .21 42.6%

For Real Yield calculated from 1945 the results are

Total Returns Benchmark Returns Alpha Beta Sharpe Sortino Information Ratio Volatility Max Drawdown
441.7% 173.2% .23 0.87 1.59 2.02 1.04 .21 38.8%

Your question "whether this is a case of datasnooping or if using post-war inflation and market data is a more realistic measure for estimating future valuations."

Meanwhile pardon this side-note, wanted to point out the shorts around 2010 in case it was inadvertent. Might have been beneficial or neutral. Happened just after weekly returns took a jump, visible on the full backtest page.

Edit: I guess I should add a note about something to watch out for. There are times when we make a change and the chart might show, say, 40% lower returns, while the dollars activated to achieve that return could have been lower, the profit on each dollar put to work can be higher. The change made was very good and just seemed worse. PvR in the code below can help keep an eye on that. The algo above makes $5 per dollar compared to starting capital. Only$2 each vs. the $2M utilized (the amount needed to achieve that result), turn on the cash_low option to see it. 13 Loading... Backtest from to with initial capital 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 ''' Implemented tactical asset allocation algorithm for value strategies discussed in this post from Alpha Architect, http://blog.alphaarchitect.com/2015/07/21/eureka-a-valuation-based-asset-allocation-strategy-that-might-work/ To compute the Real Yield Spread Metric I took the Shiller CAPE ratio data directly from Robert Shiller's website. I computed an annualized inflation rate directly from the CPI data contained in the spreadsheet and used an expanding window for the percentile calculations as well as a 1-month lag on the inflation rate for the Real Yield calculation. ''' from datetime import timedelta import pandas as pd import numpy as np def initialize(context): context.lower_mktcap = 500e6 context.limit = 500 context.num_positions = 100 context.base_weight = 1.0/context.num_positions context.rebalance_month = 6 #June context.is_rebalance_day = False context.shy = symbol('IEF') context.market_expensive = False context.market_switched = False #get Real Yield data #fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1880.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1945.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') schedule_function(set_rebalance_day,date_rules.month_start(days_offset=10), time_rules.market_close(minutes=5)) schedule_function(rebalance_sell,date_rules.month_start(days_offset=11), time_rules.market_open(minutes=240)) schedule_function(rebalance_buy,date_rules.month_start(days_offset=13), time_rules.market_open(minutes=240)) #schedule_function(info, date_rules.every_day(), time_rules.market_close()) c = context c.max_lvrg = 0 c.risk_hi = 0 c.date_prv = '' c.cash_low = c.portfolio.starting_cash c.date_end = str(get_environment('end').date()) print '{} to {} {} {}'.format(str(get_datetime().date()), c.date_end, int(c.cash_low), get_environment('data_frequency')) def set_rebalance_day(context,data): today = get_datetime() if today.month == context.rebalance_month: context.is_rebalance_day = True else: context.is_rebalance_day = False def rebalance_sell(context,data): if context.is_rebalance_day or (context.market_expensive and context.market_switched) : for s in context.portfolio.positions: order_target_percent(s,0) if context.market_switched: context.market_switched = False def rebalance_buy(context,data): if not(context.market_expensive) and (context.is_rebalance_day or context.market_switched): for s in context.longs: if s in data and s.symbol != 'NP': order_target_percent(s,context.base_weight) context.is_rebalance_day = False if context.market_switched: context.market_switched = False def before_trading_start(context,data): if context.is_rebalance_day or context.market_switched: getstocks(context) update_universe(context.longs) def getstocks(context): #103 = Financials, 207 = Utilities, 104 = Real Estate excluded_sectors = [103, 207] sector_code = fundamentals.asset_classification.morningstar_sector_code fundamental_df = get_fundamentals( query( sector_code, fundamentals.valuation.market_cap, fundamentals.valuation.enterprise_value, fundamentals.cash_flow_statement.capital_expenditure, fundamentals.operation_ratios.roic, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda, fundamentals.balance_sheet.total_assets, fundamentals.valuation_ratios.ev_to_ebitda ) .filter(fundamentals.share_class_reference.is_primary_share == True) .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK") .filter(fundamentals.share_class_reference.is_depositary_receipt == False) .filter(~sector_code.in_(excluded_sectors)) .filter(fundamentals.valuation.market_cap > context.lower_mktcap ) .filter(fundamentals.valuation_ratios.ev_to_ebitda > 0) #.filter(fundamentals.valuation.market_cap < context.upper_mktcap ) .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc()) .limit(context.limit) ) context.longs = fundamental_df.columns[:context.num_positions] def handle_data(context, data): #log.info("in handle_data") if 'Real Yield' in data and data['Real Yield']['Below 20th Percentile']: if not(context.market_expensive): context.market_switched = True context.market_expensive = True elif 'Real Yield' in data and not (data['Real Yield']['Below 20th Percentile']): if context.market_expensive: context.market_switched = True context.market_expensive = False record(leverage = context.account.leverage) info(context, data) def info(context, data): ''' Custom chart and/or log of profit_vs_risk returns and related information ''' # # # # # # # # # # Options # # # # # # # # # # record_max_lvrg = 1 # Maximum leverage encountered record_leverage = 0 # Leverage (context.account.leverage) record_q_return = 1 # Quantopian returns (percentage) record_pvr = 1 # Profit vs Risk returns (percentage) record_pnl = 0 # Profit-n-Loss record_shorting = 1 # Total value of any shorts record_risk = 0 # Risked, maximum cash spent or shorts in excess of cash at any time record_risk_hi = 0 # Highest risk overall record_cash = 0 # Cash available record_cash_low = 0 # Any new lowest cash level logging = 1 # Also log to the logging window conditionally (1) or not (0) log_method = 'risk_hi' # 'daily' or 'risk_hi' c = context # For brevity new_cash_low = 0 # To trigger logging in cash_low case date = str(get_datetime().date()) # To trigger logging in daily case cash = c.portfolio.cash if int(cash) < c.cash_low: # New cash low new_cash_low = 1 c.cash_low = int(cash) if record_cash_low: record(CashLow = int(c.cash_low)) pvr_rtrn = 0 # Profit vs Risk returns based on maximum spent profit_loss = 0 # Profit-n-loss shorts = 0 # Shorts value start = c.portfolio.starting_cash cash_dip = int(max(0, start - cash)) if record_cash: record(Cash = int(c.portfolio.cash)) # Cash if record_leverage: record(Lvrg = c.account.leverage) # Leverage if record_max_lvrg: if c.account.leverage > c.max_lvrg: c.max_lvrg = c.account.leverage record(MaxLv = c.max_lvrg) # Maximum leverage if record_pnl: profit_loss = c.portfolio.pnl record(PnL = profit_loss) # "Profit and Loss" in dollars for p in c.portfolio.positions: shrs = c.portfolio.positions[p].amount if shrs < 0: shorts += int(abs(shrs * data[p].price)) if record_shorting: record(Shorts = shorts) # Shorts value as a positve risk = int(max(cash_dip, shorts)) if record_risk: record(Risk = risk) # Amount in play, maximum of shorts or cash used new_risk_hi = 0 if risk > c.risk_hi: c.risk_hi = risk new_risk_hi = 1 if record_risk_hi: record(RiskHi = c.risk_hi) # Highest risk overall if record_pvr: # Profit_vs_Risk returns based on max amount actually spent (risk high) if c.risk_hi != 0: # Avoid zero-divide pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.risk_hi record(PvR = pvr_rtrn) # Profit_vs_Risk returns q_rtrn = 100 * (c.portfolio.portfolio_value - start) / start if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve from pytz import timezone if logging: if log_method == 'risk_hi' and new_risk_hi \ or log_method == 'daily' and c.date_prv != date \ or c.date_end == date \ or new_cash_low: qret = 'QRet ' + '%.1f' % q_rtrn mxlv = 'MaxLv ' + '%.1f' % c.max_lvrg if record_max_lvrg else '' pvr = 'PvR ' + '%.1f' % pvr_rtrn if record_pvr else '' pnl = 'PnL ' + '%.0f' % profit_loss if record_pnl else '' csh = 'Cash ' + '%.0f' % cash if record_cash else '' csh_lw = 'CshLw ' + '%.0f' % c.cash_low if record_cash_low else '' shrt = 'Shrt ' + '%.0f' % shorts if record_shorting else '' risk = 'Risk ' + '%.0f' % risk if record_risk else '' rsk_hi = 'RskHi ' + '%.0f' % c.risk_hi if record_risk_hi else '' minute = get_datetime().astimezone(timezone('US/Eastern')).time().minute log.info('{} {} {} {} {} {} {} {} {} {}'.format( minute, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, risk, rsk_hi)) if c.date_end == date: # Log on last day, like cash 125199 portfolio 126890 log.info('cash {} portfolio {}'.format( int(cash), int(c.portfolio.portfolio_value))) c.date_prv = date There was a runtime error. Hi Gary, Thanks for pointing this out. I will take a closer look at this. At first glance though I don't understand how a short can enter the portfolio since whenever an order is sent in the weight is guaranteed to be >= 0. So how can order_target_percent place a short sell? regards, Mark order_target_percent and their ilk do not take into account open orders, so if you are long and target 0%, then do that again before the first has fully executed, you might go short. That's right. This adds track_orders() to help you hone in on it with some lines added to only log sell/sold if a short would result. The sell orders in excess of current shares happen more often than actual fill. In possibly the most far-out use of the custom chart ever so far, when something is short, its security id is recorded. Only happened once, AAN 523. I ran your code earlier today, then later with no changes there was an error about DECO on the order line here. As a rough workaround I added the line above it. The only thing I can figure is, since you are pulling in as many as 500 sids, the Morningstar data was updated in the meantime resulting in a different set of securities adding DECO and maybe it became delisted. PvR/info() is turned off. Check the log window. Clone/run this. for s in context.portfolio.positions: if s not in data: continue order_target_percent(s,0) 13 Loading... Backtest from to with initial capital 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 ''' Implemented tactical asset allocation algorithm for value strategies discussed in this post from Alpha Architect, http://blog.alphaarchitect.com/2015/07/21/eureka-a-valuation-based-asset-allocation-strategy-that-might-work/ To compute the Real Yield Spread Metric I took the Shiller CAPE ratio data directly from Robert Shiller's website. I computed an annualized inflation rate directly from the CPI data contained in the spreadsheet and used an expanding window for the percentile calculations as well as a 1-month lag on the inflation rate for the Real Yield calculation. ''' from datetime import timedelta import pandas as pd import numpy as np def initialize(context): context.lower_mktcap = 500e6 context.limit = 500 context.num_positions = 100 context.base_weight = 1.0/context.num_positions context.rebalance_month = 6 #June context.is_rebalance_day = False context.shy = symbol('IEF') context.market_expensive = False context.market_switched = False #get Real Yield data #fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1880.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1945.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') schedule_function(set_rebalance_day,date_rules.month_start(days_offset=10), time_rules.market_close(minutes=5)) schedule_function(rebalance_sell,date_rules.month_start(days_offset=11), time_rules.market_open(minutes=240)) schedule_function(rebalance_buy,date_rules.month_start(days_offset=13), time_rules.market_open(minutes=240)) #schedule_function(info, date_rules.every_day(), time_rules.market_close()) c = context c.max_lvrg = 0 c.risk_hi = 0 c.date_prv = '' c.cash_low = c.portfolio.starting_cash c.date_end = str(get_environment('end').date()) print '{} to {} {} {}'.format(str(get_datetime().date()), c.date_end, int(c.cash_low), get_environment('data_frequency')) def set_rebalance_day(context,data): today = get_datetime() if today.month == context.rebalance_month: context.is_rebalance_day = True else: context.is_rebalance_day = False def rebalance_sell(context,data): if context.is_rebalance_day or (context.market_expensive and context.market_switched) : for s in context.portfolio.positions: if s not in data: continue order_target_percent(s,0) if context.market_switched: context.market_switched = False def rebalance_buy(context,data): if not(context.market_expensive) and (context.is_rebalance_day or context.market_switched): for s in context.longs: if s in data and s.symbol != 'NP': if context.base_weight < 0: log.warn('context.base_weight neg at ' + str(context.base_weight)) order_target_percent(s,context.base_weight) context.is_rebalance_day = False if context.market_switched: context.market_switched = False def before_trading_start(context,data): if context.is_rebalance_day or context.market_switched: getstocks(context) update_universe(context.longs) def getstocks(context): #103 = Financials, 207 = Utilities, 104 = Real Estate excluded_sectors = [103, 207] sector_code = fundamentals.asset_classification.morningstar_sector_code fundamental_df = get_fundamentals( query( sector_code, fundamentals.valuation.market_cap, fundamentals.valuation.enterprise_value, fundamentals.cash_flow_statement.capital_expenditure, fundamentals.operation_ratios.roic, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda, fundamentals.balance_sheet.total_assets, fundamentals.valuation_ratios.ev_to_ebitda ) .filter(fundamentals.share_class_reference.is_primary_share == True) .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK") .filter(fundamentals.share_class_reference.is_depositary_receipt == False) .filter(~sector_code.in_(excluded_sectors)) .filter(fundamentals.valuation.market_cap > context.lower_mktcap ) .filter(fundamentals.valuation_ratios.ev_to_ebitda > 0) #.filter(fundamentals.valuation.market_cap < context.upper_mktcap ) .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc()) .limit(context.limit) ) context.longs = fundamental_df.columns[:context.num_positions] def handle_data(context, data): #log.info("in handle_data") if 'Real Yield' in data and data['Real Yield']['Below 20th Percentile']: if not(context.market_expensive): context.market_switched = True context.market_expensive = True elif 'Real Yield' in data and not (data['Real Yield']['Below 20th Percentile']): if context.market_expensive: context.market_switched = True context.market_expensive = False record(leverage = context.account.leverage) #info(context, data) track_orders(context, data) def track_orders(context, data): # Log orders created or filled. if 'orders' not in context: context.orders = {} to_delete = [] for id in context.orders: o = get_order(id) sec = o.sid sym = sec.symbol if o.filled: # Filled at least some, status 1 is Filled if context.portfolio.positions[sec].amount < 0: # cover these where short occured log.info('{} now {} shares, shorted at {}'.format( sym, context.portfolio.positions[sec].amount + o.amount, context.portfolio.positions[sec].amount )) # Funky use of record, if new short, its security id record(shorted_sid_id = o.sid.sid) else: continue # skip all normal trade = 'Bot' if o.amount > 0 else 'Sold' log.info(' {} {} {} at {} id {}\n'.format( trade, o.filled, sym, data[sec].price, o.id)) to_delete.append(o.id) else: log.info(' {} {} unfilled\n'.format(o.sid.symbol, o.amount)) for sec, oo_for_sid in get_open_orders().iteritems(): # Open orders sym = sec.symbol for o in oo_for_sid: # Orders per security if context.portfolio.positions[sec].amount + o.amount < 0: # cover these where short would occur log.info('Sell {} would result in {} shares, short, currently {}'.format( sym, context.portfolio.positions[sec].amount + o.amount, context.portfolio.positions[sec].amount )) else: continue # skip all normal if o.id in to_delete: continue if o.status == 2: # Cancelled log.info(' Cancelled {} {} order\n'.format( trade, o.amount, sym, data[sec].price)) to_delete.append(o.id) elif o.id not in context.orders: # New context.orders[o.id] = 1 trade = 'Buy' if o.amount > 0 else 'Sell' if o.limit: # Limit order log.info(' {} {} {} now {} limit {}\n'.format( trade, o.amount, sym, data[sec].price, o.limit)) elif o.stop: # Stop order log.info(' {} {} {} now {} stop {}\n'.format( trade, o.amount, sym, data[sec].price, o.stop)) else: # Market order log.info(' {} {} {} at {}\n'.format( trade, o.amount, sym, data[sec].price)) for d in to_delete: del context.orders[d] def info(context, data): ''' Custom chart and/or log of profit_vs_risk returns and related information ''' # # # # # # # # # # Options # # # # # # # # # # record_max_lvrg = 1 # Maximum leverage encountered record_leverage = 0 # Leverage (context.account.leverage) record_q_return = 0 # Quantopian returns (percentage) record_pvr = 1 # Profit vs Risk returns (percentage) record_pnl = 0 # Profit-n-Loss record_shorting = 1 # Total value of any shorts record_risk = 0 # Risked, maximum cash spent or shorts in excess of cash at any time record_risk_hi = 0 # Highest risk overall record_cash = 0 # Cash available record_cash_low = 1 # Any new lowest cash level logging = 1 # Also log to the logging window conditionally (1) or not (0) log_method = 'risk_hi' # 'daily' or 'risk_hi' c = context # For brevity new_cash_low = 0 # To trigger logging in cash_low case date = str(get_datetime().date()) # To trigger logging in daily case cash = c.portfolio.cash if int(cash) < c.cash_low: # New cash low new_cash_low = 1 c.cash_low = int(cash) if record_cash_low: record(CashLow = int(c.cash_low)) pvr_rtrn = 0 # Profit vs Risk returns based on maximum spent profit_loss = 0 # Profit-n-loss shorts = 0 # Shorts value start = c.portfolio.starting_cash cash_dip = int(max(0, start - cash)) if record_cash: record(Cash = int(c.portfolio.cash)) # Cash if record_leverage: record(Lvrg = c.account.leverage) # Leverage if record_max_lvrg: if c.account.leverage > c.max_lvrg: c.max_lvrg = c.account.leverage record(MaxLv = c.max_lvrg) # Maximum leverage if record_pnl: profit_loss = c.portfolio.pnl record(PnL = profit_loss) # "Profit and Loss" in dollars for p in c.portfolio.positions: shrs = c.portfolio.positions[p].amount if shrs < 0: shorts += int(abs(shrs * data[p].price)) if record_shorting: record(Shorts = shorts) # Shorts value as a positve risk = int(max(cash_dip, shorts)) if record_risk: record(Risk = risk) # Amount in play, maximum of shorts or cash used new_risk_hi = 0 if risk > c.risk_hi: c.risk_hi = risk new_risk_hi = 1 if record_risk_hi: record(RiskHi = c.risk_hi) # Highest risk overall if record_pvr: # Profit_vs_Risk returns based on max amount actually spent (risk high) if c.risk_hi != 0: # Avoid zero-divide pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.risk_hi record(PvR = pvr_rtrn) # Profit_vs_Risk returns q_rtrn = 100 * (c.portfolio.portfolio_value - start) / start if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve from pytz import timezone if logging: if log_method == 'risk_hi' and new_risk_hi \ or log_method == 'daily' and c.date_prv != date \ or c.date_end == date \ or new_cash_low: qret = 'QRet ' + '%.1f' % q_rtrn mxlv = 'MaxLv ' + '%.1f' % c.max_lvrg if record_max_lvrg else '' pvr = 'PvR ' + '%.1f' % pvr_rtrn if record_pvr else '' pnl = 'PnL ' + '%.0f' % profit_loss if record_pnl else '' csh = 'Cash ' + '%.0f' % cash if record_cash else '' csh_lw = 'CshLw ' + '%.0f' % c.cash_low if record_cash_low else '' shrt = 'Shrt ' + '%.0f' % shorts if record_shorting else '' risk = 'Risk ' + '%.0f' % risk if record_risk else '' rsk_hi = 'RskHi ' + '%.0f' % c.risk_hi if record_risk_hi else '' minute = get_datetime().astimezone(timezone('US/Eastern')).time().minute log.info('{} {} {} {} {} {} {} {} {} {}'.format( minute, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, risk, rsk_hi)) if c.date_end == date: # Log on last day, like cash 125199 portfolio 126890 log.info('cash {} portfolio {}'.format( int(cash), int(c.portfolio.portfolio_value))) c.date_prv = date There was a runtime error. Hi Gary, Thanks a lot for taking the time to look into this. Taking Simon’s comment into account I added this line, if s not in get_open_orders(): to both the buy and sell functions, this removes the inadvertent short in the backtest, without changing the performance results. 22 Loading... Backtest from to with initial capital 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 ''' Implemented tactical asset allocation algorithm for value strategies discussed in this post from Alpha Architect, http://blog.alphaarchitect.com/2015/07/21/eureka-a-valuation-based-asset-allocation-strategy-that-might-work/ To compute the Real Yield Spread Metric I took the Shiller CAPE ratio data directly from Robert Shiller's website. I computed an annualized inflation rate directly from the CPI data contained in the spreadsheet and used an expanding window for the percentile calculations as well as a 1-month lag on the inflation rate for the Real Yield calculation. ''' from datetime import timedelta import pandas as pd import numpy as np def initialize(context): context.lower_mktcap = 500e6 context.limit = 500 context.num_positions = 100 context.base_weight = 1.0/context.num_positions context.rebalance_month = 6 #June context.is_rebalance_day = False context.shy = symbol('IEF') context.market_expensive = False context.market_switched = False #get Real Yield data #fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1880.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1945.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') schedule_function(set_rebalance_day,date_rules.month_start(days_offset=10), time_rules.market_close(minutes=5)) schedule_function(rebalance_sell,date_rules.month_start(days_offset=11), time_rules.market_open(minutes=240)) schedule_function(rebalance_buy,date_rules.month_start(days_offset=13), time_rules.market_open(minutes=240)) #schedule_function(info, date_rules.every_day(), time_rules.market_close()) c = context c.max_lvrg = 0 c.risk_hi = 0 c.date_prv = '' c.cash_low = c.portfolio.starting_cash c.date_end = str(get_environment('end').date()) print '{} to {} {} {}'.format(str(get_datetime().date()), c.date_end, int(c.cash_low), get_environment('data_frequency')) def set_rebalance_day(context,data): today = get_datetime() if today.month == context.rebalance_month: context.is_rebalance_day = True else: context.is_rebalance_day = False def rebalance_sell(context,data): if context.is_rebalance_day or (context.market_expensive and context.market_switched) : for s in context.portfolio.positions: if s not in get_open_orders(): order_target_percent(s,0) if context.market_switched: context.market_switched = False def rebalance_buy(context,data): if not(context.market_expensive) and (context.is_rebalance_day or context.market_switched): for s in context.longs: if s in data and s.symbol != 'NP': if s not in get_open_orders(): order_target_percent(s,context.base_weight) context.is_rebalance_day = False if context.market_switched: context.market_switched = False def before_trading_start(context,data): if context.is_rebalance_day or context.market_switched: getstocks(context) update_universe(context.longs) def getstocks(context): #103 = Financials, 207 = Utilities, 104 = Real Estate excluded_sectors = [103, 207] sector_code = fundamentals.asset_classification.morningstar_sector_code fundamental_df = get_fundamentals( query( sector_code, fundamentals.valuation.market_cap, fundamentals.valuation.enterprise_value, fundamentals.cash_flow_statement.capital_expenditure, fundamentals.operation_ratios.roic, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda, fundamentals.balance_sheet.total_assets, fundamentals.valuation_ratios.ev_to_ebitda ) .filter(fundamentals.share_class_reference.is_primary_share == True) .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK") .filter(fundamentals.share_class_reference.is_depositary_receipt == False) .filter(~sector_code.in_(excluded_sectors)) .filter(fundamentals.valuation.market_cap > context.lower_mktcap ) .filter(fundamentals.valuation_ratios.ev_to_ebitda > 0) #.filter(fundamentals.valuation.market_cap < context.upper_mktcap ) .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc()) .limit(context.limit) ) context.longs = fundamental_df.columns[:context.num_positions] def handle_data(context, data): #log.info("in handle_data") if 'Real Yield' in data and data['Real Yield']['Below 20th Percentile']: if not(context.market_expensive): context.market_switched = True context.market_expensive = True elif 'Real Yield' in data and not (data['Real Yield']['Below 20th Percentile']): if context.market_expensive: context.market_switched = True context.market_expensive = False record(leverage = context.account.leverage) info(context, data) def info(context, data): ''' Custom chart and/or log of profit_vs_risk returns and related information ''' # # # # # # # # # # Options # # # # # # # # # # record_max_lvrg = 1 # Maximum leverage encountered record_leverage = 0 # Leverage (context.account.leverage) record_q_return = 0 # Quantopian returns (percentage) record_pvr = 0 # Profit vs Risk returns (percentage) record_pnl = 0 # Profit-n-Loss record_shorting = 1 # Total value of any shorts record_risk = 0 # Risked, maximum cash spent or shorts in excess of cash at any time record_risk_hi = 0 # Highest risk overall record_cash = 0 # Cash available record_cash_low = 1 # Any new lowest cash level logging = 1 # Also log to the logging window conditionally (1) or not (0) log_method = 'risk_hi' # 'daily' or 'risk_hi' c = context # For brevity new_cash_low = 0 # To trigger logging in cash_low case date = str(get_datetime().date()) # To trigger logging in daily case cash = c.portfolio.cash if int(cash) < c.cash_low: # New cash low new_cash_low = 1 c.cash_low = int(cash) if record_cash_low: record(CashLow = int(c.cash_low)) pvr_rtrn = 0 # Profit vs Risk returns based on maximum spent profit_loss = 0 # Profit-n-loss shorts = 0 # Shorts value start = c.portfolio.starting_cash cash_dip = int(max(0, start - cash)) if record_cash: record(Cash = int(c.portfolio.cash)) # Cash if record_leverage: record(Lvrg = c.account.leverage) # Leverage if record_max_lvrg: if c.account.leverage > c.max_lvrg: c.max_lvrg = c.account.leverage record(MaxLv = c.max_lvrg) # Maximum leverage if record_pnl: profit_loss = c.portfolio.pnl record(PnL = profit_loss) # "Profit and Loss" in dollars for p in c.portfolio.positions: shrs = c.portfolio.positions[p].amount if shrs < 0: shorts += int(abs(shrs * data[p].price)) if record_shorting: record(Shorts = shorts) # Shorts value as a positve risk = int(max(cash_dip, shorts)) if record_risk: record(Risk = risk) # Amount in play, maximum of shorts or cash used new_risk_hi = 0 if risk > c.risk_hi: c.risk_hi = risk new_risk_hi = 1 if record_risk_hi: record(RiskHi = c.risk_hi) # Highest risk overall if record_pvr: # Profit_vs_Risk returns based on max amount actually spent (risk high) if c.risk_hi != 0: # Avoid zero-divide pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.risk_hi record(PvR = pvr_rtrn) # Profit_vs_Risk returns q_rtrn = 100 * (c.portfolio.portfolio_value - start) / start if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve from pytz import timezone if logging: if log_method == 'risk_hi' and new_risk_hi \ or log_method == 'daily' and c.date_prv != date \ or c.date_end == date \ or new_cash_low: qret = 'QRet ' + '%.1f' % q_rtrn mxlv = 'MaxLv ' + '%.1f' % c.max_lvrg if record_max_lvrg else '' pvr = 'PvR ' + '%.1f' % pvr_rtrn if record_pvr else '' pnl = 'PnL ' + '%.0f' % profit_loss if record_pnl else '' csh = 'Cash ' + '%.0f' % cash if record_cash else '' csh_lw = 'CshLw ' + '%.0f' % c.cash_low if record_cash_low else '' shrt = 'Shrt ' + '%.0f' % shorts if record_shorting else '' risk = 'Risk ' + '%.0f' % risk if record_risk else '' rsk_hi = 'RskHi ' + '%.0f' % c.risk_hi if record_risk_hi else '' minute = get_datetime().astimezone(timezone('US/Eastern')).time().minute log.info('{} {} {} {} {} {} {} {} {} {}'.format( minute, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, risk, rsk_hi)) if c.date_end == date: # Log on last day, like cash 125199 portfolio 126890 log.info('cash {} portfolio {}'.format( int(cash), int(c.portfolio.portfolio_value))) c.date_prv = date There was a runtime error. Next, I took a look at the leverage/cash_low issue that you mentioned. The leverage in the algorithm is the result of a number of zombie trades. It seems when a company gets bought the Q backtester is not able to execute the sell order on the name, so it just stays in the list of open orders. I added some code to explicitly exclude those companies, you can see in the associated backtest that the leverage stays at or below one and the cash_low is above 0 (not sure why it dips below right at the end, will need to have a look at that at some point). Interestingly, the companies that get bought out are generally profitable trades, so the Acquirer’s multiple algorithm is doing what its supposed to, i.e. finding companies that are so cheap that they are likely to get bought. Not sure how to appropriately handle this case in Q, let me know if you have some ideas. Regards, Mark 22 Loading... Backtest from to with initial capital 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 ''' Implemented tactical asset allocation algorithm for value strategies discussed in this post from Alpha Architect, http://blog.alphaarchitect.com/2015/07/21/eureka-a-valuation-based-asset-allocation-strategy-that-might-work/ To compute the Real Yield Spread Metric I took the Shiller CAPE ratio data directly from Robert Shiller's website. I computed an annualized inflation rate directly from the CPI data contained in the spreadsheet and used an expanding window for the percentile calculations as well as a 1-month lag on the inflation rate for the Real Yield calculation. ''' from datetime import timedelta import pandas as pd import numpy as np def initialize(context): context.lower_mktcap = 500e6 context.limit = 500 context.num_positions = 100 context.base_weight = 1.0/context.num_positions context.rebalance_month = 6 #June context.is_rebalance_day = False context.shy = symbol('IEF') context.market_expensive = False context.market_switched = False context.no_buy_list = [sid(27653),symbol('DCEL'),symbol('TRA'),symbol('MVL'),symbol('AMSY'),sid(7448),symbol('OMM'),sid(42021),symbol('KMR'),symbol('CRDN'),symbol('GYMB'),symbol('PD'),sid(17104),sid(209),symbol('AWIN'),symbol('CVH'),sid(25948),symbol('IAIA'),symbol('PPD'),symbol('WINN'),symbol('SHS'),symbol('GW'),symbol('KG'),sid(764),symbol('HLYW')] #get Real Yield data fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1880.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') #fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1945.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield') schedule_function(set_rebalance_day,date_rules.month_start(days_offset=10), time_rules.market_close(minutes=5)) schedule_function(rebalance_sell,date_rules.month_start(days_offset=11), time_rules.market_open(minutes=240)) schedule_function(rebalance_buy,date_rules.month_start(days_offset=13), time_rules.market_open(minutes=240)) #schedule_function(info, date_rules.every_day(), time_rules.market_close()) c = context c.max_lvrg = 0 c.risk_hi = 0 c.date_prv = '' c.cash_low = c.portfolio.starting_cash c.date_end = str(get_environment('end').date()) print '{} to {} {} {}'.format(str(get_datetime().date()), c.date_end, int(c.cash_low), get_environment('data_frequency')) def set_rebalance_day(context,data): today = get_datetime() if today.month == context.rebalance_month: context.is_rebalance_day = True else: context.is_rebalance_day = False def rebalance_sell(context,data): if context.is_rebalance_day or (context.market_expensive and context.market_switched) : for s in context.portfolio.positions: if s not in get_open_orders(): order_target_percent(s,0) if context.market_switched: context.market_switched = False def rebalance_buy(context,data): if not(context.market_expensive) and (context.is_rebalance_day or context.market_switched): for s in context.longs: if s in data and s.symbol != 'NP': if (s not in get_open_orders()) and (s not in context.no_buy_list): order_target_percent(s,context.base_weight) context.is_rebalance_day = False if context.market_switched: context.market_switched = False def before_trading_start(context,data): if context.is_rebalance_day or context.market_switched: getstocks(context) update_universe(context.longs) def getstocks(context): #103 = Financials, 207 = Utilities, 104 = Real Estate excluded_sectors = [103, 207] sector_code = fundamentals.asset_classification.morningstar_sector_code fundamental_df = get_fundamentals( query( sector_code, fundamentals.valuation.market_cap, fundamentals.valuation.enterprise_value, fundamentals.cash_flow_statement.capital_expenditure, fundamentals.operation_ratios.roic, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda, fundamentals.balance_sheet.total_assets, fundamentals.valuation_ratios.ev_to_ebitda ) .filter(fundamentals.share_class_reference.is_primary_share == True) .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK") .filter(fundamentals.share_class_reference.is_depositary_receipt == False) .filter(~sector_code.in_(excluded_sectors)) .filter(fundamentals.valuation.market_cap > context.lower_mktcap ) .filter(fundamentals.valuation_ratios.ev_to_ebitda > 0) #.filter(fundamentals.valuation.market_cap < context.upper_mktcap ) .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc()) .limit(context.limit) ) context.longs = fundamental_df.columns[:context.num_positions] def handle_data(context, data): #log.info("in handle_data") if 'Real Yield' in data and data['Real Yield']['Below 20th Percentile']: if not(context.market_expensive): context.market_switched = True context.market_expensive = True elif 'Real Yield' in data and not (data['Real Yield']['Below 20th Percentile']): if context.market_expensive: context.market_switched = True context.market_expensive = False record(leverage = context.account.leverage) info(context, data) def info(context, data): ''' Custom chart and/or log of profit_vs_risk returns and related information ''' # # # # # # # # # # Options # # # # # # # # # # record_max_lvrg = 1 # Maximum leverage encountered record_leverage = 0 # Leverage (context.account.leverage) record_q_return = 0 # Quantopian returns (percentage) record_pvr = 0 # Profit vs Risk returns (percentage) record_pnl = 0 # Profit-n-Loss record_shorting = 1 # Total value of any shorts record_risk = 0 # Risked, maximum cash spent or shorts in excess of cash at any time record_risk_hi = 0 # Highest risk overall record_cash = 0 # Cash available record_cash_low = 1 # Any new lowest cash level logging = 1 # Also log to the logging window conditionally (1) or not (0) log_method = 'risk_hi' # 'daily' or 'risk_hi' c = context # For brevity new_cash_low = 0 # To trigger logging in cash_low case date = str(get_datetime().date()) # To trigger logging in daily case cash = c.portfolio.cash if int(cash) < c.cash_low: # New cash low new_cash_low = 1 c.cash_low = int(cash) if record_cash_low: record(CashLow = int(c.cash_low)) pvr_rtrn = 0 # Profit vs Risk returns based on maximum spent profit_loss = 0 # Profit-n-loss shorts = 0 # Shorts value start = c.portfolio.starting_cash cash_dip = int(max(0, start - cash)) if record_cash: record(Cash = int(c.portfolio.cash)) # Cash if record_leverage: record(Lvrg = c.account.leverage) # Leverage if record_max_lvrg: if c.account.leverage > c.max_lvrg: c.max_lvrg = c.account.leverage record(MaxLv = c.max_lvrg) # Maximum leverage if record_pnl: profit_loss = c.portfolio.pnl record(PnL = profit_loss) # "Profit and Loss" in dollars for p in c.portfolio.positions: shrs = c.portfolio.positions[p].amount if shrs < 0: shorts += int(abs(shrs * data[p].price)) if record_shorting: record(Shorts = shorts) # Shorts value as a positve risk = int(max(cash_dip, shorts)) if record_risk: record(Risk = risk) # Amount in play, maximum of shorts or cash used new_risk_hi = 0 if risk > c.risk_hi: c.risk_hi = risk new_risk_hi = 1 if record_risk_hi: record(RiskHi = c.risk_hi) # Highest risk overall if record_pvr: # Profit_vs_Risk returns based on max amount actually spent (risk high) if c.risk_hi != 0: # Avoid zero-divide pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.risk_hi record(PvR = pvr_rtrn) # Profit_vs_Risk returns q_rtrn = 100 * (c.portfolio.portfolio_value - start) / start if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve from pytz import timezone if logging: if log_method == 'risk_hi' and new_risk_hi \ or log_method == 'daily' and c.date_prv != date \ or c.date_end == date \ or new_cash_low: qret = 'QRet ' + '%.1f' % q_rtrn mxlv = 'MaxLv ' + '%.1f' % c.max_lvrg if record_max_lvrg else '' pvr = 'PvR ' + '%.1f' % pvr_rtrn if record_pvr else '' pnl = 'PnL ' + '%.0f' % profit_loss if record_pnl else '' csh = 'Cash ' + '%.0f' % cash if record_cash else '' csh_lw = 'CshLw ' + '%.0f' % c.cash_low if record_cash_low else '' shrt = 'Shrt ' + '%.0f' % shorts if record_shorting else '' risk = 'Risk ' + '%.0f' % risk if record_risk else '' rsk_hi = 'RskHi ' + '%.0f' % c.risk_hi if record_risk_hi else '' minute = get_datetime().astimezone(timezone('US/Eastern')).time().minute log.info('{} {} {} {} {} {} {} {} {} {}'.format( minute, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, risk, rsk_hi)) if c.date_end == date: # Log on last day, like cash 125199 portfolio 126890 log.info('cash {} portfolio {}'.format( int(cash), int(c.portfolio.portfolio_value))) c.date_prv = date There was a runtime error. You increased profits on each dollar put to work by 27%, congrats. (The last algo might appear to be worse, it is better on a per-dollar-utilized basis, PvR, Profit vs. Risk). A couple of the stocks on your no-buy list show up in google finance with a price of 0.0000 right now even tho they've been trading around 10 and 50, LCC and CVH. Consider maybe skipping anything below$2 or $5 or some threshold, keep a count of the number of times each in a dictionary (that it happens), and if the count reaches some ceiling, append to no_buy_list, starting out empty. Hi Gary, Again thank you for taking a look at this. The thing is that LCC and CVH do not go to zero they get bought out, this is a situation that the Acquirer’s multiple actually seeks out so I’m not sure it makes sense to exclude these trades. I posted the previous backtest to illustrate that these kinds of trades were the cause of the extra leverage. I added the trades back in and added some logging to the latest backtest to list all open orders at the end of the backtest period. It shows in general that they are profitable trades. For example LCC was bought out by American Airlines (AAL) in 2013, the last price it traded at was$22.35 which is above the \$16.36 that the algorithm paid for it. In reality you would be given shares in AAL and be able to sell them (presumably at a similar profit), however this is not modelled in the backtest. So, for the most part the money tied up in the zombie trades could be converted to cash and made available to the algorithm. Your ‘pvr’ metric doesn’t account for this which is throwing off the measure to some degree.

22
Loading...
Backtest from to with initial capital
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
'''
Implemented tactical asset allocation algorithm for value strategies discussed in this post from Alpha Architect,
http://blog.alphaarchitect.com/2015/07/21/eureka-a-valuation-based-asset-allocation-strategy-that-might-work/

To compute the Real Yield Spread Metric I took the Shiller CAPE ratio data directly from Robert Shiller's website. I computed an annualized inflation rate directly from the CPI data contained in the spreadsheet and used an expanding window for the percentile calculations as well as a 1-month lag on the inflation rate for the Real Yield calculation.

'''

from datetime import timedelta
import pandas as pd
import numpy as np

def initialize(context):
context.lower_mktcap = 500e6
context.limit = 500
context.num_positions = 100
context.base_weight = 1.0/context.num_positions
context.rebalance_month = 6 #June
context.is_rebalance_day = False
context.shy = symbol('IEF')
context.market_expensive = False
context.market_switched = False
context.lastYear = 2015
context.lastMonth = 11
#context.no_buy_list = [sid(27653),symbol('DCEL'),symbol('TRA'),symbol('MVL'),symbol('AMSY'),sid(7448),symbol('OMM'),sid(42021),symbol('KMR'),symbol('CRDN'),symbol('GYMB'),symbol('PD'),sid(17104),sid(209),symbol('AWIN'),symbol('CVH'),sid(25948),symbol('IAIA'),symbol('PPD'),symbol('WINN'),symbol('SHS'),symbol('GW'),symbol('KG'),sid(764),symbol('HLYW')]

#get Real Yield data
fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1880.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield')
#fetch_csv( 'https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1945.csv',date_column='Date',date_format='%m/%d/%y',symbol='Real Yield')

schedule_function(set_rebalance_day,date_rules.month_start(days_offset=10),
time_rules.market_close(minutes=5))
schedule_function(rebalance_sell,date_rules.month_start(days_offset=11),
time_rules.market_open(minutes=240))
schedule_function(rebalance_buy,date_rules.month_start(days_offset=13),
time_rules.market_open(minutes=240))
#schedule_function(info, date_rules.every_day(), time_rules.market_close())
schedule_function(check_positions,date_rules.month_start(days_offset=14),
time_rules.market_open(minutes=240))

c = context
c.max_lvrg = 0
c.risk_hi  = 0
c.date_prv = ''
c.cash_low = c.portfolio.starting_cash
c.date_end = str(get_environment('end').date())
print '{} to {}  {}  {}'.format(str(get_datetime().date()), c.date_end,
int(c.cash_low), get_environment('data_frequency'))

def check_positions(context,data):
today = get_datetime()

if today.year == context.lastYear and today.month == context.lastMonth:
#log current account
log.info(" account total positions value: " + str(context.account.total_positions_value))
log.info(" account leverage: " + str(context.account.leverage))
log.info(" portfolio cash: " + str(context.portfolio.cash))
log.info(" portfolio value: " + str(context.portfolio.portfolio_value))
log.info(" portfolio positions value: " + str(context.portfolio.positions_value))
log.info(" portfolio returns: " + str(context.portfolio.returns))

open_order_sids = get_open_orders()

#log open orders
for s in context.portfolio.positions:
if s in open_order_sids:
shrs = context.portfolio.positions[s].amount
cost = context.portfolio.positions[s].cost_basis
current = data[s].price
log.info("for s: " + str(s.symbol) )
log.info(" shares: " + str(shrs))
log.info(" cost: " + str(cost))
log.info(" current: " + str(current))
log.info(" last day traded: " + str(s.end_date))

def set_rebalance_day(context,data):
today = get_datetime()

if today.month == context.rebalance_month:
context.is_rebalance_day = True
else:
context.is_rebalance_day = False

def rebalance_sell(context,data):
if context.is_rebalance_day or (context.market_expensive and context.market_switched) :
for s in context.portfolio.positions:
if s not in get_open_orders():
order_target_percent(s,0)

if context.market_switched:
context.market_switched = False

def rebalance_buy(context,data):
if not(context.market_expensive) and (context.is_rebalance_day or  context.market_switched):
for s in context.longs:
if s in data and s.symbol != 'NP':
if (s not in get_open_orders()):# and (s not in context.no_buy_list):
order_target_percent(s,context.base_weight)

context.is_rebalance_day = False
if context.market_switched:
context.market_switched = False

def before_trading_start(context,data):
if context.is_rebalance_day or  context.market_switched:
getstocks(context)
update_universe(context.longs)

#make sure open order sids in data for loggin in check_positions
today = get_datetime()
if today.year == context.lastYear and today.month == context.lastMonth:
update_universe(list(get_open_orders().keys()))

def getstocks(context):
#103 = Financials, 207 = Utilities, 104 = Real Estate
excluded_sectors = [103, 207]
sector_code = fundamentals.asset_classification.morningstar_sector_code
fundamental_df = get_fundamentals(
query(
sector_code,
fundamentals.valuation.market_cap,
fundamentals.valuation.enterprise_value,
fundamentals.cash_flow_statement.capital_expenditure,
fundamentals.operation_ratios.roic,
fundamentals.income_statement.ebit,
fundamentals.income_statement.ebitda,
fundamentals.balance_sheet.total_assets,
fundamentals.valuation_ratios.ev_to_ebitda
)
.filter(fundamentals.share_class_reference.is_primary_share == True)
.filter(fundamentals.company_reference.primary_exchange_id != "OTCPK")
.filter(fundamentals.share_class_reference.is_depositary_receipt == False)
.filter(~sector_code.in_(excluded_sectors))
.filter(fundamentals.valuation.market_cap > context.lower_mktcap )
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
#.filter(fundamentals.valuation.market_cap < context.upper_mktcap )
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
.limit(context.limit)
)

context.longs = fundamental_df.columns[:context.num_positions]

def handle_data(context, data):
#log.info("in handle_data")

if 'Real Yield' in data and data['Real Yield']['Below 20th Percentile']:
if not(context.market_expensive):
context.market_switched = True
context.market_expensive = True
elif 'Real Yield' in data and not (data['Real Yield']['Below 20th Percentile']):
if context.market_expensive:
context.market_switched = True
context.market_expensive = False

record(leverage = context.account.leverage)
info(context, data)

def info(context, data):
''' Custom chart and/or log of profit_vs_risk returns and related information
'''
# # # # # # # # # #  Options  # # # # # # # # # #
record_max_lvrg = 1          # Maximum leverage encountered
record_leverage = 0          # Leverage (context.account.leverage)
record_q_return = 0          # Quantopian returns (percentage)
record_pvr      = 1          # Profit vs Risk returns (percentage)
record_pnl      = 0          # Profit-n-Loss
record_shorting = 0          # Total value of any shorts
record_risk     = 0          # Risked, maximum cash spent or shorts in excess of cash at any time
record_risk_hi  = 0          # Highest risk overall
record_cash     = 0          # Cash available
record_cash_low = 1          # Any new lowest cash level
logging         = 1          # Also log to the logging window conditionally (1) or not (0)
log_method      = 'risk_hi'  # 'daily' or 'risk_hi'

c = context                          # For brevity
new_cash_low = 0                     # To trigger logging in cash_low case
date = str(get_datetime().date())    # To trigger logging in daily case
cash = c.portfolio.cash

if int(cash) < c.cash_low:    # New cash low
new_cash_low = 1
c.cash_low   = int(cash)
if record_cash_low:
record(CashLow = int(c.cash_low))

pvr_rtrn      = 0        # Profit vs Risk returns based on maximum spent
profit_loss   = 0        # Profit-n-loss
shorts        = 0        # Shorts value
start         = c.portfolio.starting_cash
cash_dip      = int(max(0, start - cash))

if record_cash:
record(Cash = int(c.portfolio.cash))  # Cash

if record_leverage:
record(Lvrg = c.account.leverage)     # Leverage

if record_max_lvrg:
if c.account.leverage > c.max_lvrg:
c.max_lvrg = c.account.leverage
record(MaxLv = c.max_lvrg)        # Maximum leverage

if record_pnl:
profit_loss = c.portfolio.pnl
record(PnL = profit_loss)             # "Profit and Loss" in dollars

for p in c.portfolio.positions:
shrs = c.portfolio.positions[p].amount
if shrs < 0:
shorts += int(abs(shrs * data[p].price))

if record_shorting:
record(Shorts = shorts)               # Shorts value as a positve

risk = int(max(cash_dip, shorts))
if record_risk:
record(Risk = risk)                   # Amount in play, maximum of shorts or cash used

new_risk_hi = 0
if risk > c.risk_hi:
c.risk_hi = risk
new_risk_hi = 1

if record_risk_hi:
record(RiskHi = c.risk_hi)       # Highest risk overall

if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)
if c.risk_hi != 0:     # Avoid zero-divide
pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.risk_hi
record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

q_rtrn = 100 * (c.portfolio.portfolio_value - start) / start
if record_q_return:
record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

from pytz import timezone
if logging:
if log_method == 'risk_hi' and new_risk_hi \
or log_method == 'daily' and c.date_prv != date \
or c.date_end == date \
or new_cash_low:
qret   = 'QRet '    + '%.1f' % q_rtrn
mxlv   = 'MaxLv '   + '%.1f' % c.max_lvrg   if record_max_lvrg else ''
pvr    = 'PvR '     + '%.1f' % pvr_rtrn     if record_pvr      else ''
pnl    = 'PnL '     + '%.0f' % profit_loss  if record_pnl      else ''
csh    = 'Cash '    + '%.0f' % cash         if record_cash     else ''
csh_lw = 'CshLw '   + '%.0f' % c.cash_low   if record_cash_low else ''
shrt   = 'Shrt '    + '%.0f' % shorts       if record_shorting else ''
risk   = 'Risk '    + '%.0f' % risk         if record_risk     else ''
rsk_hi = 'RskHi '   + '%.0f' % c.risk_hi    if record_risk_hi  else ''
minute = get_datetime().astimezone(timezone('US/Eastern')).time().minute
log.info('{} {} {} {} {} {} {} {} {} {}'.format(
minute, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, risk, rsk_hi))

if c.date_end == date:    # Log on last day, like cash 125199  portfolio 126890
log.info('cash {}  portfolio {}'.format(
int(cash), int(c.portfolio.portfolio_value)))

c.date_prv = date

There was a runtime error.

While acquisitions are an interesting thing to tackle for sure, regarding the last line, pvr essentially operates on pnl which is not changed by open orders. Perhaps you are saying in the real world one would be able to trade some acquired shares that in the backtest environment are no longer in data , looking forward to a run-it-ready code example if humanly possible to model in a backtest, and until then, pvr is not throwing anything off, it is passive and just factually measuring what's there, basically pnl divided by most_risked i.e. amount needed in the brokerage account to achieve that result. The only difference from the standard returns calc is the denominator, transacted instead of starting capital.

Hi, I was hoping to look at this algorithmn, but it uses a .csv from dropbox which is not available. Any ideas on how to run it w/o that or where to get the source data?

Hello,
When I cloned this algorithm nd tried too runa backtest, I got this error. How do I get the dropbox content?

thanks

## -kamal

Something went wrong. Sorry for the inconvenience. Try using the built-in debugger to analyze your code. If you would like help, send us an email.
Exception: Problem reaching https://dl.dropboxusercontent.com/u/90042659/Quantopian-RealYield-1945.csv
There was a runtime error on line 26.