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