New

11 tools. $557.98 if bought one by one. 4 new Premium desks inside.$557.98 one by one

$97 once$97See all 11See it
Free · Pine v6 · MIT · v0.6.1

Engulfing Volume Desk Free

It finds the candle that swallows the last one, grades its volume, and turns it into one frozen plan: entry, stop and a 2R target.

PanelFri Mar 20 · 10:00
STATE
Exit observed; entries closed
RVOL
Current · Low / 0.78
PLAN
Next: an engulfing with the trend · stop past both candles · 2R
One setup at a timeEngulfing Volume Desk 0.6.1

Window 1 · the first breakout fill

Simulated 4-hour gold candles, not a real market, run through Engulfing Volume Desk's own rules: the same code reproduces the script's TradingView trades on real gold, one for one. Quiet candles run as a time-lapse; the ones that matter slow down. Window 1: it starts at the first simulated setup whose breakout stop order fills, so every step shows; Another window: the chart plays on from where the last window stopped, whatever the setups do (after the twelfth, window 1 again). R is before costs.

Read as text
Times are candle opens on the simulated New York clock. The words are the script's own; R runs from the fill, before costs.
TimeEventPrices
Thu Mar 12 · 14:00Earlier: Long · Low volume · Stop +0.0REntry 4,379.05 · stop 4,321.15 · target 4,494.85
Mon Mar 16 · 18:00Earlier: Long · Medium volume · Stop −1.0REntry 4,343.30 · stop 4,313.73 · target 4,402.44
Tue Mar 17 · 06:00Window opensClose 4,317.55 · EMA 200 4,266.13
Tue Mar 17 · 10:00Long · Medium volume (RVOL 1.3): the engulfing closes, a stop order at 4,352.69Entry 4,352.69 · stop 4,308.41 · target 4,441.25
Tue Mar 17 · 14:00FilledFill 4,352.69
Tue Mar 17 · 18:00Bullish engulfing skipped: a setup is openClose 4,367.51
Wed Mar 18 · 02:00Bullish engulfing skipped: a setup is openClose 4,395.24
Wed Mar 18 · 06:00Long closed · Target +2.0RExit 4,441.25
Wed Mar 18 · 22:00Bearish engulfing skipped: against the trendClose 4,372.67
Thu Mar 19 · 02:00Long · Medium volume (RVOL 1.03): the engulfing closes, a stop order at 4,397.19Entry 4,397.19 · stop 4,366.65 · target 4,458.27
Thu Mar 19 · 06:00FilledFill 4,397.19
Thu Mar 19 · 10:00Bullish engulfing skipped: a setup is openClose 4,404.32
Thu Mar 19 · 14:00Bearish engulfing skipped: against the trendClose 4,389.52
Thu Mar 19 · 22:00Long closed · Stop −1.0RExit 4,366.65
Fri Mar 20 · 06:00Bearish engulfing skipped: against the trendClose 4,349.12

No signup · a Pine v6 strategy · open source. It draws the plan; your Strategy Tester measures it.

Body over body. Trend first.

At every close the desk reads four checks, and an engulfing becomes a plan only when all four hold.

TAKEN

All four checks hold: a long plan at 4,397.19, frozen.

  • Check 1, opposite colour: true, red, then green
  • Check 2, covers the last body: true, covers both ends
  • Check 3, a bigger body: true, 22.01 vs 21.44
  • Check 4, with the trend: true, above a rising EMA

The same simulated 4-hour gold. This film is from the replay's first setup on, the first simulated window where the desk skips an engulfing against the trend and then takes one with it. Dashed lines mark the last candle's body.

Check 1

Opposite colour

Red, then green for a long; green, then red for a short. A doji never counts.

Check 2

Covers the body

The new body reaches both ends of the last one.

Check 3

Bigger body

Strictly bigger: an equal body is not an engulfing.

Check 4

With the trend

Longs above a rising EMA 200, shorts below a falling one, on closed candles.

Volume labels. It never decides.

Drag the close and the volume: the checks make the plan, and the volume only names it Low, Medium or High.

Long · Medium volume Aggressive stop armed: stop order at 4,326.91, stop 4,316.89, target 4,346.95.
4,325.50
1.20

A fixed simulated example, the same candles each time. The verdict is the script's own checks, run on what you set.

Low

Below 1.0

Less volume than the average of the last twenty candles.

Medium

1.0 to 1.5

Up to half as much again as that average.

High

1.5 and up

At least half as much again as the average.

A label

Not a chance

In our gold test it described the candle; it did not tell good setups from bad.

Target. Stop. Or no fill.

The plan is frozen at the close, and the bracket ends it at the target or the stop; an order that never fills is cancelled.

TARGET +2.0R

Target +2.0R: the bracket closed it at 4,441.25.

  • Step 1, the engulfing: true, bullish, Medium volume
  • Step 2, the order: true, stop order at 4,352.69
  • Step 3, the fill: true, filled 4,352.69
  • Step 4, the exit: true, Target +2.0R

Four setups from the same simulated gold: from the replay's first setup on, the first simulated setup to end at its target, the first at its stop, the first whose stop was touched before the entry, and the first that expired. R runs from the fill to the exit, before costs.

Entry

One tick past

A stop order one tick beyond the last candle; the next open when the close is already through.

Stop

Both candles

One tick beyond the lower low of the two candles (the higher high for a short).

Target

Two risks away

Twice the planned risk from the planned entry, frozen with it.

No fill

Five candles

Cancelled after five candles, or as soon as a close shows the stop was touched first.

Paste it. Test it.

One file, the complete v0.6.1 strategy, ready for TradingView's Pine Editor.

On a phone? Send this page to your desktop.

  1. Copy the strategyThe button copies the whole file.
  2. Paste it in the Pine EditorOpen a new strategy, replace the sample, save.
  3. Add it, open the Strategy TesterSet your broker's commission and slippage first.

MIT licence

