Original paper
Abstract
This paper investigates a relation between investor sentiment and performance of value stocks over growth stocks. To measure noise investors' sentiment, we use gauges: the CBOE equity put-call ratio and the market volatility (VIX) index. We find that value stocks tend to outperform growth stocks when the CBOE equity put-call ratio is relatively low or the VIX is relatively high. When the put-call ratio is relatively high or the VIX is relatively low, however, growth stocks marginally outperform or perform as well as value stocks. This finding suggests that the return premium of value stocks over growth stocks is at least partially influenced by investor sentiment. A strategy that switches equity styles on the basis of the put-call ratio seems to beat the benchmarks.
Trading rules
- Utilize CBOE equity put-call ratio and VIX index for investment sentiment measurement
- Select stocks from NYSE, AMEX, and NASDAQ
- Sort stocks into deciles based on size, then into quintiles by P/B ratio within each size group
- Focus on the top three size deciles to avoid illiquid stocks
- Create an equally weighted long portfolio of value stocks (lowest P/B quintile) from the top three size deciles (large-cap stocks)
- Create an equally weighted short portfolio of growth stocks (highest P/B quintile) from the top three size deciles (large-cap stocks)
- Hold long and short positions for three months if:
- CBOE put-call ratio is below its six-month average
- VIX index is below its six-month average
Python code
Backtrader
import backtrader as bt
import pandas as pd
class SentimentStyleRotation(bt.Strategy):
params = (
('put_call_ratio_period', 126),
('vix_period', 126),
('rebalance_period', 63),
)
def __init__(self):
self.put_call_ratio = bt.indicators.SimpleMovingAverage(
self.data0.close, period=self.params.put_call_ratio_period)
self.vix = bt.indicators.SimpleMovingAverage(
self.data1.close, period=self.params.vix_period)
self.rebalance_timer = 0
def next(self):
if self.rebalance_timer <= 0:
self.rebalance()
self.rebalance_timer = self.params.rebalance_period
else:
self.rebalance_timer -= 1
def rebalance(self):
stocks = self.get_stocks()
# Filter top three size deciles and sort by P/B ratio
stocks = stocks.nsmallest(30, 'MarketCap')
stocks['P/BQuintile'] = pd.qcut(stocks['P/B'], 5, labels=False)
# Long portfolio
long_stocks = stocks[stocks['P/BQuintile'] == 0]
long_weight = 1 / len(long_stocks)
# Short portfolio
short_stocks = stocks[stocks['P/BQuintile'] == 4]
short_weight = -1 / len(short_stocks)
if self.put_call_ratio[0] < self.put_call_ratio[-1] and self.vix[0] < self.vix[-1]:
for data in self.getdatas():
if data._name in long_stocks['Ticker'].values:
self.order_target_percent(data, target=long_weight)
elif data._name in short_stocks['Ticker'].values:
self.order_target_percent(data, target=short_weight)
else:
self.order_target_percent(data, target=0)
else:
for data in self.getdatas():
self.order_target_percent(data, target=0)
def get_stocks(self):
# Fetch stock data (NYSE, AMEX, and NASDAQ) and calculate MarketCap and P/B ratio
# Dummy data frame as an example, replace with actual data fetching method
stocks = pd.DataFrame({
'Ticker': ['AAPL', 'MSFT', 'GOOG', 'AMZN'],
'MarketCap': [1e12, 1e11, 1e10, 1e9],
'P/B': [3, 2.5, 1.2, 0.8],
})
return stocks
if __name__ == '__main__':
cerebro = bt.Cerebro()
# Add data feeds for put-call ratio and VIX index
# Replace with actual data sources for put-call ratio and VIX index
data0 = bt.feeds.YahooFinanceData(dataname='^PUT_CALL_RATIO', fromdate='2000-01-01', todate='2023-01-01')
data1 = bt.feeds.YahooFinanceData(dataname='^VIX', fromdate='2000-01-01', todate='2023-01-01')
cerebro.adddata(data0)
cerebro.adddata(data1)
# Add individual stock data feeds
# Replace with actual data sources for individual stocks
stock_data = ['AAPL', 'MSFT', 'GOOG', 'AMZN']
for ticker in stock_data:
data = bt.feeds.YahooFinanceData(dataname)