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.
Files changed (35) hide show
  1. pineforge_codegen/__init__.py +53 -0
  2. pineforge_codegen/analyzer/__init__.py +60 -0
  3. pineforge_codegen/analyzer/base.py +1563 -0
  4. pineforge_codegen/analyzer/call_handlers.py +895 -0
  5. pineforge_codegen/analyzer/contracts.py +163 -0
  6. pineforge_codegen/analyzer/diagnostics.py +118 -0
  7. pineforge_codegen/analyzer/tables.py +204 -0
  8. pineforge_codegen/analyzer/types.py +250 -0
  9. pineforge_codegen/ast_nodes.py +293 -0
  10. pineforge_codegen/codegen/__init__.py +78 -0
  11. pineforge_codegen/codegen/base.py +1381 -0
  12. pineforge_codegen/codegen/emit_top.py +875 -0
  13. pineforge_codegen/codegen/helpers.py +163 -0
  14. pineforge_codegen/codegen/helpers_syminfo.py +134 -0
  15. pineforge_codegen/codegen/input.py +189 -0
  16. pineforge_codegen/codegen/security.py +1564 -0
  17. pineforge_codegen/codegen/ta.py +298 -0
  18. pineforge_codegen/codegen/tables.py +613 -0
  19. pineforge_codegen/codegen/types.py +573 -0
  20. pineforge_codegen/codegen/visit_call.py +1305 -0
  21. pineforge_codegen/codegen/visit_expr.py +701 -0
  22. pineforge_codegen/codegen/visit_stmt.py +729 -0
  23. pineforge_codegen/errors.py +98 -0
  24. pineforge_codegen/lexer.py +531 -0
  25. pineforge_codegen/parser.py +1198 -0
  26. pineforge_codegen/pragmas.py +117 -0
  27. pineforge_codegen/signatures.py +808 -0
  28. pineforge_codegen/support_checker.py +1111 -0
  29. pineforge_codegen/symbols.py +118 -0
  30. pineforge_codegen/tokens.py +406 -0
  31. pineforge_codegen/tv_input_choices.py +86 -0
  32. pineforge_codegen-0.6.5.dist-info/METADATA +462 -0
  33. pineforge_codegen-0.6.5.dist-info/RECORD +35 -0
  34. pineforge_codegen-0.6.5.dist-info/WHEEL +4 -0
  35. 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(" }")