@pineforge/codegen-pyodide 0.8.0 → 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 +797 -45
- package/pineforge_codegen/analyzer/call_handlers.py +368 -68
- package/pineforge_codegen/analyzer/contracts.py +91 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +1613 -111
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +536 -45
- package/pineforge_codegen/codegen/input.py +31 -0
- package/pineforge_codegen/codegen/security.py +791 -39
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +115 -10
- package/pineforge_codegen/codegen/types.py +478 -46
- package/pineforge_codegen/codegen/visit_call.py +331 -75
- package/pineforge_codegen/codegen/visit_expr.py +116 -14
- package/pineforge_codegen/codegen/visit_stmt.py +208 -23
- package/pineforge_codegen/parser.py +129 -29
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +320 -19
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -10
- package/pineforge_codegen-0.8.0.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, Identifier, IfStmt,
|
|
64
|
-
|
|
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:
|
|
@@ -77,6 +82,142 @@ class SecurityEmitter:
|
|
|
77
82
|
|
|
78
83
|
Mixed into ``CodeGen``; not intended to be instantiated standalone."""
|
|
79
84
|
|
|
85
|
+
def _resolve_security_tf(self, tf_node, containing_func: str):
|
|
86
|
+
"""Resolve a ``request.security`` timeframe argument to ``(tf_str, tf_expr)``.
|
|
87
|
+
|
|
88
|
+
``tf_str`` is a compile-time string literal value; ``tf_expr`` is a runtime
|
|
89
|
+
C++ expression used at evaluator-registration time. Exactly one is non-None
|
|
90
|
+
for a usable tf (both None is acceptable only as an explicit "unknown").
|
|
91
|
+
|
|
92
|
+
A function-parameter tf (e.g. ``f(tf) => request.security(sym, tf, ...)``)
|
|
93
|
+
is not visible at class scope (the evaluator is a class method), so it is
|
|
94
|
+
resolved from the function's call sites. A dead-code UDF (never called)
|
|
95
|
+
falls back to the chart timeframe — its evaluator result is never read.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(tf_node, StringLiteral):
|
|
98
|
+
return tf_node.value, None
|
|
99
|
+
if isinstance(tf_node, Identifier):
|
|
100
|
+
name = tf_node.name
|
|
101
|
+
if name in self._timeframe_period_vars:
|
|
102
|
+
return None, "script_tf_"
|
|
103
|
+
if (name in self._known_vars and name not in self._input_backed_vars
|
|
104
|
+
and isinstance(self._known_vars[name], str)):
|
|
105
|
+
return self._known_vars[name], None
|
|
106
|
+
if name in self._input_backed_vars and name in self._input_var_to_call:
|
|
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
|
|
115
|
+
# class-scope resolvable (global / input member)?
|
|
116
|
+
if self._ident_is_resolvable(name):
|
|
117
|
+
try:
|
|
118
|
+
return None, self._visit_expr(tf_node)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
# function-parameter tf -> resolve from the call sites
|
|
122
|
+
if containing_func:
|
|
123
|
+
resolved = self._resolve_param_tf_from_callsites(containing_func, name)
|
|
124
|
+
if resolved is not None:
|
|
125
|
+
return resolved
|
|
126
|
+
# graceful fallback so transpile does not hard-fail
|
|
127
|
+
return None, "input_tf_"
|
|
128
|
+
# any other expression — visit if it resolves at class scope
|
|
129
|
+
try:
|
|
130
|
+
expanded = self._security_tf_runtime_expr(tf_node)
|
|
131
|
+
return None, expanded if expanded is not None else self._visit_expr(tf_node)
|
|
132
|
+
except Exception:
|
|
133
|
+
return None, "input_tf_"
|
|
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
|
+
|
|
180
|
+
def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
|
|
181
|
+
"""For a ``request.security`` whose tf is function parameter ``param_name``
|
|
182
|
+
of user function ``func_name``, return ``(tf_str, tf_expr)`` resolved from
|
|
183
|
+
the call sites, or None. If every call passes the same literal/member tf,
|
|
184
|
+
that tf is used; mixed timeframes or a never-called (dead-code) function
|
|
185
|
+
fall back to the chart timeframe (``input_tf_``)."""
|
|
186
|
+
fdef = None
|
|
187
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
188
|
+
if isinstance(node, FuncDef) and node.name == func_name:
|
|
189
|
+
fdef = node
|
|
190
|
+
break
|
|
191
|
+
if fdef is None or param_name not in fdef.params:
|
|
192
|
+
return None
|
|
193
|
+
pidx = fdef.params.index(param_name)
|
|
194
|
+
resolved: list = []
|
|
195
|
+
found_call = False
|
|
196
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
197
|
+
if (isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
|
|
198
|
+
and node.callee.name == func_name):
|
|
199
|
+
found_call = True
|
|
200
|
+
arg = node.args[pidx] if pidx < len(node.args) else None
|
|
201
|
+
if arg is None:
|
|
202
|
+
continue
|
|
203
|
+
# Resolve the call-site arg (no further containing func — these
|
|
204
|
+
# are global-scope / input args).
|
|
205
|
+
resolved.append(self._resolve_security_tf(arg, ""))
|
|
206
|
+
if not found_call:
|
|
207
|
+
# dead code — evaluator never read; register with chart tf.
|
|
208
|
+
return (None, "input_tf_")
|
|
209
|
+
valid = [r for r in resolved if r is not None]
|
|
210
|
+
if not valid:
|
|
211
|
+
return (None, "input_tf_")
|
|
212
|
+
strs = {r[0] for r in valid}
|
|
213
|
+
exprs = {r[1] for r in valid}
|
|
214
|
+
if len(strs) == 1 and next(iter(strs), None) is not None:
|
|
215
|
+
return (next(iter(strs)), None)
|
|
216
|
+
if len(exprs) == 1 and next(iter(exprs), None) is not None:
|
|
217
|
+
return (None, next(iter(exprs)))
|
|
218
|
+
# mixed timeframes across call sites — cannot pick one statically
|
|
219
|
+
return (None, "input_tf_")
|
|
220
|
+
|
|
80
221
|
def _normalize_security_call(self, item) -> dict:
|
|
81
222
|
if hasattr(item, "sec_id"):
|
|
82
223
|
return {
|
|
@@ -88,9 +229,12 @@ class SecurityEmitter:
|
|
|
88
229
|
"gaps_node": item.gaps,
|
|
89
230
|
"lookahead_node": item.lookahead,
|
|
90
231
|
"ta_range": item.ta_range,
|
|
232
|
+
"heikinashi": bool(getattr(item, "heikinashi", False)),
|
|
91
233
|
"depends_on_mutable_globals": bool(getattr(item, "depends_on_mutable_globals", False)),
|
|
92
234
|
"mutable_globals": list(getattr(item, "mutable_globals", ()) or ()),
|
|
93
235
|
"is_lower_tf_array": bool(getattr(item, "is_lower_tf_array", False)),
|
|
236
|
+
"containing_func": getattr(item, "containing_func", "") or "",
|
|
237
|
+
"callsite_idx": getattr(item, "callsite_idx", None),
|
|
94
238
|
}
|
|
95
239
|
return {
|
|
96
240
|
"sec_id": item[0],
|
|
@@ -104,6 +248,8 @@ class SecurityEmitter:
|
|
|
104
248
|
"depends_on_mutable_globals": False,
|
|
105
249
|
"mutable_globals": [],
|
|
106
250
|
"is_lower_tf_array": False,
|
|
251
|
+
"containing_func": "",
|
|
252
|
+
"callsite_idx": None,
|
|
107
253
|
}
|
|
108
254
|
|
|
109
255
|
def _security_state_name(self, sec_id: int, name: str) -> str:
|
|
@@ -188,7 +334,7 @@ class SecurityEmitter:
|
|
|
188
334
|
return None
|
|
189
335
|
|
|
190
336
|
def _literal_int_for_security_index(self, node) -> int | None:
|
|
191
|
-
"""Integer index for
|
|
337
|
+
"""Integer index for bar-field[n] inside request.security (must be literal)."""
|
|
192
338
|
if isinstance(node, NumberLiteral):
|
|
193
339
|
v = node.value
|
|
194
340
|
if isinstance(v, bool):
|
|
@@ -207,19 +353,131 @@ class SecurityEmitter:
|
|
|
207
353
|
return None
|
|
208
354
|
return None
|
|
209
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
|
+
|
|
210
466
|
def _collect_security_ohlc_hist_fields(self, node) -> set[str]:
|
|
211
|
-
"""Which
|
|
467
|
+
"""Which security bar fields need HTF history (subscript index >= 1)."""
|
|
212
468
|
out: set[str] = set()
|
|
213
469
|
|
|
214
470
|
def walk(n):
|
|
215
471
|
if n is None:
|
|
216
472
|
return
|
|
217
473
|
if isinstance(n, Subscript) and isinstance(n.object, Identifier):
|
|
218
|
-
if n.object.name in
|
|
474
|
+
if n.object.name in SECURITY_BAR_FIELDS:
|
|
219
475
|
idx = self._literal_int_for_security_index(n.index)
|
|
220
|
-
# high[0] uses current HTF `bar`;
|
|
221
|
-
# bars from Series history (filled before push
|
|
222
|
-
|
|
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:
|
|
223
481
|
out.add(n.object.name)
|
|
224
482
|
if isinstance(n, (list, tuple)):
|
|
225
483
|
for x in n:
|
|
@@ -236,9 +494,267 @@ class SecurityEmitter:
|
|
|
236
494
|
walk(node)
|
|
237
495
|
return out
|
|
238
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
|
+
|
|
239
509
|
def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
|
|
240
510
|
return f"_sec{sec_id}_hist_{field}"
|
|
241
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
|
+
|
|
523
|
+
def _collect_security_ta_hist_indices(self, node) -> set[int]:
|
|
524
|
+
"""Which security TA call-site indices need HTF history (subscript index >= 1).
|
|
525
|
+
|
|
526
|
+
``request.security(..., ta.ema(close, 55)[1], ...)`` reads a *confirmed*
|
|
527
|
+
HTF TA value at a past-bar offset. The inner TA call runs in the security
|
|
528
|
+
(HTF) context and commits one value per COMPLETED HTF bar; offsets read a
|
|
529
|
+
per-site ``Series`` filled (gated on ``is_complete``) in
|
|
530
|
+
``_eval_security_N``. Mirrors ``_collect_security_ohlc_hist_fields`` for
|
|
531
|
+
OHLC offsets. Offset 0 reuses the current committed value (``_secval_*``)
|
|
532
|
+
and needs no Series, so only index >= 1 registers here."""
|
|
533
|
+
out: set[int] = set()
|
|
534
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
535
|
+
|
|
536
|
+
def resolve_ta_site(obj, resolving: set[str] | None = None):
|
|
537
|
+
"""_get_ta_site only matches the literal ta.* FuncCall node by
|
|
538
|
+
identity; fall back through global_expr_map for an indirect
|
|
539
|
+
binding (``v = ta.ema(close, 55)`` then ``...v[1]...``), mirroring
|
|
540
|
+
the same fallback in _build_security_expr's Subscript branch."""
|
|
541
|
+
site = self._get_ta_site(obj)
|
|
542
|
+
if site is not None:
|
|
543
|
+
return site
|
|
544
|
+
if isinstance(obj, Identifier):
|
|
545
|
+
if resolving is None:
|
|
546
|
+
resolving = set()
|
|
547
|
+
if obj.name in global_expr_map and obj.name not in resolving:
|
|
548
|
+
resolving.add(obj.name)
|
|
549
|
+
return resolve_ta_site(global_expr_map[obj.name], resolving)
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
def walk(n):
|
|
553
|
+
if n is None:
|
|
554
|
+
return
|
|
555
|
+
if isinstance(n, Subscript):
|
|
556
|
+
site = resolve_ta_site(n.object)
|
|
557
|
+
if site is not None:
|
|
558
|
+
idx_lit = self._resolve_security_index_literal(n.index)
|
|
559
|
+
if idx_lit is not None and idx_lit >= 1:
|
|
560
|
+
site_idx = self._ta_index_by_site_id.get(id(site))
|
|
561
|
+
if site_idx is not None:
|
|
562
|
+
out.add(site_idx)
|
|
563
|
+
if isinstance(n, (list, tuple)):
|
|
564
|
+
for x in n:
|
|
565
|
+
walk(x)
|
|
566
|
+
return
|
|
567
|
+
for _k, v in getattr(n, "__dict__", {}).items():
|
|
568
|
+
if isinstance(v, ASTNode):
|
|
569
|
+
walk(v)
|
|
570
|
+
elif isinstance(v, (list, tuple)):
|
|
571
|
+
for x in v:
|
|
572
|
+
if isinstance(x, ASTNode):
|
|
573
|
+
walk(x)
|
|
574
|
+
|
|
575
|
+
walk(node)
|
|
576
|
+
return out
|
|
577
|
+
|
|
578
|
+
def _security_ta_hist_series_cpp(self, member_name: str) -> str:
|
|
579
|
+
"""Per-(sec, site) ``Series<double>`` backing ``ta.<fn>(...)[k>=1]`` HTF history."""
|
|
580
|
+
return f"{member_name}_hist"
|
|
581
|
+
|
|
582
|
+
def _security_ta_hist_series_names(self, sec_id: int) -> list[str]:
|
|
583
|
+
"""Hist Series names for every security TA site (and variant) read at an
|
|
584
|
+
offset >= 1 in sec ``sec_id``."""
|
|
585
|
+
info = self._security_eval_info[sec_id]
|
|
586
|
+
names: list[str] = []
|
|
587
|
+
for idx in sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ())):
|
|
588
|
+
for variant in (info.get("ta_variants") or {}).get(idx, []):
|
|
589
|
+
names.append(self._security_ta_hist_series_cpp(variant["member_name"]))
|
|
590
|
+
return names
|
|
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
|
+
|
|
242
758
|
@staticmethod
|
|
243
759
|
def _security_series_binding(series_name: str) -> str:
|
|
244
760
|
return f"@series:{series_name}"
|
|
@@ -767,6 +1283,16 @@ class SecurityEmitter:
|
|
|
767
1283
|
if bound is not None:
|
|
768
1284
|
if isinstance(bound, str):
|
|
769
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)
|
|
770
1296
|
self._collect_security_ta_binding_stacks(
|
|
771
1297
|
bound,
|
|
772
1298
|
resolving,
|
|
@@ -775,6 +1301,7 @@ class SecurityEmitter:
|
|
|
775
1301
|
inline_ta_indices,
|
|
776
1302
|
inline_helper,
|
|
777
1303
|
)
|
|
1304
|
+
resolving.discard(bind_key)
|
|
778
1305
|
return collected
|
|
779
1306
|
|
|
780
1307
|
mutable_info = self._global_mutable_infos.get(expr_node.name)
|
|
@@ -1160,6 +1687,59 @@ class SecurityEmitter:
|
|
|
1160
1687
|
)
|
|
1161
1688
|
return out
|
|
1162
1689
|
|
|
1690
|
+
def _emit_security_ohlc_hist_pushes(self, sec_id: int, lines: list[str]) -> None:
|
|
1691
|
+
"""Emit the OHLC history-offset Series pushes for ``sec_id``, gated on
|
|
1692
|
+
``is_complete``.
|
|
1693
|
+
|
|
1694
|
+
``request.security(..., [high[1], low[1], ...], ...)`` reads HTF OHLC at
|
|
1695
|
+
past-bar offsets. Each offset is backed by a per-field Series whose
|
|
1696
|
+
history must advance once per COMPLETED HTF bar — not once per (partial)
|
|
1697
|
+
chart-bar evaluation. ``_eval_security_N`` fires on every chart bar; only
|
|
1698
|
+
the bar that completes the HTF aggregate has ``is_complete == true``.
|
|
1699
|
+
Pushing unconditionally advanced the offset history every chart bar, so
|
|
1700
|
+
``high[1]`` resolved to a recent partial bar instead of the prior
|
|
1701
|
+
completed HTF bar. Gate all pushes for this sec in one combined block."""
|
|
1702
|
+
fields = sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ()))
|
|
1703
|
+
if not fields:
|
|
1704
|
+
return
|
|
1705
|
+
lines.append(" if (is_complete) {")
|
|
1706
|
+
for field in fields:
|
|
1707
|
+
lines.append(
|
|
1708
|
+
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push({self._security_bar_field_expr(field)});"
|
|
1709
|
+
)
|
|
1710
|
+
lines.append(" }")
|
|
1711
|
+
|
|
1712
|
+
def _emit_security_ta_hist_pushes(
|
|
1713
|
+
self, sec_id: int, info: dict, ta_results: dict, lines: list[str]
|
|
1714
|
+
) -> None:
|
|
1715
|
+
"""Emit the TA history-offset Series pushes for ``sec_id``, gated on
|
|
1716
|
+
``is_complete`` (mirrors ``_emit_security_ohlc_hist_pushes``).
|
|
1717
|
+
|
|
1718
|
+
``request.security(..., ta.ema(close, 55)[1], ...)`` reads a confirmed
|
|
1719
|
+
HTF TA value at a past-bar offset. The committed value (``_secval_*``,
|
|
1720
|
+
produced with ``.compute()`` only when ``is_complete``) is pushed onto a
|
|
1721
|
+
per-site Series once per COMPLETED HTF bar, AFTER the expression
|
|
1722
|
+
assignment so the offset read sees the prior completed bar. Pushing on
|
|
1723
|
+
every chart-bar eval would otherwise advance the offset history per
|
|
1724
|
+
partial eval / chart tick (the bug this replaces, where the chart-context
|
|
1725
|
+
``_hist_call`` buffer advanced on ``is_first_tick_``)."""
|
|
1726
|
+
indices = sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ()))
|
|
1727
|
+
if not indices:
|
|
1728
|
+
return
|
|
1729
|
+
pushes: list[str] = []
|
|
1730
|
+
for idx in indices:
|
|
1731
|
+
for variant in (info.get("ta_variants") or {}).get(idx, []):
|
|
1732
|
+
result_name = ta_results.get((idx, variant["signature"]))
|
|
1733
|
+
if result_name is None:
|
|
1734
|
+
continue
|
|
1735
|
+
hist = self._security_ta_hist_series_cpp(variant["member_name"])
|
|
1736
|
+
pushes.append(f" {hist}.push({result_name});")
|
|
1737
|
+
if not pushes:
|
|
1738
|
+
return
|
|
1739
|
+
lines.append(" if (is_complete) {")
|
|
1740
|
+
lines.extend(pushes)
|
|
1741
|
+
lines.append(" }")
|
|
1742
|
+
|
|
1163
1743
|
def _emit_security_evaluators(self, lines: list[str]) -> None:
|
|
1164
1744
|
"""Emit _eval_security_N() methods and evaluate_security() dispatch."""
|
|
1165
1745
|
if not self._security_calls:
|
|
@@ -1223,7 +1803,7 @@ class SecurityEmitter:
|
|
|
1223
1803
|
)
|
|
1224
1804
|
var_name = variant["result_name"]
|
|
1225
1805
|
sec_name = variant["member_name"]
|
|
1226
|
-
lines.append(f" auto {var_name} =
|
|
1806
|
+
lines.append(f" auto {var_name} = security_series_slot_is_new({sec_id}) "
|
|
1227
1807
|
f"? {sec_name}.compute({compute_args}) "
|
|
1228
1808
|
f": {sec_name}.recompute({compute_args});")
|
|
1229
1809
|
ta_results[(idx, variant["signature"])] = var_name
|
|
@@ -1256,10 +1836,8 @@ class SecurityEmitter:
|
|
|
1256
1836
|
emitted_lines=lines,
|
|
1257
1837
|
)
|
|
1258
1838
|
lines.append(f" _req_sec_{sec_id}_{i} = {el_cpp};")
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
|
|
1262
|
-
)
|
|
1839
|
+
self._emit_security_ohlc_hist_pushes(sec_id, lines)
|
|
1840
|
+
self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
|
|
1263
1841
|
lines.append(" }")
|
|
1264
1842
|
lines.append("")
|
|
1265
1843
|
continue
|
|
@@ -1287,10 +1865,8 @@ class SecurityEmitter:
|
|
|
1287
1865
|
)
|
|
1288
1866
|
else:
|
|
1289
1867
|
lines.append(f" _req_sec_{sec_id} = {expr_cpp};")
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
|
|
1293
|
-
)
|
|
1868
|
+
self._emit_security_ohlc_hist_pushes(sec_id, lines)
|
|
1869
|
+
self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
|
|
1294
1870
|
lines.append(" }")
|
|
1295
1871
|
lines.append("")
|
|
1296
1872
|
|
|
@@ -1327,6 +1903,10 @@ class SecurityEmitter:
|
|
|
1327
1903
|
lines.append(
|
|
1328
1904
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1329
1905
|
)
|
|
1906
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1907
|
+
lines.append(f" {name}.clear();")
|
|
1908
|
+
for name in self._security_expr_hist_series_names(sec_id):
|
|
1909
|
+
lines.append(f" {name}.clear();")
|
|
1330
1910
|
lines.append(" break;")
|
|
1331
1911
|
continue
|
|
1332
1912
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
@@ -1349,16 +1929,51 @@ class SecurityEmitter:
|
|
|
1349
1929
|
lines.append(
|
|
1350
1930
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1351
1931
|
)
|
|
1932
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
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();")
|
|
1352
1961
|
lines.append(" break;")
|
|
1353
1962
|
else:
|
|
1354
1963
|
hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
1355
|
-
|
|
1964
|
+
ta_hist_names = self._security_ta_hist_series_names(sec_id)
|
|
1965
|
+
expr_hist_names = self._security_expr_hist_series_names(sec_id)
|
|
1966
|
+
if hist or ta_hist_names or expr_hist_names:
|
|
1356
1967
|
lines.append(f" case {sec_id}:")
|
|
1357
1968
|
lines.append(f" _req_sec_{sec_id} = na<double>();")
|
|
1358
1969
|
for field in sorted(hist):
|
|
1359
1970
|
lines.append(
|
|
1360
1971
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1361
1972
|
)
|
|
1973
|
+
for name in ta_hist_names:
|
|
1974
|
+
lines.append(f" {name}.clear();")
|
|
1975
|
+
for name in expr_hist_names:
|
|
1976
|
+
lines.append(f" {name}.clear();")
|
|
1362
1977
|
lines.append(" break;")
|
|
1363
1978
|
else:
|
|
1364
1979
|
lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
|
|
@@ -1406,9 +2021,7 @@ class SecurityEmitter:
|
|
|
1406
2021
|
emitted_lines,
|
|
1407
2022
|
)
|
|
1408
2023
|
bar_fields = {
|
|
1409
|
-
|
|
1410
|
-
"low": "bar.low", "open": "bar.open",
|
|
1411
|
-
"volume": "bar.volume",
|
|
2024
|
+
**SECURITY_BAR_FIELD_EXPRS,
|
|
1412
2025
|
"hl2": "((bar.high + bar.low) / 2.0)",
|
|
1413
2026
|
"hlc3": "((bar.high + bar.low + bar.close) / 3.0)",
|
|
1414
2027
|
"ohlc4": "((bar.open + bar.high + bar.low + bar.close) / 4.0)",
|
|
@@ -1439,6 +2052,15 @@ class SecurityEmitter:
|
|
|
1439
2052
|
resolving.remove(expr_node.name)
|
|
1440
2053
|
return resolved
|
|
1441
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
|
+
|
|
1442
2064
|
if isinstance(expr_node, Subscript):
|
|
1443
2065
|
index_cpp = self._build_security_expr(
|
|
1444
2066
|
sec_id,
|
|
@@ -1469,30 +2091,129 @@ class SecurityEmitter:
|
|
|
1469
2091
|
emitted_lines,
|
|
1470
2092
|
)
|
|
1471
2093
|
return f"{obj_cpp}[{index_cpp}]"
|
|
1472
|
-
if expr_node.object.name in
|
|
2094
|
+
if expr_node.object.name in SECURITY_BAR_FIELDS:
|
|
2095
|
+
field = expr_node.object.name
|
|
1473
2096
|
idx_lit = self._literal_int_for_security_index(expr_node.index)
|
|
1474
2097
|
if idx_lit is not None:
|
|
1475
|
-
bar_map = {
|
|
1476
|
-
"open": "bar.open",
|
|
1477
|
-
"high": "bar.high",
|
|
1478
|
-
"low": "bar.low",
|
|
1479
|
-
"close": "bar.close",
|
|
1480
|
-
"volume": "bar.volume",
|
|
1481
|
-
}
|
|
1482
2098
|
if idx_lit == 0:
|
|
1483
|
-
return
|
|
2099
|
+
return self._security_bar_field_expr(field)
|
|
1484
2100
|
if idx_lit >= 1:
|
|
1485
2101
|
# lookahead_off: we evaluate when an HTF bar completes; `bar` is that
|
|
1486
|
-
# bar. On the HTF series, high[0]/
|
|
1487
|
-
# bar; high[1] is one HTF bar back
|
|
1488
|
-
#
|
|
1489
|
-
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].
|
|
1490
2106
|
hist = self._security_ohlc_hist_series_cpp(sec_id, field)
|
|
1491
2107
|
return f"{hist}[{idx_lit - 1}]"
|
|
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"}}())"
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
# Indirect TA binding: ``v = ta.ema(close, 55)`` then
|
|
2124
|
+
# ``request.security(..., v[1], ...)``. _get_ta_site below only
|
|
2125
|
+
# matches the literal ta.* FuncCall node by identity, so a bare
|
|
2126
|
+
# Identifier subscript target silently misses it and falls
|
|
2127
|
+
# through to a chart-resolution read of the wrong (non-HTF)
|
|
2128
|
+
# series. Resolve through the same global_expr_map the
|
|
2129
|
+
# non-subscript Identifier path above already uses, and
|
|
2130
|
+
# recurse on a synthetic Subscript over the resolved value so
|
|
2131
|
+
# it re-enters this whole branch (TA site, OHLC field, or
|
|
2132
|
+
# helper binding, whichever the resolved expression turns out
|
|
2133
|
+
# to be) instead of duplicating that dispatch here.
|
|
2134
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
2135
|
+
if (expr_node.object.name in global_expr_map
|
|
2136
|
+
and expr_node.object.name not in resolving):
|
|
2137
|
+
resolving.add(expr_node.object.name)
|
|
2138
|
+
resolved = self._build_security_expr(
|
|
2139
|
+
sec_id,
|
|
2140
|
+
Subscript(object=global_expr_map[expr_node.object.name], index=expr_node.index),
|
|
2141
|
+
ta_range,
|
|
2142
|
+
ta_results,
|
|
2143
|
+
resolving,
|
|
2144
|
+
security_mutable_names,
|
|
2145
|
+
helper_binding_stack,
|
|
2146
|
+
emitted_lines,
|
|
2147
|
+
)
|
|
2148
|
+
resolving.remove(expr_node.object.name)
|
|
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
|
+
)
|
|
2175
|
+
ta_site = self._get_ta_site(expr_node.object)
|
|
2176
|
+
if ta_site is not None:
|
|
2177
|
+
# ``ta.<fn>(...)[k]`` inside request.security(): the inner TA call
|
|
2178
|
+
# runs in the HTF (security) context and commits one value per
|
|
2179
|
+
# COMPLETED HTF bar. Read the already-emitted security TA result —
|
|
2180
|
+
# offset 0 reuses the current committed value (``_secval_*``),
|
|
2181
|
+
# offset k>=1 reads a per-site Series that advances on
|
|
2182
|
+
# ``is_complete`` (HTF-bar boundary) in ``_eval_security_N``. The
|
|
2183
|
+
# buggy generic path re-lowered the inner TA to the CHART member
|
|
2184
|
+
# and gated a ``_hist_call`` buffer on ``is_first_tick_`` (chart
|
|
2185
|
+
# tick), so without a magnifier it advanced every chart bar and
|
|
2186
|
+
# produced the chart-tf TA instead of the confirmed HTF value.
|
|
2187
|
+
idx = self._ta_index_by_site_id.get(id(ta_site))
|
|
2188
|
+
sig = self._security_binding_stack_signature(helper_binding_stack)
|
|
2189
|
+
idx_lit = self._resolve_security_index_literal(
|
|
2190
|
+
expr_node.index, helper_binding_stack
|
|
2191
|
+
)
|
|
2192
|
+
if idx_lit is None:
|
|
1492
2193
|
self._codegen_error(
|
|
1493
2194
|
expr_node,
|
|
1494
|
-
"request.security()
|
|
2195
|
+
"request.security() TA history index must be a literal integer (e.g. ta.ema(close, 55)[1])",
|
|
2196
|
+
)
|
|
2197
|
+
if idx_lit == 0:
|
|
2198
|
+
# Current completed-HTF-bar value: reuse the bare-TA emission.
|
|
2199
|
+
return self._build_security_expr(
|
|
2200
|
+
sec_id,
|
|
2201
|
+
expr_node.object,
|
|
2202
|
+
ta_range,
|
|
2203
|
+
ta_results,
|
|
2204
|
+
resolving,
|
|
2205
|
+
security_mutable_names,
|
|
2206
|
+
helper_binding_stack,
|
|
2207
|
+
emitted_lines,
|
|
1495
2208
|
)
|
|
2209
|
+
member_name = self._security_ta_variant_names.get(
|
|
2210
|
+
(sec_id, idx, sig),
|
|
2211
|
+
f"_sec{sec_id}_{ta_site.member_name}",
|
|
2212
|
+
)
|
|
2213
|
+
hist = self._security_ta_hist_series_cpp(member_name)
|
|
2214
|
+
# ta(...)[k] -> hist[k-1]: hist[0] is the prior completed HTF bar
|
|
2215
|
+
# (current value not yet pushed — push happens after this assign).
|
|
2216
|
+
return f"{hist}[{idx_lit - 1}]"
|
|
1496
2217
|
|
|
1497
2218
|
if isinstance(expr_node, BinOp):
|
|
1498
2219
|
left = self._build_security_expr(
|
|
@@ -1569,6 +2290,37 @@ class SecurityEmitter:
|
|
|
1569
2290
|
resolving.remove(call_key)
|
|
1570
2291
|
return resolved
|
|
1571
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
|
+
|
|
1572
2324
|
site = self._get_ta_site(expr_node)
|
|
1573
2325
|
if site:
|
|
1574
2326
|
idx = self._ta_index_by_site_id.get(id(site))
|
|
@@ -1589,7 +2341,7 @@ class SecurityEmitter:
|
|
|
1589
2341
|
helper_binding_stack,
|
|
1590
2342
|
emitted_lines,
|
|
1591
2343
|
)
|
|
1592
|
-
return f"(
|
|
2344
|
+
return f"(security_series_slot_is_new({sec_id}) ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
|
|
1593
2345
|
|
|
1594
2346
|
result = self._visit_expr(expr_node)
|
|
1595
2347
|
return self._rewrite_security_cpp(result, sec_id, security_mutable_names, helper_binding_stack)
|