Berkshire Class Ratio as a Buy Point Indicator

Here, I implemented a strategy that considers, among other factors, the ratio between Berkshire Hathaway Class A stock and Class B stock as an investment signal for both Berkshire Class B stock and SPY as introduced in this SeekingAlpha article. The strategy considers several criteria as buy points for Berkshire Hathaway:

1) Class Ratio > 1510
2) Increasing VIX
3) Volume above twice the 50-day moving average volume
4) Increasing volume

The data above identify times of strong bearishness in Berkshire Hathaway stock as well as strong overall market bearishness, providing buy point indicators.

I'm interested to see what other factors might make this more robust. Anyone have other data sources to add?

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
# We implement the algorithm

from numpy import mean, std
from math import sqrt
from pytz import timezone
from datetime import datetime, timedelta
from zipline.utils.tradingcalendar import get_early_closes

# Post function for VIX data import
def rename_col_vix(df):
df = df.fillna(method='ffill')
df = df[['Settle', 'sid']]
df = df.tshift(1, freq='b')
return df

def initialize(context):

# Berkshire Class A, Berkshire Class B, SPY
context.securities = [sid(1091), sid(11100), sid(8554)]
context.class_ratio = 1
context.previous_vix = 0
context.criterion_1 = False
context.criterion_2 = False
context.criterion_3 = False
context.criterion_4 = False
context.previous_day = 0

# We look for an above average ratio between the share classes of
# Berkshire Hathaway, combined with strong market bearishness in the
# Berkshire Hathaway stock along with overall market bearishness.

# Buy Point Criteria:
# 1) Berkshire Class Ratio: >1510
# 2) Increasing VIX
# 3) Volume: Double Average Volume
# 4) Volume: Increasing

# Import VIX Front Month Future price data
fetch_csv('http://www.quandl.com/api/v1/datasets/OFDP/FUTURE_VX1.csv?&trim_start=2004-05-03&trim_end=2014-01-31&sort_order=desc',
date_column='Date',
symbol='VIX',
post_func=rename_col_vix,
date_format='%Y-%m-%d')

def handle_data(context, data):

context.criterion_1 = False
context.criterion_2 = False
context.criterion_3 = False
context.criterion_4 = False
context.criterion_5 = False

loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
date = get_datetime().date()

# Compute Class Ratio - CRITERION 1
if 'price' in data[sid(1091)] and 'price' in data[sid(11100)]:
context.class_ratio = data[sid(1091)].price/data[sid(11100)].price
if context.class_ratio > 1510:
context.criterion_1 = True

record(ClassRatio = context.class_ratio)
# Compute VIX - CRITERION 2
if 'Settle' in data['VIX']:
VIX = data['VIX']['Settle']
if VIX > context.previous_vix:
context.criterion_2 = True
if loc_dt.hour == 3 and loc_dt.minute == 59:
context.previous_vix = VIX

# Compute Volume - CRITERIA 3 & 4
if 'volume' in data[sid(1091)]:
volume_history = history(50, '1d', 'volume')
# Compute the 50 day moving average
volume_ma = sum(volume_history[sid(1091)])/50
if (volume_history[sid(1091)][-1] > 2*volume_ma):
context.criterion_3 = True
if (volume_history[sid(1091)][-1] > volume_history[sid(1091)][-2]):
context.criterion_4 = True

# Check if all the criteria are met
if (context.criterion_1 and context.criterion_2 and context.criterion_3 and context.criterion_4 and context.previous_day != loc_dt.day):
order(sid(11100), +100)
order(sid(8554), +100)
context.previous_day = loc_dt.day


This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
4 responses

I suggest you try trading this only once a day by inserting something like the following, and make a call to it in handledata. It won't be as slow. Also, you may want to use a moving average on your vix test, rather than just the different between two days. If you are investing a million, it may be nice to invest the rest while you are waiting in bonds or something. Kudos for coming up with an interesting idea.

# put in handle data

# only execute algorithm once per day
if not intradingwindow_check(context): return

# Converts all time-zones into US EST to avoid confusion
if get_datetime().day == context.yesterday:
return False

    loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
if loc_dt.hour >= 10 and loc_dt.minute >= 5:
context.yesterday = get_datetime().day
return True
else:
return False


Hey Richard,

