Overall Statistics
Total Orders
811
Average Win
4.19%
Average Loss
-5.73%
Compounding Annual Return
7.368%
Drawdown
61.400%
Expectancy
0.112
Start Equity
10000
End Equity
53846.09
Net Profit
438.461%
Sharpe Ratio
0.25
Sortino Ratio
0.148
Probabilistic Sharpe Ratio
0.004%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
0.73
Alpha
0.03
Beta
0.76
Annual Standard Deviation
0.263
Annual Variance
0.069
Information Ratio
0.079
Tracking Error
0.237
Treynor Ratio
0.087
Total Fees
$10728.49
Estimated Strategy Capacity
$11000000.00
Lowest Capacity Asset
TQQQ UK280CGTCB51
Portfolio Turnover
9.36%
from AlgorithmImports import *

class IBSMeanReversionOnIndex(QCAlgorithm):

    """
    Inspired by Quantitativo:
    https://www.quantitativo.com/p/robustness-of-the-211-sharpe-mean

    Additional reference:
    https://tradingstrategy.medium.com/the-internal-bar-strength-ibs-indicator-trading-strategies-rules-video-e638f135edb6
    
    Entry rules:
    -----------------------
    Compute the rolling mean of High minus Low over the last 25 days;
    Compute the IBS indicator: (Close - Low) / (High - Low);
    Compute a lower band as the rolling High over the last 10 days minus 2.5 x the rolling mean of High mins Low (first bullet);
    Go long whenever SPY closes under the lower band (3rd bullet) and IBS is lower than 0.3;

    Exit rules
    -----------------------
    - Close the trade whenever the SPY close is higher than yesterday's high;
    - Close the trade whenever the price is lower than the 300-SMA.
    """

    ## Initialize the algo
    ## ------------------------
    def Initialize(self):

        # Init backtest params, etc
        self.ticker = "TQQQ"             # Ticker symbol to trade
        self.SetBenchmark("SPY")  # Benchmark for reporting (buy and hold)
        self.SetStartDate(2000, 12, 1)   # Backtest start date
        # self.SetEndDate(2023, 7, 11)     # Backtest end date
        self.SetCash(10000)             # Starting portfolio balance

        # Subscrbe to a minute data feed (minute bars)
        self.symbol = self.AddEquity(self.ticker, Resolution.Minute).symbol

        # Set up a rollingwindow to store consolidated daily bars
        self.dailyBars = RollingWindow[TradeBar](2)

        # Set up the daily bar consolidator
        self.dailyConsolidator = TradeBarConsolidator(timedelta(days=1))
        self.dailyConsolidator.DataConsolidated += self.OnDailyBarFormed
        self.SubscriptionManager.AddConsolidator(self.symbol, self.dailyConsolidator)

        # 300 SMA
        self.sma_300 = self.sma(self.symbol, 300, Resolution.Daily)

        # Schedule a daily chron job to check for signals at the open
        self.Schedule.On(self.DateRules.EveryDay(), \
                            self.TimeRules.AfterMarketOpen(self.ticker, 5), 
                            self.CheckForSignals)

    def OnDailyBarFormed(self, val, dailyBar):
         self.dailyBars.add(dailyBar)

    def CheckForSignals(self):
        if not self.Portfolio.Invested:
            if self.EntrySignalFired():
                self.SetHoldings(self.ticker, 1)
        else:
            if self.ExitSignalFired():
                self.LiquidateWithMsg(self.symbol,f"IBS Signal Exit")
            #   self.Liquidate(tag=f"IBS Signal Exit")

    ## Go long when:    
    ##   - SPy closes under lower band 
    ##   - IBS < 0.3
    ## ------------------------------
    def EntrySignalFired(self):
        if self.dailyBars.IsReady:
            # if SPY closes under the lower band (3rd bullet) 
                # If IBS is lower than 0.3;
                 # IBS= (Close-Low)/(High-Low)
            
            lastBar = self.dailyBars[0]
            if(lastBar.high != lastBar.low):
                ibsValue = (lastBar.close - lastBar.low) / (lastBar.high - lastBar.low)
                return (ibsValue < 0.2 ) 
                 
        return False

    ## Exit the trade when:
    #    - SPY close is higher than yesterday's high;
    #    - price is lower than the 300-SMA.
    ## -------------------------------------------------------------------------------
    def ExitSignalFired(self):
        if self.dailyBars.IsReady:
            # if SPY closes under the lower band (3rd bullet) 
                # If IBS is lower than 0.3;
                 # IBS= (Close-Low)/(High-Low)
            
            lastBar = self.dailyBars[0]
            if(lastBar.high != lastBar.low):
                ibsValue = (lastBar.close - lastBar.low) / (lastBar.high - lastBar.low)
                return (ibsValue > 0.8 ) 
        return False

        # if self.Portfolio.Invested:
        #     if (self.dailyBars[0].close > self.dailyBars[1].high):
        #         self.Liquidate(tag=f"Exit @ last close > prev high: {self.dailyBars[0].close} > {self.dailyBars[1].high}")


    # Convenience method to liquidate with PnL message
    def LiquidateWithMsg(self, symbol, exitReason):
        
        pnl         = round(100 * self.Portfolio[symbol].UnrealizedProfitPercent,2)
        biasText    = 'Long' if (self.Portfolio[symbol].IsLong) else 'Short'
        winlossText = 'win' if pnl > 0 else 'loss'        
        orderNote   = f"[{pnl}% {winlossText}] {exitReason} | Exiting {biasText} position" 
        
        self.liquidate(symbol, tag=orderNote)