该策略投资于S&P 500共同基金(ETF)和3个月期国债。每月最后一个交易日,计算9个月简单移动平均线(SMA)及其导数。若导数为负且满足特定条件,则卖出S&P 500并投资国债;若导数为正且满足其他条件,则卖出国债并买入S&P 500。每月底进行决策。

策略概述

投资范围包括S&P 500共同基金(ETF)和3个月期国债。该策略包括在相对高点卖出指数(平仓多头头寸),并在相对低点买入指数。在每个月最后一个交易日收盘时,计算9个月SMA(简单移动平均线)趋势线函数。求取趋势线函数的导数。如果导数为负,且9个月SMA的斜率小于或等于355°的切线,2个月SMA的斜率小于或等于353°的切线,且S&P 500的开盘价或收盘价(或两者)低于9个月SMA,则触发卖出信号。在下个月第一个交易日收盘时,平仓S&P 500头寸并投资于3个月期国债。此决定在接下来的两个月内有效,即使触发买入信号,投资组合也不会改变。如果导数为正,且9个月SMA的斜率大于或等于5°的切线,则触发买入信号。在下个月第一个交易日收盘时,平仓国债头寸并买入S&P 500。每月底进行分配决策。

策略合理性

该策略本质上是一种风险开启和风险关闭的趋势跟随系统。该策略旨在跟随趋势,在顶部转换为安全的国债,但在底部时应再次切换到股票。然而,该规则似乎不够透明,论文没有解释为什么它应该奏效。我们认为该策略可能存在过拟合的风险,但归根结底,它是一个技术分析策略的示例。因此,应该进行全面的样本外分析。

论文来源

Evidence on a New Stock Trading Rule that Produces Higher Returns with Lower Risk [点击浏览原文]

<摘要>

这一新的股票市场交易规则使用三步来消除股票价格数据中的随机非系统性风险,以平滑波动性。通过实证证明,技术分析的相对高点和低点交易规则对于S&P 500指数组合相较于简单的买入持有策略表现出显著的优势,并且风险显著降低。这一新的交易规则成功的原因在于市场参与者的情绪。投资者的恐惧和恐慌性抛售导致股价在市场底部大幅低于股票的内在价值。


回测表现

年化收益率6.78%
波动率N/A
Beta0.369
夏普比率0.251
索提诺比率0.189
最大回撤N/A
胜率96%

完整python代码

from AlgorithmImports import *
import math
# endregion

