“该策略投资于纽约证券交易所上市的标普500成分股,关注股票的斐波那契回撤水平。通过公式计算回撤水平,将股票按接近程度分为五分位。对接近回撤水平的股票做多,对接近回撤的股票做空,所有股票权重相等,投资组合每周重新平衡。”
资产类别:股票 | 区域:美国 | 频率:每周 | 市场:股票 | 关键词:斐波那契,阻力位,股票交易
策略概述
投资宇宙由在纽约证券交易所(NYSE)上市的股票组成,主要包括标普500指数的成分股。我们感兴趣的变量是所选股票的斐波那契回撤水平。这些回撤水平可以通过一个简单的公式计算:L + alpha *(H-L),其中H为股票的历史最高点,L为股票的历史最低点,alpha为我们要计算的斐波那契回撤水平的小数形式;我们使用的水平为0% (0)、38.1% (0.381)、50% (0.5)、61.2% (0.612) 和100% (1)。投资宇宙根据股票价格接近的回撤水平分为五分位。基于此,我们对接近其斐波那契回撤水平的股票做多(从上方接近),对接近斐波那契回撤水平的股票做空(从下方接近)。股票的权重相等,投资组合每周重新平衡。
策略合理性
通过对数据的测试和分析,研究展示了斐波那契回撤水平对股票回报的预测能力。测试使用了各种投资组合类型和大量数据集,所有这些数据都表明回撤水平接近与金融表现之间存在反向关系。
此外,这种现象存在于广泛的市场中,因此可以在大量投资者中观察到。因此,我们知道它并不依赖于诸如反转和投资者信心等因素,而是基于更为重要和强大的因素。因此,基于这些水平的交易策略如果执行得当,应该能够提供正回报。
论文来源
Can Returns Breed Like Rabbits?, Econometric Tests for Fibonacci Retracements [点击浏览原文]
- FaSavva Shanaev, 北安普利亚大学
- Ryan Gibson, Audit Partnership Ltd
<摘要>
本研究开发了一种新颖且直观的计量经济学测试,以研究斐波那契回撤的预测能力和异常回报的生成能力。结果表明,斐波那契回撤在国际股票市场指数和外汇汇率中占有重要地位,0.0%、38.1%、50.0%、61.2% 和 100.0% 是最重要的回撤水平,而包含 14.6%、23.6%、76.4%、78.6% 或 85.4% 水平会削弱模型的预测能力。研究结果无法用日历市场异常或回报反转解释。在个别股票层面上,一种基于标普500指数的策略,对接近斐波那契回撤支撑位(阻力位)的股票做多(做空),在Fama-French多因子模型中生成了正且统计显著的阿尔法,并展示了市场时机属性。


回测表现
| 年化收益率 | 37.35% |
| 波动率 | 41.85% |
| Beta | 0.046 |
| 夏普比率 | 0.89 |
| 索提诺比率 | -0.26 |
| 最大回撤 | N/A |
| 胜率 | 51% |
完整python代码
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
