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
9 changes: 9 additions & 0 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
from ..symbols import PineType, method_receiver_cpp_token
from .tables import (
BAR_SERIES_PUSH,
TA_CHART_PREV_CLOSE_ARG,
DRAWING_TYPE_TO_CPP,
PINE_TYPE_TO_CPP,
RUNTIME_REGISTER_SECURITY_EVAL_FN,
Expand Down Expand Up @@ -1982,6 +1983,14 @@ def _emit_precalculate_and_run(self, lines: list[str]) -> None:
if self._ta_site_uses_precalc(site):
compute_args = self._ta_compute_args_for_site(site)
compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
# issue #178: the precalc pre-pass walks the bar array
# itself, so the previous CHART close is bars[i - 1]
# (the engine's prev_chart_close() tracker only advances
# at on_bar dispatch).
compute_args_bars = compute_args_bars.replace(
TA_CHART_PREV_CLOSE_ARG,
"(i > 0 ? bars[i - 1].close : na<double>())",
)
lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
finally:
self._precalc_loop_active = False
Expand Down
11 changes: 10 additions & 1 deletion pineforge_codegen/codegen/ta.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
Ternary, TupleAssign, TupleLiteral, TypeDecl, TypeField, UnaryOp, VarDecl,
WhileStmt,
)
from .tables import TA_IMPLICIT_APPEND, TA_IMPLICIT_COMPUTE_FULL
from .tables import (
TA_CHART_PREV_CLOSE,
TA_CHART_PREV_CLOSE_ARG,
TA_IMPLICIT_APPEND,
TA_IMPLICIT_COMPUTE_FULL,
)

if TYPE_CHECKING:
from ..analyzer import TACallSite
Expand Down Expand Up @@ -113,6 +118,10 @@ def _ta_compute_args_for_site(self, site: "TACallSite") -> str:

if ta_name in TA_IMPLICIT_COMPUTE_FULL:
implicit = TA_IMPLICIT_COMPUTE_FULL[ta_name]
# issue #178: chart-context atr / tr take the previous CHART
# bar's close as a 4th argument (see TA_CHART_PREV_CLOSE).
if ta_name in TA_CHART_PREV_CLOSE:
implicit = f"{implicit}, {TA_CHART_PREV_CLOSE_ARG}"
if site.compute_args:
explicit = ", ".join(self._visit_expr(a) for a in site.compute_args)
if ta_name in self._TA_IMPLICIT_REPLACE:
Expand Down
12 changes: 12 additions & 0 deletions pineforge_codegen/codegen/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,18 @@ def tz_time_field_lambda(field_expr: str, ts_arg: str, tz_arg: str) -> str:
"max": [0], "min": [0], "rci": [0],
}

# issue #178: ta.atr / ta.tr read their true range against the previous
# CHART bar's close (close[1]) even when the call site executes sparsely —
# the RMA advances on the executions only (TradingView pin 2026-09-06,
# lab tv i178-sparse-atr-sense, BINANCE:BTCUSDT 60: 398/398 executions equal
# the chart-close model, 0/398 the previous-execution model). The chart
# context therefore passes ``BacktestEngine::prev_chart_close()`` as the
# 4th compute() argument (the engine's always-on tracker); the per-object
# prev_close (3-arg form) remains for request.security contexts, whose
# ``bar.`` rewrite has no chart-close tracker.
TA_CHART_PREV_CLOSE = {"atr", "tr"}
TA_CHART_PREV_CLOSE_ARG = "prev_chart_close()"

# TA functions whose ``.compute()`` always receives bar OHLC implicitly.
TA_IMPLICIT_COMPUTE_FULL = {
"atr": "current_bar_.high, current_bar_.low, current_bar_.close",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_codegen_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def test_ta_tr_handle_na_false_routes_through_tr_class():
assert re.search(r"_ta_tr_\d+\(false\)", cpp), cpp
# Compute is invoked with the bar OHLC implicitly threaded in.
assert (
"_ta_tr_1.compute(current_bar_.high, current_bar_.low, current_bar_.close)"
"_ta_tr_1.compute(current_bar_.high, current_bar_.low, current_bar_.close, prev_chart_close())"
in cpp
)

Expand Down
65 changes: 65 additions & 0 deletions tests/test_sparse_atr_prev_close.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""issue #178: ta.atr / ta.tr take the previous CHART bar's close.

TradingView pin (2026-09-06, lab tv i178-sparse-atr-sense, BINANCE:BTCUSDT 60,
398/398 sparse executions): ``ta.atr(n)`` called inside a block that does not
execute every bar advances its RMA on the executions only, but its true range
always reads ``close[1]`` of the CHART, never the close of the site's previous
execution. The engine's per-object ``prev_close`` is the refuted model, so the
chart-context compute() passes ``BacktestEngine::prev_chart_close()`` as a 4th
argument; the precalc pre-pass (which walks ``bars[i]`` itself) passes
``bars[i - 1].close``; a request.security context keeps the 3-arg form.
"""

from __future__ import annotations

import re

import pineforge_codegen as pc

PRELUDE = '//@version=6\nstrategy("t", overlay=true)\n'


def _gen(src: str) -> str:
return pc.transpile(PRELUDE + src, check_support=False)


def test_sparse_atr_chart_context_passes_prev_chart_close():
cpp = _gen(
"reach = close[1] > open[1] and close < open\n"
"var float a = na\n"
"if reach\n"
" a := ta.atr(3)\n"
"plot(a)\n"
)
assert (
"_ta_atr_1.compute(current_bar_.high, current_bar_.low, current_bar_.close, prev_chart_close())"
in cpp
), cpp
assert (
"_ta_atr_1.recompute(current_bar_.high, current_bar_.low, current_bar_.close, prev_chart_close())"
in cpp
), cpp


def test_every_bar_tr_passes_prev_chart_close_and_precalc_uses_bars_i_minus_1():
cpp = _gen("t = ta.tr(true)\nplot(t)\n")
assert (
"_ta_tr_1.compute(current_bar_.high, current_bar_.low, current_bar_.close, prev_chart_close())"
in cpp
), cpp
# The precalc pre-pass, when emitted, walks the bar array itself.
for m in re.finditer(r"_precalc__ta_tr_1\[i\] = _ta_tr_1\.compute\(([^;]*)\);", cpp):
assert m.group(1) == "bars[i].high, bars[i].low, bars[i].close, (i > 0 ? bars[i - 1].close : na<double>())", m.group(0)
assert "prev_chart_close()" not in "".join(
l for l in cpp.splitlines() if "_precalc_" in l
), cpp


def test_security_context_atr_keeps_three_arg_form():
cpp = _gen(
'a = request.security(syminfo.tickerid, "D", ta.atr(14))\n'
"plot(a)\n"
)
sec_lines = [l for l in cpp.splitlines() if "compute(bar.high, bar.low, bar.close" in l]
assert sec_lines, cpp
assert all("prev_chart_close()" not in l for l in sec_lines), sec_lines
2 changes: 1 addition & 1 deletion tests/test_transpile_tr_handle_na.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_ta_tr_compute_uses_implicit_bar_ohlc():
# implicit bar OHLC (high, low, close) — it never sees ``handle_na``.
cpp = _generate(_wrap("x = ta.tr()"))
assert (
"_ta_tr_1.compute(current_bar_.high, current_bar_.low, current_bar_.close)"
"_ta_tr_1.compute(current_bar_.high, current_bar_.low, current_bar_.close, prev_chart_close())"
in cpp
), cpp
# Defensive: the bool literal must NOT appear inside the compute call.
Expand Down
Loading