@pineforge/codegen-pyodide 1.0.0 → 1.1.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/glue.py +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +24 -9
- package/pineforge_codegen/analyzer/base.py +190 -1
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +6 -0
- package/pineforge_codegen/analyzer/types.py +16 -5
- package/pineforge_codegen/ast_nodes.py +7 -0
- package/pineforge_codegen/codegen/base.py +136 -14
- package/pineforge_codegen/codegen/checked_settings.py +272 -0
- package/pineforge_codegen/codegen/collection_history.py +200 -0
- package/pineforge_codegen/codegen/emit_top.py +76 -14
- package/pineforge_codegen/codegen/helpers.py +12 -2
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/codegen/security.py +56 -1
- package/pineforge_codegen/codegen/ta.py +65 -1
- package/pineforge_codegen/codegen/types.py +136 -8
- package/pineforge_codegen/codegen/visit_call.py +92 -3
- package/pineforge_codegen/codegen/visit_expr.py +202 -3
- package/pineforge_codegen/codegen/visit_stmt.py +19 -4
- package/pineforge_codegen/collection_history.py +1595 -0
- package/pineforge_codegen/external_requests.py +3 -0
- package/pineforge_codegen/parser.py +19 -4
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen/support_checker.py +37 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +2 -1
- package/pineforge_codegen-1.0.0.tar.gz +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""The C++ of an array's or a matrix's history (``a[k]``, ``m[k]``).
|
|
2
|
+
|
|
3
|
+
The analyzer annotates every such read with how its value is used and lists
|
|
4
|
+
the variables whose history the script reads (``ctx.collection_history``;
|
|
5
|
+
pineforge_codegen/collection_history.py). Such a variable keeps its own
|
|
6
|
+
``std::vector`` or matrix member, so every other operation on it keeps its
|
|
7
|
+
C++; beside it, ``_pf_collection_hist_<name>`` keeps the copies its
|
|
8
|
+
executions left. Each execution of the declaration -- each bar, for a top
|
|
9
|
+
level ``var`` -- opens a slot (``open``) and the end of the bar closes it with
|
|
10
|
+
a copy (``close``), so ``a[1]`` is the array as the previous bar left it,
|
|
11
|
+
TradingView's read-only history (fixtures/array_history_tv).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ..collection_history import (
|
|
17
|
+
COLLECTION_HISTORY_CLASS_CPP,
|
|
18
|
+
COLLECTION_HISTORY_CPP,
|
|
19
|
+
COLLECTION_HISTORY_GENERIC_MATRIX_CPP,
|
|
20
|
+
COLLECTION_HISTORY_MATRIX_CPP,
|
|
21
|
+
history_annotation,
|
|
22
|
+
literal_offset,
|
|
23
|
+
)
|
|
24
|
+
from ..ast_nodes import FuncCall, Identifier, MemberAccess
|
|
25
|
+
from ..symbols import TypeSpec
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CollectionHistoryEmitter:
|
|
29
|
+
"""CodeGen mixin: the history members of array and matrix variables."""
|
|
30
|
+
|
|
31
|
+
def _collection_history_variables(self) -> list:
|
|
32
|
+
"""The declarations whose history the script reads, in key order."""
|
|
33
|
+
history = getattr(self.ctx, "collection_history", None) or {}
|
|
34
|
+
return [history[key] for key in sorted(history)]
|
|
35
|
+
|
|
36
|
+
def _collection_history_member(self, variable) -> str:
|
|
37
|
+
"""``_pf_collection_hist_<name>``; a later declaration of the same
|
|
38
|
+
name (a sibling block's) gets ``_pf_collection_hist_<ordinal>_<name>``,
|
|
39
|
+
which no Pine name (none starts with a digit) can spell: ``x``'s
|
|
40
|
+
second declaration beside a variable ``x_1``."""
|
|
41
|
+
prefix = f"{variable.ordinal}_" if variable.ordinal else ""
|
|
42
|
+
return f"_pf_collection_hist_{prefix}{self._safe_name(variable.name)}"
|
|
43
|
+
|
|
44
|
+
def _collection_history_of(self, annotation):
|
|
45
|
+
history = getattr(self.ctx, "collection_history", None) or {}
|
|
46
|
+
return history.get(annotation.get("member"))
|
|
47
|
+
|
|
48
|
+
def _collection_history_cpp_type(self, variable) -> str:
|
|
49
|
+
"""The history's element type: the variable member's own C++ type,
|
|
50
|
+
whatever its declaration spells (a wide int array included)."""
|
|
51
|
+
return f"decltype({self._safe_name(variable.name)})"
|
|
52
|
+
|
|
53
|
+
def _collection_history_capacity(self, variable) -> str:
|
|
54
|
+
"""The ring's size: the largest literal offset read plus one, or the
|
|
55
|
+
Series default (a ``max_bars_back`` cap) for a dynamic offset."""
|
|
56
|
+
if variable.capacity is None:
|
|
57
|
+
return self._series_decl_suffix()
|
|
58
|
+
return f"{{{max(2, variable.capacity)}}}"
|
|
59
|
+
|
|
60
|
+
def _emit_collection_history_helper(self, lines: list[str]) -> None:
|
|
61
|
+
variables = self._collection_history_variables()
|
|
62
|
+
if not variables:
|
|
63
|
+
return
|
|
64
|
+
float_spec = TypeSpec.primitive("float")
|
|
65
|
+
matrices = [v.spec for v in variables if v.kind == "matrix"]
|
|
66
|
+
lines.append(COLLECTION_HISTORY_CPP.strip("\n"))
|
|
67
|
+
if any(spec.element == float_spec for spec in matrices):
|
|
68
|
+
lines.append(COLLECTION_HISTORY_MATRIX_CPP.strip("\n"))
|
|
69
|
+
if any(spec.element != float_spec for spec in matrices):
|
|
70
|
+
lines.append(COLLECTION_HISTORY_GENERIC_MATRIX_CPP.strip("\n"))
|
|
71
|
+
lines.append(COLLECTION_HISTORY_CLASS_CPP.strip("\n"))
|
|
72
|
+
lines.append("")
|
|
73
|
+
|
|
74
|
+
def _collection_history_open(self, variable) -> str:
|
|
75
|
+
"""The statement opening a declaration's slot."""
|
|
76
|
+
return (f"{self._collection_history_member(variable)}.open("
|
|
77
|
+
f"{self._safe_name(variable.name)}, history_advances_new_bar());")
|
|
78
|
+
|
|
79
|
+
def _collection_history_close(self, variable) -> str:
|
|
80
|
+
return (f"{self._collection_history_member(variable)}.close("
|
|
81
|
+
f"{self._safe_name(variable.name)});")
|
|
82
|
+
|
|
83
|
+
def _collection_history_decl_statements(self, node) -> list[str]:
|
|
84
|
+
"""What an execution of a declaration does first: close the history
|
|
85
|
+
of every other declaration writing the same member (a sibling
|
|
86
|
+
block's ``x``, which keeps the array its block left), then open its
|
|
87
|
+
own slot when it is not a ``var`` (a block's on the bars it runs)."""
|
|
88
|
+
statements = []
|
|
89
|
+
own = None
|
|
90
|
+
for variable in self._collection_history_variables():
|
|
91
|
+
if id(node) in variable.closed_by:
|
|
92
|
+
statements.append(self._collection_history_close(variable))
|
|
93
|
+
elif variable.decl_node_id == id(node):
|
|
94
|
+
own = variable
|
|
95
|
+
if own is not None and not (getattr(node, "is_var", False)
|
|
96
|
+
or getattr(node, "is_varip", False)):
|
|
97
|
+
statements.append(self._collection_history_open(own))
|
|
98
|
+
return statements
|
|
99
|
+
|
|
100
|
+
def _emit_collection_history_var_opens(self, lines: list[str], pad: str) -> None:
|
|
101
|
+
"""A top-level ``var``'s slot opens on every bar, after its first
|
|
102
|
+
bar's initialization."""
|
|
103
|
+
for variable in self._collection_history_variables():
|
|
104
|
+
if variable.is_var:
|
|
105
|
+
lines.append(f"{pad}{self._collection_history_open(variable)}")
|
|
106
|
+
|
|
107
|
+
def _emit_collection_history_closes(self, lines: list[str], pad: str) -> None:
|
|
108
|
+
"""The bar's end: every open slot keeps a copy of its variable."""
|
|
109
|
+
for variable in self._collection_history_variables():
|
|
110
|
+
lines.append(f"{pad}{self._collection_history_close(variable)}")
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _roots_at_collection_history(value) -> bool:
|
|
114
|
+
"""Whether ``value`` is an array's or a matrix's history or a
|
|
115
|
+
built-in's call on one (``m[1]``, ``(m[1]).copy()``,
|
|
116
|
+
``matrix.transpose(m[1])``)."""
|
|
117
|
+
if history_annotation(value) is not None:
|
|
118
|
+
return True
|
|
119
|
+
if not isinstance(value, FuncCall):
|
|
120
|
+
return False
|
|
121
|
+
callee = value.callee
|
|
122
|
+
if isinstance(callee, MemberAccess) and history_annotation(callee.object) is not None:
|
|
123
|
+
return True
|
|
124
|
+
first = value.args[0] if value.args else None
|
|
125
|
+
return (isinstance(callee, MemberAccess)
|
|
126
|
+
and isinstance(callee.object, Identifier)
|
|
127
|
+
and callee.object.name in ("array", "matrix")
|
|
128
|
+
and first is not None and history_annotation(first) is not None)
|
|
129
|
+
|
|
130
|
+
def _collection_history_offset_cpp(self, node) -> str:
|
|
131
|
+
offset = literal_offset(node)
|
|
132
|
+
if offset is not None:
|
|
133
|
+
return str(offset)
|
|
134
|
+
return self._history_offset_cpp(self._visit_expr(node.index), node.index)
|
|
135
|
+
|
|
136
|
+
def _lower_collection_history(self, node) -> str | None:
|
|
137
|
+
"""The C++ of an annotated array or matrix history read, by its use:
|
|
138
|
+
a ``const`` copy a built-in reads (``at``), a copy for a variable or a
|
|
139
|
+
parameter (``value``), the receiver of a built-in that changes it
|
|
140
|
+
(``changed``: the run stops past a zero offset), the variable itself
|
|
141
|
+
for a for...in loop (TradingView iterates the current array) and at a
|
|
142
|
+
zero offset. None for any other read."""
|
|
143
|
+
annotation = history_annotation(node)
|
|
144
|
+
if annotation is None:
|
|
145
|
+
return None
|
|
146
|
+
current = self._visit_expr(node.object)
|
|
147
|
+
use = annotation["use"]
|
|
148
|
+
offset = literal_offset(node)
|
|
149
|
+
if use == "loop" or offset == 0:
|
|
150
|
+
return current
|
|
151
|
+
member = self._collection_history_member(self._collection_history_of(annotation))
|
|
152
|
+
index = self._collection_history_offset_cpp(node)
|
|
153
|
+
dynamic = offset is None
|
|
154
|
+
if use == "change":
|
|
155
|
+
return f"{member}.changed({index}, {current})"
|
|
156
|
+
if use == "copy":
|
|
157
|
+
return (f"{member}.value({index}, {current})" if dynamic
|
|
158
|
+
else f"{member}.value({index})")
|
|
159
|
+
return (f"{member}.at({index}, {current})" if dynamic
|
|
160
|
+
else f"{member}.at({index})")
|
|
161
|
+
|
|
162
|
+
def _collection_history_na(self, node) -> str | None:
|
|
163
|
+
"""``na(a[k])``: whether the history holds no copy there (an array
|
|
164
|
+
variable itself is never na in PineForge; a matrix may be)."""
|
|
165
|
+
annotation = history_annotation(node)
|
|
166
|
+
if annotation is None or annotation["use"] != "na":
|
|
167
|
+
return None
|
|
168
|
+
current = self._visit_expr(node.object)
|
|
169
|
+
offset = literal_offset(node)
|
|
170
|
+
if offset == 0:
|
|
171
|
+
return f"is_na({current})" if annotation["kind"] == "matrix" else "false"
|
|
172
|
+
member = self._collection_history_member(self._collection_history_of(annotation))
|
|
173
|
+
index = self._collection_history_offset_cpp(node)
|
|
174
|
+
if offset is None:
|
|
175
|
+
return f"{member}.is_na({index}, {current})"
|
|
176
|
+
return f"{member}.is_na({index})"
|
|
177
|
+
|
|
178
|
+
def _matrix_history_method_expr(self, receiver, method: str, spec, node) -> str:
|
|
179
|
+
"""A matrix method called on a matrix's history, lowered as the
|
|
180
|
+
variable's own method call is (visit_call: a matrix variable's
|
|
181
|
+
method)."""
|
|
182
|
+
from .tables import MATRIX_METHOD_KWARGS, MATRIX_METHODS, _merge_kwargs
|
|
183
|
+
|
|
184
|
+
recv = self._visit_expr(receiver)
|
|
185
|
+
self._check_matrix_method_allowed(method, spec, node)
|
|
186
|
+
param_names = MATRIX_METHOD_KWARGS.get(method)
|
|
187
|
+
raw_args = (
|
|
188
|
+
_merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
|
|
189
|
+
if param_names and node.kwargs else list(node.args)
|
|
190
|
+
)
|
|
191
|
+
args = self._matrix_bool_value_args(
|
|
192
|
+
method, [self._visit_expr(a) for a in raw_args], raw_args, spec)
|
|
193
|
+
try:
|
|
194
|
+
return MATRIX_METHODS[method](recv, args)
|
|
195
|
+
except IndexError:
|
|
196
|
+
self._codegen_error(
|
|
197
|
+
node,
|
|
198
|
+
f"matrix.{method}: wrong number of arguments",
|
|
199
|
+
hint="Check Pine v6 matrix method signature (positional vs keyword).",
|
|
200
|
+
)
|
|
@@ -130,6 +130,8 @@ class TopLevelEmitter:
|
|
|
130
130
|
)
|
|
131
131
|
lines.append('#include <pineforge/source/pine_strategy_host.hpp>')
|
|
132
132
|
lines.append('#include <pineforge/ta.hpp>')
|
|
133
|
+
lines.extend(['#if __has_include(<pineforge/checked_settings.hpp>)',
|
|
134
|
+
'#include <pineforge/checked_settings.hpp>', '#endif'])
|
|
133
135
|
if self._ta_uses_dynamic_lengths():
|
|
134
136
|
lines.append('#include <pineforge/source/pine_ta_length.hpp>')
|
|
135
137
|
lines.append('#include <pineforge/math.hpp>')
|
|
@@ -598,6 +600,7 @@ class TopLevelEmitter:
|
|
|
598
600
|
'#error "Generated lifecycle reset requires a matching PineForge engine; rebuild with script-run preparation support"',
|
|
599
601
|
"#endif",
|
|
600
602
|
" void prepare_script_run(const Bar* bars, int n, bool allow_precalculation) override {",
|
|
603
|
+
" _pf_require_settings_ok();",
|
|
601
604
|
" _pf_script_state_checkpoint_.reset();",
|
|
602
605
|
])
|
|
603
606
|
seen: set[str] = set()
|
|
@@ -1277,6 +1280,8 @@ class TopLevelEmitter:
|
|
|
1277
1280
|
lines.append(" }")
|
|
1278
1281
|
lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
|
|
1279
1282
|
lines.append(" }")
|
|
1283
|
+
from .checked_settings import emit_settings_members
|
|
1284
|
+
emit_settings_members(self, lines, ctor_body)
|
|
1280
1285
|
|
|
1281
1286
|
if self._uses_recorded_requests():
|
|
1282
1287
|
lines.extend([
|
|
@@ -1721,7 +1726,12 @@ class TopLevelEmitter:
|
|
|
1721
1726
|
name,
|
|
1722
1727
|
target_cpp_type=udt_name,
|
|
1723
1728
|
)
|
|
1724
|
-
|
|
1729
|
+
if self._binding_is_series(name, safe):
|
|
1730
|
+
# A history-read var object: its first
|
|
1731
|
+
# bar's slot holds the reference.
|
|
1732
|
+
lines.append(f" {safe}.push({cpp_val});")
|
|
1733
|
+
else:
|
|
1734
|
+
lines.append(f" {safe} = {cpp_val};")
|
|
1725
1735
|
break
|
|
1726
1736
|
continue
|
|
1727
1737
|
if self._binding_is_series(name, safe):
|
|
@@ -1797,6 +1807,10 @@ class TopLevelEmitter:
|
|
|
1797
1807
|
# c. Push non-var series (they start fresh each bar with a push)
|
|
1798
1808
|
# (actual push happens in visit_VarDecl when the decl is visited)
|
|
1799
1809
|
|
|
1810
|
+
# c1. A top-level var array's or matrix's history slot opens on every
|
|
1811
|
+
# bar (pineforge_codegen/collection_history.py).
|
|
1812
|
+
self._emit_collection_history_var_opens(lines, " ")
|
|
1813
|
+
|
|
1800
1814
|
# c2. First-bar TA resize: rebuild any TA object whose ctor args come
|
|
1801
1815
|
# from input-backed variables so strategy_set_input() actually changes
|
|
1802
1816
|
# the circular-buffer sizes. Emits nothing when no TA site depends on
|
|
@@ -1815,6 +1829,10 @@ class TopLevelEmitter:
|
|
|
1815
1829
|
finally:
|
|
1816
1830
|
self._clear_lazy_edge_ta_hoists()
|
|
1817
1831
|
|
|
1832
|
+
# d2. The bar's end: each array or matrix variable whose history the
|
|
1833
|
+
# script reads keeps a copy of the value it ends the bar with.
|
|
1834
|
+
self._emit_collection_history_closes(lines, " ")
|
|
1835
|
+
|
|
1818
1836
|
# e. ``// @pf-trace`` pragma block — emitted last so trace values
|
|
1819
1837
|
# reflect every assignment / strategy call made earlier in the
|
|
1820
1838
|
# bar. The block is wrapped in ``if (trace_enabled_)`` so cost
|
|
@@ -1876,24 +1894,38 @@ class TopLevelEmitter:
|
|
|
1876
1894
|
def _emit_extern_c(self, lines: list[str]) -> None:
|
|
1877
1895
|
lines.append('extern "C" {')
|
|
1878
1896
|
lines.append(" void* strategy_create(const char* params_json) {")
|
|
1879
|
-
lines.append(" return new GeneratedStrategy();")
|
|
1897
|
+
lines.append(" try { return new GeneratedStrategy(); } catch (...) { return nullptr; }")
|
|
1880
1898
|
lines.append(" }")
|
|
1881
1899
|
lines.append(" void run_backtest(void* s, Bar* bars, int n, ReportC* out) {")
|
|
1900
|
+
lines.append(" try {")
|
|
1882
1901
|
lines.append(" auto* strat = static_cast<GeneratedStrategy*>(s);")
|
|
1902
|
+
lines.append("#ifndef PF_SETTINGS_API_VERSION")
|
|
1903
|
+
lines.append(" if (strat->_pf_refuse_failed_setting(out)) return;")
|
|
1904
|
+
lines.append("#endif")
|
|
1883
1905
|
if self._security_calls:
|
|
1884
1906
|
# If there are security calls, use the full run path. Pass empty strings
|
|
1885
1907
|
# so the C++ runtime auto-detects input_tf from bar timestamps.
|
|
1886
1908
|
lines.append(' strat->run(bars, n, "", "", false, 4, MagnifierDistribution::ENDPOINTS);')
|
|
1887
1909
|
else:
|
|
1888
1910
|
lines.append(" strat->run(bars, n);")
|
|
1889
|
-
lines.append(" strat->fill_report(out);")
|
|
1911
|
+
lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
|
|
1912
|
+
lines.append(' } catch (const std::exception& _pf_error) {')
|
|
1913
|
+
lines.append(' if (out) *out = ReportC{};')
|
|
1914
|
+
lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", _pf_error.what());')
|
|
1915
|
+
lines.append(' } catch (...) {')
|
|
1916
|
+
lines.append(' if (out) *out = ReportC{};')
|
|
1917
|
+
lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", "unknown C++ exception");')
|
|
1918
|
+
lines.append(' }')
|
|
1890
1919
|
lines.append(" }")
|
|
1891
|
-
lines.append(" void
|
|
1920
|
+
lines.append(" static void _pf_run_backtest_full_impl(void* s, Bar* bars, int n,")
|
|
1892
1921
|
lines.append(' const char* input_tf, const char* script_tf,')
|
|
1893
1922
|
lines.append(" int bar_magnifier, int magnifier_samples,")
|
|
1894
1923
|
lines.append(" int magnifier_dist,")
|
|
1895
1924
|
lines.append(" ReportC* out) {")
|
|
1896
1925
|
lines.append(' auto* strat = static_cast<GeneratedStrategy*>(s);')
|
|
1926
|
+
lines.append("#ifndef PF_SETTINGS_API_VERSION")
|
|
1927
|
+
lines.append(" if (strat->_pf_refuse_failed_setting(out)) return;")
|
|
1928
|
+
lines.append("#endif")
|
|
1897
1929
|
lines.append(' std::string itf = input_tf ? input_tf : "";')
|
|
1898
1930
|
lines.append(' std::string stf = script_tf ? script_tf : "";')
|
|
1899
1931
|
if self._security_calls:
|
|
@@ -1930,26 +1962,42 @@ class TopLevelEmitter:
|
|
|
1930
1962
|
lines.append(" strat->run(bars, n, itf, stf, bar_magnifier != 0, magnifier_samples,")
|
|
1931
1963
|
lines.append(" static_cast<MagnifierDistribution>(magnifier_dist));")
|
|
1932
1964
|
lines.append(" }")
|
|
1933
|
-
lines.append(" strat->fill_report(out);")
|
|
1965
|
+
lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
|
|
1934
1966
|
lines.append(" }")
|
|
1967
|
+
lines.extend([
|
|
1968
|
+
" void run_backtest_full(void* s, Bar* bars, int n, const char* input_tf, const char* script_tf,",
|
|
1969
|
+
" int bar_magnifier, int magnifier_samples, int magnifier_dist, ReportC* out) {",
|
|
1970
|
+
" try { _pf_run_backtest_full_impl(s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, out); }",
|
|
1971
|
+
' catch (const std::exception& _pf_error) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", _pf_error.what()); }',
|
|
1972
|
+
' catch (...) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", "unknown C++ exception"); }',
|
|
1973
|
+
" }",
|
|
1974
|
+
])
|
|
1935
1975
|
lines.append(" void strategy_free(void* s) {")
|
|
1936
|
-
lines.append(" delete static_cast<GeneratedStrategy*>(s);")
|
|
1976
|
+
lines.append(" try { delete static_cast<GeneratedStrategy*>(s); } catch (...) {}")
|
|
1937
1977
|
lines.append(" }")
|
|
1938
1978
|
lines.append(" void report_free(ReportC* report) {")
|
|
1939
|
-
lines.append(" BacktestEngine::free_report(report);")
|
|
1979
|
+
lines.append(" try { BacktestEngine::free_report(report); } catch (...) {}")
|
|
1940
1980
|
lines.append(" }")
|
|
1941
1981
|
lines.append(" void strategy_set_input(void* s, const char* key, const char* value) {")
|
|
1942
1982
|
lines.append(" if (!s || !key || !value) return;")
|
|
1943
|
-
lines.append(" static_cast<GeneratedStrategy*>(s)->set_input(key, value);")
|
|
1983
|
+
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_input(key, value); }")
|
|
1984
|
+
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_input", _pf_error.what()); }')
|
|
1985
|
+
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_input", "unknown C++ exception"); }')
|
|
1944
1986
|
lines.append(" }")
|
|
1945
1987
|
lines.append(" void strategy_set_override(void* s, const char* key, const char* value) {")
|
|
1946
1988
|
lines.append(" if (!s || !key || !value) return;")
|
|
1947
|
-
lines.append(" static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value);")
|
|
1989
|
+
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value); }")
|
|
1990
|
+
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_override", _pf_error.what()); }')
|
|
1991
|
+
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_override", "unknown C++ exception"); }')
|
|
1948
1992
|
lines.append(" }")
|
|
1949
1993
|
lines.append(" void strategy_set_magnifier_volume_weighted(void* s, int on) {")
|
|
1950
1994
|
lines.append(" if (!s) return;")
|
|
1951
|
-
lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
|
|
1995
|
+
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0); }")
|
|
1996
|
+
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_magnifier_volume_weighted", _pf_error.what()); }')
|
|
1997
|
+
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_magnifier_volume_weighted", "unknown C++ exception"); }')
|
|
1952
1998
|
lines.append(" }")
|
|
1999
|
+
from .checked_settings import emit_settings_exports
|
|
2000
|
+
emit_settings_exports(lines)
|
|
1953
2001
|
if self._declares_bar_magnifier():
|
|
1954
2002
|
# TradingView runs a script that declares use_bar_magnifier = true
|
|
1955
2003
|
# on its bar magnifier; the host reads this export to run it on
|
|
@@ -2103,11 +2151,16 @@ class TopLevelEmitter:
|
|
|
2103
2151
|
|
|
2104
2152
|
if (
|
|
2105
2153
|
receiver_spec is not None
|
|
2106
|
-
and receiver_spec.kind == "primitive"
|
|
2154
|
+
and (receiver_spec.kind == "primitive"
|
|
2155
|
+
or (receiver_spec.kind == "udt"
|
|
2156
|
+
and (receiver_spec.name in DRAWING_TYPE_TO_CPP
|
|
2157
|
+
or receiver_spec.name in self._udt_defs)))
|
|
2107
2158
|
and p in func_sv
|
|
2108
2159
|
):
|
|
2109
2160
|
# A primitive receiver used with history is a Series boundary,
|
|
2110
|
-
# just like an ordinary history-bearing UDF parameter
|
|
2161
|
+
# just like an ordinary history-bearing UDF parameter; so is
|
|
2162
|
+
# an object or drawing receiver (``this[1]`` is the receiver of
|
|
2163
|
+
# the previous call: fixtures/udt_history_tv udth_method).
|
|
2111
2164
|
elem_cpp_t = self._series_param_element_cpp_type(
|
|
2112
2165
|
fi, i, call_site_idx
|
|
2113
2166
|
)
|
|
@@ -2118,6 +2171,9 @@ class TopLevelEmitter:
|
|
|
2118
2171
|
self._current_func_series_param_types[
|
|
2119
2172
|
self._safe_name(p)
|
|
2120
2173
|
] = elem_cpp_t
|
|
2174
|
+
if receiver_spec.kind == "udt":
|
|
2175
|
+
self._udt_param_udt[self._safe_name(p)] = receiver_spec.name
|
|
2176
|
+
self._udt_param_udt[p] = receiver_spec.name
|
|
2121
2177
|
elif is_method and i == 0 and fi.udt_type_name:
|
|
2122
2178
|
# Receiver pass modes follow Pine's value/ID families. A user
|
|
2123
2179
|
# UDT is itself a numeric object-ID handle, so pass it by value:
|
|
@@ -2352,10 +2408,16 @@ class TopLevelEmitter:
|
|
|
2352
2408
|
),
|
|
2353
2409
|
)
|
|
2354
2410
|
if (drawing_name in DRAWING_TYPE_TO_CPP
|
|
2355
|
-
and self._current_func_param_types.get(
|
|
2411
|
+
and (self._current_func_param_types.get(
|
|
2356
2412
|
param, ""
|
|
2357
2413
|
).removesuffix("&").removesuffix("*")
|
|
2358
|
-
== DRAWING_TYPE_TO_CPP[drawing_name]
|
|
2414
|
+
== DRAWING_TYPE_TO_CPP[drawing_name]
|
|
2415
|
+
# A history-read drawing parameter or receiver is a
|
|
2416
|
+
# ``const Series<Box>&``: ``(x[1]).get_top()`` dispatches on
|
|
2417
|
+
# the handle it holds (udth_drawparam).
|
|
2418
|
+
or (param in self._current_func_series_params
|
|
2419
|
+
and self._current_func_series_param_types.get(param)
|
|
2420
|
+
== DRAWING_TYPE_TO_CPP[drawing_name])))
|
|
2359
2421
|
}
|
|
2360
2422
|
self._lexical_udt_types = {
|
|
2361
2423
|
param: (
|
|
@@ -314,7 +314,13 @@ CPP_EMITTER_NAMES = frozenset("""
|
|
|
314
314
|
_pf_drawing_na _pf_collect_drawings _pf_collect_lines_ _pf_collect_boxes_
|
|
315
315
|
_pf_collect_labels_ _pf_new _pf_held
|
|
316
316
|
get_input_int get_input_float get_input_bool get_input_string
|
|
317
|
-
|
|
317
|
+
_pf_record_failure _pf_refuse_failed_setting _pf_settings_declared_config
|
|
318
|
+
_pf_record_setting_failure _pf_require_settings_ok
|
|
319
|
+
_pf_setting_failed_ _pf_setting_failure_
|
|
320
|
+
_pf_settings_inputs _pf_settings_overrides _pf_set_input_checked
|
|
321
|
+
_pf_set_override_checked _pf_settings_receipt
|
|
322
|
+
_pf_close_entries_rule_word _pf_default_qty_type_word _pf_commission_type_word
|
|
323
|
+
checked_settings trace is_na na nz fixnan
|
|
318
324
|
""".split())
|
|
319
325
|
|
|
320
326
|
# The generated history members, ``_<kind>_<n>`` (codegen/base.py
|
|
@@ -345,7 +351,10 @@ CPP_RESERVED = set(
|
|
|
345
351
|
# ``_nz_v``) or initialized itself from itself (``_pna_l > close`` did not
|
|
346
352
|
# compile). ``_safe_name`` escapes an authored spelling of any of them. A
|
|
347
353
|
# temporary whose name the emitter allocates against the authored spellings
|
|
348
|
-
# itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed.
|
|
354
|
+
# itself (``__pf_map_iter_N``, ``_pf_udt_Item__pf2``) is not listed. An
|
|
355
|
+
# array's or a matrix's history member (``_pf_collection_hist_<name>``,
|
|
356
|
+
# ``_pf_collection_hist_<n>_<name>``) is reserved the same way: a script
|
|
357
|
+
# variable of that spelling was a second member of one name.
|
|
349
358
|
CPP_TEMPORARY_NAMES = frozenset("""
|
|
350
359
|
_nz_v _nz_y _fixnan_v _pna_l _pna_r _pfc_l _pfc_r _pfc_eq _hv _hidx _sv
|
|
351
360
|
_out _func_ret _pf_v _pf_idx_v _pf_idx_t _pf_bool_v _pf_bool_t
|
|
@@ -364,6 +373,7 @@ _CPP_TEMPORARY_PATTERN = re.compile(
|
|
|
364
373
|
r"_v\d+|_secval_\d+(?:_v\d+)?|_tuple_result_\d+|_tuple_unused_\d+|__switch_val_\d+"
|
|
365
374
|
r"|_for_(?:start|end|end_eval)_\d+|_pf_(?:str|array|round)_a\d+"
|
|
366
375
|
r"|_pf_every_bar_ta_\d+|_pf_shared_\d+_\d+|__pf_array\w*|__pf_raw_\w+"
|
|
376
|
+
r"|_pf_collection_hist_\w+"
|
|
367
377
|
)
|
|
368
378
|
|
|
369
379
|
|
|
@@ -8,6 +8,7 @@ strategy class, so ``_safe_name`` renames it.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
HOST_MEMBER_NAMES = frozenset({
|
|
11
|
+
"StreamPhase",
|
|
11
12
|
"_src_close_",
|
|
12
13
|
"_src_high_",
|
|
13
14
|
"_src_hl2_",
|
|
@@ -50,6 +51,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
50
51
|
"closed_trade_profit_percent",
|
|
51
52
|
"closed_trade_size",
|
|
52
53
|
"commit_script_state",
|
|
54
|
+
"config_",
|
|
53
55
|
"configure_pine_strategy",
|
|
54
56
|
"configure_security_evaluators",
|
|
55
57
|
"count_losstrades",
|
|
@@ -74,9 +76,12 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
74
76
|
"history_advances_new_bar",
|
|
75
77
|
"initial_capital_",
|
|
76
78
|
"input_tf_",
|
|
79
|
+
"inputs_",
|
|
77
80
|
"is_first_tick",
|
|
78
81
|
"is_last_tick_",
|
|
79
82
|
"last_bar_time_",
|
|
83
|
+
"last_error",
|
|
84
|
+
"last_error_",
|
|
80
85
|
"main_period",
|
|
81
86
|
"margin_liquidation_price",
|
|
82
87
|
"max_contracts_held_all",
|
|
@@ -104,6 +109,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
104
109
|
"open_trade_profit_percent",
|
|
105
110
|
"open_trade_size",
|
|
106
111
|
"open_trades_capital_held",
|
|
112
|
+
"override_",
|
|
107
113
|
"pine_bar_index",
|
|
108
114
|
"pine_last_bar_index",
|
|
109
115
|
"pine_session_ismarket",
|
|
@@ -123,6 +129,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
123
129
|
"restore_script_state",
|
|
124
130
|
"round_to_mintick",
|
|
125
131
|
"run",
|
|
132
|
+
"script_bars_processed",
|
|
126
133
|
"script_tf_",
|
|
127
134
|
"security_column_value",
|
|
128
135
|
"security_eval_states_",
|
|
@@ -151,6 +158,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
151
158
|
"strategy_exit",
|
|
152
159
|
"strategy_exit_cancel_bracket",
|
|
153
160
|
"strategy_order",
|
|
161
|
+
"stream_phase_",
|
|
154
162
|
"symbol_facts_",
|
|
155
163
|
"symbol_feeds_",
|
|
156
164
|
"syminfo_",
|
|
@@ -85,14 +85,43 @@ class InputHelper:
|
|
|
85
85
|
while merged and merged[-1] is None:
|
|
86
86
|
merged.pop()
|
|
87
87
|
if merged and merged[0] is not None:
|
|
88
|
-
return merged[0]
|
|
88
|
+
return self._fold_enum_input_default(node, merged[0])
|
|
89
89
|
return None
|
|
90
90
|
if node.args:
|
|
91
|
-
return node.args[0]
|
|
91
|
+
return self._fold_enum_input_default(node, node.args[0])
|
|
92
92
|
if "defval" in node.kwargs:
|
|
93
|
-
return node.kwargs["defval"]
|
|
93
|
+
return self._fold_enum_input_default(node, node.kwargs["defval"])
|
|
94
94
|
return None
|
|
95
95
|
|
|
96
|
+
def _fold_enum_input_default(self, node: FuncCall, default):
|
|
97
|
+
"""Resolve an enum defval's immutable, source-ordered initializer."""
|
|
98
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
99
|
+
if namespace != "input" or func_name != "enum" or not isinstance(default, Identifier):
|
|
100
|
+
return default
|
|
101
|
+
declarations = {}
|
|
102
|
+
reassigned = self._find_reassigned_vars()
|
|
103
|
+
for statement in self.ctx.ast.body:
|
|
104
|
+
if any(candidate is node for candidate in self._walk_ast(statement)):
|
|
105
|
+
break
|
|
106
|
+
if isinstance(statement, VarDecl):
|
|
107
|
+
declarations.setdefault(statement.name, []).append(statement)
|
|
108
|
+
candidate = default
|
|
109
|
+
visited = set()
|
|
110
|
+
while isinstance(candidate, Identifier) and candidate.name not in visited:
|
|
111
|
+
visited.add(candidate.name)
|
|
112
|
+
bindings = declarations.get(candidate.name, [])
|
|
113
|
+
if len(bindings) != 1 or candidate.name in reassigned:
|
|
114
|
+
return default
|
|
115
|
+
declaration = bindings[0]
|
|
116
|
+
if declaration.is_var or declaration.is_varip:
|
|
117
|
+
return default
|
|
118
|
+
candidate = declaration.value
|
|
119
|
+
if isinstance(candidate, MemberAccess) and isinstance(candidate.object, Identifier):
|
|
120
|
+
members = self._enum_defs.get(candidate.object.name, [])
|
|
121
|
+
if candidate.member in members:
|
|
122
|
+
return candidate
|
|
123
|
+
return default
|
|
124
|
+
|
|
96
125
|
def _input_default_value(self, node: FuncCall) -> tuple[bool, object]:
|
|
97
126
|
"""``(True, value)`` when the defval is the constant a ``v = input...``
|
|
98
127
|
binding records as ``v``'s known value -- a number, bool or string
|
|
@@ -527,6 +556,10 @@ class InputHelper:
|
|
|
527
556
|
"type": form_type,
|
|
528
557
|
"default": default_val,
|
|
529
558
|
}
|
|
559
|
+
if namespace == "input" and func_name == "symbol":
|
|
560
|
+
# A symbol string, which the type does not tell from a string's:
|
|
561
|
+
# the requests of another symbol key their feeds on its value.
|
|
562
|
+
entry["kind"] = "symbol"
|
|
530
563
|
# Pull min/max/step/options by signature param name; emit only
|
|
531
564
|
# const literals so the override form never references a runtime
|
|
532
565
|
# value it can't reproduce.
|
|
@@ -88,7 +88,7 @@ from .helpers import na_preserving_int_cast, unary_sign_cpp
|
|
|
88
88
|
from ..security_contexts import GLOBAL_ANNOTATION, UNREACHED_ANNOTATION
|
|
89
89
|
from ..symbols import PineType, method_receiver_type_name
|
|
90
90
|
from .tables import (
|
|
91
|
-
BAR_BUILTINS, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
|
|
91
|
+
BAR_BUILTINS, DRAWING_TYPE_TO_CPP, MATH_FUNC_MAP, PINE_TYPE_TO_CPP, SECURITY_BAR_FIELDS,
|
|
92
92
|
SECURITY_BAR_FIELD_EXPRS, SECURITY_BAR_FIELD_TYPES, TA_TUPLE_FIELDS,
|
|
93
93
|
_math_minmax_na_expr, _math_round_digits_expr, _merge_kwargs,
|
|
94
94
|
)
|
|
@@ -6127,11 +6127,66 @@ class SecurityEmitter:
|
|
|
6127
6127
|
lines.append(" }")
|
|
6128
6128
|
lines.append("")
|
|
6129
6129
|
|
|
6130
|
+
def _refuse_security_reference_history(self, item: dict) -> None:
|
|
6131
|
+
"""A request.security expression that reads the history of an
|
|
6132
|
+
object or drawing reference, itself or in a function or method it
|
|
6133
|
+
calls. TradingView evaluates it on the requested bars, where the
|
|
6134
|
+
variable holds the objects of that timeframe's bars; PineForge keeps
|
|
6135
|
+
such a history on the chart's bars only, and the payload read the
|
|
6136
|
+
chart's (it never compiled before: the history was a
|
|
6137
|
+
``Series<double>``). Refused."""
|
|
6138
|
+
if item["expr_node"] is None:
|
|
6139
|
+
# ``request.security(sym, tf)`` with no expression: lowered to na
|
|
6140
|
+
# as it always was (TradingView refuses it, CE10165).
|
|
6141
|
+
return
|
|
6142
|
+
pending = [item["expr_node"]]
|
|
6143
|
+
seen: set[str] = set()
|
|
6144
|
+
while pending:
|
|
6145
|
+
for node, _depth in iter_ast_nodes(pending.pop()):
|
|
6146
|
+
if isinstance(node, Subscript):
|
|
6147
|
+
receiver = node.object
|
|
6148
|
+
kind = (self._series_handle_type_name(receiver.name)
|
|
6149
|
+
if isinstance(receiver, Identifier) else None)
|
|
6150
|
+
if kind is None:
|
|
6151
|
+
spec = self._type_spec_from_expr(receiver)
|
|
6152
|
+
if (spec is not None and spec.kind == "udt"
|
|
6153
|
+
and (spec.name in DRAWING_TYPE_TO_CPP
|
|
6154
|
+
or spec.name in self._udt_defs)):
|
|
6155
|
+
kind = spec.name
|
|
6156
|
+
if kind is not None:
|
|
6157
|
+
self._codegen_error(
|
|
6158
|
+
node,
|
|
6159
|
+
f"History of a {kind} reference inside a "
|
|
6160
|
+
"request.security expression is not supported in "
|
|
6161
|
+
"PineForge: TradingView reads the references the "
|
|
6162
|
+
"variable held on the requested timeframe's bars, "
|
|
6163
|
+
"which PineForge does not keep.",
|
|
6164
|
+
hint="Request the field values the expression "
|
|
6165
|
+
"needs (request.security(..., obj.field)) "
|
|
6166
|
+
"and read their history on the chart.",
|
|
6167
|
+
)
|
|
6168
|
+
if not isinstance(node, FuncCall):
|
|
6169
|
+
continue
|
|
6170
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
6171
|
+
keys = []
|
|
6172
|
+
if namespace is None and func_name in self._func_info_map:
|
|
6173
|
+
keys.append(func_name)
|
|
6174
|
+
if isinstance(node.callee, MemberAccess):
|
|
6175
|
+
keys.extend(key for key in self._func_info_map
|
|
6176
|
+
if key.endswith(f".{node.callee.member}"))
|
|
6177
|
+
for key in keys:
|
|
6178
|
+
body = getattr(self._func_info_map[key], "node", None)
|
|
6179
|
+
if key not in seen and body is not None:
|
|
6180
|
+
seen.add(key)
|
|
6181
|
+
pending.append(body)
|
|
6182
|
+
|
|
6130
6183
|
def _emit_security_evaluators(self, lines: list[str]) -> None:
|
|
6131
6184
|
"""Emit _eval_security_N() methods and evaluate_security() dispatch."""
|
|
6132
6185
|
if not self._security_calls:
|
|
6133
6186
|
return
|
|
6134
6187
|
|
|
6188
|
+
for item in self._security_calls:
|
|
6189
|
+
self._refuse_security_reference_history(item)
|
|
6135
6190
|
for item in self._security_calls:
|
|
6136
6191
|
self._emit_security_evaluator_requested(item, lines)
|
|
6137
6192
|
|