Overall Statistics
Total Orders
1806
Average Win
0.14%
Average Loss
-0.11%
Compounding Annual Return
4.789%
Drawdown
36.600%
Expectancy
0.458
Start Equity
100000
End Equity
126359.35
Net Profit
26.359%
Sharpe Ratio
0.089
Sortino Ratio
0.118
Probabilistic Sharpe Ratio
2.266%
Loss Rate
39%
Win Rate
61%
Profit-Loss Ratio
1.38
Alpha
-0.075
Beta
1.251
Annual Standard Deviation
0.213
Annual Variance
0.045
Information Ratio
-0.452
Tracking Error
0.124
Treynor Ratio
0.015
Total Fees
$1965.76
Estimated Strategy Capacity
$920000.00
Lowest Capacity Asset
EGHT R735QTJ8XC9X
Portfolio Turnover
0.67%
Drawdown Recovery
1018
# region imports
from AlgorithmImports import *
# endregion
import statistics as stat
from collections import deque


class DynamicCalibratedGearbox(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(100_000)
        self.set_warm_up(timedelta(4*365))
        self.universe_settings.schedule.on(self.date_rules.month_start('SPY'))
        self.universe_settings.resolution = Resolution.DAILY
        self.add_universe_selection(FundamentalUniverseSelectionModel(self._select_assets))
        self.set_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.UP, timedelta(31)))
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(lambda time:None))
        self.set_execution(ImmediateExecutionModel())
        
        # store ROA of tech stocks
        self._roa_values_by_symbol = {}

    def _select_assets(self, fundamentals):
        # book value == FinancialStatements.BalanceSheet.NetTangibleAssets (book value and NTA are synonyms)
        # BM (Book-to-Market) == book value / MarketCap
        # ROA == OperationRatios.ROA
        # CFROA == FinancialStatements.CashFlowStatement.OperatingCashFlow / FinancialStatements.BalanceSheet.TotalAssets
        # R&D to MktCap == FinancialStatements.IncomeStatement.ResearchAndDevelopment / MarketCap
        # CapEx to MktCap == FinancialStatements.CashFlowStatement.CapExReported / MarketCap
        # Advertising to MktCap == FinancialStatements.IncomeStatement.SellingGeneralAndAdministration / MarketCap
        #   note: this parameter may be slightly higher than pure advertising costs

        # We only want to update our ROA values every three months.
        if self.time.month % 3 != 1:
            return Universe.UNCHANGED
        # Select tech stocks that have an ROA value.
        tech_securities = [
            f for f in fundamentals
            if (f.has_fundamental_data and
                f.asset_classification.morningstar_sector_code == MorningstarSectorCode.TECHNOLOGY and
                not np.isnan(f.operation_ratios.roa.three_months))
        ]
        # Record the ROA value of each stock.
        for f in tech_securities:
            symbol = f.symbol
            if symbol not in self._roa_values_by_symbol:
                # 3 years * 4 quarters = 12 quarters of data
                self._roa_values_by_symbol[symbol] = deque(maxlen=12)
            self._roa_values_by_symbol[symbol].append(f.operation_ratios.roa.three_months)
        # We want to rebalance in the fourth month after the (fiscal) year ends
        # so that we have the most recent quarter's data.
        if self.time.month != 4 or not any(len(ROA) == ROA.maxlen for ROA in self._roa_values_by_symbol.values()):
            return Universe.UNCHANGED
        # Make sure our stocks has these fundamentals.
        tech_securities = [
            f for f in tech_securities 
            if not any([np.isnan(factor) or factor == 0 for factor in [
                f.operation_ratios.roa.one_year,
                f.financial_statements.cash_flow_statement.operating_cash_flow.twelve_months,
                f.financial_statements.balance_sheet.total_assets.twelve_months,
                f.financial_statements.income_statement.research_and_development.twelve_months,
                f.financial_statements.cash_flow_statement.cap_ex_reported.twelve_months,
                f.financial_statements.income_statement.selling_general_and_administration.twelve_months,
                f.market_cap
            ]])
        ]                                               
        # Compute the variance of the ROA for each tech stock.
        tech_VARROA = {
            symbol : stat.variance(ROA) 
            for symbol, ROA in self._roa_values_by_symbol.items() if len(ROA) == ROA.maxlen
        }
        if len(tech_VARROA) < 2:
            return Universe.UNCHANGED
        tech_VARROA_median = stat.median(tech_VARROA.values())
        
        # We will now map tech Symbols to various fundamental ratios, 
        #   and compute the median for each ratio.
        roa_1y_by_symbol = {} # ROA 1-year
        cf_roa_by_symbol = {} # Cash Flow ROA
        rd_to_mkt_cap_by_symbol = {} # R&D to MktCap
        cap_ex_to_mkt_cap_by_symbol = {} # CapEx to MktCap
        ad_to_mkt_cap_by_symbol = {} # Advertising to MktCap
        for f in tech_securities:
            roa_1y_by_symbol[f.symbol] = f.operation_ratios.roa.one_year
            cf_roa_by_symbol[f.symbol] = (
                f.financial_statements.cash_flow_statement.operating_cash_flow.twelve_months
                / f.financial_statements.balance_sheet.total_assets.twelve_months
            ) 
            rd_to_mkt_cap_by_symbol[f.symbol] = (
                f.financial_statements.income_statement.research_and_development.twelve_months
                / f.market_cap
            ) 
            cap_ex_to_mkt_cap_by_symbol[f.symbol] = (
                f.financial_statements.cash_flow_statement.cap_ex_reported.twelve_months
                / f.market_cap
            ) 
            ad_to_mkt_cap_by_symbol[f.symbol] = (
                f.financial_statements.income_statement.selling_general_and_administration.twelve_months
                / f.market_cap
            )
        median_by_factors = [
            (factor_dict, stat.median(factor_dict.values()))
            for factor_dict in [
                roa_1y_by_symbol,
                cf_roa_by_symbol,
                rd_to_mkt_cap_by_symbol,
                cap_ex_to_mkt_cap_by_symbol,
                ad_to_mkt_cap_by_symbol,
            ]
        ]
        
        # Sort all stocks by book-to-market ratio and get the lower quintile.
        has_book = [
            f for f in fundamentals 
            if not any([np.isnan(factor) or factor == 0 for factor in [
                f.financial_statements.balance_sheet.net_tangible_assets.twelve_months,
                f.market_cap
            ]])
        ]
        sorted_by_BM = sorted(
            has_book, 
            key=lambda f: f.financial_statements.balance_sheet.net_tangible_assets.twelve_months / f.market_cap
        )[:len(has_book)//4]
        # Choose tech stocks from the lower quintile.
        tech_symbols = [f.symbol for f in sorted_by_BM if f in tech_securities]
        
        def compute_g_score(symbol):
            g_score = 0
            if cf_roa_by_symbol[symbol] > roa_1y_by_symbol[symbol]:
                g_score += 1
            if symbol in tech_VARROA and tech_VARROA[symbol] < tech_VARROA_median:
                g_score += 1
            for factor_by_symbol, median in median_by_factors:
                if symbol in factor_by_symbol and factor_by_symbol[symbol] > median:
                    g_score += 1
            return g_score
        
        # Compute g-scores for each asset.
        g_scores = {symbol : compute_g_score(symbol) for symbol in tech_symbols}
        # Apply G-score threshold.
        return [symbol for symbol, g_score in g_scores.items() if g_score >= 5]