Thanks for your thoughts. While the algorithm executes on every bar, it only places an order once per day. I updated the algorithm to use a moving average on VIX as a signal, rather than looking for VIX simply increasing over the previous day. The result is a more conservative strategy - lower returns and lower volatility.

Ryan

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
# We implement the algorithm

from numpy import mean, std
from math import sqrt
from pytz import timezone
from datetime import datetime, timedelta
import pandas

# Post function for VIX data import
def rename_col_vix(df):
df = df.fillna(method='ffill')
series = df['Settle']
vix_rolling_mean = pandas.rolling_mean(series, 5)
df['Rolling Mean'] = vix_rolling_mean
df = df[['Settle','Rolling Mean', 'sid']]
df = df.tshift(1, freq='b')
return df

def initialize(context):

# Berkshire Class A, Berkshire Class B, SPY
context.securities = [sid(1091), sid(11100), sid(8554)]
context.class_ratio = 1
context.previous_vix = 0
context.criterion_1 = False
context.criterion_2 = False
context.criterion_3 = False
context.criterion_4 = False
context.previous_day = 0
context.yesterday = 0

# We look for an above average ratio between the share classes of
# Berkshire Hathaway, combined with strong market bearishness in the
# Berkshire Hathaway stock along with overall market bearishness.

# Buy Point Criteria:
# 1) Berkshire Class Ratio: >1510
# 2) Increasing VIX
# 3) Volume: Double Average Volume
# 4) Volume: Increasing

# Import VIX Front Month Future price data
fetch_csv('http://www.quandl.com/api/v1/datasets/OFDP/FUTURE_VX1.csv?&trim_start=2004-05-03&trim_end=2014-01-31&sort_order=desc',
date_column='Date',
symbol='VIX',
post_func=rename_col_vix,
date_format='%Y-%m-%d')

def handle_data(context, data):

# only execute algorithm once per day

context.criterion_1 = False
context.criterion_2 = False
context.criterion_3 = False
context.criterion_4 = False

# only execute algorithm once per day
#if not intradingwindow_check(context): return

loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
date = get_datetime().date()

# Compute Class Ratio - CRITERION 1
if 'price' in data[sid(1091)] and 'price' in data[sid(11100)]:
context.class_ratio = data[sid(1091)].price/data[sid(11100)].price
if context.class_ratio > 1510:
context.criterion_1 = True

record(ClassRatio = context.class_ratio)
# Compute VIX - CRITERION 2
if 'Settle' in data['VIX']:
VIX = data['VIX']['Settle']
if VIX > data['VIX']['Rolling Mean']:
context.criterion_2 = True
#if loc_dt.hour == 3 and loc_dt.minute == 59:
#    context.previous_vix = VIX

# Compute Volume - CRITERIA 3 & 4
if 'volume' in data[sid(1091)]:
volume_history = history(50, '1d', 'volume')
# Compute the 50 day moving average
volume_ma = sum(volume_history[sid(1091)])/50
if (volume_history[sid(1091)][-1] > 2*volume_ma):
context.criterion_3 = True
if (volume_history[sid(1091)][-1] > volume_history[sid(1091)][-2]):
context.criterion_4 = True

# Check if all the criteria are met
if (context.criterion_1 and context.criterion_2 and context.criterion_3 and context.criterion_4 and context.previous_day != loc_dt.day):
order(sid(11100), +100)
order(sid(8554), +100)
context.previous_day = loc_dt.day


This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

The original article doesn't seem very convincing. I mean the strategy only finds 7 buy points over 14 years. Then he measures performance based on the highest point of returns. That would be great if the trade is timed perfectly. Also BRK B split in 2010. So the 1510 ratio works in hindsight.

Hey Brent - thanks for your comments. You are right about the small number of buy points; seven points in fourteen years is a small sample size to determine a systematic signal and it's not always feasible to wait for two years for a buy signal. Quantopian's engine handles stock splits (help page). The buy point strategy also outperforms a buy-and-hold strategy that holds 50% of the portfolio in Berkshire Class B stock and 50% of the portfolio in SPY (see attached back-test).

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
import numpy as np
from pytz import timezone
from datetime import datetime, timedelta
from zipline.utils.tradingcalendar import get_early_closes
import pandas

def initialize(context):
context.spy = sid(8554)
context.berkb = sid(11100)

# Will be called on every trade event for the securities you specify.
def handle_data(context, data):

order_target_percent(context.spy, 0.5)
order_target_percent(context.berkb, 0.5)

This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.