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,701 @@
|
|
|
1
|
+
"""Expression-level visitors for the codegen.
|
|
2
|
+
|
|
3
|
+
``ExprVisitor`` holds the expression-level visitors. ``_visit_expr`` is
|
|
4
|
+
the central dispatcher that inspects the AST node kind and either emits
|
|
5
|
+
a literal directly (numbers, strings, booleans, ``na``, color literals)
|
|
6
|
+
or delegates to one of the kind-specific handlers:
|
|
7
|
+
|
|
8
|
+
* ``_visit_ident`` — identifier resolution (parameters, ``BAR_FIELDS`` /
|
|
9
|
+
``BAR_BUILTINS`` builtins, known constants, series-var current value
|
|
10
|
+
reads, per-call-site var remapping).
|
|
11
|
+
* ``_visit_member_access`` — namespace member access for ``strategy`` /
|
|
12
|
+
``math`` / ``ta`` / ``timeframe`` / ``barstate`` / ``syminfo`` /
|
|
13
|
+
``display`` / ``color``, nested ``strategy.*.*`` (oca / direction /
|
|
14
|
+
commission / closedtrades / opentrades), enum member access, and the
|
|
15
|
+
fallback path for UDT-style member access on known variables.
|
|
16
|
+
* ``_visit_binop`` — binary operators (with the Pine ``and`` / ``or``
|
|
17
|
+
-> ``&&`` / ``||`` rewrite and ``%`` -> ``std::fmod``).
|
|
18
|
+
* ``_visit_unaryop`` — unary ``not`` -> ``!`` plus pass-through of
|
|
19
|
+
numeric unary operators.
|
|
20
|
+
* ``_visit_subscript`` — array/series ``[k]`` history access (function
|
|
21
|
+
parameters, bar-field series, user series-vars, ``strategy.*[k]``
|
|
22
|
+
history shadow series, generic fallback).
|
|
23
|
+
|
|
24
|
+
These visitors were extracted from ``base.py``'s ``CodeGen`` class as
|
|
25
|
+
step 9 of the codegen package refactor; behaviour is preserved
|
|
26
|
+
verbatim. The mixin owns no state of its own — it reads/writes only
|
|
27
|
+
attributes already established on the host class (``CodeGen``).
|
|
28
|
+
|
|
29
|
+
Mixin contract — host class must provide the following attributes
|
|
30
|
+
(all set by ``CodeGen.__init__`` or other mixins):
|
|
31
|
+
|
|
32
|
+
- ``self.ctx`` (``AnalyzerContext``): symbol table source. The
|
|
33
|
+
visitors read ``ctx.symbols.resolve`` (``_visit_ident`` /
|
|
34
|
+
``_visit_member_access``) to disambiguate ``color`` and UDT
|
|
35
|
+
identifiers, ``ctx.series_vars`` to decide between current-value
|
|
36
|
+
reads and history reads, and ``ctx.ta_call_sites`` to find the
|
|
37
|
+
no-arg TA-property call site (``ta.obv`` / ``ta.accdist`` / ...).
|
|
38
|
+
- ``self._known_vars`` (``dict[str, bool|int|float|str]``): inlined
|
|
39
|
+
literal constants; consulted by ``_visit_ident``.
|
|
40
|
+
- ``self._input_backed_vars`` (``set[str]``): names backed by
|
|
41
|
+
``input.*`` calls — these are NOT inlined even when in
|
|
42
|
+
``_known_vars`` because ``strategy_set_input()`` can override them
|
|
43
|
+
at runtime.
|
|
44
|
+
- ``self._var_names`` (``set[str]``): names declared at module scope.
|
|
45
|
+
Consulted by ``_visit_ident`` (to decide whether ``color`` is a
|
|
46
|
+
type name vs a variable), ``_visit_member_access`` (UDT fallback)
|
|
47
|
+
and ``_visit_subscript`` (generic fallback).
|
|
48
|
+
- ``self._global_member_vars`` (``set[str]``): non-``var`` global
|
|
49
|
+
declarations emitted as class members; ``_visit_ident`` uses it as
|
|
50
|
+
part of the ``color``-as-type-name guard.
|
|
51
|
+
- ``self._current_func_locals`` (``set[str]``): function-local names
|
|
52
|
+
emitted in the current function body (used by the ``color``
|
|
53
|
+
identifier guard in ``_visit_ident``).
|
|
54
|
+
- ``self._current_func_param_types`` (``dict[str, ...]``): parameter
|
|
55
|
+
name → type for the function currently being emitted; used by
|
|
56
|
+
``_visit_ident``, ``_visit_member_access`` and ``_visit_subscript``
|
|
57
|
+
to treat parameters as plain locals (never series-member reads).
|
|
58
|
+
- ``self._current_func_series_params`` (``set[str]``): subset of the
|
|
59
|
+
current function's parameters that are series-typed; treated as
|
|
60
|
+
``Series<T>`` so ``src`` becomes ``src[0]`` and ``src[k]`` stays
|
|
61
|
+
``src[k]``.
|
|
62
|
+
- ``self._current_loop_vars`` (``set[str]``): for-in iterator names;
|
|
63
|
+
used by ``_visit_member_access`` to distinguish iterator member
|
|
64
|
+
access from enum constants.
|
|
65
|
+
- ``self._active_var_remap`` (``dict[str, str]``): per-call-site
|
|
66
|
+
rename map for cloned function-local var/series names; applied in
|
|
67
|
+
``_visit_ident`` / ``_visit_member_access`` / ``_visit_subscript``.
|
|
68
|
+
- ``self._enum_defs`` (``dict[str, dict[str, ...]]``): enum name →
|
|
69
|
+
member map; ``_visit_member_access`` consults it before falling
|
|
70
|
+
through to the generic ``Identifier.member`` path.
|
|
71
|
+
- ``self._strategy_series_vars`` (``set[str]``): collected by
|
|
72
|
+
``_visit_subscript`` whenever it encounters a
|
|
73
|
+
``strategy.<member>[k]`` history access; the emitter creates
|
|
74
|
+
matching ``_strat_<member>`` series later.
|
|
75
|
+
|
|
76
|
+
Sibling-mixin methods consumed via ``self``:
|
|
77
|
+
|
|
78
|
+
- ``NamingHelper`` (``codegen/helpers.py``): ``_safe_name``.
|
|
79
|
+
- ``CodeGen.base``: ``_visit_func_call`` (still on the host class —
|
|
80
|
+
the call-dispatch helpers and ``_visit_func_call`` itself are
|
|
81
|
+
extracted in step 10 of the refactor).
|
|
82
|
+
|
|
83
|
+
The mixin avoids importing from ``base.py`` to stay free of cycles;
|
|
84
|
+
all tables it needs come from ``codegen/tables.py`` and all AST
|
|
85
|
+
classes from ``..ast_nodes``.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
from __future__ import annotations
|
|
89
|
+
|
|
90
|
+
from ..ast_nodes import (
|
|
91
|
+
ASTNode,
|
|
92
|
+
BinOp,
|
|
93
|
+
BoolLiteral,
|
|
94
|
+
ColorLiteral,
|
|
95
|
+
FuncCall,
|
|
96
|
+
Identifier,
|
|
97
|
+
MemberAccess,
|
|
98
|
+
NaLiteral,
|
|
99
|
+
NumberLiteral,
|
|
100
|
+
StringLiteral,
|
|
101
|
+
Subscript,
|
|
102
|
+
Ternary,
|
|
103
|
+
TupleLiteral,
|
|
104
|
+
UnaryOp,
|
|
105
|
+
)
|
|
106
|
+
from .tables import (
|
|
107
|
+
ADJUSTMENT_MAP,
|
|
108
|
+
BAR_BUILTINS,
|
|
109
|
+
BAR_FIELDS,
|
|
110
|
+
BAR_SERIES_PUSH,
|
|
111
|
+
COLOR_CONST_MAP,
|
|
112
|
+
DAYOFWEEK_MAP,
|
|
113
|
+
DISPLAY_MAP,
|
|
114
|
+
ON_OFF_INHERIT_MAP,
|
|
115
|
+
ORDER_DIRECTION_MAP,
|
|
116
|
+
SKIP_NAMESPACES,
|
|
117
|
+
SYMINFO_MEMBER_MAP,
|
|
118
|
+
TA_COMPUTE_ARGS,
|
|
119
|
+
TA_IMPLICIT_COMPUTE_FULL,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _color_literal_to_int64(value: str) -> str:
|
|
124
|
+
"""Lower a Pine hex color literal to a packed ARGB int64 C++ literal.
|
|
125
|
+
|
|
126
|
+
Pine ``#RRGGBB`` is opaque; ``#RRGGBBAA`` carries an explicit alpha
|
|
127
|
+
(opacity, FF = opaque). The engine packs colors as ``0xAARRGGBB``
|
|
128
|
+
(see color.hpp: ``red == 0xFFFF0000``), so we reorder to alpha-first
|
|
129
|
+
and emit an ``int64_t`` literal — matching ``get_input_int64`` and
|
|
130
|
+
``pine_color::*``. A malformed literal falls back to 0 (transparent).
|
|
131
|
+
"""
|
|
132
|
+
h = value.lstrip("#").strip()
|
|
133
|
+
if len(h) == 6:
|
|
134
|
+
rr, gg, bb, aa = h[0:2], h[2:4], h[4:6], "ff"
|
|
135
|
+
elif len(h) == 8:
|
|
136
|
+
rr, gg, bb, aa = h[0:2], h[2:4], h[4:6], h[6:8]
|
|
137
|
+
else:
|
|
138
|
+
return "0"
|
|
139
|
+
try:
|
|
140
|
+
int(rr + gg + bb + aa, 16) # validate hex
|
|
141
|
+
except ValueError:
|
|
142
|
+
return "0"
|
|
143
|
+
return f"0x{aa}{rr}{gg}{bb}LL"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Valid Pine v6 bare builtins that have no value in a batch backtest (no live
|
|
147
|
+
# feed). Reading them used to emit an undeclared C++ symbol; they now get a
|
|
148
|
+
# tailored hint via the unknown-identifier guard in ``_visit_ident``.
|
|
149
|
+
_REALTIME_ONLY_VARS: frozenset[str] = frozenset({"ask", "bid"})
|
|
150
|
+
|
|
151
|
+
# Builtin namespace prefixes (``format.mintick``, ``barmerge.gaps_on``, …).
|
|
152
|
+
# When a member-access handler has no dedicated branch it falls through to the
|
|
153
|
+
# generic tail, which visits the root identifier bare — so these must not be
|
|
154
|
+
# flagged as unknown variables. Mirrors the namespace list in
|
|
155
|
+
# ``analyzer/base.py::_visit_Identifier``.
|
|
156
|
+
_BUILTIN_NAMESPACE_NAMES: frozenset[str] = frozenset({
|
|
157
|
+
"strategy", "ta", "input", "math", "str", "color", "display", "syminfo",
|
|
158
|
+
"timeframe", "plot", "alert", "barstate", "position", "shape", "location",
|
|
159
|
+
"size", "currency", "order", "format", "text", "extend", "xloc", "yloc",
|
|
160
|
+
"label", "line", "box", "table", "ticker", "request", "runtime", "chart",
|
|
161
|
+
"barmerge", "adjustment", "earnings", "dividends", "splits", "session",
|
|
162
|
+
"scale", "font", "hline", "backadjustment", "settlement_as_close",
|
|
163
|
+
"dayofweek", "array", "matrix", "map", "polyline", "linefill",
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ExprVisitor:
|
|
168
|
+
"""Expression-level visitor methods shared across the codegen.
|
|
169
|
+
|
|
170
|
+
Mixed into ``CodeGen``; not intended to be instantiated standalone.
|
|
171
|
+
See the module docstring for the full host-class state contract."""
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
# Expression visitors
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def _visit_expr(self, node: ASTNode | None) -> str:
|
|
178
|
+
if node is None:
|
|
179
|
+
return "/* null */"
|
|
180
|
+
if isinstance(node, NumberLiteral):
|
|
181
|
+
return str(node.value)
|
|
182
|
+
if isinstance(node, StringLiteral):
|
|
183
|
+
return f'std::string("{node.value}")'
|
|
184
|
+
if isinstance(node, BoolLiteral):
|
|
185
|
+
return "true" if node.value else "false"
|
|
186
|
+
if isinstance(node, NaLiteral):
|
|
187
|
+
return "na<double>()"
|
|
188
|
+
if isinstance(node, ColorLiteral):
|
|
189
|
+
return _color_literal_to_int64(node.value)
|
|
190
|
+
if isinstance(node, Identifier):
|
|
191
|
+
return self._visit_ident(node)
|
|
192
|
+
if isinstance(node, MemberAccess):
|
|
193
|
+
return self._visit_member_access(node)
|
|
194
|
+
if isinstance(node, BinOp):
|
|
195
|
+
return self._visit_binop(node)
|
|
196
|
+
if isinstance(node, UnaryOp):
|
|
197
|
+
return self._visit_unaryop(node)
|
|
198
|
+
if isinstance(node, Ternary):
|
|
199
|
+
c = self._visit_expr(node.condition)
|
|
200
|
+
t = self._visit_expr(node.true_val)
|
|
201
|
+
f = self._visit_expr(node.false_val)
|
|
202
|
+
return f"(({c}) ? ({t}) : ({f}))"
|
|
203
|
+
if isinstance(node, FuncCall):
|
|
204
|
+
return self._visit_func_call(node)
|
|
205
|
+
if isinstance(node, Subscript):
|
|
206
|
+
return self._visit_subscript(node)
|
|
207
|
+
if isinstance(node, TupleLiteral):
|
|
208
|
+
elems = ", ".join(self._visit_expr(e) for e in node.elements)
|
|
209
|
+
return f"std::make_tuple({elems})"
|
|
210
|
+
return "/* unknown */"
|
|
211
|
+
|
|
212
|
+
def _visit_ident(self, node: Identifier) -> str:
|
|
213
|
+
name = node.name
|
|
214
|
+
# Bare 'na' identifier → na<double>()
|
|
215
|
+
if name == "na":
|
|
216
|
+
return "na<double>()"
|
|
217
|
+
# Function parameters that are series — read current value
|
|
218
|
+
if name in self._current_func_series_params:
|
|
219
|
+
return f"{self._safe_name(name)}[0]"
|
|
220
|
+
# Function parameters are plain locals, never series members
|
|
221
|
+
if name in self._current_func_param_types:
|
|
222
|
+
return self._safe_name(name)
|
|
223
|
+
if name in BAR_FIELDS:
|
|
224
|
+
return BAR_FIELDS[name]
|
|
225
|
+
if name in BAR_BUILTINS:
|
|
226
|
+
return BAR_BUILTINS[name]
|
|
227
|
+
# Inline known constants (literals). Input-backed names are NOT inlined
|
|
228
|
+
# because strategy_set_input() can override them at runtime; emit the
|
|
229
|
+
# variable name and let get_input_*() produce the current value.
|
|
230
|
+
if name in self._known_vars and name not in self._input_backed_vars:
|
|
231
|
+
val = self._known_vars[name]
|
|
232
|
+
if isinstance(val, bool):
|
|
233
|
+
return "true" if val else "false"
|
|
234
|
+
if isinstance(val, (int, float)):
|
|
235
|
+
return str(val)
|
|
236
|
+
if isinstance(val, str):
|
|
237
|
+
return f'std::string("{val}")'
|
|
238
|
+
# Pine type name `color` used as a value (no variable) → int64 color constant.
|
|
239
|
+
# Params handled above; symbol table does not retain function locals after analysis.
|
|
240
|
+
if name == "color":
|
|
241
|
+
sym = self.ctx.symbols.resolve(node.name)
|
|
242
|
+
if (
|
|
243
|
+
sym is None
|
|
244
|
+
and name not in self.ctx.series_vars
|
|
245
|
+
and name not in self._var_names
|
|
246
|
+
and name not in self._global_member_vars
|
|
247
|
+
and name not in self._current_func_locals
|
|
248
|
+
):
|
|
249
|
+
return "(int64_t)pine_color::black"
|
|
250
|
+
# Series var — read current value
|
|
251
|
+
safe = self._safe_name(name)
|
|
252
|
+
# Apply per-call-site var remap (for function-local vars)
|
|
253
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
254
|
+
safe = self._active_var_remap[safe]
|
|
255
|
+
if name in self.ctx.series_vars:
|
|
256
|
+
return f"{safe}[0]"
|
|
257
|
+
# Safety net: by here the name resolved to none of the builtins,
|
|
258
|
+
# constants, parameters, or declared variables handled above, so
|
|
259
|
+
# ``return safe`` would emit a bare identifier that is an *undeclared
|
|
260
|
+
# C++ symbol* (e.g. ``x = ask;``, ``x = undefined_var;``). That is a
|
|
261
|
+
# silent miscompile — g++ fails against generated C++ with no Pine
|
|
262
|
+
# line. Reject loudly. (Like the call-site guard in visit_call.py,
|
|
263
|
+
# this branch only fires on input that already failed to compile, so
|
|
264
|
+
# the all-green corpus never exercises it.)
|
|
265
|
+
if not self._ident_is_resolvable(name):
|
|
266
|
+
hint = None
|
|
267
|
+
if name in _REALTIME_ONLY_VARS:
|
|
268
|
+
hint = (f"'{name}' is a realtime-only Pine v6 builtin with no "
|
|
269
|
+
f"value in a batch backtest.")
|
|
270
|
+
self._codegen_error(
|
|
271
|
+
node,
|
|
272
|
+
f"Unknown variable '{name}' — not a PineForge builtin or a "
|
|
273
|
+
f"declared variable.",
|
|
274
|
+
hint=hint,
|
|
275
|
+
)
|
|
276
|
+
return safe
|
|
277
|
+
|
|
278
|
+
def _ident_is_resolvable(self, name: str) -> bool:
|
|
279
|
+
"""True when a bare identifier maps to a builtin, constant, parameter,
|
|
280
|
+
or any declared variable/type/enum/function — i.e. emitting it will not
|
|
281
|
+
produce an undeclared C++ symbol. Deliberately generous: a missing
|
|
282
|
+
source here only means a genuinely-unknown name slips through (the
|
|
283
|
+
pre-existing behavior), whereas a wrong ``True`` is safe. Only a
|
|
284
|
+
name in NONE of these is rejected."""
|
|
285
|
+
if name == "na" or name == "color":
|
|
286
|
+
return True
|
|
287
|
+
if name in BAR_FIELDS or name in BAR_BUILTINS:
|
|
288
|
+
return True
|
|
289
|
+
if name in _BUILTIN_NAMESPACE_NAMES:
|
|
290
|
+
return True
|
|
291
|
+
if name in self._known_vars or name in self._input_backed_vars:
|
|
292
|
+
return True
|
|
293
|
+
if name in self._var_names or name in self._global_member_vars:
|
|
294
|
+
return True
|
|
295
|
+
if name in self._current_func_locals or name in self._current_loop_vars:
|
|
296
|
+
return True
|
|
297
|
+
if name in self._current_func_param_types or name in self._current_func_series_params:
|
|
298
|
+
return True
|
|
299
|
+
if name in self.ctx.series_vars:
|
|
300
|
+
return True
|
|
301
|
+
if (name in self._array_vars or name in self._map_vars
|
|
302
|
+
or name in self._matrix_specs or name in self._udt_var_types):
|
|
303
|
+
return True
|
|
304
|
+
if name in self._enum_defs or name in self._udt_defs or name in self._func_names:
|
|
305
|
+
return True
|
|
306
|
+
# Names bound anywhere in the program (block-scoped locals the per-scope
|
|
307
|
+
# sets above never see — e.g. a var declared inside an on_bar for-loop).
|
|
308
|
+
if name in self._all_bound_names:
|
|
309
|
+
return True
|
|
310
|
+
# Catch-all: the analyzer's symbol table knows every declared name it
|
|
311
|
+
# retained (globals, inputs, …). Anything it resolves is legitimate.
|
|
312
|
+
if self.ctx.symbols.resolve(name) is not None:
|
|
313
|
+
return True
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
def _visit_member_access(self, node: MemberAccess) -> str:
|
|
317
|
+
# UDT field whose type was a drawing primitive (label/line/box/
|
|
318
|
+
# linefill/polyline/table/chart.point) — the field is dropped from
|
|
319
|
+
# the emitted struct, so a raw ``recv.field`` would fail to compile.
|
|
320
|
+
# Emit a labelled zero placeholder so any read site stays well-formed.
|
|
321
|
+
# See: pineforge-codegen issue #10.
|
|
322
|
+
if self._is_omitted_udt_field(node):
|
|
323
|
+
return "/* drawing field omitted */ 0"
|
|
324
|
+
if isinstance(node.object, Identifier):
|
|
325
|
+
ns = node.object.name
|
|
326
|
+
if ns == "strategy":
|
|
327
|
+
# Direction constants
|
|
328
|
+
if node.member == "long":
|
|
329
|
+
return "true"
|
|
330
|
+
if node.member == "short":
|
|
331
|
+
return "false"
|
|
332
|
+
# Position info
|
|
333
|
+
if node.member == "position_size":
|
|
334
|
+
return "signed_position_size()"
|
|
335
|
+
if node.member == "position_avg_price":
|
|
336
|
+
return "position_entry_price_"
|
|
337
|
+
if node.member == "position_entry_name":
|
|
338
|
+
return "position_entry_name()"
|
|
339
|
+
# Trade counts
|
|
340
|
+
if node.member == "opentrades":
|
|
341
|
+
return "((int)pyramid_entries_.size())"
|
|
342
|
+
if node.member == "closedtrades":
|
|
343
|
+
return "((int)trades_.size())"
|
|
344
|
+
if node.member == "wintrades":
|
|
345
|
+
return "count_wintrades()"
|
|
346
|
+
if node.member == "losstrades":
|
|
347
|
+
return "count_losstrades()"
|
|
348
|
+
if node.member == "eventrades":
|
|
349
|
+
return "eventrades()"
|
|
350
|
+
# Equity and P&L
|
|
351
|
+
if node.member == "equity":
|
|
352
|
+
return "(current_equity() + open_profit(current_bar_.close))"
|
|
353
|
+
if node.member == "initial_capital":
|
|
354
|
+
return "initial_capital_"
|
|
355
|
+
if node.member == "netprofit":
|
|
356
|
+
return "net_profit()"
|
|
357
|
+
if node.member == "netprofit_percent":
|
|
358
|
+
return "((net_profit() / initial_capital_) * 100.0)"
|
|
359
|
+
if node.member == "openprofit":
|
|
360
|
+
return "open_profit(current_bar_.close)"
|
|
361
|
+
if node.member == "openprofit_percent":
|
|
362
|
+
return "((open_profit(current_bar_.close) / initial_capital_) * 100.0)"
|
|
363
|
+
if node.member == "grossprofit":
|
|
364
|
+
return "gross_profit()"
|
|
365
|
+
if node.member == "grossloss":
|
|
366
|
+
return "gross_loss()"
|
|
367
|
+
if node.member == "grossprofit_percent":
|
|
368
|
+
return "grossprofit_percent()"
|
|
369
|
+
if node.member == "grossloss_percent":
|
|
370
|
+
return "grossloss_percent()"
|
|
371
|
+
if node.member == "max_contracts_held_all":
|
|
372
|
+
return "max_contracts_held_all()"
|
|
373
|
+
if node.member == "max_contracts_held_long":
|
|
374
|
+
return "max_contracts_held_long()"
|
|
375
|
+
if node.member == "max_contracts_held_short":
|
|
376
|
+
return "max_contracts_held_short()"
|
|
377
|
+
if node.member == "max_drawdown":
|
|
378
|
+
return "max_drawdown_"
|
|
379
|
+
if node.member == "max_runup":
|
|
380
|
+
return "max_runup_"
|
|
381
|
+
if node.member == "max_drawdown_percent":
|
|
382
|
+
return "max_drawdown_percent()"
|
|
383
|
+
if node.member == "max_runup_percent":
|
|
384
|
+
return "max_runup_percent()"
|
|
385
|
+
if node.member == "avg_trade":
|
|
386
|
+
return "avg_trade()"
|
|
387
|
+
if node.member == "avg_trade_percent":
|
|
388
|
+
return "avg_trade_percent()"
|
|
389
|
+
if node.member == "avg_winning_trade":
|
|
390
|
+
return "avg_winning_trade()"
|
|
391
|
+
if node.member == "avg_losing_trade":
|
|
392
|
+
return "avg_losing_trade()"
|
|
393
|
+
if node.member == "avg_winning_trade_percent":
|
|
394
|
+
return "avg_winning_trade_percent()"
|
|
395
|
+
if node.member == "avg_losing_trade_percent":
|
|
396
|
+
return "avg_losing_trade_percent()"
|
|
397
|
+
if node.member == "margin_liquidation_price":
|
|
398
|
+
return "margin_liquidation_price()"
|
|
399
|
+
# Qty type / commission constants
|
|
400
|
+
if node.member == "fixed":
|
|
401
|
+
return "0"
|
|
402
|
+
if node.member == "percent_of_equity":
|
|
403
|
+
return "1"
|
|
404
|
+
if node.member == "cash":
|
|
405
|
+
return "2"
|
|
406
|
+
if node.member == "account_currency":
|
|
407
|
+
return "syminfo_.currency"
|
|
408
|
+
# Sub-namespaces (oca, direction, risk, commission) handled below
|
|
409
|
+
if ns == "math":
|
|
410
|
+
if node.member == "pi":
|
|
411
|
+
return "M_PI"
|
|
412
|
+
if node.member == "e":
|
|
413
|
+
return "M_E"
|
|
414
|
+
if node.member == "phi":
|
|
415
|
+
return "1.618033988749895"
|
|
416
|
+
if node.member == "rphi":
|
|
417
|
+
return "0.6180339887498949"
|
|
418
|
+
if node.member == "abs":
|
|
419
|
+
return "std::abs"
|
|
420
|
+
if node.member == "max":
|
|
421
|
+
return "std::max"
|
|
422
|
+
if node.member == "min":
|
|
423
|
+
return "std::min"
|
|
424
|
+
if ns == "ta":
|
|
425
|
+
if node.member == "tr":
|
|
426
|
+
return ("(std::isnan(_s_close[1]) ? "
|
|
427
|
+
"(current_bar_.high - current_bar_.low) : "
|
|
428
|
+
"std::max(current_bar_.high - current_bar_.low, "
|
|
429
|
+
"std::max(std::abs(current_bar_.high - _s_close[1]), "
|
|
430
|
+
"std::abs(current_bar_.low - _s_close[1]))))")
|
|
431
|
+
# No-arg TA indicators used as properties (ta.obv, ta.accdist, etc.)
|
|
432
|
+
if (
|
|
433
|
+
(node.member in TA_IMPLICIT_COMPUTE_FULL and node.member in TA_COMPUTE_ARGS and TA_COMPUTE_ARGS[node.member] == [])
|
|
434
|
+
or node.member == "vwap"
|
|
435
|
+
):
|
|
436
|
+
# Find the matching call site
|
|
437
|
+
for site in self.ctx.ta_call_sites:
|
|
438
|
+
ta_short = site.class_name.split("::")[-1].lower()
|
|
439
|
+
if site.member_name.startswith(f"_ta_{node.member}_"):
|
|
440
|
+
if node.member == "vwap":
|
|
441
|
+
return f"(is_first_tick_ ? {site.member_name}.compute(current_bar_.close, current_bar_.volume, current_bar_.timestamp) : {site.member_name}.recompute(current_bar_.close, current_bar_.volume, current_bar_.timestamp))"
|
|
442
|
+
return f"(is_first_tick_ ? {site.member_name}.compute({TA_IMPLICIT_COMPUTE_FULL[node.member]}) : {site.member_name}.recompute({TA_IMPLICIT_COMPUTE_FULL[node.member]}))"
|
|
443
|
+
# Fallback: no call site found — treat as string
|
|
444
|
+
return f'std::string("{node.member}")'
|
|
445
|
+
if ns == "chart":
|
|
446
|
+
# PineForge batch engine always runs on standard OHLCV bars.
|
|
447
|
+
if node.member == "is_standard":
|
|
448
|
+
return "true"
|
|
449
|
+
# All non-standard chart types are false in batch mode.
|
|
450
|
+
if node.member in ("is_heikinashi", "is_kagi", "is_linebreak",
|
|
451
|
+
"is_pnf", "is_range", "is_renko"):
|
|
452
|
+
return "false"
|
|
453
|
+
# Defensive: support_checker.UNSUPPORTED_MEMBERS should already have
|
|
454
|
+
# rejected any unhandled chart.* member. Reaching here is a bug.
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f"codegen: unhandled chart.{node.member} — analyzer should have rejected. "
|
|
457
|
+
f"Add a handler above or extend UNSUPPORTED_MEMBERS."
|
|
458
|
+
)
|
|
459
|
+
if ns == "timeframe":
|
|
460
|
+
if node.member == "period":
|
|
461
|
+
return 'script_tf_'
|
|
462
|
+
if node.member == "main_period":
|
|
463
|
+
return 'main_period()'
|
|
464
|
+
if node.member == "multiplier":
|
|
465
|
+
return 'tf_multiplier(script_tf_)'
|
|
466
|
+
if node.member == "isintraday":
|
|
467
|
+
return 'tf_is_intraday(script_tf_)'
|
|
468
|
+
if node.member == "isminutes":
|
|
469
|
+
return '(tf_is_intraday(script_tf_) && !tf_is_seconds(script_tf_))'
|
|
470
|
+
if node.member == "isdaily":
|
|
471
|
+
return 'tf_is_daily(script_tf_)'
|
|
472
|
+
if node.member == "isweekly":
|
|
473
|
+
return 'tf_is_weekly(script_tf_)'
|
|
474
|
+
if node.member == "ismonthly":
|
|
475
|
+
return 'tf_is_monthly(script_tf_)'
|
|
476
|
+
if node.member == "isdwm":
|
|
477
|
+
return '(tf_is_daily(script_tf_) || tf_is_weekly(script_tf_) || tf_is_monthly(script_tf_))'
|
|
478
|
+
if node.member == "isseconds":
|
|
479
|
+
return 'tf_is_seconds(script_tf_)'
|
|
480
|
+
if node.member == "in_seconds":
|
|
481
|
+
return 'tf_to_seconds(script_tf_)'
|
|
482
|
+
if node.member == "isticks":
|
|
483
|
+
# Batch engine does not support tick-resolution data.
|
|
484
|
+
return "false"
|
|
485
|
+
# Defensive: the if-chain above covers every Pine v6 timeframe.*
|
|
486
|
+
# namespace variable. An unknown member is invalid Pine that
|
|
487
|
+
# would otherwise emit a silent "0". Mirror the chart.* guard.
|
|
488
|
+
raise ValueError(
|
|
489
|
+
f"codegen: unhandled timeframe.{node.member} — not a valid "
|
|
490
|
+
f"Pine v6 timeframe namespace variable. Add a handler above "
|
|
491
|
+
f"if this is a new builtin."
|
|
492
|
+
)
|
|
493
|
+
if ns == "barstate":
|
|
494
|
+
if node.member == "isfirst":
|
|
495
|
+
return "(bar_index_ == 0)"
|
|
496
|
+
if node.member == "islast":
|
|
497
|
+
return "barstate_islast_"
|
|
498
|
+
if node.member == "isnew":
|
|
499
|
+
return "is_first_tick_"
|
|
500
|
+
if node.member == "isconfirmed":
|
|
501
|
+
return "is_last_tick_"
|
|
502
|
+
if node.member == "ishistory":
|
|
503
|
+
return "true"
|
|
504
|
+
if node.member == "isrealtime":
|
|
505
|
+
return "false"
|
|
506
|
+
if node.member == "islastconfirmedhistory":
|
|
507
|
+
return "barstate_islast_"
|
|
508
|
+
return "false"
|
|
509
|
+
if ns in ("backadjustment", "settlement_as_close"):
|
|
510
|
+
# backadjustment.{on,off,inherit} / settlement_as_close.{on,off,inherit}
|
|
511
|
+
# Emit as integer constants (accepted by engine, which ignores them).
|
|
512
|
+
# Codegen silently drops these from request.security kwargs.
|
|
513
|
+
# Unknown member falls back to "inherit" (2).
|
|
514
|
+
return ON_OFF_INHERIT_MAP.get(node.member, "2")
|
|
515
|
+
if ns == "adjustment":
|
|
516
|
+
# adjustment.none / dividends / splits. Unknown member -> "none" (0).
|
|
517
|
+
return ADJUSTMENT_MAP.get(node.member, "0")
|
|
518
|
+
if ns == "dayofweek":
|
|
519
|
+
return DAYOFWEEK_MAP.get(node.member, "0")
|
|
520
|
+
if ns == "session":
|
|
521
|
+
if node.member == "regular":
|
|
522
|
+
return 'std::string("regular")'
|
|
523
|
+
if node.member == "extended":
|
|
524
|
+
return 'std::string("extended")'
|
|
525
|
+
# session.is* predicates backed by engine session_ismarket_ etc.
|
|
526
|
+
# session.isfirstbar_regular / islastbar_regular are aliased to
|
|
527
|
+
# their non-_regular counterparts (engine has one session string;
|
|
528
|
+
# see session_time.hpp limitation comment).
|
|
529
|
+
if node.member == "ismarket":
|
|
530
|
+
return "pine_session_ismarket(syminfo_.session, syminfo_.timezone, current_bar_.timestamp)"
|
|
531
|
+
if node.member == "ispremarket":
|
|
532
|
+
return "pine_session_ispremarket(syminfo_.session, syminfo_.timezone, current_bar_.timestamp)"
|
|
533
|
+
if node.member == "ispostmarket":
|
|
534
|
+
return "pine_session_ispostmarket(syminfo_.session, syminfo_.timezone, current_bar_.timestamp)"
|
|
535
|
+
if node.member in ("isfirstbar", "isfirstbar_regular"):
|
|
536
|
+
return "session_isfirstbar_"
|
|
537
|
+
if node.member in ("islastbar", "islastbar_regular"):
|
|
538
|
+
return "session_islastbar_"
|
|
539
|
+
return "false"
|
|
540
|
+
if ns == "syminfo":
|
|
541
|
+
_syminfo = SYMINFO_MEMBER_MAP.get(node.member)
|
|
542
|
+
if _syminfo is not None:
|
|
543
|
+
return _syminfo
|
|
544
|
+
# Defensive: support_checker rejects any syminfo.* member not in
|
|
545
|
+
# SUPPORTED_SYMINFO (== frozenset(SYMINFO_MEMBER_MAP)). Reaching
|
|
546
|
+
# here means the checker was bypassed or the two tables drifted.
|
|
547
|
+
raise ValueError(
|
|
548
|
+
f"codegen: unhandled syminfo.{node.member} — analyzer should "
|
|
549
|
+
f"have rejected. Add it to SYMINFO_MEMBER_MAP."
|
|
550
|
+
)
|
|
551
|
+
if ns == "display":
|
|
552
|
+
# plot_display (ints for C++; TV uses these in settings — backtest ignores)
|
|
553
|
+
return DISPLAY_MAP.get(node.member, "0")
|
|
554
|
+
if ns == "color":
|
|
555
|
+
if node.member in COLOR_CONST_MAP:
|
|
556
|
+
return COLOR_CONST_MAP[node.member]
|
|
557
|
+
return "0"
|
|
558
|
+
if ns in SKIP_NAMESPACES:
|
|
559
|
+
return "0"
|
|
560
|
+
if ns == "currency":
|
|
561
|
+
return f'std::string("{node.member}")'
|
|
562
|
+
if ns == "order":
|
|
563
|
+
# order.ascending / order.descending. Unknown member -> "ascending".
|
|
564
|
+
return ORDER_DIRECTION_MAP.get(node.member, 'std::string("ascending")')
|
|
565
|
+
|
|
566
|
+
# Handle nested member access: strategy.oca.reduce, strategy.closedtrades.profit(), etc.
|
|
567
|
+
if isinstance(node.object, MemberAccess):
|
|
568
|
+
if isinstance(node.object.object, Identifier):
|
|
569
|
+
outer_ns = node.object.object.name
|
|
570
|
+
if outer_ns == "strategy":
|
|
571
|
+
sub = node.object.member
|
|
572
|
+
# strategy.commission.percent, strategy.oca.*, strategy.direction.*
|
|
573
|
+
if sub == "oca":
|
|
574
|
+
if node.member == "cancel":
|
|
575
|
+
return "1"
|
|
576
|
+
if node.member == "reduce":
|
|
577
|
+
return "2"
|
|
578
|
+
if node.member == "none":
|
|
579
|
+
return "0"
|
|
580
|
+
return "0"
|
|
581
|
+
if sub == "direction":
|
|
582
|
+
if node.member == "long":
|
|
583
|
+
return "1"
|
|
584
|
+
if node.member == "short":
|
|
585
|
+
return "-1"
|
|
586
|
+
if node.member == "all":
|
|
587
|
+
return "0"
|
|
588
|
+
return "0"
|
|
589
|
+
if sub == "commission":
|
|
590
|
+
if node.member == "percent":
|
|
591
|
+
return "0"
|
|
592
|
+
if node.member == "cash_per_order":
|
|
593
|
+
return "1"
|
|
594
|
+
if node.member == "cash_per_contract":
|
|
595
|
+
return "2"
|
|
596
|
+
return "0"
|
|
597
|
+
# strategy.closedtrades.first_index, strategy.opentrades.capital_held
|
|
598
|
+
if sub == "closedtrades" and node.member == "first_index":
|
|
599
|
+
return "0"
|
|
600
|
+
if sub == "opentrades" and node.member == "capital_held":
|
|
601
|
+
return "open_trades_capital_held()"
|
|
602
|
+
return "0"
|
|
603
|
+
|
|
604
|
+
# Enum member access: EnumName.Member -> named int constant (matches emitted const)
|
|
605
|
+
if isinstance(node.object, Identifier):
|
|
606
|
+
name = node.object.name
|
|
607
|
+
if name in self._enum_defs:
|
|
608
|
+
members = self._enum_defs[name]
|
|
609
|
+
if node.member in members:
|
|
610
|
+
return f"{name}_{node.member}"
|
|
611
|
+
|
|
612
|
+
# Unknown member access — emit as string constant (e.g., enum values)
|
|
613
|
+
obj = self._visit_expr(node.object)
|
|
614
|
+
if isinstance(node.object, Identifier):
|
|
615
|
+
name = node.object.name
|
|
616
|
+
# If the variable is known (defined in symbol table or as a var),
|
|
617
|
+
# member access on a scalar C++ type is invalid. This happens when
|
|
618
|
+
# PineScript UDTs (type declarations) are used — we don't support
|
|
619
|
+
# them yet, so emit a safe default.
|
|
620
|
+
sym = self.ctx.symbols.resolve(name)
|
|
621
|
+
if (
|
|
622
|
+
sym is not None
|
|
623
|
+
or name in self._var_names
|
|
624
|
+
or name in self._current_loop_vars
|
|
625
|
+
or name in self._current_func_param_types
|
|
626
|
+
):
|
|
627
|
+
safe = self._safe_name(name)
|
|
628
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
629
|
+
safe = self._active_var_remap[safe]
|
|
630
|
+
return f"{safe}.{node.member}"
|
|
631
|
+
if name not in self.ctx.series_vars:
|
|
632
|
+
# Unknown identifier — likely an enum value
|
|
633
|
+
return f'std::string("{node.member}")'
|
|
634
|
+
return f"{obj}.{node.member}"
|
|
635
|
+
|
|
636
|
+
def _visit_binop(self, node: BinOp) -> str:
|
|
637
|
+
left = self._visit_expr(node.left)
|
|
638
|
+
right = self._visit_expr(node.right)
|
|
639
|
+
cpp_ops = {"and": "&&", "or": "||"}
|
|
640
|
+
op = cpp_ops.get(node.op, node.op)
|
|
641
|
+
# PineScript % works on floats — use std::fmod in C++
|
|
642
|
+
if node.op == "%":
|
|
643
|
+
return f"std::fmod((double)({left}), (double)({right}))"
|
|
644
|
+
# Pine v6 always returns float for `/`, even on int/int operands
|
|
645
|
+
# (breaking change from v5). C++ does int division on int operands,
|
|
646
|
+
# so cast both sides to double to match Pine v6 semantics.
|
|
647
|
+
# Ref: https://www.tradingview.com/pine-script-docs/concepts/operators/
|
|
648
|
+
if node.op == "/":
|
|
649
|
+
return f"((double)({left}) / (double)({right}))"
|
|
650
|
+
return f"({left} {op} {right})"
|
|
651
|
+
|
|
652
|
+
def _visit_unaryop(self, node: UnaryOp) -> str:
|
|
653
|
+
operand = self._visit_expr(node.operand)
|
|
654
|
+
if node.op == "not":
|
|
655
|
+
return f"!({operand})"
|
|
656
|
+
return f"({node.op}{operand})"
|
|
657
|
+
|
|
658
|
+
def _visit_subscript(self, node: Subscript) -> str:
|
|
659
|
+
idx = self._visit_expr(node.index)
|
|
660
|
+
if isinstance(node.object, Identifier):
|
|
661
|
+
name = node.object.name
|
|
662
|
+
# Function parameters that are series — src[N] → src[N]
|
|
663
|
+
if name in self._current_func_series_params:
|
|
664
|
+
return f"{self._safe_name(name)}[{idx}]"
|
|
665
|
+
# Function parameters are scalars — src[0] → src, src[N>0] → src
|
|
666
|
+
if name in self._current_func_param_types:
|
|
667
|
+
return self._safe_name(name)
|
|
668
|
+
if name in BAR_FIELDS or name in BAR_SERIES_PUSH:
|
|
669
|
+
# Index matches Pine: [0] current bar, [k] k bars ago (runtime Series deque).
|
|
670
|
+
return f"_s_{name}[{idx}]"
|
|
671
|
+
if name in self.ctx.series_vars:
|
|
672
|
+
safe = self._safe_name(name)
|
|
673
|
+
# Apply per-call-site var remap
|
|
674
|
+
if self._active_var_remap and safe in self._active_var_remap:
|
|
675
|
+
safe = self._active_var_remap[safe]
|
|
676
|
+
# Same Pine [k] semantics as Series in runtime/series.hpp
|
|
677
|
+
return f"{safe}[{idx}]"
|
|
678
|
+
spec = self._collection_types.get(name)
|
|
679
|
+
if spec is not None and spec.kind in ("array", "map"):
|
|
680
|
+
return f"{self._safe_name(name)}[{idx}]"
|
|
681
|
+
# Handle strategy.* history access (e.g., strategy.position_size[1])
|
|
682
|
+
if isinstance(node.object, MemberAccess):
|
|
683
|
+
if isinstance(node.object.object, Identifier):
|
|
684
|
+
ns = node.object.object.name
|
|
685
|
+
if ns == "strategy":
|
|
686
|
+
# Strategy variables with history operator — use series tracking
|
|
687
|
+
member = node.object.member
|
|
688
|
+
series_name = f"_strat_{member}"
|
|
689
|
+
if series_name not in self._strategy_series_vars:
|
|
690
|
+
self._strategy_series_vars.add(series_name)
|
|
691
|
+
return f"{series_name}[{idx}]"
|
|
692
|
+
obj = self._visit_expr(node.object)
|
|
693
|
+
# If subscripting a non-series variable (e.g., function parameter),
|
|
694
|
+
# src[0] → src (current value), src[N>0] → src (can't access history)
|
|
695
|
+
if isinstance(node.object, Identifier):
|
|
696
|
+
name = node.object.name
|
|
697
|
+
if (name not in BAR_FIELDS and name not in BAR_SERIES_PUSH
|
|
698
|
+
and name not in self.ctx.series_vars
|
|
699
|
+
and name not in self._var_names):
|
|
700
|
+
return obj
|
|
701
|
+
return f"{obj}[{idx}]"
|