@pineforge/codegen-pyodide 0.10.4 → 1.0.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.
Files changed (50) hide show
  1. package/README.md +16 -16
  2. package/glue.py +24 -16
  3. package/package.json +1 -1
  4. package/pineforge_codegen/__init__.py +125 -34
  5. package/pineforge_codegen/analyzer/__init__.py +2 -0
  6. package/pineforge_codegen/analyzer/base.py +754 -76
  7. package/pineforge_codegen/analyzer/call_handlers.py +260 -40
  8. package/pineforge_codegen/analyzer/contracts.py +37 -0
  9. package/pineforge_codegen/analyzer/diagnostics.py +30 -4
  10. package/pineforge_codegen/analyzer/tables.py +49 -8
  11. package/pineforge_codegen/analyzer/types.py +33 -1
  12. package/pineforge_codegen/ast_nodes.py +32 -1
  13. package/pineforge_codegen/block_locals.py +185 -0
  14. package/pineforge_codegen/builtin_keywords.py +42 -0
  15. package/pineforge_codegen/codegen/base.py +896 -156
  16. package/pineforge_codegen/codegen/constant_fold.py +131 -0
  17. package/pineforge_codegen/codegen/drawing.py +221 -79
  18. package/pineforge_codegen/codegen/emit_top.py +946 -213
  19. package/pineforge_codegen/codegen/helpers.py +435 -14
  20. package/pineforge_codegen/codegen/host_members.py +162 -0
  21. package/pineforge_codegen/codegen/input.py +252 -85
  22. package/pineforge_codegen/codegen/security.py +4372 -377
  23. package/pineforge_codegen/codegen/session_market.py +71 -0
  24. package/pineforge_codegen/codegen/ta.py +1188 -100
  25. package/pineforge_codegen/codegen/tables.py +193 -71
  26. package/pineforge_codegen/codegen/tv_number_format.py +270 -0
  27. package/pineforge_codegen/codegen/types.py +1882 -78
  28. package/pineforge_codegen/codegen/visit_call.py +920 -131
  29. package/pineforge_codegen/codegen/visit_expr.py +738 -57
  30. package/pineforge_codegen/codegen/visit_stmt.py +595 -49
  31. package/pineforge_codegen/external_requests.py +877 -0
  32. package/pineforge_codegen/lexer.py +104 -22
  33. package/pineforge_codegen/library_inline.py +1304 -0
  34. package/pineforge_codegen/library_modules.py +126 -0
  35. package/pineforge_codegen/library_v5.py +683 -0
  36. package/pineforge_codegen/limits.py +138 -0
  37. package/pineforge_codegen/method_binding.py +33 -0
  38. package/pineforge_codegen/parser.py +384 -68
  39. package/pineforge_codegen/pine_libraries.py +266 -0
  40. package/pineforge_codegen/pine_spelling.py +216 -0
  41. package/pineforge_codegen/pragmas.py +64 -10
  42. package/pineforge_codegen/security_contexts.py +1585 -0
  43. package/pineforge_codegen/session_reads.py +84 -0
  44. package/pineforge_codegen/signatures.py +48 -23
  45. package/pineforge_codegen/support_checker.py +1106 -85
  46. package/pineforge_codegen-1.0.0.tar.gz +0 -0
  47. package/release.json +2 -2
  48. package/tables.json +23 -21
  49. package/transpile.worker.mjs +24 -16
  50. package/pineforge_codegen-0.10.4.tar.gz +0 -0
