Overall Statistics
Total Orders
140
Average Win
0.68%
Average Loss
-0.26%
Compounding Annual Return
59.578%
Drawdown
9.700%
Expectancy
2.135
Start Equity
1000000
End Equity
1599193.88
Net Profit
59.919%
Sharpe Ratio
2.387
Sortino Ratio
3.405
Probabilistic Sharpe Ratio
94.859%
Loss Rate
14%
Win Rate
86%
Profit-Loss Ratio
2.66
Alpha
0.157
Beta
1.185
Annual Standard Deviation
0.147
Annual Variance
0.022
Information Ratio
2.211
Tracking Error
0.085
Treynor Ratio
0.296
Total Fees
$632.25
Estimated Strategy Capacity
$3900000.00
Lowest Capacity Asset
TIPX VGZQ8WW1GODH
Portfolio Turnover
1.66%
#region imports
from AlgorithmImports import *
#endregion


class LongMonthlyAlphaModel(AlphaModel):

    _securities = []

    def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
        return [Insight.price(security.symbol, Expiry.END_OF_MONTH, InsightDirection.UP) for security in self._securities]

    def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
        for security in changes.removed_securities:
            if security in self._securities:
                self._securities.remove(security)
        self._securities.extend(changes.added_securities)
#region imports
from AlgorithmImports import *

from universe import JanuaryEffectUniverseSelectionModel
from alpha import LongMonthlyAlphaModel
#endregion


class JanuaryEffectInStocksAlgorithm(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.settings.minimum_order_margin_portfolio_percentage = 0
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))
        
        self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
        self.universe_settings.schedule.on(self.date_rules.month_start())
        self.add_universe_selection(JanuaryEffectUniverseSelectionModel(
            self,
            self.universe_settings,
            self.get_parameter("coarse_size", 1_000),
            self.get_parameter("fine_size", 10)
        ))

        self.add_alpha(LongMonthlyAlphaModel())

        self.settings.rebalance_portfolio_on_security_changes = False
        self.settings.rebalance_portfolio_on_insight_changes = False
        self.month = -1
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(self._rebalance_func))

        self.add_risk_management(NullRiskManagementModel())

        self.set_execution(ImmediateExecutionModel())

        self.set_warm_up(timedelta(31))

    def _rebalance_func(self, time):
        if self.month != self.time.month and not self.is_warming_up and self.current_slice.quote_bars.count > 0:
            self.month = self.time.month
            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 JanuaryEffectUniverseSelectionModel(FundamentalUniverseSelectionModel):
    def __init__(self, algorithm: QCAlgorithm, universe_settings: UniverseSettings = None, coarse_size: int = 1_000, fine_size: int = 10) -> None:
        def select(fundamental):
            # Select the securities that have the most dollar volume
            shortlisted = [c for c in sorted(fundamental, key=lambda x: x.dollar_volume, reverse=True)[:coarse_size]]
            
            fine = [i for i in shortlisted if i.earning_reports.basic_average_shares.three_months!=0
                                    and i.earning_reports.basic_eps.twelve_months!=0
                                    and i.valuation_ratios.pe_ratio!=0]
            # Sort securities by market cap
            sorted_by_market_cap = sorted(fine, key = lambda x: x.market_cap, reverse=True)
            # In January, select the securities with the smallest market caps
            if algorithm.time.month == 1:
                return [f.symbol for f in sorted_by_market_cap[-fine_size:]]
            # If it's not January, select the securities with the largest market caps
            return [f.symbol for f in sorted_by_market_cap[:fine_size]]
        
        super().__init__(select, universe_settings)