pineforge-codegen 0.6.5__py3-none-any.whl
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.
- pineforge_codegen/__init__.py +53 -0
- pineforge_codegen/analyzer/__init__.py +60 -0
- pineforge_codegen/analyzer/base.py +1563 -0
- pineforge_codegen/analyzer/call_handlers.py +895 -0
- pineforge_codegen/analyzer/contracts.py +163 -0
- pineforge_codegen/analyzer/diagnostics.py +118 -0
- pineforge_codegen/analyzer/tables.py +204 -0
- pineforge_codegen/analyzer/types.py +250 -0
- pineforge_codegen/ast_nodes.py +293 -0
- pineforge_codegen/codegen/__init__.py +78 -0
- pineforge_codegen/codegen/base.py +1381 -0
- pineforge_codegen/codegen/emit_top.py +875 -0
- pineforge_codegen/codegen/helpers.py +163 -0
- pineforge_codegen/codegen/helpers_syminfo.py +134 -0
- pineforge_codegen/codegen/input.py +189 -0
- pineforge_codegen/codegen/security.py +1564 -0
- pineforge_codegen/codegen/ta.py +298 -0
- pineforge_codegen/codegen/tables.py +613 -0
- pineforge_codegen/codegen/types.py +573 -0
- pineforge_codegen/codegen/visit_call.py +1305 -0
- pineforge_codegen/codegen/visit_expr.py +701 -0
- pineforge_codegen/codegen/visit_stmt.py +729 -0
- pineforge_codegen/errors.py +98 -0
- pineforge_codegen/lexer.py +531 -0
- pineforge_codegen/parser.py +1198 -0
- pineforge_codegen/pragmas.py +117 -0
- pineforge_codegen/signatures.py +808 -0
- pineforge_codegen/support_checker.py +1111 -0
- pineforge_codegen/symbols.py +118 -0
- pineforge_codegen/tokens.py +406 -0
- pineforge_codegen/tv_input_choices.py +86 -0
- pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
- pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
- pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
- pineforge_codegen-0.6.5.dist-info/licenses/LICENSE +197 -0
|
@@ -0,0 +1,1305 @@
|
|
|
1
|
+
"""Function-call dispatch visitors for the codegen.
|
|
2
|
+
|
|
3
|
+
``CallVisitor`` mixin holds the function-call dispatcher and the
|
|
4
|
+
per-namespace dispatch helpers (``strategy.*``, ``color.*``, ``str.*``,
|
|
5
|
+
``math.*``, ``fixnan``).
|
|
6
|
+
|
|
7
|
+
``_visit_func_call`` is the central entry point for any
|
|
8
|
+
:class:`FuncCall` AST node. It first handles UDT-method calls and
|
|
9
|
+
``obj.method(args)`` style receivers, then resolves the callee to a
|
|
10
|
+
``(func_name, namespace)`` pair and dispatches by namespace:
|
|
11
|
+
|
|
12
|
+
* ``strategy.*`` -> ``_visit_strategy_call`` (entry / exit / close /
|
|
13
|
+
cancel / order / closedtrades.* / opentrades.* / convert_to_*).
|
|
14
|
+
* ``ta.*`` -> ``ta.tr`` is inlined; other sites resolve to the
|
|
15
|
+
``member.compute(...)`` / ``member.recompute(...)`` form via the
|
|
16
|
+
``TaSiteHelper``; ``ta.pivot_point_levels`` is a free function.
|
|
17
|
+
* ``input`` / ``input.*`` -> runtime ``get_input_*()`` getters via
|
|
18
|
+
``InputHelper``.
|
|
19
|
+
* ``str.*`` -> ``_visit_str_call`` (tostring / substring / format
|
|
20
|
+
/ format_time / replace + the ``STR_FUNC_MAP`` shortcuts).
|
|
21
|
+
* ``math.*`` -> ``_visit_math_call`` (round-to-mintick, todegrees /
|
|
22
|
+
toradians, random, n-ary avg / max / min, ``MATH_FUNC_MAP``).
|
|
23
|
+
* ``color.*`` -> ``_visit_color_call`` (new / r / g / b / t / rgb /
|
|
24
|
+
from_gradient).
|
|
25
|
+
* ``array.*`` / ``map.*`` / ``matrix.*`` -> functional and method-syntax
|
|
26
|
+
forms, delegating to ``TypeInferer``'s ``_array_method_expr`` /
|
|
27
|
+
``_map_method_expr`` and the ``MATRIX_METHODS`` table.
|
|
28
|
+
* ``request.security``, ``ticker.*``, ``runtime.*``, ``log.*``,
|
|
29
|
+
``timeframe.*``, ``time`` / ``time_close`` / ``timestamp``, type
|
|
30
|
+
casts ``int`` / ``float`` / ``bool`` / ``string``.
|
|
31
|
+
* UDT constructors (``TypeName.new(...)``) and copies.
|
|
32
|
+
|
|
33
|
+
Anything left over is treated as a generic user-defined or unknown
|
|
34
|
+
function call: the visitor merges kwargs by parameter name (using
|
|
35
|
+
``FuncInfo`` for user functions or the ``signatures`` registry for
|
|
36
|
+
intrinsics), passes ``Series<T>`` references for series-typed params,
|
|
37
|
+
and emits ``namespace::func(args)`` (or the per-call-site variant
|
|
38
|
+
``func_csN`` for functions cloned by the analyzer's call-site
|
|
39
|
+
splitter).
|
|
40
|
+
|
|
41
|
+
``_visit_fixnan`` allocates a fresh persistent state member each time
|
|
42
|
+
it is called (``_prev_fixnan_<n>``).
|
|
43
|
+
|
|
44
|
+
``_resolve_func_args`` is a small helper that merges positional args
|
|
45
|
+
and kwargs into a ``{param_name: arg_node}`` dict using the parameter
|
|
46
|
+
ordering from the ``signatures`` registry; used by
|
|
47
|
+
``_visit_strategy_call`` to resolve ``strategy.entry`` /
|
|
48
|
+
``strategy.exit`` / ... by keyword.
|
|
49
|
+
|
|
50
|
+
These visitors were extracted from ``base.py``'s ``CodeGen`` class as
|
|
51
|
+
step 10 of the codegen package refactor; behaviour is preserved
|
|
52
|
+
verbatim. The mixin owns no state of its own — it reads/writes only
|
|
53
|
+
attributes already established on the host class (``CodeGen``).
|
|
54
|
+
|
|
55
|
+
Mixin contract — host class must provide the following attributes
|
|
56
|
+
(all set by ``CodeGen.__init__`` or other mixins):
|
|
57
|
+
|
|
58
|
+
- ``self.ctx`` (``AnalyzerContext``): symbol table source. The
|
|
59
|
+
visitors read ``ctx.symbols.resolve`` (``_visit_str_call`` enum
|
|
60
|
+
branch), ``ctx.series_vars`` (series-arg lowering),
|
|
61
|
+
``ctx.func_series_vars`` (per-function series-param indices),
|
|
62
|
+
``ctx.func_call_cs_map`` and ``ctx.func_call_site_counts``
|
|
63
|
+
(per-call-site variant naming).
|
|
64
|
+
- ``self._var_names`` (``set[str]``): names declared at module scope;
|
|
65
|
+
consulted by the ``obj.method`` receiver-detection branch.
|
|
66
|
+
- ``self._global_member_vars`` (``set[str]``): non-``var`` global
|
|
67
|
+
declarations emitted as class members (same branch).
|
|
68
|
+
- ``self._current_func_param_types`` (``dict[str, ...]``): parameters
|
|
69
|
+
of the function currently being emitted (treated as locals for the
|
|
70
|
+
receiver guard).
|
|
71
|
+
- ``self._current_loop_vars`` (``set[str]``): for-in iterator names
|
|
72
|
+
(also receivers of ``.method()`` calls).
|
|
73
|
+
- ``self._current_input_var_name`` (``str | None``): contextual var
|
|
74
|
+
name used as the title-fallback for ``input(...)`` calls.
|
|
75
|
+
- ``self._array_vars`` / ``self._map_vars`` (``set[str]``) and
|
|
76
|
+
``self._matrix_specs`` (``dict[str, TypeSpec]``): collection-typed
|
|
77
|
+
variables; gate the receiver-method branches in ``_visit_func_call``.
|
|
78
|
+
- ``self._udt_defs`` (``dict[str, list]``), ``self._udt_var_types``
|
|
79
|
+
(``dict[str, str]``), ``self._udt_param_udt`` (``dict[str, str]``),
|
|
80
|
+
``self._udt_field_type_specs`` (``dict[str, dict[str, TypeSpec]]``):
|
|
81
|
+
UDT type info for ``TypeName.new(...)`` constructors,
|
|
82
|
+
``TypeName.copy(...)``, and the ``obj.method()`` UDT-method dispatch.
|
|
83
|
+
- ``self._enum_member_strings`` (``dict[str, list[str]]``): enum -->
|
|
84
|
+
display-string table; used by ``_visit_str_call`` to render
|
|
85
|
+
``str.tostring(enumVar)`` as the field title rather than the int
|
|
86
|
+
index.
|
|
87
|
+
- ``self._func_info_map`` (``dict[str, FuncInfo]``): user-defined
|
|
88
|
+
function lookup; drives kwarg merging, UDT method dispatch, and the
|
|
89
|
+
series-arg classification.
|
|
90
|
+
- ``self._func_names`` (``set[str]``): user-defined function names;
|
|
91
|
+
controls when ``_func_safe_name`` is applied and when the
|
|
92
|
+
per-call-site variant naming kicks in.
|
|
93
|
+
- ``self._security_calls`` (``list[dict]``): normalized
|
|
94
|
+
``request.security`` records; matched by ``expr_node`` identity to
|
|
95
|
+
bind ``_req_sec_<id>`` result names.
|
|
96
|
+
- ``self._active_call_site_idx`` (``int | None``): set during
|
|
97
|
+
per-call-site function emission; controls ``_csN`` variant naming
|
|
98
|
+
for sub-function calls.
|
|
99
|
+
- ``self._active_var_remap`` (``dict[str, str]``): per-call-site
|
|
100
|
+
rename map for cloned function-local var/series names; consulted by
|
|
101
|
+
the series-arg lowering helper.
|
|
102
|
+
- ``self._fixnan_counter`` (``int``): monotonically incremented by
|
|
103
|
+
``_visit_fixnan`` to mint fresh ``_prev_fixnan_<n>`` member names.
|
|
104
|
+
- ``self._random_call_counter`` (``int``): monotonically incremented
|
|
105
|
+
by ``_visit_math_call`` for ``math.random`` so each call gets a
|
|
106
|
+
unique site id (used to seed the runtime PRNG).
|
|
107
|
+
|
|
108
|
+
Sibling-mixin methods consumed via ``self``:
|
|
109
|
+
|
|
110
|
+
- ``NamingHelper`` (``codegen/helpers.py``): ``_safe_name``,
|
|
111
|
+
``_resolve_callee``, ``_func_safe_name``.
|
|
112
|
+
- ``TypeInferer`` (``codegen/types.py``): ``_type_spec_to_cpp``,
|
|
113
|
+
``_type_spec_from_expr``, ``_type_spec_from_hint_name``,
|
|
114
|
+
``_default_for_spec``, ``_array_method_expr``,
|
|
115
|
+
``_map_method_expr``, ``_array_spec_for_name``,
|
|
116
|
+
``_map_spec_for_name``.
|
|
117
|
+
- ``TaSiteHelper`` (``codegen/ta.py``): ``_get_ta_site``,
|
|
118
|
+
``_ta_member_name``, ``_ta_compute_args_for_site``.
|
|
119
|
+
- ``InputHelper`` (``codegen/input.py``): ``_is_input_call_by_name``,
|
|
120
|
+
``_get_input_default``, ``_get_input_title``,
|
|
121
|
+
``_input_type_to_getter``,
|
|
122
|
+
``_enforce_enum_declared_before_input_enum``.
|
|
123
|
+
- ``TopLevelEmitter`` (``codegen/emit_top.py``):
|
|
124
|
+
``_emit_udt_method_cpp_name``.
|
|
125
|
+
- ``ExprVisitor`` (``codegen/visit_expr.py``): ``_visit_expr``.
|
|
126
|
+
- ``CodeGen.base``: ``_codegen_error``.
|
|
127
|
+
|
|
128
|
+
The mixin avoids importing from ``base.py`` to stay free of cycles;
|
|
129
|
+
all tables it needs come from ``codegen/tables.py``, AST classes from
|
|
130
|
+
``..ast_nodes``, and PineScript signatures from ``.. import signatures``.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
from __future__ import annotations
|
|
134
|
+
|
|
135
|
+
from ..ast_nodes import (
|
|
136
|
+
FuncCall,
|
|
137
|
+
Identifier,
|
|
138
|
+
MemberAccess,
|
|
139
|
+
TupleLiteral,
|
|
140
|
+
StringLiteral,
|
|
141
|
+
)
|
|
142
|
+
from ..symbols import TypeSpec
|
|
143
|
+
from .. import signatures as sigs
|
|
144
|
+
from .tables import (
|
|
145
|
+
ARRAY_METHODS,
|
|
146
|
+
BAR_FIELDS,
|
|
147
|
+
BAR_SERIES_PUSH,
|
|
148
|
+
MAP_METHODS,
|
|
149
|
+
MATH_FUNC_MAP,
|
|
150
|
+
MATRIX_METHODS,
|
|
151
|
+
MATRIX_METHOD_KWARGS,
|
|
152
|
+
MATRIX_NUMERIC_ONLY,
|
|
153
|
+
MATRIX_SORT_ALLOWED_GENERIC_ELEMS,
|
|
154
|
+
SKIP_FUNC_NAMES,
|
|
155
|
+
SKIP_NAMESPACES,
|
|
156
|
+
SKIP_VAR_TYPES,
|
|
157
|
+
STR_FUNC_MAP,
|
|
158
|
+
_merge_kwargs,
|
|
159
|
+
_merge_kwargs_with_defaults,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class CallVisitor:
|
|
164
|
+
"""Function-call dispatch visitor methods shared across the codegen.
|
|
165
|
+
|
|
166
|
+
Mixed into ``CodeGen``; not intended to be instantiated standalone.
|
|
167
|
+
See the module docstring for the full host-class state contract."""
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# Function-call dispatch
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def _visit_func_call(self, node: FuncCall) -> str:
|
|
174
|
+
callee = node.callee
|
|
175
|
+
if isinstance(callee, MemberAccess):
|
|
176
|
+
recv_spec = self._type_spec_from_expr(callee.object)
|
|
177
|
+
if recv_spec is not None and recv_spec.kind == "udt" and recv_spec.name:
|
|
178
|
+
mk = f"{recv_spec.name}.{callee.member}"
|
|
179
|
+
fi_u = self._func_info_map.get(mk)
|
|
180
|
+
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
181
|
+
fn_cpp = self._emit_udt_method_cpp_name(fi_u)
|
|
182
|
+
recv_e = self._visit_expr(callee.object)
|
|
183
|
+
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
184
|
+
# Drop the leading ``self`` slot from param_defaults so the
|
|
185
|
+
# parallel array lines up with ``param_names`` (rest of
|
|
186
|
+
# the signature). Probe: udt-method-probe-04-default-param.
|
|
187
|
+
param_defaults = list(getattr(fi_u, "param_defaults", []) or [])[1:]
|
|
188
|
+
rest_nodes = _merge_kwargs_with_defaults(
|
|
189
|
+
node.args, node.kwargs, param_names,
|
|
190
|
+
param_defaults, lambda x: x,
|
|
191
|
+
)
|
|
192
|
+
rest = [self._visit_expr(a) for a in rest_nodes]
|
|
193
|
+
return f"{fn_cpp}({', '.join([recv_e] + rest)})"
|
|
194
|
+
# obj.field.method(args) — must not lower to namespace::method (loses receiver chain).
|
|
195
|
+
if isinstance(callee, MemberAccess):
|
|
196
|
+
obj = callee.object
|
|
197
|
+
if isinstance(obj, MemberAccess):
|
|
198
|
+
root = obj.object
|
|
199
|
+
if not (isinstance(root, Identifier) and root.name in (
|
|
200
|
+
"strategy", "ta", "math", "input", "str", "timeframe", "syminfo",
|
|
201
|
+
"barstate", "color", "request", "runtime", "array", "matrix", "map",
|
|
202
|
+
)):
|
|
203
|
+
recv_spec = self._type_spec_from_expr(obj)
|
|
204
|
+
recv = self._visit_expr(obj)
|
|
205
|
+
meth = callee.member
|
|
206
|
+
raw_args = [self._visit_expr(a) for a in node.args]
|
|
207
|
+
if recv_spec is not None and recv_spec.kind == "array" and meth in ARRAY_METHODS:
|
|
208
|
+
return self._array_method_expr(recv, meth, raw_args, recv_spec)
|
|
209
|
+
if recv_spec is not None and recv_spec.kind == "map" and meth in MAP_METHODS:
|
|
210
|
+
return self._map_method_expr(recv, meth, raw_args, recv_spec)
|
|
211
|
+
args = ", ".join(raw_args)
|
|
212
|
+
if meth == "delete":
|
|
213
|
+
meth = "_delete_"
|
|
214
|
+
return f"{recv}.{meth}({args})"
|
|
215
|
+
# obj.method() where obj is a user var/param — not namespace::method
|
|
216
|
+
if isinstance(obj, Identifier):
|
|
217
|
+
oname = obj.name
|
|
218
|
+
if (
|
|
219
|
+
oname in self._var_names
|
|
220
|
+
or oname in self._current_func_param_types
|
|
221
|
+
or oname in self._current_loop_vars
|
|
222
|
+
or oname in self._global_member_vars
|
|
223
|
+
):
|
|
224
|
+
meth_raw = callee.member
|
|
225
|
+
# map.put / map.get / … must lower to unordered_map ops, not `.put` on C++
|
|
226
|
+
if oname in self._map_vars and meth_raw in MAP_METHODS:
|
|
227
|
+
m = self._safe_name(oname)
|
|
228
|
+
margs = [self._visit_expr(a) for a in node.args]
|
|
229
|
+
return self._map_method_expr(m, meth_raw, margs, self._map_spec_for_name(oname))
|
|
230
|
+
if oname in self._array_vars and meth_raw in ARRAY_METHODS:
|
|
231
|
+
arr = self._safe_name(oname)
|
|
232
|
+
margs = [self._visit_expr(a) for a in node.args]
|
|
233
|
+
return self._array_method_expr(arr, meth_raw, margs, self._array_spec_for_name(oname))
|
|
234
|
+
if oname in self._matrix_specs and meth_raw in MATRIX_METHODS:
|
|
235
|
+
arr = self._safe_name(oname)
|
|
236
|
+
self._check_matrix_method_allowed(meth_raw, self._matrix_specs[oname], node)
|
|
237
|
+
param_names = MATRIX_METHOD_KWARGS.get(meth_raw)
|
|
238
|
+
if param_names and node.kwargs:
|
|
239
|
+
margs = _merge_kwargs(
|
|
240
|
+
node.args, node.kwargs, param_names, self._visit_expr
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
margs = [self._visit_expr(a) for a in node.args]
|
|
244
|
+
fn = MATRIX_METHODS[meth_raw]
|
|
245
|
+
try:
|
|
246
|
+
return fn(arr, margs)
|
|
247
|
+
except IndexError:
|
|
248
|
+
self._codegen_error(
|
|
249
|
+
node,
|
|
250
|
+
f"matrix.{meth_raw}: wrong number of arguments",
|
|
251
|
+
hint="Check Pine v6 matrix method signature (positional vs keyword).",
|
|
252
|
+
)
|
|
253
|
+
safe_o = self._safe_name(oname)
|
|
254
|
+
udt_t = self._udt_var_types.get(oname) or self._udt_var_types.get(safe_o)
|
|
255
|
+
if udt_t is None:
|
|
256
|
+
udt_t = self._udt_param_udt.get(oname) or self._udt_param_udt.get(safe_o)
|
|
257
|
+
if udt_t is not None:
|
|
258
|
+
mk = f"{udt_t}.{meth_raw}"
|
|
259
|
+
fi_u = self._func_info_map.get(mk)
|
|
260
|
+
if fi_u is not None and getattr(fi_u, "is_udt_method", False):
|
|
261
|
+
fn_cpp = self._emit_udt_method_cpp_name(fi_u)
|
|
262
|
+
recv_e = self._visit_expr(obj)
|
|
263
|
+
param_names = list(fi_u.node.params[1:]) if fi_u.node else []
|
|
264
|
+
# Drop the leading ``self`` slot so param_defaults
|
|
265
|
+
# lines up with ``param_names``. Probe:
|
|
266
|
+
# udt-method-probe-04-default-param.
|
|
267
|
+
param_defaults = list(getattr(fi_u, "param_defaults", []) or [])[1:]
|
|
268
|
+
rest_nodes = _merge_kwargs_with_defaults(
|
|
269
|
+
node.args, node.kwargs, param_names,
|
|
270
|
+
param_defaults, lambda x: x,
|
|
271
|
+
)
|
|
272
|
+
rest = [self._visit_expr(a) for a in rest_nodes]
|
|
273
|
+
return f"{fn_cpp}({', '.join([recv_e] + rest)})"
|
|
274
|
+
args = ", ".join(self._visit_expr(a) for a in node.args)
|
|
275
|
+
recv = self._visit_expr(obj)
|
|
276
|
+
meth = meth_raw
|
|
277
|
+
if meth == "delete":
|
|
278
|
+
meth = "_delete_"
|
|
279
|
+
return f"{recv}.{meth}({args})"
|
|
280
|
+
|
|
281
|
+
func_name, namespace = self._resolve_callee(callee)
|
|
282
|
+
|
|
283
|
+
# na(x) -> is_na(x)
|
|
284
|
+
if func_name == "na" and namespace is None:
|
|
285
|
+
args = ", ".join(self._visit_expr(a) for a in node.args)
|
|
286
|
+
return f"is_na({args})"
|
|
287
|
+
|
|
288
|
+
# nz(x) / nz(x, y)
|
|
289
|
+
if func_name == "nz" and namespace is None:
|
|
290
|
+
x = self._visit_expr(node.args[0])
|
|
291
|
+
y = self._visit_expr(node.args[1]) if len(node.args) > 1 else "0.0"
|
|
292
|
+
return f"(is_na({x}) ? {y} : {x})"
|
|
293
|
+
|
|
294
|
+
# fixnan(x) -> persistent state
|
|
295
|
+
if func_name == "fixnan" and namespace is None:
|
|
296
|
+
return self._visit_fixnan(node)
|
|
297
|
+
|
|
298
|
+
# strategy.* calls
|
|
299
|
+
if namespace == "strategy":
|
|
300
|
+
return self._visit_strategy_call(func_name, node)
|
|
301
|
+
|
|
302
|
+
# ta.tr(handle_na) is dispatched through the standard TA-class path
|
|
303
|
+
# below: the analyzer assigns it a ``ta::TR`` call site (with
|
|
304
|
+
# ``handle_na`` threaded into the constructor); the property form
|
|
305
|
+
# ``ta.tr`` (no parens) stays inline in ``visit_expr`` so its
|
|
306
|
+
# legacy ``handle_na = true`` semantics remain bit-identical.
|
|
307
|
+
|
|
308
|
+
# ta.* calls -> member.compute(...)
|
|
309
|
+
site = self._get_ta_site(node)
|
|
310
|
+
if site is not None:
|
|
311
|
+
compute_args = self._ta_compute_args_for_site(site)
|
|
312
|
+
ta_mem = self._ta_member_name(site)
|
|
313
|
+
if getattr(self, "_precalc_loop_active", False) and getattr(site, "is_static", False):
|
|
314
|
+
return f"_precalc_{ta_mem}[i]"
|
|
315
|
+
if getattr(site, "is_static", False):
|
|
316
|
+
return f"(_use_precalc ? _precalc_{ta_mem}[bar_index_] : (is_first_tick_ ? {ta_mem}.compute({compute_args}) : {ta_mem}.recompute({compute_args})))"
|
|
317
|
+
return f"(is_first_tick_ ? {ta_mem}.compute({compute_args}) : {ta_mem}.recompute({compute_args}))"
|
|
318
|
+
|
|
319
|
+
# math.* calls
|
|
320
|
+
if namespace == "math":
|
|
321
|
+
return self._visit_math_call(func_name, node)
|
|
322
|
+
|
|
323
|
+
# input() / input.* calls -> runtime get_input_*()
|
|
324
|
+
if self._is_input_call_by_name(func_name, namespace):
|
|
325
|
+
if namespace == "input" and func_name == "enum":
|
|
326
|
+
self._enforce_enum_declared_before_input_enum(node)
|
|
327
|
+
title = self._get_input_title(node, var_name=self._current_input_var_name)
|
|
328
|
+
return self._render_input_value(node, func_name, namespace, title)
|
|
329
|
+
|
|
330
|
+
# strategy() declaration
|
|
331
|
+
if func_name == "strategy" and namespace is None:
|
|
332
|
+
return "/* strategy declaration */"
|
|
333
|
+
|
|
334
|
+
# str.* calls
|
|
335
|
+
if namespace == "str":
|
|
336
|
+
return self._visit_str_call(func_name, node)
|
|
337
|
+
|
|
338
|
+
# Map method syntax: m.put(key, val) where namespace is the map variable name
|
|
339
|
+
if namespace is not None and namespace in self._map_vars and func_name in MAP_METHODS:
|
|
340
|
+
m = self._safe_name(namespace)
|
|
341
|
+
args = [self._visit_expr(a) for a in node.args]
|
|
342
|
+
return self._map_method_expr(m, func_name, args, self._map_spec_for_name(namespace))
|
|
343
|
+
|
|
344
|
+
# map.method(m, args...) — functional form
|
|
345
|
+
if namespace == "map":
|
|
346
|
+
if func_name == "new":
|
|
347
|
+
spec = self._type_spec_from_expr(node) or TypeSpec.map(TypeSpec.primitive("string"), TypeSpec.primitive("float"))
|
|
348
|
+
return f"{self._type_spec_to_cpp(spec)}()"
|
|
349
|
+
if func_name in MAP_METHODS and node.args:
|
|
350
|
+
m = self._visit_expr(node.args[0])
|
|
351
|
+
rest = [self._visit_expr(a) for a in node.args[1:]]
|
|
352
|
+
spec = self._type_spec_from_expr(node.args[0]) if node.args else None
|
|
353
|
+
return self._map_method_expr(m, func_name, rest, spec)
|
|
354
|
+
return "0"
|
|
355
|
+
|
|
356
|
+
if namespace is not None and namespace in self._matrix_specs and func_name in MATRIX_METHODS:
|
|
357
|
+
arr = self._safe_name(namespace)
|
|
358
|
+
self._check_matrix_method_allowed(func_name, self._matrix_specs[namespace], node)
|
|
359
|
+
param_names = MATRIX_METHOD_KWARGS.get(func_name)
|
|
360
|
+
if param_names and node.kwargs:
|
|
361
|
+
args = _merge_kwargs(node.args, node.kwargs, param_names, self._visit_expr)
|
|
362
|
+
else:
|
|
363
|
+
args = [self._visit_expr(a) for a in node.args]
|
|
364
|
+
fn = MATRIX_METHODS[func_name]
|
|
365
|
+
try:
|
|
366
|
+
return fn(arr, args)
|
|
367
|
+
except IndexError:
|
|
368
|
+
self._codegen_error(
|
|
369
|
+
node,
|
|
370
|
+
f"matrix.{func_name}: wrong number of arguments",
|
|
371
|
+
hint="Check Pine v6 matrix method signature (positional vs keyword).",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Array method syntax: arr.push(val) where namespace is the array variable name
|
|
375
|
+
if namespace is not None and namespace in self._array_vars and func_name in ARRAY_METHODS:
|
|
376
|
+
arr = self._safe_name(namespace)
|
|
377
|
+
args = [self._visit_expr(a) for a in node.args]
|
|
378
|
+
return self._array_method_expr(arr, func_name, args, self._array_spec_for_name(namespace))
|
|
379
|
+
|
|
380
|
+
# Array operations — emit proper C++ vector operations
|
|
381
|
+
if namespace == "array":
|
|
382
|
+
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string"):
|
|
383
|
+
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
384
|
+
cpp_type = self._type_spec_to_cpp(spec)
|
|
385
|
+
init_default = self._default_for_spec(spec.element if spec.element is not None else TypeSpec.primitive("float"))
|
|
386
|
+
if node.args:
|
|
387
|
+
size_arg = self._visit_expr(node.args[0])
|
|
388
|
+
init_val = self._visit_expr(node.args[1]) if len(node.args) > 1 else init_default
|
|
389
|
+
return f"{cpp_type}((size_t)({size_arg}), {init_val})"
|
|
390
|
+
return f"{cpp_type}()"
|
|
391
|
+
if func_name == "from":
|
|
392
|
+
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
393
|
+
elems = ", ".join(self._visit_expr(a) for a in node.args)
|
|
394
|
+
return f"{self._type_spec_to_cpp(spec)}{{{elems}}}"
|
|
395
|
+
# Method calls: array.method(arr, args...)
|
|
396
|
+
if func_name in ARRAY_METHODS and node.args:
|
|
397
|
+
arr = self._visit_expr(node.args[0])
|
|
398
|
+
rest = [self._visit_expr(a) for a in node.args[1:]]
|
|
399
|
+
spec = self._type_spec_from_expr(node.args[0])
|
|
400
|
+
return self._array_method_expr(arr, func_name, rest, spec)
|
|
401
|
+
return "0"
|
|
402
|
+
|
|
403
|
+
# color.* calls
|
|
404
|
+
if namespace == "color":
|
|
405
|
+
return self._visit_color_call(func_name, node)
|
|
406
|
+
|
|
407
|
+
# Skip visual/unsupported namespace calls
|
|
408
|
+
if namespace in SKIP_NAMESPACES or namespace in SKIP_VAR_TYPES:
|
|
409
|
+
return "0"
|
|
410
|
+
if func_name in SKIP_FUNC_NAMES and namespace is None:
|
|
411
|
+
return "0"
|
|
412
|
+
|
|
413
|
+
# request.* calls
|
|
414
|
+
if namespace == "request":
|
|
415
|
+
if func_name == "security":
|
|
416
|
+
param_names = ["symbol", "timeframe", "expression", "gaps", "lookahead", "ignore_invalid_symbol", "currency"]
|
|
417
|
+
all_args = list(node.args)
|
|
418
|
+
for i, pname in enumerate(param_names):
|
|
419
|
+
if pname in node.kwargs:
|
|
420
|
+
while len(all_args) <= i:
|
|
421
|
+
all_args.append(None)
|
|
422
|
+
all_args[i] = node.kwargs[pname]
|
|
423
|
+
|
|
424
|
+
# Find matching security call ID
|
|
425
|
+
sec_id = None
|
|
426
|
+
tf_node = None
|
|
427
|
+
expr_node = None
|
|
428
|
+
for item in self._security_calls:
|
|
429
|
+
sid, tfn, exprn = item["sec_id"], item["tf_node"], item["expr_node"]
|
|
430
|
+
if item.get("is_lower_tf_array"):
|
|
431
|
+
continue
|
|
432
|
+
if exprn is all_args[2] if len(all_args) > 2 else False:
|
|
433
|
+
sec_id = sid
|
|
434
|
+
tf_node = tfn
|
|
435
|
+
expr_node = exprn
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
if sec_id is not None and expr_node is not None:
|
|
439
|
+
if isinstance(expr_node, TupleLiteral):
|
|
440
|
+
parts = []
|
|
441
|
+
for i, el in enumerate(expr_node.elements):
|
|
442
|
+
parts.append(f"_req_sec_{sec_id}_{i}")
|
|
443
|
+
return f"std::make_tuple({', '.join(parts)})"
|
|
444
|
+
return f"_req_sec_{sec_id}"
|
|
445
|
+
|
|
446
|
+
# Fallback
|
|
447
|
+
return "na<double>()"
|
|
448
|
+
if func_name == "security_lower_tf":
|
|
449
|
+
# ``request.security_lower_tf`` is matched against the
|
|
450
|
+
# registered SecurityCallInfo by AST identity of the
|
|
451
|
+
# ``expression`` argument (3rd positional or kwarg). The
|
|
452
|
+
# codegen lowers the call to the per-sec_id accumulator
|
|
453
|
+
# vector — its element type and clear/push semantics are
|
|
454
|
+
# set up by the security mixin.
|
|
455
|
+
ltf_param_names = [
|
|
456
|
+
"symbol", "timeframe", "expression",
|
|
457
|
+
"ignore_invalid_symbol", "currency",
|
|
458
|
+
"ignore_invalid_timeframe", "calc_bars_count",
|
|
459
|
+
]
|
|
460
|
+
ltf_all_args = list(node.args)
|
|
461
|
+
for i, pname in enumerate(ltf_param_names):
|
|
462
|
+
if pname in node.kwargs:
|
|
463
|
+
while len(ltf_all_args) <= i:
|
|
464
|
+
ltf_all_args.append(None)
|
|
465
|
+
ltf_all_args[i] = node.kwargs[pname]
|
|
466
|
+
ltf_expr_node = ltf_all_args[2] if len(ltf_all_args) > 2 else None
|
|
467
|
+
for item in self._security_calls:
|
|
468
|
+
if not item.get("is_lower_tf_array"):
|
|
469
|
+
continue
|
|
470
|
+
if item["expr_node"] is ltf_expr_node:
|
|
471
|
+
return f"_req_sec_lower_tf_{item['sec_id']}"
|
|
472
|
+
return "std::vector<double>{}"
|
|
473
|
+
# All other request.* functions
|
|
474
|
+
return "na<double>()"
|
|
475
|
+
|
|
476
|
+
# ticker.* calls
|
|
477
|
+
if namespace == "ticker":
|
|
478
|
+
# ticker.inherit(symbol, ...) and ticker.standard(symbol) — passthrough:
|
|
479
|
+
# emit the symbol argument unchanged (same-symbol passthrough).
|
|
480
|
+
if func_name in ("inherit", "standard"):
|
|
481
|
+
if node.args:
|
|
482
|
+
return self._visit_expr(node.args[0])
|
|
483
|
+
if "symbol" in node.kwargs:
|
|
484
|
+
return self._visit_expr(node.kwargs["symbol"])
|
|
485
|
+
# All other ticker.* calls are hard-rejected by support_checker;
|
|
486
|
+
# emit empty string as safe fallback if they somehow reach codegen.
|
|
487
|
+
return 'std::string("")'
|
|
488
|
+
|
|
489
|
+
# runtime.error() and other runtime.* calls
|
|
490
|
+
if namespace == "runtime":
|
|
491
|
+
if func_name == "error":
|
|
492
|
+
rt_args = [self._visit_expr(a) for a in node.args]
|
|
493
|
+
msg_arg = rt_args[0] if rt_args else '""'
|
|
494
|
+
return f'pine_runtime_error({msg_arg})'
|
|
495
|
+
return '"" /* unsupported runtime */'
|
|
496
|
+
|
|
497
|
+
# year(time) / month(time) / dayofmonth(time) / dayofweek(time) /
|
|
498
|
+
# hour(time[, tz]) / minute(time[, tz]) / second(time[, tz]) /
|
|
499
|
+
# weekofyear(time[, tz]).
|
|
500
|
+
#
|
|
501
|
+
# Pine v6 exposes these names as BOTH variables (current bar) AND
|
|
502
|
+
# functions (arbitrary timestamp). The variable form is wired by
|
|
503
|
+
# ``BAR_BUILTINS`` in codegen/tables.py to ``_bar_year()`` etc. The
|
|
504
|
+
# function form has no public runtime helper, so we inline the
|
|
505
|
+
# gmtime_r-based calculation that mirrors ``BacktestEngine::
|
|
506
|
+
# _decompose_bar_time()`` (see include/pineforge/engine.hpp) so the
|
|
507
|
+
# numbers agree across both forms.
|
|
508
|
+
#
|
|
509
|
+
# Timezone handling (per Pine v6 reference docs):
|
|
510
|
+
# - Bare form ``hour(time)`` defaults its tz argument to
|
|
511
|
+
# ``syminfo.timezone`` — the SYMBOL/EXCHANGE timezone, NOT the
|
|
512
|
+
# chart's display timezone. For the corpus' ETH-USDT crypto data
|
|
513
|
+
# this is ``"UTC"`` (the ``SymInfo`` constructor default), which
|
|
514
|
+
# keeps the lambda on the cheap ``gmtime_r`` fast path. The
|
|
515
|
+
# variable form ``hour`` is wired directly to ``_bar_hour()`` /
|
|
516
|
+
# ``_decompose_bar_time()`` (engine.hpp), which is hardcoded to
|
|
517
|
+
# ``gmtime_r`` and therefore matches the function-form default
|
|
518
|
+
# for the same exchange TZ.
|
|
519
|
+
#
|
|
520
|
+
# Pre-fix the harness's ``strategy_set_chart_timezone`` clobbered
|
|
521
|
+
# ``syminfo_.timezone`` with the chart display TZ, which silently
|
|
522
|
+
# shifted ``hour(time)``-bucketed accumulators by the
|
|
523
|
+
# chart-vs-exchange offset (Asia/Taipei vs UTC = +8h). That fix
|
|
524
|
+
# now lives entirely in ``BacktestEngine::set_chart_timezone``
|
|
525
|
+
# (engine.hpp), which writes to a dedicated ``chart_timezone_``
|
|
526
|
+
# slot and leaves ``syminfo_.timezone`` at its constructor
|
|
527
|
+
# default. This codegen still reads ``syminfo_.timezone``,
|
|
528
|
+
# matching TV semantics, with no emit-time changes.
|
|
529
|
+
# - Two-arg form ``hour(time, tz)`` always overrides syminfo with
|
|
530
|
+
# the explicit tz argument. Same setenv+localtime_r block as the
|
|
531
|
+
# 1-arg fallback.
|
|
532
|
+
_BAR_TIME_FUNC_EXPR = {
|
|
533
|
+
"year": "tm_buf.tm_year + 1900",
|
|
534
|
+
"month": "tm_buf.tm_mon + 1",
|
|
535
|
+
"dayofmonth": "tm_buf.tm_mday",
|
|
536
|
+
"dayofweek": "tm_buf.tm_wday + 1",
|
|
537
|
+
"hour": "tm_buf.tm_hour",
|
|
538
|
+
"minute": "tm_buf.tm_min",
|
|
539
|
+
"second": "tm_buf.tm_sec",
|
|
540
|
+
"weekofyear": "(tm_buf.tm_yday + 7 - ((tm_buf.tm_wday + 6) % 7)) / 7",
|
|
541
|
+
}
|
|
542
|
+
if (
|
|
543
|
+
namespace is None
|
|
544
|
+
and func_name in _BAR_TIME_FUNC_EXPR
|
|
545
|
+
and (node.args or node.kwargs)
|
|
546
|
+
):
|
|
547
|
+
params = sigs.get_param_names(None, func_name)
|
|
548
|
+
args = _merge_kwargs(node.args, node.kwargs, params, self._visit_expr)
|
|
549
|
+
ts_arg = args[0] if args else "current_bar_.timestamp"
|
|
550
|
+
tz_arg = args[1] if len(args) > 1 else None
|
|
551
|
+
field_expr = _BAR_TIME_FUNC_EXPR[func_name]
|
|
552
|
+
if tz_arg is None:
|
|
553
|
+
# 1-arg form — fall back to ``syminfo.timezone`` per TV
|
|
554
|
+
# docs (the EXCHANGE TZ, default "UTC" for the corpus'
|
|
555
|
+
# crypto data; NOT the chart display TZ — that lives in
|
|
556
|
+
# ``chart_timezone_`` on the engine and is intentionally
|
|
557
|
+
# ignored here). UTC / "" / "Etc/UTC" stay on the cheap
|
|
558
|
+
# gmtime_r path; anything else takes the same
|
|
559
|
+
# mutex-guarded setenv+localtime_r block as the 2-arg
|
|
560
|
+
# form.
|
|
561
|
+
tz_arg = "syminfo_.timezone"
|
|
562
|
+
# 2-arg form — honor the tz argument. Uses an inline setenv+
|
|
563
|
+
# localtime_r guarded by a function-local static mutex, mirroring
|
|
564
|
+
# ``pine_tz::ScopedTimezone`` (src/timezone.cpp) which is not
|
|
565
|
+
# exposed via any public ``<pineforge/...>`` header today.
|
|
566
|
+
return (
|
|
567
|
+
"[&]() -> int { "
|
|
568
|
+
f"std::string _tz = ({tz_arg}); "
|
|
569
|
+
f"time_t _secs = (time_t)(({ts_arg}) / 1000); "
|
|
570
|
+
"struct tm tm_buf; "
|
|
571
|
+
"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") { "
|
|
572
|
+
"gmtime_r(&_secs, &tm_buf); "
|
|
573
|
+
"} else { "
|
|
574
|
+
"static std::mutex _pf_tz_mu; "
|
|
575
|
+
"std::lock_guard<std::mutex> _pf_tz_lock(_pf_tz_mu); "
|
|
576
|
+
"const char* _old = std::getenv(\"TZ\"); "
|
|
577
|
+
"std::string _old_tz = _old ? _old : \"\"; bool _had_old = (_old != nullptr); "
|
|
578
|
+
"::setenv(\"TZ\", _tz.c_str(), 1); ::tzset(); "
|
|
579
|
+
"localtime_r(&_secs, &tm_buf); "
|
|
580
|
+
"if (_had_old) { ::setenv(\"TZ\", _old_tz.c_str(), 1); } "
|
|
581
|
+
"else { ::unsetenv(\"TZ\"); } ::tzset(); "
|
|
582
|
+
"} "
|
|
583
|
+
f"return {field_expr}; "
|
|
584
|
+
"}()"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# time(timeframe) or time(timeframe, session[, tz])
|
|
588
|
+
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
589
|
+
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time"), self._visit_expr)
|
|
590
|
+
tf_e = args[0] if len(args) > 0 else 'script_tf_'
|
|
591
|
+
sess = args[1] if len(args) > 1 else 'std::string("")'
|
|
592
|
+
tz_e = args[2] if len(args) > 2 else 'std::string("")'
|
|
593
|
+
return (
|
|
594
|
+
f"pine_time(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_)"
|
|
595
|
+
)
|
|
596
|
+
# time_close(timeframe) or time_close(tf, session, tz)
|
|
597
|
+
if func_name == "time_close" and namespace is None and (node.args or node.kwargs):
|
|
598
|
+
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time_close"), self._visit_expr)
|
|
599
|
+
tf_e = args[0] if len(args) > 0 else 'script_tf_'
|
|
600
|
+
sess = args[1] if len(args) > 1 else 'std::string("")'
|
|
601
|
+
tz_e = args[2] if len(args) > 2 else 'std::string("")'
|
|
602
|
+
return (
|
|
603
|
+
f"pine_time_close(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_)"
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# timestamp(year, month, day, hour, minute) → Unix ms
|
|
607
|
+
if func_name == "timestamp" and namespace is None:
|
|
608
|
+
is_tz_first = False
|
|
609
|
+
if node.args:
|
|
610
|
+
first_arg_spec = self._type_spec_from_expr(node.args[0])
|
|
611
|
+
if first_arg_spec is not None and first_arg_spec.kind == "primitive" and first_arg_spec.name == "string":
|
|
612
|
+
is_tz_first = True
|
|
613
|
+
elif isinstance(node.args[0], StringLiteral):
|
|
614
|
+
is_tz_first = True
|
|
615
|
+
|
|
616
|
+
if is_tz_first:
|
|
617
|
+
args = [self._visit_expr(a) for a in node.args]
|
|
618
|
+
tz = args[0]
|
|
619
|
+
yr = args[1] if len(args) > 1 else "1970"
|
|
620
|
+
mo = args[2] if len(args) > 2 else "1"
|
|
621
|
+
dy = args[3] if len(args) > 3 else "1"
|
|
622
|
+
hr = args[4] if len(args) > 4 else "0"
|
|
623
|
+
mn = args[5] if len(args) > 5 else "0"
|
|
624
|
+
sc = args[6] if len(args) > 6 else "0"
|
|
625
|
+
return (
|
|
626
|
+
f"[&]() -> int64_t {{ "
|
|
627
|
+
f"std::string _tz = ({tz}); "
|
|
628
|
+
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
629
|
+
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
630
|
+
f"static thread_local std::string _last_tz; "
|
|
631
|
+
f"static thread_local int _last_yr = -1, _last_mo = -1, _last_dy = -1, _last_hr = -1, _last_min = -1, _last_sc = -1; "
|
|
632
|
+
f"static thread_local int64_t _last_res = -1; "
|
|
633
|
+
f"if (_last_res != -1 && _last_tz == _tz && _last_yr == _yr && _last_mo == _mo && _last_dy == _dy && _last_hr == _hr && _last_min == _min && _last_sc == _sc) {{ "
|
|
634
|
+
f"return _last_res; "
|
|
635
|
+
f"}} "
|
|
636
|
+
f"struct tm t = {{}}; "
|
|
637
|
+
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
638
|
+
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
639
|
+
f"int64_t _res; "
|
|
640
|
+
f"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") {{ "
|
|
641
|
+
f"_res = (int64_t)timegm(&t) * 1000; "
|
|
642
|
+
f"}} else {{ "
|
|
643
|
+
f"static std::mutex _pf_ts_mu; "
|
|
644
|
+
f"std::lock_guard<std::mutex> _pf_ts_mu_lock(_pf_ts_mu); "
|
|
645
|
+
f"const char* _old = std::getenv(\"TZ\"); "
|
|
646
|
+
f"std::string _old_tz = _old ? _old : \"\"; bool _had_old = (_old != nullptr); "
|
|
647
|
+
f"::setenv(\"TZ\", _tz.c_str(), 1); ::tzset(); "
|
|
648
|
+
f"_res = (int64_t)mktime(&t) * 1000; "
|
|
649
|
+
f"if (_had_old) {{ ::setenv(\"TZ\", _old_tz.c_str(), 1); }} "
|
|
650
|
+
f"else {{ ::unsetenv(\"TZ\"); }} ::tzset(); "
|
|
651
|
+
f"}} "
|
|
652
|
+
f"_last_tz = _tz; _last_yr = _yr; _last_mo = _mo; _last_dy = _dy; _last_hr = _hr; _last_min = _min; _last_sc = _sc; "
|
|
653
|
+
f"_last_res = _res; "
|
|
654
|
+
f"return _res; "
|
|
655
|
+
f"}}()"
|
|
656
|
+
)
|
|
657
|
+
else:
|
|
658
|
+
args = [self._visit_expr(a) for a in node.args]
|
|
659
|
+
if len(args) >= 1:
|
|
660
|
+
if len(args) == 1:
|
|
661
|
+
return "0"
|
|
662
|
+
yr = args[0]
|
|
663
|
+
mo = args[1] if len(args) > 1 else "1"
|
|
664
|
+
dy = args[2] if len(args) > 2 else "1"
|
|
665
|
+
hr = args[3] if len(args) > 3 else "0"
|
|
666
|
+
mn = args[4] if len(args) > 4 else "0"
|
|
667
|
+
sc = args[5] if len(args) > 5 else "0"
|
|
668
|
+
return (
|
|
669
|
+
f"[&]() -> int64_t {{ "
|
|
670
|
+
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
671
|
+
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
672
|
+
f"static thread_local int _last_yr = -1, _last_mo = -1, _last_dy = -1, _last_hr = -1, _last_min = -1, _last_sc = -1; "
|
|
673
|
+
f"static thread_local int64_t _last_res = -1; "
|
|
674
|
+
f"if (_last_res != -1 && _last_yr == _yr && _last_mo == _mo && _last_dy == _dy && _last_hr == _hr && _last_min == _min && _last_sc == _sc) {{ "
|
|
675
|
+
f"return _last_res; "
|
|
676
|
+
f"}} "
|
|
677
|
+
f"struct tm t = {{}}; "
|
|
678
|
+
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
679
|
+
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
680
|
+
f"int64_t _res = (int64_t)timegm(&t) * 1000; "
|
|
681
|
+
f"_last_yr = _yr; _last_mo = _mo; _last_dy = _dy; _last_hr = _hr; _last_min = _min; _last_sc = _sc; "
|
|
682
|
+
f"_last_res = _res; "
|
|
683
|
+
f"return _res; "
|
|
684
|
+
f"}}()"
|
|
685
|
+
)
|
|
686
|
+
return "0"
|
|
687
|
+
|
|
688
|
+
# barssince() — unsupported. Defensive: support_checker rejects bare
|
|
689
|
+
# barssince(...) with a hint to use ta.barssince(...). Reaching here
|
|
690
|
+
# means the checker was bypassed.
|
|
691
|
+
if func_name == "barssince" and namespace is None:
|
|
692
|
+
raise ValueError(
|
|
693
|
+
"codegen: bare barssince(...) is not supported — analyzer should "
|
|
694
|
+
"have rejected. Use ta.barssince(...)."
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Type cast functions: int(x), float(x), bool(x), string(x)
|
|
698
|
+
if func_name == "int" and namespace is None and node.args:
|
|
699
|
+
return f"(int)({self._visit_expr(node.args[0])})"
|
|
700
|
+
if func_name == "float" and namespace is None and node.args:
|
|
701
|
+
return f"(double)({self._visit_expr(node.args[0])})"
|
|
702
|
+
if func_name == "bool" and namespace is None and node.args:
|
|
703
|
+
return f"(bool)({self._visit_expr(node.args[0])})"
|
|
704
|
+
if func_name == "string" and namespace is None and node.args:
|
|
705
|
+
return f"std::to_string({self._visit_expr(node.args[0])})"
|
|
706
|
+
|
|
707
|
+
# ta.pivot_point_levels — free function, not a stateful indicator
|
|
708
|
+
if namespace == "ta" and func_name == "pivot_point_levels":
|
|
709
|
+
if node.kwargs:
|
|
710
|
+
args = _merge_kwargs(
|
|
711
|
+
node.args,
|
|
712
|
+
node.kwargs,
|
|
713
|
+
sigs.get_param_names("ta", "pivot_point_levels"),
|
|
714
|
+
self._visit_expr,
|
|
715
|
+
)
|
|
716
|
+
else:
|
|
717
|
+
args = [self._visit_expr(a) for a in node.args]
|
|
718
|
+
if len(args) >= 4:
|
|
719
|
+
return f'ta::pivot_point_levels({", ".join(args[:4])})'
|
|
720
|
+
if 1 <= len(args) <= 3:
|
|
721
|
+
# Pine overload (type, anchor, developing). Per Pine v6
|
|
722
|
+
# semantics, `developing=false` (the default) means the pivot
|
|
723
|
+
# is computed from the LAST CLOSED period's HLC. With
|
|
724
|
+
# `anchor=true` constant, the "period" is one bar, so we
|
|
725
|
+
# consume the PREVIOUS bar's HLC via `_s_high[1]`, etc. The
|
|
726
|
+
# analyzer registers high/low/close in `series_bar_fields` so
|
|
727
|
+
# those `Series<double>` members are guaranteed to exist.
|
|
728
|
+
# Previously we passed `current_bar_.high/low/close` which
|
|
729
|
+
# produced TV-shifted-by-one-bar values for every level.
|
|
730
|
+
return (
|
|
731
|
+
f"ta::pivot_point_levels({args[0]}, _s_high[1], "
|
|
732
|
+
f"_s_low[1], _s_close[1])"
|
|
733
|
+
)
|
|
734
|
+
return f'ta::pivot_point_levels({", ".join(args)})'
|
|
735
|
+
|
|
736
|
+
# Unknown ta.* calls — safe fallback
|
|
737
|
+
if namespace == "ta":
|
|
738
|
+
return f"na<double>() /* unsupported: ta.{func_name} */"
|
|
739
|
+
|
|
740
|
+
if namespace == "syminfo":
|
|
741
|
+
if func_name == "prefix":
|
|
742
|
+
return "_pf_derive_prefix(syminfo_.tickerid)"
|
|
743
|
+
if func_name == "ticker":
|
|
744
|
+
return "syminfo_.ticker"
|
|
745
|
+
return f"na<double>() /* unsupported: syminfo.{func_name} */"
|
|
746
|
+
|
|
747
|
+
# str.* fallback now handled by _visit_str_call above
|
|
748
|
+
|
|
749
|
+
# matrix.* calls
|
|
750
|
+
if namespace == "matrix":
|
|
751
|
+
if func_name == "new":
|
|
752
|
+
targs = self._template_args_from_call(node)
|
|
753
|
+
elem_spec = self._type_spec_from_hint_name(targs[0]) if targs else TypeSpec.primitive("float")
|
|
754
|
+
args_e = [self._visit_expr(a) for a in node.args]
|
|
755
|
+
rows = args_e[0] if args_e else "0"
|
|
756
|
+
cols = args_e[1] if len(args_e) > 1 else "0"
|
|
757
|
+
if elem_spec.kind == "primitive" and elem_spec.name == "float":
|
|
758
|
+
init = args_e[2] if len(args_e) > 2 else "0.0"
|
|
759
|
+
return f"PineMatrix::new_({rows}, {cols}, {init})"
|
|
760
|
+
cpp_t = self._type_spec_to_cpp(elem_spec)
|
|
761
|
+
init = args_e[2] if len(args_e) > 2 else self._default_for_spec(elem_spec)
|
|
762
|
+
return f"PineGenericMatrix<{cpp_t}>::new_({rows}, {cols}, {init})"
|
|
763
|
+
if func_name in MATRIX_METHODS and node.args:
|
|
764
|
+
from ..ast_nodes import Identifier as _Ident
|
|
765
|
+
if func_name in MATRIX_NUMERIC_ONLY:
|
|
766
|
+
if not isinstance(node.args[0], _Ident):
|
|
767
|
+
self._codegen_error(node, f"matrix.{func_name} receiver must be a variable reference")
|
|
768
|
+
recv_name = node.args[0].name
|
|
769
|
+
if recv_name not in self._matrix_specs:
|
|
770
|
+
self._codegen_error(node, f"matrix.{func_name}: receiver '{recv_name}' is not a known matrix variable")
|
|
771
|
+
self._check_matrix_method_allowed(func_name, self._matrix_specs[recv_name], node)
|
|
772
|
+
if func_name == "sort":
|
|
773
|
+
if isinstance(node.args[0], _Ident):
|
|
774
|
+
recv_name = node.args[0].name
|
|
775
|
+
if recv_name in self._matrix_specs:
|
|
776
|
+
self._check_matrix_method_allowed(func_name, self._matrix_specs[recv_name], node)
|
|
777
|
+
obj = self._visit_expr(node.args[0])
|
|
778
|
+
param_names = MATRIX_METHOD_KWARGS.get(func_name)
|
|
779
|
+
if param_names:
|
|
780
|
+
rest = _merge_kwargs(node.args[1:], node.kwargs, param_names, self._visit_expr)
|
|
781
|
+
else:
|
|
782
|
+
rest = [self._visit_expr(a) for a in node.args[1:]]
|
|
783
|
+
fn = MATRIX_METHODS[func_name]
|
|
784
|
+
try:
|
|
785
|
+
return fn(obj, rest)
|
|
786
|
+
except IndexError:
|
|
787
|
+
self._codegen_error(
|
|
788
|
+
node,
|
|
789
|
+
f"matrix.{func_name}: wrong number of arguments",
|
|
790
|
+
hint="Check Pine v6 matrix method signature (positional vs keyword).",
|
|
791
|
+
)
|
|
792
|
+
return "0.0"
|
|
793
|
+
|
|
794
|
+
# log.* calls (log.error, log.warning, log.info)
|
|
795
|
+
if namespace == "log":
|
|
796
|
+
log_funcs = {"info": "pine_log_info", "warning": "pine_log_warning", "error": "pine_log_error"}
|
|
797
|
+
if func_name in log_funcs:
|
|
798
|
+
log_args = [self._visit_expr(a) for a in node.args]
|
|
799
|
+
msg_arg = log_args[0] if log_args else '""'
|
|
800
|
+
return f'{log_funcs[func_name]}({msg_arg})'
|
|
801
|
+
return '"" /* unsupported log */'
|
|
802
|
+
|
|
803
|
+
# timeframe.* calls (e.g., timeframe.change) — not supported in single-TF backtest
|
|
804
|
+
if namespace == "timeframe":
|
|
805
|
+
if func_name == "change":
|
|
806
|
+
tf_arg = self._visit_expr(node.args[0]) if node.args else 'script_tf_'
|
|
807
|
+
return f'tf_change(prev_bar_timestamp_, current_bar_.timestamp, {tf_arg})'
|
|
808
|
+
if func_name == "in_seconds":
|
|
809
|
+
tf_arg = self._visit_expr(node.args[0]) if node.args else 'script_tf_'
|
|
810
|
+
return f'tf_to_seconds({tf_arg})'
|
|
811
|
+
# Defensive: support_checker.NOT_YET_FUNC should already have rejected
|
|
812
|
+
# any unhandled timeframe.* call. Reaching here implies the checker was
|
|
813
|
+
# bypassed.
|
|
814
|
+
raise ValueError(
|
|
815
|
+
f"codegen: unhandled timeframe.{func_name} — analyzer should have "
|
|
816
|
+
f"rejected this. Either add a handler above or extend NOT_YET_FUNC."
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# UDT constructor: TypeName.new(field=val, ...)
|
|
820
|
+
if namespace in self._udt_defs and func_name == "new":
|
|
821
|
+
fields = self._udt_defs[namespace]
|
|
822
|
+
field_names = [f.name for f in fields]
|
|
823
|
+
init_vals = {}
|
|
824
|
+
for i, a in enumerate(node.args):
|
|
825
|
+
if i < len(field_names):
|
|
826
|
+
init_vals[field_names[i]] = self._visit_expr(a)
|
|
827
|
+
for k, v in node.kwargs.items():
|
|
828
|
+
init_vals[k] = self._visit_expr(v)
|
|
829
|
+
field_inits = []
|
|
830
|
+
field_specs = self._udt_field_type_specs.get(namespace, {})
|
|
831
|
+
for f in fields:
|
|
832
|
+
val = None
|
|
833
|
+
if f.name in init_vals:
|
|
834
|
+
val = init_vals[f.name]
|
|
835
|
+
elif f.default:
|
|
836
|
+
val = self._visit_expr(f.default)
|
|
837
|
+
if val is not None:
|
|
838
|
+
# Fix narrowing: cast na<double>() to correct type for int fields
|
|
839
|
+
f_cpp_type = self._type_spec_to_cpp(field_specs.get(f.name) or self._type_spec_from_hint_name(f.type_name))
|
|
840
|
+
if f_cpp_type == "int" and "na<double>" in val:
|
|
841
|
+
val = val.replace("na<double>()", "0")
|
|
842
|
+
field_inits.append(f".{f.name} = {val}")
|
|
843
|
+
return f"{namespace}{{{', '.join(field_inits)}}}"
|
|
844
|
+
|
|
845
|
+
# UDT copy: TypeName.copy(obj)
|
|
846
|
+
if namespace in self._udt_defs and func_name == "copy":
|
|
847
|
+
if node.args:
|
|
848
|
+
return self._visit_expr(node.args[0])
|
|
849
|
+
return f"{namespace}{{}}"
|
|
850
|
+
|
|
851
|
+
# Safety net before the generic emitter. Every builtin namespace and
|
|
852
|
+
# bare builtin that codegen knows how to emit has been dispatched (and
|
|
853
|
+
# returned) above; SKIP_NAMESPACES / SKIP_FUNC_NAMES returned "0";
|
|
854
|
+
# user-defined functions live in ``self._func_names`` and UDT
|
|
855
|
+
# constructors/copies were handled via ``self._udt_defs``. Anything
|
|
856
|
+
# still here would be written out verbatim — ``made_up(...)`` or
|
|
857
|
+
# ``qux::frobnicate(...)`` — i.e. an *undeclared C++ symbol*. That is a
|
|
858
|
+
# silent miscompile: the support checker did not reject it, so the user
|
|
859
|
+
# would otherwise only see a cryptic g++ error pointing at generated
|
|
860
|
+
# C++ instead of their Pine line. Reject loudly with the offending
|
|
861
|
+
# node's location. (Note: any script that reached this branch already
|
|
862
|
+
# failed to compile, so the all-green corpus never exercises it — this
|
|
863
|
+
# only converts garbage output into a clean diagnostic.)
|
|
864
|
+
# ``func_name is None`` means the callee is a complex expression the
|
|
865
|
+
# resolver does not reduce to a simple ``name`` / ``ns.name`` — e.g. a
|
|
866
|
+
# chained method call ``m.transpose().copy()`` whose receiver is itself
|
|
867
|
+
# a FuncCall. Those are handled by the existing generic/chained logic
|
|
868
|
+
# below; do not treat them as unknown builtins.
|
|
869
|
+
if namespace is None and func_name is not None:
|
|
870
|
+
if func_name not in self._func_names:
|
|
871
|
+
self._codegen_error(
|
|
872
|
+
node,
|
|
873
|
+
f"Unknown function '{func_name}(...)' — not a PineForge "
|
|
874
|
+
f"builtin or a user-defined function.",
|
|
875
|
+
hint="Check the spelling; the function may not be supported "
|
|
876
|
+
"by PineForge, or needs its namespace (e.g. math./str.).",
|
|
877
|
+
)
|
|
878
|
+
elif namespace is not None and namespace not in self._udt_defs:
|
|
879
|
+
self._codegen_error(
|
|
880
|
+
node,
|
|
881
|
+
f"Unknown call '{namespace}.{func_name}(...)' — '{namespace}' is "
|
|
882
|
+
f"not a PineForge-supported namespace or a user-defined type.",
|
|
883
|
+
hint="Check the spelling; this namespace may not be supported "
|
|
884
|
+
"by PineForge.",
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
# Generic function call (user-defined or unknown)
|
|
888
|
+
# Determine which params are series (need Series<double> arg, not scalar)
|
|
889
|
+
_func_series_param_indices: set[int] = set()
|
|
890
|
+
fi_lookup = self._func_info_map.get(func_name)
|
|
891
|
+
if fi_lookup and fi_lookup.node:
|
|
892
|
+
func_sv = self.ctx.func_series_vars.get(fi_lookup.name, set())
|
|
893
|
+
for p_idx, p_name in enumerate(fi_lookup.node.params):
|
|
894
|
+
if p_name in func_sv:
|
|
895
|
+
_func_series_param_indices.add(p_idx)
|
|
896
|
+
|
|
897
|
+
def _visit_arg_for_series(arg_node, arg_idx):
|
|
898
|
+
"""Visit a function argument, returning Series ref for series params."""
|
|
899
|
+
if arg_idx in _func_series_param_indices and isinstance(arg_node, Identifier):
|
|
900
|
+
aname = arg_node.name
|
|
901
|
+
# Bar field: pass _s_close instead of current_bar_.close
|
|
902
|
+
if aname in BAR_FIELDS or aname in BAR_SERIES_PUSH:
|
|
903
|
+
return f"_s_{aname}"
|
|
904
|
+
# Series var: pass the Series object directly
|
|
905
|
+
if aname in self.ctx.series_vars:
|
|
906
|
+
safe = self._safe_name(aname)
|
|
907
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
908
|
+
safe = self._active_var_remap[safe]
|
|
909
|
+
return safe
|
|
910
|
+
return self._visit_expr(arg_node)
|
|
911
|
+
|
|
912
|
+
if node.kwargs:
|
|
913
|
+
# Try to resolve kwargs using FuncInfo params for user-defined functions
|
|
914
|
+
fi = self._func_info_map.get(func_name)
|
|
915
|
+
if fi and fi.node and fi.node.params:
|
|
916
|
+
param_names = list(fi.node.params) # params is list[str]
|
|
917
|
+
# Merge kwargs then visit with series awareness
|
|
918
|
+
merged = _merge_kwargs(node.args, node.kwargs, param_names, lambda a: a)
|
|
919
|
+
all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(merged)]
|
|
920
|
+
elif sigs.is_intrinsic_function(namespace, func_name):
|
|
921
|
+
# Known intrinsic — use signature registry for kwargs resolution
|
|
922
|
+
param_names = sigs.get_param_names(namespace, func_name)
|
|
923
|
+
all_args = _merge_kwargs(node.args, node.kwargs, param_names, self._visit_expr)
|
|
924
|
+
else:
|
|
925
|
+
# Unknown function: positional args + kwargs values as fallback
|
|
926
|
+
all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(node.args)]
|
|
927
|
+
all_args.extend(self._visit_expr(v) for v in node.kwargs.values())
|
|
928
|
+
else:
|
|
929
|
+
all_args = [_visit_arg_for_series(a, i) for i, a in enumerate(node.args)]
|
|
930
|
+
# Default args (parser does not store defaults): isInSession(sess, res = timeframe.period)
|
|
931
|
+
if namespace is None and func_name in self._func_names:
|
|
932
|
+
fi = self._func_info_map.get(func_name)
|
|
933
|
+
if fi and fi.node and fi.name == "isInSession" and len(fi.node.params) >= 2 and len(all_args) == 1:
|
|
934
|
+
# Mirror Pine default `timeframe.period` instead of hard-coding 15m.
|
|
935
|
+
all_args.append("script_tf_")
|
|
936
|
+
prefix = f"{namespace}::" if namespace else ""
|
|
937
|
+
# Use safe name for user-defined functions to avoid member name collision
|
|
938
|
+
emit_name = self._func_safe_name(func_name) if func_name in self._func_names else func_name
|
|
939
|
+
# Per-call-site variant: if this function has TA/series calls, call the correct variant
|
|
940
|
+
cs_info = self.ctx.func_call_cs_map.get(id(node))
|
|
941
|
+
if self._active_call_site_idx is not None and cs_info is not None:
|
|
942
|
+
# Inside a per-call-site variant: override the cs_map index with
|
|
943
|
+
# the parent's active call-site index. This ensures sub-functions
|
|
944
|
+
# called from ma_cs6() use their _cs6 variant, not _cs0.
|
|
945
|
+
fname, _ = cs_info
|
|
946
|
+
emit_name = f"{self._func_safe_name(fname)}_cs{self._active_call_site_idx}"
|
|
947
|
+
elif cs_info is not None:
|
|
948
|
+
fname, cs_idx = cs_info
|
|
949
|
+
emit_name = f"{self._func_safe_name(fname)}_cs{cs_idx}"
|
|
950
|
+
elif (self._active_call_site_idx is not None
|
|
951
|
+
and func_name in self._func_names
|
|
952
|
+
and self.ctx.func_call_site_counts.get(func_name, 0) > 1):
|
|
953
|
+
# Inside a per-call-site variant: propagate call-site index to
|
|
954
|
+
# sub-functions that also have variants (for state isolation)
|
|
955
|
+
emit_name = f"{self._func_safe_name(func_name)}_cs{self._active_call_site_idx}"
|
|
956
|
+
return f"{prefix}{emit_name}({', '.join(all_args)})"
|
|
957
|
+
|
|
958
|
+
def _visit_fixnan(self, node: FuncCall) -> str:
|
|
959
|
+
"""Emit fixnan with persistent state member."""
|
|
960
|
+
self._fixnan_counter += 1
|
|
961
|
+
member = f"_prev_fixnan_{self._fixnan_counter}"
|
|
962
|
+
x = self._visit_expr(node.args[0])
|
|
963
|
+
return f"(is_na({x}) ? {member} : ({member} = {x}))"
|
|
964
|
+
|
|
965
|
+
def _visit_strategy_call(self, func_name: str, node: FuncCall) -> str:
|
|
966
|
+
if func_name in ("convert_to_account", "convert_to_symbol"):
|
|
967
|
+
p = self._resolve_func_args(node, f"strategy.{func_name}")
|
|
968
|
+
v = self._visit_expr(p.get("value")) if p.get("value") is not None else "0.0"
|
|
969
|
+
return f"({v})"
|
|
970
|
+
if func_name == "default_entry_qty":
|
|
971
|
+
p = self._resolve_func_args(node, "strategy.default_entry_qty")
|
|
972
|
+
fp = self._visit_expr(p.get("fill_price")) if p.get("fill_price") is not None else "0.0"
|
|
973
|
+
return f"calc_qty({fp})"
|
|
974
|
+
|
|
975
|
+
if func_name == "entry":
|
|
976
|
+
p = self._resolve_func_args(node, "strategy.entry")
|
|
977
|
+
entry_id = self._visit_expr(p.get("id")) if "id" in p else '""'
|
|
978
|
+
direction = self._visit_expr(p.get("direction")) if "direction" in p else "true"
|
|
979
|
+
stop = p.get("stop")
|
|
980
|
+
limit = p.get("limit")
|
|
981
|
+
qty = p.get("qty")
|
|
982
|
+
comment = p.get("comment")
|
|
983
|
+
oca_name = p.get("oca_name")
|
|
984
|
+
oca_type = p.get("oca_type")
|
|
985
|
+
qty_type = p.get("qty_type")
|
|
986
|
+
comment_val = self._visit_expr(comment) if comment else '""'
|
|
987
|
+
oca_name_val = self._visit_expr(oca_name) if oca_name else '""'
|
|
988
|
+
oca_type_val = self._visit_expr(oca_type) if oca_type else "0"
|
|
989
|
+
qty_type_val = self._visit_expr(qty_type) if qty_type else "-1"
|
|
990
|
+
qty_val = self._visit_expr(qty) if qty else "na<double>()"
|
|
991
|
+
if stop is not None or limit is not None or qty is not None or oca_name is not None or oca_type is not None or qty_type is not None:
|
|
992
|
+
limit_val = self._visit_expr(limit) if limit else "na<double>()"
|
|
993
|
+
stop_val = self._visit_expr(stop) if stop else "na<double>()"
|
|
994
|
+
# pineforge-engine v0.2 dropped the vestigial `market_price`
|
|
995
|
+
# third positional from `BacktestEngine::strategy_entry`
|
|
996
|
+
# (the runtime never read it; fill price always came from
|
|
997
|
+
# current_bar_.close inside the function body). Codegen now
|
|
998
|
+
# matches the new signature: (id, direction, limit, stop,
|
|
999
|
+
# qty, comment, oca_name, oca_type, qty_type).
|
|
1000
|
+
return f"strategy_entry({entry_id}, {direction}, {limit_val}, {stop_val}, {qty_val}, {comment_val}, {oca_name_val}, {oca_type_val}, {qty_type_val})"
|
|
1001
|
+
return f"strategy_entry({entry_id}, {direction}, na<double>(), na<double>(), na<double>(), {comment_val})"
|
|
1002
|
+
|
|
1003
|
+
if func_name == "close":
|
|
1004
|
+
p = self._resolve_func_args(node, "strategy.close")
|
|
1005
|
+
close_id = self._visit_expr(p.get("id")) if "id" in p else '""'
|
|
1006
|
+
comment = self._visit_expr(p.get("comment")) if p.get("comment") is not None else '""'
|
|
1007
|
+
qty = self._visit_expr(p.get("qty")) if p.get("qty") is not None else "na<double>()"
|
|
1008
|
+
qty_pct = self._visit_expr(p.get("qty_percent")) if p.get("qty_percent") is not None else "na<double>()"
|
|
1009
|
+
immediately = self._visit_expr(p.get("immediately")) if p.get("immediately") is not None else "false"
|
|
1010
|
+
return f"strategy_close({close_id}, {comment}, {qty}, {qty_pct}, {immediately})"
|
|
1011
|
+
|
|
1012
|
+
if func_name == "close_all":
|
|
1013
|
+
return "strategy_close_all()"
|
|
1014
|
+
|
|
1015
|
+
if func_name == "exit":
|
|
1016
|
+
p = self._resolve_func_args(node, "strategy.exit")
|
|
1017
|
+
exit_id = self._visit_expr(p.get("id")) if "id" in p else '""'
|
|
1018
|
+
from_id = self._visit_expr(p.get("from_entry")) if "from_entry" in p else '""'
|
|
1019
|
+
|
|
1020
|
+
limit_n = p.get("limit")
|
|
1021
|
+
stop_n = p.get("stop")
|
|
1022
|
+
profit_n = p.get("profit")
|
|
1023
|
+
loss_n = p.get("loss")
|
|
1024
|
+
trail_pts_n = p.get("trail_points")
|
|
1025
|
+
trail_off_n = p.get("trail_offset")
|
|
1026
|
+
trail_pr_n = p.get("trail_price")
|
|
1027
|
+
qty_pct_n = p.get("qty_percent")
|
|
1028
|
+
qty_n = p.get("qty")
|
|
1029
|
+
comment_n = p.get("comment")
|
|
1030
|
+
oca_name_n = p.get("oca_name")
|
|
1031
|
+
|
|
1032
|
+
has_price_exit = any(x is not None for x in
|
|
1033
|
+
[limit_n, stop_n, profit_n, loss_n,
|
|
1034
|
+
trail_pts_n, trail_off_n, trail_pr_n])
|
|
1035
|
+
if has_price_exit:
|
|
1036
|
+
limit_val = self._visit_expr(limit_n) if limit_n else "na<double>()"
|
|
1037
|
+
stop_val = self._visit_expr(stop_n) if stop_n else "na<double>()"
|
|
1038
|
+
trail_pts = self._visit_expr(trail_pts_n) if trail_pts_n else "na<double>()"
|
|
1039
|
+
trail_off = self._visit_expr(trail_off_n) if trail_off_n else "na<double>()"
|
|
1040
|
+
trail_pr = self._visit_expr(trail_pr_n) if trail_pr_n else "na<double>()"
|
|
1041
|
+
qty_pct = self._visit_expr(qty_pct_n) if qty_pct_n else "100.0"
|
|
1042
|
+
qty_val = self._visit_expr(qty_n) if qty_n else "na<double>()"
|
|
1043
|
+
comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1044
|
+
oca_val = self._visit_expr(oca_name_n) if oca_name_n is not None else '""'
|
|
1045
|
+
|
|
1046
|
+
if profit_n and not limit_n:
|
|
1047
|
+
ticks = self._visit_expr(profit_n)
|
|
1048
|
+
limit_val = f"(position_entry_price_ + (signed_position_size() > 0 ? 1.0 : -1.0) * ({ticks}) * syminfo_mintick_)"
|
|
1049
|
+
if loss_n and not stop_n:
|
|
1050
|
+
ticks = self._visit_expr(loss_n)
|
|
1051
|
+
stop_val = f"(position_entry_price_ - (signed_position_size() > 0 ? 1.0 : -1.0) * ({ticks}) * syminfo_mintick_)"
|
|
1052
|
+
|
|
1053
|
+
return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
|
|
1054
|
+
f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
|
|
1055
|
+
f"{qty_val}, {oca_val})")
|
|
1056
|
+
close_comment = self._visit_expr(comment_n) if comment_n is not None else '""'
|
|
1057
|
+
return f"strategy_close({exit_id}, {close_comment})"
|
|
1058
|
+
|
|
1059
|
+
if func_name == "cancel":
|
|
1060
|
+
p = self._resolve_func_args(node, "strategy.close") # same shape: id first
|
|
1061
|
+
cancel_id = self._visit_expr(p.get("id")) if "id" in p else '""'
|
|
1062
|
+
return f"strategy_cancel({cancel_id})"
|
|
1063
|
+
|
|
1064
|
+
if func_name == "cancel_all":
|
|
1065
|
+
return "strategy_cancel_all()"
|
|
1066
|
+
|
|
1067
|
+
if func_name == "order":
|
|
1068
|
+
p = self._resolve_func_args(node, "strategy.order")
|
|
1069
|
+
order_id = self._visit_expr(p.get("id")) if "id" in p else '""'
|
|
1070
|
+
direction = self._visit_expr(p.get("direction")) if "direction" in p else "true"
|
|
1071
|
+
qty = self._visit_expr(p.get("qty")) if "qty" in p else "0"
|
|
1072
|
+
limit_arg = self._visit_expr(p.get("limit")) if "limit" in p else "na<double>()"
|
|
1073
|
+
stop_arg = self._visit_expr(p.get("stop")) if "stop" in p else "na<double>()"
|
|
1074
|
+
oca_name = self._visit_expr(p.get("oca_name")) if "oca_name" in p else '""'
|
|
1075
|
+
oca_type = self._visit_expr(p.get("oca_type")) if "oca_type" in p else "0"
|
|
1076
|
+
return f"strategy_order({order_id}, {direction}, {qty}, {limit_arg}, {stop_arg}, {oca_name}, {oca_type})"
|
|
1077
|
+
|
|
1078
|
+
if func_name == "risk":
|
|
1079
|
+
return "/* skip */"
|
|
1080
|
+
|
|
1081
|
+
# strategy.closedtrades.*(idx) / strategy.opentrades.*(idx)
|
|
1082
|
+
# These come through as func_name="profit" etc. with nested callee
|
|
1083
|
+
if isinstance(node.callee, MemberAccess):
|
|
1084
|
+
inner = node.callee.object
|
|
1085
|
+
if isinstance(inner, MemberAccess) and inner.member in ("closedtrades", "opentrades"):
|
|
1086
|
+
idx = self._visit_expr(node.args[0]) if node.args else "0"
|
|
1087
|
+
is_open = inner.member == "opentrades"
|
|
1088
|
+
# Open trades have no exit metadata in Pine
|
|
1089
|
+
if is_open and func_name in (
|
|
1090
|
+
"exit_price", "exit_time", "exit_comment", "exit_id", "exit_bar_index",
|
|
1091
|
+
):
|
|
1092
|
+
if func_name == "exit_bar_index":
|
|
1093
|
+
return "na<int>()"
|
|
1094
|
+
if func_name == "exit_time":
|
|
1095
|
+
return "0"
|
|
1096
|
+
if func_name == "exit_price":
|
|
1097
|
+
return "na<double>()"
|
|
1098
|
+
if func_name in ("exit_comment", "exit_id"):
|
|
1099
|
+
return "std::string()"
|
|
1100
|
+
|
|
1101
|
+
prefix = "open_trade_" if is_open else "closed_trade_"
|
|
1102
|
+
suffix_map = {
|
|
1103
|
+
"profit": "profit",
|
|
1104
|
+
"profit_percent": "profit_percent",
|
|
1105
|
+
"commission": "commission",
|
|
1106
|
+
"direction": "direction",
|
|
1107
|
+
"entry_bar_index": "entry_bar_index",
|
|
1108
|
+
"exit_bar_index": "exit_bar_index",
|
|
1109
|
+
"entry_comment": "entry_comment",
|
|
1110
|
+
"exit_comment": "exit_comment",
|
|
1111
|
+
"entry_id": "entry_id",
|
|
1112
|
+
"exit_id": "exit_id",
|
|
1113
|
+
"entry_price": "entry_price",
|
|
1114
|
+
"exit_price": "exit_price",
|
|
1115
|
+
"entry_time": "entry_time",
|
|
1116
|
+
"exit_time": "exit_time",
|
|
1117
|
+
"size": "size",
|
|
1118
|
+
"max_runup": "max_runup",
|
|
1119
|
+
"max_runup_percent": "max_runup_percent",
|
|
1120
|
+
"max_drawdown": "max_drawdown",
|
|
1121
|
+
"max_drawdown_percent": "max_drawdown_percent",
|
|
1122
|
+
}
|
|
1123
|
+
fn = suffix_map.get(func_name, "profit")
|
|
1124
|
+
return f"{prefix}{fn}({idx})"
|
|
1125
|
+
|
|
1126
|
+
# Defensive: support_checker rejects unknown strategy.* calls (name not
|
|
1127
|
+
# in sigs.STRATEGY_FUNCTIONS) and unknown strategy.closedtrades.* /
|
|
1128
|
+
# strategy.opentrades.* accessors (not in the side-specific accessor
|
|
1129
|
+
# whitelists). Reaching here means the checker was bypassed or drifted.
|
|
1130
|
+
raise ValueError(
|
|
1131
|
+
f"codegen: unhandled strategy.{func_name}(...) — analyzer should "
|
|
1132
|
+
f"have rejected. Add a handler above or extend STRATEGY_FUNCTIONS."
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
def _visit_color_call(self, func_name: str, node) -> str:
|
|
1136
|
+
"""Emit color.* calls as integer representations."""
|
|
1137
|
+
args = [self._visit_expr(a) for a in node.args]
|
|
1138
|
+
if func_name == "new":
|
|
1139
|
+
if len(args) >= 2:
|
|
1140
|
+
return f'pine_color::new_color({args[0]}, (int)({args[1]}))'
|
|
1141
|
+
return "0"
|
|
1142
|
+
if func_name in ("r", "g", "b", "t"):
|
|
1143
|
+
if args:
|
|
1144
|
+
return f'pine_color::{func_name}({args[0]})'
|
|
1145
|
+
return "0"
|
|
1146
|
+
if func_name == "rgb":
|
|
1147
|
+
if len(args) >= 4:
|
|
1148
|
+
return f"pine_color::new_color(((int64_t)({args[0]}) << 16 | (int64_t)({args[1]}) << 8 | (int64_t)({args[2]})), (int)({args[3]}))"
|
|
1149
|
+
elif len(args) >= 3:
|
|
1150
|
+
return f"pine_color::new_color(((int64_t)({args[0]}) << 16 | (int64_t)({args[1]}) << 8 | (int64_t)({args[2]})), 0)"
|
|
1151
|
+
return "0"
|
|
1152
|
+
if func_name == "from_gradient":
|
|
1153
|
+
return "0"
|
|
1154
|
+
return "0"
|
|
1155
|
+
|
|
1156
|
+
def _visit_str_call(self, func_name: str, node) -> str:
|
|
1157
|
+
args = _merge_kwargs(node.args, node.kwargs,
|
|
1158
|
+
sigs.get_param_names("str", func_name), self._visit_expr)
|
|
1159
|
+
|
|
1160
|
+
if func_name == "tostring":
|
|
1161
|
+
# Pine: str.tostring(enumVar) → field title / IANA string, not the int index
|
|
1162
|
+
val_arg = node.args[0] if node.args else node.kwargs.get("value")
|
|
1163
|
+
if isinstance(val_arg, Identifier):
|
|
1164
|
+
sym = self.ctx.symbols.resolve(val_arg.name)
|
|
1165
|
+
if sym is not None and sym.enum_type_name:
|
|
1166
|
+
et = sym.enum_type_name
|
|
1167
|
+
tbl = self._enum_member_strings.get(et)
|
|
1168
|
+
if tbl:
|
|
1169
|
+
var = self._safe_name(val_arg.name)
|
|
1170
|
+
n = len(tbl)
|
|
1171
|
+
return (
|
|
1172
|
+
f"pine_enum_str_at({et}_str_values, {n}, {var})"
|
|
1173
|
+
)
|
|
1174
|
+
if len(args) >= 2:
|
|
1175
|
+
return f"pine_str_tostring({args[0]}, {args[1]}, syminfo_mintick_)"
|
|
1176
|
+
if len(args) >= 1:
|
|
1177
|
+
return f"std::to_string({args[0]})"
|
|
1178
|
+
return 'std::string("")'
|
|
1179
|
+
|
|
1180
|
+
if func_name == "substring":
|
|
1181
|
+
if len(args) == 3:
|
|
1182
|
+
return f"{args[0]}.substr({args[1]}, {args[2]} - {args[1]})"
|
|
1183
|
+
elif len(args) == 2:
|
|
1184
|
+
return f"{args[0]}.substr({args[1]})"
|
|
1185
|
+
return 'std::string("")'
|
|
1186
|
+
|
|
1187
|
+
if func_name == "format":
|
|
1188
|
+
# str.format is variadic: signature has only ``formatStr``; the
|
|
1189
|
+
# remaining args are placeholder substitutions. The runtime
|
|
1190
|
+
# ``pine_str_format(fmt, vector<string>)`` requires every arg
|
|
1191
|
+
# already converted to ``std::string``. We previously gated the
|
|
1192
|
+
# ``std::to_string`` wrap on a source-text-prefix heuristic
|
|
1193
|
+
# (``"`` / ``std::string`` / ``pine_str``), which mis-classified
|
|
1194
|
+
# any string-typed bare identifier or string-returning helper
|
|
1195
|
+
# call (e.g. ``str.tostring(x)`` bound to a variable). The type
|
|
1196
|
+
# check below uses the analyzer's inferred PineType instead so
|
|
1197
|
+
# ``std::string`` args pass through unchanged.
|
|
1198
|
+
if node.args:
|
|
1199
|
+
fmt_arg = self._visit_expr(node.args[0])
|
|
1200
|
+
rest = []
|
|
1201
|
+
for orig in node.args[1:]:
|
|
1202
|
+
visited = self._visit_expr(orig)
|
|
1203
|
+
inferred = self._infer_type(orig)
|
|
1204
|
+
if inferred == "std::string":
|
|
1205
|
+
rest.append(visited)
|
|
1206
|
+
continue
|
|
1207
|
+
# Booleans render as 0/1 via std::to_string; force the
|
|
1208
|
+
# TV-style "true"/"false" output so backtest logs and
|
|
1209
|
+
# alert messages line up with the TradingView side.
|
|
1210
|
+
if inferred == "bool":
|
|
1211
|
+
rest.append(
|
|
1212
|
+
f'({visited} ? std::string("true") : std::string("false"))'
|
|
1213
|
+
)
|
|
1214
|
+
continue
|
|
1215
|
+
rest.append(f'std::to_string({visited})')
|
|
1216
|
+
if rest:
|
|
1217
|
+
vec = "{" + ", ".join(rest) + "}"
|
|
1218
|
+
return f'pine_str_format({fmt_arg}, {vec})'
|
|
1219
|
+
return fmt_arg
|
|
1220
|
+
return 'std::string("")'
|
|
1221
|
+
|
|
1222
|
+
if func_name == "format_time":
|
|
1223
|
+
ts = args[0] if args else "0"
|
|
1224
|
+
fmt = args[1] if len(args) > 1 else '"yyyy-MM-dd"'
|
|
1225
|
+
tz = args[2] if len(args) > 2 else '"UTC"'
|
|
1226
|
+
return f'pine_str_format_time({ts}, {fmt}, {tz})'
|
|
1227
|
+
|
|
1228
|
+
if func_name == "replace":
|
|
1229
|
+
if len(args) >= 3:
|
|
1230
|
+
return f'[&](){{ std::string s={args[0]}; auto p=s.find({args[1]}); if(p!=std::string::npos) s.replace(p,{args[1]}.length(),{args[2]}); return s; }}()'
|
|
1231
|
+
return 'std::string("")'
|
|
1232
|
+
|
|
1233
|
+
if func_name in STR_FUNC_MAP and STR_FUNC_MAP[func_name] is not None:
|
|
1234
|
+
return STR_FUNC_MAP[func_name](args)
|
|
1235
|
+
|
|
1236
|
+
return f'std::string("") /* unsupported: str.{func_name} */'
|
|
1237
|
+
|
|
1238
|
+
def _visit_math_call(self, func_name: str, node: FuncCall) -> str:
|
|
1239
|
+
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names("math", func_name), self._visit_expr)
|
|
1240
|
+
# Handle special cases first
|
|
1241
|
+
if func_name == "round" and len(args) == 2:
|
|
1242
|
+
return f"(std::round({args[0]} * std::pow(10.0, {args[1]})) / std::pow(10.0, {args[1]}))"
|
|
1243
|
+
if func_name == "round_to_mintick":
|
|
1244
|
+
x = args[0] if args else "0.0"
|
|
1245
|
+
return f"(std::round({x} / syminfo_mintick_) * syminfo_mintick_)"
|
|
1246
|
+
if func_name == "todegrees":
|
|
1247
|
+
x = args[0] if args else "0.0"
|
|
1248
|
+
return f"({x} * 180.0 / M_PI)"
|
|
1249
|
+
if func_name == "toradians":
|
|
1250
|
+
x = args[0] if args else "0.0"
|
|
1251
|
+
return f"({x} * M_PI / 180.0)"
|
|
1252
|
+
if func_name == "random":
|
|
1253
|
+
lo = args[0] if len(args) > 0 else "0.0"
|
|
1254
|
+
hi = args[1] if len(args) > 1 else "1.0"
|
|
1255
|
+
seed = args[2] if len(args) > 2 else "0"
|
|
1256
|
+
call_site = self._random_call_counter
|
|
1257
|
+
self._random_call_counter += 1
|
|
1258
|
+
return f"pine_random({lo}, {call_site}u, {hi}, (uint32_t)({seed}), bar_index_)"
|
|
1259
|
+
if func_name == "avg" and len(args) > 2:
|
|
1260
|
+
sum_expr = " + ".join(f"(double)({a})" for a in args)
|
|
1261
|
+
return f"(({sum_expr}) / {len(args)}.0)"
|
|
1262
|
+
if func_name == "max" and len(args) > 2:
|
|
1263
|
+
result = f"std::max((double)({args[0]}), (double)({args[1]}))"
|
|
1264
|
+
for a in args[2:]:
|
|
1265
|
+
result = f"std::max({result}, (double)({a}))"
|
|
1266
|
+
return result
|
|
1267
|
+
if func_name == "min" and len(args) > 2:
|
|
1268
|
+
result = f"std::min((double)({args[0]}), (double)({args[1]}))"
|
|
1269
|
+
for a in args[2:]:
|
|
1270
|
+
result = f"std::min({result}, (double)({a}))"
|
|
1271
|
+
return result
|
|
1272
|
+
if func_name in MATH_FUNC_MAP:
|
|
1273
|
+
mapped = MATH_FUNC_MAP[func_name]
|
|
1274
|
+
if "{0}" in mapped:
|
|
1275
|
+
return mapped.format(*args)
|
|
1276
|
+
# std::min/std::max require same types — cast to double
|
|
1277
|
+
if func_name in ("min", "max") and len(args) == 2:
|
|
1278
|
+
return f"{mapped}((double)({args[0]}), (double)({args[1]}))"
|
|
1279
|
+
return f"{mapped}({', '.join(args)})"
|
|
1280
|
+
# Unknown math.* — safe fallback
|
|
1281
|
+
return f"0.0 /* unsupported: math.{func_name} */"
|
|
1282
|
+
|
|
1283
|
+
# ------------------------------------------------------------------
|
|
1284
|
+
# Arg/kwarg resolution (PineScript parameter signatures)
|
|
1285
|
+
# ------------------------------------------------------------------
|
|
1286
|
+
|
|
1287
|
+
def _resolve_func_args(self, node: FuncCall, sig_key: str) -> dict:
|
|
1288
|
+
"""Merge positional args and kwargs into a dict keyed by parameter name.
|
|
1289
|
+
|
|
1290
|
+
Uses the PineScript parameter ordering from signatures registry.
|
|
1291
|
+
"""
|
|
1292
|
+
# sig_key is like "strategy.entry" -> namespace="strategy", func_name="entry"
|
|
1293
|
+
parts = sig_key.split(".", 1)
|
|
1294
|
+
if len(parts) == 2:
|
|
1295
|
+
param_names = sigs.get_param_names(parts[0], parts[1]) or []
|
|
1296
|
+
else:
|
|
1297
|
+
param_names = sigs.get_param_names(None, sig_key) or []
|
|
1298
|
+
result: dict = {}
|
|
1299
|
+
# Map positional args by parameter name
|
|
1300
|
+
for i, arg in enumerate(node.args):
|
|
1301
|
+
if i < len(param_names):
|
|
1302
|
+
result[param_names[i]] = arg
|
|
1303
|
+
# kwargs override positional
|
|
1304
|
+
result.update(node.kwargs)
|
|
1305
|
+
return result
|