For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: A Pine v6 indicator with alerts for the 5m/15m chart that reproduces the entry5m harness (H1/H4 ZTD bias, pullback stop orders, HTF-extreme or pullback stop, R:R target, optional scale-out) with signal parity against pyakao.
Architecture: A transform generates the ZTD v17 core as a function f_ztd() inside entry5m/pine/ztd_entry.pine; the chart evaluates it on the bias timeframe through request.security with a one-bar shift (closed HTF bars only). Chart-level Pine ports bias.py, setup.py and execution.py (no slippage) in the harness's per-bar order, draws the levels, keeps a HUD, fires alert() messages, and exposes a data-window surface that two Python scripts compare with ZTD v17 and with run_entry5m on exported bars.
Tech Stack: Pine Script v6 on TradingView Desktop (CDP :9222, tradingview MCP), Python 3.13 (pyakao, pytest), the CDP export tooling in pyakao/validation/ and harmonic/validation/.
Spec: entry5m/docs/2026-09-06-ztd-entry-indicator-design.md (binding).
ztd_divergence.pine is NOT modified. The generated core lives between // ENTRY_CORE_BEGIN and // ENTRY_CORE_END in entry5m/pine/ztd_entry.pine and is only ever produced by entry5m/tools/make_entry_core.py (idempotent; never hand-edit inside the markers).intrabar = true, refMode = "Until superseded", maxSigPerRef = 1, timeoutBars = 240, cancelOpp = true, zeroRead = "Close", zeroTol = 0.0, oObTier = "Extreme", both engines "Rolling (24-bar)", "Percentile", elev 2.0 / ext 2.5 (fixed-mode values, unused), calib 2000, percentiles 90 / 95, sigmaLen = 200, useVolume = true, pivLen = 5, flipMin = 5, tfAny = true. tMinTier = the arm-tier input in arm mode, "Elevated" in divergence mode.f_ztdPrev() shifts every core output by one bar; request.security(..., lookahead = barmerge.lookahead_on)); every chart-level decision on bar i uses bars <= i. Per-bar order on the chart, exactly the harness (runner.py): (1) open position: stop, partial, target; pending order: fill then the same sequence, else cancel checks; (2) HTF events: ends first (close the position at the bar close, cancel the order, reset the pullback boundary), then a start; (3) divergence mode: bias ends with reason target when the bar trades through the zero-touch level; (4) pivot scan, order pricing, placement only while a bias is active and no position is open.pyakao/src/pyakao/entry5m/{bias,setup,execution}.py at HEAD; when this plan's Pine and those files disagree, the Python wins and the Pine is fixed.max_labels_count=500, max_lines_count=500, keep-last-N pruning (default 60).C:\Users\Danielshobhan\.claude\projects\D--vwap-tpo-pine\memory\tradingview-mcp-quirks.md): never call tv_launch; paste through the clipboard .ps1 + focused Monaco textarea; verify line counts; compile with Ctrl+Enter and read pine_get_errors (severity 8 only are errors); save with Ctrl+S and verify the version through pine-facade; if the bound instance is not updated in place, remove it and re-add via the editor's "Add to chart" button; indicator_set_inputs only through ui_evaluate setInputValues with the changed ids. Do not add or remove studies Daniel placed on the chart beyond what a task names; say what you added.Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>; never stage planning/, pyakao/planning/, *.bak, .superpowers/, or export JSON (entry5m/validation/data_entry/ is git-ignored in Task 5).D:\vwap tpo pine\pyakao with python -m pytest -q (baseline 300 passing).| File | Responsibility |
|---|---|
entry5m/tools/make_entry_core.py (create) |
Generates the f_ztd() core from ztd_divergence.pine into the marked region of the indicator |
entry5m/tools/tests/test_make_entry_core.py (create) |
Transform tests (region, constants, hooks, cap, idempotence) |
entry5m/pine/ztd_entry.pine (create) |
The indicator: inputs, core (generated), HTF surface, bias tracker, orders and position, drawings, HUD, alerts, data window |
entry5m/validation/compare_core_tv.py (create) |
HTF gate: the indicator's htf* plots on an H1 chart against ZTD v17's plots shifted one bar, and midLoB/midHiU against the pyakao port |
entry5m/validation/compare_entry_tv.py (create) |
Chart-level parity: indicator data window vs run_entry5m on the exported chart bars |
pyakao/src/pyakao/entry5m/runner.py (modify) |
Entry5mResult.orders log (placed orders) for the parity script |
pyakao/tests/test_entry5m_runner.py (modify) |
test for the orders log |
entry5m/validation/.gitignore (create) |
ignores data_entry/ |
USER_GUIDE_indicators.md (modify) |
section 13: ZTD Entry |
README.md, entry5m/README.md (modify) |
script row / sub-project entry |
memory project-state.md, indicator-suite.md (modify) |
outcome paragraph |
Files:
entry5m/tools/make_entry_core.py, entry5m/tools/tests/test_make_entry_core.py, entry5m/pine/ztd_entry.pine, entry5m/validation/compare_core_tv.py, entry5m/validation/.gitignoreztd_divergence.pine, harmonic/tools/make_hpb_core.py (the model), pyakao/validation/export_tv.py, harmonic/validation/export_tf.py, pyakao/validation/compare_ztd_tv.pyInterfaces:
Produces: f_ztd() returning the 12-tuple [sigBear, sigBull, contraB, contraU, midLoB, midHiU, curHi, curLo, fRefB, fRefU, ovl, ext] (all float; curHi/curLo na outside an open range; ovl 1 bear / -1 bull / 0; ext 1 bear / -1 bull / 0); f_ztdPrev() = the same shifted by one bar; chart-level series hSigBear, hSigBull, hContraB, hContraU, hMidLoB, hMidHiU, hCurHi, hCurLo, hRefB, hRefU, hOvl, hExt, hTime and bool htfNew; the input variables named in Step 3; data-window plots htfNew, htfSigBear, htfSigBull, htfOvl, htfCurHi, htfCurLo, htfMidLoB, htfMidHiU, htfContraB, htfContraU, htfExt.
[ ] Step 1: Write the failing transform tests (entry5m/tools/tests/test_make_entry_core.py)
"""make_entry_core.py: the ZTD v17 core wrapped as f_ztd() for the entry indicator."""
import re
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[3]
TOOL = ROOT / "entry5m" / "tools" / "make_entry_core.py"
DST = ROOT / "entry5m" / "pine" / "ztd_entry.pine"
SRC = ROOT / "ztd_divergence.pine"
sys.path.insert(0, str(TOOL.parent))
import make_entry_core as mec # noqa: E402
def core_text():
text = DST.read_text(encoding="utf-8")
a, b = text.index(mec.CORE_BEGIN), text.index(mec.CORE_END)
return text[a:b]
def test_transform_runs_and_is_idempotent():
before = DST.read_text(encoding="utf-8")
subprocess.run([sys.executable, str(TOOL)], check=True)
once = DST.read_text(encoding="utf-8")
subprocess.run([sys.executable, str(TOOL)], check=True)
twice = DST.read_text(encoding="utf-8")
assert once == twice
# everything outside the markers is untouched
head_b, tail_b = before.split(mec.CORE_BEGIN)[0], before.split(mec.CORE_END)[1]
head_o, tail_o = once.split(mec.CORE_BEGIN)[0], once.split(mec.CORE_END)[1]
assert head_b == head_o and tail_b == tail_o
def test_core_is_the_ztd_region_wrapped_in_a_function():
core = core_text()
assert "f_ztd() =>" in core
src = SRC.read_text(encoding="utf-8")
region = src[src.index(mec.REGION_START): src.index(mec.REGION_END)]
# a few state-machine lines must be present, indented by four spaces
for line in ("bool bearIn = ready and zOzb > zeroTol", "if bearRng and zeroAbove", "bool bullLockedNow = bullState == 2 and prevBullState != 2"):
assert line in region and ("\n " + line) in core
# and nothing from the drawings section
assert "label.new" not in core and "line.new" not in core and "plot(" not in core and "alertcondition(" not in core
def test_core_constants_are_the_v17_defaults():
core = core_text()
for const in ('bool intrabar = true', 'string refMode = "Until superseded"', 'int maxSigPerRef = 1',
'int timeoutBars = 240', 'bool cancelOpp = true', 'string zeroRead = "Close"',
'float zeroTol = 0.0', 'string oObTier = "Extreme"', 'string tAnchor = "Rolling (24-bar)"',
'string oAnchor = "Rolling (24-bar)"', 'string tThrMode = "Percentile"', 'string oThrMode = "Percentile"',
'int tCalib = 2000', 'int oCalib = 2000', 'float tPElev = 90.0', 'float tPExt = 95.0',
'float oPElev = 90.0', 'float oPExt = 95.0', 'int sigmaLen = 200', 'bool useVolume = true',
'bool tfAny = true', 'string tMinTier = armMode ? armTier : "Elevated"'):
assert const in core, const
assert "input." not in core
def test_core_hooks_percentile_cap_and_return_tuple():
core = core_text()
assert core.count("var float midLoB = na") == 1 and core.count("var float midHiU = na") == 1
assert "midLoB := prevBearState != 2 ? low : math.min(midLoB, low)" in core
assert "midHiU := prevBullState != 2 ? high : math.max(midHiU, high)" in core
assert core.count("math.max(1, math.min(tCalib, tZBars))") == 2
assert core.count("math.max(1, math.min(oCalib, oZBars))") == 2
assert "bool bearExtNow = false" in core and "bool bullExtNow = false" in core
ret = re.search(r"\n \[sigBear \* 1\.0, sigBull \* 1\.0, bearContraNow \? 1\.0 : 0\.0, bullContraNow \? 1\.0 : 0\.0, midLoB, midHiU, "
r"bearRng \? curHi : na, bullRng \? curLo : na, fRefB, fRefU, ovlBear \? 1\.0 : ovlBull \? -1\.0 : 0\.0, "
r"bearExtNow \? 1\.0 : bullExtNow \? -1\.0 : 0\.0\]\n", core)
assert ret is not None
def test_generated_file_is_ascii():
bad = [l for l in DST.read_text(encoding="utf-8").split("\n") if any(ord(c) > 126 for c in l)]
assert bad == []
Run: cd "D:\vwap tpo pine" && python -m pytest entry5m/tools/tests -q
Expected: FAIL (ModuleNotFoundError: No module named 'make_entry_core').
entry5m/pine/ztd_entry.pine (everything outside the markers; the markers start empty)//@version=6
// ZTD Entry - 5m/15m pullback entries on the H1/H4 ZTD bias.
// Spec: entry5m/docs/2026-09-06-ztd-entry-indicator-design.md. Rules = pyakao/src/pyakao/entry5m/*.py.
// The ZTD core between ENTRY_CORE_BEGIN/END is GENERATED by entry5m/tools/make_entry_core.py from
// ../../ztd_divergence.pine (v17, engines at v17 defaults). Never edit inside the markers.
indicator("ZTD Entry [5m/15m pullback on H1/H4 ZTD bias]",
shorttitle="ZTD Entry",
overlay=true,
max_bars_back=500,
max_labels_count=500,
max_lines_count=500)
// ---------------------------------------------------------------- inputs (rule level only)
string GRP_B = "Bias (ZTD on the bias timeframe)"
string biasTf = input.timeframe("60", "Bias timeframe", options=["60", "240"], group=GRP_B)
string biasSource = input.string("Divergence", "Bias source", options=["Divergence", "Extreme arm"], group=GRP_B,
tooltip="Divergence: a ZTD RDIV/HDIV fire on the bias timeframe starts the bias; it ends at the zero-touch level, on contradiction, on timeout or when replaced. Extreme arm: an AVWAP-Z triangle at the tier below overlapping the PBK z beyond its extreme level starts the bias; it ends when that oscillator range ends (zero touch), on timeout or when replaced.")
string armTier = input.string("Extreme", "Arm triangle tier (Extreme arm source)", options=["Extreme", "Elevated"], group=GRP_B)
int timeoutH = input.int(96, "Bias timeout (bias-timeframe bars)", minval=1, group=GRP_B)
string GRP_E = "Entry (chart timeframe: use 5m or 15m)"
int pivotN = input.int(3, "Pivot strength (bars each side)", minval=1, group=GRP_E)
int minPbBars = input.int(3, "Minimum pullback age (bars from origin to pivot)", minval=0, group=GRP_E)
int pendMax = input.int(48, "Pending order life (bars)", minval=1, group=GRP_E)
float minRR = input.float(1.0, "Minimum reward-to-risk at placement", minval=0.0, step=0.1, group=GRP_E)
string GRP_X = "Stop and target"
string stopMode = input.string("HTF extreme", "Stop at", options=["HTF extreme", "Pullback swing"], group=GRP_X,
tooltip="HTF extreme: one tick beyond the bias's divergence extreme (or running range extreme in arm mode); the pending order is cancelled at that level. Pullback swing: one tick beyond the 5m/15m pullback pivot; the order is cancelled when that pivot is taken out.")
string tgtMode = input.string("Fixed R:R", "Target", options=["Fixed R:R", "Zero-touch level"], group=GRP_X,
tooltip="Zero-touch level is only available with the Divergence source; the Extreme arm source always uses Fixed R:R.")
float tgtRR = input.float(3.0, "Reward-to-risk (Fixed R:R)", minval=0.1, step=0.5, group=GRP_X)
string GRP_P = "Scale-out"
bool scaleOn = input.bool(false, "Take a partial", group=GRP_P)
float scaleFrac = input.float(0.5, "Partial size (fraction of the position)", minval=0.05, maxval=0.95, step=0.05, group=GRP_P)
float scaleR = input.float(1.0, "Partial at R", minval=0.1, step=0.5, group=GRP_P)
bool beAfter = input.bool(true, "Move stop to breakeven after the partial", group=GRP_P)
string GRP_D = "Display"
bool showBias = input.bool(true, "Show bias levels", group=GRP_D)
bool showOrders = input.bool(true, "Show pending orders", group=GRP_D)
bool showPos = input.bool(true, "Show position levels", group=GRP_D)
bool showHud = input.bool(true, "Show status panel", group=GRP_D)
int keepN = input.int(60, "Recent events kept (labels/lines)", minval=5, maxval=200, group=GRP_D)
string GRP_AL = "Alerts (alert() messages on bar close)"
bool alBias = input.bool(true, "Bias start / end", group=GRP_AL)
bool alOrder = input.bool(true, "Order placed / cancelled", group=GRP_AL)
bool alFill = input.bool(true, "Fill", group=GRP_AL)
bool alPartial = input.bool(true, "Partial", group=GRP_AL)
bool alBe = input.bool(true, "Stop to breakeven", group=GRP_AL)
bool alExit = input.bool(true, "Exit", group=GRP_AL)
// effective modes
bool armMode = biasSource == "Extreme arm"
bool fixedTgt = tgtMode == "Fixed R:R" or armMode
bool htfStop = stopMode == "HTF extreme"
bool scaleOk = scaleOn and (not fixedTgt or scaleR < tgtRR)
bool chartOk = timeframe.period == "5" or timeframe.period == "15"
float tick = syminfo.mintick
// ENTRY_CORE_BEGIN
// ENTRY_CORE_END
// ---------------------------------------------------------------- HTF surface (closed bias-timeframe bars only)
f_ztdPrev() =>
[a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12] = f_ztd()
[a1[1], a2[1], a3[1], a4[1], a5[1], a6[1], a7[1], a8[1], a9[1], a10[1], a11[1], a12[1]]
[hSigBear, hSigBull, hContraB, hContraU, hMidLoB, hMidHiU, hCurHi, hCurLo, hRefB, hRefU, hOvl, hExt] = request.security(syminfo.tickerid, biasTf, f_ztdPrev(), lookahead=barmerge.lookahead_on)
int hTime = request.security(syminfo.tickerid, biasTf, time[1], lookahead=barmerge.lookahead_on)
bool htfNew = not na(hTime) and (na(hTime[1]) or hTime != hTime[1])
// ---------------------------------------------------------------- data window: HTF surface
plot(htfNew ? 1 : 0, "htfNew", color=color.new(color.white, 100), display=display.data_window)
plot(hSigBear, "htfSigBear", color=color.new(color.white, 100), display=display.data_window)
plot(hSigBull, "htfSigBull", color=color.new(color.white, 100), display=display.data_window)
plot(hOvl, "htfOvl", color=color.new(color.white, 100), display=display.data_window)
plot(hCurHi, "htfCurHi", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(hCurLo, "htfCurLo", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(hMidLoB, "htfMidLoB", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(hMidHiU, "htfMidHiU", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(hContraB, "htfContraB", color=color.new(color.white, 100), display=display.data_window)
plot(hContraU, "htfContraU", color=color.new(color.white, 100), display=display.data_window)
plot(hExt, "htfExt", color=color.new(color.white, 100), display=display.data_window)
entry5m/tools/make_entry_core.py# entry5m/tools/make_entry_core.py
"""Generate the f_ztd() core of entry5m/pine/ztd_entry.pine from ../ztd_divergence.pine (v17).
The region from the "shared blocks" header up to (not including) the "drawings" header of the ZTD
script is: (1) prefixed with a constants block that pins every ZTD input to its v17 default
(tMinTier follows the entry indicator's arm-tier input in arm mode); (2) given two hooks the
Pine script does not have but the pyakao port does - the zero-touch extremes midLoB/midHiU and
the numeric drawing-follow (ext) events; (3) given a percentile-window cap so the core also
computes on the ~1200-bar "240" leg a 5m chart can see (identical to v17 once 2000 bars exist);
(4) indented under `f_ztd() =>` and closed with the 12-value return tuple. The result replaces
whatever sits between ENTRY_CORE_BEGIN/END in the target; everything else in the target is kept
byte for byte, so re-running on an unchanged source is a no-op.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SRC = ROOT / "ztd_divergence.pine"
DST = ROOT / "entry5m" / "pine" / "ztd_entry.pine"
CORE_BEGIN = "// ENTRY_CORE_BEGIN"
CORE_END = "// ENTRY_CORE_END"
REGION_START = "// ---------------------------------------------------------------- shared blocks"
REGION_END = "// ---------------------------------------------------------------- drawings (price pane + oscillator pane)"
CONSTANTS = """// ZTD v17 inputs pinned to their defaults (the entry indicator exposes rule-level inputs only)
bool intrabar = true
bool tfAny = true
bool tf5 = false
bool tf15 = false
bool tf60 = true
bool tf240 = true
bool tfD = true
string refMode = "Until superseded"
int maxSigPerRef = 1
int timeoutBars = 240
bool cancelOpp = true
string zeroRead = "Close"
float zeroTol = 0.0
string tMinTier = armMode ? armTier : "Elevated"
string oObTier = "Extreme"
string tAnchor = "Rolling (24-bar)"
string tThrMode = "Percentile"
float tZElev = 2.0
float tZExt = 2.5
int tCalib = 2000
float tPElev = 90.0
float tPExt = 95.0
string oAnchor = "Rolling (24-bar)"
string oThrMode = "Percentile"
float oZElev = 2.0
float oZExt = 2.5
int oCalib = 2000
float oPElev = 90.0
float oPExt = 95.0
int sigmaLen = 200
bool useVolume = true
int pivLen = 5
int flipMin = 5
"""
EXT_BLOCK = """
// drawing-follow, numeric half (pyakao ztd.py): extension events after a fire
var float bearDrawnHi = na
var float bearDrawnPk = na
bool bearExtNow = false
if sigBear != 0
bearDrawnHi := fCurB
bearDrawnPk := fCurPkB
if bearState == 3 and bearFired != 0 and not bearContra and sigBear == 0 and not na(bearDrawnHi) and (curHi > bearDrawnHi or curPk > bearDrawnPk)
bearExtNow := curHi > bearDrawnHi
bearDrawnHi := curHi
bearDrawnPk := curPk
var float bullDrawnLo = na
var float bullDrawnTr = na
bool bullExtNow = false
if sigBull != 0
bullDrawnLo := fCurU
bullDrawnTr := fCurPkU
if bullState == 3 and bullFired != 0 and not bullContra and sigBull == 0 and not na(bullDrawnLo) and (curLo < bullDrawnLo or curTr < bullDrawnTr)
bullExtNow := curLo < bullDrawnLo
bullDrawnLo := curLo
bullDrawnTr := curTr
"""
RETURN = ("[sigBear * 1.0, sigBull * 1.0, bearContraNow ? 1.0 : 0.0, bullContraNow ? 1.0 : 0.0, midLoB, midHiU, "
"bearRng ? curHi : na, bullRng ? curLo : na, fRefB, fRefU, ovlBear ? 1.0 : ovlBull ? -1.0 : 0.0, "
"bearExtNow ? 1.0 : bullExtNow ? -1.0 : 0.0]")
def replace_once(text: str, old: str, new: str) -> str:
assert text.count(old) == 1, f"anchor not unique/found: {old[:60]!r}"
return text.replace(old, new, 1)
def build_core(src: str) -> str:
a, b = src.index(REGION_START), src.index(REGION_END)
region = src[a:b].rstrip("\n") + "\n"
# hooks: zero-touch extremes (pyakao ZtdRule.mid_lo_b / mid_hi_u)
region = replace_once(region, "int prevBearState = bearState\n",
"var float midLoB = na\nint prevBearState = bearState\n")
region = replace_once(region, "bool bearLockedNow = bearState == 2 and prevBearState != 2\n",
"bool bearLockedNow = bearState == 2 and prevBearState != 2\n"
"if bearState == 2\n"
" midLoB := prevBearState != 2 ? low : math.min(midLoB, low)\n"
"else if bearState != 3\n"
" midLoB := na\n")
region = replace_once(region, "int prevBullState = bullState\n",
"var float midHiU = na\nint prevBullState = bullState\n")
region = replace_once(region, "bool bullLockedNow = bullState == 2 and prevBullState != 2\n",
"bool bullLockedNow = bullState == 2 and prevBullState != 2\n"
"if bullState == 2\n"
" midHiU := prevBullState != 2 ? high : math.max(midHiU, high)\n"
"else if bullState != 3\n"
" midHiU := na\n")
# percentile-window cap: identical to v17 once 2000 bars exist, computes earlier on short HTF legs
region = replace_once(region, "float tPctElev = ta.percentile_nearest_rank(tAbsZ, tCalib, tPElev)\n",
"var int tZBars = 0\nif not na(tAbsZ)\n tZBars += 1\n"
"float tPctElev = ta.percentile_nearest_rank(tAbsZ, math.max(1, math.min(tCalib, tZBars)), tPElev)\n")
region = replace_once(region, "float tPctExt = ta.percentile_nearest_rank(tAbsZ, tCalib, tPExt)\n",
"float tPctExt = ta.percentile_nearest_rank(tAbsZ, math.max(1, math.min(tCalib, tZBars)), tPExt)\n")
region = replace_once(region, "float oPctElev = ta.percentile_nearest_rank(oAbsZ, oCalib, oPElev)\n",
"var int oZBars = 0\nif not na(oAbsZ)\n oZBars += 1\n"
"float oPctElev = ta.percentile_nearest_rank(oAbsZ, math.max(1, math.min(oCalib, oZBars)), oPElev)\n")
region = replace_once(region, "float oPctExt = ta.percentile_nearest_rank(oAbsZ, oCalib, oPExt)\n",
"float oPctExt = ta.percentile_nearest_rank(oAbsZ, math.max(1, math.min(oCalib, oZBars)), oPExt)\n")
# the oscillator colour line is drawing-only and references color.*; drop it
region = re.sub(r"\ncolor oCol = [^\n]*\n", "\n", region, count=1)
assert "oCol" not in region, "oCol still referenced inside the core"
body = region + EXT_BLOCK
indented = "\n".join((" " + l) if l.strip() else "" for l in body.split("\n"))
core = CONSTANTS + "\nf_ztd() =>\n" + indented.rstrip("\n") + "\n " + RETURN + "\n"
for forbidden in ("label.", "line.", "plot(", "alertcondition(", "table.", "input."):
assert forbidden not in core, f"{forbidden} leaked into the core"
return core
def main() -> int:
src = SRC.read_text(encoding="utf-8")
core = build_core(src)
dst = DST.read_text(encoding="utf-8")
assert dst.count(CORE_BEGIN) == 1 and dst.count(CORE_END) == 1, "markers missing in the target"
a = dst.index(CORE_BEGIN) + len(CORE_BEGIN)
b = dst.index(CORE_END)
out = dst[:a] + "\n" + core + dst[b:]
DST.write_text(out, encoding="utf-8", newline="\n")
bad = [l for l in out.split("\n") if any(ord(c) > 126 for c in l)]
print(f"wrote {DST.relative_to(ROOT)}: {out.count(chr(10))} lines, core {core.count(chr(10))} lines, non-ascii lines {len(bad)}")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
Also create entry5m/tools/tests/__init__.py (empty) and entry5m/validation/.gitignore containing data_entry/.
Run: cd "D:\vwap tpo pine" && python entry5m/tools/make_entry_core.py && python -m pytest entry5m/tools/tests -q
Expected: the transform prints the line counts with non-ascii lines 0; 5 tests pass. If replace_once fails on an anchor, the ZTD source line differs from this plan: open ztd_divergence.pine, find the equivalent line, and fix the anchor string in the transform (do not edit the ZTD file).
Paste entry5m/pine/ztd_entry.pine into a new indicator script named ZTD Entry through the recorded pipeline (clipboard .ps1 from a scratch copy, focused Monaco textarea, Ctrl+A/V, verify the line count matches the file, Ctrl+Enter, pine_get_errors). Expected errors to handle, all inside the generated core and fixed IN THE TRANSFORM (then re-run it and re-paste):
a var declared in the region colliding with a name used by the skeleton: rename in the skeleton, never in the region;
timeframe.change("D") etc. inside a function called by request.security: allowed in v6; if the compiler objects, replace them in CONSTANTS-style by bool chgD = timeframe.change("D") hoisted to script level in the transform (they only matter for calendar anchors, unused at the defaults);
ta.pivothigh(pivLen, pivLen) inside the function: allowed; keep.
Save the script (Ctrl+S) and add it to the current chart. Record the pine-facade version and the entity id in the report.
[ ] Step 7: HTF gate on an H1 chart (entry5m/validation/compare_core_tv.py)
Set the chart to EURUSD 60 with both ZTD v17 (Daniel's instance, or a fresh instance at defaults if none is present, and say so) and ZTD Entry visible. Export with python harmonic/validation/export_tf.py --tf 60 --out entry5m/validation/data_entry --match "Zero-Touch,ZTD Entry". Then:
"""HTF gate: on an H1 chart the entry indicator's htf* plots must equal ZTD v17's plots shifted by
one bar (the indicator reads the previous, closed HTF bar), and htfMidLoB/htfMidHiU must equal the
pyakao port's midLoB/midHiU on the same H1 bars (v17 does not plot them).
Usage: python compare_core_tv.py --data entry5m/validation/data_entry --symbol EURUSD --tf 60
"""
from __future__ import annotations
import argparse, json, math, sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "pyakao" / "validation"))
sys.path.insert(0, str(ROOT / "pyakao" / "src"))
from compare_ztd_tv import find_study, column, _i # noqa: E402
from pyakao.data import Bars # noqa: E402
from pyakao.ztd import ZtdConfig, run_ztd # noqa: E402
PAIRS = [("htfSigBear", "sigBear"), ("htfSigBull", "sigBull"), ("htfOvl", "ovl"), ("htfExt", "ext")]
FLOAT_PAIRS = [("htfCurHi", "curHi"), ("htfCurLo", "curLo")]
FIRST = 2100 # after the 2000-bar percentile window is full on both sides
def isna(x):
return x is None or (isinstance(x, float) and math.isnan(x))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data", default=str(ROOT / "entry5m" / "validation" / "data_entry"))
ap.add_argument("--symbol", default="EURUSD")
ap.add_argument("--tf", default="60")
ap.add_argument("--arm-tier", default="Elevated", help="tMinTier the ZTD instance on the chart uses")
a = ap.parse_args()
data = Path(a.data)
studies = json.loads((data / f"{a.symbol}_{a.tf}_studies.json").read_text())
bars = json.loads((data / f"{a.symbol}_{a.tf}.json").read_text())
ztd, ent = find_study(studies, "Zero-Touch"), find_study(studies, "ZTD Entry")
T = [int(r[0]) for r in bars]
bad = 0
for e_name, z_name in PAIRS:
ec, zc = column(ent, e_name, exact=True), column(ztd, z_name, exact=True)
for i in range(FIRST, len(T)):
if _i(ec.get(T[i], 0)) != _i(zc.get(T[i - 1], 0)):
bad += 1
if bad <= 20:
print(f"MISMATCH {e_name} at {T[i]} ({datetime.fromtimestamp(T[i], tz=timezone.utc)}): entry {ec.get(T[i])} vs ztd[prev] {zc.get(T[i - 1])}")
for e_name, z_name in FLOAT_PAIRS:
ec, zc = column(ent, e_name, exact=True), column(ztd, z_name, exact=True)
for i in range(FIRST, len(T)):
x, y = ec.get(T[i]), zc.get(T[i - 1])
if isna(x) != isna(y) or (not isna(x) and abs(x - y) > 1e-6):
bad += 1
if bad <= 20:
print(f"MISMATCH {e_name} at {T[i]}: entry {x} vs ztd[prev] {y}")
# midLoB / midHiU against the port, run on the exported bars, same shift
b = Bars(times=[datetime.fromtimestamp(t, tz=timezone.utc) for t in T], opens=[r[1] for r in bars],
highs=[r[2] for r in bars], lows=[r[3] for r in bars], closes=[r[4] for r in bars], volumes=[r[5] for r in bars])
rows = run_ztd(b, ZtdConfig(t_min_tier=a.arm_tier))
for e_name, attr in (("htfMidLoB", "midLoB"), ("htfMidHiU", "midHiU")):
ec = column(ent, e_name, exact=True)
for i in range(FIRST, len(T)):
x, y = ec.get(T[i]), getattr(rows[i - 1], attr)
if isna(x) != isna(y) or (not isna(x) and abs(x - y) > 1e-6):
bad += 1
if bad <= 20:
print(f"MISMATCH {e_name} at {T[i]}: entry {x} vs port[prev] {y}")
print(f"bars {len(T)} compared from {FIRST}: mismatches {bad}")
print("RESULT:", "PASS" if bad == 0 else "FAIL")
return 0 if bad == 0 else 1
if __name__ == "__main__":
sys.exit(main())
Run it. Expected: RESULT: PASS. The ovl pair only agrees when the ZTD instance's tMinTier equals the indicator's effective tier: in Divergence mode that is Elevated (ZTD default) — leave both at defaults for this gate. A mismatch in curHi/curLo only means the shift or the range-plot form is wrong in the core; a mismatch in sigBear from the first compared bar means the constants differ from the chart instance's inputs — check the instance with data_get_indicator. Note that the port's midLoB may lag the Pine by nothing; if it mismatches only in the first 2000 bars, the FIRST constant is too small for this export (raise it and say so).
cd "D:\vwap tpo pine" && git add entry5m/tools/make_entry_core.py entry5m/tools/tests/__init__.py entry5m/tools/tests/test_make_entry_core.py entry5m/pine/ztd_entry.pine entry5m/validation/compare_core_tv.py entry5m/validation/.gitignore
git commit -m "feat(entry-pine): ZTD core transform, indicator skeleton with the closed-HTF surface, H1 gate" -m "Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>"
Files:
entry5m/pine/ztd_entry.pine (append after the HTF-surface plots; keep the generated region untouched)Interfaces:
Consumes: hSigBear, hSigBull, hContraB, hContraU, hMidLoB, hMidHiU, hCurHi, hCurLo, hOvl, hExt, htfNew, armMode, timeoutH (Task 1).
Produces (chart-level var state, read by Tasks 3-4): biasSide (0/-1/+1), biasKind (0 none, 1 RDIV, 2 HDIV, 3 ARM), biasStartBar, biasExt, biasTarget, biasDeadline, biasId, htfCount; per-bar events biasStartNow (bool), biasEndNow (bool), biasEndCode (0 none, 1 target, 2 contradiction, 3 timeout, 4 replaced, 5 range_end), endedSide, endedId; and the position/order state declarations Task 3 fills: pendActive, pendSide, pendEntry, pendStop, pendTarget, pendCancel, pendPlacedBar, pendPbBar, pendOriginBar, posActive, posSide, posFill, posFillBar, posStop, posQty, posPartialPx, posPartialBar, posTarget, posRisk, lastPivot. Also the per-bar flags Task 3 sets and Task 4 consumes: orderPlacedNow, orderCancelCode (0 none, 1 extreme_taken, 2 expired, 3 bias_end, 4 replaced), fillNow, partialNow, beNow, exitCode (0 none, 1 stop, 2 target, 3 bias_end, 4 breakeven), exitPx, tradeR.
[ ] Step 1: Append the state declarations and the bias tracker
// ---------------------------------------------------------------- chart-level state (declared here; filled by the sections below)
// bias (port of pyakao entry5m/bias.py)
var int biasSide = 0
var int biasKind = 0 // 1 RDIV, 2 HDIV, 3 ARM
var int biasStartBar = na
var float biasExt = na // HTF extreme: the stop reference in HTF-extreme mode
var float biasTarget = na // zero-touch level (divergence mode), na in arm mode
var int biasDeadline = na // in HTF bar count
var int biasId = 0
var int htfCount = -1 // index k of the last consumed HTF bar
// pending order (port of setup.py PendingOrder)
var bool pendActive = false
var int pendSide = 0
var float pendEntry = na
var float pendStop = na
var float pendTarget = na
var float pendCancel = na
var int pendPlacedBar = na
var int pendPbBar = na
var int pendOriginBar = na
var int lastPivot = -1 // previous confirmed same-kind pivot bar since the bias started
// simulated position (port of execution.py Position)
var bool posActive = false
var int posSide = 0
var float posFill = na
var int posFillBar = na
var float posStop = na // effective stop (order stop, or the fill after the partial)
var float posQty = 1.0
var float posPartialPx = na
var int posPartialBar = na
var float posTarget = na
var float posRisk = na
var float posEntryLvl = na // the order's entry level (partial level is measured from it)
// per-bar events
bool biasStartNow = false
bool biasEndNow = false
int biasEndCode = 0
int endedSide = 0
int endedId = 0
bool orderPlacedNow = false
int orderCancelCode = 0
bool fillNow = false
bool partialNow = false
bool beNow = false
int exitCode = 0
float exitPx = na
float tradeR = na
int tooShortNow = 0
int rrRejectedNow = 0
int alreadyBrokenNow = 0
int skippedInPosNow = 0
// ---------------------------------------------------------------- step 2: HTF events (bias.py on_h1_close / _on_h1_arm)
// NOTE: step 1 (position management) is inserted ABOVE this block by Task 3; the file order then matches runner.py.
if htfNew
htfCount += 1
int k = htfCount
if biasSide != 0
if not armMode
bool contra = biasSide < 0 ? hContraB > 0.5 : hContraU > 0.5
if contra
biasEndCode := 2
else
if (biasSide < 0 and hExt > 0.5) or (biasSide > 0 and hExt < -0.5)
biasExt := biasSide < 0 ? hCurHi : hCurLo
biasDeadline := k + timeoutH
if k > biasDeadline
biasEndCode := 3
else
float cur = biasSide < 0 ? hCurHi : hCurLo
if na(cur)
biasEndCode := 5
else
if (biasSide < 0 and cur > biasExt) or (biasSide > 0 and cur < biasExt)
biasExt := cur
if k > biasDeadline
biasEndCode := 3
int trigSide = 0
int trigKind = 0
float trigExt = na
float trigTgt = na
if not armMode
float sg = hSigBear != 0 ? hSigBear : hSigBull
if sg != 0
trigSide := hSigBear != 0 ? -1 : 1
trigKind := math.abs(sg) > 1.5 ? 1 : 2
trigExt := trigSide < 0 ? hCurHi : hCurLo
trigTgt := trigSide < 0 ? hMidLoB : hMidHiU
if na(trigTgt)
trigSide := 0 // fire without a zero-touch level: ignored
else if hOvl != 0
trigSide := hOvl > 0 ? -1 : 1
trigKind := 3
trigExt := trigSide < 0 ? hCurHi : hCurLo
if na(trigExt) or (biasSide == trigSide and biasEndCode == 0)
trigSide := 0 // range ended on its trigger bar, or same-side re-trigger
if biasEndCode == 0 and trigSide != 0 and biasSide != 0
biasEndCode := 4 // replaced
if biasEndCode != 0
biasEndNow := true
endedSide := biasSide
endedId := biasId
biasSide := 0
biasKind := 0
if trigSide != 0
biasId += 1
biasSide := trigSide
biasKind := trigKind
biasStartBar := bar_index
biasExt := trigExt
biasTarget := trigTgt
biasDeadline := k + timeoutH
biasStartNow := true
lastPivot := -1
// ---------------------------------------------------------------- step 3: target touch ends a divergence bias (runner.py)
if biasSide != 0 and not armMode and ((biasSide < 0 and low <= biasTarget) or (biasSide > 0 and high >= biasTarget))
biasEndNow := true
biasEndCode := 1
endedSide := biasSide
endedId := biasId
biasSide := 0
biasKind := 0
// ---------------------------------------------------------------- data window: bias
plot(biasSide, "biasSide", color=color.new(color.white, 100), display=display.data_window)
plot(biasKind, "biasKind", color=color.new(color.white, 100), display=display.data_window)
plot(biasSide != 0 ? biasStartBar : na, "biasStartBar", color=color.new(color.white, 100), display=display.data_window)
plot(biasSide != 0 ? biasExt : na, "biasExt", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(biasSide != 0 ? biasTarget : na, "biasTarget", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(biasEndNow ? biasEndCode : 0, "biasEndCode", color=color.new(color.white, 100), display=display.data_window)
plot(biasStartNow ? 1 : 0, "biasStartNow", color=color.new(color.white, 100), display=display.data_window)
plot(bar_index, "barIndex", color=color.new(color.white, 100), display=display.data_window)
Semantics to keep straight (all from bias.py): a same-side arm re-trigger is ignored only while the bias survives this bar's end checks (hence biasEndCode == 0 in the guard); an opposite fire while a bias is active ends it with code 4 before the new start; the tracker's deadline renewal on a same-side ext moves biasExt to hCurHi/hCurLo (equal to fCurB/fCurU on fire bars in the port).
Paste the whole file, compile (zero severity-8 errors), save a new version, make sure the chart instance updated (else remove and re-add). On a 5m EURUSD chart read the data window over the last ~2000 bars with data_get_study_values at a few bars and confirm biasSide becomes non-zero after H1 fires (compare a biasStartNow == 1 bar against the ZTD H1 chart: the fire is on the H1 bar that closed just before that 5m bar opens). Record two examples (times, side, kind, ext, target) in the report.
cd "D:\vwap tpo pine" && git add entry5m/pine/ztd_entry.pine
git commit -m "feat(entry-pine): chart-level bias tracker (divergence and arm), target-touch end, bias data window" -m "Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>"
Files:
entry5m/pine/ztd_entry.pine (insert step 1 ABOVE the // step 2 block; append the bias-end cleanup, the scanner and the order/position data window after step 3)Interfaces:
Consumes: Task 2 state and per-bar flags; inputs pivotN, minPbBars, pendMax, minRR, htfStop, fixedTgt, tgtRR, scaleOk, scaleFrac, scaleR, beAfter, tick.
Produces: the pending/position state and the per-bar flags listed in Task 2's Interfaces, plus arrays hiArr, loArr (one float per chart bar since bar 0), and data-window plots pendEntry, pendStop, pendTarget, pendCancel, pendPlacedBar, posQty, posFill, posFillBar, posStop, partialPx, exitCode, exitPx, tradeR, orderCancelCode, fillNow, tooShort, rrRejected, alreadyBroken, skippedInPos.
[ ] Step 1: Insert step 1 (position management and fills) ABOVE the // step 2 block
// ---------------------------------------------------------------- step 1: open position, then the pending order (execution.py Broker.on_bar; no slippage)
// helper: one bar of an open position: stop -> partial -> target. Returns [exitCode, exitPx, partialNow, beNow]
f_manage(int side, float fill, float stopEff, float qty, float entryLvl, float risk, float target, bool canScale, float frac, float pr, bool be, float hi, float lo) =>
int code = 0
float px = na
bool part = false
bool beMove = false
float newStop = stopEff
float newQty = qty
float partPx = na
bool short = side < 0
if (short ? hi >= stopEff : lo <= stopEff)
code := qty < 1.0 and stopEff == fill ? 4 : 1
px := stopEff
else
if canScale and qty >= 1.0
float lvl = short ? entryLvl - pr * risk : entryLvl + pr * risk
if (short ? lo <= lvl : hi >= lvl)
newQty := 1.0 - frac
partPx := lvl
part := true
if be
newStop := fill
beMove := true
if (short ? lo <= target : hi >= target)
code := 2
px := target
[code, px, part, beMove, newStop, newQty, partPx]
f_tradeR(int side, float fill, float px, float risk, float qty, float partPx) =>
float r = (px - fill) * side / risk
qty < 1.0 ? (1.0 - qty) * (partPx - fill) * side / risk + qty * r : r
if posActive
[c1, p1, part1, be1, ns1, nq1, pp1] = f_manage(posSide, posFill, posStop, posQty, posEntryLvl, posRisk, posTarget, scaleOk, scaleFrac, scaleR, beAfter, high, low)
if part1
partialNow := true
posPartialPx := pp1
posPartialBar := bar_index
posQty := nq1
if be1
beNow := true
posStop := ns1
if c1 != 0
exitCode := c1
exitPx := p1
tradeR := f_tradeR(posSide, posFill, p1, posRisk, posQty, posPartialPx)
posActive := false
if pendActive and not posActive
bool touched = pendSide < 0 ? low <= pendEntry : high >= pendEntry
if touched
float fillPx = pendSide < 0 ? math.min(open, pendEntry) : math.max(open, pendEntry)
posActive := true
posSide := pendSide
posFill := fillPx
posFillBar := bar_index
posStop := pendStop
posQty := 1.0
posPartialPx := na
posPartialBar := na
posTarget := pendTarget
posRisk := math.abs(pendStop - pendEntry)
posEntryLvl := pendEntry
pendActive := false
fillNow := true
[c2, p2, part2, be2, ns2, nq2, pp2] = f_manage(posSide, posFill, posStop, posQty, posEntryLvl, posRisk, posTarget, scaleOk, scaleFrac, scaleR, beAfter, high, low)
if part2
partialNow := true
posPartialPx := pp2
posPartialBar := bar_index
posQty := nq2
if be2
beNow := true
posStop := ns2
if c2 != 0
exitCode := c2
exitPx := p2
tradeR := f_tradeR(posSide, posFill, p2, posRisk, posQty, posPartialPx)
posActive := false
else
bool taken = pendSide < 0 ? high > pendCancel : low < pendCancel
if taken
pendActive := false
orderCancelCode := 1
else if bar_index - pendPlacedBar >= pendMax
pendActive := false
orderCancelCode := 2
f_manage is a pure function (no globals modified) returning what the caller writes back, which is how Pine allows shared logic between the open-position path and the fill path. The breakeven stop set on the partial bar is applied from the next bar (the stop was checked first in this call), exactly as execution.py.
// ---------------------------------------------------------------- bias end cleanup (runner.py bias_ended): close at the close, cancel, reset the boundary
if biasEndNow
if posActive
exitCode := 3
exitPx := close
tradeR := f_tradeR(posSide, posFill, close, posRisk, posQty, posPartialPx)
posActive := false
if pendActive
pendActive := false
orderCancelCode := 3
lastPivot := -1
// ---------------------------------------------------------------- step 4: pivots, pullbacks, order pricing (setup.py PullbackScanner)
var float[] hiArr = array.new_float()
var float[] loArr = array.new_float()
array.push(hiArr, high)
array.push(loArr, low)
float ph = ta.pivothigh(high, pivotN, pivotN)
float pl = ta.pivotlow(low, pivotN, pivotN)
if biasSide != 0
float piv = biasSide < 0 ? ph : pl
if not na(piv)
int p = bar_index - pivotN
if p >= biasStartBar
int q = lastPivot >= 0 ? lastPivot + 1 : biasStartBar
lastPivot := p
int origin = q
float originPx = biasSide < 0 ? array.get(loArr, q) : array.get(hiArr, q)
if p > q
for j = q + 1 to p
float v = biasSide < 0 ? array.get(loArr, j) : array.get(hiArr, j)
if (biasSide < 0 and v < originPx) or (biasSide > 0 and v > originPx)
originPx := v
origin := j
if p - origin < minPbBars
tooShortNow := 1
else if posActive
skippedInPosNow := 1
else
float entry = biasSide < 0 ? originPx - tick : originPx + tick
float stopRef = htfStop ? biasExt : piv
float stopPx = biasSide < 0 ? stopRef + tick : stopRef - tick
float risk = math.abs(stopPx - entry)
float target = fixedTgt ? (biasSide < 0 ? entry - tgtRR * risk : entry + tgtRR * risk) : biasTarget
float reward = biasSide < 0 ? entry - target : target - entry
bool broken = false
for j = p + 1 to bar_index
float v = biasSide < 0 ? array.get(loArr, j) : array.get(hiArr, j)
if (biasSide < 0 and v <= entry) or (biasSide > 0 and v >= entry)
broken := true
if na(stopRef) or risk <= 0 or reward / risk < minRR
rrRejectedNow := 1
else if broken
alreadyBrokenNow := 1
else
if pendActive
orderCancelCode := 4 // replaced by the newer pullback
pendActive := true
pendSide := biasSide
pendEntry := entry
pendStop := stopPx
pendTarget := target
pendCancel := stopRef
pendPlacedBar := bar_index
pendPbBar := p
pendOriginBar := origin
orderPlacedNow := true
// ---------------------------------------------------------------- data window: orders and position
plot(pendActive ? pendEntry : na, "pendEntry", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(pendActive ? pendStop : na, "pendStop", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(pendActive ? pendTarget : na, "pendTarget", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(pendActive ? pendCancel : na, "pendCancel", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(pendActive ? pendPlacedBar : na, "pendPlacedBar", color=color.new(color.white, 100), display=display.data_window)
plot(orderPlacedNow ? 1 : 0, "orderPlaced", color=color.new(color.white, 100), display=display.data_window)
plot(orderCancelCode, "orderCancelCode", color=color.new(color.white, 100), display=display.data_window)
plot(posActive ? posQty : 0, "posQty", color=color.new(color.white, 100), display=display.data_window)
plot(posActive ? posFill : na, "posFill", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(posActive ? posFillBar : na, "posFillBar", color=color.new(color.white, 100), display=display.data_window)
plot(posActive ? posStop : na, "posStop", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(posActive ? posPartialPx : na, "partialPx", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(fillNow ? 1 : 0, "fillNow", color=color.new(color.white, 100), display=display.data_window)
plot(partialNow ? 1 : 0, "partialNow", color=color.new(color.white, 100), display=display.data_window)
plot(exitCode, "exitCode", color=color.new(color.white, 100), display=display.data_window)
plot(exitPx, "exitPx", color=color.new(color.white, 100), display=display.data_window, precision=6)
plot(tradeR, "tradeR", color=color.new(color.white, 100), display=display.data_window, precision=4)
plot(tooShortNow, "tooShort", color=color.new(color.white, 100), display=display.data_window)
plot(rrRejectedNow, "rrRejected", color=color.new(color.white, 100), display=display.data_window)
plot(alreadyBrokenNow, "alreadyBroken", color=color.new(color.white, 100), display=display.data_window)
plot(skippedInPosNow, "skippedInPos", color=color.new(color.white, 100), display=display.data_window)
Order-pricing notes from setup.py: the origin search ties go to the earliest bar (strict </> in the loop keeps the first minimum); pendCancel is the pullback extreme in pullback-stop mode and biasExt in HTF-extreme mode; the "already broken" window is (p, c] = p + 1 .. bar_index; a na stopRef counts as rrRejected like the harness. The origin loop is guarded by if p > q because Pine reverses a to loop whose end is below its start; the already-broken loop p + 1 .. bar_index always has pivotN >= 1 elements and needs no guard.
Output count so far: 11 (HTF) + 8 (bias) + 21 (orders/position) = 40 plots. Task 4 adds one alertcondition.
Compile clean, save, ensure the chart instance updated. On the 5m EURUSD chart with the defaults (Divergence, HTF extreme, Fixed R:R 3, scale-out off) read the data window around a biasStartNow bar and confirm the sequence: a pivot high confirmed pivotN bars after the pullback top, orderPlaced == 1 on that bar with pendEntry one tick below the origin low and pendStop one tick above biasExt, then fillNow on the first bar whose low reaches pendEntry. Record one full sequence (bar times and levels) in the report. Then flip Take a partial on through setInputValues and confirm a partialNow bar followed by posStop == posFill.
cd "D:\vwap tpo pine" && git add entry5m/pine/ztd_entry.pine
git commit -m "feat(entry-pine): pullback scanner, order pricing, simulated position with scale-out and breakeven" -m "Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>"
Files:
entry5m/pine/ztd_entry.pine (append after the orders/position data window)Interfaces:
Consumes: every state variable and per-bar flag from Tasks 2-3; inputs showBias, showOrders, showPos, showHud, keepN, alBias, alOrder, alFill, alPartial, alBe, alExit, chartOk, biasTf, biasSource, stopMode, tgtMode, tgtRR, scaleOk, scaleFrac, scaleR, beAfter.
Produces: nothing consumed later; one alertcondition "ZTD Entry: any event".
[ ] Step 1: Append drawings
// ---------------------------------------------------------------- drawings (budgeted: keep the last keepN of each kind)
var label[] evLbls = array.new_label()
var line[] evLns = array.new_line()
keepLbl(label l) =>
array.push(evLbls, l)
while array.size(evLbls) > keepN
label.delete(array.shift(evLbls))
keepLn(line l) =>
array.push(evLns, l)
while array.size(evLns) > math.min(keepN * 3, 480)
line.delete(array.shift(evLns))
color colBear = color.new(color.red, 0)
color colBull = color.new(color.green, 0)
colSide(int s) => s < 0 ? colBear : colBull
sideTxt(int s) => s < 0 ? "SHORT" : "LONG"
kindTxt(int k) => k == 1 ? "RDIV" : k == 2 ? "HDIV" : k == 3 ? "ARM" : ""
endTxt(int c) => c == 1 ? "target" : c == 2 ? "contradiction" : c == 3 ? "timeout" : c == 4 ? "replaced" : c == 5 ? "range end" : ""
exitTxt(int c) => c == 1 ? "stop" : c == 2 ? "target" : c == 3 ? "bias end" : c == 4 ? "breakeven" : ""
cancelTxt(int c) => c == 1 ? "extreme taken" : c == 2 ? "expired" : c == 3 ? "bias end" : c == 4 ? "replaced" : ""
px(float v) => str.tostring(v, format.mintick)
// bias: one line at the HTF extreme (stop reference) and, in divergence mode, one at the zero-touch target; both extend while the bias lives
var line biasLnExt = na
var line biasLnTgt = na
if showBias
if biasStartNow
biasLnExt := line.new(bar_index, biasExt, bar_index + 1, biasExt, color=colSide(biasSide), style=line.style_dashed, width=2)
keepLn(biasLnExt)
if not na(biasTarget)
biasLnTgt := line.new(bar_index, biasTarget, bar_index + 1, biasTarget, color=color.new(colSide(biasSide), 40), style=line.style_dotted, width=1)
keepLn(biasLnTgt)
keepLbl(label.new(bar_index, biasExt, "ZTD bias " + sideTxt(biasSide) + " " + kindTxt(biasKind) + "\nstop ref " + px(biasExt) + (na(biasTarget) ? "" : "\nzero-touch " + px(biasTarget)),
style=biasSide < 0 ? label.style_label_down : label.style_label_up, color=color.new(colSide(biasSide), 20), textcolor=color.white, size=size.small))
if biasSide != 0 and not na(biasLnExt)
line.set_xy2(biasLnExt, bar_index, biasExt)
line.set_y1(biasLnExt, biasExt)
if not na(biasLnTgt)
line.set_x2(biasLnTgt, bar_index)
if biasEndNow and not na(biasLnExt)
keepLbl(label.new(bar_index, endedSide < 0 ? high : low, "bias end: " + endTxt(biasEndCode), style=endedSide < 0 ? label.style_label_down : label.style_label_up,
color=color.new(color.gray, 30), textcolor=color.white, size=size.tiny))
// pending order: entry (dotted), stop and target (thin), from the placement bar while pending
var line pendLnE = na
var line pendLnS = na
var line pendLnT = na
if showOrders
if orderPlacedNow
pendLnE := line.new(bar_index, pendEntry, bar_index + 1, pendEntry, color=colSide(pendSide), style=line.style_dotted, width=2)
pendLnS := line.new(bar_index, pendStop, bar_index + 1, pendStop, color=color.new(color.orange, 30), style=line.style_dotted, width=1)
pendLnT := line.new(bar_index, pendTarget, bar_index + 1, pendTarget, color=color.new(color.teal, 30), style=line.style_dotted, width=1)
keepLn(pendLnE)
keepLn(pendLnS)
keepLn(pendLnT)
keepLbl(label.new(bar_index, pendEntry, (pendSide < 0 ? "sell stop " : "buy stop ") + px(pendEntry) + "\nstop " + px(pendStop) + " tgt " + px(pendTarget),
style=pendSide < 0 ? label.style_label_up : label.style_label_down, color=color.new(colSide(pendSide), 50), textcolor=color.white, size=size.tiny))
if pendActive and not na(pendLnE)
line.set_x2(pendLnE, bar_index)
line.set_x2(pendLnS, bar_index)
line.set_x2(pendLnT, bar_index)
if orderCancelCode != 0 and not fillNow
keepLbl(label.new(bar_index, close, "order cancelled: " + cancelTxt(orderCancelCode), style=label.style_label_left, color=color.new(color.gray, 50), textcolor=color.white, size=size.tiny))
// position: entry (solid), stop (moves to breakeven), partial, target
var line posLnE = na
var line posLnS = na
var line posLnT = na
var line posLnP = na
if showPos
if fillNow
posLnE := line.new(bar_index, posFill, bar_index + 1, posFill, color=colSide(posSide), width=2)
posLnS := line.new(bar_index, posStop, bar_index + 1, posStop, color=color.orange, width=1)
posLnT := line.new(bar_index, posTarget, bar_index + 1, posTarget, color=color.teal, width=1)
keepLn(posLnE)
keepLn(posLnS)
keepLn(posLnT)
if scaleOk
float lvl = posSide < 0 ? posEntryLvl - scaleR * posRisk : posEntryLvl + scaleR * posRisk
posLnP := line.new(bar_index, lvl, bar_index + 1, lvl, color=color.new(color.teal, 50), style=line.style_dashed, width=1)
keepLn(posLnP)
keepLbl(label.new(bar_index, posFill, "filled " + px(posFill), style=posSide < 0 ? label.style_label_up : label.style_label_down, color=color.new(colSide(posSide), 0), textcolor=color.white, size=size.tiny))
if posActive and not na(posLnE)
line.set_x2(posLnE, bar_index)
line.set_xy2(posLnS, bar_index, posStop)
line.set_x2(posLnT, bar_index)
if not na(posLnP)
line.set_x2(posLnP, bar_index)
if partialNow
keepLbl(label.new(bar_index, posPartialPx, "partial " + px(posPartialPx) + (beNow ? "\nstop -> BE " + px(posStop) : ""), style=label.style_label_left, color=color.new(color.teal, 20), textcolor=color.white, size=size.tiny))
if exitCode != 0
keepLbl(label.new(bar_index, exitPx, "exit " + exitTxt(exitCode) + " " + px(exitPx) + "\nR " + str.tostring(tradeR, "#.##"),
style=label.style_label_left, color=color.new(tradeR > 0 ? color.green : color.red, 20), textcolor=color.white, size=size.tiny))
// ---------------------------------------------------------------- HUD
var table hud = table.new(position.top_right, 2, 8, border_width=1)
if showHud and (barstate.islast or barstate.islastconfirmedhistory)
string warn = chartOk ? "" : " (chart is not 5m/15m)"
table.cell(hud, 0, 0, "ZTD Entry", text_color=color.white, bgcolor=color.new(color.blue, 20), text_size=size.small)
table.cell(hud, 1, 0, biasTf + "m " + biasSource + warn, text_color=color.white, bgcolor=color.new(color.blue, 20), text_size=size.small)
table.cell(hud, 0, 1, "bias", text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
table.cell(hud, 1, 1, biasSide == 0 ? "none" : sideTxt(biasSide) + " " + kindTxt(biasKind) + " " + str.tostring(bar_index - biasStartBar) + " bars",
text_color=color.white, bgcolor=biasSide == 0 ? color.new(color.gray, 60) : color.new(colSide(biasSide), 40), text_size=size.small)
table.cell(hud, 0, 2, "stop ref / zero-touch", text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
table.cell(hud, 1, 2, biasSide == 0 ? "-" : px(biasExt) + (na(biasTarget) ? "" : " / " + px(biasTarget)), text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
table.cell(hud, 0, 3, "pending", text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
table.cell(hud, 1, 3, pendActive ? (pendSide < 0 ? "sell stop " : "buy stop ") + px(pendEntry) + " stop " + px(pendStop) + " tgt " + px(pendTarget) : "none",
text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
table.cell(hud, 0, 4, "position", text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
float runR = posActive ? f_tradeR(posSide, posFill, close, posRisk, posQty, posPartialPx) : na
table.cell(hud, 1, 4, posActive ? sideTxt(posSide) + " " + str.tostring(posQty, "#.##") + " @ " + px(posFill) + " stop " + px(posStop) + " R " + str.tostring(runR, "#.##") : "flat",
text_color=color.white, bgcolor=posActive ? color.new(colSide(posSide), 40) : color.new(color.gray, 60), text_size=size.small)
table.cell(hud, 0, 5, "rules", text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
table.cell(hud, 1, 5, stopMode + " " + (fixedTgt ? "R:R " + str.tostring(tgtRR, "#.#") : "zero-touch") + (scaleOk ? " partial " + str.tostring(scaleFrac, "#.##") + " @ " + str.tostring(scaleR, "#.#") + "R" + (beAfter ? " BE" : "") : ""),
text_color=color.white, bgcolor=color.new(color.gray, 60), text_size=size.small)
// ---------------------------------------------------------------- alerts (alert() with prices, once per bar close) + one classic alertcondition
string tag = "ZTD ENTRY " + syminfo.ticker + " " + timeframe.period + "m: "
bool anyEvent = false
if barstate.isconfirmed
if alBias and biasEndNow
alert(tag + "bias end " + endTxt(biasEndCode) + " (" + sideTxt(endedSide) + ")", alert.freq_once_per_bar_close)
anyEvent := true
if alBias and biasStartNow
alert(tag + "bias start " + sideTxt(biasSide) + " " + kindTxt(biasKind) + " stop-ref " + px(biasExt) + (na(biasTarget) ? "" : " zero-touch " + px(biasTarget)), alert.freq_once_per_bar_close)
anyEvent := true
if alOrder and orderCancelCode != 0 and not fillNow
alert(tag + "order cancelled " + cancelTxt(orderCancelCode), alert.freq_once_per_bar_close)
anyEvent := true
if alOrder and orderPlacedNow
alert(tag + "order placed " + (pendSide < 0 ? "SELL STOP " : "BUY STOP ") + px(pendEntry) + " stop " + px(pendStop) + " target " + px(pendTarget) + " cancel-at " + px(pendCancel), alert.freq_once_per_bar_close)
anyEvent := true
if alFill and fillNow
alert(tag + "filled " + sideTxt(posSide) + " " + px(posFill) + " stop " + px(posStop) + " target " + px(posTarget), alert.freq_once_per_bar_close)
anyEvent := true
if alPartial and partialNow
alert(tag + "partial " + str.tostring(scaleFrac, "#.##") + " at " + px(posPartialPx), alert.freq_once_per_bar_close)
anyEvent := true
if alBe and beNow
alert(tag + "stop to breakeven " + px(posStop), alert.freq_once_per_bar_close)
anyEvent := true
if alExit and exitCode != 0
alert(tag + "exit " + exitTxt(exitCode) + " " + px(exitPx) + " R " + str.tostring(tradeR, "#.##"), alert.freq_once_per_bar_close)
anyEvent := true
alertcondition(biasStartNow or biasEndNow or orderPlacedNow or orderCancelCode != 0 or fillNow or partialNow or beNow or exitCode != 0, "ZTD Entry: any event", "ZTD Entry event on {{ticker}} {{interval}}")
Note alert() must be called with a dynamic message inside a condition, which is what the block does; alertcondition needs a constant message.
Compile clean (the label/line helpers must be declared before use; Pine forbids modifying globals inside functions, which is why every state write sits at top level), save, update the chart instance. Take capture_screenshot on a stretch with a bias, an order and a fill, and read the HUD with data_get_pine_tables. Confirm no drawing budget error over the loaded 5m history (status().type === 2). Record the screenshot path and the HUD text in the report.
cd "D:\vwap tpo pine" && git add entry5m/pine/ztd_entry.pine
git commit -m "feat(entry-pine): bias/order/position drawings, HUD, alert() messages" -m "Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>"
Files:
pyakao/src/pyakao/entry5m/runner.py (orders log), pyakao/tests/test_entry5m_runner.pyentry5m/validation/compare_entry_tv.pyInterfaces:
Consumes: the indicator's data window (Tasks 1-3 plot titles), run_entry5m, Entry5mConfig, Bars, the export tooling.
Produces: Entry5mResult.orders: list[PendingOrder] (every placed order, in placement order); the parity script with RESULT: PASS|FAIL.
[ ] Step 1: Orders log in the runner (failing test first) — append to pyakao/tests/test_entry5m_runner.py:
def test_runner_logs_every_placed_order():
res = run_entry5m(bars_from(SCENARIO), scenario_cfg(), engine=Scripted(FIRE))
assert len(res.orders) == res.counters["orders_placed"] == 1
od = res.orders[0]
assert (od.side, od.placed_bar, od.pb_bar, od.origin_bar) == (-1, 59, 56, 52)
assert od.entry == pytest.approx(94.99) and od.stop == pytest.approx(98.01) and od.target == 90.0
Run it (expect AttributeError: 'Entry5mResult' object has no attribute 'orders'), then in runner.py add orders: list = field(default_factory=list) to Entry5mResult and res.orders.append(order) right after broker.place(order) in the placement branch. Run the whole suite (expect 301).
python harmonic/validation/export_tf.py --tf 5 --out entry5m/validation/data_entry --match "ZTD Entry" on EURUSD. Then a second export after setting the instance to Take a partial = on via setInputValues (find the in_N ids with data_get_indicator), written with --out entry5m/validation/data_entry_partial.
entry5m/validation/compare_entry_tv.py"""Chart-level parity: the ZTD Entry indicator's data window on the exported 5m/15m bars against
pyakao's run_entry5m on the SAME bars (slippage 0, matching inputs).
Compared, by chart bar time: bias starts (side, kind, ext, target), bias ends (code), orders
(placed bar, entry, stop, target, cancel), fills (bar, price), partials (bar, price), exits
(bar, code, price, R). Pass = zero mismatches on bias and orders; fill/exit mismatches are listed
with the bars involved so each can be traced to an HTF-alignment difference (spec section 8).
Usage: python compare_entry_tv.py --data entry5m/validation/data_entry --symbol EURUSD --tf 5 [--stop-mode h1_extreme] [--target-mode fixed_r] [--target-r 3] [--bias-source divergence] [--scale-out-r 0]
"""
from __future__ import annotations
import argparse, json, math, sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "pyakao" / "validation"))
sys.path.insert(0, str(ROOT / "pyakao" / "src"))
from compare_ztd_tv import find_study, column, _i # noqa: E402
from pyakao.data import Bars # noqa: E402
from pyakao.entry5m import Entry5mConfig, run_entry5m # noqa: E402
KIND = {"RDIV": 1, "HDIV": 2, "ARM": 3}
END = {"target": 1, "contradiction": 2, "timeout": 3, "replaced": 4, "range_end": 5}
EXIT = {"stop": 1, "target": 2, "bias_end": 3, "breakeven": 4}
def isna(x):
return x is None or (isinstance(x, float) and math.isnan(x))
def close(a, b, tol=1e-6):
return (isna(a) and isna(b)) or (not isna(a) and not isna(b) and abs(a - b) <= tol)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--data", default=str(ROOT / "entry5m" / "validation" / "data_entry"))
ap.add_argument("--symbol", default="EURUSD")
ap.add_argument("--tf", default="5")
ap.add_argument("--stop-mode", default="h1_extreme")
ap.add_argument("--target-mode", default="fixed_r")
ap.add_argument("--target-r", type=float, default=3.0)
ap.add_argument("--bias-source", default="divergence")
ap.add_argument("--scale-out-r", type=float, default=0.0)
ap.add_argument("--first", type=int, default=0, help="first chart bar to compare (after the HTF warm-up)")
a = ap.parse_args()
data = Path(a.data)
studies = json.loads((data / f"{a.symbol}_{a.tf}_studies.json").read_text())
raw = json.loads((data / f"{a.symbol}_{a.tf}.json").read_text())
ent = find_study(studies, "ZTD Entry")
T = [int(r[0]) for r in raw]
bars = Bars(times=[datetime.fromtimestamp(t, tz=timezone.utc) for t in T], opens=[r[1] for r in raw],
highs=[r[2] for r in raw], lows=[r[3] for r in raw], closes=[r[4] for r in raw], volumes=[r[5] for r in raw])
cfg = Entry5mConfig(stop_mode=a.stop_mode, target_mode=a.target_mode, target_r=a.target_r, bias_source=a.bias_source,
scale_out_r=a.scale_out_r, slippage_ticks=0)
res = run_entry5m(bars, cfg, symbol=a.symbol) # slippage_ticks=0 is kept (it differs from the default)
col = {k: column(ent, k, exact=True) for k in ("biasStartNow", "biasSide", "biasKind", "biasExt", "biasTarget", "biasEndCode",
"orderPlaced", "pendEntry", "pendStop", "pendTarget", "pendCancel",
"fillNow", "posFill", "partialNow", "partialPx", "exitCode", "exitPx", "tradeR")}
bad = []
first = max(a.first, 0)
# bias starts
py_starts = {b.start_5m: b for b in res.biases if b.start_5m >= first}
tv_starts = {i for i in range(first, len(T)) if _i(col["biasStartNow"].get(T[i], 0)) == 1}
for i in sorted(set(py_starts) | tv_starts):
b = py_starts.get(i)
if b is None or i not in tv_starts:
bad.append(f"bias start only in {'pyakao' if b else 'TV'} at bar {i} {datetime.fromtimestamp(T[i], tz=timezone.utc)}")
continue
side, kind = _i(col["biasSide"].get(T[i], 0)), _i(col["biasKind"].get(T[i], 0))
if side != b.side or kind != KIND[b.kind] or not close(col["biasExt"].get(T[i]), b.ext) or not close(col["biasTarget"].get(T[i]), b.target):
bad.append(f"bias start fields differ at bar {i}: TV side {side} kind {kind} ext {col['biasExt'].get(T[i])} tgt {col['biasTarget'].get(T[i])} vs pyakao {b.side} {b.kind} {b.ext} {b.target}")
# bias ends
py_ends = {b.end_5m: END[b.end_reason] for b in res.biases if b.end_5m >= first}
for i in range(first, len(T)):
c = _i(col["biasEndCode"].get(T[i], 0))
if c != py_ends.get(i, 0):
bad.append(f"bias end differs at bar {i}: TV code {c} vs pyakao {py_ends.get(i, 0)}")
# orders
py_orders = {o.placed_bar: o for o in res.orders if o.placed_bar >= first}
for i in range(first, len(T)):
placed = _i(col["orderPlaced"].get(T[i], 0)) == 1
o = py_orders.get(i)
if placed != (o is not None):
bad.append(f"order placement differs at bar {i}: TV {placed} vs pyakao {o is not None}")
elif o is not None and not all(close(col[k].get(T[i]), v) for k, v in (("pendEntry", o.entry), ("pendStop", o.stop), ("pendTarget", o.target), ("pendCancel", o.pb_extreme))):
bad.append(f"order levels differ at bar {i}: TV {[col[k].get(T[i]) for k in ('pendEntry', 'pendStop', 'pendTarget', 'pendCancel')]} vs pyakao {(o.entry, o.stop, o.target, o.pb_extreme)}")
n_bias_order = len(bad)
# fills / partials / exits (listed, not required)
py_fills = {t.entry_bar: t for t in res.trades if t.entry_bar >= first}
for i in range(first, len(T)):
f = _i(col["fillNow"].get(T[i], 0)) == 1
t = py_fills.get(i)
if f != (t is not None) or (t is not None and not close(col["posFill"].get(T[i]), t.entry)):
bad.append(f"fill differs at bar {i}: TV {f} {col['posFill'].get(T[i])} vs pyakao {t is not None} {getattr(t, 'entry', None)}")
py_exits = {t.exit_bar: t for t in res.trades if t.exit_bar >= first}
for i in range(first, len(T)):
c = _i(col["exitCode"].get(T[i], 0))
t = py_exits.get(i)
want = EXIT[t.reason] if t else 0
if c != want or (t is not None and (not close(col["exitPx"].get(T[i]), t.exit) or not close(col["tradeR"].get(T[i]), t.r, 1e-3))):
bad.append(f"exit differs at bar {i}: TV code {c} px {col['exitPx'].get(T[i])} R {col['tradeR'].get(T[i])} vs pyakao {t.reason if t else None} {getattr(t, 'exit', None)} {getattr(t, 'r', None)}")
for line in bad[:60]:
print("MISMATCH", line)
print(f"bars {len(T)} from {first}; pyakao biases {len(res.biases)} orders {len(res.orders)} trades {len(res.trades)}; "
f"mismatches: bias/order {n_bias_order}, fills/exits {len(bad) - n_bias_order}")
print("RESULT:", "PASS" if n_bias_order == 0 else "FAIL")
return 0 if n_bias_order == 0 else 1
if __name__ == "__main__":
sys.exit(main())
python entry5m/validation/compare_entry_tv.py --data entry5m/validation/data_entry --symbol EURUSD --tf 5 --first <bar> (the pyakao mintick for EURUSD is 0.00001, the same as syminfo.mintick on the FXCM feed; if the chart's feed differs, pass the chart's tick by editing Entry5mConfig(mintick=...) in the script and say so) where <bar> is the first chart bar after the H1 leg has 2100 closed bars (about 2100 x 12 = 25,200 5m bars — if the export holds fewer, use the first bar at which the indicator's htfSigBear/htfSigBull history exists at all and say so; the pyakao run needs ~2000 H1 bars of warm-up too, so both sides are blind on the same stretch). Then the partial export with --scale-out-r 1.0. Expected: RESULT: PASS on bias/orders. Every fill/exit mismatch line is traced in the report to its cause (a 5m bar whose HTF bucket TV assigns differently from build_htf, or a gap-open fill) — if a mismatch has no such cause, it is a bug in the Pine port or the plan: fix the Pine (the Python is the reference), re-export, re-run.
With replay_start at three dates from the parity export (one divergence bias start, one arm bias start after switching the instance to Extreme arm, one partial + breakeven with the partial on), step bar by bar and confirm the indicator's data window at the event bars equals the export (same-bar, non-repainting), reading replay_status as the cursor truth and accepting a value only when two reads agree. replay_stop afterwards; restore the instance inputs to the defaults.
cd "D:\vwap tpo pine" && git add pyakao/src/pyakao/entry5m/runner.py pyakao/tests/test_entry5m_runner.py entry5m/validation/compare_entry_tv.py
git commit -m "validation(entry-pine): orders log in the runner, chart-level parity script, replay checks" -m "Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>"
Files:
Modify: USER_GUIDE_indicators.md (new section 13 "ZTD Entry"), README.md (script table row), entry5m/README.md (Pine section), spec Status: line
Memory: C:\Users\Danielshobhan\.claude\projects\D--vwap-tpo-pine\memory\indicator-suite.md (one paragraph), project-state.md (status line)
[ ] Step 1: Guide section 13 — what it is (one paragraph), how the H1 bias reaches the 5m chart and why it never repaints, the inputs table from spec section 3 with the input ids as reported by data_get_indicator, how to read the drawings and HUD, the alert messages (one line each with the exact prefix), the data-window titles, the verification results (H1 gate mismatches, chart parity counts, replay checks) with dates and the pine-facade version, and the known limits from spec section 9.
[ ] Step 2: README rows — root README.md script table: entry5m/pine/ztd_entry.pine "ZTD Entry" row with the one-line purpose and the version; entry5m/README.md: a "Pine indicator" section pointing to the spec, the plan, the transform command (python entry5m/tools/make_entry_core.py), the parity commands, and the verification numbers. Spec Status: becomes built <date>, ZTD Entry v<N>; verified: H1 gate PASS, chart parity <counts>.
[ ] Step 3: Memory — indicator-suite.md: a paragraph "ZTD Entry (2026-09-0x)" naming the script, the security-wrapped core idiom, the input ids, the verification status, and the entity id on Daniel's chart; project-state.md: replace the "next" sentence with "ZTD Entry indicator built and verified; the entry rules remain research-dead (six pre-registered fails), the indicator is the forward-observation tool".
[ ] Step 4: Commit
cd "D:\vwap tpo pine" && git add USER_GUIDE_indicators.md README.md entry5m/README.md entry5m/docs/2026-09-06-ztd-entry-indicator-design.md
git commit -m "docs(entry-pine): guide section 13, README rows, spec status" -m "Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>"
Spec coverage. Section 2 architecture: Task 1 (transform, f_ztd, f_ztdPrev, security with lookahead_on, htfNew, constants incl. the tMinTier rule). Section 3 inputs: Task 1 skeleton (every row of the table, chart warning in Task 4's HUD). Section 4 bias tracker: Task 2 (divergence and arm branches, target-touch end, replace, deadline renewal). Section 5 pullbacks/orders: Task 3 (pivots, origin search with earliest-tie, age, pricing per stop/target mode, already-broken, RR floor, replace, cancel at the cancel level, expiry, no order while in position). Section 6 simulated position: Task 3 (f_manage: stop, partial, breakeven from the next bar, target; fill at entry or gap open; no slippage; size-weighted R). Section 7 drawings/HUD/alerts/data window: Tasks 2-4 (all listed titles present: HTF surface 11, bias 8, orders/position 21; alert() messages per event; one alertcondition). Section 8 verification: Task 1 step 7 (H1 gate, midLoB/midHiU against the port), Task 5 (chart parity with the orders log, partial export, replay checks, pipeline per the quirks memory). Section 9 limits: documented in Task 6.
Placeholder scan. No TBD/TODO. Every Pine block is complete; the two conditional instructions (Task 1 step 6 compile fallbacks, Task 5 step 4 warm-up bar) say exactly what to do.
Type consistency. Plot titles used by compare_core_tv.py (htfSigBear, htfSigBull, htfOvl, htfExt, htfCurHi, htfCurLo, htfMidLoB, htfMidHiU) match Task 1's plots; titles used by compare_entry_tv.py (biasStartNow, biasSide, biasKind, biasExt, biasTarget, biasEndCode, orderPlaced, pendEntry, pendStop, pendTarget, pendCancel, fillNow, posFill, partialNow, partialPx, exitCode, exitPx, tradeR) match Tasks 2-3; codes (biasEndCode 1-5, exitCode 1-4, orderCancelCode 1-4, biasKind 1-3) match the Python reason strings through the END/EXIT/KIND maps; f_manage's 7-tuple is destructured identically at both call sites; Entry5mResult.orders holds PendingOrder whose pb_extreme is the cancel level, as pendCancel.