@pineforge/codegen-pyodide 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +453 -15
- package/pineforge_codegen/analyzer/call_handlers.py +200 -11
- package/pineforge_codegen/analyzer/contracts.py +62 -0
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +509 -44
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +286 -27
- package/pineforge_codegen/codegen/input.py +14 -0
- package/pineforge_codegen/codegen/security.py +294 -13
- package/pineforge_codegen/codegen/tables.py +59 -3
- package/pineforge_codegen/codegen/types.py +369 -19
- package/pineforge_codegen/codegen/visit_call.py +147 -22
- package/pineforge_codegen/codegen/visit_expr.py +54 -0
- package/pineforge_codegen/codegen/visit_stmt.py +161 -14
- package/pineforge_codegen/parser.py +56 -23
- package/pineforge_codegen/support_checker.py +281 -17
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +2 -8
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -60,8 +60,8 @@ from __future__ import annotations
|
|
|
60
60
|
|
|
61
61
|
from ..ast_nodes import (
|
|
62
62
|
ASTNode, Assignment, BinOp, BreakStmt, ContinueStmt, ExprStmt, ForStmt,
|
|
63
|
-
ForInStmt, FuncCall, Identifier, IfStmt, NumberLiteral,
|
|
64
|
-
SwitchStmt, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
|
|
63
|
+
ForInStmt, FuncCall, FuncDef, Identifier, IfStmt, NumberLiteral, StringLiteral,
|
|
64
|
+
Subscript, SwitchStmt, Ternary, TupleAssign, TupleLiteral, UnaryOp, VarDecl,
|
|
65
65
|
WhileStmt,
|
|
66
66
|
)
|
|
67
67
|
from ..analyzer import (
|
|
@@ -77,6 +77,89 @@ class SecurityEmitter:
|
|
|
77
77
|
|
|
78
78
|
Mixed into ``CodeGen``; not intended to be instantiated standalone."""
|
|
79
79
|
|
|
80
|
+
def _resolve_security_tf(self, tf_node, containing_func: str):
|
|
81
|
+
"""Resolve a ``request.security`` timeframe argument to ``(tf_str, tf_expr)``.
|
|
82
|
+
|
|
83
|
+
``tf_str`` is a compile-time string literal value; ``tf_expr`` is a runtime
|
|
84
|
+
C++ expression used at evaluator-registration time. Exactly one is non-None
|
|
85
|
+
for a usable tf (both None is acceptable only as an explicit "unknown").
|
|
86
|
+
|
|
87
|
+
A function-parameter tf (e.g. ``f(tf) => request.security(sym, tf, ...)``)
|
|
88
|
+
is not visible at class scope (the evaluator is a class method), so it is
|
|
89
|
+
resolved from the function's call sites. A dead-code UDF (never called)
|
|
90
|
+
falls back to the chart timeframe — its evaluator result is never read.
|
|
91
|
+
"""
|
|
92
|
+
if isinstance(tf_node, StringLiteral):
|
|
93
|
+
return tf_node.value, None
|
|
94
|
+
if isinstance(tf_node, Identifier):
|
|
95
|
+
name = tf_node.name
|
|
96
|
+
if name in self._timeframe_period_vars:
|
|
97
|
+
return None, "script_tf_"
|
|
98
|
+
if (name in self._known_vars and name not in self._input_backed_vars
|
|
99
|
+
and isinstance(self._known_vars[name], str)):
|
|
100
|
+
return self._known_vars[name], None
|
|
101
|
+
if name in self._input_backed_vars and name in self._input_var_to_call:
|
|
102
|
+
return None, self._visit_expr(self._input_var_to_call[name])
|
|
103
|
+
# class-scope resolvable (global / input member)?
|
|
104
|
+
if self._ident_is_resolvable(name):
|
|
105
|
+
try:
|
|
106
|
+
return None, self._visit_expr(tf_node)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
# function-parameter tf -> resolve from the call sites
|
|
110
|
+
if containing_func:
|
|
111
|
+
resolved = self._resolve_param_tf_from_callsites(containing_func, name)
|
|
112
|
+
if resolved is not None:
|
|
113
|
+
return resolved
|
|
114
|
+
# graceful fallback so transpile does not hard-fail
|
|
115
|
+
return None, "input_tf_"
|
|
116
|
+
# any other expression — visit if it resolves at class scope
|
|
117
|
+
try:
|
|
118
|
+
return None, self._visit_expr(tf_node)
|
|
119
|
+
except Exception:
|
|
120
|
+
return None, "input_tf_"
|
|
121
|
+
|
|
122
|
+
def _resolve_param_tf_from_callsites(self, func_name: str, param_name: str):
|
|
123
|
+
"""For a ``request.security`` whose tf is function parameter ``param_name``
|
|
124
|
+
of user function ``func_name``, return ``(tf_str, tf_expr)`` resolved from
|
|
125
|
+
the call sites, or None. If every call passes the same literal/member tf,
|
|
126
|
+
that tf is used; mixed timeframes or a never-called (dead-code) function
|
|
127
|
+
fall back to the chart timeframe (``input_tf_``)."""
|
|
128
|
+
fdef = None
|
|
129
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
130
|
+
if isinstance(node, FuncDef) and node.name == func_name:
|
|
131
|
+
fdef = node
|
|
132
|
+
break
|
|
133
|
+
if fdef is None or param_name not in fdef.params:
|
|
134
|
+
return None
|
|
135
|
+
pidx = fdef.params.index(param_name)
|
|
136
|
+
resolved: list = []
|
|
137
|
+
found_call = False
|
|
138
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
139
|
+
if (isinstance(node, FuncCall) and isinstance(node.callee, Identifier)
|
|
140
|
+
and node.callee.name == func_name):
|
|
141
|
+
found_call = True
|
|
142
|
+
arg = node.args[pidx] if pidx < len(node.args) else None
|
|
143
|
+
if arg is None:
|
|
144
|
+
continue
|
|
145
|
+
# Resolve the call-site arg (no further containing func — these
|
|
146
|
+
# are global-scope / input args).
|
|
147
|
+
resolved.append(self._resolve_security_tf(arg, ""))
|
|
148
|
+
if not found_call:
|
|
149
|
+
# dead code — evaluator never read; register with chart tf.
|
|
150
|
+
return (None, "input_tf_")
|
|
151
|
+
valid = [r for r in resolved if r is not None]
|
|
152
|
+
if not valid:
|
|
153
|
+
return (None, "input_tf_")
|
|
154
|
+
strs = {r[0] for r in valid}
|
|
155
|
+
exprs = {r[1] for r in valid}
|
|
156
|
+
if len(strs) == 1 and next(iter(strs), None) is not None:
|
|
157
|
+
return (next(iter(strs)), None)
|
|
158
|
+
if len(exprs) == 1 and next(iter(exprs), None) is not None:
|
|
159
|
+
return (None, next(iter(exprs)))
|
|
160
|
+
# mixed timeframes across call sites — cannot pick one statically
|
|
161
|
+
return (None, "input_tf_")
|
|
162
|
+
|
|
80
163
|
def _normalize_security_call(self, item) -> dict:
|
|
81
164
|
if hasattr(item, "sec_id"):
|
|
82
165
|
return {
|
|
@@ -88,9 +171,12 @@ class SecurityEmitter:
|
|
|
88
171
|
"gaps_node": item.gaps,
|
|
89
172
|
"lookahead_node": item.lookahead,
|
|
90
173
|
"ta_range": item.ta_range,
|
|
174
|
+
"heikinashi": bool(getattr(item, "heikinashi", False)),
|
|
91
175
|
"depends_on_mutable_globals": bool(getattr(item, "depends_on_mutable_globals", False)),
|
|
92
176
|
"mutable_globals": list(getattr(item, "mutable_globals", ()) or ()),
|
|
93
177
|
"is_lower_tf_array": bool(getattr(item, "is_lower_tf_array", False)),
|
|
178
|
+
"containing_func": getattr(item, "containing_func", "") or "",
|
|
179
|
+
"callsite_idx": getattr(item, "callsite_idx", None),
|
|
94
180
|
}
|
|
95
181
|
return {
|
|
96
182
|
"sec_id": item[0],
|
|
@@ -104,6 +190,8 @@ class SecurityEmitter:
|
|
|
104
190
|
"depends_on_mutable_globals": False,
|
|
105
191
|
"mutable_globals": [],
|
|
106
192
|
"is_lower_tf_array": False,
|
|
193
|
+
"containing_func": "",
|
|
194
|
+
"callsite_idx": None,
|
|
107
195
|
}
|
|
108
196
|
|
|
109
197
|
def _security_state_name(self, sec_id: int, name: str) -> str:
|
|
@@ -239,6 +327,75 @@ class SecurityEmitter:
|
|
|
239
327
|
def _security_ohlc_hist_series_cpp(self, sec_id: int, field: str) -> str:
|
|
240
328
|
return f"_sec{sec_id}_hist_{field}"
|
|
241
329
|
|
|
330
|
+
def _collect_security_ta_hist_indices(self, node) -> set[int]:
|
|
331
|
+
"""Which security TA call-site indices need HTF history (subscript index >= 1).
|
|
332
|
+
|
|
333
|
+
``request.security(..., ta.ema(close, 55)[1], ...)`` reads a *confirmed*
|
|
334
|
+
HTF TA value at a past-bar offset. The inner TA call runs in the security
|
|
335
|
+
(HTF) context and commits one value per COMPLETED HTF bar; offsets read a
|
|
336
|
+
per-site ``Series`` filled (gated on ``is_complete``) in
|
|
337
|
+
``_eval_security_N``. Mirrors ``_collect_security_ohlc_hist_fields`` for
|
|
338
|
+
OHLC offsets. Offset 0 reuses the current committed value (``_secval_*``)
|
|
339
|
+
and needs no Series, so only index >= 1 registers here."""
|
|
340
|
+
out: set[int] = set()
|
|
341
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
342
|
+
|
|
343
|
+
def resolve_ta_site(obj, resolving: set[str] | None = None):
|
|
344
|
+
"""_get_ta_site only matches the literal ta.* FuncCall node by
|
|
345
|
+
identity; fall back through global_expr_map for an indirect
|
|
346
|
+
binding (``v = ta.ema(close, 55)`` then ``...v[1]...``), mirroring
|
|
347
|
+
the same fallback in _build_security_expr's Subscript branch."""
|
|
348
|
+
site = self._get_ta_site(obj)
|
|
349
|
+
if site is not None:
|
|
350
|
+
return site
|
|
351
|
+
if isinstance(obj, Identifier):
|
|
352
|
+
if resolving is None:
|
|
353
|
+
resolving = set()
|
|
354
|
+
if obj.name in global_expr_map and obj.name not in resolving:
|
|
355
|
+
resolving.add(obj.name)
|
|
356
|
+
return resolve_ta_site(global_expr_map[obj.name], resolving)
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def walk(n):
|
|
360
|
+
if n is None:
|
|
361
|
+
return
|
|
362
|
+
if isinstance(n, Subscript):
|
|
363
|
+
site = resolve_ta_site(n.object)
|
|
364
|
+
if site is not None:
|
|
365
|
+
idx_lit = self._literal_int_for_security_index(n.index)
|
|
366
|
+
if idx_lit is not None and idx_lit >= 1:
|
|
367
|
+
site_idx = self._ta_index_by_site_id.get(id(site))
|
|
368
|
+
if site_idx is not None:
|
|
369
|
+
out.add(site_idx)
|
|
370
|
+
if isinstance(n, (list, tuple)):
|
|
371
|
+
for x in n:
|
|
372
|
+
walk(x)
|
|
373
|
+
return
|
|
374
|
+
for _k, v in getattr(n, "__dict__", {}).items():
|
|
375
|
+
if isinstance(v, ASTNode):
|
|
376
|
+
walk(v)
|
|
377
|
+
elif isinstance(v, (list, tuple)):
|
|
378
|
+
for x in v:
|
|
379
|
+
if isinstance(x, ASTNode):
|
|
380
|
+
walk(x)
|
|
381
|
+
|
|
382
|
+
walk(node)
|
|
383
|
+
return out
|
|
384
|
+
|
|
385
|
+
def _security_ta_hist_series_cpp(self, member_name: str) -> str:
|
|
386
|
+
"""Per-(sec, site) ``Series<double>`` backing ``ta.<fn>(...)[k>=1]`` HTF history."""
|
|
387
|
+
return f"{member_name}_hist"
|
|
388
|
+
|
|
389
|
+
def _security_ta_hist_series_names(self, sec_id: int) -> list[str]:
|
|
390
|
+
"""Hist Series names for every security TA site (and variant) read at an
|
|
391
|
+
offset >= 1 in sec ``sec_id``."""
|
|
392
|
+
info = self._security_eval_info[sec_id]
|
|
393
|
+
names: list[str] = []
|
|
394
|
+
for idx in sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ())):
|
|
395
|
+
for variant in (info.get("ta_variants") or {}).get(idx, []):
|
|
396
|
+
names.append(self._security_ta_hist_series_cpp(variant["member_name"]))
|
|
397
|
+
return names
|
|
398
|
+
|
|
242
399
|
@staticmethod
|
|
243
400
|
def _security_series_binding(series_name: str) -> str:
|
|
244
401
|
return f"@series:{series_name}"
|
|
@@ -1160,6 +1317,59 @@ class SecurityEmitter:
|
|
|
1160
1317
|
)
|
|
1161
1318
|
return out
|
|
1162
1319
|
|
|
1320
|
+
def _emit_security_ohlc_hist_pushes(self, sec_id: int, lines: list[str]) -> None:
|
|
1321
|
+
"""Emit the OHLC history-offset Series pushes for ``sec_id``, gated on
|
|
1322
|
+
``is_complete``.
|
|
1323
|
+
|
|
1324
|
+
``request.security(..., [high[1], low[1], ...], ...)`` reads HTF OHLC at
|
|
1325
|
+
past-bar offsets. Each offset is backed by a per-field Series whose
|
|
1326
|
+
history must advance once per COMPLETED HTF bar — not once per (partial)
|
|
1327
|
+
chart-bar evaluation. ``_eval_security_N`` fires on every chart bar; only
|
|
1328
|
+
the bar that completes the HTF aggregate has ``is_complete == true``.
|
|
1329
|
+
Pushing unconditionally advanced the offset history every chart bar, so
|
|
1330
|
+
``high[1]`` resolved to a recent partial bar instead of the prior
|
|
1331
|
+
completed HTF bar. Gate all pushes for this sec in one combined block."""
|
|
1332
|
+
fields = sorted(self._security_ohlc_hist_fields_by_sec.get(sec_id, ()))
|
|
1333
|
+
if not fields:
|
|
1334
|
+
return
|
|
1335
|
+
lines.append(" if (is_complete) {")
|
|
1336
|
+
for field in fields:
|
|
1337
|
+
lines.append(
|
|
1338
|
+
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
|
|
1339
|
+
)
|
|
1340
|
+
lines.append(" }")
|
|
1341
|
+
|
|
1342
|
+
def _emit_security_ta_hist_pushes(
|
|
1343
|
+
self, sec_id: int, info: dict, ta_results: dict, lines: list[str]
|
|
1344
|
+
) -> None:
|
|
1345
|
+
"""Emit the TA history-offset Series pushes for ``sec_id``, gated on
|
|
1346
|
+
``is_complete`` (mirrors ``_emit_security_ohlc_hist_pushes``).
|
|
1347
|
+
|
|
1348
|
+
``request.security(..., ta.ema(close, 55)[1], ...)`` reads a confirmed
|
|
1349
|
+
HTF TA value at a past-bar offset. The committed value (``_secval_*``,
|
|
1350
|
+
produced with ``.compute()`` only when ``is_complete``) is pushed onto a
|
|
1351
|
+
per-site Series once per COMPLETED HTF bar, AFTER the expression
|
|
1352
|
+
assignment so the offset read sees the prior completed bar. Pushing on
|
|
1353
|
+
every chart-bar eval would otherwise advance the offset history per
|
|
1354
|
+
partial eval / chart tick (the bug this replaces, where the chart-context
|
|
1355
|
+
``_hist_call`` buffer advanced on ``is_first_tick_``)."""
|
|
1356
|
+
indices = sorted(self._security_ta_hist_idx_by_sec.get(sec_id, ()))
|
|
1357
|
+
if not indices:
|
|
1358
|
+
return
|
|
1359
|
+
pushes: list[str] = []
|
|
1360
|
+
for idx in indices:
|
|
1361
|
+
for variant in (info.get("ta_variants") or {}).get(idx, []):
|
|
1362
|
+
result_name = ta_results.get((idx, variant["signature"]))
|
|
1363
|
+
if result_name is None:
|
|
1364
|
+
continue
|
|
1365
|
+
hist = self._security_ta_hist_series_cpp(variant["member_name"])
|
|
1366
|
+
pushes.append(f" {hist}.push({result_name});")
|
|
1367
|
+
if not pushes:
|
|
1368
|
+
return
|
|
1369
|
+
lines.append(" if (is_complete) {")
|
|
1370
|
+
lines.extend(pushes)
|
|
1371
|
+
lines.append(" }")
|
|
1372
|
+
|
|
1163
1373
|
def _emit_security_evaluators(self, lines: list[str]) -> None:
|
|
1164
1374
|
"""Emit _eval_security_N() methods and evaluate_security() dispatch."""
|
|
1165
1375
|
if not self._security_calls:
|
|
@@ -1223,7 +1433,7 @@ class SecurityEmitter:
|
|
|
1223
1433
|
)
|
|
1224
1434
|
var_name = variant["result_name"]
|
|
1225
1435
|
sec_name = variant["member_name"]
|
|
1226
|
-
lines.append(f" auto {var_name} =
|
|
1436
|
+
lines.append(f" auto {var_name} = security_series_slot_is_new({sec_id}) "
|
|
1227
1437
|
f"? {sec_name}.compute({compute_args}) "
|
|
1228
1438
|
f": {sec_name}.recompute({compute_args});")
|
|
1229
1439
|
ta_results[(idx, variant["signature"])] = var_name
|
|
@@ -1256,10 +1466,8 @@ class SecurityEmitter:
|
|
|
1256
1466
|
emitted_lines=lines,
|
|
1257
1467
|
)
|
|
1258
1468
|
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
|
-
)
|
|
1469
|
+
self._emit_security_ohlc_hist_pushes(sec_id, lines)
|
|
1470
|
+
self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
|
|
1263
1471
|
lines.append(" }")
|
|
1264
1472
|
lines.append("")
|
|
1265
1473
|
continue
|
|
@@ -1287,10 +1495,8 @@ class SecurityEmitter:
|
|
|
1287
1495
|
)
|
|
1288
1496
|
else:
|
|
1289
1497
|
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
|
-
)
|
|
1498
|
+
self._emit_security_ohlc_hist_pushes(sec_id, lines)
|
|
1499
|
+
self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
|
|
1294
1500
|
lines.append(" }")
|
|
1295
1501
|
lines.append("")
|
|
1296
1502
|
|
|
@@ -1327,6 +1533,8 @@ class SecurityEmitter:
|
|
|
1327
1533
|
lines.append(
|
|
1328
1534
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1329
1535
|
)
|
|
1536
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1537
|
+
lines.append(f" {name}.clear();")
|
|
1330
1538
|
lines.append(" break;")
|
|
1331
1539
|
continue
|
|
1332
1540
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
@@ -1349,16 +1557,21 @@ class SecurityEmitter:
|
|
|
1349
1557
|
lines.append(
|
|
1350
1558
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1351
1559
|
)
|
|
1560
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1561
|
+
lines.append(f" {name}.clear();")
|
|
1352
1562
|
lines.append(" break;")
|
|
1353
1563
|
else:
|
|
1354
1564
|
hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
1355
|
-
|
|
1565
|
+
ta_hist_names = self._security_ta_hist_series_names(sec_id)
|
|
1566
|
+
if hist or ta_hist_names:
|
|
1356
1567
|
lines.append(f" case {sec_id}:")
|
|
1357
1568
|
lines.append(f" _req_sec_{sec_id} = na<double>();")
|
|
1358
1569
|
for field in sorted(hist):
|
|
1359
1570
|
lines.append(
|
|
1360
1571
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1361
1572
|
)
|
|
1573
|
+
for name in ta_hist_names:
|
|
1574
|
+
lines.append(f" {name}.clear();")
|
|
1362
1575
|
lines.append(" break;")
|
|
1363
1576
|
else:
|
|
1364
1577
|
lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
|
|
@@ -1494,6 +1707,74 @@ class SecurityEmitter:
|
|
|
1494
1707
|
"request.security() OHLC history index must be a literal integer (e.g. high[1])",
|
|
1495
1708
|
)
|
|
1496
1709
|
|
|
1710
|
+
# Indirect TA binding: ``v = ta.ema(close, 55)`` then
|
|
1711
|
+
# ``request.security(..., v[1], ...)``. _get_ta_site below only
|
|
1712
|
+
# matches the literal ta.* FuncCall node by identity, so a bare
|
|
1713
|
+
# Identifier subscript target silently misses it and falls
|
|
1714
|
+
# through to a chart-resolution read of the wrong (non-HTF)
|
|
1715
|
+
# series. Resolve through the same global_expr_map the
|
|
1716
|
+
# non-subscript Identifier path above already uses, and
|
|
1717
|
+
# recurse on a synthetic Subscript over the resolved value so
|
|
1718
|
+
# it re-enters this whole branch (TA site, OHLC field, or
|
|
1719
|
+
# helper binding, whichever the resolved expression turns out
|
|
1720
|
+
# to be) instead of duplicating that dispatch here.
|
|
1721
|
+
global_expr_map = getattr(self.ctx, "global_expr_map", {}) or {}
|
|
1722
|
+
if (expr_node.object.name in global_expr_map
|
|
1723
|
+
and expr_node.object.name not in resolving):
|
|
1724
|
+
resolving.add(expr_node.object.name)
|
|
1725
|
+
resolved = self._build_security_expr(
|
|
1726
|
+
sec_id,
|
|
1727
|
+
Subscript(object=global_expr_map[expr_node.object.name], index=expr_node.index),
|
|
1728
|
+
ta_range,
|
|
1729
|
+
ta_results,
|
|
1730
|
+
resolving,
|
|
1731
|
+
security_mutable_names,
|
|
1732
|
+
helper_binding_stack,
|
|
1733
|
+
emitted_lines,
|
|
1734
|
+
)
|
|
1735
|
+
resolving.remove(expr_node.object.name)
|
|
1736
|
+
return resolved
|
|
1737
|
+
ta_site = self._get_ta_site(expr_node.object)
|
|
1738
|
+
if ta_site is not None:
|
|
1739
|
+
# ``ta.<fn>(...)[k]`` inside request.security(): the inner TA call
|
|
1740
|
+
# runs in the HTF (security) context and commits one value per
|
|
1741
|
+
# COMPLETED HTF bar. Read the already-emitted security TA result —
|
|
1742
|
+
# offset 0 reuses the current committed value (``_secval_*``),
|
|
1743
|
+
# offset k>=1 reads a per-site Series that advances on
|
|
1744
|
+
# ``is_complete`` (HTF-bar boundary) in ``_eval_security_N``. The
|
|
1745
|
+
# buggy generic path re-lowered the inner TA to the CHART member
|
|
1746
|
+
# and gated a ``_hist_call`` buffer on ``is_first_tick_`` (chart
|
|
1747
|
+
# tick), so without a magnifier it advanced every chart bar and
|
|
1748
|
+
# produced the chart-tf TA instead of the confirmed HTF value.
|
|
1749
|
+
idx = self._ta_index_by_site_id.get(id(ta_site))
|
|
1750
|
+
sig = self._security_binding_stack_signature(helper_binding_stack)
|
|
1751
|
+
idx_lit = self._literal_int_for_security_index(expr_node.index)
|
|
1752
|
+
if idx_lit is None:
|
|
1753
|
+
self._codegen_error(
|
|
1754
|
+
expr_node,
|
|
1755
|
+
"request.security() TA history index must be a literal integer (e.g. ta.ema(close, 55)[1])",
|
|
1756
|
+
)
|
|
1757
|
+
if idx_lit == 0:
|
|
1758
|
+
# Current completed-HTF-bar value: reuse the bare-TA emission.
|
|
1759
|
+
return self._build_security_expr(
|
|
1760
|
+
sec_id,
|
|
1761
|
+
expr_node.object,
|
|
1762
|
+
ta_range,
|
|
1763
|
+
ta_results,
|
|
1764
|
+
resolving,
|
|
1765
|
+
security_mutable_names,
|
|
1766
|
+
helper_binding_stack,
|
|
1767
|
+
emitted_lines,
|
|
1768
|
+
)
|
|
1769
|
+
member_name = self._security_ta_variant_names.get(
|
|
1770
|
+
(sec_id, idx, sig),
|
|
1771
|
+
f"_sec{sec_id}_{ta_site.member_name}",
|
|
1772
|
+
)
|
|
1773
|
+
hist = self._security_ta_hist_series_cpp(member_name)
|
|
1774
|
+
# ta(...)[k] -> hist[k-1]: hist[0] is the prior completed HTF bar
|
|
1775
|
+
# (current value not yet pushed — push happens after this assign).
|
|
1776
|
+
return f"{hist}[{idx_lit - 1}]"
|
|
1777
|
+
|
|
1497
1778
|
if isinstance(expr_node, BinOp):
|
|
1498
1779
|
left = self._build_security_expr(
|
|
1499
1780
|
sec_id, expr_node.left, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
|
|
@@ -1589,7 +1870,7 @@ class SecurityEmitter:
|
|
|
1589
1870
|
helper_binding_stack,
|
|
1590
1871
|
emitted_lines,
|
|
1591
1872
|
)
|
|
1592
|
-
return f"(
|
|
1873
|
+
return f"(security_series_slot_is_new({sec_id}) ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
|
|
1593
1874
|
|
|
1594
1875
|
result = self._visit_expr(expr_node)
|
|
1595
1876
|
return self._rewrite_security_cpp(result, sec_id, security_mutable_names, helper_binding_stack)
|
|
@@ -255,7 +255,12 @@ TA_TUPLE_FIELDS = {
|
|
|
255
255
|
# must be promoted to ``int64_t`` so the ``na`` sentinel (``INT64_MIN``)
|
|
256
256
|
# survives — narrowing to 32-bit ``int`` collapses it to ``0`` and breaks
|
|
257
257
|
# ``is_na<int>`` detection.
|
|
258
|
-
INT64_BUILTINS = {"time", "time_close", "timestamp", "time_tradingday"}
|
|
258
|
+
INT64_BUILTINS = {"time", "time_close", "timenow", "timestamp", "time_tradingday"}
|
|
259
|
+
|
|
260
|
+
# Subset of INT64_BUILTINS spelled as a bare identifier (no call args) in Pine.
|
|
261
|
+
# ``entryTime := time`` parses the RHS as an ``Identifier`` named ``time`` (not a
|
|
262
|
+
# ``FuncCall``), so int64 promotion must match these by identifier name too.
|
|
263
|
+
INT64_BUILTIN_IDENTIFIERS = {"time", "time_close", "timenow"}
|
|
259
264
|
|
|
260
265
|
PINE_TYPE_TO_CPP = {
|
|
261
266
|
"int": "int", "float": "double", "bool": "bool", "string": "std::string",
|
|
@@ -263,15 +268,66 @@ PINE_TYPE_TO_CPP = {
|
|
|
263
268
|
PineType.STRING: "std::string", PineType.NA: "double",
|
|
264
269
|
PineType.UNKNOWN: "double", PineType.VOID: "double",
|
|
265
270
|
PineType.COLOR: "int",
|
|
271
|
+
# Drawing-objects-as-data: the value-view handle structs (see
|
|
272
|
+
# drawing.hpp). Explicit-hint decls (``var line x``), UDT-method drawing
|
|
273
|
+
# params, and ``_type_for_decl`` resolve through here.
|
|
274
|
+
"line": "Line", "box": "Box", "label": "Label",
|
|
275
|
+
"linefill": "Linefill", "chart.point": "ChartPoint",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Drawing-objects-as-data (spec §4.1)
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Pine drawing type name -> C++ handle struct (pineforge/drawing.hpp).
|
|
282
|
+
# ``table`` / ``polyline`` are DELIBERATELY absent — they stay 100% no-op
|
|
283
|
+
# (kept in SKIP_NAMESPACES / SKIP_VAR_TYPES / _DRAWING_TYPES_INIT).
|
|
284
|
+
DRAWING_TYPE_TO_CPP = {
|
|
285
|
+
"line": "Line", "box": "Box", "label": "Label",
|
|
286
|
+
"linefill": "Linefill", "chart.point": "ChartPoint",
|
|
266
287
|
}
|
|
288
|
+
# Function-call namespaces routed to the per-type arena dispatch.
|
|
289
|
+
DRAWING_NS = {"line", "box", "label", "linefill"}
|
|
290
|
+
# Constant member reads (``line.style_solid`` -> "0"). chart.point/linefill
|
|
291
|
+
# carry no style constants.
|
|
292
|
+
DRAWING_STYLE_NS = {"line", "box", "label"}
|
|
293
|
+
# Per-type arena member name on the generated strategy.
|
|
294
|
+
DRAWING_ARENA = {
|
|
295
|
+
"line": "_pf_lines_", "box": "_pf_boxes_",
|
|
296
|
+
"label": "_pf_labels_", "linefill": "_pf_linefills_",
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# Pine typed array constructors for drawing handles — ``array.new_line(...)``
|
|
300
|
+
# is the typed alias of ``array.new<line>(...)`` (and likewise for box/label/
|
|
301
|
+
# linefill). Maps the constructor name to the drawing element type name, so the
|
|
302
|
+
# element TypeSpec resolves to the C++ handle struct (Line/Box/Label/Linefill).
|
|
303
|
+
ARRAY_DRAWING_NEW_CTORS = {
|
|
304
|
+
"new_line": "line", "new_box": "box",
|
|
305
|
+
"new_label": "label", "new_linefill": "linefill",
|
|
306
|
+
}
|
|
307
|
+
# Pine array methods whose C++ lowering is void (or a non-scalar iterator) and
|
|
308
|
+
# whose Pine semantics is mutate-in-place: a function whose body ends in one of
|
|
309
|
+
# these returns void, so the codegen must emit it as a statement + default
|
|
310
|
+
# return rather than ``return arr.clear();`` / ``return arr.insert(...);``.
|
|
311
|
+
ARRAY_VOID_METHODS = frozenset({
|
|
312
|
+
"push", "unshift", "insert", "clear", "set", "fill",
|
|
313
|
+
"sort", "reverse", "concat",
|
|
314
|
+
})
|
|
315
|
+
# All recognised ``array.new_*`` scalar+drawing constructor names (``new`` itself
|
|
316
|
+
# is template-form and handled separately).
|
|
317
|
+
ARRAY_NEW_CTORS = frozenset(
|
|
318
|
+
{"new_float", "new_int", "new_bool", "new_string"} | set(ARRAY_DRAWING_NEW_CTORS)
|
|
319
|
+
)
|
|
267
320
|
|
|
268
321
|
SKIP_FUNC_NAMES = {
|
|
269
322
|
"plot", "plotshape", "plotchar", "plotcandle", "plotbar", "plotarrow",
|
|
270
323
|
"fill", "hline", "barcolor", "bgcolor", "alert", "alertcondition",
|
|
271
324
|
}
|
|
325
|
+
# line / box / label / linefill REMOVED — they now route to real drawing
|
|
326
|
+
# codegen (gated on _uses_drawing). table / polyline / chart / display / size
|
|
327
|
+
# / position stay no-op.
|
|
272
328
|
SKIP_NAMESPACES = {
|
|
273
|
-
"table", "
|
|
274
|
-
"
|
|
329
|
+
"table", "polyline", "chart",
|
|
330
|
+
"display", "size", "position",
|
|
275
331
|
}
|
|
276
332
|
SKIP_VAR_TYPES = {"table"}
|
|
277
333
|
|