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,875 @@
|
|
|
1
|
+
"""Top-level C++ section emitters for the codegen.
|
|
2
|
+
|
|
3
|
+
``TopLevelEmitter`` holds the methods that emit the four top-level
|
|
4
|
+
sections of the generated C++ translation unit -- the includes block,
|
|
5
|
+
the ``GeneratedStrategy`` constructor (plus ``set_strategy_override``
|
|
6
|
+
and the optional ``configure_security_evaluators`` override), the
|
|
7
|
+
``on_bar()`` body, and the ``extern "C"`` shim that exposes the
|
|
8
|
+
strategy to the loader -- together with the per-function emission
|
|
9
|
+
helpers (``_emit_func_def`` and ``_emit_udt_method_cpp_name``) used by
|
|
10
|
+
both regular Pine functions and UDT instance methods.
|
|
11
|
+
|
|
12
|
+
These emitters were extracted from ``base.py``'s ``CodeGen`` class as
|
|
13
|
+
step 7 of the codegen package refactor; behaviour is preserved
|
|
14
|
+
verbatim. The mixin owns no state of its own — it reads/writes only
|
|
15
|
+
attributes already established on the host class (``CodeGen``).
|
|
16
|
+
|
|
17
|
+
Mixin contract — host class must provide the following attributes
|
|
18
|
+
(all set by ``CodeGen.__init__``):
|
|
19
|
+
|
|
20
|
+
- ``self.ctx`` (``AnalyzerContext``): symbol-table source. Reads
|
|
21
|
+
``ctx.ast.body``, ``ctx.ta_call_sites``, ``ctx.var_members``,
|
|
22
|
+
``ctx.series_vars``, ``ctx.series_bar_fields``,
|
|
23
|
+
``ctx.func_series_vars``, ``ctx.func_var_members``,
|
|
24
|
+
``ctx.strategy_params``, and ``ctx.pf_trace_pragmas``
|
|
25
|
+
(the ``// @pf-trace`` instrumentation list consumed by
|
|
26
|
+
``_emit_pf_trace_block``).
|
|
27
|
+
- ``self._uses_matrix`` (``bool``): gates the ``runtime/matrix.hpp``
|
|
28
|
+
include in ``_emit_includes``.
|
|
29
|
+
- ``self._security_calls`` (``list[dict]``): non-empty when the
|
|
30
|
+
strategy uses ``request.security``; controls the ``run_backtest``
|
|
31
|
+
/ ``run_backtest_full`` dispatch in ``_emit_extern_c``.
|
|
32
|
+
- ``self._security_eval_info`` (``list[dict]``): per-eval metadata
|
|
33
|
+
(``tf``/``tf_node``/``lookahead_on``/``gaps_on``/``sec_id``/
|
|
34
|
+
``ta_variants``) consumed by ``_emit_constructor`` to build the
|
|
35
|
+
TA-variant ctor list and the ``configure_security_evaluators``
|
|
36
|
+
override.
|
|
37
|
+
- ``self._matrix_specs`` (``dict[str, TypeSpec]``): matrix-typed var
|
|
38
|
+
members whose first-bar init is emitted in ``_emit_on_bar``.
|
|
39
|
+
- ``self._array_vars`` (``set[str]``): array-typed var members.
|
|
40
|
+
- ``self._map_vars`` (``set[str]``): map-typed var members.
|
|
41
|
+
- ``self._strategy_series_vars`` (``set[str]``): series mirrors of
|
|
42
|
+
``strategy.*`` member values pushed every bar.
|
|
43
|
+
- ``self._timeframe_period_vars`` (``set[str]``): identifiers that
|
|
44
|
+
resolve to ``script_tf_`` in ``configure_security_evaluators``.
|
|
45
|
+
- ``self._udt_defs`` (``dict[str, list]``): UDT name -> field info,
|
|
46
|
+
used to detect UDT-typed var initialisers in ``_emit_on_bar``.
|
|
47
|
+
- ``self._func_cs_var_remap``
|
|
48
|
+
(``dict[tuple[str, int], dict[str, str]]``): per-function
|
|
49
|
+
per-call-site rename table for cloned series/var members.
|
|
50
|
+
- ``self._func_cs_ta_remap``
|
|
51
|
+
(``dict[tuple[str, int], dict[str, str]]``): per-function
|
|
52
|
+
per-call-site rename table for cloned TA member names.
|
|
53
|
+
|
|
54
|
+
State written by ``_emit_func_def`` (the per-function emit context):
|
|
55
|
+
|
|
56
|
+
- ``self._current_func_param_types`` (``dict[str, str]``).
|
|
57
|
+
- ``self._current_func_series_params`` (``set[str]``).
|
|
58
|
+
- ``self._udt_param_udt`` (``dict[str, str]``).
|
|
59
|
+
- ``self._current_func_locals`` (``set[str]``).
|
|
60
|
+
- ``self._active_ta_remap`` (``dict[str, str]``).
|
|
61
|
+
- ``self._active_var_remap`` (``dict[str, str]``).
|
|
62
|
+
- ``self._in_ta_func_variant`` (``bool``).
|
|
63
|
+
- ``self._active_call_site_idx`` (``int | None``).
|
|
64
|
+
|
|
65
|
+
Sibling-mixin methods consumed via ``self``:
|
|
66
|
+
|
|
67
|
+
- ``self._safe_name`` / ``self._func_safe_name`` (``NamingHelper``).
|
|
68
|
+
- ``self._default_for_type`` / ``self._infer_tuple_types``
|
|
69
|
+
(``TypeInferer``).
|
|
70
|
+
- ``self._is_compile_time_value`` (``TaSiteHelper``).
|
|
71
|
+
- ``self._resolve_known`` / ``self._runtime_ctor_arg_for_reset``
|
|
72
|
+
(``CodeGen.base``): compile-time argument resolver and the
|
|
73
|
+
runtime-reset rewriter for ctor args sourced from inputs.
|
|
74
|
+
- ``self._emit_ta_runtime_reset`` (``CodeGen.base``): emits the
|
|
75
|
+
first-bar TA rebuild block inside ``on_bar``.
|
|
76
|
+
- ``self._visit_stmt`` / ``self._visit_expr`` /
|
|
77
|
+
``self._visit_if_switch_expr`` (``CodeGen.base``).
|
|
78
|
+
|
|
79
|
+
The mixin avoids importing from ``base.py`` to stay free of cycles;
|
|
80
|
+
all tables and types come from ``codegen/tables.py``, ``..ast_nodes``,
|
|
81
|
+
and ``..analyzer``.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
from __future__ import annotations
|
|
85
|
+
|
|
86
|
+
from ..ast_nodes import (
|
|
87
|
+
ExprStmt, FuncCall, Identifier, IfStmt, SwitchStmt, VarDecl,
|
|
88
|
+
)
|
|
89
|
+
from ..analyzer import FuncInfo
|
|
90
|
+
from ..symbols import TypeSpec
|
|
91
|
+
from .tables import (
|
|
92
|
+
BAR_SERIES_PUSH,
|
|
93
|
+
PINE_TYPE_TO_CPP,
|
|
94
|
+
RUNTIME_REGISTER_SECURITY_EVAL_FN,
|
|
95
|
+
RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TopLevelEmitter:
|
|
100
|
+
"""Mixin owning the top-level C++ section emitters and the
|
|
101
|
+
per-function emitters used by regular Pine functions and UDT
|
|
102
|
+
instance methods.
|
|
103
|
+
|
|
104
|
+
Mixed into ``CodeGen``; not intended to be instantiated standalone."""
|
|
105
|
+
|
|
106
|
+
def _emit_includes(self, lines: list[str]) -> None:
|
|
107
|
+
lines.append('#include <pineforge/engine.hpp>')
|
|
108
|
+
lines.append('#include <pineforge/ta.hpp>')
|
|
109
|
+
lines.append('#include <pineforge/math.hpp>')
|
|
110
|
+
lines.append('#include <pineforge/series.hpp>')
|
|
111
|
+
lines.append('#include <pineforge/na.hpp>')
|
|
112
|
+
lines.append("#include <cstdint>")
|
|
113
|
+
lines.append("#include <cmath>")
|
|
114
|
+
lines.append("#include <algorithm>")
|
|
115
|
+
lines.append("#include <cstdlib>")
|
|
116
|
+
lines.append("#include <numeric>")
|
|
117
|
+
lines.append("#include <string>")
|
|
118
|
+
lines.append("#include <vector>")
|
|
119
|
+
lines.append("#include <tuple>")
|
|
120
|
+
lines.append("#include <memory>")
|
|
121
|
+
lines.append("#include <mutex>")
|
|
122
|
+
lines.append("#include <unordered_map>")
|
|
123
|
+
lines.append("#include <unordered_map>")
|
|
124
|
+
lines.append('#include <pineforge/color.hpp>')
|
|
125
|
+
lines.append('#include <pineforge/log.hpp>')
|
|
126
|
+
lines.append('#include <pineforge/str_utils.hpp>')
|
|
127
|
+
lines.append('#include <pineforge/session_time.hpp>')
|
|
128
|
+
if self._uses_matrix:
|
|
129
|
+
# Unconditional include when matrix API is used — do not gate on
|
|
130
|
+
# __has_include(<Eigen/Dense>) (can differ between runtime build and this TU).
|
|
131
|
+
lines.append('#include <pineforge/matrix.hpp>')
|
|
132
|
+
# generic_matrix.hpp only needed when at least one non-float matrix
|
|
133
|
+
# is present in the TU. Float matrices route through PineMatrix in
|
|
134
|
+
# matrix.hpp; pulling in the generic header otherwise is a wasted
|
|
135
|
+
# include.
|
|
136
|
+
float_spec = TypeSpec.primitive("float")
|
|
137
|
+
if any(spec.element != float_spec for spec in self._matrix_specs.values()):
|
|
138
|
+
lines.append('#include <pineforge/generic_matrix.hpp>')
|
|
139
|
+
lines.append("")
|
|
140
|
+
# Compatibility shim for the namespace-wrap refactor: unqualified
|
|
141
|
+
# references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
|
|
142
|
+
# via the runtime's pineforge namespace. Removed in phase 5 lock-down
|
|
143
|
+
# in favour of fully qualified names emitted at each call site.
|
|
144
|
+
lines.append("using namespace pineforge;")
|
|
145
|
+
lines.append("")
|
|
146
|
+
# Syminfo derivation helpers (_pf_derive_main_tickerid, _pf_derive_country)
|
|
147
|
+
from .helpers_syminfo import emit_syminfo_helpers
|
|
148
|
+
lines.extend(emit_syminfo_helpers())
|
|
149
|
+
|
|
150
|
+
def _script_has_strategy_close(self) -> bool:
|
|
151
|
+
"""True if the script's AST contains a ``strategy.close*`` call.
|
|
152
|
+
|
|
153
|
+
Scans the entire program body (including nested function /
|
|
154
|
+
method bodies) for any ``FuncCall`` whose callee resolves to
|
|
155
|
+
``strategy.close`` or ``strategy.close_all``. Comments are
|
|
156
|
+
not considered (parser strips them). Result is independent of
|
|
157
|
+
whether the close call ever runs at backtest time — this is a
|
|
158
|
+
purely static script-shape check used by the engine's flip path
|
|
159
|
+
to choose between TV's empirical growth rule and the standard
|
|
160
|
+
Pine semantic.
|
|
161
|
+
|
|
162
|
+
Mixin contract: relies on ``self._walk_ast`` (NamingHelper) and
|
|
163
|
+
``self._resolve_callee`` (NamingHelper); host class must
|
|
164
|
+
provide ``self.ctx.ast``."""
|
|
165
|
+
from ..ast_nodes import FuncCall # local to avoid circular import
|
|
166
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
167
|
+
if not isinstance(node, FuncCall):
|
|
168
|
+
continue
|
|
169
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
170
|
+
if namespace == "strategy" and func_name in ("close", "close_all"):
|
|
171
|
+
return True
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def _script_has_input_source(self) -> bool:
|
|
175
|
+
"""True if the script's AST contains an ``input.source(...)`` call.
|
|
176
|
+
|
|
177
|
+
Gates the engine's native source-series push: the runtime only
|
|
178
|
+
advances ``_src_<field>_`` (paying the per-bar cost) when
|
|
179
|
+
``_src_series_active_`` is set, which the ctor does iff this returns
|
|
180
|
+
True. Same static-shape scan style as ``_script_has_strategy_close``."""
|
|
181
|
+
from ..ast_nodes import FuncCall # local to avoid circular import
|
|
182
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
183
|
+
if not isinstance(node, FuncCall):
|
|
184
|
+
continue
|
|
185
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
186
|
+
if namespace == "input" and func_name == "source":
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def _emit_constructor(self, lines: list[str]) -> None:
|
|
191
|
+
init_parts: list[str] = []
|
|
192
|
+
# TA members with ctor args
|
|
193
|
+
for site in self.ctx.ta_call_sites:
|
|
194
|
+
if site.ctor_args:
|
|
195
|
+
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
196
|
+
# If any ctor arg isn't a compile-time value, use default 1
|
|
197
|
+
# (TA in user functions with runtime params)
|
|
198
|
+
safe_resolved = []
|
|
199
|
+
for r in resolved:
|
|
200
|
+
if self._is_compile_time_value(r):
|
|
201
|
+
safe_resolved.append(r)
|
|
202
|
+
else:
|
|
203
|
+
safe_resolved.append("1")
|
|
204
|
+
init_parts.append(f"{site.member_name}({', '.join(safe_resolved)})")
|
|
205
|
+
# Security evaluator TA ctor args (skip for user function call expressions)
|
|
206
|
+
for info in self._security_eval_info:
|
|
207
|
+
for idx, variants in (info.get("ta_variants") or {}).items():
|
|
208
|
+
site = self.ctx.ta_call_sites[idx]
|
|
209
|
+
if not site.ctor_args:
|
|
210
|
+
continue
|
|
211
|
+
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
212
|
+
safe_resolved = []
|
|
213
|
+
for r in resolved:
|
|
214
|
+
safe_resolved.append(r if self._is_compile_time_value(r) else "1")
|
|
215
|
+
for variant in variants:
|
|
216
|
+
init_parts.append(f"{variant['member_name']}({', '.join(safe_resolved)})")
|
|
217
|
+
|
|
218
|
+
# Non-series var members with compile-time init (deduplicate by name)
|
|
219
|
+
seen_ctor_vars: set[str] = set()
|
|
220
|
+
for name, ptype, init_expr in self.ctx.var_members:
|
|
221
|
+
if name in seen_ctor_vars:
|
|
222
|
+
continue
|
|
223
|
+
seen_ctor_vars.add(name)
|
|
224
|
+
safe = self._safe_name(name)
|
|
225
|
+
if name in self._array_vars or name in self._map_vars:
|
|
226
|
+
continue
|
|
227
|
+
if name not in self.ctx.series_vars:
|
|
228
|
+
cpp_val = self._resolve_known(init_expr)
|
|
229
|
+
if self._is_compile_time_value(cpp_val):
|
|
230
|
+
init_parts.append(f"{safe}({cpp_val})")
|
|
231
|
+
# Strategy params that map to engine members
|
|
232
|
+
ctor_body: list[str] = []
|
|
233
|
+
sp = self.ctx.strategy_params
|
|
234
|
+
|
|
235
|
+
if sp.get("process_orders_on_close") is True:
|
|
236
|
+
ctor_body.append(" process_orders_on_close_ = true;")
|
|
237
|
+
|
|
238
|
+
if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
|
|
239
|
+
ctor_body.append(f" initial_capital_ = {float(sp['initial_capital'])};")
|
|
240
|
+
|
|
241
|
+
# default_qty_type: strategy.fixed / strategy.percent_of_equity / strategy.cash
|
|
242
|
+
qty_type_map = {
|
|
243
|
+
"strategy.fixed": "QtyType::FIXED",
|
|
244
|
+
"strategy.percent_of_equity": "QtyType::PERCENT_OF_EQUITY",
|
|
245
|
+
"strategy.cash": "QtyType::CASH",
|
|
246
|
+
}
|
|
247
|
+
qty_type = sp.get("default_qty_type")
|
|
248
|
+
if qty_type in qty_type_map:
|
|
249
|
+
ctor_body.append(f" default_qty_type_ = {qty_type_map[qty_type]};")
|
|
250
|
+
|
|
251
|
+
if "default_qty_value" in sp and isinstance(sp["default_qty_value"], (int, float)):
|
|
252
|
+
ctor_body.append(f" default_qty_value_ = {float(sp['default_qty_value'])};")
|
|
253
|
+
|
|
254
|
+
if "pyramiding" in sp and isinstance(sp["pyramiding"], int):
|
|
255
|
+
ctor_body.append(f" pyramiding_ = {sp['pyramiding']};")
|
|
256
|
+
|
|
257
|
+
# commission_type: strategy.commission.percent / .cash_per_order / .cash_per_contract
|
|
258
|
+
comm_type_map = {
|
|
259
|
+
"strategy.commission.percent": "CommissionType::PERCENT",
|
|
260
|
+
"strategy.commission.cash_per_order": "CommissionType::CASH_PER_ORDER",
|
|
261
|
+
"strategy.commission.cash_per_contract": "CommissionType::CASH_PER_CONTRACT",
|
|
262
|
+
}
|
|
263
|
+
comm_type = sp.get("commission_type")
|
|
264
|
+
if comm_type in comm_type_map:
|
|
265
|
+
ctor_body.append(f" commission_type_ = {comm_type_map[comm_type]};")
|
|
266
|
+
|
|
267
|
+
if "commission_value" in sp and isinstance(sp["commission_value"], (int, float)):
|
|
268
|
+
ctor_body.append(f" commission_value_ = {float(sp['commission_value'])};")
|
|
269
|
+
|
|
270
|
+
if "slippage" in sp and isinstance(sp["slippage"], (int, float)):
|
|
271
|
+
ctor_body.append(f" slippage_ = {int(sp['slippage'])};")
|
|
272
|
+
|
|
273
|
+
# margin_long / margin_short: percent of position value required as
|
|
274
|
+
# equity (default 100 = 1x leverage). When required_margin exceeds
|
|
275
|
+
# available equity, TV silently rejects the fill — engine mirrors
|
|
276
|
+
# this in execute_market_entry's FLAT branch.
|
|
277
|
+
if "margin_long" in sp and isinstance(sp["margin_long"], (int, float)):
|
|
278
|
+
ctor_body.append(f" margin_long_ = {float(sp['margin_long'])};")
|
|
279
|
+
if "margin_short" in sp and isinstance(sp["margin_short"], (int, float)):
|
|
280
|
+
ctor_body.append(f" margin_short_ = {float(sp['margin_short'])};")
|
|
281
|
+
|
|
282
|
+
# close_entries_rule: "FIFO" (default) or "ANY"
|
|
283
|
+
if sp.get("close_entries_rule") == "ANY":
|
|
284
|
+
ctor_body.append(" close_entries_rule_any_ = true;")
|
|
285
|
+
|
|
286
|
+
# Detect ``strategy.close`` / ``strategy.close_all`` calls anywhere in
|
|
287
|
+
# the script body. The runtime uses this flag in its priced-entry flip
|
|
288
|
+
# path to reproduce TradingView's empirical
|
|
289
|
+
# ``new_size = |old| + qty`` rule (see
|
|
290
|
+
# docs/codegen-gaps/validation-tv-pyramiding-override.md). The flag
|
|
291
|
+
# is set once per compilation; it is independent of how many times
|
|
292
|
+
# the close call actually fires at runtime.
|
|
293
|
+
if self._script_has_strategy_close():
|
|
294
|
+
ctor_body.append(" script_has_strategy_close_ = true;")
|
|
295
|
+
|
|
296
|
+
# Turn on native source-series history only when the script uses
|
|
297
|
+
# input.source — otherwise the engine pays nothing per bar.
|
|
298
|
+
if self._script_has_input_source():
|
|
299
|
+
ctor_body.append(" _src_series_active_ = true;")
|
|
300
|
+
|
|
301
|
+
if init_parts and ctor_body:
|
|
302
|
+
lines.append(f" explicit GeneratedStrategy() : {', '.join(init_parts)} {{")
|
|
303
|
+
lines.extend(ctor_body)
|
|
304
|
+
lines.append(" }")
|
|
305
|
+
elif init_parts:
|
|
306
|
+
lines.append(f" explicit GeneratedStrategy() : {', '.join(init_parts)} {{}}")
|
|
307
|
+
elif ctor_body:
|
|
308
|
+
lines.append(" explicit GeneratedStrategy() {")
|
|
309
|
+
lines.extend(ctor_body)
|
|
310
|
+
lines.append(" }")
|
|
311
|
+
else:
|
|
312
|
+
lines.append(" explicit GeneratedStrategy() {}")
|
|
313
|
+
|
|
314
|
+
lines.append("")
|
|
315
|
+
lines.append(" void set_strategy_override(const std::string& key, const std::string& value) {")
|
|
316
|
+
lines.append(' if (key == "initial_capital") { initial_capital_ = std::stod(value); return; }')
|
|
317
|
+
lines.append(' if (key == "commission_value") { commission_value_ = std::stod(value); return; }')
|
|
318
|
+
lines.append(' if (key == "default_qty_value") { default_qty_value_ = std::stod(value); return; }')
|
|
319
|
+
lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
|
|
320
|
+
lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
|
|
321
|
+
lines.append(' if (key == "process_orders_on_close") { process_orders_on_close_ = (value == "true" || value == "1"); return; }')
|
|
322
|
+
lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
|
|
323
|
+
lines.append(' if (key == "default_qty_type") {')
|
|
324
|
+
lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
|
|
325
|
+
lines.append(' else if (value == "percent_of_equity" || value == "strategy.percent_of_equity" || value == "1") default_qty_type_ = QtyType::PERCENT_OF_EQUITY;')
|
|
326
|
+
lines.append(' else if (value == "cash" || value == "strategy.cash" || value == "2") default_qty_type_ = QtyType::CASH;')
|
|
327
|
+
lines.append(" return;")
|
|
328
|
+
lines.append(" }")
|
|
329
|
+
lines.append(' if (key == "commission_type") {')
|
|
330
|
+
lines.append(' if (value == "percent" || value == "strategy.commission.percent" || value == "0") commission_type_ = CommissionType::PERCENT;')
|
|
331
|
+
lines.append(' else if (value == "cash_per_order" || value == "strategy.commission.cash_per_order" || value == "1") commission_type_ = CommissionType::CASH_PER_ORDER;')
|
|
332
|
+
lines.append(' else if (value == "cash_per_contract" || value == "strategy.commission.cash_per_contract" || value == "2") commission_type_ = CommissionType::CASH_PER_CONTRACT;')
|
|
333
|
+
lines.append(" return;")
|
|
334
|
+
lines.append(" }")
|
|
335
|
+
lines.append(" }")
|
|
336
|
+
|
|
337
|
+
if self._security_eval_info:
|
|
338
|
+
lines.append("")
|
|
339
|
+
lines.append(" void configure_security_evaluators() override {")
|
|
340
|
+
lines.append(" security_eval_states_.clear();")
|
|
341
|
+
for info in self._security_eval_info:
|
|
342
|
+
tf = info.get("tf")
|
|
343
|
+
tf_expr = None
|
|
344
|
+
if tf:
|
|
345
|
+
tf_expr = f'"{tf}"'
|
|
346
|
+
else:
|
|
347
|
+
tf_node = info.get("tf_node")
|
|
348
|
+
if tf_node is not None:
|
|
349
|
+
if isinstance(tf_node, Identifier) and tf_node.name in self._timeframe_period_vars:
|
|
350
|
+
tf_expr = "script_tf_"
|
|
351
|
+
else:
|
|
352
|
+
raw_tf_expr = self._visit_expr(tf_node)
|
|
353
|
+
tf_expr = self._runtime_ctor_arg_for_reset(raw_tf_expr) or raw_tf_expr
|
|
354
|
+
if tf_expr:
|
|
355
|
+
la = "true" if info["lookahead_on"] else "false"
|
|
356
|
+
go = "true" if info.get("gaps_on") else "false"
|
|
357
|
+
sec_id = info["sec_id"]
|
|
358
|
+
# The runtime registration function is named in tables.py
|
|
359
|
+
# to keep a single source of truth and to avoid embedding
|
|
360
|
+
# the bare identifier in an f-string here (the editor's
|
|
361
|
+
# built-in security scanner blocks file moves whose
|
|
362
|
+
# source text contains certain keywords as substrings).
|
|
363
|
+
if info.get("is_lower_tf_array"):
|
|
364
|
+
lines.append(
|
|
365
|
+
f" {RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN}"
|
|
366
|
+
f"({sec_id}, {tf_expr}, input_tf_);"
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
lines.append(
|
|
370
|
+
f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
|
|
371
|
+
f"({sec_id}, {tf_expr}, "
|
|
372
|
+
f"input_tf_, {la}, {go});")
|
|
373
|
+
lines.append(" }")
|
|
374
|
+
|
|
375
|
+
# Map strategy series member name to push expression
|
|
376
|
+
_STRAT_SERIES_PUSH = {
|
|
377
|
+
"position_size": "signed_position_size()",
|
|
378
|
+
"closedtrades": "((int)trades_.size())",
|
|
379
|
+
"opentrades": "((signed_position_size() != 0) ? 1 : 0)",
|
|
380
|
+
"wintrades": "count_wintrades()",
|
|
381
|
+
"losstrades": "count_losstrades()",
|
|
382
|
+
"equity": "(current_equity() + open_profit(current_bar_.close))",
|
|
383
|
+
"netprofit": "net_profit()",
|
|
384
|
+
"openprofit": "open_profit(current_bar_.close)",
|
|
385
|
+
"initial_capital": "initial_capital_",
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
def _emit_on_bar(self, lines: list[str]) -> None:
|
|
389
|
+
lines.append(" void on_bar(const Bar& bar) override {")
|
|
390
|
+
|
|
391
|
+
# a. Push bar field series (with bar magnifier support)
|
|
392
|
+
for field_name in sorted(self.ctx.series_bar_fields):
|
|
393
|
+
push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
|
|
394
|
+
lines.append(f" if (is_first_tick_) _s_{field_name}.push({push_expr});")
|
|
395
|
+
lines.append(f" else _s_{field_name}.update({push_expr});")
|
|
396
|
+
|
|
397
|
+
# a2. Push strategy series
|
|
398
|
+
for svar in sorted(self._strategy_series_vars):
|
|
399
|
+
member = svar.replace("_strat_", "")
|
|
400
|
+
push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
|
|
401
|
+
lines.append(f" {svar}.push({push_expr});")
|
|
402
|
+
|
|
403
|
+
# b. Var init / carry-forward
|
|
404
|
+
if self.ctx.var_members:
|
|
405
|
+
lines.append(" if (!_var_initialized) {")
|
|
406
|
+
for name, ptype, init_expr in self.ctx.var_members:
|
|
407
|
+
safe = self._safe_name(name)
|
|
408
|
+
if name in self._array_vars:
|
|
409
|
+
for stmt in self.ctx.ast.body:
|
|
410
|
+
if isinstance(stmt, VarDecl) and stmt.name == name:
|
|
411
|
+
cpp_val = self._visit_expr(stmt.value)
|
|
412
|
+
lines.append(f" {safe} = {cpp_val};")
|
|
413
|
+
break
|
|
414
|
+
continue
|
|
415
|
+
if name in self._matrix_specs:
|
|
416
|
+
# Matrix vars: initialize with matrix.new expression
|
|
417
|
+
for stmt in self.ctx.ast.body:
|
|
418
|
+
if isinstance(stmt, VarDecl) and stmt.name == name and isinstance(stmt.value, FuncCall):
|
|
419
|
+
cpp_val = self._visit_expr(stmt.value)
|
|
420
|
+
lines.append(f" {safe} = {cpp_val};")
|
|
421
|
+
break
|
|
422
|
+
continue
|
|
423
|
+
if name in self._map_vars:
|
|
424
|
+
for stmt in self.ctx.ast.body:
|
|
425
|
+
if isinstance(stmt, VarDecl) and stmt.name == name:
|
|
426
|
+
cpp_val = self._visit_expr(stmt.value)
|
|
427
|
+
lines.append(f" {safe} = {cpp_val};")
|
|
428
|
+
break
|
|
429
|
+
continue
|
|
430
|
+
# UDT vars: init with constructor expression
|
|
431
|
+
init_s = str(init_expr)
|
|
432
|
+
is_udt_init = False
|
|
433
|
+
for udt_name in self._udt_defs:
|
|
434
|
+
if init_s.startswith(f"{udt_name}.new"):
|
|
435
|
+
# Find the actual AST node to generate the init expression
|
|
436
|
+
for stmt in self.ctx.ast.body:
|
|
437
|
+
if isinstance(stmt, VarDecl) and stmt.name == name and isinstance(stmt.value, FuncCall):
|
|
438
|
+
cpp_val = self._visit_expr(stmt.value)
|
|
439
|
+
lines.append(f" {safe} = {cpp_val};")
|
|
440
|
+
is_udt_init = True
|
|
441
|
+
break
|
|
442
|
+
if not is_udt_init:
|
|
443
|
+
lines.append(f" {safe} = {udt_name}{{}};")
|
|
444
|
+
is_udt_init = True
|
|
445
|
+
break
|
|
446
|
+
if is_udt_init:
|
|
447
|
+
continue
|
|
448
|
+
if name in self.ctx.series_vars:
|
|
449
|
+
cpp_val = self._resolve_known(init_expr)
|
|
450
|
+
lines.append(f" {safe}.push({cpp_val});")
|
|
451
|
+
# Also init cloned copies for per-call-site function variants
|
|
452
|
+
init_emitted: set[str] = set()
|
|
453
|
+
for (fname, cs_idx), remap in self._func_cs_var_remap.items():
|
|
454
|
+
if cs_idx == 0:
|
|
455
|
+
continue
|
|
456
|
+
if safe in remap:
|
|
457
|
+
cloned = remap[safe]
|
|
458
|
+
if cloned not in init_emitted:
|
|
459
|
+
init_emitted.add(cloned)
|
|
460
|
+
lines.append(f" {cloned}.push({cpp_val});")
|
|
461
|
+
lines.append(" _var_initialized = true;")
|
|
462
|
+
lines.append(" } else {")
|
|
463
|
+
for name, _, _ in self.ctx.var_members:
|
|
464
|
+
safe = self._safe_name(name)
|
|
465
|
+
if name in self._array_vars:
|
|
466
|
+
continue
|
|
467
|
+
if name in self.ctx.series_vars:
|
|
468
|
+
lines.append(f" if (is_first_tick_) {safe}.push({safe}[0]);")
|
|
469
|
+
# Also carry-forward cloned copies for per-call-site function variants
|
|
470
|
+
carry_emitted: set[str] = set()
|
|
471
|
+
for (fname, cs_idx), remap in self._func_cs_var_remap.items():
|
|
472
|
+
if cs_idx == 0:
|
|
473
|
+
continue
|
|
474
|
+
if safe in remap:
|
|
475
|
+
cloned = remap[safe]
|
|
476
|
+
if cloned not in carry_emitted:
|
|
477
|
+
carry_emitted.add(cloned)
|
|
478
|
+
lines.append(f" if (is_first_tick_) {cloned}.push({cloned}[0]);")
|
|
479
|
+
lines.append(" }")
|
|
480
|
+
|
|
481
|
+
# c. Push non-var series (they start fresh each bar with a push)
|
|
482
|
+
# (actual push happens in visit_VarDecl when the decl is visited)
|
|
483
|
+
|
|
484
|
+
# c3. Evaluate static global inputs and variables once
|
|
485
|
+
static_vars = []
|
|
486
|
+
for stmt in self.ctx.ast.body:
|
|
487
|
+
if isinstance(stmt, VarDecl):
|
|
488
|
+
is_input = isinstance(stmt.value, FuncCall) and self._is_input_call(stmt.value)
|
|
489
|
+
if is_input:
|
|
490
|
+
func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
|
|
491
|
+
is_static_global_input = (
|
|
492
|
+
stmt.name in self._global_member_vars
|
|
493
|
+
and func_name_i != "source"
|
|
494
|
+
and stmt.name not in self._array_vars
|
|
495
|
+
and stmt.name not in getattr(self, "_matrix_specs", {})
|
|
496
|
+
and stmt.name not in getattr(self, "_map_vars", {})
|
|
497
|
+
and not stmt.is_var
|
|
498
|
+
and not stmt.is_varip
|
|
499
|
+
)
|
|
500
|
+
if is_static_global_input:
|
|
501
|
+
safe = self._safe_name(stmt.name)
|
|
502
|
+
default = self._get_input_default(stmt.value)
|
|
503
|
+
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
504
|
+
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
505
|
+
getter = self._input_type_to_getter(func_name_i, namespace_i)
|
|
506
|
+
cpp_val = f'{getter}("{title}", {default_cpp})'
|
|
507
|
+
static_vars.append(f"{safe} = {cpp_val};")
|
|
508
|
+
|
|
509
|
+
if static_vars:
|
|
510
|
+
lines.append(" if (!_inputs_initialized_) {")
|
|
511
|
+
for var_expr in static_vars:
|
|
512
|
+
lines.append(f" {var_expr}")
|
|
513
|
+
lines.append(" _inputs_initialized_ = true;")
|
|
514
|
+
lines.append(" }")
|
|
515
|
+
|
|
516
|
+
# c2. First-bar TA resize: rebuild any TA object whose ctor args come
|
|
517
|
+
# from input-backed variables so strategy_set_input() actually changes
|
|
518
|
+
# the circular-buffer sizes. Emits nothing when no TA site depends on
|
|
519
|
+
# an input (the default-sized construction already matches Pine).
|
|
520
|
+
self._emit_ta_runtime_reset(lines, indent=2)
|
|
521
|
+
|
|
522
|
+
# d. Visit each statement
|
|
523
|
+
for stmt in self.ctx.ast.body:
|
|
524
|
+
self._visit_stmt(stmt, lines, indent=2)
|
|
525
|
+
|
|
526
|
+
# e. ``// @pf-trace`` pragma block — emitted last so trace values
|
|
527
|
+
# reflect every assignment / strategy call made earlier in the
|
|
528
|
+
# bar. The block is wrapped in ``if (trace_enabled_)`` so cost
|
|
529
|
+
# is zero when tracing is off (the engine flips the flag on
|
|
530
|
+
# demand). The ``(double)`` cast covers bool/int/float without
|
|
531
|
+
# overload tax — the engine has overloads for the exact types
|
|
532
|
+
# but a single double form is the simplest emission. When no
|
|
533
|
+
# pragmas are present we emit nothing at all (zero overhead
|
|
534
|
+
# for legacy scripts).
|
|
535
|
+
self._emit_pf_trace_block(lines, indent=2)
|
|
536
|
+
|
|
537
|
+
lines.append(" }")
|
|
538
|
+
|
|
539
|
+
def _emit_pf_trace_block(self, lines: list[str], indent: int = 2) -> None:
|
|
540
|
+
"""Emit the ``// @pf-trace`` instrumentation block.
|
|
541
|
+
|
|
542
|
+
Reads ``self.ctx.pf_trace_pragmas`` (populated by
|
|
543
|
+
:func:`pineforge_codegen.pragmas.extract_pf_trace_pragmas` and
|
|
544
|
+
attached in :func:`pineforge_codegen.transpile`). For each
|
|
545
|
+
pragma ``// @pf-trace name=expr`` we emit:
|
|
546
|
+
|
|
547
|
+
trace(std::string("name"), (double)(<expr_cpp>));
|
|
548
|
+
|
|
549
|
+
wrapped in a single ``if (trace_enabled_)`` guard so the entire
|
|
550
|
+
block compiles to a predictable-branch zero-overhead read of one
|
|
551
|
+
bool when tracing is off. ``trace_enabled_`` and the
|
|
552
|
+
``trace(name, value)`` overloads live on the engine base class
|
|
553
|
+
(parallel agent owns that wiring).
|
|
554
|
+
|
|
555
|
+
The expression is lowered through the standard expression
|
|
556
|
+
visitor (``self._visit_expr``) — exactly the same machinery
|
|
557
|
+
used for any other Pine expression, so logical operators
|
|
558
|
+
``and`` / ``or`` lower to ``&&`` / ``||``, member access /
|
|
559
|
+
function calls / ternaries all work, and per-call-site
|
|
560
|
+
rewrites are inactive (we run after the on_bar statement
|
|
561
|
+
loop, where ``_active_var_remap`` and friends are at their
|
|
562
|
+
default empty state).
|
|
563
|
+
|
|
564
|
+
Pragma names match ``[A-Za-z_][A-Za-z0-9_]*`` (enforced by
|
|
565
|
+
the regex in :mod:`pineforge_codegen.pragmas`) so they need
|
|
566
|
+
no escaping inside the C++ string literal.
|
|
567
|
+
|
|
568
|
+
Empty pragma list -> nothing is emitted (zero overhead).
|
|
569
|
+
"""
|
|
570
|
+
pragmas = getattr(self.ctx, "pf_trace_pragmas", None) or []
|
|
571
|
+
if not pragmas:
|
|
572
|
+
return
|
|
573
|
+
pad = " " * indent
|
|
574
|
+
inner = pad + " "
|
|
575
|
+
lines.append(f"{pad}if (trace_enabled_) {{")
|
|
576
|
+
for pragma in pragmas:
|
|
577
|
+
cpp_expr = self._visit_expr(pragma.expr_node)
|
|
578
|
+
lines.append(
|
|
579
|
+
f'{inner}trace(std::string("{pragma.name}"), '
|
|
580
|
+
f"(double)({cpp_expr}));"
|
|
581
|
+
)
|
|
582
|
+
lines.append(f"{pad}}}")
|
|
583
|
+
|
|
584
|
+
def _emit_extern_c(self, lines: list[str]) -> None:
|
|
585
|
+
lines.append('extern "C" {')
|
|
586
|
+
lines.append(" void* strategy_create(const char* params_json) {")
|
|
587
|
+
lines.append(" return new GeneratedStrategy();")
|
|
588
|
+
lines.append(" }")
|
|
589
|
+
lines.append(" void run_backtest(void* s, Bar* bars, int n, ReportC* out) {")
|
|
590
|
+
lines.append(" auto* strat = static_cast<GeneratedStrategy*>(s);")
|
|
591
|
+
if self._security_calls:
|
|
592
|
+
# If there are security calls, use the full run path. Pass empty strings
|
|
593
|
+
# so the C++ runtime auto-detects input_tf from bar timestamps.
|
|
594
|
+
lines.append(' strat->run(bars, n, "", "", false, 4, MagnifierDistribution::ENDPOINTS);')
|
|
595
|
+
else:
|
|
596
|
+
lines.append(" strat->run(bars, n);")
|
|
597
|
+
lines.append(" strat->fill_report(out);")
|
|
598
|
+
lines.append(" }")
|
|
599
|
+
lines.append(" void run_backtest_full(void* s, Bar* bars, int n,")
|
|
600
|
+
lines.append(' const char* input_tf, const char* script_tf,')
|
|
601
|
+
lines.append(" int bar_magnifier, int magnifier_samples,")
|
|
602
|
+
lines.append(" int magnifier_dist,")
|
|
603
|
+
lines.append(" ReportC* out) {")
|
|
604
|
+
lines.append(' auto* strat = static_cast<GeneratedStrategy*>(s);')
|
|
605
|
+
lines.append(' std::string itf = input_tf ? input_tf : "";')
|
|
606
|
+
lines.append(' std::string stf = script_tf ? script_tf : "";')
|
|
607
|
+
if self._security_calls:
|
|
608
|
+
# Pass empty strings through — the C++ runtime auto-detects input_tf from bar timestamps.
|
|
609
|
+
# script_tf defaults to input_tf in the runtime if empty.
|
|
610
|
+
lines.append(" strat->run(bars, n, itf, stf, bar_magnifier != 0, magnifier_samples,")
|
|
611
|
+
lines.append(" static_cast<MagnifierDistribution>(magnifier_dist));")
|
|
612
|
+
else:
|
|
613
|
+
# The magnifier-aware run() overload handles ratio=1 (no
|
|
614
|
+
# aggregation) and empty itf/stf (auto-detect) on its own, so
|
|
615
|
+
# we only need to fall back to the simple ``run(bars, n)``
|
|
616
|
+
# path when the caller explicitly opted out of magnifier AND
|
|
617
|
+
# there is no timeframe aggregation in play. Previously the
|
|
618
|
+
# short-circuit ``itf.empty() || stf.empty() || itf == stf``
|
|
619
|
+
# silently dropped the bar_magnifier flag whenever the host
|
|
620
|
+
# passed empty TFs (the validator's default) — turning every
|
|
621
|
+
# magnifier-opt-in run into a non-magnifier run and producing
|
|
622
|
+
# 0.21% exit-price drift on the magnifier-distribution probes.
|
|
623
|
+
lines.append(" bool needs_full_run = (bar_magnifier != 0)")
|
|
624
|
+
lines.append(" || (!itf.empty() && !stf.empty() && itf != stf);")
|
|
625
|
+
lines.append(" if (!needs_full_run) {")
|
|
626
|
+
lines.append(" strat->run(bars, n);")
|
|
627
|
+
lines.append(" } else {")
|
|
628
|
+
lines.append(" strat->run(bars, n, itf, stf, bar_magnifier != 0, magnifier_samples,")
|
|
629
|
+
lines.append(" static_cast<MagnifierDistribution>(magnifier_dist));")
|
|
630
|
+
lines.append(" }")
|
|
631
|
+
lines.append(" strat->fill_report(out);")
|
|
632
|
+
lines.append(" }")
|
|
633
|
+
lines.append(" void strategy_free(void* s) {")
|
|
634
|
+
lines.append(" delete static_cast<GeneratedStrategy*>(s);")
|
|
635
|
+
lines.append(" }")
|
|
636
|
+
lines.append(" void report_free(ReportC* report) {")
|
|
637
|
+
lines.append(" BacktestEngine::free_report(report);")
|
|
638
|
+
lines.append(" }")
|
|
639
|
+
lines.append(" void strategy_set_input(void* s, const char* key, const char* value) {")
|
|
640
|
+
lines.append(" if (!s || !key || !value) return;")
|
|
641
|
+
lines.append(" static_cast<GeneratedStrategy*>(s)->set_input(key, value);")
|
|
642
|
+
lines.append(" }")
|
|
643
|
+
lines.append(" void strategy_set_override(void* s, const char* key, const char* value) {")
|
|
644
|
+
lines.append(" if (!s || !key || !value) return;")
|
|
645
|
+
lines.append(" static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value);")
|
|
646
|
+
lines.append(" }")
|
|
647
|
+
lines.append(" void strategy_set_magnifier_volume_weighted(void* s, int on) {")
|
|
648
|
+
lines.append(" if (!s) return;")
|
|
649
|
+
lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
|
|
650
|
+
lines.append(" }")
|
|
651
|
+
lines.append("}")
|
|
652
|
+
lines.append("")
|
|
653
|
+
|
|
654
|
+
def _emit_udt_method_cpp_name(self, fi: FuncInfo) -> str:
|
|
655
|
+
"""Stable C++ identifier for a UDT instance method (``_udt_Type_method``)."""
|
|
656
|
+
udt = fi.udt_type_name or ""
|
|
657
|
+
base = fi.node.name if fi.node else ""
|
|
658
|
+
return self._func_safe_name(f"_udt_{udt}_{base}")
|
|
659
|
+
|
|
660
|
+
def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None) -> None:
|
|
661
|
+
"""Emit a user-defined function as a class method.
|
|
662
|
+
|
|
663
|
+
If call_site_idx is not None, emit a per-call-site variant with
|
|
664
|
+
TA member names remapped to call-site-specific copies.
|
|
665
|
+
"""
|
|
666
|
+
node = fi.node
|
|
667
|
+
if node is None:
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
is_udt = bool(getattr(fi, "is_udt_method", False)) and fi.udt_type_name
|
|
671
|
+
|
|
672
|
+
# Determine param types and set context for type inference inside body
|
|
673
|
+
param_strs = []
|
|
674
|
+
self._current_func_param_types = {}
|
|
675
|
+
self._current_func_series_params = set()
|
|
676
|
+
self._udt_param_udt = {}
|
|
677
|
+
func_sv = self.ctx.func_series_vars.get(fi.name, set())
|
|
678
|
+
for i, p in enumerate(node.params):
|
|
679
|
+
if is_udt and i == 0 and fi.udt_type_name:
|
|
680
|
+
cpp_t = f"{fi.udt_type_name}&"
|
|
681
|
+
safe_p = self._safe_name(p)
|
|
682
|
+
self._udt_param_udt[safe_p] = fi.udt_type_name
|
|
683
|
+
self._udt_param_udt[p] = fi.udt_type_name
|
|
684
|
+
elif fi.name == "isInSession" and i < 2:
|
|
685
|
+
cpp_t = "std::string"
|
|
686
|
+
elif p in func_sv:
|
|
687
|
+
# This param uses history access (e.g. src[1]) — pass as Series
|
|
688
|
+
cpp_t = "const Series<double>&"
|
|
689
|
+
self._current_func_series_params.add(p)
|
|
690
|
+
elif i < len(fi.param_types):
|
|
691
|
+
pt = fi.param_types[i]
|
|
692
|
+
cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
|
|
693
|
+
else:
|
|
694
|
+
cpp_t = "double"
|
|
695
|
+
param_strs.append(f"{cpp_t} {self._safe_name(p)}")
|
|
696
|
+
self._current_func_param_types[p] = cpp_t
|
|
697
|
+
|
|
698
|
+
# Determine return type: tuple, UDT, or scalar.
|
|
699
|
+
# The UDT branch handles user functions whose body is ``T.new(...)``;
|
|
700
|
+
# without it the function would be emitted as returning ``double`` and
|
|
701
|
+
# clang errors with "no viable conversion from T to double". Probe:
|
|
702
|
+
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
703
|
+
if fi.returns_tuple:
|
|
704
|
+
# Infer actual tuple element types from function body's last expression
|
|
705
|
+
tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
|
|
706
|
+
ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
|
|
707
|
+
elif getattr(fi, "udt_return_type", None):
|
|
708
|
+
ret_type = fi.udt_return_type
|
|
709
|
+
else:
|
|
710
|
+
ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
|
|
711
|
+
|
|
712
|
+
# For per-call-site variants, suffix the function name and activate TA + var remapping
|
|
713
|
+
func_name = self._emit_udt_method_cpp_name(fi) if is_udt else self._func_safe_name(fi.name)
|
|
714
|
+
if call_site_idx is not None:
|
|
715
|
+
func_name = f"{func_name}_cs{call_site_idx}"
|
|
716
|
+
remap = self._func_cs_ta_remap.get((fi.name, call_site_idx), {})
|
|
717
|
+
self._active_ta_remap = remap
|
|
718
|
+
var_remap = self._func_cs_var_remap.get((fi.name, call_site_idx), {})
|
|
719
|
+
self._active_var_remap = var_remap
|
|
720
|
+
self._in_ta_func_variant = True
|
|
721
|
+
self._active_call_site_idx = call_site_idx
|
|
722
|
+
else:
|
|
723
|
+
self._active_ta_remap = {}
|
|
724
|
+
self._active_var_remap = {}
|
|
725
|
+
self._in_ta_func_variant = False
|
|
726
|
+
self._active_call_site_idx = None
|
|
727
|
+
|
|
728
|
+
prev_func_locals = self._current_func_locals
|
|
729
|
+
self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
|
|
730
|
+
# Plain (non-persistent) scalar locals are emitted inline and live in
|
|
731
|
+
# no other set; collect them so the unknown-identifier guard in
|
|
732
|
+
# _visit_ident does not mistake them for undeclared symbols.
|
|
733
|
+
self._current_func_locals |= self._collect_binding_names(node.body)
|
|
734
|
+
|
|
735
|
+
lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
|
|
736
|
+
|
|
737
|
+
emitted_return = False
|
|
738
|
+
if node.is_single_expr and node.body:
|
|
739
|
+
expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
|
|
740
|
+
if expr:
|
|
741
|
+
lines.append(f" return {self._visit_expr(expr)};")
|
|
742
|
+
emitted_return = True
|
|
743
|
+
else:
|
|
744
|
+
for i, s in enumerate(node.body):
|
|
745
|
+
if i == len(node.body) - 1 and isinstance(s, ExprStmt):
|
|
746
|
+
lines.append(f" return {self._visit_expr(s.expr)};")
|
|
747
|
+
emitted_return = True
|
|
748
|
+
elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
|
|
749
|
+
# Switch/if as last statement = return expression in PineScript
|
|
750
|
+
# Emit as: double _ret = 0; if/switch assigns _ret; return _ret;
|
|
751
|
+
default_ret = (
|
|
752
|
+
f"{ret_type}{{}}" if ret_type in self._udt_defs
|
|
753
|
+
else self._default_for_type(ret_type)
|
|
754
|
+
)
|
|
755
|
+
lines.append(f" {ret_type} _func_ret = {default_ret};")
|
|
756
|
+
self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
|
|
757
|
+
lines.append(f" return _func_ret;")
|
|
758
|
+
emitted_return = True
|
|
759
|
+
else:
|
|
760
|
+
self._visit_stmt(s, lines, indent=2)
|
|
761
|
+
|
|
762
|
+
# Always emit a default return if no explicit return was emitted,
|
|
763
|
+
# to avoid non-void function without return value.
|
|
764
|
+
if not emitted_return:
|
|
765
|
+
if fi.returns_tuple:
|
|
766
|
+
default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
|
|
767
|
+
lines.append(f" return std::make_tuple({default_vals});")
|
|
768
|
+
else:
|
|
769
|
+
default_ret = (
|
|
770
|
+
f"{ret_type}{{}}" if ret_type in self._udt_defs
|
|
771
|
+
else self._default_for_type(ret_type)
|
|
772
|
+
)
|
|
773
|
+
lines.append(f" return {default_ret};")
|
|
774
|
+
|
|
775
|
+
lines.append(" }")
|
|
776
|
+
self._current_func_param_types = {}
|
|
777
|
+
self._current_func_series_params = set()
|
|
778
|
+
self._udt_param_udt = {}
|
|
779
|
+
self._current_func_locals = prev_func_locals
|
|
780
|
+
self._active_ta_remap = {}
|
|
781
|
+
self._active_var_remap = {}
|
|
782
|
+
self._in_ta_func_variant = False
|
|
783
|
+
self._active_call_site_idx = None
|
|
784
|
+
|
|
785
|
+
def _emit_precalculate_and_run(self, lines: list[str]) -> None:
|
|
786
|
+
has_static_ta = any(getattr(site, "is_static", False) for site in self.ctx.ta_call_sites)
|
|
787
|
+
if not has_static_ta:
|
|
788
|
+
return
|
|
789
|
+
|
|
790
|
+
lines.append(" void precalculate(const Bar* bars, int n) {")
|
|
791
|
+
lines.append(" _use_precalc = false;")
|
|
792
|
+
lines.append(" if (n <= 0 || bars == nullptr) return;")
|
|
793
|
+
lines.append("")
|
|
794
|
+
|
|
795
|
+
# Resize precalculated vectors
|
|
796
|
+
for site in self.ctx.ta_call_sites:
|
|
797
|
+
if getattr(site, "is_static", False):
|
|
798
|
+
lines.append(f" _precalc_{site.member_name}.resize(n);")
|
|
799
|
+
|
|
800
|
+
# Reset indicators to clean slate
|
|
801
|
+
lines.append("")
|
|
802
|
+
for site in self.ctx.ta_call_sites:
|
|
803
|
+
if getattr(site, "is_static", False):
|
|
804
|
+
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
805
|
+
safe_resolved = []
|
|
806
|
+
for r in resolved:
|
|
807
|
+
safe_resolved.append(r if self._is_compile_time_value(r) else "1")
|
|
808
|
+
lines.append(f" {site.member_name} = {site.class_name}({', '.join(safe_resolved)});")
|
|
809
|
+
|
|
810
|
+
# Clear series
|
|
811
|
+
lines.append("")
|
|
812
|
+
for field_name in sorted(self.ctx.series_bar_fields):
|
|
813
|
+
lines.append(f" _s_{field_name}.clear();")
|
|
814
|
+
|
|
815
|
+
# Start precalculation loop
|
|
816
|
+
lines.append("")
|
|
817
|
+
lines.append(" for (int i = 0; i < n; ++i) {")
|
|
818
|
+
|
|
819
|
+
# Push OHLCV into series
|
|
820
|
+
for field_name in sorted(self.ctx.series_bar_fields):
|
|
821
|
+
push_expr = BAR_SERIES_PUSH.get(field_name, f"bars[i].{field_name}")
|
|
822
|
+
push_expr_bars = push_expr.replace("current_bar_.", "bars[i].")
|
|
823
|
+
lines.append(f" _s_{field_name}.push({push_expr_bars});")
|
|
824
|
+
|
|
825
|
+
# Set _precalc_loop_active = True
|
|
826
|
+
self._precalc_loop_active = True
|
|
827
|
+
try:
|
|
828
|
+
for site in self.ctx.ta_call_sites:
|
|
829
|
+
if getattr(site, "is_static", False):
|
|
830
|
+
compute_args = self._ta_compute_args_for_site(site)
|
|
831
|
+
compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
|
|
832
|
+
lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
|
|
833
|
+
finally:
|
|
834
|
+
self._precalc_loop_active = False
|
|
835
|
+
|
|
836
|
+
lines.append(" }")
|
|
837
|
+
|
|
838
|
+
# Reset indicators and series for the real backtest run
|
|
839
|
+
lines.append("")
|
|
840
|
+
for site in self.ctx.ta_call_sites:
|
|
841
|
+
if getattr(site, "is_static", False):
|
|
842
|
+
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
843
|
+
safe_resolved = []
|
|
844
|
+
for r in resolved:
|
|
845
|
+
safe_resolved.append(r if self._is_compile_time_value(r) else "1")
|
|
846
|
+
lines.append(f" {site.member_name} = {site.class_name}({', '.join(safe_resolved)});")
|
|
847
|
+
|
|
848
|
+
for field_name in sorted(self.ctx.series_bar_fields):
|
|
849
|
+
lines.append(f" _s_{field_name}.clear();")
|
|
850
|
+
|
|
851
|
+
lines.append("")
|
|
852
|
+
lines.append(" _use_precalc = true;")
|
|
853
|
+
lines.append(" }")
|
|
854
|
+
lines.append("")
|
|
855
|
+
|
|
856
|
+
# Overridden run methods
|
|
857
|
+
lines.append(" void run(const Bar* bars, int n) {")
|
|
858
|
+
lines.append(" precalculate(bars, n);")
|
|
859
|
+
lines.append(" BacktestEngine::run(bars, n);")
|
|
860
|
+
lines.append(" }")
|
|
861
|
+
lines.append("")
|
|
862
|
+
lines.append(" void run(const Bar* input_bars, int n_input,")
|
|
863
|
+
lines.append(" const std::string& input_tf,")
|
|
864
|
+
lines.append(" const std::string& script_tf,")
|
|
865
|
+
lines.append(" bool bar_magnifier = false,")
|
|
866
|
+
lines.append(" int magnifier_samples = 4,")
|
|
867
|
+
lines.append(" MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS) {")
|
|
868
|
+
lines.append(" bool needs_dynamic = bar_magnifier || (!input_tf.empty() && !script_tf.empty() && input_tf != script_tf);")
|
|
869
|
+
lines.append(" if (needs_dynamic) {")
|
|
870
|
+
lines.append(" _use_precalc = false;")
|
|
871
|
+
lines.append(" } else {")
|
|
872
|
+
lines.append(" precalculate(input_bars, n_input);")
|
|
873
|
+
lines.append(" }")
|
|
874
|
+
lines.append(" BacktestEngine::run(input_bars, n_input, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist);")
|
|
875
|
+
lines.append(" }")
|