engulfing-volume-desk.pine · 458 linesDownload engulfing-volume-desk.pine
//@version=6// © 2026 ProEA Lab · Engulfing Volume Desk v0.6.1 · MIT License (see LICENSE in the download).// Measured on OANDA:XAUUSD 4H with these defaults: 2025-02..2026-09 made money after costs; the untouched// 2023-03..2024-12 test lost. That is a research result, not a forecast. Read the lab notes before trading it.// Volume tiers are a rubric, not calibrated win probabilities.// Decisions occur at close; orders are live from the next tick. No signal-bar fills.// Cost defaults are an assumed XAUUSD research scenario, not a broker quote.// Standard preserves the C2 baseline. Adaptive ER is a research hypothesis, not a proven edge.strategy("Engulfing Volume Desk · v0.6.1", overlay = true,     initial_capital = 10000, currency = currency.USD, pyramiding = 2, close_entries_rule = "ANY",     commission_type = strategy.commission.cash_per_contract, commission_value = 0.30,     slippage = 100, backtest_fill_limits_assumption = 1, margin_long = 100, margin_short = 100,     calc_on_every_tick = false, calc_on_order_fills = false, process_orders_on_close = false,     max_labels_count = 100) string mode = input.string("Aggressive", "Entry mode", options = ["Aggressive", "Retest", "Both"], group = "Setup")string pattern = input.string("Body", "Engulfing definition", options = ["Body", "Body + full range"], group = "Setup")string directions = input.string("Both", "Direction", options = ["Both", "Long", "Short"], group = "Setup")int volumeLength = input.int(20, "Prior volume bars", minval = 2, maxval = 500, group = "Volume")float mediumAt = input.float(1.0, "Medium from RVOL", minval = 0.01, step = 0.1, group = "Volume")float highAt = input.float(1.5, "High from RVOL", minval = 0.02, step = 0.1, group = "Volume")string minimumTier = input.string("Low", "Minimum confirmation", options = ["Low", "Medium", "High"], group = "Volume",     tooltip = "Volume strength only. No measured win probability. Missing or zero volume blocks setups.")bool useTrend = input.bool(true, "EMA trend filter", group = "Context",     tooltip = "Long: close above EMA 200 and EMA higher than 5 bars ago. Short mirrors it. Uses confirmed chart bars only.")string exposurePolicy = input.string("Standard", "Exposure policy", options = ["Standard", "Fixed half", "Adaptive ER"], group = "Context",     tooltip = "Research comparison: Standard = 1x; Fixed half = 0.5x; Adaptive ER = 0.5x when 20-bar price-path efficiency is below 0.30, otherwise 1x. Scales risk and notional caps, frozen at setup admission. Does not resize open trades.")bool useWindow = input.bool(false, "Use research date window", group = "Research")int startTime = input.time(timestamp("06 Jul 2026 00:00 +0000"), "Start (inclusive signal close)", group = "Research")int endTime = input.time(timestamp("01 Sep 2026 00:00 +0000"), "End (exclusive signal close)", group = "Research",     tooltip = "At the first close at/after End, pending entries are cancelled and positions receive a next-tick market close. Terminal settlement can be after End.")int breakTicks = input.int(1, "Breakout buffer (ticks)", minval = 1, group = "Orders")float retestDepth = input.float(50, "Retest depth into prior range (%)", minval = 0, maxval = 100, step = 5, group = "Orders",     tooltip = "0 = broken edge; 50 = midpoint; 100 = opposite edge. Armed only after a confirmed breakout close.")int expiryBars = input.int(5, "Pending lifetime (post-signal bars)", minval = 1, maxval = 100, group = "Orders")int stopTicks = input.int(1, "Stop buffer (ticks)", minval = 1, group = "Risk")float targetR = input.float(2, "Target (planned R)", minval = 0.1, step = 0.1, group = "Risk")float riskPercent = input.float(0.5, "Planned setup risk (% equity)", minval = 0.01, maxval = 5, step = 0.05, group = "Risk",     tooltip = "Both splits this budget 50/50. Gaps, slippage and fees can increase actual loss. Quantity also has a 95% equity notional cap.")bool showLevels = input.bool(true, "Show active plan", group = "Display")bool showSignals = input.bool(true, "Show admitted setups", group = "Display")bool showPanel = input.bool(true, "Show status", group = "Display")string decisionPolicy = input.string("Legacy", "Decision policy", options = ["Legacy", "Context Aggressive", "Context Retest", "Context Router", "Context Router + exit"], group = "Research controller",     tooltip = "Research only. Context requires directional EMA200/slope5 and ER20 >= 0.30. Router uses Aggressive at extension <= 0.50 structural risk, otherwise Retest. + exit requests a next-tick close after 24h from the first entry-bar open or two confirmed closes losing EMA context. Legacy keeps Entry mode. No self-learning or proven profit.")string executionPlan = input.string("Legacy", "Execution plan", options = ["Legacy", "Shared objective", "Shared objective + cutoff", "Cost-aware intraday"], group = "Research execution",     tooltip = "Research only. Shared objective keeps the original Aggressive target for either entry. Cutoff freezes the next New York 17:00 rollover less two chart durations; admission must precede it by another chart duration. Cost-aware also requires a conservative net reward/risk screen >= 1.50. No guarantee of pre-rollover fills or profit.")float screenCommission = input.float(1.20, "Screen commission (USD / quantity round trip)", minval = 0, step = 0.10, group = "Research execution")int screenSlippage = input.int(400, "Screen slippage reserve (ticks / round trip)", minval = 0, group = "Research execution",     tooltip = "Conservative common screening reserve for both routes, independent of actual strategy cost settings. Not an exact limit-order cost or funding estimate.") // BEGIN PURE HELPERS: extracted verbatim by the deterministic native probe.f_direction(float o, float h, float l, float c, float po, float ph, float pl, float pc, bool fullRange) =>    bool complete = not na(o) and not na(h) and not na(l) and not na(c) and not na(po) and not na(ph) and not na(pl) and not na(pc)    bool valid = complete and h >= math.max(o, c) and l <= math.min(o, c) and ph >= math.max(po, pc) and pl <= math.min(po, pc)    bool larger = math.abs(c - o) > math.abs(pc - po)    bool outside = not fullRange or (h >= ph and l <= pl)    bool bull = valid and larger and outside and pc < po and c > o and o <= pc and c >= po    bool bear = valid and larger and outside and pc > po and c < o and o >= pc and c <= po    bull ? 1 : bear ? -1 : 0 

No coding needed.

View the full code

01 · Free

No signup

The full source under MIT; nothing is locked.

02 · Pine v6

A strategy

It draws the plan and sends its orders to the Strategy Tester.

03 · Your chart

Any market

Any symbol with real volume; we measured gold's 4-hour chart.

04 · Research

Switches off

Retest entry, both legs and a cost screen are inputs, off by default.

All eleven. Once.

Eleven things we make, one payment, and nothing that renews.

Complete Suite Everything on the list, one payment

$97, one payment.

Renews never Full source 7-day refund before download

The Suite adds no Engulfing Volume features; this desk stays free.

What's insideOn its own
  1. Drift Desk PremiumThe bigger trend, the pullback, size first.$49.00
  2. Opening Range Desk ProOpening ranges graded A to C, markets in play first.$49.00
  3. Signal Desk PremiumA 20-market Radar, sized tickets, a daily lock.$49.00
  4. Breakaway Desk PremiumA Trend Radar for six crypto perpetuals on 4-hour candles.$49.00
  5. Gold Sweep Desk ProGold's session pools, the plan before the window.$49.00
  6. Pine Complete Bundle11 Pine v6 tools, every source included.$46.00
  7. Pine Evolution LabA local strategy factory that kills weak ideas.$79.00
  8. MTR FullSystemEAA complete MT5 EA: all 21 source files.$49.00
  9. TradeLensA self-hosted journal whose AI coach prices your leaks.$47.00
  10. The WatchA self-hosted paper-trading board.$59.00
  11. AI Starter PacksPrompt Vault, Fixer, Builder Lite, Chart Reader.$32.98
