Complete Suite Everything on the list, one payment
Renews never / Full source / 7-day refund before download
The Suite adds no Engulfing Volume features; this desk stays 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.
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.
| Time | Event | Prices |
|---|---|---|
| Thu Mar 12 · 14:00 | Earlier: Long · Low volume · Stop +0.0R | Entry 4,379.05 · stop 4,321.15 · target 4,494.85 |
| Mon Mar 16 · 18:00 | Earlier: Long · Medium volume · Stop −1.0R | Entry 4,343.30 · stop 4,313.73 · target 4,402.44 |
| Tue Mar 17 · 06:00 | Window opens | Close 4,317.55 · EMA 200 4,266.13 |
| Tue Mar 17 · 10:00 | Long · Medium volume (RVOL 1.3): the engulfing closes, a stop order at 4,352.69 | Entry 4,352.69 · stop 4,308.41 · target 4,441.25 |
| Tue Mar 17 · 14:00 | Filled | Fill 4,352.69 |
| Tue Mar 17 · 18:00 | Bullish engulfing skipped: a setup is open | Close 4,367.51 |
| Wed Mar 18 · 02:00 | Bullish engulfing skipped: a setup is open | Close 4,395.24 |
| Wed Mar 18 · 06:00 | Long closed · Target +2.0R | Exit 4,441.25 |
| Wed Mar 18 · 22:00 | Bearish engulfing skipped: against the trend | Close 4,372.67 |
| Thu Mar 19 · 02:00 | Long · Medium volume (RVOL 1.03): the engulfing closes, a stop order at 4,397.19 | Entry 4,397.19 · stop 4,366.65 · target 4,458.27 |
| Thu Mar 19 · 06:00 | Filled | Fill 4,397.19 |
| Thu Mar 19 · 10:00 | Bullish engulfing skipped: a setup is open | Close 4,404.32 |
| Thu Mar 19 · 14:00 | Bearish engulfing skipped: against the trend | Close 4,389.52 |
| Thu Mar 19 · 22:00 | Long closed · Stop −1.0R | Exit 4,366.65 |
| Fri Mar 20 · 06:00 | Bearish engulfing skipped: against the trend | Close 4,349.12 |
No signup · a Pine v6 strategy · open source. It draws the plan; your Strategy Tester measures it.
At every close the desk reads four checks, and an engulfing becomes a plan only when all four hold.
All four checks hold: a long plan at 4,397.19, frozen.
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.
Red, then green for a long; green, then red for a short. A doji never counts.
The new body reaches both ends of the last one.
Strictly bigger: an equal body is not an engulfing.
Longs above a rising EMA 200, shorts below a falling one, on closed candles.
Drag the close and the volume: the checks make the plan, and the volume only names it Low, Medium or High.
A fixed simulated example, the same candles each time. The verdict is the script's own checks, run on what you set.
Less volume than the average of the last twenty candles.
Up to half as much again as that average.
At least half as much again as the average.
In our gold test it described the candle; it did not tell good setups from bad.
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: the bracket closed it at 4,441.25.
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.
A stop order one tick beyond the last candle; the next open when the close is already through.
One tick beyond the lower low of the two candles (the higher high for a short).
Twice the planned risk from the planned entry, frozen with it.
Cancelled after five candles, or as soon as a close shows the stop was touched first.
One file, the complete v0.6.1 strategy, ready for TradingView's Pine Editor.
On a phone? Send this page to your desktop.
//@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.
The full source under MIT; nothing is locked.
It draws the plan and sends its orders to the Strategy Tester.
Any symbol with real volume; we measured gold's 4-hour chart.
Retest entry, both legs and a cost screen are inputs, off by default.
Yes. The full source under MIT, no signup, nothing locked.
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.
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.
No. Every check reads a closed candle, the order is live from the next tick, and the plan is frozen once it is made.
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.
Complete Suite · $97 · one payment
The Suite adds no Engulfing Volume features; this desk stays free.
Every rule, setting and panel row, one topic at a time.
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.
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.

Download the strategy (.pine)MIT licence
//@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)
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.
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.
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.
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.
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.
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.
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.
Engulfing Volume Desk v0.6.1 Shipped
Engulfing candles graded by relative volume, each turned into one frozen breakout plan.
A small friction or a big idea. Tell us what would make this tool work better for you.
Requests for a new tool go on the same queue: see what is asked for and what is being built.