@@ -0,0 +1,84 @@
1
+ """The ``session.*`` flag reads at a history offset that generated C++ holds.
2
+
3
+ ``session.ismarket[1]`` and the other ``session.<flag>[k]`` reads need state:
4
+ a Series per flag at the top level, one per call site inside a function
5
+ (codegen). A read the codegen never emits needs none: it sits in a call the
6
+ codegen skips with all its arguments (``plot``, ``alert``, the ``table.*``
7
+ namespace, ...: codegen/tables.py ``SKIP_FUNC_NAMES`` / ``SKIP_NAMESPACES``,
8
+ and ``max_bars_back``) or in a ``strategy.*`` parameter it drops
9
+ (support_checker.py ``STRATEGY_UNSUPPORTED_PARAMS``, e.g. ``alert_message``).
10
+ The analyzer (which functions may be emitted once per call site) and the
11
+ codegen (which reads get a Series) ask this module the same question. The
12
+ C++ decides which of those functions get clones: ``pineforge_codegen._generate``
13
+ clones only the functions whose reads a pass's C++ holds, so a read it
14
+ counts that the codegen leaves out some other way (it renders the argument,
15
+ then drops it: ``color.from_gradient``, a drawing's xloc) costs nothing, or an
16
+ unused Series in a function cloned for another read. One it leaves out that
17
+ the codegen does emit finds no Series and is refused, never given shared
18
+ state.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from .ast_nodes import FuncCall, Identifier, MemberAccess, Subscript
24
+ from .limits import syntax_children
25
+ from .signatures import SESSION_FLAG_MEMBERS, get_param_names
26
+
27
+
28
+ def is_session_history(node) -> bool:
29
+ """Whether ``node`` is a ``session.*`` flag read at an offset
30
+ (``session.ismarket[1]``)."""
31
+ return (isinstance(node, Subscript)
32
+ and isinstance(node.object, MemberAccess)
33
+ and isinstance(node.object.object, Identifier)
34
+ and node.object.object.name == "session"
35
+ and node.object.member in SESSION_FLAG_MEMBERS)
36
+
37
+
38
+ def _dropped_arguments(call: FuncCall) -> list:
39
+ """The arguments of ``call`` the codegen never visits: all of a skipped
40
+ call's, a ``strategy.*`` call's unsupported parameters."""
41
+ # Imported here: the codegen package imports the analyzer, which imports
42
+ # this module.
43
+ from .codegen.tables import SKIP_FUNC_NAMES, SKIP_NAMESPACES, SKIP_VAR_TYPES
44
+ from .support_checker import STRATEGY_UNSUPPORTED_PARAMS
45
+
46
+ callee = call.callee
47
+ everything = [*call.args, *call.kwargs.values()]
48
+ if isinstance(callee, Identifier):
49
+ if callee.name in SKIP_FUNC_NAMES or callee.name == "max_bars_back":
50
+ return everything
51
+ return []
52
+ if not (isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier)):
53
+ return []
54
+ namespace, name = callee.object.name, callee.member
55
+ if namespace in SKIP_NAMESPACES or namespace in SKIP_VAR_TYPES:
56
+ return everything
57
+ if namespace == "strategy" and name in STRATEGY_UNSUPPORTED_PARAMS:
58
+ dropped = STRATEGY_UNSUPPORTED_PARAMS[name]
59
+ params = get_param_names("strategy", name) or []
60
+ return ([value for key, value in call.kwargs.items() if key in dropped]
61
+ + [arg for index, arg in enumerate(call.args)
62
+ if index < len(params) and params[index] in dropped])
63
+ return []
64
+
65
+
66
+ def emitted_session_reads(root, skip: frozenset[int] | set[int] = frozenset()) -> list[Subscript]:
67
+ """Every ``session.<flag>[k]`` under ``root`` the codegen emits, leaving
68
+ out the subtrees whose node id is in ``skip`` (a request.security
69
+ expression, whose reads keep the requested clock's history)."""
70
+ reads: list[Subscript] = []
71
+ stack = [root]
72
+ while stack:
73
+ node = stack.pop()
74
+ if node is None or id(node) in skip:
75
+ continue
76
+ if is_session_history(node):
77
+ reads.append(node)
78
+ children = list(syntax_children(node))
79
+ if isinstance(node, FuncCall):
80
+ dropped = {id(arg) for arg in _dropped_arguments(node)}
81
+ if dropped:
82
+ children = [child for child in children if id(child) not in dropped]
83
+ stack.extend(children)
84
+ return reads
@@ -79,7 +79,7 @@ def _func(name: str, *sigs: FuncSig) -> IntrinsicFunc:
79
79
 
80
80
  # Shortcuts for common param types
81
81
  F = PineType.FLOAT
82
- I = PineType.INT
82
+ I = PineType.INT # noqa: E741 - signature table uses single-letter type aliases
83
83
  B = PineType.BOOL
84
84
  S = PineType.STRING
85
85
  NA = PineType.NA
@@ -95,6 +95,12 @@ TA_FUNCTIONS: dict[str, IntrinsicFunc] = {}
95
95
  def _ta(short_name: str, *sigs: FuncSig) -> None:
96
96
  TA_FUNCTIONS[short_name] = _func(f"ta.{short_name}", *sigs)
97
97
 
98
+ # Parameter names, order and the required/optional split are TradingView's
99
+ # (Pine v6 reference): the support checker binds every ta.* call to one of
100
+ # these signatures and refuses an argument that binds to no parameter
101
+ # (``_check_ta_arguments``). A default marks an optional parameter; None, a
102
+ # required one.
103
+
98
104
  # --- Moving Averages ---
99
105
  _ta("sma", _sig([("source", F), ("length", I)]))
100
106
  _ta("ema", _sig([("source", F), ("length", I)]))
@@ -102,9 +108,10 @@ _ta("rma", _sig([("source", F), ("length", I)]))
102
108
  _ta("wma", _sig([("source", F), ("length", I)]))
103
109
  _ta("hma", _sig([("source", F), ("length", I)]))
104
110
  _ta("vwma", _sig([("source", F), ("length", I)]))
105
- _ta("alma", _sig([("source", F), ("length", I), ("offset", F, 0.85), ("sigma", F, 6.0)]))
111
+ _ta("alma", _sig([("series", F), ("length", I), ("offset", F), ("sigma", F),
112
+ ("floor", B, False)]))
106
113
  _ta("swma", _sig([("source", F)]))
107
- _ta("linreg", _sig([("source", F), ("length", I), ("offset", I, 0)]))
114
+ _ta("linreg", _sig([("source", F), ("length", I), ("offset", I)]))
108
115
 
109
116
  # --- Oscillators ---
110
117
  _ta("rsi", _sig([("source", F), ("length", I)]))
@@ -113,7 +120,7 @@ _ta("cci", _sig([("source", F), ("length", I)]))
113
120
  _ta("mfi", _sig([("series", F), ("length", I)]))
114
121
  _ta("mom", _sig([("source", F), ("length", I)]))
115
122
  _ta("roc", _sig([("source", F), ("length", I)]))
116
- _ta("cmo", _sig([("source", F), ("length", I)]))
123
+ _ta("cmo", _sig([("series", F), ("length", I)]))
117
124
  _ta("tsi", _sig([("source", F), ("short_length", I), ("long_length", I)]))
118
125
  _ta("wpr", _sig([("length", I)]))
119
126
  _ta("cog", _sig([("source", F), ("length", I)]))
@@ -123,12 +130,12 @@ _ta("macd", _sig([("source", F), ("fastlen", I), ("slowlen", I), ("siglen", I
123
130
  returns_tuple=True, tuple_count=3))
124
131
 
125
132
  # --- Bollinger Bands / Keltner ---
126
- _ta("bb", _sig([("source", F), ("length", I), ("mult", F, 2.0)],
133
+ _ta("bb", _sig([("series", F), ("length", I), ("mult", F)],
127
134
  returns_tuple=True, tuple_count=3))
128
- _ta("kc", _sig([("source", F), ("length", I), ("mult", F, 1.5)],
135
+ _ta("kc", _sig([("series", F), ("length", I), ("mult", F), ("useTrueRange", B, True)],
129
136
  returns_tuple=True, tuple_count=3))
130
- _ta("bbw", _sig([("source", F), ("length", I), ("mult", F, 2.0)]))
131
- _ta("kcw", _sig([("source", F), ("length", I), ("mult", F, 1.5)]))
137
+ _ta("bbw", _sig([("series", F), ("length", I), ("mult", F)]))
138
+ _ta("kcw", _sig([("series", F), ("length", I), ("mult", F), ("useTrueRange", B, True)]))
132
139
 
133
140
  # --- Volatility & Range ---
134
141
  _ta("atr", _sig([("length", I)]))
@@ -141,7 +148,7 @@ _ta("supertrend", _sig([("factor", F), ("atrPeriod", I)],
141
148
  returns_tuple=True, tuple_count=2))
142
149
  _ta("dmi", _sig([("diLength", I), ("adxSmoothing", I)],
143
150
  returns_tuple=True, tuple_count=3))
144
- _ta("sar", _sig([("start", F, 0.02), ("inc", F, 0.02), ("max", F, 0.2)]))
151
+ _ta("sar", _sig([("start", F), ("inc", F), ("max", F)]))
145
152
 
146
153
  # --- Crossover / State ---
147
154
  _ta("crossover", _sig([("source1", F), ("source2", F)], ret=B))
@@ -161,8 +168,14 @@ _ta("highest",
161
168
  _ta("lowest",
162
169
  _sig([("source", F), ("length", I)]),
163
170
  _sig([("length", I)]))
164
- _ta("highestbars", _sig([("source", F), ("length", I)], ret=I))
165
- _ta("lowestbars", _sig([("source", F), ("length", I)], ret=I))
171
+ # ta.highestbars / ta.lowestbars: "One arg version: length is the number of
172
+ # bars back. Algorithm uses high [low] as a source series."
173
+ _ta("highestbars",
174
+ _sig([("source", F), ("length", I)], ret=I),
175
+ _sig([("length", I)], ret=I))
176
+ _ta("lowestbars",
177
+ _sig([("source", F), ("length", I)], ret=I),
178
+ _sig([("length", I)], ret=I))
166
179
  _ta("median", _sig([("source", F), ("length", I)]))
167
180
  _ta("percentrank", _sig([("source", F), ("length", I)]))
168
181
  _ta("percentile_nearest_rank", _sig([("source", F), ("length", I), ("percentage", F)]))
@@ -180,14 +193,17 @@ _ta("pivotlow",
180
193
  _sig([("leftbars", I), ("rightbars", I)]))
181
194
 
182
195
  # --- Volume indicators ---
183
- # ta.vwap has TWO overloads:
184
- # ta.vwap(source, anchor, stdev_mult) — 3-arg bands form (primary), returns [vwap, upper, lower]
185
- # ta.vwap(source) — scalar 1-arg form (daily anchor)
186
- # Note: primary is the 3-arg form so param_names covers all kwargs; the
187
- # analyzer remaps 3-arg calls to the internal "vwap_bands" dispatch key.
188
- _ta("vwap", _sig([("source", F), ("anchor", B), ("stdev_mult", F)],
196
+ # ta.vwap has TWO overloads (TradingView):
197
+ # ta.vwap(source, anchor, stdev_mult) — bands form (primary), returns [vwap, upper, lower]
198
+ # ta.vwap(source, anchor) — scalar form
199
+ # ``anchor`` is optional in both ("The default is equivalent to passing
200
+ # timeframe.change() with "1D" as its argument"). The primary is the widest
201
+ # form so param_names covers every keyword; the analyzer remaps the bands
202
+ # form to the internal "vwap_bands" dispatch key.
203
+ _VWAP_DAILY_ANCHOR = 'timeframe.change("1D")'
204
+ _ta("vwap", _sig([("source", F), ("anchor", B, _VWAP_DAILY_ANCHOR), ("stdev_mult", F)],
189
205
  returns_tuple=True, tuple_count=3),
190
- _sig([("source", F)]))
206
+ _sig([("source", F), ("anchor", B, _VWAP_DAILY_ANCHOR)]))
191
207
 
192
208
  # --- Statistical (additional) ---
193
209
  _ta("mode", _sig([("source", F), ("length", I)]))
@@ -337,7 +353,7 @@ _str("tostring",
337
353
  _sig([("value", F)], ret=S),
338
354
  _sig([("value", F), ("format", S)], ret=S))
339
355
  _str("tonumber", _sig([("string", S)], ret=F))
340
- _str("format", _sig([("formatStr", S)], ret=S)) # variadic
356
+ _str("format", _sig([("formatString", S)], ret=S)) # variadic
341
357
  _str("format_time", _sig([("time", I), ("format", S), ("timezone", S, "")], ret=S))
342
358
  _str("length", _sig([("string", S)], ret=I))
343
359
  _str("contains", _sig([("source", S), ("str", S)], ret=B))
@@ -352,7 +368,7 @@ _str("replace_all", _sig([("source", S), ("target", S), ("replacement", S)], ret
352
368
  _str("lower", _sig([("source", S)], ret=S))
353
369
  _str("upper", _sig([("source", S)], ret=S))
354
370
  _str("trim", _sig([("source", S)], ret=S))
355
- _str("repeat", _sig([("source", S), ("count", I)], ret=S))
371
+ _str("repeat", _sig([("source", S), ("repeat", I), ("separator", S, "")], ret=S))
356
372
  _str("match", _sig([("source", S), ("regex", S)], ret=S))
357
373
  _str("split", _sig([("string", S), ("separator", S)], ret=S)) # returns array<string>
358
374
 
@@ -403,9 +419,9 @@ def _builtin(name: str, *sigs: FuncSig) -> None:
403
419
 
404
420
  _builtin("na", _sig([("x", F)], ret=B))
405
421
  _builtin("nz",
406
- _sig([("x", F)], ret=F),
407
- _sig([("x", F), ("y", F)], ret=F))
408
- _builtin("fixnan", _sig([("x", F)], ret=F))
422
+ _sig([("source", F)], ret=F),
423
+ _sig([("source", F), ("replacement", F)], ret=F))
424
+ _builtin("fixnan", _sig([("source", F)], ret=F))
409
425
  _builtin("timestamp",
410
426
  _sig([("year", I), ("month", I), ("day", I),
411
427
  ("hour", I, 0), ("minute", I, 0), ("second", I, 0)], ret=I))
@@ -523,6 +539,13 @@ BUILTIN_VARIABLES: dict[str, PineType] = {
523
539
  "session.islastbar": B, "session.islastbar_regular": B,
524
540
  }
525
541
 
542
+ # The session.* booleans (session.ismarket ... session.islastbar_regular): the
543
+ # flags a script can read at a history offset, ``session.ismarket[1]``.
544
+ SESSION_FLAG_MEMBERS = frozenset(
545
+ name.split(".", 1)[1] for name, ptype in BUILTIN_VARIABLES.items()
546
+ if name.startswith("session.") and ptype == B
547
+ )
548
+
526
549
  # Strategy variables (read-only properties, not function calls)
527
550
  STRATEGY_VARIABLES: dict[str, PineType] = {
528
551
  "strategy.position_size": F,
@@ -674,6 +697,8 @@ DISPLAY_VARIABLES: dict[str, PineType] = {
674
697
  TA_DEFAULT_SOURCE = {
675
698
  "highest": "high",
676
699
  "lowest": "low",
700
+ "highestbars": "high",
701
+ "lowestbars": "low",
677
702
  "pivothigh": "high",
678
703
  "pivotlow": "low",
679
704
  }