@pineforge/codegen-pyodide 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +797 -45
- package/pineforge_codegen/analyzer/call_handlers.py +368 -68
- package/pineforge_codegen/analyzer/contracts.py +91 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/analyzer/types.py +53 -0
- package/pineforge_codegen/codegen/__init__.py +4 -0
- package/pineforge_codegen/codegen/base.py +1613 -111
- package/pineforge_codegen/codegen/drawing.py +560 -0
- package/pineforge_codegen/codegen/emit_top.py +536 -45
- package/pineforge_codegen/codegen/input.py +31 -0
- package/pineforge_codegen/codegen/security.py +791 -39
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +115 -10
- package/pineforge_codegen/codegen/types.py +478 -46
- package/pineforge_codegen/codegen/visit_call.py +331 -75
- package/pineforge_codegen/codegen/visit_expr.py +116 -14
- package/pineforge_codegen/codegen/visit_stmt.py +208 -23
- package/pineforge_codegen/parser.py +129 -29
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +320 -19
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +7 -10
- package/pineforge_codegen-0.8.0.tar.gz +0 -0
|
@@ -90,6 +90,7 @@ from ..analyzer import FuncInfo
|
|
|
90
90
|
from ..symbols import TypeSpec
|
|
91
91
|
from .tables import (
|
|
92
92
|
BAR_SERIES_PUSH,
|
|
93
|
+
DRAWING_TYPE_TO_CPP,
|
|
93
94
|
PINE_TYPE_TO_CPP,
|
|
94
95
|
RUNTIME_REGISTER_SECURITY_EVAL_FN,
|
|
95
96
|
RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN,
|
|
@@ -117,6 +118,8 @@ class TopLevelEmitter:
|
|
|
117
118
|
lines.append("#include <string>")
|
|
118
119
|
lines.append("#include <vector>")
|
|
119
120
|
lines.append("#include <tuple>")
|
|
121
|
+
lines.append("#include <optional>")
|
|
122
|
+
lines.append("#include <type_traits>")
|
|
120
123
|
lines.append("#include <memory>")
|
|
121
124
|
lines.append("#include <mutex>")
|
|
122
125
|
lines.append("#include <unordered_map>")
|
|
@@ -136,6 +139,11 @@ class TopLevelEmitter:
|
|
|
136
139
|
float_spec = TypeSpec.primitive("float")
|
|
137
140
|
if any(spec.element != float_spec for spec in self._matrix_specs.values()):
|
|
138
141
|
lines.append('#include <pineforge/generic_matrix.hpp>')
|
|
142
|
+
# Drawing-objects-as-data runtime (line/box/label/linefill arenas +
|
|
143
|
+
# ChartPoint). Gated on _uses_drawing so non-drawing strategies stay
|
|
144
|
+
# byte-identical — mirrors the matrix.hpp gating above.
|
|
145
|
+
if getattr(self, "_uses_drawing", False):
|
|
146
|
+
lines.append('#include <pineforge/drawing.hpp>')
|
|
139
147
|
lines.append("")
|
|
140
148
|
# Compatibility shim for the namespace-wrap refactor: unqualified
|
|
141
149
|
# references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
|
|
@@ -186,6 +194,139 @@ class TopLevelEmitter:
|
|
|
186
194
|
return True
|
|
187
195
|
return False
|
|
188
196
|
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _script_state_member_name(decl_line: str) -> str | None:
|
|
199
|
+
"""Extract a generated class-member name from one declaration line.
|
|
200
|
+
|
|
201
|
+
``CodeGen.generate`` emits the complete persistent script state as a
|
|
202
|
+
contiguous block of one-line declarations before the constructor. The
|
|
203
|
+
rollback checkpoint is derived from that block rather than from a
|
|
204
|
+
second, hand-maintained inventory: adding a new TA/helper/collection
|
|
205
|
+
member therefore automatically makes it part of Pine's historical
|
|
206
|
+
execution rollback.
|
|
207
|
+
|
|
208
|
+
The supported declaration shapes are the only shapes emitted in that
|
|
209
|
+
block today::
|
|
210
|
+
|
|
211
|
+
Type name;
|
|
212
|
+
Type name = value;
|
|
213
|
+
Type name(args);
|
|
214
|
+
Type name{args};
|
|
215
|
+
|
|
216
|
+
A declaration that does not match fails generation loudly. Silently
|
|
217
|
+
omitting an unfamiliar member would be materially worse: it would let
|
|
218
|
+
state leak between ``calc_on_order_fills`` executions.
|
|
219
|
+
"""
|
|
220
|
+
text = decl_line.strip()
|
|
221
|
+
if not text:
|
|
222
|
+
return None
|
|
223
|
+
if not text.endswith(";"):
|
|
224
|
+
raise AssertionError(
|
|
225
|
+
f"unexpected generated script-state declaration: {decl_line!r}"
|
|
226
|
+
)
|
|
227
|
+
text = text[:-1].rstrip()
|
|
228
|
+
if " = " in text:
|
|
229
|
+
text = text.split(" = ", 1)[0].rstrip()
|
|
230
|
+
else:
|
|
231
|
+
# Series<T> members can carry a max_bars_back ctor suffix and
|
|
232
|
+
# drawing arenas use brace initialization. Both suffixes start in
|
|
233
|
+
# the final declarator token; C++ types emitted here never contain
|
|
234
|
+
# parentheses or braces.
|
|
235
|
+
last_space = text.rfind(" ")
|
|
236
|
+
if last_space < 0:
|
|
237
|
+
raise AssertionError(
|
|
238
|
+
f"missing type in generated script-state declaration: {decl_line!r}"
|
|
239
|
+
)
|
|
240
|
+
declarator = text[last_space + 1:]
|
|
241
|
+
cut = len(declarator)
|
|
242
|
+
for marker in ("(", "{"):
|
|
243
|
+
pos = declarator.find(marker)
|
|
244
|
+
if pos >= 0:
|
|
245
|
+
cut = min(cut, pos)
|
|
246
|
+
text = text[:last_space + 1] + declarator[:cut]
|
|
247
|
+
|
|
248
|
+
name = text.rsplit(" ", 1)[-1].strip()
|
|
249
|
+
if not name or not (name[0].isalpha() or name[0] == "_") \
|
|
250
|
+
or not all(ch.isalnum() or ch == "_" for ch in name):
|
|
251
|
+
raise AssertionError(
|
|
252
|
+
f"cannot identify generated script-state member: {decl_line!r}"
|
|
253
|
+
)
|
|
254
|
+
return name
|
|
255
|
+
|
|
256
|
+
def _collect_script_state_members(self, declaration_lines: list[str]) -> list[str]:
|
|
257
|
+
"""Return every rollback-relevant generated member in declaration order.
|
|
258
|
+
|
|
259
|
+
Precalculated TA result vectors and their mode flag are immutable once
|
|
260
|
+
the engine starts its broker walk. Copying an O(number-of-bars) cache
|
|
261
|
+
before every COOF execution would be both unnecessary and catastrophic,
|
|
262
|
+
so those implementation caches are the sole exclusions. The live TA
|
|
263
|
+
objects themselves remain captured because dynamic/magnifier runs call
|
|
264
|
+
``compute`` and mutate them.
|
|
265
|
+
"""
|
|
266
|
+
members: list[str] = []
|
|
267
|
+
seen: set[str] = set()
|
|
268
|
+
for line in declaration_lines:
|
|
269
|
+
name = self._script_state_member_name(line)
|
|
270
|
+
if name is None:
|
|
271
|
+
continue
|
|
272
|
+
if name == "_use_precalc" or name.startswith("_precalc_"):
|
|
273
|
+
continue
|
|
274
|
+
if name in seen:
|
|
275
|
+
raise AssertionError(f"duplicate generated script-state member: {name}")
|
|
276
|
+
seen.add(name)
|
|
277
|
+
members.append(name)
|
|
278
|
+
return members
|
|
279
|
+
|
|
280
|
+
def _emit_script_state_hooks(self, lines: list[str], members: list[str]) -> None:
|
|
281
|
+
"""Emit the engine's Pine rollback checkpoint hook implementation.
|
|
282
|
+
|
|
283
|
+
The checkpoint owns value copies of all generated mutable state. Every
|
|
284
|
+
runtime container used by generated code (Series, std::vector/map,
|
|
285
|
+
PineMatrix/generic matrices, UDTs and drawing arenas) has value
|
|
286
|
+
semantics, so copying recursively preserves data without retaining
|
|
287
|
+
pointers into live state. Drawing handles themselves are stable ids;
|
|
288
|
+
their arenas are captured in the same checkpoint.
|
|
289
|
+
|
|
290
|
+
The static assertions deliberately turn any future non-copyable member
|
|
291
|
+
into a compile failure instead of a nominal, shallow rollback. Engine
|
|
292
|
+
broker/order state lives in the base class and is intentionally absent:
|
|
293
|
+
fills must survive while Pine script variables roll back.
|
|
294
|
+
"""
|
|
295
|
+
lines.append(" struct _PFScriptState {")
|
|
296
|
+
for idx, name in enumerate(members):
|
|
297
|
+
lines.append(
|
|
298
|
+
f" decltype(GeneratedStrategy::{name}) _pf_value_{idx};"
|
|
299
|
+
)
|
|
300
|
+
lines.append(" };")
|
|
301
|
+
lines.append(
|
|
302
|
+
" static_assert(std::is_copy_constructible_v<_PFScriptState>, "
|
|
303
|
+
'"generated Pine state must be deep-copy constructible");'
|
|
304
|
+
)
|
|
305
|
+
lines.append(
|
|
306
|
+
" static_assert(std::is_copy_assignable_v<_PFScriptState>, "
|
|
307
|
+
'"generated Pine state must be deep-copy assignable");'
|
|
308
|
+
)
|
|
309
|
+
lines.append(" std::optional<_PFScriptState> _pf_script_state_checkpoint_;")
|
|
310
|
+
lines.append("")
|
|
311
|
+
lines.append(" void snapshot_script_state() override {")
|
|
312
|
+
lines.append(" _pf_script_state_checkpoint_.emplace(_PFScriptState{")
|
|
313
|
+
for name in members:
|
|
314
|
+
lines.append(f" {name},")
|
|
315
|
+
lines.append(" });")
|
|
316
|
+
lines.append(" }")
|
|
317
|
+
lines.append("")
|
|
318
|
+
lines.append(" void restore_script_state() override {")
|
|
319
|
+
lines.append(" if (!_pf_script_state_checkpoint_) return;")
|
|
320
|
+
for idx, name in enumerate(members):
|
|
321
|
+
lines.append(
|
|
322
|
+
f" this->{name} = _pf_script_state_checkpoint_->_pf_value_{idx};"
|
|
323
|
+
)
|
|
324
|
+
lines.append(" }")
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append(" void commit_script_state() override {")
|
|
327
|
+
lines.append(" snapshot_script_state();")
|
|
328
|
+
lines.append(" }")
|
|
329
|
+
|
|
189
330
|
def _typed_na_init(self, cpp_val: str, name: str, ptype) -> str:
|
|
190
331
|
"""Re-type a bare ``na<double>()`` initializer to match a non-double
|
|
191
332
|
member's C++ type. A ``var int x = na`` resolves its RHS to
|
|
@@ -206,11 +347,35 @@ class TopLevelEmitter:
|
|
|
206
347
|
def _emit_constructor(self, lines: list[str]) -> None:
|
|
207
348
|
init_parts: list[str] = []
|
|
208
349
|
# TA members with ctor args
|
|
209
|
-
for site in self.ctx.ta_call_sites:
|
|
350
|
+
for ta_idx, site in enumerate(self.ctx.ta_call_sites):
|
|
351
|
+
# Skip dead-code function TA sites entirely — their buffers never
|
|
352
|
+
# run and their ctor args (bare param names) can never be sized.
|
|
353
|
+
if ta_idx in self._dead_ta_indices:
|
|
354
|
+
continue
|
|
210
355
|
if site.ctor_args:
|
|
356
|
+
# If a ctor arg is neither a compile-time literal nor expandable
|
|
357
|
+
# to an input-backed runtime expression, the old code silently
|
|
358
|
+
# emitted period 1 with no overwriting reset — a wrong indicator
|
|
359
|
+
# masquerading as a working one. Refuse loudly instead. Args that
|
|
360
|
+
# DO expand to a runtime expr (input-backed / arithmetic-over-input,
|
|
361
|
+
# incl. function-derived lengths) are safe: the `!_ta_initialized_`
|
|
362
|
+
# reset overwrites the placeholder before the first compute.
|
|
363
|
+
for a in site.ctor_args:
|
|
364
|
+
r = self._resolve_known(a)
|
|
365
|
+
if (not self._is_compile_time_value(r)
|
|
366
|
+
and self._runtime_ctor_arg_for_reset(a) is None):
|
|
367
|
+
self._codegen_error(
|
|
368
|
+
getattr(site, "node", None),
|
|
369
|
+
f"Unsupported TA constructor length '{a}' for "
|
|
370
|
+
f"{site.class_name}: it is neither a compile-time "
|
|
371
|
+
f"constant nor derived from an input, so PineForge "
|
|
372
|
+
f"cannot size the indicator buffer.",
|
|
373
|
+
hint=("Use a literal, an input.*() value, or "
|
|
374
|
+
"arithmetic over those for TA lengths."),
|
|
375
|
+
)
|
|
211
376
|
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
212
|
-
#
|
|
213
|
-
# (
|
|
377
|
+
# Compile-time placeholder for the init list; the runtime reset
|
|
378
|
+
# (when the arg is input-derived) overwrites it on the first bar.
|
|
214
379
|
safe_resolved = []
|
|
215
380
|
for r in resolved:
|
|
216
381
|
if self._is_compile_time_value(r):
|
|
@@ -240,11 +405,24 @@ class TopLevelEmitter:
|
|
|
240
405
|
safe = self._safe_name(name)
|
|
241
406
|
if name in self._array_vars or name in self._map_vars:
|
|
242
407
|
continue
|
|
408
|
+
# Function-scoped ``var`` members are initialized once-per-variant
|
|
409
|
+
# on first call (see _emit_func_var_init_block); exclude them from
|
|
410
|
+
# the constructor so they are not double-initialized and so their
|
|
411
|
+
# (possibly bar-dependent) initializer is lowered in the function's
|
|
412
|
+
# own scope (with its active var remap for clones).
|
|
413
|
+
if name in getattr(self, "_func_local_var_names", ()):
|
|
414
|
+
continue
|
|
243
415
|
# UDT-typed var members (``var SDZone z = na``) default-construct to
|
|
244
416
|
# na via the struct's in-class ``__pf_na = true``; a ctor init like
|
|
245
417
|
# ``z(na<double>())`` would not type-match the struct member.
|
|
246
418
|
if name in self._udt_var_types and self._udt_var_types[name] in self._udt_defs:
|
|
247
419
|
continue
|
|
420
|
+
# Drawing handle var member (L-N3): ``var line x`` / ``var box b``
|
|
421
|
+
# default-construct to {-1} (na). A ``b(na<double>())`` ctor init
|
|
422
|
+
# would not type-match the handle struct — skip it (the in-class
|
|
423
|
+
# member default is the once-only persistent na init).
|
|
424
|
+
if name in self._udt_var_types and self._udt_var_types[name] in DRAWING_TYPE_TO_CPP:
|
|
425
|
+
continue
|
|
248
426
|
if name not in self.ctx.series_vars:
|
|
249
427
|
cpp_val = self._resolve_known(init_expr)
|
|
250
428
|
cpp_val = self._typed_na_init(cpp_val, name, ptype)
|
|
@@ -257,6 +435,9 @@ class TopLevelEmitter:
|
|
|
257
435
|
if sp.get("process_orders_on_close") is True:
|
|
258
436
|
ctor_body.append(" process_orders_on_close_ = true;")
|
|
259
437
|
|
|
438
|
+
if sp.get("calc_on_order_fills") is True:
|
|
439
|
+
ctor_body.append(" calc_on_order_fills_ = true;")
|
|
440
|
+
|
|
260
441
|
if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
|
|
261
442
|
ctor_body.append(f" initial_capital_ = {float(sp['initial_capital'])};")
|
|
262
443
|
|
|
@@ -341,6 +522,7 @@ class TopLevelEmitter:
|
|
|
341
522
|
lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
|
|
342
523
|
lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
|
|
343
524
|
lines.append(' if (key == "process_orders_on_close") { process_orders_on_close_ = (value == "true" || value == "1"); return; }')
|
|
525
|
+
lines.append(' if (key == "calc_on_order_fills") { calc_on_order_fills_ = (value == "true" || value == "1"); return; }')
|
|
344
526
|
lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
|
|
345
527
|
lines.append(' if (key == "default_qty_type") {')
|
|
346
528
|
lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
|
|
@@ -362,20 +544,22 @@ class TopLevelEmitter:
|
|
|
362
544
|
lines.append(" security_eval_states_.clear();")
|
|
363
545
|
for info in self._security_eval_info:
|
|
364
546
|
tf = info.get("tf")
|
|
365
|
-
tf_expr =
|
|
547
|
+
tf_expr = info.get("tf_expr")
|
|
366
548
|
if tf:
|
|
367
549
|
tf_expr = f'"{tf}"'
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
raw_tf_expr = self._visit_expr(tf_node)
|
|
375
|
-
tf_expr = self._runtime_ctor_arg_for_reset(raw_tf_expr) or raw_tf_expr
|
|
550
|
+
elif not tf_expr:
|
|
551
|
+
# No static tf and no resolvable runtime expression — fall
|
|
552
|
+
# back to the chart timeframe so registration still compiles
|
|
553
|
+
# (e.g. a request.security inside a dead-code UDF, or one
|
|
554
|
+
# whose tf is a function param called with mixed timeframes).
|
|
555
|
+
tf_expr = "input_tf_"
|
|
376
556
|
if tf_expr:
|
|
377
557
|
la = "true" if info["lookahead_on"] else "false"
|
|
378
558
|
go = "true" if info.get("gaps_on") else "false"
|
|
559
|
+
# Heikin-Ashi same-symbol read: emit the 6th arg only when set
|
|
560
|
+
# so every non-HA strategy's generated code stays byte-identical
|
|
561
|
+
# (the engine param defaults to false).
|
|
562
|
+
ha_arg = ", true" if info.get("heikinashi") else ""
|
|
379
563
|
sec_id = info["sec_id"]
|
|
380
564
|
# The runtime registration function is named in tables.py
|
|
381
565
|
# to keep a single source of truth and to avoid embedding
|
|
@@ -391,7 +575,7 @@ class TopLevelEmitter:
|
|
|
391
575
|
lines.append(
|
|
392
576
|
f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
|
|
393
577
|
f"({sec_id}, {tf_expr}, "
|
|
394
|
-
f"input_tf_, {la}, {go});")
|
|
578
|
+
f"input_tf_, {la}, {go}{ha_arg});")
|
|
395
579
|
lines.append(" }")
|
|
396
580
|
|
|
397
581
|
# Map strategy series member name to push expression
|
|
@@ -407,14 +591,45 @@ class TopLevelEmitter:
|
|
|
407
591
|
"initial_capital": "initial_capital_",
|
|
408
592
|
}
|
|
409
593
|
|
|
594
|
+
@staticmethod
|
|
595
|
+
def _emit_history_series_write(
|
|
596
|
+
lines: list[str], pad: str, member: str, value: str) -> None:
|
|
597
|
+
"""Emit one Pine-series write without conflating history with isnew.
|
|
598
|
+
|
|
599
|
+
Historical fill recalculations keep ``barstate.isnew`` true, but a
|
|
600
|
+
post-close recalculation restored from the completed ordinary-close
|
|
601
|
+
checkpoint must replace that bar's current history slot rather than
|
|
602
|
+
append a duplicate slot. The engine exposes those independent facts
|
|
603
|
+
as ``is_first_tick_`` and ``history_advances_new_bar()`` respectively.
|
|
604
|
+
"""
|
|
605
|
+
lines.append(
|
|
606
|
+
f"{pad}if (history_advances_new_bar()) {member}.push({value});"
|
|
607
|
+
)
|
|
608
|
+
lines.append(f"{pad}else {member}.update({value});")
|
|
609
|
+
|
|
410
610
|
def _emit_on_bar(self, lines: list[str]) -> None:
|
|
411
611
|
lines.append(" void on_bar(const Bar& bar) override {")
|
|
412
612
|
|
|
613
|
+
# reset_run_state() owns engine/broker state, while these generated
|
|
614
|
+
# Series members belong to the strategy object. Clear all of them on
|
|
615
|
+
# the first genuine history slot of bar zero, unconditionally: a site
|
|
616
|
+
# may live behind a branch that does not execute on bar zero. This is a
|
|
617
|
+
# narrow synthetic-buffer reset, not a promise that every generated
|
|
618
|
+
# member/init latch supports full same-handle reruns. The post-C rollback
|
|
619
|
+
# execution has history_advances_new_bar()==false, so it preserves the
|
|
620
|
+
# committed slot.
|
|
621
|
+
for info in self._inline_history_members:
|
|
622
|
+
lines.append(
|
|
623
|
+
" if (history_advances_new_bar() && bar_index_ == 0) "
|
|
624
|
+
f"{info['member_name']}.clear();"
|
|
625
|
+
)
|
|
626
|
+
|
|
413
627
|
# a. Push bar field series (with bar magnifier support)
|
|
414
628
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
415
629
|
push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
|
|
416
|
-
|
|
417
|
-
|
|
630
|
+
self._emit_history_series_write(
|
|
631
|
+
lines, " ", f"_s_{field_name}", push_expr
|
|
632
|
+
)
|
|
418
633
|
|
|
419
634
|
# a1. Push history-referenced scalar bar builtins (time[n], bar_index[n],
|
|
420
635
|
# hl2[n], …). They land in ``series_vars`` and are declared as Series
|
|
@@ -429,22 +644,25 @@ class TopLevelEmitter:
|
|
|
429
644
|
if _bname in self._var_names:
|
|
430
645
|
continue
|
|
431
646
|
_bexpr = BAR_BUILTINS.get(_bname)
|
|
432
|
-
if _bexpr is None or f"{_bname}("
|
|
647
|
+
if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
|
|
433
648
|
continue
|
|
434
649
|
_bsafe = self._safe_name(_bname)
|
|
435
|
-
|
|
436
|
-
lines.append(f" else {_bsafe}.update({_bexpr});")
|
|
650
|
+
self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
|
|
437
651
|
|
|
438
652
|
# a2. Push strategy series
|
|
439
653
|
for svar in sorted(self._strategy_series_vars):
|
|
440
654
|
member = svar.replace("_strat_", "")
|
|
441
655
|
push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
|
|
442
|
-
|
|
656
|
+
self._emit_history_series_write(lines, " ", svar, push_expr)
|
|
443
657
|
|
|
444
658
|
# b. Var init / carry-forward
|
|
445
659
|
if self.ctx.var_members:
|
|
446
660
|
lines.append(" if (!_var_initialized) {")
|
|
447
661
|
for name, ptype, init_expr in self.ctx.var_members:
|
|
662
|
+
# Function-scoped ``var`` members are init'd once-per-variant
|
|
663
|
+
# on first call (see _emit_func_var_init_block), not here.
|
|
664
|
+
if name in getattr(self, "_func_local_var_names", ()):
|
|
665
|
+
continue
|
|
448
666
|
safe = self._safe_name(name)
|
|
449
667
|
if name in self._array_vars:
|
|
450
668
|
for stmt in self.ctx.ast.body:
|
|
@@ -507,7 +725,9 @@ class TopLevelEmitter:
|
|
|
507
725
|
if name in self._array_vars:
|
|
508
726
|
continue
|
|
509
727
|
if name in self.ctx.series_vars:
|
|
510
|
-
|
|
728
|
+
self._emit_history_series_write(
|
|
729
|
+
lines, " ", safe, f"{safe}[0]"
|
|
730
|
+
)
|
|
511
731
|
# Also carry-forward cloned copies for per-call-site function variants
|
|
512
732
|
carry_emitted: set[str] = set()
|
|
513
733
|
for (fname, cs_idx), remap in self._func_cs_var_remap.items():
|
|
@@ -517,7 +737,9 @@ class TopLevelEmitter:
|
|
|
517
737
|
cloned = remap[safe]
|
|
518
738
|
if cloned not in carry_emitted:
|
|
519
739
|
carry_emitted.add(cloned)
|
|
520
|
-
|
|
740
|
+
self._emit_history_series_write(
|
|
741
|
+
lines, " ", cloned, f"{cloned}[0]"
|
|
742
|
+
)
|
|
521
743
|
lines.append(" }")
|
|
522
744
|
|
|
523
745
|
# c. Push non-var series (they start fresh each bar with a push)
|
|
@@ -545,6 +767,7 @@ class TopLevelEmitter:
|
|
|
545
767
|
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
546
768
|
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
547
769
|
getter = self._input_type_to_getter(func_name_i, namespace_i)
|
|
770
|
+
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
548
771
|
cpp_val = f'{getter}("{title}", {default_cpp})'
|
|
549
772
|
static_vars.append(f"{safe} = {cpp_val};")
|
|
550
773
|
|
|
@@ -709,11 +932,17 @@ class TopLevelEmitter:
|
|
|
709
932
|
base = fi.node.name if fi.node else ""
|
|
710
933
|
return self._func_safe_name(f"_udt_{udt}_{base}")
|
|
711
934
|
|
|
712
|
-
def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None
|
|
935
|
+
def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
|
|
936
|
+
instance: dict | None = None) -> None:
|
|
713
937
|
"""Emit a user-defined function as a class method.
|
|
714
938
|
|
|
715
939
|
If call_site_idx is not None, emit a per-call-site variant with
|
|
716
940
|
TA member names remapped to call-site-specific copies.
|
|
941
|
+
|
|
942
|
+
If ``instance`` is provided (a fresh context-sensitive instance minted by
|
|
943
|
+
``_build_func_instances``), emit a uniquely-named clone whose TA/var
|
|
944
|
+
members come from the instance's composed remaps instead of the flat
|
|
945
|
+
``_func_cs_*_remap`` tables.
|
|
717
946
|
"""
|
|
718
947
|
node = fi.node
|
|
719
948
|
if node is None:
|
|
@@ -724,12 +953,19 @@ class TopLevelEmitter:
|
|
|
724
953
|
# Determine param types and set context for type inference inside body
|
|
725
954
|
param_strs = []
|
|
726
955
|
self._current_func_param_types = {}
|
|
956
|
+
self._current_func_param_specs = {}
|
|
727
957
|
self._current_func_series_params = set()
|
|
728
958
|
self._udt_param_udt = {}
|
|
729
959
|
func_sv = self.ctx.func_series_vars.get(fi.name, set())
|
|
730
960
|
for i, p in enumerate(node.params):
|
|
961
|
+
spec = None
|
|
731
962
|
if is_udt and i == 0 and fi.udt_type_name:
|
|
732
|
-
|
|
963
|
+
# A method receiver whose type is a drawing primitive
|
|
964
|
+
# (egoigor's ``method slope(line ln)``) must emit ``Line&`` not
|
|
965
|
+
# the unknown ``line&``. Register _udt_param_udt so the body's
|
|
966
|
+
# getters dispatch through the §4.3 drawing path (L.6d / U.5).
|
|
967
|
+
recv_cpp = DRAWING_TYPE_TO_CPP.get(fi.udt_type_name, fi.udt_type_name)
|
|
968
|
+
cpp_t = f"{recv_cpp}&"
|
|
733
969
|
safe_p = self._safe_name(p)
|
|
734
970
|
self._udt_param_udt[safe_p] = fi.udt_type_name
|
|
735
971
|
self._udt_param_udt[p] = fi.udt_type_name
|
|
@@ -739,6 +975,24 @@ class TopLevelEmitter:
|
|
|
739
975
|
# This param uses history access (e.g. src[1]) — pass as Series
|
|
740
976
|
cpp_t = "const Series<double>&"
|
|
741
977
|
self._current_func_series_params.add(p)
|
|
978
|
+
elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
|
|
979
|
+
# Precise per-param TypeSpec (declared hint or call-site inference):
|
|
980
|
+
# ``pivot hi`` -> ``pivot&``, ``line ln`` -> ``Line&``, an untyped
|
|
981
|
+
# ``s`` used as a string -> ``std::string``. UDT / collection
|
|
982
|
+
# params pass by reference (Pine UDTs/arrays are reference types,
|
|
983
|
+
# so mutations propagate and member access compiles).
|
|
984
|
+
spec = fi.param_type_specs[i]
|
|
985
|
+
cpp_t = self._type_spec_to_cpp(spec)
|
|
986
|
+
if spec.kind == "udt":
|
|
987
|
+
self._udt_param_udt[p] = spec.name
|
|
988
|
+
self._udt_param_udt[self._safe_name(p)] = spec.name
|
|
989
|
+
cpp_t = f"{cpp_t}&"
|
|
990
|
+
elif spec.kind in ("array", "map"):
|
|
991
|
+
elem = spec.element if spec.kind == "array" else spec.value
|
|
992
|
+
if elem is not None and elem.kind == "udt":
|
|
993
|
+
self._udt_param_udt[p] = elem.name
|
|
994
|
+
self._udt_param_udt[self._safe_name(p)] = elem.name
|
|
995
|
+
cpp_t = f"{cpp_t}&"
|
|
742
996
|
elif i < len(fi.param_types):
|
|
743
997
|
pt = fi.param_types[i]
|
|
744
998
|
cpp_t = PINE_TYPE_TO_CPP.get(pt, "double")
|
|
@@ -746,6 +1000,9 @@ class TopLevelEmitter:
|
|
|
746
1000
|
cpp_t = "double"
|
|
747
1001
|
param_strs.append(f"{cpp_t} {self._safe_name(p)}")
|
|
748
1002
|
self._current_func_param_types[p] = cpp_t
|
|
1003
|
+
if spec is not None:
|
|
1004
|
+
self._current_func_param_specs[p] = spec
|
|
1005
|
+
self._current_func_param_specs[self._safe_name(p)] = spec
|
|
749
1006
|
|
|
750
1007
|
# Determine return type: tuple, UDT, or scalar.
|
|
751
1008
|
# The UDT branch handles user functions whose body is ``T.new(...)``;
|
|
@@ -757,28 +1014,69 @@ class TopLevelEmitter:
|
|
|
757
1014
|
tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
|
|
758
1015
|
ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
|
|
759
1016
|
elif getattr(fi, "udt_return_type", None):
|
|
760
|
-
|
|
1017
|
+
# A function returning a drawing handle must emit the C++ handle
|
|
1018
|
+
# struct (Line/Box/Label/Linefill), not the unknown lowercase name.
|
|
1019
|
+
ret_type = DRAWING_TYPE_TO_CPP.get(fi.udt_return_type, fi.udt_return_type)
|
|
1020
|
+
elif getattr(fi, "return_type_spec", None) is not None:
|
|
1021
|
+
# Array-returning function (``f() => array.from(...)``) — emit the
|
|
1022
|
+
# vector type from the inferred element TypeSpec.
|
|
1023
|
+
ret_type = self._type_spec_to_cpp(fi.return_type_spec)
|
|
761
1024
|
else:
|
|
762
1025
|
ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
|
|
763
1026
|
|
|
764
1027
|
# For per-call-site variants, suffix the function name and activate TA + var remapping
|
|
765
1028
|
func_name = self._emit_udt_method_cpp_name(fi) if is_udt else self._func_safe_name(fi.name)
|
|
766
|
-
|
|
1029
|
+
var_init_flag: str | None = None
|
|
1030
|
+
if instance is not None:
|
|
1031
|
+
# Fresh context-sensitive instance: name + composed remaps come from
|
|
1032
|
+
# the instance record. No textual cs index (dispatch is via the
|
|
1033
|
+
# instance map), but it IS a state-isolated variant.
|
|
1034
|
+
func_name = instance["name"]
|
|
1035
|
+
self._active_ta_remap = instance["ta_remap"]
|
|
1036
|
+
self._active_var_remap = instance["var_remap"]
|
|
1037
|
+
self._active_fixnan_remap = instance.get("fixnan_remap", {})
|
|
1038
|
+
self._in_ta_func_variant = True
|
|
1039
|
+
self._active_call_site_idx = None
|
|
1040
|
+
self._current_instance_name = instance["name"]
|
|
1041
|
+
var_init_flag = f"_fvinit_{instance['name']}"
|
|
1042
|
+
elif call_site_idx is not None:
|
|
767
1043
|
func_name = f"{func_name}_cs{call_site_idx}"
|
|
768
1044
|
remap = self._func_cs_ta_remap.get((fi.name, call_site_idx), {})
|
|
769
1045
|
self._active_ta_remap = remap
|
|
770
1046
|
var_remap = self._func_cs_var_remap.get((fi.name, call_site_idx), {})
|
|
771
1047
|
self._active_var_remap = var_remap
|
|
1048
|
+
self._active_fixnan_remap = self._func_cs_fixnan_remap.get((fi.name, call_site_idx), {})
|
|
772
1049
|
self._in_ta_func_variant = True
|
|
773
1050
|
self._active_call_site_idx = call_site_idx
|
|
1051
|
+
# Use the actual emitted name. Plain UDFs are unchanged; UDT
|
|
1052
|
+
# methods carry their `_udt_Type_method` prefix. This identity is
|
|
1053
|
+
# shared with _build_func_instances and synthetic-history member
|
|
1054
|
+
# registration, so method call paths can dispatch independently.
|
|
1055
|
+
self._current_instance_name = func_name
|
|
774
1056
|
else:
|
|
775
1057
|
self._active_ta_remap = {}
|
|
776
1058
|
self._active_var_remap = {}
|
|
1059
|
+
self._active_fixnan_remap = {}
|
|
777
1060
|
self._in_ta_func_variant = False
|
|
778
1061
|
self._active_call_site_idx = None
|
|
1062
|
+
self._current_instance_name = None
|
|
779
1063
|
|
|
780
1064
|
prev_func_locals = self._current_func_locals
|
|
1065
|
+
prev_func_local_types = self._current_func_local_types
|
|
1066
|
+
prev_func_body = getattr(self, "_current_func_body", None)
|
|
1067
|
+
prev_func_name = getattr(self, "_active_func_name", None)
|
|
1068
|
+
# The function body is the lexical scope used by the UDT-alias analysis
|
|
1069
|
+
# (BUG C): a local initialised from a var/global UDT lvalue and later
|
|
1070
|
+
# mutated through must alias, not value-copy.
|
|
1071
|
+
self._current_func_body = node.body
|
|
1072
|
+
self._active_func_name = fi.name
|
|
1073
|
+
# Pointer-aliased UDT locals are function-scoped: a name like ``p_ivot``
|
|
1074
|
+
# may be a rebinding pointer alias in one function and a ``pivot&``
|
|
1075
|
+
# parameter in another, so reset per function to avoid cross-contamination.
|
|
1076
|
+
prev_ptr_alias = self._udt_ptr_alias_locals
|
|
1077
|
+
self._udt_ptr_alias_locals = set()
|
|
781
1078
|
self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
|
|
1079
|
+
self._current_func_local_types = {}
|
|
782
1080
|
# Plain (non-persistent) scalar locals are emitted inline and live in
|
|
783
1081
|
# no other set; collect them so the unknown-identifier guard in
|
|
784
1082
|
# _visit_ident does not mistake them for undeclared symbols.
|
|
@@ -786,24 +1084,47 @@ class TopLevelEmitter:
|
|
|
786
1084
|
|
|
787
1085
|
lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
|
|
788
1086
|
|
|
1087
|
+
# Function-scoped ``var`` one-shot initializer: Pine ``var`` inside a
|
|
1088
|
+
# function is a function-local static — its initializer runs exactly
|
|
1089
|
+
# once, on the first call to THIS variant, with the first bar's values
|
|
1090
|
+
# the function actually sees. Each clone (cs0/cs1/…) is independent.
|
|
1091
|
+
self._emit_func_var_init_block(fi, call_site_idx, lines, flag_override=var_init_flag)
|
|
1092
|
+
|
|
789
1093
|
emitted_return = False
|
|
790
1094
|
if node.is_single_expr and node.body:
|
|
791
1095
|
expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
|
|
792
|
-
if expr:
|
|
1096
|
+
if expr and self._call_is_void(expr):
|
|
1097
|
+
# void setter as the sole body expr — emit as statement, fall
|
|
1098
|
+
# through to the default return.
|
|
1099
|
+
self._visit_stmt(node.body[0], lines, indent=2)
|
|
1100
|
+
elif expr:
|
|
793
1101
|
lines.append(f" return {self._visit_expr(expr)};")
|
|
794
1102
|
emitted_return = True
|
|
795
1103
|
else:
|
|
796
1104
|
for i, s in enumerate(node.body):
|
|
797
1105
|
if i == len(node.body) - 1 and isinstance(s, ExprStmt):
|
|
798
|
-
|
|
799
|
-
|
|
1106
|
+
# A void drawing setter / delete / visual-noop, or a dropped
|
|
1107
|
+
# table/polyline method call (``panel.cell(...)``), used as
|
|
1108
|
+
# the last statement cannot be the return value (it lowers to
|
|
1109
|
+
# a void / no-op C++ call). Emit it as a plain statement
|
|
1110
|
+
# (which ``_is_skip_expr`` drops) and let the default-return
|
|
1111
|
+
# path below supply the function's result.
|
|
1112
|
+
if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
|
|
1113
|
+
self._visit_stmt(s, lines, indent=2)
|
|
1114
|
+
else:
|
|
1115
|
+
lines.append(f" return {self._visit_expr(s.expr)};")
|
|
1116
|
+
emitted_return = True
|
|
800
1117
|
elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
|
|
801
1118
|
# Switch/if as last statement = return expression in PineScript
|
|
802
1119
|
# Emit as: double _ret = 0; if/switch assigns _ret; return _ret;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1120
|
+
# A drawing-handle / UDT return type must brace-init its
|
|
1121
|
+
# default (``Label _func_ret = Label{};``) — falling through
|
|
1122
|
+
# to ``_default_for_type`` would emit ``0.0`` and clang would
|
|
1123
|
+
# reject ``Label _func_ret = 0.0;``.
|
|
1124
|
+
if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
|
|
1125
|
+
default_ret = f"{ret_type}{{}}"
|
|
1126
|
+
else:
|
|
1127
|
+
default_ret = self._default_for_type(ret_type)
|
|
807
1128
|
lines.append(f" {ret_type} _func_ret = {default_ret};")
|
|
808
1129
|
self._visit_if_switch_expr(s, "_func_ret", lines, indent=2)
|
|
809
1130
|
lines.append(f" return _func_ret;")
|
|
@@ -818,41 +1139,130 @@ class TopLevelEmitter:
|
|
|
818
1139
|
default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
|
|
819
1140
|
lines.append(f" return std::make_tuple({default_vals});")
|
|
820
1141
|
else:
|
|
821
|
-
|
|
822
|
-
f"{ret_type}{{}}"
|
|
823
|
-
|
|
824
|
-
|
|
1142
|
+
if ret_type in self._udt_defs or ret_type in DRAWING_TYPE_TO_CPP.values():
|
|
1143
|
+
default_ret = f"{ret_type}{{}}"
|
|
1144
|
+
else:
|
|
1145
|
+
default_ret = self._default_for_type(ret_type)
|
|
825
1146
|
lines.append(f" return {default_ret};")
|
|
826
1147
|
|
|
827
1148
|
lines.append(" }")
|
|
828
1149
|
self._current_func_param_types = {}
|
|
1150
|
+
self._current_func_param_specs = {}
|
|
829
1151
|
self._current_func_series_params = set()
|
|
830
1152
|
self._udt_param_udt = {}
|
|
831
1153
|
self._current_func_locals = prev_func_locals
|
|
1154
|
+
self._current_func_local_types = prev_func_local_types
|
|
1155
|
+
self._current_func_body = prev_func_body
|
|
1156
|
+
self._active_func_name = prev_func_name
|
|
1157
|
+
self._udt_ptr_alias_locals = prev_ptr_alias
|
|
832
1158
|
self._active_ta_remap = {}
|
|
833
1159
|
self._active_var_remap = {}
|
|
1160
|
+
self._active_fixnan_remap = {}
|
|
834
1161
|
self._in_ta_func_variant = False
|
|
835
1162
|
self._active_call_site_idx = None
|
|
1163
|
+
self._current_instance_name = None
|
|
1164
|
+
|
|
1165
|
+
def _func_var_init_flag_name(self, fname: str, call_site_idx: int | None) -> str:
|
|
1166
|
+
suffix = f"_cs{call_site_idx}" if call_site_idx is not None else ""
|
|
1167
|
+
return f"_fvinit_{self._func_safe_name(fname)}{suffix}"
|
|
1168
|
+
|
|
1169
|
+
def _emit_func_var_init_block(self, fi: FuncInfo, call_site_idx: int | None,
|
|
1170
|
+
lines: list[str], flag_override: str | None = None) -> None:
|
|
1171
|
+
"""Emit the one-shot initializer block for a function's ``var`` members.
|
|
1172
|
+
|
|
1173
|
+
Pine ``var`` declared inside a function is a function-local static:
|
|
1174
|
+
the initializer runs exactly once on the FIRST call to this variant
|
|
1175
|
+
(using the first bar's values the function actually sees) and the
|
|
1176
|
+
result persists for the strategy's lifetime. Each per-call-site clone
|
|
1177
|
+
is an independent instance with its own flag and its own set of
|
|
1178
|
+
(remapped) members.
|
|
1179
|
+
|
|
1180
|
+
This closes the gap where a function-scoped ``var line x = line.new(...)``
|
|
1181
|
+
(or any non-compile-time initializer — drawing handles, UDT ctors,
|
|
1182
|
+
arrays, runtime expressions) was declared as a default-constructed
|
|
1183
|
+
class member but its initializer was dropped, leaving the member ``na``
|
|
1184
|
+
/ uninitialised and causing "drawing access on na handle" at runtime.
|
|
1185
|
+
"""
|
|
1186
|
+
members = self.ctx.func_var_members.get(fi.name)
|
|
1187
|
+
if not members:
|
|
1188
|
+
return
|
|
1189
|
+
flag = flag_override or self._func_var_init_flag_name(fi.name, call_site_idx)
|
|
1190
|
+
# ``_active_var_remap`` is already set for this variant by the caller,
|
|
1191
|
+
# so lowering each init expression here correctly resolves references
|
|
1192
|
+
# to sibling var members (which are themselves remapped for clones).
|
|
1193
|
+
init_lines: list[str] = []
|
|
1194
|
+
for name, ptype, _init_str in members:
|
|
1195
|
+
init_ast = self.ctx.var_member_init_exprs.get(name)
|
|
1196
|
+
safe = self._safe_name(name)
|
|
1197
|
+
target = self._active_var_remap.get(safe, safe)
|
|
1198
|
+
if name in self.ctx.series_vars:
|
|
1199
|
+
if init_ast is None:
|
|
1200
|
+
continue
|
|
1201
|
+
init_cpp = self._visit_expr(init_ast)
|
|
1202
|
+
init_cpp = self._typed_na_init(init_cpp, name, ptype)
|
|
1203
|
+
init_lines.append(f" {target}.push({init_cpp});")
|
|
1204
|
+
continue
|
|
1205
|
+
if init_ast is None:
|
|
1206
|
+
# No initializer to lower (e.g. bare ``var box b``); leave the
|
|
1207
|
+
# member at its default-constructed value.
|
|
1208
|
+
continue
|
|
1209
|
+
# Skip a plain ``na`` initializer for drawing handles / UDTs whose
|
|
1210
|
+
# default-constructed member is already the na sentinel; assigning
|
|
1211
|
+
# ``na<double>()`` would not type-match the handle / struct.
|
|
1212
|
+
udt_t = self._udt_var_types.get(name)
|
|
1213
|
+
is_drawing = udt_t in DRAWING_TYPE_TO_CPP if udt_t else False
|
|
1214
|
+
is_udt = udt_t in self._udt_defs if udt_t else False
|
|
1215
|
+
from ..ast_nodes import NaLiteral
|
|
1216
|
+
if (is_drawing or is_udt) and isinstance(init_ast, NaLiteral):
|
|
1217
|
+
continue
|
|
1218
|
+
init_cpp = self._visit_expr(init_ast)
|
|
1219
|
+
init_lines.append(f" {target} = {init_cpp};")
|
|
1220
|
+
if not init_lines:
|
|
1221
|
+
return
|
|
1222
|
+
lines.append(f" if (!{flag}) {{")
|
|
1223
|
+
lines.extend(init_lines)
|
|
1224
|
+
lines.append(f" {flag} = true;")
|
|
1225
|
+
lines.append(" }")
|
|
836
1226
|
|
|
837
1227
|
def _emit_precalculate_and_run(self, lines: list[str]) -> None:
|
|
838
|
-
has_static_ta = any(
|
|
1228
|
+
has_static_ta = any(
|
|
1229
|
+
self._ta_site_uses_precalc(site)
|
|
1230
|
+
for _ti, site in enumerate(self.ctx.ta_call_sites)
|
|
1231
|
+
if _ti not in self._dead_ta_indices
|
|
1232
|
+
)
|
|
839
1233
|
if not has_static_ta:
|
|
840
1234
|
return
|
|
841
1235
|
|
|
1236
|
+
replayed_source_series: list[str] = []
|
|
1237
|
+
for stmt in self.ctx.ast.body:
|
|
1238
|
+
if not isinstance(stmt, VarDecl):
|
|
1239
|
+
continue
|
|
1240
|
+
if stmt.name not in self._global_member_vars:
|
|
1241
|
+
continue
|
|
1242
|
+
if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
|
|
1243
|
+
continue
|
|
1244
|
+
if stmt.name in self.ctx.series_vars:
|
|
1245
|
+
replayed_source_series.append(self._safe_name(stmt.name))
|
|
1246
|
+
replayed_source_series = sorted(set(replayed_source_series))
|
|
1247
|
+
|
|
842
1248
|
lines.append(" void precalculate(const Bar* bars, int n) {")
|
|
843
1249
|
lines.append(" _use_precalc = false;")
|
|
844
1250
|
lines.append(" if (n <= 0 || bars == nullptr) return;")
|
|
845
1251
|
lines.append("")
|
|
846
1252
|
|
|
847
1253
|
# Resize precalculated vectors
|
|
848
|
-
for site in self.ctx.ta_call_sites:
|
|
849
|
-
if
|
|
1254
|
+
for _ti, site in enumerate(self.ctx.ta_call_sites):
|
|
1255
|
+
if _ti in self._dead_ta_indices:
|
|
1256
|
+
continue
|
|
1257
|
+
if self._ta_site_uses_precalc(site):
|
|
850
1258
|
lines.append(f" _precalc_{site.member_name}.resize(n);")
|
|
851
1259
|
|
|
852
1260
|
# Reset indicators to clean slate
|
|
853
1261
|
lines.append("")
|
|
854
|
-
for site in self.ctx.ta_call_sites:
|
|
855
|
-
if
|
|
1262
|
+
for _ti, site in enumerate(self.ctx.ta_call_sites):
|
|
1263
|
+
if _ti in self._dead_ta_indices:
|
|
1264
|
+
continue
|
|
1265
|
+
if self._ta_site_uses_precalc(site):
|
|
856
1266
|
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
857
1267
|
safe_resolved = []
|
|
858
1268
|
for r in resolved:
|
|
@@ -863,6 +1273,13 @@ class TopLevelEmitter:
|
|
|
863
1273
|
lines.append("")
|
|
864
1274
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
865
1275
|
lines.append(f" _s_{field_name}.clear();")
|
|
1276
|
+
for safe in replayed_source_series:
|
|
1277
|
+
lines.append(f" {safe}.clear();")
|
|
1278
|
+
if self._script_has_input_source():
|
|
1279
|
+
lines.append(" _src_open_.clear(); _src_high_.clear(); _src_low_.clear();")
|
|
1280
|
+
lines.append(" _src_close_.clear(); _src_volume_.clear();")
|
|
1281
|
+
lines.append(" _src_hl2_.clear(); _src_hlc3_.clear();")
|
|
1282
|
+
lines.append(" _src_ohlc4_.clear(); _src_hlcc4_.clear();")
|
|
866
1283
|
|
|
867
1284
|
# Start precalculation loop
|
|
868
1285
|
lines.append("")
|
|
@@ -874,11 +1291,76 @@ class TopLevelEmitter:
|
|
|
874
1291
|
push_expr_bars = push_expr.replace("current_bar_.", "bars[i].")
|
|
875
1292
|
lines.append(f" _s_{field_name}.push({push_expr_bars});")
|
|
876
1293
|
|
|
1294
|
+
# Advance the native input.source() backing series (_src_open_ etc.)
|
|
1295
|
+
# from bars[i] too. A static TA site's compute args can reference an
|
|
1296
|
+
# input.source()-derived member (e.g. ``ta.stdev(bbSourceInput, 20)``)
|
|
1297
|
+
# which resolves at runtime to ``get_input_source(...)`` reading one
|
|
1298
|
+
# of these series — normally advanced once per real bar by
|
|
1299
|
+
# ``_push_source_series()`` inside ``dispatch_bar()``, which this
|
|
1300
|
+
# standalone precalc loop never calls. Without this, every static TA
|
|
1301
|
+
# site fed by an input.source() reads an empty series (0.0) for the
|
|
1302
|
+
# entire precalculation, silently corrupting its precalculated
|
|
1303
|
+
# values (e.g. a Bollinger Band's stdev collapsing to 0). Gated on
|
|
1304
|
+
# ``_src_series_active_`` to stay a no-op for scripts with no
|
|
1305
|
+
# input.source() usage; cleared before and after the precalc pass so
|
|
1306
|
+
# replayed source history cannot leak into the real run.
|
|
1307
|
+
lines.append(" if (_src_series_active_) {")
|
|
1308
|
+
lines.append(" const double _pc_o = bars[i].open;")
|
|
1309
|
+
lines.append(" const double _pc_h = bars[i].high;")
|
|
1310
|
+
lines.append(" const double _pc_l = bars[i].low;")
|
|
1311
|
+
lines.append(" const double _pc_c = bars[i].close;")
|
|
1312
|
+
lines.append(" const double _pc_v = bars[i].volume;")
|
|
1313
|
+
lines.append(" _src_open_.push(_pc_o); _src_high_.push(_pc_h); _src_low_.push(_pc_l);")
|
|
1314
|
+
lines.append(" _src_close_.push(_pc_c); _src_volume_.push(_pc_v);")
|
|
1315
|
+
lines.append(" _src_hl2_.push((_pc_h + _pc_l) / 2.0);")
|
|
1316
|
+
lines.append(" _src_hlc3_.push((_pc_h + _pc_l + _pc_c) / 3.0);")
|
|
1317
|
+
lines.append(" _src_ohlc4_.push((_pc_o + _pc_h + _pc_l + _pc_c) / 4.0);")
|
|
1318
|
+
lines.append(" _src_hlcc4_.push((_pc_h + _pc_l + _pc_c + _pc_c) / 4.0);")
|
|
1319
|
+
lines.append(" }")
|
|
1320
|
+
|
|
1321
|
+
# Replay every top-level ``X = input.source(...)`` (or bare
|
|
1322
|
+
# ``X = input(close)``) assignment. A static TA site's compute args
|
|
1323
|
+
# often don't reference ``get_input_source(...)`` inline — they
|
|
1324
|
+
# reference the top-level variable the script bound it to (e.g.
|
|
1325
|
+
# ``bbSourceInput = input.source(close, "BB Source")`` then
|
|
1326
|
+
# ``ta.stdev(bbSourceInput, 20)``). That variable is deliberately
|
|
1327
|
+
# NOT covered by the ``_inputs_initialized_`` once-only static-input
|
|
1328
|
+
# block above (see ``is_static_global_input``'s ``_is_source_input``
|
|
1329
|
+
# exclusion) because it tracks a live per-bar series, not a frozen
|
|
1330
|
+
# config value — under normal per-bar dispatch it is reassigned every
|
|
1331
|
+
# real bar. This precalc loop has no other path that reassigns it, so
|
|
1332
|
+
# without this replay every is_static site downstream of it would
|
|
1333
|
+
# keep reading its ctor-initialized 0.0 for the whole precalculation
|
|
1334
|
+
# even with the ``_src_*_`` fix above.
|
|
1335
|
+
for stmt in self.ctx.ast.body:
|
|
1336
|
+
if not isinstance(stmt, VarDecl):
|
|
1337
|
+
continue
|
|
1338
|
+
if stmt.name not in self._global_member_vars:
|
|
1339
|
+
continue
|
|
1340
|
+
if not (isinstance(stmt.value, FuncCall) and self._is_source_input(stmt.value)):
|
|
1341
|
+
continue
|
|
1342
|
+
safe = self._safe_name(stmt.name)
|
|
1343
|
+
default = self._get_input_default(stmt.value)
|
|
1344
|
+
base = self._source_defval_to_base_series(default)
|
|
1345
|
+
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
1346
|
+
cpp_val = f'get_input_source("{title}", {base})[0]'
|
|
1347
|
+
# A source var subscripted elsewhere in the script (e.g. ``src[1]``)
|
|
1348
|
+
# is declared ``Series<double>``, not a scalar double, mirroring
|
|
1349
|
+
# the normal per-bar path's ``{safe}.push({cpp_val})`` (see
|
|
1350
|
+
# ``_visit_var_decl``'s ``node.name in self.ctx.series_vars``
|
|
1351
|
+
# branch) — a plain ``=`` there is a compile error.
|
|
1352
|
+
if stmt.name in self.ctx.series_vars:
|
|
1353
|
+
lines.append(f' {safe}.push({cpp_val});')
|
|
1354
|
+
else:
|
|
1355
|
+
lines.append(f' {safe} = {cpp_val};')
|
|
1356
|
+
|
|
877
1357
|
# Set _precalc_loop_active = True
|
|
878
1358
|
self._precalc_loop_active = True
|
|
879
1359
|
try:
|
|
880
|
-
for site in self.ctx.ta_call_sites:
|
|
881
|
-
if
|
|
1360
|
+
for _ti, site in enumerate(self.ctx.ta_call_sites):
|
|
1361
|
+
if _ti in self._dead_ta_indices:
|
|
1362
|
+
continue
|
|
1363
|
+
if self._ta_site_uses_precalc(site):
|
|
882
1364
|
compute_args = self._ta_compute_args_for_site(site)
|
|
883
1365
|
compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
|
|
884
1366
|
lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
|
|
@@ -889,8 +1371,10 @@ class TopLevelEmitter:
|
|
|
889
1371
|
|
|
890
1372
|
# Reset indicators and series for the real backtest run
|
|
891
1373
|
lines.append("")
|
|
892
|
-
for site in self.ctx.ta_call_sites:
|
|
893
|
-
if
|
|
1374
|
+
for _ti, site in enumerate(self.ctx.ta_call_sites):
|
|
1375
|
+
if _ti in self._dead_ta_indices:
|
|
1376
|
+
continue
|
|
1377
|
+
if self._ta_site_uses_precalc(site):
|
|
894
1378
|
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
895
1379
|
safe_resolved = []
|
|
896
1380
|
for r in resolved:
|
|
@@ -899,6 +1383,13 @@ class TopLevelEmitter:
|
|
|
899
1383
|
|
|
900
1384
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
901
1385
|
lines.append(f" _s_{field_name}.clear();")
|
|
1386
|
+
for safe in replayed_source_series:
|
|
1387
|
+
lines.append(f" {safe}.clear();")
|
|
1388
|
+
if self._script_has_input_source():
|
|
1389
|
+
lines.append(" _src_open_.clear(); _src_high_.clear(); _src_low_.clear();")
|
|
1390
|
+
lines.append(" _src_close_.clear(); _src_volume_.clear();")
|
|
1391
|
+
lines.append(" _src_hl2_.clear(); _src_hlc3_.clear();")
|
|
1392
|
+
lines.append(" _src_ohlc4_.clear(); _src_hlcc4_.clear();")
|
|
902
1393
|
|
|
903
1394
|
lines.append("")
|
|
904
1395
|
lines.append(" _use_precalc = true;")
|