All eleven, one by one$557.98
The Suite$97

Straight answers.

Is it really free?

Yes. The full source under MIT, no signup, nothing locked.

Does it make money?

Not reliably. On gold's 4-hour chart, after costs and overnight financing, it finished 2025–26 ahead and lost on 2023–24, the history we had held back. Both runs are in the lab notes.

Does High volume mean a stronger setup?

Not in our test. The grade describes the engulfing candle; it did not tell good setups from bad ones. It is printed on every order, so you can sort your own record.

Does it repaint?

No. Every check reads a closed candle, the order is live from the next tick, and the plan is frozen once it is made.

What does the Suite add?

No Engulfing Volume features. It holds the other things we make, for one payment; this desk stays free either way.

We, ProEA Lab, build Engulfing Volume Desk. The films run its rules on simulated candles, not market data; R is before costs. Not financial advice.

Full manual.

Every rule, setting and panel row, one topic at a time.

Open the full manualClose the full manual

00 What makes an engulfing?

Two candles of opposite colour, where the second one's body reaches both ends of the first one's body and is strictly bigger. The desk reads it only once the candle has closed, and only with the trend: longs above a rising EMA 200, shorts below a falling one. Grow the body and watch the checks.

✓ covers both ends✓ bigger body✓ opposite colour▲ ENGULFING

01 Where does the order go?

The plan is made at the engulfing's close and never moves: a stop order one tick past the last candle's high (its low for a short), a stop one tick beyond both candles, and a target twice the planned risk away. If the close is already past that high, the order is a market order at the next open, measured from the close.

target 2Rentrystoplast high

02 Read your chart.

Engulfing Volume Desk on the OANDA gold 4-hour chart: short and long engulfing setups labelled with their volume grade, each with its frozen stop, entry and target lines, exits marked Target or Stop, a short position open with its plan drawn, and the status panel.
OANDA:XAUUSD 4h on TradingView: each setup's order carries its grade (“Short · Low volume”), and its exit reads Target or Stop.
  1. The triangles: a setup the rules admitted, under the candle for a long and over it for a short.
  2. The lines: the planned entry in blue, the frozen stop in red, the target in the setup's colour, and the last candle's high and low faint, while the setup lives.
  3. The orders: TradingView prints each fill with its comment (“Long · High volume”) and each exit as Target or Stop.
State
Waiting for engulfing · Aggressive stop armed · Aggressive next-tick market · Position active · Exit observed; entries closed · Stop touched; entries closed · Pending orders expired.
Current / Setup RVOL
The grade and the ratio of the last closed candle, or of the setup the desk owns, frozen when it was made.
Admitted / size skips
How many setups the rules accepted on the loaded chart, and how many were skipped because no valid size fit.
Planned risk / target
The risk per setup (half a percent of equity by default) and the target rule in force (2R).
Volume strength only
Probability untested: Low, Medium and High describe volume, and no chance of success is claimed.

03 Install and check.

  1. Open a chart with real volumeStandard candles, not Heikin Ashi. On forex and CFD feeds the volume is tick volume, which counts price updates.
  2. Paste and addPine Editor → New strategy → paste → Save → Add to chart.
  3. Set your costsIn the strategy's Properties, your broker's commission and slippage. TradingView does not charge overnight financing.
The strategy · the complete source · engulfing-volume-desk.pine
//@version=6
// © 2026 ProEA Lab · Engulfing Volume Desk v0.6.1 · MIT License (see LICENSE in the download).
// Measured on OANDA:XAUUSD 4H with these defaults: 2025-02..2026-09 made money after costs; the untouched
// 2023-03..2024-12 test lost. That is a research result, not a forecast. Read the lab notes before trading it.
// Volume tiers are a rubric, not calibrated win probabilities.
// Decisions occur at close; orders are live from the next tick. No signal-bar fills.
// Cost defaults are an assumed XAUUSD research scenario, not a broker quote.
// Standard preserves the C2 baseline. Adaptive ER is a research hypothesis, not a proven edge.
strategy("Engulfing Volume Desk · v0.6.1", overlay = true,
     initial_capital = 10000, currency = currency.USD, pyramiding = 2, close_entries_rule = "ANY",
     commission_type = strategy.commission.cash_per_contract, commission_value = 0.30,
     slippage = 100, backtest_fill_limits_assumption = 1, margin_long = 100, margin_short = 100,
     calc_on_every_tick = false, calc_on_order_fills = false, process_orders_on_close = false,
     max_labels_count = 100)

string mode = input.string("Aggressive", "Entry mode", options = ["Aggressive", "Retest", "Both"], group = "Setup")
string pattern = input.string("Body", "Engulfing definition", options = ["Body", "Body + full range"], group = "Setup")
string directions = input.string("Both", "Direction", options = ["Both", "Long", "Short"], group = "Setup")
int volumeLength = input.int(20, "Prior volume bars", minval = 2, maxval = 500, group = "Volume")
float mediumAt = input.float(1.0, "Medium from RVOL", minval = 0.01, step = 0.1, group = "Volume")
float highAt = input.float(1.5, "High from RVOL", minval = 0.02, step = 0.1, group = "Volume")
string minimumTier = input.string("Low", "Minimum confirmation", options = ["Low", "Medium", "High"], group = "Volume",
     tooltip = "Volume strength only. No measured win probability. Missing or zero volume blocks setups.")
bool useTrend = input.bool(true, "EMA trend filter", group = "Context",
     tooltip = "Long: close above EMA 200 and EMA higher than 5 bars ago. Short mirrors it. Uses confirmed chart bars only.")
string exposurePolicy = input.string("Standard", "Exposure policy", options = ["Standard", "Fixed half", "Adaptive ER"], group = "Context",
     tooltip = "Research comparison: Standard = 1x; Fixed half = 0.5x; Adaptive ER = 0.5x when 20-bar price-path efficiency is below 0.30, otherwise 1x. Scales risk and notional caps, frozen at setup admission. Does not resize open trades.")
bool useWindow = input.bool(false, "Use research date window", group = "Research")
int startTime = input.time(timestamp("06 Jul 2026 00:00 +0000"), "Start (inclusive signal close)", group = "Research")
int endTime = input.time(timestamp("01 Sep 2026 00:00 +0000"), "End (exclusive signal close)", group = "Research",
     tooltip = "At the first close at/after End, pending entries are cancelled and positions receive a next-tick market close. Terminal settlement can be after End.")
int breakTicks = input.int(1, "Breakout buffer (ticks)", minval = 1, group = "Orders")
float retestDepth = input.float(50, "Retest depth into prior range (%)", minval = 0, maxval = 100, step = 5, group = "Orders",
     tooltip = "0 = broken edge; 50 = midpoint; 100 = opposite edge. Armed only after a confirmed breakout close.")
