Overall Statistics
Total Trades
7909
Average Win
0.20%
Average Loss
-0.20%
Compounding Annual Return
1.729%
Drawdown
11.000%
Expectancy
0.052
Net Profit
48.606%
Sharpe Ratio
0.352
Probabilistic Sharpe Ratio
0.008%
Loss Rate
47%
Win Rate
53%
Profit-Loss Ratio
0.99
Alpha
0.008
Beta
0.082
Annual Standard Deviation
0.036
Annual Variance
0.001
Information Ratio
-0.297
Tracking Error
0.153
Treynor Ratio
0.153
Total Fees
$18311.47
Estimated Strategy Capacity
$500000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
# https://quantpedia.com/strategies/market-sentiment-and-an-overnight-anomaly/
#
# The investment universe consists of SPY ETF, and the price of SPY, price of VIX and Brain Market Sentiment (BMS) indicator
# are used to identify the market sentiment. The investor buys SPY ETF and holds it overnight; when the price of SPY is above its 20-day moving average,
# the price of VIX is below its moving average, and the value of the BMS indicator is greater than its 20-day moving average.
# Note that the authors suggest using this strategy as an overlay when deciding whether to make a trade rather than using this system on its own.
#
# QC Implementation:

# region imports
from AlgorithmImports import *
# endregion

class MarketSentimentAndAnOvernightAnomaly(QCAlgorithm):

    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"