@pineforge/codegen-pyodide 0.7.6 → 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 +532 -51
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +331 -30
- package/pineforge_codegen/codegen/helpers.py +26 -1
- package/pineforge_codegen/codegen/input.py +39 -2
- package/pineforge_codegen/codegen/security.py +321 -9
- package/pineforge_codegen/codegen/ta.py +4 -1
- package/pineforge_codegen/codegen/tables.py +63 -7
- package/pineforge_codegen/codegen/types.py +383 -21
- package/pineforge_codegen/codegen/visit_call.py +159 -22
- package/pineforge_codegen/codegen/visit_expr.py +91 -3
- package/pineforge_codegen/codegen/visit_stmt.py +188 -26
- package/pineforge_codegen/lexer.py +68 -3
- package/pineforge_codegen/parser.py +106 -49
- package/pineforge_codegen/support_checker.py +361 -37
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +4 -18
- package/pineforge_codegen-0.7.6.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
|
|
@@ -1232,6 +1442,35 @@ class SecurityEmitter:
|
|
|
1232
1442
|
|
|
1233
1443
|
self._emit_security_rebinds(sec_id, info, lines, ta_results, indent=2, emitted_lines=lines)
|
|
1234
1444
|
emit_security_ta(post_rebind_ta_indices)
|
|
1445
|
+
returns_tuple = item.get("returns_tuple", False)
|
|
1446
|
+
tuple_size = item.get("tuple_size", 0)
|
|
1447
|
+
if (
|
|
1448
|
+
returns_tuple
|
|
1449
|
+
and tuple_size
|
|
1450
|
+
and tuple_size > 0
|
|
1451
|
+
and isinstance(expr_node, TupleLiteral)
|
|
1452
|
+
):
|
|
1453
|
+
# A tuple body destructures into per-element scalar members
|
|
1454
|
+
# ``_req_sec_{sec_id}_{i}`` (declared in ``base.py`` and reset in
|
|
1455
|
+
# ``clear_security``). Assign each element individually rather
|
|
1456
|
+
# than building the whole ``TupleLiteral`` (which lowers to an
|
|
1457
|
+
# ``std::make_tuple(...)`` against the non-existent aggregate
|
|
1458
|
+
# member ``_req_sec_{sec_id}``).
|
|
1459
|
+
for i, el in enumerate(expr_node.elements):
|
|
1460
|
+
el_cpp = self._build_security_expr(
|
|
1461
|
+
sec_id,
|
|
1462
|
+
el,
|
|
1463
|
+
None,
|
|
1464
|
+
ta_results,
|
|
1465
|
+
security_mutable_names=security_mutable_names,
|
|
1466
|
+
emitted_lines=lines,
|
|
1467
|
+
)
|
|
1468
|
+
lines.append(f" _req_sec_{sec_id}_{i} = {el_cpp};")
|
|
1469
|
+
self._emit_security_ohlc_hist_pushes(sec_id, lines)
|
|
1470
|
+
self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
|
|
1471
|
+
lines.append(" }")
|
|
1472
|
+
lines.append("")
|
|
1473
|
+
continue
|
|
1235
1474
|
expr_cpp = self._build_security_expr(
|
|
1236
1475
|
sec_id,
|
|
1237
1476
|
expr_node,
|
|
@@ -1256,10 +1495,8 @@ class SecurityEmitter:
|
|
|
1256
1495
|
)
|
|
1257
1496
|
else:
|
|
1258
1497
|
lines.append(f" _req_sec_{sec_id} = {expr_cpp};")
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.push(bar.{field});"
|
|
1262
|
-
)
|
|
1498
|
+
self._emit_security_ohlc_hist_pushes(sec_id, lines)
|
|
1499
|
+
self._emit_security_ta_hist_pushes(sec_id, info, ta_results, lines)
|
|
1263
1500
|
lines.append(" }")
|
|
1264
1501
|
lines.append("")
|
|
1265
1502
|
|
|
@@ -1296,6 +1533,8 @@ class SecurityEmitter:
|
|
|
1296
1533
|
lines.append(
|
|
1297
1534
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1298
1535
|
)
|
|
1536
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1537
|
+
lines.append(f" {name}.clear();")
|
|
1299
1538
|
lines.append(" break;")
|
|
1300
1539
|
continue
|
|
1301
1540
|
if returns_tuple and tuple_size and tuple_size > 0 and isinstance(expr_node, TupleLiteral):
|
|
@@ -1318,16 +1557,21 @@ class SecurityEmitter:
|
|
|
1318
1557
|
lines.append(
|
|
1319
1558
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1320
1559
|
)
|
|
1560
|
+
for name in self._security_ta_hist_series_names(sec_id):
|
|
1561
|
+
lines.append(f" {name}.clear();")
|
|
1321
1562
|
lines.append(" break;")
|
|
1322
1563
|
else:
|
|
1323
1564
|
hist = self._security_ohlc_hist_fields_by_sec.get(sec_id, ())
|
|
1324
|
-
|
|
1565
|
+
ta_hist_names = self._security_ta_hist_series_names(sec_id)
|
|
1566
|
+
if hist or ta_hist_names:
|
|
1325
1567
|
lines.append(f" case {sec_id}:")
|
|
1326
1568
|
lines.append(f" _req_sec_{sec_id} = na<double>();")
|
|
1327
1569
|
for field in sorted(hist):
|
|
1328
1570
|
lines.append(
|
|
1329
1571
|
f" {self._security_ohlc_hist_series_cpp(sec_id, field)}.clear();"
|
|
1330
1572
|
)
|
|
1573
|
+
for name in ta_hist_names:
|
|
1574
|
+
lines.append(f" {name}.clear();")
|
|
1331
1575
|
lines.append(" break;")
|
|
1332
1576
|
else:
|
|
1333
1577
|
lines.append(f" case {sec_id}: _req_sec_{sec_id} = na<double>(); break;")
|
|
@@ -1463,6 +1707,74 @@ class SecurityEmitter:
|
|
|
1463
1707
|
"request.security() OHLC history index must be a literal integer (e.g. high[1])",
|
|
1464
1708
|
)
|
|
1465
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
|
+
|
|
1466
1778
|
if isinstance(expr_node, BinOp):
|
|
1467
1779
|
left = self._build_security_expr(
|
|
1468
1780
|
sec_id, expr_node.left, ta_range, ta_results, resolving, security_mutable_names, helper_binding_stack, emitted_lines
|
|
@@ -1558,7 +1870,7 @@ class SecurityEmitter:
|
|
|
1558
1870
|
helper_binding_stack,
|
|
1559
1871
|
emitted_lines,
|
|
1560
1872
|
)
|
|
1561
|
-
return f"(
|
|
1873
|
+
return f"(security_series_slot_is_new({sec_id}) ? {sec_name}.compute({compute_args}) : {sec_name}.recompute({compute_args}))"
|
|
1562
1874
|
|
|
1563
1875
|
result = self._visit_expr(expr_node)
|
|
1564
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
|
|
|
@@ -382,14 +438,14 @@ ORDER_DIRECTION_MAP = {
|
|
|
382
438
|
|
|
383
439
|
# Methods called as ``array.method(arr, ...)`` or ``arr.method(...)``.
|
|
384
440
|
ARRAY_METHODS = {
|
|
385
|
-
"get": lambda a, args: f"{a}[{args[0]}]",
|
|
386
|
-
"set": lambda a, args: f"{a}[{args[0]}] = {args[1]}",
|
|
441
|
+
"get": lambda a, args: f"{a}[({args[0]})]",
|
|
442
|
+
"set": lambda a, args: f"{a}[({args[0]})] = {args[1]}",
|
|
387
443
|
"push": lambda a, args: f"{a}.push_back({args[0]})",
|
|
388
444
|
"unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
|
|
389
445
|
"insert": lambda a, args: f"{a}.insert({a}.begin() + (int)({args[0]}), {args[1]})",
|
|
390
446
|
"pop": lambda a, args: f"[&](){{ auto v={a}.back(); {a}.pop_back(); return v; }}()",
|
|
391
447
|
"shift": lambda a, args: f"[&](){{ auto v={a}.front(); {a}.erase({a}.begin()); return v; }}()",
|
|
392
|
-
"remove": lambda a, args: f"[&](){{ auto v={a}[{args[0]}]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
|
|
448
|
+
"remove": lambda a, args: f"[&](){{ auto v={a}[({args[0]})]; {a}.erase({a}.begin()+(int)({args[0]})); return v; }}()",
|
|
393
449
|
"first": lambda a, args: f"{a}.front()",
|
|
394
450
|
"last": lambda a, args: f"{a}.back()",
|
|
395
451
|
"size": lambda a, args: f"(double){a}.size()",
|
|
@@ -435,7 +491,7 @@ ARRAY_METHODS = {
|
|
|
435
491
|
"mode": lambda a, args: f"[&](){{ std::unordered_map<double,int> m; for(auto v:{a})m[v]++; double best=0; int bc=0; for(auto&[v,c]:m)if(c>bc||(c==bc&&v<best)){{bc=c;best=v;}} return best; }}()",
|
|
436
492
|
"percentile_linear_interpolation": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); double k=({args[0]}/100.0)*c.size()-0.5; int i=std::max(0,(int)k); double f=k-i; if(i+1>=(int)c.size()) return c.back(); return c[i]*(1-f)+c[i+1]*f; }}()",
|
|
437
493
|
"percentile_nearest_rank": lambda a, args: f"[&](){{ auto c={a}; std::sort(c.begin(),c.end()); int r=(int)std::ceil(({args[0]}/100.0)*c.size()); return c[std::min(r-1,(int)c.size()-1)]; }}()",
|
|
438
|
-
"percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[{args[0]}]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
|
|
494
|
+
"percentrank": lambda a, args: f"[&](){{ if({a}.size()<=1) return na<double>(); double v={a}[({args[0]})]; if(std::isnan(v)) return na<double>(); int le=0; for(auto x:{a}) if(!std::isnan(x) && x<=v) le++; return (double)(le-1)/({a}.size()-1)*100.0; }}()",
|
|
439
495
|
"abs": lambda a, args: f"[&](){{ std::vector<double> r; for(auto v:{a})r.push_back(std::abs(v)); return r; }}()",
|
|
440
496
|
"join": lambda a, args: "[&](){{ std::string r; for(size_t i=0;i<{arr}.size();i++){{ if(i>0)r+={sep}; r+=std::to_string({arr}[i]); }} return r; }}()".format(arr=a, sep=args[0] if args else 'std::string(",")'),
|
|
441
497
|
"standardize": lambda a, args: f"[&](){{ double m=std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size(); double s=0; for(auto v:{a})s+=(v-m)*(v-m); s=std::sqrt(s/{a}.size()); std::vector<double> r; for(auto v:{a})r.push_back(s==0?1.0:(v-m)/s); return r; }}()",
|