int expiryBars = input.int(5, "Pending lifetime (post-signal bars)", minval = 1, maxval = 100, group = "Orders")
int stopTicks = input.int(1, "Stop buffer (ticks)", minval = 1, group = "Risk")
float targetR = input.float(2, "Target (planned R)", minval = 0.1, step = 0.1, group = "Risk")
float riskPercent = input.float(0.5, "Planned setup risk (% equity)", minval = 0.01, maxval = 5, step = 0.05, group = "Risk",
     tooltip = "Both splits this budget 50/50. Gaps, slippage and fees can increase actual loss. Quantity also has a 95% equity notional cap.")
bool showLevels = input.bool(true, "Show active plan", group = "Display")
bool showSignals = input.bool(true, "Show admitted setups", group = "Display")
bool showPanel = input.bool(true, "Show status", group = "Display")
string decisionPolicy = input.string("Legacy", "Decision policy", options = ["Legacy", "Context Aggressive", "Context Retest", "Context Router", "Context Router + exit"], group = "Research controller",
     tooltip = "Research only. Context requires directional EMA200/slope5 and ER20 >= 0.30. Router uses Aggressive at extension <= 0.50 structural risk, otherwise Retest. + exit requests a next-tick close after 24h from the first entry-bar open or two confirmed closes losing EMA context. Legacy keeps Entry mode. No self-learning or proven profit.")
string executionPlan = input.string("Legacy", "Execution plan", options = ["Legacy", "Shared objective", "Shared objective + cutoff", "Cost-aware intraday"], group = "Research execution",
     tooltip = "Research only. Shared objective keeps the original Aggressive target for either entry. Cutoff freezes the next New York 17:00 rollover less two chart durations; admission must precede it by another chart duration. Cost-aware also requires a conservative net reward/risk screen >= 1.50. No guarantee of pre-rollover fills or profit.")
float screenCommission = input.float(1.20, "Screen commission (USD / quantity round trip)", minval = 0, step = 0.10, group = "Research execution")
int screenSlippage = input.int(400, "Screen slippage reserve (ticks / round trip)", minval = 0, group = "Research execution",
     tooltip = "Conservative common screening reserve for both routes, independent of actual strategy cost settings. Not an exact limit-order cost or funding estimate.")

// BEGIN PURE HELPERS: extracted verbatim by the deterministic native probe.
f_direction(float o, float h, float l, float c, float po, float ph, float pl, float pc, bool fullRange) =>
    bool complete = not na(o) and not na(h) and not na(l) and not na(c) and not na(po) and not na(ph) and not na(pl) and not na(pc)
    bool valid = complete and h >= math.max(o, c) and l <= math.min(o, c) and ph >= math.max(po, pc) and pl <= math.min(po, pc)
    bool larger = math.abs(c - o) > math.abs(pc - po)
    bool outside = not fullRange or (h >= ph and l <= pl)
    bool bull = valid and larger and outside and pc < po and c > o and o <= pc and c >= po
    bool bear = valid and larger and outside and pc > po and c < o and o >= pc and c <= po
    bull ? 1 : bear ? -1 : 0

f_tier(float v, float baseline, bool windowReady, float medium, float highThreshold) =>
    bool valid = windowReady and not na(v) and not na(baseline) and v > 0 and baseline > 0 and not na(medium) and medium > 0 and highThreshold > medium
    float ratio = valid ? v / baseline : na
    int tier = not valid ? 0 : ratio >= highThreshold ? 3 : ratio >= medium ? 2 : 1
    [ratio, tier]

f_geometry(int direction, float ph, float pl, float sh, float sl, float signalClose, float tick, int buffer, int stopBuffer, float depth, float rr) =>
    float breakout = direction == 1 ? (math.round(ph / tick) + buffer) * tick : (math.round(pl / tick) - buffer) * tick
    float stop = direction == 1 ? (math.round(math.min(pl, sl) / tick) - stopBuffer) * tick : (math.round(math.max(ph, sh) / tick) + stopBuffer) * tick
    float retestRaw = direction == 1 ? ph - (ph - pl) * depth / 100 : pl + (ph - pl) * depth / 100
    float retest = math.round(retestRaw / tick) * tick
    bool crossed = direction * (signalClose - breakout) >= 0
    float aggressive = crossed ? signalClose : breakout
    float targetA = math.round((aggressive + direction * math.abs(aggressive - stop) * rr) / tick) * tick
    float targetRPrice = math.round((retest + direction * math.abs(retest - stop) * rr) / tick) * tick
    [breakout, stop, retest, aggressive, targetA, targetRPrice, crossed]

f_quantity(float budget, float capital, float entry, float stop, float accountPointValue, float step) =>
    float loss = math.abs(entry - stop) * accountPointValue
    float notional = math.abs(entry) * accountPointValue
    bool valid = not na(loss) and loss > 0 and not na(notional) and notional > 0 and not na(step) and step > 0 and budget > 0 and capital > 0
    float raw = valid ? math.min(budget / loss, capital / notional) : na
    float quantity = valid ? math.floor(raw / step) * step : na
    not na(quantity) and quantity >= step ? quantity : na

// Priority is explicit: native exit, invalidation, expiry. These are close observations.
f_pendingReason(bool nativeExit, bool stopTouch, int currentBar, int signalBar, int lifetime) =>
    nativeExit ? 1 : stopTouch ? 2 : currentBar >= signalBar + lifetime ? 3 : 0

f_efficiency(float displacement, float pathLength) =>
    bool valid = not na(displacement) and not na(pathLength) and displacement >= 0 and pathLength >= 0
    valid ? (pathLength == 0 ? (displacement == 0 ? 0.0 : na) : math.min(1.0, displacement / pathLength)) : na

f_exposure(int policy, float efficiency) =>
    policy == 0 ? 1.0 : policy == 1 ? 0.5 : policy == 2 and not na(efficiency) and efficiency >= 0 and efficiency <= 1 ? (efficiency < 0.30 ? 0.5 : 1.0) : na

f_extension(int direction, float signalClose, float breakout, float stop) =>
    float structuralRisk = direction * (breakout - stop)
    bool valid = (direction == 1 or direction == -1) and not na(signalClose) and not na(structuralRisk) and structuralRisk > 0
    valid ? math.max(0.0, direction * (signalClose - breakout)) / structuralRisk : na

// Codes: 0 skip, 1 Aggressive, 2 Retest, 3 Both. The returned choice is frozen at admission.
f_route(int policy, int legacyMode, bool trendMatches, float efficiency, float extension) =>
    bool contextReady = trendMatches and not na(efficiency) and efficiency >= 0.30 and efficiency <= 1 and not na(extension) and extension >= 0
    policy == 0 ? legacyMode : not contextReady ? 0 : policy == 1 ? 1 : policy == 2 ? 2 : (policy == 3 or policy == 4) ? (extension <= 0.50 ? 1 : 2) : 0

