Overall Statistics
Total Trades
2510
Average Win
0.05%
Average Loss
-0.04%
Compounding Annual Return
2.504%
Drawdown
12.000%
Expectancy
0.084
Net Profit
5.070%
Sharpe Ratio
0.26
Probabilistic Sharpe Ratio
12.597%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.17
Alpha
0.026
Beta
-0.011
Annual Standard Deviation
0.097
Annual Variance
0.009
Information Ratio
-0.484
Tracking Error
0.168
Treynor Ratio
-2.295
Total Fees
$2521.68
Estimated Strategy Capacity
$7100000.00
Lowest Capacity Asset
MDC R735QTJ8XC9X
from scipy.stats import linregress
import numpy as np
import pandas as pd

class MultidimensionalVerticalCompensator(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2018, 1, 1)  # Set Start Date
        self.SetEndDate(2019, 12,31)
        self.SetCash(100000)  # Set Strategy Cash
        # self.AddEquity("SPY", Resolution.Minute)

        self.benchmark = 'SPY'
    
        #self.benchmark_obj = self.AddEquity(self.benchmark, Resolution.Hour)
        
        self.SetBenchmark(self.benchmark)
        
    
        
        # Strategy params
        self.window = 252
        self.filter_window = 200
        self.n_assets = 20
        self.mom_threshold = 60
        
        
        self.UniverseSettings.Resolution = Resolution.Daily
        
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        self.AddAlpha(ClenowAlphaModel())#ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(minutes = 20), 0.025, None))
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.lastMonth = None
        
        # Flag to use the fine filter
        self.fine = True

        
        self.current_portfolio = []

    def OnData(self, data):
        '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
            Arguments:
                data: Slice object keyed by symbol containing the stock data
        '''

        pass


    def OnSecuritiesChanged(self, changes):
        pass
 

    # def weight(self, security, atr):
    #     risk = float(self.Portfolio.TotalPortfolioValue)*0.0001
    #     weight = (((risk/atr) * float(self.Securities[security].Price))/float(self.Portfolio.TotalPortfolioValue)*100)
    #     return weight if not np.isnan(weight) else 0.0
        
            
    def gapper(self,security,period):
        
        security_data = self.History(security,period,Resolution.Daily)
        close_data = [float(data) for data in security_data['close']]
        
        return np.max(np.abs(np.diff(close_data))/close_data[:-1])>=0.15
        
    def moving_average_condition(self, security, period):

        security_data = self.History(security,period,Resolution.Daily)
        close_data = [float(data) for data in security_data['close']]
        
        return close_data[-1] > np.nanmean(close_data)

    # Get SP500
    def CoarseSelectionFunction(self, coarse):
        

        if self.Time.month == self.lastMonth:
            self.fine = False
            return Universe.Unchanged
        
        self.fine = True
        self.lastMonth = self.Time.month
            
        sortedByDollarVolume = sorted([x for x in coarse if x.HasFundamentalData and x.Volume > 0 and x.Price > 0],
                                        key= lambda x: x.DollarVolume, reverse=True)[:500]
                                        
        
        if len(sortedByDollarVolume) == 0:
            return Universe.Unchanged
            
        return [x.Symbol for x in sortedByDollarVolume]
        
        
    # Filter by momentum and trend
    def FineSelectionFunction(self, fine):
        
        if not self.fine:
            return Universe.Unchanged
        
        selection = []
        
        for asset in fine:
            slope = self._slope(asset.Symbol, self.window)
            
            
            trend_filter = self.History(asset.Symbol, self.filter_window, Resolution.Daily)
            trend_filter = trend_filter.close[-1] > trend_filter.close.mean()
            
            if trend_filter:
                selection.append(
                    (asset.Symbol, slope)    
                )
            
            
        selection = sorted(selection, key= lambda x: x[1], reverse=True)
        
        selected = [x[0] for x in selection[:self.n_assets] if x[1] > self.mom_threshold]
        teste = selected[0]
        
    
        
        filtered = []
        
        for stock in selected:
            isUpTrend = self.moving_average_condition(stock, 120) 
            isGapper = self.gapper(stock, 90)
            if isUpTrend and not isGapper:
                filtered.append(stock)
    
        return filtered
    
    def _slope(self, symbol, time_span):
        
        hist = self.History(symbol, time_span, Resolution.Daily)
        
        y = np.log(hist.close)
        x = range(len(y))
        
        slope, _, r_value, _, _ = linregress(x, y)
        
        annualized_slope = (np.power(np.exp(slope), 250) - 1) * 100
        annualized_slope = annualized_slope * (r_value ** 2)
        
        return annualized_slope
        
        
    def _atr(self, symbol, atr_window):
        
        data = self.History(symbol, self.window)
        
        h_minus_l = data.high - data.low
        h_minus_p_close = np.abs(data.high - data.close.shift(1))
        l_minus_p_close = np.abs(data.low - data.close.shift(1))
        
        
        tr = [max(x,y,z) for x,y,z in zip(h_minus_l, h_minus_p_close, l_minus_p_close)]
    
        atr = pd.Series(tr).rolling(atr_window).mean()
    
        return atr.values[-1]
        
        
        
class ClenowAlphaModel(AlphaModel):
    '''Alpha model that uses an EMA cross to create insights'''

    def __init__(self, resolution = Resolution.Daily):

        self.resolution = resolution
        self.predictionInterval = Time.Multiply(Extensions.ToTimeSpan(resolution), 20)
        self.stocks_to_trade = []
        self.removed = []
        self.lastMonth = None


    def Update(self, algorithm, data):
        '''Updates this alpha model with the latest data from the algorithm.
        This is called each time the algorithm receives data for subscribed securities
        Args:
            algorithm: The algorithm instance
            data: The new data available
        Returns:
            The new insights generated'''
            
        dt = datetime(algorithm.Time.year,algorithm.Time.month,algorithm.Time.day)
        
        same_month = self.lastMonth == algorithm.Time.month
        
        wednesday = dt.weekday() == 3
        
        trade_condition = (not same_month) and wednesday
        
        if not trade_condition:# or self.Securities[self.spy].Price < self.spy_200_sma.Current.Value:
            return []
        
        insights = []
        for symbol in self.stocks_to_trade:
            
            
            insight = Insight(
                symbol, self.predictionInterval, 
                InsightType.Price, InsightDirection.Up
            )
            
            insights.append(insight)

        self.lastMonth = algorithm.Time.month

        return insights
        

                
    def OnSecuritiesChanged(self, algorithm, changes):
        
        dt = datetime(algorithm.Time.year,algorithm.Time.month,algorithm.Time.day)
        if dt.weekday() != 3 and self.lastMonth == algorithm.Time.month:# or self.Securities[self.spy].Price < self.spy_200_sma.Current.Value:
            return 
        
        self.removed = [ x.Symbol for x in changes.RemovedSecurities ]
        for stock in self.removed:
            try:
                algorithm.current_portfolio.remove(stock)
                self.stocks_to_trade.remove(stock)
            except:
                pass
        
        self.stocks_to_trade += [stock.Symbol for stock in changes.AddedSecurities]
        
                
    def weight(self, algorithm, security, atr):
        risk = float(algorithm.Portfolio.TotalPortfolioValue)*0.0001
        weight = (((risk/atr) * float(self.Securities[security].Price))/float(self.Portfolio.TotalPortfolioValue)*100)
        return weight if not np.isnan(weight) else 0.0
        
            
    def gapper(self, algorithm, security,period):
        if not algorithm.Securities.ContainsKey(security):
            return 0
        security_data = algorithm.History(security,period,Resolution.Daily)
        close_data = [float(data) for data in security_data['close']]
        return np.max(np.abs(np.diff(close_data))/close_data[:-1])>=0.15
        
    def moving_average(self, algorithm, security, period):
        if not algorithm.Securities.ContainsKey(security):
            return 0
        security_data = algorithm.History(security,period,Resolution.Daily)
        close_data = [float(data) for data in security_data['close']]
        return np.nanmean(close_data)