//@version=6 // This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/ // © 2026 Logan's Grove Incorporated · logansgrove.com // Strategy Lab, version 1.0, October 2026. Results in "Instruction Set Trading" by Rex Dale Johnson, Chapter 2. // How to run it and what I found: logansgrove.com/test-scripts // Education only, not financial advice. Run it yourself and check my work. // strategy("Logan's Grove Strategy Lab", shorttitle = "Strategy Lab", overlay = true, initial_capital = 1000000, currency = currency.USD, pyramiding = 0, margin_long = 1, margin_short = 1, commission_value = 0, slippage = 0, max_bars_back = 3000) // Tests seven standard strategies under the same rules, so students can see real results // instead of hunting for the Holy Grail. // 4-hour chart: setups are judged and entered at the 4-hour close. // 1-hour chart: the same 4-hour setups, entered early on the 1-hour chart. // Results are in R (entry to initial stop), after the round-trip cost set below. // Pass rule, set in advance: 100+ trades, average R of +0.10 or better, t-stat of 2 or more. // ─────────────────────────── Inputs ─────────────────────────── const string S_OBOS = "Overbought / oversold (RSI)" const string S_BRK = "Consolidation breakout" const string S_REV = "Major reversal" const string S_SWEET = "Sweet spot re-entry" const string S_MA = "Moving average crossover" const string S_MACD = "MACD crossover" const string S_PAT = "Patterns (inside the formation)" const string S_NEST = "Nested pullback re-entry" grpS = "Strategy" strat = input.string(S_OBOS, "Strategy", options = [S_OBOS, S_BRK, S_REV, S_SWEET, S_MA, S_MACD, S_PAT, S_NEST], group = grpS) dirMode = input.string("Both", "Direction", options = ["Both", "Long only", "Short only"], group = grpS) startT = input.time(timestamp("01 Jan 2010 00:00 +0000"), "Test from", group = grpS) grpC = "Risk, exits and costs (all strategies)" riskPct = input.float(0.5, "Risk per trade (% of equity)", minval = 0.05, step = 0.05, group = grpC) noiseAtr = input.float(0.25, "Room for noise beyond stops (x 4H ATR)", minval = 0, step = 0.05, group = grpC) targetR = input.float(2.0, "Target for breakout, reversal and sweet spot (R)", minval = 0.5, step = 0.25, group = grpC) indStop = input.float(2.0, "Stop for RSI, MA and MACD (x 4H ATR)", minval = 0.5, step = 0.25, group = grpC) maxHold = input.int(30, "Close after (4H bars) if no exit", minval = 1, group = grpC, tooltip = "Not used by the MA and MACD crossovers, which stay in until the opposite cross or the stop.") costTicks = input.float(10, "Round-trip cost (ticks)", minval = 0, step = 1, group = grpC, tooltip = "Spread plus commission plus slippage for one round trip, in ticks of this symbol. Suggested: EURUSD, GBPUSD, AUDUSD, USDJPY 10 (1 pip); EURJPY, GBPJPY 20; XAUUSD 30; ES 2; NQ 3; YM 3; SPX500 CFD 5; NAS100 CFD 15.") grpO = "Overbought / oversold" rsiLen = input.int(14, "RSI length", minval = 2, group = grpO) rsiLo = input.float(30, "Buy when RSI crosses back above", group = grpO) rsiHi = input.float(70, "Sell when RSI crosses back below", group = grpO) rsiExit = input.float(50, "Exit when RSI reaches", group = grpO) grpB = "Consolidation breakout" consLen = input.int(20, "Consolidation length (4H bars)", minval = 5, group = grpB) consAtr = input.float(3.0, "Consolidation is no wider than (x 4H ATR)", minval = 0.5, step = 0.25, group = grpB, tooltip = "Buy a close above the range, sell a close below it. Stop at the middle of the range.") grpR = "Major reversal" lvlStr = input.int(5, "Support/resistance swing size (4H bars each side)", minval = 2, group = grpR) touchAt = input.float(0.25, "Counts as a hit within (x 4H ATR)", minval = 0, step = 0.05, group = grpR) revPct = input.float(30, "Reversal needed (% of the move into the level)", minval = 5, maxval = 90, step = 5, group = grpR) moveLen = input.int(20, "Move into the level measured over (4H bars)", minval = 5, group = grpR) revArm = input.int(10, "Reversal must come within (4H bars)", minval = 1, group = grpR, tooltip = "Stop beyond the level (or the extreme, whichever is further) plus the noise room.") grpW = "Sweet spot re-entry" trendLen = input.int(50, "Trend EMA (4H)", minval = 5, group = grpW) pbAtr = input.float(0.5, "Pullback at least (x 4H ATR)", minval = 0.1, step = 0.1, group = grpW) swArm = input.int(2, "Continuation must come within (4H bars)", minval = 1, group = grpW, tooltip = "In a trend, price pulls back and makes a pivot. Entry when price closes beyond the pivot bar. Stop beyond the pivot plus the noise room.") const string H_OFF = "Off" const string H_D = "Daily" const string H_DW = "Daily + weekly" const string H_DWM = "Daily + weekly + monthly" grpH = "Higher-timeframe cycles (top-down filter, all strategies)" htfMode = input.string(H_OFF, "Trade only with the cycle on", options = [H_OFF, H_D, H_DW, H_DWM], group = grpH, tooltip = "A cycle is rising after its last confirmed swing low and falling after its last confirmed swing high (a zigzag of pivots, as in Pivot ZigZag). Longs only while every chosen cycle is rising, shorts only while every one is falling. Values come from the previous completed daily, weekly and monthly bar, so nothing is known early.") htfLeft = input.int(5, "Higher-timeframe swing size (bars each side)", minval = 1, group = grpH) const string N_TGT = "Target (R, set above)" const string N_DAY = "When the daily cycle turns" grpN = "Nested pullback re-entry" nestExit = input.string(N_TGT, "Exit", options = [N_TGT, N_DAY], group = grpN, tooltip = "Long: weekly cycle rising, daily cycle falling (a pullback in the bigger cycle), then the 4H confirms a swing low and closes above that swing bar's high. Short is the mirror. Stop beyond the 4H swing plus the noise room. 'When the daily cycle turns' has no target or time exit: it holds until the daily cycle has turned back and then confirms its next swing against the trade. Uses the higher-timeframe swing size above and the 4H swing size under Patterns and swings; the cycle filter setting is ignored.") nestDaily = nestExit == N_DAY grpM = "Moving averages and MACD" maFast = input.int(50, "MA fast (4H)", minval = 2, group = grpM) maSlow = input.int(200, "MA slow (4H)", minval = 3, group = grpM) maType = input.string("Simple", "MA type", options = ["Simple", "Exponential"], group = grpM) macdFast = input.int(12, "MACD fast", minval = 2, group = grpM) macdSlow = input.int(26, "MACD slow", minval = 3, group = grpM) macdSig = input.int(9, "MACD signal", minval = 2, group = grpM) macdZero = input.bool(false, "MACD: buy only below zero, sell only above zero", group = grpM) grpP = "Patterns and swings" patSet = input.string("All four", "Patterns traded", options = ["All four", "Head & shoulders only", "W and M only"], group = grpP) swLeft = input.int(3, "Swing size (4H bars each side)", minval = 1, group = grpP) trigBars = input.int(2, "Pivot confirmed after (chart bars)", minval = 1, group = grpP, tooltip = "The right-leg, right-shoulder and pullback pivots are confirmed after this many chart bars: 8 hours on the 4H chart, 2 hours on the 1H chart. That is the early entry.") shapeTol = input.float(1.0, "Matching lows, highs and neckline within (x 4H ATR)", minval = 0.1, step = 0.1, group = grpP) minDepth = input.float(1.0, "W and M middle swing at least (x 4H ATR)", minval = 0.1, step = 0.1, group = grpP) wExit = input.float(0.9, "W and M: out at this share of the way to the middle swing", minval = 0.3, maxval = 1, step = 0.05, group = grpP) minRR = input.float(0.5, "Skip if the planned reward is under (R)", minval = 0, step = 0.25, group = grpP) showTable = input.bool(true, "Show results table", group = "Display") tblPos = input.string("Top right", "Table position", options = ["Top right", "Middle right", "Bottom right", "Top left", "Bottom left"], group = "Display") tblSize = input.string("Normal", "Table text size", options = ["Small", "Normal", "Large"], group = "Display", tooltip = "Normal or Large reads well on a shared screen or a recorded class.") // ───────────────────────── Timeframe ───────────────────────── chartSec = timeframe.in_seconds() setupSec = timeframe.in_seconds("240") if barstate.isfirst and (chartSec > setupSec or setupSec % chartSec != 0) runtime.error("Run the Strategy Lab on a 4-hour chart (entries at the 4H close) or a 1-hour chart (1H early entry).") mult = math.max(1, int(math.round(setupSec / chartSec))) isEarly = mult > 1 // ───────────────────── 4-hour values (completed bars only) ───────────────────── f4() => ch = ta.change(close) ag = ta.rma(math.max(ch, 0.0), rsiLen) al = ta.rma(math.max(-ch, 0.0), rsiLen) e1 = ta.ema(close, macdFast) e2 = ta.ema(close, macdSlow) sg = ta.ema(e1 - e2, macdSig) et = ta.ema(close, trendLen) [close[1], ta.atr(14)[1], ag[1], al[1], math.sum(close, maFast - 1)[1], math.sum(close, maSlow - 1)[1], e1[1], e2[1], sg[1], et[1], et[6], ta.ema(close, maFast)[1], ta.ema(close, maSlow)[1]] [c4, atr4, ag4, al4, sumF4, sumS4, e14, e24, sg4, et4, et4Old, emF4, emS4] = request.security(syminfo.tickerid, "240", f4(), lookahead = barmerge.lookahead_on) // The 4H indicator as it would read if the current 4H bar closed now. On the 4H chart this is // the normal value; on the 1H chart it moves with each 1H close, which is what makes the entry early. chD = close - c4 agD = (ag4 * (rsiLen - 1) + math.max(chD, 0.0)) / rsiLen alD = (al4 * (rsiLen - 1) + math.max(-chD, 0.0)) / rsiLen rsiD = alD == 0 ? 100.0 : 100.0 - 100.0 / (1.0 + agD / alD) isEma = maType == "Exponential" smaF = isEma ? emF4 + 2.0 / (maFast + 1) * (close - emF4) : (sumF4 + close) / maFast smaS = isEma ? emS4 + 2.0 / (maSlow + 1) * (close - emS4) : (sumS4 + close) / maSlow e1D = e14 + 2.0 / (macdFast + 1) * (close - e14) e2D = e24 + 2.0 / (macdSlow + 1) * (close - e24) macdD = e1D - e2D sgD = sg4 + 2.0 / (macdSig + 1) * (macdD - sg4) etD = et4 + 2.0 / (trendLen + 1) * (close - et4) trendUp = close > etD and et4 > et4Old trendDn = close < etD and et4 < et4Old atrN = nz(atr4, ta.atr(14 * mult)) noise = noiseAtr * atrN // ───────────────── Higher-timeframe cycles (daily, weekly, monthly) ───────────────── // Leg direction of a pivot zigzag: 1 rising (last confirmed swing was a low), -1 falling, 0 none yet. // Taken from the previous completed higher-timeframe bar, so the filter never sees the future. legDir(int lb) => hp = ta.pivothigh(high, lb, lb) lp = ta.pivotlow(low, lb, lb) var int d = 0 if not na(hp) d := -1 if not na(lp) d := 1 d[1] dLeg = request.security(syminfo.tickerid, "D", legDir(htfLeft), lookahead = barmerge.lookahead_on) wLeg = request.security(syminfo.tickerid, "W", legDir(htfLeft), lookahead = barmerge.lookahead_on) mLeg = request.security(syminfo.tickerid, "M", legDir(htfLeft), lookahead = barmerge.lookahead_on) needD = htfMode != H_OFF needW = htfMode == H_DW or htfMode == H_DWM needM = htfMode == H_DWM cycOkL = not needD or (dLeg == 1 and (not needW or wLeg == 1) and (not needM or mLeg == 1)) cycOkS = not needD or (dLeg == -1 and (not needW or wLeg == -1) and (not needM or mLeg == -1)) // ───────────────────────── Swings ───────────────────────── L = swLeft * mult R = 2 * mult TR = trigBars ph = ta.pivothigh(high, L, R) pl = ta.pivotlow(low, L, R) tph = ta.pivothigh(high, L, TR) tpl = ta.pivotlow(low, L, TR) // Zigzag of confirmed 4H-scale swings: type (1 high, -1 low), price, bar var array zT = array.new() var array zP = array.new() var array zB = array.new() pushPiv(int t, float p, int b) => int n = zT.size() if n > 0 and zT.get(n - 1) == t if (t == 1 and p > zP.get(n - 1)) or (t == -1 and p < zP.get(n - 1)) zP.set(n - 1, p) zB.set(n - 1, b) else zT.push(t) zP.push(p) zB.push(b) if zT.size() > 12 zT.shift() zP.shift() zB.shift() true // ───────────────────────── Signals ───────────────────────── bool sigL = false bool sigS = false float stL = na float tgL = na float stS = na float tgS = na string tagL = "" string tagS = "" bool exitL = false bool exitS = false string exitTag = "" okRR(bool isLong, float stop, float tgt) => risk = isLong ? close - stop : stop - close rew = isLong ? tgt - close : close - tgt risk > 0 and rew > 0 and rew / risk >= minRR // 1. Overbought / oversold rsiUp = ta.crossover(rsiD, rsiLo) rsiDn = ta.crossunder(rsiD, rsiHi) if strat == S_OBOS if rsiUp sigL := true stL := close - indStop * atrN tagL := "RSI up through " + str.tostring(rsiLo) if rsiDn sigS := true stS := close + indStop * atrN tagS := "RSI down through " + str.tostring(rsiHi) exitL := rsiD >= rsiExit exitS := rsiD <= rsiExit exitTag := "RSI " + str.tostring(rsiExit) // 2. Consolidation breakout cl = consLen * mult rgHi = ta.highest(high, cl)[1] rgLo = ta.lowest(low, cl)[1] isCons = rgHi - rgLo <= consAtr * atrN if strat == S_BRK and isCons mid = (rgHi + rgLo) / 2 if close > rgHi and close[1] <= rgHi sigL := true stL := mid tgL := close + targetR * (close - mid) tagL := "Breakout up" else if close < rgLo and close[1] >= rgLo sigS := true stS := mid tgS := close - targetR * (mid - close) tagS := "Breakout down" // 3. Major reversal: price hits a previous swing level, then reverses a share of the move into it lph = ta.pivothigh(high, lvlStr * mult, lvlStr * mult) lpl = ta.pivotlow(low, lvlStr * mult, lvlStr * mult) var float res = na var float sup = na if not na(lph) res := lph if not na(lpl) sup := lpl moveLo = ta.lowest(low, moveLen * mult) moveHi = ta.highest(high, moveLen * mult) touch = touchAt * atrN var bool rvS = false var float rvLvlS = na var float rvHiS = na var float rvLoS = na var int rvBarS = na var bool rvL = false var float rvLvlL = na var float rvHiL = na var float rvLoL = na var int rvBarL = na if strat == S_REV // Resistance: approach from below if not rvS and not na(res) and high >= res - touch and high[1] < res - touch and moveLo < res rvS := true rvLvlS := res rvHiS := high rvLoS := moveLo rvBarS := bar_index res := na if rvS rvHiS := math.max(rvHiS, high) if close > rvLvlS + noise rvS := false else if close <= rvHiS - revPct / 100 * (rvHiS - rvLoS) sigS := true stS := math.max(rvLvlS, rvHiS) + noise tgS := close - targetR * (stS - close) tagS := "Reversal at resistance" rvS := false else if bar_index - rvBarS >= revArm * mult rvS := false // Support: approach from above if not rvL and not na(sup) and low <= sup + touch and low[1] > sup + touch and moveHi > sup rvL := true rvLvlL := sup rvLoL := low rvHiL := moveHi rvBarL := bar_index sup := na if rvL rvLoL := math.min(rvLoL, low) if close < rvLvlL - noise rvL := false else if close >= rvLoL + revPct / 100 * (rvHiL - rvLoL) sigL := true stL := math.min(rvLvlL, rvLoL) - noise tgL := close + targetR * (close - stL) tagL := "Reversal at support" rvL := false else if bar_index - rvBarL >= revArm * mult rvL := false // 4. Sweet spot re-entry: trend, pullback, pivot, continuation hiAround = ta.highest(high, L + TR + 1) loAround = ta.lowest(low, L + TR + 1) var bool swL = false var float swPivL = na var float swTrgL = na var int swBarL = na var bool swS = false var float swPivS = na var float swTrgS = na var int swBarS = na if strat == S_SWEET if not na(tpl) and trendUp and hiAround - tpl >= pbAtr * atrN swL := true swPivL := tpl swTrgL := high[TR] swBarL := bar_index if swL if low < swPivL swL := false else if close > swTrgL sigL := true stL := swPivL - noise tgL := close + targetR * (close - stL) tagL := "Sweet spot long" swL := false else if bar_index - swBarL >= swArm * mult swL := false if not na(tph) and trendDn and tph - loAround >= pbAtr * atrN swS := true swPivS := tph swTrgS := low[TR] swBarS := bar_index if swS if high > swPivS swS := false else if close < swTrgS sigS := true stS := swPivS + noise tgS := close - targetR * (stS - close) tagS := "Sweet spot short" swS := false else if bar_index - swBarS >= swArm * mult swS := false // 8. Nested pullback re-entry: the weekly cycle is in place, the daily cycle pulls back, // and the 4H turns back with the weekly. Unlike the sweet spot, there is no 4H trend condition: // during a daily pullback the 4H is usually falling, and its turn is the entry. var bool nsL = false var float nsPivL = na var float nsTrgL = na var int nsBarL = na var bool nsS = false var float nsPivS = na var float nsTrgS = na var int nsBarS = na if strat == S_NEST if not na(tpl) and wLeg == 1 and dLeg == -1 nsL := true nsPivL := tpl nsTrgL := high[TR] nsBarL := bar_index if nsL if low < nsPivL or wLeg != 1 nsL := false else if close > nsTrgL sigL := true stL := nsPivL - noise tgL := nestDaily ? na : close + targetR * (close - stL) tagL := "Nested pullback long" nsL := false else if bar_index - nsBarL >= swArm * mult nsL := false if not na(tph) and wLeg == -1 and dLeg == 1 nsS := true nsPivS := tph nsTrgS := low[TR] nsBarS := bar_index if nsS if high > nsPivS or wLeg != -1 nsS := false else if close < nsTrgS sigS := true stS := nsPivS + noise tgS := nestDaily ? na : close - targetR * (stS - close) tagS := "Nested pullback short" nsS := false else if bar_index - nsBarS >= swArm * mult nsS := false // 5. Moving average crossover and 6. MACD crossover (stop and reverse) maUp = ta.crossover(smaF, smaS) maDn = ta.crossunder(smaF, smaS) macdUp = ta.crossover(macdD, sgD) macdDn = ta.crossunder(macdD, sgD) if strat == S_MA or strat == S_MACD up = strat == S_MA ? maUp : macdUp and (not macdZero or macdD < 0) dn = strat == S_MA ? maDn : macdDn and (not macdZero or macdD > 0) if up sigL := true stL := close - indStop * atrN tagL := strat == S_MA ? "MA cross up" : "MACD cross up" if dn sigS := true stS := close + indStop * atrN tagS := strat == S_MA ? "MA cross down" : "MACD cross down" // 7. Patterns, traded inside the formation (the formation never has to complete) var int usedLegL = na var int usedLegS = na doHS = patSet != "W and M only" doWM = patSet != "Head & shoulders only" tol = shapeTol * atrN if strat == S_PAT n = zT.size() // Long trades: the right shoulder or right leg is a new low pivot if not na(tpl) and n >= 2 and zT.get(n - 1) == 1 and bar_index - TR > zB.get(n - 1) and zB.get(n - 1) != nz(usedLegL, -1) bool done = false if doHS and n >= 4 and zT.get(n - 4) == -1 s1 = zP.get(n - 4) n1 = zP.get(n - 3) hd = zP.get(n - 2) n2 = zP.get(n - 1) neck = math.min(n1, n2) if hd < s1 and tpl > hd + 0.25 * atrN and tpl < neck - 0.5 * atrN and math.abs(n1 - n2) <= tol st = tpl - noise if okRR(true, st, neck) sigL := true stL := st tgL := neck tagL := "Inverse H&S right shoulder" done := true if not done and doWM h1 = zP.get(n - 1) l1 = zP.get(n - 2) if math.abs(tpl - l1) <= tol and h1 - math.max(l1, tpl) >= minDepth * atrN st = tpl - noise tg = tpl + wExit * (h1 - tpl) if okRR(true, st, tg) sigL := true stL := st tgL := tg tagL := "W right leg" if sigL usedLegL := zB.get(n - 1) // Short trades: the right shoulder or right top is a new high pivot if not na(tph) and n >= 2 and zT.get(n - 1) == -1 and bar_index - TR > zB.get(n - 1) and zB.get(n - 1) != nz(usedLegS, -1) bool done = false if doHS and n >= 4 and zT.get(n - 4) == 1 s1 = zP.get(n - 4) n1 = zP.get(n - 3) hd = zP.get(n - 2) n2 = zP.get(n - 1) neck = math.max(n1, n2) if hd > s1 and tph < hd - 0.25 * atrN and tph > neck + 0.5 * atrN and math.abs(n1 - n2) <= tol st = tph + noise if okRR(false, st, neck) sigS := true stS := st tgS := neck tagS := "H&S right shoulder" done := true if not done and doWM l1 = zP.get(n - 1) h1 = zP.get(n - 2) if math.abs(tph - h1) <= tol and math.min(h1, tph) - l1 >= minDepth * atrN st = tph + noise tg = tph - wExit * (tph - l1) if okRR(false, st, tg) sigS := true stS := st tgS := tg tagS := "M right top" if sigS usedLegS := zB.get(n - 1) // Update the zigzag after the trigger checks, so a right leg is judged against the swings before it if not na(pl) pushPiv(-1, pl, bar_index - R) if not na(ph) pushPiv(1, ph, bar_index - R) // ───────────────────── Results tracking (in R, after costs) ───────────────────── var array tStop = array.new() var array tDist = array.new() var int seen = 0 var float nT = 0 var float nWin = 0 var float sumR = 0 var float sumR2 = 0 var float cumR = 0 var float peakR = 0 var float maxDD = 0 var int streak = 0 var int maxStreak = 0 var int entryBar = na if strategy.closedtrades > seen for i = seen to strategy.closedtrades - 1 if i < tStop.size() ep = strategy.closedtrades.entry_price(i) xp = strategy.closedtrades.exit_price(i) dir = strategy.closedtrades.size(i) > 0 ? 1.0 : -1.0 risk = math.abs(ep - tStop.get(i)) if risk < 0.25 * tDist.get(i) risk := tDist.get(i) r = dir * (xp - ep) / risk - costTicks * syminfo.mintick / risk nT += 1 nWin += r > 0 ? 1 : 0 sumR += r sumR2 += r * r cumR += r peakR := math.max(peakR, cumR) maxDD := math.max(maxDD, peakR - cumR) streak := r < 0 ? streak + 1 : 0 maxStreak := math.max(maxStreak, streak) log.info("{0} | {1} | R {2}", strategy.closedtrades.entry_comment(i), dir > 0 ? "long" : "short", str.tostring(r, "0.00")) seen := strategy.closedtrades // ───────────────────────── Orders ───────────────────────── enter(bool isLong, float stop, float tgt, string tag) => float dist = isLong ? close - stop : stop - close bool ok = not na(stop) and dist > 0 if ok q = math.max(1.0, math.floor(strategy.equity * riskPct / 100 / (dist * syminfo.pointvalue))) tStop.push(stop) tDist.push(dist) string id = isLong ? "Long" : "Short" // The stop distance rides in the comment so a List of trades export can be turned into R exactly strategy.entry(id, isLong ? strategy.long : strategy.short, qty = q, comment = tag + " | risk " + str.tostring(dist, format.mintick)) strategy.exit(id + " exit", id, stop = stop, limit = tgt) ok reversing = strat == S_MA or strat == S_MACD // The nested strategy has its cycle conditions built in, so the cycle filter doesn't apply to it isNest = strat == S_NEST allowL = dirMode != "Short only" and (isNest or cycOkL) allowS = dirMode != "Long only" and (isNest or cycOkS) inTime = time >= startT pos = strategy.position_size // Nested, daily-cycle exit: wait for the daily cycle to turn back with the trade, then leave // when it confirms its next swing against it var bool dayTurned = false if isNest and nestDaily and pos != 0 if (pos > 0 and dLeg == 1) or (pos < 0 and dLeg == -1) dayTurned := true if dayTurned and ((pos > 0 and dLeg == -1) or (pos < 0 and dLeg == 1)) strategy.close_all(comment = "Daily cycle turned") // Indicator exits and time exits if pos > 0 and exitL strategy.close("Long", comment = exitTag) if pos < 0 and exitS strategy.close("Short", comment = exitTag) if reversing and pos > 0 and sigS and not allowS strategy.close("Long", comment = "Opposite cross") if reversing and pos < 0 and sigL and not allowL strategy.close("Short", comment = "Opposite cross") if not reversing and not (isNest and nestDaily) and pos != 0 and not na(entryBar) and bar_index - entryBar >= maxHold * mult strategy.close_all(comment = "Time exit") bool tookL = false bool tookS = false if inTime and sigL and not sigS and allowL and (pos == 0 or (reversing and pos < 0)) tookL := enter(true, stL, tgL, tagL) else if inTime and sigS and not sigL and allowS and (pos == 0 or (reversing and pos > 0)) tookS := enter(false, stS, tgS, tagS) if tookL or tookS entryBar := bar_index dayTurned := false // ─────────────────────────── Visuals ─────────────────────────── plotshape(tookL, "Long", shape.labelup, location.belowbar, color.new(#03B050, 0), text = "L", textcolor = color.white, size = size.tiny) plotshape(tookS, "Short", shape.labeldown, location.abovebar, color.new(#B4472F, 0), text = "S", textcolor = color.white, size = size.tiny) tPos = tblPos == "Top left" ? position.top_left : tblPos == "Bottom left" ? position.bottom_left : tblPos == "Bottom right" ? position.bottom_right : tblPos == "Middle right" ? position.middle_right : position.top_right tTxt = tblSize == "Large" ? size.large : tblSize == "Normal" ? size.normal : size.small var table tb = table.new(tPos, 2, 15, bgcolor = color.new(#252525, 5), frame_color = color.new(#74B3CE, 40), frame_width = 1, border_color = color.new(#74B3CE, 80), border_width = 1) row(int r, string k, string v, color vc = color.white) => table.cell(tb, 0, r, k, text_color = color.new(#E6FAFC, 0), text_size = tTxt, text_halign = text.align_left) table.cell(tb, 1, r, v, text_color = vc, text_size = tTxt, text_halign = text.align_right) sgn(float x, string f) => na(x) ? "–" : (x >= 0 ? "+" : "") + str.tostring(x, f) // A strategy only calculates when a bar closes, so on a live chart it never runs on the open last bar. // Drawing on the last completed bar as well keeps the table visible while the market is open. if showTable and (barstate.islastconfirmedhistory or barstate.islast) mean = nT > 0 ? sumR / nT : na sd = nT > 1 ? math.sqrt(math.max(sumR2 - nT * mean * mean, 0) / (nT - 1)) : na t = nT > 1 and sd > 0 ? mean / (sd / math.sqrt(nT)) : na pass = nT >= 100 and mean >= 0.10 and t >= 2 verdict = nT < 100 ? "Too few trades (" + str.tostring(nT, "#") + " of 100)" : pass ? "PASSES" : "No edge" vCol = pass ? color.new(#03B050, 0) : nT < 100 ? color.new(#74B3CE, 0) : color.new(#E8826D, 0) table.cell(tb, 0, 0, "LOGAN'S GROVE STRATEGY LAB", text_color = color.new(#03B050, 0), text_size = tTxt, text_halign = text.align_center) table.merge_cells(tb, 0, 0, 1, 0) row(1, "Strategy", strat, color.new(#03B050, 0)) row(2, "Entry timing", isEarly ? "1H early entry" : "4H close") row(3, "Market", syminfo.ticker) row(4, "Cost per round trip", str.tostring(costTicks, "#.#") + " ticks") row(5, "Cycle filter", isNest ? "Built in: W up, D pullback" + (nestDaily ? ", daily exit" : "") : htfMode, isNest or needD ? color.new(#03B050, 0) : color.white) row(6, "Trades", str.tostring(nT, "#")) row(7, "Win rate", nT > 0 ? str.tostring(100 * nWin / nT, "#") + "%" : "–") row(8, "Average R after costs", sgn(mean, "0.00")) row(9, "Total R", sgn(nT > 0 ? sumR : na, "0.0")) row(10, "t-stat", sgn(t, "0.0")) row(11, "Largest drawdown", nT > 0 ? str.tostring(maxDD, "0.0") + " R" : "–") row(12, "Longest losing streak", str.tostring(maxStreak) + " trades") row(13, "Verdict", verdict, vCol) table.cell(tb, 0, 14, "Pass: 100+ trades, average +0.10R or better, t-stat 2+", text_color = color.new(#74B3CE, 0), text_size = tblSize == "Small" ? size.tiny : size.small, text_halign = text.align_center) table.merge_cells(tb, 0, 14, 1, 14)