class StockTradingRuleThatProducesHigherReturnsWithLowerRisk(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)

        self.curr_month:int = -1
        self.long_period:int = 9 * 21
        self.short_period:int = 2 * 21

        # market data
        security:Equity = self.AddEquity("SPY", Resolution.Minute)
        security.SetFeeModel(CustomFeeModel())
        security.SetLeverage(5)
        self.spy_symbol:Symbol = security.Symbol

        # bills data
        security:Equity = self.AddEquity("BIL", Resolution.Minute)
        security.SetFeeModel(CustomFeeModel())
        security.SetLeverage(5)
        self.bil_symbol:Symbol = security.Symbol

        # sma data objects
        self.long_SMA_data:SMAData = SMAData(self, self.spy_symbol, self.long_period)
        self.short_SMA_data:SMAData = SMAData(self, self.spy_symbol, self.short_period)

        self.SetWarmup(self.long_period, Resolution.Daily)
        
        # placing MOC orders is allowed 15,5 minutes before market close
        self.Schedule.On(self.DateRules.EveryDay(self.spy_symbol), self.TimeRules.BeforeMarketClose(self.spy_symbol, 16), self.EveryDayBeforeMarketClose)

    def EveryDayBeforeMarketClose(self):
        # update sma each day
        if self.long_SMA_data.sma_is_ready() and self.short_SMA_data.sma_is_ready():
            # store sma values to internal list
            self.long_SMA_data.update_values()
            self.short_SMA_data.update_values()

        # wait until warmup is done and rebalance monthly
        if self.IsWarmingUp or (self.curr_month == self.Time.month):
            return
        self.curr_month = self.Time.month

        # storage of SMA values is ready
        if self.long_SMA_data.is_ready() and self.short_SMA_data.is_ready():
            long_SMA_slope:float = self.long_SMA_data.calc_slope()
            short_SMA_slope:float = self.short_SMA_data.calc_slope()

            long_SMA_value:float = self.long_SMA_data.SMA.Current.Value
            short_SMA_value:float = self.short_SMA_data.SMA.Current.Value

            # tangent
            tan_353:float = math.tan(math.pi * (353 / 180))
            tan_355:float = math.tan(math.pi * (355 / 180))
            tan_5:float = math.tan(math.pi * (5 / 180))

            # if the derivative is negative, the slope of the nine-month SMA is lower or equal to the tangent of 355°, the slope of the two-month SMA is lower or equal to the tangent of 353°
            if long_SMA_slope < 0 and (long_SMA_slope <= tan_355) and (short_SMA_slope <= tan_353):
                # s&p one day history
                history = self.History(self.spy_symbol, 1, Resolution.Daily)
                if history.empty:
                    return

                close_price:float = history.loc[self.spy_symbol].close[0]
                open_price:float = history.loc[self.spy_symbol].open[0]
                
                # either (or both) opening price of the S&P 500 or closing price of the S&P 500 is below the nine-month SMA
                if (close_price < long_SMA_value) or (open_price < long_SMA_value):
                    # close S&P and buy BIL
                    if self.Portfolio[self.spy_symbol].Invested:
                        self.MarketOnCloseOrder(self.spy_symbol, -self.Portfolio[self.spy_symbol].Quantity)

                    if self.Securities.ContainsKey(self.bil_symbol) and self.Securities[self.bil_symbol].Price != 0:
                        q:int = int(self.Portfolio.MarginRemaining / self.Securities[self.bil_symbol].Price)
                        # q = self.CalculateOrderQuantity(self.bil_symbol, 1)
                        self.MarketOnCloseOrder(self.bil_symbol, q - self.Portfolio[self.bil_symbol].Quantity)
            
            # if the derivative is positive and the slope of the nine-month SMA is higher or equal to the tangent of 5°
            elif long_SMA_slope > 0 and (long_SMA_slope >= tan_5):
                # buy signal close BIL and buy S&P
                if self.Portfolio[self.bil_symbol].Invested:
                    self.MarketOnCloseOrder(self.bil_symbol, -self.Portfolio[self.bil_symbol].Quantity)

                # self.SetHoldings(self.spy_symbol, 1)
                if self.Securities.ContainsKey(self.spy_symbol) and self.Securities[self.spy_symbol].Price != 0:
                    q:int = int(self.Portfolio.MarginRemaining / self.Securities[self.spy_symbol].Price)
                    # q = self.CalculateOrderQuantity(self.spy_symbol, 1)
                    self.MarketOnCloseOrder(self.spy_symbol, q - self.Portfolio[self.spy_symbol].Quantity)

class SMAData:
    def __init__(self, algorithm:QCAlgorithm, symbol:Symbol, period:float) -> None:
        self.SMA_period:float = period
        self.SMA:SimpleMovingAverage = algorithm.SMA(symbol, period, Resolution.Daily)
        self.SMA_values:RollingWindow = RollingWindow[float](period)

    def update_values(self) -> None:
        self.SMA_values.Add(self.SMA.Current.Value)

    def sma_is_ready(self) -> bool:
        return self.SMA.IsReady

    def is_ready(self) -> bool:
        return self.SMA_values.IsReady

    def calc_slope(self) -> float:
        delta_SMA:float = self.SMA_values[0] - self.SMA_values[self.SMA_period-1]

        # return slope
        return delta_SMA / self.SMA_period

# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))            

Leave a Reply

Discover more from Quant Buffet

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

Continue reading