//@version=5 // ============================================================================= // THE 18-BAR TREND SYSTEM — Rosputnia / Williams 18-Day Moving Average Method // ============================================================================= // PROVENANCE — read this before you publish anything about it. // // Inna Rosputnia publishes an 18-day moving-average trend system on her own site // and in MoneyShow. The entry/exit rules below are transcribed from those pages, // not invented. Where her two accounts disagree, both options are exposed as // inputs rather than silently picking one. // // That same system — the 18-day MA of closes, the two-consecutive-bars rule, the // entry at the extreme of those two bars, the opposite-signal stop — is Larry // Williams' published "18 bar entry technique". Likewise the two indicators her // syndicated bios call "her proprietary" tools: // ProGo -> Larry Williams, dated 1997 on ireallytrade.com // POIV -> Larry Williams, Futures magazine, December 2007 // Note in fairness: her OWN websites never claim either indicator. The // "proprietary" wording appears only on third-party syndicated bio pages. // // Every rule below is tagged: // [HER] - transcribed from a page where she states it // [LW] - Larry Williams' published formula that she references by name // [MINE] - an engineering decision neither of them specified // // ----------------------------------------------------------------------------- // KNOWN CONTRADICTIONS IN THE SOURCE MATERIAL (exposed as inputs, not hidden) // 1. Entry trigger: her site says "highest high of the two bars"; MoneyShow // says "true high", which in Williams' vocabulary is max(high, prev close). // -> input: Entry Trigger // 2. MA length: on her own site she says the length is not important; the // MoneyShow piece calls 18 the most accurate. Both are hers, ~10 months // apart. Do NOT present 18 as a tested, settled parameter. // 3. ProGo signal direction: published sources contradict each other on // whether pro-above-public is bullish or bearish. -> input: invert toggle // // She states plainly that this system is standalone-profitable but only // middling-accuracy and whipsaw-prone. Expect that in the backtest. // // ----------------------------------------------------------------------------- // [FIX] VALIDATED IMPROVEMENTS (defaults below reflect these) // Each candidate fix was backtested across 5 commodity futures (GC, CL, SI, HG, // NG) in two disjoint windows (2015-2019 in-sample, 2020-2026 out-of-sample). // A fix was accepted only if it beat baseline in >=3 of 5 markets IN BOTH // windows. Results: // ACCEPTED Setup Valid = 20 bars (was 5): monotone improvement 5->10->20 in // 9/10 market-windows, plateau at 40. Replicated out-of-sample. // ACCEPTED Long-only (shorts off): PF up in 9/10 market-windows, drawdown // DOWN in 10/10. Short side lost money in 9/10 cells across BOTH // regimes - the drag is structural, not 2020s bull-market luck. // ACCEPTED Instruments: GC + CL core (PF > 1.5 both windows under combo); // SI watchlist-only (out-of-sample only); HG, NG, ES excluded. // REJECTED SMA-100/200 regime filter (one-good-window curve-fit signature; // redundant with long-only), ADX filter (collapsed out-of-sample, // noise-floor trade counts), ATR protective stop on the combo // (PF slightly worse in every cell - the opposite-signal exit // already does the job), short-only, setup_valid=40. // Combined result, GC full period 2015-2026: PF 1.30 -> 2.49, max DD 15% -> 9%. // CL: PF 1.40 -> 2.00, DD 13% -> 8.6%. Caveat: some of the out-of-sample lift // is long-bias riding the 2020-2026 commodity bull; the in-sample improvements // (GC 1.06->1.79, CL 1.24->1.57) are the evidence it is more than regime luck. // To run HER faithful system instead: Setup Valid = 5, Allow Shorts = on. // ============================================================================= strategy("The 18-Bar Trend System (Rosputnia / Williams)", shorttitle = "18BAR", overlay = true, initial_capital = 100000, default_qty_type = strategy.fixed, default_qty_value = 1, commission_type = strategy.commission.cash_per_contract, commission_value = 2.50, slippage = 2, pyramiding = 2, calc_on_every_tick = false, process_orders_on_close = false, max_bars_back = 500) // ============================================================================= // INPUTS // ============================================================================= grpCore = "Core System (18-Bar Method)" maLenInput = input.int(18, "MA Length [HER: 18-day SMA of closes]", minval = 2, group = grpCore) maTypeInput = input.string("SMA", "MA Type [HER: simple]", options = ["SMA", "EMA"], group = grpCore) setupBarsInput = input.int(2, "Consecutive Bars Beyond MA [HER: 2]", minval = 1, maxval = 5, group = grpCore) excludeInsideInput = input.bool(true, "Exclude Inside Days [HER]", group = grpCore) entryTrigInput = input.string("Highest High (her site)", "Entry Trigger [source conflict]", options = ["Highest High (her site)", "True High (MoneyShow)"], group = grpCore) setupValidInput = input.int(20, "Setup Valid For (bars) [FIX validated; her-faithful = 5]", minval = 1, group = grpCore) // ----------------------------------------------------------------------------- // ProGo has NO canonical formula. Larry Williams names it on his own site // (dated 1997) but never published the calculation. Every version in // circulation is a third-party reconstruction and they disagree on BOTH: // (a) sign of the public line: open-close[1] vs close[1]-open // (b) structure: 14-period SMA of daily deltas vs a CUMULATIVE A/D line // The published signal direction also conflicts across sources. So all three // choices are inputs. Do NOT treat any single combination as "the" ProGo. // ----------------------------------------------------------------------------- grpProGo = "ProGo Filter [LW 1997 — formula NOT canonical, see notes]" useProGoInput = input.bool(false, "Enable ProGo Filter", group = grpProGo) proGoLenInput = input.int(14, "ProGo Smoothing Length", minval = 1, group = grpProGo) proGoModeInput = input.string("SMA of deltas", "Construction [sources conflict]", options = ["SMA of deltas", "Cumulative A/D"], group = grpProGo) proGoSignInput = input.string("open - prevClose", "Public Line Sign [sources conflict]", options = ["open - prevClose", "prevClose - open"], group = grpProGo) proGoInvertInput = input.bool(false, "Invert Signal Direction [sources conflict]", group = grpProGo) grpPyr = "Pyramiding [HER: add on first two MA retests only]" usePyramidInput = input.bool(false, "Enable Pyramiding on MA Retests", group = grpPyr) maxAddOnsInput = input.int(2, "Max Add-Ons [HER: 2]", minval = 1, maxval = 4, group = grpPyr) grpRisk = "Risk [MINE — she publishes NO stop distance or sizing rule]" useProtStopInput = input.bool(false, "Add Protective Stop (not in her system)", group = grpRisk) atrLenInput = input.int(14, "ATR Length", minval = 2, group = grpRisk) atrStopMultInput = input.float(3.0, "Protective Stop (x ATR)", minval = 0.5, step = 0.5, group = grpRisk) useRiskSizeInput = input.bool(false, "Risk-Based Sizing (else fixed contracts)", group = grpRisk) riskPctInput = input.float(1.0, "Risk Per Trade (% of Equity)", minval = 0.05, step = 0.05, group = grpRisk) fixedQtyInput = input.int(1, "Fixed Contracts [HER: records reconcile to 1]", minval = 1, group = grpRisk) grpDir = "Direction & Dates" allowLongsInput = input.bool(true, "Allow Longs", group = grpDir) allowShortsInput = input.bool(false, "Allow Shorts [FIX: off — short side lost in 9/10 tests; her-faithful = on]", group = grpDir) useDateInput = input.bool(false, "Restrict to Date Range", group = grpDir) startDateInput = input.time(timestamp("2018-01-01T00:00"), "Start Date", group = grpDir) endDateInput = input.time(timestamp("2099-12-31T00:00"), "End Date", group = grpDir) // ============================================================================= // CORE MA [HER] // ta.* calls are hoisted, never placed inside a ternary — a conditional // history-dependent call raises CW10003 and can desynchronise its buffer. // ============================================================================= smaVal = ta.sma(close, maLenInput) emaVal = ta.ema(close, maLenInput) maVal = maTypeInput == "SMA" ? smaVal : emaVal atrVal = ta.atr(atrLenInput) // ============================================================================= // PROGO [LW] Professional vs Public money flow. // Professional line = Close - Open (pros trade the session) // Public line = Open - PrevClose (public trades the gap) // Both smoothed. Williams' rationale: more professional buying than public // buying precedes rallies. // ============================================================================= // Professional leg is agreed across every source: close - open. proRaw = close - open // Public leg sign is NOT agreed. Both conventions exposed. publicRaw = proGoSignInput == "open - prevClose" ? open - nz(close[1], open) : nz(close[1], open) - open // Both constructions computed unconditionally, then selected. Computing them // inside the branch would put a history-dependent call in a conditional path // (CW10003) and desynchronise the unselected series' buffer. proSma = ta.sma(proRaw, proGoLenInput) publicSma = ta.sma(publicRaw, proGoLenInput) var float proCum = 0.0 var float publicCum = 0.0 proCum := proCum + proRaw publicCum := publicCum + publicRaw useCumulative = proGoModeInput == "Cumulative A/D" proLine = useCumulative ? proCum : proSma publicLine = useCumulative ? publicCum : publicSma proGoRawBull = proLine > publicLine proGoBull = proGoInvertInput ? not proGoRawBull : proGoRawBull proGoBear = proGoInvertInput ? proGoRawBull : not proGoRawBull proGoLongOK = not useProGoInput or proGoBull proGoShortOK = not useProGoInput or proGoBear // ============================================================================= // INSIDE DAY [HER] // An inside day is fully contained within the prior bar's range. Her rule // disqualifies a setup if EITHER of the qualifying bars is an inside day. // ============================================================================= isInsideDay = high <= high[1] and low >= low[1] // ============================================================================= // SETUP DETECTION [HER] // Long : N consecutive bars whose LOWS are above the MA, none an inside day. // Short : N consecutive bars whose HIGHS are below the MA, none an inside day. // Loops are bounded by a maxval'd input so the bar-count is always small. // ============================================================================= longBarsOK = true shortBarsOK = true for i = 0 to setupBarsInput - 1 lowAboveMA = low[i] > maVal[i] highBelowMA = high[i] < maVal[i] insideBar = excludeInsideInput and (high[i] <= high[i + 1] and low[i] >= low[i + 1]) if not lowAboveMA or insideBar longBarsOK := false if not highBelowMA or insideBar shortBarsOK := false // Entry trigger price. "True high" = max(high, previous close), Williams' // definition; "highest high" is the plain bar high. Both variants exposed. useTrueRange = entryTrigInput == "True High (MoneyShow)" float longTrigger = na float shortTrigger = na for i = 0 to setupBarsInput - 1 thisHigh = useTrueRange ? math.max(high[i], nz(close[i + 1], high[i])) : high[i] thisLow = useTrueRange ? math.min(low[i], nz(close[i + 1], low[i])) : low[i] longTrigger := na(longTrigger) ? thisHigh : math.max(longTrigger, thisHigh) shortTrigger := na(shortTrigger) ? thisLow : math.min(shortTrigger, thisLow) // ============================================================================= // ARMED SETUPS [HER setup, MINE validity window] // She does not say how long a setup stays live. A stop order left working // forever would fire on unrelated price action months later, so it expires. // ============================================================================= var float armedLongPrice = na var int armedLongBar = na var float armedShortPrice = na var int armedShortBar = na if longBarsOK and not na(longTrigger) armedLongPrice := longTrigger armedLongBar := bar_index if shortBarsOK and not na(shortTrigger) armedShortPrice := shortTrigger armedShortBar := bar_index if not na(armedLongBar) and bar_index - armedLongBar > setupValidInput armedLongPrice := na armedLongBar := na if not na(armedShortBar) and bar_index - armedShortBar > setupValidInput armedShortPrice := na armedShortBar := na longArmed = not na(armedLongPrice) shortArmed = not na(armedShortPrice) // ============================================================================= // SIZING [MINE — she publishes no sizing rule; her records reconcile to 1 lot] // Declared BEFORE the entry conditions: Pine has no forward references. // Risk reference is the setup's own opposite extreme, which is the natural // invalidation point of her pattern. No forced minimum lot — an unfundable // trade is skipped, never silently up-sized. // ============================================================================= // ta.lowest/ta.highest hoisted to global scope — a history-dependent call // placed inside a ternary raises CW10003 and can desynchronise its buffer. setupLowest = ta.lowest(low, setupBarsInput) setupHighest = ta.highest(high, setupBarsInput) riskDistLong = longArmed ? math.max(armedLongPrice - setupLowest, syminfo.mintick) : na riskDistShort = shortArmed ? math.max(setupHighest - armedShortPrice, syminfo.mintick) : na riskCapital = strategy.equity * (riskPctInput / 100) qtyLong = if useRiskSizeInput na(riskDistLong) ? 0 : math.floor(riskCapital / (riskDistLong * syminfo.pointvalue)) else fixedQtyInput qtyShort = if useRiskSizeInput na(riskDistShort) ? 0 : math.floor(riskCapital / (riskDistShort * syminfo.pointvalue)) else fixedQtyInput longSizeOK = qtyLong >= 1 shortSizeOK = qtyShort >= 1 // ============================================================================= // FILTERS // ============================================================================= inDateRange = not useDateInput or (time >= startDateInput and time <= endDateInput) // ============================================================================= // ENTRIES [HER] // A stop order at the trigger price — her rule is "enter at the highest high of // the two bars", i.e. a breakout stop, not a market order at the close. // strategy.entry auto-reverses an opposite position, which is exactly her // stop-and-reverse exit. No separate exit call is needed for the core system. // ============================================================================= // The position guards matter. Without them the base entry would be re-issued on // every armed bar while already in the trade, and with pyramiding > 0 that // silently stacks contracts the pyramid rule never authorised. Guarded this way // the base stop works only when flat or opposite — which is exactly her // stop-and-reverse behaviour, since strategy.entry auto-reverses. canLong = allowLongsInput and longArmed and longSizeOK and inDateRange and proGoLongOK and strategy.position_size <= 0 canShort = allowShortsInput and shortArmed and shortSizeOK and inDateRange and proGoShortOK and strategy.position_size >= 0 if canLong strategy.entry("Long", strategy.long, qty = qtyLong, stop = armedLongPrice, alert_message = "18BAR LONG stop @ " + str.tostring(armedLongPrice, format.mintick)) if canShort strategy.entry("Short", strategy.short, qty = qtyShort, stop = armedShortPrice, alert_message = "18BAR SHORT stop @ " + str.tostring(armedShortPrice, format.mintick)) // Cancel a working stop order once its setup has expired, so a stale order // cannot fill on unrelated price action later. if not canLong strategy.cancel("Long") if not canShort strategy.cancel("Short") // ============================================================================= // PYRAMIDING ON MA RETESTS [HER: first two retests only] // She adds on retests of the MA and explicitly flags the third+ retest as much // higher risk. Counter resets whenever the position goes flat or flips. // ============================================================================= var int addOnCount = 0 if strategy.position_size == 0 or (strategy.position_size > 0) != (strategy.position_size[1] > 0) addOnCount := 0 touchedMALong = strategy.position_size > 0 and low <= maVal and close > maVal touchedMAShort = strategy.position_size < 0 and high >= maVal and close < maVal // Fixed entry IDs on purpose. A computed id ("Long Add " + count) relies on // strategy.entry accepting a series string, which is an unnecessary risk when a // constant id behaves identically here — addOnCount already caps the adds. if usePyramidInput and addOnCount < maxAddOnsInput if touchedMALong and longSizeOK addOnCount += 1 strategy.entry("LongAdd", strategy.long, qty = qtyLong) if touchedMAShort and shortSizeOK addOnCount += 1 strategy.entry("ShortAdd", strategy.short, qty = qtyShort) // ============================================================================= // [FIX] EXIT ON THE OPPOSITE SETUP WHEN REVERSING IS DISALLOWED // Her system's ONLY exit is the reverse trade. With shorts disabled (the // validated default), the reverse can never fire — so without this block a // long-only run would buy once and hold forever. The opposite SETUP still // closes the position (a sell stop at the short trigger price); it just does // not open the opposite trade. Mirrors the validated Python engine exactly. // strategy.exit without from_entry covers the base entry AND any pyramid adds. // ============================================================================= if strategy.position_size > 0 and not allowShortsInput if shortArmed strategy.exit("RevExit L", stop = armedShortPrice) else strategy.cancel("RevExit L") if strategy.position_size < 0 and not allowLongsInput if longArmed strategy.exit("RevExit S", stop = armedLongPrice) else strategy.cancel("RevExit S") // ============================================================================= // OPTIONAL PROTECTIVE STOP [MINE — explicitly NOT part of her system] // Her published system has no stop other than the opposite signal. Enabling // this makes the backtest safer but less faithful; it is off by default so the // default run reflects what she actually describes. // ============================================================================= if useProtStopInput and strategy.position_size > 0 strategy.exit("Long Prot", stop = strategy.position_avg_price - atrVal * atrStopMultInput) if useProtStopInput and strategy.position_size < 0 strategy.exit("Short Prot", stop = strategy.position_avg_price + atrVal * atrStopMultInput) // ============================================================================= // PLOTS // ============================================================================= plot(maVal, "18 MA", color = color.new(color.orange, 0), linewidth = 2) plotshape(longArmed and not longArmed[1], title = "Long Setup Armed", style = shape.triangleup, location = location.belowbar, color = color.new(color.teal, 0), size = size.tiny) plotshape(shortArmed and not shortArmed[1], title = "Short Setup Armed", style = shape.triangledown, location = location.abovebar, color = color.new(color.maroon, 0), size = size.tiny) plot(longArmed ? armedLongPrice : na, "Long Trigger", color = color.new(color.teal, 0), style = plot.style_linebr) plot(shortArmed ? armedShortPrice : na, "Short Trigger", color = color.new(color.maroon, 0), style = plot.style_linebr) // Diagnostic: a valid setup skipped because the risk budget could not fund one // contract. If this litters the chart, raise capital or use micro contracts. plotchar(useRiskSizeInput and ((longArmed and not longSizeOK) or (shortArmed and not shortSizeOK)), title = "Skipped: under 1 contract", char = "!", location = location.top, color = color.new(color.yellow, 0), size = size.tiny) // ============================================================================= // ALERTS // alertcondition() is deliberately NOT used: it compiles inside a strategy but // is silently inert — TradingView only raises those events from indicators. // ============================================================================= if canLong and not canLong[1] alert("18BAR long setup armed. Buy stop @ " + str.tostring(armedLongPrice, format.mintick), alert.freq_once_per_bar_close) if canShort and not canShort[1] alert("18BAR short setup armed. Sell stop @ " + str.tostring(armedShortPrice, format.mintick), alert.freq_once_per_bar_close)