Overall Statistics
Total Trades
38398
Average Win
0.00%
Average Loss
0.00%
Compounding Annual Return
-46.777%
Drawdown
41.000%
Expectancy
-0.869
Net Profit
-40.968%
Sharpe Ratio
-19.824
Probabilistic Sharpe Ratio
0%
Loss Rate
92%
Win Rate
8%
Profit-Loss Ratio
0.58
Alpha
-0.406
Beta
0.016
Annual Standard Deviation
0.02
Annual Variance
0
Information Ratio
-4.993
Tracking Error
0.096
Treynor Ratio
-25.716
Total Fees
$38398.00
Estimated Strategy Capacity
$47000000.00
from dateutil.relativedelta import relativedelta
from datetime import datetime
import pandas as pd

class SymbolData:
    '''Contains data specific to a symbol required by this model'''
    def __init__(self, security):
        self.Security = security
        self.Symbol = security.Symbol
        self.Fast = None
        self.Slow = None

        # True if the fast is above the slow, otherwise false.
        # This is used to prevent emitting the same signal repeatedly
        self.FastIsOverSlow = False

    @property
    def SlowIsOverFast(self):
        return not self.FastIsOverSlow


class PairsTradingAlphaModel(AlphaModel):

    def __init__(self,universeObject,leverage=1):
        self.pair = [ ]
        self.spreadMean = SimpleMovingAverage(25)
        self.spreadStd = StandardDeviation(25)
        self.period = timedelta(hours=2)
        self.leverage = leverage
        
        
        self.lastDatetime = datetime(1800,1,1)
        self.rebalanceDelta = relativedelta(days=1)
        
        self.universeObject = universeObject
        
        self.hasOpenPositions = False
        
        self.leverage = 1.0
        
    def Update(self, algorithm, data):
        if algorithm.Time + self.rebalanceDelta < self.lastDatetime:
            return []
        
        outputInsights = []
        
        # Open positions in the morning
        if not self.hasOpenPositions and algorithm.Time.hour == 10:
            for pos,symb in enumerate(self.universeObject.unweightedComponentPortfolio["symbols"]):
                weight = self.universeObject.unweightedComponentPortfolio[0]["rescaledEtfWeights"][pos]
                prop = self.universeObject.unweightedComponentPortfolio[0]["explainedVarianceProportion"] 
                if weight < 0:
                    outputInsights.append(Insight(symb, timedelta(minutes=20), InsightType.Price, InsightDirection.Down, 0.0025, 1.00, None, prop*weight*self.leverage))
                elif weight > 0:
                    outputInsights.append(Insight(symb, timedelta(minutes=20), InsightType.Price, InsightDirection.Up, 0.0025, 1.00, None, prop*weight*self.leverage))
            self.hasOpenPositions = True
        
        # Close positions at the end of the day
        if algorithm.Time.hour == 15 and self.hasOpenPositions:
            algorithm.Liquidate()
            self.hasOpenPositions = False
        
        algorithm.Plot("Explained Variance", "Exp.Var", 100.0*self.universeObject.unweightedComponentPortfolio[0]["lastReturn"])
        self.lastDatetime = algorithm.Time
        
        if outputInsights == []:
            return []
        else:
            return Insight.Group(outputInsights)
    
    def OnSecuritiesChanged(self, algorithm, changes):
        pass
from Alphas.PearsonCorrelationPairsTradingAlphaModel import PearsonCorrelationPairsTradingAlphaModel
from Execution.ImmediateExecutionModel import ImmediateExecutionModel
#from Portfolio.MeanVarianceOptimizationPortfolioConstructionModel import MeanVarianceOptimizationPortfolioConstructionModel
from PortfolioConstruction import EqualWeightingPortfolioConstructionModel
from Risk.MaximumDrawdownPercentPerSecurity import MaximumDrawdownPercentPerSecurity
from UniverseSelection import QC500UniverseSelectionModel
from PaperAlpha import PairsTradingAlphaModel

class FatOrangeMonkey(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2006, 5, 2)  # Set Start Date
        self.SetEndDate(2007, 3, 2)  # Set Start Date
        self.SetCash(100000)  # Set Strategy Cash
        # self.AddEquity("SPY", Resolution.Minute)
        
        self.SetExecution(ImmediateExecutionModel())
        
        universeObject = QC500UniverseSelectionModel()
        
        self.SetUniverseSelection(universeObject)
        
        #self.AddAlpha(PearsonCorrelationPairsTradingAlphaModel(252, Resolution.Daily))
        
        self.AddAlpha(PairsTradingAlphaModel(universeObject))

        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())

        self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(0.01))