// First fill uses the native historical ENTRY BAR OPEN clock, not an intrabar timestamp.
f_managedReason(bool enabled, bool positionOpen, int firstEntryBarTime, int closeTime, int failedCloses) =>
    bool aged = not na(firstEntryBarTime) and not na(closeTime) and closeTime - firstEntryBarTime >= 24 * 60 * 60 * 1000
    enabled and positionOpen ? (aged ? 5 : failedCloses >= 2 ? 6 : 0) : 0

// Calendar clock: DST-aware next NY rollover, not 24 elapsed hours. Frozen per setup.
f_rolloverClock(int closeTime, int durationMs, bool supported) =>
    string zone = "America/New_York"
    bool valid = supported and not na(closeTime) and not na(durationMs) and durationMs > 0 and durationMs <= 240 * 60 * 1000
    int localHour = valid ? hour(closeTime, zone) : na
    int localDay = valid ? dayofweek(closeTime, zone) : na
    int rollover = valid ? timestamp(zone, year(closeTime, zone), month(closeTime, zone), dayofmonth(closeTime, zone) + (localHour >= 18 ? 1 : 0), 17, 0) : na
    int deadline = valid ? rollover - 2 * durationMs : na
    bool weekend = valid and (localDay == dayofweek.saturday or (localDay == dayofweek.friday and localHour >= 17) or (localDay == dayofweek.sunday and localHour < 18))
    bool allowed = valid and not weekend and localHour != 17 and closeTime < rollover - 3 * durationMs
    [allowed, deadline]

// A conservative cost screen, not expected return or a calibrated win probability.
f_netRewardRisk(int direction, float entry, float stop, float target, float pointValue, float friction) =>
    bool valid = (direction == 1 or direction == -1) and not na(entry) and not na(stop) and not na(target) and not na(pointValue) and pointValue > 0 and not na(friction) and friction >= 0
    float grossRisk = valid ? direction * (entry - stop) * pointValue : na
    float netReward = valid ? direction * (target - entry) * pointValue - friction : na
    float netRisk = valid ? grossRisk + friction : na
    valid and grossRisk > 0 and netReward > 0 and netRisk > 0 ? netReward / netRisk : na
// END PURE HELPERS

f_tierName(int tier) =>
    tier == 3 ? "High" : tier == 2 ? "Medium" : tier == 1 ? "Low" : "Unavailable"

// Chart label only; no order behaviour depends on it.
f_entryLabel(int direction, bool retest, int tier) =>
    (direction == 1 ? "Long" : "Short") + (retest ? " retest" : "") + " · " + f_tierName(tier) + " volume"

f_open(string entryId) =>
    bool found = false
    if strategy.opentrades > 0
        for i = 0 to strategy.opentrades - 1
            if strategy.opentrades.entry_id(i) == entryId
                found := true
    found

if barstate.isfirst and highAt <= mediumAt
    runtime.error("High RVOL threshold must be greater than Medium.")
if barstate.isfirst and useWindow and endTime <= startTime
    runtime.error("Research End must be after Start.")

// BP: entry.confirmation / regime. Evaluate every history-dependent series every bar.
float baselineVolume = ta.sma(volume, volumeLength)[1]
float validVolumeFraction = ta.sma(not na(volume) and volume > 0 ? 1.0 : 0.0, volumeLength)[1]
bool volumeReady = not na(validVolumeFraction) and validVolumeFraction == 1.0
[relativeVolume, currentTier] = f_tier(volume, baselineVolume, volumeReady, mediumAt, highAt)
int candidateDirection = f_direction(open, high, low, close, open[1], high[1], low[1], close[1], pattern == "Body + full range")
int requiredTier = minimumTier == "High" ? 3 : minimumTier == "Medium" ? 2 : 1
bool directionAllowed = directions == "Both" or (directions == "Long" and candidateDirection == 1) or (directions == "Short" and candidateDirection == -1)
float trendEma = ta.ema(close, 200)
bool trendReady = bar_index >= 204 and not na(trendEma[5])
bool directionalTrend = trendReady and candidateDirection * (close - trendEma) > 0 and candidateDirection * (trendEma - trendEma[5]) > 0
bool trendAllowed = not useTrend or directionalTrend
// Fixed, causal research overlay. Every rolling calculation runs on every chart bar.
float priceChange = ta.change(close)
float pricePath = math.sum(math.abs(priceChange), 20)
// math.sum skips na; require exactly the most recent 20 differences to be available.
float validPriceChanges = math.sum(not na(priceChange) ? 1.0 : 0.0, 20)
float efficiency = f_efficiency(math.abs(close - close[20]), validPriceChanges == 20 ? pricePath : na)
int policyCode = exposurePolicy == "Standard" ? 0 : exposurePolicy == "Fixed half" ? 1 : 2
float currentExposure = f_exposure(policyCode, efficiency)
bool inWindow = not useWindow or (time_close >= startTime and time_close < endTime)
float accountPointValue = strategy.convert_to_account(syminfo.pointvalue)
bool metadataReady = not na(syminfo.mintick) and syminfo.mintick > 0 and not na(syminfo.mincontract) and syminfo.mincontract > 0 and not na(accountPointValue) and accountPointValue > 0
int decisionCode = decisionPolicy == "Legacy" ? 0 : decisionPolicy == "Context Aggressive" ? 1 : decisionPolicy == "Context Retest" ? 2 : decisionPolicy == "Context Router" ? 3 : 4
int legacyMode = mode == "Aggressive" ? 1 : mode == "Retest" ? 2 : 3
[candidateBreak, candidateStop, candidateRetest, candidateA, candidateATarget, candidateRTarget, crossed] = f_geometry(candidateDirection, high[1], low[1], high, low, close, syminfo.mintick, breakTicks, stopTicks, retestDepth, targetR)
float candidateExtension = f_extension(candidateDirection, close, candidateBreak, candidateStop)
int candidateMode = f_route(decisionCode, legacyMode, directionalTrend, efficiency, candidateExtension)
int executionCode = executionPlan == "Legacy" ? 0 : executionPlan == "Shared objective" ? 1 : executionPlan == "Shared objective + cutoff" ? 2 : 3
int chartDurationMs = timeframe.isseconds or timeframe.isminutes ? int(timeframe.in_seconds() * 1000) : na
[candidateClockAllowed, candidateDeadline] = f_rolloverClock(time_close, chartDurationMs, timeframe.isintraday and chart.is_standard)
float selectedRTarget = executionCode >= 1 ? candidateATarget : candidateRTarget
float screeningFriction = screenCommission + screenSlippage * syminfo.mintick * accountPointValue
float candidateNetA = f_netRewardRisk(candidateDirection, candidateA, candidateStop, candidateATarget, accountPointValue, screeningFriction)
float candidateNetR = f_netRewardRisk(candidateDirection, candidateRetest, candidateStop, selectedRTarget, accountPointValue, screeningFriction)
float candidateNetRR = candidateMode == 1 ? candidateNetA : candidateMode == 2 ? candidateNetR : candidateMode == 3 ? math.min(candidateNetA, candidateNetR) : na

