Overall Statistics
Total Orders
389
Average Win
2.61%
Average Loss
-2.16%
Compounding Annual Return
13.924%
Drawdown
20.700%
Expectancy
0.409
Start Equity
100000
End Equity
539264.47
Net Profit
439.264%
Sharpe Ratio
0.754
Sortino Ratio
0.525
Probabilistic Sharpe Ratio
24.989%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
1.20
Alpha
0.051
Beta
0.302
Annual Standard Deviation
0.115
Annual Variance
0.013
Information Ratio
-0.21
Tracking Error
0.158
Treynor Ratio
0.287
Total Fees
$3626.64
Estimated Strategy Capacity
$250000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
12.32%
from AlgorithmImports import *
from collections import deque
import numpy as np

class GAPMRatios:
    def __init__(self, upGaps=0, downGaps=0):
        self.UpGaps     = upGaps
        self.DownGaps   = downGaps        
        pass

class GAPM(PythonIndicator):
    """
    The GAPM (Gap Measure) Indicator:

    Reference: 
        - https://www.traders.com/Documentation/FEEDbk_docs/2024/01/TradersTips.html#item5
        - https://financial-hacker.com/the-gap-momentum-system/

    Purpose:
        - Measures the ratio of upward price gaps to downward price gaps over a specified period.
        - Utilizes price gaps between the current open and the previous close to identify potential trends.

    Calculation:
        1. Iterates over a defined 'period', calculating the gap between the opening price of the current day
           and the closing price of the previous day.
        2. Accumulates total upward gaps (positive differences) and total downward gaps (negative differences).
        3. Calculates the Gap Ratio as follows:
            - If there are no downward gaps (dn_gaps == 0), the Gap Ratio is set to 1 to avoid division by zero.
            - Otherwise, the Gap Ratio is calculated as 100 times the sum of upward gaps divided by the absolute sum
              of downward gaps, highlighting the dominance of upward or downward gaps within the period.
        4. Applies a Simple Moving Average (SMA) to the Gap Ratio over a 'signal_period' to smooth the indicator,
           producing the final GAPM value.

    Usage:
        - The GAPM indicator can be used to identify potential bullish or bearish trends based on the prevalence of
          gap movements in one direction over another.
        - A higher GAPM value indicates a dominance of upward gaps, suggesting potential bullish trends, while
          a lower value (closer to 1) suggests a dominance of downward gaps or an equal presence of both, potentially
          indicating bearish trends or a lack of strong directional movement.

    Parameters:
        - 'period': The lookback period over which to calculate price gaps.
        - 'signal_period': The period over which to apply the SMA to the Gap Ratio, smoothing the final indicator value.

    Returns:
        - The smoothed GAPM indicator, providing a measure of the relative presence of upward to downward gaps,
          useful for trend identification and trading decisions.
    """

    def __init__(self, period, signal_period):
        """
        Initialize the GAPM indicator.

        :param period: The lookback period over which to calculate price gaps.
        :param signal_period: The period over which to apply the SMA to the Gap Ratio.
        """
        self.period = period
        self.signal_period = signal_period
        # self.sma            = SimpleMovingAverage(signal_period)
        self.sma            = ExponentialMovingAverage(signal_period)
        
        self.prev_close     = None
        self.gaps           = deque(maxlen=period)
        self.Value          = 0
        self.WarmUpPeriod   = max(period, signal_period)
        self.GapSMAs        = GAPMRatios()

    @property
    def IsReady(self) -> bool:
        """
        Check if the indicator is ready.

        :return: True if the indicator is ready, False otherwise.
        """
        return self.sma.IsReady and (len(self.gaps) == self.gaps.maxlen)

    def Reset(self):
        """Reset the indicator to its initial state."""
        self.sma.Reset()
        self.prev_close = None
        self.gaps.clear()
        self.Value      = 0
        

    def Update(self, input_data):
        """
        Update the GAPM indicator with the latest price data.

        :param input_data: The input price data (bar).
        :return: True if the indicator is ready, False otherwise.
        """
        if input_data is None:
            return False

        if self.prev_close is not None:
            gap = input_data.Open - self.prev_close
            self.gaps.append(gap)

            up_gaps = sum(g for g in self.gaps if g > 0)
            dn_gaps = sum(abs(g) for g in self.gaps if g < 0)

            # Get Up-Gap Ratio and take SMA
            up_gap_ratio = 1 if dn_gaps == 0 else 100 * up_gaps / dn_gaps
            self.sma.Update(input_data.Time, up_gap_ratio)

            self.GapSMAs = GAPMRatios()
            self.GapSMAs.UpGaps   = self.sma.Current.Value
            self.Current.Value    = self.sma.Current.Value
            self.Value            = self.Current.Value 
            

        self.prev_close = input_data.Close

        return self.IsReady
from AlgorithmImports import *
from GAPM import *

class GapSignals(QCAlgorithm):
    def Initialize(self):

        ## Backtest Params
        self.SetStartDate(2011, 1, 1)
        self.SetEndDate(2023, 12, 1)
        self.SetCash(100000)
        
        ## Subscribe to asset and set benchmark
        self.ticker = "QQQ"
        self.symbol = self.AddEquity(self.ticker, Resolution.Daily).Symbol
        self.SetBenchmark(self.ticker)

        ## Init Indicators
        self.atr      = AverageTrueRange(14)
        self.emaFast  = ExponentialMovingAverage(50)
        self.emaSlow  = ExponentialMovingAverage(100)        
        self.gapm     = GAPM(40, 20)

        ## Register Indicators w/Timeframe
        self.RegisterIndicator(self.symbol, self.gapm, Resolution.Daily)
        self.RegisterIndicator(self.symbol, self.emaFast, Resolution.Daily)
        self.RegisterIndicator(self.symbol, self.emaSlow, Resolution.Daily)

        ## Initialize GAPM Rolling Window (track rise/fall over 2 bars)
        self.gapmWindow      = RollingWindow[float](2) 
        self.gapm.Updated += self.OnGapmUpdated
        
    def OnGapmUpdated(self, indicator, data):
        # self.Plot("GAPM Value", "Gapm", self.gapm.GapSMAs.UpGaps)
        self.gapmWindow.Add(self.gapm.GapSMAs.UpGaps)
        return

    def OnData(self, data):        
        if (self.ticker not in data ) or (data[self.ticker] is None):
            return

        if not (self.gapm.IsReady and self.gapmWindow.IsReady and \
                self.emaFast.IsReady and self.emaSlow.IsReady):
            return
        
        if not self.Portfolio.Invested:
            
            if self.UpGapsRising() and (self.emaFast.Current.Value >= self.emaSlow.Current.Value) :
                self.SetHoldings(self.symbol, 1.5)
                    
        else:
            if self.UpGapsFalling():
                self.Liquidate(self.symbol)    

    def UpGapsRising(self):
        upGapsOrderedList = list(self.gapmWindow)[::-1]
        upGapsRising = all(upGapsOrderedList[i] < upGapsOrderedList[i+1] for i in range(len(upGapsOrderedList)-1))
        return upGapsRising

    def UpGapsFalling(self):
        upGapsReversedList = list(self.gapmWindow)
        upGapsFalling = all(upGapsReversedList[i] < upGapsReversedList[i+1] for i in range(len(upGapsReversedList)-1))
        return upGapsFalling