Overall Statistics |
Total Orders 425 Average Win 1.91% Average Loss -1.59% Compounding Annual Return 10.014% Drawdown 19.800% Expectancy 0.390 Start Equity 100000 End Equity 345892.03 Net Profit 245.892% Sharpe Ratio 0.669 Sortino Ratio 0.497 Probabilistic Sharpe Ratio 21.632% Loss Rate 37% Win Rate 63% Profit-Loss Ratio 1.20 Alpha 0.027 Beta 0.247 Annual Standard Deviation 0.085 Annual Variance 0.007 Information Ratio -0.436 Tracking Error 0.149 Treynor Ratio 0.231 Total Fees $2515.93 Estimated Strategy Capacity $580000000.00 Lowest Capacity Asset QQQ RIWIV7K5Z9LX Portfolio Turnover 8.93% |
from AlgorithmImports import * from collections import deque import numpy as np class GAPM(PythonIndicator): def __init__(self, period, signal_period): """ :param period: The lookback period over which to calculate price gaps. :param signal_period: The period over which to apply the EMA to the Gap Ratio. """ self.period = period self.signal_period = signal_period self.ema = ExponentialMovingAverage(signal_period) self.prev_close = None self.gaps = deque(maxlen=period) self.Value = 0 self.WarmUpPeriod = max(period, signal_period) @property def IsReady(self) -> bool: return self.ema.IsReady and (len(self.gaps) == self.gaps.maxlen) def Update(self, input_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) up_gap_ratio = 1 if dn_gaps == 0 else 100 * up_gaps / dn_gaps self.ema.Update(input_data.Time, up_gap_ratio) self.Current.Value = self.ema.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): """ Gap Momentum Trading Strategy This is an implementation of Perry Kaufman's Gap Momentum strategy from TASC magazine (Jan'24). It uses the GAPM indicator to track the ratio of cumulative upward to downward price gaps. The strategy enters long when the EMA of the Gap Ratio is rising and a trend filter is met. It exits long when the EMA of the Gap Ratio is falling. Modifications: Added simple trend filter and used EMA instead of SMA for Gap ratio. Reference: https://financial-hacker.com/the-gap-momentum-system/ https://www.traders.com/Documentation/FEEDbk_docs/2024/01/TradersTips.html#item5 u/shock_and_awful """ ## System method, algo entry point def Initialize(self): self.InitBacktest() self.InitData() self.InitIndicators() ## Backtest Params def InitBacktest(self): self.SetStartDate(2011, 1, 1) self.SetEndDate(2023, 12, 30) self.SetCash(100000) # Subscribe to asset feed def InitData(self): self.ticker = "QQQ" self.symbol = self.AddEquity(self.ticker, Resolution.Daily).Symbol self.SetBenchmark(self.ticker) ## Init Indicators def InitIndicators(self): # Create GAPM, EMA indicators and set warmup period self.gapm = GAPM(40, 20) self.emaFast = ExponentialMovingAverage(50) self.emaSlow = ExponentialMovingAverage(200) self.SetWarmup(200, Resolution.Daily) # 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 n bars) self.windowLength = 2 self.gapmWindow = RollingWindow[float](self.windowLength) self.gapm.Updated += self.OnGapmUpdated ## Update the rolling window when a new GAPM value is calculated def OnGapmUpdated(self, indicator, data): self.gapmWindow.Add(self.gapm.Current.Value) return ## System method, called as every new bar of data arrives ## Logic for entry / exit signals and trades. def OnData(self, data): # Make sure data is available and indicators are ready if (self.ticker not in data ) or (data[self.ticker] is None) \ or not (self.gapm.IsReady and self.gapmWindow.IsReady and \ self.emaFast.IsReady and self.emaSlow.IsReady and not self.IsWarmingUp): return # If we're not invested, check for the rising gaps (and go long) if not self.Portfolio.Invested: if self.UpGapsRising() and (self.emaFast.Current.Value >= self.emaSlow.Current.Value) : self.SetHoldings(self.symbol, 1) # If we're alread invested, check for falling gaps (and liquidate) else: if self.UpGapsFalling(): self.Liquidate(self.symbol) ## Check if gaps are rising in sequence. ## Note: Rolling windows store items in reverse. def UpGapsRising(self): upGapsOrderedList = list(self.gapmWindow)[::-1] upGapsRising = all(upGapsOrderedList[i] < upGapsOrderedList[i+1] for i in range(len(upGapsOrderedList)-1)) return upGapsRising ## Check if gaps are falling in sequence. def UpGapsFalling(self): upGapsReversedList = list(self.gapmWindow) upGapsFalling = all(upGapsReversedList[i] < upGapsReversedList[i+1] for i in range(len(upGapsReversedList)-1)) return upGapsFalling