@pineforge/codegen-pyodide 0.10.2 → 0.10.3
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/codegen/base.py +66 -18
- package/pineforge_codegen/codegen/emit_top.py +58 -20
- package/pineforge_codegen/codegen/ta.py +569 -287
- package/pineforge_codegen/codegen/tables.py +10 -5
- package/pineforge_codegen/codegen/visit_call.py +111 -57
- package/pineforge_codegen/codegen/visit_expr.py +12 -2
- package/pineforge_codegen-0.10.3.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -5,6 +5,7 @@ results from AnalyzerContext instead of walking the AST to collect info.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
|
+
import re
|
|
8
9
|
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
|
|
@@ -309,6 +310,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
309
310
|
# Set of var/series member names that belong to user functions (need cloning)
|
|
310
311
|
self._func_var_members_set: set[str] = set()
|
|
311
312
|
self._precalc_loop_active: bool = False
|
|
313
|
+
# Top-level lazy-edge TA sites hoisted to every-bar evaluation for the
|
|
314
|
+
# statement currently being lowered: FuncCall id -> local name, and
|
|
315
|
+
# Subscript id -> ``_hist_call_*`` member (see ``ta.py``).
|
|
316
|
+
self._hoisted_ta_values: dict[int, str] = {}
|
|
317
|
+
self._hoisted_hist_reads: dict[int, str] = {}
|
|
312
318
|
# Names of ``var`` members that live in a callable scope (not global).
|
|
313
319
|
# Their exact declaration statements own initialization; they must not
|
|
314
320
|
# be initialized by the constructor or the global on_bar preamble.
|
|
@@ -934,6 +940,8 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
934
940
|
self._register_global_aggregate_member_types()
|
|
935
941
|
self._uses_map = self._detect_map_usage()
|
|
936
942
|
self._uses_matrix = self._detect_matrix_usage()
|
|
943
|
+
self._uses_pine_time = self._detect_pine_time_usage()
|
|
944
|
+
self._uses_vwap_anchor = self._detect_vwap_usage()
|
|
937
945
|
# Drawing-objects-as-data: gate all new emission (drawing.hpp include +
|
|
938
946
|
# the per-type arenas) on this flag so non-drawing strategies stay
|
|
939
947
|
# byte-identical. Caps come from the strategy() header max_*_count.
|
|
@@ -2110,6 +2118,45 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
2110
2118
|
return True
|
|
2111
2119
|
return False
|
|
2112
2120
|
|
|
2121
|
+
def _detect_pine_time_usage(self) -> bool:
|
|
2122
|
+
"""True if emitted C++ calls the engine's pine_time()/pine_time_close().
|
|
2123
|
+
|
|
2124
|
+
Gates the PF_PINE_TIME_SESSION_DAY_ARGS prelude shim (emit_top.py) so
|
|
2125
|
+
scripts that never call ``time(tf[, session[, tz]])`` /
|
|
2126
|
+
``time_close(...)`` stay byte-identical. A ``var`` initializer is
|
|
2127
|
+
also text-scanned, mirroring ``_detect_matrix_usage``.
|
|
2128
|
+
"""
|
|
2129
|
+
pat = re.compile(r"(?<![\w.])time(?:_close)?\s*\(")
|
|
2130
|
+
for _, _, init_str in self.ctx.var_members:
|
|
2131
|
+
if init_str and pat.search(str(init_str)):
|
|
2132
|
+
return True
|
|
2133
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2134
|
+
if isinstance(node, FuncCall) and (node.args or node.kwargs):
|
|
2135
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2136
|
+
if ns is None and fn in ("time", "time_close"):
|
|
2137
|
+
return True
|
|
2138
|
+
return False
|
|
2139
|
+
|
|
2140
|
+
def _detect_vwap_usage(self) -> bool:
|
|
2141
|
+
"""True if emitted C++ calls ta::VWAP / ta::VWAPBands compute().
|
|
2142
|
+
|
|
2143
|
+
Gates the PF_VWAP_SESSION_ANCHOR_ARGS prelude shim (emit_top.py):
|
|
2144
|
+
``ta.vwap(...)`` calls, the bare ``ta.vwap`` property read, and
|
|
2145
|
+
``var`` initializers are covered.
|
|
2146
|
+
"""
|
|
2147
|
+
for _, _, init_str in self.ctx.var_members:
|
|
2148
|
+
if init_str and "ta.vwap" in str(init_str):
|
|
2149
|
+
return True
|
|
2150
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
2151
|
+
if isinstance(node, FuncCall):
|
|
2152
|
+
fn, ns = self._resolve_callee(node.callee)
|
|
2153
|
+
if ns == "ta" and fn == "vwap":
|
|
2154
|
+
return True
|
|
2155
|
+
elif isinstance(node, MemberAccess) and node.member == "vwap":
|
|
2156
|
+
if getattr(node.object, "name", None) == "ta":
|
|
2157
|
+
return True
|
|
2158
|
+
return False
|
|
2159
|
+
|
|
2113
2160
|
def _detect_generic_matrix_usage(self) -> bool:
|
|
2114
2161
|
"""True if emitted C++ mentions a non-float matrix specialization."""
|
|
2115
2162
|
float_spec = TypeSpec.primitive("float")
|
|
@@ -3760,7 +3807,7 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3760
3807
|
# request.security helper-call results read at a history offset
|
|
3761
3808
|
# (``myHelper()[k]``). Maps (sec_id, node-id) -> backing Series metadata.
|
|
3762
3809
|
self._security_expr_hist_by_node: dict[tuple[int, int], dict] = {}
|
|
3763
|
-
self.
|
|
3810
|
+
self._prepare_lazy_source_clock_sites()
|
|
3764
3811
|
|
|
3765
3812
|
lines: list[str] = []
|
|
3766
3813
|
|
|
@@ -3993,11 +4040,11 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
3993
4040
|
)
|
|
3994
4041
|
lines.append("")
|
|
3995
4042
|
|
|
3996
|
-
#
|
|
3997
|
-
# script state themselves. Their
|
|
3998
|
-
#
|
|
3999
|
-
# checkpoint inventory.
|
|
4000
|
-
self.
|
|
4043
|
+
# Hold-last source clocks (change/mom/roc below a top-level lazy edge)
|
|
4044
|
+
# are generated support types, not script state themselves. Their
|
|
4045
|
+
# per-callsite instances are declared below inside GeneratedStrategy
|
|
4046
|
+
# and therefore join the automatic COOF checkpoint inventory.
|
|
4047
|
+
self._emit_lazy_source_clock_helper(lines)
|
|
4001
4048
|
|
|
4002
4049
|
# 2. Open class
|
|
4003
4050
|
lines.append("class GeneratedStrategy : public BacktestEngine {")
|
|
@@ -4153,20 +4200,21 @@ class CodeGen(CallVisitor, ExprVisitor, StmtVisitor, TopLevelEmitter, SecurityEm
|
|
|
4153
4200
|
lines.append(f" std::vector<{vtype}> _precalc_{site.member_name};")
|
|
4154
4201
|
lines.append(" bool _use_precalc = false;")
|
|
4155
4202
|
|
|
4156
|
-
for
|
|
4157
|
-
lines.append(
|
|
4158
|
-
f" {self._lazy_saturated_roc3_type_name} {clock_name};"
|
|
4159
|
-
)
|
|
4160
|
-
if self._lazy_saturated_roc3_clock_by_node:
|
|
4161
|
-
# Dedicated eager close[3] fallback. Its fixed four-slot capacity
|
|
4162
|
-
# is independent of the user's max_bars_back directive, which may
|
|
4163
|
-
# legitimately be smaller than the offset this generated route
|
|
4164
|
-
# requires. It is ordinary copyable script state and therefore
|
|
4165
|
-
# joins the automatic COOF checkpoint below.
|
|
4203
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
4166
4204
|
lines.append(
|
|
4167
|
-
"
|
|
4168
|
-
f"{self._lazy_saturated_roc3_history_name}{{4}};"
|
|
4205
|
+
f" {self._lazy_source_clock_type_name} {info['clock']};"
|
|
4169
4206
|
)
|
|
4207
|
+
# Held-source history on the chart clock. A literal length sizes
|
|
4208
|
+
# it exactly (independent of the user's max_bars_back directive,
|
|
4209
|
+
# which may legitimately be smaller than this generated route
|
|
4210
|
+
# needs); a runtime length uses the Series default capacity. Both
|
|
4211
|
+
# are ordinary copyable script state and join the automatic COOF
|
|
4212
|
+
# checkpoint below.
|
|
4213
|
+
literal = info["length_literal"]
|
|
4214
|
+
capacity = f"{{{literal + 1}}}" if literal is not None and literal >= 1 else ""
|
|
4215
|
+
lines.append(f" Series<double> {info['hist']}{capacity};")
|
|
4216
|
+
if info["chart"] is not None:
|
|
4217
|
+
lines.append(f" Series<double> {info['chart']}{capacity};")
|
|
4170
4218
|
|
|
4171
4219
|
# Security evaluator TA members (cloned from expression dependencies)
|
|
4172
4220
|
# Skip for user function call expressions — their TA deps are internal to the function
|
|
@@ -136,6 +136,28 @@ class TopLevelEmitter:
|
|
|
136
136
|
lines.append('#include <pineforge/log.hpp>')
|
|
137
137
|
lines.append('#include <pineforge/str_utils.hpp>')
|
|
138
138
|
lines.append('#include <pineforge/session_time.hpp>')
|
|
139
|
+
# Feature-guarded argument tails (lab experiment cells build the
|
|
140
|
+
# candidate codegen against the BASE engine too): the symbol clock
|
|
141
|
+
# (syminfo tz + session) reaches time()/time_close() and ta.vwap only
|
|
142
|
+
# when the engine header advertises the overloads; otherwise the
|
|
143
|
+
# macros expand to nothing and the calls collapse to the legacy
|
|
144
|
+
# 5-arg pine_time / 3-arg VWAP::compute shapes (session filter in
|
|
145
|
+
# UTC, UTC-day VWAP anchor — the honest "factor absent" behaviour).
|
|
146
|
+
# Gated on the pre-scans so scripts without such calls stay
|
|
147
|
+
# byte-identical. The expansion carries no extra parentheses so the
|
|
148
|
+
# preprocessed call is byte-identical to the plain 7-arg form.
|
|
149
|
+
if getattr(self, "_uses_pine_time", False):
|
|
150
|
+
lines.append("#ifdef PF_PINE_TIME_HAS_SESSION_DAY")
|
|
151
|
+
lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess) , tz, sess")
|
|
152
|
+
lines.append("#else")
|
|
153
|
+
lines.append("#define PF_PINE_TIME_SESSION_DAY_ARGS(tz, sess)")
|
|
154
|
+
lines.append("#endif")
|
|
155
|
+
if getattr(self, "_uses_vwap_anchor", False):
|
|
156
|
+
lines.append("#ifdef PF_VWAP_HAS_SESSION_ANCHOR")
|
|
157
|
+
lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess) , tz, sess")
|
|
158
|
+
lines.append("#else")
|
|
159
|
+
lines.append("#define PF_VWAP_SESSION_ANCHOR_ARGS(tz, sess)")
|
|
160
|
+
lines.append("#endif")
|
|
139
161
|
if self._uses_matrix:
|
|
140
162
|
# Unconditional include when matrix API is used — do not gate on
|
|
141
163
|
# __has_include(<Eigen/Dense>) (can differ between runtime build and this TU).
|
|
@@ -912,28 +934,38 @@ class TopLevelEmitter:
|
|
|
912
934
|
|
|
913
935
|
# A GeneratedStrategy handle may execute multiple batch runs or
|
|
914
936
|
# streaming lifecycles. BacktestEngine resets broker/base state, but
|
|
915
|
-
# generated members survive. Reset
|
|
916
|
-
#
|
|
917
|
-
#
|
|
918
|
-
#
|
|
919
|
-
#
|
|
920
|
-
#
|
|
921
|
-
|
|
937
|
+
# generated members survive. Reset the hold-last source clocks
|
|
938
|
+
# (change/mom/roc below a top-level lazy edge) and their held-source
|
|
939
|
+
# Series at the first genuine slot of each lifecycle, then freeze this
|
|
940
|
+
# bar's base and record it once per chart bar. This lives in on_bar
|
|
941
|
+
# rather than a generated run wrapper because stream_begin() enters the
|
|
942
|
+
# base run path directly. COOF post-close recalculations have
|
|
943
|
+
# history_advances_new_bar()==false and therefore keep the same base.
|
|
944
|
+
if self._lazy_source_clock_by_node:
|
|
922
945
|
lines.append(
|
|
923
946
|
" if (history_advances_new_bar() && bar_index_ == 0) {"
|
|
924
947
|
)
|
|
925
|
-
for
|
|
926
|
-
lines.append(f" {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
)
|
|
948
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
949
|
+
lines.append(f" {info['clock']}.reset();")
|
|
950
|
+
lines.append(f" {info['hist']}.clear();")
|
|
951
|
+
if info["chart"] is not None:
|
|
952
|
+
lines.append(f" {info['chart']}.clear();")
|
|
930
953
|
lines.append(" }")
|
|
931
|
-
self.
|
|
932
|
-
lines
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
954
|
+
for info in self._lazy_source_clock_by_node.values():
|
|
955
|
+
lines.append(f" {info['clock']}.begin_bar(bar_index_);")
|
|
956
|
+
self._emit_history_series_write(
|
|
957
|
+
lines,
|
|
958
|
+
" ",
|
|
959
|
+
info["hist"],
|
|
960
|
+
f"{info['clock']}.bar_base_source",
|
|
961
|
+
)
|
|
962
|
+
if info["chart"] is not None:
|
|
963
|
+
self._emit_history_series_write(
|
|
964
|
+
lines,
|
|
965
|
+
" ",
|
|
966
|
+
info["chart"],
|
|
967
|
+
self._visit_expr(info["chart_source"]),
|
|
968
|
+
)
|
|
937
969
|
|
|
938
970
|
# reset_run_state() owns engine/broker state, while these generated
|
|
939
971
|
# Series members belong to the strategy object. Clear all of them on
|
|
@@ -1196,9 +1228,15 @@ class TopLevelEmitter:
|
|
|
1196
1228
|
# an input (the default-sized construction already matches Pine).
|
|
1197
1229
|
self._emit_ta_runtime_reset(lines, indent=2)
|
|
1198
1230
|
|
|
1199
|
-
# d. Visit each statement
|
|
1231
|
+
# d. Visit each statement. A stateful ``ta.*`` site below a lazy
|
|
1232
|
+
# ``and``/``or`` RHS or ternary arm of a top-level statement is
|
|
1233
|
+
# evaluated every bar BEFORE the statement (TV rule, see ``ta.py``).
|
|
1200
1234
|
for stmt in self.ctx.ast.body:
|
|
1201
|
-
self.
|
|
1235
|
+
self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
|
|
1236
|
+
try:
|
|
1237
|
+
self._visit_stmt(stmt, lines, indent=2)
|
|
1238
|
+
finally:
|
|
1239
|
+
self._clear_lazy_edge_ta_hoists()
|
|
1202
1240
|
|
|
1203
1241
|
# e. ``// @pf-trace`` pragma block — emitted last so trace values
|
|
1204
1242
|
# reflect every assignment / strategy call made earlier in the
|