Overall Statistics
Total Orders
3958
Average Win
0.12%
Average Loss
-0.12%
Compounding Annual Return
14.567%
Drawdown
15.400%
Expectancy
0.366
Start Equity
100000
End Equity
206937.98
Net Profit
106.938%
Sharpe Ratio
0.732
Sortino Ratio
0.766
Probabilistic Sharpe Ratio
45.685%
Loss Rate
35%
Win Rate
65%
Profit-Loss Ratio
1.09
Alpha
0.048
Beta
0.373
Annual Standard Deviation
0.103
Annual Variance
0.011
Information Ratio
0.006
Tracking Error
0.137
Treynor Ratio
0.203
Total Fees
$4473.52
Estimated Strategy Capacity
$2000000.00
Lowest Capacity Asset
SO R735QTJ8XC9X
Portfolio Turnover
5.04%
from AlgorithmImports import *
import numpy as np

class MonthlyVolatilityEffectAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2025, 6, 1)
        self.SetCash(100000)

        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction)
        self.AddAlpha(NullAlphaModel())  # Manual alpha emission
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        self.SetRiskManagement(TrailingStopRiskManagementModel(0.05))
        self.SetRiskManagement(MaximumDrawdownPercentPortfolio(0.1))  

        self.last_selection_time = datetime.min
        self.selected_symbols = []

        # Schedule: Emit insights only on first Monday of each month at 10:00 AM
        self.Schedule.On(
            self.DateRules.Every(DayOfWeek.Monday),
            self.TimeRules.At(10, 0),
            self.CheckAndEmitMonthlyInsights
        )

    def CoarseSelectionFunction(self, coarse):
        # Limit selection to once per month
        if (self.Time - self.last_selection_time).days < 30:
            return Universe.Unchanged

        self.last_selection_time = self.Time

        # Filter for price > $5 and high volume
        filtered = [x for x in coarse if x.HasFundamentalData and x.Price > 5]
        sorted_by_volume = sorted(filtered, key=lambda x: x.Volume, reverse=True)[:500]
        symbols = [x.Symbol for x in sorted_by_volume]

        # 60-day historical volatility
        history = self.History(symbols, 60, Resolution.Daily)
        if history.empty:
            return []

        vol_dict = {}
        for symbol in symbols:
            if symbol not in history.index.levels[0]:
                continue
            close = history.loc[symbol]['close']
            if len(close) < 60:
                continue
            returns = close.pct_change().dropna()
            vol = returns.std() * np.sqrt(252)
            vol_dict[symbol] = vol

        # Select 10 lowest-volatility stocks
        sorted_vols = sorted(vol_dict.items(), key=lambda x: x[1])[:30]
        self.selected_symbols = [s[0] for s in sorted_vols]

        self.Debug(f"{self.Time.date()} - Selected: {[str(s) for s in self.selected_symbols]}")
        return self.selected_symbols

    def CheckAndEmitMonthlyInsights(self):
        today = self.Time.date()
        # Emit only on the first Monday of each month
        if today.day <= 7 and today.weekday() == 0:
            self.Debug(f"{today} is the first Monday of the month. Emitting insights.")
            self.EmitMonthlyInsights()

    def EmitMonthlyInsights(self):
        if not self.selected_symbols:
            self.Debug("No symbols to emit insights for.")
            return

        insights = [
            Insight.Price(symbol, timedelta(days=30), InsightDirection.Up)
            for symbol in self.selected_symbols
        ]
        self.Debug(f"{self.Time.date()} - Emitting insights for: {[str(i.Symbol) for i in insights]}")
        self.EmitInsights(insights)

    def OnSecuritiesChanged(self, changes):
        for removed in changes.RemovedSecurities:
            if self.Portfolio[removed.Symbol].Invested:
                self.Liquidate(removed.Symbol)