
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)).
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Weekly | MARKET: equities | KEYWORD: Fibonacci, Resistances , Stock Trading
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 Return | 37.35% |
| Volatility | 41.85% |
| Beta | 0.046 |
| Sharpe Ratio | 0.89 |
| Sortino Ratio | -0.26 |
| Maximum Drawdown | N/A |
| Win Rate | 51% |
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