Created with Highcharts 12.1.2Equity200020022004200620082010201220142016201820202022202420260250k500k-20000.050.101201G020M40M242526
Overall Statistics
Total Orders
236
Average Win
2.89%
Average Loss
-1.78%
Compounding Annual Return
5.698%
Drawdown
33.600%
Expectancy
0.721
Start Equity
100000
End Equity
403785.13
Net Profit
303.785%
Sharpe Ratio
0.216
Sortino Ratio
0.139
Probabilistic Sharpe Ratio
0.031%
Loss Rate
34%
Win Rate
66%
Profit-Loss Ratio
1.63
Alpha
0.005
Beta
0.407
Annual Standard Deviation
0.101
Annual Variance
0.01
Information Ratio
-0.167
Tracking Error
0.122
Treynor Ratio
0.054
Total Fees
$1897.71
Estimated Strategy Capacity
$1400000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
2.29%
# https://quantpedia.com/strategies/time-series-reversal-in-sp-500-using-eom-signal/
# 
# This strategy’s investment universe focuses on major U.S. stock indices, specifically the S&P 500. Individual instruments are selected based on their inclusion in 
# these indices, which consist of highly capitalized and liquid American companies. (The approach can also include the Dow Jones Industrial Average, as the research 
# paper suggests that the reversal pattern is evident in these indices. The strategy does not apply to smaller indices like the Russell 2000, as the reversal pattern 
# does not appear to exist.)
# (You can replicate via SPY and VOO ETFs or CFDs.)
# (Data for closing prices is from the Global Financial Data (GFD).)
# Strategy Rationale Recapitulation: The trading rules are based on the negative correlation between end-of-the-month returns and the one-month-ahead returns. The primary 
# tool calculates the end-of-the-month return, defined as the return from the fourth Friday to the month’s last trading day. The methodology generates a buy signal if the 
# end-of-the-month return is negative, indicating a potential upward reversal in the following month. Conversely, a sell signal is generated if the positive end-of-the-month 
# return suggests a potential downward reversal. The strategy does not rely on complex indicators but on this straightforward rule derived from the observed pattern.
# Our Variant Selection: Long-only.
# Trading rule (pg. 10): Buying units of the S&P 500 if the end-of-the-month return is negative. (Liquidate position at next EOM.)
# Else, stay in cash (hold risk-free asset).
# Rebalancing & Weighting: Performed every month. Always invest the entire amount allocated to the strategy only to one asset at a time.
# 
# QC Implementation changes:
#   - End-of-the-month returns are calculated as the return from the day t-3 of the month to the month’s last trading day.

# region imports
from AlgorithmImports import *
from pandas.core.frame import DataFrame
# endregion

class TimeSeriesReversalInSP500UsingEOMSignal(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2000, 1, 1)
        self.set_cash(100_000)
    
        self._history_period: int = 10
        self._observed_day: int = 4
        leverage: int = 4

        # Calculate returns from last friday in month.
        self._last_friday_flag: bool = False
        if self._last_friday_flag:
            self._observed_day = 4

        self._spy: Symbol = self.add_equity("SPY", Resolution.DAILY, leverage=leverage).symbol
        self._bil: Symbol = self.add_equity("BIL", Resolution.DAILY, leverage=leverage).symbol

        self._rebalance_flag: bool = False
        self.schedule.on(
            self.date_rules.month_end(self._spy), 
            self.time_rules.before_market_close(self._spy),
            self._rebalance
        )

    def on_data(self, slice: Slice) -> None:
        if not self._rebalance_flag:
            return
        self._rebalance_flag = False

        history: DataFrame = self.history(self._spy, timedelta(days=self._history_period)).unstack(level=0)
        if history.empty:
            return
        
        if self._last_friday_flag:
            observed_day: int = self._observed_day
            observed_day_price_df: DataFrame = history.loc[history.index.weekday == self._observed_day, 'close']
            # When friday is not trading day.
            while observed_day_price_df.empty:
                observed_day -= 1
                observed_day_price_df = history.loc[history.index.weekday == observed_day, 'close']
            observed_day_price: float = observed_day_price_df.values[0][0]
        else:
            observed_day_price = history.close.iloc[-self._observed_day].values[0]

        traded_asset: Symbol = self._spy if (history.close.iloc[-1].values[0] / observed_day_price - 1) < 0 else self._bil

        self.set_holdings(traded_asset, 1, True)

    def _rebalance(self) -> None:
        if all(self.securities[symbol].get_last_data() for symbol in [self._spy, self._bil]):
            self._rebalance_flag = True