@pineforge/codegen-pyodide 0.10.0 → 0.10.2

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.
@@ -10,14 +10,11 @@ Buckets:
10
10
  * HARD_REJECT_FUNC / HARD_REJECT_NAMESPACE - calls that have no PineForge
11
11
  semantics at all (e.g. ``request.financial``, ``ticker.*``).
12
12
  * DIVERGENT_VARS - built-in variables whose PineForge value diverges from
13
- TradingView. Most are reported as WARNING (e.g. ``bar_index`` depends on the
14
- data window, ``timenow`` is not wall-clock) — these often appear in visual or
15
- logging code that does not affect trade outcomes. A subset
16
- (DIVERGENT_VARS_ERROR: ``last_bar_index`` aliased to the *current* bar index,
17
- ``time_close`` aliased to the bar *open* timestamp) are silent MIS-ALIASES:
18
- they produce a plausible-looking but wrong value that flows straight into
19
- trade logic, so a backtest would be silently wrong. Those are escalated to
20
- ERROR (rejected) rather than merely warned.
13
+ TradingView. They are reported as WARNING (e.g. ``bar_index`` and
14
+ ``last_bar_index`` depend on the fed data window, ``timenow`` is not
15
+ wall-clock) — these often appear in visual or logging code that does not
16
+ affect trade outcomes. DIVERGENT_VARS_ERROR is reserved for silent
17
+ mis-aliases severe enough to reject; it is currently empty.
21
18
  * NOT_YET - calls the runtime could support but the transpiler does not yet
22
19
  emit (e.g. ``max_bars_back``, bare ``barssince``).
23
20
  * request.security - only ``symbol`` / ``timeframe`` / ``expression`` allowed,
@@ -81,6 +78,9 @@ SUPPORTED_INPUT: frozenset[str] = frozenset(sigs.INPUT_FUNCTIONS)
81
78
  SUPPORTED_ARRAY: frozenset[str] = frozenset(set(ARRAY_METHODS) | {"new", "new_float", "new_int", "new_bool", "new_string", "from"} | set(ARRAY_DRAWING_NEW_CTORS))
82
79
  SUPPORTED_MAP: frozenset[str] = frozenset(set(MAP_METHODS) | {"new"})
83
80
  SUPPORTED_MATRIX: frozenset[str] = frozenset(set(MATRIX_METHODS) | {"new"})
81
+ _SUPPORTED_MAP_VALUE_TYPES: frozenset[str] = frozenset(
82
+ {"float", "int", "bool", "string"}
83
+ )
84
84
  SUPPORTED_SYMINFO: frozenset[str] = frozenset(SYMINFO_MEMBER_MAP)
85
85
  # Drawing-objects-as-data (spec §4.5). Geometry methods are REAL (route to the
86
86
  # per-type arena); *_NOOP visual setters are accepted no-ops (Level.WARNING).
