该策略投资于纽约证券交易所上市的标普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 [点击浏览原文]

<摘要>

本研究开发了一种新颖且直观的计量经济学测试,以研究斐波那契回撤的预测能力和异常回报的生成能力。结果表明,斐波那契回撤在国际股票市场指数和外汇汇率中占有重要地位,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%
Beta0.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

Leave a Reply

Discover more from Quant Buffet

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

Continue reading