1Copy the scriptPress Copy script above, or the copy button on the code below.
2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.
Source Code
233 lines of Pine Script v5 from crates/app/scripts/copilot.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.
copilot.pine
//@version=5
// Copilot — a "can I trade this?" companion for range-reversal order flow.
//
// HOW TO READ THE CHART (three layers, calmest to loudest):
//
// 1. The ribbon — small marks floating one ATR above the recent highs:
// teal dot -> context OK: sideways tape, reversals may be armed
// red cross -> stand down: the tape is trending, skip reversals
// No ribbon at all = the copilot is still warming up and holds no
// opinion yet. The ribbon never covers a candle.
// 2. The structure — a line joining the two touches plus a DT chip
// (double top) or DB chip (double bottom). A "touch" is a confirmed
// pivot that revisits the window's own extreme (the level the tape
// remembers), not merely the previous pivot. Structure draws even
// when the ribbon says stand down: it is context, not a signal.
// 3. The signals — on the bar that confirms the second touch the
// copilot scores four conditions: regime OK, CVD divergence at the
// retest, absorption at the touch, and an aggression spike against
// the extreme (which may arrive up to `trigger_validity` bars later
// — the copilot stays armed and fires when it lands).
// FULL triangle (SELL above / BUY below, solid): all four agree.
// NEAR-MISS triangle (translucent, with a note): exactly one
// condition failed — the note names it. Information, not advice:
// the last decision belongs to the trader holding the checklist.
// A thin line with a "stop" tag marks the touch extreme on full
// signals: the invalidation lives beyond that price.
//
// Measured on 18 recorded WIN sessions (2026-07-21..08-12): the full
// conjunction fires ~0-2 times per 18 days; near-misses ~2 per day. The
// near-miss layer is the working surface; the full triangle is the rare
// all-clear. A marker can appear while its bar is still forming and
// vanish if the close disproves it — live, but never repainted after
// the bar commits.
//
// Absorption is measured against the bar BODY (volume per point of
// |close - open|): a rejection wick with a tiny body is absorption
// shouting, and the old range-based measure punished exactly that.
//
// Inputs are numbered in reading order: 1 context, 2 structure, 3 proof,
// 4 trigger. Defaults are starting points — calibrate on recorded
// sessions, not on faith.
indicator("Copilot", overlay=true)
// 1 — context: how wide may the recent window get before it is a trend?
regime_len = input.int(80, "1 Context: window (bars)", minval=10, maxval=300)
regime_k = input.float(6.0, "1 Context: max height (×ATR)", minval=1.0, maxval=20.0, step=0.25)
regime_confirm = input.int(3, "1 Context: bars to confirm a flip", minval=1, maxval=20)
// 2 — structure: what counts as a retest of the extreme?
pivot_l = input.int(5, "2 Structure: bars left of a pivot", minval=1, maxval=20)
pivot_r = input.int(3, "2 Structure: bars to confirm a pivot", minval=1, maxval=20)
tol_atr = input.float(0.5, "2 Structure: retest tolerance (×ATR)", minval=0.05, maxval=2.0, step=0.05)
min_gap = input.int(5, "2 Structure: touches min bars apart", minval=1, maxval=50)
// The lookback is declared as a span BEYOND `min_gap`, not as an absolute
// "max bars apart": two independent sliders for min and max would let one
// drag push the window negative, and a coupled clamp in a generic dialog
// would be per-indicator knowledge smuggled into the renderer. A span is
// valid at every slider position by construction. (Was `max_gap`, default
// 80 absolute = 75 beyond the min.)
gap_span = input.int(75, "2 Structure: extreme lookback beyond min (bars)", minval=6, maxval=300)
// 3 — proof: the second touch must be absorbed, not merely revisited.
er_thr = input.float(1.5, "3 Proof: absorption min (×avg effort)", minval=1.0, maxval=5.0, step=0.1)
er_len = input.int(100, "3 Proof: effort average window (bars)", minval=10, maxval=300)
// 4 — trigger: how unusual must the aggression at the extreme be?
z_thr = input.float(2.0, "4 Trigger: aggression (sigmas)", minval=0.5, maxval=6.0, step=0.1)
z_len = input.int(100, "4 Trigger: sigma window (bars)", minval=10, maxval=300)
trigger_validity = input.int(6, "4 Trigger: bars the setup stays armed", minval=0, maxval=30)
// signals — the near-miss layer can be silenced. Titled "Display:" like
// the layer switches below, so the dialog groups all four together.
show_near = input.bool(true, "Display: near-misses (3 of 4)")
// advanced plumbing — rarely touched.
atr_len = input.int(14, "Advanced: ATR length", minval=2, maxval=100)
rng_floor = input.float(0.1, "Advanced: min bar body (×ATR)", minval=0.01, maxval=0.5, step=0.01)
// display — one switch per drawn layer, gating draw sites only (the
// scoring pipeline always runs, so flipping a layer back on never changes
// what the copilot concluded). Appended AFTER every older input: the state
// file stores values positionally, and an insertion mid-list would remap
// every saved value one slot over.
show_semaphore = input.bool(true, "Display: context semaphore")
show_structure = input.bool(true, "Display: structure (DT/DB)")
show_signals = input.bool(true, "Display: full signals + stop")
atr = ta.atr(atr_len)
// --- 1. context -----------------------------------------------------------
// Every stateful ta.* call lives at top level: the dialect's `and` is
// short-circuit and kernels have call-site identity, so a kernel on the
// right of `and` would only warm up on bars where the left side is true.
hi_w = ta.highest(high, regime_len)
lo_w = ta.lowest(low, regime_len)
trending = hi_w - lo_w > regime_k * atr
// The verdict flips only after `regime_confirm` bars agree — one opinion,
// held, instead of a light that flickers on every borderline bar.
trend_run = ta.barssince(not trending)
range_run = ta.barssince(trending)
var bool stand_down = false
if not stand_down and nz(trend_run, regime_confirm) >= regime_confirm
stand_down := true
if stand_down and nz(range_run, regime_confirm) >= regime_confirm
stand_down := false
ribbon_y = hi_w + atr
plotshape(not show_semaphore or stand_down ? na : ribbon_y, "Context OK", style=shape.circle, location=location.absolute, color=color.new(color.teal, 45))
plotshape(show_semaphore and stand_down ? ribbon_y : na, "Context: stand down", style=shape.cross, location=location.absolute, color=color.new(color.red, 25))
// --- shared effort series ---------------------------------------------------
// Sigmas of per-bar delta: "how unusual is this aggression?" relative to
// the recent tape, so the trigger survives calm and violent days alike.
z = (delta - ta.sma(delta, z_len)) / ta.stdev(delta, z_len)
z_low = ta.lowest(z, pivot_r + 1)
z_high = ta.highest(z, pivot_r + 1)
// Effort × result against the BODY: volume per point of |close - open|.
// The floor keeps a doji from dividing by zero; a rejection wick with a
// small body is exactly the absorption signature this must reward.
er = volume / math.max(math.abs(close - open), atr * rng_floor)
er_rel = er / ta.sma(er, er_len)
// --- 2. structure: a pivot that retests the window's extreme ----------------
// The prior extreme is the highest high (lowest low) of the window that
// ends `min_gap` bars before the touch and spans `gap_span` bars beyond —
// the level the tape remembers, not merely the previous pivot, so a minor
// wiggle between the touches cannot steal the comparison. Every series is
// read at the confirmation bar, `pivot_r` bars after the touch.
ph = ta.pivothigh(high, pivot_l, pivot_r)
top_now = not na(ph)
prior_top = ta.highest(high, gap_span)[pivot_r + min_gap]
prior_top_off = ta.highestbars(high, gap_span)[pivot_r + min_gap]
pl = ta.pivotlow(low, pivot_l, pivot_r)
bot_now = not na(pl)
prior_bot = ta.lowest(low, gap_span)[pivot_r + min_gap]
prior_bot_off = ta.lowestbars(low, gap_span)[pivot_r + min_gap]
// Bars from the current (confirmation) bar back to the prior extreme:
// the offsets from highestbars/lowestbars are negative within their
// window, which itself sits `pivot_r + min_gap` bars back.
top_prior_gap = pivot_r + min_gap - prior_top_off
bot_prior_gap = pivot_r + min_gap - prior_bot_off
double_top = top_now and not na(prior_top) and math.abs(ph - prior_top) <= tol_atr * atr
double_bottom = bot_now and not na(prior_bot) and math.abs(pl - prior_bot) <= tol_atr * atr
if show_structure and double_top
line.new(bar_index - top_prior_gap, prior_top, bar_index - pivot_r, ph, color=color.red, width=2)
label.new(bar_index - pivot_r, ph, text="DT", style=label.style_label_down, color=color.new(color.red, 20))
if show_structure and double_bottom
line.new(bar_index - bot_prior_gap, prior_bot, bar_index - pivot_r, pl, color=color.teal, width=2)
label.new(bar_index - pivot_r, pl, text="DB", style=label.style_label_up, color=color.new(color.teal, 20))
// --- 3..4. scoring, arming, and the two signal tiers -------------------------
// Every history read lives at top level for the same reason the kernels
// do: a `[n]` site only records on bars where it evaluates, so one hidden
// behind a short-circuit `and` would wake up empty on the bar it matters.
cvd_touch = cvd[pivot_r]
// nz() on the dynamic offsets: the interpreter skips recording a history
// site whose offset is na, so a site first read after warmup would wake
// up with an empty ring. Offset 0 reads the current bar AND records.
cvd_prior_top = cvd[nz(top_prior_gap, 0)]
cvd_prior_bot = cvd[nz(bot_prior_gap, 0)]
er_touch = er_rel[pivot_r]
top_diverges = double_top and cvd_touch < cvd_prior_top
bot_diverges = double_bottom and cvd_touch > cvd_prior_bot
top_absorbed = double_top and not na(er_touch) and er_touch >= er_thr
bot_absorbed = double_bottom and not na(er_touch) and er_touch >= er_thr
// A structure whose only missing piece is the aggression stays ARMED for
// `trigger_validity` bars: on the measured tape the spike often lands a
// few bars after confirmation, not inside it.
var int short_armed = 0
var float short_level = na
var int long_armed = 0
var float long_level = na
sell_hit = z_low <= -z_thr
buy_hit = z_high >= z_thr
// full signal on the confirmation bar itself…
short_setup_now = double_top and not stand_down and top_diverges and top_absorbed and sell_hit
long_setup_now = double_bottom and not stand_down and bot_diverges and bot_absorbed and buy_hit
// …or when the spike lands while a fully-proven structure is still armed.
short_setup_armed = short_armed > 0 and not stand_down and sell_hit
long_setup_armed = long_armed > 0 and not stand_down and buy_hit
short_setup = short_setup_now or short_setup_armed
long_setup = long_setup_now or long_setup_armed
// near-miss: exactly one of the four conditions failed at confirmation.
// An "aggression missing" near-miss is only declared once the armed
// window has expired without a spike — see the bookkeeping below.
top_score = (stand_down ? 0 : 1) + (top_diverges ? 1 : 0) + (top_absorbed ? 1 : 0) + (sell_hit ? 1 : 0)
bot_score = (stand_down ? 0 : 1) + (bot_diverges ? 1 : 0) + (bot_absorbed ? 1 : 0) + (buy_hit ? 1 : 0)
short_near = show_near and double_top and not short_setup_now and top_score == 3 and not (top_diverges and top_absorbed and not stand_down)
long_near = show_near and double_bottom and not long_setup_now and bot_score == 3 and not (bot_diverges and bot_absorbed and not stand_down)
// arming bookkeeping — after the reads above, so this bar's scores win.
short_expired = short_armed == 1 and not short_setup_armed
long_expired = long_armed == 1 and not long_setup_armed
if double_top and not stand_down and top_diverges and top_absorbed and not sell_hit
short_armed := trigger_validity
short_level := ph
else if short_armed > 0
short_armed := short_setup_armed ? 0 : short_armed - 1
if double_bottom and not stand_down and bot_diverges and bot_absorbed and not buy_hit
long_armed := trigger_validity
long_level := pl
else if long_armed > 0
long_armed := long_setup_armed ? 0 : long_armed - 1
// the near-miss note names the missing condition, on the chart.
if short_near
label.new(bar_index, high, text=stand_down ? "3/4 trending" : not top_diverges ? "3/4 no divergence" : "3/4 no absorption", style=label.style_none, textcolor=color.new(color.red, 45))
if long_near
label.new(bar_index, low, text=stand_down ? "3/4 trending" : not bot_diverges ? "3/4 no divergence" : "3/4 no absorption", style=label.style_none, textcolor=color.new(color.teal, 45))
if show_near and short_expired
label.new(bar_index, high, text="3/4 aggression never came", style=label.style_none, textcolor=color.new(color.red, 45))
if show_near and long_expired
label.new(bar_index, low, text="3/4 aggression never came", style=label.style_none, textcolor=color.new(color.teal, 45))
// stop line on full signals: the invalidation is the touch extreme.
stop_top = short_setup_now ? ph : short_level
stop_bot = long_setup_now ? pl : long_level
if show_signals and short_setup
line.new(bar_index - pivot_r, stop_top, bar_index, stop_top, color=color.red, width=1)
label.new(bar_index, stop_top, text="stop", style=label.style_none, textcolor=color.new(color.red, 20))
if show_signals and long_setup
line.new(bar_index - pivot_r, stop_bot, bar_index, stop_bot, color=color.teal, width=1)
label.new(bar_index, stop_bot, text="stop", style=label.style_none, textcolor=color.new(color.teal, 20))
plotshape(show_signals and short_setup, "Short setup", style=shape.triangledown, location=location.abovebar, color=color.red, text="SELL")
plotshape(show_signals and long_setup, "Long setup", style=shape.triangleup, location=location.belowbar, color=color.teal, text="BUY")
plotshape(short_near or (show_near and short_expired), "Short near-miss", style=shape.triangledown, location=location.abovebar, color=color.new(color.red, 65))
plotshape(long_near or (show_near and long_expired), "Long near-miss", style=shape.triangleup, location=location.belowbar, color=color.new(color.teal, 65))
Overview
Copilot evaluates four criteria: market context (sideways vs trending regime based on ATR height), structure (double top or double bottom retests), absorption (volume per bar body relative to average effort), and aggression spikes (volume standard deviations). The script plots a context ribbon, structure lines, full trade signals with invalidation stop levels, and optional near-miss signals with diagnostic tags. Settings customize context thresholds, pivot rules, absorption sensitivity, and trigger validity.
Context ribbon for market regime classification
Double top and double bottom pivot retest detection
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
1 Context: window (bars)
int
80
regime_len
1 Context: max height (×ATR)
float
6.0
regime_k
1 Context: bars to confirm a flip
int
3
regime_confirm
2 Structure: bars left of a pivot
int
5
pivot_l
2 Structure: bars to confirm a pivot
int
3
pivot_r
2 Structure: retest tolerance (×ATR)
float
0.5
tol_atr
2 Structure: touches min bars apart
int
5
min_gap
2 Structure: extreme lookback beyond min (bars)
int
75
gap_span
3 Proof: absorption min (×avg effort)
float
1.5
er_thr
3 Proof: effort average window (bars)
int
100
er_len
4 Trigger: aggression (sigmas)
float
2.0
z_thr
4 Trigger: sigma window (bars)
int
100
z_len
Show all 19 settings
Setting
Type
Default
Variable
4 Trigger: bars the setup stays armed
int
6
trigger_validity
Display: near-misses (3 of 4)
bool
true
show_near
Advanced: ATR length
int
14
atr_len
Advanced: min bar body (×ATR)
float
0.1
rng_floor
Display: context semaphore
bool
true
show_semaphore
Display: structure (DT/DB)
bool
true
show_structure
Display: full signals + stop
bool
true
show_signals
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
This indicator detects equal highs and lows by comparing prices within user-defined left and right bars using a tolerance percentage. It plots horizontal lines on the price chart for equal highs in one color and lows in another. A screener allows input of mult
This Pine Script indicator plots a ZigZag on the price chart. It identifies reversal points when price moves more than a set percentage from the last pivot. The script calculates forecasts using zigzag data and labels detected geometric patterns including harm
This indicator plots market structure on the price chart by identifying swing highs and lows. It connects these points with lines to visualize trend direction and detect Break of Structure (BOS) when price breaks a prior swing in the current trend. Change of C
This indicator implements the Fractal Model concept to plot market structure shifts, liquidity sweeps, and Change in State of Delivery (CISD) levels. It overlays custom higher-timeframe candle previews, open prices, and time markers directly on the price chart
This indicator plots two Hull moving averages on the price chart, one of the selected price and one of close, and optionally smooths both with a Kahlman filter. It fills the space between them and marks buy and sell labels when they cross, provided scaled volu
The indicator detects liquidity sweeps across key price levels and calculates a cumulative volume delta proxy. It uses pivot high and low lookbacks to identify swing points, marking sweeps when price breaks beyond a level and quickly closes back inside. Featur