CAT researchOverviewEURUSDGBPUSDXAUUSDGBPCADEURCADAUDJPYGBPJPYResearch notes

ZTD 5m Entry Setups — design

Status: built and run 2026-09-06; verdict: FAIL (see research/2026-09-06-entry5m-screen.md). Date: 2026-09-06. Approach: 1 (pyakao first on five years of Tickstory data behind pre-registered criteria; a Pine strategy only if the rule survives).

1. Purpose

ZTD (AVWAP-Z x PBK Zero-Touch Divergence) is the bias layer: an H1 divergence fire says which way to trade and roughly how far. This project adds the entry layer Daniel described: on the 5m chart, wait for a pullback against the bias, then enter on the structure break the pullback formed, with the stop behind the pullback and the target at the H1 zero-touch extreme.

The question the project answers is narrow: does "ZTD H1 bias + any 5m pullback + pivot-break stop entry" make money on five years of data, under criteria written down before the run? Everything else (Pine strategy, alerts, sizing polish) is conditional on a pass.

Decisions already taken by Daniel (2026-09-06), reproduced so nothing drifts:

Question Decision
Setup shape Structure break after pullback, in the bias direction
Bias window From the divergence fire until target or contradiction (with a bar timeout)
Bias target The H1 zero-touch extreme between range 1 and range 2 ("the range-1 low/high")
Pullback level No level; any 5m pullback qualifies
5m mechanics Pivot break, stop order; stop-loss behind the pullback extreme; one entry per pullback
Order of work pyakao first, Pine second

2. Scope

In scope:

  • pyakao/src/pyakao/entry5m/: bias (H1 ZTD -> bias windows on the 5m clock), setup (5m pivots, pullbacks, orders), execution (fills, exits, R accounting), report (the pre-registered table), screen CLI.
  • Two small additions to the ZTD port (pyakao/src/pyakao/ztd.py): per-bar contradiction events and the zero-touch extreme, exported on ZtdRow.
  • Tests: hand-built 5m scenarios for every rule below, plus a no-lookahead guard.
  • The pre-registered criteria (section 8) committed before any five-year run.

Out of scope for this spec: the Pine strategy (section 10 sketches it; it gets its own short spec if section 8 passes), the 15m variant (same code with a different resample, run only as a secondary report), trailing stops, pyramiding, session filters, the ZTD Pine script itself (untouched).

3. Data

  • Source: Tickstory MT5 minute bars at D:/tickstory/History data/<SYMBOL>_mt5_bars.csv, loaded with pyakao.feeds.tickstory.load_tickstory(path, timeframe="5m"), the same loader and server-offset handling the ZMS screen uses.
  • Instruments and their role:
Symbol Years Role
EURUSD 5 (2021-03 .. 2026-03) primary
GBPUSD 5 reported, but counted as the same test as EURUSD (correlated USD pair)
XAUUSD 5 independent market
EURJPY 5 non-USD control
  • EURGBP, the control named in the ZMS study, is unusable: the Tickstory file holds four hours of data (240 minute bars). EURJPY replaces it. No equities or other gapping instruments.
  • H1 bars are aggregated from the 5m bars with pyakao.zms.htf.build_htf(bars, "60"), which also indexes the last CLOSED H1 bar for every 5m bar. The bias only ever reads closed H1 bars.

4. Bias layer (entry5m/bias.py)

Input. The H1 bar series and a ZtdConfig (defaults, tf_ok=True). ZtdEngine.update is driven once per closed H1 bar.

ZTD port additions (in ztd.py). ZtdRow gains four fields the Pine script computes but does not plot:

  • contraB, contraU (int 0/1): the "contradicted now" events (bear_contra_now / bull_contra_now, already inside ZtdRuleOut).
  • midLoB, midHiU (float, na when not applicable): the zero-touch extreme. midLoB is the lowest bar low seen while the bear side is in state 2 (locked: after the zero touch, before range 2 opens), reset when the side leaves state 2 for state 1 or 0; carried unchanged through state 3 so it is readable on the fire bar. midHiU mirrors with highs. The golden test keeps comparing only the exported plot titles, so these extra fields do not disturb parity.

