@pineforge/codegen-pyodide 0.9.0 → 0.10.1
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/__init__.py +3 -0
- package/pineforge_codegen/analyzer/base.py +3999 -140
- package/pineforge_codegen/analyzer/call_handlers.py +662 -48
- package/pineforge_codegen/analyzer/contracts.py +104 -7
- package/pineforge_codegen/analyzer/types.py +951 -14
- package/pineforge_codegen/codegen/base.py +2359 -249
- package/pineforge_codegen/codegen/drawing.py +94 -14
- package/pineforge_codegen/codegen/emit_top.py +732 -172
- package/pineforge_codegen/codegen/security.py +1456 -150
- package/pineforge_codegen/codegen/ta.py +435 -99
- package/pineforge_codegen/codegen/tables.py +224 -77
- package/pineforge_codegen/codegen/types.py +1196 -215
- package/pineforge_codegen/codegen/visit_call.py +1168 -99
- package/pineforge_codegen/codegen/visit_expr.py +354 -33
- package/pineforge_codegen/codegen/visit_stmt.py +1144 -200
- package/pineforge_codegen/finite_ta_length.py +798 -0
- package/pineforge_codegen/method_binding.py +163 -0
- package/pineforge_codegen/parser.py +48 -9
- package/pineforge_codegen/signatures.py +4 -4
- package/pineforge_codegen/support_checker.py +194 -19
- package/pineforge_codegen/symbols.py +65 -0
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +9 -11
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""TA (technical analysis) call-site helpers for the codegen.
|
|
2
2
|
|
|
3
3
|
Holds the eval-free TA helpers: call-site lookup, ``.compute()`` arg
|
|
4
|
-
construction,
|
|
5
|
-
``_is_compile_time_value`` predicate. The runtime-reset chain that
|
|
4
|
+
construction, and a small ``_is_compile_time_value`` predicate. The runtime-reset chain that
|
|
6
5
|
depends on Python's compile-time expression evaluator
|
|
7
6
|
(``_resolve_known`` / ``_runtime_ctor_arg_for_reset`` /
|
|
8
7
|
``_collect_ta_runtime_resets`` / ``_emit_ta_runtime_reset``) stays
|
|
@@ -13,8 +12,6 @@ Mixin contract — host class must provide:
|
|
|
13
12
|
|
|
14
13
|
- ``self._ta_site_map`` (``dict[int, TACallSite]``).
|
|
15
14
|
- ``self._active_ta_remap`` (``dict[str, str] | None``).
|
|
16
|
-
- ``self._hoist_var_counter`` (``int``, optional — auto-managed).
|
|
17
|
-
|
|
18
15
|
Sibling-mixin methods consumed via ``self``:
|
|
19
16
|
|
|
20
17
|
- ``self._visit_expr`` / ``self._visit_stmt`` (visitor mixins, currently
|
|
@@ -29,8 +26,9 @@ from typing import TYPE_CHECKING
|
|
|
29
26
|
|
|
30
27
|
from ..ast_nodes import (
|
|
31
28
|
Assignment, BinOp, BoolLiteral, ColorLiteral, ExprStmt, FuncCall,
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
ForInStmt, ForStmt, FuncDef, Identifier, MemberAccess, NaLiteral,
|
|
30
|
+
NumberLiteral, StringLiteral, Subscript, Ternary, TupleAssign,
|
|
31
|
+
TupleLiteral, TypeDecl, UnaryOp, VarDecl,
|
|
34
32
|
)
|
|
35
33
|
from .tables import TA_IMPLICIT_APPEND, TA_IMPLICIT_COMPUTE_FULL
|
|
36
34
|
|
|
@@ -39,7 +37,7 @@ if TYPE_CHECKING:
|
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
class TaSiteHelper:
|
|
42
|
-
"""TA call-site lookups
|
|
40
|
+
"""TA call-site lookups and ``.compute()`` argument construction."""
|
|
43
41
|
|
|
44
42
|
# TA functions where an explicit source argument REPLACES the implicit
|
|
45
43
|
# bar-data default (vs. ATR / supertrend / DMI where bar OHLC must
|
|
@@ -92,97 +90,6 @@ class TaSiteHelper:
|
|
|
92
90
|
f"'_ta_<name>_<n>' convention. Internal codegen bug."
|
|
93
91
|
)
|
|
94
92
|
|
|
95
|
-
# ------------------------------------------------------------------
|
|
96
|
-
# TA hoisting in if-bodies (computations unconditional, result conditional)
|
|
97
|
-
# ------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
def _if_body_has_ta(self, stmts: list) -> bool:
|
|
100
|
-
"""True if any statement in ``stmts`` references a TA call-site (recursively)."""
|
|
101
|
-
for s in stmts:
|
|
102
|
-
if isinstance(s, VarDecl) and s.value is not None:
|
|
103
|
-
if self._expr_contains_ta(s.value):
|
|
104
|
-
return True
|
|
105
|
-
if isinstance(s, Assignment) and hasattr(s, "value"):
|
|
106
|
-
if self._expr_contains_ta(s.value):
|
|
107
|
-
return True
|
|
108
|
-
if isinstance(s, ExprStmt):
|
|
109
|
-
if self._expr_contains_ta(s.expr):
|
|
110
|
-
return True
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
|
-
def _is_result_assignment(self, stmt) -> bool:
|
|
114
|
-
"""True iff ``stmt`` is an assignment to the synthetic ``result`` variable.
|
|
115
|
-
|
|
116
|
-
``result`` is the function-body return target injected when a Pine
|
|
117
|
-
function body becomes a C++ function-call site; assignments to it
|
|
118
|
-
carry semantic weight in TA hoisting (they are the conditional-emit
|
|
119
|
-
targets)."""
|
|
120
|
-
if isinstance(stmt, Assignment):
|
|
121
|
-
target_name = self._get_target_name(stmt.target)
|
|
122
|
-
if target_name == "result":
|
|
123
|
-
return True
|
|
124
|
-
return False
|
|
125
|
-
|
|
126
|
-
def _expr_contains_ta(self, expr) -> bool:
|
|
127
|
-
"""Recursive check: does any subnode of ``expr`` resolve to a TA site?"""
|
|
128
|
-
if expr is None:
|
|
129
|
-
return False
|
|
130
|
-
if self._get_ta_site(expr) is not None:
|
|
131
|
-
return True
|
|
132
|
-
if isinstance(expr, BinOp):
|
|
133
|
-
return self._expr_contains_ta(expr.left) or self._expr_contains_ta(expr.right)
|
|
134
|
-
if isinstance(expr, UnaryOp):
|
|
135
|
-
return self._expr_contains_ta(expr.operand)
|
|
136
|
-
if isinstance(expr, Ternary):
|
|
137
|
-
return (self._expr_contains_ta(expr.true_val)
|
|
138
|
-
or self._expr_contains_ta(expr.false_val))
|
|
139
|
-
if isinstance(expr, FuncCall):
|
|
140
|
-
return any(self._expr_contains_ta(a) for a in expr.args)
|
|
141
|
-
return False
|
|
142
|
-
|
|
143
|
-
def _hoist_if_body(self, stmts: list, cond: str, lines: list[str], pad: str, indent: int) -> None:
|
|
144
|
-
"""Emit an if-body with TA hoisting.
|
|
145
|
-
|
|
146
|
-
Pine evaluates TA on every bar regardless of branch; C++ TA
|
|
147
|
-
instances must compute() unconditionally to keep their state
|
|
148
|
-
in sync. We split each result-assignment whose RHS contains a
|
|
149
|
-
TA call into:
|
|
150
|
-
|
|
151
|
-
- an unconditional ``double _hoist_<n> = <rhs>;`` line,
|
|
152
|
-
- a conditional ``if (<cond>) { result = _hoist_<n>; }``.
|
|
153
|
-
|
|
154
|
-
Non-result statements are emitted unconditionally inside an
|
|
155
|
-
opening scope block; result assignments without a TA reference
|
|
156
|
-
stay fully conditional."""
|
|
157
|
-
lines.append(f"{pad}{{")
|
|
158
|
-
conditional_stmts: list = []
|
|
159
|
-
_hoist_counter = getattr(self, "_hoist_var_counter", 0)
|
|
160
|
-
|
|
161
|
-
for s in stmts:
|
|
162
|
-
if self._is_result_assignment(s):
|
|
163
|
-
rhs = s.value if hasattr(s, "value") else None
|
|
164
|
-
if rhs is not None and self._expr_contains_ta(rhs):
|
|
165
|
-
_hoist_counter += 1
|
|
166
|
-
tmp_var = f"_hoist_{_hoist_counter}"
|
|
167
|
-
compute_expr = self._visit_expr(rhs)
|
|
168
|
-
lines.append(f"{pad} double {tmp_var} = {compute_expr};")
|
|
169
|
-
conditional_stmts.append(("result", tmp_var))
|
|
170
|
-
else:
|
|
171
|
-
conditional_stmts.append(("stmt", s))
|
|
172
|
-
else:
|
|
173
|
-
self._visit_stmt(s, lines, indent + 1)
|
|
174
|
-
|
|
175
|
-
if conditional_stmts:
|
|
176
|
-
lines.append(f"{pad} if ({cond}) {{")
|
|
177
|
-
for item in conditional_stmts:
|
|
178
|
-
if item[0] == "result":
|
|
179
|
-
lines.append(f"{pad} result = {item[1]};")
|
|
180
|
-
else:
|
|
181
|
-
self._visit_stmt(item[1], lines, indent + 2)
|
|
182
|
-
lines.append(f"{pad} }}")
|
|
183
|
-
lines.append(f"{pad}}}")
|
|
184
|
-
self._hoist_var_counter = _hoist_counter
|
|
185
|
-
|
|
186
93
|
# ------------------------------------------------------------------
|
|
187
94
|
# .compute() arg-string construction
|
|
188
95
|
# ------------------------------------------------------------------
|
|
@@ -293,6 +200,413 @@ class TaSiteHelper:
|
|
|
293
200
|
return False
|
|
294
201
|
return False
|
|
295
202
|
|
|
203
|
+
def _ta_call_nodes_by_lazy_scope(self) -> tuple[set[int], set[int], set[int]]:
|
|
204
|
+
"""Classify chart ``ta.*`` sites below Pine-v6 lazy-expression edges.
|
|
205
|
+
|
|
206
|
+
The first set contains sites below an ``and`` RHS, preserving the
|
|
207
|
+
already accepted lazy-SMA scope. The second contains sites below any
|
|
208
|
+
Pine-v6 lazy edge: an ``and``/``or`` RHS or either ``?:`` branch. The
|
|
209
|
+
broader set is consumed only by the recursive-EMA route;
|
|
210
|
+
other TA families retain their current precalculation behavior. The
|
|
211
|
+
third is the narrow ``and``-only subset: the site is below an ``and``
|
|
212
|
+
RHS and is not nested anywhere inside an ``or`` or ternary expression.
|
|
213
|
+
It gives source-shaped lazy call clocks a stable AST identity without
|
|
214
|
+
inspecting generated member names.
|
|
215
|
+
"""
|
|
216
|
+
cached = getattr(self, "_lazy_scope_ta_call_nodes", None)
|
|
217
|
+
if cached is not None:
|
|
218
|
+
return cached
|
|
219
|
+
|
|
220
|
+
and_rhs: set[int] = set()
|
|
221
|
+
lazy_rhs: set[int] = set()
|
|
222
|
+
plain_and_rhs: set[int] = set()
|
|
223
|
+
|
|
224
|
+
def note_ta_calls(
|
|
225
|
+
expr,
|
|
226
|
+
under_and_rhs: bool,
|
|
227
|
+
under_lazy_rhs: bool,
|
|
228
|
+
under_disallowed_shape: bool,
|
|
229
|
+
) -> None:
|
|
230
|
+
if expr is None:
|
|
231
|
+
return
|
|
232
|
+
if isinstance(expr, FuncCall):
|
|
233
|
+
callee = expr.callee
|
|
234
|
+
is_security = (
|
|
235
|
+
isinstance(callee, MemberAccess)
|
|
236
|
+
and isinstance(callee.object, Identifier)
|
|
237
|
+
and callee.object.name == "request"
|
|
238
|
+
and callee.member in ("security", "security_lower_tf")
|
|
239
|
+
)
|
|
240
|
+
if isinstance(expr.callee, MemberAccess):
|
|
241
|
+
obj = expr.callee.object
|
|
242
|
+
if isinstance(obj, Identifier) and obj.name == "ta":
|
|
243
|
+
if under_and_rhs:
|
|
244
|
+
and_rhs.add(id(expr))
|
|
245
|
+
if under_lazy_rhs:
|
|
246
|
+
lazy_rhs.add(id(expr))
|
|
247
|
+
if under_and_rhs and not under_disallowed_shape:
|
|
248
|
+
plain_and_rhs.add(id(expr))
|
|
249
|
+
# A call target may itself be an evaluated expression, as in
|
|
250
|
+
# ``array.new_float(...).get(0)``. Walk the callee subtree so
|
|
251
|
+
# TA nested in a chained receiver inherits the surrounding
|
|
252
|
+
# lazy context. Plain identifiers and namespace receivers are
|
|
253
|
+
# leaves, so ordinary ``ta.sma`` / ``request.security`` calls
|
|
254
|
+
# remain unaffected here.
|
|
255
|
+
note_ta_calls(
|
|
256
|
+
callee,
|
|
257
|
+
under_and_rhs,
|
|
258
|
+
under_lazy_rhs,
|
|
259
|
+
under_disallowed_shape,
|
|
260
|
+
)
|
|
261
|
+
for idx, arg in enumerate(getattr(expr, "args", ()) or ()):
|
|
262
|
+
# The third request.security* argument is evaluated by its
|
|
263
|
+
# own security evaluator. Symbol, timeframe, and remaining
|
|
264
|
+
# options are chart-context expressions and must still be
|
|
265
|
+
# inspected for lazy chart TA.
|
|
266
|
+
if is_security and idx == 2:
|
|
267
|
+
continue
|
|
268
|
+
note_ta_calls(
|
|
269
|
+
arg,
|
|
270
|
+
under_and_rhs,
|
|
271
|
+
under_lazy_rhs,
|
|
272
|
+
under_disallowed_shape,
|
|
273
|
+
)
|
|
274
|
+
for key, value in (getattr(expr, "kwargs", None) or {}).items():
|
|
275
|
+
if is_security and key == "expression":
|
|
276
|
+
continue
|
|
277
|
+
note_ta_calls(
|
|
278
|
+
value,
|
|
279
|
+
under_and_rhs,
|
|
280
|
+
under_lazy_rhs,
|
|
281
|
+
under_disallowed_shape,
|
|
282
|
+
)
|
|
283
|
+
return
|
|
284
|
+
if isinstance(expr, BinOp):
|
|
285
|
+
if expr.op == "and":
|
|
286
|
+
# LHS always runs first; RHS is short-circuit conditional.
|
|
287
|
+
note_ta_calls(
|
|
288
|
+
expr.left,
|
|
289
|
+
under_and_rhs,
|
|
290
|
+
under_lazy_rhs,
|
|
291
|
+
under_disallowed_shape,
|
|
292
|
+
)
|
|
293
|
+
note_ta_calls(
|
|
294
|
+
expr.right,
|
|
295
|
+
True,
|
|
296
|
+
True,
|
|
297
|
+
under_disallowed_shape,
|
|
298
|
+
)
|
|
299
|
+
elif expr.op == "or":
|
|
300
|
+
# ``or`` preserves an enclosing ``and`` scope, but its own
|
|
301
|
+
# RHS is a Pine-v6 lazy edge for the EMA classifier.
|
|
302
|
+
note_ta_calls(expr.left, under_and_rhs, under_lazy_rhs, True)
|
|
303
|
+
note_ta_calls(expr.right, under_and_rhs, True, True)
|
|
304
|
+
else:
|
|
305
|
+
note_ta_calls(
|
|
306
|
+
expr.left,
|
|
307
|
+
under_and_rhs,
|
|
308
|
+
under_lazy_rhs,
|
|
309
|
+
under_disallowed_shape,
|
|
310
|
+
)
|
|
311
|
+
note_ta_calls(
|
|
312
|
+
expr.right,
|
|
313
|
+
under_and_rhs,
|
|
314
|
+
under_lazy_rhs,
|
|
315
|
+
under_disallowed_shape,
|
|
316
|
+
)
|
|
317
|
+
return
|
|
318
|
+
if isinstance(expr, Ternary):
|
|
319
|
+
note_ta_calls(expr.condition, under_and_rhs, under_lazy_rhs, True)
|
|
320
|
+
note_ta_calls(expr.true_val, under_and_rhs, True, True)
|
|
321
|
+
note_ta_calls(expr.false_val, under_and_rhs, True, True)
|
|
322
|
+
return
|
|
323
|
+
if isinstance(expr, UnaryOp):
|
|
324
|
+
note_ta_calls(
|
|
325
|
+
expr.operand,
|
|
326
|
+
under_and_rhs,
|
|
327
|
+
under_lazy_rhs,
|
|
328
|
+
under_disallowed_shape,
|
|
329
|
+
)
|
|
330
|
+
return
|
|
331
|
+
if isinstance(expr, (MemberAccess, Subscript)):
|
|
332
|
+
note_ta_calls(
|
|
333
|
+
getattr(expr, "object", None),
|
|
334
|
+
under_and_rhs,
|
|
335
|
+
under_lazy_rhs,
|
|
336
|
+
under_disallowed_shape,
|
|
337
|
+
)
|
|
338
|
+
note_ta_calls(
|
|
339
|
+
getattr(expr, "index", None),
|
|
340
|
+
under_and_rhs,
|
|
341
|
+
under_lazy_rhs,
|
|
342
|
+
under_disallowed_shape,
|
|
343
|
+
)
|
|
344
|
+
return
|
|
345
|
+
if isinstance(expr, TupleLiteral):
|
|
346
|
+
for elem in expr.elements:
|
|
347
|
+
note_ta_calls(
|
|
348
|
+
elem,
|
|
349
|
+
under_and_rhs,
|
|
350
|
+
under_lazy_rhs,
|
|
351
|
+
under_disallowed_shape,
|
|
352
|
+
)
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
def walk_stmt(stmt) -> None:
|
|
356
|
+
if stmt is None:
|
|
357
|
+
return
|
|
358
|
+
if isinstance(stmt, VarDecl):
|
|
359
|
+
note_ta_calls(stmt.value, False, False, False)
|
|
360
|
+
return
|
|
361
|
+
if isinstance(stmt, ExprStmt):
|
|
362
|
+
note_ta_calls(
|
|
363
|
+
getattr(stmt, "value", None) or getattr(stmt, "expr", None),
|
|
364
|
+
False,
|
|
365
|
+
False,
|
|
366
|
+
False,
|
|
367
|
+
)
|
|
368
|
+
return
|
|
369
|
+
if isinstance(stmt, Assignment):
|
|
370
|
+
note_ta_calls(getattr(stmt, "target", None), False, False, False)
|
|
371
|
+
note_ta_calls(getattr(stmt, "value", None), False, False, False)
|
|
372
|
+
return
|
|
373
|
+
if isinstance(stmt, TupleAssign):
|
|
374
|
+
note_ta_calls(getattr(stmt, "value", None), False, False, False)
|
|
375
|
+
return
|
|
376
|
+
if isinstance(stmt, TypeDecl):
|
|
377
|
+
for field in getattr(stmt, "fields", ()) or ():
|
|
378
|
+
note_ta_calls(
|
|
379
|
+
getattr(field, "default", None), False, False, False
|
|
380
|
+
)
|
|
381
|
+
return
|
|
382
|
+
# If / for / while / assign-like — best-effort field walk
|
|
383
|
+
for attr in ("condition", "body", "else_body", "else_ifs", "value", "target", "iterable"):
|
|
384
|
+
child = getattr(stmt, attr, None)
|
|
385
|
+
if child is None:
|
|
386
|
+
continue
|
|
387
|
+
if isinstance(child, list):
|
|
388
|
+
for item in child:
|
|
389
|
+
if isinstance(item, (list, tuple)):
|
|
390
|
+
for sub in item:
|
|
391
|
+
walk_stmt(sub)
|
|
392
|
+
elif hasattr(item, "body") or hasattr(item, "name") or hasattr(item, "value"):
|
|
393
|
+
walk_stmt(item)
|
|
394
|
+
else:
|
|
395
|
+
note_ta_calls(item, False, False, False)
|
|
396
|
+
elif hasattr(child, "op") or hasattr(child, "args") or hasattr(child, "left"):
|
|
397
|
+
note_ta_calls(child, False, False, False)
|
|
398
|
+
elif hasattr(child, "body") or hasattr(child, "value") or hasattr(child, "condition"):
|
|
399
|
+
walk_stmt(child)
|
|
400
|
+
|
|
401
|
+
ast = getattr(self.ctx, "ast", None)
|
|
402
|
+
for stmt in getattr(ast, "body", ()) or ():
|
|
403
|
+
walk_stmt(stmt)
|
|
404
|
+
# User function bodies (original sites live here; clones share node ids
|
|
405
|
+
# only when they reuse the same FuncCall object — still best-effort).
|
|
406
|
+
for finfo in getattr(self.ctx, "func_infos", None) or []:
|
|
407
|
+
node = getattr(finfo, "node", None)
|
|
408
|
+
body = getattr(node, "body", None) if node is not None else None
|
|
409
|
+
if body:
|
|
410
|
+
for stmt in body:
|
|
411
|
+
walk_stmt(stmt)
|
|
412
|
+
|
|
413
|
+
result = (and_rhs, lazy_rhs, plain_and_rhs)
|
|
414
|
+
self._lazy_scope_ta_call_nodes = result
|
|
415
|
+
return result
|
|
416
|
+
|
|
417
|
+
def _ta_call_nodes_under_and_rhs(self) -> set[int]:
|
|
418
|
+
return self._ta_call_nodes_by_lazy_scope()[0]
|
|
419
|
+
|
|
420
|
+
def _ta_call_nodes_under_lazy_rhs(self) -> set[int]:
|
|
421
|
+
return self._ta_call_nodes_by_lazy_scope()[1]
|
|
422
|
+
|
|
423
|
+
def _ta_call_nodes_under_plain_and_rhs(self) -> set[int]:
|
|
424
|
+
return self._ta_call_nodes_by_lazy_scope()[2]
|
|
425
|
+
|
|
426
|
+
def _ta_call_nodes_in_top_level_var_values(self) -> set[int]:
|
|
427
|
+
cached = getattr(self, "_top_level_var_value_ta_nodes", None)
|
|
428
|
+
if cached is not None:
|
|
429
|
+
return cached
|
|
430
|
+
result: set[int] = set()
|
|
431
|
+
ast = getattr(self.ctx, "ast", None)
|
|
432
|
+
for stmt in getattr(ast, "body", ()) or ():
|
|
433
|
+
if not isinstance(stmt, VarDecl):
|
|
434
|
+
continue
|
|
435
|
+
for child in self._walk_ast(stmt.value):
|
|
436
|
+
if isinstance(child, FuncCall):
|
|
437
|
+
result.add(id(child))
|
|
438
|
+
self._top_level_var_value_ta_nodes = result
|
|
439
|
+
return result
|
|
440
|
+
|
|
441
|
+
def _script_has_user_binding(self, name: str) -> bool:
|
|
442
|
+
cache = getattr(self, "_user_binding_names", None)
|
|
443
|
+
if cache is None:
|
|
444
|
+
cache = set()
|
|
445
|
+
ast = getattr(self.ctx, "ast", None)
|
|
446
|
+
for node in self._walk_ast(ast):
|
|
447
|
+
if isinstance(node, VarDecl):
|
|
448
|
+
cache.add(node.name)
|
|
449
|
+
elif isinstance(node, TupleAssign):
|
|
450
|
+
cache.update(node.names)
|
|
451
|
+
elif isinstance(node, ForStmt):
|
|
452
|
+
cache.add(node.var)
|
|
453
|
+
elif isinstance(node, ForInStmt):
|
|
454
|
+
if node.var:
|
|
455
|
+
cache.add(node.var)
|
|
456
|
+
cache.update(node.vars or ())
|
|
457
|
+
elif isinstance(node, FuncDef):
|
|
458
|
+
cache.add(node.name)
|
|
459
|
+
cache.update(node.params)
|
|
460
|
+
self._user_binding_names = cache
|
|
461
|
+
return name in cache
|
|
462
|
+
|
|
463
|
+
def _is_lazy_saturated_roc3_site(self, site: "TACallSite") -> bool:
|
|
464
|
+
"""Return whether ``site`` has the oracle-backed lazy ROC source shape.
|
|
465
|
+
|
|
466
|
+
Selection is deliberately syntactic and callsite-local: a direct
|
|
467
|
+
top-level variable value containing ``ta.roc(close, 3)`` below a plain
|
|
468
|
+
``and`` RHS, where ``close`` is not shadowed by any user binding. User
|
|
469
|
+
functions, control-flow bodies, request.security evaluators,
|
|
470
|
+
aliases/other sources, named or dynamic lengths, shadowed built-ins,
|
|
471
|
+
and any surrounding ``or``/ternary shape remain on their existing eager
|
|
472
|
+
route. Comparator polarity is intentionally absent; two otherwise
|
|
473
|
+
identical callsites must not acquire different history semantics merely
|
|
474
|
+
because one compares above zero and one below it.
|
|
475
|
+
"""
|
|
476
|
+
node = getattr(site, "node", None)
|
|
477
|
+
if (
|
|
478
|
+
node is None
|
|
479
|
+
or getattr(site, "owner_func", None) is not None
|
|
480
|
+
or self._ta_name_from_site(site) != "roc"
|
|
481
|
+
or id(node) not in self._ta_call_nodes_under_plain_and_rhs()
|
|
482
|
+
or id(node) not in self._ta_call_nodes_in_top_level_var_values()
|
|
483
|
+
or self._script_has_user_binding("close")
|
|
484
|
+
or not isinstance(node, FuncCall)
|
|
485
|
+
or getattr(node, "kwargs", None)
|
|
486
|
+
or len(getattr(node, "args", ())) != 2
|
|
487
|
+
):
|
|
488
|
+
return False
|
|
489
|
+
source, length = node.args
|
|
490
|
+
return (
|
|
491
|
+
isinstance(source, Identifier)
|
|
492
|
+
and source.name == "close"
|
|
493
|
+
and isinstance(length, NumberLiteral)
|
|
494
|
+
and not isinstance(length.value, bool)
|
|
495
|
+
and length.value == 3
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def _prepare_lazy_saturated_roc3_sites(self) -> None:
|
|
499
|
+
"""Assign one copyable clock member to each selected AST callsite."""
|
|
500
|
+
if hasattr(self, "_lazy_saturated_roc3_clock_by_node"):
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
def allocate(base: str, reserved: set[str]) -> str:
|
|
504
|
+
candidate = base
|
|
505
|
+
suffix = 2
|
|
506
|
+
while candidate in reserved:
|
|
507
|
+
candidate = f"{base}_{suffix}"
|
|
508
|
+
suffix += 1
|
|
509
|
+
reserved.add(candidate)
|
|
510
|
+
return candidate
|
|
511
|
+
|
|
512
|
+
# Pine permits leading-underscore identifiers. Reserve every emitted
|
|
513
|
+
# user member, not only history/var members already tracked by
|
|
514
|
+
# _all_member_names, before minting generated support names.
|
|
515
|
+
reserved_members = set(getattr(self, "_all_member_names", set()))
|
|
516
|
+
reserved_members.update(
|
|
517
|
+
self._safe_name(name)
|
|
518
|
+
for name, _ptype in getattr(self.ctx, "global_var_decls", ())
|
|
519
|
+
)
|
|
520
|
+
reserved_members.update(
|
|
521
|
+
self._safe_name(name)
|
|
522
|
+
for name, _ptype, _init in getattr(self.ctx, "var_members", ())
|
|
523
|
+
)
|
|
524
|
+
reserved_types = set(getattr(self, "_udt_defs", {}))
|
|
525
|
+
for info in getattr(self.ctx, "func_infos", ()):
|
|
526
|
+
emitted = self._func_cpp_base_name(info.name)
|
|
527
|
+
reserved_members.add(emitted)
|
|
528
|
+
reserved_types.add(emitted)
|
|
529
|
+
total = self.ctx.func_call_site_counts.get(info.name, 0)
|
|
530
|
+
for callsite in range(total):
|
|
531
|
+
reserved_members.add(f"{emitted}_cs{callsite}")
|
|
532
|
+
for instance in getattr(self, "_fresh_instances", ()):
|
|
533
|
+
reserved_members.add(instance["name"])
|
|
534
|
+
self._lazy_saturated_roc3_type_name = allocate(
|
|
535
|
+
"_PFLazySaturatedROC3Clock", reserved_types
|
|
536
|
+
)
|
|
537
|
+
self._lazy_saturated_roc3_history_name = allocate(
|
|
538
|
+
"_pf_lazy_saturated_roc3_close_history", reserved_members
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
clocks: dict[int, str] = {}
|
|
542
|
+
for index, site in enumerate(self.ctx.ta_call_sites):
|
|
543
|
+
if index in getattr(self, "_dead_ta_indices", set()):
|
|
544
|
+
continue
|
|
545
|
+
if self._is_lazy_saturated_roc3_site(site):
|
|
546
|
+
clocks[id(site.node)] = allocate(
|
|
547
|
+
f"_pf_lazy_saturated_roc3_clock_{len(clocks) + 1}",
|
|
548
|
+
reserved_members,
|
|
549
|
+
)
|
|
550
|
+
self._lazy_saturated_roc3_clock_by_node = clocks
|
|
551
|
+
|
|
552
|
+
def _lazy_saturated_roc3_clock_name(self, site: "TACallSite") -> str:
|
|
553
|
+
self._prepare_lazy_saturated_roc3_sites()
|
|
554
|
+
return self._lazy_saturated_roc3_clock_by_node[id(site.node)]
|
|
555
|
+
|
|
556
|
+
def _lazy_saturated_roc3_expr(self, site: "TACallSite") -> str:
|
|
557
|
+
"""Lower a reached lazy ROC site through its per-callsite clock."""
|
|
558
|
+
clock = self._lazy_saturated_roc3_clock_name(site)
|
|
559
|
+
history = self._lazy_saturated_roc3_history_name
|
|
560
|
+
return (
|
|
561
|
+
f"{clock}.evaluate(current_bar_.close, "
|
|
562
|
+
f"{history}[3], bar_index_)"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def _emit_lazy_saturated_roc3_helper(self, lines: list[str]) -> None:
|
|
566
|
+
"""Emit the value-copyable generated runtime helper when needed."""
|
|
567
|
+
self._prepare_lazy_saturated_roc3_sites()
|
|
568
|
+
if not self._lazy_saturated_roc3_clock_by_node:
|
|
569
|
+
return
|
|
570
|
+
type_name = self._lazy_saturated_roc3_type_name
|
|
571
|
+
lines.extend(
|
|
572
|
+
[
|
|
573
|
+
f"struct {type_name} {{",
|
|
574
|
+
" double committed_source = na<double>();",
|
|
575
|
+
" int committed_bar = -1;",
|
|
576
|
+
" double bar_base_source = na<double>();",
|
|
577
|
+
" int bar_base_bar = -1;",
|
|
578
|
+
" int working_bar = -1;",
|
|
579
|
+
"",
|
|
580
|
+
" void reset() {",
|
|
581
|
+
" committed_source = na<double>();",
|
|
582
|
+
" committed_bar = -1;",
|
|
583
|
+
" bar_base_source = na<double>();",
|
|
584
|
+
" bar_base_bar = -1;",
|
|
585
|
+
" working_bar = -1;",
|
|
586
|
+
" }",
|
|
587
|
+
"",
|
|
588
|
+
" double evaluate(double source, double eager_previous, int bar) {",
|
|
589
|
+
" if (working_bar != bar) {",
|
|
590
|
+
" bar_base_source = committed_source;",
|
|
591
|
+
" bar_base_bar = committed_bar;",
|
|
592
|
+
" working_bar = bar;",
|
|
593
|
+
" }",
|
|
594
|
+
" const bool saturated = bar_base_bar >= 0 &&",
|
|
595
|
+
" bar - bar_base_bar >= 3;",
|
|
596
|
+
" const double previous = saturated ? bar_base_source : eager_previous;",
|
|
597
|
+
" double result = na<double>();",
|
|
598
|
+
" if (!is_na(source) && !is_na(previous) && previous != 0.0) {",
|
|
599
|
+
" result = (source - previous) / previous * 100.0;",
|
|
600
|
+
" }",
|
|
601
|
+
" committed_source = source;",
|
|
602
|
+
" committed_bar = bar;",
|
|
603
|
+
" return result;",
|
|
604
|
+
" }",
|
|
605
|
+
"};",
|
|
606
|
+
"",
|
|
607
|
+
]
|
|
608
|
+
)
|
|
609
|
+
|
|
296
610
|
def _ta_site_uses_precalc(self, site: "TACallSite") -> bool:
|
|
297
611
|
"""Whether a static TA site can safely read from ``_precalc_*``.
|
|
298
612
|
|
|
@@ -302,9 +616,31 @@ class TaSiteHelper:
|
|
|
302
616
|
``ha_close = close`` is static in that analyzer sense, yet its Series is
|
|
303
617
|
empty during precompute, so ``ta.stdev(ha_close, 20)`` precalculates as
|
|
304
618
|
all-``na``. Opting that site out preserves correctness; it simply uses
|
|
305
|
-
the ordinary stateful TA object during ``on_bar``.
|
|
619
|
+
the ordinary stateful TA object during ``on_bar``.
|
|
620
|
+
|
|
621
|
+
A chart-context ``ta.sma`` nested under an ``and`` RHS is also opted
|
|
622
|
+
out: precalc advances it every bar, which is eager and TV-incorrect for
|
|
623
|
+
the pinned dual-volume-SMA case. Recursive chart ``ta.ema`` sites below
|
|
624
|
+
any Pine-v6 lazy edge follow the same call-clock rule. Other TA families
|
|
625
|
+
and request.security sites retain their existing behavior."""
|
|
306
626
|
if not getattr(site, "is_static", False):
|
|
307
627
|
return False
|
|
628
|
+
if self._is_lazy_saturated_roc3_site(site):
|
|
629
|
+
return False
|
|
630
|
+
node = getattr(site, "node", None)
|
|
631
|
+
ta_name = self._ta_name_from_site(site)
|
|
632
|
+
if (
|
|
633
|
+
ta_name == "sma"
|
|
634
|
+
and node is not None
|
|
635
|
+
and id(node) in self._ta_call_nodes_under_and_rhs()
|
|
636
|
+
):
|
|
637
|
+
return False
|
|
638
|
+
if (
|
|
639
|
+
ta_name == "ema"
|
|
640
|
+
and node is not None
|
|
641
|
+
and id(node) in self._ta_call_nodes_under_lazy_rhs()
|
|
642
|
+
):
|
|
643
|
+
return False
|
|
308
644
|
return all(self._expr_safe_for_ta_precalc(arg) for arg in site.compute_args)
|
|
309
645
|
|
|
310
646
|
def _security_ta_compute_args_for_site(
|