Overall Statistics
Total Orders
1371
Average Win
1.69%
Average Loss
-2.20%
Compounding Annual Return
9.305%
Drawdown
35.600%
Expectancy
0.160
Start Equity
10000
End Equity
82384.21
Net Profit
723.842%
Sharpe Ratio
0.356
Sortino Ratio
0.296
Probabilistic Sharpe Ratio
0.104%
Loss Rate
34%
Win Rate
66%
Profit-Loss Ratio
0.77
Alpha
0.025
Beta
0.621
Annual Standard Deviation
0.15
Annual Variance
0.023
Information Ratio
0.057
Tracking Error
0.128
Treynor Ratio
0.086
Total Fees
$2563.09
Estimated Strategy Capacity
$27000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
15.78%
from AlgorithmImports import *

class IBSMeanReversionOnIndex(QCAlgorithm):

    """
    Internal Bar Strength Strategy 

    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 = "QQQ"             # 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)