Stochastic Pop Desk · Free & open-source
What turns a pullback into a fresh signal?
Follow price, momentum and the entry checklist.
An entry checklist for TradingView. The indicator does not place orders.

- Check the trend
Direction and distance from the trend line.
- Wait for a fresh cross
Reset, then momentum crossing its band.
- See the missing step
Four checks, a trail and a drawn plan.
What is Stochastic Pop Desk?
Stochastic Pop Desk is a free TradingView momentum indicator that combines a smoothed stochastic cross with EMA trend, distance and timing checks. It shows momentum in a lower pane and the trend, checklist and reference plan on the price chart. An optional strategy script tests those rules separately.
Why is there no new POP while the oscillator stays above 55?
Staying above 55 is not a fresh cross. After an accepted long signal, the default reset requires a confirmed reading below 45 before another POP can qualify. Trend, distance, cooldown and open-trade checks still apply. Read POP and DROP conditions
Why does Stochastic Pop Desk use two chart panels?
The lower pane shows the smoothed stochastic and its trigger bands. Price-chart overlays show the EMA, trail, reference ticket and checklist. Read both together: the oscillator crossing a band does not satisfy the other entry checks. Read the two-panel guide
Does the sideways flag automatically block entries?
No. The sideways flag describes low directional efficiency relative to recent history. Blocking new entries while flagged is an optional setting and is off by default. Turn it on only when you intend to test that additional filter. Read the sideways filter rules
Three separate clues. One checklist.
A reader wanted three scripts combined: a price average, momentum line and checklist.
- Price close
- 108.0
- Momentum
- 79.0 / 100
First, check the direction.
Candles show price. The blue running average weights recent closes more. Its length is 200. Price is above it; the average is higher than ten candles ago. Long ideas are allowed.
Fictional prices; calculated lines. An earlier long has already used its reset.
How could this help a trade?
Repeatable entry and exit rules make the idea testable. Include costs; an entry can still lose.
A cross alone is not enough.
Reveal ten candles. Compare the final decision.
- Price close
- 108.0
- Momentum
- 70.3 / 100
Waiting for a fresh reset.
An earlier long used its reset. Staying above the average or above 55 does not create another entry.
- Price trend: passesAbove rising EMA
- Distance: passes2.3 ATR · limit 3
- Fresh reset: not metNot below 45 yet
- New cross: not met63.7 → 70.3
- Closed candle: passesClosed
Compare cases at the same candle. Pullback changes recalculate both panels.
What is supplied in this example?
330 generated bars warm up EMA 200, ATR 10 and stochastic 14 → SMA 5 → EMA 4. Supplied trade state: flat, cooldown complete, long reset already spent. The warm-up is not claimed to have produced that earlier signal. Optional agreement gates and exits are off. These are fictional prices, not live market data.
Does 55 mean a chance of success?
No. Stochastic describes price location in a recent range. Smoothing slows its changes. It does not measure buying volume or the chance of the next move.
Find the same clues on your chart.
Price above. Momentum below.

