Overall Statistics
Total Orders
1671
Average Win
0.26%
Average Loss
-0.20%
Compounding Annual Return
4.431%
Drawdown
17.900%
Expectancy
0.067
Start Equity
1000000
End Equity
1044517.15
Net Profit
4.452%
Sharpe Ratio
-0.038
Sortino Ratio
-0.055
Probabilistic Sharpe Ratio
18.434%
Loss Rate
54%
Win Rate
46%
Profit-Loss Ratio
1.33
Alpha
0.004
Beta
-0.063
Annual Standard Deviation
0.177
Annual Variance
0.031
Information Ratio
-0.818
Tracking Error
0.207
Treynor Ratio
0.106
Total Fees
$7411.10
Estimated Strategy Capacity
$5300000.00
Lowest Capacity Asset
CRBP VZR6X1TTY8H1
Portfolio Turnover
17.77%
#region imports
from AlgorithmImports import *
#endregion


class ShortTermReversalAlphaModel(AlphaModel):

    _securities = []
    _week = 1

    def __init__(self, num_securities_per_side, lookback_days):
        self._num_securities_per_side = num_securities_per_side
        self._LOOKBACK_DAYS = lookback_days

    def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
        # Reset indicators when corporate actions occur
        securities_to_reset = []
        for symbol in set(data.splits.keys() + data.dividends.keys()):
            security = algorithm.securities[symbol]
            if security in self._securities:
                algorithm.unregister_indicator(security.indicator)
                securities_to_reset.append(security)
        if securities_to_reset:
            self._set_up_indicators(algorithm, securities_to_reset, False)

        # Only emit insights when there is quote data, not when we get corporate action
        if data.quote_bars.count == 0:
            return []

        # Rebalance weekly
        week = data.time.date().isocalendar()[1]
        if self._week == week:
            return []
        self._week = week
       
        # Select securities that have the highest/lowest factor values
        tradable_securities = [s for s in self._securities if s.indicator.is_ready and s.symbol in data.quote_bars and s.price]
        sorted_by_roc = sorted(tradable_securities, key=lambda s: s.indicator.current.value)
        longs = sorted_by_roc[:self._num_securities_per_side]
        shorts = sorted_by_roc[-self._num_securities_per_side:]

        # Create insights to long stocks with the lowest ROC and short stocks with the greatest ROC. 
        # Hold positions until the next month.
        insights = [Insight.price(security.symbol, Expiry.END_OF_WEEK, InsightDirection.UP) for security in longs]
        insights += [Insight.price(security.symbol, Expiry.END_OF_WEEK, InsightDirection.DOWN) for security in shorts]
        return insights

    def _set_up_indicators(self, algorithm, securities, save_security_reference=True):
        # Create and register indicator for each security in the universe
        security_by_symbol = {}
        for security in securities:
            security.indicator = algorithm.roc(security.symbol, self._LOOKBACK_DAYS, Resolution.DAILY)
            security_by_symbol[security.symbol] = security
            if save_security_reference:
                self._securities.append(security)
        
        # Warm up the indicators of newly-added stocks
        if security_by_symbol:
            history = algorithm.history[TradeBar](list(security_by_symbol.keys()), (self._LOOKBACK_DAYS+1), Resolution.DAILY, data_normalization_mode=DataNormalizationMode.SCALED_RAW)
            for trade_bars in history:
                for bar in trade_bars.values():
                    security_by_symbol[bar.symbol].indicator.update(bar.end_time, bar.close)

    def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        self._set_up_indicators(algorithm, changes.added_securities)

        # Stop updating indicators when the security leaves the universe
        for security in changes.removed_securities:
            if security in self._securities:
                algorithm.unregister_indicator(security.indicator)
                self._securities.remove(security)

#region imports
from AlgorithmImports import *

from universe import MostLiquidFundamentalUniverseSelectionModel
from alpha import ShortTermReversalAlphaModel
#endregion


class ShortTermReversalAlgorithm(QCAlgorithm):

    _undesired_symbols_from_previous_deployment = []
    _checked_symbols_from_previous_deployment = False

    def initialize(self):
        self.set_start_date(2023, 3, 1)
        self.set_end_date(2024, 3, 1) 
        self.set_cash(1_000_000)

        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))

        self.settings.minimum_order_margin_portfolio_percentage = 0

        self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
        self.universe_settings.schedule.on(self.date_rules.week_start())
        universe_size = self.get_parameter("universe_size", 100)
        self.add_universe_selection(MostLiquidFundamentalUniverseSelectionModel(self.universe_settings, universe_size))

        self.add_alpha(ShortTermReversalAlphaModel(
            int(self.get_parameter("roc_selection_factor", 0.1) * universe_size),
            self.get_parameter("lookback_days", 22)
        ))

        self.settings.rebalance_portfolio_on_security_changes = False
        self.settings.rebalance_portfolio_on_insight_changes = False
        self._week = -1
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(self.rebalance_func))

        self.add_risk_management(NullRiskManagementModel())

        self.set_execution(ImmediateExecutionModel())

        self.set_warm_up(timedelta(14))

    def rebalance_func(self, time):
        # Rebalance weekly
        week = self.time.date().isocalendar()[1]
        if self._week != week and not self.is_warming_up and self.current_slice.quote_bars.count > 0:
            self._week = week
            return time
        return None

    def on_data(self, data):
        # Exit positions that aren't backed by existing insights.
        # If you don't want this behavior, delete this method definition.
        if not self.is_warming_up and not self._checked_symbols_from_previous_deployment:
            for security_holding in self.portfolio.values():
                if not security_holding.invested:
                    continue
                symbol = security_holding.symbol
                if not self.insights.has_active_insights(symbol, self.utc_time):
                    self._undesired_symbols_from_previous_deployment.append(symbol)
            self._checked_symbols_from_previous_deployment = True
        
        for symbol in self._undesired_symbols_from_previous_deployment:
            if self.is_market_open(symbol):
                self.liquidate(symbol, tag="Holding from previous deployment that's no longer desired")
                self._undesired_symbols_from_previous_deployment.remove(symbol)
#region imports
from AlgorithmImports import *
#endregion


class MostLiquidFundamentalUniverseSelectionModel(CoarseFundamentalUniverseSelectionModel):

    def __init__(self, universe_settings: UniverseSettings, universe_size: int = 100) -> None:
        self._universe_size = universe_size
        super().__init__(self.select_coarse, universe_settings)

    def select_coarse(self, coarse: List[CoarseFundamental]) -> List[Symbol]:
        # Select the stocks with the greatest dollar volume
        sorted_by_dollar_volume = sorted(coarse, key=lambda c: c.dollar_volume, reverse=True)
        return [c.symbol for c in sorted_by_dollar_volume[:self._universe_size]]