Overall Statistics
Total Trades
3485
Average Win
0.12%
Average Loss
-0.14%
Compounding Annual Return
4.327%
Drawdown
36.100%
Expectancy
0.030
Net Profit
8.842%
Sharpe Ratio
0.251
Probabilistic Sharpe Ratio
9.992%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
0.88
Alpha
-0.054
Beta
1.321
Annual Standard Deviation
0.255
Annual Variance
0.065
Information Ratio
-0.128
Tracking Error
0.199
Treynor Ratio
0.048
Total Fees
$3605.71
Estimated Strategy Capacity
$6800000.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.benchmark = 'SPY'
        
        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())
        
        pcm = EqualWeightingPortfolioConstructionModel(Resolution.Daily)
        
        self.SetPortfolioConstruction(pcm)
        
        self.SetExecution(ImmediateExecutionModel())
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.lastMonth = None
        
        # Flag to use the fine filter
        self.fine = True
        
        self.current_portfolio = []

    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):
    
    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):
        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, timedelta(20), 
                InsightType.Price, InsightDirection.Up
            )
            
            insights.append(insight)

        self.lastMonth = algorithm.Time.month

        return insights
        
    def OnSecuritiesChanged(self, algorithm, changes):
        
        self.removed = [ x.Symbol for x in changes.RemovedSecurities ]
        for stock in self.removed:
            if stock in self.stocks_to_trade:
                self.stocks_to_trade.remove(stock)
        
        self.stocks_to_trade += [stock.Symbol for stock in changes.AddedSecurities]