Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions AGENTS.md

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions CLAUDE.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pineforge_codegen/analyzer/call_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,9 +511,9 @@ def _handle_ta_call(self, func_name: str, node: FuncCall) -> PineType:
if func_name == "vwap" and not all_args:
# The bare ``ta.vwap`` property is the VWAP of hlc3 (Pine v6
# reference: "It uses hlc3 as its source series"); TradingView's
# own read equals hlc3 on a daily bar (lab tv
# notrade-session-vwap-f1d, NYSE:F 1D 2025-07-01..08-31, 42/42,
# 2026-09-05), where the engine used to read the bar's close.
# own read equals hlc3 on a daily bar (a daily US-stock probe,
# tapes kept outside this repository), where the engine used to
# read the bar's close.
default_src = Identifier(name="hlc3")
self._visit(default_src)
for field in ("high", "low", "close"):
Expand Down
2 changes: 1 addition & 1 deletion pineforge_codegen/codegen/session_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
timeframe.

TradingView flags a bar by its own open time: on an extended-hours
NASDAQ:AAPL 60 chart the 09:00 bar, which holds the 09:30 open, is
US-listed stock's 60-minute chart the 09:00 bar, which holds the 09:30 open, is
pre-market and the 16:00 bar post-market. It reads a session's day mask per
session day, so an overnight session's Sunday-evening open belongs to
Monday's, and a ``0000-2400`` day is in market throughout; a D/W/M bar holds
Expand Down
69 changes: 36 additions & 33 deletions pineforge_codegen/codegen/ta.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,34 +407,36 @@ def _ta_call_nodes_under_lazy_rhs(self) -> set[int]:
# TradingView's per-family clocks below a top-level lazy edge
# ------------------------------------------------------------------
#
# Pinned 2026-09-03 with ``lab tv`` on NYSE:F 1D (range 2025-04-01 ..
# 2026-05-01; cadence-7 ternary probes ``v = bar_index % 7 == 3 ? <call> :
# Pinned on a daily US-stock probe (tapes kept outside this repository;
# cadence-7 ternary probes ``v = bar_index % 7 == 3 ? <call> :
# na`` exposing the value through the entry size, plus lazy-``and``
# probes), each scored per call against three models:
#
# every-bar natives highest 9/9, sma 25/25, ema 23/23 -- the runtime
# every-bar natives highest, sma, ema (every trade matched) -- the runtime
# advances the built-in on every chart bar; only the
# value is gated (``_lazy_edge_ta_hoist_plan``).
# hold-last source roc 38/38 (+39/39 entries), change 39/39, mom
# 39/39 -- TradingView computes these from the CALL'S
# hold-last source roc, change, mom (every trade matched) --
# TradingView computes these from the CALL'S
# OWN ``source[length]`` history: the source is
# written only on bars where the call executes, the
# last executed value is held on skipped bars, and
# the history is na before the first execution
# (call 1 of every value probe has no TV entry).
# every-bar 0/38..0/39, ring-of-executions 0..1/39.
# per-execution cum, barssince, valuewhen, cross, crossover 39/39,
# rising 39/39 (strictly monotonic over its executed
# samples) and math.sum 39/39 (na until three
# executed samples) -- the native only ever sees the
# samples of bars where the call executes, which is
# exactly the reached-only inline ``compute`` lowering
# (every-bar 0..31/39).
# the every-bar clock matched almost no trade and the
# ring-of-executions clock at most one.
# per-execution cum, barssince, valuewhen, cross, crossover,
# rising (strictly monotonic over its executed
# samples) and math.sum (na until three
# executed samples), every trade matched -- the native
# only ever sees the samples of bars where the call
# executes, which is exactly the reached-only inline
# ``compute`` lowering (the every-bar clock matched
# fewer than all of them).
#
# The hoist is an ALLOW-LIST and requires a direct ``[k]`` read. A broad
# every-bar hoist of every stateful family measured on Cloud Run against
# the full population (2026-09-04, like-for-like vs the same lab bundle)
# fixed the pinned shapes but cost 170 tiers / 30 hard-lane regressions,
# the full population (like-for-like vs the same lab bundle)
# fixed the pinned shapes but cost many tiers and hard-lane regressions,
# and hoisting allow-listed families WITHOUT a history read still broke
# four ETH hard-lane probes that main matched at 100%: ``adxVal > 30 and
# adxVal > ta.sma(adxVal, 7)`` (louislapis9), ``volume < ta.sma(volume,
Expand All @@ -444,12 +446,12 @@ def _ta_call_nodes_under_lazy_rhs(self) -> set[int]:
# ``ta.highest(high, n) / entry > 1.05`` under ``and`` (miemomo3). Every
# every-bar tape (highest/sma/ema ``[1]`` under ``and``, the ternary
# ``ta.highest(high, 5)[1]``, robmagnaye) reads ``[k]`` on the call, and
# the bare twins of those tapes (2026-09-04, same cadence-7 probes) are
# per-execution: ``close > ta.sma(close, 5)`` ring 39/39 (every-bar 23),
# ``close > ta.ema(close, 5)`` ring 39/39 (26), ``? ta.sma(close, 5) : na``
# ring 39/39 (2, na until five executed samples), ``? ta.highest(high, 5)
# : na`` ring 38/39 (11; TV is na with one executed sample, then the
# partial maximum). So: TradingView keeps a lazily executed call on the
# the bare twins of those tapes (same cadence-7 probes) are
# per-execution: ``close > ta.sma(close, 5)`` ring matched every trade
# (every-bar did not), likewise ``close > ta.ema(close, 5)``,
# ``? ta.sma(close, 5) : na`` (na until five executed samples) and
# ``? ta.highest(high, 5) : na`` (ring matched all but one trade; TV is na
# with one executed sample, then the partial maximum). So: TradingView keeps a lazily executed call on the
# reached-only clock unless the call's own history is referenced (``[k]``),
# in which case it materialises a continuous series and the call is
# evaluated on every bar. Only ``LAZY_EVERY_BAR_TA`` families
Expand Down Expand Up @@ -670,12 +672,13 @@ def _ta_site_uses_precalc(self, site: "TACallSite") -> bool:
per-execution inline compute (cum/barssince/valuewhen/cross*/rising/...)
must never precalc.

