CAT researchOverviewEURUSDGBPUSDXAUUSDGBPCADEURCADAUDJPYGBPJPYResearch notes

Zero-Touch Divergence [ZTD] — design

Date: 2026-09-04 Status: approved approach (option 1, standalone script); spec pending Daniel's review Parents: avwap_zscore_band.pine (AVWAP Z-Score Band [Stage-0] v38), playbook_indicator.pine (AVWAPxAKAO Playbook [PBK] v4) Deliverable: ztd_divergence.pine — one new TradingView indicator, saved on Daniel's account as "AVWAP-Z x PBK Zero-Touch Divergence [ZTD]", short title ZTD.

1. Purpose

A same-bar, non-repainting divergence detector that combines the two parents' outputs into one rule. Daniel's rule, upper side (lower side mirrors it):

  1. On an H1, H4 or D chart, AVWAP-Z prints an elevated or extreme triangle while PBK's z line is beyond its overbought level on the same bar.
  2. Keep tracking: record the highest PBK-z value and the high of the triangle bars.
  3. PBK's z comes back to the zero line.
  4. Price trades beyond the recorded triangle high.
  5. On that bar compare PBK's z with the recorded peak:
    • z lower than the peak → regular bearish divergence (price higher high, oscillator lower high).
    • Hidden bearish divergence is the mirror shape: PBK's z exceeds the recorded peak while price has not exceeded the recorded high.

Neither parent alone can do this: AVWAP-Z has no view of PBK's Rolling-24 z, and PBK has no Auto-anchor triangle state. Reading the parents through source inputs was rejected because it breaks Daniel's zero-configuration requirement, so the script recomputes both z measures.

2. Components

2.1 One z engine, two instances

Both parents compute the same thing: volume-weighted anchored VWAP of hlc3, spread close − basis, sd = ta.stdev(spread, sigmaLen), z = spread / sd, and tier thresholds either fixed (2.0 / 2.5) or percentile-calibrated (ta.percentile_nearest_rank(|z|, calibLen, p)). They differ only in anchor set and defaults.

ZTD carries one engine block, written out twice at top level (Pine function-parameter history is unreliable per tradingview-mcp-quirks; every ta.* call stays unconditional at top level). Each instance has its own input group:

Instance Role Anchor default Threshold default Anchor options
T (triangle engine) reproduces AVWAP-Z triangles Rolling (24-bar) Percentile 90 / 95 over 2000 bars Auto, Auto (24-bar), Rolling (24-bar), Swing, Flip, Daily, 4 Hour, Weekly, Monthly, Quarterly, Yearly
O (oscillator engine) reproduces PBK's pane z line Rolling (24-bar) Percentile 90 / 95 over 2000 bars same list

Shared inputs: sigma lookback (200), volume weighting (on), Swing pivot strength (5), Flip debounce (5).

Defaults mirror Daniel's live chart instances (read 2026-09-04 from the EURUSD 1h layout, not the scripts' stock defaults): AVWAP-Z runs Rolling (24-bar) / Percentile / sigma 200, and PBK's z layer runs Rolling (24-bar) / Percentile / sigma 200. Consequence: with defaults, zT and zO are the same series, so the arm condition reduces to "z at or beyond the extreme percentile"; the two engines stay separate so either side can be re-anchored (Swing, Flip, Daily) without touching the other. The Fixed date anchor mode is dropped (irrelevant to the rule).

Engine outputs per instance: basis, z, thrElev, thrExt, state ∈ {−2,−1,0,1,2} using exactly AVWAP-Z's classification (z >= thrExt → 2, z >= thrElev → 1, mirrored).

Agreement requirement: with T set like the chart's AVWAP-Z instance and O set like the chart's PBK instance, ZTD's zT and zO series must match the parents' exported plots to 1e-9 at every bar. This is a verification gate, not a hope.

