Overall Statistics
Total Trades
5709
Average Win
0.05%
Average Loss
-0.09%
Compounding Annual Return
4.100%
Drawdown
24.700%
Expectancy
0.035
Net Profit
40.702%
Sharpe Ratio
0.305
Probabilistic Sharpe Ratio
0.738%
Loss Rate
34%
Win Rate
66%
Profit-Loss Ratio
0.57
Alpha
0.013
Beta
0.223
Annual Standard Deviation
0.114
Annual Variance
0.013
Information Ratio
-0.433
Tracking Error
0.146
Treynor Ratio
0.157
Total Fees
$115367.92
Estimated Strategy Capacity
$11000.00
Lowest Capacity Asset
LSXMB W9TJFGFY3VJ9
#region imports
from AlgorithmImports import *
#endregion
# https://quantpedia.com/Screener/Details/53
from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Data import SubscriptionDataSource
from QuantConnect.Python import PythonData
from QuantConnect.Python import PythonQuandl
from datetime import date, timedelta, datetime
from decimal import Decimal
import numpy as np
from math import floor
import json

class SentimentAndStyleRotationAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetEndDate(2018, 7, 1)
        self.SetCash(10000000)
        
        self.AddData(CBOE, "VIX", Resolution.Daily)
        self.AddData(PutCallRatio, "PutCallRatio", Resolution.Daily)
        
        self.vix_SMA_1 = SimpleMovingAverage(21)
        self.vix_SMA_6 = SimpleMovingAverage(21*6)
        self.PCRatio_SMA_1 = SimpleMovingAverage(21)
        self.PCRatio_SMA_6 = SimpleMovingAverage(21*6)
        # initialize the indicator with the history request
        PCRatio_history = self.History(["PutCallRatio"], 21*10, Resolution.Daily)
        vix_history = self.History(["VIX"], 21*10, Resolution.Daily)

        for tuple in vix_history.unstack(0).itertuples():
            self.vix_SMA_6.Update(tuple.Index, tuple[-1])
            self.vix_SMA_1.Update(tuple.Index, tuple[-1])
        for tuple in PCRatio_history.unstack(0).itertuples():
            self.PCRatio_SMA_1.Update(tuple.Index, tuple[-1])
            self.PCRatio_SMA_6.Update(tuple.Index, tuple[-1])

        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.AddEquity("SPY", Resolution.Daily)
        
        self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(0, 0), self.rebalance)
        
        self.month_start = False
        self.selection = False
        self.months = -1        
    
    def CoarseSelectionFunction(self, coarse):
        if self.month_start:
            # drop stocks which have no fundamental data or have low price
            self.filtered_coarse = [x.Symbol for x in coarse if (x.HasFundamentalData)]
            return self.filtered_coarse
        else: 
            return Universe.Unchanged

    def FineSelectionFunction(self, fine):
        if self.month_start:
            self.selection = True

            fine = [i for i in fine if i.EarningReports.BasicAverageShares.ThreeMonths>0
                                    and i.EarningReports.BasicEPS.TwelveMonths>0
                                    and i.ValuationRatios.PERatio>0
                                    and i.ValuationRatios.PBRatio>0]
            
            sotrted_market_cap = sorted(fine, key = lambda x:x.MarketCap, reverse=True)
            decile_top1 = sotrted_market_cap[:floor(len(sotrted_market_cap)/10)]
            decile_top2 = sotrted_market_cap[floor(len(sotrted_market_cap)/10):floor(len(sotrted_market_cap)*2/10)]
            decile_top3 = sotrted_market_cap[floor(len(sotrted_market_cap)*2/10):floor(len(sotrted_market_cap)*3/10)]
            sorted_PB1 = sorted(decile_top1, key = lambda x: x.ValuationRatios.PBRatio)
            sorted_PB2 = sorted(decile_top2, key = lambda x: x.ValuationRatios.PBRatio)
            sorted_PB3 = sorted(decile_top3, key = lambda x: x.ValuationRatios.PBRatio)
            # The value portfolio consists of all firms included in the quintile with the lowest P/B ratio
            PB_bottom1 = sorted_PB1[:floor(len(decile_top1)/5)]
            PB_bottom2 = sorted_PB2[:floor(len(decile_top2)/5)]
            PB_bottom3 = sorted_PB3[:floor(len(decile_top3)/5)]
            self.value_portfolio = [i.Symbol for i in PB_bottom1 + PB_bottom2 + PB_bottom3]   

            # The growth portfolio consists of all firms included in the quintile with the highest P/B ratio
            PB_top1 = sorted_PB1[-floor(len(decile_top1)/5):]
            PB_top2 = sorted_PB2[-floor(len(decile_top2)/5):]
            PB_top3 = sorted_PB3[-floor(len(decile_top3)/5):]
            self.growth_portfolio = [i.Symbol for i in PB_top1 + PB_top2 + PB_top3]   

            return self.value_portfolio + self.growth_portfolio

        else:
            return Universe.Unchanged

    def rebalance(self):
        # rebalance every three months
        self.months += 1
        if self.months%3 == 0:
            self.month_start = True
            
    def OnData(self, data):
        if not data.ContainsKey("VIX") or not data.ContainsKey("PutCallRatio") \
        or data["VIX"].Value == 0 or data["PutCallRatio"].Value == 0: 
            return

        self.vix_SMA_1.Update(self.Time, data["VIX"].Value)
        self.vix_SMA_6.Update(self.Time, data["VIX"].Value)
        self.PCRatio_SMA_1.Update(self.Time, data["PutCallRatio"].Value)
        self.PCRatio_SMA_6.Update(self.Time, data["PutCallRatio"].Value)

        if self.month_start and self.selection: 
            self.month_start = False
            self.selection = False

            stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
            for i in stocks_invested:
                if i not in self.value_portfolio+self.growth_portfolio:
                    self.Liquidate(i)

            if self.vix_SMA_1.Current.Value > self.vix_SMA_6.Current.Value:
                if self.PCRatio_SMA_1.Current.Value < self.PCRatio_SMA_6.Current.Value:
                    long_weight = 1/len(self.value_portfolio)
                    for long in self.value_portfolio:
                        self.SetHoldings(long, long_weight)
                elif self.PCRatio_SMA_1.Current.Value > self.PCRatio_SMA_6.Current.Value:
                    short_weight = 1/len(self.value_portfolio)
                    for short in self.value_portfolio:
                        self.SetHoldings(short, -short_weight)              

            else:
                long_weight = 1/len(self.value_portfolio+self.growth_portfolio)
                for long in self.value_portfolio+self.growth_portfolio:
                    self.SetHoldings(long, long_weight)
        
        
class PutCallRatio(PythonData):
    '''Cboe Equity Volume Put/Call Ratios (11-01-2006 to 2019) Custom Data Class'''
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("https://cdn.cboe.com/resources/options/volume_and_call_put_ratios/totalpc.csv", SubscriptionTransportMedium.RemoteFile)

    def Reader(self, config, line, date, isLiveMode):
        if not (line.strip() and line[0].isdigit()): return None
        index = CBOE()
        index.Symbol = config.Symbol

        try:
            # Example File Format:
            # DATE       CALL      PUT       TOTAL      P/C Ratio
            # 11/1/06    976510    623929    1600439    0.64
            data = line.split(',')
            index.Time = datetime.strptime(data[0], "%m/%d/%Y").strftime("%Y-%m-%d")
            index.Value = Decimal(data[4])

        except ValueError:
            return None

        return index