Overall Statistics
Total Orders
4869
Average Win
0.09%
Average Loss
-0.09%
Compounding Annual Return
5.329%
Drawdown
29.900%
Expectancy
0.057
Start Equity
10000000
End Equity
15546759.61
Net Profit
55.468%
Sharpe Ratio
0.273
Sortino Ratio
0.295
Probabilistic Sharpe Ratio
1.033%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
1.06
Alpha
0.008
Beta
0.316
Annual Standard Deviation
0.132
Annual Variance
0.017
Information Ratio
-0.348
Tracking Error
0.151
Treynor Ratio
0.114
Total Fees
$120995.15
Estimated Strategy Capacity
$4500000.00
Lowest Capacity Asset
MHC R735QTJ8XC9X
Portfolio Turnover
1.27%
#region imports
from AlgorithmImports import *

from decimal import Decimal
from math import floor
#endregion
# https://quantpedia.com/Screener/Details/53


class SentimentAndStyleRotationAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2010, 1, 1)
        self.set_end_date(2018, 7, 1)
        self.set_cash(10000000)

        self.set_security_initializer(BrokerageModelSecurityInitializer(
            self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        
        self.add_data(CBOE, "VIX", Resolution.DAILY)
        self.add_data(PutCallRatio, "PutCallRatio", Resolution.DAILY)
        
        self._vix_sma_1 = SimpleMovingAverage(21)
        self._vix_sma_6 = SimpleMovingAverage(21*6)
        self._pc_ratio_sma_1 = SimpleMovingAverage(21)
        self._pc_ratio_sma_6 = SimpleMovingAverage(21*6)
        # initialize the indicator with the history request
        pc_ratio_history = self.history(["PutCallRatio"], 21*10, Resolution.DAILY)
        vix_history = self.history(["VIX"], 21*10, Resolution.DAILY)

        for t, value in vix_history.loc['VIX']['value'].items():
            self._vix_sma_6.update(t, value)
            self._vix_sma_1.update(t, value)
        for t, value in pc_ratio_history.loc['PutCallRatio']['value'].items():
            self._pc_ratio_sma_1.update(t, value)
            self._pc_ratio_sma_6.update(t, value)

        self.add_universe(self._coarse_selection_function, self._fine_selection_function)
        self.add_equity("SPY", Resolution.DAILY)
        
        self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.at(0, 0), self._rebalance)
        
        self._month_start = False
        self._selection = False
        self._months = -1        
    
    def _coarse_selection_function(self, coarse):
        if self._month_start:
            # drop stocks which have no fundamental data or have low price
            return [x.symbol for x in coarse if (x.has_fundamental_data)]
        else: 
            return Universe.UNCHANGED

    def _fine_selection_function(self, fine):
        if self._month_start:
            self._selection = True

            fine = [i for i in fine if i.earning_reports.basic_average_shares.three_months>0
                                    and i.earning_reports.basic_eps.twelve_months>0
                                    and i.valuation_ratios.pe_ratio>0
                                    and i.valuation_ratios.pb_ratio>0]
            
            sorted_market_cap = sorted(fine, key=lambda x: x.market_cap, reverse=True)
            decile_top1 = sorted_market_cap[:floor(len(sorted_market_cap)/10)]
            decile_top2 = sorted_market_cap[floor(len(sorted_market_cap)/10):floor(len(sorted_market_cap)*2/10)]
            decile_top3 = sorted_market_cap[floor(len(sorted_market_cap)*2/10):floor(len(sorted_market_cap)*3/10)]
            sorted_pb1 = sorted(decile_top1, key=lambda x: x.valuation_ratios.pb_ratio)
            sorted_pb2 = sorted(decile_top2, key=lambda x: x.valuation_ratios.pb_ratio)
            sorted_pb3 = sorted(decile_top3, key=lambda x: x.valuation_ratios.pb_ratio)
            # 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 _get_tradable_assets(self, symbols):
        tradable_assets = []
        for symbol in symbols:
            security = self.securities[symbol]
            if security.price and security.is_tradable:
                tradable_assets.append(symbol)
        return tradable_assets

    def on_data(self, data):
        if (not data.contains_key("VIX") or not data.contains_key("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._pc_ratio_sma_1.update(self.time, data["PutCallRatio"].value)
        self._pc_ratio_sma_6.update(self.time, data["PutCallRatio"].value)

        if self._month_start and self._selection: 
            self._month_start = False
            self._selection = False

            self._value_portfolio = self._get_tradable_assets(self._value_portfolio)
            self._growth_portfolio = self._get_tradable_assets(self._growth_portfolio)

            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._pc_ratio_sma_1.current.value < self._pc_ratio_sma_6.current.value:
                    long_weight = 1/len(self._value_portfolio)
                    for long_ in self._value_portfolio:
                        self.set_holdings(long_, long_weight)
                elif self._pc_ratio_sma_1.current.value > self._pc_ratio_sma_6.current.value:
                    short_weight = 1/len(self._value_portfolio)
                    for short in self._value_portfolio:
                        self.set_holdings(short, -short_weight)              

            else:
                long_weight = 1/len(self._value_portfolio+self._growth_portfolio)
                for long_ in self._value_portfolio+self._growth_portfolio:
                    self.set_holdings(long_, long_weight)

       
class PutCallRatio(PythonData):

    def get_source(self, config, date, is_live_mode):
        return SubscriptionDataSource(
            "https://cdn.cboe.com/resources/options/volume_and_call_put_ratios/totalpc.csv", 
            SubscriptionTransportMedium.REMOTE_FILE)

    def reader(self, config, line, date, is_live_mode):
        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