Overall Statistics
Total Orders
12
Average Win
2.65%
Average Loss
-0.87%
Compounding Annual Return
0.396%
Drawdown
4.700%
Expectancy
1.521
Start Equity
100000
End Equity
110531.61
Net Profit
10.532%
Sharpe Ratio
-0.922
Sortino Ratio
-0.121
Probabilistic Sharpe Ratio
0.000%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
3.03
Alpha
-0.02
Beta
0.014
Annual Standard Deviation
0.022
Annual Variance
0
Information Ratio
-0.38
Tracking Error
0.159
Treynor Ratio
-1.438
Total Fees
$95.78
Estimated Strategy Capacity
$990000.00
Lowest Capacity Asset
IEF SGNKIKYGE9NP
Portfolio Turnover
0.17%
# https://quantpedia.com/strategies/front-running-rebalancing-signals/
# 
# The investment universe for this strategy primarily consists of equity and bond markets, specifically focusing on the S&P 500 Index and the 10-year Treasury note. 
# (The selection of these instruments is based on their representation of a typical balanced portfolio, such as the 60/40 equity/bond allocation commonly used by 
# institutional investors. The strategy also considers futures contracts for these indices, as they provide liquidity and the ability to execute trades efficiently. 
# The selection of individual instruments is guided by the predictable rebalancing activities of significant funds, which are known to impact these markets.)
# (Data Sources: Daily prices for the E-mini S&P 500 Index and the 10-year Treasury note from Bloomberg, also obtain daily index data for the S&P 500 Total Return 
# Index, the Bloomberg U.S. Aggregate Bond Total Return Index, and international equities well as implied volatility measures from there too. Commodity Futures 
# Trading Commission (CFTC) data, Federal Reserve’s Financial Accounts database, the National Center for Education Statistics.)
# Broad Overview: The strategy employs signals derived from rebalancing activities, specifically Threshold and Calendar signals. The Threshold signal is generated
# by monitoring deviations from target equity and bond allocations. For a 60/40 portfolio, deviations are calculated based on excess returns, and signals are 
# triggered when these deviations exceed a predefined threshold, such as ±2.5%. The Calendar signal focuses on end-of-month effects, capturing patterns of 
# rebalancing activities during the last week of each month. Buy and sell rules are straightforward: buy equities or bonds when they are underweight relative to 
# target allocations and sell when they are overweight. The strategy anticipates price reversals by adjusting positions based on Calendar signals, particularly at 
# month-end.
# Individual Signals: Subsection 2.2 describes the signal precisely, detailing how threshold and calendar signals are composed and combined.
# For the Threshold signal, consider regression eq. (1) where the dependent variable R_et_t+1 is the difference between S&P 500 and 10-year Treasury note futures 
# returns and the independent variable Threshold Signal^δ_t.
# For the Calendar signal, consider regression eq. (3), which has the same dependent variable but a different independent variable Calendar Signal_t.
# Combined Strategy Variant: Perform rebalancing-based strategy R^Strategy_t constructed as described in Section 5:
# Build a simple, implementable, real-time trading strategy by combining Threshold and Calendar signals. This strategy simulates an investor’s actions who, based 
# on rebalancing signals, enters the equity and bond markets as a front-runner. On average, this investor buys equities and sells bonds after bonds have relatively 
# outperformed and buys bonds while selling equities after equities have outperformed.
# The trading strategy takes a position in an S&P 500 futures contract and an opposite position in a 10-year Treasury note futures contract as follows formula on 
# pg. 32 (section 5 Front-Running Rebalancers), where the portfolio weight w-Strategy_t is defined as the average of modified versions of the Threshold and Calendar 
# signals. The Threshold signal is rescaled to −(Threshold Signal_t / 1.5%) to ensure that both rebalancing signals contribute approximately equal risk to the 
# trading strategy. Take the negative of the signal since a positive Threshold signal indicates that the S&P 500 is overweight relative to the 10-year Treasury note.
# While the Threshold strategy can take a position on any day of the month, the Calendar strategy focuses on the end-of-month effect. Therefore, the Calendar signal 
# is modified to sign (−Calendar Signalt) if t falls within the last week of a month to capture the “week4” effect. Furthermore, on the first business day of a new 
# month, the modified Calendar signal is set to sign Calendar Signal−4 to capture potential reversal effects. On any other day, the modified version of the signal 
# is set to zero.
# Rebalancing & Weighting: Position sizes are determined as a fixed percentage of the portfolio’s total value, ensuring consistent exposure across trades. Rebalanced monthly.
# 
# Implementation changes:
#   - Excess returns predefined threshold is set to 2.5%
#   - Portfolio allocation change is calculated 4 days before month end.

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