// One setup owns both legs. Never release ownership merely because one leg exited.
var bool owned = false
var int setupBar = na
var int setupTime = na
var int setupDirection = 0
var int setupTier = 0
var float setupRvol = na
var float setupEfficiency = na
var float setupExposure = na
var int setupMode = 0
var int setupDecision = 0
var int setupExecution = 0
var int setupDeadline = na
var float setupExtension = na
var int firstEntryBarTime = na
var int failedContextCloses = 0
var bool managedCloseRequested = false
var int closedAtAdmission = 0
var bool aPending = false
var bool rPending = false
var bool rCanArm = false
var bool entriesClosed = false
var float priorTop = na
var float priorBottom = na
var float breakoutLevel = na
var float stopLevel = na
var float retestLevel = na
var float aggressiveLevel = na
var float aTarget = na
var float rTarget = na
var float aQuantity = na
var float rQuantity = na
var string status = "Waiting for engulfing"
var int admittedCount = 0
var int skippedSizeCount = 0
bool admitted = false
bool released = false
int cancelReason = 0
int managedRequest = 0
int firstBlock = 0
bool aOpen = f_open("A")
bool rOpen = f_open("R")

if barstate.isconfirmed
    if owned and bar_index > setupBar
        if aOpen
            aPending := false
        if rOpen
            rPending := false
        if na(firstEntryBarTime) and (aOpen or rOpen)
            for i = 0 to strategy.opentrades - 1
                int entryClock = strategy.opentrades.entry_time(i)
                firstEntryBarTime := na(firstEntryBarTime) ? entryClock : math.min(firstEntryBarTime, entryClock)
        bool setupTrendMatches = trendReady and setupDirection * (close - trendEma) > 0 and setupDirection * (trendEma - trendEma[5]) > 0
        if setupDecision == 4 and (aOpen or rOpen) and not managedCloseRequested
            failedContextCloses := setupTrendMatches ? 0 : failedContextCloses + 1
        bool nativeExitObserved = strategy.closedtrades > closedAtAdmission
        bool stopTouched = setupDirection == 1 ? low <= stopLevel : high >= stopLevel
        cancelReason := f_pendingReason(nativeExitObserved, stopTouched, bar_index, setupBar, expiryBars)
        // These are close-observed cancellations, never invented intrabar ordering.
        if not entriesClosed and cancelReason != 0
            strategy.cancel("A")
            strategy.cancel("R")
            aPending := false
            rPending := false
            rCanArm := false
            entriesClosed := true
            status := nativeExitObserved ? "Exit observed; entries closed" : stopTouched ? "Stop touched; entries closed" : "Pending orders expired"
        bool deadlineDue = setupExecution >= 2 and not na(setupDeadline) and time_close >= setupDeadline and (aOpen or rOpen or aPending or rPending or rCanArm)
        int manageReason = deadlineDue ? 7 : f_managedReason(setupDecision == 4, aOpen or rOpen, firstEntryBarTime, time_close, failedContextCloses)
        if manageReason != 0 and not managedCloseRequested and not (useWindow and time_close >= endTime)
            strategy.cancel("A")
            strategy.cancel("R")
            aPending := false
            rPending := false
            rCanArm := false
            entriesClosed := true
            managedCloseRequested := aOpen or rOpen
            managedRequest := aOpen or rOpen ? manageReason : 0
            cancelReason := manageReason
            if aOpen or rOpen
                strategy.close_all(comment = manageReason == 7 ? "Pre-rollover exit" : manageReason == 5 ? "Controller age exit" : "Controller context exit")
            status := manageReason == 7 ? "Rollover cutoff processed" : manageReason == 5 ? "Age exit requested" : "Context exit requested"
        // Breakout candle's earlier retest is not tradable: new limit starts next tick.
        bool breakoutClose = setupDirection * (close - breakoutLevel) >= 0
        if rCanArm and not entriesClosed and breakoutClose and inWindow
            strategy.entry("R", setupDirection == 1 ? strategy.long : strategy.short, qty = rQuantity, limit = retestLevel,
                 comment = f_entryLabel(setupDirection, true, setupTier), alert_message = "Retest entry filled")
            strategy.exit("R-X", from_entry = "R", stop = stopLevel, limit = rTarget, comment_profit = "Target", comment_loss = "Stop", alert_message = "Retest bracket filled")
            rPending := true
            rCanArm := false
            status := "Retest limit armed"
        if strategy.position_size == 0 and not aPending and not rPending and not rCanArm
            owned := false
            released := true
            strategy.cancel("A-X")
            strategy.cancel("R-X")

    // BP: risk. Freeze sizing and all levels once; skip if any requested leg is invalid.
    // Equal warm-up for all candidates, so the trend comparison starts on the same data.
    // First-blocker order is explicit. Counts describe opportunities, not hypothetical returns.
    firstBlock := candidateDirection == 0 ? 1 : not inWindow ? 2 : not trendReady ? 3 : (not chart.is_standard or not metadataReady or na(currentExposure)) ? 4 : currentTier < requiredTier ? 5 : not directionAllowed ? 6 : (decisionCode == 0 and not trendAllowed) ? 7 : (owned or released) ? 8 : candidateMode == 0 ? 9 : (executionCode >= 2 and not candidateClockAllowed) ? 11 : (executionCode == 3 and (na(candidateNetRR) or candidateNetRR < 1.50)) ? 12 : 0
    bool eligible = firstBlock == 0
    if eligible
        float weight = candidateMode == 3 ? 0.5 : 1.0
        float budget = strategy.equity * riskPercent / 100 * weight * currentExposure
        float capital = strategy.equity * 0.95 * weight * currentExposure
        float qtyA = f_quantity(budget, capital, candidateA, candidateStop, accountPointValue, syminfo.mincontract)
        float qtyR = f_quantity(budget, capital, candidateRetest, candidateStop, accountPointValue, syminfo.mincontract)
        bool useA = candidateMode == 1 or candidateMode == 3
        bool useR = candidateMode == 2 or candidateMode == 3
        bool geometryA = candidateDirection * (candidateA - candidateStop) > 0 and candidateDirection * (candidateATarget - candidateA) >= syminfo.mintick * 0.5
        bool geometryR = candidateDirection * (candidateRetest - candidateStop) > 0 and candidateDirection * (selectedRTarget - candidateRetest) >= syminfo.mintick * 0.5
        bool sized = (not useA or (not na(qtyA) and geometryA)) and (not useR or (not na(qtyR) and geometryR))
        if sized
            owned := true
            admitted := true
            admittedCount += 1
            setupBar := bar_index
            setupTime := time
            setupDirection := candidateDirection
            setupTier := currentTier
            setupRvol := relativeVolume
            setupEfficiency := efficiency
            setupExposure := currentExposure
            setupMode := candidateMode
            setupDecision := decisionCode
            setupExecution := executionCode
            setupDeadline := executionCode >= 2 ? candidateDeadline : na
            setupExtension := candidateExtension
            firstEntryBarTime := na
            failedContextCloses := 0
            managedCloseRequested := false
            closedAtAdmission := strategy.closedtrades
            priorTop := high[1]
            priorBottom := low[1]
            breakoutLevel := candidateBreak
            stopLevel := candidateStop
            retestLevel := candidateRetest
            aggressiveLevel := candidateA
            aTarget := candidateATarget
            rTarget := selectedRTarget
            aQuantity := qtyA
            rQuantity := qtyR
            aPending := false
            rPending := false
            rCanArm := useR
            entriesClosed := false
            status := "Setup confirmed"
            if useA
                // Already confirmed beyond the break: next-tick market, never backfill.
                strategy.entry("A", setupDirection == 1 ? strategy.long : strategy.short, qty = aQuantity,
                     stop = crossed ? na : breakoutLevel, comment = f_entryLabel(setupDirection, false, setupTier), alert_message = "Aggressive entry filled")
                strategy.exit("A-X", from_entry = "A", stop = stopLevel, limit = aTarget, comment_profit = "Target", comment_loss = "Stop", alert_message = "Aggressive bracket filled")
                aPending := true
                status := crossed ? "Aggressive next-tick market" : "Aggressive stop armed"
            if useR and crossed
                strategy.entry("R", setupDirection == 1 ? strategy.long : strategy.short, qty = rQuantity, limit = retestLevel,
                     comment = f_entryLabel(setupDirection, true, setupTier), alert_message = "Retest entry filled")
                strategy.exit("R-X", from_entry = "R", stop = stopLevel, limit = rTarget, comment_profit = "Target", comment_loss = "Stop", alert_message = "Retest bracket filled")
                rPending := true
                rCanArm := false
                status := setupMode == 3 ? "Both legs armed" : "Retest limit armed"
        else
            firstBlock := 10
            skippedSizeCount += 1
            status := "Skipped: quantity / geometry"

    if owned and useWindow and time_close >= endTime
        strategy.cancel("A")
        strategy.cancel("R")
        aPending := false
        rPending := false
        rCanArm := false
        entriesClosed := true
        cancelReason := 4
        if strategy.position_size != 0
            strategy.close_all(comment = "Research window settlement")
        status := "Research window ended"

    // BP: exit. Brackets survive expiry, sibling closure, missing volume and new signals.
    if owned
        if aPending or aOpen
            strategy.exit("A-X", from_entry = "A", stop = stopLevel, limit = aTarget, comment_profit = "Target", comment_loss = "Stop", alert_message = "Aggressive bracket filled")
        if rPending or rOpen
            strategy.exit("R-X", from_entry = "R", stop = stopLevel, limit = rTarget, comment_profit = "Target", comment_loss = "Stop", alert_message = "Retest bracket filled")

