@pineforge/codegen-pyodide 0.8.1 → 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 +350 -36
- package/pineforge_codegen/analyzer/call_handlers.py +244 -133
- package/pineforge_codegen/analyzer/contracts.py +29 -0
- package/pineforge_codegen/analyzer/diagnostics.py +18 -4
- package/pineforge_codegen/analyzer/tables.py +20 -2
- package/pineforge_codegen/codegen/base.py +1147 -110
- package/pineforge_codegen/codegen/drawing.py +1 -1
- package/pineforge_codegen/codegen/emit_top.py +253 -21
- package/pineforge_codegen/codegen/input.py +17 -0
- package/pineforge_codegen/codegen/security.py +506 -35
- package/pineforge_codegen/codegen/ta.py +87 -1
- package/pineforge_codegen/codegen/tables.py +56 -7
- package/pineforge_codegen/codegen/types.py +112 -30
- package/pineforge_codegen/codegen/visit_call.py +184 -53
- package/pineforge_codegen/codegen/visit_expr.py +62 -14
- package/pineforge_codegen/codegen/visit_stmt.py +74 -36
- package/pineforge_codegen/parser.py +73 -6
- package/pineforge_codegen/signatures.py +2 -2
- package/pineforge_codegen/support_checker.py +39 -2
- package/pineforge_codegen-0.9.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +6 -3
- package/pineforge_codegen-0.8.1.tar.gz +0 -0
|
@@ -255,7 +255,7 @@ class DrawingVisitor:
|
|
|
255
255
|
vals = self._merge_drawing_args(node, ["price"])
|
|
256
256
|
price = (self._visit_expr(vals["price"]) if vals.get("price") is not None
|
|
257
257
|
else "current_bar_.close")
|
|
258
|
-
return (f"ChartPoint{{ .index=
|
|
258
|
+
return (f"ChartPoint{{ .index=(int64_t)(pine_bar_index()), "
|
|
259
259
|
f".time=(int64_t)current_bar_.timestamp, .price=({price}) }}")
|
|
260
260
|
if func_name == "from_index":
|
|
261
261
|
vals = self._merge_drawing_args(node, ["index", "price"])
|
|
@@ -118,6 +118,8 @@ class TopLevelEmitter:
|
|
|
118
118
|
lines.append("#include <string>")
|
|
119
119
|
lines.append("#include <vector>")
|
|
120
120
|
lines.append("#include <tuple>")
|
|
121
|
+
lines.append("#include <optional>")
|
|
122
|
+
lines.append("#include <type_traits>")
|
|
121
123
|
lines.append("#include <memory>")
|
|
122
124
|
lines.append("#include <mutex>")
|
|
123
125
|
lines.append("#include <unordered_map>")
|
|
@@ -192,6 +194,139 @@ class TopLevelEmitter:
|
|
|
192
194
|
return True
|
|
193
195
|
return False
|
|
194
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
|
+
|
|
195
330
|
def _typed_na_init(self, cpp_val: str, name: str, ptype) -> str:
|
|
196
331
|
"""Re-type a bare ``na<double>()`` initializer to match a non-double
|
|
197
332
|
member's C++ type. A ``var int x = na`` resolves its RHS to
|
|
@@ -212,7 +347,11 @@ class TopLevelEmitter:
|
|
|
212
347
|
def _emit_constructor(self, lines: list[str]) -> None:
|
|
213
348
|
init_parts: list[str] = []
|
|
214
349
|
# TA members with ctor args
|
|
215
|
-
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
|
|
216
355
|
if site.ctor_args:
|
|
217
356
|
# If a ctor arg is neither a compile-time literal nor expandable
|
|
218
357
|
# to an input-backed runtime expression, the old code silently
|
|
@@ -296,6 +435,9 @@ class TopLevelEmitter:
|
|
|
296
435
|
if sp.get("process_orders_on_close") is True:
|
|
297
436
|
ctor_body.append(" process_orders_on_close_ = true;")
|
|
298
437
|
|
|
438
|
+
if sp.get("calc_on_order_fills") is True:
|
|
439
|
+
ctor_body.append(" calc_on_order_fills_ = true;")
|
|
440
|
+
|
|
299
441
|
if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
|
|
300
442
|
ctor_body.append(f" initial_capital_ = {float(sp['initial_capital'])};")
|
|
301
443
|
|
|
@@ -380,6 +522,7 @@ class TopLevelEmitter:
|
|
|
380
522
|
lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
|
|
381
523
|
lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
|
|
382
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; }')
|
|
383
526
|
lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
|
|
384
527
|
lines.append(' if (key == "default_qty_type") {')
|
|
385
528
|
lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
|
|
@@ -448,14 +591,45 @@ class TopLevelEmitter:
|
|
|
448
591
|
"initial_capital": "initial_capital_",
|
|
449
592
|
}
|
|
450
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
|
+
|
|
451
610
|
def _emit_on_bar(self, lines: list[str]) -> None:
|
|
452
611
|
lines.append(" void on_bar(const Bar& bar) override {")
|
|
453
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
|
+
|
|
454
627
|
# a. Push bar field series (with bar magnifier support)
|
|
455
628
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
456
629
|
push_expr = BAR_SERIES_PUSH.get(field_name, f"current_bar_.{field_name}")
|
|
457
|
-
|
|
458
|
-
|
|
630
|
+
self._emit_history_series_write(
|
|
631
|
+
lines, " ", f"_s_{field_name}", push_expr
|
|
632
|
+
)
|
|
459
633
|
|
|
460
634
|
# a1. Push history-referenced scalar bar builtins (time[n], bar_index[n],
|
|
461
635
|
# hl2[n], …). They land in ``series_vars`` and are declared as Series
|
|
@@ -470,17 +644,16 @@ class TopLevelEmitter:
|
|
|
470
644
|
if _bname in self._var_names:
|
|
471
645
|
continue
|
|
472
646
|
_bexpr = BAR_BUILTINS.get(_bname)
|
|
473
|
-
if _bexpr is None or f"{_bname}("
|
|
647
|
+
if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
|
|
474
648
|
continue
|
|
475
649
|
_bsafe = self._safe_name(_bname)
|
|
476
|
-
|
|
477
|
-
lines.append(f" else {_bsafe}.update({_bexpr});")
|
|
650
|
+
self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
|
|
478
651
|
|
|
479
652
|
# a2. Push strategy series
|
|
480
653
|
for svar in sorted(self._strategy_series_vars):
|
|
481
654
|
member = svar.replace("_strat_", "")
|
|
482
655
|
push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
|
|
483
|
-
|
|
656
|
+
self._emit_history_series_write(lines, " ", svar, push_expr)
|
|
484
657
|
|
|
485
658
|
# b. Var init / carry-forward
|
|
486
659
|
if self.ctx.var_members:
|
|
@@ -552,7 +725,9 @@ class TopLevelEmitter:
|
|
|
552
725
|
if name in self._array_vars:
|
|
553
726
|
continue
|
|
554
727
|
if name in self.ctx.series_vars:
|
|
555
|
-
|
|
728
|
+
self._emit_history_series_write(
|
|
729
|
+
lines, " ", safe, f"{safe}[0]"
|
|
730
|
+
)
|
|
556
731
|
# Also carry-forward cloned copies for per-call-site function variants
|
|
557
732
|
carry_emitted: set[str] = set()
|
|
558
733
|
for (fname, cs_idx), remap in self._func_cs_var_remap.items():
|
|
@@ -562,7 +737,9 @@ class TopLevelEmitter:
|
|
|
562
737
|
cloned = remap[safe]
|
|
563
738
|
if cloned not in carry_emitted:
|
|
564
739
|
carry_emitted.add(cloned)
|
|
565
|
-
|
|
740
|
+
self._emit_history_series_write(
|
|
741
|
+
lines, " ", cloned, f"{cloned}[0]"
|
|
742
|
+
)
|
|
566
743
|
lines.append(" }")
|
|
567
744
|
|
|
568
745
|
# c. Push non-var series (they start fresh each bar with a push)
|
|
@@ -776,10 +953,12 @@ class TopLevelEmitter:
|
|
|
776
953
|
# Determine param types and set context for type inference inside body
|
|
777
954
|
param_strs = []
|
|
778
955
|
self._current_func_param_types = {}
|
|
956
|
+
self._current_func_param_specs = {}
|
|
779
957
|
self._current_func_series_params = set()
|
|
780
958
|
self._udt_param_udt = {}
|
|
781
959
|
func_sv = self.ctx.func_series_vars.get(fi.name, set())
|
|
782
960
|
for i, p in enumerate(node.params):
|
|
961
|
+
spec = None
|
|
783
962
|
if is_udt and i == 0 and fi.udt_type_name:
|
|
784
963
|
# A method receiver whose type is a drawing primitive
|
|
785
964
|
# (egoigor's ``method slope(line ln)``) must emit ``Line&`` not
|
|
@@ -821,6 +1000,9 @@ class TopLevelEmitter:
|
|
|
821
1000
|
cpp_t = "double"
|
|
822
1001
|
param_strs.append(f"{cpp_t} {self._safe_name(p)}")
|
|
823
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
|
|
824
1006
|
|
|
825
1007
|
# Determine return type: tuple, UDT, or scalar.
|
|
826
1008
|
# The UDT branch handles user functions whose body is ``T.new(...)``;
|
|
@@ -852,6 +1034,7 @@ class TopLevelEmitter:
|
|
|
852
1034
|
func_name = instance["name"]
|
|
853
1035
|
self._active_ta_remap = instance["ta_remap"]
|
|
854
1036
|
self._active_var_remap = instance["var_remap"]
|
|
1037
|
+
self._active_fixnan_remap = instance.get("fixnan_remap", {})
|
|
855
1038
|
self._in_ta_func_variant = True
|
|
856
1039
|
self._active_call_site_idx = None
|
|
857
1040
|
self._current_instance_name = instance["name"]
|
|
@@ -862,17 +1045,24 @@ class TopLevelEmitter:
|
|
|
862
1045
|
self._active_ta_remap = remap
|
|
863
1046
|
var_remap = self._func_cs_var_remap.get((fi.name, call_site_idx), {})
|
|
864
1047
|
self._active_var_remap = var_remap
|
|
1048
|
+
self._active_fixnan_remap = self._func_cs_fixnan_remap.get((fi.name, call_site_idx), {})
|
|
865
1049
|
self._in_ta_func_variant = True
|
|
866
1050
|
self._active_call_site_idx = call_site_idx
|
|
867
|
-
|
|
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
|
|
868
1056
|
else:
|
|
869
1057
|
self._active_ta_remap = {}
|
|
870
1058
|
self._active_var_remap = {}
|
|
1059
|
+
self._active_fixnan_remap = {}
|
|
871
1060
|
self._in_ta_func_variant = False
|
|
872
1061
|
self._active_call_site_idx = None
|
|
873
1062
|
self._current_instance_name = None
|
|
874
1063
|
|
|
875
1064
|
prev_func_locals = self._current_func_locals
|
|
1065
|
+
prev_func_local_types = self._current_func_local_types
|
|
876
1066
|
prev_func_body = getattr(self, "_current_func_body", None)
|
|
877
1067
|
prev_func_name = getattr(self, "_active_func_name", None)
|
|
878
1068
|
# The function body is the lexical scope used by the UDT-alias analysis
|
|
@@ -886,6 +1076,7 @@ class TopLevelEmitter:
|
|
|
886
1076
|
prev_ptr_alias = self._udt_ptr_alias_locals
|
|
887
1077
|
self._udt_ptr_alias_locals = set()
|
|
888
1078
|
self._current_func_locals = {n for n, _, _ in self.ctx.func_var_members.get(fi.name, [])}
|
|
1079
|
+
self._current_func_local_types = {}
|
|
889
1080
|
# Plain (non-persistent) scalar locals are emitted inline and live in
|
|
890
1081
|
# no other set; collect them so the unknown-identifier guard in
|
|
891
1082
|
# _visit_ident does not mistake them for undeclared symbols.
|
|
@@ -956,14 +1147,17 @@ class TopLevelEmitter:
|
|
|
956
1147
|
|
|
957
1148
|
lines.append(" }")
|
|
958
1149
|
self._current_func_param_types = {}
|
|
1150
|
+
self._current_func_param_specs = {}
|
|
959
1151
|
self._current_func_series_params = set()
|
|
960
1152
|
self._udt_param_udt = {}
|
|
961
1153
|
self._current_func_locals = prev_func_locals
|
|
1154
|
+
self._current_func_local_types = prev_func_local_types
|
|
962
1155
|
self._current_func_body = prev_func_body
|
|
963
1156
|
self._active_func_name = prev_func_name
|
|
964
1157
|
self._udt_ptr_alias_locals = prev_ptr_alias
|
|
965
1158
|
self._active_ta_remap = {}
|
|
966
1159
|
self._active_var_remap = {}
|
|
1160
|
+
self._active_fixnan_remap = {}
|
|
967
1161
|
self._in_ta_func_variant = False
|
|
968
1162
|
self._active_call_site_idx = None
|
|
969
1163
|
self._current_instance_name = None
|
|
@@ -1031,24 +1225,44 @@ class TopLevelEmitter:
|
|
|
1031
1225
|
lines.append(" }")
|
|
1032
1226
|
|
|
1033
1227
|
def _emit_precalculate_and_run(self, lines: list[str]) -> None:
|
|
1034
|
-
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
|
+
)
|
|
1035
1233
|
if not has_static_ta:
|
|
1036
1234
|
return
|
|
1037
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
|
+
|
|
1038
1248
|
lines.append(" void precalculate(const Bar* bars, int n) {")
|
|
1039
1249
|
lines.append(" _use_precalc = false;")
|
|
1040
1250
|
lines.append(" if (n <= 0 || bars == nullptr) return;")
|
|
1041
1251
|
lines.append("")
|
|
1042
1252
|
|
|
1043
1253
|
# Resize precalculated vectors
|
|
1044
|
-
for site in self.ctx.ta_call_sites:
|
|
1045
|
-
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):
|
|
1046
1258
|
lines.append(f" _precalc_{site.member_name}.resize(n);")
|
|
1047
1259
|
|
|
1048
1260
|
# Reset indicators to clean slate
|
|
1049
1261
|
lines.append("")
|
|
1050
|
-
for site in self.ctx.ta_call_sites:
|
|
1051
|
-
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):
|
|
1052
1266
|
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
1053
1267
|
safe_resolved = []
|
|
1054
1268
|
for r in resolved:
|
|
@@ -1059,6 +1273,13 @@ class TopLevelEmitter:
|
|
|
1059
1273
|
lines.append("")
|
|
1060
1274
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
1061
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();")
|
|
1062
1283
|
|
|
1063
1284
|
# Start precalculation loop
|
|
1064
1285
|
lines.append("")
|
|
@@ -1081,8 +1302,8 @@ class TopLevelEmitter:
|
|
|
1081
1302
|
# entire precalculation, silently corrupting its precalculated
|
|
1082
1303
|
# values (e.g. a Bollinger Band's stdev collapsing to 0). Gated on
|
|
1083
1304
|
# ``_src_series_active_`` to stay a no-op for scripts with no
|
|
1084
|
-
# input.source() usage; cleared
|
|
1085
|
-
#
|
|
1305
|
+
# input.source() usage; cleared before and after the precalc pass so
|
|
1306
|
+
# replayed source history cannot leak into the real run.
|
|
1086
1307
|
lines.append(" if (_src_series_active_) {")
|
|
1087
1308
|
lines.append(" const double _pc_o = bars[i].open;")
|
|
1088
1309
|
lines.append(" const double _pc_h = bars[i].high;")
|
|
@@ -1136,8 +1357,10 @@ class TopLevelEmitter:
|
|
|
1136
1357
|
# Set _precalc_loop_active = True
|
|
1137
1358
|
self._precalc_loop_active = True
|
|
1138
1359
|
try:
|
|
1139
|
-
for site in self.ctx.ta_call_sites:
|
|
1140
|
-
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):
|
|
1141
1364
|
compute_args = self._ta_compute_args_for_site(site)
|
|
1142
1365
|
compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
|
|
1143
1366
|
lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
|
|
@@ -1148,8 +1371,10 @@ class TopLevelEmitter:
|
|
|
1148
1371
|
|
|
1149
1372
|
# Reset indicators and series for the real backtest run
|
|
1150
1373
|
lines.append("")
|
|
1151
|
-
for site in self.ctx.ta_call_sites:
|
|
1152
|
-
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):
|
|
1153
1378
|
resolved = [self._resolve_known(a) for a in site.ctor_args]
|
|
1154
1379
|
safe_resolved = []
|
|
1155
1380
|
for r in resolved:
|
|
@@ -1158,6 +1383,13 @@ class TopLevelEmitter:
|
|
|
1158
1383
|
|
|
1159
1384
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
1160
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();")
|
|
1161
1393
|
|
|
1162
1394
|
lines.append("")
|
|
1163
1395
|
lines.append(" _use_precalc = true;")
|
|
@@ -206,6 +206,23 @@ class InputHelper:
|
|
|
206
206
|
default = self._get_input_default(node)
|
|
207
207
|
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
208
208
|
getter = self._input_type_to_getter(func_name, namespace)
|
|
209
|
+
# The generic ``input(...)`` overload is typed by its defval in Pine
|
|
210
|
+
# v6 (an int default yields an int input). The static getter table
|
|
211
|
+
# cannot see the default, so infer the getter from the default's
|
|
212
|
+
# literal type here. This matters for TA lengths: ``input(15)`` must
|
|
213
|
+
# route to ``get_input_int`` so the RMA/EMA ctor receives an int.
|
|
214
|
+
if func_name == "input" and namespace is None:
|
|
215
|
+
if isinstance(default, BoolLiteral):
|
|
216
|
+
getter = "get_input_bool"
|
|
217
|
+
elif isinstance(default, NumberLiteral):
|
|
218
|
+
if isinstance(default.value, bool):
|
|
219
|
+
getter = "get_input_bool"
|
|
220
|
+
elif isinstance(default.value, int):
|
|
221
|
+
getter = "get_input_int"
|
|
222
|
+
else:
|
|
223
|
+
getter = "get_input_double"
|
|
224
|
+
elif isinstance(default, StringLiteral):
|
|
225
|
+
getter = "get_input_string"
|
|
209
226
|
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
210
227
|
return f'{getter}("{title}", {default_cpp})'
|
|
211
228
|
|