Overall Statistics
Total Trades
2897
Average Win
0.12%
Average Loss
-0.12%
Compounding Annual Return
-30.236%
Drawdown
50.400%
Expectancy
-0.287
Net Profit
-33.070%
Sharpe Ratio
-0.283
Probabilistic Sharpe Ratio
5.007%
Loss Rate
63%
Win Rate
37%
Profit-Loss Ratio
0.95
Alpha
-0.085
Beta
1.856
Annual Standard Deviation
0.466
Annual Variance
0.217
Information Ratio
-0.31
Tracking Error
0.344
Treynor Ratio
-0.071
Total Fees
$2958.96
Estimated Strategy Capacity
$14000000.00
Lowest Capacity Asset
MCW XPMG4J9N0TID
import numpy as np
from scipy import stats
from collections import deque
#region imports
from AlgorithmImports import *
#endregion
from Execution.ImmediateExecutionModel import ImmediateExecutionModel
from Portfolio.EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel

class NadionTransdimensionalAutosequencers(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2021, 10, 1)  # Set Start Date
        self.SetCash(100000)  # Set Strategy Cash
        self.SetBenchmark("SPY")
        
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        self.UniverseSettings.Leverage = 0
        self.UniverseSettings.Resolution=Resolution.Daily
        self.SetUniverseSelection( QC500UniverseSelectionModel() )

        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel(lambda time: Expiry.EndOfWeek(time)))

        self.SetExecution(ImmediateExecutionModel())
        
        self.rebalance = self.Time
        self.securities = []
        self.symbol_data_by_symbol = {}
        self.sorted_mom = []
        self.mom_scores = {}
        self.num_stocks = 30

    def OnData(self, data):
        insight = []

        if self.rebalance >= self.Time:
            return 

        self.mom_scores = {}
        for k,v in  self.symbol_data_by_symbol.items():   
            if not v.mom.IsReady:
                continue
            if v.mom.Score >= 40:
                self.mom_scores[k] = v.mom.Score 
        self.sorted_mom = sorted([k for k,v in self.mom_scores.items()],
            key=lambda x: self.mom_scores[x], reverse=True)
        
        self.selected = self.sorted_mom[:self.num_stocks]
        for security in self.selected:
            insight += [(Insight.Price(security, Expiry.EndOfWeek, InsightDirection.Up))]
        self.rebalance = Expiry.EndOfWeek(self.Time)

        self.EmitInsights(insight)



    def OnSecuritiesChanged(self, changes):        
        for security in changes.AddedSecurities:
            self.symbol_data_by_symbol[security.Symbol] = SymbolData(self, security.Symbol)

        for security in changes.RemovedSecurities:
            if security.Symbol in self.symbol_data_by_symbol:
                symbol_data = self.symbol_data_by_symbol.pop(security.Symbol, None)
                if symbol_data:
                    symbol_data.dispose()
            if security in self.securities:
                self.securities.remove(security)
                             
        self.securities.extend(changes.AddedSecurities)
        
        for security in self.securities:
            if security.Symbol not in self.ActiveSecurities:
                self.securities.remove(security)
                symbol_data = self.symbol_data_by_symbol.pop(security.Symbol, None)
                if symbol_data:
                    symbol_data.dispose()
            
                    
class SymbolData:
    def __init__(self, algorithm, symbol):
        self.algorithm = algorithm
        self.symbol = symbol
        self.mom = CustomMomentum("momentum", 125)
        self.consolidator = TradeBarConsolidator(1)
        algorithm.SubscriptionManager.AddConsolidator(symbol, self.consolidator)
        algorithm.RegisterIndicator(symbol, self.mom, self.consolidator)
        algorithm.WarmUpIndicator(self.symbol, self.mom)
        
    def dispose(self):
        self.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.consolidator)


class CustomMomentum(PythonIndicator):
    def __init__(self, name, period):
        self.Name = name
        self.WarmUpPeriod = period
        self.Time = datetime.min
        self.Value = 0
        self.queue = deque(maxlen=period)

    def __repr__(self):
        return "{0} -> IsReady: {1}. Time: {2}. Value: {3}".format(self.Name, self.IsReady, self.Time, self.Score)

    def Update(self, input: BaseData) -> bool:
        self.queue.appendleft(input.Value)  
        x = np.arange(len(self.queue))
        log_ts = np.log(self.queue)
        slope, intercept, r_value, p_value, std_err = stats.linregress(x, log_ts)
        annualized_slope = (np.power(np.exp(slope), 252) - 1) * 100
        self.Time = input.Time
        self.Score = float(annualized_slope * (r_value**2))
        return x == self.queue.maxlen