CAT researchOverviewEURUSDGBPUSDXAUUSDGBPCADEURCADAUDJPYGBPJPYResearch notes

ZTD Entry Indicator (Pine) — design

Status: built 2026-09-06/07, extended 2026-09-07 to ZTD Entry v8.0 (entity 3wowED); verified: H1 gate PASS 0/25,541, chart parity PASS 0/0 on both exports, 3 replay checks matched; arm-tier gate PASS (0 mismatches on all 9 pairs, 39,122 H1 bars compared), arm parity PASS (21 biases, 80 orders, 31 trades, 0 mismatches after the sub-tick tie allowance; 21 range_end and 0 replaced bias ends in the window); v8.0 opposite-side-HTF-triangle exit gate PASS (0 mismatches on all 11 pairs incl. the new htfTriB/htfTriU, 39,138 H1 bars) and parity with the input ON PASS (0/0, 13,782 5m bars, 3 exitCode 5 events in the window). Date: 2026-09-06. Sub-project: entry5m/ (the pyakao harness is the executable reference for every rule below).

1. Purpose

A Pine v6 indicator with alerts for the 5m or 15m chart that reproduces the entry5m harness on TradingView: the H1/H4 ZTD bias (divergence fire, or the extreme-triangle arm), the 5m/15m pullback and pivot-break stop order, the stop at the HTF extreme or the pullback swing, a take-profit at a settable R:R, and an optional half-off at a settable R with a breakeven stop. It draws every level, tracks a simulated position so alerts can say what to do next, and exposes a data-window surface for parity checks against pyakao.

Not a strategy: no tester, no strategy.* calls. Verification is signal parity (bias, order levels, simulated fill bars), not trade-for-trade P&L.

Decisions taken by Daniel (2026-09-06): indicator with alerts; rule-level inputs only, ZTD engines at v17 defaults; signal-parity verification.

2. Architecture

ztd_divergence.pine  --(entry5m/tools/make_entry_core.py)-->  f_ztd() core  (embedded, marked ENTRY_CORE_BEGIN/END)
                                                                  |
                       request.security(tickerid, biasTf, f_ztdPrev(), lookahead_on)   -> closed-HTF-bar surface
                                                                  |
