TRADERS’ TIPS
For this month’s Traders’ Tips, the focus is Perry Kaufman’s article in the June 2024 issue, “Trading Opening Gaps And Extreme Closes In Stocks.” Here, we present the July 2024 Traders’ Tips code with possible implementations in various software.
You can right-click on any chart to open it in a new tab or window and view it at it’s originally supplied size, often much larger than the version printed in the magazine.
The Traders’ Tips section is provided to help the reader implement a selected technique from an article in this issue or another recent issue. The entries here are contributed by software developers or programmers for software that is capable of customization.
For the type of research shown in Perry Kaufman’s article in the June 2024 issue, “Trading Opening Gaps And Extreme Closes In Stocks,” we can use the backtest engine in RealTest.
The following script, when run in the “optimize” mode, outputs a results list in RealTest that resembles the author’s table of stats for different gap ranges:
Notes: implements Perry Kaufman's research into intraday movement on gap days run Import and then Opimize to produce an entire table of pullback and runup stats Import: DataSource: Norgate IncludeList: .S&P 500 Current & Past StartDate: 1/1/2010 EndDate: Latest SaveAs: sp500.rtd Settings: DataFile: sp500.rtd StartDate: Earliest EndDate: Latest BarSize: Daily TestName: Select(border < -0.11, "gap < -11%", border >= 0.11, "gap >= 11%", Format("from {%} to {%}", border, border + 0.02)) Parameters: border: from -0.13 to 0.11 step 0.02 Data: gap: O[-1] / C - 1 // look ahead to tomorrow's open for research purposes gap_in_range: Select(border < -0.11, gap < -0.11, border >= 0.11, gap >= 0.11, gap >= border and gap < border + 0.02) Strategy: gap_data Side: Long EntrySetup: InSPX and gap_in_range EntryTime: NextOpen // buy at open of gap day ExitRule: 1 ExitTime: NextClose // sell at close of gap day Quantity: 1 // one share (position size is not relevant to this study) Results: // override default set of results columns with ones specific to this study Instances: {#} Sum(S.Exits,S.Number) OpenToClose: {%2} Sum(S.TradePct,S.Number) / Instances Pullback: {%2} TradeStatAvg(T.Lowest / T.PriceIn - 1) Runup: {%2} TradeStatAvg(T.Highest / T.PriceIn - 1)
Rather than a fixed universe of symbols, this uses the historical constituents of the S&P 500 index for each date since 2010 (the author’s start date in his study).
The RealTest output is shown in Figure 1. Like the author’s, these results show that the average “open to close” change is insignificant regardless of gap size.
FIGURE 1: REALTEST. A script in RealTest, run in the “optimize” mode, outputs a list of metrics for different gap ranges.
Rather than only including “pullback” for gaps up and “upward pullback” (runup) for gaps down, this includes both values for all gap ranges.
It is interesting to note, though not surprising, that larger gaps in either direction have larger average intraday moves in both directions.
RealTest users can use the following script for testing to see how useful the research may be for them in practice. This script that I built backtests the following comparison for each gap range:
Notes: tests Perry Kaufman's idea with gap-up days in long positions: - sell the open, buy intraday pullback, sell on close - vs. always selling the close Import: DataSource: Norgate IncludeList: .S&P 500 Current & Past StartDate: 1/1/2010 EndDate: Latest SaveAs: sp500.rtd Settings: DataFile: sp500.rtd StartDate: Earliest EndDate: 1/1/20 BarSize: Daily AccountSize: 1e6 // $1M account size TestName: Select(border >= 0.11, Format("gap >= 11%, pullback = {~}", pullback), Format("from {%} to {%}, pullback = {~}", border, border + 0.02, pullback)) Parameters: border: from 0.01 to 0.11 step 0.02 def 0.07 // gap up size lower border pullback: 0, 1 // always hold until close if 0, sell at open then place limit order to buy again if 1 Data: gap: O[-1] / C - 1 // look ahead to tomorrow's open gap_in_range: gap >= border and (border = 0.11 or gap < border + 0.02) Strategy: gap_sell_open_or_close Side: Long Quantity: 10000 QtyType: Value // $10K fixed position size EntrySetup: InSPX and gap_in_range EntryTime: ThisClose // enter on close when tomorrow will gap up ExitRule: 1 ExitTime: NextClose // sell next close when not using pullback ExitStop: if(pullback, O[-1], 0) // or sell next open when using pullback Strategy: gap_buy_pullback_sell_close Side: Long Quantity: 10000 QtyType: Value // $10K fixed position size again EntrySetup: pullback and InSPX and gap_in_range EntryLimit: O[-1] * (1 - border/2) // enter at pullback to half the min. gap of this test ExitRule: 1 ExitTime: ThisClose // exit on the close of the entry day
This uses RealTest’s multi-strategy capability to combine two strategies: one for the initial entry, the other for the pullback reentry.
To keep this a bit simpler, I omitted a downward gap study; presumably, that would compare holding to the close versus exiting with a limit order on a runup.
The results of those tests are shown in Figure 2. Ignore the specific dollar amount—this is not a tradable strategy, just a research project. Instead, focus on the difference between each pair of tests for the same gap range.
FIGURE 2: REALTEST. For comparison and for testing or research, the same script could be rerun without the downward gap study, producing results such as those shown here.
The first test of each such pair buys the close prior to the gap up and sells the following close. The second test of each pair sells the open and then reenters on pullback to half the lower gap range if touched, thus the higher trade counts.
Note that the profit differences either way are minimal and one way is not consistently better than the other, regardless of gap size.
Readers can use the scripts provided here to test for themselves some of these concepts.
In “Trading Opening Gaps And Extreme Closes In Stocks” in the June 2024 issue, Perry Kaufman presents some research into price behavior surrounding opening gaps and large closes. Several tables of data are given with statistics.
Wealth-Lab users have access to an opensource tool they can use in Wealth-Lab to plot such data to make it easier to visualize. The tool is called ScottPlot.
Wealth-Lab 8 can display various custom plots such as line or bar charts, pie graphs, and scatter plots by throwing in some code. The code given here accomplishes a visualization for the purposes of the article with the help of ScottPlot.
Figure 3 consists of two histogram plots:
For both histograms, down gaps are clustered on the left and up gaps to the right of axis X.
FIGURE 3: WEALTH-LAB. In the upper histogram, you see a count of opening gaps versus their % range, or cohort. In the lower graph, you see the average gap size (%) versus average pullback size (%). For both histograms, down gaps are clustered on the left and up gaps to the right of the x axis.
If you prefer raw data, on the debug log tab, you’ll find the data behind those charts in a tabular form: the gaps themselves by symbol, cohort (the bins for gap sizes i.e. <1%, 1–3% gaps, and so on, average gap size up/down %, and average pullback % size).
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Data; using WealthLab.Indicators; using System.Collections.Generic; using System.Drawing; using ScottPlot; using System.Linq; using System.IO; namespace WealthScript8 { public class Gap { public string Symbol { get; set; } public double Size { get; set; } public double PullbackSize { get; set; } public int Cohort { get; set; } public bool Up { get; set; } public DateTime Date { get; set; } public Gap() { } public Gap(string symbol, double size, double pullback, int cohort, bool isGapUp, DateTime dt) { Symbol = symbol; Size = size; PullbackSize = pullback; Cohort = cohort; Up = isGapUp; Date = dt; } } public class AverageGap { public string Symbol { get; set; } public double AverageSize { get; set; } public double AveragePullback { get; set; } public int Cohort { get; set; } public bool Up { get; set; } public AverageGap() { } public AverageGap(string symbol, double size, double pullback, int cohort, bool isGapUp) { Symbol = symbol; AverageSize = size; AveragePullback = pullback; Cohort = cohort; Up = isGapUp; } } public class TradersTips_Jul2024 : UserStrategyBase { public override void PreExecute(DateTime dt, List<BarHistory> participants) { foreach (BarHistory bars in participants) { int bar = GetCurrentIndex (bars); if (bar == bars.Count - 1) { int step = 2; for (int i = 1; i < bars.Count; i++) { double gap = 100.0 * (bars.Open[i] / bars.Close[i - 1] - 1); double _raw = Math.Abs(gap); if (bars. IsGapUp (i) || bars. IsGapDown (i)) { double pullback = 100.0 * ( (bars. IsGapUp (i) ? bars.Low[i] : bars.High[i]) / bars.Open[i] - 1); int cohort = 0; for (int j = 0; j <= 7; j++) { if (_raw >= j && _raw < j + step) cohort = j; if ((_raw > 0 & _raw < 1) || (_raw < 0 & _raw > -1)) { cohort = 0; break; } } Gap g = new Gap(bars.Symbol, gap, pullback, (bars. IsGapUp (i) ? cohort : -cohort), (bars. IsGapUp (i) ? true : false), bars.DateTimes[i]); if (gap != 0 && !double.IsNaN(gap)) /* filter out small gaps within 0.5% */ if (_raw >= 0.5) lstGaps.Add(g); } } } } } static List<Gap> lstGaps = new List<Gap>(); static List<AverageGap> lstAvgGapsUp = new List<AverageGap>(); static List<AverageGap> lstAvgGapsDn = new List<AverageGap>(); Bitmap bmp = null; Plot plt1, plt2; string[] lstTicks = new string[14] { ">-11%", "-9-11%", "-7-9%", "-5-7%", "-3-5%", "-1-3%", ">-1%", "<1%", "1-3%", "3-5%", "5-7%", "7-9%", "9-11%", ">11%" }; string path; public override void Initialize( BarHistory bars) { bmp = null; plt1 = new ScottPlot.Plot(700, 350); plt2 = new ScottPlot.Plot(700, 350); /* Plot actual chart of average gap and pullback % */ plt1.Title(string.Format("Opening Gaps Count vs. Range")); plt1.YLabel(string.Format("Average % Gap Up/Pullback")); plt1.XLabel("Cohort"); plt1.Grid(enable: false, lineStyle: ScottPlot.LineStyle.Dot); plt1.YAxis.Label("Count (#)"); plt1.XAxis.Label("Range (%)"); plt2.SetCulture(decimalDigits: 2); plt2.YAxis.Label("Avg. Pullback Size (%)"); plt2.XAxis.Label("Avg. Gap Size (%)"); path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); } public override void Execute ( BarHistory bars, int idx) { } public override void BacktestComplete() { /* gap debug stats */ lstGaps.OrderBy(g => g.Up); WriteToDebugLog ("Symbol" + "\t" + "Date" + "\t" + "Size" + "\t" + "Cohort" + "\t" + "Pullback Size" + "\t" + "Up?"); foreach (var item in lstGaps) WriteToDebugLog (item.Symbol + "\t" + item.Date.ToShortDateString() + "\t" + Math.Round(item.Size, 2) + "\t" + item.Cohort + "\t" + Math.Round(item.PullbackSize, 2) + "\t" + item.Up, true); /* average gap size and pullback by cohort */ lstAvgGapsUp = lstGaps.Where(i => i.Up == true).GroupBy(i => i.Cohort). Select(g => new AverageGap { Cohort = g.Key, AverageSize = g.Average(a => a.Size), AveragePullback = g.Average(a => a.PullbackSize) }).ToList(); lstAvgGapsDn = lstGaps.Where(i => i.Up == false).GroupBy(i => i.Cohort). Select(g => new AverageGap { Cohort = g.Key, AverageSize = g.Average(a => a.Size), AveragePullback = g.Average(a => a.PullbackSize) }).ToList(); /* create a histogram with a fixed number of bins */ var hist1 = ScottPlot.Statistics.Histogram.WithFixedBinSize(min: 1, max: 11, binSize: 2); var hist2 = ScottPlot.Statistics.Histogram.WithFixedBinSize(min: -11, max: -1, binSize: 2); hist1.AddRange(lstGaps.Where(i => i.Up == true).Select(g => g.Size)); hist2.AddRange(lstGaps.Where(i => i.Up == false).Select(g => g.Size)); /* show the histogram counts as a bar plot */ var bar1 = plt1.AddBar(values: hist1.Counts, positions: hist1.Bins); var bar2 = plt1.AddBar(values: hist2.Counts, positions: hist2.Bins); bar2.BarWidth = bar1.BarWidth = 2; /* average gap debug stats */ WriteToDebugLog (Environment.NewLine + "Cohort" + "\t" + "Avg.Size %" + "\t" + "Avg.Pullback %"); WriteToDebugLog (Environment.NewLine + "Gaps Up"); foreach (var item in lstAvgGapsUp) WriteToDebugLog (item.Cohort + "\t" + Math.Round(item.AverageSize, 2) + "\t" + Math.Round(item.AveragePullback, 2), true); WriteToDebugLog (Environment.NewLine + "Gaps Down"); foreach (var item in lstAvgGapsDn) WriteToDebugLog (item.Cohort + "\t" + Math.Round(item.AverageSize, 2) + "\t" + Math.Round(item.AveragePullback, 2), true); plt2.PlotBar(lstAvgGapsUp.Select(a => (double)a.Cohort).ToArray(), lstAvgGapsUp.Select(a => Math.Round(a.AverageSize, 2)).ToArray(), showValues: true); plt2.PlotBar(lstAvgGapsUp.Select(a => (double)a.Cohort).ToArray(), lstAvgGapsUp.Select(a => Math.Round(a.AveragePullback, 2)).ToArray(), showValues: true); plt2.PlotBar(lstAvgGapsDn.Select(a => (double)a.Cohort).ToArray(), lstAvgGapsDn.Select(a => Math.Round(a.AverageSize, 2)).ToArray(), showValues: true); plt2.PlotBar(lstAvgGapsDn.Select(a => (double)a.Cohort).ToArray(), lstAvgGapsDn.Select(a => Math.Round(a.AveragePullback, 2)).ToArray(), showValues: true); bmp = plt1.GetBitmap(); DrawImageAt( bmp, 0, 0); bmp = plt2.GetBitmap(); DrawImageAt( bmp, 0, 350); } } }
The following Pine Script for TradingView creates a table showing the extreme price move frequencies (gaps or extreme closes), average pullbacks, and next closes, similar to the type of research Perry Kaufman presents in his June 2024 article, “Trading Opening Gaps And Extreme Closes In Stocks.”
// TASC Issue: July 2024 // Article: Trading Opening Gaps // And Extreme Closes In Stocks. // Article By: Perry J. Kaufman // Language: TradingView's Pine Script™ v5 // Provided By: PineCoders, for tradingview.com //@version=5 string title = 'TASC 2024.07 Gaps and Extreme Closes' string stitle = 'G&C' indicator(title, stitle, false) // Input: string it0 = "Opening Gaps" string it1 = "Extreme Closes" string it2 = "Upward" string it3 = "Downward" string out1 = input.string(it0, "Output", [it0, it1]) string out2 = input.string(it2, "Direction", [it2, it3]) // Data structure: type Threshold int gap_freq_up = 0 int gap_freq_dn = 0 float gap_up = 0.0 float gap_dn = 0.0 float gap_up_pb = 0.0 float gap_dn_pb = 0.0 float gap_up_close = 0.0 float gap_dn_close = 0.0 int close_freq_up = 0 int close_freq_dn = 0 float close_up = 0.0 float close_dn = 0.0 float next_open_up = 0.0 float next_open_dn = 0.0 float next_close_up = 0.0 float next_close_dn = 0.0 type Data float[] thresholds map<float, Threshold> bins // Functions: get_data () => // Gap bins threshold levels: var thresholds = array.new<float>(7) var bins = map.new<float, Threshold>() if barstate.isfirst float x = -0.01 for _i = 0 to 6 x += 0.02 thresholds.set(_i, x) bins.put(x, Threshold.new()) // Gap logic: float gap = open / close[1] - 1.0 // opening gap higher: if gap > 0.0 int saveindex = 0 bool found = false for _i = 0 to 6 if not found if gap < thresholds.get(_i) found := true bini = bins.get(thresholds.get(_i)) bini.gap_freq_up += 1 bini.gap_up += gap saveindex := _i if not found bini = bins.get(thresholds.get(6)) bini.gap_freq_up += 1 bini.gap_up += gap saveindex := 6 // pullback from gap higher: bini = bins.get(thresholds.get(saveindex)) float gap_pb = low / open - 1.0 bini.gap_up_pb += gap_pb // close relative to gap open: float gap_close = close / open - 1.0 bini.gap_up_close += gap_close // opening gap lower: if gap < 0.0 float agap = math.abs(gap) int saveindex = 0 bool found = false for _i = 0 to 6 if not found if agap < thresholds.get(_i) found := true bini = bins.get(thresholds.get(_i)) bini.gap_freq_dn += 1 bini.gap_dn += agap saveindex := _i if not found bini = bins.get(thresholds.get(6)) bini.gap_freq_dn += 1 bini.gap_dn += agap saveindex := 6 // upward pullback from gap lower: bini = bins.get(thresholds.get(saveindex)) float gap_pb = high / open - 1.0 bini.gap_dn_pb += gap_pb // close relative to gap open: float gap_close = close / open - 1.0 bini.gap_dn_close += gap_close // Extreme close logic: float extreme_close = close / close[1] - 1.0 // extreme close higher: if extreme_close > 0.0 int saveindex = 0 bool found = false for _i = 0 to 6 if not found if extreme_close < thresholds.get(_i) found := true bini = bins.get(thresholds.get(_i)) bini.close_freq_up += 1 bini.close_up += extreme_close saveindex := _i log.info('found!') if not found bini = bins.get(thresholds.get(6)) bini.close_freq_up += 1 bini.close_up += extreme_close saveindex := 6 // next open and close: bini = bins.get(thresholds.get(saveindex)) bini.next_open_up += open / close[1] - 1.0 bini.next_close_up += close / close[1] - 1.0 // extreme close lower: if extreme_close < 0.0 float aextreme_close = math.abs(extreme_close) int saveindex = 0 bool found = false for _i = 0 to 6 if not found if aextreme_close < thresholds.get(_i) found := true bini = bins.get(thresholds.get(_i)) bini.close_freq_dn += 1 bini.close_dn += aextreme_close saveindex := _i if not found bini = bins.get(thresholds.get(6)) bini.close_freq_up += 1 bini.close_up += aextreme_close saveindex := 6 // next open and close: bini = bins.get(thresholds.get(saveindex)) bini.next_open_dn += open / close[1] - 1.0 bini.next_close_dn += close / close[1] - 1.0 Data data = Data.new(thresholds, bins) data // Data to lists: method get_gaps_up_freq (Data data) => int[] _freq = array.new<int>(7, int(na)) for [_i, _thrs] in data.thresholds _freq.set(_i, data.bins.get(_thrs).gap_freq_up) _freq method get_gaps_dn_freq (Data data) => int[] _freq = array.new<int>(7, int(na)) for [_i, _thrs] in data.thresholds _freq.set(_i, data.bins.get(_thrs).gap_freq_dn) _freq method get_gaps_up_pb (Data data) => float[] _pb = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pbi = data.bins.get(_thrs).gap_up_pb int _fqi = data.bins.get(_thrs).gap_freq_up if _fqi != 0 // protect agains div by 0 _pb.set(_i, _pbi / _fqi) _pb method get_gaps_dn_pb (Data data) => float[] _pb = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pbi = data.bins.get(_thrs).gap_dn_pb int _fqi = data.bins.get(_thrs).gap_freq_dn if _fqi != 0 // protect agains div by 0 _pb.set(_i, _pbi / _fqi) _pb method get_gaps_up_close_vs_open (Data data) => float[] _cvo = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pi = data.bins.get(_thrs).gap_up_close int _fqi = data.bins.get(_thrs).gap_freq_up if _fqi != 0 // protect agains div by 0 _cvo.set(_i, _pi / _fqi) _cvo method get_gaps_dn_close_vs_open (Data data) => float[] _cvo = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pi = data.bins.get(_thrs).gap_dn_close int _fqi = data.bins.get(_thrs).gap_freq_dn if _fqi != 0 // protect agains div by 0 _cvo.set(_i, _pi / _fqi) _cvo method get_close_up_freq (Data data) => int[] _freq = array.new<int>(7, int(na)) for [_i, _thrs] in data.thresholds _freq.set(_i, data.bins.get(_thrs).close_freq_up) _freq method get_close_dn_freq (Data data) => int[] _freq = array.new<int>(7, int(na)) for [_i, _thrs] in data.thresholds _freq.set(_i, data.bins.get(_thrs).close_freq_dn) _freq method get_close_up_next_open (Data data) => float[] _pb = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pi = data.bins.get(_thrs).next_open_up int _fi = data.bins.get(_thrs).close_freq_up if _fi != 0 // protect agains div by 0 _pb.set(_i, _pi / _fi) _pb method get_close_dn_next_open (Data data) => float[] _pb = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pi = data.bins.get(_thrs).next_open_dn int _fi = data.bins.get(_thrs).close_freq_dn if _fi != 0 // protect agains div by 0 _pb.set(_i, _pi / _fi) _pb method get_close_up_next_close (Data data) => float[] _cvo = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pi = data.bins.get(_thrs).next_close_up int _fi = data.bins.get(_thrs).close_freq_up if _fi != 0 // protect agains div by 0 _cvo.set(_i, _pi / _fi) _cvo method get_close_dn_next_close (Data data) => float[] _cvo = array.new<float>(7, float(na)) for [_i, _thrs] in data.thresholds float _pi = data.bins.get(_thrs).next_close_dn int _fi = data.bins.get(_thrs).close_freq_dn if _fi != 0 // protect agains div by 0 _cvo.set(_i, _pi / _fi) _cvo // Table: method gap_table (Data data, bool gaps, bool dir) => table _tbl = table.new(position.bottom_center, 8, 5) string _title = switch gaps true => dir ? 'GAP UP' : 'GAP DOWN' false => dir ? 'CLOSE UP' : 'CLOSE DOWN' color _col_bg = color.rgb(200, 208, 231) color _col_bg_title = color.rgb(160, 170, 180) color _col_bg_dir = dir ? color.lime : color.orange _tbl.cell(0, 0, _title, bgcolor=_col_bg_title) _tbl.cell(7, 0, _title, bgcolor=_col_bg_title) _tbl.merge_cells(0, 0, 7, 0) _tbl.cell(0, 1, 'Ranges:', bgcolor=_col_bg_dir) _tbl.cell(0, 2, 'Frequency:', bgcolor=_col_bg_dir) _tbl.cell(0, 3, 'Pullback:', bgcolor=_col_bg_dir) _tbl.cell(0, 4, 'Close vs Open:', bgcolor=_col_bg_dir) int[] _freq = switch gaps true => dir ? data.get_gaps_up_freq() : data.get_gaps_dn_freq() false => dir ? data.get_close_up_freq() : data.get_close_dn_freq() float[] _pb = switch gaps true => dir ? data.get_gaps_up_pb() : data.get_gaps_dn_pb() false => dir ? data.get_close_up_next_open() : data.get_close_dn_next_open() float[] _cvo = switch gaps true => dir ? data.get_gaps_up_close_vs_open() : data.get_gaps_dn_close_vs_open() false => dir ? data.get_close_up_next_close() : data.get_close_dn_next_close() for [_i, _thrs] in data.thresholds _r = str.tostring(_i*2-1)+'-'+str.tostring(_i*2+1)+'%' _tbl.cell(1+_i, 1, _r, bgcolor=_col_bg_dir) _tbl.cell(1+_i, 2, str.format('{0,number,integer}', _freq.get(_i)), bgcolor=_col_bg) _tbl.cell(1+_i, 3, str.format('{0,number,#.##%}', _pb.get(_i)), bgcolor=_col_bg) _tbl.cell(1+_i, 4, str.format('{0,number,#.##%}', _cvo.get(_i)), bgcolor=_col_bg) _tbl.cell(1, 1, '<1%', bgcolor=_col_bg_dir) _tbl.cell(7, 1, '>11%', bgcolor=_col_bg_dir) _tbl // Calculations: bool gaps = out1 == it0 bool dir = out2 == it2 data = get_data() if barstate.islastconfirmedhistory table tb = data.gap_table(gaps, dir)
The script is available on TradingView from the PineCodersTASC account: https://www.tradingview.com/u/PineCodersTASC/#published-scripts.
An example output table is shown in Figure 4.
FIGURE 4: TRADINGVIEW. A script creates a table showing the extreme price move frequencies in the data (gaps or extreme closes), average pullbacks, and next closes.
The calculations performed in Perry Kaufman’s article in the June 2024 issue, “Trading Opening Gaps And Extreme Closes In Stocks,” can be easily implemented in NeuroShell Trader by combining some of NeuroShell Trader’s 800+ indicators. To implement the calculations, select “new indicator” from the insert menu and use the indicator wizard to create the following indicators:
Gap: Subtract( Divide( Open, Lag(Close,1) ), 1 ) GapPBup: Subtract( Divide( Low, Open), 1 ) GapPBdown: Subtract( Divide( High, Open), 1 ) GapClose: Subtract( Divide( Close, Open), 1 ) GapUpThreshold: A<B<C ( LowerThreshold, Gap, UpperThreshold ) GapFreqUp: CumSum ( IfThenElse( GapUpThreshold, 1, 0 ), 0 ) GapUp: CumSum ( IfThenElse( GapUpThreshold, Gap, 0 ), 0 ) GapUpPB: CumSum ( IfThenElse( GapUpThreshold, GapPBup, 0 ), 0 ) GapUpClose: CumSum ( IfThenElse( GapUpThreshold, GapClose, 0 ), 0 ) GapDownThreshold: A<B<C ( Mult2(UpperThreshold,-1), Gap, Mult2(LowerThreshold,-1) ) GapFreqDown: CumSum ( IfThenElse( GapDownThreshold, 1, 0 ), 0 ) GapDown: CumSum ( IfThenElse( GapDownThreshold, Gap, 0 ), 0 ) GapDownPB: CumSum ( IfThenElse( GapDownThreshold, GapPBdown, 0 ), 0 ) GapDownClose: CumSum ( IfThenElse( GapDownThreshold, GapClose, 0 ), 0 ) ExtremeClose: Subtract( Divide( Close, Lag(Close, 1) ), 1 ) NextOpen: Subtract( Divide( Open, Lag(Close,1) ), 1 ) NextClose: Subtract( Divide( Close, Lag(Close,1) ), 1 ) CloseUpThreshold: A<B<C ( LowerThreshold, ExtremeClose, UpperThreshold ) CloseFreqUp: CumSum ( IfThenElse( CloseUpThreshold, 1, 0 ), 0 ) CloseUp: CumSum ( IfThenElse( CloseUpThreshold, ExtremeClose, 0 ), 0 ) NextOpenUp: CumSum ( IfThenElse( CloseUpThreshold, NextOpen, 0 ), 0 ) NextCloseUp: CumSum ( IfThenElse( CloseUpThreshold, NextClose, 0 ), 0 ) CloseDownThreshold: A<B<C( Mult2(UpperThreshold,-1), NextClose, Mult2(LowerThreshold,-1) ) CloseFreqDown: CumSum ( IfThenElse( CloseDownThreshold, 1, 0 ), 0 ) CloseDown: CumSum ( IfThenElse( CloseDownThreshold, ExtremeClose, 0 ), 0 ) NextOpenDown: CumSum ( IfThenElse( CloseDownThreshold, NextOpen, 0 ), 0 ) NextCloseDown: CumSum ( IfThenElse( CloseDownThreshold, NextClose, 0 ), 0 ) GAP UP: GapFreqUp GAP UP PULLBACK: Multiply ( 100, Divide( GapUpPB, GapFreqUp ) GAP UP CLOSE VS OPEN: Multiply( 100, Divide( GapUpClose, GapFreqUp ) GAP DOWN: GapFreqUp GAP DOWN PULLBACK: Multiply ( 100, Divide( GapDownPB, GapFreqDown ) GAP DOWN CLOSE VS OPEN: Multiply( 100, Divide( GapDownClose, GapFreqDown ) CLOSE UP: CloseFreqUp NEXT OPEN: Multiply ( 100, Divide( NextOpenUp, CloseFreqUp) NEXT CLOSE: Multiply ( 100, Divide( NextCloseUp, CloseFreqUp) CLOSE DOWN: CloseFreqDown NEXT OPEN: Multiply ( 100, Divide( NextOpenDown, CloseFreqDown) NEXT CLOSE: Multiply ( 100, Divide( NextCloseDown, CloseFreqDown)
Users of NeuroShell Trader can go to the Stocks & Commodities section of the NeuroShell Trader free technical support website to download a copy of this or any previous Traders’ Tips.
In his article in the June 2024 issue, “Trading Opening Gaps And Extreme Closes In Stocks,” Perry Kaufman examines opening gaps and subsequent movements. The goal is not a trading system but rather a statistical analysis of gap-dependent price behavior. The premise investigated is that stocks tend to experience a strong pullback after a large upward gap, but revert to the opening price at the close of the day. This would present an opportunity to sell the stock at market open and then buy it back at a lower price during the day.
In the article, the author provides some EasyLanguage code that makes use of variables and arrays and allows the reader to create one table at a time. In the Zorro platform, users can generate a spreadsheet for the analysis of gaps, pullbacks, and relative closes of seven selected stocks. Here is code in C for use in Zorro to accomplish that:
#define STOCKS "AAPL","AMZN","BABA","BAC","NVDA","TSLA","WMT" #define NA 7 // number of stocks #define NR 7 // number of ranges var Ranges[NR] = { 1,3,5,7,9,11,9999 }; // the gap ranges int Gaps[NA][NR]; var Pullbacks[NA][NR],Closes[NA][NR]; int i,j; void run() { BarPeriod = 1440; StartDate = 20100101; EndDate = 20240101; assetList("AssetsSP500"); // the S&P 500 index if(is(INITRUN)) { // reset the arrays memset(Gaps,0,NA*NR*sizeof(int)); memset(Pullbacks,0,NA*NR*sizeof(var)); memset(Closes,0,NA*NR*sizeof(var)); } for(j=0; 1; j++) { // calculate gaps, pullbacks, relative closes if(!asset(of(STOCKS))) break; // select the stock if(Bar < AssetFirstBar) continue; var Gap = 100*(priceO(0)/priceC(1)-1.); for(i=0; i<NR; i++) if(Gap < Ranges[i]) { Gaps[j][i]++; Pullbacks[j][i] += priceL(0)/priceO(0)-1.; Closes[j][i] += priceC(0)/priceO(0)-1.; break; } } if(is(EXITRUN)) { // print the results to spreadsheet print(TO_CSV,"Range,< 1%%,1-3%%,3-5%%,5-7%%,7-9%%,9-11%%,> 11%%\n"); for(j=0; 1; j++) { if(!asset(of(STOCKS))) break; print(TO_CSV,"%s Gaps",Asset); for(i=0; i<NR; i++) print(TO_CSV,",%i",Gaps[j][i]); print(TO_CSV,"\n"); } print(TO_CSV,"\n"); for(j=0; 1; j++) { if(!asset(of(STOCKS))) break; print(TO_CSV,"%s PBs",Asset); for(i=0; i<NR; i++) print(TO_CSV,",%.2f%%",100*Pullbacks[j][i]/fix0(Gaps[j][i])); print(TO_CSV,"\n"); } print(TO_CSV,"\n"); for(j=0; 1; j++) { if(!asset(of(STOCKS))) break; print(TO_CSV,"%s Cls",Asset); for(i=0; i<NR; i++) print(TO_CSV,",%.2f%%",100*Closes[j][i]/fix0(Gaps[j][i])); print(TO_CSV,"\n"); } exec("Data\\Gaps.csv",0,0); // open spreadsheet in Excel }
I’ll explain some of the code. The of function takes a list of items, such as stock names, and returns one after the other at any call. At the end of the list it returns 0. Since some of the assets did not yet exist in 2010, the AssetFirstBar variable is used to skip that empty part of the history. The memset function initializes all arrays to zero at start. At the end of the history, the EXITRUN flag becomes true and the collected data is printed to a CSV spreadsheet. I’m printing all three statistics to the same spreadsheet. The fix0 function prevents a division-by-zero error, since some ranges have zero gaps. For replicating Kaufman’s results, I have also included negative gaps in the first “< 1%” range.
The spreadsheet shown in Figure 5 yields similar results as in Kaufman’s article, with some deviations since Kaufman probably used a slightly different time range.
FIGURE 5: ZORRO. In the Zorro platform, users can generate a spreadsheet for the analysis of metrics such as gaps, pullbacks, and relative closes of seven selected stocks.
The script can be downloaded from the 2024 script repository on https://financial-hacker.com. The Zorro platform for C/C++ algo trading can be downloaded from https://zorro-project.com.
The importable AIQ EDS file for Perry Kaufman’s article “Trading Opening Gaps And Extreme Closes In Stocks” can be obtained on request via rdencpa@gmail.com. The code is also shown here:
! Trading Openng Gaps And Extreme Closes In Stocks ! Author: Perry Kaufman, TASC July 2024 ! Coded by: Richard Denning, 5/14/2024 gapSize is 7. O is [open]. C is [close]. C1 is valresult(C,1). gap is (O/C1-1)*100. gapdn if gap < 0. gapdn1 is iff(gap < 0 and gap > -1,gap,""). gapdn3 is iff(gap <= -1 and gap > -3,gap,""). gapdn5 is iff(gap <= -3 and gap > -5,gap,""). gapdn7 is iff(gap <= -5 and gap > -7,gap,""). gapdn9 is iff(gap <= -7 and gap > -9,gap,""). gapdn11 is iff(gap <= -9 and gap > -11,gap,""). gapdn11p is iff(gap <= -11 ,gap,""). gapd1 if gap < 0 and gap > -1. gapd3 if gap <= -1 and gap > -3. gapd5 if gap <= -3 and gap > -5. gapd7 if gap <= -5 and gap > -7. gapd9 if gap <= -5 and gap > -9. gapd11 if gap <= -7 and gap > -11. gapd11p if gap <= -11. HD is hasdatafor(2000). !countgapdn1 is iff(showValues,countof(gapd1,HD),""). countgapdn1 is countof(gapd1,HD). countgapdn3 is countof(gapd3,HD). countgapdn5 is countof(gapd5,HD). countgapdn7 is countof(gapd7,HD). countgapdn9 is countof(gapd9,HD). countgapdn11 is countof(gapd11,HD). countgapdn11p is countof(gapd11p,HD). rtrnToClose is (C/O - 1)*100. showValues if C > 10 and HD = 2000. RetOnGapDn if showValues and gap < -gapSize.
A portion of the author’s code is set up in the AIQ EDS code file (the part of the code for the gap downs). The rule “ShowValues” will create a report counting the number of gap downs for stocks on your list that have 2,000 days of data or more and are greater than $10 in price as of the end date of the listing. This report can be exported to a CSV file for further analysis. The rule “RetOnGapDn” can be used to create a backtest in EDS that buys at the open if there is a gap down greater than the “gapSize” input and sells at the close of the same bar to determine the return from open to close. Figures 6 and 7 show the backtesting setup in EDS for the “RetOnGapDn” rule.
FIGURE 6: AIQ. This shows an AIQ setup screen for the pricing with entry and exit on the current bar for a backtest of down gaps.
FIGURE 7: AIQ. This shows an AIQ setup screen for "exit on same bar as entry" for a backtest of down gaps.
A script based on the article “Trading Opening Gaps And Extreme Closes In Stocks” in the June 2024 issue is available for download at the following link for NinjaTrader 8:
Once the file is downloaded, you can import it into NinjaTrader 8 from within the control center by selecting Tools → Import → NinjaScript Add-On and then selecting the downloaded file for NinjaTrader 8.
You can review the source code in NinjaTrader 8 by selecting the menu New → NinjaScript Editor → Indicators folder from within the control center window and selecting the ExtremeGapsAndCloses file.
FIGURE 8: NINJATRADER. Users can explore price behavior in stocks surrounding the open and close.
NinjaScript uses compiled DLLs that run native, not interpreted, to provide you with the highest performance possible.
In “Trading Opening Gaps And Extreme Closes In Stocks,” author Perry Kaufman explores the question, “What is a stock likely to do when large opening moves and large closing moves occur?” The EasyLanguage study provided here writes data as a CSV file in a formatted table for a single symbol and allows you to specify the output file through an input.
Indicator // TASC JULY 2024 // // Extreme Moves Gaps and Closes // Copyright 2023–2024, P J Kaufman. All rights reserved. // Show frequency of gaps within a table of ranges: // gap, pullback, relative close // Show frequency closes within a table of ranges: // close, next open, next close inputs: iFileName( "C:\TradeStation\Extreme_Moves.csv" ); variables: double gap( 0 ), double gappb( 0 ), double gapclose( 0 ), double extremeclose( 0 ), bool found( false ), int ix( 0 ), int saveindex( 0 ), double x( 0 ), string FormatString( "" ), string RangeRowHeaderText( "" ), string Txt( "" ); arrays: double thresholds[7](0), double freq[7](0), double gapfrequp[7](0), double gapfreqdown[7](0), double gapup[7](0), double gapdown[7](0), double gapuppb[7](0), double gapdownpb[7](0), double gapupclose[7](0), double gapdownclose[7](0), double closefrequp[7](0), double closefreqdown[7](0), double closeup[7](0), double closedown[7](0), double nextopenup[7](0), double nextopendown[7](0), double nextCloseup[7](0), double nextClosedown[7](0); method void WriteData( string pTitle ) variables: string OutTxt; begin OutTxt = string.Format( pTitle + "," + FormatString, freq[1], freq[2], freq[3], freq[4], freq[5], freq[6], freq[7]); FileAppend( iFileName, OutTxt ); end; once begin x = -0.01; for ix = 1 to 7 begin x = x + 0.02; thresholds[ix] = x; end; end; // GAP LOGIC gap = open/Close[1] - 1; // opening gap higher if gap > 0 then begin found = false; for ix = 1 to 7 begin if found = false then begin if gap < thresholds[ix] then begin found = true; gapfrequp[ix] = gapfrequp[ix] + 1; gapup[ix] = gapup[ix] + gap; saveindex = ix; end; end; end; if found = false then begin gapfrequp[7] = gapfrequp[7] + 1; gapup[7] = gapup[7] + gap; saveindex = 7; end; // pullback from gap higher gappb = Low / Open - 1; gapuppb[saveindex] = gapuppb[saveindex] + gappb; // close relative to gap open gapclose = Close / Open - 1; gapupclose[saveindex] = gapupclose[saveindex] + gapclose; end; // opening gap lower if gap < 0 then begin found = false; for ix = 1 to 7 begin if found = false then begin if gap < 0 and AbsValue(gap) < thresholds[ix] then begin found = true; gapfreqdown[ix] = gapfreqdown[ix] + 1; gapdown[ix] = gapdown[ix] + gap; saveindex = ix; end; end; end; // upward pullback from gap lower gappb = High / Open - 1; gapdownpb[saveindex] = gapdownpb[saveindex] + gappb; // Close relative to gap open gapclose = Close / Open - 1; gapdownclose[saveindex] = gapdownclose[saveindex] + gapclose; end; // EXTREME CLOSE LOGIC extremeclose = Close / Close[1] - 1; // extreme Close higher if extremeclose > 0 then begin found = false; for ix = 1 to 7 begin if found = false then begin if extremeclose < thresholds[ix] then begin found = true; closefrequp[ix] = closefrequp[ix] + 1; closeup[ix] = closeup[ix] + extremeclose; saveindex = ix; end; end; end; // next open and Close nextopenup[saveindex] = nextopenup[saveindex] + Open / Close[1] - 1; nextcloseup[saveindex] = nextcloseup[saveindex] + Close / Close[1] - 1; end; // extreme Close lower if extremeclose < 0 then begin found = false; for ix = 1 to 7 begin if found = false then begin if absvalue(extremeclose) < thresholds[ix] then begin found = true; closefreqdown[ix] = closefreqdown[ix] + 1; closedown[ix] = closedown[ix] + extremeclose; saveindex = ix; end; end; end; // next open and close nextopendown[saveindex] = nextopendown[saveindex] + Open / Close[1] - 1; nextClosedown[saveindex] = nextclosedown[saveindex] + Close / Close[1] - 1; end; once( LastBarOnChartEx ) begin FormatString = "{0:F2},{1:F2},{2:F2},{3:F2},"; FormatString += "{4:F2},{5:F2},{6:F2}" + NewLine; RangeRowHeaderText = "Range>>,<1%, <3%, <5%, <7%,"; RangeRowHeaderText += "<9%, <11%, >11%" + NewLine; // GAPS UP FileAppend( iFileName, RangeRowHeaderText ); Txt = string.Format( "GAP UP," + FormatString, gapfrequp[1], gapfrequp[2], gapfrequp[3], gapfrequp[4], gapfrequp[5], gapfrequp[6], gapfrequp[7]); FileAppend( iFileName, Txt ); for ix = 1 to 7 begin freq[ix] = 0; if gapfrequp[ix] <> 0 then freq[ix] = 100 * gapuppb[ix] / gapfrequp[ix]; end; WriteData( "GAP UP PULLBACK" ); for ix = 1 to 7 begin freq[ix] = 0; if gapfrequp[ix] <> 0 then freq[ix] = 100 * gapupclose[ix] / gapfrequp[ix]; end; WriteData( "GAP UP CLOSE VS OPEN" ); // GAPS DOWN FileAppend( iFileName, NewLine + RangeRowHeaderText ); Txt = string.Format( "GAP DOWN," + FormatString, gapfreqdown[1], gapfreqdown[2], gapfreqdown[3], gapfreqdown[4], gapfreqdown[5], gapfreqdown[6], gapfreqdown[7]); FileAppend( iFileName, Txt ); for ix = 1 to 7 begin freq[ix] = 0; if gapfreqdown[ix] <> 0 then freq[ix] = 100 * gapdownpb[ix] / gapfreqdown[ix]; end; WriteData( "GAP DOWN PULLBACK" ); for ix = 1 to 7 begin freq[ix] = 0; if gapfreqdown[ix] <> 0 then freq[ix] = 100 * gapdownclose[ix] / gapfreqdown[ix]; end; WriteData( "GAP DOWN CLOSE VS OPEN" ); // CLOSE UP FileAppend( iFileName, NewLine + RangeRowHeaderText ); Txt = string.Format( "CLOSE UP," + FormatString, closefrequp[1], closefrequp[2], closefrequp[3], closefrequp[4], closefrequp[5], closefrequp[6], closefrequp[7]); FileAppend( iFileName, Txt ); for ix = 1 to 7 begin freq[ix] = 0; if closefrequp[ix] <> 0 then freq[ix] = 100 * nextopenup[ix] / closefrequp[ix]; end; WriteData( "NEXT OPEN" ); for ix = 1 to 7 begin freq[ix] = 0; if Closefrequp[ix] <> 0 then freq[ix] = 100 * nextcloseup[ix] / closefrequp[ix]; end; WriteData( "NEXT CLOSE" ); // CLOSE DOWN FileAppend( iFileName, NewLine + RangeRowHeaderText ); Txt = string.Format( "CLOSE DOWN," + FormatString, closefreqdown[1], closefreqdown[2], closefreqdown[3], closefreqdown[4], closefreqdown[5], closefreqdown[6], closefreqdown[7]); FileAppend( iFileName, Txt ); for ix = 1 to 7 begin freq[ix] = 0; if Closefreqdown[ix] <> 0 then freq[ix] = 100 * nextopendown[ix] / Closefreqdown[ix]; end; WriteData( "NEXT OPEN" ); for ix = 1 to 7 begin freq[ix] = 0; if Closefreqdown[ix] <> 0 then freq[ix] = 100 * nextClosedown[ix] / Closefreqdown[ix]; end; WriteData( "NEXT CLOSE" ); end;
FIGURE 9: TRADESTATION. This demonstrates sample output from the indicator applied to a 15-year daily chart of Boeing (BA).