Bias record. Bias(side, start_h1, start_5m, ref, ext, target, deadline_h1, kind):

  • side: -1 for sigBear (RDIV-/HDIV-), +1 for sigBull.
  • ref: fRefB / fRefU (range-1 extreme), informational.
  • ext: the range-2 extreme fCurB / fCurU; updated when the port reports an extension (ext != 0 on the same side).
  • target: midLoB for a short bias, midHiU for a long bias, read on the fire bar. If it is na (cannot happen after a valid fire, but guard it) the fire is ignored and counted in the report as "no target".
  • kind: RDIV when abs(sig) == 2, else HDIV (same code as the combo study).
  • deadline_h1: fire bar index + timeout_h1 (default 96). Every extension on the same side sets deadline_h1 = ext bar index + timeout_h1. Extensions do not restart the bias or move the target.

Activation on the 5m clock. A bias fired on H1 bar k becomes active on the first 5m bar whose open time is at or after the close time of H1 bar k. Identical for every later H1 event (contradiction, timeout, opposite fire): the event is known at the H1 close and takes effect on the next 5m bar.

End of a bias, whichever comes first:

  1. Target. A 5m bar trades through the target (short: low <= target; long: high >= target). Ends at that bar's close (any open trade was already exited at the target inside that bar, section 6).
  2. Contradiction. contraB / contraU on the bias side at an H1 close.
  3. Timeout. The H1 bar index passes deadline_h1.
  4. Opposite fire. A sigBull while a short bias is active replaces it (and vice versa). The new bias starts normally.

One bias per side at a time; a same-side fire while that side is active replaces the record (new ref, ext, target, deadline). The report counts how each bias ended.

5. Setup layer (entry5m/setup.py)

All rules stated for a short bias; the long side mirrors every high/low, min/max and inequality. Bars are closed 5m bars; nothing looks at bar i+1 when deciding on bar i.

Pivots. pivot_high(N) at bar p: high[p] is greater than or equal to the highs of the N bars before it and strictly greater than the highs of the N bars after it (Pine ta.pivothigh(N, N) semantics, validated 2026-09-07); it is confirmed at bar p+N. Default N = 3. Pivot lows mirror.

Pullback. A confirmed pivot high at bar p is a pullback for the short bias if:

  • p >= the bias start bar (the pullback formed inside the bias), and
  • the origin low exists and is old enough: origin = argmin(low[q..p]) where q is the bar after the previous confirmed pivot high since the bias started, or the bias start bar if there is none, and p - origin >= min_pb_bars (default 3).

Origin low = the swing the pullback formed; its break is the structure break Daniel trades.

Order. On the close of the confirmation bar c = p+N:

  • entry = sell stop at low[origin] - tick;
  • stop-loss = high[p] + tick;
  • target = the bias target;
  • reward-to-risk = (entry - target) / (stop - entry); if < min_rr (default 1.0), no order (counted as "rr rejected");
  • if low of any bar in (p, c] is already <= entry the level is gone: no order (counted as "already broken").

Pending-order life. One pending order at a time. It is cancelled when:

  • a bar's high > high[p] (pullback extreme taken out) before the fill;
  • the bias ends;
  • a newer qualifying pullback confirms (it replaces the order: tighter stop, same or higher entry);
  • pending_max_bars (default 48 5m bars = 4 hours) elapse without a fill.

Position. One open position per instrument. While a position is open no order is placed; pullbacks that confirm meanwhile are counted as "skipped, in position". After an exit the bias, if still active, can produce the next entry from the next pullback.

6. Execution (entry5m/execution.py)

A small purpose-built broker, not the ZMS Broker (it carries ZMS-specific pyramiding, ratchets and sizing modes that this study does not use).

  • Fill. A pending sell stop fills on the first bar with low <= entry. Fill price = min(open, entry) - slip (a gap below the entry fills at the open). slip = slippage_ticks * mintick from pyakao.feeds.base.spec_for(symbol), the same constants ZMS used.
  • Same-bar conflict. If the fill bar's high >= stop, the trade is a loss at the stop (conservative, same as the combo study). If a later bar hits both stop and target, it is a loss.
  • Exits. Stop: high >= stop, exit at stop + slip. Target: low <= target, exit at target (limit, no slippage). Bias end (contradiction, timeout, opposite fire): exit at the close of the 5m bar on which the end takes effect, minus slip. Time stop: max_hold_bars (default 0 = off) bars after the fill, exit at close. A pending order may still fill during the 5m bar on which an H1 bias end takes effect; the position is then closed at that bar's close as a bias-end exit.
  • Accounting. Every trade records entry, exit, prices, bars held, reason, and R = pnl / (stop - entry) in risk units, plus MFE and MAE in R. Money accounting is a fixed 1 R = 1% of a notional 10,000 account, only to feed the existing ZMS metric helpers; R is the unit that is judged.
  • No lookahead. Orders placed at bar c can fill at bar c+1 at the earliest.