@@ -174,11 +174,11 @@ HARD_REJECT_NAMESPACE: dict[str, str] = {
174
174
  # logging or visual logic that does not affect trade outcomes. The checker still
175
175
  # flags divergence so users see the risk.
176
176
  #
177
- # DIVERGENT_VARS_ERROR is a SUBSET that is escalated to ERROR (rejected): these
178
- # are silent MIS-ALIASES, not merely data-window divergences. They return a
179
- # plausible value that is the WRONG quantity (last_bar_index -> current bar
180
- # index) and that value flows directly into trade logic, so the backtest would
181
- # be silently wrong. A WARNING is not enough.
177
+ # DIVERGENT_VARS_ERROR is a SUBSET reserved for silent MIS-ALIASES that must be
178
+ # escalated to ERROR (rejected), rather than merely warned. It is currently
179
+ # empty: last_bar_index lowers to the true final index of the fed data window,
180
+ # but that index can still differ from TradingView when the window does not
181
+ # cover the same chart history.
182
182
  #
183
183
  # NOTE: the bare ``time_close`` variable is NOT divergent — codegen lowers it to
184
184
  # the engine's ``time_close()`` accessor, which returns the true bar-close
@@ -188,20 +188,32 @@ HARD_REJECT_NAMESPACE: dict[str, str] = {
188
188
  # separate supported builtin handled in visit_call.)
189
189
  DIVERGENT_VARS: dict[str, str] = {
190
190
  "bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
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.",
191
+ "last_bar_index": "last_bar_index is the final index of PineForge's fed data window; it can diverge from TradingView when the window does not cover the same chart history.",
192
192
  "timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time.",
193
193
  }
194
194
 
195
195
  # Subset of DIVERGENT_VARS escalated from WARNING to ERROR (see comment above).
196
- DIVERGENT_VARS_ERROR: frozenset[str] = frozenset({"last_bar_index"})
196
+ DIVERGENT_VARS_ERROR: frozenset[str] = frozenset()
197
197
 
198
198
  BARSTATE_APPROX_VARS: dict[str, str] = {
199
- "barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
199
+ "barstate.islast": (
200
+ "In direct batch chart-scope evaluation, barstate.islast is true only "
201
+ "on the final bar of PineForge's fed chart-data window. Inside a "
202
+ "request.security() history index, PineForge currently selects the "
203
+ "false branch; requested-context last-bar state is not modeled."
204
+ ),
200
205
  "barstate.ishistory": "barstate.ishistory is always true in PineForge batch backtests.",
201
206
  "barstate.isrealtime": "barstate.isrealtime is always false in PineForge batch backtests.",
202
207
  "barstate.isnew": "barstate.isnew follows PineForge first-tick execution state.",
203
208
  "barstate.isconfirmed": "barstate.isconfirmed follows PineForge last-tick execution state.",
204
- "barstate.islastconfirmedhistory": "barstate.islastconfirmedhistory is always false in PineForge batch backtests.",
209
+ "barstate.islastconfirmedhistory": (
210
+ "In direct historical batch chart-scope evaluation, PineForge "
211
+ "approximates barstate.islastconfirmedhistory as true only on the "
212
+ "final bar of the fed chart-data window. Inside a request.security() "
213
+ "history index, PineForge currently selects the false branch; "
214
+ "TradingView's requested-context and realtime-boundary semantics are "
215
+ "not modeled."
216
+ ),
205
217
  }
206
218
 
207
219
  STRATEGY_UNSUPPORTED_PARAMS: dict[str, set[str]] = {
@@ -209,9 +221,13 @@ STRATEGY_UNSUPPORTED_PARAMS: dict[str, set[str]] = {
209
221
  "order": {"comment", "alert_message", "disable_alert", "qty_type"},
210
222
  "exit": {"comment_profit", "comment_loss", "comment_trailing", "alert_message", "alert_profit", "alert_loss", "alert_trailing", "disable_alert"},
211
223
  "close": {"alert_message", "disable_alert"},
212
- "close_all": {"comment", "alert_message", "disable_alert", "immediately"},
224
+ "close_all": {"alert_message", "disable_alert"},
213
225
  }
214
226
 
227
+ # Alert delivery remains outside the backtest runtime contract. Keep these
228
+ # parameters warning-only so strategies that use webhook metadata still run;
229
+ # a follow-up must plumb alert events before codegen may claim support.
230
+
215
231
  # strategy.closedtrades / strategy.opentrades accessor surfaces are NOT
216
232
  # symmetric in Pine v6. opentrades has no exit_* fields (a trade has not
217
233
  # closed yet). Both lack ``direction`` (Pine has ``size`` whose sign carries
@@ -461,6 +477,12 @@ class SupportChecker:
461
477
  # shapes before codegen.
462
478
  self._udt_drawing_fields: dict[str, set[str]] = {}
463
479
  self._var_udt_types: dict[str, str] = {}
480
+ # PineMap IDs require runtime snapshots during COOF rollback. History
481
+ # buffers and generic matrices still value-copy their elements, so
482
+ # map-bearing shapes that would silently retain live aliases are gated
483
+ # until those containers have recursive checkpoint adapters.
484
+ self._udt_field_type_names: dict[str, dict[str, str]] = {}
485
+ self._map_bearing_udts: set[str] = set()
464
486
  # Names (vars and function params) declared as a scalar visual-container
465
487
  # type (table/box/line/label/linefill). A method call on one of these
466
488
  # (``panel.cell(...)``) is a visual sink whose args may carry visual
@@ -494,11 +516,20 @@ class SupportChecker:
494
516
  # then ``request.security(haTicker, ...)``, or ``reqSym = cond ? other :
495
517
  # syminfo.tickerid``. First binding wins (closest to declaration).
496
518
  self._scalar_defs: dict[str, ASTNode] = {}
519
+ # EVERY reassignment value bound to a scalar name (``name := <expr>``
520
+ # and the compound forms). A declaration alone does not pin the value a
521
+ # ``request.security`` symbol argument actually carries: the engine is
522
+ # registered with no symbol at all, so an accepted-but-divergent rebind
523
+ # silently backtests the CHART feed. Collected in a whole-AST pre-pass
524
+ # (below) as well as during the visit, so a rebind that lexically
525
+ # FOLLOWS the request.security call cannot slip past.
526
+ self._scalar_rebinds: dict[str, list[ASTNode]] = {}
497
527
 
498
528
  # -- Public API --
499
529
 
500
530
  def check(self) -> list[Diagnostic]:
501
531
  self._collect_user_definitions(self._ast)
532
+ self._collect_scalar_rebinds(self._ast)
502
533
  for stmt in self._ast.body:
503
534
  self._visit(stmt)
504
535
  return self._diagnostics
@@ -511,10 +542,50 @@ class SupportChecker:
511
542
 
512
543
  # -- Setup --
513
544
 
545
+ def _record_scalar_rebind(self, node: Assignment) -> None:
546
+ """Remember ``name := <expr>`` (and the compound forms) by name.
547
+
548
+ A compound op (``+=``, ``*=``, …) records its right-hand operand, which
549
+ is never a current-symbol expression — so a string-built symbol is
550
+ rejected by the same rule that rejects a plain divergent ``:=``.
551
+ """
552
+ target = node.target
553
+ if not isinstance(target, Identifier) or node.value is None:
554
+ return
555
+ recorded = self._scalar_rebinds.setdefault(target.name, [])
556
+ if not any(value is node.value for value in recorded):
557
+ recorded.append(node.value)
558
+
559
+ def _collect_scalar_rebinds(self, node: ASTNode) -> None:
560
+ """Pre-pass: collect every scalar rebind anywhere in the AST.
561
+
562
+ The visit walks top-level statements in source order, so a rebind that
563
+ FOLLOWS a ``request.security`` call would otherwise never be seen by
564
+ that call's symbol check. Collecting up front is strictly fail-closed:
565
+ it can only add rejections, never remove one.
566
+ """
567
+ if isinstance(node, Assignment):
568
+ self._record_scalar_rebind(node)
569
+ for value in vars(node).values():
570
+ if isinstance(value, ASTNode):
571
+ self._collect_scalar_rebinds(value)
572
+ elif isinstance(value, list):
573
+ for item in value:
574
+ if isinstance(item, ASTNode):
575
+ self._collect_scalar_rebinds(item)
576
+ elif isinstance(value, dict):
577
+ for item in value.values():
578
+ if isinstance(item, ASTNode):
579
+ self._collect_scalar_rebinds(item)
580
+
514
581
  def _collect_user_definitions(self, ast: Program) -> None:
515
582
  for stmt in ast.body:
516
583
  if isinstance(stmt, TypeDecl):
517
584
  self._user_types.add(stmt.name)
585
+ self._udt_field_type_names[stmt.name] = {
586
+ field.name: str(field.type_name or "").replace(" ", "")
587
+ for field in stmt.fields
588
+ }
518
589
  drawing_fields = {
519
590
  field.name
520
591
  for field in stmt.fields
@@ -536,6 +607,140 @@ class SupportChecker:
536
607
  self._user_methods.add(stmt.name)
537
608
  self._collect_visual_container_params(stmt)
538
609
 
610
+ # Resolve direct and transitively nested map-bearing UDTs. Pine type
611
+ # declarations are normally dependency ordered, but the fixed point
612
+ # also keeps this guard deterministic for synthetic test ASTs.
613
+ changed = True
614
+ while changed:
615
+ changed = False
616
+ for udt_name, fields in self._udt_field_type_names.items():
617
+ bearing_fields = {
618
+ field_name
619
+ for field_name, type_name in fields.items()
620
+ if "map<" in type_name
621
+ or any(
622
+ type_name == nested
623
+ or f"<{nested}>" in type_name
624
+ or f"<{nested}," in type_name
625
+ or f",{nested}>" in type_name
626
+ for nested in self._map_bearing_udts
627
+ )
628
+ }
629
+ if bearing_fields:
630
+ if udt_name not in self._map_bearing_udts:
631
+ self._map_bearing_udts.add(udt_name)
632
+ changed = True
633
+
634
+ # Constructor checks alone are insufficient: a typed ``na``
635
+ # declaration, UDT field, or unused callable parameter can introduce
636
+ # the same unsupported runtime shape without ever calling map.new() or
637
+ # matrix.new(). Validate every declared type boundary after the UDT
638
+ # fixed point is known so unsupported shapes fail with a Pine source
639
+ # diagnostic instead of a generated-C++ static_assert.
640
+ for stmt in ast.body:
641
+ if isinstance(stmt, TypeDecl):
642
+ for field in stmt.fields:
643
+ self._validate_declared_type_hint(
644
+ stmt,
645
+ field.type_name,
646
+ context=f"field '{stmt.name}.{field.name}'",
647
+ )
648
+ elif isinstance(stmt, (FuncDef, MethodDef)):
649
+ hints = (
650
+ (getattr(stmt, "annotations", None) or {})
651
+ .get("param_type_hints")
652
+ or []
653
+ )
654
+ for index, param_name in enumerate(stmt.params):
655
+ hint = hints[index] if index < len(hints) else None
656
+ if hint:
657
+ self._validate_declared_type_hint(
658
+ stmt,
659
+ hint,
660
+ context=f"parameter '{param_name}'",
661
+ )
662
+
663
+ @staticmethod
664
+ def _split_declared_type_args(text: str) -> list[str]:
665
+ """Split ``K,V`` while preserving nested generic arguments."""
666
+ args: list[str] = []
667
+ current: list[str] = []
668
+ depth = 0
669
+ for char in text:
670
+ if char == "<":
671
+ depth += 1
672
+ elif char == ">":
673
+ depth -= 1
674
+ if char == "," and depth == 0:
675
+ args.append("".join(current))
676
+ current = []
677
+ else:
678
+ current.append(char)
679
+ if current:
680
+ args.append("".join(current))
681
+ return args
682
+
683
+ def _validate_declared_type_hint(
684
+ self,
685
+ node: ASTNode,
686
+ type_name: str | None,
687
+ *,
688
+ context: str,
689
+ ) -> None:
690
+ """Fail closed for runtime collection limits at every type boundary."""
691
+ if not type_name:
692
+ return
693
+ compact = str(type_name).replace(" ", "")
694
+ if compact.endswith("[]"):
695
+ compact = f"array<{compact[:-2]}>"
696
+
697
+ if compact.startswith("map<") and compact.endswith(">"):
698
+ parts = self._split_declared_type_args(compact[4:-1])
699
+ if len(parts) != 2:
700
+ return
701
+ key_type, value_type = parts
702
+ if key_type != "string":
703
+ self._err(
704
+ node,
705
+ f"{context}: map keys must be string in PineForge's "
706
+ "supported map subset.",
707
+ )
708
+ if value_type not in _SUPPORTED_MAP_VALUE_TYPES:
709
+ self._err(
710
+ node,
711
+ f"{context}: map values must be primitive in PineForge's "
712
+ "supported map subset.",
713
+ )
714
+ return
715
+
716
+ if compact.startswith("matrix<") and compact.endswith(">"):
717
+ element_type = compact[len("matrix<"):-1]
718
+ if element_type in self._map_bearing_udts:
719
+ self._err(
720
+ node,
721
+ f"matrix<{element_type}> is not supported when the UDT "
722
+ "contains a map field; rollback would retain live map "
723
+ "aliases.",
724
+ hint="Use array<UDT> for recursively checkpointed state.",
725
+ )
726
+ return
727
+ if "<" in element_type:
728
+ self._err(
729
+ node,
730
+ f"matrix<{element_type}>: nested collection element types "
731
+ "not supported in v1.",
732
+ )
733
+ return
734
+
735
+ # Arrays are recursively checkpointed and may contain valid maps, but
736
+ # the nested map still has the same key/value boundary.
737
+ if compact.startswith("array<") and compact.endswith(">"):
738
+ self._validate_declared_type_hint(
739
+ node,
740
+ compact[len("array<"):-1],
741
+ context=context,
742
+ )
743
+
539
744
  def _collect_visual_container_params(self, fn) -> None:
540
745
  """Register a function's parameters that are declared with a scalar
541
746
  visual-container type (``table panel``, ``line ln``) so method calls on
@@ -814,6 +1019,12 @@ class SupportChecker:
814
1019
  # so a direct ``dash.cell(..., text.align_left)`` is treated as a visual
815
1020
  # sink (mirrors the table/box/line/label/linefill PARAM tracking).
816
1021
  decl_hint = str(node.type_hint).replace(" ", "") if node.type_hint else None
1022
+ if decl_hint:
1023
+ self._validate_declared_type_hint(
1024
+ node,
1025
+ decl_hint,
1026
+ context=f"variable '{node.name}'",
1027
+ )
817
1028
  if decl_hint in _VISUAL_CONTAINER_TYPES:
818
1029
  self._visual_container_vars.add(node.name)
819
1030
  elif isinstance(node.value, FuncCall):
@@ -835,6 +1046,14 @@ class SupportChecker:
835
1046
  )
836
1047
  self._visit_children(node)
837
1048
 
1049
+ def _visit_Assignment(self, node: Assignment) -> None:
1050
+ # ``_scalar_defs`` records only the DECLARATION, so a later ``:=``
1051
+ # rebind used to be invisible to the request.security symbol check.
1052
+ # (``check`` also pre-collects these; recording here keeps the visit
1053
+ # self-consistent for any caller that drives visitors directly.)
1054
+ self._record_scalar_rebind(node)
1055
+ self._visit_children(node)
1056
+
838
1057
  def _visit_TupleAssign(self, node: TupleAssign) -> None:
839
1058
  drawing_mask = self._tuple_assign_drawing_mask(node.value)
840
1059
  if drawing_mask:
@@ -1160,7 +1379,7 @@ class SupportChecker:
1160
1379
  targs = [str(t).replace(" ", "") for t in targs]
1161
1380
  if targs and targs[0] != "string":
1162
1381
  self._err(node, "map keys must be string in PineForge's supported map subset.")
1163
- if len(targs) > 1 and targs[1] not in {"float", "int", "bool", "string"}:
1382
+ if len(targs) > 1 and targs[1] not in _SUPPORTED_MAP_VALUE_TYPES:
1164
1383
  self._err(node, "map values must be primitive in PineForge's supported map subset.")
1165
1384
  if ns == "matrix" and name == "new":
1166
1385
  targs = (getattr(node.callee, "annotations", None) or {}).get("template_args") or []
@@ -1172,6 +1391,15 @@ class SupportChecker:
1172
1391
  self._visit_children(node)
1173
1392
  return
1174
1393
  allowed_prim = {"float", "int", "bool", "string", "color"}
1394
+ if t in self._map_bearing_udts:
1395
+ self._err(
1396
+ node,
1397
+ f"matrix<{t}> is not supported when the UDT contains "
1398
+ "a map field; rollback would retain live map aliases.",
1399
+ hint="Use array<UDT> for recursively checkpointed state.",
1400
+ )
1401
+ self._visit_children(node)
1402
+ return
1175
1403
  if t not in allowed_prim and t not in self._user_types:
1176
1404
  self._err(node, f"matrix<{t}> element type not supported. Allowed: float, int, bool, string, color, or a declared UDT.")
1177
1405
  self._visit_children(node)
@@ -1548,10 +1776,21 @@ class SupportChecker:
1548
1776
  or self._is_current_symbol_expr(node.false_val, _seen))
1549
1777
  # Def-use: resolve a bare identifier through its declaration value so an
1550
1778
  # 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)
1779
+ # ``request.security(haTicker, ...)``). Every ``:=`` rebind of the name
1780
+ # must resolve to the chart symbol too: the declaration alone does not
1781
+ # pin the value the call actually receives. Name-cycle-guarded.
1782
+ if isinstance(node, Identifier) and node.name not in _seen:
1783
+ definition = self._scalar_defs.get(node.name)
1784
+ rebinds = self._scalar_rebinds.get(node.name)
1785
+ if definition is not None or rebinds:
1786
+ _seen.add(node.name)
1787
+ if rebinds and not all(
1788
+ self._is_current_symbol_expr(value, _seen) for value in rebinds
1789
+ ):
1790
+ return False
1791
+ if definition is None:
1792
+ return False
1793
+ return self._is_current_symbol_expr(definition, _seen)
1555
1794
  return False
1556
1795
 
1557
1796
  # -- Pine timeframe-literal validation --
@@ -56,9 +56,74 @@ class TypeSpec:
56
56
  return f"array<{self.element}>"
57
57
  if self.kind == "map" and self.key is not None and self.value is not None:
58
58
  return f"map<{self.key},{self.value}>"
59
+ if self.kind == "matrix" and self.element is not None:
60
+ return f"matrix<{self.element}>"
59
61
  return self.name or self.kind
60
62
 
61
63
 
64
+ def method_receiver_type_name(spec: TypeSpec | None) -> str | None:
65
+ """Canonical Pine type name used to key a user method receiver.
66
+
67
+ ``MethodDef.type_name`` is the parser-normalized spelling (for example
68
+ ``array<int>``). Calls are resolved from structured ``TypeSpec`` values,
69
+ so both sides need one lossless spelling across primitive, UDT, and
70
+ collection receivers. Incomplete collection specs stay unresolved rather
71
+ than accidentally colliding under a coarse ``array``/``map``/``matrix``
72
+ key.
73
+ """
74
+
75
+ if spec is None:
76
+ return None
77
+ if spec.kind in {"primitive", "udt"}:
78
+ return spec.name
79
+ if spec.kind == "array" and spec.element is not None:
80
+ element = method_receiver_type_name(spec.element)
81
+ return f"array<{element}>" if element is not None else None
82
+ if spec.kind == "map" and spec.key is not None and spec.value is not None:
83
+ key = method_receiver_type_name(spec.key)
84
+ value = method_receiver_type_name(spec.value)
85
+ if key is not None and value is not None:
86
+ return f"map<{key},{value}>"
87
+ return None
88
+ if spec.kind == "matrix" and spec.element is not None:
89
+ element = method_receiver_type_name(spec.element)
90
+ return f"matrix<{element}>" if element is not None else None
91
+ return None
92
+
93
+
94
+ def method_receiver_cpp_token(
95
+ spec: TypeSpec | None,
96
+ fallback_name: str | None = None,
97
+ ) -> str:
98
+ """Return a stable C++ identifier fragment for a method receiver type.
99
+
100
+ Existing primitive, UDT, and drawing receiver spellings remain unchanged.
101
+ Generic Pine types cannot be embedded directly in an identifier, so their
102
+ structure is encoded recursively (``array<int>`` -> ``array_int``). The
103
+ fallback only supports older/synthetic ``FuncInfo`` records that lack the
104
+ structured receiver spec.
105
+ """
106
+
107
+ if spec is not None:
108
+ if spec.kind in {"primitive", "udt"} and spec.name:
109
+ return spec.name
110
+ if spec.kind == "array" and spec.element is not None:
111
+ return f"array_{method_receiver_cpp_token(spec.element)}"
112
+ if spec.kind == "map" and spec.key is not None and spec.value is not None:
113
+ return (
114
+ f"map_{method_receiver_cpp_token(spec.key)}_"
115
+ f"{method_receiver_cpp_token(spec.value)}"
116
+ )
117
+ if spec.kind == "matrix" and spec.element is not None:
118
+ return f"matrix_{method_receiver_cpp_token(spec.element)}"
119
+
120
+ raw = fallback_name or "receiver"
121
+ token = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in raw)
122
+ if token and token[0].isdigit():
123
+ token = f"type_{token}"
124
+ return token or "receiver"
125
+
126
+
62
127
  @dataclass
63
128
  class Symbol:
64
129
  name: str
Binary file
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "codegen": "0.10.0",
2
+ "codegen": "0.10.2",
3
3
  "pyodide": "314.0.0",
4
4
  "python": "3.14.0",
5
5
  "emscripten": "emscripten_5_0_3",
6
- "sha256": "66102b3b3eb6f1e118de1a816f9b56a73311c0340a0a208fd97241f568b43fb1"
6
+ "sha256": "97ebb8d0b14c32d3c88b9521c8f13150ee9cb9e3859227af970ec4dcbfa6bf71"
7
7
  }
package/tables.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "CODEGEN_VERSION": "0.10.0",
2
+ "CODEGEN_VERSION": "0.10.2",
3
3
  "TA_CLASS_MAP_KEYS": [
4
4
  "sma",
5
5
  "ema",
@@ -861,14 +861,14 @@
861
861
  "qty",
862
862
  "qty_percent",
863
863
  "alert_message",
864
- "disable_alert",
865
- "immediately"
864
+ "immediately",
865
+ "disable_alert"
866
866
  ],
867
867
  "strategy.close_all": [
868
868
  "comment",
869
869
  "alert_message",
870
- "disable_alert",
871
- "immediately"
870
+ "immediately",
871
+ "disable_alert"
872
872
  ],
873
873
  "strategy.cancel": [
874
874
  "id"
@@ -1449,16 +1449,16 @@
1449
1449
  },
1450
1450
  "DIVERGENT_VARS": {
1451
1451
  "bar_index": "bar_index depends on the data window; PineForge and TradingView produce different values for the same script.",
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.",
1452
+ "last_bar_index": "last_bar_index is the final index of PineForge's fed data window; it can diverge from TradingView when the window does not cover the same chart history.",
1453
1453
  "timenow": "timenow is aliased to the current bar timestamp in PineForge; it is not real wall-clock time."
1454
1454
  },
1455
1455
  "BARSTATE_APPROX_VARS": {
1456
- "barstate.islast": "barstate.islast is always false in PineForge batch backtests.",
1456
+ "barstate.islast": "In direct batch chart-scope evaluation, barstate.islast is true only on the final bar of PineForge's fed chart-data window. Inside a request.security() history index, PineForge currently selects the false branch; requested-context last-bar state is not modeled.",
1457
1457
  "barstate.ishistory": "barstate.ishistory is always true in PineForge batch backtests.",
1458
1458
  "barstate.isrealtime": "barstate.isrealtime is always false in PineForge batch backtests.",
1459
1459
  "barstate.isnew": "barstate.isnew follows PineForge first-tick execution state.",
1460
1460
  "barstate.isconfirmed": "barstate.isconfirmed follows PineForge last-tick execution state.",
1461
- "barstate.islastconfirmedhistory": "barstate.islastconfirmedhistory is always false in PineForge batch backtests."
1461
+ "barstate.islastconfirmedhistory": "In direct historical batch chart-scope evaluation, PineForge approximates barstate.islastconfirmedhistory as true only on the final bar of the fed chart-data window. Inside a request.security() history index, PineForge currently selects the false branch; TradingView's requested-context and realtime-boundary semantics are not modeled."
1462
1462
  },
1463
1463
  "STRATEGY_UNSUPPORTED_PARAMS": {
1464
1464
  "entry": [
@@ -1487,9 +1487,7 @@
1487
1487
  ],
1488
1488
  "close_all": [
1489
1489
  "alert_message",
1490
- "comment",
1491
- "disable_alert",
1492
- "immediately"
1490
+ "disable_alert"
1493
1491
  ]
1494
1492
  },
1495
1493
  "CLOSED_TRADE_ACCESSOR_METHODS": [
Binary file