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
14 changes: 13 additions & 1 deletion pineforge_codegen/analyzer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5973,7 +5973,19 @@ def _visit_MemberAccess(self, node: MemberAccess) -> PineType:
callee=MemberAccess(object=Identifier(name="ta"), member=node.member),
args=[], kwargs={},
)
return self._handle_ta_call(node.member, synthetic_call)
before = len(self._ta_call_sites)
result = self._handle_ta_call(node.member, synthetic_call)
# The site belongs to THIS read: key it on the AST node the
# codegen will meet (the MemberAccess), not on the synthetic
# call nobody else holds. Without this a bare ``ta.vwap``
# inside request.security() could not find its own site and
# was lowered to the first live CHART member (advanced by the
# chart bar AND every requested sub-bar, read through
# history_advances_new_bar()); a second top-level read bound
# to the first read's member as well.
if len(self._ta_call_sites) > before:
self._ta_call_sites[-1].node = node
return result
return PineType.FLOAT

# math.* properties
Expand Down
10 changes: 8 additions & 2 deletions pineforge_codegen/analyzer/call_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,9 +437,15 @@ def _handle_ta_call(self, func_name: str, node: FuncCall) -> PineType:
all_args = [default_arg]

if func_name == "vwap" and not all_args:
default_src = Identifier(name="close")
# 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.
default_src = Identifier(name="hlc3")
self._visit(default_src)
self._series_bar_fields.add("close")
for field in ("high", "low", "close"):
self._series_bar_fields.add(field)
all_args = [default_src]

# Handle ta.highest(length) / ta.lowest(length) with 1 arg:
Expand Down
28 changes: 21 additions & 7 deletions pineforge_codegen/codegen/visit_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,26 +673,40 @@ def _visit_member_access(self, node: MemberAccess) -> str:
# to dead ``f5``'s first-in-order ``_ta_vwap_10`` and emitted
# ``use of undeclared identifier '_ta_vwap_10'``. A live read
# must bind to a live site.
for _i, site in enumerate(self.ctx.ta_call_sites):
if _i in self._dead_ta_indices:
# The read's OWN site first (the analyzer keys the
# synthetic call-site on this MemberAccess node), so two
# bare reads are two sites, each advanced once per bar;
# the first-live-site scan below stays as the fallback.
_own = self._get_ta_site(node)
_candidates = (
[(self._ta_index_by_site_id.get(id(_own)), _own)]
if _own is not None else []
) + list(enumerate(self.ctx.ta_call_sites))
for _i, site in _candidates:
if _i is None or _i in self._dead_ta_indices:
continue
ta_short = site.class_name.split("::")[-1].lower()
if site.member_name.startswith(f"_ta_{node.member}_"):
if node.member == "vwap":
# 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
# a daily bar), never of the close.
_hlc3 = "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)"
return (
f"(history_advances_new_bar() ? {site.member_name}.compute("
"current_bar_.close, current_bar_.volume, current_bar_.timestamp"
f"(history_advances_new_bar() ? {self._ta_member_name(site)}.compute("
f"{_hlc3}, current_bar_.volume, current_bar_.timestamp"
" PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)) "
f": {site.member_name}.recompute(current_bar_.close, "
f": {self._ta_member_name(site)}.recompute({_hlc3}, "
"current_bar_.volume, current_bar_.timestamp"
" PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)))"
)
return (
f"(history_advances_new_bar() ? {site.member_name}.compute("
f"(history_advances_new_bar() ? {self._ta_member_name(site)}.compute("
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}) : "
f"{site.member_name}.recompute("
f"{self._ta_member_name(site)}.recompute("
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}))"
)
# No registered call site for this TA property read —
Expand Down
6 changes: 4 additions & 2 deletions tests/test_anchor_vwap.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ def test_ta_vwap_precalc_path_threads_symbol_clock():


def test_ta_vwap_bare_property_read_threads_symbol_clock():
# The bare property is the VWAP of hlc3 (tests/test_vwap_property_source.py).
cpp = transpile(_pine("v = ta.vwap\nw = ta.vwap"))
calls = [c for c in _calls(cpp, "compute") if "current_bar_.close" in c and "vwap" in c]
hlc3 = "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)"
calls = [c for c in _calls(cpp, "compute") if hlc3 in c and "vwap" in c]
assert calls, cpp
for call in calls:
assert "current_bar_.close, current_bar_.volume, current_bar_.timestamp " + SYM_TAIL in call, call
assert hlc3 + ", current_bar_.volume, current_bar_.timestamp " + SYM_TAIL in call, call


def test_ta_vwap_bands_form_threads_symbol_clock():
Expand Down
127 changes: 127 additions & 0 deletions tests/test_vwap_property_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""The bare ``ta.vwap`` property is the VWAP of hlc3, bound to its own site.

Pine v6 reference: ``ta.vwap`` "uses hlc3 as its source series". TradingView's
read equals hlc3 on a daily bar (lab tv notrade-session-vwap-f1d, NYSE:F 1D
2025-07-01..08-31, 42/42 bars, 2026-09-05) where the generated code used to
feed the bar's close (therealbouga-apex-mtf-index-model F@1D: ``close < vwap``
never true, 0 engine trades vs 9). And the property read is one TA site per
read, keyed on its own AST node: a bare ``ta.vwap`` inside
``request.security()`` is that evaluator's own ``_sec<id>__ta_vwap_<n>``
member (advanced by ``security_series_slot_is_new``), not the first live
CHART member advanced by the chart bar and every requested sub-bar
(nightowlxtrader-azt F@1D ``vwap5 = request.security(tickerid,
timeframe.period, ta.vwap)``).
"""

from __future__ import annotations

import re

from pineforge_codegen import transpile


HLC3_CHART = "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)"
HLC3_SEC = "((bar.high + bar.low + bar.close) / 3.0)"
SYM_TAIL = "PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session))"


def _pine(body: str) -> str:
return f'//@version=6\nstrategy("T")\n{body}\nplot(close)\n'


def _calls(cpp: str, symbol: str) -> list[str]:
return [m.group(0) for m in re.finditer(rf"\b{re.escape(symbol)}\([^;]*", cpp)]


def test_bare_property_reads_hlc3_not_close():
cpp = transpile(_pine("v = ta.vwap"))
computes = [c for c in _calls(cpp, "_ta_vwap_1.compute") if "current_bar_" in c]
assert computes, cpp
for call in computes:
assert call.startswith("_ta_vwap_1.compute(" + HLC3_CHART + ", current_bar_.volume, current_bar_.timestamp " + SYM_TAIL), call
assert "current_bar_.close, current_bar_.volume" not in call, call
for call in _calls(cpp, "_ta_vwap_1.recompute"):
assert call.startswith("_ta_vwap_1.recompute(" + HLC3_CHART), call
# The historical precalculation loop feeds bars[i] with the same source.
precalc = [c for c in _calls(cpp, "_ta_vwap_1.compute") if "bars[i]" in c]
assert precalc, cpp
for call in precalc:
assert call.startswith("_ta_vwap_1.compute(((bars[i].high + bars[i].low + bars[i].close) / 3.0), bars[i].volume"), call


def test_property_and_explicit_hlc3_call_agree():
cpp = transpile(_pine("v = ta.vwap\nw = ta.vwap(hlc3)"))
v_calls = [c for c in _calls(cpp, "_ta_vwap_1.compute") if "current_bar_" in c]
w_calls = [c for c in _calls(cpp, "_ta_vwap_2.compute") if "current_bar_" in c]
assert v_calls and w_calls, cpp
assert v_calls[0].replace("_ta_vwap_1", "X") == w_calls[0].replace("_ta_vwap_2", "X")


def test_two_bare_reads_are_two_sites_each_advanced_once():
cpp = transpile(_pine("v = ta.vwap\nw = ta.vwap"))
body = cpp[cpp.index("void on_bar("):]
body = body[: body.index("void precalculate(")] if "void precalculate(" in body else body
assert len([c for c in _calls(body, "_ta_vwap_1.compute")]) == 1, body
assert len([c for c in _calls(body, "_ta_vwap_2.compute")]) == 1, body


def test_bare_property_inside_security_binds_to_its_own_evaluator_member():
cpp = transpile(_pine(
'v = ta.vwap\n'
's = request.security(syminfo.tickerid, "D", ta.vwap)\n'
'if v > s\n'
' strategy.entry("L", strategy.long)\n'
))
evaluator = cpp[cpp.index("void _eval_security_0("):]
evaluator = evaluator[: evaluator.index("}\n")]
# Its own member, advanced by the requested context's slot rule, fed the
# requested bar's hlc3 -- never the chart member or the chart tick rule.
assert "security_series_slot_is_new(0) ? _sec0__ta_vwap_2.compute(" + HLC3_SEC + ", bar.volume, bar.timestamp " + SYM_TAIL in evaluator, evaluator
assert "_ta_vwap_1" not in evaluator, evaluator
assert "history_advances_new_bar()" not in evaluator, evaluator
assert "current_bar_" not in evaluator, evaluator
assert "ta::VWAP _sec0__ta_vwap_2;" in cpp, cpp
# The chart read keeps its own member and hlc3 source.
chart = [c for c in _calls(cpp, "_ta_vwap_1.compute") if "current_bar_" in c]
assert chart and chart[0].startswith("_ta_vwap_1.compute(" + HLC3_CHART), chart


def test_bare_property_inside_security_same_tf_as_chart():
# nightowlxtrader's shape: timeframe.period on the chart's own timeframe.
cpp = transpile(_pine(
'vw = request.security(syminfo.tickerid, timeframe.period, ta.vwap, lookahead=barmerge.lookahead_off)\n'
'if close > vw\n'
' strategy.entry("L", strategy.long)\n'
))
evaluator = cpp[cpp.index("void _eval_security_0("):]
evaluator = evaluator[: evaluator.index("}\n")]
assert "_sec0__ta_vwap_1.compute(" + HLC3_SEC in evaluator, evaluator
assert "history_advances_new_bar()" not in evaluator, evaluator


def test_user_declared_vwap_variable_is_not_the_property():
"""A script's OWN ``vwap`` is a user variable, never the ``ta.vwap`` property.

van007trader-vwap-deviation-score-dyna computes ``float vwap = sumV > 0 ?
sumPV / sumV : na`` from its session accumulators and reads ``close - vwap``;
it matched TradingView 100% on seven intraday lanes before and after the
property rule above (ea90029 emits byte-identical C++ for it, 2026-09-05).
The hlc3 / own-site rule applies to ``ta.vwap`` reads only: no VWAP site,
no ``ta::VWAP`` member, and the read is the user's variable.
"""
cpp = transpile(_pine(
"var float sumPV = 0.0\n"
"var float sumV = 0.0\n"
"float v = math.max(nz(volume, 0.0), 1.0)\n"
"sumPV := sumPV + hlc3 * v\n"
"sumV := sumV + v\n"
"float vwap = sumV > 0 ? sumPV / sumV : na\n"
"float z = (close - vwap) / 2.0\n"
"if z > 2.0\n"
" strategy.entry(\"S\", strategy.short)\n"
))
assert "_ta_vwap_" not in cpp, cpp
assert "ta::VWAP" not in cpp, cpp
assert "PF_VWAP_SESSION_ANCHOR_ARGS" not in cpp, cpp
assert "vwap" in cpp # the user's own variable is what the read resolves to

5 changes: 3 additions & 2 deletions tests/test_vwap_tuple_unpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def test_vwap_3arg_kwargs():


def test_vwap_bare_property():
"""Bare property form ta.vwap (no parens) should compile and compute from close, volume, timestamp."""
"""Bare property form ta.vwap (no parens) should compile and compute from hlc3, volume, timestamp."""
src = PRELUDE + """\
v = ta.vwap
if close > v
Expand All @@ -143,4 +143,5 @@ def test_vwap_bare_property():
assert _has_no_errors(src)
cpp = _transpile(src)
assert "ta::VWAP" in cpp
assert "compute(current_bar_.close, current_bar_.volume, current_bar_.timestamp PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session))" in cpp
# The bare property is the VWAP of hlc3 (tests/test_vwap_property_source.py).
assert "compute(((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0), current_bar_.volume, current_bar_.timestamp PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session))" in cpp
Loading