class FrontRunningRebalancingSignals(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2000, 1, 1)
        self.set_cash(100_000)
        
        leverage: int = 4
        days_offset: int = 4
        self._threshold: float = .03
        self._allocations: List[float] = [.6, .4]

        self._spy: Symbol = self.add_equity("SPY", Resolution.MINUTE, leverage=leverage).symbol
        self._ief: Symbol = self.add_equity("IEF", Resolution.MINUTE, leverage=leverage).symbol

        self._rebalance_flag: bool = False
        self._reverse_flag: bool = False
        self.schedule.on(   # Open position n days before month end.
            self.date_rules.month_end(self._spy, days_offset),
            self.time_rules.after_market_open(self._spy),
            self._rebalance
        )

        self.schedule.on(   # Reverse positions.
            self.date_rules.month_start(self._spy),
            self.time_rules.after_market_open(self._spy),
            self._reverse
        )

        self.schedule.on(   # Liquidate positions.
            self.date_rules.month_start(self._spy, 1),
            self.time_rules.after_market_open(self._spy),
            lambda: self.liquidate()
        )

    def on_data(self, slice: Slice) -> None:
        # Reverse position.
        if self._reverse_flag:
            self._reverse_flag = False

            targets: List[PortfolioTarget] = []
            for i, symbol in enumerate([self._spy, self._ief]):
                if slice.contains_key(symbol) and slice[symbol]:
                    targets.append(PortfolioTarget(symbol, -1 * np.sign(self.portfolio[symbol].quantity)))
            
            self.set_holdings(targets, True)

        if not all(self.securities[symbol].get_last_data() for symbol in [self._spy, self._ief]):
            self._rebalance_flag = False
            self.log('insufficient data for signal determination.')
            return

        if not self._rebalance_flag:
            return
        self._rebalance_flag = False

        # Last month performance.
        history: DataFrame = self.history([self._spy, self._ief], start=self.time - relativedelta(months=1), end=self.time).unstack(level=0).resample('D').last()
        history = history[history.index.month == self.time.month]
        last_month_performance: DataFrame = history.close.iloc[-1] / history.close.iloc[0] - 1
        
        if len(last_month_performance.dropna()) < 2:
            return

        spy_allocation, ief_allocation = self._allocations[0], self._allocations[1]

        spy_returns: float = spy_allocation * (1 + last_month_performance[self._spy])
        ief_returns: float = ief_allocation * (1 + last_month_performance[self._ief])

        # Calculate new allocations.
        spy_new_allocation: float = spy_returns / (spy_returns + ief_returns)
        allocation_diff: float = spy_new_allocation - spy_allocation

        # Threshold and trade execution.
        if abs(allocation_diff) > self._threshold:
            traded_directions: List[int] = [-1 * np.sign(allocation_diff), np.sign(allocation_diff)]

            targets: List[PortfolioTarget] = []
            for i, symbol in enumerate([self._spy, self._ief]):
                if slice.contains_key(symbol) and slice[symbol]:
                    targets.append(PortfolioTarget(symbol, traded_directions[i]))
            
            self.set_holdings(targets, True)

    def _rebalance(self) -> None:
        self._rebalance_flag = True

    def _reverse(self) -> None:
        if self.portfolio.invested:
            self._reverse_flag = True