Overall Statistics
Total Orders
1208
Average Win
0.66%
Average Loss
-0.63%
Compounding Annual Return
5.466%
Drawdown
35.300%
Expectancy
0.093
Start Equity
1000000
End Equity
1304983.45
Net Profit
30.498%
Sharpe Ratio
0.073
Sortino Ratio
0.083
Probabilistic Sharpe Ratio
2.853%
Loss Rate
47%
Win Rate
53%
Profit-Loss Ratio
1.06
Alpha
-0.026
Beta
0.607
Annual Standard Deviation
0.168
Annual Variance
0.028
Information Ratio
-0.327
Tracking Error
0.154
Treynor Ratio
0.02
Total Fees
$3830.79
Estimated Strategy Capacity
$610000000.00
Lowest Capacity Asset
BEN R735QTJ8XC9X
Portfolio Turnover
4.84%
Drawdown Recovery
1389
# region imports
from AlgorithmImports import *
# endregion


class FundamentalFactorAlphaModel(AlphaModel):
    
    def __init__(self, algorithm, date_rule, num_assets, quality_weight, value_weight, size_weight):
        self._algorithm = algorithm
        self._num_assets = num_assets
        self._securities = []
        self._insights = []
        # Add a Scheduled Event to create new insights each month.
        algorithm.schedule.on(date_rule, algorithm.time_rules.at(8, 0), self._create_insights)
        # Normalize the quality, value, and size weights.
        weight_sum = sum([quality_weight, value_weight, size_weight])
        self._quality_weight = quality_weight / weight_sum
        self._value_weight = value_weight / weight_sum
        self._size_weight = size_weight / weight_sum

    # As assets enter and leave the universe, update our `_securities` list.
    def on_securities_changed(self, algorithm, changes):
        for security in changes.added_securities:
            self._securities.append(security)
        for security in changes.removed_securities:
            if security in self._securities:
                self._securities.remove(security)

    def _create_insights(self):
        # Assign quality, value, size score to each stock.
        quality_by_security = self._get_scores([
            (lambda x: x.fundamentals.operation_ratios.gross_margin.value, True, 2), 
            (lambda x: x.fundamentals.operation_ratios.quick_ratio.Value, True, 1), 
            (lambda x: x.fundamentals.operation_ratios.debt_to_assets.Value, False, 2)
        ])
        value_by_security = self._get_scores([
            (lambda x: x.fundamentals.valuation_ratios.book_value_per_share, True, 0.5),
            (lambda x: x.fundamentals.valuation_ratios.cash_return, True, 0.25),
            (lambda x: x.fundamentals.valuation_ratios.earning_yield, True, 0.25)
        ])
        size_by_security = self._get_scores([(lambda x: x.fundamentals.market_cap, False, 1)])
        # Assign a combined score to each stock.
        score_by_security = {
            symbol: (
                quality_value * self._quality_weight 
                + value_by_security[symbol] * self._value_weight 
                + size_by_security[symbol] * self._size_weight
            )
            for symbol, quality_value in quality_by_security.items()   
        }
        # Sort the securities by their scores.
        longs = sorted(score_by_security, key=lambda security: score_by_security[security])[:self._num_assets]
        # Create insights for each stock.
        self._insights = [
            Insight.price(security, Expiry.END_OF_MONTH, InsightDirection.UP, weight=(len(longs) - i)**2)
            for i, security in enumerate(longs)
        ]

    def update(self, algorithm, data):
        insights = self._insights.copy()
        self._insights.clear()
        return insights

    def _get_scores(self, fundamentals):
        '''Assigns scores to each stock in securities
        Args: 
            fundamentals: list of 3-tuples (lambda function, bool, float)
        Returns:
            Dictionary with score for each security'''
        # Normalize the factor weights.
        weights = [tup[2] for tup in fundamentals]
        weights = [float(i)/sum(weights) for i in weights]
        # Sort the securities by each factor.
        sorted_securities = [
            sorted(self._securities, key=fundamental, reverse=reverse)
            for fundamental, reverse, _ in fundamentals
        ]
        # Calculate a score for each stock.
        return {
            security: sum([
                sorted_securities[i].index(security) * weights[i] 
                for i in range(len(fundamentals))
            ])
            for security in self._securities    
        }
        
# region imports
from AlgorithmImports import *
# endregion
from AlphaModel import FundamentalFactorAlphaModel


class VerticalTachyonRegulators(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(1_000_000)
        self.settings.seed_initial_prices = True
        # Add universe selection.
        self._liquidity_filter_size = 200
        self.universe_settings.resolution = Resolution.DAILY
        date_rule = self.date_rules.month_start('SPY')
        self.universe_settings.schedule.on(date_rule)
        self.add_universe(self._select_assets)
        # Add an Alpha model.
        self.add_alpha(FundamentalFactorAlphaModel(self, date_rule, 20, 2, 2, 1))
        # Add a portfolio construction model.
        self.settings.rebalance_portfolio_on_insight_changes = False
        self.settings.rebalance_portfolio_on_security_changes = False
        self.set_portfolio_construction(InsightWeightingPortfolioConstructionModel(date_rule))
        # Add a risk model.
        self.set_risk_management(TrailingStopRiskManagementModel(self.get_parameter("stopRisk", 0.1)))
        # Add an execution model.
        self.set_execution(ImmediateExecutionModel())
        # Plot the portfolio state each week.
        self.schedule.on(
            self.date_rules.every(DayOfWeek.MONDAY), 
            self.time_rules.at(10, 30),
            lambda: self.plot(
                "Positions", 
                "Num", 
                len([symbol for symbol, holding in self.portfolio.items() if holding.invested])
            )
        )
        # Add a warm-up period so the algorithm trades on deployment.
        self.set_warm_up(timedelta(45))

    def _select_assets(self, fundamentals):        
        # Select only those with fundamental data and a sufficiently large price.
        filtered = [f for f in fundamentals if f.has_fundamental_data and f.price > 5]
        # Select the subset of stocks that are most liquid.
        filtered = sorted(filtered, key = lambda x: x.dollar_volume)[-self._liquidity_filter_size:]
        # Select the subset of stocks with the following fundamental factors:
        return [
            f.symbol for f in filtered 
            if (f.operation_ratios.gross_margin.value > 0 and 
                f.operation_ratios.quick_ratio.value > 0 and 
                f.operation_ratios.debt_to_assets.value > 0 and 
                f.valuation_ratios.book_value_per_share > 0 and 
                f.valuation_ratios.cash_return > 0 and 
                f.valuation_ratios.earning_yield > 0 and 
                f.market_cap > 0)
        ]