@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
|
@@ -50,11 +50,15 @@ from .tv_input_choices import INPUT_SOURCE_SERIES_IDS
|
|
|
50
50
|
from .analyzer import TA_CLASS_MAP
|
|
51
51
|
from .codegen import (
|
|
52
52
|
MATH_FUNC_MAP, STR_FUNC_MAP,
|
|
53
|
-
ARRAY_METHODS, MAP_METHODS, MATRIX_METHODS,
|
|
53
|
+
ARRAY_METHODS, ARRAY_DRAWING_NEW_CTORS, MAP_METHODS, MATRIX_METHODS,
|
|
54
54
|
SYMINFO_MEMBER_MAP, COLOR_CONST_MAP,
|
|
55
55
|
SKIP_FUNC_NAMES, SKIP_NAMESPACES,
|
|
56
56
|
BAR_BUILTINS, BAR_FIELDS,
|
|
57
57
|
)
|
|
58
|
+
from .codegen.drawing import (
|
|
59
|
+
LINE_METHODS, LINE_NOOP, BOX_METHODS, BOX_NOOP,
|
|
60
|
+
LABEL_METHODS, LABEL_NOOP, LINEFILL_METHODS, LINEFILL_NOOP,
|
|
61
|
+
)
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
# ---------------------------------------------------------------------------
|
|
@@ -74,10 +78,38 @@ SUPPORTED_MATH: frozenset[str] = frozenset(
|
|
|
74
78
|
)
|
|
75
79
|
SUPPORTED_STR: frozenset[str] = frozenset(set(STR_FUNC_MAP) | {"format_time"})
|
|
76
80
|
SUPPORTED_INPUT: frozenset[str] = frozenset(sigs.INPUT_FUNCTIONS)
|
|
77
|
-
SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"})
|
|
81
|
+
SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"} | set(ARRAY_DRAWING_NEW_CTORS))
|
|
78
82
|
SUPPORTED_MAP: frozenset[str] = frozenset(set(MAP_METHODS) | {"new"})
|
|
79
83
|
SUPPORTED_MATRIX: frozenset[str] = frozenset(set(MATRIX_METHODS) | {"new"})
|
|
80
84
|
SUPPORTED_SYMINFO: frozenset[str] = frozenset(SYMINFO_MEMBER_MAP)
|
|
85
|
+
# Drawing-objects-as-data (spec §4.5). Geometry methods are REAL (route to the
|
|
86
|
+
# per-type arena); *_NOOP visual setters are accepted no-ops (Level.WARNING).
|
|
87
|
+
# Any line/box/label/linefill method NOT in these sets rejects loudly so an
|
|
88
|
+
# unknown drawing method is not silently emitted. ``table``/``polyline`` are
|
|
89
|
+
# intentionally absent — they stay in SKIP_NAMESPACES (no-op, never reclassified).
|
|
90
|
+
SUPPORTED_LINE: frozenset[str] = frozenset(LINE_METHODS | LINE_NOOP | {"new"})
|
|
91
|
+
SUPPORTED_BOX: frozenset[str] = frozenset(BOX_METHODS | BOX_NOOP | {"new"})
|
|
92
|
+
SUPPORTED_LABEL: frozenset[str] = frozenset(LABEL_METHODS | LABEL_NOOP | {"new"})
|
|
93
|
+
SUPPORTED_LINEFILL: frozenset[str] = frozenset(LINEFILL_METHODS | LINEFILL_NOOP | {"new"})
|
|
94
|
+
SUPPORTED_CHART_POINT: frozenset[str] = frozenset(
|
|
95
|
+
{"new", "now", "from_index", "from_time", "copy"}
|
|
96
|
+
)
|
|
97
|
+
# Visual no-op drawing setters (warn, don't reject) keyed by namespace.
|
|
98
|
+
_DRAWING_NOOP_BY_NS: dict[str, frozenset[str]] = {
|
|
99
|
+
"line": LINE_NOOP, "box": BOX_NOOP, "label": LABEL_NOOP, "linefill": LINEFILL_NOOP,
|
|
100
|
+
}
|
|
101
|
+
_DRAWING_SUPPORTED_BY_NS: dict[str, frozenset[str]] = {
|
|
102
|
+
"line": SUPPORTED_LINE, "box": SUPPORTED_BOX,
|
|
103
|
+
"label": SUPPORTED_LABEL, "linefill": SUPPORTED_LINEFILL,
|
|
104
|
+
}
|
|
105
|
+
_DRAWING_TYPE_NAMES: frozenset[str] = frozenset({"line", "box", "label", "linefill"})
|
|
106
|
+
# Scalar visual-container types whose *method* calls (``panel.cell(...)``,
|
|
107
|
+
# ``ln.set_xy1(...)``) legitimately carry visual-constant args (``text.align_*``,
|
|
108
|
+
# ``size.*``). A method on a receiver of one of these types — including a
|
|
109
|
+
# table/box/line/label/linefill PARAMETER — is a visual sink, so its argument
|
|
110
|
+
# subtree is visited with constant-namespace reads allowed (mirrors the
|
|
111
|
+
# namespace-form ``table.cell``/``label.new`` handling).
|
|
112
|
+
_VISUAL_CONTAINER_TYPES: frozenset[str] = _DRAWING_TYPE_NAMES | frozenset({"table"})
|
|
81
113
|
SUPPORTED_COLOR_CONST: frozenset[str] = frozenset(COLOR_CONST_MAP)
|
|
82
114
|
SUPPORTED_COLOR_FUNC: frozenset[str] = frozenset({"new", "rgb", "r", "g", "b", "t"})
|
|
83
115
|
# Cosmetic color builders with no backtest-logic effect. Warned (not rejected);
|
|
@@ -100,11 +132,10 @@ HARD_REJECT_FUNC: dict[str, str] = {
|
|
|
100
132
|
"request.seed": "External seed data feeds not available in PineForge.",
|
|
101
133
|
"request.quandl": "External Quandl data not available in PineForge.",
|
|
102
134
|
"request.currency_rate": "Currency conversion data not available in PineForge.",
|
|
103
|
-
# ticker.* chart-type modifiers and cross-symbol constructors — hard reject
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
),
|
|
135
|
+
# ticker.* chart-type modifiers and cross-symbol constructors — hard reject.
|
|
136
|
+
# NOTE: ticker.heikinashi is handled contextually in _visit_FuncCall (allowed
|
|
137
|
+
# for the chart's own symbol — the engine applies the HA candle transform
|
|
138
|
+
# inside the same-symbol security eval — rejected for cross-symbol).
|
|
108
139
|
"ticker.renko": (
|
|
109
140
|
"ticker.renko() chart-type modifier / cross-symbol construction not supported — "
|
|
110
141
|
"PineForge runs same-symbol MTF only via request.security with the chart's own tickerid."
|
|
@@ -146,17 +177,23 @@ HARD_REJECT_NAMESPACE: dict[str, str] = {
|
|
|
146
177
|
# DIVERGENT_VARS_ERROR is a SUBSET that is escalated to ERROR (rejected): these
|
|
147
178
|
# are silent MIS-ALIASES, not merely data-window divergences. They return a
|
|
148
179
|
# plausible value that is the WRONG quantity (last_bar_index -> current bar
|
|
149
|
-
# index
|
|
150
|
-
#
|
|
180
|
+
# index) and that value flows directly into trade logic, so the backtest would
|
|
181
|
+
# be silently wrong. A WARNING is not enough.
|
|
182
|
+
#
|
|
183
|
+
# NOTE: the bare ``time_close`` variable is NOT divergent — codegen lowers it to
|
|
184
|
+
# the engine's ``time_close()`` accessor, which returns the true bar-close
|
|
185
|
+
# timestamp (bar open + chart-timeframe duration via ``pine_time_close``),
|
|
186
|
+
# deterministic and TV-faithful. It was previously mis-listed here as a bar-open
|
|
187
|
+
# alias; that was stale. (The session-aware ``time_close(...)`` FUNCTION is a
|
|
188
|
+
# separate supported builtin handled in visit_call.)
|
|
151
189
|
DIVERGENT_VARS: dict[str, str] = {
|
|
152
190
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
153
191
|
"last_bar_index": "last_bar_index is aliased to the CURRENT bar index in PineForge codegen (not the index of the last bar); backtest would be silently wrong — rejected.",
|
|
154
192
|
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time.",
|
|
155
|
-
"time_close": "time_close is aliased to the bar OPEN timestamp in PineForge; it does not represent the bar close time; backtest would be silently wrong — rejected.",
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
# Subset of DIVERGENT_VARS escalated from WARNING to ERROR (see comment above).
|
|
159
|
-
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset({"last_bar_index"
|
|
196
|
+
DIVERGENT_VARS_ERROR: frozenset[str] = frozenset({"last_bar_index"})
|
|
160
197
|
|
|
161
198
|
BARSTATE_APPROX_VARS: dict[str, str] = {
|
|
162
199
|
"barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
|
|
@@ -305,10 +342,14 @@ UNSUPPORTED_NAMESPACE_VARS: dict[str, str] = {
|
|
|
305
342
|
# request.security parameter rules.
|
|
306
343
|
# Codegen supports symbol/timeframe/expression plus gaps/lookahead (read in
|
|
307
344
|
# _eval_security_* emission and forwarded to register_security_eval).
|
|
308
|
-
#
|
|
309
|
-
#
|
|
345
|
+
# `currency` is still rejected loudly (it changes the returned values via FX
|
|
346
|
+
# conversion, which codegen drops — silently wrong). `ignore_invalid_symbol` is
|
|
347
|
+
# a guaranteed no-op here: codegen forces request.security onto the current
|
|
348
|
+
# chart symbol (see the "symbol must reference current chart symbol" check),
|
|
349
|
+
# which is always valid, so the flag can never change the result — accept+ignore.
|
|
310
350
|
SECURITY_ALLOWED_PARAMS: frozenset[str] = frozenset(
|
|
311
351
|
{"symbol", "timeframe", "expression", "gaps", "lookahead",
|
|
352
|
+
"ignore_invalid_symbol",
|
|
312
353
|
# Data-adjustment constants — silently accepted and ignored by codegen;
|
|
313
354
|
# the underlying engine uses a fixed unadjusted data source.
|
|
314
355
|
"backadjustment", "settlement_as_close", "adjustment"}
|
|
@@ -411,6 +452,28 @@ class SupportChecker:
|
|
|
411
452
|
self._user_enums: set[str] = set()
|
|
412
453
|
self._user_funcs: set[str] = set()
|
|
413
454
|
self._user_methods: set[str] = set()
|
|
455
|
+
# Drawing lifecycle gap guard (spec L.3/U.7): only bare identifiers are
|
|
456
|
+
# promoted to Series<T> by analyzer/base.py::_visit_Subscript. A history
|
|
457
|
+
# read on a drawing handle nested in a UDT field (``p.a[1]``) or on a
|
|
458
|
+
# tuple element known to be a drawing handle (``la[1]`` after
|
|
459
|
+
# ``[la, lb] = makePair()``) would otherwise silently read the current
|
|
460
|
+
# bar. Track enough lightweight declaration information to reject those
|
|
461
|
+
# shapes before codegen.
|
|
462
|
+
self._udt_drawing_fields: dict[str, set[str]] = {}
|
|
463
|
+
self._var_udt_types: dict[str, str] = {}
|
|
464
|
+
# Names (vars and function params) declared as a scalar visual-container
|
|
465
|
+
# type (table/box/line/label/linefill). A method call on one of these
|
|
466
|
+
# (``panel.cell(...)``) is a visual sink whose args may carry visual
|
|
467
|
+
# constants, so it routes through ``_visit_children_const_ok``.
|
|
468
|
+
self._visual_container_vars: set[str] = set()
|
|
469
|
+
# User helpers whose body is only a visual sink (for example
|
|
470
|
+
# ``cell(table t, ..., align) => t.cell(..., text_halign=align)``).
|
|
471
|
+
# A call to such a helper is itself a visual context, so style
|
|
472
|
+
# constants passed through its arguments are safe and should not trip
|
|
473
|
+
# the free-expression constant-namespace rejection.
|
|
474
|
+
self._visual_sink_funcs: set[str] = set()
|
|
475
|
+
self._drawing_tuple_vars: set[str] = set()
|
|
476
|
+
self._func_tuple_drawing_returns: dict[str, list[bool]] = {}
|
|
414
477
|
# Track whether we are inside an if/ternary condition expression.
|
|
415
478
|
self._in_conditional_depth: int = 0
|
|
416
479
|
# Track whether we are inside an argument subtree that legitimately
|
|
@@ -425,6 +488,12 @@ class SupportChecker:
|
|
|
425
488
|
# bare ``time_close`` variable) must NOT be flagged as a divergent
|
|
426
489
|
# variable read. Populated as _visit_FuncCall descends into children.
|
|
427
490
|
self._callee_node_ids: set[int] = set()
|
|
491
|
+
# Declaration value of each scalar variable (``name = <expr>``), so the
|
|
492
|
+
# request.security symbol check can resolve an aliased symbol through
|
|
493
|
+
# its definition — e.g. ``haTicker = ticker.heikinashi(syminfo.tickerid)``
|
|
494
|
+
# then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
|
|
495
|
+
# syminfo.tickerid``. First binding wins (closest to declaration).
|
|
496
|
+
self._scalar_defs: dict[str, ASTNode] = {}
|
|
428
497
|
|
|
429
498
|
# -- Public API --
|
|
430
499
|
|
|
@@ -446,12 +515,106 @@ class SupportChecker:
|
|
|
446
515
|
for stmt in ast.body:
|
|
447
516
|
if isinstance(stmt, TypeDecl):
|
|
448
517
|
self._user_types.add(stmt.name)
|
|
518
|
+
drawing_fields = {
|
|
519
|
+
field.name
|
|
520
|
+
for field in stmt.fields
|
|
521
|
+
if self._type_name_contains_drawing(field.type_name)
|
|
522
|
+
}
|
|
523
|
+
if drawing_fields:
|
|
524
|
+
self._udt_drawing_fields[stmt.name] = drawing_fields
|
|
449
525
|
elif isinstance(stmt, EnumDecl):
|
|
450
526
|
self._user_enums.add(stmt.name)
|
|
451
527
|
elif isinstance(stmt, FuncDef):
|
|
452
528
|
self._user_funcs.add(stmt.name)
|
|
529
|
+
tuple_returns = self._single_expr_tuple_drawing_mask(stmt)
|
|
530
|
+
if tuple_returns:
|
|
531
|
+
self._func_tuple_drawing_returns[stmt.name] = tuple_returns
|
|
532
|
+
self._collect_visual_container_params(stmt)
|
|
533
|
+
if self._func_body_is_visual_sink(stmt):
|
|
534
|
+
self._visual_sink_funcs.add(stmt.name)
|
|
453
535
|
elif isinstance(stmt, MethodDef):
|
|
454
536
|
self._user_methods.add(stmt.name)
|
|
537
|
+
self._collect_visual_container_params(stmt)
|
|
538
|
+
|
|
539
|
+
def _collect_visual_container_params(self, fn) -> None:
|
|
540
|
+
"""Register a function's parameters that are declared with a scalar
|
|
541
|
+
visual-container type (``table panel``, ``line ln``) so method calls on
|
|
542
|
+
them inside the body (``panel.cell(...)``) are treated as visual sinks.
|
|
543
|
+
"""
|
|
544
|
+
hints = (getattr(fn, "annotations", None) or {}).get("param_type_hints") or []
|
|
545
|
+
for i, pname in enumerate(getattr(fn, "params", []) or []):
|
|
546
|
+
hint = hints[i] if i < len(hints) else None
|
|
547
|
+
if hint and str(hint).replace(" ", "") in _VISUAL_CONTAINER_TYPES:
|
|
548
|
+
self._visual_container_vars.add(pname)
|
|
549
|
+
|
|
550
|
+
def _expr_is_visual_sink_call(self, expr: ASTNode | None) -> bool:
|
|
551
|
+
if not isinstance(expr, FuncCall):
|
|
552
|
+
return False
|
|
553
|
+
ns, name = _qualified_name(expr.callee)
|
|
554
|
+
if ns is None and name in SKIP_FUNC_NAMES:
|
|
555
|
+
return True
|
|
556
|
+
if ns is not None and ns in SKIP_NAMESPACES:
|
|
557
|
+
return True
|
|
558
|
+
if ns in _DRAWING_NOOP_BY_NS and name in _DRAWING_NOOP_BY_NS[ns]:
|
|
559
|
+
return True
|
|
560
|
+
return ns is not None and ns in self._visual_container_vars
|
|
561
|
+
|
|
562
|
+
def _func_body_is_visual_sink(self, fn: FuncDef | MethodDef) -> bool:
|
|
563
|
+
if not fn.body:
|
|
564
|
+
return False
|
|
565
|
+
for stmt in fn.body:
|
|
566
|
+
expr = stmt.expr if isinstance(stmt, ExprStmt) else None
|
|
567
|
+
if not self._expr_is_visual_sink_call(expr):
|
|
568
|
+
return False
|
|
569
|
+
return True
|
|
570
|
+
|
|
571
|
+
@staticmethod
|
|
572
|
+
def _type_name_contains_drawing(type_name: str | None) -> bool:
|
|
573
|
+
if not type_name:
|
|
574
|
+
return False
|
|
575
|
+
compact = str(type_name).replace(" ", "")
|
|
576
|
+
if compact.endswith("[]"):
|
|
577
|
+
compact = f"array<{compact[:-2]}>"
|
|
578
|
+
if compact in _DRAWING_TYPE_NAMES:
|
|
579
|
+
return True
|
|
580
|
+
return any(f"<{drawing}>" in compact for drawing in _DRAWING_TYPE_NAMES)
|
|
581
|
+
|
|
582
|
+
@staticmethod
|
|
583
|
+
def _is_literal_zero_subscript(node: Subscript) -> bool:
|
|
584
|
+
return isinstance(node.index, NumberLiteral) and node.index.value == 0
|
|
585
|
+
|
|
586
|
+
def _expr_is_drawing_handle(self, expr: ASTNode | None) -> bool:
|
|
587
|
+
if isinstance(expr, FuncCall):
|
|
588
|
+
ns, name = _qualified_name(expr.callee)
|
|
589
|
+
if ns in _DRAWING_TYPE_NAMES and name in {"new", "copy"}:
|
|
590
|
+
return True
|
|
591
|
+
if ns == "linefill" and name in {"get_line1", "get_line2"}:
|
|
592
|
+
return True
|
|
593
|
+
if isinstance(expr, Identifier):
|
|
594
|
+
return expr.name in self._drawing_tuple_vars
|
|
595
|
+
if isinstance(expr, Ternary):
|
|
596
|
+
return self._expr_is_drawing_handle(expr.true_val) or self._expr_is_drawing_handle(expr.false_val)
|
|
597
|
+
return False
|
|
598
|
+
|
|
599
|
+
def _single_expr_tuple_drawing_mask(self, fn: FuncDef) -> list[bool] | None:
|
|
600
|
+
if not fn.is_single_expr or len(fn.body) != 1:
|
|
601
|
+
return None
|
|
602
|
+
stmt = fn.body[0]
|
|
603
|
+
expr = stmt.expr if isinstance(stmt, ExprStmt) else None
|
|
604
|
+
if not isinstance(expr, TupleLiteral):
|
|
605
|
+
return None
|
|
606
|
+
mask = [self._expr_is_drawing_handle(item) for item in expr.elements]
|
|
607
|
+
return mask if any(mask) else None
|
|
608
|
+
|
|
609
|
+
def _tuple_assign_drawing_mask(self, value: ASTNode | None) -> list[bool] | None:
|
|
610
|
+
if isinstance(value, TupleLiteral):
|
|
611
|
+
mask = [self._expr_is_drawing_handle(item) for item in value.elements]
|
|
612
|
+
return mask if any(mask) else None
|
|
613
|
+
if isinstance(value, FuncCall):
|
|
614
|
+
ns, name = _qualified_name(value.callee)
|
|
615
|
+
if ns is None and name in self._func_tuple_drawing_returns:
|
|
616
|
+
return self._func_tuple_drawing_returns[name]
|
|
617
|
+
return None
|
|
455
618
|
|
|
456
619
|
# -- Diagnostic emission --
|
|
457
620
|
|
|
@@ -639,6 +802,24 @@ class SupportChecker:
|
|
|
639
802
|
self._visit_children_const_ok(node)
|
|
640
803
|
|
|
641
804
|
def _visit_VarDecl(self, node: VarDecl) -> None:
|
|
805
|
+
if node.name and node.value is not None:
|
|
806
|
+
self._scalar_defs.setdefault(node.name, node.value)
|
|
807
|
+
if node.type_hint in self._user_types:
|
|
808
|
+
self._var_udt_types[node.name] = node.type_hint
|
|
809
|
+
elif isinstance(node.value, FuncCall):
|
|
810
|
+
ns, name = _qualified_name(node.value.callee)
|
|
811
|
+
if name == "new" and ns in self._user_types:
|
|
812
|
+
self._var_udt_types[node.name] = ns
|
|
813
|
+
# Track scalar visual-container vars (``var table dash = table.new(...)``)
|
|
814
|
+
# so a direct ``dash.cell(..., text.align_left)`` is treated as a visual
|
|
815
|
+
# sink (mirrors the table/box/line/label/linefill PARAM tracking).
|
|
816
|
+
decl_hint = str(node.type_hint).replace(" ", "") if node.type_hint else None
|
|
817
|
+
if decl_hint in _VISUAL_CONTAINER_TYPES:
|
|
818
|
+
self._visual_container_vars.add(node.name)
|
|
819
|
+
elif isinstance(node.value, FuncCall):
|
|
820
|
+
vns, vname = _qualified_name(node.value.callee)
|
|
821
|
+
if vname == "new" and vns in _VISUAL_CONTAINER_TYPES:
|
|
822
|
+
self._visual_container_vars.add(node.name)
|
|
642
823
|
if node.is_varip:
|
|
643
824
|
self._err(
|
|
644
825
|
node,
|
|
@@ -655,6 +836,11 @@ class SupportChecker:
|
|
|
655
836
|
self._visit_children(node)
|
|
656
837
|
|
|
657
838
|
def _visit_TupleAssign(self, node: TupleAssign) -> None:
|
|
839
|
+
drawing_mask = self._tuple_assign_drawing_mask(node.value)
|
|
840
|
+
if drawing_mask:
|
|
841
|
+
for idx, name in enumerate(node.names):
|
|
842
|
+
if idx < len(drawing_mask) and drawing_mask[idx]:
|
|
843
|
+
self._drawing_tuple_vars.add(name)
|
|
658
844
|
if isinstance(node.value, FuncCall):
|
|
659
845
|
ns, name = _qualified_name(node.value.callee)
|
|
660
846
|
if ns == "ta" and name == "stoch":
|
|
@@ -665,6 +851,40 @@ class SupportChecker:
|
|
|
665
851
|
)
|
|
666
852
|
self._visit_children(node)
|
|
667
853
|
|
|
854
|
+
def _visit_Subscript(self, node: Subscript) -> None:
|
|
855
|
+
# Best-effort defensive guard for the silent-wrong shapes called out in
|
|
856
|
+
# spec L.3/U.7. This is not a full expression type checker; it catches
|
|
857
|
+
# the known dangerous surface (UDT vars with drawing fields, and tuple
|
|
858
|
+
# destructured drawing handles) without trying to prove every nested or
|
|
859
|
+
# parameter-passed UDT expression. Literal [0] is current-bar in Pine and
|
|
860
|
+
# safe to leave to codegen's existing current-value behavior.
|
|
861
|
+
if self._is_literal_zero_subscript(node):
|
|
862
|
+
self._visit_children(node)
|
|
863
|
+
return
|
|
864
|
+
if isinstance(node.object, MemberAccess) and isinstance(node.object.object, Identifier):
|
|
865
|
+
var_name = node.object.object.name
|
|
866
|
+
udt_name = self._var_udt_types.get(var_name)
|
|
867
|
+
if udt_name is not None and node.object.member in self._udt_drawing_fields.get(udt_name, set()):
|
|
868
|
+
self._err(
|
|
869
|
+
node,
|
|
870
|
+
"History references on UDT drawing fields are not supported in PineForge.",
|
|
871
|
+
hint=(
|
|
872
|
+
f"{var_name}.{node.object.member}[...] would be read as the current-bar field, "
|
|
873
|
+
"not prior-bar history. Copy the field to a bare drawing variable first, "
|
|
874
|
+
"or remove the history reference."
|
|
875
|
+
),
|
|
876
|
+
)
|
|
877
|
+
elif isinstance(node.object, Identifier) and node.object.name in self._drawing_tuple_vars:
|
|
878
|
+
self._err(
|
|
879
|
+
node,
|
|
880
|
+
"History references on tuple-destructured drawing handles are not supported in PineForge.",
|
|
881
|
+
hint=(
|
|
882
|
+
f"{node.object.name}[...] is a drawing handle from tuple destructuring; "
|
|
883
|
+
"PineForge cannot historize that tuple element safely."
|
|
884
|
+
),
|
|
885
|
+
)
|
|
886
|
+
self._visit_children(node)
|
|
887
|
+
|
|
668
888
|
def _visit_FuncCall(self, node: FuncCall) -> None:
|
|
669
889
|
ns, name = _qualified_name(node.callee)
|
|
670
890
|
|
|
@@ -686,6 +906,25 @@ class SupportChecker:
|
|
|
686
906
|
self._visit_children(node)
|
|
687
907
|
return
|
|
688
908
|
|
|
909
|
+
# Heikin-Ashi: supported for the chart's OWN symbol only. The engine
|
|
910
|
+
# applies the HA candle transform inside the same-symbol security eval
|
|
911
|
+
# (request.security(ticker.heikinashi(syminfo.tickerid), ...)); a
|
|
912
|
+
# cross-symbol HA construction (an alternate symbol's candles) is not.
|
|
913
|
+
if full == "ticker.heikinashi":
|
|
914
|
+
arg0 = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
915
|
+
if arg0 is None or not self._is_current_symbol_expr(arg0):
|
|
916
|
+
self._err(
|
|
917
|
+
node,
|
|
918
|
+
"ticker.heikinashi(...) is supported only for the chart's own symbol.",
|
|
919
|
+
hint=(
|
|
920
|
+
"Pass syminfo.tickerid: PineForge applies the Heikin-Ashi candle "
|
|
921
|
+
"transform to the chart symbol inside request.security, but cannot "
|
|
922
|
+
"load an alternate symbol's HA candles."
|
|
923
|
+
),
|
|
924
|
+
)
|
|
925
|
+
self._visit_children(node)
|
|
926
|
+
return
|
|
927
|
+
|
|
689
928
|
# Hard rejects by namespace (e.g. ticker.*).
|
|
690
929
|
if ns is not None and ns in HARD_REJECT_NAMESPACE:
|
|
691
930
|
self._err(
|
|
@@ -942,6 +1181,34 @@ class SupportChecker:
|
|
|
942
1181
|
self._visit_children(node)
|
|
943
1182
|
return
|
|
944
1183
|
|
|
1184
|
+
# Drawing-objects-as-data (line/box/label/linefill). Geometry methods
|
|
1185
|
+
# are REAL data; *_NOOP visual setters are accepted no-ops (warned);
|
|
1186
|
+
# any OTHER drawing method rejects loudly. Args carry constant-namespace
|
|
1187
|
+
# members (xloc.*, yloc.*, text.align_*, style consts), so children are
|
|
1188
|
+
# visited with const reads allowed. NOTE: user methods on a drawing var
|
|
1189
|
+
# (egoigor ``ln.slope()``) have ns="ln" (the receiver), NOT a DRAWING_NS,
|
|
1190
|
+
# so they are never gated here.
|
|
1191
|
+
if ns in _DRAWING_SUPPORTED_BY_NS:
|
|
1192
|
+
if name not in _DRAWING_SUPPORTED_BY_NS[ns]:
|
|
1193
|
+
self._err(node, f"{ns}.{name}(...) is not implemented in PineForge runtime.")
|
|
1194
|
+
self._visit_children(node)
|
|
1195
|
+
return
|
|
1196
|
+
if name in _DRAWING_NOOP_BY_NS[ns]:
|
|
1197
|
+
self._warn(
|
|
1198
|
+
node,
|
|
1199
|
+
f"{ns}.{name}(...) is a visual no-op in PineForge backtests "
|
|
1200
|
+
f"(drawing geometry is data; color/style/size are dropped).",
|
|
1201
|
+
)
|
|
1202
|
+
self._visit_children_const_ok(node)
|
|
1203
|
+
return
|
|
1204
|
+
if ns == "chart.point":
|
|
1205
|
+
if name not in SUPPORTED_CHART_POINT:
|
|
1206
|
+
self._err(node, f"chart.point.{name}(...) is not implemented in PineForge runtime.")
|
|
1207
|
+
self._visit_children(node)
|
|
1208
|
+
return
|
|
1209
|
+
self._visit_children_const_ok(node)
|
|
1210
|
+
return
|
|
1211
|
+
|
|
945
1212
|
# Drawing / charting / alert namespaces — codegen drops silently. Warn,
|
|
946
1213
|
# don't error: many strategies include these for the TradingView UI.
|
|
947
1214
|
# Their argument subtrees legitimately carry constant-namespace
|
|
@@ -962,6 +1229,21 @@ class SupportChecker:
|
|
|
962
1229
|
self._visit_children_const_ok(node)
|
|
963
1230
|
return
|
|
964
1231
|
|
|
1232
|
+
# Method call on a scalar visual-container receiver (``panel.cell(...)``
|
|
1233
|
+
# where ``panel`` is a ``table`` param, ``ln.set_xy1(...)`` on a ``line``
|
|
1234
|
+
# var). ``ns`` is the receiver variable/param name, not a namespace, so
|
|
1235
|
+
# the namespace-form drawing/table branches above did not fire. These are
|
|
1236
|
+
# visual sinks whose args legitimately carry visual constants
|
|
1237
|
+
# (``text.align_*``, ``size.*``), so visit children with const reads
|
|
1238
|
+
# allowed instead of tripping the free-expression const-namespace reject.
|
|
1239
|
+
if ns is not None and ns in self._visual_container_vars:
|
|
1240
|
+
self._visit_children_const_ok(node)
|
|
1241
|
+
return
|
|
1242
|
+
|
|
1243
|
+
if ns is None and name in self._visual_sink_funcs:
|
|
1244
|
+
self._visit_children_const_ok(node)
|
|
1245
|
+
return
|
|
1246
|
+
|
|
965
1247
|
self._visit_children(node)
|
|
966
1248
|
|
|
967
1249
|
def _visit_Identifier(self, node: Identifier) -> None:
|
|
@@ -1238,19 +1520,38 @@ class SupportChecker:
|
|
|
1238
1520
|
return False
|
|
1239
1521
|
return node.member in allowed
|
|
1240
1522
|
|
|
1241
|
-
def _is_current_symbol_expr(self, node: ASTNode) -> bool:
|
|
1523
|
+
def _is_current_symbol_expr(self, node: ASTNode, _seen: set[str] | None = None) -> bool:
|
|
1524
|
+
if _seen is None:
|
|
1525
|
+
_seen = set()
|
|
1242
1526
|
chain = _resolve_member_chain(node)
|
|
1243
1527
|
if chain in SECURITY_CURRENT_SYMBOL_NAMES:
|
|
1244
1528
|
return True
|
|
1245
|
-
# ticker.inherit(symbol, ...)
|
|
1246
|
-
#
|
|
1529
|
+
# ticker.inherit(symbol, ...) / ticker.standard(symbol) are passthrough,
|
|
1530
|
+
# and ticker.heikinashi(symbol) is the chart's OWN symbol with a causal
|
|
1531
|
+
# Heikin-Ashi candle transform (the engine applies it inside the security
|
|
1532
|
+
# eval via register_security_eval's heikinashi flag — no alternate symbol
|
|
1533
|
+
# is loaded). All allow a current-symbol first argument; a non-current
|
|
1534
|
+
# arg is genuine cross-symbol construction and still falls through.
|
|
1247
1535
|
if isinstance(node, FuncCall):
|
|
1248
1536
|
ns, fname = _qualified_name(node.callee)
|
|
1249
|
-
if ns == "ticker" and fname in ("inherit", "standard"):
|
|
1250
|
-
if node.args and self._is_current_symbol_expr(node.args[0]):
|
|
1537
|
+
if ns == "ticker" and fname in ("inherit", "standard", "heikinashi"):
|
|
1538
|
+
if node.args and self._is_current_symbol_expr(node.args[0], _seen):
|
|
1251
1539
|
return True
|
|
1252
|
-
if "symbol" in node.kwargs and self._is_current_symbol_expr(node.kwargs["symbol"]):
|
|
1540
|
+
if "symbol" in node.kwargs and self._is_current_symbol_expr(node.kwargs["symbol"], _seen):
|
|
1253
1541
|
return True
|
|
1542
|
+
# A ternary symbol (``cond ? other : syminfo.tickerid``) resolves to the
|
|
1543
|
+
# chart symbol whenever EITHER branch does. PineForge only ever loads the
|
|
1544
|
+
# chart symbol, so this is exact at any config where the chosen branch is
|
|
1545
|
+
# the chart symbol; an unconditional cross-symbol expression still errors.
|
|
1546
|
+
if isinstance(node, Ternary):
|
|
1547
|
+
return (self._is_current_symbol_expr(node.true_val, _seen)
|
|
1548
|
+
or self._is_current_symbol_expr(node.false_val, _seen))
|
|
1549
|
+
# Def-use: resolve a bare identifier through its declaration value so an
|
|
1550
|
+
# aliased symbol is accepted (``haTicker = ticker.heikinashi(...)`` then
|
|
1551
|
+
# ``request.security(haTicker, ...)``). Name-cycle-guarded.
|
|
1552
|
+
if isinstance(node, Identifier) and node.name in self._scalar_defs and node.name not in _seen:
|
|
1553
|
+
_seen.add(node.name)
|
|
1554
|
+
return self._is_current_symbol_expr(self._scalar_defs[node.name], _seen)
|
|
1254
1555
|
return False
|
|
1255
1556
|
|
|
1256
1557
|
# -- Pine timeframe-literal validation --
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "0.
|
|
2
|
+
"codegen": "0.9.0",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "fc828c2a32ba93bbeeffa8e0a4c4696a417a6cc64448c1b70bdce2267cd051d9"
|
|
7
7
|
}
|
package/tables.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"CODEGEN_VERSION": "0.
|
|
2
|
+
"CODEGEN_VERSION": "0.9.0",
|
|
3
3
|
"TA_CLASS_MAP_KEYS": [
|
|
4
4
|
"sma",
|
|
5
5
|
"ema",
|
|
@@ -321,12 +321,8 @@
|
|
|
321
321
|
"plotshape"
|
|
322
322
|
],
|
|
323
323
|
"SKIP_NAMESPACES": [
|
|
324
|
-
"box",
|
|
325
324
|
"chart",
|
|
326
325
|
"display",
|
|
327
|
-
"label",
|
|
328
|
-
"line",
|
|
329
|
-
"linefill",
|
|
330
326
|
"polyline",
|
|
331
327
|
"position",
|
|
332
328
|
"size",
|
|
@@ -581,11 +577,13 @@
|
|
|
581
577
|
],
|
|
582
578
|
"ta.stdev": [
|
|
583
579
|
"source",
|
|
584
|
-
"length"
|
|
580
|
+
"length",
|
|
581
|
+
"biased"
|
|
585
582
|
],
|
|
586
583
|
"ta.variance": [
|
|
587
584
|
"source",
|
|
588
|
-
"length"
|
|
585
|
+
"length",
|
|
586
|
+
"biased"
|
|
589
587
|
],
|
|
590
588
|
"ta.supertrend": [
|
|
591
589
|
"factor",
|
|
@@ -1438,7 +1436,6 @@
|
|
|
1438
1436
|
"request.seed": "External seed data feeds not available in PineForge.",
|
|
1439
1437
|
"request.quandl": "External Quandl data not available in PineForge.",
|
|
1440
1438
|
"request.currency_rate": "Currency conversion data not available in PineForge.",
|
|
1441
|
-
"ticker.heikinashi": "ticker.heikinashi() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1442
1439
|
"ticker.renko": "ticker.renko() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1443
1440
|
"ticker.kagi": "ticker.kagi() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
1444
1441
|
"ticker.linebreak": "ticker.linebreak() chart-type modifier / cross-symbol construction not supported — PineForge runs same-symbol MTF only via request.security with the chart's own tickerid.",
|
|
@@ -1453,8 +1450,7 @@
|
|
|
1453
1450
|
"DIVERGENT_VARS": {
|
|
1454
1451
|
"bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
|
|
1455
1452
|
"last_bar_index": "last_bar_index is aliased to the CURRENT bar index in PineForge codegen (not the index of the last bar); backtest would be silently wrong — rejected.",
|
|
1456
|
-
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
|
|
1457
|
-
"time_close": "time_close is aliased to the bar OPEN timestamp in PineForge; it does not represent the bar close time; backtest would be silently wrong — rejected."
|
|
1453
|
+
"timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
|
|
1458
1454
|
},
|
|
1459
1455
|
"BARSTATE_APPROX_VARS": {
|
|
1460
1456
|
"barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
|
|
@@ -1580,6 +1576,7 @@
|
|
|
1580
1576
|
"backadjustment",
|
|
1581
1577
|
"expression",
|
|
1582
1578
|
"gaps",
|
|
1579
|
+
"ignore_invalid_symbol",
|
|
1583
1580
|
"lookahead",
|
|
1584
1581
|
"settlement_as_close",
|
|
1585
1582
|
"symbol",
|
|
Binary file
|