7. Report and screen (entry5m/report.py, entry5m/screen.py)

python -m pyakao.entry5m.screen --symbols EURUSD,GBPUSD,XAUUSD,EURJPY runs the primary cell on each symbol and prints one table with, per symbol:

  • biases: total, by kind (RDIV/HDIV), by end reason (target / contradiction / timeout / replaced), "no target";
  • pullbacks: qualifying, rr rejected, already broken, skipped in position, cancelled (by reason), filled;
  • trades: count, wins, losses, win rate, net R, expectancy R, PF, ex-outlier PF (drop the three best trades), max drawdown in R, best month share of net R and which month, time-split halves (net R of each half), average bars held;
  • the verdict against section 8.

A second table, clearly labelled sensitivity, not judged, repeats net R / PF / trades for N in {2, 3, 5} and min_rr in {1.0, 1.5}. It exists to show fragility, not to pick a cell.

--tf 15 reruns the same code on a 15m resample as a secondary report.

8. Pre-registered criteria (binding; committed before any five-year run)

The primary cell is: N = 3, min_pb_bars = 3, timeout_h1 = 96, min_rr = 1.0, pending_max_bars = 48, max_hold_bars = 0, ZTD defaults. Nothing in this cell is tuned after seeing results.

The rule passes only if ALL of the following hold on at least two of the three independent groups {EURUSD+GBPUSD (judged on EURUSD; GBPUSD net R must not be negative), XAUUSD, EURJPY}:

  1. at least 200 trades over the five years;
  2. PF > 1.15 and ex-outlier PF > 1.0;
  3. expectancy > +0.05 R per trade after slippage;
  4. best month <= 35% of net R;
  5. both time-split halves net-positive.

Anything else is a fail, and the rule as specified is dead. A fail is not followed by a parameter search; if Daniel wants a variant, it gets its own pre-registration before running. The sensitivity table is reported either way.

9. Testing

  • Unit tests with hand-built 5m bar sequences (small integer prices, easy to reason about) for: pivot confirmation timing; pullback qualification (origin age, previous-pivot boundary, bias-start boundary); order placement price and rr rejection; "already broken"; each cancel reason; fill at open on a gap; same-bar stop conflict; every exit reason; bias activation offset (fire on H1 k -> first 5m bar after its close); each bias end reason; extension moves the deadline but not the target; opposite fire replaces.
  • ZTD port additions: a test that midLoB equals the minimum low over the state-2 bars for every fire in a synthetic run, and that contraB is set exactly on bars where flagBear later reports the contradiction bit.
  • Golden test unchanged (extra ZtdRow fields ignored by design).
  • No-lookahead guard: shifting the bar series forward by one bar must not change any decision made before the shift point; a random-walk run with the same seed must produce identical trades whether fed bar-by-bar or all at once.
  • Whole-package test count grows from 202; all green before the screen runs.

10. After a pass: the Pine strategy (sketch only)

A strategy script in entry5m/ on the 5m chart that computes the ZTD H1 core through request.security on "60" and reproduces sections 4-6 with strategy.entry(... stop=...) and strategy.exit. It would be verified against pyakao's trade list bar-for-bar on the TradingView window before any live use. Not started until section 8 passes.

11. Costs and known limits

  • Tickstory bars carry tick volume, as TradingView's FX feeds do; ZTD keeps its use_volume default. The two feeds still differ bar by bar, so any parity check between pyakao and TradingView on H1 must use the CDP export, as the ZTD golden test does.
  • XAUUSD has a daily maintenance gap; the gap-at-open fill rule (section 6) handles it.
  • Five years of 5m bars per symbol is about 370k bars; the run must stay under a few minutes per symbol (pure Python, one pass).
  • Stop-loss exits are modelled at the stop price plus slippage even when a bar opens beyond the stop; only entries model the gap. This flatters results slightly and cannot have produced a fail.