chart (5m/15m):  bias tracker  ->  pivots / pullbacks / order pricing  ->  simulated position  ->  drawings, HUD, alerts, data window
  • Core generation. make_entry_core.py reads ztd_divergence.pine, keeps the shared blocks, engines T and O, rule atoms and both state machines (the region between the shared blocks and drawings headers), drops drawings, HUD, alerts and pane code, wraps the result in f_ztd() returning a tuple, and writes it between // ENTRY_CORE_BEGIN / // ENTRY_CORE_END markers in entry5m/pine/ztd_entry.pine, preserving everything outside the markers. Idempotent; fail-loud asserts on the anchors it expects (the harmonic transform is the model). The ZTD inputs the core needs become script-level constants equal to the v17 defaults (intrabar = true, refMode = "Until superseded", maxSigPerRef = 1, timeoutBars = 240, cancelOpp = true, zeroRead = "Close", zeroTol = 0, oObTier = "Extreme", both engines Rolling (24-bar) / Percentile 90/95 / calib 2000 / sigma 200 / volume on), except tMinTier, which follows the arm-tier input in arm mode and is "Elevated" in divergence mode (as v17).
  • HTF evaluation. f_ztdPrev() calls f_ztd() and returns each element shifted by one bar; request.security(syminfo.tickerid, biasTf, f_ztdPrev(), lookahead = barmerge.lookahead_on) therefore yields the last CLOSED HTF bar on every chart bar, with no repainting on history or in real time (the idiom documented in pyakao/src/pyakao/zms/htf.py). A new HTF bar is detected as htfNew: the one-bar-shifted time is carried through the SAME request.security tuple, and htfNew is true on the chart bar where that value changes. The tracker therefore steps exactly once per closed HTF bar, tied to the security series itself rather than to the chart clock (timeframe.change would key off the chart's own bar boundaries and can fire on a bar the security series has not yet advanced past). This matches the harness: an H1 event takes effect on the first 5m bar that opens at or after the H1 close.
  • Surface returned by f_ztd() (all float, ints carried as floats): sigBear, sigBull, contraB, contraU, midLoB, midHiU, curHi, curLo, fRefB, fRefU, ovl, ext — the same names as ZtdRow fields the pyakao BiasTracker reads.

3. Inputs (rule level only)

Group Input Default Notes
Bias Bias timeframe 60 options 60, 240
Bias Bias source Divergence options Divergence, Extreme arm
Bias Arm triangle tier Extreme options Extreme, Elevated; used in arm mode only
Bias Timeout (bias timeframe bars) 96
Entry Pivot strength 3 ta.pivothigh/low(n, n)
Entry Minimum pullback age (bars) 3 origin must be this many bars before the pivot
Entry Pending order life (bars) 48
Entry Minimum reward-to-risk 1.0
Stop Stop at HTF extreme options HTF extreme, Pullback swing
Target Target Fixed R:R options Fixed R:R, Zero-touch level (the latter only in divergence mode; arm mode forces Fixed R:R)
Target Reward-to-risk 3.0
Target Exit on opposite-side HTF triangle off v8.0; the v7 rule of docs/2026-09-07-entry5m-v7-opptri-prereg.md, in the "Stop and target" group
Scale-out Take partial off
Scale-out Partial size (fraction) 0.5
Scale-out Partial at R 1.0 must be below the R:R
Scale-out Move stop to breakeven after partial on
Display Show bias, orders, position, HUD; label/line budget (keep last N, default 60)
Alerts One toggle per event class (bias, order, fill, partial, breakeven, exit) on

The entry timeframe is the chart. If the chart is not 5m or 15m the HUD shows a warning and nothing else changes (the rules still run).

4. Bias tracker (chart-level port of pyakao/src/pyakao/entry5m/bias.py)

State (var): biasSide (0 none, −1 short, +1 long), biasKind ("RDIV", "HDIV", "ARM"), biasStartBar (chart bar index), biasExt (HTF extreme level; the stop reference in HTF-extreme mode), biasTarget (zero-touch level, na in arm mode), biasDeadline (HTF bar count), biasId.

On each chart bar where htfNew is true (the shifted security time changed; see section 2), using the HTF surface:

  • Divergence mode (bias.py::on_h1_close): contradiction on the bias side ends it; a same-side ext moves biasExt to curHi/curLo and renews the deadline; timeout ends it; a fire (sigBear/sigBull) with a non-na midLoB/midHiU replaces any active bias and starts a new one (ext = curHi/curLo, target = midLoB/midHiU, kind RDIV when abs(sig) == 2 else HDIV); a fire with a na target is ignored.
  • Arm mode (bias.py::_on_h1_arm): the bias ends when its side's curHi/curLo is na (range end); otherwise biasExt follows the running extreme, then timeout; ovl != 0 on the other side (or with no bias) starts a new ARM bias with ext = curHi/curLo and na target; a same-side ovl is ignored; an ovl whose running extreme is na is skipped.
  • Every chart bar (runner.py): in divergence mode the bias ends with reason target when the bar trades through biasTarget.

A bias end closes the simulated position at the bar close (reason bias_end) and cancels the pending order; a bias start resets the pullback boundary.

5. Pullbacks and orders (port of setup.py)

  • Pivots via ta.pivothigh(high, n, n) / ta.pivotlow(low, n, n); a pivot confirmed on bar c sits at c - n. Short bias uses pivot highs, long bias pivot lows. Tie rule: an equal high among the left neighbours is allowed, among the right neighbours it is not (Pine semantics; pyakao.pinets._Pivot matches since 2026-09-07).
  • Pullback qualification, origin search (argmin low / argmax high from the bar after the previous confirmed same-kind pivot since the bias start, ties to the earliest bar), the age rule, order pricing (entry one tick beyond the origin; stop one tick beyond the pullback extreme or biasExt; target = biasTarget or entry ± R:R × risk), the "already broken" check over (p, c], and the reward-to-risk floor are exactly setup.py::PullbackScanner. Rejections are counted in the data window (rrRejected, alreadyBroken, tooShort).
  • One pending order (var fields: entry, stop, target, cancel level, placed bar, pullback bar, origin bar). A newer qualifying pullback replaces it; it is cancelled when price trades through the cancel level (pullback extreme, or biasExt in HTF-extreme mode), at bias end, or after the pending life.
  • No order while the simulated position is open (skippedInPosition counter).

6. Simulated position (port of execution.py)

Per chart bar, in the harness order: (1) open position: effective stop, then the partial level (entry ± partialR × risk, sets the half off and, if enabled, moves the effective stop to the fill price from the next bar), then the target, then nothing else (no time stop in the indicator); (2) pending order: fill when the bar trades through the entry (fill price = the entry, or the open when the bar gaps through it; no slippage model in the indicator), then the same stop/partial/target sequence on the fill bar; (3) cancellation checks. A bar that hits the stop and a favourable level is a stop. The position records fill bar and price, quantity (1 or 1 − fraction), partial price and bar, and the running R (size-weighted after the partial), which the HUD shows and the exit alert reports. Slippage is deliberately not modelled: the indicator reports levels, not P&L.

  • Opposite-side HTF triangle exit (v8.0, input off by default). When the input is on, a bias is active and a position is open, a new closed bias-timeframe bar whose ZTD core prints a triangle at the Elevated tier or above on the side opposite the trade (htfTriU for a short, htfTriB for a long) closes the position at the chart bar's closeexitCode 5, reason opp triangle, R computed exactly as the bias-end cleanup does. It is evaluated inside the htfNew block before the bias contradiction/extension/timeout checks, mirroring bias.py::on_h1_close, which emits ("opp_tri", bias) ahead of that bar's end/start events and only while tracker.active is set. It is an exit only: the bias is untouched and a pending order stays live and may still fill later in the same bias. The harness equivalent is broker.close_at(i, t, close, "opp_tri") with counter exit_opp_tri; the indicator has no slippage, so both close at close.

7. Drawings, HUD, alerts, data window

  • Drawings (budgeted, keep-last-N like ZTD): bias line at biasExt from the bias start bar, dashed, coloured by side, ending at the bias end; target line at biasTarget (divergence mode); pending entry line (dotted) with its stop and target; on fill, solid entry, stop (moves to breakeven), partial and target lines; small labels at bias start ("ZTD bias ▼ RDIV / ARM"), order placed ("stop sell @ …"), fill, partial, exits. Colours mirror ZTD (bear red, bull green).
  • HUD (top right): chart-TF warning, bias timeframe and source, bias side/kind/age and its stop reference, pending order levels or "none", position state with quantity and running R. One table cell set.
  • Alerts: alert() with a dynamic message on bar close for each event, gated by the toggles: ZTD ENTRY bias start <side> <kind> stop-ref <px> [target <px>], bias end <reason>, order placed <side> entry <px> stop <px> target <px> cancel <px>, order cancelled <reason>, filled <px>, partial <px> qty <frac>, stop to breakeven <px>, exit <reason> <px> R <r>. Plus one alertcondition "ZTD ENTRY: any event" for users who prefer the classic dialog.
  • Data window (hidden plots, parity surface): htfNew (1 on the chart bar that consumed a new HTF bar), htfSigBear, htfSigBull, htfOvl, htfCurHi, htfCurLo, htfMidLoB, htfMidHiU, htfContraB, htfContraU, htfExt, htfTriB, htfTriU (v8.0), biasSide, biasKind (1 RDIV / 2 HDIV / 3 ARM), biasStartBar, biasExt, biasTarget, biasEndCode, biasStartNow, barIndex, pendEntry, pendStop, pendTarget, pendCancel, pendPlacedBar, orderPlaced, orderCancelCode, posQty, posFill, posFillBar, posStop, partialPx, fillNow, partialNow, exitCode (1 stop / 2 target / 3 bias end / 4 breakeven / 5 opp triangle), exitPx, tradeR, tooShort, rrRejected, alreadyBroken, skippedInPos. 42 plots + 1 alertcondition, under the 64-output budget.

8. Verification (signal parity)

  1. HTF gate. On an H1 chart the generated core's surface must match ZTD v17's data window bar for bar (the existing pyakao/validation/compare_ztd_tv.py flow with the export tooling, --match on both scripts). Divergence mode compares sigBear/sigBull/curHi/curLo/midLoB/midHiU (the latter two against the pyakao port, since v17 does not plot them); arm mode compares ovl with tMinTier = Extreme against a ZTD instance set to that tier.
  2. Chart-level parity. Export the 5m chart's bars plus the indicator's data-window plots over the whole TradingView window (harmonic/validation/export_tf.py pattern, --tf 5 --match "ZTD Entry"), feed the exported 5m bars to run_entry5m with the matching config (slippage_ticks = 0, the same inputs), and compare: bias starts (chart bar, side, kind, ext, target), bias ends (bar, reason), pending orders (placed bar, entry, stop, target, cancel), fills (bar, price), partials, exits (bar, reason). Pass = zero mismatches on bias and order levels; fills may differ only where Pine's HTF alignment differs from build_htf (documented per case). entry5m/validation/compare_entry_tv.py prints the audit.
  3. Replay spot checks on three events (a divergence bias, an arm bias, a partial + breakeven) with the existing replay tooling.
  4. Editor pipeline, compile, save and instance handling as recorded in the TradingView MCP quirks memory.

Done, 2026-09-07 (v7.0, entity 3wowED, FX:EURUSD).

  • Divergence-mode H1 gate (data_entry, 25,541 H1 bars): RESULT: PASS, 0 mismatches on all nine pairs — htfSigBear/htfSigBull/htfOvl/htfExt/htfCurHi/htfCurLo/htfContra Pine-vs-Pine over 25,540 compared bars (from bar 1) and htfMidLoB/htfMidHiU against the port over 23,141 (from bar 2400).
  • Arm-mode H1 gate (data_entry_arm, 41,522 H1 bars, ZTD v17 Triangle tier that arms = Extreme, indicator Bias source = Extreme arm): RESULT: PASS, 0 mismatches on the same nine pairs, ovl included, over 39,122 compared bars. That export reaches the studies' first bar, so the Pine-vs-Pine gates also need the 2400-bar floor there (--first-pine 2400): the generated core carries make_entry_core.py's deliberate percentile-window cap (math.min(tCalib, tZBars)) that v17 lacks, so while cold the core produces tiers from bar ~223 and v17 only from ~1999. Every mismatch on that export sat in bars 223-2116 and none after.
  • Arm-mode chart parity (data_entry_arm 5m, 40,291 bars, --first 26700 → 13,591 compared bars): pyakao's run over the export holds 21 biases, 81 orders and 32 trades (80/31 once the tie below is allowed for); one bias starting at bar 26653 and the order it placed at 26668 fall before the --first floor and are therefore not compared. Inside the window: 0 mismatches on biases, bias ends, order levels, fills, partials and exits — after allowing a sub-tick tolerance on the two price comparisons that decide a fill and the already-broken test (see below). The window holds 21 biasEndCode 5 (range_end) events and 0 code 4 (replaced) events; an arm bias here always ends because its side's running extreme goes na before the opposite side can arm.
  • Known numerical difference, not a rule difference. Run strictly (no tolerance) the arm parity reports 1 bias/order and 4 fill/exit mismatches at bars 36822, 36825, 40214, 40219, 36833. All of them are decided by operands that differ by exactly one ULP (~2e-16, i.e. 1/50,000,000 of a pip): bar 40214 high = 1.16231 (3ff298d2_5edd0529) against pendEntry = 1.1623100000000002 (…052a), and bar 36821 low = 1.16748 (3ff2adff_822bbecb) against entry = 1.1674799999999999 (…beca). Pine resolves both as touching, Python's IEEE comparison does not. Bit patterns of the bars themselves are identical between TradingView and the export, and pendEntry/pendStop match Python's arithmetic bit-for-bit, so this is the comparison, not the data or the port. Controlled confirmation: adding 1e-9 (far below the 1e-5 tick, far above one ULP) to exactly those two comparisons in execution.py and setup.py — nothing else — turns the run into 0/0; the patch was reverted and is not committed. The Pine was not changed: it expresses the same rule as setup.py.

Done, 2026-09-07 (v8.0, the opposite-side HTF triangle exit, entity 3wowED updated in place, 42 plots + 1 alertcondition).

  • H1 gate (data_entry, fresh 41,538-bar H1 export, --first-pine 2400 since it reaches the studies' first bar): RESULT: PASS, 0 mismatches on all eleven pairs over 39,138 compared bars — the nine existing ones plus htfTriB and htfTriU, both against the pyakao port's ZtdRow.triB/triU from bar 2400 (ZTD v17 does not plot the triangle atoms).
  • Chart parity with the input ON (data_entry_opptri, 40,482 5m bars, --first 26700 → 13,782 compared bars, compare_entry_tv.py --exit-opp-tri running the port with Entry5mConfig(exit_on_opp_tri=True)): RESULT: PASS, 0 bias/order and 0 fill/exit mismatches; pyakao's run holds 8 biases, 55 orders and 19 trades. The window contains 3 exitCode 5 events (2026-07-16 18:00, 2026-09-02 14:00, 2026-09-03 08:00 UTC), 13 over the whole export. The 1-ULP ties recorded below did not recur on this export.
  • Replay spot check on the 2026-09-03 08:00 UTC exitCode 5 bar (data window read twice, both reads identical): htfNew 1, htfTriB 1, htfTriU 0 on a LONG bias, exitCode 5, exitPx 1.160380 — exit code and price match the export exactly. tradeR read 0.8301 versus the export's 0.8341 because the replay session's H1 running low came back one tick lower (biasExt 1.156620 vs 1.156630), which shifts the stop and the risk denominator; a feed-depth difference in the H1 leg between the two sessions, not a difference in the rule.

9. Scope and limits

  • In: the indicator, the transform, the two validation scripts, guide section 13, README rows, memory.
  • Out: a strategy() version, slippage or P&L modelling, sessions, the 15m as anything other than "the chart", changes to ztd_divergence.pine (untouched).
  • Known limits, stated now: TradingView's 5m history (a few months) bounds the parity window; the H4 bias is computed but only parity-checked on H1; request.security on an expression with var state re-evaluates the whole H1 series once per compile (acceptable; the ZMS strategy does the same).