Overall Statistics
Total Trades
30754
Average Win
0.00%
Average Loss
-0.01%
Compounding Annual Return
-37.440%
Drawdown
62.800%
Expectancy
-0.883
Net Profit
-62.823%
Sharpe Ratio
-3.429
Probabilistic Sharpe Ratio
0%
Loss Rate
92%
Win Rate
8%
Profit-Loss Ratio
0.50
Alpha
-0.324
Beta
0.208
Annual Standard Deviation
0.092
Annual Variance
0.009
Information Ratio
-2.451
Tracking Error
0.143
Treynor Ratio
-1.526
Total Fees
$30755.58
Estimated Strategy Capacity
$8300000.00
from dateutil.relativedelta import relativedelta
from datetime import datetime
import pandas as pd


class PairsTradingAlphaModel(AlphaModel):

    def __init__(self,universeObject,leverage=1):
        self.pair = [ ]
        self.spreadMean = SimpleMovingAverage(25)
        self.spreadStd = StandardDeviation(25)
        self.period = timedelta(hours=1)
        self.leverage = leverage
        
        
        self.lastDatetime = datetime(1800,1,1)
        self.rebalanceDelta = relativedelta(days=1)
        
        self.universeObject = universeObject
        
        self.hasOpenPositions = False
        
        self.positions = {}
        #self.Portfolio = {}
        
        
        self.expiry = {}
        self.trail_cut_loss = {}
        
        self.leverage = 1.0
        
    def Update(self, algorithm, data):
        outputInsights = []
        
        if algorithm.Time + self.rebalanceDelta < self.lastDatetime:
        
            # Check for losses
            
            
            # Check whether want to liquidate position at the end of the day
            if algorithm.Time.hour == 15:
                for symb in algorithm.Portfolio.keys():
                    q = -1 * int(algorithm.Portfolio[symb].Quantity)
                    
                    if abs(q) > 0:
                        outputInsights.append(Insight(symb, timedelta(hours=5), InsightType.Price,InsightDirection.Flat, None, None, None, 0.00))
                    
                if outputInsights == []:
                    return []
                else:
                    return Insight.Group(outputInsights)
        
        
        
        
        # If no new positions to open
        if self.positions == {}:
            return outputInsights
        
        local_position = dict(self.positions)
        for c,symb in enumerate(self.positions.keys()):
            weight = self.positions[symb]["weight"]
            if weight < 0:
                outputInsights.append(Insight(symb, timedelta(hours=15), InsightType.Price,InsightDirection.Down, None, None, None, abs(weight)*self.leverage))
                pass
            elif weight > 0:
                outputInsights.append(Insight(symb, timedelta(hours=15), InsightType.Price, InsightDirection.Up, None, None, None, abs(weight)*self.leverage))
                pass
                
            try:
                del local_position[symb]
            except KeyError:
                pass
        
        self.positions = local_position
        # 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(hours=1), InsightType.Price, InsightDirection.Down, 0.0025, 1.00, None, prop*weight*self.leverage))
        #        elif weight > 0:
        #            outputInsights.append(Insight(symb, timedelta(hours=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 CancelAllOrders(self, algorithm, symbol):
        
        #if 'debug' in Roboto.MSGS: self.Debug("{} Cancelling all orders for {}".format(self.Time, symbol))
        openOrders = algorithm.Transactions.CancelOpenOrders(symbol)
        for oo in openOrders:
            if not (oo.Status == OrderStatus.CancelPending):
                r = oo.Cancel()
                #if not r.IsSuccess:
                #    if 'error' in Roboto.MSGS: self.Error("{} Failed to cancel open order {} of {} for reason: {}, {}".format(self.Time, oo.Quantity, oo.Symbol, r.ErrorMessage, r.ErrorCode))

    
    def OnSecuritiesChanged(self, algorithm, changes):
        
        #self.positions
        
        self.positions = {}
        
        for security in changes.AddedSecurities:
            #self.CancelAllOrders(algorithm,security.Symbol)
            if not security.Invested and (not security.Symbol in self.positions.keys()):
                pass
                self.positions[str(security.Symbol)] =  {"weight":
                    #weight = self.universeObject.beta_loading[0][c]
                    self.universeObject.beta_loading[0][list(self.universeObject.unweightedComponentPortfolio["symbols"]).index(security.Symbol)]
                    #self.universeObject.unweightedComponentPortfolio[0]["rescaledEtfWeights"][list(self.universeObject.unweightedComponentPortfolio["symbols"]).index(security.Symbol)]
                }
        
        pass
    
    def OnOrderEvent(self, algorithm, orderEvent):
        
        if orderEvent.Status == OrderStatus.Filled:
            
            order = self.Transactions.GetOrderById(orderEvent.OrderId)
            
            # if completely liquidating position, stop tracking position age
            if not algorithm.Portfolio[order.Symbol].Invested:
                try:
                    del self.expiry[order.Symbol]
                    del self.trail_cut_loss[order.Symbol]
                    #if 'debug' in Roboto.MSGS: self.Debug("{} No longer tracking {}".format(self.Time, order.Symbol))
                except Error:
                    pass
                #    if 'error' in Roboto.MSGS: self.Error("{} Key deletion failed for {}".format(self.Time, order.Symbol))
            
            # if position is completely new, start tracking position age
            else:
                if (order.Symbol not in self.expiry):
                    self.expiry[order.Symbol] = self.Time
                else:
                    pass
                    #if 'error' in Roboto.MSGS: self.Error("{} Key already existed for {}".format(self.Time, order.Symbol))
                
                if (order.Symbol not in self.trail_cut_loss):
                    self.trail_cut_loss[order.Symbol] = 0
                else:
                    pass
                    #if 'error' in Roboto.MSGS: self.Error("{} Key already existed for {}".format(self.Time, order.Symbol))
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, 4)  # Set Start Date
        self.SetEndDate(2008, 6, 11)  # 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 = 500 #1000
        self.numberOfSymbolsFine = 500
        self.dollarVolumeBySymbol = {}
        
        self.historyPointsLookback = 60
        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