Overall Statistics
Total Orders
3818
Average Win
0.72%
Average Loss
-0.40%
Compounding Annual Return
149.530%
Drawdown
28.600%
Expectancy
0.255
Start Equity
100000
End Equity
411793.87
Net Profit
311.794%
Sharpe Ratio
2.099
Sortino Ratio
2.691
Probabilistic Sharpe Ratio
79.823%
Loss Rate
55%
Win Rate
45%
Profit-Loss Ratio
1.78
Alpha
1.041
Beta
0.028
Annual Standard Deviation
0.497
Annual Variance
0.247
Information Ratio
1.845
Tracking Error
0.508
Treynor Ratio
37.379
Total Fees
$21910.83
Estimated Strategy Capacity
$6500000.00
Lowest Capacity Asset
VENA XNMKHPKBFQSL
Portfolio Turnover
103.38%
from AlgorithmImports import *
# from Portfolio.MeanReversionPortfolioConstructionModel import *
from scipy.stats import norm, zscore

"""
AE Changes
1. Trading securities with significant overlap/correlation. E.g. 
 QQQ, TQQQ, SQQQ
 Added HasFundamentalData filter in CoarseSelectionFunction to only trade stocks.
2. changed number_of_symbols to be an input parameter
3. added z_model_period as input parameter

Best performance via 30 symbol  s and z model period = 50
"""

class MeanReversionPortfolioAlgorithm(QCAlgorithm):
    # number_of_symbols = 10

    def Initialize(self):
        self.SetStartDate(2022, 12, 1)  # Set Start Date to Dec 2022
        self.SetEndDate(2024, 6, 18)  # Set End Date to June 18th 2024
        self.SetCash(100000)

        self.Settings.RebalancePortfolioOnInsightChanges = True
        self.Settings.RebalancePortfolioOnSecurityChanges = False
        
        self.SetSecurityInitializer(
            lambda security: security.SetMarketPrice(
                self.GetLastKnownPrice(security)
            )
        )
        self.resolution = Resolution.Hour
        self.UniverseSettings.Resolution = self.resolution
        
        # Requesting data
        self.AddUniverse(self.CoarseSelectionFunction)
        # Feed in data for 100 trading hours before the start date
        # self.SetWarmUp(100, Resolution.Hour)

        # Get the input parameters
        self.number_of_symbols = 50 #int(self.GetParameter("number_of_symbols"))
        self.z_model_period = 30 #int(self.GetParameter("z_model_period"))

        self.AddAlpha(
            ZScoreAlphaModel(
                lookupPeriod = self.z_model_period, 
                resolution = Resolution.Daily
            )
        )
        '''
        self.SetPortfolioConstruction(
            InsightWeightingPortfolioConstructionModel(Resolution.Hour)
        )
        '''
# Test only rebalancing every day
        # self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(Resolution.Hour))
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(Resolution.Daily))
        self.Settings.MinAbsolutePortfolioTargetPercentage = \
            0.00000000000000000000001
        self.AddRiskManagement(MaximumDrawdownPercentPerSecurity())
        #self.AddRiskManagement(TrailingStopRiskManagementModel())
        self.SetExecution(ImmediateExecutionModel())

    def CoarseSelectionFunction(self, coarse):
        hasdatasymbols = list(filter(lambda x: x.HasFundamentalData, coarse))
        sortedByDollarVolume = sorted(
            hasdatasymbols, key=lambda x: x.DollarVolume, reverse=True
        )
        highest_volume_symbols = \
            [x.Symbol for x in sortedByDollarVolume[:self.number_of_symbols]]
        return highest_volume_symbols

    def OnSecuritiesChanged(self, changes):
        # liquidate removed securities
        for security in changes.RemovedSecurities:
            if security.Invested:
                self.Liquidate(security.Symbol)

class ZScoreAlphaModel(AlphaModel):
    '''Alpha model that uses an Z score to create insights'''
    def __init__(self,
                 lookupPeriod = 20,
                 resolution = Resolution.Daily, 
                 max_weight = 0.2):
        '''Initializes a new instance of the ZScoreAlphaModel class
        Args:
            lookupPeriod: Look up period of history
            resolution: Resoultion of the history'''
        self.lookupPeriod = lookupPeriod
        self.resolution = resolution
        self.predictionInterval = \
            Time.Multiply(Extensions.ToTimeSpan(resolution), lookupPeriod)
        self.symbolDataBySymbol = []

    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'''
        insights = []
        df = algorithm.History(
            self.symbolDataBySymbol, self.lookupPeriod, self.resolution
        )
        if df.empty: 
            return insights

        # Make all of them into a single time index.
        df = df.close.unstack(level=0)
        # Mean of the stocks
        df_mean = df.mean()
        # standard deviation
        df_std = df.std()
        # get last prices
        df = df.iloc[-1]
        # calculate z_score
        z_score = (df.subtract(df_mean)).divide(df_std)
        magnitude = -z_score*df_std/df
        confidence = (-z_score).apply(norm.cdf)

        algorithm.Log(f'{algorithm.Time}: Average Z Score: {z_score.mean()}')
        algorithm.Log(f'{algorithm.Time}: Max Z Score: {z_score.max()}')
        algorithm.Log(f'{algorithm.Time}: Min Z Score: {z_score.min()}')

        # weight = confidence - 1 / (magnitude + 1)
        weights = {}
        for symbol in z_score.index:
            if z_score[symbol] > 3:
                insights.append(
                    Insight.Price(
                        symbol=symbol,
                        period=timedelta(hours=1), 
                        direction=InsightDirection.Down, 
                        magnitude=magnitude[symbol], 
                        confidence=confidence[symbol], 
                        sourceModel=None, 
                        weight=z_score[symbol] - 3
                    )
                )
                algorithm.Log(f'Down Insight for {symbol}, Zscore: {z_score[symbol]}, Magnitude: {magnitude[symbol]}, Confidence: {confidence[symbol]},Weight: {z_score[symbol] - 3}')

            elif z_score[symbol] < -3:
                insights.append(
                    Insight.Price(
                        symbol=symbol, 
                        period=timedelta(hours=1), 
                        direction=InsightDirection.Up, 
                        magnitude=magnitude[symbol], 
                        confidence=confidence[symbol], 
                        sourceModel=None,  
                        weight= abs(z_score[symbol]) - 3
                    )
                )
                algorithm.Log(f'Up Insight for {symbol}, Zscore: {z_score[symbol]}, Magnitude: {magnitude[symbol]}, Confidence: {confidence[symbol]},Weight: {z_score[symbol] - 3}')

        return insights

    def OnSecuritiesChanged(self, algorithm, changes):
        '''Event fired each time the we add/remove securities from the data feed
        Args:
            algorithm: The algorithm instance that experienced the change in securities
            changes: The security additions and removals from the algorithm'''
        for added in changes.AddedSecurities:
            if added.Symbol not in self.symbolDataBySymbol:
                #symbolData = SymbolData(added, self.fastPeriod, self.slowPeriod, algorithm, self.resolution)
                self.symbolDataBySymbol.append(added.Symbol)
        for removed in changes.RemovedSecurities:
            if removed.Symbol in self.symbolDataBySymbol:
                data = self.symbolDataBySymbol.remove(removed.Symbol)