@pineforge/codegen-pyodide 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +13 -1
- package/pineforge_codegen/analyzer/call_handlers.py +8 -2
- package/pineforge_codegen/codegen/base.py +77 -21
- package/pineforge_codegen/codegen/emit_top.py +73 -20
- package/pineforge_codegen/codegen/ta.py +579 -288
- package/pineforge_codegen/codegen/tables.py +22 -5
- package/pineforge_codegen/codegen/types.py +195 -6
- package/pineforge_codegen/codegen/visit_call.py +120 -57
- package/pineforge_codegen/codegen/visit_expr.py +39 -8
- package/pineforge_codegen/codegen/visit_stmt.py +15 -3
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -5973,7 +5973,19 @@ class Analyzer(CallHandlers, DiagnosticsHelper, TypeHelper):
|
|
|
5973
5973
|
callee=MemberAccess(object=Identifier(name="ta"), member=node.member),
|
|
5974
5974
|
args=[], kwargs={},
|
|
5975
5975
|
)
|
|
5976
|
-
|
|
5976
|
+
before = len(self._ta_call_sites)
|
|
5977
|
+
result = self._handle_ta_call(node.member, synthetic_call)
|
|
5978
|
+
# The site belongs to THIS read: key it on the AST node the
|
|
5979
|
+
# codegen will meet (the MemberAccess), not on the synthetic
|
|
5980
|
+
# call nobody else holds. Without this a bare ``ta.vwap``
|
|
5981
|
+
# inside request.security() could not find its own site and
|
|
5982
|
+
# was lowered to the first live CHART member (advanced by the
|
|
5983
|
+
# chart bar AND every requested sub-bar, read through
|
|
5984
|
+
# history_advances_new_bar()); a second top-level read bound
|
|
5985
|
+
# to the first read's member as well.
|
|
5986
|
+
if len(self._ta_call_sites) > before:
|
|
5987
|
+
self._ta_call_sites[-1].node = node
|
|
5988
|
+
return result
|
|
5977
5989
|
return PineType.FLOAT
|
|
5978
5990
|
|
|
5979
5991
|
# math.* properties
|
|
@@ -437,9 +437,15 @@ class CallHandlers:
|
|
|
437
437
|
all_args = [default_arg]
|
|
438
438
|
|
|
439
439
|
if func_name == "vwap" and not all_args:
|
|
440
|
-
|
|
440
|
+
# The bare ``ta.vwap`` property is the VWAP of hlc3 (Pine v6
|
|
441
|
+
# reference: "It uses hlc3 as its source series"); TradingView's
|
|
442
|
+
# own read equals hlc3 on a daily bar (lab tv
|
|
443
|
+
# notrade-session-vwap-f1d, NYSE:F 1D 2025-07-01..08-31, 42/42,
|
|
444
|
+
# 2026-09-05), where the engine used to read the bar's close.
|
|
445
|
+
default_src = Identifier(name="hlc3")
|
|
441
446
|
self._visit(default_src)
|
|
442
|
-
|
|
447
|
+
for field in ("high", "low", "close"):
|
|
448
|
+
self._series_bar_fields.add(field)
|
|
443
449
|
all_args = [default_src]
|
|
444
450
|
|
|
445
451
|
# Handle ta.highest(length) / ta.lowest(length) with 1 arg:
|
|
@@ -5,6 +5,7 @@ results from AnalyzerContext instead of walking the AST to collect info.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
|
+
import re
|
|
8
9
|
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
|
|
@@ -309,6 +310,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
309
310
|
# Set of var/series member names that belong to user functions (need cloning)
|
|
310
311
|
self._func_var_members_set: set[str] = set()
|
|
311
312
|
self._precalc_loop_active: bool = False
|
|
313
|
+
# Top-level lazy-edge TA sites hoisted to every-bar evaluation for the
|
|
314
|
+
# statement currently being lowered: FuncCall id -> local name, and
|
|
315
|
+
# Subscript id -> ``_hist_call_*`` member (see ``ta.py``).
|
|
316
|
+
self._hoisted_ta_values: dict[int, str] = {}
|
|
317
|
+
self._hoisted_hist_reads: dict[int, str] = {}
|
|
312
318
|
# Names of ``var`` members that live in a callable scope (not global).
|
|
313
319
|
# Their exact declaration statements own initialization; they must not
|
|
314
320
|
# be initialized by the constructor or the global on_bar preamble.
|
|
@@ -934,6 +940,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
934
940
|
self._register_global_aggregate_member_types()
|
|
935
941
|
self._uses_map = self._detect_map_usage()
|
|
936
942
|
self._uses_matrix = self._detect_matrix_usage()
|
|
943
|
+
self._uses_pine_time = self._detect_pine_time_usage()
|
|
944
|
+
self._uses_vwap_anchor = self._detect_vwap_usage()
|
|
937
945
|
# Drawing-objects-as-data: gate all new emission (drawing.hpp include +
|
|
938
946
|
# the per-type arenas) on this flag so non-drawing strategies stay
|
|
939
947
|
# byte-identical. Caps come from the strategy() header max_*_count.
|
|
@@ -1906,7 +1914,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1906
1914
|
self._collection_types[name] = spec
|
|
1907
1915
|
elif ns == "array" and fn in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
|
|
1908
1916
|
self._array_vars.add(name)
|
|
1909
|
-
spec = self.
|
|
1917
|
+
spec = self._widen_array_spec_for_name(
|
|
1918
|
+
name,
|
|
1919
|
+
self._type_spec_from_expr(expr) or self._array_spec_for_name(name),
|
|
1920
|
+
)
|
|
1910
1921
|
self._collection_types[name] = spec
|
|
1911
1922
|
elif ns == "map" and fn == "new":
|
|
1912
1923
|
self._map_vars.add(name)
|
|
@@ -1926,7 +1937,10 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
1926
1937
|
fn2, ns2 = self._resolve_callee(expr.callee)
|
|
1927
1938
|
if ns2 == "array" and fn2 in ({"new", "from"} | set(ARRAY_NEW_CTORS)):
|
|
1928
1939
|
self._array_vars.add(name)
|
|
1929
|
-
spec2 = self.
|
|
1940
|
+
spec2 = self._widen_array_spec_for_name(
|
|
1941
|
+
name,
|
|
1942
|
+
self._type_spec_from_expr(expr) or self._array_spec_for_name(name),
|
|
1943
|
+
)
|
|
1930
1944
|
self._collection_types[name] = spec2
|
|
1931
1945
|
continue
|
|
1932
1946
|
if name in self._matrix_specs:
|
|
@@ -2110,6 +2124,45 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2110
2124
|
return True
|
|
2111
2125
|
return False
|
|
2112
2126
|
|
|
2127
|
+
def _detect_pine_time_usage(self) -> bool:
|
|
2128
|
+
"""True if emitted C++ calls the engine's pine_time()/pine_time_close().
|
|
2129
|
+
|
|
2130
|
+
Gates the PF_PINE_TIME_SESSION_DAY_ARGS prelude shim (emit_top.py) so
|
|
2131
|
+
scripts that never call ``time(tf[, session[, tz]])`` /
|
|
2132
|
+
``time_close(...)`` stay byte-identical. A ``var`` initializer is
|
|
2133
|
+
also text-scanned, mirroring ``_detect_matrix_usage``.
|
|
2134
|
+
"""
|
|
2135
|
+
pat = re.compile(r"(?<![\w.])time(?:_close)?\s*\(")
|
|
2136
|
+
for _, _, init_str in self.ctx.var_members:
|
|
2137
|
+
if init_str and pat.search(str(init_str)):
|
|
2138
|
+
return True
|
|
2139
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2140
|
+
if isinstance(node, FuncCall) and (node.args or node.kwargs):
|
|
2141
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2142
|
+
if ns is None and fn in ("time", "time_close"):
|
|
2143
|
+
return True
|
|
2144
|
+
return False
|
|
2145
|
+
|
|
2146
|
+
def _detect_vwap_usage(self) -> bool:
|
|
2147
|
+
"""True if emitted C++ calls ta::VWAP / ta::VWAPBands compute().
|
|
2148
|
+
|
|
2149
|
+
Gates the PF_VWAP_SESSION_ANCHOR_ARGS prelude shim (emit_top.py):
|
|
2150
|
+
``ta.vwap(...)`` calls, the bare ``ta.vwap`` property read, and
|
|
2151
|
+
``var`` initializers are covered.
|
|
2152
|
+
"""
|
|
2153
|
+
for _, _, init_str in self.ctx.var_members:
|
|
2154
|
+
if init_str and "ta.vwap" in str(init_str):
|
|
2155
|
+
return True
|
|
2156
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2157
|
+
if isinstance(node, FuncCall):
|
|
2158
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2159
|
+
if ns == "ta" and fn == "vwap":
|
|
2160
|
+
return True
|
|
2161
|
+
elif isinstance(node, MemberAccess) and node.member == "vwap":
|
|
2162
|
+
if getattr(node.object, "name", None) == "ta":
|
|
2163
|
+
return True
|
|
2164
|
+
return False
|
|
2165
|
+
|
|
2113
2166
|
def _detect_generic_matrix_usage(self) -> bool:
|
|
2114
2167
|
"""True if emitted C++ mentions a non-float matrix specialization."""
|
|
2115
2168
|
float_spec = TypeSpec.primitive("float")
|
|
@@ -3760,7 +3813,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3760
3813
|
# request.security helper-call results read at a history offset
|
|
3761
3814
|
# (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
|
|
3762
3815
|
self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
|
|
3763
|
-
self.
|
|
3816
|
+
self._prepare_lazy_source_clock_sites()
|
|
3764
3817
|
|
|
3765
3818
|
lines: list[str] = []
|
|
3766
3819
|
|
|
@@ -3993,11 +4046,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3993
4046
|
)
|
|
3994
4047
|
lines.append("")
|
|
3995
4048
|
|
|
3996
|
-
#
|
|
3997
|
-
# script state themselves. Their
|
|
3998
|
-
#
|
|
3999
|
-
# checkpoint inventory.
|
|
4000
|
-
self.
|
|
4049
|
+
# Hold-last source clocks (change/mom/roc below a top-level lazy edge)
|
|
4050
|
+
# are generated support types, not script state themselves. Their
|
|
4051
|
+
# per-callsite instances are declared below inside GeneratedStrategy
|
|
4052
|
+
# and therefore join the automatic COOF checkpoint inventory.
|
|
4053
|
+
self._emit_lazy_source_clock_helper(lines)
|
|
4001
4054
|
|
|
4002
4055
|
# 2. Open class
|
|
4003
4056
|
lines.append("class GeneratedStrategy : public BacktestEngine {")
|
|
@@ -4153,20 +4206,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4153
4206
|
lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
|
|
4154
4207
|
lines.append(" bool _use_precalc = false;")
|
|
4155
4208
|
|
|
4156
|
-
for
|
|
4209
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
4157
4210
|
lines.append(
|
|
4158
|
-
f" {self.
|
|
4159
|
-
)
|
|
4160
|
-
if self._lazy_saturated_roc3_clock_by_node:
|
|
4161
|
-
# Dedicated eager close[3] fallback. Its fixed four-slot capacity
|
|
4162
|
-
# is independent of the user's max_bars_back directive, which may
|
|
4163
|
-
# legitimately be smaller than the offset this generated route
|
|
4164
|
-
# requires. It is ordinary copyable script state and therefore
|
|
4165
|
-
# joins the automatic COOF checkpoint below.
|
|
4166
|
-
lines.append(
|
|
4167
|
-
" Series<double> "
|
|
4168
|
-
f"{self._lazy_saturated_roc3_history_name}{{4}};"
|
|
4211
|
+
f" {self._lazy_source_clock_type_name} {info['clock']};"
|
|
4169
4212
|
)
|
|
4213
|
+
# Held-source history on the chart clock. A literal length sizes
|
|
4214
|
+
# it exactly (independent of the user's max_bars_back directive,
|
|
4215
|
+
# which may legitimately be smaller than this generated route
|
|
4216
|
+
# needs); a runtime length uses the Series default capacity. Both
|
|
4217
|
+
# are ordinary copyable script state and join the automatic COOF
|
|
4218
|
+
# checkpoint below.
|
|
4219
|
+
literal = info["length_literal"]
|
|
4220
|
+
capacity = f"{{{literal + 1}}}" if literal is not None and literal >= 1 else ""
|
|
4221
|
+
lines.append(f" Series<double> {info['hist']}{capacity};")
|
|
4222
|
+
if info["chart"] is not None:
|
|
4223
|
+
lines.append(f" Series<double> {info['chart']}{capacity};")
|
|
4170
4224
|
|
|
4171
4225
|
# Security evaluator TA members (cloned from expression dependencies)
|
|
4172
4226
|
# Skip for user function call expressions — their TA deps are internal to the function
|
|
@@ -4227,7 +4281,9 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4227
4281
|
)
|
|
4228
4282
|
):
|
|
4229
4283
|
self._array_vars.add(name)
|
|
4230
|
-
array_spec =
|
|
4284
|
+
array_spec = self._widen_array_spec_for_name(
|
|
4285
|
+
name, exact_member_spec or self._array_spec_for_name(name)
|
|
4286
|
+
)
|
|
4231
4287
|
lines.append(
|
|
4232
4288
|
f" {self._type_spec_to_cpp(array_spec)} {safe};"
|
|
4233
4289
|
)
|
|
@@ -89,6 +89,7 @@ from ..analyzer import FuncInfo
|
|
|
89
89
|
from ..symbols import PineType, method_receiver_cpp_token
|
|
90
90
|
from .tables import (
|
|
91
91
|
BAR_SERIES_PUSH,
|
|
92
|
+
TA_CHART_PREV_CLOSE_ARG,
|
|
92
93
|
DRAWING_TYPE_TO_CPP,
|
|
93
94
|
PINE_TYPE_TO_CPP,
|
|
94
95
|
RUNTIME_REGISTER_SECURITY_EVAL_FN,
|
|
@@ -136,6 +137,28 @@ class TopLevelEmitter:
|
|
|
136
137
|
lines.append('#include <pineforge/log.hpp>')
|
|
137
138
|
lines.append('#include <pineforge/str_utils.hpp>')
|
|
138
139
|
lines.append('#include <pineforge/session_time.hpp>')
|
|
140
|
+
# Feature-guarded argument tails (lab experiment cells build the
|
|
141
|
+
# candidate codegen against the BASE engine too): the symbol clock
|
|
142
|
+
# (syminfo tz + session) reaches time()/time_close() and ta.vwap only
|
|
143
|
+
# when the engine header advertises the overloads; otherwise the
|
|
144
|
+
# macros expand to nothing and the calls collapse to the legacy
|
|
145
|
+
# 5-arg pine_time / 3-arg VWAP::compute shapes (session filter in
|
|
146
|
+
# UTC, UTC-day VWAP anchor — the honest "factor absent" behaviour).
|
|
147
|
+
# Gated on the pre-scans so scripts without such calls stay
|
|
148
|
+
# byte-identical. The expansion carries no extra parentheses so the
|
|
149
|
+
# preprocessed call is byte-identical to the plain 7-arg form.
|
|
150
|
+
if getattr(self, "_uses_pine_time", False):
|
|
151
|
+
lines.append("#ifdef PF_PINE_TIME_HAS_SESSION_DAY")
|
|
152
|
+
lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess) , tz, sess")
|
|
153
|
+
lines.append("#else")
|
|
154
|
+
lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess)")
|
|
155
|
+
lines.append("#endif")
|
|
156
|
+
if getattr(self, "_uses_vwap_anchor", False):
|
|
157
|
+
lines.append("#ifdef PF_VWAP_HAS_SESSION_ANCHOR")
|
|
158
|
+
lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess) , tz, sess")
|
|
159
|
+
lines.append("#else")
|
|
160
|
+
lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess)")
|
|
161
|
+
lines.append("#endif")
|
|
139
162
|
if self._uses_matrix:
|
|
140
163
|
# Unconditional include when matrix API is used — do not gate on
|
|
141
164
|
# __has_include(<Eigen/Dense>) (can differ between runtime build and this TU).
|
|
@@ -912,28 +935,38 @@ class TopLevelEmitter:
|
|
|
912
935
|
|
|
913
936
|
# A GeneratedStrategy handle may execute multiple batch runs or
|
|
914
937
|
# streaming lifecycles. BacktestEngine resets broker/base state, but
|
|
915
|
-
# generated members survive. Reset
|
|
916
|
-
#
|
|
917
|
-
#
|
|
918
|
-
#
|
|
919
|
-
#
|
|
920
|
-
#
|
|
921
|
-
|
|
938
|
+
# generated members survive. Reset the hold-last source clocks
|
|
939
|
+
# (change/mom/roc below a top-level lazy edge) and their held-source
|
|
940
|
+
# Series at the first genuine slot of each lifecycle, then freeze this
|
|
941
|
+
# bar's base and record it once per chart bar. This lives in on_bar
|
|
942
|
+
# rather than a generated run wrapper because stream_begin() enters the
|
|
943
|
+
# base run path directly. COOF post-close recalculations have
|
|
944
|
+
# history_advances_new_bar()==false and therefore keep the same base.
|
|
945
|
+
if self._lazy_source_clock_by_node:
|
|
922
946
|
lines.append(
|
|
923
947
|
" if (history_advances_new_bar() && bar_index_ == 0) {"
|
|
924
948
|
)
|
|
925
|
-
for
|
|
926
|
-
lines.append(f" {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
)
|
|
949
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
950
|
+
lines.append(f" {info['clock']}.reset();")
|
|
951
|
+
lines.append(f" {info['hist']}.clear();")
|
|
952
|
+
if info["chart"] is not None:
|
|
953
|
+
lines.append(f" {info['chart']}.clear();")
|
|
930
954
|
lines.append(" }")
|
|
931
|
-
self.
|
|
932
|
-
lines
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
955
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
956
|
+
lines.append(f" {info['clock']}.begin_bar(bar_index_);")
|
|
957
|
+
self._emit_history_series_write(
|
|
958
|
+
lines,
|
|
959
|
+
" ",
|
|
960
|
+
info["hist"],
|
|
961
|
+
f"{info['clock']}.bar_base_source",
|
|
962
|
+
)
|
|
963
|
+
if info["chart"] is not None:
|
|
964
|
+
self._emit_history_series_write(
|
|
965
|
+
lines,
|
|
966
|
+
" ",
|
|
967
|
+
info["chart"],
|
|
968
|
+
self._visit_expr(info["chart_source"]),
|
|
969
|
+
)
|
|
937
970
|
|
|
938
971
|
# reset_run_state() owns engine/broker state, while these generated
|
|
939
972
|
# Series members belong to the strategy object. Clear all of them on
|
|
@@ -1003,6 +1036,11 @@ class TopLevelEmitter:
|
|
|
1003
1036
|
# declaration statements, not in the global on_bar preamble.
|
|
1004
1037
|
if name in getattr(self, "_func_local_var_names", ()):
|
|
1005
1038
|
continue
|
|
1039
|
+
# A wide int array member (``std::vector<int64_t>``, see
|
|
1040
|
+
# ``_wide_int_array_names``) needs its first-bar constructor
|
|
1041
|
+
# spelled with the same element type: name the target for
|
|
1042
|
+
# visit_call's ``array.new_*`` / ``array.from`` lowering.
|
|
1043
|
+
self._array_ctor_target_name = name
|
|
1006
1044
|
safe = self._safe_name(name)
|
|
1007
1045
|
runtime_info = self._runtime_scalar_var_init_by_member.get(name)
|
|
1008
1046
|
if (runtime_info is not None
|
|
@@ -1099,6 +1137,7 @@ class TopLevelEmitter:
|
|
|
1099
1137
|
if cloned not in init_emitted:
|
|
1100
1138
|
init_emitted.add(cloned)
|
|
1101
1139
|
lines.append(f" {cloned}.push({cpp_val});")
|
|
1140
|
+
self._array_ctor_target_name = None
|
|
1102
1141
|
lines.append(" _var_initialized = true;")
|
|
1103
1142
|
lines.append(" } else {")
|
|
1104
1143
|
for name, _, _ in self.ctx.var_members:
|
|
@@ -1196,9 +1235,15 @@ class TopLevelEmitter:
|
|
|
1196
1235
|
# an input (the default-sized construction already matches Pine).
|
|
1197
1236
|
self._emit_ta_runtime_reset(lines, indent=2)
|
|
1198
1237
|
|
|
1199
|
-
# d. Visit each statement
|
|
1238
|
+
# d. Visit each statement. A stateful ``ta.*`` site below a lazy
|
|
1239
|
+
# ``and``/``or`` RHS or ternary arm of a top-level statement is
|
|
1240
|
+
# evaluated every bar BEFORE the statement (TV rule, see ``ta.py``).
|
|
1200
1241
|
for stmt in self.ctx.ast.body:
|
|
1201
|
-
self.
|
|
1242
|
+
self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
|
|
1243
|
+
try:
|
|
1244
|
+
self._visit_stmt(stmt, lines, indent=2)
|
|
1245
|
+
finally:
|
|
1246
|
+
self._clear_lazy_edge_ta_hoists()
|
|
1202
1247
|
|
|
1203
1248
|
# e. ``// @pf-trace`` pragma block — emitted last so trace values
|
|
1204
1249
|
# reflect every assignment / strategy call made earlier in the
|
|
@@ -1938,6 +1983,14 @@ class TopLevelEmitter:
|
|
|
1938
1983
|
if self._ta_site_uses_precalc(site):
|
|
1939
1984
|
compute_args = self._ta_compute_args_for_site(site)
|
|
1940
1985
|
compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
|
|
1986
|
+
# issue #178: the precalc pre-pass walks the bar array
|
|
1987
|
+
# itself, so the previous CHART close is bars[i - 1]
|
|
1988
|
+
# (the engine's prev_chart_close() tracker only advances
|
|
1989
|
+
# at on_bar dispatch).
|
|
1990
|
+
compute_args_bars = compute_args_bars.replace(
|
|
1991
|
+
TA_CHART_PREV_CLOSE_ARG,
|
|
1992
|
+
"(i > 0 ? bars[i - 1].close : na<double>())",
|
|
1993
|
+
)
|
|
1941
1994
|
lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
|
|
1942
1995
|
finally:
|
|
1943
1996
|
self._precalc_loop_active = False
|