@pineforge/codegen-pyodide 0.10.4 → 1.0.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/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 +754 -76
- package/pineforge_codegen/analyzer/call_handlers.py +260 -40
- 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 +896 -156
- package/pineforge_codegen/codegen/constant_fold.py +131 -0
- package/pineforge_codegen/codegen/drawing.py +221 -79
- package/pineforge_codegen/codegen/emit_top.py +946 -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 +1188 -100
- package/pineforge_codegen/codegen/tables.py +193 -71
- package/pineforge_codegen/codegen/tv_number_format.py +270 -0
- package/pineforge_codegen/codegen/types.py +1882 -78
- package/pineforge_codegen/codegen/visit_call.py +920 -131
- package/pineforge_codegen/codegen/visit_expr.py +738 -57
- package/pineforge_codegen/codegen/visit_stmt.py +595 -49
- 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-1.0.0.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.4.tar.gz +0 -0
|
@@ -82,8 +82,11 @@ 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
|
|
@@ -92,9 +95,13 @@ from .tables import (
|
|
|
92
95
|
TA_CHART_PREV_CLOSE_ARG,
|
|
93
96
|
DRAWING_TYPE_TO_CPP,
|
|
94
97
|
PINE_TYPE_TO_CPP,
|
|
98
|
+
PINE_V6_STRATEGY_DEFAULTS,
|
|
95
99
|
RUNTIME_REGISTER_SECURITY_EVAL_FN,
|
|
96
100
|
RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN,
|
|
97
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
|
|
98
105
|
|
|
99
106
|
|
|
100
107
|
class TopLevelEmitter:
|
|
@@ -105,8 +112,26 @@ class TopLevelEmitter:
|
|
|
105
112
|
Mixed into ``CodeGen``; not intended to be instantiated standalone."""
|
|
106
113
|
|
|
107
114
|
def _emit_includes(self, lines: list[str]) -> None:
|
|
108
|
-
|
|
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>')
|
|
109
132
|
lines.append('#include <pineforge/ta.hpp>')
|
|
133
|
+
if self._ta_uses_dynamic_lengths():
|
|
134
|
+
lines.append('#include <pineforge/source/pine_ta_length.hpp>')
|
|
110
135
|
lines.append('#include <pineforge/math.hpp>')
|
|
111
136
|
lines.append('#include <pineforge/series.hpp>')
|
|
112
137
|
lines.append('#include <pineforge/na.hpp>')
|
|
@@ -119,10 +144,15 @@ class TopLevelEmitter:
|
|
|
119
144
|
lines.append("#include <numeric>")
|
|
120
145
|
lines.append("#include <string>")
|
|
121
146
|
lines.append("#include <vector>")
|
|
147
|
+
if self._uses_tv_number_format:
|
|
148
|
+
lines.extend(("#include <charconv>", "#include <system_error>",
|
|
149
|
+
"#include <stdexcept>"))
|
|
122
150
|
if getattr(self, "_udt_defs", {}):
|
|
123
151
|
lines.append("#include <deque>")
|
|
124
152
|
lines.append("#include <functional>")
|
|
125
153
|
lines.append("#include <limits>")
|
|
154
|
+
if self._udt_has_array_fields():
|
|
155
|
+
lines.append("#include <memory>")
|
|
126
156
|
lines.append("#include <tuple>")
|
|
127
157
|
lines.append("#include <optional>")
|
|
128
158
|
lines.append("#include <type_traits>")
|
|
@@ -174,6 +204,11 @@ class TopLevelEmitter:
|
|
|
174
204
|
# byte-identical — mirrors the matrix.hpp gating above.
|
|
175
205
|
if getattr(self, "_uses_drawing", False):
|
|
176
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
|
+
])
|
|
177
212
|
lines.append("")
|
|
178
213
|
# Compatibility shim for the namespace-wrap refactor: unqualified
|
|
179
214
|
# references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
|
|
@@ -181,33 +216,228 @@ class TopLevelEmitter:
|
|
|
181
216
|
# in favour of fully qualified names emitted at each call site.
|
|
182
217
|
lines.append("using namespace pineforge;")
|
|
183
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("")
|
|
184
225
|
# Syminfo derivation helpers (_pf_derive_main_tickerid, _pf_derive_country)
|
|
185
226
|
from .helpers_syminfo import emit_syminfo_helpers
|
|
186
227
|
lines.extend(emit_syminfo_helpers())
|
|
187
228
|
|
|
188
|
-
def
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return
|
|
210
|
-
|
|
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
|
+
])
|
|
211
441
|
|
|
212
442
|
def _script_has_input_source(self) -> bool:
|
|
213
443
|
"""True if the script's AST contains an ``input.source(...)`` call.
|
|
@@ -215,7 +445,7 @@ class TopLevelEmitter:
|
|
|
215
445
|
Gates the engine's native source-series push: the runtime only
|
|
216
446
|
advances ``_src_<field>_`` (paying the per-bar cost) when
|
|
217
447
|
``_src_series_active_`` is set, which the ctor does iff this returns
|
|
218
|
-
True.
|
|
448
|
+
True. This is a history-storage requirement, not a broker policy."""
|
|
219
449
|
from ..ast_nodes import FuncCall # local to avoid circular import
|
|
220
450
|
for node in self._walk_ast(self.ctx.ast):
|
|
221
451
|
if not isinstance(node, FuncCall):
|
|
@@ -295,6 +525,7 @@ class TopLevelEmitter:
|
|
|
295
525
|
"""
|
|
296
526
|
members: list[str] = []
|
|
297
527
|
seen: set[str] = set()
|
|
528
|
+
varip_members = self._varip_state_member_names()
|
|
298
529
|
for line in declaration_lines:
|
|
299
530
|
name = self._script_state_member_name(line)
|
|
300
531
|
if name is None:
|
|
@@ -304,9 +535,109 @@ class TopLevelEmitter:
|
|
|
304
535
|
if name in seen:
|
|
305
536
|
raise AssertionError(f"duplicate generated script-state member: {name}")
|
|
306
537
|
seen.add(name)
|
|
538
|
+
if name in varip_members:
|
|
539
|
+
continue
|
|
307
540
|
members.append(name)
|
|
308
541
|
return members
|
|
309
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
|
+
|
|
310
641
|
def _emit_handle_checkpoint_traits(self, lines: list[str]) -> None:
|
|
311
642
|
"""Emit recursive rollback adapters for shared-ID collection state.
|
|
312
643
|
|
|
@@ -470,6 +801,32 @@ class TopLevelEmitter:
|
|
|
470
801
|
"",
|
|
471
802
|
])
|
|
472
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
|
+
|
|
473
830
|
# Snapshot backing records field-by-field. A nested UDT field is only
|
|
474
831
|
# a numeric handle, so the primary trait copies its ID without recursing
|
|
475
832
|
# into another record. This makes self/cyclic object graphs finite;
|
|
@@ -482,7 +839,8 @@ class TopLevelEmitter:
|
|
|
482
839
|
for field in fields
|
|
483
840
|
if field.name not in self._udt_omitted_fields.get(type_name, set())
|
|
484
841
|
]
|
|
485
|
-
checkpoint_fields = [field.name
|
|
842
|
+
checkpoint_fields = [self._safe_name(field.name)
|
|
843
|
+
for field in emitted_fields]
|
|
486
844
|
lines.append("template <>")
|
|
487
845
|
lines.append(f"struct {checkpoint_traits}<{record_type}> {{")
|
|
488
846
|
lines.append(" struct snapshot_type {")
|
|
@@ -641,6 +999,10 @@ class TopLevelEmitter:
|
|
|
641
999
|
# run and their ctor args (bare param names) can never be sized.
|
|
642
1000
|
if ta_idx in self._dead_ta_indices:
|
|
643
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
|
|
644
1006
|
if site.ctor_args:
|
|
645
1007
|
# If a ctor arg is neither a compile-time literal nor expandable
|
|
646
1008
|
# to an input-backed runtime expression, the old code silently
|
|
@@ -649,10 +1011,14 @@ class TopLevelEmitter:
|
|
|
649
1011
|
# DO expand to a runtime expr (input-backed / arithmetic-over-input,
|
|
650
1012
|
# incl. function-derived lengths) are safe: the `!_ta_initialized_`
|
|
651
1013
|
# reset overwrites the placeholder before the first compute.
|
|
652
|
-
for a in site.ctor_args:
|
|
1014
|
+
for arg_pos, a in enumerate(site.ctor_args):
|
|
653
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.
|
|
654
1019
|
if (not self._is_compile_time_value(r)
|
|
655
|
-
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))):
|
|
656
1022
|
# A TA source reached through request.security can have
|
|
657
1023
|
# several helper-bound constructor variants. Validate
|
|
658
1024
|
# only variants of this exact source node before the
|
|
@@ -664,24 +1030,39 @@ class TopLevelEmitter:
|
|
|
664
1030
|
self._collect_ta_runtime_resets(
|
|
665
1031
|
security_source_node=site.node
|
|
666
1032
|
)
|
|
667
|
-
self.
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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)
|
|
676
1054
|
resolved = [self._resolve_ta_ctor_arg(a) for a in site.ctor_args]
|
|
677
1055
|
# Compile-time placeholder for the init list; the runtime reset
|
|
678
1056
|
# (when the arg is input-derived) overwrites it on the first bar.
|
|
679
1057
|
safe_resolved = []
|
|
680
|
-
for r in resolved:
|
|
1058
|
+
for arg_pos, r in enumerate(resolved):
|
|
681
1059
|
if self._is_compile_time_value(r):
|
|
682
|
-
|
|
1060
|
+
rendered = r
|
|
683
1061
|
else:
|
|
684
|
-
|
|
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)
|
|
685
1066
|
init_parts.append(f"{site.member_name}({', '.join(safe_resolved)})")
|
|
686
1067
|
# Security evaluator TA ctor args (skip for user function call expressions)
|
|
687
1068
|
for info in self._security_eval_info:
|
|
@@ -690,6 +1071,12 @@ class TopLevelEmitter:
|
|
|
690
1071
|
if not site.ctor_args:
|
|
691
1072
|
continue
|
|
692
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
|
|
693
1080
|
ctor_args, _ctor_arg_stability = self._security_ta_ctor_args_for_variant(
|
|
694
1081
|
info["sec_id"],
|
|
695
1082
|
site,
|
|
@@ -697,10 +1084,11 @@ class TopLevelEmitter:
|
|
|
697
1084
|
)
|
|
698
1085
|
resolved = [self._resolve_ta_ctor_arg(a) for a in ctor_args]
|
|
699
1086
|
safe_resolved = []
|
|
700
|
-
for r in resolved:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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)
|
|
704
1092
|
init_parts.append(f"{variant['member_name']}({', '.join(safe_resolved)})")
|
|
705
1093
|
|
|
706
1094
|
# Non-series var members with compile-time init (deduplicate by name)
|
|
@@ -747,18 +1135,43 @@ class TopLevelEmitter:
|
|
|
747
1135
|
cpp_val = self._typed_na_init(cpp_val, name, ptype)
|
|
748
1136
|
if self._is_compile_time_value(cpp_val):
|
|
749
1137
|
init_parts.append(f"{safe}({cpp_val})")
|
|
750
|
-
#
|
|
751
|
-
|
|
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.
|
|
752
1158
|
sp = self.ctx.strategy_params
|
|
753
1159
|
|
|
754
1160
|
if sp.get("process_orders_on_close") is True:
|
|
755
|
-
ctor_body.append("
|
|
1161
|
+
ctor_body.append(" cfg.process_orders_on_close = true;")
|
|
756
1162
|
|
|
757
1163
|
if sp.get("calc_on_order_fills") is True:
|
|
758
|
-
ctor_body.append("
|
|
1164
|
+
ctor_body.append(" cfg.calc_on_order_fills = true;")
|
|
759
1165
|
|
|
760
|
-
|
|
761
|
-
|
|
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)};")
|
|
762
1175
|
|
|
763
1176
|
# default_qty_type: strategy.fixed / strategy.percent_of_equity / strategy.cash
|
|
764
1177
|
qty_type_map = {
|
|
@@ -766,15 +1179,18 @@ class TopLevelEmitter:
|
|
|
766
1179
|
"strategy.percent_of_equity": "QtyType::PERCENT_OF_EQUITY",
|
|
767
1180
|
"strategy.cash": "QtyType::CASH",
|
|
768
1181
|
}
|
|
769
|
-
qty_type =
|
|
1182
|
+
qty_type = declared_or_v6_default("default_qty_type")
|
|
770
1183
|
if qty_type in qty_type_map:
|
|
771
|
-
ctor_body.append(
|
|
1184
|
+
ctor_body.append(
|
|
1185
|
+
f" cfg.default_qty_type = static_cast<int>({qty_type_map[qty_type]});"
|
|
1186
|
+
)
|
|
772
1187
|
|
|
773
|
-
|
|
774
|
-
|
|
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)};")
|
|
775
1191
|
|
|
776
1192
|
if "pyramiding" in sp and isinstance(sp["pyramiding"], int):
|
|
777
|
-
ctor_body.append(f"
|
|
1193
|
+
ctor_body.append(f" cfg.pyramiding = {sp['pyramiding']};")
|
|
778
1194
|
|
|
779
1195
|
# commission_type: strategy.commission.percent / .cash_per_order / .cash_per_contract
|
|
780
1196
|
comm_type_map = {
|
|
@@ -784,41 +1200,35 @@ class TopLevelEmitter:
|
|
|
784
1200
|
}
|
|
785
1201
|
comm_type = sp.get("commission_type")
|
|
786
1202
|
if comm_type in comm_type_map:
|
|
787
|
-
ctor_body.append(
|
|
1203
|
+
ctor_body.append(
|
|
1204
|
+
f" cfg.commission_type = static_cast<int>({comm_type_map[comm_type]});"
|
|
1205
|
+
)
|
|
788
1206
|
|
|
789
1207
|
if "commission_value" in sp and isinstance(sp["commission_value"], (int, float)):
|
|
790
|
-
ctor_body.append(f"
|
|
1208
|
+
ctor_body.append(f" cfg.commission_value = {float(sp['commission_value'])};")
|
|
791
1209
|
|
|
792
1210
|
if "slippage" in sp and isinstance(sp["slippage"], (int, float)):
|
|
793
|
-
ctor_body.append(f"
|
|
1211
|
+
ctor_body.append(f" cfg.slippage = {int(sp['slippage'])};")
|
|
794
1212
|
|
|
795
1213
|
# margin_long / margin_short: percent of position value required as
|
|
796
1214
|
# equity (default 100 = 1x leverage). When required_margin exceeds
|
|
797
1215
|
# available equity, TV silently rejects the fill — engine mirrors
|
|
798
1216
|
# this in execute_market_entry's FLAT branch.
|
|
799
1217
|
if "margin_long" in sp and isinstance(sp["margin_long"], (int, float)):
|
|
800
|
-
ctor_body.append(f"
|
|
1218
|
+
ctor_body.append(f" cfg.margin_long = {float(sp['margin_long'])};")
|
|
801
1219
|
if "margin_short" in sp and isinstance(sp["margin_short"], (int, float)):
|
|
802
|
-
ctor_body.append(f"
|
|
1220
|
+
ctor_body.append(f" cfg.margin_short = {float(sp['margin_short'])};")
|
|
803
1221
|
|
|
804
1222
|
# close_entries_rule: "FIFO" (default) or "ANY"
|
|
805
1223
|
if sp.get("close_entries_rule") == "ANY":
|
|
806
|
-
ctor_body.append("
|
|
807
|
-
|
|
808
|
-
# Detect ``strategy.close`` / ``strategy.close_all`` calls anywhere in
|
|
809
|
-
# the script body. The runtime uses this flag in its priced-entry flip
|
|
810
|
-
# path to reproduce TradingView's empirical
|
|
811
|
-
# ``new_size = |old| + qty`` rule (see
|
|
812
|
-
# docs/codegen-gaps/validation-tv-pyramiding-override.md). The flag
|
|
813
|
-
# is set once per compilation; it is independent of how many times
|
|
814
|
-
# the close call actually fires at runtime.
|
|
815
|
-
if self._script_has_strategy_close():
|
|
816
|
-
ctor_body.append(" script_has_strategy_close_ = true;")
|
|
1224
|
+
ctor_body.append(" cfg.close_entries_rule_any = true;")
|
|
817
1225
|
|
|
818
1226
|
# Turn on native source-series history only when the script uses
|
|
819
1227
|
# input.source — otherwise the engine pays nothing per bar.
|
|
820
1228
|
if self._script_has_input_source():
|
|
821
|
-
ctor_body.append("
|
|
1229
|
+
ctor_body.append(" cfg.src_series_active = true;")
|
|
1230
|
+
|
|
1231
|
+
ctor_body.append(" configure_pine_strategy(cfg);")
|
|
822
1232
|
|
|
823
1233
|
if init_parts and ctor_body:
|
|
824
1234
|
lines.append(f" explicit GeneratedStrategy() : {', '.join(init_parts)} {{")
|
|
@@ -835,43 +1245,81 @@ class TopLevelEmitter:
|
|
|
835
1245
|
|
|
836
1246
|
lines.append("")
|
|
837
1247
|
lines.append(" void set_strategy_override(const std::string& key, const std::string& value) {")
|
|
838
|
-
lines.append(
|
|
839
|
-
lines.append(' if (key == "
|
|
840
|
-
lines.append(
|
|
841
|
-
lines.append(' if (key == "
|
|
842
|
-
lines.append(
|
|
843
|
-
lines.append(' if (key == "
|
|
844
|
-
lines.append(
|
|
845
|
-
lines.append(' if (key == "
|
|
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('
|
|
855
|
-
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 {")
|
|
856
1276
|
lines.append(" return;")
|
|
857
1277
|
lines.append(" }")
|
|
1278
|
+
lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
|
|
858
1279
|
lines.append(" }")
|
|
859
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
|
+
])
|
|
860
1301
|
if self._security_eval_info:
|
|
861
1302
|
lines.append("")
|
|
1303
|
+
if any(info.get("foreign") for info in self._security_eval_info):
|
|
1304
|
+
self._emit_foreign_security_lookups(lines)
|
|
862
1305
|
lines.append(" void configure_security_evaluators() override {")
|
|
863
1306
|
lines.append(" security_eval_states_.clear();")
|
|
1307
|
+
lines.extend(self._security_tf_replay_prologue())
|
|
864
1308
|
for info in self._security_eval_info:
|
|
865
1309
|
tf = info.get("tf")
|
|
866
1310
|
tf_expr = info.get("tf_expr")
|
|
867
1311
|
if tf:
|
|
868
1312
|
tf_expr = f'"{tf}"'
|
|
869
|
-
elif not tf_expr:
|
|
870
|
-
#
|
|
871
|
-
# back to the chart timeframe so registration still compiles
|
|
872
|
-
# (e.g. a request.security inside a dead-code UDF, or one
|
|
873
|
-
# 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.
|
|
874
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
|
+
)
|
|
875
1323
|
if tf_expr:
|
|
876
1324
|
la = "true" if info["lookahead_on"] else "false"
|
|
877
1325
|
go = "true" if info.get("gaps_on") else "false"
|
|
@@ -890,6 +1338,8 @@ class TopLevelEmitter:
|
|
|
890
1338
|
f" {RUNTIME_REGISTER_SECURITY_LOWER_TF_EVAL_FN}"
|
|
891
1339
|
f"({sec_id}, {tf_expr}, input_tf_);"
|
|
892
1340
|
)
|
|
1341
|
+
elif info.get("foreign"):
|
|
1342
|
+
self._emit_foreign_security_registration(info, tf_expr, la, go, lines)
|
|
893
1343
|
else:
|
|
894
1344
|
lines.append(
|
|
895
1345
|
f" {RUNTIME_REGISTER_SECURITY_EVAL_FN}"
|
|
@@ -897,6 +1347,109 @@ class TopLevelEmitter:
|
|
|
897
1347
|
f"input_tf_, {la}, {go}{ha_arg});")
|
|
898
1348
|
lines.append(" }")
|
|
899
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
|
+
|
|
900
1453
|
# Map strategy series member name to push expression
|
|
901
1454
|
_STRAT_SERIES_PUSH = {
|
|
902
1455
|
"position_size": "signed_position_size()",
|
|
@@ -931,7 +1484,7 @@ class TopLevelEmitter:
|
|
|
931
1484
|
self._lexical_udt_types = {}
|
|
932
1485
|
self._lexical_series_bindings = {}
|
|
933
1486
|
self._lexical_known_var_tombstones = set()
|
|
934
|
-
lines.append(" void
|
|
1487
|
+
lines.append(" void on_source_bar(const Bar& bar) override {")
|
|
935
1488
|
|
|
936
1489
|
# A GeneratedStrategy handle may execute multiple batch runs or
|
|
937
1490
|
# streaming lifecycles. BacktestEngine resets broker/base state, but
|
|
@@ -949,8 +1502,6 @@ class TopLevelEmitter:
|
|
|
949
1502
|
for info in self._lazy_source_clock_by_node.values():
|
|
950
1503
|
lines.append(f" {info['clock']}.reset();")
|
|
951
1504
|
lines.append(f" {info['hist']}.clear();")
|
|
952
|
-
if info["chart"] is not None:
|
|
953
|
-
lines.append(f" {info['chart']}.clear();")
|
|
954
1505
|
lines.append(" }")
|
|
955
1506
|
for info in self._lazy_source_clock_by_node.values():
|
|
956
1507
|
lines.append(f" {info['clock']}.begin_bar(bar_index_);")
|
|
@@ -960,13 +1511,6 @@ class TopLevelEmitter:
|
|
|
960
1511
|
info["hist"],
|
|
961
1512
|
f"{info['clock']}.bar_base_source",
|
|
962
1513
|
)
|
|
963
|
-
if info["chart"] is not None:
|
|
964
|
-
self._emit_history_series_write(
|
|
965
|
-
lines,
|
|
966
|
-
" ",
|
|
967
|
-
info["chart"],
|
|
968
|
-
self._visit_expr(info["chart_source"]),
|
|
969
|
-
)
|
|
970
1514
|
|
|
971
1515
|
# reset_run_state() owns engine/broker state, while these generated
|
|
972
1516
|
# Series members belong to the strategy object. Clear all of them on
|
|
@@ -986,10 +1530,12 @@ class TopLevelEmitter:
|
|
|
986
1530
|
# even when lazy control flow skips its written call on this bar. Seed
|
|
987
1531
|
# the new slot with the prior current value (``na`` before first reach);
|
|
988
1532
|
# an executed call later in the bar updates this same slot with its
|
|
989
|
-
# scalar actual.
|
|
990
|
-
#
|
|
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.
|
|
991
1537
|
for info in self._inline_history_members:
|
|
992
|
-
if info["kind"]
|
|
1538
|
+
if info["kind"] not in ("udf_series_arg", "fn_global_hist"):
|
|
993
1539
|
continue
|
|
994
1540
|
member = info["member_name"]
|
|
995
1541
|
lines.append(
|
|
@@ -1009,17 +1555,18 @@ class TopLevelEmitter:
|
|
|
1009
1555
|
# members (base.py section 6) but — unlike user series vars (pushed at
|
|
1010
1556
|
# their assignment) and bar fields (pushed above) — have no push site,
|
|
1011
1557
|
# so ``[n]`` would read an unfed buffer (the na sentinel) on every bar.
|
|
1012
|
-
# Push each from its scalar lowering.
|
|
1013
|
-
#
|
|
1014
|
-
#
|
|
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.
|
|
1015
1562
|
from .tables import BAR_BUILTINS
|
|
1016
1563
|
for _bname in sorted(self.ctx.series_vars):
|
|
1017
1564
|
if _bname in self._var_names:
|
|
1018
1565
|
continue
|
|
1019
1566
|
_bexpr = BAR_BUILTINS.get(_bname)
|
|
1020
|
-
if _bexpr is None or _bexpr.strip().startswith(f"{_bname}("):
|
|
1021
|
-
continue
|
|
1022
1567
|
_bsafe = self._safe_name(_bname)
|
|
1568
|
+
if _bexpr is None or _bexpr.strip().startswith(f"{_bsafe}("):
|
|
1569
|
+
continue
|
|
1023
1570
|
self._emit_history_series_write(lines, " ", _bsafe, _bexpr)
|
|
1024
1571
|
|
|
1025
1572
|
# a2. Push strategy series
|
|
@@ -1028,6 +1575,53 @@ class TopLevelEmitter:
|
|
|
1028
1575
|
push_expr = self._STRAT_SERIES_PUSH.get(member, "0")
|
|
1029
1576
|
self._emit_history_series_write(lines, " ", svar, push_expr)
|
|
1030
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
|
+
|
|
1031
1625
|
# b. Var init / carry-forward
|
|
1032
1626
|
if self.ctx.var_members:
|
|
1033
1627
|
lines.append(" if (!_var_initialized) {")
|
|
@@ -1106,6 +1700,13 @@ class TopLevelEmitter:
|
|
|
1106
1700
|
break
|
|
1107
1701
|
continue
|
|
1108
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});")
|
|
1109
1710
|
continue
|
|
1110
1711
|
# Persistent authored UDT variables initialize from their full
|
|
1111
1712
|
# source RHS, not only from ``Type.new``. In particular,
|
|
@@ -1196,39 +1797,6 @@ class TopLevelEmitter:
|
|
|
1196
1797
|
# c. Push non-var series (they start fresh each bar with a push)
|
|
1197
1798
|
# (actual push happens in visit_VarDecl when the decl is visited)
|
|
1198
1799
|
|
|
1199
|
-
# c3. Evaluate static global inputs and variables once
|
|
1200
|
-
static_vars = []
|
|
1201
|
-
for stmt in self.ctx.ast.body:
|
|
1202
|
-
if isinstance(stmt, VarDecl):
|
|
1203
|
-
is_input = isinstance(stmt.value, FuncCall) and self._is_input_call(stmt.value)
|
|
1204
|
-
if is_input:
|
|
1205
|
-
func_name_i, namespace_i = self._resolve_callee(stmt.value.callee)
|
|
1206
|
-
is_static_global_input = (
|
|
1207
|
-
stmt.name in self._global_member_vars
|
|
1208
|
-
and not self._is_source_input(stmt.value)
|
|
1209
|
-
and stmt.name not in self._array_vars
|
|
1210
|
-
and stmt.name not in getattr(self, "_matrix_specs", {})
|
|
1211
|
-
and stmt.name not in getattr(self, "_map_vars", {})
|
|
1212
|
-
and not stmt.is_var
|
|
1213
|
-
and not stmt.is_varip
|
|
1214
|
-
)
|
|
1215
|
-
if is_static_global_input:
|
|
1216
|
-
safe = self._safe_name(stmt.name)
|
|
1217
|
-
default = self._get_input_default(stmt.value)
|
|
1218
|
-
default_cpp = self._visit_expr(default) if default is not None else "0"
|
|
1219
|
-
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
1220
|
-
getter = self._input_type_to_getter(func_name_i, namespace_i)
|
|
1221
|
-
default_cpp = self._coerce_string_input_default(getter, default_cpp)
|
|
1222
|
-
cpp_val = f'{getter}("{title}", {default_cpp})'
|
|
1223
|
-
static_vars.append(f"{safe} = {cpp_val};")
|
|
1224
|
-
|
|
1225
|
-
if static_vars:
|
|
1226
|
-
lines.append(" if (!_inputs_initialized_) {")
|
|
1227
|
-
for var_expr in static_vars:
|
|
1228
|
-
lines.append(f" {var_expr}")
|
|
1229
|
-
lines.append(" _inputs_initialized_ = true;")
|
|
1230
|
-
lines.append(" }")
|
|
1231
|
-
|
|
1232
1800
|
# c2. First-bar TA resize: rebuild any TA object whose ctor args come
|
|
1233
1801
|
# from input-backed variables so strategy_set_input() actually changes
|
|
1234
1802
|
# the circular-buffer sizes. Emits nothing when no TA site depends on
|
|
@@ -1237,9 +1805,11 @@ class TopLevelEmitter:
|
|
|
1237
1805
|
|
|
1238
1806
|
# d. Visit each statement. A stateful ``ta.*`` site below a lazy
|
|
1239
1807
|
# ``and``/``or`` RHS or ternary arm of a top-level statement is
|
|
1240
|
-
# 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``).
|
|
1241
1810
|
for stmt in self.ctx.ast.body:
|
|
1242
1811
|
self._emit_lazy_edge_ta_hoists(stmt, lines, indent=2)
|
|
1812
|
+
self._emit_lazy_call_history_hoists(stmt, lines, indent=2)
|
|
1243
1813
|
try:
|
|
1244
1814
|
self._visit_stmt(stmt, lines, indent=2)
|
|
1245
1815
|
finally:
|
|
@@ -1380,9 +1950,37 @@ class TopLevelEmitter:
|
|
|
1380
1950
|
lines.append(" if (!s) return;")
|
|
1381
1951
|
lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
|
|
1382
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(" }")
|
|
1383
1961
|
lines.append("}")
|
|
1384
1962
|
lines.append("")
|
|
1385
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
|
+
|
|
1386
1984
|
def _emit_udt_method_cpp_name(self, fi: FuncInfo) -> str:
|
|
1387
1985
|
"""Stable C++ identifier for a typed instance method."""
|
|
1388
1986
|
receiver_spec = (
|
|
@@ -1399,6 +1997,22 @@ class TopLevelEmitter:
|
|
|
1399
1997
|
|
|
1400
1998
|
def _emit_func_def(self, fi: FuncInfo, lines: list[str], call_site_idx: int | None = None,
|
|
1401
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:
|
|
1402
2016
|
"""Emit a user-defined function as a class method.
|
|
1403
2017
|
|
|
1404
2018
|
If call_site_idx is not None, emit a per-call-site variant with
|
|
@@ -1412,6 +2026,12 @@ class TopLevelEmitter:
|
|
|
1412
2026
|
node = fi.node
|
|
1413
2027
|
if node is None:
|
|
1414
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
|
+
)
|
|
1415
2035
|
|
|
1416
2036
|
# Collection registries historically used raw variable names for the
|
|
1417
2037
|
# whole translation unit. Emit each callable against copy-on-write
|
|
@@ -1465,9 +2085,9 @@ class TopLevelEmitter:
|
|
|
1465
2085
|
)
|
|
1466
2086
|
variant_param_types = (
|
|
1467
2087
|
getattr(self.ctx, "func_callsite_param_types", {}).get(
|
|
1468
|
-
(fi.name,
|
|
2088
|
+
(fi.name, type_call_site_idx), ()
|
|
1469
2089
|
)
|
|
1470
|
-
if
|
|
2090
|
+
if type_call_site_idx is not None
|
|
1471
2091
|
else ()
|
|
1472
2092
|
)
|
|
1473
2093
|
for i, p in enumerate(node.params):
|
|
@@ -1576,7 +2196,7 @@ class TopLevelEmitter:
|
|
|
1576
2196
|
PineType.FLOAT: "double",
|
|
1577
2197
|
PineType.BOOL: "bool",
|
|
1578
2198
|
PineType.STRING: "std::string",
|
|
1579
|
-
PineType.COLOR: "
|
|
2199
|
+
PineType.COLOR: "int64_t",
|
|
1580
2200
|
}[variant_pt]
|
|
1581
2201
|
elif i < len(getattr(fi, "param_type_specs", [])) and fi.param_type_specs[i] is not None:
|
|
1582
2202
|
# Precise per-param TypeSpec (declared hint or call-site inference):
|
|
@@ -1585,7 +2205,8 @@ class TopLevelEmitter:
|
|
|
1585
2205
|
# a string -> ``std::string``. User UDT field mutation propagates
|
|
1586
2206
|
# through the arena even though the handle parameter is by value.
|
|
1587
2207
|
spec = fi.param_type_specs[i]
|
|
1588
|
-
cpp_t = self.
|
|
2208
|
+
cpp_t = self._wide_declared_int_param(
|
|
2209
|
+
fi, i, self._type_spec_to_cpp(spec))
|
|
1589
2210
|
if spec.kind == "udt":
|
|
1590
2211
|
self._udt_param_udt[p] = spec.name
|
|
1591
2212
|
self._udt_param_udt[self._safe_name(p)] = spec.name
|
|
@@ -1600,7 +2221,8 @@ class TopLevelEmitter:
|
|
|
1600
2221
|
cpp_t = f"{cpp_t}&"
|
|
1601
2222
|
elif i < len(fi.param_types):
|
|
1602
2223
|
pt = fi.param_types[i]
|
|
1603
|
-
cpp_t =
|
|
2224
|
+
cpp_t = self._wide_declared_int_param(
|
|
2225
|
+
fi, i, PINE_TYPE_TO_CPP.get(pt, "double"))
|
|
1604
2226
|
else:
|
|
1605
2227
|
cpp_t = "double"
|
|
1606
2228
|
param_strs.append(f"{cpp_t} {self._safe_name(p)}")
|
|
@@ -1614,14 +2236,27 @@ class TopLevelEmitter:
|
|
|
1614
2236
|
# without it the function would be emitted as returning ``double`` and
|
|
1615
2237
|
# clang errors with "no viable conversion from T to double". Probe:
|
|
1616
2238
|
# data/validation/udt-method-probe-20-udt-return-from-func.
|
|
1617
|
-
|
|
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:
|
|
1618
2248
|
# Infer actual tuple element types from function body's last expression
|
|
1619
2249
|
tuple_types_list = self._infer_tuple_types(node, fi.tuple_element_count)
|
|
1620
2250
|
ret_type = f"std::tuple<{', '.join(tuple_types_list)}>"
|
|
1621
|
-
elif
|
|
2251
|
+
elif return_udt_name:
|
|
1622
2252
|
# A function returning a drawing handle must emit the C++ handle
|
|
1623
2253
|
# struct (Line/Box/Label/Linefill), not the unknown lowercase name.
|
|
1624
|
-
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
|
+
)
|
|
1625
2260
|
elif self._func_int_return_uses_wide_history(
|
|
1626
2261
|
fi, call_site_idx=call_site_idx
|
|
1627
2262
|
):
|
|
@@ -1629,14 +2264,14 @@ class TopLevelEmitter:
|
|
|
1629
2264
|
# values and their na sentinel cannot narrow at the return edge.
|
|
1630
2265
|
ret_type = "int64_t"
|
|
1631
2266
|
elif (
|
|
1632
|
-
|
|
2267
|
+
type_call_site_idx is not None
|
|
1633
2268
|
and self._callsite_callable_return_pine_type(
|
|
1634
|
-
fi,
|
|
2269
|
+
fi, type_call_site_idx
|
|
1635
2270
|
) != PineType.UNKNOWN
|
|
1636
2271
|
):
|
|
1637
2272
|
ret_type = PINE_TYPE_TO_CPP.get(
|
|
1638
2273
|
self._callsite_callable_return_pine_type(
|
|
1639
|
-
fi,
|
|
2274
|
+
fi, type_call_site_idx
|
|
1640
2275
|
),
|
|
1641
2276
|
"double",
|
|
1642
2277
|
)
|
|
@@ -1646,13 +2281,16 @@ class TopLevelEmitter:
|
|
|
1646
2281
|
ret_type = self._type_spec_to_cpp(fi.return_type_spec)
|
|
1647
2282
|
else:
|
|
1648
2283
|
ret_type = PINE_TYPE_TO_CPP.get(fi.return_type, "double")
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
|
1656
2294
|
|
|
1657
2295
|
# For per-call-site variants, suffix the function name and activate TA + var remapping
|
|
1658
2296
|
func_name = (
|
|
@@ -1749,22 +2387,45 @@ class TopLevelEmitter:
|
|
|
1749
2387
|
self._current_func_locals |= self._collect_binding_names(node.body)
|
|
1750
2388
|
|
|
1751
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)
|
|
1752
2403
|
|
|
1753
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] = []
|
|
1754
2408
|
if node.is_single_expr and node.body:
|
|
2409
|
+
hoisted = self._emit_lazy_call_history_hoists(node.body[0], lines, indent=2)
|
|
1755
2410
|
expr = node.body[0].expr if isinstance(node.body[0], ExprStmt) else None
|
|
1756
2411
|
if expr and self._call_is_void(expr):
|
|
1757
2412
|
# void setter as the sole body expr — emit as statement, fall
|
|
1758
2413
|
# through to the default return.
|
|
1759
2414
|
self._visit_stmt(node.body[0], lines, indent=2)
|
|
1760
2415
|
elif expr:
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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),
|
|
1764
2422
|
)
|
|
2423
|
+
lines.append(f" return {ret_cpp};")
|
|
1765
2424
|
emitted_return = True
|
|
1766
2425
|
else:
|
|
1767
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)
|
|
1768
2429
|
if i == len(node.body) - 1 and isinstance(s, ExprStmt):
|
|
1769
2430
|
# A void drawing setter / delete / visual-noop, or a dropped
|
|
1770
2431
|
# table/polyline method call (``panel.cell(...)``), used as
|
|
@@ -1775,10 +2436,14 @@ class TopLevelEmitter:
|
|
|
1775
2436
|
if self._call_is_void(s.expr) or self._is_skip_expr(s.expr):
|
|
1776
2437
|
self._visit_stmt(s, lines, indent=2)
|
|
1777
2438
|
else:
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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),
|
|
1781
2445
|
)
|
|
2446
|
+
lines.append(f" return {ret_cpp};")
|
|
1782
2447
|
emitted_return = True
|
|
1783
2448
|
elif i == len(node.body) - 1 and isinstance(s, (SwitchStmt, IfStmt)):
|
|
1784
2449
|
# Switch/if as last statement = return expression in PineScript
|
|
@@ -1787,31 +2452,92 @@ class TopLevelEmitter:
|
|
|
1787
2452
|
# default (``Label _func_ret = Label{};``) — falling through
|
|
1788
2453
|
# to ``_default_for_type`` would emit ``0.0`` and clang would
|
|
1789
2454
|
# reject ``Label _func_ret = 0.0;``.
|
|
1790
|
-
if
|
|
2455
|
+
if return_udt or ret_type in DRAWING_TYPE_TO_CPP.values():
|
|
1791
2456
|
default_ret = f"{ret_type}{{}}"
|
|
2457
|
+
elif fi.returns_tuple:
|
|
2458
|
+
default_ret = self._tuple_default_expr(tuple_types_list)
|
|
1792
2459
|
else:
|
|
1793
2460
|
default_ret = self._default_for_type(ret_type)
|
|
1794
2461
|
lines.append(f" {ret_type} _func_ret = {default_ret};")
|
|
1795
|
-
self.
|
|
2462
|
+
self._visit_selection_value(
|
|
1796
2463
|
s,
|
|
1797
2464
|
"_func_ret",
|
|
1798
2465
|
lines,
|
|
1799
2466
|
indent=2,
|
|
1800
|
-
target_cpp_type=
|
|
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,
|
|
1801
2473
|
)
|
|
1802
|
-
lines.append(
|
|
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(
|
|
2518
|
+
s,
|
|
2519
|
+
"_func_ret",
|
|
2520
|
+
lines,
|
|
2521
|
+
indent=2,
|
|
2522
|
+
target_cpp_type=ret_type,
|
|
2523
|
+
reset=False,
|
|
2524
|
+
)
|
|
2525
|
+
lines.append(" return _func_ret;")
|
|
1803
2526
|
emitted_return = True
|
|
1804
2527
|
else:
|
|
1805
2528
|
self._visit_stmt(s, lines, indent=2)
|
|
2529
|
+
self._clear_lazy_call_history_hoists(hoisted)
|
|
1806
2530
|
|
|
1807
2531
|
# Always emit a default return if no explicit return was emitted,
|
|
1808
2532
|
# to avoid non-void function without return value.
|
|
1809
2533
|
if not emitted_return:
|
|
1810
|
-
if
|
|
2534
|
+
if request_struct is not None:
|
|
2535
|
+
lines.append(f" return {request_struct}{{}};")
|
|
2536
|
+
elif fi.returns_tuple:
|
|
1811
2537
|
default_vals = ", ".join(["0.0"] * fi.tuple_element_count)
|
|
1812
2538
|
lines.append(f" return std::make_tuple({default_vals});")
|
|
1813
2539
|
else:
|
|
1814
|
-
if
|
|
2540
|
+
if return_udt or ret_type in DRAWING_TYPE_TO_CPP.values():
|
|
1815
2541
|
default_ret = f"{ret_type}{{}}"
|
|
1816
2542
|
else:
|
|
1817
2543
|
default_ret = self._default_for_type(ret_type)
|
|
@@ -1844,13 +2570,42 @@ class TopLevelEmitter:
|
|
|
1844
2570
|
self._active_call_site_idx = None
|
|
1845
2571
|
self._current_instance_name = None
|
|
1846
2572
|
|
|
1847
|
-
def
|
|
1848
|
-
|
|
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(
|
|
1849
2602
|
self._ta_site_uses_precalc(site)
|
|
1850
2603
|
for _ti, site in enumerate(self.ctx.ta_call_sites)
|
|
1851
2604
|
if _ti not in self._dead_ta_indices
|
|
1852
2605
|
)
|
|
1853
|
-
|
|
2606
|
+
|
|
2607
|
+
def _emit_precalculate_and_run(self, lines: list[str]) -> None:
|
|
2608
|
+
if not self._has_precalculated_ta():
|
|
1854
2609
|
return
|
|
1855
2610
|
|
|
1856
2611
|
replayed_source_series: list[str] = []
|
|
@@ -1877,17 +2632,15 @@ class TopLevelEmitter:
|
|
|
1877
2632
|
if self._ta_site_uses_precalc(site):
|
|
1878
2633
|
lines.append(f" _precalc_{site.member_name}.resize(n);")
|
|
1879
2634
|
|
|
1880
|
-
# 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).
|
|
1881
2637
|
lines.append("")
|
|
1882
2638
|
for _ti, site in enumerate(self.ctx.ta_call_sites):
|
|
1883
2639
|
if _ti in self._dead_ta_indices:
|
|
1884
2640
|
continue
|
|
1885
2641
|
if self._ta_site_uses_precalc(site):
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
for r in resolved:
|
|
1889
|
-
safe_resolved.append(r if self._is_compile_time_value(r) else "1")
|
|
1890
|
-
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)});")
|
|
1891
2644
|
|
|
1892
2645
|
# Clear series
|
|
1893
2646
|
lines.append("")
|
|
@@ -1963,7 +2716,7 @@ class TopLevelEmitter:
|
|
|
1963
2716
|
default = self._get_input_default(stmt.value)
|
|
1964
2717
|
base = self._source_defval_to_base_series(default)
|
|
1965
2718
|
title = self._get_input_title(stmt.value, var_name=stmt.name)
|
|
1966
|
-
cpp_val = f'get_input_source(
|
|
2719
|
+
cpp_val = f'get_input_source({self._input_key_literal(title)}, {base})[0]'
|
|
1967
2720
|
# A source var subscripted elsewhere in the script (e.g. ``src[1]``)
|
|
1968
2721
|
# is declared ``Series<double>``, not a scalar double, mirroring
|
|
1969
2722
|
# the normal per-bar path's ``{safe}.push({cpp_val})`` (see
|
|
@@ -2003,11 +2756,8 @@ class TopLevelEmitter:
|
|
|
2003
2756
|
if _ti in self._dead_ta_indices:
|
|
2004
2757
|
continue
|
|
2005
2758
|
if self._ta_site_uses_precalc(site):
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
for r in resolved:
|
|
2009
|
-
safe_resolved.append(r if self._is_compile_time_value(r) else "1")
|
|
2010
|
-
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)});")
|
|
2011
2761
|
|
|
2012
2762
|
for field_name in sorted(self.ctx.series_bar_fields):
|
|
2013
2763
|
lines.append(f" _s_{field_name}.clear();")
|
|
@@ -2024,23 +2774,6 @@ class TopLevelEmitter:
|
|
|
2024
2774
|
lines.append(" }")
|
|
2025
2775
|
lines.append("")
|
|
2026
2776
|
|
|
2027
|
-
#
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
lines.append(" BacktestEngine::run(bars, n);")
|
|
2031
|
-
lines.append(" }")
|
|
2032
|
-
lines.append("")
|
|
2033
|
-
lines.append(" void run(const Bar* input_bars, int n_input,")
|
|
2034
|
-
lines.append(" const std::string& input_tf,")
|
|
2035
|
-
lines.append(" const std::string& script_tf,")
|
|
2036
|
-
lines.append(" bool bar_magnifier = false,")
|
|
2037
|
-
lines.append(" int magnifier_samples = 4,")
|
|
2038
|
-
lines.append(" MagnifierDistribution magnifier_dist = MagnifierDistribution::ENDPOINTS) {")
|
|
2039
|
-
lines.append(" bool needs_dynamic = bar_magnifier || !input_tf.empty() || !script_tf.empty();")
|
|
2040
|
-
lines.append(" if (needs_dynamic) {")
|
|
2041
|
-
lines.append(" _use_precalc = false;")
|
|
2042
|
-
lines.append(" } else {")
|
|
2043
|
-
lines.append(" precalculate(input_bars, n_input);")
|
|
2044
|
-
lines.append(" }")
|
|
2045
|
-
lines.append(" BacktestEngine::run(input_bars, n_input, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist);")
|
|
2046
|
-
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.
|