| 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.IsReadyfrom 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