Overall Statistics
Total Trades
2407
Average Win
1.06%
Average Loss
-0.94%
Compounding Annual Return
11.496%
Drawdown
43.500%
Expectancy
0.111
Net Profit
206.051%
Sharpe Ratio
0.533
Probabilistic Sharpe Ratio
3.859%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.14
Alpha
0.13
Beta
-0.062
Annual Standard Deviation
0.231
Annual Variance
0.054
Information Ratio
0.056
Tracking Error
0.284
Treynor Ratio
-1.988
Total Fees
$153399.65
# https://quantpedia.com/strategies/catching-falling-knife-stocks/
#
# The investment universe consists of all US based stocks after excluding the least liquid stocks (priced under $0.5) 
# and closed-end funds. Each month, the investor selects stocks that have suffered losses of 50 percent or more in the
# last 500 trading days in relation to the benchmark, which is the S&P 500 index. He/she then classifies the stocks from
# this sub-universe according to their position within the industry with regards to the Debt/Equity ratio. Stocks that have
# a Debt/Equity ratio within at least 10% of the lowest in the industry are selected for the investment portfolio. 
# The portfolio is rebalanced monthly and stocks are weighted equally.

from collections import deque
import numpy as np

class Catching_Falling_Knife_Stocks_TVIX(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetEndDate(datetime.now())
        self.SetCash(1000000)
        # Adjust the cash buffer from the default 2.5% to 10%
        # self.Settings.FreePortfolioValuePercentage = 0.1

        self.last_course_month = -1
        self.traded_this_month = False
        self.course_count = 1000
        self.period = 500
        self.SetWarmUp(self.period)
        
        self.long = []
        
        self.spy = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.data = deque(maxlen=self.period)   # SPY price
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.AddEquity("TVIX", Resolution.Daily)
        
        # Weight of portfolio without TVIX
        self.weightStocks = 0.95
        
    def CoarseSelectionFunction(self, coarse):
        if self.IsWarmingUp: return
    
        if self.last_course_month == self.Time.month:
            return Universe.Unchanged
        self.last_course_month = self.Time.month

        selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5],
            key=lambda x: x.DollarVolume, reverse=True)
        
        return [x.Symbol for x in selected[:self.course_count]]

    def FineSelectionFunction(self, fine):
        selected = [x for x in fine if x.OperationRatios.TotalDebtEquityRatio.ThreeMonths > 0]

        if len(selected) == 0: return
    
        group = {}
        returns = {}
        debt_to_equity = {}
        
        for stock in selected:
            symbol = stock.Symbol
            industry_group_code = stock.AssetClassification.MorningstarIndustryGroupCode
            if industry_group_code == 0: continue
            
            # Adding stocks in groups
            if not industry_group_code in group:
                group[industry_group_code] = []
            group[industry_group_code].append(symbol)

            # Debt to equity ratio
            debt_to_equity[symbol] = stock.OperationRatios.TotalDebtEquityRatio.ThreeMonths
            
            # Return calc
            hist = self.History([symbol], self.period, Resolution.Daily)
            if 'close' in hist.columns:
                closes = hist['close']
                if len(closes) == self.period:
                    returns[symbol] = self.Return(closes)
                #else: return
        
        if len(group) == 0: return
        if len(returns) == 0: return
        if len(debt_to_equity) == 0: return

        if len(self.data) == self.data.maxlen:
            spy_prices = [x for x in self.data]
            spy_ret = self.Return(spy_prices)
                
            for industry_code in group:
                industry_debt_to_equity_10th_percentile = np.percentile([debt_to_equity[sym] for sym in group[industry_code]], 10)
                
                # Stocks that suffered losses of 50 percent or more than s&p
                # and
                # stocks that have a Debt/Equity ratio within at least 10% of the lowest in the industry
                long = [sym for sym in group[industry_code] if sym in returns and returns[sym] <= (spy_ret - 0.5) and sym in debt_to_equity and debt_to_equity[sym] <= industry_debt_to_equity_10th_percentile]
                
                debts = [debt_to_equity[x] for x in long]
                if len(debts) != 0:
                    foo = 4
                
                for symbol in long: self.long.append(symbol)
        else: return
        self.traded_this_month = False
        
        return self.long

    def OnData(self, data):
        if self.Securities.ContainsKey(self.spy):
            price = self.Securities[self.spy].Price
            if price != 0:
                self.data.append(price)

        if self.traded_this_month == True:
            return
        
        count = len(self.long)
        if count == 0: return
        
        self.Liquidate()

        for symbol in self.long:
            self.SetHoldings(symbol, self.weightStocks * 1 / count)
        if 1 - self.weightStocks > 0:
            self.SetHoldings("TVIX", 1 - self.weightStocks)
            

        self.long.clear()
        self.traded_this_month = True
        
    def Return(self, history):
        return (history[-1] - history[0]) / history[0]