from QuantConnect.Data.UniverseSelection import *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
from itertools import groupby
from math import ceil
from dateutil.relativedelta import relativedelta
from datetime import datetime
import pandas as pd

from sklearn.decomposition import PCA

class QC500UniverseSelectionModel(FundamentalUniverseSelectionModel):
    '''Defines the QC500 universe as a universe selection model for framework algorithm
    For details: https://github.com/QuantConnect/Lean/pull/1663'''

    def __init__(self, filterFineData = True, universeSettings = None, securityInitializer = None):
        '''Initializes a new default instance of the QC500UniverseSelectionModel'''
        super().__init__(filterFineData, universeSettings, securityInitializer)
        self.numberOfSymbolsCoarse = 100 #1000
        self.numberOfSymbolsFine = 500
        self.dollarVolumeBySymbol = {}
        
        self.historyPointsLookback = 30
        self.historyResolution = Resolution.Daily

        self.lastDatetime = datetime(1800,1,1)
        self.rebalanceDelta = relativedelta(days=1)
        
        self.targetExplainedVariance = 0.8  # Proportion of the variance explained by PCA components
        
        self.stock_std = None
        
        self.unweightedComponentPortfolio = {}
        self.beta_loading = {}
        
        # Toggle rescaling per component
        self.rescale_eigenportfolio = False
    def SelectCoarse(self, algorithm, coarse):
        '''Performs coarse selection for the QC500 constituents.
        The stocks must have fundamental data
        The stock must have positive previous-day close price
        The stock must have positive volume on the previous trading day'''
        if algorithm.Time + self.rebalanceDelta < self.lastDatetime:
            return Universe.Unchanged
            
        sortedByDollarVolume = sorted([x for x in coarse if x.HasFundamentalData  
                                                            and x.Volume > 0 
                                                            and x.Price > 0
                                                            and x.Market == "usa"],
                                     key = lambda x: x.DollarVolume, reverse=True)[:self.numberOfSymbolsCoarse]

        self.dollarVolumeBySymbol = {x.Symbol:x.DollarVolume for x in sortedByDollarVolume}

        # If no security has met the QC500 criteria, the universe is unchanged.
        # A new selection will be attempted on the next trading day as self.lastMonth is not updated
        if len(self.dollarVolumeBySymbol) == 0:
            return Universe.Unchanged

        # return the symbol objects our sorted collection
        return list(self.dollarVolumeBySymbol.keys())

    def SelectFine(self, algorithm, fine):
        '''Performs fine selection for the QC500 constituents
        The company's headquarter must in the U.S.
        The stock must be traded on either the NYSE or NASDAQ
        At least half a year since its initial public offering
        The stock's market cap must be greater than 500 million'''
        
        
        # Might want to add Volume, DollarVolume filters
        sortedBySector = sorted([x for x in fine if (algorithm.Time - x.SecurityReference.IPODate).days > 180
                                        and x.MarketCap > 5e8],
                               key = lambda x: x.CompanyReference.IndustryTemplateCode)

        count = len(sortedBySector)
        
        # If no security has met the QC500 criteria, the universe is unchanged.
        # A new selection will be attempted on the next trading day as self.lastMonth is not updated
        if count == 0:
            return Universe.Unchanged

        # Update self.lastMonth after all QC500 criteria checks passed
        self.lastDatetime = algorithm.Time

        #percent = self.numberOfSymbolsFine / count
        #sortedByDollarVolume = []
        

        # select stocks with top dollar volume in every single sector
        #for code, g in groupby(sortedBySector, lambda x: x.CompanyReference.IndustryTemplateCode):
        #    y = sorted(g, key = lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse = True)
        #    c = ceil(len(y) * percent)
        #    sortedByDollarVolume.extend(y[:c])

        #sortedByDollarVolume = sorted(sortedByDollarVolume, key = lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse=True)
        
        #selectedSymbols =  [x.Symbol for x in sortedByDollarVolume[:self.numberOfSymbolsFine]]
        
        
        # Calculate Returns matrix
        for c,s in enumerate(sortedBySector):
            history = algorithm.History(s.Symbol, self.historyPointsLookback+1, self.historyResolution)

            pct_col = history[["close"]].pct_change().reset_index(drop=True).rename(columns={"close":s.Symbol})
            
            if c == 0:
                comb_returns = pct_col
            else:
                comb_returns.insert(0,s.Symbol,pct_col[s.Symbol])
                
        # Drop top NA row, followed by any other columns with mising da
        comb_returns = comb_returns[1:].dropna(axis=1)
        symbols = comb_returns.keys()
        
        self.stock_std = comb_returns.std()
        standard_return = (comb_returns-comb_returns.mean())/self.stock_std
        
        # Calculate corralation matrix
        corr_mat = standard_return.corr()
        
        pca = PCA()
        pca_obj = pca.fit(corr_mat)
        
        
        # Find number of components required for the target explained variance
        sum_var = 0
        for num_vars,var in enumerate(pca_obj.explained_variance_ratio_):
            sum_var += var
            if sum_var > self.targetExplainedVariance:
                num_vars += 1
                break
        
        
        #componentReturns = pd.Dataframe()
        
        # Create a dictionary of weights per component
        self.unweightedComponentPortfolio["symbols"] =  symbols
        for i in range(num_vars):
            # "Normalise" components i.e., "buy" ETF at 1x leverage
            tmp_comp = pca_obj.components_[i]/self.stock_std
            
            
            if self.rescale_eigenportfolio:
                tmp_comp = tmp_comp/(abs(tmp_comp).sum())
            
            componentReturns_tmp = (comb_returns*tmp_comp).sum(axis=1)
            
            # Calculate historic returns per Component
            if i == 0:
                componentReturns = pd.DataFrame(componentReturns_tmp)
            else:
                componentReturns.insert(0,i,componentReturns_tmp)
                
            self.unweightedComponentPortfolio[i] = {
               "component":pca_obj.components_[i],
               "etfWeights":list(tmp_comp),
               "rescaledEtfWeights":list(tmp_comp/(abs(tmp_comp).sum())),
               "lastReturn":componentReturns_tmp.iloc[-1],
               "explainedVarianceProportion":pca_obj.explained_variance_ratio_[i]
           }
           
        
        # Calculate Beta loadings per component. Equation (11) page 16
        for i in componentReturns:
            tmp_ret = componentReturns[i]
            self.beta_loading[i] = comb_returns.apply(lambda x: tmp_ret.cov(x))/tmp_ret.var()
        
        return symbols
