Indicator · Pine Script v5 · On the chart

Copilot

by milocaetano

Copilot is an order flow range-reversal indicator designed to overlay trading setups directly on the price chart.

View on GitHub

GitHub stars48on the repository
Pine versionv5runs as is
Lines of code233
Settings19inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 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.

Details

Published by

milocaetano on GitHub

Type

Indicator, Pine Script v5

Draws

On the price chart

Category

SMC / ICT / price action, Support & resistance, Trend, Volume

Markets

Any symbol

Timeframes

M1-M5, M15-H1

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
1 Context: window (bars)int80regime_len
1 Context: max height (×ATR)float6.0regime_k
1 Context: bars to confirm a flipint3regime_confirm
2 Structure: bars left of a pivotint5pivot_l
2 Structure: bars to confirm a pivotint3pivot_r
2 Structure: retest tolerance (×ATR)float0.5tol_atr
2 Structure: touches min bars apartint5min_gap
2 Structure: extreme lookback beyond min (bars)int75gap_span
3 Proof: absorption min (×avg effort)float1.5er_thr
3 Proof: effort average window (bars)int100er_len
4 Trigger: aggression (sigmas)float2.0z_thr
4 Trigger: sigma window (bars)int100z_len
Show all 19 settings
SettingTypeDefaultVariable
4 Trigger: bars the setup stays armedint6trigger_validity
Display: near-misses (3 of 4)booltrueshow_near
Advanced: ATR lengthint14atr_len
Advanced: min bar body (×ATR)float0.1rng_floor
Display: context semaphorebooltrueshow_semaphore
Display: structure (DT/DB)booltrueshow_structure
Display: full signals + stopbooltrueshow_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.

Similar Pine Scripts

575 stars

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

Indicator v5 Support & resistanceTrend
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows with Screener

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

Indicator v4 TrendSupport & resistance
218 stars

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

Indicator v6 TrendSupport & resistance
Updated 3 months ago · None stated GitHub repository of Market Structure - BOS & CHoCH

Open Source Fractal

by cantolab
183 stars

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

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · MIT GitHub repository of Open Source Fractal

Moon Dev GEX Sweep & CVD

by moondevonyt
92 stars

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

Indicator v5 Support & resistanceSMC / ICT / price action
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Sweep & CVD

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers