diff --git a/pineforge_codegen/analyzer/base.py b/pineforge_codegen/analyzer/base.py index e9d488a..770d20d 100644 --- a/pineforge_codegen/analyzer/base.py +++ b/pineforge_codegen/analyzer/base.py @@ -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 diff --git a/pineforge_codegen/analyzer/call_handlers.py b/pineforge_codegen/analyzer/call_handlers.py index ab524f2..f1ef7de 100644 --- a/pineforge_codegen/analyzer/call_handlers.py +++ b/pineforge_codegen/analyzer/call_handlers.py @@ -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: diff --git a/pineforge_codegen/codegen/visit_expr.py b/pineforge_codegen/codegen/visit_expr.py index 48a8fe7..c1a2ccb 100644 --- a/pineforge_codegen/codegen/visit_expr.py +++ b/pineforge_codegen/codegen/visit_expr.py @@ -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 — diff --git a/tests/test_anchor_vwap.py b/tests/test_anchor_vwap.py index f1dae45..ffac923 100644 --- a/tests/test_anchor_vwap.py +++ b/tests/test_anchor_vwap.py @@ -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(): diff --git a/tests/test_vwap_property_source.py b/tests/test_vwap_property_source.py new file mode 100644 index 0000000..79901a5 --- /dev/null +++ b/tests/test_vwap_property_source.py @@ -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__ta_vwap_`` +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 + diff --git a/tests/test_vwap_tuple_unpack.py b/tests/test_vwap_tuple_unpack.py index 5760f03..e9d9896 100644 --- a/tests/test_vwap_tuple_unpack.py +++ b/tests/test_vwap_tuple_unpack.py @@ -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 @@ -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