Fibonacci Supports and Resistances in Cross-Sectional Stock Trading
Log in to collectAcademic paper
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.
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.
Backtest performance
Annualised return37.35%
Volatility41.85%
Beta0.046
Sharpe ratio0.89
Sortino ratio-0.26
Win rate51%
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