Overall Statistics
Total Orders
225
Average Win
2.30%
Average Loss
-1.73%
Compounding Annual Return
11.489%
Drawdown
35.900%
Expectancy
0.350
Start Equity
100000
End Equity
172179.65
Net Profit
72.180%
Sharpe Ratio
0.294
Sortino Ratio
0.366
Probabilistic Sharpe Ratio
7.109%
Loss Rate
42%
Win Rate
58%
Profit-Loss Ratio
1.33
Alpha
0.009
Beta
0.739
Annual Standard Deviation
0.205
Annual Variance
0.042
Information Ratio
-0.052
Tracking Error
0.18
Treynor Ratio
0.081
Total Fees
$1290.80
Estimated Strategy Capacity
$220000.00
Lowest Capacity Asset
VSTA XGMI1RX3ULPH
Portfolio Turnover
0.53%
Drawdown Recovery
430
# region imports
from AlgorithmImports import *
# endregion


class VerticalTachyonRegulators(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(100_000)
        self.settings.seed_initial_prices = True
        # Add a universe of US Equities.
        self._universe_size = 25
        self._date_rule = self.date_rules.year_start("SPY")
        self.universe_settings.resolution = Resolution.DAILY
        self.universe_settings.schedule.on(self._date_rule)
        self.add_universe(self._select)
        # Define the factors and their weights.
        self._factor_by_weights = {'quality': 0, 'value': 2/3, 'size': 1/3}
        self._specs = {
            'quality': [
                # True means higher value should translate to higher portfolio weight.
                (lambda x: x.operation_ratios.quick_ratio.value, True, 1)
            ],  
            'value': [
                (lambda x: x.valuation_ratios.total_yield, True, 1),
                (lambda x: x.valuation_ratios.earning_yield, True, 1),
                (lambda x: x.operation_ratios.total_assets_growth.one_year, False, 1),
                (lambda x: x.valuation_ratios.ev_to_ebit, False, 1),
                (lambda x: x.valuation_ratios.book_value_yield, True, 0.5),
            ],
            'size': [
                (lambda x: x.market_cap, False, 1)
            ]
        }
        # Add warmup so the algorithm trades on deployment.
        self.set_warmup(timedelta(400))

    def _select(self, fundamentals: List[Fundamental]) -> List[Symbol]:
        # Filter to top 50k most liquid stocks with fundamental data.
        top_liquid = sorted(
            [f for f in fundamentals if f.has_fundamental_data],
            key=lambda f: f.dollar_volume
        )[-50_000:]
        # Select small-cap value stocks with positive valuation metrics.
        small_cap_value = [
            f for f in top_liquid
            if (80_000_000 < f.market_cap < 1_000_000_000 and 
                f.valuation_ratios.book_value_yield > 0 and
                f.valuation_ratios.total_yield > 0 and
                f.valuation_ratios.earning_yield > 0 and
                f.valuation_ratios.ev_to_ebit > 0)
        ]
        if not small_cap_value:
            return []
        # Rank all the assets by their factor values.
        factor_scores = {
            name: self._rank_by_factors(small_cap_value, fs)
            for name, fs in self._specs.items()
        }
        score_by_symbol = {
            f.symbol: sum(
                factor_scores[name][f.symbol] * self._factor_by_weights[name]
                for name in self._specs
            )
            for f in small_cap_value
        }
        # Select the assets with the best scores.
        self._universe = sorted(score_by_symbol, key=lambda symbol: score_by_symbol[symbol], reverse=True)[:self._universe_size]
        return self._universe

    def _rank_by_factors(self, securities: List[Fundamental], factors: List[tuple]) -> dict:
        # Normalize factor weights.
        weights = [f[2] for f in factors]
        total = sum(weights)
        weights = [w / total for w in weights]
        # Compute rank dictionaries for each factor.
        rank_dicts = []
        for accessor, higher_is_better, _ in factors:
            ranked = sorted(securities, key=accessor, reverse=not higher_is_better)
            rank_dicts.append({s.symbol: i for i, s in enumerate(ranked)})
        # Combine ranks across factors with weighted average.
        return {
            s.symbol: sum(
                rank_dicts[j].get(s.symbol) * weights[j]
                for j in range(len(factors))
            )
            for s in securities
        }

    def on_warmup_finished(self):
        # Add a Scheduled event to rebalance the portfolio yearly.
        time_rule = self.time_rules.at(8, 0)
        self.schedule.on(self._date_rule, time_rule, self._rebalance)
        # Rebalance the portfolio today too.
        self._rebalance()

    def _rebalance(self):
        # Apply quadratic weighting (highest rank gets highest weight).
        weights = {
            symbol: float(len(self._universe) - i) ** 2 
            for i, symbol in enumerate(self._universe)
        }
        # Make weights sum to 1.
        total_weight = sum(weights.values())
        weights = {symbol: w / total_weight for symbol, w in weights.items()}
        # Place orders to rebalance the portfolio.
        targets = [PortfolioTarget(symbol, weight) for symbol, weight in weights.items()]
        self.set_holdings(targets, True)