Overall Statistics
Total Orders
1950
Average Win
0.43%
Average Loss
-0.27%
Compounding Annual Return
50.230%
Drawdown
7.900%
Expectancy
0.305
Start Equity
1000000
End Equity
1502298.93
Net Profit
50.230%
Sharpe Ratio
2.011
Sortino Ratio
2.768
Probabilistic Sharpe Ratio
87.441%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.61
Alpha
0.251
Beta
0.339
Annual Standard Deviation
0.156
Annual Variance
0.024
Information Ratio
0.767
Tracking Error
0.167
Treynor Ratio
0.926
Total Fees
$30533.30
Estimated Strategy Capacity
$1600000.00
Lowest Capacity Asset
WGP VC8EGN8O5EG5
Portfolio Turnover
47.88%
Drawdown Recovery
56
from AlgorithmImports import * 


class CrossSectionalMomentum(QCAlgorithm):

    def Initialize(self):

        self.SetStartDate(2019, 1, 1)    
        self.SetEndDate(2020, 1, 1)      
        self.SetCash(1000000)
        self.SetBrokerageModel(AlphaStreamsBrokerageModel())
        
        self.UniverseSettings.Resolution = Resolution.Daily
        
        self.AddUniverse(self.CoarseSelection)
        
        self.SetAlpha(ReversalFakeoutAlpha())
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetRiskManagement(TrailingStopRiskManagementModel(0.02))
        self.SetExecution(ImmediateExecutionModel())
        
    def CoarseSelection(self, coarse):
        sortedCoarse = sorted(coarse, key=lambda c:c.DollarVolume, reverse=True)
        return [c.Symbol for c in sortedCoarse][:1000]
        


class ReversalFakeoutAlpha(AlphaModel):
    
    def __init__(self):
        self.symbols = {}
        self.lastWeek = -1
        
    def Update(self, algorithm, data):
        
        insights = []
        
        # Update daily rolling windows with daily data
        for symbol, symbolData in self.symbols.items():
            if data.ContainsKey(symbol) and data[symbol] != None:
                symbolData.dailyWindow.Add(data[symbol])
        
        # Retrieve week number from datetime
        thisWeek = algorithm.Time.isocalendar()[1]
        
        # Only rebalance once a week
        if self.lastWeek == thisWeek:
            return insights
        
        self.lastWeek = thisWeek
        
        # Retrieve all symbols that are ready
        symbols = [symbol for symbol, symbolData in self.symbols.items() if symbolData.IsReady]
        
        # Sort by annualized returns in descending order
        sortedByReturns = sorted(symbols, key=lambda s: self.symbols[s].AnnualizedReturn, reverse=True)
        
        winningSymbols = sortedByReturns[:100]
        losingSymbols = sortedByReturns[-100:]
        
        # Sort symbols by continuity and filter for symbols with recent reversals
        longContinuity = [symbol for symbol in sorted(winningSymbols, key=lambda s: self.symbols[s].Continuity, reverse=False) if self.symbols[symbol].RecentDownTrend]
        shortContinuity = [symbol for symbol in sorted(losingSymbols, key=lambda s: self.symbols[s].Continuity, reverse=True) if self.symbols[symbol].RecentUpTrend]
        
        # Create insights for the top 5 most continous symbols which are in a recent reversal
        insights += [Insight.Price(symbol, timedelta(days = 5), InsightDirection.Up) for symbol in longContinuity[:5]]
        insights += [Insight.Price(symbol, timedelta(days = 5), InsightDirection.Down) for symbol in shortContinuity[:5]]
        
        return insights
        
        
    def OnSecuritiesChanged(self, algorithm, changes):
        
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in self.symbols:
                self.symbols[symbol] = SymbolData(algorithm, symbol)
        
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.symbols:
                # Remove consolidators from algorithm and remove symbol from dictionary
                algorithm.SubscriptionManager.RemoveConsolidator(symbol, self.symbols[symbol].monthlyConsolidator)
                algorithm.SubscriptionManager.RemoveConsolidator(symbol, self.symbols[symbol].dailyConsolidator)
                self.symbols.pop(symbol, None)


class SymbolData:
    
    def __init__(self, algorithm, symbol):
        self.algorithm = algorithm
        self.symbol = symbol
        
        # Define daily and monthly rolling windows
        self.monthlyWindow = RollingWindow[TradeBar](13)
        self.dailyWindow = RollingWindow[TradeBar](280)
        
        # Define daily and monthly consolidators
        self.monthlyConsolidator = algorithm.Consolidate(symbol, Calendar.Monthly, self.OnMonthlyData)
        self.dailyConsolidator = TradeBarConsolidator(timedelta(days = 1))
        
        # Register daily consolistor to algorithm
        algorithm.SubscriptionManager.AddConsolidator(symbol, self.dailyConsolidator)
        
        # Define and register ADX indicator
        self.adxThreshold = 25
        self.adx = AverageDirectionalIndex(20)
        algorithm.RegisterIndicator(symbol, self.adx, self.dailyConsolidator)
        
                # Use historical data to warmup rolling windows, consolidators, and indicators
        history = algorithm.history(symbol, 280, Resolution.DAILY)
        if not history.empty:
            for index, row in history.iterrows():
                tbar = TradeBar()
                tbar.Symbol = symbol
                tbar.Time = index[1] if isinstance(index, tuple) else index
                tbar.Open = row.open
                tbar.High = row.high
                tbar.Low = row.low
                tbar.Close = row.close
                tbar.Volume = row.volume if hasattr(row, 'volume') else 0
                self.dailyWindow.Add(tbar)
                self.monthlyConsolidator.Update(tbar)
                self.adx.Update(tbar)
        
        
    def OnMonthlyData(self, bar):
        """Adds monthly bars to monthly rolling window"""
        self.monthlyWindow.Add(bar)
    
    @property
    def Continuity(self):
        """Returns the difference between losing days and winning days as a percentage of trading days"""
        positives = 0
        negatives = 0
        for bar in self.dailyWindow:
            dreturn = (bar.Close-bar.Open/bar.Open)
            if dreturn > 0:
                positives += 1
            else:
                negatives += 1
        return (negatives - positives)/(negatives + positives)
        
    @property
    def AnnualizedReturn(self):
        """Returns the 12 month compounded monthly return over a 13 month lookback period
        skipping the latest month."""
        returns = []
        
        for bar in self.monthlyWindow:
            monthlyReturn = (bar.Close/bar.Open)
            returns.append(monthlyReturn)
        returns.pop(0)
        return np.prod(returns) - 1
    
    
    
    
    @property
    def RecentDownTrend(self):
        """Returns true if the ADX is above a given threshold and DX+ is lower than DX-"""
        return self.adx.Current.Value > self.adxThreshold and \
        self.adx.PositiveDirectionalIndex.Current.Value < self.adx.NegativeDirectionalIndex.Current.Value
        
    @property
    def RecentUpTrend(self):
        """Returns true if the ADX is above a given threshold and DX+ is higher than DX-"""
        return self.adx.Current.Value > self.adxThreshold and \
        self.adx.PositiveDirectionalIndex.Current.Value > self.adx.NegativeDirectionalIndex.Current.Value
     
    @property
    def IsReady(self):
        """Returns true if all the rolling windows and indicators are ready: have been updated
        with enough inputs to yield valid values"""
        return self.monthlyWindow.IsReady and self.dailyWindow.IsReady and self.adx.IsReady