Overall Statistics
Total Trades
12
Average Win
0%
Average Loss
0%
Compounding Annual Return
-0.202%
Drawdown
2.800%
Expectancy
0
Net Profit
-0.201%
Sharpe Ratio
-0.03
Probabilistic Sharpe Ratio
10.864%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
-0.024
Beta
0.155
Annual Standard Deviation
0.031
Annual Variance
0.001
Information Ratio
-2.665
Tracking Error
0.056
Treynor Ratio
-0.006
Total Fees
$13.28
Estimated Strategy Capacity
$15000000.00
Lowest Capacity Asset
BCM R735QTJ8XC9X
#region imports
from AlgorithmImports import *
#endregion
class PortfolioRebalanceOnCustomFuncRegressionAlgorithm(QCAlgorithm):
    def Initialize(self):
        ''' Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.'''

        self.UniverseSettings.Resolution = Resolution.Daily



        self.SetStartDate(2017, 1, 1)
        self.SetEndDate(2018, 1, 1)

        self.Settings.RebalancePortfolioOnInsightChanges = False
        self.Settings.RebalancePortfolioOnSecurityChanges = False
        self.num_coarse = 500
        self.month= 0

        
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

        
        self.AddAlpha(FundamentalFactorAlphaModel())
        self.SetPortfolioConstruction(MyEqualWeightingPortfolioConstructionModel(self.IsRebalanceDue, ag=self))
        self.SetExecution(ImmediateExecutionModel())
   

    def CoarseSelectionFunction(self, coarse):
        # If not time to rebalance, keep the same universe
        if not self.IsRebalanceDue(self.Time): 
            return Universe.Unchanged

        # Select only those with fundamental data and a sufficiently large price
        # Sort by top dollar volume: most liquid to least liquid
        selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5],
                            key = lambda x: x.DollarVolume, reverse=True)

        return [x.Symbol for x in selected[:self.num_coarse]]


    def FineSelectionFunction(self, fine):
        # Filter the fine data for equities that IPO'd more than 5 years ago in selected sectors
        
        sectors = [
            MorningstarSectorCode.FinancialServices,
            MorningstarSectorCode.RealEstate,
            MorningstarSectorCode.Healthcare,
            MorningstarSectorCode.Utilities,
            MorningstarSectorCode.Technology]
        
        filtered_fine = [x.Symbol for x in fine if x.SecurityReference.IPODate + timedelta(365*5) < self.Time
                                    and x.AssetClassification.MorningstarSectorCode in sectors
                                    and x.OperationRatios.ROE.Value > 0.15
                                    and x.OperationRatios.NetMargin.Value > 0
                                    and 30 > x.ValuationRatios.PERatio > 0]
        # self.Debug(f'{self.Time} len(filtered_fine) {len(filtered_fine)}')
                
        return filtered_fine

    def IsRebalanceDue(self, time):
        # Rebalance on the first day of the Quarter
        if time.month == self.month or time.month not in [1, 4, 7, 10]:
            return None
            
        self.month = time.month
        self.Debug(f'{self.Time} utc {self.UtcTime}, IsRebalanceDue time {time}')
        return time



class MyEqualWeightingPortfolioConstructionModel(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, ag=None):
        '''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)'''
        super().__init__()
        self.portfolioBias = portfolioBias 
        self.rebalanceTime = datetime.min
        
     

        # 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)
            ag.Debug(f'pcm init rebalancingFunc')
        self.ag = ag

    def DetermineTargetPercent(self, activeInsights):
        '''Will determine the target percent for each insight
        Args:
            activeInsights: The active insights to generate a target for'''
        
        # self.ag.Debug(f'{self.ag.Time} pcm-1 triggered at {self.ag.Time}')
        # if self.ag.Time < self.rebalanceTime:
        #     return {}
        self.ag.Debug(f'{self.ag.Time} pcm triggered at {self.ag.Time} utc{self.ag.UtcTime}')
        # Set the rebalance time to match the insight expiry
        self.rebalanceTime = Expiry.EndOfQuarter(self.ag.Time)

        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) * percent
        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

class FundamentalFactorAlphaModel(AlphaModel):
    
    def __init__(self):
        self.rebalanceTime = datetime.min
        # Dictionary containing set of securities in each sector
        # e.g. {technology: set(AAPL, TSLA, ...), healthcare: set(XYZ, ABC, ...), ... }
        self.sectors = {}

    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:
            New insights'''

        if algorithm.Time < self.rebalanceTime:
            return []
        
        # Set the rebalance time to match the insight expiry
        self.rebalanceTime = Expiry.EndOfQuarter(algorithm.Time)
        algorithm.Debug(f'{algorithm.Time} alpha rebalanceTime {self.rebalanceTime}')
        
        insights = []
        
        for sector in self.sectors:
            securities = self.sectors[sector]
            sortedByROE = sorted(securities, key=lambda x: x.Fundamentals.OperationRatios.ROE.Value, reverse=True)
            sortedByPM = sorted(securities, key=lambda x: x.Fundamentals.OperationRatios.NetMargin.Value, reverse=True)
            sortedByPE = sorted(securities, key=lambda x: x.Fundamentals.ValuationRatios.PERatio, reverse=False)

            # Dictionary holding a dictionary of scores for each security in the sector
            scores = {}
            for security in securities:
                score = sum([sortedByROE.index(security), sortedByPM.index(security), sortedByPE.index(security)])
                scores[security] = score
                
            # Add best 20% of each sector to longs set (minimum 1)
            length = max(int(len(scores)/5), 1)
            for security in sorted(scores.items(), key=lambda x: x[1], reverse=False)[:length]:
                symbol = security[0].Symbol
                # Use Expiry.EndOfQuarter in this case to match Universe, Alpha and PCM
                insights.append(Insight.Price(symbol, Expiry.EndOfQuarter, InsightDirection.Up))
        
        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'''
        
        # Remove security from sector set
        for security in changes.RemovedSecurities:
            for sector in self.sectors:
                if security in self.sectors[sector]:
                    self.sectors[sector].remove(security)
        
        # Add security to corresponding sector set
        for security in changes.AddedSecurities:
            sector = security.Fundamentals.AssetClassification.MorningstarSectorCode
            if sector not in self.sectors:
                self.sectors[sector] = set()
            self.sectors[sector].add(security)