Overall Statistics
Total Trades
23353
Average Win
0.09%
Average Loss
-0.05%
Compounding Annual Return
38.450%
Drawdown
36.500%
Expectancy
0.625
Net Profit
3051.026%
Sharpe Ratio
1.601
Probabilistic Sharpe Ratio
91.359%
Loss Rate
43%
Win Rate
57%
Profit-Loss Ratio
1.84
Alpha
0.35
Beta
-0.104
Annual Standard Deviation
0.21
Annual Variance
0.044
Information Ratio
0.751
Tracking Error
0.27
Treynor Ratio
-3.238
Total Fees
$63920.00
Estimated Strategy Capacity
$240000.00
Lowest Capacity Asset
CFFI R7MUOOUMGFHH
from Execution.ImmediateExecutionModel import ImmediateExecutionModel
from Portfolio.EqualWeightingPortfolioConstructionModel import InsightWeightingPortfolioConstructionModel
from Portfolio.EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
from Risk.MaximumDrawdownPercentPerSecurity import MaximumDrawdownPercentPerSecurity
from Risk.TrailingStopRiskManagementModel import TrailingStopRiskManagementModel
from AlphaModel import FundamentalFactorAlphaModel

class VerticalTachyonRegulators(QCAlgorithm):

    def Initialize(self):
        self.SetBrokerageModel(BrokerageName.AlphaStreams)
        self.SetStartDate(2011, 1, 1)
        #self.SetEndDate(2021, 8, 1)
        self.startCash = 100000
        self.averages = {}
        self.slowPeriod = self.GetParameter("slowPeriod")
        if self.slowPeriod is None:
            self.slowPeriod = 160
        self.fastPeriod = self.GetParameter("fastPeriod")
        if self.fastPeriod is None:
            self.fastPeriod = 15
        self.netMargin = self.GetParameter("netMargin")
        if self.netMargin is None:
            self.netMargin = 0.2
        self.netMargin = float(self.netMargin)

        # Benchmark
        self.SetBenchmark("SPY")

        # Execution model
        self.SetExecution(ImmediateExecutionModel())
        
        # Portfolio construction model
        # insightWeighting seems to outperform
        self.SetPortfolioConstruction(InsightWeightingPortfolioConstructionModel())
        #self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())

        # Risk model
        stopRisk = self.GetParameter("stopRisk")
        if stopRisk is None:
            stopRisk = 0.25
        #self.SetRiskManagement(TrailingStopRiskManagementModel(float(stopRisk)))
        self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(float(stopRisk)))
        
        # Universe selection
        self.num_coarse = 8000
        self.num_fine = 20
        self.UniverseSettings.Leverage = 1
        self.UniverseSettings.Resolution = Resolution.Daily
        #self.UniverseSettings.MinimumTimeInUniverse = timedelta(days=30)
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        # rebalancing
        self.lastMonth = -1

        # Set factor weighting 
        # Quality is currently ROE
        quality_weight = self.GetParameter("quality_weight")
        if quality_weight is None:
            quality_weight = 8

        size_weight = self.GetParameter("size_weight")
        if size_weight is None:
            size_weight = 4
        
        # value is currently Net Profit Margin    
        value_weight = self.GetParameter("value_weight")
        if value_weight is None:
            value_weight = 16

        # Alpha Model
        self.AddAlpha(FundamentalFactorAlphaModel(self.num_fine, quality_weight, \
            value_weight, size_weight))
        
        # schedule weekly plotting
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday), 
                        self.TimeRules.At(10, 30),
                        self.Plotting)
                        

    def Plotting(self):
        self.Plot("Positions", "Num", len([x.Symbol for x in self.Portfolio.Values \
        if self.Portfolio[x.Symbol].Invested]))
        self.Plot("Margin remaining", "USD", self.Portfolio.MarginRemaining)


    def CoarseSelectionFunction(self, coarse):
        selector = []
        
        # If not time to rebalance, keep the same universe
        if self.Time.month == self.lastMonth: 
            return Universe.Unchanged
        
        # Else reassign the month variable
        self.lastMonth = self.Time.month
        
        # 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)
            
        selected = selected[:self.num_coarse]

        # Only add up or down trending securities (EMA based)
        for finer in selected:
            symbol = finer.Symbol

            if symbol not in self.averages:
                # Call history to get an array of X days of history data
                history = self.History(symbol, self.slowPeriod, Resolution.Daily)
                
                #Pass in the history result to SelectionData
                self.averages[symbol] = SelectionData(history, self.slowPeriod, self.fastPeriod)

            self.averages[symbol].update(self.Time, finer.AdjustedPrice)
            
            # EMA change direction here
            if  self.averages[symbol].is_ready() and \
            self.averages[symbol].fast > self.averages[symbol].slow:
                selector.append(finer)

        return [x.Symbol for x in selector]


    def FineSelectionFunction(self, fine):
        # Filter the fine data for equities with non-zero/non-null values
        filtered_fine = [x.Symbol for x in fine if x.OperationRatios.ROE.SixMonths > 0.05
                                                #and x.ValuationRatios.PriceChange1M > 0
                                                #and x.ValuationRatios.FCFYield > 0
                                                #and x.CompanyReference.IndustryTemplateCode == 'B'
                                                and x.OperationRatios.NetMargin.ThreeMonths > self.netMargin
                                                and x.MarketCap > 0]

        return filtered_fine