color directionColor = setupDirection == 1 ? color.rgb(0, 155, 135) : color.rgb(220, 75, 85)
plot(showLevels and owned ? priorTop : na, "Prior candle high", color.new(chart.fg_color, 70), style = plot.style_linebr)
plot(showLevels and owned ? priorBottom : na, "Prior candle low", color.new(chart.fg_color, 70), style = plot.style_linebr)
plot(showLevels and owned and setupMode != 2 ? aggressiveLevel : na, "Aggressive planned entry", color.blue, 2, plot.style_linebr)
plot(showLevels and owned and setupMode != 1 ? retestLevel : na, "Retest planned entry", color.orange, 2, plot.style_linebr)
plot(showLevels and owned ? stopLevel : na, "Frozen stop", color.red, 2, plot.style_linebr)
plot(showLevels and owned and setupMode != 2 ? aTarget : na, "Aggressive target", color.new(directionColor, 25), style = plot.style_linebr)
plot(showLevels and owned and setupMode != 1 ? rTarget : na, "Retest target", color.new(directionColor, 50), style = plot.style_linebr)
plotshape(showSignals and admitted and setupDirection == 1, "Long setup admitted", shape.triangleup, location.belowbar, color.rgb(0, 155, 135), size = size.tiny)
plotshape(showSignals and admitted and setupDirection == -1, "Short setup admitted", shape.triangledown, location.abovebar, color.rgb(220, 75, 85), size = size.tiny)
plot(relativeVolume, "RVOL (current closed bar)", display = display.data_window)
plot(currentTier, "Volume tier: 0 unavailable, 1 Low, 2 Medium, 3 High", display = display.data_window)
// Bounded numeric audit channels; do not create drawings or alter economic events.
plot(setupTime, "Audit setup time", display = display.data_window)
plot(setupBar, "Audit setup bar", display = display.data_window)
plot(admitted ? 1 : 0, "Audit admitted", display = display.data_window)
plot(owned ? 1 : 0, "Audit owned", display = display.data_window)
plot(aPending ? 1 : 0, "Audit pending A", display = display.data_window)
plot(rPending ? 1 : 0, "Audit pending R", display = display.data_window)
plot(rCanArm ? 1 : 0, "Audit can arm R", display = display.data_window)
plot(entriesClosed ? 1 : 0, "Audit entries closed", display = display.data_window)
plot(stopLevel, "Audit stop", display = display.data_window)
plot(aggressiveLevel, "Audit entry A", display = display.data_window)
plot(retestLevel, "Audit entry R", display = display.data_window)
plot(aTarget, "Audit target A", display = display.data_window)
plot(rTarget, "Audit target R", display = display.data_window)
plot(aQuantity, "Audit quantity A", display = display.data_window)
plot(rQuantity, "Audit quantity R", display = display.data_window)
plot(strategy.position_size, "Audit position", display = display.data_window)
plot(strategy.closedtrades, "Audit closed trades", display = display.data_window)
plot(useWindow ? 1 : 0, "Audit window enabled", display = display.data_window)
plot(startTime, "Audit start", display = display.data_window)
plot(endTime, "Audit end", display = display.data_window)
plot(useTrend ? 1 : 0, "Audit trend enabled", display = display.data_window)
plot(requiredTier, "Audit minimum tier", display = display.data_window)
plot(cancelReason, "Audit cancel reason", display = display.data_window)
plot(inWindow ? 1 : 0, "Audit in window", display = display.data_window)
plot(efficiency, "Audit ER20", display = display.data_window)
plot(policyCode, "Audit exposure policy", display = display.data_window)
plot(currentExposure, "Audit current exposure", display = display.data_window)
plot(setupEfficiency, "Audit setup ER20", display = display.data_window)
plot(setupExposure, "Audit setup exposure", display = display.data_window)
plot(decisionCode, "Audit decision policy", display = display.data_window)
plot(candidateDirection, "Audit candidate direction", display = display.data_window)
plot(candidateMode, "Audit candidate mode", display = display.data_window)
plot(firstBlock, "Audit first blocker", display = display.data_window)
plot(candidateExtension, "Audit current extension", display = display.data_window)
plot(setupMode, "Audit setup mode", display = display.data_window)
plot(setupDecision, "Audit setup decision", display = display.data_window)
plot(setupExtension, "Audit setup extension", display = display.data_window)
plot(time_close, "Audit bar close", display = display.data_window)
plot(firstEntryBarTime, "Audit first entry bar time", display = display.data_window)
plot(failedContextCloses, "Audit failed context closes", display = display.data_window)
plot(managedRequest, "Audit managed exit request", display = display.data_window)
plot(managedCloseRequested ? 1 : 0, "Audit managed close requested", display = display.data_window)
plot(executionCode, "Audit execution plan", display = display.data_window)
plot(candidateDeadline, "Audit candidate deadline", display = display.data_window)
plot(setupDeadline, "Audit setup deadline", display = display.data_window)
plot(setupExecution, "Audit setup execution plan", display = display.data_window)
plot(candidateNetRR, "Audit candidate net reward risk", display = display.data_window)

