投资范围包括SPY ETF,投资者通过SPY价格、VIX价格和Brain市场情绪(BMS)指标识别市场情绪。当SPY价格高于20日移动平均线、VIX价格低于其移动平均线且BMS指标高于20日移动平均线时,投资者买入SPY并持有至隔夜。建议将该策略作为交易决策的叠加因素,而非单独使用。

策略概述

投资范围包括SPY ETF,投资者使用SPY价格、VIX价格和Brain市场情绪(BMS)指标来识别市场情绪。投资者在满足以下条件时买入SPY ETF并持有至隔夜:SPY价格高于其20日移动平均线,VIX价格低于其移动平均线,且BMS指标值高于其20日移动平均线。

注意,作者建议将此策略作为决定是否交易的叠加因素,而不是单独使用该系统。

策略合理性

关于隔夜异常现象,有多种可能的解释。学术研究表明,隔夜异常现象的部分原因是来自市场参与者的市价订单积累导致的高开盘价,这些价格随后在交易的第一个小时内下跌。由于流动性溢价,预期部分隔夜正回报是可能的,但流动性仅能解释隔夜和白天回报差异的一小部分。 此外,市场情绪的解释非常简单。当情绪看涨时,市场上的整体情绪良好,这意味着投资者会更多地买入,从而进一步增强市场情绪。而当情绪看跌时,市场整体情绪不佳,投资者买入意愿减少,导致情绪进一步恶化。

论文来源

Market Sentiment and an Overnight Anomaly [点击浏览原文]

<摘要>

各种研究表明,市场情绪(或称投资者情绪)在市场回报中起到了一定作用。市场情绪是指金融市场上的总体情绪和投资者的交易倾向。市场情绪分为两种主要类型,看涨和看跌。通常,价格上涨表明市场情绪看涨,反之,价格下跌则表明情绪看跌。本文展示了多种衡量市场情绪的方法及其对回报的影响。

此外,我们结合三种市场情绪指标分析了隔夜异常现象。我们在VIX和SPY ETF的短期趋势之外,进一步分析了Brain市场情绪指标。我们的目标不是构建交易系统,而是分析金融市场行为。总体来说,这类策略的交易成本会很高,因此更适合将其作为决定交易时机的叠加工具,而不是独立使用该系统。

回测表现

年化收益率15.58%
波动率7.33%
Beta0.082
夏普比率0.34
索提诺比率N/A
最大回撤11%
胜率53%

完整python代码

from AlgoLib import *
# endregion

class MarketSentimentAndAnOvernightAnomaly(XXX):

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

        self.period:int = 20 # sma period

        self.weight:float = 0
        self.price_data:dict = {}

        self.spy_symbol:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
        self.vix_symbol:Symbol = self.AddData(QuandlVix, 'CBOE/VIX', Resolution.Daily).Symbol       # starts in 2004
        self.bms_symbol:Symbol = self.AddData(QuantpediaBMS, 'BMS_GLOBAL', Resolution.Daily).Symbol # starts in 2018

        for symbol in [self.spy_symbol, self.vix_symbol, self.bms_symbol]:
            self.price_data[symbol] = RollingWindow[float](self.period)

    def OnData(self, data: Slice):
        # calculate signal from SPY 16 minutes before close
        if self.spy_symbol in data and data[self.spy_symbol] and self.Time.hour == 15 and self.Time.minute == 44:
            weight:float = 0.

            for symbol in [self.spy_symbol, self.vix_symbol, self.bms_symbol]:
                # trade only sub-strategies with underlying data available
                if self.Securities[symbol].GetLastData() and (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days <= 3:
                    price:float = self.Securities[symbol].GetLastData().Price
                    rolling_window:RollingWindow = self.price_data[symbol]
                    if rolling_window.IsReady and self.GetSignal(price, rolling_window, True if symbol != self.vix_symbol else False):
                        weight += (1 / 3)

                    rolling_window.Add(price)
            
            q:int = int((self.Portfolio.TotalPortfolioValue * weight) / data[self.spy_symbol].Value)
            if q != 0:
                self.MarketOnCloseOrder(self.spy_symbol, q)
                self.MarketOnOpenOrder(self.spy_symbol, -q)

    def GetSignal(self, curr_value:float, rolling_window:RollingWindow, signal_above_sma:bool) -> bool:
        prices:list[float] = [x for x in rolling_window]
        moving_average:float = sum(prices) / len(prices)

        result:bool = False
        if signal_above_sma and (curr_value > moving_average):
            result = True
        elif not signal_above_sma and (curr_value < moving_average):
            result = True

        return result

# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaBMS(PythonData):
    def GetSource(self, config, date, isLiveMode):
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/index/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)

    def Reader(self, config, line, date, isLiveMode):
        data:QuantpediaBMS = QuantpediaBMS()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None

        split:list = line.split(',')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data.Value = float(split[2])

        return data

class QuandlVix(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = "VIX Close"

Leave a Reply

Discover more from Quant Buffet

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

Continue reading