@pineforge/codegen-pyodide 0.8.1 → 0.9.0
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 +350 -36
- package/pineforge_codegen/analyzer/call_handlers.py +244 -133
- package/pineforge_codegen/analyzer/contracts.py +29 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/codegen/base.py +1147 -110
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +253 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +506 -35
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +112 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +62 -14
- package/pineforge_codegen/codegen/visit_stmt.py +74 -36
- package/pineforge_codegen/parser.py +73 -6
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +39 -2
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +6 -3
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
|
@@ -136,6 +136,7 @@ from ..ast_nodes import (
|
|
|
136
136
|
FuncCall,
|
|
137
137
|
Identifier,
|
|
138
138
|
MemberAccess,
|
|
139
|
+
NaLiteral,
|
|
139
140
|
TupleLiteral,
|
|
140
141
|
StringLiteral,
|
|
141
142
|
)
|
|
@@ -160,6 +161,7 @@ from .tables import (
|
|
|
160
161
|
SKIP_VAR_TYPES,
|
|
161
162
|
STR_FUNC_MAP,
|
|
162
163
|
TIME_FIELD_EXPRS,
|
|
164
|
+
_math_minmax_na_expr,
|
|
163
165
|
_merge_kwargs,
|
|
164
166
|
_merge_kwargs_with_defaults,
|
|
165
167
|
tz_time_field_lambda,
|
|
@@ -174,9 +176,21 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
174
176
|
"MMM DD YYYY ..." forms. A dateString without a time zone is GMT+0 per
|
|
175
177
|
the Pine reference. Returns None when the string cannot be parsed.
|
|
176
178
|
"""
|
|
177
|
-
|
|
179
|
+
import re
|
|
180
|
+
from datetime import datetime, timezone, timedelta
|
|
178
181
|
|
|
179
182
|
txt = text.strip()
|
|
183
|
+
# Pine dateStrings may carry a trailing timezone WORD ("2024-01-01 00:00 UTC",
|
|
184
|
+
# "1 Jan 2020 09:30 GMT+2") that neither fromisoformat nor the strptime forms
|
|
185
|
+
# below can read. Peel it off and fold it into an explicit offset (UTC/GMT
|
|
186
|
+
# with an optional ±H[:MM] suffix; the bare word is +00:00).
|
|
187
|
+
tzoff = None
|
|
188
|
+
m = re.search(r"\s+(?:UTC|GMT)([+-]\d{1,2})?(?::?(\d{2}))?$", txt, re.I)
|
|
189
|
+
if m:
|
|
190
|
+
txt = txt[: m.start()].strip()
|
|
191
|
+
h = int(m.group(1) or 0)
|
|
192
|
+
mm = int(m.group(2) or 0)
|
|
193
|
+
tzoff = timezone(timedelta(hours=h, minutes=(mm if h >= 0 else -mm)))
|
|
180
194
|
dt = None
|
|
181
195
|
try:
|
|
182
196
|
dt = datetime.fromisoformat(txt)
|
|
@@ -186,6 +200,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
186
200
|
"%d %b %Y %H:%M:%S", "%d %b %Y %H:%M", "%d %b %Y",
|
|
187
201
|
"%b %d %Y %H:%M:%S %z", "%b %d %Y %H:%M %z",
|
|
188
202
|
"%b %d %Y %H:%M:%S", "%b %d %Y %H:%M", "%b %d %Y",
|
|
203
|
+
"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d",
|
|
189
204
|
):
|
|
190
205
|
try:
|
|
191
206
|
dt = datetime.strptime(txt, fmt)
|
|
@@ -195,7 +210,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
195
210
|
if dt is None:
|
|
196
211
|
return None
|
|
197
212
|
if dt.tzinfo is None:
|
|
198
|
-
dt = dt.replace(tzinfo=timezone.utc)
|
|
213
|
+
dt = dt.replace(tzinfo=(tzoff or timezone.utc))
|
|
199
214
|
return int(dt.timestamp() * 1000)
|
|
200
215
|
|
|
201
216
|
|
|
@@ -209,6 +224,63 @@ class CallVisitor:
|
|
|
209
224
|
# Function-call dispatch
|
|
210
225
|
# ------------------------------------------------------------------
|
|
211
226
|
|
|
227
|
+
def _udt_method_call_emit_name(self, fi, node: FuncCall) -> str:
|
|
228
|
+
"""Resolve a UDT method call through the ordinary UDF clone graph."""
|
|
229
|
+
base = self._emit_udt_method_cpp_name(fi)
|
|
230
|
+
dispatch = self._instance_dispatch.get(
|
|
231
|
+
(self._current_instance_name, id(node))
|
|
232
|
+
)
|
|
233
|
+
if dispatch is not None:
|
|
234
|
+
return dispatch
|
|
235
|
+
|
|
236
|
+
cs_info = self.ctx.func_call_cs_map.get(id(node))
|
|
237
|
+
if self._active_call_site_idx is not None and cs_info is not None:
|
|
238
|
+
return f"{base}_cs{self._active_call_site_idx}"
|
|
239
|
+
if cs_info is not None and cs_info[0] == fi.name:
|
|
240
|
+
return f"{base}_cs{cs_info[1]}"
|
|
241
|
+
if (self._active_call_site_idx is not None
|
|
242
|
+
and self.ctx.func_call_site_counts.get(fi.name, 0) > 1):
|
|
243
|
+
return f"{base}_cs{self._active_call_site_idx}"
|
|
244
|
+
return base
|
|
245
|
+
|
|
246
|
+
def _array_init_value_expr(self, elem_spec: TypeSpec | None, value_node) -> str:
|
|
247
|
+
if isinstance(value_node, NaLiteral):
|
|
248
|
+
if elem_spec is not None and elem_spec.kind == "udt":
|
|
249
|
+
return self._default_for_spec(elem_spec)
|
|
250
|
+
cpp_type = self._type_spec_to_cpp(elem_spec)
|
|
251
|
+
if cpp_type in ("double", "int", "int64_t", "bool", "std::string"):
|
|
252
|
+
return f"na<{cpp_type}>()"
|
|
253
|
+
return self._default_for_spec(elem_spec)
|
|
254
|
+
return self._visit_expr(value_node)
|
|
255
|
+
|
|
256
|
+
def _array_method_args(
|
|
257
|
+
self, method: str, arg_nodes: list, spec: TypeSpec | None,
|
|
258
|
+
) -> list[str]:
|
|
259
|
+
elem_spec = (
|
|
260
|
+
spec.element
|
|
261
|
+
if spec is not None and spec.kind == "array" and spec.element is not None
|
|
262
|
+
else TypeSpec.primitive("float")
|
|
263
|
+
)
|
|
264
|
+
value_arg_indexes = {
|
|
265
|
+
"set": {1},
|
|
266
|
+
"push": {0},
|
|
267
|
+
"unshift": {0},
|
|
268
|
+
"insert": {1},
|
|
269
|
+
"fill": {0},
|
|
270
|
+
"includes": {0},
|
|
271
|
+
"indexof": {0},
|
|
272
|
+
"lastindexof": {0},
|
|
273
|
+
"binary_search": {0},
|
|
274
|
+
"binary_search_leftmost": {0},
|
|
275
|
+
"binary_search_rightmost": {0},
|
|
276
|
+
}.get(method, set())
|
|
277
|
+
return [
|
|
278
|
+
self._array_init_value_expr(elem_spec, arg)
|
|
279
|
+
if idx in value_arg_indexes
|
|
280
|
+
else self._visit_expr(arg)
|
|
281
|
+
for idx, arg in enumerate(arg_nodes)
|
|
282
|
+
]
|
|
283
|
+
|
|
212
284
|
def _visit_func_call(self, node: FuncCall) -> str:
|
|
213
285
|
callee = node.callee
|
|
214
286
|
if isinstance(callee, MemberAccess):
|
|
@@ -217,7 +289,7 @@ class CallVisitor:
|
|
|
217
289
|
mk = f"{recv_spec.name}.{callee.member}"
|
|
218
290
|
fi_u = self._func_info_map.get(mk)
|
|
219
291
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
220
|
-
fn_cpp = self.
|
|
292
|
+
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
221
293
|
recv_e = self._visit_expr(callee.object)
|
|
222
294
|
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
223
295
|
# Drop the leading ``self`` slot from param_defaults so the
|
|
@@ -270,7 +342,9 @@ class CallVisitor:
|
|
|
270
342
|
meth = callee.member
|
|
271
343
|
raw_args = [self._visit_expr(a) for a in node.args]
|
|
272
344
|
if recv_spec is not None and recv_spec.kind == "array" and meth in ARRAY_METHODS:
|
|
273
|
-
return self._array_method_expr(
|
|
345
|
+
return self._array_method_expr(
|
|
346
|
+
recv, meth, self._array_method_args(meth, node.args, recv_spec), recv_spec
|
|
347
|
+
)
|
|
274
348
|
if recv_spec is not None and recv_spec.kind == "map" and meth in MAP_METHODS:
|
|
275
349
|
return self._map_method_expr(recv, meth, raw_args, recv_spec)
|
|
276
350
|
args = ", ".join(raw_args)
|
|
@@ -294,7 +368,9 @@ class CallVisitor:
|
|
|
294
368
|
return self._map_method_expr(m, meth_raw, margs, self._map_spec_for_name(oname))
|
|
295
369
|
if oname in self._array_vars and meth_raw in ARRAY_METHODS:
|
|
296
370
|
arr = self._safe_name(oname)
|
|
297
|
-
margs =
|
|
371
|
+
margs = self._array_method_args(
|
|
372
|
+
meth_raw, node.args, self._array_spec_for_name(oname)
|
|
373
|
+
)
|
|
298
374
|
return self._array_method_expr(arr, meth_raw, margs, self._array_spec_for_name(oname))
|
|
299
375
|
if oname in self._matrix_specs and meth_raw in MATRIX_METHODS:
|
|
300
376
|
arr = self._safe_name(oname)
|
|
@@ -323,7 +399,7 @@ class CallVisitor:
|
|
|
323
399
|
mk = f"{udt_t}.{meth_raw}"
|
|
324
400
|
fi_u = self._func_info_map.get(mk)
|
|
325
401
|
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
326
|
-
fn_cpp = self.
|
|
402
|
+
fn_cpp = self._udt_method_call_emit_name(fi_u, node)
|
|
327
403
|
recv_e = self._visit_expr(obj)
|
|
328
404
|
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
329
405
|
# Drop the leading ``self`` slot so param_defaults
|
|
@@ -387,11 +463,19 @@ class CallVisitor:
|
|
|
387
463
|
if site is not None:
|
|
388
464
|
compute_args = self._ta_compute_args_for_site(site)
|
|
389
465
|
ta_mem = self._ta_member_name(site)
|
|
390
|
-
|
|
466
|
+
uses_precalc = self._ta_site_uses_precalc(site)
|
|
467
|
+
if getattr(self, "_precalc_loop_active", False) and uses_precalc:
|
|
391
468
|
return f"_precalc_{ta_mem}[i]"
|
|
392
|
-
if
|
|
393
|
-
return
|
|
394
|
-
|
|
469
|
+
if uses_precalc:
|
|
470
|
+
return (
|
|
471
|
+
f"(_use_precalc ? _precalc_{ta_mem}[bar_index_] : "
|
|
472
|
+
f"(history_advances_new_bar() ? {ta_mem}.compute({compute_args}) "
|
|
473
|
+
f": {ta_mem}.recompute({compute_args})))"
|
|
474
|
+
)
|
|
475
|
+
return (
|
|
476
|
+
f"(history_advances_new_bar() ? {ta_mem}.compute({compute_args}) "
|
|
477
|
+
f": {ta_mem}.recompute({compute_args}))"
|
|
478
|
+
)
|
|
395
479
|
|
|
396
480
|
# math.* calls
|
|
397
481
|
if namespace == "math":
|
|
@@ -451,18 +535,23 @@ class CallVisitor:
|
|
|
451
535
|
# Array method syntax: arr.push(val) where namespace is the array variable name
|
|
452
536
|
if namespace is not None and namespace in self._array_vars and func_name in ARRAY_METHODS:
|
|
453
537
|
arr = self._safe_name(namespace)
|
|
454
|
-
|
|
455
|
-
|
|
538
|
+
spec = self._array_spec_for_name(namespace)
|
|
539
|
+
args = self._array_method_args(func_name, node.args, spec)
|
|
540
|
+
return self._array_method_expr(arr, func_name, args, spec)
|
|
456
541
|
|
|
457
542
|
# Array operations — emit proper C++ vector operations
|
|
458
543
|
if namespace == "array":
|
|
459
544
|
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string") or func_name in ARRAY_DRAWING_NEW_CTORS:
|
|
460
545
|
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
461
546
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
462
|
-
|
|
547
|
+
elem_spec = spec.element if spec.element is not None else TypeSpec.primitive("float")
|
|
548
|
+
init_default = self._default_for_spec(elem_spec)
|
|
463
549
|
if node.args:
|
|
464
550
|
size_arg = self._visit_expr(node.args[0])
|
|
465
|
-
|
|
551
|
+
if len(node.args) > 1:
|
|
552
|
+
init_val = self._array_init_value_expr(elem_spec, node.args[1])
|
|
553
|
+
else:
|
|
554
|
+
init_val = init_default
|
|
466
555
|
return f"{cpp_type}((size_t)({size_arg}), {init_val})"
|
|
467
556
|
return f"{cpp_type}()"
|
|
468
557
|
if func_name == "from":
|
|
@@ -472,8 +561,8 @@ class CallVisitor:
|
|
|
472
561
|
# Method calls: array.method(arr, args...)
|
|
473
562
|
if func_name in ARRAY_METHODS and node.args:
|
|
474
563
|
arr = self._visit_expr(node.args[0])
|
|
475
|
-
rest = [self._visit_expr(a) for a in node.args[1:]]
|
|
476
564
|
spec = self._type_spec_from_expr(node.args[0])
|
|
565
|
+
rest = self._array_method_args(func_name, node.args[1:], spec)
|
|
477
566
|
return self._array_method_expr(arr, func_name, rest, spec)
|
|
478
567
|
return "0"
|
|
479
568
|
|
|
@@ -665,11 +754,12 @@ class CallVisitor:
|
|
|
665
754
|
# mutex-guarded setenv+localtime_r block as the 2-arg
|
|
666
755
|
# form.
|
|
667
756
|
tz_arg = "syminfo_.timezone"
|
|
668
|
-
#
|
|
669
|
-
#
|
|
670
|
-
#
|
|
671
|
-
#
|
|
672
|
-
|
|
757
|
+
# Route through the engine's cached pine_<field>() (session_time.hpp),
|
|
758
|
+
# same as the bare variable forms (BAR_BUILTINS) — value-identical but
|
|
759
|
+
# free of the per-call setenv+tzset churn (KI-35). field_expr is unused
|
|
760
|
+
# now (the engine applies the Pine offsets internally).
|
|
761
|
+
del field_expr
|
|
762
|
+
return f"pine_{func_name}((int64_t)({ts_arg}), {tz_arg})"
|
|
673
763
|
|
|
674
764
|
# time(timeframe) or time(timeframe, session[, tz])
|
|
675
765
|
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
@@ -699,6 +789,8 @@ class CallVisitor:
|
|
|
699
789
|
is_tz_first = True
|
|
700
790
|
elif isinstance(node.args[0], StringLiteral):
|
|
701
791
|
is_tz_first = True
|
|
792
|
+
elif self._infer_type(node.args[0]) == "std::string":
|
|
793
|
+
is_tz_first = True
|
|
702
794
|
|
|
703
795
|
if is_tz_first:
|
|
704
796
|
# A single string argument is the timestamp(dateString)
|
|
@@ -748,7 +840,7 @@ class CallVisitor:
|
|
|
748
840
|
sc = args[6] if len(args) > 6 else "0"
|
|
749
841
|
return (
|
|
750
842
|
f"[&]() -> int64_t {{ "
|
|
751
|
-
f"std::string _tz = ({tz}); "
|
|
843
|
+
f"std::string _tz = pineforge::normalize_timezone_for_posix(({tz})); "
|
|
752
844
|
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
753
845
|
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
754
846
|
f"static thread_local std::string _last_tz; "
|
|
@@ -760,6 +852,7 @@ class CallVisitor:
|
|
|
760
852
|
f"struct tm t = {{}}; "
|
|
761
853
|
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
762
854
|
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
855
|
+
f"t.tm_isdst = -1; "
|
|
763
856
|
f"int64_t _res; "
|
|
764
857
|
f"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") {{ "
|
|
765
858
|
f"_res = (int64_t)timegm(&t) * 1000; "
|
|
@@ -840,7 +933,15 @@ class CallVisitor:
|
|
|
840
933
|
if func_name == "float" and namespace is None and node.args:
|
|
841
934
|
return f"(double)({self._visit_expr(node.args[0])})"
|
|
842
935
|
if func_name == "bool" and namespace is None and node.args:
|
|
843
|
-
|
|
936
|
+
# Pine v6 bools are two-state. Explicit bool(int/float) treats na
|
|
937
|
+
# like false, while a raw C++ cast would make NaN truthy.
|
|
938
|
+
x = self._visit_expr(node.args[0])
|
|
939
|
+
return (
|
|
940
|
+
f"[&](){{ auto _pf_v = ({x}); "
|
|
941
|
+
f"using _pf_t = std::decay_t<decltype(_pf_v)>; "
|
|
942
|
+
f"if constexpr (std::is_same_v<_pf_t, bool>) {{ return _pf_v; }} "
|
|
943
|
+
f"else {{ return is_na(_pf_v) ? false : (bool)_pf_v; }} }}()"
|
|
944
|
+
)
|
|
844
945
|
if func_name == "string" and namespace is None and node.args:
|
|
845
946
|
# Pine string(x) cast — same emission as str.tostring(x), with
|
|
846
947
|
# string passthrough and TV-style "true"/"false" for bools
|
|
@@ -1060,17 +1161,32 @@ class CallVisitor:
|
|
|
1060
1161
|
|
|
1061
1162
|
def _visit_arg_for_series(arg_node, arg_idx):
|
|
1062
1163
|
"""Visit a function argument, returning Series ref for series params."""
|
|
1063
|
-
if arg_idx in _func_series_param_indices
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
safe
|
|
1073
|
-
|
|
1164
|
+
if arg_idx in _func_series_param_indices:
|
|
1165
|
+
if isinstance(arg_node, Identifier):
|
|
1166
|
+
aname = arg_node.name
|
|
1167
|
+
# Bar field: pass _s_close instead of current_bar_.close
|
|
1168
|
+
if aname in BAR_FIELDS or aname in BAR_SERIES_PUSH:
|
|
1169
|
+
return f"_s_{aname}"
|
|
1170
|
+
# Series var: pass the Series object directly
|
|
1171
|
+
if aname in self.ctx.series_vars:
|
|
1172
|
+
safe = self._safe_name(aname)
|
|
1173
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
1174
|
+
safe = self._active_var_remap[safe]
|
|
1175
|
+
return safe
|
|
1176
|
+
expr_cpp = self._visit_expr(arg_node)
|
|
1177
|
+
cpp_t = self._infer_type(arg_node)
|
|
1178
|
+
if cpp_t not in ("double", "int", "bool"):
|
|
1179
|
+
cpp_t = "double"
|
|
1180
|
+
member = self._inline_history_member(
|
|
1181
|
+
"series_arg", node, arg_idx=arg_idx
|
|
1182
|
+
)
|
|
1183
|
+
return (
|
|
1184
|
+
f"([&]() -> const Series<{cpp_t}>& {{ "
|
|
1185
|
+
f"{cpp_t} _sv = ({expr_cpp}); "
|
|
1186
|
+
f"if (history_advances_new_bar()) {member}.push(_sv); "
|
|
1187
|
+
f"else {member}.update(_sv); "
|
|
1188
|
+
f"return {member}; }}())"
|
|
1189
|
+
)
|
|
1074
1190
|
return self._visit_expr(arg_node)
|
|
1075
1191
|
|
|
1076
1192
|
if node.kwargs:
|
|
@@ -1167,8 +1283,34 @@ class CallVisitor:
|
|
|
1167
1283
|
|
|
1168
1284
|
def _visit_fixnan(self, node: FuncCall) -> str:
|
|
1169
1285
|
"""Emit fixnan with persistent state member."""
|
|
1170
|
-
|
|
1171
|
-
|
|
1286
|
+
# Variant-aware lookup keyed off the analyzer-tracked site:
|
|
1287
|
+
# * function-owned site -> dispatch through the active per-call-site
|
|
1288
|
+
# remap so each emitted variant (cs0/cs1/__ni{N}) references its
|
|
1289
|
+
# OWN previous-value member.
|
|
1290
|
+
# * top-level site (owner_func is None) -> use ``site.member_name``
|
|
1291
|
+
# directly. The declarations come from ``ctx.fixnan_sites`` keyed
|
|
1292
|
+
# by these member names, so referencing anything else (e.g. the
|
|
1293
|
+
# legacy monotonic counter) would either dangle a declaration or
|
|
1294
|
+
# silently alias another site's state. In particular, when a
|
|
1295
|
+
# function-owned fixnan is analyzed BEFORE a top-level one, the
|
|
1296
|
+
# counter would restart at 1 and collide with the function's
|
|
1297
|
+
# ``_prev_fixnan_1`` -- corrupting both. Using the site's own
|
|
1298
|
+
# member name keeps declaration and reference in lockstep.
|
|
1299
|
+
# * unmapped site (node not in the site map, e.g. a fixnan reached
|
|
1300
|
+
# only through a path the analyzer didn't register) -> fall back
|
|
1301
|
+
# to the legacy monotonic counter so emission still produces a
|
|
1302
|
+
# referenceable member.
|
|
1303
|
+
site = self._fixnan_site_map.get(id(node))
|
|
1304
|
+
if site is not None:
|
|
1305
|
+
if site.member_name in self._func_fixnan_members:
|
|
1306
|
+
member = self._active_fixnan_remap.get(
|
|
1307
|
+
site.member_name, site.member_name
|
|
1308
|
+
)
|
|
1309
|
+
else:
|
|
1310
|
+
member = site.member_name
|
|
1311
|
+
else:
|
|
1312
|
+
self._fixnan_counter += 1
|
|
1313
|
+
member = f"_prev_fixnan_{self._fixnan_counter}"
|
|
1172
1314
|
x = self._visit_expr(node.args[0])
|
|
1173
1315
|
return f"(is_na({x}) ? {member} : ({member} = {x}))"
|
|
1174
1316
|
|
|
@@ -1252,17 +1394,17 @@ class CallVisitor:
|
|
|
1252
1394
|
qty_val = self._visit_expr(qty_n) if qty_n else "na<double>()"
|
|
1253
1395
|
comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1254
1396
|
oca_val = self._visit_expr(oca_name_n) if oca_name_n is not None else '""'
|
|
1397
|
+
profit_ticks = "na<double>()"
|
|
1398
|
+
loss_ticks = "na<double>()"
|
|
1255
1399
|
|
|
1256
1400
|
if profit_n and not limit_n:
|
|
1257
|
-
|
|
1258
|
-
limit_val = f"(position_entry_price_ + (signed_position_size() > 0 ? 1.0 : -1.0) * ({ticks}) * syminfo_mintick_)"
|
|
1401
|
+
profit_ticks = self._visit_expr(profit_n)
|
|
1259
1402
|
if loss_n and not stop_n:
|
|
1260
|
-
|
|
1261
|
-
stop_val = f"(position_entry_price_ - (signed_position_size() > 0 ? 1.0 : -1.0) * ({ticks}) * syminfo_mintick_)"
|
|
1403
|
+
loss_ticks = self._visit_expr(loss_n)
|
|
1262
1404
|
|
|
1263
1405
|
return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
|
|
1264
1406
|
f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
|
|
1265
|
-
f"{qty_val}, {oca_val})")
|
|
1407
|
+
f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})")
|
|
1266
1408
|
close_comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1267
1409
|
return f"strategy_close({exit_id}, {close_comment})"
|
|
1268
1410
|
|
|
@@ -1483,23 +1625,12 @@ class CallVisitor:
|
|
|
1483
1625
|
if func_name == "avg" and len(args) > 2:
|
|
1484
1626
|
sum_expr = " + ".join(f"(double)({a})" for a in args)
|
|
1485
1627
|
return f"(({sum_expr}) / {len(args)}.0)"
|
|
1486
|
-
if func_name
|
|
1487
|
-
|
|
1488
|
-
for a in args[2:]:
|
|
1489
|
-
result = f"std::max({result}, (double)({a}))"
|
|
1490
|
-
return result
|
|
1491
|
-
if func_name == "min" and len(args) > 2:
|
|
1492
|
-
result = f"std::min((double)({args[0]}), (double)({args[1]}))"
|
|
1493
|
-
for a in args[2:]:
|
|
1494
|
-
result = f"std::min({result}, (double)({a}))"
|
|
1495
|
-
return result
|
|
1628
|
+
if func_name in ("min", "max"):
|
|
1629
|
+
return _math_minmax_na_expr(func_name, args)
|
|
1496
1630
|
if func_name in MATH_FUNC_MAP:
|
|
1497
1631
|
mapped = MATH_FUNC_MAP[func_name]
|
|
1498
1632
|
if "{0}" in mapped:
|
|
1499
1633
|
return mapped.format(*args)
|
|
1500
|
-
# std::min/std::max require same types — cast to double
|
|
1501
|
-
if func_name in ("min", "max") and len(args) == 2:
|
|
1502
|
-
return f"{mapped}((double)({args[0]}), (double)({args[1]}))"
|
|
1503
1634
|
return f"{mapped}({', '.join(args)})"
|
|
1504
1635
|
# Unknown math.* — safe fallback
|
|
1505
1636
|
return f"0.0 /* unsupported: math.{func_name} */"
|
|
@@ -275,6 +275,16 @@ class ExprVisitor:
|
|
|
275
275
|
return str(val)
|
|
276
276
|
if isinstance(val, str):
|
|
277
277
|
return f'std::string("{val}")'
|
|
278
|
+
# TA runtime-reset lowering: an input-backed var renders as its
|
|
279
|
+
# override-aware getter (not the member name), because the reset may
|
|
280
|
+
# run before the input members are initialised (evaluate_security path).
|
|
281
|
+
if (self._reset_input_getter_mode
|
|
282
|
+
and name in self._input_backed_vars
|
|
283
|
+
and name in self._input_var_to_call):
|
|
284
|
+
call_node = self._input_var_to_call[name]
|
|
285
|
+
func_name_i, namespace_i = self._resolve_callee(call_node.callee)
|
|
286
|
+
title = self._get_input_title(call_node, var_name=name)
|
|
287
|
+
return self._render_input_value(call_node, func_name_i, namespace_i, title)
|
|
278
288
|
# Pine type name `color` used as a value (no variable) → int64 color constant.
|
|
279
289
|
# Params handled above; symbol table does not retain function locals after analysis.
|
|
280
290
|
if name == "color":
|
|
@@ -373,7 +383,12 @@ class ExprVisitor:
|
|
|
373
383
|
if node.member == "position_size":
|
|
374
384
|
return "signed_position_size()"
|
|
375
385
|
if node.member == "position_avg_price":
|
|
376
|
-
|
|
386
|
+
# Pine returns `na` when the position is flat
|
|
387
|
+
# (position_size == 0); the engine field is 0.0 when flat.
|
|
388
|
+
# Guard so the common `na(strategy.position_avg_price)`
|
|
389
|
+
# flat/in-position idiom is not silently defeated.
|
|
390
|
+
return ("(signed_position_size() == 0.0 ? na<double>() "
|
|
391
|
+
": position_entry_price_)")
|
|
377
392
|
if node.member == "position_entry_name":
|
|
378
393
|
return "position_entry_name()"
|
|
379
394
|
# Trade counts
|
|
@@ -478,13 +493,34 @@ class ExprVisitor:
|
|
|
478
493
|
(node.member in TA_IMPLICIT_COMPUTE_FULL and node.member in TA_COMPUTE_ARGS and TA_COMPUTE_ARGS[node.member] == [])
|
|
479
494
|
or node.member == "vwap"
|
|
480
495
|
):
|
|
481
|
-
# Find the matching call site
|
|
482
|
-
|
|
496
|
+
# Find the matching call site. Skip sites pruned as dead
|
|
497
|
+
# code (their owner function is never called): a dead site's
|
|
498
|
+
# member declaration is never emitted (see base.py
|
|
499
|
+
# ``_dead_ta_indices`` / emit_top.py member-decl guard), so
|
|
500
|
+
# binding a LIVE bare property read to one references an
|
|
501
|
+
# undeclared member. Regression: nightowlxtrader-azt — a live
|
|
502
|
+
# ``ta.vwap`` (top-level + inside request.security) resolved
|
|
503
|
+
# to dead ``f5``'s first-in-order ``_ta_vwap_10`` and emitted
|
|
504
|
+
# ``use of undeclared identifier '_ta_vwap_10'``. A live read
|
|
505
|
+
# must bind to a live site.
|
|
506
|
+
for _i, site in enumerate(self.ctx.ta_call_sites):
|
|
507
|
+
if _i in self._dead_ta_indices:
|
|
508
|
+
continue
|
|
483
509
|
ta_short = site.class_name.split("::")[-1].lower()
|
|
484
510
|
if site.member_name.startswith(f"_ta_{node.member}_"):
|
|
485
511
|
if node.member == "vwap":
|
|
486
|
-
return
|
|
487
|
-
|
|
512
|
+
return (
|
|
513
|
+
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
514
|
+
"current_bar_.close, current_bar_.volume, current_bar_.timestamp) "
|
|
515
|
+
f": {site.member_name}.recompute(current_bar_.close, "
|
|
516
|
+
"current_bar_.volume, current_bar_.timestamp))"
|
|
517
|
+
)
|
|
518
|
+
return (
|
|
519
|
+
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
520
|
+
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}) : "
|
|
521
|
+
f"{site.member_name}.recompute("
|
|
522
|
+
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}))"
|
|
523
|
+
)
|
|
488
524
|
# No registered call site for this TA property read —
|
|
489
525
|
# the old fallback emitted std::string("<name>"), a
|
|
490
526
|
# silent type mismatch. Reject loudly instead.
|
|
@@ -729,6 +765,18 @@ class ExprVisitor:
|
|
|
729
765
|
right = self._visit_expr(node.right)
|
|
730
766
|
cpp_ops = {"and": "&&", "or": "||"}
|
|
731
767
|
op = cpp_ops.get(node.op, node.op)
|
|
768
|
+
if node.op == "+":
|
|
769
|
+
lt = self._infer_type(node.left)
|
|
770
|
+
rt = self._infer_type(node.right)
|
|
771
|
+
if lt == "std::string" or rt == "std::string":
|
|
772
|
+
def _as_string(rendered, inferred):
|
|
773
|
+
if inferred == "std::string":
|
|
774
|
+
return rendered
|
|
775
|
+
if inferred == "bool":
|
|
776
|
+
return f'(({rendered}) ? std::string("true") : std::string("false"))'
|
|
777
|
+
return f"std::to_string({rendered})"
|
|
778
|
+
|
|
779
|
+
return f"({_as_string(left, lt)} + {_as_string(right, rt)})"
|
|
732
780
|
# PineScript % works on floats — use std::fmod in C++
|
|
733
781
|
if node.op == "%":
|
|
734
782
|
return f"std::fmod((double)({left}), (double)({right}))"
|
|
@@ -784,26 +832,26 @@ class ExprVisitor:
|
|
|
784
832
|
# ``ta.highest(high, 10)[1]`` or ``f()[2]``. In Pine the call yields a
|
|
785
833
|
# series, so ``[k]`` reads its value k bars ago — but the call lowers to
|
|
786
834
|
# a freshly-computed C++ scalar, and ``scalar[k]`` is not subscriptable.
|
|
787
|
-
# Materialize the result into a
|
|
835
|
+
# Materialize the result into a checkpoint-owned class-member
|
|
788
836
|
# ``Series<T>`` that pushes (new bar) / updates (intrabar) the value
|
|
789
837
|
# exactly once per evaluation — same semantics as every other series in
|
|
790
838
|
# the strategy — and read ``[k]`` off it. The inner call is emitted once
|
|
791
|
-
# so its own stateful indicator is not double-stepped
|
|
792
|
-
#
|
|
793
|
-
#
|
|
839
|
+
# so its own stateful indicator is not double-stepped. The deterministic
|
|
840
|
+
# member identity includes the emitted UDF variant; separate Pine call
|
|
841
|
+
# sites never share history, and on_bar clears every synthetic member at
|
|
842
|
+
# run-start even when this particular expression is conditional.
|
|
794
843
|
if isinstance(node.object, FuncCall):
|
|
795
844
|
inner = self._visit_expr(node.object)
|
|
796
845
|
cpp_t = self._infer_type(node.object)
|
|
797
846
|
if cpp_t not in ("double", "int", "bool"):
|
|
798
847
|
cpp_t = "double"
|
|
848
|
+
member = self._inline_history_member("hist_call", node)
|
|
799
849
|
return (
|
|
800
850
|
f"([&]() -> {cpp_t} {{ "
|
|
801
|
-
f"static thread_local Series<{cpp_t}> _hist_call; "
|
|
802
|
-
f"if (is_first_tick_ && bar_index_ == 0) _hist_call.clear(); "
|
|
803
851
|
f"{cpp_t} _hv = ({inner}); "
|
|
804
|
-
f"if (
|
|
805
|
-
f"else
|
|
806
|
-
f"return
|
|
852
|
+
f"if (history_advances_new_bar()) {member}.push(_hv); "
|
|
853
|
+
f"else {member}.update(_hv); "
|
|
854
|
+
f"return {member}[(int)({idx})]; }}())"
|
|
807
855
|
)
|
|
808
856
|
obj = self._visit_expr(node.object)
|
|
809
857
|
# If subscripting a non-series variable (e.g., function parameter),
|