Re-pinned 2026-09-03: the old opt-out for a chart ``ta.sma`` under an
Re-pinned: the old opt-out for a chart ``ta.sma`` under an
``and`` RHS (pf-probe-oliver-dual-vol-sma, "eager precalc is
TV-incorrect") and the matching recursive ``ta.ema`` rule encoded a
per-call clock that ``lab tv`` refuted for top-level shapes (NYSE:F 1D,
``... and close > ta.sma(close,5)[1]``: every-bar 25/25 vs per-call 28;
``ta.ema``: 23/23 vs 27). The analyzer never marks a site inside an
per-call clock that TradingView refuted for top-level shapes (a daily
US-stock probe, ``... and close > ta.sma(close,5)[1]``: the every-bar
clock matched every trade and the per-call clock did not; ``ta.ema``
likewise). The analyzer never marks a site inside an
``if``/loop/function scope static, so after hoisting the opt-outs
only reach a static lazy-edge ``ta.sma``/``ta.ema`` that is not a
top-level statement operand -- in practice a UDT field default
Expand Down Expand Up @@ -717,21 +720,21 @@ def _ta_site_uses_precalc(self, site: "TACallSite") -> bool:
# Every-bar hoisting of stateful ``ta.*`` sites below top-level lazy edges
# ------------------------------------------------------------------
#
# TradingView rule, pinned 2026-09-03 with ``lab tv`` on NYSE:F 1D (tapes
# out-pin-ring-lazyand / lazyand-sma / lazyand-ema / ring-ternary): a
# TradingView rule, pinned on a daily US-stock probe (tapes kept outside
# this repository; out-pin-ring-lazyand / lazyand-sma / lazyand-ema / ring-ternary): a
# stateful ``ta.*`` call inside ANY expression operand of a top-level
# statement -- the RHS of a Pine-v6 lazy ``and``/``or``, either branch of a
# ternary, nested comparisons -- is evaluated on EVERY bar. Short-circuiting
# and branch selection gate only the *value*, never the built-in's state,
# and ``[1]`` on such a call is the previous BAR's value:
#
# c = bar_index % 7 == 3 and close > ta.highest(high,5)[1] 9/9 entries
# c = bar_index % 7 == 3 and close > ta.sma(close,5)[1] 25/25
# c = bar_index % 7 == 3 and close > ta.ema(close,5)[1] 23/23
# v = bar_index % 7 == 3 ? ta.highest(high,5)[1] : na 38/39
# c = bar_index % 7 == 3 and close > ta.highest(high,5)[1]
# c = bar_index % 7 == 3 and close > ta.sma(close,5)[1]
# c = bar_index % 7 == 3 and close > ta.ema(close,5)[1]
# v = bar_index % 7 == 3 ? ta.highest(high,5)[1] : na
#
# (the per-call / "previous evaluation" model predicts 8/9, 28, 27 and
# 2/39). Production instance: robmagnaye14 ``bullMSS = setupAlive and
# (the per-call / "previous evaluation" model mispredicts these).
# Production instance: robmagnaye14 ``bullMSS = setupAlive and
# dir == 1 and close > ta.highest(high, 10)[1]``.
#
# A stateful call INSIDE an ``if``/local-block/function body that does not
Expand Down
4 changes: 2 additions & 2 deletions pineforge_codegen/codegen/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,8 +563,8 @@ def _pure_int_literal_value(self, node, names: bool = True,
milliseconds) is 7 776 000 000 on TradingView. Emitted as C++ ``int``
literal arithmetic the same product overflows (wraps to -813 934 592),
so a ``(time - t0) > threeMonths`` expiry fires on every bar (round 8
family U: latibonit15 execution-signals-confluence, six lanes; lab tv
u-lati-levels-nq15 vs the engine, 2026-09-05). ``400 * MS`` over
family U: latibonit15 execution-signals-confluence, six lanes; a recorded
probe tape vs the engine). ``400 * MS`` over
``const int MS = 7200000`` is 2 880 000 000 too (lab tv
pf-cgs-int64-const, 2026-09-28): the constant is emitted as its
literal, so the product was the same C++ ``int`` overflow. Folding
Expand Down
4 changes: 2 additions & 2 deletions pineforge_codegen/codegen/visit_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,8 +865,8 @@ def _visit_member_access(self, node: MemberAccess) -> str:
# Same implicit tail as TA_IMPLICIT_APPEND["vwap"]:
# the symbol clock keys the Daily anchor reset.
# The bare property is the VWAP of hlc3 (Pine
# v6 reference; lab tv notrade-session-vwap-f1d,
# 2026-09-05: TradingView's read equals hlc3 on
# v6 reference; a recorded daily US-stock probe:
# TradingView's read equals hlc3 on
# a daily bar), never of the close.
_hlc3 = "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)"
return (
Expand Down
43 changes: 43 additions & 0 deletions tests/_private_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Find a fixture that lives in the private fixtures repository.

Fixtures that cannot be public are overlaid by the lab onto the paired ENGINE tree, under ``tests/codegen-fixtures/``, with
``PINEFORGE_PRIVATE_FIXTURES=<commit>`` exported (``lab remote submit --private-fixtures``). A test names such a file
relative to that folder and calls ``private_fixture``:

* the file is there: its path comes back;
* the file is not there and no overlay was asked for: the test skips with ``private fixtures not present: <path>``;
* the file is not there although the overlay is present: ``AssertionError``, never a skip, so a broken overlay cannot turn a
private test into a silent pass.

The engine tree is found the way the rest of the suite finds it: ``PINEFORGE_ENGINE_INCLUDE`` is ``<engine>/include``.
"""

from __future__ import annotations

import os
from pathlib import Path

import pytest

FOLDER = Path("tests") / "codegen-fixtures"


def _engine_dir() -> Path | None:
raw = os.environ.get("PINEFORGE_ENGINE_INCLUDE", "")
return Path(raw).resolve().parent if raw else None


def private_fixture(rel: str) -> Path:
"""The path of ``tests/codegen-fixtures/<rel>`` in the paired engine tree, or a skip (see the module text)."""
target = Path(rel)
if target.is_absolute() or ".." in target.parts:
raise ValueError(f"private fixture path must stay inside the folder: {rel}")
engine = _engine_dir()
path = engine / FOLDER / target if engine is not None else None
if path is not None and path.is_file():
return path
if os.environ.get("PINEFORGE_PRIVATE_FIXTURES"):
raise AssertionError(
f"the private fixtures are overlaid (PINEFORGE_PRIVATE_FIXTURES is set) but {rel} is missing from "
f"{FOLDER} in the engine tree")
pytest.skip(f"private fixtures not present: {rel}")
13 changes: 10 additions & 3 deletions tests/fixtures/c7_tv_evidence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,21 @@ The period is `O=1821.59, H=1842.57, L=1816.64, C=1837.47`, followed by an ancho
## VWAP first-value evidence

`vwap_first_value_probe.pine` is the public TA1 probe for the first value of
the omitted, explicit daily, and custom-anchor forms. The two TradingView
exports are preserved under `tv-eurusd/` and `tv-eth/`:
the omitted, explicit daily, and custom-anchor forms. The public TradingView
export is preserved under `tv-eth/`:

| chart | `ta.vwap(close)` | `ta.vwap(close, timeframe.change("1D"))` | `ta.vwap(close, bar_index == 5)` |
|---|---|---|---|
| OANDA:EURUSD, 15m, 2025-04-01 | bar 0 (`V_default_vwap`, 08:00) | first day change (`E_explicit_vwap`, 2025-04-02 05:00) | bar 5 (`A_anchor5_vwap`, 09:15) |
| BINANCE:ETHUSDT.P, 15m, 2025-04-01 00:00 UTC | bar 0 (`V_default_vwap`, 00:00) | first day change (`E_explicit_vwap`, 2025-04-02 00:00) | bar 5 (`A_anchor5_vwap`, 01:15) |

The codegen E2E reproduces the same bar positions on the engine feed. The
explicit daily forms therefore use the anchored shim; only omitted-anchor VWAP
and its band form retain `ta::VWAP` / `ta::VWAPBands`.

### Private VWAP first-value tape

TradingView trade list for forex, kept in a private repository and overlaid by
the lab onto the paired engine tree at `tests/codegen-fixtures/c7_tv_evidence/`.
The source is read by the private fixture battery in
`tests/test_host_member_names.py`. `tests/test_e2e_vwap_anchor.py` checks the
first-value rule on the public engine feed.
16 changes: 0 additions & 16 deletions tests/fixtures/c7_tv_evidence/tv-eurusd/meta.json

This file was deleted.

40 changes: 0 additions & 40 deletions tests/fixtures/c7_tv_evidence/tv-eurusd/metrics.json

This file was deleted.

42 changes: 0 additions & 42 deletions tests/fixtures/c7_tv_evidence/tv-eurusd/strategy.pine

This file was deleted.

11 changes: 0 additions & 11 deletions tests/fixtures/c7_tv_evidence/tv-eurusd/tv_trades.csv

This file was deleted.

Loading
Loading