2.2 Rule atoms

  • triBear = stateT >= minTier where minTier input is Elevated (1, default) or Extreme (2). triBull = stateT <= −minTier.
  • obLevel = obTier == "Extreme" ? thrExtO : thrElevO (default Extreme). overbought = zO >= obLevel, oversold = zO <= −obLevel.
  • armBear = triBear and overbought, armBull = triBull and oversold.
  • zeroTouchFromAbove = zO <= zeroTol, zeroTouchFromBelow = zO >= −zeroTol (zeroTol input, default 0.0, so "touch" means at or through zero).
  • tfOk = not chartGate or timeframe.period in {"60", "240", "D"} (input default on).

2.3 State machine (per side; bear side written, bull mirrors) — REVISED 2026-09-05

Daniel (2026-09-05): "get the extreme in oscillator and extreme in price as per the 2 negative/positive ranges of the oscillator which are on either side of the zero line."

Ranges. A range is one excursion of the oscillator on one side of zero, bounded by zero touches. On the bear side a bar belongs to a positive range while zOzb > zeroTol; the range ends on the bar where zOzb <= zeroTol (the zero touch; that bar is outside the range, so its own high/low do NOT count — the range extreme is the previous bar's running value) — membership is the exact complement of the touch, so a range never starts and ends on one bar. zOzb is the close-based z by default (input "Zero touch / range reading"); in intrabar mode with "Bar extreme" it is z of the low (bull: high). Arming, peaks and break comparisons use zOb = z of the high in intrabar mode.

Range extremes are taken over ALL bars of the range: highest high and highest z (bear), lowest low and lowest z (bull). A range is qualified if any of its bars arms (triBear and zOb >= obLevel, and the chart-timeframe gate passes).

States: IDLE (0), RANGE 1 (1) = a qualified range is open, LOCKED (2) = range 1 is final and the next same-side range is awaited, RANGE 2 (3) = the comparison range is open.

Per bar, in this order:

  1. Timeout: bar_index − since > timeoutBars → IDLE (range tracking continues; the range may still qualify and become range 1 at its end).
  2. Range start / continue: on the first bar of a new positive range, reset the running extremes and flags (curTri, curOB, curQual). If the side was LOCKED, the new range is range 2 (→ RANGE 2, fired := 0, contra := false). Otherwise update running extremes. Then curTri |= triBear, curOB |= zOb >= obLevel, and on an arm curQual := true (IDLE → RANGE 1).
  3. Compare (RANGE 2 only, running extremes, fires early):
    • not yet fired: curHi > refHigh and curPk < peakHiRDIV−; else curPk > peakHi and curHi < refHighHDIV−. The values used are snapshotted to f* plots and to the drawings.
    • already fired: contradiction when the relation flips — regular: curPk >= peakHi; hidden: curHi >= refHigh. The label is marked X and dimmed, never deleted.
    • attention: the first bar range 2 prints a triangle or an OB reading after (or at) the fire, the label gets a ! prefix.
    • follow (2026-09-05): while fired and not contradicted, a new range-2 price extreme or oscillator extreme moves the corresponding line end and label to it and rewrites the price / dz; a new price extreme raises the extended alert with the new price.
  4. Range end (zero touch): if RANGE 2 and fired, export flag = attention(1) + contradicted(2). If the range qualified it becomes range 1 (refHigh/peakHi := cur*, → LOCKED); otherwise → IDLE. fired/contra reset.

The opposite side arming does not by itself invalidate a pending comparison (a negative range between two positive ranges is the normal path). Two later additions (2026-09-05): reference lifetime — by default a qualified range 1 stays the reference for every later same-side range until a new qualified range replaces it or the timeout (240 bars from the lock) expires, at most one signal per reference (inputs allow next-range-only and unlimited); and opposite-extreme cancel (input, default OFF since v21 on 2026-09-14; on until then) - if the oscillator reaches the other side's OB/OS level while the reference stands, the reference is kept and marked (~opp on the labels and HUD, cancelB/cancelU = 2 in the data window); with the input on the reference is dropped instead (cancelB/cancelU = 1 on that bar). One signal per side per range 2. A range 2 that qualifies is automatically range 1 for the next comparison, so consecutive divergences chain.

2.4 Chart output

Price pane (force_overlay=true, indicator is overlay=false):

  • Labels at the trigger bar: RDIV − / HDIV − red above high; RDIV + / HDIV + teal below low. Text shows the trigger price (the bar's high / low, format.mintick), the reference price it is measured against (refHigh / refLow), and Δz = zO − peakO to two decimals. (Daniel, 2026-09-04: the price where the divergence occurs must be readable.)
  • A line from (refBar, refHigh) to (bar_index, high) for bearish signals (mirror for bullish). Regular lines solid, hidden lines dotted.
  • Overlap triangles (default on): on every bar where a T-tier triangle coincides with the oscillator beyond its overbought/oversold level (triBear and zO >= obLevel, mirror below), print AVWAP-Z's triangle in AVWAP-Z's colours (extreme red / elevated orange above the bar, extreme teal / elevated blue below), size small. These are exactly the bars that arm a cycle (before the chart gate), so the user sees the cluster the rule is tracking. (Daniel, 2026-09-04.)
  • Labels carry ! when range 2 printed a triangle/OB reading and X (dimmed) when range 2 later contradicted the divergence (2026-09-05).
  • Optional (default off) re-plot of ALL of T's tier triangles, tiny, so the full cluster is visible without the parent on the chart.
  • FIFO cap on labels + lines (input "Recent signals kept", default 12), same helper pattern as PBK's npKeepLbl.

Indicator pane:

  • zO line coloured by tier (grey / orange / red), ±thrElevO silver, ±thrExtO red, zero dotted — visually identical to PBK's pane so the user recognises it.
  • On lock, a tiny label in the pane at (peakBar, peakO) reading peak with the value; deleted when the cycle resolves or is abandoned.
  • On every signal, a divergence line in the pane from (peakBar, peakO) to (bar_index, zO), same colour and solid/dotted convention as the price-pane line, plus a label at (bar_index, zO) reading the signal name and the trigger price, so the divergence is visible on both panes. Both are FIFO-capped by "Recent signals kept", separately from the price-pane drawings. (Daniel, 2026-09-04.)

HUD table (top right, default on), one row per side: state name, bars in state, ref price, peak z, plus a header row showing the two resolved anchors ("T: Daily · O: Roll 24") and the chart-gate verdict.

Data window plots: zT, stateT, zO, bearState, bullState, refHigh, refLow, peakHi, peakLo, sig. All hidden from the pane (display.data_window). These are the export surface for verification.

Alerts (alertcondition): ZTD: regular bearish, ZTD: regular bullish, ZTD: hidden bearish, ZTD: hidden bullish, ZTD: cycle locked (zero touch) (either side).

2.5 Inputs summary

Group Input Default
Triangle engine (AVWAP-Z) Anchor mode Rolling (24-bar)
Threshold mode / Elev / Ext / calib / pctElev / pctExt Percentile / 2.0 / 2.5 / 2000 / 90 / 95
Triangle tier that arms Elevated
Oscillator engine (PBK) Anchor mode Rolling (24-bar)
Threshold mode / Elev / Ext / calib / pctElev / pctExt Percentile / 2.0 / 2.5 / 2000 / 90 / 95
Overbought/oversold tier Extreme
Shared Sigma lookback; Volume-weight; Swing pivot strength; Flip debounce 200; on; 5; 5
Rule Only fire on 1h / 4h / D charts on
Cycle timeout (bars) 120
Zero-line tolerance (z) 0.0
Display Show labels; Show lines; Show overlap triangles; Show all T triangles; Show HUD; Recent signals kept on; on; on; off; on; 12

No per-symbol settings. Anchor Auto resolves per chart timeframe exactly as in AVWAP-Z.

3. Non-repainting

Every decision uses the current bar's confirmed high, low, close, volume and the two z series computed from them. No request.security, no lookahead, no pivot-based labels placed on past bars except the reference line's start point, which is drawn from a bar whose values were fixed when it was recorded. Swing/Flip anchors inherit AVWAP-Z's retro-placement, which alters only future basis values. Replay must reproduce the exported series exactly (the established verifier).

4. Error handling

  • Warm-up: any na in zT, zO or thresholds forces that bar to skip arm/resolve; state is preserved but the timeout clock keeps running.
  • Missing volume (na / 0) falls back to weight 1.0, as in both parents.
  • Percentile calibration on short histories: ta.percentile_nearest_rank returns na until calibLen bars exist; the rule simply stays idle (same as AVWAP-Z's behaviour).
  • Label/line counts bounded by max_labels_count=200, max_lines_count=200 plus the FIFO cap.

5. Testing and verification

  1. Compile via the editor paste pipeline (ASCII-only source, Ctrl+Enter, real-mouse save).
  2. Engine agreement (gate): on EURUSD 1h with AVWAP-Z and PBK as configured on Daniel's chart (both Rolling-24, Percentile, sigma 200) on the chart, export zT, stateT, zO and the parents' Z-score, State, z plots through CDP (pyakao/validation/export_tv.py pattern) and diff. Pass = max abs difference < 1e-9 and identical state codes on every bar.
  3. Rule replication: feed the exported stateT, zO, high, low series into a ~60-line Python reference implementation of §2.3 and diff its signal series against the exported sig plot. Pass = identical. (DEFERRED 2026-09-04 by Daniel; the shipped gate is an invariant audit. An unshipped independent replay in the final review matched 0 mismatches on 21,150 bars.)
  4. Non-repaint: replay from three dates spanning at least one signal each; scraped sig and state plots at matched bars must equal the exported history.
  5. Eyeball: screenshot two RDIV and one HDIV example on EURUSD 1h; confirm the reference line starts at the cluster's highest triangle and the label sits on the breach bar.

No trade-outcome claims are made; this is a marker. Forward observation as with PBK.

6. Out of scope

  • Any strategy wrapper, backtest or expectancy measurement.
  • Reading the parents via source inputs (rejected, see §1).
  • AKAO oscillator (PBK's hidden kernel oscillator) — Daniel chose the visible z line.
  • Multi-timeframe evaluation from a lower chart; the rule runs on the chart's own timeframe and the gate simply refuses to fire elsewhere.
  • A pyakao port (would follow the existing zms/ pattern if wanted later).

Addendum 2026-09-15 — naked POC confluence (v22)

A TPO "naked POC zone" layer, ported and trimmed from akao_confluence_engine.pine's naked-POC module, adds confluence to the divergence marker above (ztd_divergence.pine only).

  1. Profiles. A Daily TPO profile from 30-minute blocks, a Weekly profile from H4 blocks; one TPO per block per price bucket its [low, high] touched. Bucket: auto (~1.5bp of price, 1-2-5 snap, frozen on the first bar) or manual. Value area = a configurable % of TPOs grown from the POC outward, tie → up. No Monthly profile.
  2. Block handling keys off timeframe.in_seconds(): chart TF shorter than the block runs a commit-then-reset tracker; chart TF equal to the block commits the bar's own high/low; chart TF longer than the block pulls completed intrabars via request.security_lower_tf.
  3. Naked lifecycle. A zone holds POC/VAH/VAL, a crossed flag, birth bar and drawing handles. It is crossed once a bar wicks through the POC by ≥ the cross-buffer ticks; a crossed zone stays live until its own timeframe's next boundary — never removed on the bar of the cross itself. Oldest zone drops once a timeframe exceeds its cap. Not ported: sticky side, stretch/extension, VA FAIL, fib marks, overlap boxes, stacked drawings.
  4. Confluence test. Each fired divergence tests the range-2 price extreme ZTD already compared (at the fire, and again on each "extended" update) against every live zone: inside a Daily/Weekly value area → hit D/W (tag nPOC-D / nPOC-W), both → the ASCII tag nPOC x2; a live POC within max(cross buffer, half a bucket) of the extreme adds a POC-touch flag (a * suffix). The result replaces the label's previous tag rather than stacking.
  5. Surfaces. Nearest N live zones per side draw as boxes with a dotted POC line (Daily/Weekly in distinct colours); a fifth HUD row reports nearest Daily/Weekly POC, live counts and the current tag; one data-window plot carries the hit code; one new alert fires on a confluent divergence. Two little-used intrabar data-window plots were retired to hold the total at the existing plot cap.
  6. Inputs. One new group, after the symbol-bias group and before the Advanced groups: the Daily/Weekly toggles, auto/manual bucket, the two value-area percentages, the cross buffer, the max-zones cap, the nearest-N-drawn count, and the draw toggle.