1 Price direction
Compare price with its average. The colored trail is separate.
2 The lower line
POP crosses up; DROP crosses down. Other checks must pass too.
3 The checklist
Read Missing, then the trade card. READY alone is not entry.
Explore the full price simulator
Change the close, pullback, reset rule and confirmation. Lines recalculate from fictional prices.
Can this close create a fresh pop?
Change the close or pullback. Candles, EMA and stochastic recalculate.
How the calculation works & more controls
Stochastic 14 → SMA 5 → EMA 4. Strict long reset below 45, cross above 55. EMA slope: ten bars. ATR: ten bars.
Entry 112.00; stop 90.29; targets 133.71 / 155.43. Closed bars replay chronologically, existing stop first.
Calculated events: candle 10: Long pop.
Illustrative inputs · changing a slider recalculates this example · no market feed
Paste it into TradingView.
Both complete Pine v6 scripts are free, under MIT.
- 1
Open Pine Editor
Create a new indicator on your chart.
- 2
Paste the code
Copy below and replace the sample.
- 3
Add to chart
Save it. Keep defaults initially.
The indicator
Opens in its own pane and draws on the price chart too.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════
// STOCHASTIC POP DESK — EMA 200 · Stochastic POP · Analysis panel · Co-Pilot · Pine Script v6 · v1.5.0
// One script that replaces three: the EMA 200 trend filter, the Stochastic POP (Jake Bernstein's method, the
// ChrisMoody "Method 2" bands 55 / 45, with an EMA 4 on the stochastic line) and an analysis panel that walks the
// four steps of the rule in order and tells you — in words — what is still missing before a signal can fire.
//
// HOW IT READS THE MARKET (every decision commits on the CLOSED bar — nothing repaints):
// 1 TREND price above a RISING EMA 200 = long side only · below a FALLING EMA 200 = short side only.
// 2 STRETCH the close must sit within N ATR of the EMA — a pop that starts far away is a chase.
// 3 FRESH the stochastic line must have visited the other band since the last signal (no re-entries).
// 4 CROSS the EMA-4 of the smoothed stochastic (14, 5) crosses ABOVE 55 → long pop · BELOW 45 → short drop.
// EXIT the line crosses back through its band (momentum gone) · or a stop / target ladder · or both.
// CONTEXT MA stack 5·10·20·100·200, EMA 8 / 21, RSI, volume — shown as agreements, optionally used as gates.
// SWITCHES (all off by default, each measured — numbers in the tooltips) ATR trailing stop after TP1 · session
// filter · time stop · opposite-signal exit · higher-timeframe EMA agreement.
//
// The stochastic lives in this pane; the EMA, the signals, the trade levels, the panel and the Co-Pilot are drawn
// on the price chart with force_overlay — one script, one settings dialog, two views.
// Paper ledger has NO costs — a description of the past on your chart, not a promise.
//
// License: MIT — share freely, keep this header. © 2026 ProEA Lab · built for r/pinescript
// ═══════════════════════════════════════════════════════════════════════════════
//@version=6
indicator("Stochastic Pop Desk — EMA 200 · Stochastic POP · Checklist", "StochPop", overlay = false, max_labels_count = 300, max_boxes_count = 160, max_lines_count = 100)
// ─────────────────────────────────────────────────────────────────── INPUTS · trend (the EMA 200 exactly as the built-in)
gT = "Trend · EMA 200"
inEmaLen = input.int(200, "EMA length", minval = 1, group = gT, display = display.none)
inEmaSrc = input.source(close, "Source", group = gT, display = display.none)
inEmaOff = input.int(0, "Offset", minval = -500, maxval = 500, group = gT, display = display.none)
inSmType = input.string("SMA", "Smoothing line", options = ["None", "SMA", "EMA", "SMMA (RMA)", "WMA", "VWMA"], group = gT, tooltip = "The built-in EMA's second line: a moving average OF the EMA. Drawn dotted; it does not affect signals.", display = display.none)
inSmLen = input.int(5, "Smoothing length", minval = 1, group = gT, display = display.none)
inSlopeLen = input.int(10, "EMA must be rising / falling over N bars", minval = 1, maxval = 100, group = gT, tooltip = "Long side only while the EMA 200 is higher than N bars ago; short side only while it is lower.", display = display.none)
inMaxDist = input.float(3.0, "Not stretched: price within N ATR of the EMA", minval = 0.5, maxval = 20, step = 0.5, group = gT, tooltip = "A pop that fires far above a rising EMA is a chase. Signals are skipped when the close is farther than this from the EMA.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · stochastic POP (CM_Stochastic POP Method 2 + EMA 4)
gS = "Stochastic POP (Method 2)"
inStLen = input.int(14, "Stochastic length", minval = 1, group = gS, display = display.none)
inStSmooth = input.int(5, "Smoothing (%K)", minval = 1, group = gS, display = display.none)
inStEma = input.int(4, "EMA on the stochastic", minval = 1, group = gS, tooltip = "The 'plus EMA 4' — the signal line is an EMA of the smoothed stochastic. It cuts the whipsaws; it adds a little lag.", display = display.none)
inUpper = input.float(55, "Pop band (long above)", minval = 50, maxval = 95, group = gS, display = display.none)
inLower = input.float(45, "Drop band (short below)", minval = 5, maxval = 50, group = gS, display = display.none)
inMustReset = input.bool(true, "Fresh pop only (the line must visit the other band between signals)", group = gS, tooltip = "A long pop counts only if the line has been below the drop band since the last long pop — no re-entries on every wiggle across 55.", display = display.none)
inCooldown = input.int(5, "Cooldown between signals (bars)", minval = 0, maxval = 200, group = gS, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · context components
gA = "Context (agreements)"
inUseStack = input.bool(true, "MA stack 5 · 10 · 20 · 100 · 200", group = gA, display = display.none)
inUse821 = input.bool(true, "EMA 8 / 21 cross", group = gA, display = display.none)
inUseRsi = input.bool(true, "RSI above / below 50", group = gA, display = display.none)
inRsiLen = input.int(9, "RSI length", minval = 2, group = gA, display = display.none)
inUseVol = input.bool(true, "Volume above its average", group = gA, display = display.none)
inVolLen = input.int(20, "Volume average length", minval = 2, group = gA, display = display.none)
inAtrLen = input.int(10, "ATR length (stops, stretch, panel)", minval = 1, group = gA, display = display.none)
inGate = input.string("Trend + POP only", "Signal needs", options = ["Trend + POP only", "Trend + POP + majority of agreements", "Trend + POP + all agreements"], group = gA,
tooltip = "What must be true for a signal. 'Trend + POP only' is the classic Bernstein rule; the agreements are then context. The stricter modes use them as gates.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · trade plan
gE = "Trade plan (paper — no costs)"
inPlan = input.bool(true, "Show entry / stop / targets", group = gE, display = display.none)
inStopMode = input.string("Swing low / high", "Stop", options = ["Swing low / high", "ATR"], group = gE, display = display.none)
inSwingN = input.int(10, "Swing lookback (bars)", minval = 2, maxval = 100, group = gE, display = display.none)
inStopAtr = input.float(2.0, "ATR stop (× ATR)", minval = 0.3, maxval = 10, step = 0.1, group = gE, display = display.none)
inStopBuf = input.float(0.2, "Stop buffer (× ATR)", minval = 0, maxval = 3, step = 0.1, group = gE, display = display.none)
inTp1 = input.float(1.0, "TP1 (R) → stop to breakeven", minval = 0.3, maxval = 10, step = 0.1, group = gE, display = display.none)
inTp2 = input.float(2.0, "TP2 (R) final", minval = 0.5, maxval = 20, step = 0.1, group = gE, display = display.none)
inExitMode = input.string("Stop / targets only", "Exit", options = ["Stop / targets only", "Band cross back or TP2", "Band cross back only"], group = gE,
tooltip = "Stop / targets only = the stop (swing or ATR), TP1 to breakeven, TP2 final — measured best on 2 of 3 test charts, tied on the third. Band cross back = the stochastic line crossing back through its band closes the trade early (the classic exit) — it cut winners short in the measurement. Both = the ladder plus the band exit.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · switches (all off — measured, see tooltips)
gX = "Switches (off by default)"
inTrail = input.bool(false, "Trailing stop after TP1 (ATR)", group = gX, tooltip = "Once TP1 is reached the stop trails N ATR behind price instead of parking at breakeven. Measured with costs: BTCUSD 1H PF 1.51 → 1.58 (more winners, smaller), XAUUSD 1H 1.31 → 1.10 — market-dependent.", display = display.none)
inTrailAtr = input.float(2.0, " Trail distance (× ATR)", minval = 0.5, maxval = 10, step = 0.5, group = gX, display = display.none)
inTrailStop = input.string("Off", "Stop follows the trend trail", options = ["Off", "After TP1", "From entry"], group = gX,
tooltip = "The chandelier trend trail drawn on the chart becomes the stop: it ratchets with the trend and the trade closes when the trail flips to the other side of price. 'After TP1' keeps the swing / ATR stop until TP1 is paid, then hands the trade to the trail; 'From entry' uses the trail from the first bar. Uses the trail settings in Style. Measured with costs (profit factor, ladder default → switch): After TP1 — BTC 1H 1.51 → 1.48 (win rate 30 → 44 %), gold 1H 1.31 → 1.18, gold 15m 0.81 → 0.88. From entry — BTC 1H 1.51 → 1.81 (drawdown −27 %), gold 1H 1.31 → 0.96, gold 15m 0.81 → 0.55. It pays where trends run (bitcoin) and costs where price chops around the EMA 200 (gold) — read your chart first.", display = display.none)
inSessOn = input.bool(false, "Session filter (signals only inside the sessions)", group = gX, tooltip = "Two windows, exchange time as set below. Off = any time. Measured: XAUUSD 1H PF 1.31 → 1.23 with a third of the trades; meaningless on 24/7 markets.", display = display.none)
inSessTz = input.string("America/New_York", " Session timezone (IANA)", group = gX, display = display.none)
inSess1 = input.session("0200-0500", " Session 1", group = gX, display = display.none)
inSess2 = input.session("0830-1100", " Session 2", group = gX, display = display.none)
inTimeStop = input.int(0, "Time stop: close after N bars if TP1 not reached (0 = off)", minval = 0, maxval = 500, group = gX, tooltip = "Measured at 30 bars: BTCUSD 1H PF 1.51 → 1.33, XAUUSD 1H 1.31 → 1.23 — it hurt on both.", display = display.none)
inOppExit = input.bool(false, "Opposite-signal exit (a drop cross closes a long, a pop cross closes a short)", group = gX, tooltip = "Measured: BTCUSD 1H PF 1.51 → 1.68, XAUUSD 1H 1.31 → 1.02 — market-dependent.", display = display.none)
inHtfOn = input.bool(false, "Higher-timeframe agreement (price on the same side of the EMA on the HTF)", group = gX, tooltip = "Read from the last CLOSED higher-timeframe bar — no lookahead. Adds a row to the readiness list. The one switch that helped on every chart tried (240 on 1H / 15m): BTCUSD 1H PF 1.51 → 1.89, XAUUSD 1H 1.31 → 1.41, XAUUSD 15m 0.81 → 0.98 — fewer trades each time.", display = display.none)
inHtf = input.timeframe("240", " Higher timeframe", group = gX, display = display.none)
inRangeBlock = input.bool(false, "Skip signals in a sideways regime", group = gX,
tooltip = "The sideways detector is always shown (state line, step 1 text, the trail turns grey). With this on, step 1 also fails while the market is sideways, so no pop / drop fires inside a range. Sideways = Kaufman efficiency (net move ÷ path over N bars) in the choppiest N % of the last 200 bars, released 15 points higher so it does not flicker. Measured with costs (profit factor, ladder default → switch): BTC 1H 1.51 → 1.90 (27 → 13 trades), gold 1H 1.31 → 2.55 (83 → 42 trades, drawdown −60 %), gold 15m 0.81 → 0.50 (44 → 19 trades). Strong on the hourly charts, wrong on the 15-minute one — read your chart first.", display = display.none)
inRangeLen = input.int(20, " Sideways detector: bars", minval = 5, maxval = 200, group = gX, display = display.none)
inRangePct = input.int(25, " Sideways = the choppiest N % of the last 200 bars", minval = 5, maxval = 60, group = gX, tooltip = "Efficiency = |close − close[N]| ÷ Σ|close − close[1]| over N bars (100 % = a straight line, 0 % = pure chop). Instead of a fixed threshold — which is never portable between charts — the market is 'sideways' while its efficiency sits in the bottom N % of the last 200 bars, and stays flagged until it climbs above the (N + 15) % mark. Self-calibrating on any symbol and timeframe.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · style / panel / co-pilot / alerts
gV = "Style"
inTheme = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inShowSm = input.bool(true, "Draw the EMA smoothing line", group = gV, display = display.none)
inShowBand = input.bool(false, "Draw the stretch band (EMA ± N ATR)", group = gV, display = display.none)
inTrendTrail = input.bool(true, "Trend trail line + fill (green under price in an uptrend, red over price in a downtrend)", group = gV,
tooltip = "A chandelier-style trail: highest high − N ATR while the EMA 200 regime is up, lowest low + N ATR while it is down; it only ratchets with the trend and resets when the regime flips. The soft fill between the trail and price is the regime at a glance. Display only — signals still come from the four steps.", display = display.none)
inTrailMult2 = input.float(3.0, " Trail distance (× ATR)", minval = 0.5, maxval = 10, step = 0.5, group = gV, display = display.none)
inZones = input.bool(true, "Gradient risk / reward zones", group = gV, display = display.none)
inTint = input.bool(true, "Tint candles while a trade is open", group = gV, display = display.none)
inShade = input.bool(false, "Shade the pane while in a pop / drop", group = gV, display = display.none)
inCandleTrend = input.bool(false, "Colour candles by trend (green · red · grey)", group = gV, tooltip = "Soft green = price above a rising EMA 200 (long side) · soft red = below a falling EMA 200 (short side) · grey = no clean side. While a trade is open the candles turn accent-coloured instead, so the trade period is visible at a glance.", display = display.none)
gD = "Dashboard"
inDash = input.bool(true, "Show the dashboard", group = gD, display = display.none)
inDashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
inShowCtx = input.bool(false, "Show the context section (MA stack · EMA 8/21 · RSI · volume)", group = gD, display = display.none)
gN = "Narrative · Co-Pilot"
inNarr = input.string("Brief", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN, tooltip = "Brief = one sentence (NOW). Standard adds WHY and PLAN. Detailed adds the track record and a glossary.", display = display.none)
inNarrPos = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap = input.int(64, "Line width (characters)", minval = 30, maxval = 120, group = gN, display = display.none)
gL = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gL, tooltip = "Choose 'Any alert() function call' when creating the alert.", display = display.none)
// ─────────────────────────────────────────────────────────────────── THEME
type Theme
color bg = #0B0E14
color frame = #B8923A
color accent = #D8B14A
color accentHi = #E8C25A
color up = #2BE7C7
color down = #FF3B5C
color flat = #6B7280
color txt = #D7DCE6
color txtHi = #ECEFF5
themeOf(string name) =>
Theme t = Theme.new()
if name == "Aurora Neon"
t.bg := #0E1118
t.frame := #2C3A5A
t.accent := #7C9CFF
t.accentHi := #A9B8FF
t.up := #2AF0C8
t.down := #FF4D6D
t.flat := #5B6472
t.txt := #E6E9EF
t.txtHi := #F2F4F9
else if name == "Colorblind-safe"
t.bg := #0E1014
t.frame := #3A4150
t.accent := #56B4E9
t.accentHi := #8ED0F5
t.up := #E69F00
t.down := #0072B2
t.flat := #8C92A0
t.txt := #E8EAED
t.txtHi := #FFFFFF
t
Theme th = themeOf(inTheme)
fmt(float x) => str.tostring(x, format.mintick)
// ═══════════════════════════════════════════════════════════════════ ENGINE INPUTS (ta.* at global scope)
float ema200 = ta.ema(inEmaSrc, inEmaLen)
float smSma = ta.sma(ema200, inSmLen)
float smEma = ta.ema(ema200, inSmLen)
float smRma = ta.rma(ema200, inSmLen)
float smWma = ta.wma(ema200, inSmLen)
float smVwma = ta.vwma(ema200, inSmLen)
float emaSm = inSmType == "SMA" ? smSma : inSmType == "EMA" ? smEma : inSmType == "SMMA (RMA)" ? smRma : inSmType == "WMA" ? smWma : inSmType == "VWMA" ? smVwma : ema200
float atr = ta.atr(inAtrLen)
float atrU = math.max(atr, syminfo.mintick)
float kRaw = ta.stoch(close, high, low, inStLen)
float kS = ta.sma(kRaw, inStSmooth)
float kE = ta.ema(kS, inStEma)
float ma5 = ta.ema(close, 5)
float ma10 = ta.ema(close, 10)
float ma20 = ta.ema(close, 20)
float ma100 = ta.ema(close, 100)
float ma200 = ta.ema(close, 200)
float ema8 = ta.ema(close, 8)
float ema21 = ta.ema(close, 21)
float rsi = ta.rsi(close, inRsiLen)
float volMa = ta.sma(nz(volume, 0.0), inVolLen)
float swLo = ta.lowest(low, inSwingN)
float swHi = ta.highest(high, inSwingN)
bool inSess = not inSessOn or not na(time(timeframe.period, inSess1, inSessTz)) or not na(time(timeframe.period, inSess2, inSessTz))
htfEmaFn() => ta.ema(close, inEmaLen)
float htfEma = request.security(syminfo.tickerid, inHtf, htfEmaFn()[1], lookahead = barmerge.lookahead_on)
float htfClose = request.security(syminfo.tickerid, inHtf, close[1], lookahead = barmerge.lookahead_on)
bool htfOk = timeframe.in_seconds(inHtf) > timeframe.in_seconds()
bool htfUp = not inHtfOn or not htfOk or htfClose > htfEma
bool htfDn = not inHtfOn or not htfOk or htfClose < htfEma
bool crossUp = ta.crossover(kE, inUpper)
bool crossDn = ta.crossunder(kE, inLower)
bool backDn = ta.crossunder(kE, inUpper) // long momentum gone
bool backUp = ta.crossover(kE, inLower) // short momentum gone
// trend
bool emaRising = ema200 > ema200[inSlopeLen]
bool emaFalling = ema200 < ema200[inSlopeLen]
bool trendUpRaw = close > ema200 and emaRising
bool trendDnRaw = close < ema200 and emaFalling
// sideways detector: Kaufman efficiency (net move ÷ path) with hysteresis; always shown, blocks signals only when the switch is on
float erNet = math.abs(close - close[inRangeLen])
float erPath = math.sum(math.abs(close - close[1]), inRangeLen)
float er = erPath > 0 ? erNet / erPath : 0.0
float erLo = ta.percentile_nearest_rank(er, 200, inRangePct)
float erHi = ta.percentile_nearest_rank(er, 200, math.min(inRangePct + 15, 95))
var bool sideways = false
sideways := sideways ? er < erHi : er < erLo
bool rangeBlock = inRangeBlock and sideways
bool trendUp = trendUpRaw and not rangeBlock
bool trendDn = trendDnRaw and not rangeBlock
float distAtr = (close - ema200) / atrU
bool stretchedUp = distAtr > inMaxDist
bool stretchedDn = -distAtr > inMaxDist
// context (agreements)
int stackUp = (close > ma5 ? 1 : 0) + (close > ma10 ? 1 : 0) + (close > ma20 ? 1 : 0) + (close > ma100 ? 1 : 0) + (close > ma200 ? 1 : 0)
int stackDn = 5 - stackUp
bool agStackL = stackUp >= 4
bool agStackS = stackDn >= 4
bool ag821L = ema8 > ema21
bool ag821S = ema8 < ema21
bool agRsiL = rsi > 50
bool agRsiS = rsi < 50
bool agVol = volMa > 0 and nz(volume, 0.0) > volMa
int agCountL = (inUseStack and agStackL ? 1 : 0) + (inUse821 and ag821L ? 1 : 0) + (inUseRsi and agRsiL ? 1 : 0) + (inUseVol and agVol ? 1 : 0)
int agCountS = (inUseStack and agStackS ? 1 : 0) + (inUse821 and ag821S ? 1 : 0) + (inUseRsi and agRsiS ? 1 : 0) + (inUseVol and agVol ? 1 : 0)
int agTotal = (inUseStack ? 1 : 0) + (inUse821 ? 1 : 0) + (inUseRsi ? 1 : 0) + (inUseVol ? 1 : 0)
bool gateL = inGate == "Trend + POP only" ? true : inGate == "Trend + POP + all agreements" ? agCountL == agTotal : agCountL * 2 >= agTotal
bool gateS = inGate == "Trend + POP only" ? true : inGate == "Trend + POP + all agreements" ? agCountS == agTotal : agCountS * 2 >= agTotal
// trail state (filled in the display section; the state machine reads the PREVIOUS bar's values — no repaint)
var float trail = na
var int trailDir = 0
// ═══════════════════════════════════════════════════════════════════ STATE (confirmed bars only)
type Trade
int dir = 0 // 0 flat · 1 long · −1 short
float entry = na
float sl = na
float sl0 = na
float r = na
float tp1 = na
float tp2 = na
int bar = na
bool tp1Hit = false
int lastSig = -1000
bool resetL = true // a long pop is allowed (the line has visited the drop band since the last long)
bool resetS = true
float lastR = na // last closed trade's R (for the panel)
string lastWhy = ""
type Stat
int n = 0
int wins = 0
int flat = 0
float sumR = 0.0
int nBand = 0
int nStop = 0
int nTp = 0
type Ev
bool sigL = false
bool sigS = false
bool exit = false
bool tp1 = false
string why = ""
float r = na
int dir = 0
var Trade t = Trade.new()
var Stat st = Stat.new()
Ev ev = Ev.new()
closeTrade(float px, string why) =>
float rr = (px - t.entry) * t.dir / t.r
ev.exit := true
ev.why := why
ev.r := rr
ev.dir := t.dir
st.n := st.n + 1
st.wins := st.wins + (rr >= 0.05 ? 1 : 0)
st.flat := st.flat + (math.abs(rr) < 0.05 ? 1 : 0)
st.sumR := st.sumR + rr
if why == "BAND" or why == "FLIP" or why == "TIME"
st.nBand := st.nBand + 1
else if why == "TP2"
st.nTp := st.nTp + 1
else
st.nStop := st.nStop + 1
t.lastR := rr
t.lastWhy := why
t.dir := 0
openTrade(int d) =>
float anchor = inStopMode == "ATR" ? (d == 1 ? close - inStopAtr * atrU : close + inStopAtr * atrU) : (d == 1 ? swLo - inStopBuf * atrU : swHi + inStopBuf * atrU)
float slPx = d == 1 ? math.min(anchor, close - syminfo.mintick) : math.max(anchor, close + syminfo.mintick)
t.dir := d
t.entry := close
t.sl := slPx
t.sl0 := slPx
t.r := math.abs(close - slPx)
t.tp1 := close + d * inTp1 * t.r
t.tp2 := close + d * inTp2 * t.r
t.bar := bar_index
t.tp1Hit := false
t.lastSig := bar_index
if barstate.isconfirmed and not na(atr) and not na(kE) and bar_index > inEmaLen
if kE < inLower
t.resetL := true
if kE > inUpper
t.resetS := true
// ── manage an open trade (stop first)
if t.dir != 0
int d = t.dir
bool stopHit = d == 1 ? low <= t.sl : high >= t.sl
bool useLadder = inExitMode != "Band cross back only"
bool useBand = inExitMode != "Stop / targets only"
if useLadder and stopHit
closeTrade(t.sl, (inTrailStop != "Off" or (inTrail and t.tp1Hit)) and t.sl != t.entry and t.sl != t.sl0 ? "TRAIL" : t.tp1Hit ? "BE" : "SL")
else if useLadder and (d == 1 ? high >= t.tp2 : low <= t.tp2)
closeTrade(t.tp2, "TP2")
else
if useLadder and not t.tp1Hit and (d == 1 ? high >= t.tp1 : low <= t.tp1)
t.tp1Hit := true
ev.tp1 := true
t.sl := t.entry
if inTrail and t.tp1Hit
float trailPx = d == 1 ? close - inTrailAtr * atrU : close + inTrailAtr * atrU
t.sl := d == 1 ? math.max(t.sl, trailPx) : math.min(t.sl, trailPx)
if inTrailStop != "Off" and not na(trail) and (inTrailStop == "From entry" or t.tp1Hit)
if trailDir == d
t.sl := d == 1 ? math.max(t.sl, trail) : math.min(t.sl, trail)
else if bar_index - t.bar >= 1
closeTrade(close, "TRAIL")
if useBand and (d == 1 ? backDn : backUp)
closeTrade(close, "BAND")
else if inOppExit and (d == 1 ? crossDn : crossUp)
closeTrade(close, "FLIP")
else if inTimeStop > 0 and not t.tp1Hit and bar_index - t.bar >= inTimeStop
closeTrade(close, "TIME")
// ── signals (only when flat)
if t.dir == 0
bool cool = bar_index - t.lastSig >= inCooldown
bool okL = crossUp and trendUp and not stretchedUp and gateL and cool and (not inMustReset or t.resetL) and inSess and htfUp
bool okS = crossDn and trendDn and not stretchedDn and gateS and cool and (not inMustReset or t.resetS) and inSess and htfDn
if okL
openTrade(1)
t.resetL := false
ev.sigL := true
else if okS
openTrade(-1)
t.resetS := false
ev.sigS := true
// ═══════════════════════════════════════════════════════════════════ READINESS (display only — the four steps of the rule, in order)
int side = t.dir != 0 ? t.dir : trendUp ? 1 : trendDn ? -1 : (close > ema200 ? 1 : -1)
bool s1 = side == 1 ? trendUp : trendDn
bool s2 = side == 1 ? not stretchedUp : not stretchedDn
bool s3 = not inMustReset or (side == 1 ? t.resetL : t.resetS)
bool s4 = side == 1 ? kE > inUpper : kE < inLower // the line is on the pop side (the cross is the event)
bool s5 = side == 1 ? gateL : gateS
bool gateOn = inGate != "Trend + POP only"
bool s6 = side == 1 ? htfUp : htfDn
bool s7 = inSess
int ready = (s1 ? 1 : 0) + (s2 ? 1 : 0) + (s3 ? 1 : 0) + (s4 ? 1 : 0) + (gateOn and s5 ? 1 : 0) + (inHtfOn and s6 ? 1 : 0) + (inSessOn and s7 ? 1 : 0)
int readyMax = 4 + (gateOn ? 1 : 0) + (inHtfOn ? 1 : 0) + (inSessOn ? 1 : 0)
bool allPre = s1 and s2 and s3 and (not gateOn or s5) and (not inHtfOn or s6) and (not inSessOn or s7)
string sideWord = side == 1 ? "long" : "short"
string erTxt = "efficiency " + str.tostring(er * 100, "0") + " % over " + str.tostring(inRangeLen) + " bars" + (sideways ? ", the choppiest " + str.tostring(inRangePct) + " % of recent bars" : "")
string s1Base = side == 1 ? (close > ema200 ? (emaRising ? "above a rising EMA 200" : "above the EMA, but it is not rising") : "price is below the EMA 200") : (close < ema200 ? (emaFalling ? "below a falling EMA 200" : "below the EMA, but it is not falling") : "price is above the EMA 200")
string s1Txt = rangeBlock and (side == 1 ? trendUpRaw : trendDnRaw) ? "sideways — " + erTxt + " · signals skipped" : s1Base + (sideways ? " · sideways (" + erTxt + ")" : "")
string s2Txt = str.tostring(math.abs(distAtr), "0.0") + " ATR from the EMA" + (s2 ? " (≤ " + str.tostring(inMaxDist, "0.0") + ")" : " — too far, > " + str.tostring(inMaxDist, "0.0"))
string s3Txt = not inMustReset ? "not required" : (s3 ? "armed — the line visited " + str.tostring(side == 1 ? inLower : inUpper, "0") : "spent — needs a dip " + (side == 1 ? "below " + str.tostring(inLower, "0") : "above " + str.tostring(inUpper, "0")))
string s4Txt = "line " + str.tostring(kE, "0.0") + (s4 ? " · on the pop side" : (side == 1 ? " · waiting for a cross above " + str.tostring(inUpper, "0") : " · waiting for a cross below " + str.tostring(inLower, "0")))
string s5Txt = str.tostring(side == 1 ? agCountL : agCountS) + " / " + str.tostring(agTotal) + (inGate == "Trend + POP only" ? " agree · context only" : s5 ? " agree · gate passed" : " agree · gate blocks")
string missing = t.dir != 0 ? "in a trade" : allPre ? (s4 ? "nothing — a fresh cross is the trigger" : "only the cross itself") : (not s1 ? (rangeBlock ? "step 1 (sideways)" : "step 1 (trend)") : "") + (not s2 ? (not s1 ? " · " : "") + "step 2 (stretched)" : "") + (not s3 ? ((not s1 or not s2) ? " · " : "") + "step 3 (fresh pop)" : "") + (gateOn and not s5 ? ((not s1 or not s2 or not s3) ? " · " : "") + "agreements" : "") + (inHtfOn and not s6 ? " · HTF disagrees" : "") + (inSessOn and not s7 ? " · outside the sessions" : "")
// ── trend trail (display + optional stop): a chandelier-style trailing line that flips ONLY when price closes through
// it — a pullback that hugs the EMA 200 does not flip it, so a range does not zig-zag the line. It seeds from the flip
// bar's own extreme (always on the right side of price) and ratchets from the extreme reached since the flip.
// Colour: full = the EMA 200 slope confirms the side (the rule's regime); muted = the slope does not confirm yet;
// grey = the sideways detector is on.
var float regExt = na
if trailDir == 0
if not na(atrU) and not na(ema200)
trailDir := close > ema200 ? 1 : -1
regExt := trailDir == 1 ? high : low
trail := trailDir == 1 ? regExt - inTrailMult2 * atrU : regExt + inTrailMult2 * atrU
else if trailDir == 1
if close < trail
trailDir := -1
regExt := low
trail := low + inTrailMult2 * atrU
else
regExt := math.max(nz(regExt, high), high)
float candU = regExt - inTrailMult2 * atrU
trail := math.max(nz(trail, candU), candU)
else
if close > trail
trailDir := 1
regExt := high
trail := high - inTrailMult2 * atrU
else
regExt := math.min(nz(regExt, low), low)
float candD = regExt + inTrailMult2 * atrU
trail := math.min(nz(trail, candD), candD)
bool trailStrong = trailDir == 1 ? emaRising : emaFalling
// ═══════════════════════════════════════════════════════════════════ PANE · the stochastic
color kCol = color.from_gradient(kE, inLower, inUpper, th.down, th.up)
pU = plot(inUpper, "Pop band", color = color.new(th.up, 55), linewidth = 1, style = plot.style_circles, display = display.pane)
pL = plot(inLower, "Drop band", color = color.new(th.down, 55), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(50, "Midline", color = color.new(th.flat, 70), linewidth = 1, style = plot.style_circles, display = display.pane)
fill(pU, pL, color = color.new(th.flat, 92), title = "Neutral band")
pKE = plot(kE, "Stochastic · EMA", color = kCol, linewidth = 2)
pMid = plot(50, "Midline anchor", color = na, display = display.none)
fill(pKE, pMid, top_value = math.max(kE, 50), bottom_value = math.min(kE, 50),
top_color = kE >= 50 ? color.new(th.up, 78) : color.new(th.down, 97), bottom_color = kE >= 50 ? color.new(th.up, 97) : color.new(th.down, 78), title = "Momentum fill")
bool popZoneL = kE > inUpper and trendUp
bool popZoneS = kE < inLower and trendDn
bgcolor(inShade and popZoneL ? color.new(th.up, 93) : inShade and popZoneS ? color.new(th.down, 93) : na, title = "Pop / drop state")
// ═══════════════════════════════════════════════════════════════════ CHART · EMA, band, signals, trade levels (force_overlay)
color emaCol = emaRising ? th.up : emaFalling ? th.down : th.flat
plot(ema200, "EMA 200", color = color.new(emaCol, 35), linewidth = 1, offset = inEmaOff, force_overlay = true, display = display.pane)
color trailBase = trailDir == 1 ? th.up : th.down
color trailCol = sideways ? color.from_gradient(0.75, 0.0, 1.0, trailBase, th.flat) : trailStrong ? trailBase : color.from_gradient(0.45, 0.0, 1.0, th.flat, trailBase)
float trailUp = inTrendTrail and trailDir == 1 ? trail : na
float trailDn = inTrendTrail and trailDir == -1 ? trail : na
plot(trailUp, "Trend trail · up glow", color = color.new(trailCol, 84), linewidth = 6, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(trailDn, "Trend trail · down glow", color = color.new(trailCol, 84), linewidth = 6, style = plot.style_linebr, force_overlay = true, display = display.pane)
pTrailUp = plot(trailUp, "Trend trail · up", color = color.new(trailCol, 0), linewidth = 2, style = plot.style_linebr, force_overlay = true, display = display.pane)
pTrailDn = plot(trailDn, "Trend trail · down", color = color.new(trailCol, 0), linewidth = 2, style = plot.style_linebr, force_overlay = true, display = display.pane)
pPx = plot(inTrendTrail ? close : na, "Trend fill anchor", color = color.new(th.bg, 100), force_overlay = true, display = display.none)
// gradient: dense at the trail line, fading toward price; lighter while sideways
int fillDense = sideways ? 90 : trailStrong ? 72 : 84
fill(pTrailUp, pPx, top_value = not na(trailUp) ? math.max(trail, close) : na, bottom_value = not na(trailUp) ? math.min(trail, close) : na,
top_color = color.new(trailCol, 97), bottom_color = color.new(trailCol, fillDense), title = "Trend fill · up")
fill(pTrailDn, pPx, top_value = not na(trailDn) ? math.max(trail, close) : na, bottom_value = not na(trailDn) ? math.min(trail, close) : na,
top_color = color.new(trailCol, fillDense), bottom_color = color.new(trailCol, 97), title = "Trend fill · down")
plot(inShowSm and inSmType != "None" ? emaSm : na, "EMA 200 · smoothing", color = color.new(th.accent, 40), linewidth = 1, style = plot.style_circles, offset = inEmaOff, force_overlay = true, display = display.pane)
plot(inShowBand ? ema200 + inMaxDist * atrU : na, "Stretch band · top", color = color.new(emaCol, 84), linewidth = 1, force_overlay = true, display = display.pane)
plot(inShowBand ? ema200 - inMaxDist * atrU : na, "Stretch band · bottom", color = color.new(emaCol, 84), linewidth = 1, force_overlay = true, display = display.pane)
plotshape(ev.sigL, "POP long", shape.triangleup, location.belowbar, color = th.up, size = size.small, force_overlay = true, display = display.pane)
plotshape(ev.sigS, "DROP short", shape.triangledown, location.abovebar, color = th.down, size = size.small, force_overlay = true, display = display.pane)
bool live = t.dir != 0 or ev.exit
plot(inPlan and live ? t.entry : na, "Trade · entry", color = color.new(th.txtHi, 15), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(inPlan and live ? t.sl : na, "Trade · stop", color = color.new(th.down, 15), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(inPlan and live ? t.tp1 : na, "Trade · TP1", color = color.new(th.up, 55), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(inPlan and live ? t.tp2 : na, "Trade · TP2", color = color.new(th.up, 15), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
color regCol = trendUp ? th.up : trendDn ? th.down : th.flat
barcolor(inTint and t.dir != 0 ? color.new(th.accentHi, 15) : inCandleTrend ? (trendUp or trendDn ? color.new(regCol, 30) : color.new(th.flat, 35)) : na, title = "Trend candles / trade tint")
// gradient risk / reward zones: four bands each, densest at the entry, extended while the trade is open
var array<box> zones = array.new<box>()
var array<box> liveZ = array.new<box>()
gradBand(float top, float bot, color c, int x1, int x2, bool denseTop) =>
float h = (top - bot) / 4
for k = 0 to 3
float bTop = top - h * k
float bBot = bTop - h
int alpha = denseTop ? 78 + k * 5 : 93 - k * 5
box b = box.new(x1, bTop, x2, bBot, border_color = color.new(c, 100), bgcolor = color.new(c, alpha), force_overlay = true)
array.push(zones, b)
array.push(liveZ, b)
if barstate.isconfirmed
if (ev.sigL or ev.sigS) and inZones and inPlan
array.clear(liveZ)
int d = t.dir
// risk: entry → stop (dense at the entry) · reward: entry → TP2 (dense at the entry)
gradBand(math.max(t.entry, t.sl), math.min(t.entry, t.sl), th.down, bar_index, bar_index + 1, d == 1)
gradBand(math.max(t.entry, t.tp2), math.min(t.entry, t.tp2), th.up, bar_index, bar_index + 1, d == -1)
while array.size(zones) > 96
box.delete(array.shift(zones))
else if (t.dir != 0 or ev.exit) and array.size(liveZ) > 0
for b in liveZ
box.set_right(b, bar_index)
// event labels on the chart
if ev.sigL
label.new(bar_index, low, "POP ▲ " + fmt(t.entry) + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2), style = label.style_label_up, color = color.new(th.up, 12), textcolor = th.bg, size = size.small, force_overlay = true,
tooltip = "Long pop: the stochastic EMA crossed above " + str.tostring(inUpper, "0") + " with price above a rising EMA 200 and within " + str.tostring(inMaxDist, "0.0") + " ATR of it.\nentry " + fmt(t.entry) + " · stop " + fmt(t.sl) + " (" + fmt(t.r) + " = 1R)\nTP1 " + fmt(t.tp1) + " → breakeven · TP2 " + fmt(t.tp2))
if ev.sigS
label.new(bar_index, high, "DROP ▼ " + fmt(t.entry) + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2), style = label.style_label_down, color = color.new(th.down, 12), textcolor = th.bg, size = size.small, force_overlay = true,
tooltip = "Short drop: the stochastic EMA crossed below " + str.tostring(inLower, "0") + " with price below a falling EMA 200 and within " + str.tostring(inMaxDist, "0.0") + " ATR of it.\nentry " + fmt(t.entry) + " · stop " + fmt(t.sl) + " (" + fmt(t.r) + " = 1R)\nTP1 " + fmt(t.tp1) + " → breakeven · TP2 " + fmt(t.tp2))
if ev.exit
string rTxt = (ev.r >= 0 ? "+" : "") + str.tostring(ev.r, "0.0") + "R"
string whyTxt = ev.why == "BAND" ? "momentum exit" : ev.why == "TP2" ? "target" : ev.why == "BE" ? "breakeven" : "stop"
label.new(bar_index, ev.dir == 1 ? high : low, (ev.dir == 1 ? "exit long · " : "exit short · ") + whyTxt + " " + rTxt, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 15), textcolor = ev.r >= 0 ? th.up : th.down, size = size.tiny, force_overlay = true)
// ═══════════════════════════════════════════════════════════════════ WORDS (one source of truth)
string stateWord = t.dir == 1 ? "LONG OPEN" : t.dir == -1 ? "SHORT OPEN" : (side == 1 ? "FLAT · long side" : "FLAT · short side") + (sideways ? " · sideways" : "")
color stateCol = t.dir == 1 ? th.up : t.dir == -1 ? th.down : s1 ? (side == 1 ? th.up : th.down) : th.flat
float openR = t.dir != 0 ? (close - t.entry) * t.dir / t.r : na
string chipTxt = t.dir != 0 ? (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R now" + (t.tp1Hit ? " · stop at breakeven" : "") : allPre ? (s4 ? "set · on the pop side" : "set · waiting for the cross") : str.tostring(ready) + "/" + str.tostring(readyMax) + " steps true"
// ═══════════════════════════════════════════════════════════════════ DASHBOARD (force_overlay)
posOf(string p) =>
p == "Top Left" ? position.top_left : p == "Bottom Right" ? position.bottom_right : p == "Bottom Left" ? position.bottom_left : p == "Middle Right" ? position.middle_right : p == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large : size.normal
szBody() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.normal : size.small
szSub() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.small : size.tiny
row3(table tb, int r, string k, bool ok, string why, bool showMark) =>
color stripe = r % 2 == 1 ? color.new(th.accent, 94) : th.bg
table.cell(tb, 0, r, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe)
table.cell(tb, 1, r, showMark ? (ok ? "✓" : "✗") : "", text_color = ok ? th.up : th.down, text_size = szBody(), text_halign = text.align_center, bgcolor = stripe)
table.cell(tb, 2, r, why, text_color = ok ? th.txt : th.txtHi, text_size = szBody(), text_halign = text.align_left, text_font_family = font.family_monospace, bgcolor = stripe)
section(table tb, int r, string title) =>
table.cell(tb, 0, r, title, text_color = th.accent, text_size = szSub(), text_halign = text.align_left, bgcolor = color.new(th.accent, 90))
table.cell(tb, 1, r, "", bgcolor = color.new(th.accent, 90))
table.cell(tb, 2, r, "", bgcolor = color.new(th.accent, 90))
table.merge_cells(tb, 0, r, 2, r)
var table dash = table.new(posOf(inDashPos), 3, 20, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0, force_overlay = true)
if barstate.islast and inDash
table.clear(dash, 0, 0, 2, 19)
int r = 0
// header
table.cell(dash, 0, r, "STOCHASTIC POP DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "EMA 200 trend · Stochastic POP (Method 2, EMA 4 on the line) · the four steps of the rule, in order. Everything commits on the closed bar.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
table.cell(dash, 2, r, syminfo.ticker + " · " + timeframe.period, text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
r += 1
// state + chip
table.cell(dash, 0, r, (t.dir == 1 ? "▲ " : t.dir == -1 ? "▼ " : "● ") + stateWord, text_color = stateCol, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "The open paper trade, or the side the EMA 200 allows right now.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
color chipBg = t.dir != 0 ? color.new(openR >= 0 ? th.up : th.down, 75) : color.new(color.from_gradient(ready, 0, readyMax, th.down, th.up), 75)
table.cell(dash, 2, r, chipTxt, text_color = th.txtHi, text_size = szBody(), text_halign = text.align_right, bgcolor = chipBg, tooltip = t.dir != 0 ? "Open R of the paper trade at this close." : "How many of the listed steps are true right now. 'Set' = everything but the cross itself; the cross is the trigger and must be fresh.")
r += 1
if t.dir == 0
// readiness — the rule in order
section(dash, r, "READINESS FOR A " + str.upper(sideWord) + " · " + str.tostring(ready) + " OF " + str.tostring(readyMax) + " STEPS")
r += 1
row3(dash, r, "1 · Trend", s1, s1Txt, true)
r += 1
row3(dash, r, "2 · Not stretched", s2, s2Txt, true)
r += 1
row3(dash, r, "3 · Fresh pop", s3, s3Txt, true)
r += 1
row3(dash, r, "4 · Cross " + str.tostring(side == 1 ? inUpper : inLower, "0"), s4, s4Txt, true)
r += 1
if gateOn
row3(dash, r, "5 · Agreements", s5, s5Txt, true)
r += 1
if inHtfOn
row3(dash, r, "HTF " + inHtf, s6, htfOk ? (htfClose > htfEma ? "price above the HTF EMA" : "price below the HTF EMA") : "HTF must be higher than the chart", true)
r += 1
if inSessOn
row3(dash, r, "Session", s7, s7 ? "inside " + inSess1 + " / " + inSess2 : "outside the sessions (" + inSessTz + ")", true)
r += 1
row3(dash, r, "Missing", allPre, missing, false)
r += 1
else
section(dash, r, "TRADE · " + (t.dir == 1 ? "LONG" : "SHORT") + " · " + str.tostring(bar_index - t.bar) + " BARS")
r += 1
row3(dash, r, "Entry", true, fmt(t.entry) + " · 1R = " + fmt(t.r), false)
r += 1
row3(dash, r, "Stop", true, fmt(t.sl) + (inTrailStop != "Off" and t.sl != t.sl0 and t.sl != t.entry ? " · following the trail" : t.tp1Hit ? " · breakeven" : inStopMode == "ATR" ? " · ATR stop" : " · swing"), false)
r += 1
row3(dash, r, "Targets", t.tp1Hit, "TP1 " + fmt(t.tp1) + (t.tp1Hit ? " ✓" : "") + " · TP2 " + fmt(t.tp2), false)
r += 1
row3(dash, r, "Exit rule", true, (inExitMode == "Stop / targets only" ? "stop or TP2" : (t.dir == 1 ? "line back below " + str.tostring(inUpper, "0") : "line back above " + str.tostring(inLower, "0")) + (inExitMode == "Band cross back only" ? "" : " · or TP2 / stop")) + (inTrail ? " · trail " + str.tostring(inTrailAtr, "0.0") + " ATR" : "") + (inTrailStop != "Off" ? " · stop = trend trail" + (inTrailStop == "After TP1" ? " after TP1" : "") : "") + (inOppExit ? " · flip" : "") + (inTimeStop > 0 ? " · time " + str.tostring(inTimeStop) : ""), false)
r += 1
row3(dash, r, "Stochastic", s4, str.tostring(kE, "0.0") + (s4 ? " · momentum intact" : " · momentum fading"), true)
r += 1
if inShowCtx
section(dash, r, "CONTEXT · " + str.tostring(side == 1 ? agCountL : agCountS) + " OF " + str.tostring(agTotal) + " AGREE" + (inGate == "Trend + POP only" ? "" : " · GATE"))
r += 1
if inUseStack
row3(dash, r, "MA stack", side == 1 ? agStackL : agStackS, str.tostring(stackUp) + "/5 EMAs below price (5·10·20·100·200)", true)
r += 1
if inUse821
row3(dash, r, "EMA 8 / 21", side == 1 ? ag821L : ag821S, ema8 > ema21 ? "8 above 21" : "8 below 21", true)
r += 1
if inUseRsi
row3(dash, r, "RSI " + str.tostring(inRsiLen), side == 1 ? agRsiL : agRsiS, str.tostring(rsi, "0.0") + (rsi > 50 ? " · above 50" : " · below 50"), true)
r += 1
if inUseVol
row3(dash, r, "Volume", agVol, volMa > 0 ? str.tostring(nz(volume, 0.0) / volMa, "0.0") + "× its " + str.tostring(inVolLen) + "-bar average" : "n/a", true)
r += 1
section(dash, r, "RECORD · PAPER, NO COSTS")
r += 1
row3(dash, r, "Closed", st.n >= 5, st.n < 5 ? str.tostring(st.n) + " trades · too few to read" : str.tostring(st.n) + " · " + str.tostring(math.round(st.wins / st.n * 100)) + "% win" + (st.flat > 0 ? " · " + str.tostring(st.flat) + " flat" : "") + " · avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R", false)
r += 1
row3(dash, r, "How they ended", true, "band " + str.tostring(st.nBand) + " · stop " + str.tostring(st.nStop) + " · TP2 " + str.tostring(st.nTp) + (na(t.lastR) ? "" : " · last " + (t.lastR >= 0 ? "+" : "") + str.tostring(t.lastR, "0.0") + "R"), false)
r += 1
table.cell(dash, 0, r, "EMA " + str.tostring(inEmaLen) + " · stoch " + str.tostring(inStLen) + "/" + str.tostring(inStSmooth) + " · EMA " + str.tostring(inStEma) + " · " + str.tostring(inUpper, "0") + "/" + str.tostring(inLower, "0"), text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Your settings, as the thread listed them.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
string sw = (inRangeBlock ? "range-skip · " : "") + (inTrail ? "trail · " : "") + (inTrailStop != "Off" ? "trail-stop · " : "") + (inSessOn ? "session · " : "") + (inTimeStop > 0 ? "time · " : "") + (inOppExit ? "flip · " : "") + (inHtfOn ? "HTF " + inHtf + " · " : "")
table.cell(dash, 2, r, (sw == "" ? "no switches · " : sw) + (inGate == "Trend + POP only" ? "classic rule" : inGate == "Trend + POP + all agreements" ? "strict gate" : "majority gate"), text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "Active switches and the signal gate mode. ATR " + fmt(atr) + " is the unit for stops and the stretch band.")
// ═══════════════════════════════════════════════════════════════════ NARRATIVE · Co-Pilot (translates state; never a new signal)
wrap(string sIn, int width) =>
array<string> words = str.split(sIn, " ")
string out = ""
string line = ""
for wd in words
if line != "" and str.length(line) + str.length(wd) + 1 > width
out := out + (out == "" ? "" : "\n") + line
line := wd
else
line := line == "" ? wd : line + " " + wd
out == "" ? line : out + "\n" + line
narrNow() =>
string o = ""
if t.dir != 0
o := "in a " + (t.dir == 1 ? "long" : "short") + " from " + fmt(t.entry) + " for " + str.tostring(bar_index - t.bar) + " bars, " + (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R at this close — stop " + fmt(t.sl) + (t.tp1Hit ? " (moved to breakeven after TP1)" : "") + ", the stochastic line is at " + str.tostring(kE, "0.0") + (s4 ? " so momentum is still on the trade's side" : " — momentum is fading toward the band")
else
o := "flat — " + (s1 ? "the EMA 200 allows the " + sideWord + " side (" + s1Txt + ")" : "no clean side yet (" + s1Txt + ")") + (sideways ? "; the last " + str.tostring(inRangeLen) + " bars are a range (" + erTxt + ")" + (rangeBlock ? ", so signals are skipped" : " — pops inside a range are the weakest kind") : "") + "; the stochastic line is at " + str.tostring(kE, "0.0") + (s4 ? ", already on the pop side, " : ", ") + (allPre ? (s4 ? "every condition is true — the next fresh cross fires" : "every condition is true — the cross is the only thing left") : "and what is still false: " + missing)
o
narrWhy() =>
"Bernstein's Stochastic Pop, made mechanical: the side comes from a rising / falling EMA " + str.tostring(inEmaLen) + ", the entry from the EMA-" + str.tostring(inStEma) + " stochastic crossing " + str.tostring(inUpper, "0") + " / " + str.tostring(inLower, "0") + ", no pops that start more than " + str.tostring(inMaxDist, "0.0") + " ATR from the EMA, fresh pops only. The four dashboard steps are that rule in order."
narrPlan() =>
string o = ""
if t.dir != 0
o := "Stop first on every closed bar. " + (inExitMode == "Stop / targets only" ? "" : "If the line crosses back " + (t.dir == 1 ? "below " + str.tostring(inUpper, "0") : "above " + str.tostring(inLower, "0")) + ", the momentum exit closes the trade at that close. ") + (inExitMode == "Band cross back only" ? "" : "TP1 " + fmt(t.tp1) + (inTrail ? " starts a " + str.tostring(inTrailAtr, "0.0") + " ATR trailing stop" : " moves the stop to breakeven") + "; TP2 " + fmt(t.tp2) + " is the final target. ") + (inOppExit ? "An opposite cross closes it. " : "") + (inTimeStop > 0 ? "If TP1 is not reached within " + str.tostring(inTimeStop) + " bars, it closes on time. " : "") + "The idea is wrong beyond " + fmt(t.sl0) + "."
else
o := "A " + sideWord + " fires when " + (s1 ? "" : "price gets back on the right side of a " + (side == 1 ? "rising" : "falling") + " EMA 200, ") + (s2 ? "" : "price comes back within " + str.tostring(inMaxDist, "0.0") + " ATR of the EMA, ") + (s3 ? "" : "the line dips " + (side == 1 ? "below " + str.tostring(inLower, "0") : "above " + str.tostring(inUpper, "0")) + " to re-arm, ") + (inHtfOn and not s6 ? "the " + inHtf + " chart gets on the same side of its EMA, " : "") + (inSessOn and not s7 ? "a session opens, " : "") + "the line crosses " + (side == 1 ? "above " + str.tostring(inUpper, "0") : "below " + str.tostring(inLower, "0")) + " on a closed bar. Then: entry at that close, stop at the " + (inStopMode == "ATR" ? str.tostring(inStopAtr, "0.0") + " ATR distance" : str.tostring(inSwingN) + "-bar swing " + (side == 1 ? "low" : "high") + " minus a buffer") + ", TP1 " + str.tostring(inTp1, "0.0") + "R to breakeven, TP2 " + str.tostring(inTp2, "0.0") + "R" + (inExitMode == "Stop / targets only" ? "." : ", exit early if the line crosses back through its band.")
o
narrTrack() =>
st.n < 5 ? "Too few closed trades on this chart to say anything yet." : str.tostring(st.n) + " paper trades closed here: " + str.tostring(math.round(st.wins / st.n * 100)) + "% won, avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R; " + str.tostring(st.nBand) + " ended on the momentum exit, " + str.tostring(st.nStop) + " on the stop / breakeven, " + str.tostring(st.nTp) + " at TP2. No costs, small sample — a description of the past on THIS chart, not an edge."
narrPosOf(string p) =>
p == "Top Left" ? position.top_left : p == "Top Right" ? position.top_right : p == "Bottom Right" ? position.bottom_right : p == "Middle Left" ? position.middle_left : p == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()
var table narrT = na
if barstate.islast
if not na(narrT)
table.delete(narrT)
narrT := na
if inNarr != "Off"
array<string> labs = array.new<string>()
array<string> vals = array.new<string>()
array.push(labs, "NOW")
array.push(vals, wrap(narrNow() + " (decision support, not a trade instruction)", inNarrWrap))
bool deep = inNarr == "Standard" or inNarr == "Detailed"
if deep
array.push(labs, "WHY")
array.push(vals, wrap(narrWhy(), inNarrWrap))
array.push(labs, "PLAN")
array.push(vals, wrap(narrPlan(), inNarrWrap))
if inNarr == "Detailed"
array.push(labs, "TRACK REC")
array.push(vals, wrap(narrTrack(), inNarrWrap))
array.push(labs, "GLOSSARY")
array.push(vals, wrap("Pop = the stochastic line breaking above " + str.tostring(inUpper, "0") + " — momentum arriving. Drop = breaking below " + str.tostring(inLower, "0") + ". Fresh pop = the line has visited the other band since the last signal. Stretched = price too far from the EMA 200 to be a fair entry. Momentum exit = the line crossing back through its band.", inNarrWrap))
int nr = array.size(labs)
bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
int rows = nr + 2 + (atBottom ? 1 : 0)
narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1, force_overlay = true)
table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
table.cell(narrT, 1, 0, stateWord + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
for i = 0 to nr - 1
table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
table.cell(narrT, 1, nr + 1, "Honest: levels are geometry, not promises. The record has no costs — measure before you trust it.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
if atBottom
table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)
// ═══════════════════════════════════════════════════════════════════ ALERTS (confirmed bars only by construction)
payload(string evName, string why) =>
string js = '{"src":"StochasticPopDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"' + evName + '","side":"' + (t.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + '","entry":' + fmt(nz(t.entry, close)) + ',"sl":' + fmt(nz(t.sl, 0)) + ',"tp1":' + fmt(nz(t.tp1, 0)) + ',"tp2":' + fmt(nz(t.tp2, 0)) + ',"stoch":' + str.tostring(kE, "0.0") + ',"ema200":' + fmt(ema200) + ',"reason":"' + why + '"' + (na(ev.r) ? "" : ',"r":' + str.tostring(ev.r, "0.00")) + '}'
string tx = "Stochastic Pop Desk · " + syminfo.ticker + " " + timeframe.period + " · " + evName + " · " + (t.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + " · entry " + fmt(nz(t.entry, close)) + " · SL " + fmt(nz(t.sl, 0)) + " · TP " + fmt(nz(t.tp1, 0)) + " / " + fmt(nz(t.tp2, 0)) + (why != "" ? " · " + why : "")
inAlertJson ? js : tx
alertcondition(ev.sigL, "POP long", "Stochastic Pop Desk: long pop — stochastic crossed above the pop band with price above a rising EMA 200")
alertcondition(ev.sigS, "DROP short", "Stochastic Pop Desk: short drop — stochastic crossed below the drop band with price below a falling EMA 200")
alertcondition(ev.tp1, "TP1 reached", "Stochastic Pop Desk: TP1 reached — stop to breakeven")
alertcondition(ev.exit and ev.why == "BAND", "Momentum exit", "Stochastic Pop Desk: the stochastic crossed back through its band — exit")
alertcondition(ev.exit and ev.why == "TP2", "TP2 reached", "Stochastic Pop Desk: final target reached")
alertcondition(ev.exit and (ev.why == "SL" or ev.why == "BE"), "Stopped", "Stochastic Pop Desk: stop hit")
alertcondition(t.dir == 0 and allPre and not s4, "Ready — waiting for the cross", "Stochastic Pop Desk: every condition but the cross is true — the next stochastic cross fires a signal")
alertcondition(trailDir != trailDir[1] and trailDir[1] != 0, "Trend flipped", "Stochastic Pop Desk: the trend trail flipped to the other side of price")
if ev.sigL
alert(payload("POP", ""), alert.freq_once_per_bar_close)
if ev.sigS
alert(payload("DROP", ""), alert.freq_once_per_bar_close)
if ev.tp1
alert(payload("TP1", ""), alert.freq_once_per_bar_close)
if ev.exit
alert(payload("EXIT", ev.why), alert.freq_once_per_bar_close)
The strategy twin · optional
Measure the rules with costs on a second chart.
Copy the code as it is. No coding needed.
View the full code
// ═══════════════════════════════════════════════════════════════════════════════
// STOCHASTIC POP DESK — STRATEGY TWIN · Pine Script v6 · v1.5.0 (generated from pop_desk.pine by tools/gen-twin.mjs)
// One script that replaces three: the EMA 200 trend filter, the Stochastic POP (Jake Bernstein's method, the
// ChrisMoody "Method 2" bands 55 / 45, with an EMA 4 on the stochastic line) and an analysis panel that walks the
// four steps of the rule in order and tells you — in words — what is still missing before a signal can fire.
//
// HOW IT READS THE MARKET (every decision commits on the CLOSED bar — nothing repaints):
// 1 TREND price above a RISING EMA 200 = long side only · below a FALLING EMA 200 = short side only.
// 2 STRETCH the close must sit within N ATR of the EMA — a pop that starts far away is a chase.
// 3 FRESH the stochastic line must have visited the other band since the last signal (no re-entries).
// 4 CROSS the EMA-4 of the smoothed stochastic (14, 5) crosses ABOVE 55 → long pop · BELOW 45 → short drop.
// EXIT the line crosses back through its band (momentum gone) · or a stop / target ladder · or both.
// CONTEXT MA stack 5·10·20·100·200, EMA 8 / 21, RSI, volume — shown as agreements, optionally used as gates.
// SWITCHES (all off by default, each measured — numbers in the tooltips) ATR trailing stop after TP1 · session
// filter · time stop · opposite-signal exit · higher-timeframe EMA agreement.
//
// The stochastic lives in this pane; the EMA, the signals, the trade levels, the panel and the Co-Pilot are drawn
// on the price chart with force_overlay — one script, one settings dialog, two views.
// Paper ledger has NO costs — a description of the past on your chart, not a promise.
//
// License: MIT — share freely, keep this header. © 2026 ProEA Lab · built for r/pinescript
// ═══════════════════════════════════════════════════════════════════════════════
//@version=6
strategy("Stochastic Pop Desk — Strategy Twin", "StochPopS", overlay = false, initial_capital = 100000, default_qty_type = strategy.fixed, default_qty_value = 1, pyramiding = 0,
commission_type = strategy.commission.percent, commission_value = 0.02, slippage = 5, process_orders_on_close = true, calc_on_every_tick = false,
max_labels_count = 300, max_boxes_count = 160, max_lines_count = 100)
// COSTS ARE ON: 0.02% commission per order + 5 ticks slippage per fill (edit the strategy() header for your broker). Quantity = 1 unit.
// Entries fill at the signal bar's close (process_orders_on_close) — the same price the paper ledger uses.
// ─────────────────────────────────────────────────────────────────── INPUTS · trend (the EMA 200 exactly as the built-in)
gT = "Trend · EMA 200"
inEmaLen = input.int(200, "EMA length", minval = 1, group = gT, display = display.none)
inEmaSrc = input.source(close, "Source", group = gT, display = display.none)
inEmaOff = input.int(0, "Offset", minval = -500, maxval = 500, group = gT, display = display.none)
inSmType = input.string("SMA", "Smoothing line", options = ["None", "SMA", "EMA", "SMMA (RMA)", "WMA", "VWMA"], group = gT, tooltip = "The built-in EMA's second line: a moving average OF the EMA. Drawn dotted; it does not affect signals.", display = display.none)
inSmLen = input.int(5, "Smoothing length", minval = 1, group = gT, display = display.none)
inSlopeLen = input.int(10, "EMA must be rising / falling over N bars", minval = 1, maxval = 100, group = gT, tooltip = "Long side only while the EMA 200 is higher than N bars ago; short side only while it is lower.", display = display.none)
inMaxDist = input.float(3.0, "Not stretched: price within N ATR of the EMA", minval = 0.5, maxval = 20, step = 0.5, group = gT, tooltip = "A pop that fires far above a rising EMA is a chase. Signals are skipped when the close is farther than this from the EMA.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · stochastic POP (CM_Stochastic POP Method 2 + EMA 4)
gS = "Stochastic POP (Method 2)"
inStLen = input.int(14, "Stochastic length", minval = 1, group = gS, display = display.none)
inStSmooth = input.int(5, "Smoothing (%K)", minval = 1, group = gS, display = display.none)
inStEma = input.int(4, "EMA on the stochastic", minval = 1, group = gS, tooltip = "The 'plus EMA 4' — the signal line is an EMA of the smoothed stochastic. It cuts the whipsaws; it adds a little lag.", display = display.none)
inUpper = input.float(55, "Pop band (long above)", minval = 50, maxval = 95, group = gS, display = display.none)
inLower = input.float(45, "Drop band (short below)", minval = 5, maxval = 50, group = gS, display = display.none)
inMustReset = input.bool(true, "Fresh pop only (the line must visit the other band between signals)", group = gS, tooltip = "A long pop counts only if the line has been below the drop band since the last long pop — no re-entries on every wiggle across 55.", display = display.none)
inCooldown = input.int(5, "Cooldown between signals (bars)", minval = 0, maxval = 200, group = gS, display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · context components
gA = "Context (agreements)"
inUseStack = input.bool(true, "MA stack 5 · 10 · 20 · 100 · 200", group = gA, display = display.none)
inUse821 = input.bool(true, "EMA 8 / 21 cross", group = gA, display = display.none)
inUseRsi = input.bool(true, "RSI above / below 50", group = gA, display = display.none)
inRsiLen = input.int(9, "RSI length", minval = 2, group = gA, display = display.none)
inUseVol = input.bool(true, "Volume above its average", group = gA, display = display.none)
inVolLen = input.int(20, "Volume average length", minval = 2, group = gA, display = display.none)
inAtrLen = input.int(10, "ATR length (stops, stretch, panel)", minval = 1, group = gA, display = display.none)
inGate = input.string("Trend + POP only", "Signal needs", options = ["Trend + POP only", "Trend + POP + majority of agreements", "Trend + POP + all agreements"], group = gA,
tooltip = "What must be true for a signal. 'Trend + POP only' is the classic Bernstein rule; the agreements are then context. The stricter modes use them as gates.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · trade plan
gE = "Trade plan (paper — no costs)"
inPlan = input.bool(true, "Show entry / stop / targets", group = gE, display = display.none)
inStopMode = input.string("Swing low / high", "Stop", options = ["Swing low / high", "ATR"], group = gE, display = display.none)
inSwingN = input.int(10, "Swing lookback (bars)", minval = 2, maxval = 100, group = gE, display = display.none)
inStopAtr = input.float(2.0, "ATR stop (× ATR)", minval = 0.3, maxval = 10, step = 0.1, group = gE, display = display.none)
inStopBuf = input.float(0.2, "Stop buffer (× ATR)", minval = 0, maxval = 3, step = 0.1, group = gE, display = display.none)
inTp1 = input.float(1.0, "TP1 (R) → stop to breakeven", minval = 0.3, maxval = 10, step = 0.1, group = gE, display = display.none)
inTp2 = input.float(2.0, "TP2 (R) final", minval = 0.5, maxval = 20, step = 0.1, group = gE, display = display.none)
inExitMode = input.string("Stop / targets only", "Exit", options = ["Stop / targets only", "Band cross back or TP2", "Band cross back only"], group = gE,
tooltip = "Stop / targets only = the stop (swing or ATR), TP1 to breakeven, TP2 final — measured best on 2 of 3 test charts, tied on the third. Band cross back = the stochastic line crossing back through its band closes the trade early (the classic exit) — it cut winners short in the measurement. Both = the ladder plus the band exit.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · switches (all off — measured, see tooltips)
gX = "Switches (off by default)"
inTrail = input.bool(false, "Trailing stop after TP1 (ATR)", group = gX, tooltip = "Once TP1 is reached the stop trails N ATR behind price instead of parking at breakeven. Measured with costs: BTCUSD 1H PF 1.51 → 1.58 (more winners, smaller), XAUUSD 1H 1.31 → 1.10 — market-dependent.", display = display.none)
inTrailAtr = input.float(2.0, " Trail distance (× ATR)", minval = 0.5, maxval = 10, step = 0.5, group = gX, display = display.none)
inTrailStop = input.string("Off", "Stop follows the trend trail", options = ["Off", "After TP1", "From entry"], group = gX,
tooltip = "The chandelier trend trail drawn on the chart becomes the stop: it ratchets with the trend and the trade closes when the trail flips to the other side of price. 'After TP1' keeps the swing / ATR stop until TP1 is paid, then hands the trade to the trail; 'From entry' uses the trail from the first bar. Uses the trail settings in Style. Measured with costs (profit factor, ladder default → switch): After TP1 — BTC 1H 1.51 → 1.48 (win rate 30 → 44 %), gold 1H 1.31 → 1.18, gold 15m 0.81 → 0.88. From entry — BTC 1H 1.51 → 1.81 (drawdown −27 %), gold 1H 1.31 → 0.96, gold 15m 0.81 → 0.55. It pays where trends run (bitcoin) and costs where price chops around the EMA 200 (gold) — read your chart first.", display = display.none)
inSessOn = input.bool(false, "Session filter (signals only inside the sessions)", group = gX, tooltip = "Two windows, exchange time as set below. Off = any time. Measured: XAUUSD 1H PF 1.31 → 1.23 with a third of the trades; meaningless on 24/7 markets.", display = display.none)
inSessTz = input.string("America/New_York", " Session timezone (IANA)", group = gX, display = display.none)
inSess1 = input.session("0200-0500", " Session 1", group = gX, display = display.none)
inSess2 = input.session("0830-1100", " Session 2", group = gX, display = display.none)
inTimeStop = input.int(0, "Time stop: close after N bars if TP1 not reached (0 = off)", minval = 0, maxval = 500, group = gX, tooltip = "Measured at 30 bars: BTCUSD 1H PF 1.51 → 1.33, XAUUSD 1H 1.31 → 1.23 — it hurt on both.", display = display.none)
inOppExit = input.bool(false, "Opposite-signal exit (a drop cross closes a long, a pop cross closes a short)", group = gX, tooltip = "Measured: BTCUSD 1H PF 1.51 → 1.68, XAUUSD 1H 1.31 → 1.02 — market-dependent.", display = display.none)
inHtfOn = input.bool(false, "Higher-timeframe agreement (price on the same side of the EMA on the HTF)", group = gX, tooltip = "Read from the last CLOSED higher-timeframe bar — no lookahead. Adds a row to the readiness list. The one switch that helped on every chart tried (240 on 1H / 15m): BTCUSD 1H PF 1.51 → 1.89, XAUUSD 1H 1.31 → 1.41, XAUUSD 15m 0.81 → 0.98 — fewer trades each time.", display = display.none)
inHtf = input.timeframe("240", " Higher timeframe", group = gX, display = display.none)
inRangeBlock = input.bool(false, "Skip signals in a sideways regime", group = gX,
tooltip = "The sideways detector is always shown (state line, step 1 text, the trail turns grey). With this on, step 1 also fails while the market is sideways, so no pop / drop fires inside a range. Sideways = Kaufman efficiency (net move ÷ path over N bars) in the choppiest N % of the last 200 bars, released 15 points higher so it does not flicker. Measured with costs (profit factor, ladder default → switch): BTC 1H 1.51 → 1.90 (27 → 13 trades), gold 1H 1.31 → 2.55 (83 → 42 trades, drawdown −60 %), gold 15m 0.81 → 0.50 (44 → 19 trades). Strong on the hourly charts, wrong on the 15-minute one — read your chart first.", display = display.none)
inRangeLen = input.int(20, " Sideways detector: bars", minval = 5, maxval = 200, group = gX, display = display.none)
inRangePct = input.int(25, " Sideways = the choppiest N % of the last 200 bars", minval = 5, maxval = 60, group = gX, tooltip = "Efficiency = |close − close[N]| ÷ Σ|close − close[1]| over N bars (100 % = a straight line, 0 % = pure chop). Instead of a fixed threshold — which is never portable between charts — the market is 'sideways' while its efficiency sits in the bottom N % of the last 200 bars, and stays flagged until it climbs above the (N + 15) % mark. Self-calibrating on any symbol and timeframe.", display = display.none)
// ─────────────────────────────────────────────────────────────────── INPUTS · style / panel / co-pilot / alerts
gV = "Style"
inTheme = input.string("Aurora Neon", "Theme", options = ["Aurora Neon", "Royal Gold", "Colorblind-safe"], group = gV, display = display.none)
inShowSm = input.bool(true, "Draw the EMA smoothing line", group = gV, display = display.none)
inShowBand = input.bool(false, "Draw the stretch band (EMA ± N ATR)", group = gV, display = display.none)
inTrendTrail = input.bool(true, "Trend trail line + fill (green under price in an uptrend, red over price in a downtrend)", group = gV,
tooltip = "A chandelier-style trail: highest high − N ATR while the EMA 200 regime is up, lowest low + N ATR while it is down; it only ratchets with the trend and resets when the regime flips. The soft fill between the trail and price is the regime at a glance. Display only — signals still come from the four steps.", display = display.none)
inTrailMult2 = input.float(3.0, " Trail distance (× ATR)", minval = 0.5, maxval = 10, step = 0.5, group = gV, display = display.none)
inZones = input.bool(false, "Gradient risk / reward zones", group = gV, display = display.none)
inTint = input.bool(true, "Tint candles while a trade is open", group = gV, display = display.none)
inShade = input.bool(false, "Shade the pane while in a pop / drop", group = gV, display = display.none)
inCandleTrend = input.bool(false, "Colour candles by trend (green · red · grey)", group = gV, tooltip = "Soft green = price above a rising EMA 200 (long side) · soft red = below a falling EMA 200 (short side) · grey = no clean side. While a trade is open the candles turn accent-coloured instead, so the trade period is visible at a glance.", display = display.none)
gD = "Dashboard"
inDash = input.bool(false, "Show the dashboard", group = gD, display = display.none)
inDashPos = input.string("Top Right", "Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left", "Middle Right", "Middle Left"], group = gD, display = display.none)
inSize = input.string("Medium", "Size", options = ["Small", "Medium", "Large"], group = gD, display = display.none)
inShowCtx = input.bool(false, "Show the context section (MA stack · EMA 8/21 · RSI · volume)", group = gD, display = display.none)
gN = "Narrative · Co-Pilot"
inNarr = input.string("Off", "Narrative depth", options = ["Off", "Brief", "Standard", "Detailed"], group = gN, tooltip = "Brief = one sentence (NOW). Standard adds WHY and PLAN. Detailed adds the track record and a glossary.", display = display.none)
inNarrPos = input.string("Bottom Left", "Position", options = ["Bottom Left", "Bottom Right", "Top Left", "Top Right", "Middle Left", "Middle Right"], group = gN, display = display.none)
inNarrSize = input.string("Auto", "Size", options = ["Auto", "Small", "Medium", "Large"], group = gN, display = display.none)
inNarrWrap = input.int(64, "Line width (characters)", minval = 30, maxval = 120, group = gN, display = display.none)
gL = "Alerts"
inAlertJson = input.bool(true, "alert() payload as JSON (webhooks)", group = gL, tooltip = "Choose 'Any alert() function call' when creating the alert.", display = display.none)
// ─────────────────────────────────────────────────────────────────── THEME
type Theme
color bg = #0B0E14
color frame = #B8923A
color accent = #D8B14A
color accentHi = #E8C25A
color up = #2BE7C7
color down = #FF3B5C
color flat = #6B7280
color txt = #D7DCE6
color txtHi = #ECEFF5
themeOf(string name) =>
Theme t = Theme.new()
if name == "Aurora Neon"
t.bg := #0E1118
t.frame := #2C3A5A
t.accent := #7C9CFF
t.accentHi := #A9B8FF
t.up := #2AF0C8
t.down := #FF4D6D
t.flat := #5B6472
t.txt := #E6E9EF
t.txtHi := #F2F4F9
else if name == "Colorblind-safe"
t.bg := #0E1014
t.frame := #3A4150
t.accent := #56B4E9
t.accentHi := #8ED0F5
t.up := #E69F00
t.down := #0072B2
t.flat := #8C92A0
t.txt := #E8EAED
t.txtHi := #FFFFFF
t
Theme th = themeOf(inTheme)
fmt(float x) => str.tostring(x, format.mintick)
// ═══════════════════════════════════════════════════════════════════ ENGINE INPUTS (ta.* at global scope)
float ema200 = ta.ema(inEmaSrc, inEmaLen)
float smSma = ta.sma(ema200, inSmLen)
float smEma = ta.ema(ema200, inSmLen)
float smRma = ta.rma(ema200, inSmLen)
float smWma = ta.wma(ema200, inSmLen)
float smVwma = ta.vwma(ema200, inSmLen)
float emaSm = inSmType == "SMA" ? smSma : inSmType == "EMA" ? smEma : inSmType == "SMMA (RMA)" ? smRma : inSmType == "WMA" ? smWma : inSmType == "VWMA" ? smVwma : ema200
float atr = ta.atr(inAtrLen)
float atrU = math.max(atr, syminfo.mintick)
float kRaw = ta.stoch(close, high, low, inStLen)
float kS = ta.sma(kRaw, inStSmooth)
float kE = ta.ema(kS, inStEma)
float ma5 = ta.ema(close, 5)
float ma10 = ta.ema(close, 10)
float ma20 = ta.ema(close, 20)
float ma100 = ta.ema(close, 100)
float ma200 = ta.ema(close, 200)
float ema8 = ta.ema(close, 8)
float ema21 = ta.ema(close, 21)
float rsi = ta.rsi(close, inRsiLen)
float volMa = ta.sma(nz(volume, 0.0), inVolLen)
float swLo = ta.lowest(low, inSwingN)
float swHi = ta.highest(high, inSwingN)
bool inSess = not inSessOn or not na(time(timeframe.period, inSess1, inSessTz)) or not na(time(timeframe.period, inSess2, inSessTz))
htfEmaFn() => ta.ema(close, inEmaLen)
float htfEma = request.security(syminfo.tickerid, inHtf, htfEmaFn()[1], lookahead = barmerge.lookahead_on)
float htfClose = request.security(syminfo.tickerid, inHtf, close[1], lookahead = barmerge.lookahead_on)
bool htfOk = timeframe.in_seconds(inHtf) > timeframe.in_seconds()
bool htfUp = not inHtfOn or not htfOk or htfClose > htfEma
bool htfDn = not inHtfOn or not htfOk or htfClose < htfEma
bool crossUp = ta.crossover(kE, inUpper)
bool crossDn = ta.crossunder(kE, inLower)
bool backDn = ta.crossunder(kE, inUpper) // long momentum gone
bool backUp = ta.crossover(kE, inLower) // short momentum gone
// trend
bool emaRising = ema200 > ema200[inSlopeLen]
bool emaFalling = ema200 < ema200[inSlopeLen]
bool trendUpRaw = close > ema200 and emaRising
bool trendDnRaw = close < ema200 and emaFalling
// sideways detector: Kaufman efficiency (net move ÷ path) with hysteresis; always shown, blocks signals only when the switch is on
float erNet = math.abs(close - close[inRangeLen])
float erPath = math.sum(math.abs(close - close[1]), inRangeLen)
float er = erPath > 0 ? erNet / erPath : 0.0
float erLo = ta.percentile_nearest_rank(er, 200, inRangePct)
float erHi = ta.percentile_nearest_rank(er, 200, math.min(inRangePct + 15, 95))
var bool sideways = false
sideways := sideways ? er < erHi : er < erLo
bool rangeBlock = inRangeBlock and sideways
bool trendUp = trendUpRaw and not rangeBlock
bool trendDn = trendDnRaw and not rangeBlock
float distAtr = (close - ema200) / atrU
bool stretchedUp = distAtr > inMaxDist
bool stretchedDn = -distAtr > inMaxDist
// context (agreements)
int stackUp = (close > ma5 ? 1 : 0) + (close > ma10 ? 1 : 0) + (close > ma20 ? 1 : 0) + (close > ma100 ? 1 : 0) + (close > ma200 ? 1 : 0)
int stackDn = 5 - stackUp
bool agStackL = stackUp >= 4
bool agStackS = stackDn >= 4
bool ag821L = ema8 > ema21
bool ag821S = ema8 < ema21
bool agRsiL = rsi > 50
bool agRsiS = rsi < 50
bool agVol = volMa > 0 and nz(volume, 0.0) > volMa
int agCountL = (inUseStack and agStackL ? 1 : 0) + (inUse821 and ag821L ? 1 : 0) + (inUseRsi and agRsiL ? 1 : 0) + (inUseVol and agVol ? 1 : 0)
int agCountS = (inUseStack and agStackS ? 1 : 0) + (inUse821 and ag821S ? 1 : 0) + (inUseRsi and agRsiS ? 1 : 0) + (inUseVol and agVol ? 1 : 0)
int agTotal = (inUseStack ? 1 : 0) + (inUse821 ? 1 : 0) + (inUseRsi ? 1 : 0) + (inUseVol ? 1 : 0)
bool gateL = inGate == "Trend + POP only" ? true : inGate == "Trend + POP + all agreements" ? agCountL == agTotal : agCountL * 2 >= agTotal
bool gateS = inGate == "Trend + POP only" ? true : inGate == "Trend + POP + all agreements" ? agCountS == agTotal : agCountS * 2 >= agTotal
// trail state (filled in the display section; the state machine reads the PREVIOUS bar's values — no repaint)
var float trail = na
var int trailDir = 0
// ═══════════════════════════════════════════════════════════════════ STATE (confirmed bars only)
type Trade
int dir = 0 // 0 flat · 1 long · −1 short
float entry = na
float sl = na
float sl0 = na
float r = na
float tp1 = na
float tp2 = na
int bar = na
bool tp1Hit = false
int lastSig = -1000
bool resetL = true // a long pop is allowed (the line has visited the drop band since the last long)
bool resetS = true
float lastR = na // last closed trade's R (for the panel)
string lastWhy = ""
type Stat
int n = 0
int wins = 0
int flat = 0
float sumR = 0.0
int nBand = 0
int nStop = 0
int nTp = 0
type Ev
bool sigL = false
bool sigS = false
bool exit = false
bool tp1 = false
string why = ""
float r = na
int dir = 0
var Trade t = Trade.new()
var Stat st = Stat.new()
Ev ev = Ev.new()
closeTrade(float px, string why) =>
float rr = (px - t.entry) * t.dir / t.r
ev.exit := true
ev.why := why
ev.r := rr
ev.dir := t.dir
st.n := st.n + 1
st.wins := st.wins + (rr >= 0.05 ? 1 : 0)
st.flat := st.flat + (math.abs(rr) < 0.05 ? 1 : 0)
st.sumR := st.sumR + rr
if why == "BAND" or why == "FLIP" or why == "TIME"
st.nBand := st.nBand + 1
else if why == "TP2"
st.nTp := st.nTp + 1
else
st.nStop := st.nStop + 1
t.lastR := rr
t.lastWhy := why
t.dir := 0
openTrade(int d) =>
float anchor = inStopMode == "ATR" ? (d == 1 ? close - inStopAtr * atrU : close + inStopAtr * atrU) : (d == 1 ? swLo - inStopBuf * atrU : swHi + inStopBuf * atrU)
float slPx = d == 1 ? math.min(anchor, close - syminfo.mintick) : math.max(anchor, close + syminfo.mintick)
t.dir := d
t.entry := close
t.sl := slPx
t.sl0 := slPx
t.r := math.abs(close - slPx)
t.tp1 := close + d * inTp1 * t.r
t.tp2 := close + d * inTp2 * t.r
t.bar := bar_index
t.tp1Hit := false
t.lastSig := bar_index
if barstate.isconfirmed and not na(atr) and not na(kE) and bar_index > inEmaLen
if kE < inLower
t.resetL := true
if kE > inUpper
t.resetS := true
// ── manage an open trade (stop first)
if t.dir != 0
int d = t.dir
bool stopHit = d == 1 ? low <= t.sl : high >= t.sl
bool useLadder = inExitMode != "Band cross back only"
bool useBand = inExitMode != "Stop / targets only"
if useLadder and stopHit
closeTrade(t.sl, (inTrailStop != "Off" or (inTrail and t.tp1Hit)) and t.sl != t.entry and t.sl != t.sl0 ? "TRAIL" : t.tp1Hit ? "BE" : "SL")
else if useLadder and (d == 1 ? high >= t.tp2 : low <= t.tp2)
closeTrade(t.tp2, "TP2")
else
if useLadder and not t.tp1Hit and (d == 1 ? high >= t.tp1 : low <= t.tp1)
t.tp1Hit := true
ev.tp1 := true
t.sl := t.entry
if inTrail and t.tp1Hit
float trailPx = d == 1 ? close - inTrailAtr * atrU : close + inTrailAtr * atrU
t.sl := d == 1 ? math.max(t.sl, trailPx) : math.min(t.sl, trailPx)
if inTrailStop != "Off" and not na(trail) and (inTrailStop == "From entry" or t.tp1Hit)
if trailDir == d
t.sl := d == 1 ? math.max(t.sl, trail) : math.min(t.sl, trail)
else if bar_index - t.bar >= 1
closeTrade(close, "TRAIL")
if useBand and (d == 1 ? backDn : backUp)
closeTrade(close, "BAND")
else if inOppExit and (d == 1 ? crossDn : crossUp)
closeTrade(close, "FLIP")
else if inTimeStop > 0 and not t.tp1Hit and bar_index - t.bar >= inTimeStop
closeTrade(close, "TIME")
// ── signals (only when flat)
if t.dir == 0
bool cool = bar_index - t.lastSig >= inCooldown
bool okL = crossUp and trendUp and not stretchedUp and gateL and cool and (not inMustReset or t.resetL) and inSess and htfUp
bool okS = crossDn and trendDn and not stretchedDn and gateS and cool and (not inMustReset or t.resetS) and inSess and htfDn
if okL
openTrade(1)
t.resetL := false
ev.sigL := true
else if okS
openTrade(-1)
t.resetS := false
ev.sigS := true
// ═══════════════════════════════════════════════════════════════════ READINESS (display only — the four steps of the rule, in order)
int side = t.dir != 0 ? t.dir : trendUp ? 1 : trendDn ? -1 : (close > ema200 ? 1 : -1)
bool s1 = side == 1 ? trendUp : trendDn
bool s2 = side == 1 ? not stretchedUp : not stretchedDn
bool s3 = not inMustReset or (side == 1 ? t.resetL : t.resetS)
bool s4 = side == 1 ? kE > inUpper : kE < inLower // the line is on the pop side (the cross is the event)
bool s5 = side == 1 ? gateL : gateS
bool gateOn = inGate != "Trend + POP only"
bool s6 = side == 1 ? htfUp : htfDn
bool s7 = inSess
int ready = (s1 ? 1 : 0) + (s2 ? 1 : 0) + (s3 ? 1 : 0) + (s4 ? 1 : 0) + (gateOn and s5 ? 1 : 0) + (inHtfOn and s6 ? 1 : 0) + (inSessOn and s7 ? 1 : 0)
int readyMax = 4 + (gateOn ? 1 : 0) + (inHtfOn ? 1 : 0) + (inSessOn ? 1 : 0)
bool allPre = s1 and s2 and s3 and (not gateOn or s5) and (not inHtfOn or s6) and (not inSessOn or s7)
string sideWord = side == 1 ? "long" : "short"
string erTxt = "efficiency " + str.tostring(er * 100, "0") + " % over " + str.tostring(inRangeLen) + " bars" + (sideways ? ", the choppiest " + str.tostring(inRangePct) + " % of recent bars" : "")
string s1Base = side == 1 ? (close > ema200 ? (emaRising ? "above a rising EMA 200" : "above the EMA, but it is not rising") : "price is below the EMA 200") : (close < ema200 ? (emaFalling ? "below a falling EMA 200" : "below the EMA, but it is not falling") : "price is above the EMA 200")
string s1Txt = rangeBlock and (side == 1 ? trendUpRaw : trendDnRaw) ? "sideways — " + erTxt + " · signals skipped" : s1Base + (sideways ? " · sideways (" + erTxt + ")" : "")
string s2Txt = str.tostring(math.abs(distAtr), "0.0") + " ATR from the EMA" + (s2 ? " (≤ " + str.tostring(inMaxDist, "0.0") + ")" : " — too far, > " + str.tostring(inMaxDist, "0.0"))
string s3Txt = not inMustReset ? "not required" : (s3 ? "armed — the line visited " + str.tostring(side == 1 ? inLower : inUpper, "0") : "spent — needs a dip " + (side == 1 ? "below " + str.tostring(inLower, "0") : "above " + str.tostring(inUpper, "0")))
string s4Txt = "line " + str.tostring(kE, "0.0") + (s4 ? " · on the pop side" : (side == 1 ? " · waiting for a cross above " + str.tostring(inUpper, "0") : " · waiting for a cross below " + str.tostring(inLower, "0")))
string s5Txt = str.tostring(side == 1 ? agCountL : agCountS) + " / " + str.tostring(agTotal) + (inGate == "Trend + POP only" ? " agree · context only" : s5 ? " agree · gate passed" : " agree · gate blocks")
string missing = t.dir != 0 ? "in a trade" : allPre ? (s4 ? "nothing — a fresh cross is the trigger" : "only the cross itself") : (not s1 ? (rangeBlock ? "step 1 (sideways)" : "step 1 (trend)") : "") + (not s2 ? (not s1 ? " · " : "") + "step 2 (stretched)" : "") + (not s3 ? ((not s1 or not s2) ? " · " : "") + "step 3 (fresh pop)" : "") + (gateOn and not s5 ? ((not s1 or not s2 or not s3) ? " · " : "") + "agreements" : "") + (inHtfOn and not s6 ? " · HTF disagrees" : "") + (inSessOn and not s7 ? " · outside the sessions" : "")
// ── trend trail (display + optional stop): a chandelier-style trailing line that flips ONLY when price closes through
// it — a pullback that hugs the EMA 200 does not flip it, so a range does not zig-zag the line. It seeds from the flip
// bar's own extreme (always on the right side of price) and ratchets from the extreme reached since the flip.
// Colour: full = the EMA 200 slope confirms the side (the rule's regime); muted = the slope does not confirm yet;
// grey = the sideways detector is on.
var float regExt = na
if trailDir == 0
if not na(atrU) and not na(ema200)
trailDir := close > ema200 ? 1 : -1
regExt := trailDir == 1 ? high : low
trail := trailDir == 1 ? regExt - inTrailMult2 * atrU : regExt + inTrailMult2 * atrU
else if trailDir == 1
if close < trail
trailDir := -1
regExt := low
trail := low + inTrailMult2 * atrU
else
regExt := math.max(nz(regExt, high), high)
float candU = regExt - inTrailMult2 * atrU
trail := math.max(nz(trail, candU), candU)
else
if close > trail
trailDir := 1
regExt := high
trail := high - inTrailMult2 * atrU
else
regExt := math.min(nz(regExt, low), low)
float candD = regExt + inTrailMult2 * atrU
trail := math.min(nz(trail, candD), candD)
bool trailStrong = trailDir == 1 ? emaRising : emaFalling
// ═══════════════════════════════════════════════════════════════════ PANE · the stochastic
color kCol = color.from_gradient(kE, inLower, inUpper, th.down, th.up)
pU = plot(inUpper, "Pop band", color = color.new(th.up, 55), linewidth = 1, style = plot.style_circles, display = display.pane)
pL = plot(inLower, "Drop band", color = color.new(th.down, 55), linewidth = 1, style = plot.style_circles, display = display.pane)
plot(50, "Midline", color = color.new(th.flat, 70), linewidth = 1, style = plot.style_circles, display = display.pane)
fill(pU, pL, color = color.new(th.flat, 92), title = "Neutral band")
pKE = plot(kE, "Stochastic · EMA", color = kCol, linewidth = 2)
pMid = plot(50, "Midline anchor", color = na, display = display.none)
fill(pKE, pMid, top_value = math.max(kE, 50), bottom_value = math.min(kE, 50),
top_color = kE >= 50 ? color.new(th.up, 78) : color.new(th.down, 97), bottom_color = kE >= 50 ? color.new(th.up, 97) : color.new(th.down, 78), title = "Momentum fill")
bool popZoneL = kE > inUpper and trendUp
bool popZoneS = kE < inLower and trendDn
bgcolor(inShade and popZoneL ? color.new(th.up, 93) : inShade and popZoneS ? color.new(th.down, 93) : na, title = "Pop / drop state")
// ═══════════════════════════════════════════════════════════════════ CHART · EMA, band, signals, trade levels (force_overlay)
color emaCol = emaRising ? th.up : emaFalling ? th.down : th.flat
plot(ema200, "EMA 200", color = color.new(emaCol, 35), linewidth = 1, offset = inEmaOff, force_overlay = true, display = display.pane)
color trailBase = trailDir == 1 ? th.up : th.down
color trailCol = sideways ? color.from_gradient(0.75, 0.0, 1.0, trailBase, th.flat) : trailStrong ? trailBase : color.from_gradient(0.45, 0.0, 1.0, th.flat, trailBase)
float trailUp = inTrendTrail and trailDir == 1 ? trail : na
float trailDn = inTrendTrail and trailDir == -1 ? trail : na
plot(trailUp, "Trend trail · up glow", color = color.new(trailCol, 84), linewidth = 6, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(trailDn, "Trend trail · down glow", color = color.new(trailCol, 84), linewidth = 6, style = plot.style_linebr, force_overlay = true, display = display.pane)
pTrailUp = plot(trailUp, "Trend trail · up", color = color.new(trailCol, 0), linewidth = 2, style = plot.style_linebr, force_overlay = true, display = display.pane)
pTrailDn = plot(trailDn, "Trend trail · down", color = color.new(trailCol, 0), linewidth = 2, style = plot.style_linebr, force_overlay = true, display = display.pane)
pPx = plot(inTrendTrail ? close : na, "Trend fill anchor", color = color.new(th.bg, 100), force_overlay = true, display = display.none)
// gradient: dense at the trail line, fading toward price; lighter while sideways
int fillDense = sideways ? 90 : trailStrong ? 72 : 84
fill(pTrailUp, pPx, top_value = not na(trailUp) ? math.max(trail, close) : na, bottom_value = not na(trailUp) ? math.min(trail, close) : na,
top_color = color.new(trailCol, 97), bottom_color = color.new(trailCol, fillDense), title = "Trend fill · up")
fill(pTrailDn, pPx, top_value = not na(trailDn) ? math.max(trail, close) : na, bottom_value = not na(trailDn) ? math.min(trail, close) : na,
top_color = color.new(trailCol, fillDense), bottom_color = color.new(trailCol, 97), title = "Trend fill · down")
plot(inShowSm and inSmType != "None" ? emaSm : na, "EMA 200 · smoothing", color = color.new(th.accent, 40), linewidth = 1, style = plot.style_circles, offset = inEmaOff, force_overlay = true, display = display.pane)
plot(inShowBand ? ema200 + inMaxDist * atrU : na, "Stretch band · top", color = color.new(emaCol, 84), linewidth = 1, force_overlay = true, display = display.pane)
plot(inShowBand ? ema200 - inMaxDist * atrU : na, "Stretch band · bottom", color = color.new(emaCol, 84), linewidth = 1, force_overlay = true, display = display.pane)
plotshape(ev.sigL, "POP long", shape.triangleup, location.belowbar, color = th.up, size = size.small, force_overlay = true, display = display.pane)
plotshape(ev.sigS, "DROP short", shape.triangledown, location.abovebar, color = th.down, size = size.small, force_overlay = true, display = display.pane)
bool live = t.dir != 0 or ev.exit
plot(inPlan and live ? t.entry : na, "Trade · entry", color = color.new(th.txtHi, 15), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(inPlan and live ? t.sl : na, "Trade · stop", color = color.new(th.down, 15), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(inPlan and live ? t.tp1 : na, "Trade · TP1", color = color.new(th.up, 55), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
plot(inPlan and live ? t.tp2 : na, "Trade · TP2", color = color.new(th.up, 15), linewidth = 1, style = plot.style_linebr, force_overlay = true, display = display.pane)
color regCol = trendUp ? th.up : trendDn ? th.down : th.flat
barcolor(inTint and t.dir != 0 ? color.new(th.accentHi, 15) : inCandleTrend ? (trendUp or trendDn ? color.new(regCol, 30) : color.new(th.flat, 35)) : na, title = "Trend candles / trade tint")
// gradient risk / reward zones: four bands each, densest at the entry, extended while the trade is open
var array<box> zones = array.new<box>()
var array<box> liveZ = array.new<box>()
gradBand(float top, float bot, color c, int x1, int x2, bool denseTop) =>
float h = (top - bot) / 4
for k = 0 to 3
float bTop = top - h * k
float bBot = bTop - h
int alpha = denseTop ? 78 + k * 5 : 93 - k * 5
box b = box.new(x1, bTop, x2, bBot, border_color = color.new(c, 100), bgcolor = color.new(c, alpha), force_overlay = true)
array.push(zones, b)
array.push(liveZ, b)
if barstate.isconfirmed
if (ev.sigL or ev.sigS) and inZones and inPlan
array.clear(liveZ)
int d = t.dir
// risk: entry → stop (dense at the entry) · reward: entry → TP2 (dense at the entry)
gradBand(math.max(t.entry, t.sl), math.min(t.entry, t.sl), th.down, bar_index, bar_index + 1, d == 1)
gradBand(math.max(t.entry, t.tp2), math.min(t.entry, t.tp2), th.up, bar_index, bar_index + 1, d == -1)
while array.size(zones) > 96
box.delete(array.shift(zones))
else if (t.dir != 0 or ev.exit) and array.size(liveZ) > 0
for b in liveZ
box.set_right(b, bar_index)
// event labels on the chart
if ev.sigL
label.new(bar_index, low, "POP ▲ " + fmt(t.entry) + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2), style = label.style_label_up, color = color.new(th.up, 12), textcolor = th.bg, size = size.small, force_overlay = true,
tooltip = "Long pop: the stochastic EMA crossed above " + str.tostring(inUpper, "0") + " with price above a rising EMA 200 and within " + str.tostring(inMaxDist, "0.0") + " ATR of it.\nentry " + fmt(t.entry) + " · stop " + fmt(t.sl) + " (" + fmt(t.r) + " = 1R)\nTP1 " + fmt(t.tp1) + " → breakeven · TP2 " + fmt(t.tp2))
if ev.sigS
label.new(bar_index, high, "DROP ▼ " + fmt(t.entry) + "\nstop " + fmt(t.sl) + " · TP2 " + fmt(t.tp2), style = label.style_label_down, color = color.new(th.down, 12), textcolor = th.bg, size = size.small, force_overlay = true,
tooltip = "Short drop: the stochastic EMA crossed below " + str.tostring(inLower, "0") + " with price below a falling EMA 200 and within " + str.tostring(inMaxDist, "0.0") + " ATR of it.\nentry " + fmt(t.entry) + " · stop " + fmt(t.sl) + " (" + fmt(t.r) + " = 1R)\nTP1 " + fmt(t.tp1) + " → breakeven · TP2 " + fmt(t.tp2))
if ev.exit
string rTxt = (ev.r >= 0 ? "+" : "") + str.tostring(ev.r, "0.0") + "R"
string whyTxt = ev.why == "BAND" ? "momentum exit" : ev.why == "TP2" ? "target" : ev.why == "BE" ? "breakeven" : "stop"
label.new(bar_index, ev.dir == 1 ? high : low, (ev.dir == 1 ? "exit long · " : "exit short · ") + whyTxt + " " + rTxt, style = ev.dir == 1 ? label.style_label_down : label.style_label_up, color = color.new(th.bg, 15), textcolor = ev.r >= 0 ? th.up : th.down, size = size.tiny, force_overlay = true)
// ═══════════════════════════════════════════════════════════════════ WORDS (one source of truth)
string stateWord = t.dir == 1 ? "LONG OPEN" : t.dir == -1 ? "SHORT OPEN" : (side == 1 ? "FLAT · long side" : "FLAT · short side") + (sideways ? " · sideways" : "")
color stateCol = t.dir == 1 ? th.up : t.dir == -1 ? th.down : s1 ? (side == 1 ? th.up : th.down) : th.flat
float openR = t.dir != 0 ? (close - t.entry) * t.dir / t.r : na
string chipTxt = t.dir != 0 ? (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R now" + (t.tp1Hit ? " · stop at breakeven" : "") : allPre ? (s4 ? "set · on the pop side" : "set · waiting for the cross") : str.tostring(ready) + "/" + str.tostring(readyMax) + " steps true"
// ═══════════════════════════════════════════════════════════════════ DASHBOARD (force_overlay)
posOf(string p) =>
p == "Top Left" ? position.top_left : p == "Bottom Right" ? position.bottom_right : p == "Bottom Left" ? position.bottom_left : p == "Middle Right" ? position.middle_right : p == "Middle Left" ? position.middle_left : position.top_right
szLead() => inSize == "Small" ? size.small : inSize == "Large" ? size.large : size.normal
szBody() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.normal : size.small
szSub() => inSize == "Small" ? size.tiny : inSize == "Large" ? size.small : size.tiny
row3(table tb, int r, string k, bool ok, string why, bool showMark) =>
color stripe = r % 2 == 1 ? color.new(th.accent, 94) : th.bg
table.cell(tb, 0, r, k, text_color = th.flat, text_size = szBody(), text_halign = text.align_left, bgcolor = stripe)
table.cell(tb, 1, r, showMark ? (ok ? "✓" : "✗") : "", text_color = ok ? th.up : th.down, text_size = szBody(), text_halign = text.align_center, bgcolor = stripe)
table.cell(tb, 2, r, why, text_color = ok ? th.txt : th.txtHi, text_size = szBody(), text_halign = text.align_left, text_font_family = font.family_monospace, bgcolor = stripe)
section(table tb, int r, string title) =>
table.cell(tb, 0, r, title, text_color = th.accent, text_size = szSub(), text_halign = text.align_left, bgcolor = color.new(th.accent, 90))
table.cell(tb, 1, r, "", bgcolor = color.new(th.accent, 90))
table.cell(tb, 2, r, "", bgcolor = color.new(th.accent, 90))
table.merge_cells(tb, 0, r, 2, r)
var table dash = table.new(posOf(inDashPos), 3, 20, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_width = 0, force_overlay = true)
if barstate.islast and inDash
table.clear(dash, 0, 0, 2, 19)
int r = 0
// header
table.cell(dash, 0, r, "STOCHASTIC POP DESK", text_color = th.accentHi, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "EMA 200 trend · Stochastic POP (Method 2, EMA 4 on the line) · the four steps of the rule, in order. Everything commits on the closed bar.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
table.cell(dash, 2, r, syminfo.ticker + " · " + timeframe.period, text_color = th.accent, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg)
r += 1
// state + chip
table.cell(dash, 0, r, (t.dir == 1 ? "▲ " : t.dir == -1 ? "▼ " : "● ") + stateWord, text_color = stateCol, text_size = szLead(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "The open paper trade, or the side the EMA 200 allows right now.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
color chipBg = t.dir != 0 ? color.new(openR >= 0 ? th.up : th.down, 75) : color.new(color.from_gradient(ready, 0, readyMax, th.down, th.up), 75)
table.cell(dash, 2, r, chipTxt, text_color = th.txtHi, text_size = szBody(), text_halign = text.align_right, bgcolor = chipBg, tooltip = t.dir != 0 ? "Open R of the paper trade at this close." : "How many of the listed steps are true right now. 'Set' = everything but the cross itself; the cross is the trigger and must be fresh.")
r += 1
if t.dir == 0
// readiness — the rule in order
section(dash, r, "READINESS FOR A " + str.upper(sideWord) + " · " + str.tostring(ready) + " OF " + str.tostring(readyMax) + " STEPS")
r += 1
row3(dash, r, "1 · Trend", s1, s1Txt, true)
r += 1
row3(dash, r, "2 · Not stretched", s2, s2Txt, true)
r += 1
row3(dash, r, "3 · Fresh pop", s3, s3Txt, true)
r += 1
row3(dash, r, "4 · Cross " + str.tostring(side == 1 ? inUpper : inLower, "0"), s4, s4Txt, true)
r += 1
if gateOn
row3(dash, r, "5 · Agreements", s5, s5Txt, true)
r += 1
if inHtfOn
row3(dash, r, "HTF " + inHtf, s6, htfOk ? (htfClose > htfEma ? "price above the HTF EMA" : "price below the HTF EMA") : "HTF must be higher than the chart", true)
r += 1
if inSessOn
row3(dash, r, "Session", s7, s7 ? "inside " + inSess1 + " / " + inSess2 : "outside the sessions (" + inSessTz + ")", true)
r += 1
row3(dash, r, "Missing", allPre, missing, false)
r += 1
else
section(dash, r, "TRADE · " + (t.dir == 1 ? "LONG" : "SHORT") + " · " + str.tostring(bar_index - t.bar) + " BARS")
r += 1
row3(dash, r, "Entry", true, fmt(t.entry) + " · 1R = " + fmt(t.r), false)
r += 1
row3(dash, r, "Stop", true, fmt(t.sl) + (inTrailStop != "Off" and t.sl != t.sl0 and t.sl != t.entry ? " · following the trail" : t.tp1Hit ? " · breakeven" : inStopMode == "ATR" ? " · ATR stop" : " · swing"), false)
r += 1
row3(dash, r, "Targets", t.tp1Hit, "TP1 " + fmt(t.tp1) + (t.tp1Hit ? " ✓" : "") + " · TP2 " + fmt(t.tp2), false)
r += 1
row3(dash, r, "Exit rule", true, (inExitMode == "Stop / targets only" ? "stop or TP2" : (t.dir == 1 ? "line back below " + str.tostring(inUpper, "0") : "line back above " + str.tostring(inLower, "0")) + (inExitMode == "Band cross back only" ? "" : " · or TP2 / stop")) + (inTrail ? " · trail " + str.tostring(inTrailAtr, "0.0") + " ATR" : "") + (inTrailStop != "Off" ? " · stop = trend trail" + (inTrailStop == "After TP1" ? " after TP1" : "") : "") + (inOppExit ? " · flip" : "") + (inTimeStop > 0 ? " · time " + str.tostring(inTimeStop) : ""), false)
r += 1
row3(dash, r, "Stochastic", s4, str.tostring(kE, "0.0") + (s4 ? " · momentum intact" : " · momentum fading"), true)
r += 1
if inShowCtx
section(dash, r, "CONTEXT · " + str.tostring(side == 1 ? agCountL : agCountS) + " OF " + str.tostring(agTotal) + " AGREE" + (inGate == "Trend + POP only" ? "" : " · GATE"))
r += 1
if inUseStack
row3(dash, r, "MA stack", side == 1 ? agStackL : agStackS, str.tostring(stackUp) + "/5 EMAs below price (5·10·20·100·200)", true)
r += 1
if inUse821
row3(dash, r, "EMA 8 / 21", side == 1 ? ag821L : ag821S, ema8 > ema21 ? "8 above 21" : "8 below 21", true)
r += 1
if inUseRsi
row3(dash, r, "RSI " + str.tostring(inRsiLen), side == 1 ? agRsiL : agRsiS, str.tostring(rsi, "0.0") + (rsi > 50 ? " · above 50" : " · below 50"), true)
r += 1
if inUseVol
row3(dash, r, "Volume", agVol, volMa > 0 ? str.tostring(nz(volume, 0.0) / volMa, "0.0") + "× its " + str.tostring(inVolLen) + "-bar average" : "n/a", true)
r += 1
section(dash, r, "RECORD · PAPER, NO COSTS")
r += 1
row3(dash, r, "Closed", st.n >= 5, st.n < 5 ? str.tostring(st.n) + " trades · too few to read" : str.tostring(st.n) + " · " + str.tostring(math.round(st.wins / st.n * 100)) + "% win" + (st.flat > 0 ? " · " + str.tostring(st.flat) + " flat" : "") + " · avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R", false)
r += 1
row3(dash, r, "How they ended", true, "band " + str.tostring(st.nBand) + " · stop " + str.tostring(st.nStop) + " · TP2 " + str.tostring(st.nTp) + (na(t.lastR) ? "" : " · last " + (t.lastR >= 0 ? "+" : "") + str.tostring(t.lastR, "0.0") + "R"), false)
r += 1
table.cell(dash, 0, r, "EMA " + str.tostring(inEmaLen) + " · stoch " + str.tostring(inStLen) + "/" + str.tostring(inStSmooth) + " · EMA " + str.tostring(inStEma) + " · " + str.tostring(inUpper, "0") + "/" + str.tostring(inLower, "0"), text_color = th.flat, text_size = szSub(), text_halign = text.align_left, bgcolor = th.bg, tooltip = "Your settings, as the thread listed them.")
table.cell(dash, 1, r, "", bgcolor = th.bg)
string sw = (inRangeBlock ? "range-skip · " : "") + (inTrail ? "trail · " : "") + (inTrailStop != "Off" ? "trail-stop · " : "") + (inSessOn ? "session · " : "") + (inTimeStop > 0 ? "time · " : "") + (inOppExit ? "flip · " : "") + (inHtfOn ? "HTF " + inHtf + " · " : "")
table.cell(dash, 2, r, (sw == "" ? "no switches · " : sw) + (inGate == "Trend + POP only" ? "classic rule" : inGate == "Trend + POP + all agreements" ? "strict gate" : "majority gate"), text_color = th.flat, text_size = szSub(), text_halign = text.align_right, bgcolor = th.bg, tooltip = "Active switches and the signal gate mode. ATR " + fmt(atr) + " is the unit for stops and the stretch band.")
// ═══════════════════════════════════════════════════════════════════ NARRATIVE · Co-Pilot (translates state; never a new signal)
wrap(string sIn, int width) =>
array<string> words = str.split(sIn, " ")
string out = ""
string line = ""
for wd in words
if line != "" and str.length(line) + str.length(wd) + 1 > width
out := out + (out == "" ? "" : "\n") + line
line := wd
else
line := line == "" ? wd : line + " " + wd
out == "" ? line : out + "\n" + line
narrNow() =>
string o = ""
if t.dir != 0
o := "in a " + (t.dir == 1 ? "long" : "short") + " from " + fmt(t.entry) + " for " + str.tostring(bar_index - t.bar) + " bars, " + (openR >= 0 ? "+" : "") + str.tostring(openR, "0.0") + "R at this close — stop " + fmt(t.sl) + (t.tp1Hit ? " (moved to breakeven after TP1)" : "") + ", the stochastic line is at " + str.tostring(kE, "0.0") + (s4 ? " so momentum is still on the trade's side" : " — momentum is fading toward the band")
else
o := "flat — " + (s1 ? "the EMA 200 allows the " + sideWord + " side (" + s1Txt + ")" : "no clean side yet (" + s1Txt + ")") + (sideways ? "; the last " + str.tostring(inRangeLen) + " bars are a range (" + erTxt + ")" + (rangeBlock ? ", so signals are skipped" : " — pops inside a range are the weakest kind") : "") + "; the stochastic line is at " + str.tostring(kE, "0.0") + (s4 ? ", already on the pop side, " : ", ") + (allPre ? (s4 ? "every condition is true — the next fresh cross fires" : "every condition is true — the cross is the only thing left") : "and what is still false: " + missing)
o
narrWhy() =>
"Bernstein's Stochastic Pop, made mechanical: the side comes from a rising / falling EMA " + str.tostring(inEmaLen) + ", the entry from the EMA-" + str.tostring(inStEma) + " stochastic crossing " + str.tostring(inUpper, "0") + " / " + str.tostring(inLower, "0") + ", no pops that start more than " + str.tostring(inMaxDist, "0.0") + " ATR from the EMA, fresh pops only. The four dashboard steps are that rule in order."
narrPlan() =>
string o = ""
if t.dir != 0
o := "Stop first on every closed bar. " + (inExitMode == "Stop / targets only" ? "" : "If the line crosses back " + (t.dir == 1 ? "below " + str.tostring(inUpper, "0") : "above " + str.tostring(inLower, "0")) + ", the momentum exit closes the trade at that close. ") + (inExitMode == "Band cross back only" ? "" : "TP1 " + fmt(t.tp1) + (inTrail ? " starts a " + str.tostring(inTrailAtr, "0.0") + " ATR trailing stop" : " moves the stop to breakeven") + "; TP2 " + fmt(t.tp2) + " is the final target. ") + (inOppExit ? "An opposite cross closes it. " : "") + (inTimeStop > 0 ? "If TP1 is not reached within " + str.tostring(inTimeStop) + " bars, it closes on time. " : "") + "The idea is wrong beyond " + fmt(t.sl0) + "."
else
o := "A " + sideWord + " fires when " + (s1 ? "" : "price gets back on the right side of a " + (side == 1 ? "rising" : "falling") + " EMA 200, ") + (s2 ? "" : "price comes back within " + str.tostring(inMaxDist, "0.0") + " ATR of the EMA, ") + (s3 ? "" : "the line dips " + (side == 1 ? "below " + str.tostring(inLower, "0") : "above " + str.tostring(inUpper, "0")) + " to re-arm, ") + (inHtfOn and not s6 ? "the " + inHtf + " chart gets on the same side of its EMA, " : "") + (inSessOn and not s7 ? "a session opens, " : "") + "the line crosses " + (side == 1 ? "above " + str.tostring(inUpper, "0") : "below " + str.tostring(inLower, "0")) + " on a closed bar. Then: entry at that close, stop at the " + (inStopMode == "ATR" ? str.tostring(inStopAtr, "0.0") + " ATR distance" : str.tostring(inSwingN) + "-bar swing " + (side == 1 ? "low" : "high") + " minus a buffer") + ", TP1 " + str.tostring(inTp1, "0.0") + "R to breakeven, TP2 " + str.tostring(inTp2, "0.0") + "R" + (inExitMode == "Stop / targets only" ? "." : ", exit early if the line crosses back through its band.")
o
narrTrack() =>
st.n < 5 ? "Too few closed trades on this chart to say anything yet." : str.tostring(st.n) + " paper trades closed here: " + str.tostring(math.round(st.wins / st.n * 100)) + "% won, avg " + (st.sumR / st.n >= 0 ? "+" : "") + str.tostring(st.sumR / st.n, "0.00") + "R; " + str.tostring(st.nBand) + " ended on the momentum exit, " + str.tostring(st.nStop) + " on the stop / breakeven, " + str.tostring(st.nTp) + " at TP2. No costs, small sample — a description of the past on THIS chart, not an edge."
narrPosOf(string p) =>
p == "Top Left" ? position.top_left : p == "Top Right" ? position.top_right : p == "Bottom Right" ? position.bottom_right : p == "Middle Left" ? position.middle_left : p == "Middle Right" ? position.middle_right : position.bottom_left
narrBodySz() => inNarrSize == "Small" ? size.small : inNarrSize == "Medium" ? size.normal : inNarrSize == "Large" ? size.large : szBody()
narrHeadSz() => inNarrSize == "Small" ? size.normal : inNarrSize == "Medium" ? size.large : inNarrSize == "Large" ? size.large : szLead()
var table narrT = na
if barstate.islast
if not na(narrT)
table.delete(narrT)
narrT := na
if inNarr != "Off"
array<string> labs = array.new<string>()
array<string> vals = array.new<string>()
array.push(labs, "NOW")
array.push(vals, wrap(narrNow() + " (decision support, not a trade instruction)", inNarrWrap))
bool deep = inNarr == "Standard" or inNarr == "Detailed"
if deep
array.push(labs, "WHY")
array.push(vals, wrap(narrWhy(), inNarrWrap))
array.push(labs, "PLAN")
array.push(vals, wrap(narrPlan(), inNarrWrap))
if inNarr == "Detailed"
array.push(labs, "TRACK REC")
array.push(vals, wrap(narrTrack(), inNarrWrap))
array.push(labs, "GLOSSARY")
array.push(vals, wrap("Pop = the stochastic line breaking above " + str.tostring(inUpper, "0") + " — momentum arriving. Drop = breaking below " + str.tostring(inLower, "0") + ". Fresh pop = the line has visited the other band since the last signal. Stretched = price too far from the EMA 200 to be a fair entry. Momentum exit = the line crossing back through its band.", inNarrWrap))
int nr = array.size(labs)
bool atBottom = inNarrPos == "Bottom Left" or inNarrPos == "Bottom Right"
int rows = nr + 2 + (atBottom ? 1 : 0)
narrT := table.new(narrPosOf(inNarrPos), 2, rows, bgcolor = th.bg, frame_color = th.frame, frame_width = 1, border_color = color.new(th.frame, 70), border_width = 1, force_overlay = true)
table.cell(narrT, 0, 0, "◆ CO-PILOT", text_color = th.accentHi, bgcolor = color.new(th.accent, 88), text_size = narrHeadSz(), text_halign = text.align_left)
table.cell(narrT, 1, 0, stateWord + " · " + str.upper(inNarr), text_color = th.accent, bgcolor = color.new(th.accent, 88), text_size = narrBodySz(), text_halign = text.align_right)
for i = 0 to nr - 1
table.cell(narrT, 0, i + 1, array.get(labs, i), text_color = th.accent, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 1, i + 1, array.get(vals, i), text_color = th.txt, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left, text_valign = text.align_top)
table.cell(narrT, 0, nr + 1, "", bgcolor = th.bg)
table.cell(narrT, 1, nr + 1, "Honest: levels are geometry, not promises. The record has no costs — measure before you trust it.", text_color = th.flat, bgcolor = th.bg, text_size = narrBodySz(), text_halign = text.align_left)
if atBottom
table.cell(narrT, 0, nr + 2, " ", bgcolor = th.bg)
table.cell(narrT, 1, nr + 2, " ", bgcolor = th.bg)
// ═══════════════════════════════════════════════════════════════════ STRATEGY BRIDGE (real orders mirror the paper ledger; the engine above is byte-identical to the indicator)
// ORDER MATTERS: cancel_all() cancels every pending order placed this bar — a close_all() issued before it would be cancelled too.
if ev.exit
strategy.cancel_all()
if strategy.position_size != 0
strategy.close_all(comment = ev.why)
if ev.sigL
strategy.entry("L", strategy.long, comment = "POP")
if ev.sigS
strategy.entry("S", strategy.short, comment = "DROP")
if t.dir != 0 and strategy.position_size != 0 and inExitMode != "Band cross back only"
strategy.exit("X", stop = t.sl, limit = t.tp2, comment_profit = "TP2", comment_loss = t.tp1Hit ? "BE" : "SL")
// SYNC guard: if the emulator holds a position the ledger does not know about, flatten at once and tag it.
if strategy.position_size != 0 and t.dir == 0 and not ev.exit and not ev.sigL and not ev.sigS
strategy.cancel_all()
strategy.close_all(comment = "SYNC")
// ═══════════════════════════════════════════════════════════════════ ALERTS (confirmed bars only by construction)
payload(string evName, string why) =>
string js = '{"src":"StochasticPopDesk","symbol":"' + syminfo.ticker + '","tf":"' + timeframe.period + '","event":"' + evName + '","side":"' + (t.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + '","entry":' + fmt(nz(t.entry, close)) + ',"sl":' + fmt(nz(t.sl, 0)) + ',"tp1":' + fmt(nz(t.tp1, 0)) + ',"tp2":' + fmt(nz(t.tp2, 0)) + ',"stoch":' + str.tostring(kE, "0.0") + ',"ema200":' + fmt(ema200) + ',"reason":"' + why + '"' + (na(ev.r) ? "" : ',"r":' + str.tostring(ev.r, "0.00")) + '}'
string tx = "Stochastic Pop Desk · " + syminfo.ticker + " " + timeframe.period + " · " + evName + " · " + (t.dir == 1 or ev.dir == 1 ? "LONG" : "SHORT") + " · entry " + fmt(nz(t.entry, close)) + " · SL " + fmt(nz(t.sl, 0)) + " · TP " + fmt(nz(t.tp1, 0)) + " / " + fmt(nz(t.tp2, 0)) + (why != "" ? " · " + why : "")
inAlertJson ? js : tx
alertcondition(ev.sigL, "POP long", "Stochastic Pop Desk: long pop — stochastic crossed above the pop band with price above a rising EMA 200")
alertcondition(ev.sigS, "DROP short", "Stochastic Pop Desk: short drop — stochastic crossed below the drop band with price below a falling EMA 200")
alertcondition(ev.tp1, "TP1 reached", "Stochastic Pop Desk: TP1 reached — stop to breakeven")
alertcondition(ev.exit and ev.why == "BAND", "Momentum exit", "Stochastic Pop Desk: the stochastic crossed back through its band — exit")
alertcondition(ev.exit and ev.why == "TP2", "TP2 reached", "Stochastic Pop Desk: final target reached")
alertcondition(ev.exit and (ev.why == "SL" or ev.why == "BE"), "Stopped", "Stochastic Pop Desk: stop hit")
alertcondition(t.dir == 0 and allPre and not s4, "Ready — waiting for the cross", "Stochastic Pop Desk: every condition but the cross is true — the next stochastic cross fires a signal")
alertcondition(trailDir != trailDir[1] and trailDir[1] != 0, "Trend flipped", "Stochastic Pop Desk: the trend trail flipped to the other side of price")
if ev.sigL
alert(payload("POP", ""), alert.freq_once_per_bar_close)
if ev.sigS
alert(payload("DROP", ""), alert.freq_once_per_bar_close)
if ev.tp1
alert(payload("TP1", ""), alert.freq_once_per_bar_close)
if ev.exit
alert(payload("EXIT", ev.why), alert.freq_once_per_bar_close)
No signal yet?
Allow enough loaded history. Check Missing, the fresh reset and cooldown. A line staying beyond its band is not a new cross.
Go deeper, one question at a time.
Full rules and settings, when you need them.
What qualifies as POP or DROP?
Long: close above EMA 200; EMA above its value ten candles ago; price within 3 ATR of EMA; a new smoothed cross from 55 or below to strictly above 55. ATR measures recent price movement, including gaps.
Fresh pop: after an accepted long, a closed reading strictly below 45 re-arms that side. Short signals mirror the rules: falling EMA, cross below 45, reset above 55. Both reset flags start ready when the script begins.
Wait at least five candles after either side’s previous signal. Entry also needs a closed candle, sufficient history and no open paper trade. It manages an older trade before checking entry; an exit and new entry can share a candle.
Why two chart panels?
Price chart: EMA, trail, tickets, levels and panels. Lower pane: stochastic 14 → SMA 5 → EMA 4. This smoothed line crosses the bands.
The extra SMA 5 drawn beside EMA 200 is a separate, display-only smoothing line. Its type and the EMA drawing offset do not change entry calculations.
How do the stop and targets work?
1R is initial entry-to-stop distance. Default long stop: the lowest low of the last ten candles, including entry, minus 0.2 ATR. It is a rolling extreme, not a confirmed pivot. ATR stop mode instead uses 2 ATR with no extra swing buffer.
- Stop firstA touch ends the paper trade, even if a target was touched too.
- TP2 nextThe final 2R target closes the trade.
- TP1 nextAt 1R, stop moves to entry. No partial profit.
The raised stop is not checked against an earlier wick on that same candle. New entries are created after management, so their entry candle’s earlier wick does not stop them immediately.
Stop / targets only is default. Band cross back only exits a long on a cross back below 55, a short above 45, and disables the stop/target ladder. Band cross back or TP2 keeps the ladder plus that band exit.
What does the colored trail do?
A chandelier-style line tracks price extremes with an ATR offset. It changes side after a qualifying close through the previous trail. Color shows whether EMA slope agrees; sideways can turn it grey. Drawing the trail is on by default; using it as a stop is off.
Two separate options: ATR trailing after TP1 follows the close at a set ATR distance. Stop follows the trend trail uses the chart trail after TP1 or from the first management candle after entry. It reads the previous candle’s trail state to tighten the stop. An opposing prior trail state can also close the trade at the current close, with reason TRAIL, after at least one held candle.
What does sideways mean?
Efficiency compares net progress with total close-to-close travel over twenty candles. The tool ranks that reading against the last two hundred values. Default: flag below the lowest-quarter threshold; release at or above the fortieth-percentile threshold.
Different entry and release thresholds reduce flicker. This is a relative price measurement, not proof of market structure. Blocking new entries while flagged is a separate switch, off by default.
What do the optional filters add?
Signal needs: Trend + POP only is default. Optional agreements can require at least half or all enabled checks. “MA stack” means price above or below four of five EMAs, not their ordering. “EMA 8/21 cross” checks their current ordering, not a new cross. RSI checks its side of 50; volume compares with its average.
Higher timeframe: compares the last closed higher candle’s close with its EMA. Choosing an equal or lower timeframe bypasses that gate. Sessions: restrict new entries to the configured timezone and windows.
Opposite exit: needs an opposite oscillator cross, not a full opposing setup. Time stop: exits after the selected wait only if TP1 has not been reached. These switches all start off.
How do I read the panel and Co-Pilot?
State: trend, sideways or an open trade. Trend / Not stretched / Fresh pop / Cross: the checklist. Missing: its unmet checks. Trade card: entry, current stop, targets, exit rule and momentum. Record: closed paper outcomes without costs.
The Cross tick means the line is currently beyond its band, not that it just crossed. READY excludes cooldown, so the next cross may still be blocked.
Brief Co-Pilot shows NOW. Standard adds WHY and PLAN. Detailed adds the record and glossary. Context rows are optional; showing them does not enable an entry gate.

What does each setting change?
All sixty-two inputs, in settings order. Indicator and twin share the rules; the twin initially hides risk zones, dashboard and Co-Pilot.
Trend and distance
The main EMA selects the side. Its extra smoothing line and drawing offset do not change signals.
- Main price-average length
- 200
- Price used by the main EMA
- Close
- Shift the EMA drawings left or right
- 0 candles
- Type of the extra EMA smoothing line
- SMA
- Length of the extra smoothing line
- 5
- Compare EMA with this many candles ago
- 10
- Maximum entry distance from EMA
- ATR × 3
Momentum and a fresh trigger
Stochastic → SMA → EMA. After a signal, that side needs a reset across the opposite band before another fresh cross can qualify.
- Price-range lookback for stochastic
- 14 candles
- First smoothing of the stochastic line
- SMA 5
- Final smoothing before the band check
- EMA 4
- Long trigger: cross above this band
- 55
- Short trigger: cross below this band
- 45
- Require a reset between same-side signals
- On
- Minimum candles between accepted signals
- 5
Optional agreement checks
Context by default. Signal needs can require at least half or all enabled checks; hiding context rows does not change their calculations.
- Compare price with five moving averages
- On
- Compare EMA 8 with EMA 21
- On
- Check which side of 50 RSI occupies
- On
- Candles used by RSI
- 9
- Compare volume with its average
- On
- Candles used by the volume average
- 20
- Candles used to measure typical movement
- 10
- Whether agreements must pass for entry
- Trend + POP only
Entry, stop and targets
R is initial entry-to-stop distance. TP1 moves stop to entry without partial profit. Band cross back only turns off the stop/target ladder.
- Draw the paper entry, stop and targets
- On
- Choose the initial stop anchor
- Swing low / high
- Lookback for the rolling high or low
- 10 candles
- Stop distance when ATR mode is selected
- ATR × 2
- Extra space beyond the rolling high or low
- ATR × 0.2
- Milestone that moves stop to entry
- 1R
- Final target that closes the paper trade
- 2R
- Choose ladder, band exit or both
- Stop / targets only
Optional filters and exits
All seven switches start off. ATR trail and chart-trail stop are different options. Sessions filter new entries, not session-end exits.
- After TP1, let stop follow the close
- Off
- Distance behind the close after TP1
- ATR × 2
- Use the chart trail to manage stop
- Off
- Allow entries only during session windows
- Off
- Timezone used by session windows
- America/New_York
- First allowed session window
- 02:00–05:00
- Second allowed session window
- 08:30–11:00
- Maximum waiting time for TP1
- 0 candles · off
- Exit on an opposite oscillator cross
- Off
- Require higher-timeframe price agreement
- Off
- Timeframe used for that agreement
- 240 minutes
- Skip entries while sideways is flagged
- Off
- Candles used by the sideways detector
- 20
- Sideways threshold in the recent ranking
- 25 · lowest quarter
Chart appearance
Aurora Neon, Royal Gold or Colorblind-safe. The chart trail is visible by default; using it as a stop is separate. Its distance affects that optional stop too.
- Choose the color theme
- Aurora Neon
- Show the extra EMA smoothing line
- On
- Show allowed distance around EMA
- Off
- Show the chart trail and fill
- On
- Distance used by the chart trail
- ATR × 3
- Shade initial risk and reward areas
- On · twin: Off
- Highlight candles during an open paper trade
- On
- Shade the oscillator’s pop or drop phase
- Off
- Color candles by allowed trend side
- Off
Dashboard
The checklist explains readiness; the trade card explains an open plan. The record describes paper outcomes without costs.
- Show the dashboard
- On · twin: Off
- Choose dashboard position
- Top Right
- Choose dashboard text size
- Medium
- Show optional agreement rows
- Off
Co-Pilot
Brief shows NOW. Standard adds WHY and PLAN. Detailed adds the record and glossary. Off hides the explanation.
- Choose the amount of explanation
- Brief · twin: Off
- Choose explanation position
- Bottom Left
- Choose explanation text size
- Auto
- Wrap text after this line width
- 64 characters
Alerts
Dynamic messages cover POP, DROP, TP1 and EXIT. Choose JSON or readable text; separate Ready and Trend flipped conditions are not included.
- Send dynamic messages as JSON
- On
Which events can send an alert?
The indicator has eight named conditions: POP long, DROP short, TP1, Momentum exit, TP2, Stopped, Ready and Trend flipped. Stopped covers initial/breakeven stops; a TRAIL exit is separate in the dynamic exit reason.
Any alert() function call sends POP, DROP, TP1 and EXIT as JSON or readable text. It does not send Ready or Trend flipped. The payload includes entry, sl, tp1, tp2, stoch, ema200, reason and optional r.
Committed entries and trade events use closed candles. Ready and Trend flipped conditions are not explicitly close-gated; choose Once Per Bar Close if you want their closed readings. Recreate alerts after changing settings.
How do I test it on my own chart?
- Keep defaults and follow the checklist across waiting and entry examples.
- Include stopped trades. Read the exit reason, not just the entry label.
- Load the twin on a second chart; set realistic size, costs and slippage.
- Change one setting, then check a separate period.
Twin code defaults: capital 100,000, quantity 1, commission 0.02 percent per order and five ticks of slippage. The fill emulator can differ from the indicator’s paper ledger.

What did our measurements show?
We compared exits and switches on three charts with costs on. The band exit cut trades short; band-only and time-stop variants hurt that sample. The stop and target ladder stayed default. Higher-timeframe agreement helped each chart tried, but remains optional.
One window per chart, small in-sample sets. Fast-chart results were around break-even. The lab notes keep all numbers beside their limits; they do not establish future outcomes.
Read the lab notesWhat can change the reading?
A live candle can change before close. Different history, feeds, timeframes or inputs can recalculate results. EMA and smoothing respond with a delay; higher-timeframe agreement uses the last closed higher candle.
The record has no costs. The tool does not predict prices or account for news, fills or trade size. Trading risk is yours; this is research tooling, not financial advice.
Every change, dated
What shipped and what changed in Stochastic Pop Desk, newest first. Ask for the next change below; what gets built lands here with a date, and on the lab queue on the front page.
Stochastic Pop Desk v1.5.0 Shipped
Checks momentum, trend and timing before an entry.
Three scripts, one checklist Request Stochastic Pop Desk
The ask it was built from: an EMA 200, the Stochastic Pop with an EMA on the line, and an analysis table, combined.
- Received
- Building
- Done
“An EMA 200 with its smoothing line, the Stochastic Pop with an EMA 4 on the line, and an analysis table. Can these be one script that says what is missing?”
Measured on three charts before it shipped Lab note Stochastic Pop Desk
Run with costs on across three charts. The textbook exit closed trades early; the plain ladder became the default. The fast chart stayed marginal.
Something to change in Stochastic Pop Desk?
A small friction or a big idea. Tell us what would make this tool work better for you.
Requests for a new tool go on the same queue: see what is asked for and what is being built.