@pineforge/codegen-pyodide 0.8.1 → 0.10.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 +1348 -113
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +258 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +559 -36
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +165 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +149 -15
- package/pineforge_codegen/codegen/visit_stmt.py +85 -38
- 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.10.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
|
@@ -59,16 +59,21 @@ tables and types come from ``codegen/tables.py``, ``..ast_nodes``,
|
|
|
59
59
|
from __future__ import annotations
|
|
60
60
|
|
|
61
61
|
from ..ast_nodes import (
|
|
62
|
-
ASTNode, Assignment, BinOp, BreakStmt, ContinueStmt, ExprStmt,
|
|
63
|
-
ForInStmt, FuncCall, FuncDef, Identifier, IfStmt,
|
|
64
|
-
Subscript, SwitchStmt, Ternary, TupleAssign,
|
|
65
|
-
WhileStmt,
|
|
62
|
+
ASTNode, Assignment, BinOp, BoolLiteral, BreakStmt, ContinueStmt, ExprStmt,
|
|
63
|
+
ForStmt, ForInStmt, FuncCall, FuncDef, Identifier, IfStmt, MemberAccess,
|
|
64
|
+
NumberLiteral, StringLiteral, Subscript, SwitchStmt, Ternary, TupleAssign,
|
|
65
|
+
TupleLiteral, UnaryOp, VarDecl, WhileStmt,
|
|
66
66
|
)
|
|
67
67
|
from ..analyzer import (
|
|
68
68
|
FuncInfo, TACallSite, TA_MULTI_CTOR, TA_NO_CTOR, TA_PERIOD_ARG,
|
|
69
69
|
)
|
|
70
|
+
from .. import signatures as sigs
|
|
70
71
|
from ..symbols import PineType
|
|
71
|
-
from .tables import
|
|
72
|
+
from .tables import (
|
|
73
|
+
MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
|
|
74
|
+
SECURITY_BAR_FIELD_EXPRS, SECURITY_BAR_FIELD_TYPES, _math_minmax_na_expr,
|
|
75
|
+
_merge_kwargs,
|
|
76
|
+
)
|
|
72
77
|
|
|
73
78
|
|
|
74
79
|
class SecurityEmitter:
|
|
@@ -100,6 +105,13 @@ class SecurityEmitter:
|
|
|
100
105
|
return self._known_vars[name], None
|
|
101
106
|
if name in self._input_backed_vars and name in self._input_var_to_call:
|
|
102
107
|
return None, self._visit_expr(self._input_var_to_call[name])
|
|
108
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
109
|
+
if name in global_expr_map:
|
|
110
|
+
expanded = self._security_tf_runtime_expr(
|
|
111
|
+
global_expr_map[name], resolving={name}
|
|
112
|
+
)
|
|
113
|
+
if expanded is not None:
|
|
114
|
+
return None, expanded
|
|
103
115
|
# class-scope resolvable (global / input member)?
|
|
104
116
|
if self._ident_is_resolvable(name):
|
|
105
117
|
try:
|
|
@@ -115,10 +127,105 @@ class SecurityEmitter:
|
|
|
115
127
|
return None, "input_tf_"
|
|
116
128
|
# any other expression — visit if it resolves at class scope
|
|
117
129
|
try:
|
|
118
|
-
|
|
130
|
+
expanded = self._security_tf_runtime_expr(tf_node)
|
|
131
|
+
return None, expanded if expanded is not None else self._visit_expr(tf_node)
|
|
119
132
|
except Exception:
|
|
120
133
|
return None, "input_tf_"
|
|
121
134
|
|
|
135
|
+
def _security_tf_runtime_expr(self, node, resolving: set[str] | None = None) -> str | None:
|
|
136
|
+
"""Render a request.security timeframe expression for registration time.
|
|
137
|
+
|
|
138
|
+
Security evaluators are registered before ``on_bar()`` initializes global
|
|
139
|
+
variables, so input-backed aliases such as ``tf = useChart ? timeframe.period
|
|
140
|
+
: inputTf`` must be expanded to their source expression with direct
|
|
141
|
+
``get_input_*`` reads. Emitting the member name would register with its
|
|
142
|
+
default-constructed value (usually an empty string).
|
|
143
|
+
|
|
144
|
+
Delegates to :meth:`_substitute_tf_input_reads`, which rewrites the
|
|
145
|
+
input-derived *leaves* of the expression tree — including leaves buried
|
|
146
|
+
inside a ternary CONDITION or any BinOp / UnaryOp / FuncCall — and then
|
|
147
|
+
renders the substituted tree through the normal expression visitor.
|
|
148
|
+
"""
|
|
149
|
+
if node is None:
|
|
150
|
+
return None
|
|
151
|
+
substituted = self._substitute_tf_input_reads(node, resolving or set())
|
|
152
|
+
return self._visit_expr(substituted)
|
|
153
|
+
|
|
154
|
+
def _substitute_tf_input_reads(self, node, resolving: set[str]):
|
|
155
|
+
"""Return ``node`` with input-derived leaves rewritten to expressions
|
|
156
|
+
that are valid at security-registration time (before ``on_bar()``
|
|
157
|
+
assigns members from their inputs):
|
|
158
|
+
|
|
159
|
+
* an input-backed var -> its ``input.*()`` source call (``get_input_*``);
|
|
160
|
+
* a ``timeframe.period`` alias var -> ``timeframe.period`` (``script_tf_``);
|
|
161
|
+
* a known compile-time string var -> that string literal;
|
|
162
|
+
* a global alias -> its defining expression, expanded recursively.
|
|
163
|
+
|
|
164
|
+
The walk descends through Ternary / BinOp / UnaryOp / FuncCall /
|
|
165
|
+
Subscript, so an input-backed identifier nested inside e.g. a ternary
|
|
166
|
+
condition (``mode == "15" ? "240" : "60"``) resolves to its input read
|
|
167
|
+
instead of the uninitialised member. A subtree containing no
|
|
168
|
+
input-derived leaf is returned unchanged (same object) so unaffected
|
|
169
|
+
timeframe expressions render byte-identically to before the fix.
|
|
170
|
+
MemberAccess and literals are left verbatim: ``timeframe.period`` is
|
|
171
|
+
already lowered to ``script_tf_`` by the expression visitor.
|
|
172
|
+
"""
|
|
173
|
+
if not isinstance(node, ASTNode):
|
|
174
|
+
return node
|
|
175
|
+
if isinstance(node, Identifier):
|
|
176
|
+
name = node.name
|
|
177
|
+
if name in self._timeframe_period_vars:
|
|
178
|
+
return MemberAccess(object=Identifier(name="timeframe"), member="period")
|
|
179
|
+
if name in self._input_backed_vars and name in self._input_var_to_call:
|
|
180
|
+
return self._input_var_to_call[name]
|
|
181
|
+
if (name in self._known_vars and name not in self._input_backed_vars
|
|
182
|
+
and isinstance(self._known_vars[name], str)):
|
|
183
|
+
return StringLiteral(value=self._known_vars[name])
|
|
184
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
185
|
+
if name in global_expr_map and name not in resolving:
|
|
186
|
+
return self._substitute_tf_input_reads(
|
|
187
|
+
global_expr_map[name], resolving | {name})
|
|
188
|
+
return node
|
|
189
|
+
if isinstance(node, Ternary):
|
|
190
|
+
cond = self._substitute_tf_input_reads(node.condition, resolving)
|
|
191
|
+
tv = self._substitute_tf_input_reads(node.true_val, resolving)
|
|
192
|
+
fv = self._substitute_tf_input_reads(node.false_val, resolving)
|
|
193
|
+
if cond is node.condition and tv is node.true_val and fv is node.false_val:
|
|
194
|
+
return node
|
|
195
|
+
return Ternary(condition=cond, true_val=tv, false_val=fv)
|
|
196
|
+
if isinstance(node, BinOp):
|
|
197
|
+
left = self._substitute_tf_input_reads(node.left, resolving)
|
|
198
|
+
right = self._substitute_tf_input_reads(node.right, resolving)
|
|
199
|
+
if left is node.left and right is node.right:
|
|
200
|
+
return node
|
|
201
|
+
return BinOp(left=left, op=node.op, right=right)
|
|
202
|
+
if isinstance(node, UnaryOp):
|
|
203
|
+
operand = self._substitute_tf_input_reads(node.operand, resolving)
|
|
204
|
+
if operand is node.operand:
|
|
205
|
+
return node
|
|
206
|
+
return UnaryOp(op=node.op, operand=operand)
|
|
207
|
+
if isinstance(node, FuncCall):
|
|
208
|
+
new_args = [self._substitute_tf_input_reads(a, resolving) for a in node.args]
|
|
209
|
+
new_kwargs = {
|
|
210
|
+
k: (self._substitute_tf_input_reads(v, resolving)
|
|
211
|
+
if isinstance(v, ASTNode) else v)
|
|
212
|
+
for k, v in node.kwargs.items()
|
|
213
|
+
}
|
|
214
|
+
unchanged = (
|
|
215
|
+
all(a is b for a, b in zip(new_args, node.args))
|
|
216
|
+
and all(new_kwargs[k] is node.kwargs[k] for k in node.kwargs)
|
|
217
|
+
)
|
|
218
|
+
if unchanged:
|
|
219
|
+
return node
|
|
220
|
+
return FuncCall(callee=node.callee, args=new_args, kwargs=new_kwargs)
|
|
221
|
+
if isinstance(node, Subscript):
|
|
222
|
+
obj = self._substitute_tf_input_reads(node.object, resolving)
|
|
223
|
+
idx = self._substitute_tf_input_reads(node.index, resolving)
|
|
224
|
+
if obj is node.object and idx is node.index:
|
|
225
|
+
return node
|
|
226
|
+
return Subscript(object=obj, index=idx)
|
|
227
|
+
return node
|
|
228
|
+
|
|
122
229
|
def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
|
|
123
230
|
"""For a ``request.security`` whose tf is function parameter ``param_name``
|
|
124
231
|
of user function ``func_name``, return ``(tf_str, tf_expr)`` resolved from
|
|
@@ -276,7 +383,7 @@ class SecurityEmitter:
|
|
|
276
383
|
return None
|
|
277
384
|
|
|
278
385
|
def _literal_int_for_security_index(self, node) -> int | None:
|
|
279
|
-
"""Integer index for
|
|
386
|
+
"""Integer index for bar-field[n] inside request.security (must be literal)."""
|
|
280
387
|
if isinstance(node, NumberLiteral):
|
|
281
388
|
v = node.value
|
|
282
389
|
if isinstance(v, bool):
|
|
@@ -295,19 +402,131 @@ class SecurityEmitter:
|
|
|
295
402
|
return None
|
|
296
403
|
return None
|
|
297
404
|
|
|
405
|
+
# In PineForge batch backtests these barstate flags are compile-time
|
|
406
|
+
# constants (see support_checker / codegen.visit_expr barstate emission):
|
|
407
|
+
# every bar is historical, none realtime/last. The other flags
|
|
408
|
+
# (isfirst/isnew/isconfirmed) depend on runtime tick state, so they are
|
|
409
|
+
# deliberately absent and leave a fold "unknown".
|
|
410
|
+
_SECURITY_CONST_BARSTATE = {
|
|
411
|
+
"isrealtime": False,
|
|
412
|
+
"islast": False,
|
|
413
|
+
"ishistory": True,
|
|
414
|
+
"islastconfirmedhistory": False,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
def _fold_security_const_bool(
|
|
418
|
+
self,
|
|
419
|
+
node,
|
|
420
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
421
|
+
resolving: set[str] | None = None,
|
|
422
|
+
) -> bool | None:
|
|
423
|
+
"""Fold a boolean expression to a compile-time constant, or None.
|
|
424
|
+
|
|
425
|
+
Only reduces expressions whose non-literal leaves are the batch-constant
|
|
426
|
+
barstate flags above; anything runtime-dependent returns None. Used to
|
|
427
|
+
resolve a request.security history index expressed as
|
|
428
|
+
``barstate.isrealtime ? 1 : 0`` and friends."""
|
|
429
|
+
if resolving is None:
|
|
430
|
+
resolving = set()
|
|
431
|
+
if isinstance(node, BoolLiteral):
|
|
432
|
+
return bool(node.value)
|
|
433
|
+
if (
|
|
434
|
+
isinstance(node, MemberAccess)
|
|
435
|
+
and isinstance(node.object, Identifier)
|
|
436
|
+
and node.object.name == "barstate"
|
|
437
|
+
):
|
|
438
|
+
return self._SECURITY_CONST_BARSTATE.get(node.member)
|
|
439
|
+
if isinstance(node, UnaryOp) and node.op == "not":
|
|
440
|
+
inner = self._fold_security_const_bool(
|
|
441
|
+
node.operand, helper_binding_stack, resolving
|
|
442
|
+
)
|
|
443
|
+
return None if inner is None else (not inner)
|
|
444
|
+
if isinstance(node, BinOp) and node.op in ("and", "or"):
|
|
445
|
+
left = self._fold_security_const_bool(
|
|
446
|
+
node.left, helper_binding_stack, resolving
|
|
447
|
+
)
|
|
448
|
+
right = self._fold_security_const_bool(
|
|
449
|
+
node.right, helper_binding_stack, resolving
|
|
450
|
+
)
|
|
451
|
+
if left is None or right is None:
|
|
452
|
+
return None
|
|
453
|
+
return (left and right) if node.op == "and" else (left or right)
|
|
454
|
+
if isinstance(node, Identifier):
|
|
455
|
+
bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
|
|
456
|
+
if bound is not None:
|
|
457
|
+
return self._fold_security_const_bool(
|
|
458
|
+
bound, helper_binding_stack, resolving
|
|
459
|
+
)
|
|
460
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
461
|
+
if node.name in global_expr_map and node.name not in resolving:
|
|
462
|
+
resolving.add(node.name)
|
|
463
|
+
out = self._fold_security_const_bool(
|
|
464
|
+
global_expr_map[node.name], helper_binding_stack, resolving
|
|
465
|
+
)
|
|
466
|
+
resolving.remove(node.name)
|
|
467
|
+
return out
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
def _resolve_security_index_literal(
|
|
471
|
+
self,
|
|
472
|
+
node,
|
|
473
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
474
|
+
resolving: set[str] | None = None,
|
|
475
|
+
) -> int | None:
|
|
476
|
+
"""Resolve a request.security TA/bar-field history index to a literal int.
|
|
477
|
+
|
|
478
|
+
Extends ``_literal_int_for_security_index`` by also resolving the index
|
|
479
|
+
through helper-parameter bindings and global aliases and constant-folding
|
|
480
|
+
a ternary whose condition is a batch-constant barstate flag (e.g.
|
|
481
|
+
``idxHigher = barstate.isrealtime ? 1 : 0`` -> 0). A literal index short-
|
|
482
|
+
circuits on the first line, so behaviour is unchanged for every already-
|
|
483
|
+
literal index. Returns None when the index cannot be reduced to a
|
|
484
|
+
compile-time literal, so the caller keeps its existing rejection."""
|
|
485
|
+
direct = self._literal_int_for_security_index(node)
|
|
486
|
+
if direct is not None:
|
|
487
|
+
return direct
|
|
488
|
+
if resolving is None:
|
|
489
|
+
resolving = set()
|
|
490
|
+
if isinstance(node, Identifier):
|
|
491
|
+
bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
|
|
492
|
+
if bound is not None:
|
|
493
|
+
return self._resolve_security_index_literal(
|
|
494
|
+
bound, helper_binding_stack, resolving
|
|
495
|
+
)
|
|
496
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
497
|
+
if node.name in global_expr_map and node.name not in resolving:
|
|
498
|
+
resolving.add(node.name)
|
|
499
|
+
out = self._resolve_security_index_literal(
|
|
500
|
+
global_expr_map[node.name], helper_binding_stack, resolving
|
|
501
|
+
)
|
|
502
|
+
resolving.remove(node.name)
|
|
503
|
+
return out
|
|
504
|
+
return None
|
|
505
|
+
if isinstance(node, Ternary):
|
|
506
|
+
cond = self._fold_security_const_bool(node.condition, helper_binding_stack)
|
|
507
|
+
if cond is None:
|
|
508
|
+
return None
|
|
509
|
+
chosen = node.true_val if cond else node.false_val
|
|
510
|
+
return self._resolve_security_index_literal(
|
|
511
|
+
chosen, helper_binding_stack, resolving
|
|
512
|
+
)
|
|
513
|
+
return None
|
|
514
|
+
|
|
298
515
|
def _collect_security_ohlc_hist_fields(self, node) -> set[str]:
|
|
299
|
-
"""Which
|
|
516
|
+
"""Which security bar fields need HTF history (subscript index >= 1)."""
|
|
300
517
|
out: set[str] = set()
|
|
301
518
|
|
|
302
519
|
def walk(n):
|
|
303
520
|
if n is None:
|
|
304
521
|
return
|
|
305
522
|
if isinstance(n, Subscript) and isinstance(n.object, Identifier):
|
|
306
|
-
if n.object.name in
|
|
523
|
+
if n.object.name in SECURITY_BAR_FIELDS:
|
|
307
524
|
idx = self._literal_int_for_security_index(n.index)
|
|
308
|
-
# high[0] uses current HTF `bar`;
|
|
309
|
-
# bars from Series history (filled before push
|
|
310
|
-
|
|
525
|
+
# high[0]/time[0] uses current HTF `bar`; k>=1 reads prior
|
|
526
|
+
# completed HTF bars from Series history (filled before push
|
|
527
|
+
# in _eval_security_*). Dynamic indices may be >=1 at
|
|
528
|
+
# runtime, so they need the same backing Series.
|
|
529
|
+
if idx is None or idx >= 1:
|
|
311
530
|
out.add(n.object.name)
|
|
312
531
|
if isinstance(n, (list, tuple)):
|
|
313
532
|
for x in n:
|
|
@@ -324,9 +543,32 @@ class SecurityEmitter:
|
|
|
324
543
|
walk(node)
|
|
325
544
|
return out
|
|
326
545
|
|
|
546
|
+
def _collect_security_ohlc_hist_fields_for_call(self, item: dict) -> set[str]:
|
|
547
|
+
"""Collect HTF OHLC history needed by a security expression and any
|
|
548
|
+
mutable-global rebinds replayed inside that security evaluator."""
|
|
549
|
+
fields = self._collect_security_ohlc_hist_fields(item.get("expr_node"))
|
|
550
|
+
for name in item.get("mutable_globals", []) or []:
|
|
551
|
+
info = self._global_mutable_infos.get(name)
|
|
552
|
+
if info is None:
|
|
553
|
+
continue
|
|
554
|
+
for stmt in getattr(info, "source_stmts", []) or []:
|
|
555
|
+
fields |= self._collect_security_ohlc_hist_fields(stmt)
|
|
556
|
+
return fields
|
|
557
|
+
|
|
327
558
|
def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
|
|
328
559
|
return f"_sec{sec_id}_hist_{field}"
|
|
329
560
|
|
|
561
|
+
def _security_bar_hist_type(self, field: str) -> str:
|
|
562
|
+
return SECURITY_BAR_FIELD_TYPES.get(field, "double")
|
|
563
|
+
|
|
564
|
+
def _security_bar_field_expr(self, field: str) -> str:
|
|
565
|
+
return SECURITY_BAR_FIELD_EXPRS.get(field, f"bar.{field}")
|
|
566
|
+
|
|
567
|
+
@staticmethod
|
|
568
|
+
def _security_tuple_result_default(cpp_type: str, tuple_size: int) -> str:
|
|
569
|
+
vals = ", ".join("na<double>()" for _ in range(max(0, tuple_size)))
|
|
570
|
+
return f"{cpp_type}{{{vals}}}"
|
|
571
|
+
|
|
330
572
|
def _collect_security_ta_hist_indices(self, node) -> set[int]:
|
|
331
573
|
"""Which security TA call-site indices need HTF history (subscript index >= 1).
|
|
332
574
|
|
|
@@ -362,7 +604,7 @@ class SecurityEmitter:
|
|
|
362
604
|
if isinstance(n, Subscript):
|
|
363
605
|
site = resolve_ta_site(n.object)
|
|
364
606
|
if site is not None:
|
|
365
|
-
idx_lit = self.
|
|
607
|
+
idx_lit = self._resolve_security_index_literal(n.index)
|
|
366
608
|
if idx_lit is not None and idx_lit >= 1:
|
|
367
609
|
site_idx = self._ta_index_by_site_id.get(id(site))
|
|
368
610
|
if site_idx is not None:
|
|
@@ -396,6 +638,172 @@ class SecurityEmitter:
|
|
|
396
638
|
names.append(self._security_ta_hist_series_cpp(variant["member_name"]))
|
|
397
639
|
return names
|
|
398
640
|
|
|
641
|
+
def _collect_security_expr_hist_subscripts(
|
|
642
|
+
self, node, resolving: set[str] | None = None
|
|
643
|
+
) -> list[Subscript]:
|
|
644
|
+
"""Subscripted helper-call results needing security-context history."""
|
|
645
|
+
if node is None:
|
|
646
|
+
return []
|
|
647
|
+
if resolving is None:
|
|
648
|
+
resolving = set()
|
|
649
|
+
|
|
650
|
+
out: list[Subscript] = []
|
|
651
|
+
seen: set[int] = set()
|
|
652
|
+
|
|
653
|
+
def add(n: Subscript) -> None:
|
|
654
|
+
key = id(n)
|
|
655
|
+
if key not in seen:
|
|
656
|
+
seen.add(key)
|
|
657
|
+
out.append(n)
|
|
658
|
+
|
|
659
|
+
def walk(n) -> None:
|
|
660
|
+
if n is None:
|
|
661
|
+
return
|
|
662
|
+
if isinstance(n, Identifier):
|
|
663
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
664
|
+
if n.name in global_expr_map and n.name not in resolving:
|
|
665
|
+
resolving.add(n.name)
|
|
666
|
+
walk(global_expr_map[n.name])
|
|
667
|
+
resolving.remove(n.name)
|
|
668
|
+
return
|
|
669
|
+
if (
|
|
670
|
+
isinstance(n, Subscript)
|
|
671
|
+
and isinstance(n.object, FuncCall)
|
|
672
|
+
and self._get_ta_site(n.object) is None
|
|
673
|
+
):
|
|
674
|
+
add(n)
|
|
675
|
+
if isinstance(n, (list, tuple)):
|
|
676
|
+
for x in n:
|
|
677
|
+
walk(x)
|
|
678
|
+
return
|
|
679
|
+
for _k, v in getattr(n, "__dict__", {}).items():
|
|
680
|
+
if isinstance(v, ASTNode):
|
|
681
|
+
walk(v)
|
|
682
|
+
elif isinstance(v, (list, tuple)):
|
|
683
|
+
for x in v:
|
|
684
|
+
if isinstance(x, ASTNode):
|
|
685
|
+
walk(x)
|
|
686
|
+
|
|
687
|
+
walk(node)
|
|
688
|
+
return out
|
|
689
|
+
|
|
690
|
+
def _security_expr_hist_series_names(self, sec_id: int) -> list[str]:
|
|
691
|
+
names = []
|
|
692
|
+
for (sid, _node_id), meta in sorted(self._security_expr_hist_by_node.items()):
|
|
693
|
+
if sid == sec_id:
|
|
694
|
+
names.append(meta["name"])
|
|
695
|
+
return names
|
|
696
|
+
|
|
697
|
+
def _emit_security_expr_hist_members(
|
|
698
|
+
self, sec_id: int, expr_node, lines: list[str], mbb_suffix: str
|
|
699
|
+
) -> None:
|
|
700
|
+
for idx, node in enumerate(self._collect_security_expr_hist_subscripts(expr_node)):
|
|
701
|
+
cpp_t = self._infer_type(node.object)
|
|
702
|
+
if cpp_t not in ("double", "int", "bool"):
|
|
703
|
+
cpp_t = "double"
|
|
704
|
+
name = f"_sec{sec_id}_expr_hist_{idx}"
|
|
705
|
+
self._security_expr_hist_by_node[(sec_id, id(node))] = {
|
|
706
|
+
"name": name,
|
|
707
|
+
"type": cpp_t,
|
|
708
|
+
}
|
|
709
|
+
lines.append(f" Series<{cpp_t}> {name}{mbb_suffix};")
|
|
710
|
+
|
|
711
|
+
def _build_security_math_call(
|
|
712
|
+
self,
|
|
713
|
+
sec_id: int,
|
|
714
|
+
func_name: str,
|
|
715
|
+
node: FuncCall,
|
|
716
|
+
ta_range,
|
|
717
|
+
ta_results: dict,
|
|
718
|
+
resolving: set[str],
|
|
719
|
+
security_mutable_names: set[str],
|
|
720
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...],
|
|
721
|
+
emitted_lines: list[str] | None,
|
|
722
|
+
) -> str:
|
|
723
|
+
visit = lambda arg: self._build_security_expr(
|
|
724
|
+
sec_id,
|
|
725
|
+
arg,
|
|
726
|
+
ta_range,
|
|
727
|
+
ta_results,
|
|
728
|
+
resolving,
|
|
729
|
+
security_mutable_names,
|
|
730
|
+
helper_binding_stack,
|
|
731
|
+
emitted_lines,
|
|
732
|
+
)
|
|
733
|
+
args = _merge_kwargs(
|
|
734
|
+
node.args,
|
|
735
|
+
node.kwargs,
|
|
736
|
+
sigs.get_param_names("math", func_name),
|
|
737
|
+
visit,
|
|
738
|
+
)
|
|
739
|
+
if func_name == "round" and len(args) == 2:
|
|
740
|
+
return f"(std::round({args[0]} * std::pow(10.0, {args[1]})) / std::pow(10.0, {args[1]}))"
|
|
741
|
+
if func_name == "round_to_mintick":
|
|
742
|
+
x = args[0] if args else "0.0"
|
|
743
|
+
return f"round_to_mintick({x})"
|
|
744
|
+
if func_name == "todegrees":
|
|
745
|
+
x = args[0] if args else "0.0"
|
|
746
|
+
return f"({x} * 180.0 / M_PI)"
|
|
747
|
+
if func_name == "toradians":
|
|
748
|
+
x = args[0] if args else "0.0"
|
|
749
|
+
return f"({x} * M_PI / 180.0)"
|
|
750
|
+
if func_name == "random":
|
|
751
|
+
lo = args[0] if len(args) > 0 else "0.0"
|
|
752
|
+
hi = args[1] if len(args) > 1 else "1.0"
|
|
753
|
+
seed = args[2] if len(args) > 2 else "0"
|
|
754
|
+
call_site = self._random_call_counter
|
|
755
|
+
self._random_call_counter += 1
|
|
756
|
+
return f"pine_random({lo}, {call_site}u, {hi}, (uint32_t)({seed}), bar_index_)"
|
|
757
|
+
if func_name == "avg" and len(args) > 2:
|
|
758
|
+
sum_expr = " + ".join(f"(double)({a})" for a in args)
|
|
759
|
+
return f"(({sum_expr}) / {len(args)}.0)"
|
|
760
|
+
if func_name in ("min", "max"):
|
|
761
|
+
return _math_minmax_na_expr(func_name, args)
|
|
762
|
+
if func_name in MATH_FUNC_MAP:
|
|
763
|
+
mapped = MATH_FUNC_MAP[func_name]
|
|
764
|
+
if "{0}" in mapped:
|
|
765
|
+
return mapped.format(*args)
|
|
766
|
+
return f"{mapped}({', '.join(args)})"
|
|
767
|
+
return f"0.0 /* unsupported: math.{func_name} */"
|
|
768
|
+
|
|
769
|
+
def _security_timeframe_expr(self, sec_id: int) -> str:
|
|
770
|
+
"""C++ expression for the timeframe of a request.security evaluator."""
|
|
771
|
+
info = self._security_eval_info[sec_id]
|
|
772
|
+
if info.get("tf"):
|
|
773
|
+
return f'"{info["tf"]}"'
|
|
774
|
+
if info.get("tf_expr"):
|
|
775
|
+
return info["tf_expr"]
|
|
776
|
+
return "input_tf_"
|
|
777
|
+
|
|
778
|
+
def _build_security_timeframe_member(self, sec_id: int, member: str) -> str | None:
|
|
779
|
+
"""Lower timeframe.* reads inside request.security to the requested TF."""
|
|
780
|
+
tf = self._security_timeframe_expr(sec_id)
|
|
781
|
+
if member == "period":
|
|
782
|
+
return tf
|
|
783
|
+
if member == "main_period":
|
|
784
|
+
return "main_period()"
|
|
785
|
+
if member == "multiplier":
|
|
786
|
+
return f"tf_multiplier({tf})"
|
|
787
|
+
if member == "isintraday":
|
|
788
|
+
return f"tf_is_intraday({tf})"
|
|
789
|
+
if member == "isminutes":
|
|
790
|
+
return f"(tf_is_intraday({tf}) && !tf_is_seconds({tf}))"
|
|
791
|
+
if member == "isdaily":
|
|
792
|
+
return f"tf_is_daily({tf})"
|
|
793
|
+
if member == "isweekly":
|
|
794
|
+
return f"tf_is_weekly({tf})"
|
|
795
|
+
if member == "ismonthly":
|
|
796
|
+
return f"tf_is_monthly({tf})"
|
|
797
|
+
if member == "isdwm":
|
|
798
|
+
return f"(tf_is_daily({tf}) || tf_is_weekly({tf}) || tf_is_monthly({tf}))"
|
|
799
|
+
if member == "isseconds":
|
|
800
|
+
return f"tf_is_seconds({tf})"
|
|
801
|
+
if member == "in_seconds":
|
|
802
|
+
return f"tf_to_seconds({tf})"
|
|
803
|
+
if member == "isticks":
|
|
804
|
+
return "false"
|
|
805
|
+
return None
|
|
806
|
+
|
|
399
807
|
@staticmethod
|
|
400
808
|
def _security_series_binding(series_name: str) -> str:
|
|
401
809
|
return f"@series:{series_name}"
|
|
@@ -924,6 +1332,16 @@ class SecurityEmitter:
|
|
|
924
1332
|
if bound is not None:
|
|
925
1333
|
if isinstance(bound, str):
|
|
926
1334
|
return collected
|
|
1335
|
+
# Guard against a cyclic helper binding: an identifier bound to
|
|
1336
|
+
# an expression that transitively references the same binding
|
|
1337
|
+
# (e.g. mutually-referential helper params) would recurse here
|
|
1338
|
+
# forever — the mutable/global/func paths below already carry a
|
|
1339
|
+
# `resolving` guard, this path did not. Keyed by the bound node
|
|
1340
|
+
# so acyclic bindings are unaffected (byte-identical emission).
|
|
1341
|
+
bind_key = f"bind:{id(bound)}"
|
|
1342
|
+
if bind_key in resolving:
|
|
1343
|
+
return collected
|
|
1344
|
+
resolving.add(bind_key)
|
|
927
1345
|
self._collect_security_ta_binding_stacks(
|
|
928
1346
|
bound,
|
|
929
1347
|
resolving,
|
|
@@ -932,6 +1350,7 @@ class SecurityEmitter:
|
|
|
932
1350
|
inline_ta_indices,
|
|
933
1351
|
inline_helper,
|
|
934
1352
|
)
|
|
1353
|
+
resolving.discard(bind_key)
|
|
935
1354
|
return collected
|
|
936
1355
|
|
|
937
1356
|
mutable_info = self._global_mutable_infos.get(expr_node.name)
|
|
@@ -1335,7 +1754,7 @@ class SecurityEmitter:
|
|
|
1335
1754
|
lines.append(" if (is_complete) {")
|
|
1336
1755
|
for field in fields:
|
|
1337
1756
|
lines.append(
|
|
1338
|
-
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(
|
|
1757
|
+
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push({self._security_bar_field_expr(field)});"
|
|
1339
1758
|
)
|
|
1340
1759
|
lines.append(" }")
|
|
1341
1760
|
|
|
@@ -1535,6 +1954,8 @@ class SecurityEmitter:
|
|
|
1535
1954
|
)
|
|
1536
1955
|
for name in self._security_ta_hist_series_names(sec_id):
|
|
1537
1956
|
lines.append(f" {name}.clear();")
|
|
1957
|
+
for name in self._security_expr_hist_series_names(sec_id):
|
|
1958
|
+
lines.append(f" {name}.clear();")
|
|
1538
1959
|
lines.append(" break;")
|
|
1539
1960
|
continue
|
|
1540
1961
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
@@ -1559,11 +1980,39 @@ class SecurityEmitter:
|
|
|
1559
1980
|
)
|
|
1560
1981
|
for name in self._security_ta_hist_series_names(sec_id):
|
|
1561
1982
|
lines.append(f" {name}.clear();")
|
|
1983
|
+
for name in self._security_expr_hist_series_names(sec_id):
|
|
1984
|
+
lines.append(f" {name}.clear();")
|
|
1985
|
+
lines.append(" break;")
|
|
1986
|
+
elif returns_tuple and tuple_size and tuple_size > 0:
|
|
1987
|
+
site = self._get_ta_site(expr_node)
|
|
1988
|
+
ta_name = self._ta_name_from_site(site) if site is not None else ""
|
|
1989
|
+
ctype = {
|
|
1990
|
+
"macd": "ta::MACDResult",
|
|
1991
|
+
"supertrend": "ta::SupertrendResult",
|
|
1992
|
+
"dmi": "ta::DMIResult",
|
|
1993
|
+
"bb": "ta::BBResult",
|
|
1994
|
+
"kc": "ta::KCResult",
|
|
1995
|
+
"vwap_bands": "ta::VWAPBandsResult",
|
|
1996
|
+
}.get(ta_name, "std::tuple<double, double>")
|
|
1997
|
+
lines.append(f" case {sec_id}:")
|
|
1998
|
+
lines.append(
|
|
1999
|
+
f" _req_sec_{sec_id} = "
|
|
2000
|
+
f"{self._security_tuple_result_default(ctype, tuple_size)};"
|
|
2001
|
+
)
|
|
2002
|
+
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
2003
|
+
lines.append(
|
|
2004
|
+
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
2005
|
+
)
|
|
2006
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
2007
|
+
lines.append(f" {name}.clear();")
|
|
2008
|
+
for name in self._security_expr_hist_series_names(sec_id):
|
|
2009
|
+
lines.append(f" {name}.clear();")
|
|
1562
2010
|
lines.append(" break;")
|
|
1563
2011
|
else:
|
|
1564
2012
|
hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
1565
2013
|
ta_hist_names = self._security_ta_hist_series_names(sec_id)
|
|
1566
|
-
|
|
2014
|
+
expr_hist_names = self._security_expr_hist_series_names(sec_id)
|
|
2015
|
+
if hist or ta_hist_names or expr_hist_names:
|
|
1567
2016
|
lines.append(f" case {sec_id}:")
|
|
1568
2017
|
lines.append(f" _req_sec_{sec_id} = na<double>();")
|
|
1569
2018
|
for field in sorted(hist):
|
|
@@ -1572,6 +2021,8 @@ class SecurityEmitter:
|
|
|
1572
2021
|
)
|
|
1573
2022
|
for name in ta_hist_names:
|
|
1574
2023
|
lines.append(f" {name}.clear();")
|
|
2024
|
+
for name in expr_hist_names:
|
|
2025
|
+
lines.append(f" {name}.clear();")
|
|
1575
2026
|
lines.append(" break;")
|
|
1576
2027
|
else:
|
|
1577
2028
|
lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
|
|
@@ -1619,9 +2070,7 @@ class SecurityEmitter:
|
|
|
1619
2070
|
emitted_lines,
|
|
1620
2071
|
)
|
|
1621
2072
|
bar_fields = {
|
|
1622
|
-
|
|
1623
|
-
"low": "bar.low", "open": "bar.open",
|
|
1624
|
-
"volume": "bar.volume",
|
|
2073
|
+
**SECURITY_BAR_FIELD_EXPRS,
|
|
1625
2074
|
"hl2": "((bar.high + bar.low) / 2.0)",
|
|
1626
2075
|
"hlc3": "((bar.high + bar.low + bar.close) / 3.0)",
|
|
1627
2076
|
"ohlc4": "((bar.open + bar.high + bar.low + bar.close) / 4.0)",
|
|
@@ -1652,6 +2101,15 @@ class SecurityEmitter:
|
|
|
1652
2101
|
resolving.remove(expr_node.name)
|
|
1653
2102
|
return resolved
|
|
1654
2103
|
|
|
2104
|
+
if (
|
|
2105
|
+
isinstance(expr_node, MemberAccess)
|
|
2106
|
+
and isinstance(expr_node.object, Identifier)
|
|
2107
|
+
and expr_node.object.name == "timeframe"
|
|
2108
|
+
):
|
|
2109
|
+
resolved = self._build_security_timeframe_member(sec_id, expr_node.member)
|
|
2110
|
+
if resolved is not None:
|
|
2111
|
+
return resolved
|
|
2112
|
+
|
|
1655
2113
|
if isinstance(expr_node, Subscript):
|
|
1656
2114
|
index_cpp = self._build_security_expr(
|
|
1657
2115
|
sec_id,
|
|
@@ -1682,29 +2140,33 @@ class SecurityEmitter:
|
|
|
1682
2140
|
emitted_lines,
|
|
1683
2141
|
)
|
|
1684
2142
|
return f"{obj_cpp}[{index_cpp}]"
|
|
1685
|
-
if expr_node.object.name in
|
|
2143
|
+
if expr_node.object.name in SECURITY_BAR_FIELDS:
|
|
2144
|
+
field = expr_node.object.name
|
|
1686
2145
|
idx_lit = self._literal_int_for_security_index(expr_node.index)
|
|
1687
2146
|
if idx_lit is not None:
|
|
1688
|
-
bar_map = {
|
|
1689
|
-
"open": "bar.open",
|
|
1690
|
-
"high": "bar.high",
|
|
1691
|
-
"low": "bar.low",
|
|
1692
|
-
"close": "bar.close",
|
|
1693
|
-
"volume": "bar.volume",
|
|
1694
|
-
}
|
|
1695
2147
|
if idx_lit == 0:
|
|
1696
|
-
return
|
|
2148
|
+
return self._security_bar_field_expr(field)
|
|
1697
2149
|
if idx_lit >= 1:
|
|
1698
2150
|
# lookahead_off: we evaluate when an HTF bar completes; `bar` is that
|
|
1699
|
-
# bar. On the HTF series, high[0]/
|
|
1700
|
-
# bar; high[1] is one HTF bar back
|
|
1701
|
-
#
|
|
1702
|
-
field
|
|
2151
|
+
# bar. On the HTF series, high[0]/time[0] is the current
|
|
2152
|
+
# (just-finished) bar; high[1]/time[1] is one HTF bar back
|
|
2153
|
+
# = hist[field][0] *before* we push `bar` (Series [0] =
|
|
2154
|
+
# most recent prior push). field[k] -> hist[k-1].
|
|
1703
2155
|
hist = self._security_ohlc_hist_series_cpp(sec_id, field)
|
|
1704
2156
|
return f"{hist}[{idx_lit - 1}]"
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
2157
|
+
if idx_lit is not None:
|
|
2158
|
+
self._codegen_error(
|
|
2159
|
+
expr_node,
|
|
2160
|
+
"request.security() bar-field history index must be non-negative",
|
|
2161
|
+
)
|
|
2162
|
+
hist = self._security_ohlc_hist_series_cpp(sec_id, field)
|
|
2163
|
+
cpp_t = self._security_bar_hist_type(field)
|
|
2164
|
+
current = self._security_bar_field_expr(field)
|
|
2165
|
+
return (
|
|
2166
|
+
f"([&]() -> {cpp_t} {{ "
|
|
2167
|
+
f"int _hidx = (int)({index_cpp}); "
|
|
2168
|
+
f"return (_hidx <= 0) ? {current} : {hist}[_hidx - 1]; "
|
|
2169
|
+
f"}}())"
|
|
1708
2170
|
)
|
|
1709
2171
|
|
|
1710
2172
|
# Indirect TA binding: ``v = ta.ema(close, 55)`` then
|
|
@@ -1734,6 +2196,31 @@ class SecurityEmitter:
|
|
|
1734
2196
|
)
|
|
1735
2197
|
resolving.remove(expr_node.object.name)
|
|
1736
2198
|
return resolved
|
|
2199
|
+
if (
|
|
2200
|
+
isinstance(expr_node.object, FuncCall)
|
|
2201
|
+
and self._get_ta_site(expr_node.object) is None
|
|
2202
|
+
):
|
|
2203
|
+
meta = self._security_expr_hist_by_node.get((sec_id, id(expr_node)))
|
|
2204
|
+
hist = meta["name"] if meta else f"_sec{sec_id}_expr_hist_missing"
|
|
2205
|
+
cpp_t = meta["type"] if meta else "double"
|
|
2206
|
+
inner = self._build_security_expr(
|
|
2207
|
+
sec_id,
|
|
2208
|
+
expr_node.object,
|
|
2209
|
+
ta_range,
|
|
2210
|
+
ta_results,
|
|
2211
|
+
resolving,
|
|
2212
|
+
security_mutable_names,
|
|
2213
|
+
helper_binding_stack,
|
|
2214
|
+
emitted_lines,
|
|
2215
|
+
)
|
|
2216
|
+
return (
|
|
2217
|
+
f"([&]() -> {cpp_t} {{ "
|
|
2218
|
+
f"{cpp_t} _hv = ({inner}); "
|
|
2219
|
+
f"int _hidx = (int)({index_cpp}); "
|
|
2220
|
+
f"{cpp_t} _out = (_hidx <= 0) ? _hv : {hist}[_hidx - 1]; "
|
|
2221
|
+
f"if (is_complete) {hist}.push(_hv); "
|
|
2222
|
+
f"return _out; }}())"
|
|
2223
|
+
)
|
|
1737
2224
|
ta_site = self._get_ta_site(expr_node.object)
|
|
1738
2225
|
if ta_site is not None:
|
|
1739
2226
|
# ``ta.<fn>(...)[k]`` inside request.security(): the inner TA call
|
|
@@ -1748,7 +2235,9 @@ class SecurityEmitter:
|
|
|
1748
2235
|
# produced the chart-tf TA instead of the confirmed HTF value.
|
|
1749
2236
|
idx = self._ta_index_by_site_id.get(id(ta_site))
|
|
1750
2237
|
sig = self._security_binding_stack_signature(helper_binding_stack)
|
|
1751
|
-
idx_lit = self.
|
|
2238
|
+
idx_lit = self._resolve_security_index_literal(
|
|
2239
|
+
expr_node.index, helper_binding_stack
|
|
2240
|
+
)
|
|
1752
2241
|
if idx_lit is None:
|
|
1753
2242
|
self._codegen_error(
|
|
1754
2243
|
expr_node,
|
|
@@ -1786,7 +2275,10 @@ class SecurityEmitter:
|
|
|
1786
2275
|
op = cpp_ops.get(expr_node.op, expr_node.op)
|
|
1787
2276
|
if expr_node.op == "%":
|
|
1788
2277
|
return f"std::fmod((double)({left}), (double)({right}))"
|
|
1789
|
-
|
|
2278
|
+
# KI-71: honour Pine's falsy-on-na relational rule inside
|
|
2279
|
+
# request.security expressions too (this builder is a second
|
|
2280
|
+
# relational emission site independent of _visit_binop).
|
|
2281
|
+
return self._lower_relational(op, expr_node.left, expr_node.right, left, right)
|
|
1790
2282
|
|
|
1791
2283
|
if isinstance(expr_node, UnaryOp):
|
|
1792
2284
|
operand = self._build_security_expr(
|
|
@@ -1850,6 +2342,37 @@ class SecurityEmitter:
|
|
|
1850
2342
|
resolving.remove(call_key)
|
|
1851
2343
|
return resolved
|
|
1852
2344
|
|
|
2345
|
+
if (
|
|
2346
|
+
isinstance(expr_node, FuncCall)
|
|
2347
|
+
and isinstance(expr_node.callee, MemberAccess)
|
|
2348
|
+
and isinstance(expr_node.callee.object, Identifier)
|
|
2349
|
+
and expr_node.callee.object.name == "math"
|
|
2350
|
+
):
|
|
2351
|
+
# A rolling/stateful math reducer (e.g. math.sum -> math::Sum) is
|
|
2352
|
+
# precomputed into a committed _secval_* by the security TA
|
|
2353
|
+
# machinery, exactly like a ta.* call. Return that committed value
|
|
2354
|
+
# instead of falling through to _build_security_math_call, whose
|
|
2355
|
+
# inline lowering only covers scalar math and emits a broken
|
|
2356
|
+
# "unsupported: math.<f>" 0.0 for a reducer. Scalar math
|
|
2357
|
+
# (abs/round/min/max/...) is not a TA site, so this is a no-op.
|
|
2358
|
+
math_site = self._get_ta_site(expr_node)
|
|
2359
|
+
if math_site is not None:
|
|
2360
|
+
math_idx = self._ta_index_by_site_id.get(id(math_site))
|
|
2361
|
+
math_sig = self._security_binding_stack_signature(helper_binding_stack)
|
|
2362
|
+
if math_idx is not None and (math_idx, math_sig) in ta_results:
|
|
2363
|
+
return ta_results[(math_idx, math_sig)]
|
|
2364
|
+
return self._build_security_math_call(
|
|
2365
|
+
sec_id,
|
|
2366
|
+
expr_node.callee.member,
|
|
2367
|
+
expr_node,
|
|
2368
|
+
ta_range,
|
|
2369
|
+
ta_results,
|
|
2370
|
+
resolving,
|
|
2371
|
+
security_mutable_names,
|
|
2372
|
+
helper_binding_stack,
|
|
2373
|
+
emitted_lines,
|
|
2374
|
+
)
|
|
2375
|
+
|
|
1853
2376
|
site = self._get_ta_site(expr_node)
|
|
1854
2377
|
if site:
|
|
1855
2378
|
idx = self._ta_index_by_site_id.get(id(site))
|