Overall Statistics
Total Orders
1085
Average Win
1.49%
Average Loss
-1.26%
Compounding Annual Return
5.261%
Drawdown
40.300%
Expectancy
0.068
Start Equity
100000
End Equity
129232.43
Net Profit
29.232%
Sharpe Ratio
0.141
Sortino Ratio
0.157
Probabilistic Sharpe Ratio
2.346%
Loss Rate
51%
Win Rate
49%
Profit-Loss Ratio
1.18
Alpha
0.019
Beta
0.334
Annual Standard Deviation
0.282
Annual Variance
0.079
Information Ratio
-0.079
Tracking Error
0.293
Treynor Ratio
0.118
Total Fees
$1539.19
Estimated Strategy Capacity
$100000000.00
Lowest Capacity Asset
CORT SXTX9AOGG2G5
Portfolio Turnover
5.88%
Drawdown Recovery
671
#region imports
from AlgorithmImports import *
#endregion


class SeasonalitySignalAlgorithm(QCAlgorithm):
    '''
    A strategy that takes long and short positions based on historical same-calendar month returns
    Paper: https://www.nber.org/papers/w20815.pdf
    '''

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(100_000)
        self.settings.seed_initial_prices = True
        # Define some parameters.
        self._liquidity_filter_size = 100
        self._position_per_size = 5 
        # Add a universe of US Equities based on historical 
        # same-calendar month returns.
        self._date_rule = self.date_rules.month_start('SPY')
        self.universe_settings.schedule.on(self._date_rule)
        self.universe_settings.resolution = Resolution.DAILY
        self._universe = self.add_universe(self._select_assets)
        # Add a warm-up period so that algorithm trades on deployment.
        self.set_warm_up(timedelta(45))

    def _select_assets(self, fundamentals):
        '''
        Universe selection based on historical same-calendar month returns
        '''
        # Select the most liquid US Equities above $5.
        filtered = [f for f in fundamentals if f.price > 5]
        filtered = sorted(filtered, key=lambda x: x.dollar_volume)[-self._liquidity_filter_size:]
        # Get price data of all the filtered Equities for the same 
        # calendar month in the previous year.
        start = self.time.replace(day=1, year=self.time.year-1)
        end = Expiry.END_OF_MONTH(start) - timedelta(1)
        history = self.history([f.symbol for f in filtered], start, end, Resolution.DAILY).close.unstack(level=0)
        # Calculate the monthly return of each Equity during that period.
        monthly_return_by_symbol = {
            symbol: prices.iloc[-1] / prices.iloc[0] - 1 
            for symbol, prices in history.items()
        }
        # Split the Equities into long and short buckets.
        sorted_by_return = sorted(monthly_return_by_symbol, key=lambda symbol: monthly_return_by_symbol[symbol])
        self._longs = sorted_by_return[-self._position_per_size:]
        self._shorts = sorted_by_return[:self._position_per_size]
        return self._longs + self._shorts

    def on_warmup_finished(self):
        # Add a Scheduled Event to rebalance the portfolio each month.
        time_rule = self.time_rules.at(8, 0)
        self.schedule.on(self._date_rule, time_rule, self._rebalance)
        # Rebalance today too.
        if self.live_mode:
            self._rebalance()
        else:
            self.schedule.on(self.date_rules.today, time_rule, self._rebalance)

    def _rebalance(self):
        '''
        Rebalance every month based on same-calendar month returns effect
        '''
        # Form a long-short portfolio.
        targets = [PortfolioTarget(symbol, 0.5/len(self._longs)) for symbol in self._longs]
        targets += [PortfolioTarget(symbol, -0.5/len(self._shorts)) for symbol in self._shorts]
        self.set_holdings(targets, True)