var table panel = table.new(position.bottom_right, 2, 6, bgcolor = chart.bg_color, frame_color = color.new(chart.fg_color, 70), frame_width = 1)
if barstate.islastconfirmedhistory or (barstate.isrealtime and barstate.isconfirmed)
    if showPanel
        string readiness = not chart.is_standard ? "Use standard OHLC chart" : not metadataReady ? "Instrument metadata unavailable" : not inWindow ? "Outside research window" : currentTier == 0 ? "Volume unavailable / warming up" : status
        table.cell(panel, 0, 0, "ENGULFING VOLUME", text_color = chart.fg_color, text_size = size.small)
        string displayedMode = owned ? (setupMode == 1 ? "Aggressive" : setupMode == 2 ? "Retest" : "Both") : decisionCode == 0 ? mode : decisionPolicy
        table.cell(panel, 1, 0, displayedMode + (policyCode == 0 ? "" : " / " + exposurePolicy), text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 0, 1, "State", text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 1, 1, owned ? (strategy.position_size != 0 ? "Position active" : status) : readiness, text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 0, 2, owned ? "Setup RVOL" : "Current RVOL", text_color = chart.fg_color, text_size = size.small)
        float displayedRvol = owned ? setupRvol : relativeVolume
        table.cell(panel, 1, 2, f_tierName(owned ? setupTier : currentTier) + " / " + (na(displayedRvol) ? "—" : str.tostring(displayedRvol, "#.##")), text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 0, 3, "Admitted / size skips", text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 1, 3, str.tostring(admittedCount) + " / " + str.tostring(skippedSizeCount), text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 0, 4, "Planned risk / target", text_color = chart.fg_color, text_size = size.small)
        string targetLabel = (owned ? setupExecution : executionCode) >= 1 ? "Shared price target" : str.tostring(targetR) + "R"
        table.cell(panel, 1, 4, str.tostring(riskPercent * (owned ? setupExposure : currentExposure)) + "% / " + targetLabel, text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 0, 5, "Volume strength only", text_color = chart.fg_color, text_size = size.small)
        table.cell(panel, 1, 5, "Probability untested", text_color = chart.fg_color, text_size = size.small)
    else
        table.clear(panel, 0, 0, 1, 5)

04 Every rule, one question at a time.

Exactly how is a setup made?

Engulfing: at the close, the previous candle and this one have opposite, non-doji bodies; this body covers both ends of the previous body, inclusive, and is strictly larger. “Body + full range” also needs the high and low to cover the previous candle's.

Trend: a long needs the close above the EMA 200 and the EMA above its value five candles ago; a short mirrors it. Every check needs at least 205 loaded candles.

Plan: the entry one tick past the previous candle's high (a long), or a market order at the next open when the close is already through; the stop one tick beyond the lower low of both candles; the target twice the planned risk, rounded to the tick. Entry, stop, target, grade and size freeze when the setup is accepted.

One setup at a time: while a setup is owned, new engulfings are skipped; there are no reversals.

How is the volume graded?

RVOL is this candle's volume divided by the average of the twenty before it. Below 1.0 is Low, 1.0 to 1.5 Medium, 1.5 and above High. Missing or zero volume blocks the setup. The thresholds and a minimum grade are inputs; the default accepts every grade.

When is an order cancelled?

At the close of the fifth candle after the signal, if it has not filled, or as soon as a close shows the stop was touched before the entry. An open position keeps its stop and target until one of them closes it.

Which settings matter?

Entry mode (breakout, retest, both) · engulfing definition · direction · prior volume bars and the Medium and High thresholds · minimum grade · the EMA trend filter · target in R · risk per setup · pending lifetime · breakout and stop buffers in ticks.

The research groups (decision policy, execution plan, exposure policy, research date window) default to off or legacy: they are the experiments behind the lab notes, there for you to measure on your own chart.

What did we measure?

On gold's 4-hour chart, after commission, slippage and overnight financing, the default rule finished 2025–26 ahead; on 2023–24, the history we had held back and ran once, it lost, before financing too. The volume grade described the candle; it did not tell good setups from bad ones. Every number and how we measured it is in the lab notes, linked above.

What can change the reading?

Every decision reads closed candles. Changing the symbol, timeframe, feed or inputs recalculates the history. Your broker's fills, costs and financing can differ from TradingView's report. It is research tooling, not financial advice.

05 · THE LOG

Every change, dated

What shipped and what changed in Engulfing Volume 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.

  1. Engulfing Volume Desk v0.6.1 Shipped

    Engulfing candles graded by relative volume, each turned into one frozen breakout plan.

BUILT WITH TRADERS

Something to change in Engulfing Volume Desk?

A small friction or a big idea. Tell us what would make this tool work better for you.

What would help?

Describe your rules and what you want the tool to help you see.

Add chart details Optional

For a question about your idea or an update if it ships.

Guides & new tools Optional

Add an email above to choose this.

Show us on your chart Optional

Crop out any account details you don't want to share.

No account needed. Sent privately to the lab.
  1. We read it
  2. We choose what to build
  3. Shipped changes get a date

Requests for a new tool go on the same queue: see what is asked for and what is being built.

  • Drift Desk

    Compare a volume-graded engulfing with a candle-pressure read; each becomes one fixed plan.

  • Breakaway Desk

    Compare a breakout stop past the prior candle with a resting limit in a breakout's first gap.

Built by ProEA Lab. Compare all free indicators.