@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
|
@@ -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,56 @@ 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
|
+
if node is None:
|
|
145
|
+
return None
|
|
146
|
+
resolving = resolving or set()
|
|
147
|
+
if isinstance(node, StringLiteral):
|
|
148
|
+
return self._visit_expr(node)
|
|
149
|
+
if isinstance(node, Identifier):
|
|
150
|
+
name = node.name
|
|
151
|
+
if name in self._timeframe_period_vars:
|
|
152
|
+
return "script_tf_"
|
|
153
|
+
if name in self._input_backed_vars and name in self._input_var_to_call:
|
|
154
|
+
return self._visit_expr(self._input_var_to_call[name])
|
|
155
|
+
if (name in self._known_vars and name not in self._input_backed_vars
|
|
156
|
+
and isinstance(self._known_vars[name], str)):
|
|
157
|
+
return self._visit_expr(StringLiteral(value=self._known_vars[name]))
|
|
158
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
159
|
+
if name in global_expr_map and name not in resolving:
|
|
160
|
+
resolving.add(name)
|
|
161
|
+
out = self._security_tf_runtime_expr(global_expr_map[name], resolving)
|
|
162
|
+
resolving.remove(name)
|
|
163
|
+
return out
|
|
164
|
+
return self._visit_expr(node)
|
|
165
|
+
if (
|
|
166
|
+
isinstance(node, MemberAccess)
|
|
167
|
+
and isinstance(node.object, Identifier)
|
|
168
|
+
and node.object.name == "timeframe"
|
|
169
|
+
and node.member == "period"
|
|
170
|
+
):
|
|
171
|
+
return "script_tf_"
|
|
172
|
+
if isinstance(node, Ternary):
|
|
173
|
+
cond = self._security_tf_runtime_expr(node.condition, resolving)
|
|
174
|
+
tv = self._security_tf_runtime_expr(node.true_val, resolving)
|
|
175
|
+
fv = self._security_tf_runtime_expr(node.false_val, resolving)
|
|
176
|
+
if cond is not None and tv is not None and fv is not None:
|
|
177
|
+
return f"(({cond}) ? ({tv}) : ({fv}))"
|
|
178
|
+
return self._visit_expr(node)
|
|
179
|
+
|
|
122
180
|
def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
|
|
123
181
|
"""For a ``request.security`` whose tf is function parameter ``param_name``
|
|
124
182
|
of user function ``func_name``, return ``(tf_str, tf_expr)`` resolved from
|
|
@@ -276,7 +334,7 @@ class SecurityEmitter:
|
|
|
276
334
|
return None
|
|
277
335
|
|
|
278
336
|
def _literal_int_for_security_index(self, node) -> int | None:
|
|
279
|
-
"""Integer index for
|
|
337
|
+
"""Integer index for bar-field[n] inside request.security (must be literal)."""
|
|
280
338
|
if isinstance(node, NumberLiteral):
|
|
281
339
|
v = node.value
|
|
282
340
|
if isinstance(v, bool):
|
|
@@ -295,19 +353,131 @@ class SecurityEmitter:
|
|
|
295
353
|
return None
|
|
296
354
|
return None
|
|
297
355
|
|
|
356
|
+
# In PineForge batch backtests these barstate flags are compile-time
|
|
357
|
+
# constants (see support_checker / codegen.visit_expr barstate emission):
|
|
358
|
+
# every bar is historical, none realtime/last. The other flags
|
|
359
|
+
# (isfirst/isnew/isconfirmed) depend on runtime tick state, so they are
|
|
360
|
+
# deliberately absent and leave a fold "unknown".
|
|
361
|
+
_SECURITY_CONST_BARSTATE = {
|
|
362
|
+
"isrealtime": False,
|
|
363
|
+
"islast": False,
|
|
364
|
+
"ishistory": True,
|
|
365
|
+
"islastconfirmedhistory": False,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
def _fold_security_const_bool(
|
|
369
|
+
self,
|
|
370
|
+
node,
|
|
371
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
372
|
+
resolving: set[str] | None = None,
|
|
373
|
+
) -> bool | None:
|
|
374
|
+
"""Fold a boolean expression to a compile-time constant, or None.
|
|
375
|
+
|
|
376
|
+
Only reduces expressions whose non-literal leaves are the batch-constant
|
|
377
|
+
barstate flags above; anything runtime-dependent returns None. Used to
|
|
378
|
+
resolve a request.security history index expressed as
|
|
379
|
+
``barstate.isrealtime ? 1 : 0`` and friends."""
|
|
380
|
+
if resolving is None:
|
|
381
|
+
resolving = set()
|
|
382
|
+
if isinstance(node, BoolLiteral):
|
|
383
|
+
return bool(node.value)
|
|
384
|
+
if (
|
|
385
|
+
isinstance(node, MemberAccess)
|
|
386
|
+
and isinstance(node.object, Identifier)
|
|
387
|
+
and node.object.name == "barstate"
|
|
388
|
+
):
|
|
389
|
+
return self._SECURITY_CONST_BARSTATE.get(node.member)
|
|
390
|
+
if isinstance(node, UnaryOp) and node.op == "not":
|
|
391
|
+
inner = self._fold_security_const_bool(
|
|
392
|
+
node.operand, helper_binding_stack, resolving
|
|
393
|
+
)
|
|
394
|
+
return None if inner is None else (not inner)
|
|
395
|
+
if isinstance(node, BinOp) and node.op in ("and", "or"):
|
|
396
|
+
left = self._fold_security_const_bool(
|
|
397
|
+
node.left, helper_binding_stack, resolving
|
|
398
|
+
)
|
|
399
|
+
right = self._fold_security_const_bool(
|
|
400
|
+
node.right, helper_binding_stack, resolving
|
|
401
|
+
)
|
|
402
|
+
if left is None or right is None:
|
|
403
|
+
return None
|
|
404
|
+
return (left and right) if node.op == "and" else (left or right)
|
|
405
|
+
if isinstance(node, Identifier):
|
|
406
|
+
bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
|
|
407
|
+
if bound is not None:
|
|
408
|
+
return self._fold_security_const_bool(
|
|
409
|
+
bound, helper_binding_stack, resolving
|
|
410
|
+
)
|
|
411
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
412
|
+
if node.name in global_expr_map and node.name not in resolving:
|
|
413
|
+
resolving.add(node.name)
|
|
414
|
+
out = self._fold_security_const_bool(
|
|
415
|
+
global_expr_map[node.name], helper_binding_stack, resolving
|
|
416
|
+
)
|
|
417
|
+
resolving.remove(node.name)
|
|
418
|
+
return out
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
def _resolve_security_index_literal(
|
|
422
|
+
self,
|
|
423
|
+
node,
|
|
424
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...] | None = None,
|
|
425
|
+
resolving: set[str] | None = None,
|
|
426
|
+
) -> int | None:
|
|
427
|
+
"""Resolve a request.security TA/bar-field history index to a literal int.
|
|
428
|
+
|
|
429
|
+
Extends ``_literal_int_for_security_index`` by also resolving the index
|
|
430
|
+
through helper-parameter bindings and global aliases and constant-folding
|
|
431
|
+
a ternary whose condition is a batch-constant barstate flag (e.g.
|
|
432
|
+
``idxHigher = barstate.isrealtime ? 1 : 0`` -> 0). A literal index short-
|
|
433
|
+
circuits on the first line, so behaviour is unchanged for every already-
|
|
434
|
+
literal index. Returns None when the index cannot be reduced to a
|
|
435
|
+
compile-time literal, so the caller keeps its existing rejection."""
|
|
436
|
+
direct = self._literal_int_for_security_index(node)
|
|
437
|
+
if direct is not None:
|
|
438
|
+
return direct
|
|
439
|
+
if resolving is None:
|
|
440
|
+
resolving = set()
|
|
441
|
+
if isinstance(node, Identifier):
|
|
442
|
+
bound = self._security_lookup_helper_binding(node.name, helper_binding_stack)
|
|
443
|
+
if bound is not None:
|
|
444
|
+
return self._resolve_security_index_literal(
|
|
445
|
+
bound, helper_binding_stack, resolving
|
|
446
|
+
)
|
|
447
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
448
|
+
if node.name in global_expr_map and node.name not in resolving:
|
|
449
|
+
resolving.add(node.name)
|
|
450
|
+
out = self._resolve_security_index_literal(
|
|
451
|
+
global_expr_map[node.name], helper_binding_stack, resolving
|
|
452
|
+
)
|
|
453
|
+
resolving.remove(node.name)
|
|
454
|
+
return out
|
|
455
|
+
return None
|
|
456
|
+
if isinstance(node, Ternary):
|
|
457
|
+
cond = self._fold_security_const_bool(node.condition, helper_binding_stack)
|
|
458
|
+
if cond is None:
|
|
459
|
+
return None
|
|
460
|
+
chosen = node.true_val if cond else node.false_val
|
|
461
|
+
return self._resolve_security_index_literal(
|
|
462
|
+
chosen, helper_binding_stack, resolving
|
|
463
|
+
)
|
|
464
|
+
return None
|
|
465
|
+
|
|
298
466
|
def _collect_security_ohlc_hist_fields(self, node) -> set[str]:
|
|
299
|
-
"""Which
|
|
467
|
+
"""Which security bar fields need HTF history (subscript index >= 1)."""
|
|
300
468
|
out: set[str] = set()
|
|
301
469
|
|
|
302
470
|
def walk(n):
|
|
303
471
|
if n is None:
|
|
304
472
|
return
|
|
305
473
|
if isinstance(n, Subscript) and isinstance(n.object, Identifier):
|
|
306
|
-
if n.object.name in
|
|
474
|
+
if n.object.name in SECURITY_BAR_FIELDS:
|
|
307
475
|
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
|
-
|
|
476
|
+
# high[0]/time[0] uses current HTF `bar`; k>=1 reads prior
|
|
477
|
+
# completed HTF bars from Series history (filled before push
|
|
478
|
+
# in _eval_security_*). Dynamic indices may be >=1 at
|
|
479
|
+
# runtime, so they need the same backing Series.
|
|
480
|
+
if idx is None or idx >= 1:
|
|
311
481
|
out.add(n.object.name)
|
|
312
482
|
if isinstance(n, (list, tuple)):
|
|
313
483
|
for x in n:
|
|
@@ -324,9 +494,32 @@ class SecurityEmitter:
|
|
|
324
494
|
walk(node)
|
|
325
495
|
return out
|
|
326
496
|
|
|
497
|
+
def _collect_security_ohlc_hist_fields_for_call(self, item: dict) -> set[str]:
|
|
498
|
+
"""Collect HTF OHLC history needed by a security expression and any
|
|
499
|
+
mutable-global rebinds replayed inside that security evaluator."""
|
|
500
|
+
fields = self._collect_security_ohlc_hist_fields(item.get("expr_node"))
|
|
501
|
+
for name in item.get("mutable_globals", []) or []:
|
|
502
|
+
info = self._global_mutable_infos.get(name)
|
|
503
|
+
if info is None:
|
|
504
|
+
continue
|
|
505
|
+
for stmt in getattr(info, "source_stmts", []) or []:
|
|
506
|
+
fields |= self._collect_security_ohlc_hist_fields(stmt)
|
|
507
|
+
return fields
|
|
508
|
+
|
|
327
509
|
def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
|
|
328
510
|
return f"_sec{sec_id}_hist_{field}"
|
|
329
511
|
|
|
512
|
+
def _security_bar_hist_type(self, field: str) -> str:
|
|
513
|
+
return SECURITY_BAR_FIELD_TYPES.get(field, "double")
|
|
514
|
+
|
|
515
|
+
def _security_bar_field_expr(self, field: str) -> str:
|
|
516
|
+
return SECURITY_BAR_FIELD_EXPRS.get(field, f"bar.{field}")
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def _security_tuple_result_default(cpp_type: str, tuple_size: int) -> str:
|
|
520
|
+
vals = ", ".join("na<double>()" for _ in range(max(0, tuple_size)))
|
|
521
|
+
return f"{cpp_type}{{{vals}}}"
|
|
522
|
+
|
|
330
523
|
def _collect_security_ta_hist_indices(self, node) -> set[int]:
|
|
331
524
|
"""Which security TA call-site indices need HTF history (subscript index >= 1).
|
|
332
525
|
|
|
@@ -362,7 +555,7 @@ class SecurityEmitter:
|
|
|
362
555
|
if isinstance(n, Subscript):
|
|
363
556
|
site = resolve_ta_site(n.object)
|
|
364
557
|
if site is not None:
|
|
365
|
-
idx_lit = self.
|
|
558
|
+
idx_lit = self._resolve_security_index_literal(n.index)
|
|
366
559
|
if idx_lit is not None and idx_lit >= 1:
|
|
367
560
|
site_idx = self._ta_index_by_site_id.get(id(site))
|
|
368
561
|
if site_idx is not None:
|
|
@@ -396,6 +589,172 @@ class SecurityEmitter:
|
|
|
396
589
|
names.append(self._security_ta_hist_series_cpp(variant["member_name"]))
|
|
397
590
|
return names
|
|
398
591
|
|
|
592
|
+
def _collect_security_expr_hist_subscripts(
|
|
593
|
+
self, node, resolving: set[str] | None = None
|
|
594
|
+
) -> list[Subscript]:
|
|
595
|
+
"""Subscripted helper-call results needing security-context history."""
|
|
596
|
+
if node is None:
|
|
597
|
+
return []
|
|
598
|
+
if resolving is None:
|
|
599
|
+
resolving = set()
|
|
600
|
+
|
|
601
|
+
out: list[Subscript] = []
|
|
602
|
+
seen: set[int] = set()
|
|
603
|
+
|
|
604
|
+
def add(n: Subscript) -> None:
|
|
605
|
+
key = id(n)
|
|
606
|
+
if key not in seen:
|
|
607
|
+
seen.add(key)
|
|
608
|
+
out.append(n)
|
|
609
|
+
|
|
610
|
+
def walk(n) -> None:
|
|
611
|
+
if n is None:
|
|
612
|
+
return
|
|
613
|
+
if isinstance(n, Identifier):
|
|
614
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
615
|
+
if n.name in global_expr_map and n.name not in resolving:
|
|
616
|
+
resolving.add(n.name)
|
|
617
|
+
walk(global_expr_map[n.name])
|
|
618
|
+
resolving.remove(n.name)
|
|
619
|
+
return
|
|
620
|
+
if (
|
|
621
|
+
isinstance(n, Subscript)
|
|
622
|
+
and isinstance(n.object, FuncCall)
|
|
623
|
+
and self._get_ta_site(n.object) is None
|
|
624
|
+
):
|
|
625
|
+
add(n)
|
|
626
|
+
if isinstance(n, (list, tuple)):
|
|
627
|
+
for x in n:
|
|
628
|
+
walk(x)
|
|
629
|
+
return
|
|
630
|
+
for _k, v in getattr(n, "__dict__", {}).items():
|
|
631
|
+
if isinstance(v, ASTNode):
|
|
632
|
+
walk(v)
|
|
633
|
+
elif isinstance(v, (list, tuple)):
|
|
634
|
+
for x in v:
|
|
635
|
+
if isinstance(x, ASTNode):
|
|
636
|
+
walk(x)
|
|
637
|
+
|
|
638
|
+
walk(node)
|
|
639
|
+
return out
|
|
640
|
+
|
|
641
|
+
def _security_expr_hist_series_names(self, sec_id: int) -> list[str]:
|
|
642
|
+
names = []
|
|
643
|
+
for (sid, _node_id), meta in sorted(self._security_expr_hist_by_node.items()):
|
|
644
|
+
if sid == sec_id:
|
|
645
|
+
names.append(meta["name"])
|
|
646
|
+
return names
|
|
647
|
+
|
|
648
|
+
def _emit_security_expr_hist_members(
|
|
649
|
+
self, sec_id: int, expr_node, lines: list[str], mbb_suffix: str
|
|
650
|
+
) -> None:
|
|
651
|
+
for idx, node in enumerate(self._collect_security_expr_hist_subscripts(expr_node)):
|
|
652
|
+
cpp_t = self._infer_type(node.object)
|
|
653
|
+
if cpp_t not in ("double", "int", "bool"):
|
|
654
|
+
cpp_t = "double"
|
|
655
|
+
name = f"_sec{sec_id}_expr_hist_{idx}"
|
|
656
|
+
self._security_expr_hist_by_node[(sec_id, id(node))] = {
|
|
657
|
+
"name": name,
|
|
658
|
+
"type": cpp_t,
|
|
659
|
+
}
|
|
660
|
+
lines.append(f" Series<{cpp_t}> {name}{mbb_suffix};")
|
|
661
|
+
|
|
662
|
+
def _build_security_math_call(
|
|
663
|
+
self,
|
|
664
|
+
sec_id: int,
|
|
665
|
+
func_name: str,
|
|
666
|
+
node: FuncCall,
|
|
667
|
+
ta_range,
|
|
668
|
+
ta_results: dict,
|
|
669
|
+
resolving: set[str],
|
|
670
|
+
security_mutable_names: set[str],
|
|
671
|
+
helper_binding_stack: tuple[dict[str, ASTNode], ...],
|
|
672
|
+
emitted_lines: list[str] | None,
|
|
673
|
+
) -> str:
|
|
674
|
+
visit = lambda arg: self._build_security_expr(
|
|
675
|
+
sec_id,
|
|
676
|
+
arg,
|
|
677
|
+
ta_range,
|
|
678
|
+
ta_results,
|
|
679
|
+
resolving,
|
|
680
|
+
security_mutable_names,
|
|
681
|
+
helper_binding_stack,
|
|
682
|
+
emitted_lines,
|
|
683
|
+
)
|
|
684
|
+
args = _merge_kwargs(
|
|
685
|
+
node.args,
|
|
686
|
+
node.kwargs,
|
|
687
|
+
sigs.get_param_names("math", func_name),
|
|
688
|
+
visit,
|
|
689
|
+
)
|
|
690
|
+
if func_name == "round" and len(args) == 2:
|
|
691
|
+
return f"(std::round({args[0]} * std::pow(10.0, {args[1]})) / std::pow(10.0, {args[1]}))"
|
|
692
|
+
if func_name == "round_to_mintick":
|
|
693
|
+
x = args[0] if args else "0.0"
|
|
694
|
+
return f"round_to_mintick({x})"
|
|
695
|
+
if func_name == "todegrees":
|
|
696
|
+
x = args[0] if args else "0.0"
|
|
697
|
+
return f"({x} * 180.0 / M_PI)"
|
|
698
|
+
if func_name == "toradians":
|
|
699
|
+
x = args[0] if args else "0.0"
|
|
700
|
+
return f"({x} * M_PI / 180.0)"
|
|
701
|
+
if func_name == "random":
|
|
702
|
+
lo = args[0] if len(args) > 0 else "0.0"
|
|
703
|
+
hi = args[1] if len(args) > 1 else "1.0"
|
|
704
|
+
seed = args[2] if len(args) > 2 else "0"
|
|
705
|
+
call_site = self._random_call_counter
|
|
706
|
+
self._random_call_counter += 1
|
|
707
|
+
return f"pine_random({lo}, {call_site}u, {hi}, (uint32_t)({seed}), bar_index_)"
|
|
708
|
+
if func_name == "avg" and len(args) > 2:
|
|
709
|
+
sum_expr = " + ".join(f"(double)({a})" for a in args)
|
|
710
|
+
return f"(({sum_expr}) / {len(args)}.0)"
|
|
711
|
+
if func_name in ("min", "max"):
|
|
712
|
+
return _math_minmax_na_expr(func_name, args)
|
|
713
|
+
if func_name in MATH_FUNC_MAP:
|
|
714
|
+
mapped = MATH_FUNC_MAP[func_name]
|
|
715
|
+
if "{0}" in mapped:
|
|
716
|
+
return mapped.format(*args)
|
|
717
|
+
return f"{mapped}({', '.join(args)})"
|
|
718
|
+
return f"0.0 /* unsupported: math.{func_name} */"
|
|
719
|
+
|
|
720
|
+
def _security_timeframe_expr(self, sec_id: int) -> str:
|
|
721
|
+
"""C++ expression for the timeframe of a request.security evaluator."""
|
|
722
|
+
info = self._security_eval_info[sec_id]
|
|
723
|
+
if info.get("tf"):
|
|
724
|
+
return f'"{info["tf"]}"'
|
|
725
|
+
if info.get("tf_expr"):
|
|
726
|
+
return info["tf_expr"]
|
|
727
|
+
return "input_tf_"
|
|
728
|
+
|
|
729
|
+
def _build_security_timeframe_member(self, sec_id: int, member: str) -> str | None:
|
|
730
|
+
"""Lower timeframe.* reads inside request.security to the requested TF."""
|
|
731
|
+
tf = self._security_timeframe_expr(sec_id)
|
|
732
|
+
if member == "period":
|
|
733
|
+
return tf
|
|
734
|
+
if member == "main_period":
|
|
735
|
+
return "main_period()"
|
|
736
|
+
if member == "multiplier":
|
|
737
|
+
return f"tf_multiplier({tf})"
|
|
738
|
+
if member == "isintraday":
|
|
739
|
+
return f"tf_is_intraday({tf})"
|
|
740
|
+
if member == "isminutes":
|
|
741
|
+
return f"(tf_is_intraday({tf}) && !tf_is_seconds({tf}))"
|
|
742
|
+
if member == "isdaily":
|
|
743
|
+
return f"tf_is_daily({tf})"
|
|
744
|
+
if member == "isweekly":
|
|
745
|
+
return f"tf_is_weekly({tf})"
|
|
746
|
+
if member == "ismonthly":
|
|
747
|
+
return f"tf_is_monthly({tf})"
|
|
748
|
+
if member == "isdwm":
|
|
749
|
+
return f"(tf_is_daily({tf}) || tf_is_weekly({tf}) || tf_is_monthly({tf}))"
|
|
750
|
+
if member == "isseconds":
|
|
751
|
+
return f"tf_is_seconds({tf})"
|
|
752
|
+
if member == "in_seconds":
|
|
753
|
+
return f"tf_to_seconds({tf})"
|
|
754
|
+
if member == "isticks":
|
|
755
|
+
return "false"
|
|
756
|
+
return None
|
|
757
|
+
|
|
399
758
|
@staticmethod
|
|
400
759
|
def _security_series_binding(series_name: str) -> str:
|
|
401
760
|
return f"@series:{series_name}"
|
|
@@ -924,6 +1283,16 @@ class SecurityEmitter:
|
|
|
924
1283
|
if bound is not None:
|
|
925
1284
|
if isinstance(bound, str):
|
|
926
1285
|
return collected
|
|
1286
|
+
# Guard against a cyclic helper binding: an identifier bound to
|
|
1287
|
+
# an expression that transitively references the same binding
|
|
1288
|
+
# (e.g. mutually-referential helper params) would recurse here
|
|
1289
|
+
# forever — the mutable/global/func paths below already carry a
|
|
1290
|
+
# `resolving` guard, this path did not. Keyed by the bound node
|
|
1291
|
+
# so acyclic bindings are unaffected (byte-identical emission).
|
|
1292
|
+
bind_key = f"bind:{id(bound)}"
|
|
1293
|
+
if bind_key in resolving:
|
|
1294
|
+
return collected
|
|
1295
|
+
resolving.add(bind_key)
|
|
927
1296
|
self._collect_security_ta_binding_stacks(
|
|
928
1297
|
bound,
|
|
929
1298
|
resolving,
|
|
@@ -932,6 +1301,7 @@ class SecurityEmitter:
|
|
|
932
1301
|
inline_ta_indices,
|
|
933
1302
|
inline_helper,
|
|
934
1303
|
)
|
|
1304
|
+
resolving.discard(bind_key)
|
|
935
1305
|
return collected
|
|
936
1306
|
|
|
937
1307
|
mutable_info = self._global_mutable_infos.get(expr_node.name)
|
|
@@ -1335,7 +1705,7 @@ class SecurityEmitter:
|
|
|
1335
1705
|
lines.append(" if (is_complete) {")
|
|
1336
1706
|
for field in fields:
|
|
1337
1707
|
lines.append(
|
|
1338
|
-
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(
|
|
1708
|
+
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push({self._security_bar_field_expr(field)});"
|
|
1339
1709
|
)
|
|
1340
1710
|
lines.append(" }")
|
|
1341
1711
|
|
|
@@ -1535,6 +1905,8 @@ class SecurityEmitter:
|
|
|
1535
1905
|
)
|
|
1536
1906
|
for name in self._security_ta_hist_series_names(sec_id):
|
|
1537
1907
|
lines.append(f" {name}.clear();")
|
|
1908
|
+
for name in self._security_expr_hist_series_names(sec_id):
|
|
1909
|
+
lines.append(f" {name}.clear();")
|
|
1538
1910
|
lines.append(" break;")
|
|
1539
1911
|
continue
|
|
1540
1912
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
@@ -1559,11 +1931,39 @@ class SecurityEmitter:
|
|
|
1559
1931
|
)
|
|
1560
1932
|
for name in self._security_ta_hist_series_names(sec_id):
|
|
1561
1933
|
lines.append(f" {name}.clear();")
|
|
1934
|
+
for name in self._security_expr_hist_series_names(sec_id):
|
|
1935
|
+
lines.append(f" {name}.clear();")
|
|
1936
|
+
lines.append(" break;")
|
|
1937
|
+
elif returns_tuple and tuple_size and tuple_size > 0:
|
|
1938
|
+
site = self._get_ta_site(expr_node)
|
|
1939
|
+
ta_name = self._ta_name_from_site(site) if site is not None else ""
|
|
1940
|
+
ctype = {
|
|
1941
|
+
"macd": "ta::MACDResult",
|
|
1942
|
+
"supertrend": "ta::SupertrendResult",
|
|
1943
|
+
"dmi": "ta::DMIResult",
|
|
1944
|
+
"bb": "ta::BBResult",
|
|
1945
|
+
"kc": "ta::KCResult",
|
|
1946
|
+
"vwap_bands": "ta::VWAPBandsResult",
|
|
1947
|
+
}.get(ta_name, "std::tuple<double, double>")
|
|
1948
|
+
lines.append(f" case {sec_id}:")
|
|
1949
|
+
lines.append(
|
|
1950
|
+
f" _req_sec_{sec_id} = "
|
|
1951
|
+
f"{self._security_tuple_result_default(ctype, tuple_size)};"
|
|
1952
|
+
)
|
|
1953
|
+
for field in sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ())):
|
|
1954
|
+
lines.append(
|
|
1955
|
+
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1956
|
+
)
|
|
1957
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1958
|
+
lines.append(f" {name}.clear();")
|
|
1959
|
+
for name in self._security_expr_hist_series_names(sec_id):
|
|
1960
|
+
lines.append(f" {name}.clear();")
|
|
1562
1961
|
lines.append(" break;")
|
|
1563
1962
|
else:
|
|
1564
1963
|
hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
1565
1964
|
ta_hist_names = self._security_ta_hist_series_names(sec_id)
|
|
1566
|
-
|
|
1965
|
+
expr_hist_names = self._security_expr_hist_series_names(sec_id)
|
|
1966
|
+
if hist or ta_hist_names or expr_hist_names:
|
|
1567
1967
|
lines.append(f" case {sec_id}:")
|
|
1568
1968
|
lines.append(f" _req_sec_{sec_id} = na<double>();")
|
|
1569
1969
|
for field in sorted(hist):
|
|
@@ -1572,6 +1972,8 @@ class SecurityEmitter:
|
|
|
1572
1972
|
)
|
|
1573
1973
|
for name in ta_hist_names:
|
|
1574
1974
|
lines.append(f" {name}.clear();")
|
|
1975
|
+
for name in expr_hist_names:
|
|
1976
|
+
lines.append(f" {name}.clear();")
|
|
1575
1977
|
lines.append(" break;")
|
|
1576
1978
|
else:
|
|
1577
1979
|
lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
|
|
@@ -1619,9 +2021,7 @@ class SecurityEmitter:
|
|
|
1619
2021
|
emitted_lines,
|
|
1620
2022
|
)
|
|
1621
2023
|
bar_fields = {
|
|
1622
|
-
|
|
1623
|
-
"low": "bar.low", "open": "bar.open",
|
|
1624
|
-
"volume": "bar.volume",
|
|
2024
|
+
**SECURITY_BAR_FIELD_EXPRS,
|
|
1625
2025
|
"hl2": "((bar.high + bar.low) / 2.0)",
|
|
1626
2026
|
"hlc3": "((bar.high + bar.low + bar.close) / 3.0)",
|
|
1627
2027
|
"ohlc4": "((bar.open + bar.high + bar.low + bar.close) / 4.0)",
|
|
@@ -1652,6 +2052,15 @@ class SecurityEmitter:
|
|
|
1652
2052
|
resolving.remove(expr_node.name)
|
|
1653
2053
|
return resolved
|
|
1654
2054
|
|
|
2055
|
+
if (
|
|
2056
|
+
isinstance(expr_node, MemberAccess)
|
|
2057
|
+
and isinstance(expr_node.object, Identifier)
|
|
2058
|
+
and expr_node.object.name == "timeframe"
|
|
2059
|
+
):
|
|
2060
|
+
resolved = self._build_security_timeframe_member(sec_id, expr_node.member)
|
|
2061
|
+
if resolved is not None:
|
|
2062
|
+
return resolved
|
|
2063
|
+
|
|
1655
2064
|
if isinstance(expr_node, Subscript):
|
|
1656
2065
|
index_cpp = self._build_security_expr(
|
|
1657
2066
|
sec_id,
|
|
@@ -1682,29 +2091,33 @@ class SecurityEmitter:
|
|
|
1682
2091
|
emitted_lines,
|
|
1683
2092
|
)
|
|
1684
2093
|
return f"{obj_cpp}[{index_cpp}]"
|
|
1685
|
-
if expr_node.object.name in
|
|
2094
|
+
if expr_node.object.name in SECURITY_BAR_FIELDS:
|
|
2095
|
+
field = expr_node.object.name
|
|
1686
2096
|
idx_lit = self._literal_int_for_security_index(expr_node.index)
|
|
1687
2097
|
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
2098
|
if idx_lit == 0:
|
|
1696
|
-
return
|
|
2099
|
+
return self._security_bar_field_expr(field)
|
|
1697
2100
|
if idx_lit >= 1:
|
|
1698
2101
|
# 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
|
|
2102
|
+
# bar. On the HTF series, high[0]/time[0] is the current
|
|
2103
|
+
# (just-finished) bar; high[1]/time[1] is one HTF bar back
|
|
2104
|
+
# = hist[field][0] *before* we push `bar` (Series [0] =
|
|
2105
|
+
# most recent prior push). field[k] -> hist[k-1].
|
|
1703
2106
|
hist = self._security_ohlc_hist_series_cpp(sec_id, field)
|
|
1704
2107
|
return f"{hist}[{idx_lit - 1}]"
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
2108
|
+
if idx_lit is not None:
|
|
2109
|
+
self._codegen_error(
|
|
2110
|
+
expr_node,
|
|
2111
|
+
"request.security() bar-field history index must be non-negative",
|
|
2112
|
+
)
|
|
2113
|
+
hist = self._security_ohlc_hist_series_cpp(sec_id, field)
|
|
2114
|
+
cpp_t = self._security_bar_hist_type(field)
|
|
2115
|
+
current = self._security_bar_field_expr(field)
|
|
2116
|
+
return (
|
|
2117
|
+
f"([&]() -> {cpp_t} {{ "
|
|
2118
|
+
f"int _hidx = (int)({index_cpp}); "
|
|
2119
|
+
f"return (_hidx <= 0) ? {current} : {hist}[_hidx - 1]; "
|
|
2120
|
+
f"}}())"
|
|
1708
2121
|
)
|
|
1709
2122
|
|
|
1710
2123
|
# Indirect TA binding: ``v = ta.ema(close, 55)`` then
|
|
@@ -1734,6 +2147,31 @@ class SecurityEmitter:
|
|
|
1734
2147
|
)
|
|
1735
2148
|
resolving.remove(expr_node.object.name)
|
|
1736
2149
|
return resolved
|
|
2150
|
+
if (
|
|
2151
|
+
isinstance(expr_node.object, FuncCall)
|
|
2152
|
+
and self._get_ta_site(expr_node.object) is None
|
|
2153
|
+
):
|
|
2154
|
+
meta = self._security_expr_hist_by_node.get((sec_id, id(expr_node)))
|
|
2155
|
+
hist = meta["name"] if meta else f"_sec{sec_id}_expr_hist_missing"
|
|
2156
|
+
cpp_t = meta["type"] if meta else "double"
|
|
2157
|
+
inner = self._build_security_expr(
|
|
2158
|
+
sec_id,
|
|
2159
|
+
expr_node.object,
|
|
2160
|
+
ta_range,
|
|
2161
|
+
ta_results,
|
|
2162
|
+
resolving,
|
|
2163
|
+
security_mutable_names,
|
|
2164
|
+
helper_binding_stack,
|
|
2165
|
+
emitted_lines,
|
|
2166
|
+
)
|
|
2167
|
+
return (
|
|
2168
|
+
f"([&]() -> {cpp_t} {{ "
|
|
2169
|
+
f"{cpp_t} _hv = ({inner}); "
|
|
2170
|
+
f"int _hidx = (int)({index_cpp}); "
|
|
2171
|
+
f"{cpp_t} _out = (_hidx <= 0) ? _hv : {hist}[_hidx - 1]; "
|
|
2172
|
+
f"if (is_complete) {hist}.push(_hv); "
|
|
2173
|
+
f"return _out; }}())"
|
|
2174
|
+
)
|
|
1737
2175
|
ta_site = self._get_ta_site(expr_node.object)
|
|
1738
2176
|
if ta_site is not None:
|
|
1739
2177
|
# ``ta.<fn>(...)[k]`` inside request.security(): the inner TA call
|
|
@@ -1748,7 +2186,9 @@ class SecurityEmitter:
|
|
|
1748
2186
|
# produced the chart-tf TA instead of the confirmed HTF value.
|
|
1749
2187
|
idx = self._ta_index_by_site_id.get(id(ta_site))
|
|
1750
2188
|
sig = self._security_binding_stack_signature(helper_binding_stack)
|
|
1751
|
-
idx_lit = self.
|
|
2189
|
+
idx_lit = self._resolve_security_index_literal(
|
|
2190
|
+
expr_node.index, helper_binding_stack
|
|
2191
|
+
)
|
|
1752
2192
|
if idx_lit is None:
|
|
1753
2193
|
self._codegen_error(
|
|
1754
2194
|
expr_node,
|
|
@@ -1850,6 +2290,37 @@ class SecurityEmitter:
|
|
|
1850
2290
|
resolving.remove(call_key)
|
|
1851
2291
|
return resolved
|
|
1852
2292
|
|
|
2293
|
+
if (
|
|
2294
|
+
isinstance(expr_node, FuncCall)
|
|
2295
|
+
and isinstance(expr_node.callee, MemberAccess)
|
|
2296
|
+
and isinstance(expr_node.callee.object, Identifier)
|
|
2297
|
+
and expr_node.callee.object.name == "math"
|
|
2298
|
+
):
|
|
2299
|
+
# A rolling/stateful math reducer (e.g. math.sum -> math::Sum) is
|
|
2300
|
+
# precomputed into a committed _secval_* by the security TA
|
|
2301
|
+
# machinery, exactly like a ta.* call. Return that committed value
|
|
2302
|
+
# instead of falling through to _build_security_math_call, whose
|
|
2303
|
+
# inline lowering only covers scalar math and emits a broken
|
|
2304
|
+
# "unsupported: math.<f>" 0.0 for a reducer. Scalar math
|
|
2305
|
+
# (abs/round/min/max/...) is not a TA site, so this is a no-op.
|
|
2306
|
+
math_site = self._get_ta_site(expr_node)
|
|
2307
|
+
if math_site is not None:
|
|
2308
|
+
math_idx = self._ta_index_by_site_id.get(id(math_site))
|
|
2309
|
+
math_sig = self._security_binding_stack_signature(helper_binding_stack)
|
|
2310
|
+
if math_idx is not None and (math_idx, math_sig) in ta_results:
|
|
2311
|
+
return ta_results[(math_idx, math_sig)]
|
|
2312
|
+
return self._build_security_math_call(
|
|
2313
|
+
sec_id,
|
|
2314
|
+
expr_node.callee.member,
|
|
2315
|
+
expr_node,
|
|
2316
|
+
ta_range,
|
|
2317
|
+
ta_results,
|
|
2318
|
+
resolving,
|
|
2319
|
+
security_mutable_names,
|
|
2320
|
+
helper_binding_stack,
|
|
2321
|
+
emitted_lines,
|
|
2322
|
+
)
|
|
2323
|
+
|
|
1853
2324
|
site = self._get_ta_site(expr_node)
|
|
1854
2325
|
if site:
|
|
1855
2326
|
idx = self._ta_index_by_site_id.get(id(site))
|