class EqualWeightingPortfolioConstructionModel(PortfolioConstructionModel):
    '''Provides an implementation of IPortfolioConstructionModel that gives equal weighting to all securities.
    The target percent holdings of each security is 1/N where N is the number of securities.
    For insights of direction InsightDirection.Up, long targets are returned and
    for insights of direction InsightDirection.Down, short targets are returned.'''

    def __init__(self, rebalance = Resolution.Daily, portfolioBias = PortfolioBias.LongShort):
        '''Initialize a new instance of EqualWeightingPortfolioConstructionModel
        Args:
            rebalance: Rebalancing parameter. If it is a timedelta, date rules or Resolution, it will be converted into a function.
                              If None will be ignored.
                              The function returns the next expected rebalance time for a given algorithm UTC DateTime.
                              The function returns null if unknown, in which case the function will be called again in the
                              next loop. Returning current time will trigger rebalance.
            portfolioBias: Specifies the bias of the portfolio (Short, Long/Short, Long)'''
        self.portfolioBias = portfolioBias

        # If the argument is an instance of Resolution or Timedelta
        # Redefine rebalancingFunc
        rebalancingFunc = rebalance
        if isinstance(rebalance, int):
            rebalance = Extensions.ToTimeSpan(rebalance)
        if isinstance(rebalance, timedelta):
            rebalancingFunc = lambda dt: dt + rebalance
        if rebalancingFunc:
            self.SetRebalancingFunc(rebalancingFunc)

    def DetermineTargetPercent(self, activeInsights):
        '''Will determine the target percent for each insight
        Args:
            activeInsights: The active insights to generate a target for'''
        result = {}

        # give equal weighting to each security
        count = sum(x.Direction != InsightDirection.Flat and self.RespectPortfolioBias(x) for x in activeInsights)
        percent = 0 if count == 0 else 1.0 / count
        for insight in activeInsights:
            result[insight] = (insight.Direction if self.RespectPortfolioBias(insight) else InsightDirection.Flat) * insight.Weight
        return result

    def RespectPortfolioBias(self, insight):
        '''Method that will determine if a given insight respects the portfolio bias
        Args:
            insight: The insight to create a target for
        '''
        return self.portfolioBias == PortfolioBias.LongShort or insight.Direction == self.portfolioBias