The investment universe consists of stocks listed on the NYSE, mainly those that make up the composition of the S&P500. The variables of interest are the Fibonacci retracement levels of the desired stock. These can be computed using a simple formula L + alpha *(H-L) where H = stock all-time high, L = stock all-time low, and alpha = Fibonacci retracement level we want to compute in decimal form; levels we use are (0% (0), 38.1% (0.381), 50% (0.5), 61.2% (0.612), 100% (1)).

I. STRATEGY IN A NUTSHELL

The strategy invests in NYSE stocks, primarily S&P 500 constituents, by analyzing their prices relative to Fibonacci retracement levels (0%, 38.1%, 50%, 61.2%, 100%). Stocks approaching a retracement level from above are bought, while those approaching from below are sold short. Portfolios are equally weighted and rebalanced weekly to capture potential returns based on the predictive relationship between retracement levels and stock performance.

II. ECONOMIC RATIONALE

Empirical analysis shows that Fibonacci retracement levels can predict future stock returns. The approach leverages behavioral and technical tendencies of market participants, as price reactions near these levels tend to follow consistent, exploitable patterns across various markets.

III. SOURCE PAPER

Can Returns Breed Like Rabbits?, Econometric Tests for Fibonacci Retracements [Click to Open PDF]

Savva Shanaev, Ryan Gibson, Northumbria University, Audit Partnership Ltd

<Abstract>

This study develops a novel and intuitive econometric test to investigate the predictive power and abnormal return-generating capacity of Fibonacci retracements. Results suggest Fibonacci retracements are prominent for international stock market indices and foreign exchange rates, with 0.0%, 38.1%, 50.0%, 61.2%, and 100.0% being the most important retracements, while the inclusion of 14.6%, 23.6%, 76.4%, 78.6%, or 85.4% levels reduces the predictive power of the model. The findings cannot be explained by calendar market anomalies or return reversals. On individual stock level, an S&P 500-based strategy that longs (shorts) stocks closer to Fibonacci retracement support (resistance) generates positive and statistically significant alpha in Fama-French multi-factor models as well as demonstrates market-timing properties.

IV. BACKTEST PERFORMANCE

Annualised Return37.35%
Volatility41.85%
Beta0.046
Sharpe Ratio0.89
Sortino Ratio-0.26
Maximum DrawdownN/A
Win Rate51%

V. FULL PYTHON CODE

from AlgorithmImports import *
from data_tools import CustomFeeModel, SymbolData
from datetime import date
from pandas.core.frame import dataframe
# endregion

class FibonacciSupportsAndResistancesInCrossSectionalStockTrading(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        
        self.history_start:datetime.date = date(1999, 1, 1)

        self.leverage:int = 5
        self.quantile:int = 5
        self.total_portfolio_parts:int = 2  # long + short

        # fibinacci levels: 0, 0.381, 0.5, 0.612, 1
        self.fibonacci_levels:List[float] = [0.5]
        
        self.data:Dict[Symbol, SymbolData] = {}
        self.managed_queue:List[List[Symbol, float]] = []
        self.prev_managed_queue:List[List[Symbol, float]] = []

        self.market_symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

        self.coarse_count:int = 500
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.Schedule.On(self.DateRules.WeekStart(self.market_symbol), self.TimeRules.BeforeMarketClose(self.market_symbol, 0), self.Selection)

    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)

    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        for equity in fundamental:
            symbol:Symbol = equity.Symbol

            if symbol in self.data:
                self.data[symbol].update(equity.AdjustedPrice)

        if not self.selection_flag:
            return Universe.Unchanged
        
        selected:List[Fundamental] = sorted([x for x in fundamental if x.HasFundamentalData and \
            x.SecurityReference.ExchangeId == 'NYS' and x.MarketCap != 0],
                key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]

        warm_up_period:int = (self.Time.date() - self.history_start).days
        approach_values:Dict[float, Dict[Symbol, float]] = { fibonacci_level: {} for fibonacci_level in self.fibonacci_levels }

        for stock in selected:
            symbol:Symbol = stock.Symbol

            if symbol not in self.data:
                self.data[symbol] = SymbolData()

                history:dataframe = self.History(symbol, warm_up_period, Resolution.Daily)
                if history.empty:
                    continue
                
                closes:pd.Series = history.loc[symbol].close

                for _, close in closes.items():
                    self.data[symbol].update(close)

            if self.data[symbol].ath_atl_ready():
                for fibonacci_level in self.fibonacci_levels:
                    fib_level_value:float = self.data[symbol].get_fibonacci_level_value(fibonacci_level)
                    approach_value:float = self.data[symbol].get_approach_value(fib_level_value)

                    approach_values[fibonacci_level][symbol] = approach_value

        if len(list(approach_values.values())[0]) < self.quantile:
            return Universe.Unchanged

        selected_symbols:Set(Symbol) = set()

        total_fibonacci_levels:int = len(self.fibonacci_levels)
        for fibonacci_level, approach_value_by_symbol in approach_values.items():
            quantile:int = int(len(approach_value_by_symbol) / self.quantile)
            sorted_by_approach:List[Symbol] = [x[0] for x in sorted(approach_value_by_symbol.items(), key=lambda item: abs(item[1]))]
            lowest_quantile:List[Symbol] = sorted_by_approach[:quantile]

            long:List[Symbol] = list(filter(lambda symbol: approach_value_by_symbol[symbol] > 0, lowest_quantile))
            short:List[Symbol] = list(filter(lambda symbol: approach_value_by_symbol[symbol] < 0, lowest_quantile))

            if len(long) > 0 and len(short) > 0:
                for i, portfolio in enumerate([long, short]):
                    w:float = self.Portfolio.TotalPortfolioValue / total_fibonacci_levels / self.total_portfolio_parts / len(portfolio)
                    for symbol in portfolio:
                        selected_symbols.add(symbol)
                        quantity:float = ((-1) ** i) * np.floor(w / self.data[symbol].get_latest_price())
                        self.managed_queue.append([symbol, quantity])

        return list(selected_symbols)
        
    def OnData(self, data: Slice) -> None:
        # rebalance monthly
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # liquidate prev month trades
        for symbol, quantity in self.prev_managed_queue:
            if self.Securities[symbol].Invested:
                self.MarketOrder(symbol, -quantity)

        for symbol, quantity in self.managed_queue:
            if symbol in data and data[symbol]:
                self.MarketOrder(symbol, quantity)

        self.prev_managed_queue = self.managed_queue
        self.managed_queue = []
        
    def Selection(self) -> None:
        self.selection_flag = True

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading