@pineforge/codegen-pyodide 0.10.3 → 1.0.0-rc.1
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/README.md +16 -16
- package/glue.py +24 -16
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +125 -34
- package/pineforge_codegen/analyzer/__init__.py +2 -0
- package/pineforge_codegen/analyzer/base.py +767 -77
- package/pineforge_codegen/analyzer/call_handlers.py +268 -42
- package/pineforge_codegen/analyzer/contracts.py +37 -0
- package/pineforge_codegen/analyzer/diagnostics.py +30 -4
- package/pineforge_codegen/analyzer/tables.py +49 -8
- package/pineforge_codegen/analyzer/types.py +33 -1
- package/pineforge_codegen/ast_nodes.py +32 -1
- package/pineforge_codegen/block_locals.py +185 -0
- package/pineforge_codegen/builtin_keywords.py +42 -0
- package/pineforge_codegen/codegen/base.py +907 -159
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +961 -213
- package/pineforge_codegen/codegen/helpers.py +435 -14
- package/pineforge_codegen/codegen/host_members.py +162 -0
- package/pineforge_codegen/codegen/input.py +252 -85
- package/pineforge_codegen/codegen/security.py +4372 -377
- package/pineforge_codegen/codegen/session_market.py +71 -0
- package/pineforge_codegen/codegen/ta.py +1198 -101
- package/pineforge_codegen/codegen/tables.py +205 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +2055 -62
- package/pineforge_codegen/codegen/visit_call.py +929 -131
- package/pineforge_codegen/codegen/visit_expr.py +765 -63
- package/pineforge_codegen/codegen/visit_stmt.py +610 -52
- package/pineforge_codegen/external_requests.py +877 -0
- package/pineforge_codegen/lexer.py +104 -22
- package/pineforge_codegen/library_inline.py +1304 -0
- package/pineforge_codegen/library_modules.py +126 -0
- package/pineforge_codegen/library_v5.py +683 -0
- package/pineforge_codegen/limits.py +138 -0
- package/pineforge_codegen/method_binding.py +33 -0
- package/pineforge_codegen/parser.py +384 -68
- package/pineforge_codegen/pine_libraries.py +266 -0
- package/pineforge_codegen/pine_spelling.py +216 -0
- package/pineforge_codegen/pragmas.py +64 -10
- package/pineforge_codegen/security_contexts.py +1585 -0
- package/pineforge_codegen/session_reads.py +84 -0
- package/pineforge_codegen/signatures.py +48 -23
- package/pineforge_codegen/support_checker.py +1106 -85
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-1.0.0-rc.1.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +23 -21
- package/transpile.worker.mjs +24 -16
- package/pineforge_codegen-0.10.3.tar.gz +0 -0
|
@@ -82,18 +82,26 @@ and ``..analyzer``.
|
|
|
82
82
|
|
|
83
83
|
from __future__ import annotations
|
|
84
84
|
|
|
85
|
+
import re
|
|
86
|
+
|
|
85
87
|
from ..ast_nodes import (
|
|
86
|
-
ExprStmt,
|
|
88
|
+
BoolLiteral, ExprStmt, ForInStmt, ForStmt, FuncCall, Identifier, IfStmt,
|
|
89
|
+
MemberAccess, SwitchStmt, TupleAssign, TupleLiteral, VarDecl, WhileStmt,
|
|
87
90
|
)
|
|
88
91
|
from ..analyzer import FuncInfo
|
|
89
92
|
from ..symbols import PineType, method_receiver_cpp_token
|
|
90
93
|
from .tables import (
|
|
91
94
|
BAR_SERIES_PUSH,
|
|
95
|
+
TA_CHART_PREV_CLOSE_ARG,
|
|
92
96
|
DRAWING_TYPE_TO_CPP,
|
|
93
97
|
PINE_TYPE_TO_CPP,
|
|
98
|
+
PINE_V6_STRATEGY_DEFAULTS,
|
|
94
99
|
RUNTIME_REGISTER_SECURITY_EVAL_FN,
|
|
95
100
|
RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN,
|
|
96
101
|
)
|
|
102
|
+
from ..limits import iter_ast_nodes
|
|
103
|
+
from .drawing import DRAWING_LIFETIME_CPP
|
|
104
|
+
from .tv_number_format import TV_NUMBER_FORMAT_CPP
|
|
97
105
|
|
|
98
106
|
|
|
99
107
|
class TopLevelEmitter:
|
|
@@ -104,8 +112,26 @@ class TopLevelEmitter:
|
|
|
104
112
|
Mixed into ``CodeGen``; not intended to be instantiated standalone."""
|
|
105
113
|
|
|
106
114
|
def _emit_includes(self, lines: list[str]) -> None:
|
|
107
|
-
|
|
115
|
+
roots = [self.ctx.ast] + [
|
|
116
|
+
pragma.expr_node for pragma in (self.ctx.pf_trace_pragmas or [])
|
|
117
|
+
]
|
|
118
|
+
# Every syntax child (``iter_ast_nodes``): ``_walk_ast`` does not enter
|
|
119
|
+
# a tuple literal, so ``str.tostring`` in a request.security tuple
|
|
120
|
+
# payload left ``pine_str_tostring_tv`` undeclared.
|
|
121
|
+
self._uses_tv_number_format = any(
|
|
122
|
+
(namespace == "str" and func_name in {"format", "tostring"})
|
|
123
|
+
or (namespace is None and func_name == "tostring")
|
|
124
|
+
or (namespace == "log" and func_name in {"info", "warning", "error"}
|
|
125
|
+
and len(node.args) > 1)
|
|
126
|
+
for root in roots
|
|
127
|
+
for node, _depth in iter_ast_nodes(root)
|
|
128
|
+
if isinstance(node, FuncCall)
|
|
129
|
+
for func_name, namespace in [self._resolve_callee(node.callee)]
|
|
130
|
+
)
|
|
131
|
+
lines.append('#include <pineforge/source/pine_strategy_host.hpp>')
|
|
108
132
|
lines.append('#include <pineforge/ta.hpp>')
|
|
133
|
+
if self._ta_uses_dynamic_lengths():
|
|
134
|
+
lines.append('#include <pineforge/source/pine_ta_length.hpp>')
|
|
109
135
|
lines.append('#include <pineforge/math.hpp>')
|
|
110
136
|
lines.append('#include <pineforge/series.hpp>')
|
|
111
137
|
lines.append('#include <pineforge/na.hpp>')
|
|
@@ -118,10 +144,15 @@ class TopLevelEmitter:
|
|
|
118
144
|
lines.append("#include <numeric>")
|
|
119
145
|
lines.append("#include <string>")
|
|
120
146
|
lines.append("#include <vector>")
|
|
147
|
+
if self._uses_tv_number_format:
|
|
148
|
+
lines.extend(("#include <charconv>", "#include <system_error>",
|
|
149
|
+
"#include <stdexcept>"))
|
|
121
150
|
if getattr(self, "_udt_defs", {}):
|
|
122
151
|
lines.append("#include <deque>")
|
|
123
152
|
lines.append("#include <functional>")
|
|
124
153
|
lines.append("#include <limits>")
|
|
154
|
+
if self._udt_has_array_fields():
|
|
155
|
+
lines.append("#include <memory>")
|
|
125
156
|
lines.append("#include <tuple>")
|
|
126
157
|
lines.append("#include <optional>")
|
|
127
158
|
lines.append("#include <type_traits>")
|
|
@@ -173,6 +204,11 @@ class TopLevelEmitter:
|
|
|
173
204
|
# byte-identical — mirrors the matrix.hpp gating above.
|
|
174
205
|
if getattr(self, "_uses_drawing", False):
|
|
175
206
|
lines.append('#include <pineforge/drawing.hpp>')
|
|
207
|
+
lines.extend([
|
|
208
|
+
"#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1",
|
|
209
|
+
'#error "generated code requires pineforge-engine native lowering v1 (PINEFORGE_HAS_NATIVE_LOWERING_V1)"',
|
|
210
|
+
"#endif",
|
|
211
|
+
])
|
|
176
212
|
lines.append("")
|
|
177
213
|
# Compatibility shim for the namespace-wrap refactor: unqualified
|
|
178
214
|
# references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
|
|
@@ -180,33 +216,228 @@ class TopLevelEmitter:
|
|
|
180
216
|
# in favour of fully qualified names emitted at each call site.
|
|
181
217
|
lines.append("using namespace pineforge;")
|
|
182
218
|
lines.append("")
|
|
219
|
+
self._emit_ta_compat_shims(lines)
|
|
220
|
+
if getattr(self, "_uses_drawing", False):
|
|
221
|
+
lines.append(DRAWING_LIFETIME_CPP)
|
|
222
|
+
if self._uses_tv_number_format:
|
|
223
|
+
lines.append(TV_NUMBER_FORMAT_CPP)
|
|
224
|
+
lines.append("")
|
|
183
225
|
# Syminfo derivation helpers (_pf_derive_main_tickerid, _pf_derive_country)
|
|
184
226
|
from .helpers_syminfo import emit_syminfo_helpers
|
|
185
227
|
lines.extend(emit_syminfo_helpers())
|
|
186
228
|
|
|
187
|
-
def
|
|
188
|
-
"""
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return
|
|
209
|
-
|
|
229
|
+
def _emit_ta_compat_shims(self, lines: list[str]) -> None:
|
|
230
|
+
"""Emit source-level adapters for TA1 arguments and anchored forms.
|
|
231
|
+
|
|
232
|
+
The adapters are selected by feature macros exported from
|
|
233
|
+
``pineforge/ta.hpp``. This lets one generated TU compile against the
|
|
234
|
+
TA1 engine and the previous engine while keeping the Pine call-site
|
|
235
|
+
shape identical. The old branch deliberately preserves the historical
|
|
236
|
+
lowering; it is only selected when the corresponding macro is absent.
|
|
237
|
+
"""
|
|
238
|
+
classes = {
|
|
239
|
+
site.class_name for site in getattr(self.ctx, "ta_call_sites", ())
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if "_PFALMA" in classes:
|
|
243
|
+
lines.extend([
|
|
244
|
+
"#ifdef PF_ALMA_HAS_FLOOR",
|
|
245
|
+
"class _PFALMA {",
|
|
246
|
+
" ta::ALMA impl_;",
|
|
247
|
+
"public:",
|
|
248
|
+
" _PFALMA(int length, double offset, double sigma, bool floor = false)"
|
|
249
|
+
" : impl_(length, offset, sigma, floor) {}",
|
|
250
|
+
" double compute(double src) { return impl_.compute(src); }",
|
|
251
|
+
" double recompute(double src) { return impl_.recompute(src); }",
|
|
252
|
+
"};",
|
|
253
|
+
"#else",
|
|
254
|
+
"class _PFALMA {",
|
|
255
|
+
" ta::ALMA impl_;",
|
|
256
|
+
"public:",
|
|
257
|
+
" _PFALMA(int length, double offset, double sigma, bool = false)"
|
|
258
|
+
" : impl_(length, offset, sigma) {}",
|
|
259
|
+
" double compute(double src) { return impl_.compute(src); }",
|
|
260
|
+
" double recompute(double src) { return impl_.recompute(src); }",
|
|
261
|
+
"};",
|
|
262
|
+
"#endif",
|
|
263
|
+
"",
|
|
264
|
+
])
|
|
265
|
+
|
|
266
|
+
if "_PFKC" in classes:
|
|
267
|
+
lines.extend([
|
|
268
|
+
"#ifdef PF_KC_HAS_USE_TRUE_RANGE",
|
|
269
|
+
"class _PFKC {",
|
|
270
|
+
" ta::KC impl_;",
|
|
271
|
+
"public:",
|
|
272
|
+
" _PFKC(int length, double mult, bool use_true_range = true)"
|
|
273
|
+
" : impl_(length, mult, use_true_range) {}",
|
|
274
|
+
" ta::KCResult compute(double src, double high, double low, double close)"
|
|
275
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
276
|
+
" ta::KCResult recompute(double src, double high, double low, double close)"
|
|
277
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
278
|
+
"};",
|
|
279
|
+
"#else",
|
|
280
|
+
"class _PFKC {",
|
|
281
|
+
" ta::KC impl_;",
|
|
282
|
+
"public:",
|
|
283
|
+
" _PFKC(int length, double mult, bool = true) : impl_(length, mult) {}",
|
|
284
|
+
" ta::KCResult compute(double src, double high, double low, double close)"
|
|
285
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
286
|
+
" ta::KCResult recompute(double src, double high, double low, double close)"
|
|
287
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
288
|
+
"};",
|
|
289
|
+
"#endif",
|
|
290
|
+
"",
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
if "_PFKCW" in classes:
|
|
294
|
+
lines.extend([
|
|
295
|
+
"#ifdef PF_KC_HAS_USE_TRUE_RANGE",
|
|
296
|
+
"class _PFKCW {",
|
|
297
|
+
" ta::KCW impl_;",
|
|
298
|
+
"public:",
|
|
299
|
+
" _PFKCW(int length, double mult, bool use_true_range = true)"
|
|
300
|
+
" : impl_(length, mult, use_true_range) {}",
|
|
301
|
+
" double compute(double src, double high, double low, double close)"
|
|
302
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
303
|
+
" double recompute(double src, double high, double low, double close)"
|
|
304
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
305
|
+
"};",
|
|
306
|
+
"#else",
|
|
307
|
+
"class _PFKCW {",
|
|
308
|
+
" ta::KCW impl_;",
|
|
309
|
+
"public:",
|
|
310
|
+
" _PFKCW(int length, double mult, bool = true) : impl_(length, mult) {}",
|
|
311
|
+
" double compute(double src, double high, double low, double close)"
|
|
312
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.compute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
313
|
+
" double recompute(double src, double high, double low, double close)"
|
|
314
|
+
" { bool _pf_prior = ta::ema_na_warmup_flag(); ta::ema_na_warmup_flag() = true; auto _pf_out = impl_.recompute(src, high, low, close); ta::ema_na_warmup_flag() = _pf_prior; return _pf_out; }",
|
|
315
|
+
"};",
|
|
316
|
+
"#endif",
|
|
317
|
+
"",
|
|
318
|
+
])
|
|
319
|
+
|
|
320
|
+
if "_PFAnchoredVWAP" in classes:
|
|
321
|
+
lines.extend([
|
|
322
|
+
"#ifdef PF_VWAP_HAS_ANCHOR_INPUT",
|
|
323
|
+
"class _PFAnchoredVWAP {",
|
|
324
|
+
" ta::AnchoredVWAP impl_;",
|
|
325
|
+
"public:",
|
|
326
|
+
" double compute(double src, double volume, int64_t timestamp,"
|
|
327
|
+
" const std::string& tz, const std::string& session, bool anchor) {",
|
|
328
|
+
" (void)timestamp; (void)tz; (void)session;",
|
|
329
|
+
" return impl_.compute(src, volume, anchor);",
|
|
330
|
+
" }",
|
|
331
|
+
" double recompute(double src, double volume, int64_t timestamp,"
|
|
332
|
+
" const std::string& tz, const std::string& session, bool anchor) {",
|
|
333
|
+
" (void)timestamp; (void)tz; (void)session;",
|
|
334
|
+
" return impl_.recompute(src, volume, anchor);",
|
|
335
|
+
" }",
|
|
336
|
+
"};",
|
|
337
|
+
"#else",
|
|
338
|
+
"class _PFAnchoredVWAP {",
|
|
339
|
+
" ta::VWAP impl_;",
|
|
340
|
+
"public:",
|
|
341
|
+
" double compute(double src, double volume, int64_t timestamp,"
|
|
342
|
+
" const std::string& tz, const std::string& session, bool) {",
|
|
343
|
+
"#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
|
|
344
|
+
" return impl_.compute(src, volume, timestamp, tz, session);",
|
|
345
|
+
"#else",
|
|
346
|
+
" (void)tz; (void)session;",
|
|
347
|
+
" return impl_.compute(src, volume, timestamp);",
|
|
348
|
+
"#endif",
|
|
349
|
+
" }",
|
|
350
|
+
" double recompute(double src, double volume, int64_t timestamp,"
|
|
351
|
+
" const std::string& tz, const std::string& session, bool) {",
|
|
352
|
+
"#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
|
|
353
|
+
" return impl_.recompute(src, volume, timestamp, tz, session);",
|
|
354
|
+
"#else",
|
|
355
|
+
" (void)tz; (void)session;",
|
|
356
|
+
" return impl_.recompute(src, volume, timestamp);",
|
|
357
|
+
"#endif",
|
|
358
|
+
" }",
|
|
359
|
+
"};",
|
|
360
|
+
"#endif",
|
|
361
|
+
"",
|
|
362
|
+
])
|
|
363
|
+
|
|
364
|
+
if "_PFAnchoredVWAPBands" in classes:
|
|
365
|
+
lines.extend([
|
|
366
|
+
"#ifdef PF_VWAP_HAS_ANCHOR_INPUT",
|
|
367
|
+
"class _PFAnchoredVWAPBands {",
|
|
368
|
+
" ta::AnchoredVWAPBands impl_;",
|
|
369
|
+
"public:",
|
|
370
|
+
" explicit _PFAnchoredVWAPBands(double mult) : impl_(mult) {}",
|
|
371
|
+
" ta::VWAPBandsResult compute(double src, double volume, int64_t timestamp,"
|
|
372
|
+
" const std::string& tz, const std::string& session, bool anchor) {",
|
|
373
|
+
" (void)timestamp; (void)tz; (void)session;",
|
|
374
|
+
" return impl_.compute(src, volume, anchor);",
|
|
375
|
+
" }",
|
|
376
|
+
" ta::VWAPBandsResult recompute(double src, double volume, int64_t timestamp,"
|
|
377
|
+
" const std::string& tz, const std::string& session, bool anchor) {",
|
|
378
|
+
" (void)timestamp; (void)tz; (void)session;",
|
|
379
|
+
" return impl_.recompute(src, volume, anchor);",
|
|
380
|
+
" }",
|
|
381
|
+
"};",
|
|
382
|
+
"#else",
|
|
383
|
+
"class _PFAnchoredVWAPBands {",
|
|
384
|
+
" ta::VWAP impl_;",
|
|
385
|
+
" double mult_;",
|
|
386
|
+
"public:",
|
|
387
|
+
" explicit _PFAnchoredVWAPBands(double mult) : mult_(mult) {}",
|
|
388
|
+
" ta::VWAPBandsResult compute(double src, double volume, int64_t timestamp,"
|
|
389
|
+
" const std::string& tz, const std::string& session, bool) {",
|
|
390
|
+
"#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
|
|
391
|
+
" return impl_.compute_bands(src, volume, timestamp, mult_, tz, session);",
|
|
392
|
+
"#else",
|
|
393
|
+
" (void)tz; (void)session;",
|
|
394
|
+
" return impl_.compute_bands(src, volume, timestamp, mult_);",
|
|
395
|
+
"#endif",
|
|
396
|
+
" }",
|
|
397
|
+
" ta::VWAPBandsResult recompute(double src, double volume, int64_t timestamp,"
|
|
398
|
+
" const std::string& tz, const std::string& session, bool) {",
|
|
399
|
+
"#ifdef PF_VWAP_HAS_SESSION_ANCHOR",
|
|
400
|
+
" return impl_.recompute_bands(src, volume, timestamp, mult_, tz, session);",
|
|
401
|
+
"#else",
|
|
402
|
+
" (void)tz; (void)session;",
|
|
403
|
+
" return impl_.recompute_bands(src, volume, timestamp, mult_);",
|
|
404
|
+
"#endif",
|
|
405
|
+
" }",
|
|
406
|
+
"};",
|
|
407
|
+
"#endif",
|
|
408
|
+
"",
|
|
409
|
+
])
|
|
410
|
+
|
|
411
|
+
if "_PFPivotPointLevels" in classes:
|
|
412
|
+
lines.extend([
|
|
413
|
+
"#ifdef PF_PIVOT_LEVELS_HAS_ANCHOR",
|
|
414
|
+
"class _PFPivotPointLevels {",
|
|
415
|
+
" ta::PivotPointLevels impl_;",
|
|
416
|
+
"public:",
|
|
417
|
+
" std::vector<double> compute(const std::string& type, bool anchor,"
|
|
418
|
+
" bool developing, double open, double high, double low, double close) {",
|
|
419
|
+
" return impl_.compute(type, anchor, developing, open, high, low, close);",
|
|
420
|
+
" }",
|
|
421
|
+
" std::vector<double> recompute(const std::string& type, bool anchor,"
|
|
422
|
+
" bool developing, double open, double high, double low, double close) {",
|
|
423
|
+
" return impl_.recompute(type, anchor, developing, open, high, low, close);",
|
|
424
|
+
" }",
|
|
425
|
+
"};",
|
|
426
|
+
"#else",
|
|
427
|
+
"class _PFPivotPointLevels {",
|
|
428
|
+
"public:",
|
|
429
|
+
" std::vector<double> compute(const std::string& type, bool, bool,"
|
|
430
|
+
" double, double high, double low, double close) {",
|
|
431
|
+
" return ta::pivot_point_levels(type, high, low, close);",
|
|
432
|
+
" }",
|
|
433
|
+
" std::vector<double> recompute(const std::string& type, bool, bool,"
|
|
434
|
+
" double, double high, double low, double close) {",
|
|
435
|
+
" return ta::pivot_point_levels(type, high, low, close);",
|
|
436
|
+
" }",
|
|
437
|
+
"};",
|
|
438
|
+
"#endif",
|
|
439
|
+
"",
|
|
440
|
+
])
|
|
210
441
|
|
|
211
442
|
def _script_has_input_source(self) -> bool:
|
|
212
443
|
"""True if the script's AST contains an ``input.source(...)`` call.
|
|
@@ -214,7 +445,7 @@ class TopLevelEmitter:
|
|
|
214
445
|
Gates the engine's native source-series push: the runtime only
|
|
215
446
|
advances ``_src_<field>_`` (paying the per-bar cost) when
|
|
216
447
|
``_src_series_active_`` is set, which the ctor does iff this returns
|
|
217
|
-
True.
|
|
448
|
+
True. This is a history-storage requirement, not a broker policy."""
|
|
218
449
|
from ..ast_nodes import FuncCall # local to avoid circular import
|
|
219
450
|
for node in self._walk_ast(self.ctx.ast):
|
|
220
451
|
if not isinstance(node, FuncCall):
|
|
@@ -294,6 +525,7 @@ class TopLevelEmitter:
|
|
|
294
525
|
"""
|
|
295
526
|
members: list[str] = []
|
|
296
527
|
seen: set[str] = set()
|
|
528
|
+
varip_members = self._varip_state_member_names()
|
|
297
529
|
for line in declaration_lines:
|
|
298
530
|
name = self._script_state_member_name(line)
|
|
299
531
|
if name is None:
|
|
@@ -303,9 +535,109 @@ class TopLevelEmitter:
|
|
|
303
535
|
if name in seen:
|
|
304
536
|
raise AssertionError(f"duplicate generated script-state member: {name}")
|
|
305
537
|
seen.add(name)
|
|
538
|
+
if name in varip_members:
|
|
539
|
+
continue
|
|
306
540
|
members.append(name)
|
|
307
541
|
return members
|
|
308
542
|
|
|
543
|
+
def _varip_state_member_names(self) -> set[str]:
|
|
544
|
+
"""Generated members holding ``varip`` state, with their first-run
|
|
545
|
+
initialization latches.
|
|
546
|
+
|
|
547
|
+
A historical bar executes the script once, so ``varip`` keeps its
|
|
548
|
+
value exactly like ``var``, except that a calc_on_order_fills
|
|
549
|
+
recalculation rolls ``var`` back to the bar's committed state and
|
|
550
|
+
leaves ``varip`` alone (TradingView tapes, COOF on/off and with the
|
|
551
|
+
bar magnifier). The rollback checkpoint therefore skips these members;
|
|
552
|
+
a new run still resets them (``prepare_script_run``).
|
|
553
|
+
"""
|
|
554
|
+
names: set[str] = set()
|
|
555
|
+
metadata = getattr(self.ctx, "var_member_metadata_by_node", {}) or {}
|
|
556
|
+
for node_id, meta in metadata.items():
|
|
557
|
+
decl = meta[0]
|
|
558
|
+
if not (isinstance(decl, VarDecl) and decl.is_varip):
|
|
559
|
+
continue
|
|
560
|
+
base = self._safe_name(meta[1])
|
|
561
|
+
storages = {base}
|
|
562
|
+
for remap in self._func_cs_var_remap.values():
|
|
563
|
+
if base in remap:
|
|
564
|
+
storages.add(remap[base])
|
|
565
|
+
for instance in self._fresh_instances:
|
|
566
|
+
clone = (instance.get("var_remap") or {}).get(base)
|
|
567
|
+
if clone is not None:
|
|
568
|
+
storages.add(clone)
|
|
569
|
+
names |= storages
|
|
570
|
+
for storage in storages:
|
|
571
|
+
flag = self._runtime_var_init_flags.get((node_id, storage))
|
|
572
|
+
if flag is not None:
|
|
573
|
+
names.add(flag)
|
|
574
|
+
return names
|
|
575
|
+
|
|
576
|
+
def _emit_script_run_prepare(self, lines: list[str], declarations: list[str]) -> None:
|
|
577
|
+
"""Reset every declared script member, then prepare this run's cache.
|
|
578
|
+
|
|
579
|
+
Unlike per-bar rollback, a new lifecycle also invalidates precalculated
|
|
580
|
+
vectors and initialization latches. Constructor arguments are taken from
|
|
581
|
+
the constructor's own plan; declaration defaults cover every other
|
|
582
|
+
member. The only nonassignable state is the UDT undo coordinator/arenas,
|
|
583
|
+
whose explicit reset retains their stable internal pointers.
|
|
584
|
+
"""
|
|
585
|
+
constructor_values: dict[str, str] = {}
|
|
586
|
+
for initializer in self._script_constructor_initializers:
|
|
587
|
+
name, separator, arguments = initializer.partition("(")
|
|
588
|
+
if not separator or not arguments.endswith(")") or name in constructor_values:
|
|
589
|
+
raise AssertionError(f"unexpected generated constructor initializer: {initializer!r}")
|
|
590
|
+
constructor_values[name] = "(" + arguments
|
|
591
|
+
|
|
592
|
+
udt_state = set(getattr(self, "_udt_arena_member_names", {}).values())
|
|
593
|
+
if self._udt_defs:
|
|
594
|
+
udt_state.add(self._udt_undo_coordinator_member_name)
|
|
595
|
+
|
|
596
|
+
lines.extend([
|
|
597
|
+
"#ifndef PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1",
|
|
598
|
+
'#error "Generated lifecycle reset requires a matching PineForge engine; rebuild with script-run preparation support"',
|
|
599
|
+
"#endif",
|
|
600
|
+
" void prepare_script_run(const Bar* bars, int n, bool allow_precalculation) override {",
|
|
601
|
+
" _pf_script_state_checkpoint_.reset();",
|
|
602
|
+
])
|
|
603
|
+
seen: set[str] = set()
|
|
604
|
+
for declaration in declarations:
|
|
605
|
+
name = self._script_state_member_name(declaration)
|
|
606
|
+
if name is None:
|
|
607
|
+
continue
|
|
608
|
+
if name in seen:
|
|
609
|
+
raise AssertionError(f"duplicate generated lifecycle member: {name}")
|
|
610
|
+
seen.add(name)
|
|
611
|
+
if name in udt_state:
|
|
612
|
+
lines.append(f" this->{name}.reset_for_run();")
|
|
613
|
+
continue
|
|
614
|
+
if name in constructor_values:
|
|
615
|
+
value = f"decltype(this->{name}){constructor_values[name]}"
|
|
616
|
+
else:
|
|
617
|
+
text = declaration.strip()
|
|
618
|
+
match = re.search(r"(?<![A-Za-z_0-9])" + re.escape(name)
|
|
619
|
+
+ r"(?=\s*(?:[=({;]))", text)
|
|
620
|
+
if match is None:
|
|
621
|
+
raise AssertionError(f"cannot reset generated declaration: {declaration!r}")
|
|
622
|
+
suffix = text[match.end():-1].strip()
|
|
623
|
+
if not suffix:
|
|
624
|
+
value = f"decltype(this->{name}){{}}"
|
|
625
|
+
elif suffix.startswith("=") and suffix[1:].strip():
|
|
626
|
+
value = suffix[1:].strip()
|
|
627
|
+
elif ((suffix.startswith("(") and suffix.endswith(")"))
|
|
628
|
+
or (suffix.startswith("{") and suffix.endswith("}"))):
|
|
629
|
+
value = f"decltype(this->{name}){suffix}"
|
|
630
|
+
else:
|
|
631
|
+
raise AssertionError(f"unknown generated reset initializer: {declaration!r}")
|
|
632
|
+
lines.append(f" this->{name} = {value};")
|
|
633
|
+
if not set(constructor_values).issubset(seen) or not udt_state.issubset(seen):
|
|
634
|
+
raise AssertionError("generated lifecycle reset does not cover all constructor/UDT state")
|
|
635
|
+
if self._has_precalculated_ta():
|
|
636
|
+
lines.append(" if (allow_precalculation) precalculate(bars, n);")
|
|
637
|
+
else:
|
|
638
|
+
lines.append(" (void)bars; (void)n; (void)allow_precalculation;")
|
|
639
|
+
lines.append(" }")
|
|
640
|
+
|
|
309
641
|
def _emit_handle_checkpoint_traits(self, lines: list[str]) -> None:
|
|
310
642
|
"""Emit recursive rollback adapters for shared-ID collection state.
|
|
311
643
|
|
|
@@ -469,6 +801,32 @@ class TopLevelEmitter:
|
|
|
469
801
|
"",
|
|
470
802
|
])
|
|
471
803
|
|
|
804
|
+
if self._udt_has_array_fields():
|
|
805
|
+
# A UDT array field: its binding, and the contents of an array it
|
|
806
|
+
# owns (an aliased script array is checkpointed as itself).
|
|
807
|
+
lines.extend([
|
|
808
|
+
"template <typename _PFElement>",
|
|
809
|
+
f"struct {checkpoint_traits}<_PFArrayField<_PFElement>> {{",
|
|
810
|
+
f" using vector_traits = {checkpoint_traits}<std::vector<_PFElement>>;",
|
|
811
|
+
" struct snapshot_type {",
|
|
812
|
+
" std::shared_ptr<std::vector<_PFElement>> owned;",
|
|
813
|
+
" std::vector<_PFElement>* data;",
|
|
814
|
+
" typename vector_traits::snapshot_type contents;",
|
|
815
|
+
" };",
|
|
816
|
+
" static snapshot_type take(const _PFArrayField<_PFElement>& value) {",
|
|
817
|
+
" snapshot_type snapshot{value.owned(), value.data(), {}};",
|
|
818
|
+
" if (value.owned()) snapshot.contents = vector_traits::take(*value.owned());",
|
|
819
|
+
" return snapshot;",
|
|
820
|
+
" }",
|
|
821
|
+
" static void restore(_PFArrayField<_PFElement>& value,",
|
|
822
|
+
" const snapshot_type& snapshot) {",
|
|
823
|
+
" value.rebind(snapshot.owned, snapshot.data);",
|
|
824
|
+
" if (snapshot.owned) vector_traits::restore(*snapshot.owned, snapshot.contents);",
|
|
825
|
+
" }",
|
|
826
|
+
"};",
|
|
827
|
+
"",
|
|
828
|
+
])
|
|
829
|
+
|
|
472
830
|
# Snapshot backing records field-by-field. A nested UDT field is only
|
|
473
831
|
# a numeric handle, so the primary trait copies its ID without recursing
|
|
474
832
|
# into another record. This makes self/cyclic object graphs finite;
|
|
@@ -481,7 +839,8 @@ class TopLevelEmitter:
|
|
|
481
839
|
for field in fields
|
|
482
840
|
if field.name not in self._udt_omitted_fields.get(type_name, set())
|
|
483
841
|
]
|
|
484
|
-
checkpoint_fields = [field.name
|
|
842
|
+
checkpoint_fields = [self._safe_name(field.name)
|
|
843
|
+
for field in emitted_fields]
|
|
485
844
|
lines.append("template <>")
|
|
486
845
|
lines.append(f"struct {checkpoint_traits}<{record_type}> {{")
|
|
487
846
|
lines.append(" struct snapshot_type {")
|
|
@@ -640,6 +999,10 @@ class TopLevelEmitter:
|
|
|
640
999
|
# run and their ctor args (bare param names) can never be sized.
|
|
641
1000
|
if ta_idx in self._dead_ta_indices:
|
|
642
1001
|
continue
|
|
1002
|
+
if self._ta_dynamic_plan(site) is not None:
|
|
1003
|
+
# A simple or series length is read on the bar
|
|
1004
|
+
# (``_ta_dynamic_plan``): the member is default-constructed.
|
|
1005
|
+
continue
|
|
643
1006
|
if site.ctor_args:
|
|
644
1007
|
# If a ctor arg is neither a compile-time literal nor expandable
|
|
645
1008
|
# to an input-backed runtime expression, the old code silently
|
|
@@ -648,10 +1011,14 @@ class TopLevelEmitter:
|
|
|
648
1011
|
# DO expand to a runtime expr (input-backed / arithmetic-over-input,
|
|
649
1012
|
# incl. function-derived lengths) are safe: the `!_ta_initialized_`
|
|
650
1013
|
# reset overwrites the placeholder before the first compute.
|
|
651
|
-
for a in site.ctor_args:
|
|
1014
|
+
for arg_pos, a in enumerate(site.ctor_args):
|
|
652
1015
|
r = self._resolve_ta_ctor_arg(a)
|
|
1016
|
+
# An argument ``_ta_arg_takes_plan`` sends to the lowering
|
|
1017
|
+
# of ``_ta_dynamic_plan`` that it cannot spell is refused
|
|
1018
|
+
# here, as before string literals were spelled.
|
|
653
1019
|
if (not self._is_compile_time_value(r)
|
|
654
|
-
and self._runtime_ctor_arg_for_reset(a) is None
|
|
1020
|
+
and (self._runtime_ctor_arg_for_reset(a) is None
|
|
1021
|
+
or self._ta_arg_takes_plan(site, arg_pos, a))):
|
|
655
1022
|
# A TA source reached through request.security can have
|
|
656
1023
|
# several helper-bound constructor variants. Validate
|
|
657
1024
|
# only variants of this exact source node before the
|
|
@@ -663,24 +1030,39 @@ class TopLevelEmitter:
|
|
|
663
1030
|
self._collect_ta_runtime_resets(
|
|
664
1031
|
security_source_node=site.node
|
|
665
1032
|
)
|
|
666
|
-
self.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1033
|
+
if self._ta_ctor_arg_is_bool(site, arg_pos):
|
|
1034
|
+
message = (
|
|
1035
|
+
f"Unsupported TA constructor flag '{a}' for "
|
|
1036
|
+
f"{site.class_name}: it is neither a compile-time "
|
|
1037
|
+
"constant nor derived from an input, so PineForge "
|
|
1038
|
+
"cannot initialize the indicator with a stable "
|
|
1039
|
+
"per-run value."
|
|
1040
|
+
)
|
|
1041
|
+
hint = ("Use a literal, an input.*() value, or "
|
|
1042
|
+
"arithmetic over those for TA constructor "
|
|
1043
|
+
"arguments.")
|
|
1044
|
+
else:
|
|
1045
|
+
message = (
|
|
1046
|
+
f"Unsupported TA constructor length '{a}' for "
|
|
1047
|
+
f"{site.class_name}: it is neither a compile-time "
|
|
1048
|
+
"constant nor derived from an input, so PineForge "
|
|
1049
|
+
"cannot size the indicator buffer."
|
|
1050
|
+
)
|
|
1051
|
+
hint = ("Use a literal, an input.*() value, or "
|
|
1052
|
+
"arithmetic over those for TA lengths.")
|
|
1053
|
+
self._codegen_error(getattr(site, "node", None), message, hint=hint)
|
|
675
1054
|
resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
|
|
676
1055
|
# Compile-time placeholder for the init list; the runtime reset
|
|
677
1056
|
# (when the arg is input-derived) overwrites it on the first bar.
|
|
678
1057
|
safe_resolved = []
|
|
679
|
-
for r in resolved:
|
|
1058
|
+
for arg_pos, r in enumerate(resolved):
|
|
680
1059
|
if self._is_compile_time_value(r):
|
|
681
|
-
|
|
1060
|
+
rendered = r
|
|
682
1061
|
else:
|
|
683
|
-
|
|
1062
|
+
rendered = "1"
|
|
1063
|
+
if self._ta_ctor_arg_is_bool(site, arg_pos):
|
|
1064
|
+
rendered = self._ta_ctor_bool_cpp(rendered)
|
|
1065
|
+
safe_resolved.append(rendered)
|
|
684
1066
|
init_parts.append(f"{site.member_name}({', '.join(safe_resolved)})")
|
|
685
1067
|
# Security evaluator TA ctor args (skip for user function call expressions)
|
|
686
1068
|
for info in self._security_eval_info:
|
|
@@ -689,6 +1071,12 @@ class TopLevelEmitter:
|
|
|
689
1071
|
if not site.ctor_args:
|
|
690
1072
|
continue
|
|
691
1073
|
for variant in variants:
|
|
1074
|
+
if self._ta_security_plan(
|
|
1075
|
+
info["sec_id"], site, variant.get("binding_stack", ())
|
|
1076
|
+
) is not None:
|
|
1077
|
+
# Default-constructed: the length is read on the bar
|
|
1078
|
+
# (``_ta_security_plan``).
|
|
1079
|
+
continue
|
|
692
1080
|
ctor_args, _ctor_arg_stability = self._security_ta_ctor_args_for_variant(
|
|
693
1081
|
info["sec_id"],
|
|
694
1082
|
site,
|
|
@@ -696,10 +1084,11 @@ class TopLevelEmitter:
|
|
|
696
1084
|
)
|
|
697
1085
|
resolved = [self._resolve_ta_ctor_arg(a) for a in ctor_args]
|
|
698
1086
|
safe_resolved = []
|
|
699
|
-
for r in resolved:
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1087
|
+
for arg_pos, r in enumerate(resolved):
|
|
1088
|
+
rendered = r if self._is_compile_time_value(r) else "1"
|
|
1089
|
+
if self._ta_ctor_arg_is_bool(site, arg_pos):
|
|
1090
|
+
rendered = self._ta_ctor_bool_cpp(rendered)
|
|
1091
|
+
safe_resolved.append(rendered)
|
|
703
1092
|
init_parts.append(f"{variant['member_name']}({', '.join(safe_resolved)})")
|
|
704
1093
|
|
|
705
1094
|
# Non-series var members with compile-time init (deduplicate by name)
|
|
@@ -746,18 +1135,43 @@ class TopLevelEmitter:
|
|
|
746
1135
|
cpp_val = self._typed_na_init(cpp_val, name, ptype)
|
|
747
1136
|
if self._is_compile_time_value(cpp_val):
|
|
748
1137
|
init_parts.append(f"{safe}({cpp_val})")
|
|
749
|
-
#
|
|
750
|
-
|
|
1138
|
+
# This exact initializer plan also defines a cold generated lifecycle.
|
|
1139
|
+
# Engine configuration in the constructor body is deliberately excluded.
|
|
1140
|
+
self._script_constructor_initializers = tuple(init_parts)
|
|
1141
|
+
|
|
1142
|
+
# New engines default to an independent native constructor. Select
|
|
1143
|
+
# Pine compatibility before strategy_create returns and before any
|
|
1144
|
+
# host metadata setter. The adapter currently owns cap and retained-
|
|
1145
|
+
# parent priority only. Cap-only engines retain their legacy priority
|
|
1146
|
+
# default; older headers need neither hook. Always compile/link with
|
|
1147
|
+
# matching engine headers and runtime. This is not a binary ABI bridge.
|
|
1148
|
+
# This is configuration, deliberately outside script-state reset.
|
|
1149
|
+
ctor_body: list[str] = [
|
|
1150
|
+
"#if defined(PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1)",
|
|
1151
|
+
" pineforge::source::PineStrategyHost::attach_pine_execution_adapter();",
|
|
1152
|
+
"#elif defined(PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1)",
|
|
1153
|
+
" pineforge::source::PineStrategyHost::enable_pine_intraday_cap();",
|
|
1154
|
+
"#endif",
|
|
1155
|
+
" pineforge::source::PineStrategyConfig cfg{};",
|
|
1156
|
+
]
|
|
1157
|
+
# Strategy params that map to the source host's config surface.
|
|
751
1158
|
sp = self.ctx.strategy_params
|
|
752
1159
|
|
|
753
1160
|
if sp.get("process_orders_on_close") is True:
|
|
754
|
-
ctor_body.append("
|
|
1161
|
+
ctor_body.append(" cfg.process_orders_on_close = true;")
|
|
755
1162
|
|
|
756
1163
|
if sp.get("calc_on_order_fills") is True:
|
|
757
|
-
ctor_body.append("
|
|
1164
|
+
ctor_body.append(" cfg.calc_on_order_fills = true;")
|
|
758
1165
|
|
|
759
|
-
|
|
760
|
-
|
|
1166
|
+
# An omitted initial_capital / default_qty_type / default_qty_value is
|
|
1167
|
+
# TradingView's Pine v6 default, not the host's (see
|
|
1168
|
+
# PINE_V6_STRATEGY_DEFAULTS); a declared argument is emitted as before.
|
|
1169
|
+
def declared_or_v6_default(key: str):
|
|
1170
|
+
return sp[key] if key in sp else PINE_V6_STRATEGY_DEFAULTS[key]
|
|
1171
|
+
|
|
1172
|
+
initial_capital = declared_or_v6_default("initial_capital")
|
|
1173
|
+
if isinstance(initial_capital, (int, float)):
|
|
1174
|
+
ctor_body.append(f" cfg.initial_capital = {float(initial_capital)};")
|
|
761
1175
|
|
|
762
1176
|
# default_qty_type: strategy.fixed / strategy.percent_of_equity / strategy.cash
|
|
763
1177
|
qty_type_map = {
|
|
@@ -765,15 +1179,18 @@ class TopLevelEmitter:
|
|
|
765
1179
|
"strategy.percent_of_equity": "QtyType::PERCENT_OF_EQUITY",
|
|
766
1180
|
"strategy.cash": "QtyType::CASH",
|
|
767
1181
|
}
|
|
768
|
-
qty_type =
|
|
1182
|
+
qty_type = declared_or_v6_default("default_qty_type")
|
|
769
1183
|
if qty_type in qty_type_map:
|
|
770
|
-
ctor_body.append(
|
|
1184
|
+
ctor_body.append(
|
|
1185
|
+
f" cfg.default_qty_type = static_cast<int>({qty_type_map[qty_type]});"
|
|
1186
|
+
)
|
|
771
1187
|
|
|
772
|
-
|
|
773
|
-
|
|
1188
|
+
qty_value = declared_or_v6_default("default_qty_value")
|
|
1189
|
+
if isinstance(qty_value, (int, float)):
|
|
1190
|
+
ctor_body.append(f" cfg.default_qty_value = {float(qty_value)};")
|
|
774
1191
|
|
|
775
1192
|
if "pyramiding" in sp and isinstance(sp["pyramiding"], int):
|
|
776
|
-
ctor_body.append(f"
|
|
1193
|
+
ctor_body.append(f" cfg.pyramiding = {sp['pyramiding']};")
|
|
777
1194
|
|
|
778
1195
|
# commission_type: strategy.commission.percent / .cash_per_order / .cash_per_contract
|
|
779
1196
|
comm_type_map = {
|
|
@@ -783,41 +1200,35 @@ class TopLevelEmitter:
|
|
|
783
1200
|
}
|
|
784
1201
|
comm_type = sp.get("commission_type")
|
|
785
1202
|
if comm_type in comm_type_map:
|
|
786
|
-
ctor_body.append(
|
|
1203
|
+
ctor_body.append(
|
|
1204
|
+
f" cfg.commission_type = static_cast<int>({comm_type_map[comm_type]});"
|
|
1205
|
+
)
|
|
787
1206
|
|
|
788
1207
|
if "commission_value" in sp and isinstance(sp["commission_value"], (int, float)):
|
|
789
|
-
ctor_body.append(f"
|
|
1208
|
+
ctor_body.append(f" cfg.commission_value = {float(sp['commission_value'])};")
|
|
790
1209
|
|
|
791
1210
|
if "slippage" in sp and isinstance(sp["slippage"], (int, float)):
|
|
792
|
-
ctor_body.append(f"
|
|
1211
|
+
ctor_body.append(f" cfg.slippage = {int(sp['slippage'])};")
|
|
793
1212
|
|
|
794
1213
|
# margin_long / margin_short: percent of position value required as
|
|
795
1214
|
# equity (default 100 = 1x leverage). When required_margin exceeds
|
|
796
1215
|
# available equity, TV silently rejects the fill — engine mirrors
|
|
797
1216
|
# this in execute_market_entry's FLAT branch.
|
|
798
1217
|
if "margin_long" in sp and isinstance(sp["margin_long"], (int, float)):
|
|
799
|
-
ctor_body.append(f"
|
|
1218
|
+
ctor_body.append(f" cfg.margin_long = {float(sp['margin_long'])};")
|
|
800
1219
|
if "margin_short" in sp and isinstance(sp["margin_short"], (int, float)):
|
|
801
|
-
ctor_body.append(f"
|
|
1220
|
+
ctor_body.append(f" cfg.margin_short = {float(sp['margin_short'])};")
|
|
802
1221
|
|
|
803
1222
|
# close_entries_rule: "FIFO" (default) or "ANY"
|
|
804
1223
|
if sp.get("close_entries_rule") == "ANY":
|
|
805
|
-
ctor_body.append("
|
|
806
|
-
|
|
807
|
-
# Detect ``strategy.close`` / ``strategy.close_all`` calls anywhere in
|
|
808
|
-
# the script body. The runtime uses this flag in its priced-entry flip
|
|
809
|
-
# path to reproduce TradingView's empirical
|
|
810
|
-
# ``new_size = |old| + qty`` rule (see
|
|
811
|
-
# docs/codegen-gaps/validation-tv-pyramiding-override.md). The flag
|
|
812
|
-
# is set once per compilation; it is independent of how many times
|
|
813
|
-
# the close call actually fires at runtime.
|
|
814
|
-
if self._script_has_strategy_close():
|
|
815
|
-
ctor_body.append(" script_has_strategy_close_ = true;")
|
|
1224
|
+
ctor_body.append(" cfg.close_entries_rule_any = true;")
|
|
816
1225
|
|
|
817
1226
|
# Turn on native source-series history only when the script uses
|
|
818
1227
|
# input.source — otherwise the engine pays nothing per bar.
|
|
819
1228
|
if self._script_has_input_source():
|
|
820
|
-
ctor_body.append("
|
|
1229
|
+
ctor_body.append(" cfg.src_series_active = true;")
|
|
1230
|
+
|
|
1231
|
+
ctor_body.append(" configure_pine_strategy(cfg);")
|
|
821
1232
|
|
|
822
1233
|
if init_parts and ctor_body:
|
|
823
1234
|
lines.append(f" explicit GeneratedStrategy() : {', '.join(init_parts)} {{")
|
|
@@ -834,43 +1245,81 @@ class TopLevelEmitter:
|
|
|
834
1245
|
|
|
835
1246
|
lines.append("")
|
|
836
1247
|
lines.append(" void set_strategy_override(const std::string& key, const std::string& value) {")
|
|
837
|
-
lines.append(
|
|
838
|
-
lines.append(' if (key == "
|
|
839
|
-
lines.append(
|
|
840
|
-
lines.append(' if (key == "
|
|
841
|
-
lines.append(
|
|
842
|
-
lines.append(' if (key == "
|
|
843
|
-
lines.append(
|
|
844
|
-
lines.append(' if (key == "
|
|
845
|
-
lines.append(
|
|
846
|
-
lines.append('
|
|
847
|
-
lines.append(
|
|
848
|
-
lines.append('
|
|
849
|
-
lines.append(
|
|
850
|
-
lines.append(
|
|
851
|
-
lines.append('
|
|
852
|
-
lines.append('
|
|
853
|
-
lines.append('
|
|
854
|
-
lines.append('
|
|
1248
|
+
lines.append(" pineforge::source::StrategyOverrides overrides{};")
|
|
1249
|
+
lines.append(' if (key == "initial_capital") {')
|
|
1250
|
+
lines.append(" overrides.initial_capital = std::stod(value);")
|
|
1251
|
+
lines.append(' } else if (key == "commission_value") {')
|
|
1252
|
+
lines.append(" overrides.commission_value = std::stod(value);")
|
|
1253
|
+
lines.append(' } else if (key == "default_qty_value") {')
|
|
1254
|
+
lines.append(" overrides.default_qty_value = std::stod(value);")
|
|
1255
|
+
lines.append(' } else if (key == "pyramiding") {')
|
|
1256
|
+
lines.append(" overrides.pyramiding = std::stoi(value);")
|
|
1257
|
+
lines.append(' } else if (key == "slippage") {')
|
|
1258
|
+
lines.append(" overrides.slippage = std::stoi(value);")
|
|
1259
|
+
lines.append(' } else if (key == "process_orders_on_close") {')
|
|
1260
|
+
lines.append(' overrides.process_orders_on_close = (value == "true" || value == "1");')
|
|
1261
|
+
lines.append(' } else if (key == "calc_on_order_fills") {')
|
|
1262
|
+
lines.append(' overrides.calc_on_order_fills = (value == "true" || value == "1");')
|
|
1263
|
+
lines.append(' } else if (key == "close_entries_rule") {')
|
|
1264
|
+
lines.append(' overrides.close_entries_rule = (value == "ANY" || value == "any" || value == "1");')
|
|
1265
|
+
lines.append(' } else if (key == "default_qty_type") {')
|
|
1266
|
+
lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") overrides.default_qty_type = static_cast<int>(QtyType::FIXED);')
|
|
1267
|
+
lines.append(' else if (value == "percent_of_equity" || value == "strategy.percent_of_equity" || value == "1") overrides.default_qty_type = static_cast<int>(QtyType::PERCENT_OF_EQUITY);')
|
|
1268
|
+
lines.append(' else if (value == "cash" || value == "strategy.cash" || value == "2") overrides.default_qty_type = static_cast<int>(QtyType::CASH);')
|
|
1269
|
+
lines.append(" else return;")
|
|
1270
|
+
lines.append(' } else if (key == "commission_type") {')
|
|
1271
|
+
lines.append(' if (value == "percent" || value == "strategy.commission.percent" || value == "0") overrides.commission_type = static_cast<int>(CommissionType::PERCENT);')
|
|
1272
|
+
lines.append(' else if (value == "cash_per_order" || value == "strategy.commission.cash_per_order" || value == "1") overrides.commission_type = static_cast<int>(CommissionType::CASH_PER_ORDER);')
|
|
1273
|
+
lines.append(' else if (value == "cash_per_contract" || value == "strategy.commission.cash_per_contract" || value == "2") overrides.commission_type = static_cast<int>(CommissionType::CASH_PER_CONTRACT);')
|
|
1274
|
+
lines.append(" else return;")
|
|
1275
|
+
lines.append(" } else {")
|
|
855
1276
|
lines.append(" return;")
|
|
856
1277
|
lines.append(" }")
|
|
1278
|
+
lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
|
|
857
1279
|
lines.append(" }")
|
|
858
1280
|
|
|
1281
|
+
if self._uses_recorded_requests():
|
|
1282
|
+
lines.extend([
|
|
1283
|
+
"",
|
|
1284
|
+
"#ifdef PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1",
|
|
1285
|
+
" // request.earnings / dividends / splits / financial: the series the requests",
|
|
1286
|
+
" // manifest records under the request's key, TradingView's value on the chart",
|
|
1287
|
+
" // bar opening at this bar's time (na where it has no row). A key nobody",
|
|
1288
|
+
" // installed reads na here and sets the request's flag, which its reads",
|
|
1289
|
+
" // test to stop the run.",
|
|
1290
|
+
" double _pf_recorded(const std::string& key, bool& missing) const {",
|
|
1291
|
+
" missing = recorded_series_.count(key) == 0;",
|
|
1292
|
+
" return missing ? na<double>() : recorded_series_value(key);",
|
|
1293
|
+
" }",
|
|
1294
|
+
"#else",
|
|
1295
|
+
" double _pf_recorded(const std::string&, bool& missing) const {",
|
|
1296
|
+
" missing = true;",
|
|
1297
|
+
" return na<double>();",
|
|
1298
|
+
" }",
|
|
1299
|
+
"#endif",
|
|
1300
|
+
])
|
|
859
1301
|
if self._security_eval_info:
|
|
860
1302
|
lines.append("")
|
|
1303
|
+
if any(info.get("foreign") for info in self._security_eval_info):
|
|
1304
|
+
self._emit_foreign_security_lookups(lines)
|
|
861
1305
|
lines.append(" void configure_security_evaluators() override {")
|
|
862
1306
|
lines.append(" security_eval_states_.clear();")
|
|
1307
|
+
lines.extend(self._security_tf_replay_prologue())
|
|
863
1308
|
for info in self._security_eval_info:
|
|
864
1309
|
tf = info.get("tf")
|
|
865
1310
|
tf_expr = info.get("tf_expr")
|
|
866
1311
|
if tf:
|
|
867
1312
|
tf_expr = f'"{tf}"'
|
|
868
|
-
elif not tf_expr:
|
|
869
|
-
#
|
|
870
|
-
# back to the chart timeframe so registration still compiles
|
|
871
|
-
# (e.g. a request.security inside a dead-code UDF, or one
|
|
872
|
-
# whose tf is a function param called with mixed timeframes).
|
|
1313
|
+
elif tf == "" and not tf_expr:
|
|
1314
|
+
# An empty timeframe string is the chart's.
|
|
873
1315
|
tf_expr = "input_tf_"
|
|
1316
|
+
elif not tf_expr:
|
|
1317
|
+
# Every timeframe is resolved or refused
|
|
1318
|
+
# (``_resolve_security_tf``); none registers by default.
|
|
1319
|
+
self._codegen_error(
|
|
1320
|
+
info.get("tf_node"),
|
|
1321
|
+
"request.security timeframe was not resolved for registration",
|
|
1322
|
+
)
|
|
874
1323
|
if tf_expr:
|
|
875
1324
|
la = "true" if info["lookahead_on"] else "false"
|
|
876
1325
|
go = "true" if info.get("gaps_on") else "false"
|
|
@@ -889,6 +1338,8 @@ class TopLevelEmitter:
|
|
|
889
1338
|
f" {RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN}"
|
|
890
1339
|
f"({sec_id}, {tf_expr}, input_tf_);"
|
|
891
1340
|
)
|
|
1341
|
+
elif info.get("foreign"):
|
|
1342
|
+
self._emit_foreign_security_registration(info, tf_expr, la, go, lines)
|
|
892
1343
|
else:
|
|
893
1344
|
lines.append(
|
|
894
1345
|
f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
|
|
@@ -896,6 +1347,109 @@ class TopLevelEmitter:
|
|
|
896
1347
|
f"input_tf_, {la}, {go}{ha_arg});")
|
|
897
1348
|
lines.append(" }")
|
|
898
1349
|
|
|
1350
|
+
def _emit_foreign_security_lookups(self, lines: list[str]) -> None:
|
|
1351
|
+
"""The run-time lookups of another symbol's request (lane XSYM-E):
|
|
1352
|
+
whether its symbol string is the chart's, and whether its data is
|
|
1353
|
+
installed (``strategy_set_symbol_feed`` / ``_facts``). An engine
|
|
1354
|
+
without the symbol-keyed registration has neither."""
|
|
1355
|
+
lines.extend([
|
|
1356
|
+
"#ifdef PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1",
|
|
1357
|
+
" // A request of another symbol reads the chart when its symbol string is the",
|
|
1358
|
+
" // chart's: the one the requests manifest resolves it to, else the chart's",
|
|
1359
|
+
" // ticker id or ticker (syminfo.ticker reads the chart).",
|
|
1360
|
+
" bool _pf_symbol_is_chart(const std::string& symbol) const {",
|
|
1361
|
+
" const auto facts = symbol_facts_.find(symbol);",
|
|
1362
|
+
" if (facts != symbol_facts_.end() && !facts->second.canonical.empty())",
|
|
1363
|
+
" return facts->second.canonical == syminfo_.tickerid;",
|
|
1364
|
+
" return symbol == syminfo_.tickerid || symbol == syminfo_.ticker;",
|
|
1365
|
+
" }",
|
|
1366
|
+
" // Otherwise it reads the feed installed for (symbol, timeframe), in the",
|
|
1367
|
+
" // engine's timeframe spelling (\"D\" is \"1D\"), holding the named",
|
|
1368
|
+
" // column it reads (a footprint's), or the symbol's facts say it is",
|
|
1369
|
+
" // invalid (na under ignore_invalid_symbol, a stopped run without it).",
|
|
1370
|
+
" bool _pf_symbol_data_installed(const std::string& symbol,",
|
|
1371
|
+
" const std::string& timeframe,",
|
|
1372
|
+
" const char* column = nullptr) const {",
|
|
1373
|
+
" const auto facts = symbol_facts_.find(symbol);",
|
|
1374
|
+
" if (facts != symbol_facts_.end() && facts->second.valid && !*facts->second.valid)",
|
|
1375
|
+
" return true;",
|
|
1376
|
+
" std::string tf = timeframe.empty() ? script_tf_ : timeframe;",
|
|
1377
|
+
" if (tf.size() == 1 && (tf[0] == 'D' || tf[0] == 'W' || tf[0] == 'M' || tf[0] == 'S'))",
|
|
1378
|
+
" tf = \"1\" + tf;",
|
|
1379
|
+
" for (const auto& feed : symbol_feeds_) {",
|
|
1380
|
+
" if (feed.instrument != symbol || feed.tf != tf) continue;",
|
|
1381
|
+
" if (column == nullptr) return true;",
|
|
1382
|
+
" for (const auto& named : feed.columns) {",
|
|
1383
|
+
" if (named.name == column) return true;",
|
|
1384
|
+
" }",
|
|
1385
|
+
" }",
|
|
1386
|
+
" return false;",
|
|
1387
|
+
" }",
|
|
1388
|
+
" // Its payload seeds ta.ema as TradingView does in the requested context:",
|
|
1389
|
+
" // na until `length` values, then their mean (EmaSeeding::SimpleAverage).",
|
|
1390
|
+
" struct _PFForeignEmaSeeding {",
|
|
1391
|
+
" bool prior_ = ta::ema_na_warmup_flag();",
|
|
1392
|
+
" _PFForeignEmaSeeding() { ta::ema_na_warmup_flag() = true; }",
|
|
1393
|
+
" ~_PFForeignEmaSeeding() { ta::ema_na_warmup_flag() = prior_; }",
|
|
1394
|
+
" _PFForeignEmaSeeding(const _PFForeignEmaSeeding&) = delete;",
|
|
1395
|
+
" _PFForeignEmaSeeding& operator=(const _PFForeignEmaSeeding&) = delete;",
|
|
1396
|
+
" };",
|
|
1397
|
+
])
|
|
1398
|
+
if any(self._security_footprint_column(info["sec_id"]) for info in self._security_eval_info):
|
|
1399
|
+
lines.extend([
|
|
1400
|
+
" // A footprint's delta: the named column of the requested bar.",
|
|
1401
|
+
" double _pf_symbol_column(int sec_id, const char* name) const {",
|
|
1402
|
+
" return security_column_value(sec_id, name);",
|
|
1403
|
+
" }",
|
|
1404
|
+
"#else",
|
|
1405
|
+
" double _pf_symbol_column(int, const char*) const { return na<double>(); }",
|
|
1406
|
+
])
|
|
1407
|
+
lines.append("#endif")
|
|
1408
|
+
|
|
1409
|
+
def _emit_foreign_security_registration(
|
|
1410
|
+
self, info: dict, tf_expr: str, la: str, go: str, lines: list[str]) -> None:
|
|
1411
|
+
"""Register a request of another symbol by its symbol string as the
|
|
1412
|
+
run computes it before the first bar: on the chart when the string
|
|
1413
|
+
is the chart's, on the symbol's installed feed otherwise, and not at
|
|
1414
|
+
all without one -- its reads then stop the run
|
|
1415
|
+
(``_pf_sec_missing_N``), never reading the chart instead."""
|
|
1416
|
+
sec_id = info["sec_id"]
|
|
1417
|
+
symbol = self._security_tf_runtime_expr(info["symbol_node"])
|
|
1418
|
+
column = self._security_footprint_column(sec_id)
|
|
1419
|
+
column = f', "{column}"' if column else ""
|
|
1420
|
+
ignore_node = info.get("ignore_invalid_node")
|
|
1421
|
+
if ignore_node is None:
|
|
1422
|
+
ignore = "false"
|
|
1423
|
+
elif isinstance(ignore_node, BoolLiteral):
|
|
1424
|
+
ignore = "true" if ignore_node.value else "false"
|
|
1425
|
+
else:
|
|
1426
|
+
ignore = f"static_cast<bool>({self._security_tf_runtime_expr(ignore_node)})"
|
|
1427
|
+
# A footprint is read from a feed's column, which the chart's bars do
|
|
1428
|
+
# not carry: its site reads a feed even for the chart's own symbol.
|
|
1429
|
+
chart = [] if column else [
|
|
1430
|
+
" if (_pf_symbol_is_chart(_pf_symbol)) {",
|
|
1431
|
+
f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}({sec_id}, {tf_expr}, input_tf_, {la}, {go});",
|
|
1432
|
+
f" _pf_sec_missing_{sec_id} = false;",
|
|
1433
|
+
]
|
|
1434
|
+
feed = f"if (_pf_symbol_data_installed(_pf_symbol, {tf_expr}{column})) {{"
|
|
1435
|
+
lines.extend([
|
|
1436
|
+
"#ifdef PINEFORGE_HAS_SYMBOL_SECURITY_EVAL_V1",
|
|
1437
|
+
" {",
|
|
1438
|
+
f" const std::string _pf_symbol = {symbol};",
|
|
1439
|
+
*chart,
|
|
1440
|
+
(" } else " if chart else " ") + feed,
|
|
1441
|
+
f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}({sec_id}, _pf_symbol, {tf_expr}, "
|
|
1442
|
+
f"input_tf_, {la}, {go}, {ignore});",
|
|
1443
|
+
f" _pf_sec_missing_{sec_id} = false;",
|
|
1444
|
+
" } else {",
|
|
1445
|
+
f" _pf_sec_missing_{sec_id} = true;",
|
|
1446
|
+
" }",
|
|
1447
|
+
" }",
|
|
1448
|
+
"#else",
|
|
1449
|
+
f" _pf_sec_missing_{sec_id} = true;",
|
|
1450
|
+
"#endif",
|
|
1451
|
+
])
|
|
1452
|
+
|
|
899
1453
|
# Map strategy series member name to push expression
|
|
900
1454
|
_STRAT_SERIES_PUSH = {
|
|
901
1455
|
"position_size": "signed_position_size()",
|
|
@@ -930,7 +1484,7 @@ class TopLevelEmitter:
|
|
|
930
1484
|
self._lexical_udt_types = {}
|
|
931
1485
|
self._lexical_series_bindings = {}
|
|
932
1486
|
self._lexical_known_var_tombstones = set()
|
|
933
|
-
lines.append(" void
|
|
1487
|
+
lines.append(" void on_source_bar(const Bar& bar) override {")
|
|
934
1488
|
|
|
935
1489
|
# A GeneratedStrategy handle may execute multiple batch runs or
|
|
936
1490
|
# streaming lifecycles. BacktestEngine resets broker/base state, but
|
|
@@ -948,8 +1502,6 @@ class TopLevelEmitter:
|
|
|
948
1502
|
for info in self._lazy_source_clock_by_node.values():
|
|
949
1503
|
lines.append(f" {info['clock']}.reset();")
|
|
950
1504
|
lines.append(f" {info['hist']}.clear();")
|
|
951
|
-
if info["chart"] is not None:
|
|
952
|
-
lines.append(f" {info['chart']}.clear();")
|
|
953
1505
|
lines.append(" }")
|
|
954
1506
|
for info in self._lazy_source_clock_by_node.values():
|
|
955
1507
|
lines.append(f" {info['clock']}.begin_bar(bar_index_);")
|
|
@@ -959,13 +1511,6 @@ class TopLevelEmitter:
|
|
|
959
1511
|
info["hist"],
|
|
960
1512
|
f"{info['clock']}.bar_base_source",
|
|
961
1513
|
)
|
|
962
|
-
if info["chart"] is not None:
|
|
963
|
-
self._emit_history_series_write(
|
|
964
|
-
lines,
|
|
965
|
-
" ",
|
|
966
|
-
info["chart"],
|
|
967
|
-
self._visit_expr(info["chart_source"]),
|
|
968
|
-
)
|
|
969
1514
|
|
|
970
1515
|
# reset_run_state() owns engine/broker state, while these generated
|
|
971
1516
|
# Series members belong to the strategy object. Clear all of them on
|
|
@@ -985,10 +1530,12 @@ class TopLevelEmitter:
|
|
|
985
1530
|
# even when lazy control flow skips its written call on this bar. Seed
|
|
986
1531
|
# the new slot with the prior current value (``na`` before first reach);
|
|
987
1532
|
# an executed call later in the bar updates this same slot with its
|
|
988
|
-
# scalar actual.
|
|
989
|
-
#
|
|
1533
|
+
# scalar actual. A plain UDF's history of a script variable
|
|
1534
|
+
# (``fn_global_hist``) is on the same clock. Typed-method
|
|
1535
|
+
# ``series_arg`` bridges intentionally keep their existing
|
|
1536
|
+
# execution-clock behavior.
|
|
990
1537
|
for info in self._inline_history_members:
|
|
991
|
-
if info["kind"]
|
|
1538
|
+
if info["kind"] not in ("udf_series_arg", "fn_global_hist"):
|
|
992
1539
|
continue
|
|
993
1540
|
member = info["member_name"]
|
|
994
1541
|
lines.append(
|
|
@@ -1008,17 +1555,18 @@ class TopLevelEmitter:
|
|
|
1008
1555
|
# members (base.py section 6) but — unlike user series vars (pushed at
|
|
1009
1556
|
# their assignment) and bar fields (pushed above) — have no push site,
|
|
1010
1557
|
# so ``[n]`` would read an unfed buffer (the na sentinel) on every bar.
|
|
1011
|
-
# Push each from its scalar lowering.
|
|
1012
|
-
#
|
|
1013
|
-
#
|
|
1558
|
+
# Push each from its scalar lowering. ``time_close``'s Series
|
|
1559
|
+
# member is escaped (``_time_close_``) so its ``time_close()``
|
|
1560
|
+
# lowering still calls the host; a lowering that would call its
|
|
1561
|
+
# own member is skipped.
|
|
1014
1562
|
from .tables import BAR_BUILTINS
|
|
1015
1563
|
for _bname in sorted(self.ctx.series_vars):
|
|
1016
1564
|
if _bname in self._var_names:
|
|
1017
1565
|
continue
|
|
1018
1566
|
_bexpr = BAR_BUILTINS.get(_bname)
|
|
1019
|
-
if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
|
|
1020
|
-
continue
|
|
1021
1567
|
_bsafe = self._safe_name(_bname)
|
|
1568
|
+
if _bexpr is None or _bexpr.strip().startswith(f"{_bsafe}("):
|
|
1569
|
+
continue
|
|
1022
1570
|
self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
|
|
1023
1571
|
|
|
1024
1572
|
# a2. Push strategy series
|
|
@@ -1027,6 +1575,53 @@ class TopLevelEmitter:
|
|
|
1027
1575
|
push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
|
|
1028
1576
|
self._emit_history_series_write(lines, " ", svar, push_expr)
|
|
1029
1577
|
|
|
1578
|
+
# a3. Push the session.* flags the top level reads at an offset: its
|
|
1579
|
+
# history is the chart bars', whether or not the read runs on this
|
|
1580
|
+
# bar (TradingView's tapes: tests/test_e2e_session_history.py).
|
|
1581
|
+
for flag in sorted(self._session_history_flags):
|
|
1582
|
+
value = self._visit_expr(MemberAccess(object=Identifier(name="session"),
|
|
1583
|
+
member=flag))
|
|
1584
|
+
self._emit_history_series_write(
|
|
1585
|
+
lines, " ", self._session_history_member(flag), value)
|
|
1586
|
+
|
|
1587
|
+
# b0. Evaluate static global inputs once, BEFORE the first-bar ``var``
|
|
1588
|
+
# latch below: a ``var`` array / matrix / map / UDT initializer
|
|
1589
|
+
# reads input members (``array.new_bool(gridLines, false)``); built
|
|
1590
|
+
# first, it read their zero and sized an empty array. TradingView
|
|
1591
|
+
# requires constant input defaults, so the inputs read nothing the
|
|
1592
|
+
# latch builds.
|
|
1593
|
+
static_vars = []
|
|
1594
|
+
for stmt in self.ctx.ast.body:
|
|
1595
|
+
if isinstance(stmt, VarDecl):
|
|
1596
|
+
is_input = isinstance(stmt.value, FuncCall) and self._is_input_call(stmt.value)
|
|
1597
|
+
if is_input:
|
|
1598
|
+
func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
|
|
1599
|
+
is_static_global_input = (
|
|
1600
|
+
stmt.name in self._global_member_vars
|
|
1601
|
+
and not self._is_source_input(stmt.value)
|
|
1602
|
+
and stmt.name not in self._array_vars
|
|
1603
|
+
and stmt.name not in getattr(self, "_matrix_specs", {})
|
|
1604
|
+
and stmt.name not in getattr(self, "_map_vars", {})
|
|
1605
|
+
and not stmt.is_var
|
|
1606
|
+
and not stmt.is_varip
|
|
1607
|
+
)
|
|
1608
|
+
if is_static_global_input:
|
|
1609
|
+
safe = self._safe_name(stmt.name)
|
|
1610
|
+
default = self._get_input_default(stmt.value)
|
|
1611
|
+
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
1612
|
+
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
1613
|
+
getter = self._input_getter_for_call(stmt.value, func_name_i, namespace_i)
|
|
1614
|
+
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
1615
|
+
cpp_val = f'{getter}({self._input_key_literal(title)}, {default_cpp})'
|
|
1616
|
+
static_vars.append(f"{safe} = {cpp_val};")
|
|
1617
|
+
|
|
1618
|
+
if static_vars:
|
|
1619
|
+
lines.append(" if (!_inputs_initialized_) {")
|
|
1620
|
+
for var_expr in static_vars:
|
|
1621
|
+
lines.append(f" {var_expr}")
|
|
1622
|
+
lines.append(" _inputs_initialized_ = true;")
|
|
1623
|
+
lines.append(" }")
|
|
1624
|
+
|
|
1030
1625
|
# b. Var init / carry-forward
|
|
1031
1626
|
if self.ctx.var_members:
|
|
1032
1627
|
lines.append(" if (!_var_initialized) {")
|
|
@@ -1035,6 +1630,11 @@ class TopLevelEmitter:
|
|
|
1035
1630
|
# declaration statements, not in the global on_bar preamble.
|
|
1036
1631
|
if name in getattr(self, "_func_local_var_names", ()):
|
|
1037
1632
|
continue
|
|
1633
|
+
# A wide int array member (``std::vector<int64_t>``, see
|
|
1634
|
+
# ``_wide_int_array_names``) needs its first-bar constructor
|
|
1635
|
+
# spelled with the same element type: name the target for
|
|
1636
|
+
# visit_call's ``array.new_*`` / ``array.from`` lowering.
|
|
1637
|
+
self._array_ctor_target_name = name
|
|
1038
1638
|
safe = self._safe_name(name)
|
|
1039
1639
|
runtime_info = self._runtime_scalar_var_init_by_member.get(name)
|
|
1040
1640
|
if (runtime_info is not None
|
|
@@ -1100,6 +1700,13 @@ class TopLevelEmitter:
|
|
|
1100
1700
|
break
|
|
1101
1701
|
continue
|
|
1102
1702
|
if name in self._runtime_scalar_var_init_members:
|
|
1703
|
+
if runtime_info is not None and runtime_info.get("is_series"):
|
|
1704
|
+
# A history-read var whose initializer reads what the
|
|
1705
|
+
# body computes (``_series_var_init_keeps_preamble``):
|
|
1706
|
+
# the first bar's slot holds its na until the
|
|
1707
|
+
# declaration replaces it (``_visit_var_decl``).
|
|
1708
|
+
na_cpp = self._typed_na_init("na<double>()", name, ptype)
|
|
1709
|
+
lines.append(f" {safe}.push({na_cpp});")
|
|
1103
1710
|
continue
|
|
1104
1711
|
# Persistent authored UDT variables initialize from their full
|
|
1105
1712
|
# source RHS, not only from ``Type.new``. In particular,
|
|
@@ -1131,6 +1738,7 @@ class TopLevelEmitter:
|
|
|
1131
1738
|
if cloned not in init_emitted:
|
|
1132
1739
|
init_emitted.add(cloned)
|
|
1133
1740
|
lines.append(f" {cloned}.push({cpp_val});")
|
|
1741
|
+
self._array_ctor_target_name = None
|
|
1134
1742
|
lines.append(" _var_initialized = true;")
|
|
1135
1743
|
lines.append(" } else {")
|
|
1136
1744
|
for name, _, _ in self.ctx.var_members:
|
|
@@ -1189,39 +1797,6 @@ class TopLevelEmitter:
|
|
|
1189
1797
|
# c. Push non-var series (they start fresh each bar with a push)
|
|
1190
1798
|
# (actual push happens in visit_VarDecl when the decl is visited)
|
|
1191
1799
|
|
|
1192
|
-
# c3. Evaluate static global inputs and variables once
|
|
1193
|
-
static_vars = []
|
|
1194
|
-
for stmt in self.ctx.ast.body:
|
|
1195
|
-
if isinstance(stmt, VarDecl):
|
|
1196
|
-
is_input = isinstance(stmt.value, FuncCall) and self._is_input_call(stmt.value)
|
|
1197
|
-
if is_input:
|
|
1198
|
-
func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
|
|
1199
|
-
is_static_global_input = (
|
|
1200
|
-
stmt.name in self._global_member_vars
|
|
1201
|
-
and not self._is_source_input(stmt.value)
|
|
1202
|
-
and stmt.name not in self._array_vars
|
|
1203
|
-
and stmt.name not in getattr(self, "_matrix_specs", {})
|
|
1204
|
-
and stmt.name not in getattr(self, "_map_vars", {})
|
|
1205
|
-
and not stmt.is_var
|
|
1206
|
-
and not stmt.is_varip
|
|
1207
|
-
)
|
|
1208
|
-
if is_static_global_input:
|
|
1209
|
-
safe = self._safe_name(stmt.name)
|
|
1210
|
-
default = self._get_input_default(stmt.value)
|
|
1211
|
-
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
1212
|
-
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
1213
|
-
getter = self._input_type_to_getter(func_name_i, namespace_i)
|
|
1214
|
-
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
1215
|
-
cpp_val = f'{getter}("{title}", {default_cpp})'
|
|
1216
|
-
static_vars.append(f"{safe} = {cpp_val};")
|
|
1217
|
-
|
|
1218
|
-
if static_vars:
|
|
1219
|
-
lines.append(" if (!_inputs_initialized_) {")
|
|
1220
|
-
for var_expr in static_vars:
|
|
1221
|
-
lines.append(f" {var_expr}")
|
|
1222
|
-
lines.append(" _inputs_initialized_ = true;")
|
|
1223
|
-
lines.append(" }")
|
|
1224
|
-
|
|
1225
1800
|
# c2. First-bar TA resize: rebuild any TA object whose ctor args come
|
|
1226
1801
|
# from input-backed variables so strategy_set_input() actually changes
|
|
1227
1802
|
# the circular-buffer sizes. Emits nothing when no TA site depends on
|
|
@@ -1230,9 +1805,11 @@ class TopLevelEmitter:
|
|
|
1230
1805
|
|
|
1231
1806
|
# d. Visit each statement. A stateful ``ta.*`` site below a lazy
|
|
1232
1807
|
# ``and``/``or`` RHS or ternary arm of a top-level statement is
|
|
1233
|
-
# evaluated every bar BEFORE the statement
|
|
1808
|
+
# evaluated every bar BEFORE the statement, and so is a pure user
|
|
1809
|
+
# call read at an offset there (TV rules, see ``ta.py``).
|
|
1234
1810
|
for stmt in self.ctx.ast.body:
|
|
1235
1811
|
self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
|
|
1812
|
+
self._emit_lazy_call_history_hoists(stmt, lines, indent=2)
|
|
1236
1813
|
try:
|
|
1237
1814
|
self._visit_stmt(stmt, lines, indent=2)
|
|
1238
1815
|
finally:
|
|
@@ -1373,9 +1950,37 @@ class TopLevelEmitter:
|
|
|
1373
1950
|
lines.append(" if (!s) return;")
|
|
1374
1951
|
lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
|
|
1375
1952
|
lines.append(" }")
|
|
1953
|
+
if self._declares_bar_magnifier():
|
|
1954
|
+
# TradingView runs a script that declares use_bar_magnifier = true
|
|
1955
|
+
# on its bar magnifier; the host reads this export to run it on
|
|
1956
|
+
# intrabars with the magnifier on (the run parameters are the
|
|
1957
|
+
# host's: bar_magnifier, a finer input_tf and its feed).
|
|
1958
|
+
lines.append(" int strategy_declares_bar_magnifier(void) {")
|
|
1959
|
+
lines.append(" return 1;")
|
|
1960
|
+
lines.append(" }")
|
|
1376
1961
|
lines.append("}")
|
|
1377
1962
|
lines.append("")
|
|
1378
1963
|
|
|
1964
|
+
def _declares_bar_magnifier(self) -> bool:
|
|
1965
|
+
"""``strategy(use_bar_magnifier = true)``. The argument is a const
|
|
1966
|
+
bool; one that is not a literal cannot be read here, so the TU does
|
|
1967
|
+
not declare the magnifier and the codegen warns."""
|
|
1968
|
+
from ..ast_nodes import BoolLiteral, StrategyDecl
|
|
1969
|
+
for node in self._walk_ast(self.ctx.ast):
|
|
1970
|
+
if not isinstance(node, StrategyDecl):
|
|
1971
|
+
continue
|
|
1972
|
+
value = node.kwargs.get("use_bar_magnifier")
|
|
1973
|
+
if value is None or isinstance(value, BoolLiteral):
|
|
1974
|
+
return value is not None and value.value is True
|
|
1975
|
+
self._codegen_warning(
|
|
1976
|
+
value,
|
|
1977
|
+
"strategy(use_bar_magnifier=...) is not a literal bool: the "
|
|
1978
|
+
"generated strategy does not declare the bar magnifier, so a host "
|
|
1979
|
+
"runs it without one.",
|
|
1980
|
+
hint="Write use_bar_magnifier = true or false.")
|
|
1981
|
+
return False
|
|
1982
|
+
return False
|
|
1983
|
+
|
|
1379
1984
|
def _emit_udt_method_cpp_name(self, fi: FuncInfo) -> str:
|
|
1380
1985
|
"""Stable C++ identifier for a typed instance method."""
|
|
1381
1986
|
receiver_spec = (
|
|
@@ -1392,6 +1997,22 @@ class TopLevelEmitter:
|
|
|
1392
1997
|
|
|
1393
1998
|
def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
|
|
1394
1999
|
instance: dict | None = None) -> None:
|
|
2000
|
+
"""Emit a user-defined function as a class method, under v5's rules
|
|
2001
|
+
when it was inlined from a v5 library (``library_v5``)."""
|
|
2002
|
+
node = fi.node
|
|
2003
|
+
previous = self._pine_v5_body
|
|
2004
|
+
self._pine_v5_body = (
|
|
2005
|
+
node is not None
|
|
2006
|
+
and (getattr(node, "annotations", None) or {}).get("pine_version") == 5
|
|
2007
|
+
)
|
|
2008
|
+
try:
|
|
2009
|
+
self._emit_func_def_body(fi, lines, call_site_idx, instance)
|
|
2010
|
+
finally:
|
|
2011
|
+
self._pine_v5_body = previous
|
|
2012
|
+
|
|
2013
|
+
def _emit_func_def_body(self, fi: FuncInfo, lines: list[str],
|
|
2014
|
+
call_site_idx: int | None = None,
|
|
2015
|
+
instance: dict | None = None) -> None:
|
|
1395
2016
|
"""Emit a user-defined function as a class method.
|
|
1396
2017
|
|
|
1397
2018
|
If call_site_idx is not None, emit a per-call-site variant with
|
|
@@ -1405,6 +2026,12 @@ class TopLevelEmitter:
|
|
|
1405
2026
|
node = fi.node
|
|
1406
2027
|
if node is None:
|
|
1407
2028
|
return
|
|
2029
|
+
# The written call whose argument types type this emission: the
|
|
2030
|
+
# variant's own, or the one a fresh nested instance runs.
|
|
2031
|
+
type_call_site_idx = (
|
|
2032
|
+
call_site_idx if call_site_idx is not None
|
|
2033
|
+
else (instance or {}).get("type_call_site_idx")
|
|
2034
|
+
)
|
|
1408
2035
|
|
|
1409
2036
|
# Collection registries historically used raw variable names for the
|
|
1410
2037
|
# whole translation unit. Emit each callable against copy-on-write
|
|
@@ -1458,9 +2085,9 @@ class TopLevelEmitter:
|
|
|
1458
2085
|
)
|
|
1459
2086
|
variant_param_types = (
|
|
1460
2087
|
getattr(self.ctx, "func_callsite_param_types", {}).get(
|
|
1461
|
-
(fi.name,
|
|
2088
|
+
(fi.name, type_call_site_idx), ()
|
|
1462
2089
|
)
|
|
1463
|
-
if
|
|
2090
|
+
if type_call_site_idx is not None
|
|
1464
2091
|
else ()
|
|
1465
2092
|
)
|
|
1466
2093
|
for i, p in enumerate(node.params):
|
|
@@ -1569,7 +2196,7 @@ class TopLevelEmitter:
|
|
|
1569
2196
|
PineType.FLOAT: "double",
|
|
1570
2197
|
PineType.BOOL: "bool",
|
|
1571
2198
|
PineType.STRING: "std::string",
|
|
1572
|
-
PineType.COLOR: "
|
|
2199
|
+
PineType.COLOR: "int64_t",
|
|
1573
2200
|
}[variant_pt]
|
|
1574
2201
|
elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
|
|
1575
2202
|
# Precise per-param TypeSpec (declared hint or call-site inference):
|
|
@@ -1578,7 +2205,8 @@ class TopLevelEmitter:
|
|
|
1578
2205
|
# a string -> ``std::string``. User UDT field mutation propagates
|
|
1579
2206
|
# through the arena even though the handle parameter is by value.
|
|
1580
2207
|
spec = fi.param_type_specs[i]
|
|
1581
|
-
cpp_t = self.
|
|
2208
|
+
cpp_t = self._wide_declared_int_param(
|
|
2209
|
+
fi, i, self._type_spec_to_cpp(spec))
|
|
1582
2210
|
if spec.kind == "udt":
|
|
1583
2211
|
self._udt_param_udt[p] = spec.name
|
|
1584
2212
|
self._udt_param_udt[self._safe_name(p)] = spec.name
|
|
@@ -1593,7 +2221,8 @@ class TopLevelEmitter:
|
|
|
1593
2221
|
cpp_t = f"{cpp_t}&"
|
|
1594
2222
|
elif i < len(fi.param_types):
|
|
1595
2223
|
pt = fi.param_types[i]
|
|
1596
|
-
cpp_t =
|
|
2224
|
+
cpp_t = self._wide_declared_int_param(
|
|
2225
|
+
fi, i, PINE_TYPE_TO_CPP.get(pt, "double"))
|
|
1597
2226
|
else:
|
|
1598
2227
|
cpp_t = "double"
|
|
1599
2228
|
param_strs.append(f"{cpp_t} {self._safe_name(p)}")
|
|
@@ -1607,14 +2236,27 @@ class TopLevelEmitter:
|
|
|
1607
2236
|
# without it the function would be emitted as returning ``double`` and
|
|
1608
2237
|
# clang errors with "no viable conversion from T to double". Probe:
|
|
1609
2238
|
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
1610
|
-
|
|
2239
|
+
return_udt_name = getattr(fi, "udt_return_type", None)
|
|
2240
|
+
return_udt = bool(return_udt_name and return_udt_name in self._udt_defs)
|
|
2241
|
+
request_struct = self._security_helper_request_struct(node)
|
|
2242
|
+
if request_struct is not None:
|
|
2243
|
+
# A helper whose value is a request of a TA tuple
|
|
2244
|
+
# (``request.security(..., ta.macd(...))``) returns the request's
|
|
2245
|
+
# stored result struct, which ``[m, s, h] = htf()`` decomposes.
|
|
2246
|
+
ret_type = request_struct
|
|
2247
|
+
elif fi.returns_tuple:
|
|
1611
2248
|
# Infer actual tuple element types from function body's last expression
|
|
1612
2249
|
tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
|
|
1613
2250
|
ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
|
|
1614
|
-
elif
|
|
2251
|
+
elif return_udt_name:
|
|
1615
2252
|
# A function returning a drawing handle must emit the C++ handle
|
|
1616
2253
|
# struct (Line/Box/Label/Linefill), not the unknown lowercase name.
|
|
1617
|
-
ret_type = DRAWING_TYPE_TO_CPP.get(
|
|
2254
|
+
ret_type = DRAWING_TYPE_TO_CPP.get(
|
|
2255
|
+
return_udt_name,
|
|
2256
|
+
self._safe_name(return_udt_name)
|
|
2257
|
+
if return_udt
|
|
2258
|
+
else return_udt_name,
|
|
2259
|
+
)
|
|
1618
2260
|
elif self._func_int_return_uses_wide_history(
|
|
1619
2261
|
fi, call_site_idx=call_site_idx
|
|
1620
2262
|
):
|
|
@@ -1622,14 +2264,14 @@ class TopLevelEmitter:
|
|
|
1622
2264
|
# values and their na sentinel cannot narrow at the return edge.
|
|
1623
2265
|
ret_type = "int64_t"
|
|
1624
2266
|
elif (
|
|
1625
|
-
|
|
2267
|
+
type_call_site_idx is not None
|
|
1626
2268
|
and self._callsite_callable_return_pine_type(
|
|
1627
|
-
fi,
|
|
2269
|
+
fi, type_call_site_idx
|
|
1628
2270
|
) != PineType.UNKNOWN
|
|
1629
2271
|
):
|
|
1630
2272
|
ret_type = PINE_TYPE_TO_CPP.get(
|
|
1631
2273
|
self._callsite_callable_return_pine_type(
|
|
1632
|
-
fi,
|
|
2274
|
+
fi, type_call_site_idx
|
|
1633
2275
|
),
|
|
1634
2276
|
"double",
|
|
1635
2277
|
)
|
|
@@ -1639,13 +2281,16 @@ class TopLevelEmitter:
|
|
|
1639
2281
|
ret_type = self._type_spec_to_cpp(fi.return_type_spec)
|
|
1640
2282
|
else:
|
|
1641
2283
|
ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2284
|
+
if return_udt:
|
|
2285
|
+
# Keep the authored UDT spelling here for target typing; the
|
|
2286
|
+
# expression visitor converts it to the escaped C++ handle name
|
|
2287
|
+
# at the point it emits ``T{}``.
|
|
2288
|
+
rhs_return_cpp_type = return_udt_name
|
|
2289
|
+
elif (self._is_nullable_collection_cpp_type(ret_type)
|
|
2290
|
+
or ret_type in DRAWING_TYPE_TO_CPP.values()):
|
|
2291
|
+
rhs_return_cpp_type = ret_type
|
|
2292
|
+
else:
|
|
2293
|
+
rhs_return_cpp_type = None
|
|
1649
2294
|
|
|
1650
2295
|
# For per-call-site variants, suffix the function name and activate TA + var remapping
|
|
1651
2296
|
func_name = (
|
|
@@ -1742,22 +2387,45 @@ class TopLevelEmitter:
|
|
|
1742
2387
|
self._current_func_locals |= self._collect_binding_names(node.body)
|
|
1743
2388
|
|
|
1744
2389
|
lines.append(f" {ret_type} {func_name}({', '.join(param_strs)}) {{")
|
|
2390
|
+
self._emit_function_global_history_updates(fi, lines)
|
|
2391
|
+
|
|
2392
|
+
# A session.* flag the body reads at an offset: its history is this
|
|
2393
|
+
# call site's calls, so push the flag once per call, before a lazy
|
|
2394
|
+
# operand or a block can skip the read (TradingView's function tape,
|
|
2395
|
+
# tests/test_e2e_session_history.py).
|
|
2396
|
+
for flag in sorted(self._session_call_flags.get(fi.name, ())):
|
|
2397
|
+
member = self._inline_history_member_by_key.get(
|
|
2398
|
+
("session_call", fi.name, flag, self._current_instance_name))
|
|
2399
|
+
if member is not None:
|
|
2400
|
+
value = self._visit_expr(MemberAccess(object=Identifier(name="session"),
|
|
2401
|
+
member=flag))
|
|
2402
|
+
self._emit_history_series_write(lines, " ", member, value)
|
|
1745
2403
|
|
|
1746
2404
|
emitted_return = False
|
|
2405
|
+
# A pure call read at an offset below a lazy edge runs once per call
|
|
2406
|
+
# of this function, before its statement (``ta.py``).
|
|
2407
|
+
hoisted: list[int] = []
|
|
1747
2408
|
if node.is_single_expr and node.body:
|
|
2409
|
+
hoisted = self._emit_lazy_call_history_hoists(node.body[0], lines, indent=2)
|
|
1748
2410
|
expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
|
|
1749
2411
|
if expr and self._call_is_void(expr):
|
|
1750
2412
|
# void setter as the sole body expr — emit as statement, fall
|
|
1751
2413
|
# through to the default return.
|
|
1752
2414
|
self._visit_stmt(node.body[0], lines, indent=2)
|
|
1753
2415
|
elif expr:
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2416
|
+
ret_cpp = self._coerce_int_slot(
|
|
2417
|
+
self._visit_rhs_value(
|
|
2418
|
+
expr, target_cpp_type=rhs_return_cpp_type
|
|
2419
|
+
),
|
|
2420
|
+
expr,
|
|
2421
|
+
self._int_slot_cpp_type(None, ret_type),
|
|
1757
2422
|
)
|
|
2423
|
+
lines.append(f" return {ret_cpp};")
|
|
1758
2424
|
emitted_return = True
|
|
1759
2425
|
else:
|
|
1760
2426
|
for i, s in enumerate(node.body):
|
|
2427
|
+
self._clear_lazy_call_history_hoists(hoisted)
|
|
2428
|
+
hoisted = self._emit_lazy_call_history_hoists(s, lines, indent=2)
|
|
1761
2429
|
if i == len(node.body) - 1 and isinstance(s, ExprStmt):
|
|
1762
2430
|
# A void drawing setter / delete / visual-noop, or a dropped
|
|
1763
2431
|
# table/polyline method call (``panel.cell(...)``), used as
|
|
@@ -1768,10 +2436,14 @@ class TopLevelEmitter:
|
|
|
1768
2436
|
if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
|
|
1769
2437
|
self._visit_stmt(s, lines, indent=2)
|
|
1770
2438
|
else:
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2439
|
+
ret_cpp = self._coerce_int_slot(
|
|
2440
|
+
self._visit_rhs_value(
|
|
2441
|
+
s.expr, target_cpp_type=rhs_return_cpp_type
|
|
2442
|
+
),
|
|
2443
|
+
s.expr,
|
|
2444
|
+
self._int_slot_cpp_type(None, ret_type),
|
|
1774
2445
|
)
|
|
2446
|
+
lines.append(f" return {ret_cpp};")
|
|
1775
2447
|
emitted_return = True
|
|
1776
2448
|
elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
|
|
1777
2449
|
# Switch/if as last statement = return expression in PineScript
|
|
@@ -1780,31 +2452,92 @@ class TopLevelEmitter:
|
|
|
1780
2452
|
# default (``Label _func_ret = Label{};``) — falling through
|
|
1781
2453
|
# to ``_default_for_type`` would emit ``0.0`` and clang would
|
|
1782
2454
|
# reject ``Label _func_ret = 0.0;``.
|
|
1783
|
-
if
|
|
2455
|
+
if return_udt or ret_type in DRAWING_TYPE_TO_CPP.values():
|
|
1784
2456
|
default_ret = f"{ret_type}{{}}"
|
|
2457
|
+
elif fi.returns_tuple:
|
|
2458
|
+
default_ret = self._tuple_default_expr(tuple_types_list)
|
|
1785
2459
|
else:
|
|
1786
2460
|
default_ret = self._default_for_type(ret_type)
|
|
1787
2461
|
lines.append(f" {ret_type} _func_ret = {default_ret};")
|
|
1788
|
-
self.
|
|
2462
|
+
self._visit_selection_value(
|
|
2463
|
+
s,
|
|
2464
|
+
"_func_ret",
|
|
2465
|
+
lines,
|
|
2466
|
+
indent=2,
|
|
2467
|
+
target_cpp_type=(
|
|
2468
|
+
rhs_return_cpp_type
|
|
2469
|
+
if rhs_return_cpp_type is not None
|
|
2470
|
+
else self._int_slot_cpp_type(None, ret_type)
|
|
2471
|
+
),
|
|
2472
|
+
slot_cpp_type=None if fi.returns_tuple else ret_type,
|
|
2473
|
+
)
|
|
2474
|
+
lines.append(" return _func_ret;")
|
|
2475
|
+
emitted_return = True
|
|
2476
|
+
elif (i == len(node.body) - 1 and isinstance(s, TupleAssign)
|
|
2477
|
+
and fi.returns_tuple and "_" not in s.names):
|
|
2478
|
+
# ``[p, q] = pair(v)`` last returns the declared tuple.
|
|
2479
|
+
self._visit_stmt(s, lines, indent=2)
|
|
2480
|
+
declared = TupleLiteral(
|
|
2481
|
+
elements=[Identifier(name=name) for name in s.names]
|
|
2482
|
+
)
|
|
2483
|
+
declared.loc = s.loc
|
|
2484
|
+
for element in declared.elements:
|
|
2485
|
+
element.loc = s.loc
|
|
2486
|
+
lines.append(
|
|
2487
|
+
f" return {self._visit_rhs_value(declared, target_cpp_type=rhs_return_cpp_type)};"
|
|
2488
|
+
)
|
|
2489
|
+
emitted_return = True
|
|
2490
|
+
elif (i == len(node.body) - 1
|
|
2491
|
+
and self._statement_value_node(s) is not None
|
|
2492
|
+
and self._tail_value_fits(s, ret_type)):
|
|
2493
|
+
# ``_f := expr`` / ``y += v`` / ``b = a * 3`` last: the
|
|
2494
|
+
# function returns the value the statement leaves. It
|
|
2495
|
+
# used to fall through to the default return, so
|
|
2496
|
+
# cs-lev-tradleware's Gaussian filter was always 0.0.
|
|
2497
|
+
self._visit_stmt(s, lines, indent=2)
|
|
2498
|
+
value = self._statement_value_node(s)
|
|
2499
|
+
ret_cpp = self._coerce_int_slot(
|
|
2500
|
+
self._visit_rhs_value(
|
|
2501
|
+
value, target_cpp_type=rhs_return_cpp_type
|
|
2502
|
+
),
|
|
2503
|
+
value,
|
|
2504
|
+
self._int_slot_cpp_type(None, ret_type),
|
|
2505
|
+
)
|
|
2506
|
+
lines.append(f" return {ret_cpp};")
|
|
2507
|
+
emitted_return = True
|
|
2508
|
+
elif (i == len(node.body) - 1
|
|
2509
|
+
and isinstance(s, (ForStmt, ForInStmt, WhileStmt))
|
|
2510
|
+
and not fi.returns_tuple
|
|
2511
|
+
and self._loop_value_cpp_type(s, ret_type) is not None):
|
|
2512
|
+
# A loop last: its body's last value on the last iteration
|
|
2513
|
+
# that reached it, na when none did.
|
|
2514
|
+
lines.append(
|
|
2515
|
+
f" {ret_type} _func_ret = {self._na_value_for_type(ret_type)};"
|
|
2516
|
+
)
|
|
2517
|
+
self._emit_loop_with_assign(
|
|
1789
2518
|
s,
|
|
1790
2519
|
"_func_ret",
|
|
1791
2520
|
lines,
|
|
1792
2521
|
indent=2,
|
|
1793
|
-
target_cpp_type=
|
|
2522
|
+
target_cpp_type=ret_type,
|
|
2523
|
+
reset=False,
|
|
1794
2524
|
)
|
|
1795
|
-
lines.append(
|
|
2525
|
+
lines.append(" return _func_ret;")
|
|
1796
2526
|
emitted_return = True
|
|
1797
2527
|
else:
|
|
1798
2528
|
self._visit_stmt(s, lines, indent=2)
|
|
2529
|
+
self._clear_lazy_call_history_hoists(hoisted)
|
|
1799
2530
|
|
|
1800
2531
|
# Always emit a default return if no explicit return was emitted,
|
|
1801
2532
|
# to avoid non-void function without return value.
|
|
1802
2533
|
if not emitted_return:
|
|
1803
|
-
if
|
|
2534
|
+
if request_struct is not None:
|
|
2535
|
+
lines.append(f" return {request_struct}{{}};")
|
|
2536
|
+
elif fi.returns_tuple:
|
|
1804
2537
|
default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
|
|
1805
2538
|
lines.append(f" return std::make_tuple({default_vals});")
|
|
1806
2539
|
else:
|
|
1807
|
-
if
|
|
2540
|
+
if return_udt or ret_type in DRAWING_TYPE_TO_CPP.values():
|
|
1808
2541
|
default_ret = f"{ret_type}{{}}"
|
|
1809
2542
|
else:
|
|
1810
2543
|
default_ret = self._default_for_type(ret_type)
|
|
@@ -1837,13 +2570,42 @@ class TopLevelEmitter:
|
|
|
1837
2570
|
self._active_call_site_idx = None
|
|
1838
2571
|
self._current_instance_name = None
|
|
1839
2572
|
|
|
1840
|
-
def
|
|
1841
|
-
|
|
2573
|
+
def _emit_function_global_history_updates(
|
|
2574
|
+
self, fi: FuncInfo, lines: list[str]) -> None:
|
|
2575
|
+
"""Record, on entry, each script variable the body reads through history.
|
|
2576
|
+
|
|
2577
|
+
The on_bar preamble has already advanced the call site's buffer by one
|
|
2578
|
+
chart slot, holding the previous value; this call replaces that slot
|
|
2579
|
+
with the value the call sees, so ``x[k]`` reads the site's latest call
|
|
2580
|
+
at or before ``k`` bars ago (``na`` before its first call).
|
|
2581
|
+
"""
|
|
2582
|
+
names = self.ctx.func_global_history_reads.get(fi.name, ())
|
|
2583
|
+
first_read = {}
|
|
2584
|
+
for owner, name, subscript in self.ctx.func_global_history_nodes.values():
|
|
2585
|
+
if owner == fi.name:
|
|
2586
|
+
first_read.setdefault(name, subscript.object)
|
|
2587
|
+
cpp_types = {
|
|
2588
|
+
info["member_name"]: info["cpp_type"]
|
|
2589
|
+
for info in self._inline_history_members
|
|
2590
|
+
}
|
|
2591
|
+
for name in names:
|
|
2592
|
+
member = self._function_global_history_member(name)
|
|
2593
|
+
if member is None:
|
|
2594
|
+
continue
|
|
2595
|
+
value = self._series_bridge_value_expr(
|
|
2596
|
+
self._visit_expr(first_read[name]), cpp_types[member]
|
|
2597
|
+
)
|
|
2598
|
+
lines.append(f" {member}.update({value});")
|
|
2599
|
+
|
|
2600
|
+
def _has_precalculated_ta(self) -> bool:
|
|
2601
|
+
return any(
|
|
1842
2602
|
self._ta_site_uses_precalc(site)
|
|
1843
2603
|
for _ti, site in enumerate(self.ctx.ta_call_sites)
|
|
1844
2604
|
if _ti not in self._dead_ta_indices
|
|
1845
2605
|
)
|
|
1846
|
-
|
|
2606
|
+
|
|
2607
|
+
def _emit_precalculate_and_run(self, lines: list[str]) -> None:
|
|
2608
|
+
if not self._has_precalculated_ta():
|
|
1847
2609
|
return
|
|
1848
2610
|
|
|
1849
2611
|
replayed_source_series: list[str] = []
|
|
@@ -1870,17 +2632,15 @@ class TopLevelEmitter:
|
|
|
1870
2632
|
if self._ta_site_uses_precalc(site):
|
|
1871
2633
|
lines.append(f" _precalc_{site.member_name}.resize(n);")
|
|
1872
2634
|
|
|
1873
|
-
# Reset indicators to clean slate
|
|
2635
|
+
# Reset indicators to clean slate, built with the arguments this run
|
|
2636
|
+
# uses (an input override resizes the precalculated series too).
|
|
1874
2637
|
lines.append("")
|
|
1875
2638
|
for _ti, site in enumerate(self.ctx.ta_call_sites):
|
|
1876
2639
|
if _ti in self._dead_ta_indices:
|
|
1877
2640
|
continue
|
|
1878
2641
|
if self._ta_site_uses_precalc(site):
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
for r in resolved:
|
|
1882
|
-
safe_resolved.append(r if self._is_compile_time_value(r) else "1")
|
|
1883
|
-
lines.append(f" {site.member_name} = {site.class_name}({', '.join(safe_resolved)});")
|
|
2642
|
+
run_args, _any_runtime = self._ta_run_ctor_args(site)
|
|
2643
|
+
lines.append(f" {site.member_name} = {site.class_name}({', '.join(run_args)});")
|
|
1884
2644
|
|
|
1885
2645
|
# Clear series
|
|
1886
2646
|
lines.append("")
|
|
@@ -1956,7 +2716,7 @@ class TopLevelEmitter:
|
|
|
1956
2716
|
default = self._get_input_default(stmt.value)
|
|
1957
2717
|
base = self._source_defval_to_base_series(default)
|
|
1958
2718
|
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
1959
|
-
cpp_val = f'get_input_source(
|
|
2719
|
+
cpp_val = f'get_input_source({self._input_key_literal(title)}, {base})[0]'
|
|
1960
2720
|
# A source var subscripted elsewhere in the script (e.g. ``src[1]``)
|
|
1961
2721
|
# is declared ``Series<double>``, not a scalar double, mirroring
|
|
1962
2722
|
# the normal per-bar path's ``{safe}.push({cpp_val})`` (see
|
|
@@ -1976,6 +2736,14 @@ class TopLevelEmitter:
|
|
|
1976
2736
|
if self._ta_site_uses_precalc(site):
|
|
1977
2737
|
compute_args = self._ta_compute_args_for_site(site)
|
|
1978
2738
|
compute_args_bars = compute_args.replace("current_bar_.", "bars[i].")
|
|
2739
|
+
# issue #178: the precalc pre-pass walks the bar array
|
|
2740
|
+
# itself, so the previous CHART close is bars[i - 1]
|
|
2741
|
+
# (the engine's prev_chart_close() tracker only advances
|
|
2742
|
+
# at on_bar dispatch).
|
|
2743
|
+
compute_args_bars = compute_args_bars.replace(
|
|
2744
|
+
TA_CHART_PREV_CLOSE_ARG,
|
|
2745
|
+
"(i > 0 ? bars[i - 1].close : na<double>())",
|
|
2746
|
+
)
|
|
1979
2747
|
lines.append(f" _precalc_{site.member_name}[i] = {site.member_name}.compute({compute_args_bars});")
|
|
1980
2748
|
finally:
|
|
1981
2749
|
self._precalc_loop_active = False
|
|
@@ -1988,11 +2756,8 @@ class TopLevelEmitter:
|
|
|
1988
2756
|
if _ti in self._dead_ta_indices:
|
|
1989
2757
|
continue
|
|
1990
2758
|
if self._ta_site_uses_precalc(site):
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
for r in resolved:
|
|
1994
|
-
safe_resolved.append(r if self._is_compile_time_value(r) else "1")
|
|
1995
|
-
lines.append(f" {site.member_name} = {site.class_name}({', '.join(safe_resolved)});")
|
|
2759
|
+
run_args, _any_runtime = self._ta_run_ctor_args(site)
|
|
2760
|
+
lines.append(f" {site.member_name} = {site.class_name}({', '.join(run_args)});")
|
|
1996
2761
|
|
|
1997
2762
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
1998
2763
|
lines.append(f" _s_{field_name}.clear();")
|
|
@@ -2009,23 +2774,6 @@ class TopLevelEmitter:
|
|
|
2009
2774
|
lines.append(" }")
|
|
2010
2775
|
lines.append("")
|
|
2011
2776
|
|
|
2012
|
-
#
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
lines.append(" BacktestEngine::run(bars, n);")
|
|
2016
|
-
lines.append(" }")
|
|
2017
|
-
lines.append("")
|
|
2018
|
-
lines.append(" void run(const Bar* input_bars, int n_input,")
|
|
2019
|
-
lines.append(" const std::string& input_tf,")
|
|
2020
|
-
lines.append(" const std::string& script_tf,")
|
|
2021
|
-
lines.append(" bool bar_magnifier = false,")
|
|
2022
|
-
lines.append(" int magnifier_samples = 4,")
|
|
2023
|
-
lines.append(" MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS) {")
|
|
2024
|
-
lines.append(" bool needs_dynamic = bar_magnifier || !input_tf.empty() || !script_tf.empty();")
|
|
2025
|
-
lines.append(" if (needs_dynamic) {")
|
|
2026
|
-
lines.append(" _use_precalc = false;")
|
|
2027
|
-
lines.append(" } else {")
|
|
2028
|
-
lines.append(" precalculate(input_bars, n_input);")
|
|
2029
|
-
lines.append(" }")
|
|
2030
|
-
lines.append(" BacktestEngine::run(input_bars, n_input, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist);")
|
|
2031
|
-
lines.append(" }")
|
|
2777
|
+
# All run overloads are inherited. The base-owned lifecycle hook resets
|
|
2778
|
+
# generated state before preparing this run's cache, including calls
|
|
2779
|
+
# entered through stream_begin rather than the generated C wrappers.
|