class SelectionData():
    def __init__(self, history, slowPeriod, fastPeriod):
        #Save the fast and slow ExponentialMovingAverage
        self.slow = ExponentialMovingAverage(slowPeriod)
        self.fast = ExponentialMovingAverage(fastPeriod)
        
        #Loop over the history data and update the indicators
        for bar in history.itertuples():
            self.fast.Update(bar.Index[1], bar.close)
            self.slow.Update(bar.Index[1], bar.close)
    
    #Check if our indicators are ready
    def is_ready(self):
        return self.slow.IsReady and self.fast.IsReady
    
    #Use the "indicator.Update" method to update the time and price of both indicators
    def update(self, time, price):
        self.fast.Update(time, price)
        self.slow.Update(time, price)
from datetime import timedelta

# For small/large cap preference, change line ca. 80
# For portfolio construction weights, line 56

class FundamentalFactorAlphaModel(AlphaModel):
    
    def __init__(self, num_fine, quality_weight, value_weight, size_weight):
        
        # Initialize the various variables/helpers we'll need
        self.lastMonth = -1
        self.long = {}
        self.num_fine = num_fine
        self.period = timedelta(31)

        # normalize quality, value, size weights
        weights = [float(quality_weight), float(value_weight), float(size_weight)]
        weights = [float(i)/sum(weights) for i in weights]
        
        self.quality_weight = weights[0]
        self.value_weight = weights[1]
        self.size_weight = weights[2]


    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 newa available
        Returns:
            New insights'''
        
        # Return no insights if it's not time to rebalance
        if algorithm.Time.month == self.lastMonth: 
            return []
        self.lastMonth = algorithm.Time.month
        
        # List of insights
        # Insights of the form: Insight(symbol, timedelta, type, direction, \
        #                        magnitude, confidence, sourceModel, weight)
        insights = []
        sorted_symbol = [tup[0] for tup in self.long]
        longs = [security.Symbol for security in sorted_symbol]

        # Close old positions if they aren't in longs
        for security in algorithm.Portfolio.Values:
            if security.Invested and security.Symbol not in longs:

                insights.append(Insight(security.Symbol, self.period,
                                InsightType.Price, 
                               InsightDirection.Flat, None, None, None, None))

        length = len(self.long)
        for i, elem in enumerate(self.long):
            
            algorithm.Log(str(elem[0].Symbol)+ " investing")
            
            # the weight is the position in the self.long tuple divided with 
            # the length of the self.long tuple. The 0 entry is the highest
            j = length - i
            alt_weight = j**2
            weight = j/length
            
            # Insight(symbol, timedelta, type, direction, magnitude=None, confidence=None, sourceModel=None, weight=None)
            # so in our case the weights will be from the lenghts which again is max self.num_fine
            insights.append(Insight(elem[0].Symbol, self.period, InsightType.Price, 
                                    InsightDirection.Up, None, alt_weight,\
                                    None, alt_weight))
        
        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'''

        # Get the added securities
        addedSecurities = [x for x in changes.AddedSecurities]

        # Assign quality, value, size score to each stock
        quality_scores = self.Scores(addedSecurities, \
            [(lambda x: x.Fundamentals.OperationRatios.ROE.SixMonths, True, 1)]) 
            #(lambda x: x.Fundamentals.OperationRatios.QuickRatio.Value, True, 1)]) 
            #(lambda x: x.Fundamentals.OperationRatios.DebttoAssets.Value, False, 1)])
        
        value_scores = self.Scores(addedSecurities, \
            [(lambda x: x.Fundamentals.OperationRatios.NetMargin.SixMonths, True, 1)])
        
        # Larger caps = True, smaller caps = False
        size_scores = self.Scores(addedSecurities, \
            [(lambda x: x.Fundamentals.MarketCap, True, 1)])
        
        scores = {}
        self.long = {}
        # Assign a combined score to each stock 
        for symbol,value in quality_scores.items():
            quality_rank = value
            value_rank = value_scores[symbol]
            size_rank = size_scores[symbol]
            scores[symbol] = quality_rank*self.quality_weight + \
                       value_rank*self.value_weight + size_rank*self.size_weight
                       
        # Sort the securities by their scores
        # shouldnt this be sorted reverse = True??
        sorted_scores = sorted(scores.items(), key=lambda tup : tup[1], reverse=True)
        #algorithm.Log(sorted_scores)
        self.long = sorted_scores[:self.num_fine]


    def Scores(self, addedSecurities, fundamentals):
        '''Assigns scores to each stock in added
        Args: 
            added: list of sceurities 
            fundamentals: list of 3-tuples (lambda function, bool, float)
        Returns:
            Dictionary with score for each security'''
        
        length = len(fundamentals)
        
        if length == 0:
            return {}
        
        # Initialize helper variables
        scores = {}
        sortedBy = []
        rank = [0 for _ in fundamentals]
        
        # Normalize weights
        weights = [tup[2] for tup in fundamentals]
        weights = [float(i)/sum(weights) for i in weights]
        
        # Create sorted list for each fundamental factor passed
        for tup in fundamentals:
            sortedBy.append(sorted(addedSecurities, key=tup[0], reverse=tup[1]))
        
        # Create and save score for each symbol
        for index,symbol in enumerate(sortedBy[0]):
            
            # Save symbol's rank for each fundamental factor
            rank[0] = index
            for j in range(1, length):
                rank[j] = sortedBy[j].index(symbol)
            
            # Save symbol's total score
            score = 0
            for i in range(length):
                score += rank[i] * weights[i]
            scores[symbol] = score
            
        return scores