@pineforge/codegen-pyodide 0.10.3 → 1.0.0-rc.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/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.3.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([("
|
|
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
|
|
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([("
|
|
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([("
|
|
133
|
+
_ta("bb", _sig([("series", F), ("length", I), ("mult", F)],
|
|
127
134
|
returns_tuple=True, tuple_count=3))
|
|
128
|
-
_ta("kc", _sig([("
|
|
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([("
|
|
131
|
-
_ta("kcw", _sig([("
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
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) —
|
|
185
|
-
# ta.vwap(source)
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
|
|
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([("
|
|
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), ("
|
|
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([("
|
|
407
|
-
_sig([("
|
|
408
|
-
_builtin("fixnan", _sig([("
|
|
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
|
}
|