@pineforge/codegen-pyodide 1.0.1 → 1.1.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/glue.py +2 -1
- package/package.json +1 -1
- package/pineforge_codegen/__init__.py +24 -9
- package/pineforge_codegen/analyzer/base.py +1 -0
- package/pineforge_codegen/analyzer/call_handlers.py +2 -0
- package/pineforge_codegen/analyzer/contracts.py +3 -0
- package/pineforge_codegen/codegen/checked_settings.py +272 -0
- package/pineforge_codegen/codegen/emit_top.py +44 -9
- package/pineforge_codegen/codegen/helpers.py +7 -1
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/external_requests.py +3 -0
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +2 -1
- package/pineforge_codegen-1.0.1.tar.gz +0 -0
package/glue.py
CHANGED
|
@@ -40,4 +40,5 @@ def transpile_json(source: str) -> str:
|
|
|
40
40
|
# A script that transpiled carries its warnings in the same entry format.
|
|
41
41
|
return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"],
|
|
42
42
|
"strategyParams": full["strategyParams"],
|
|
43
|
-
"diagnostics": _diagnostic_entries(full["diagnostics"])
|
|
43
|
+
"diagnostics": _diagnostic_entries(full["diagnostics"]),
|
|
44
|
+
"requests": full["requests"]})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pineforge/codegen-pyodide",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Gate-validated Pyodide payload for the PineScript v6 -> C++ transpiler: archive (run in Pyodide), unpacked source, introspected tables, and release metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -13,6 +13,7 @@ from .finite_ta_length import expand_finite_choice_extrema_lengths
|
|
|
13
13
|
from .library_inline import inline_libraries
|
|
14
14
|
from .limits import TimeBudget, check_ast_depth, check_source_size, ensure_recursion_headroom
|
|
15
15
|
from .pragmas import extract_pf_trace_pragmas
|
|
16
|
+
from .request_discovery import discover_requests, request_sites
|
|
16
17
|
from .security_contexts import specialize_security_contexts
|
|
17
18
|
from .block_locals import rename_block_locals
|
|
18
19
|
from .support_checker import check_support as _support_diagnostics
|
|
@@ -52,7 +53,9 @@ def _generate(pine_source: str, check_support: bool, filename: str,
|
|
|
52
53
|
the member of its name cannot hold gets a name of its own, and the
|
|
53
54
|
pipeline runs again (``block_locals``).
|
|
54
55
|
|
|
55
|
-
Returns ``(codegen, ctx, cpp, support_diagnostics)
|
|
56
|
+
Returns ``(codegen, ctx, cpp, support_diagnostics, sites)``: ``sites``
|
|
57
|
+
are the requests of another symbol's data the support checker lowered
|
|
58
|
+
(``request_discovery.request_sites``)."""
|
|
56
59
|
budget = None
|
|
57
60
|
clones: frozenset[str] = frozenset()
|
|
58
61
|
renamed: frozenset = frozenset()
|
|
@@ -67,6 +70,7 @@ def _generate(pine_source: str, check_support: bool, filename: str,
|
|
|
67
70
|
if any(d.level == Level.ERROR for d in support_diagnostics):
|
|
68
71
|
raise CompileError(support_diagnostics)
|
|
69
72
|
budget.check(phase=Phase.ANALYZER)
|
|
73
|
+
sites = request_sites(ast)
|
|
70
74
|
ast = lower_no_data_requests(ast)
|
|
71
75
|
ast = specialize_security_contexts(ast, filename=filename)
|
|
72
76
|
ast = bind_builtin_keywords(expand_finite_choice_extrema_lengths(ast))
|
|
@@ -86,18 +90,18 @@ def _generate(pine_source: str, check_support: bool, filename: str,
|
|
|
86
90
|
apart = gen.block_locals_needing_names - renamed
|
|
87
91
|
if apart:
|
|
88
92
|
renamed |= apart
|
|
89
|
-
del ast, ctx, gen, cpp
|
|
93
|
+
del ast, ctx, gen, cpp, sites
|
|
90
94
|
continue
|
|
91
95
|
needed = gen.session_functions_needing_clones
|
|
92
96
|
if not needed:
|
|
93
|
-
return gen, ctx, cpp, support_diagnostics
|
|
97
|
+
return gen, ctx, cpp, support_diagnostics, sites
|
|
94
98
|
# Only uncloned functions ask, so the set grows each time and the
|
|
95
99
|
# loop ends by the time every function that reads a flag at an
|
|
96
100
|
# offset is cloned.
|
|
97
101
|
if needed <= clones:
|
|
98
102
|
raise AssertionError("a cloned function asked for session clones again")
|
|
99
103
|
clones |= needed
|
|
100
|
-
del ast, ctx, gen, cpp
|
|
104
|
+
del ast, ctx, gen, cpp, sites
|
|
101
105
|
|
|
102
106
|
|
|
103
107
|
def transpile(pine_source: str, *, check_support: bool = True, filename: str = "<input>",
|
|
@@ -140,7 +144,8 @@ def transpile(pine_source: str, *, check_support: bool = True, filename: str = "
|
|
|
140
144
|
Returns:
|
|
141
145
|
Generated C++ source string.
|
|
142
146
|
"""
|
|
143
|
-
_gen, _ctx, cpp, _support = _generate(pine_source, check_support, filename,
|
|
147
|
+
_gen, _ctx, cpp, _support, _sites = _generate(pine_source, check_support, filename,
|
|
148
|
+
libraries)
|
|
144
149
|
return cpp
|
|
145
150
|
|
|
146
151
|
|
|
@@ -161,26 +166,36 @@ def transpile_full(pine_source: str, *, check_support: bool = True,
|
|
|
161
166
|
source order). Each has ``title`` / ``type`` /
|
|
162
167
|
``default`` and optionally ``min`` / ``max`` / ``step`` / ``options``
|
|
163
168
|
(omitted when the corresponding signature argument is absent or
|
|
164
|
-
references a non-const value).
|
|
165
|
-
:meth:`CodeGen.extract_input_manifest`.
|
|
169
|
+
references a non-const value); an ``input.symbol`` entry also has
|
|
170
|
+
``kind: "symbol"``. See :meth:`CodeGen.extract_input_manifest`.
|
|
166
171
|
- ``strategyParams``: the literal ``strategy(...)`` kwargs the analyzer
|
|
167
172
|
surfaced (e.g. ``initial_capital``, ``pyramiding``).
|
|
168
173
|
- ``diagnostics``: the warnings (:class:`~pineforge_codegen.errors.Diagnostic`,
|
|
169
174
|
``Level.WARNING``) the support checker and the analyzer raised for a
|
|
170
175
|
script that transpiled -- e.g. an approximated ``ta.vwap`` anchor. An
|
|
171
176
|
error still raises ``CompileError``, which carries the warnings too.
|
|
177
|
+
- ``requests``: every request site that reads another symbol's feed,
|
|
178
|
+
by line: its symbol and timeframe as registration computes them
|
|
179
|
+
before the first bar (``literal`` / ``input`` / ``computed`` /
|
|
180
|
+
``unresolvable``; ``literal`` / ``chart`` / ``input`` / ``computed``),
|
|
181
|
+
``lookahead``, ``gaps`` and ``ignore_invalid_symbol``. A site lowered
|
|
182
|
+
to ``na`` (its value reaches display sinks only) or in a helper
|
|
183
|
+
nothing reaches is not listed. See
|
|
184
|
+
:mod:`pineforge_codegen.request_discovery`.
|
|
172
185
|
|
|
173
186
|
Args mirror :func:`transpile`.
|
|
174
187
|
|
|
175
188
|
Returns:
|
|
176
189
|
``{"cpp": str, "inputs": list[dict], "strategyParams": dict,
|
|
177
|
-
"diagnostics": list[Diagnostic]}``.
|
|
190
|
+
"diagnostics": list[Diagnostic], "requests": list[dict]}``.
|
|
178
191
|
"""
|
|
179
|
-
gen, ctx, cpp, support_diagnostics = _generate(
|
|
192
|
+
gen, ctx, cpp, support_diagnostics, sites = _generate(
|
|
193
|
+
pine_source, check_support, filename, libraries)
|
|
180
194
|
return {
|
|
181
195
|
"cpp": cpp,
|
|
182
196
|
"inputs": gen.extract_input_manifest(),
|
|
183
197
|
"strategyParams": dict(ctx.strategy_params),
|
|
184
198
|
"diagnostics": [d for d in (*support_diagnostics, *ctx.diagnostics)
|
|
185
199
|
if d.level == Level.WARNING],
|
|
200
|
+
"requests": discover_requests(gen, ctx, sites),
|
|
186
201
|
}
|
|
@@ -876,6 +876,7 @@ class CallHandlers:
|
|
|
876
876
|
dead=bool(notes.get(DEAD_ANNOTATION)),
|
|
877
877
|
foreign=notes.get(LOWERING_ANNOTATION) == FEED_LOWERING,
|
|
878
878
|
ignore_invalid=node.kwargs.get("ignore_invalid_symbol"),
|
|
879
|
+
loc=node.loc,
|
|
879
880
|
))
|
|
880
881
|
|
|
881
882
|
return PineType.STRING if string_result else PineType.FLOAT
|
|
@@ -998,6 +999,7 @@ class CallHandlers:
|
|
|
998
999
|
is_lower_tf_array=True,
|
|
999
1000
|
context_resolved=context is not None,
|
|
1000
1001
|
dead=bool(notes.get(DEAD_ANNOTATION)),
|
|
1002
|
+
loc=node.loc,
|
|
1001
1003
|
))
|
|
1002
1004
|
|
|
1003
1005
|
# ``request.security_lower_tf`` returns an array; the value-level
|
|
@@ -196,6 +196,9 @@ class SecurityCallInfo:
|
|
|
196
196
|
# ``symbol`` string and ``ignore_invalid_symbol`` (the node, or None).
|
|
197
197
|
foreign: bool = False
|
|
198
198
|
ignore_invalid: Any = None
|
|
199
|
+
# The request call's source location (``request_discovery`` names the
|
|
200
|
+
# site by its line).
|
|
201
|
+
loc: Any = None
|
|
199
202
|
|
|
200
203
|
|
|
201
204
|
@dataclass
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Opt-in settings metadata, validation and exception-contained C exports."""
|
|
2
|
+
|
|
3
|
+
from ..ast_nodes import Identifier, MemberAccess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
|
|
7
|
+
lines.extend([
|
|
8
|
+
" bool _pf_setting_failed_ = false;",
|
|
9
|
+
" std::string _pf_setting_failure_;",
|
|
10
|
+
" void _pf_record_failure(const char* entrypoint, const char* message) noexcept {",
|
|
11
|
+
" try { last_error_ = entrypoint; last_error_ += \": \"; last_error_ += message; } catch (...) {}",
|
|
12
|
+
" }",
|
|
13
|
+
" void _pf_record_setting_failure(const char* entrypoint, const char* message) noexcept {",
|
|
14
|
+
" if (!_pf_setting_failed_) {",
|
|
15
|
+
" _pf_setting_failed_ = true;",
|
|
16
|
+
' try { _pf_setting_failure_ = entrypoint; _pf_setting_failure_ += ": "; _pf_setting_failure_ += message; } catch (...) {}',
|
|
17
|
+
" }",
|
|
18
|
+
" _pf_refuse_failed_setting(nullptr);",
|
|
19
|
+
" }",
|
|
20
|
+
" void _pf_require_settings_ok() const {",
|
|
21
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
22
|
+
' if (_pf_setting_failed_) throw ::pineforge::checked_settings::LatchedSettingsFailure(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
|
|
23
|
+
"#else",
|
|
24
|
+
' if (_pf_setting_failed_) throw std::runtime_error(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
|
|
25
|
+
"#endif",
|
|
26
|
+
" }",
|
|
27
|
+
" bool _pf_refuse_failed_setting(ReportC* out) noexcept {",
|
|
28
|
+
" if (!_pf_setting_failed_) return false;",
|
|
29
|
+
' try { last_error_ = _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_; } catch (...) {}',
|
|
30
|
+
" if (out) *out = ReportC{};",
|
|
31
|
+
" return true;",
|
|
32
|
+
" }",
|
|
33
|
+
])
|
|
34
|
+
inputs = []
|
|
35
|
+
for node, binding in emitter._global_input_calls_with_names():
|
|
36
|
+
func_name, namespace = emitter._resolve_callee(node.callee)
|
|
37
|
+
name = emitter._get_input_title(node, var_name=binding)
|
|
38
|
+
key = emitter._input_key_literal(name)
|
|
39
|
+
default = emitter._get_input_default(node)
|
|
40
|
+
getter = emitter._input_getter_for_call(node, func_name, namespace)
|
|
41
|
+
default_cpp = emitter._visit_expr(default) if default is not None else "0"
|
|
42
|
+
default_cpp = emitter._coerce_string_input_default(getter, default_cpp)
|
|
43
|
+
value_type = {
|
|
44
|
+
"get_input_int": "int", "get_input_int64": "int",
|
|
45
|
+
"get_input_double": "float", "get_input_bool": "bool",
|
|
46
|
+
"get_input_string": "string",
|
|
47
|
+
}[getter]
|
|
48
|
+
kind = emitter._FORM_TYPE.get(func_name, value_type) if namespace == "input" else value_type
|
|
49
|
+
options = []
|
|
50
|
+
option_values = []
|
|
51
|
+
supported = "true"
|
|
52
|
+
default_serialized = None
|
|
53
|
+
names, merged = emitter._merged_args(node, func_name, namespace)
|
|
54
|
+
arguments = dict(zip(names or [], merged))
|
|
55
|
+
arguments.update(node.kwargs)
|
|
56
|
+
if emitter._is_source_input(node):
|
|
57
|
+
value_type = "source"
|
|
58
|
+
source = emitter._source_defval_to_base_series(default)[5:-1]
|
|
59
|
+
default_cpp = emitter._input_key_literal(source)
|
|
60
|
+
effective = f'(inputs_.count({key}) ? inputs_.at({key}) : {default_cpp})'
|
|
61
|
+
options = [emitter._input_key_literal(value)
|
|
62
|
+
for value in sorted(emitter._NATIVE_SOURCE_SERIES)]
|
|
63
|
+
elif namespace == "input" and func_name == "enum":
|
|
64
|
+
value_type = "enum"
|
|
65
|
+
declared = getattr(arguments.get("options"), "elements", None)
|
|
66
|
+
enum_members = ([default] if isinstance(default, MemberAccess) else []) + list(declared or [])
|
|
67
|
+
enum_names = {member.object.name for member in enum_members
|
|
68
|
+
if isinstance(member, MemberAccess) and isinstance(member.object, Identifier)}
|
|
69
|
+
if (len(enum_names) == 1 and enum_members
|
|
70
|
+
and all(isinstance(member, MemberAccess) and isinstance(member.object, Identifier)
|
|
71
|
+
for member in enum_members)):
|
|
72
|
+
enum_name = next(iter(enum_names))
|
|
73
|
+
members = emitter._enum_defs.get(enum_name, [])
|
|
74
|
+
selected = [member.member for member in declared] if declared is not None else members
|
|
75
|
+
options = [emitter._input_key_literal(f"{enum_name}.{member}")
|
|
76
|
+
for member in selected]
|
|
77
|
+
option_values = [emitter._input_key_literal(str(members.index(member)))
|
|
78
|
+
for member in selected if member in members]
|
|
79
|
+
supported = "true" if options and len(options) == len(option_values) else "false"
|
|
80
|
+
else:
|
|
81
|
+
supported = "false"
|
|
82
|
+
if not isinstance(default, MemberAccess):
|
|
83
|
+
supported = "false"
|
|
84
|
+
default_serialized = 'std::string("na")'
|
|
85
|
+
effective = f'(inputs_.count({key}) ? inputs_.at({key}) : std::string("na"))'
|
|
86
|
+
else:
|
|
87
|
+
effective = f'::pineforge::checked_settings::number({getter}({key}, {default_cpp}))'
|
|
88
|
+
else:
|
|
89
|
+
declared = getattr(arguments.get("options"), "elements", None)
|
|
90
|
+
if declared:
|
|
91
|
+
for option in declared:
|
|
92
|
+
option_cpp = emitter._visit_expr(option)
|
|
93
|
+
options.append(option_cpp if getter == "get_input_string" else
|
|
94
|
+
f'::pineforge::checked_settings::number({option_cpp})')
|
|
95
|
+
expression = f'{getter}({key}, {default_cpp})'
|
|
96
|
+
effective = (expression if getter == "get_input_string" else
|
|
97
|
+
f'::pineforge::checked_settings::number({expression})')
|
|
98
|
+
if default_serialized is None:
|
|
99
|
+
default_serialized = (default_cpp if value_type in ("string", "source") else
|
|
100
|
+
f'::pineforge::checked_settings::number({default_cpp})')
|
|
101
|
+
constraints = [emitter._visit_expr(arguments[name]) if arguments.get(name) is not None
|
|
102
|
+
else "std::numeric_limits<double>::quiet_NaN()"
|
|
103
|
+
for name in ("minval", "maxval", "step")]
|
|
104
|
+
metadata = (f'{{{key}, "{value_type}", {default_serialized}, '
|
|
105
|
+
f'{{{", ".join(options)}}}, {", ".join(constraints)}, '
|
|
106
|
+
f'{64 if getter == "get_input_int64" else 32}, {supported}, '
|
|
107
|
+
f'{{{", ".join(option_values)}}}, "{kind}"}}')
|
|
108
|
+
inputs.append((metadata, effective))
|
|
109
|
+
|
|
110
|
+
overrides = [
|
|
111
|
+
("initial_capital", "float", "initial_capital", "initial_capital", "0.0"),
|
|
112
|
+
("commission_value", "float", "commission_value", "commission_value", "0.0"),
|
|
113
|
+
("default_qty_value", "float", "default_qty_value", "default_qty_value", "0.0"),
|
|
114
|
+
("pyramiding", "int", "pyramiding", "pyramiding", "0.0"),
|
|
115
|
+
("slippage", "int", "slippage", "slippage", "0.0"),
|
|
116
|
+
("process_orders_on_close", "bool", "process_orders_on_close", "process_orders_on_close", "nan"),
|
|
117
|
+
("calc_on_order_fills", "bool", "calc_on_order_fills", "calc_on_order_fills", "nan"),
|
|
118
|
+
("close_entries_rule", "string", "close_entries_rule_any", "close_entries_rule", "nan"),
|
|
119
|
+
("default_qty_type", "string", "default_qty_type", "default_qty_type", "nan"),
|
|
120
|
+
("commission_type", "string", "commission_type", "commission_type", "nan"),
|
|
121
|
+
]
|
|
122
|
+
enum_options = {
|
|
123
|
+
"close_entries_rule": ["FIFO", "ANY"],
|
|
124
|
+
"default_qty_type": ["fixed", "percent_of_equity", "cash"],
|
|
125
|
+
"commission_type": ["percent", "cash_per_order", "cash_per_contract"],
|
|
126
|
+
}
|
|
127
|
+
lines.extend([
|
|
128
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
129
|
+
" static pineforge::source::PineStrategyConfig _pf_settings_declared_config() {",
|
|
130
|
+
" pineforge::source::PineStrategyConfig cfg{};",
|
|
131
|
+
])
|
|
132
|
+
lines.extend(statement for statement in constructor if statement.startswith(" cfg."))
|
|
133
|
+
lines.extend([
|
|
134
|
+
" return cfg;", " }",
|
|
135
|
+
" std::vector<::pineforge::checked_settings::Setting> _pf_settings_inputs() const {",
|
|
136
|
+
" return {",
|
|
137
|
+
])
|
|
138
|
+
lines.extend(f" {metadata}," for metadata, _effective in inputs)
|
|
139
|
+
lines.extend([
|
|
140
|
+
" };", " }",
|
|
141
|
+
" std::vector<::pineforge::checked_settings::Setting> _pf_settings_overrides() const {",
|
|
142
|
+
" const double _pf_nan = std::numeric_limits<double>::quiet_NaN();",
|
|
143
|
+
" const auto _pf_defaults = _pf_settings_declared_config();",
|
|
144
|
+
" return {",
|
|
145
|
+
])
|
|
146
|
+
override_effective = []
|
|
147
|
+
for name, value_type, config_field, override_field, minimum in overrides:
|
|
148
|
+
raw = f'config_.{config_field}'
|
|
149
|
+
declared_default = f'_pf_defaults.{config_field}'
|
|
150
|
+
overridden = f'override_.{override_field}'
|
|
151
|
+
selected = (f'(std::isnan({overridden}) ? {raw} : {overridden})'
|
|
152
|
+
if value_type == "float" else
|
|
153
|
+
f'({overridden} < 0 ? {raw} : {overridden})')
|
|
154
|
+
if name in enum_options:
|
|
155
|
+
options = enum_options[name]
|
|
156
|
+
default_value = f'_pf_{name}_word({declared_default})'
|
|
157
|
+
effective = f'_pf_{name}_word({selected})'
|
|
158
|
+
else:
|
|
159
|
+
options = []
|
|
160
|
+
default_value = f'::pineforge::checked_settings::number({declared_default})'
|
|
161
|
+
effective = f'::pineforge::checked_settings::number({selected})'
|
|
162
|
+
if value_type == "bool":
|
|
163
|
+
effective = f'::pineforge::checked_settings::number(static_cast<bool>({selected}))'
|
|
164
|
+
option_cpp = ", ".join(emitter._input_key_literal(option) for option in options)
|
|
165
|
+
floor = "_pf_nan" if minimum == "nan" else minimum
|
|
166
|
+
lines.append(f' {{"{name}", "{value_type}", {default_value}, '
|
|
167
|
+
f'{{{option_cpp}}}, {floor}}},')
|
|
168
|
+
override_effective.append(effective)
|
|
169
|
+
lines.extend([" };", " }"])
|
|
170
|
+
for name, options in enum_options.items():
|
|
171
|
+
lines.append(f' static std::string _pf_{name}_word(int _pf_value) {{')
|
|
172
|
+
for index, option in enumerate(options):
|
|
173
|
+
lines.append(f' if (_pf_value == {index}) return "{option}";')
|
|
174
|
+
lines.extend([' return "invalid";', ' }'])
|
|
175
|
+
lines.extend([
|
|
176
|
+
" void _pf_set_input_checked(const std::string& _pf_key, const std::string& _pf_value) {",
|
|
177
|
+
" if (_pf_refuse_failed_setting(nullptr)) throw ::pineforge::checked_settings::Error{PF_SETTINGS_RUN_FAILED, last_error_.c_str()};",
|
|
178
|
+
' ::pineforge::checked_settings::require(script_bars_processed() == 0 && stream_phase_ == StreamPhase::IDLE, "settings are frozen after execution begins", PF_SETTINGS_UNSUPPORTED);',
|
|
179
|
+
" const auto _pf_inputs = _pf_settings_inputs();",
|
|
180
|
+
" const ::pineforge::checked_settings::Setting* _pf_match = nullptr;",
|
|
181
|
+
" for (const auto& _pf_input : _pf_inputs) {",
|
|
182
|
+
" if (_pf_input.name != _pf_key) continue;",
|
|
183
|
+
' ::pineforge::checked_settings::require(_pf_match == nullptr, "ambiguous input key", PF_SETTINGS_UNSUPPORTED);',
|
|
184
|
+
" _pf_match = &_pf_input;",
|
|
185
|
+
" }",
|
|
186
|
+
' ::pineforge::checked_settings::require(_pf_match != nullptr, "unknown input key");',
|
|
187
|
+
" const auto _pf_canonical = ::pineforge::checked_settings::validate(*_pf_match, _pf_value);",
|
|
188
|
+
" set_input(_pf_key, _pf_canonical);",
|
|
189
|
+
' ::pineforge::checked_settings::require(inputs_.count(_pf_key) && inputs_.at(_pf_key) == _pf_canonical, "input was not installed", PF_SETTINGS_UNSUPPORTED);',
|
|
190
|
+
" }",
|
|
191
|
+
" void _pf_set_override_checked(const std::string& _pf_key, const std::string& _pf_value) {",
|
|
192
|
+
" if (_pf_refuse_failed_setting(nullptr)) throw ::pineforge::checked_settings::Error{PF_SETTINGS_RUN_FAILED, last_error_.c_str()};",
|
|
193
|
+
' ::pineforge::checked_settings::require(script_bars_processed() == 0 && stream_phase_ == StreamPhase::IDLE, "settings are frozen after execution begins", PF_SETTINGS_UNSUPPORTED);',
|
|
194
|
+
" for (const auto& _pf_override : _pf_settings_overrides()) {",
|
|
195
|
+
" if (_pf_override.name != _pf_key) continue;",
|
|
196
|
+
" auto _pf_alias = _pf_value;",
|
|
197
|
+
])
|
|
198
|
+
for name, options in enum_options.items():
|
|
199
|
+
lines.append(f' if (_pf_key == "{name}") {{')
|
|
200
|
+
for index, option in enumerate(options):
|
|
201
|
+
aliases = [str(index)]
|
|
202
|
+
if name == "close_entries_rule":
|
|
203
|
+
aliases.append(option.lower())
|
|
204
|
+
else:
|
|
205
|
+
prefix = "strategy.commission." if name == "commission_type" else "strategy."
|
|
206
|
+
aliases.append(prefix + option)
|
|
207
|
+
condition = " || ".join(f'_pf_value == "{alias}"' for alias in aliases)
|
|
208
|
+
lines.append(f' if ({condition}) _pf_alias = "{option}";')
|
|
209
|
+
lines.append(" }")
|
|
210
|
+
lines.extend([
|
|
211
|
+
" const auto _pf_canonical = ::pineforge::checked_settings::validate(_pf_override, _pf_alias);",
|
|
212
|
+
" set_strategy_override(_pf_key, _pf_canonical);",
|
|
213
|
+
" return;",
|
|
214
|
+
" }",
|
|
215
|
+
' throw ::pineforge::checked_settings::Error{PF_SETTINGS_INVALID_ARGUMENT, "unknown override key"};',
|
|
216
|
+
" }",
|
|
217
|
+
" std::string _pf_settings_receipt() const {",
|
|
218
|
+
' ::pineforge::checked_settings::require(!_pf_setting_failed_, _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_.c_str(), PF_SETTINGS_RUN_FAILED);',
|
|
219
|
+
' std::string _pf_document = "{\\\"version\\\":1,\\\"inputs\\\":[";',
|
|
220
|
+
" const auto _pf_inputs = _pf_settings_inputs();",
|
|
221
|
+
])
|
|
222
|
+
for index, (_metadata, effective) in enumerate(inputs):
|
|
223
|
+
if index:
|
|
224
|
+
lines.append(" _pf_document += ',';")
|
|
225
|
+
lines.append(f" _pf_document += ::pineforge::checked_settings::describe(_pf_inputs[{index}], {effective});")
|
|
226
|
+
lines.extend([
|
|
227
|
+
' _pf_document += "],\\\"overrides\\\":[";',
|
|
228
|
+
" const auto _pf_overrides = _pf_settings_overrides();",
|
|
229
|
+
])
|
|
230
|
+
for index, effective in enumerate(override_effective):
|
|
231
|
+
if index:
|
|
232
|
+
lines.append(" _pf_document += ',';")
|
|
233
|
+
lines.append(f" _pf_document += ::pineforge::checked_settings::describe(_pf_overrides[{index}], {effective});")
|
|
234
|
+
lines.extend([' return _pf_document + "]}";', " }", "#endif"])
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def emit_settings_exports(lines: list[str]) -> None:
|
|
238
|
+
lines.extend([
|
|
239
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
240
|
+
" uint32_t strategy_settings_api_version(void) { return PF_SETTINGS_API_VERSION; }",
|
|
241
|
+
" int strategy_create_checked(const char* params_json, void** out, char* error, size_t error_capacity) {",
|
|
242
|
+
" if (out) *out = nullptr;",
|
|
243
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
244
|
+
' ::pineforge::checked_settings::require(out != nullptr, "strategy output pointer is null");',
|
|
245
|
+
' ::pineforge::checked_settings::require(!params_json || !*params_json, "params_json is reserved; use checked setters", PF_SETTINGS_UNSUPPORTED);',
|
|
246
|
+
" *out = new GeneratedStrategy();",
|
|
247
|
+
" });",
|
|
248
|
+
" }",
|
|
249
|
+
])
|
|
250
|
+
for kind in ("input", "override"):
|
|
251
|
+
lines.extend([
|
|
252
|
+
f" int strategy_set_{kind}_checked(void* s, const char* key, const char* value, char* error, size_t error_capacity) {{",
|
|
253
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
254
|
+
' ::pineforge::checked_settings::require(s && key && value, "null strategy, key or value");',
|
|
255
|
+
f" static_cast<GeneratedStrategy*>(s)->_pf_set_{kind}_checked(key, value);",
|
|
256
|
+
" });", " }",
|
|
257
|
+
])
|
|
258
|
+
lines.extend([
|
|
259
|
+
" int strategy_get_effective_settings(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
|
|
260
|
+
" if (required) *required = 0;",
|
|
261
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
262
|
+
' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
|
|
263
|
+
" ::pineforge::checked_settings::receipt(static_cast<GeneratedStrategy*>(s)->_pf_settings_receipt(), json, capacity, required);",
|
|
264
|
+
" });", " }",
|
|
265
|
+
" int run_backtest_full_checked(void* s, Bar* bars, int n, const char* input_tf, const char* script_tf, int bar_magnifier, int magnifier_samples, int magnifier_dist, ReportC* out, char* error, size_t error_capacity) {",
|
|
266
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
267
|
+
' ::pineforge::checked_settings::require(s && out && n >= 0 && (n == 0 || bars), "invalid batch arguments");',
|
|
268
|
+
" _pf_run_backtest_full_impl(s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, out);",
|
|
269
|
+
" const auto& _pf_error = static_cast<GeneratedStrategy*>(s)->last_error();",
|
|
270
|
+
' ::pineforge::checked_settings::require(_pf_error.empty(), _pf_error.c_str(), PF_SETTINGS_RUN_FAILED);',
|
|
271
|
+
" });", " }", "#endif",
|
|
272
|
+
])
|
|
@@ -130,6 +130,8 @@ class TopLevelEmitter:
|
|
|
130
130
|
)
|
|
131
131
|
lines.append('#include <pineforge/source/pine_strategy_host.hpp>')
|
|
132
132
|
lines.append('#include <pineforge/ta.hpp>')
|
|
133
|
+
lines.extend(['#if __has_include(<pineforge/checked_settings.hpp>)',
|
|
134
|
+
'#include <pineforge/checked_settings.hpp>', '#endif'])
|
|
133
135
|
if self._ta_uses_dynamic_lengths():
|
|
134
136
|
lines.append('#include <pineforge/source/pine_ta_length.hpp>')
|
|
135
137
|
lines.append('#include <pineforge/math.hpp>')
|
|
@@ -598,6 +600,7 @@ class TopLevelEmitter:
|
|
|
598
600
|
'#error "Generated lifecycle reset requires a matching PineForge engine; rebuild with script-run preparation support"',
|
|
599
601
|
"#endif",
|
|
600
602
|
" void prepare_script_run(const Bar* bars, int n, bool allow_precalculation) override {",
|
|
603
|
+
" _pf_require_settings_ok();",
|
|
601
604
|
" _pf_script_state_checkpoint_.reset();",
|
|
602
605
|
])
|
|
603
606
|
seen: set[str] = set()
|
|
@@ -1277,6 +1280,8 @@ class TopLevelEmitter:
|
|
|
1277
1280
|
lines.append(" }")
|
|
1278
1281
|
lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
|
|
1279
1282
|
lines.append(" }")
|
|
1283
|
+
from .checked_settings import emit_settings_members
|
|
1284
|
+
emit_settings_members(self, lines, ctor_body)
|
|
1280
1285
|
|
|
1281
1286
|
if self._uses_recorded_requests():
|
|
1282
1287
|
lines.extend([
|
|
@@ -1889,24 +1894,38 @@ class TopLevelEmitter:
|
|
|
1889
1894
|
def _emit_extern_c(self, lines: list[str]) -> None:
|
|
1890
1895
|
lines.append('extern "C" {')
|
|
1891
1896
|
lines.append(" void* strategy_create(const char* params_json) {")
|
|
1892
|
-
lines.append(" return new GeneratedStrategy();")
|
|
1897
|
+
lines.append(" try { return new GeneratedStrategy(); } catch (...) { return nullptr; }")
|
|
1893
1898
|
lines.append(" }")
|
|
1894
1899
|
lines.append(" void run_backtest(void* s, Bar* bars, int n, ReportC* out) {")
|
|
1900
|
+
lines.append(" try {")
|
|
1895
1901
|
lines.append(" auto* strat = static_cast<GeneratedStrategy*>(s);")
|
|
1902
|
+
lines.append("#ifndef PF_SETTINGS_API_VERSION")
|
|
1903
|
+
lines.append(" if (strat->_pf_refuse_failed_setting(out)) return;")
|
|
1904
|
+
lines.append("#endif")
|
|
1896
1905
|
if self._security_calls:
|
|
1897
1906
|
# If there are security calls, use the full run path. Pass empty strings
|
|
1898
1907
|
# so the C++ runtime auto-detects input_tf from bar timestamps.
|
|
1899
1908
|
lines.append(' strat->run(bars, n, "", "", false, 4, MagnifierDistribution::ENDPOINTS);')
|
|
1900
1909
|
else:
|
|
1901
1910
|
lines.append(" strat->run(bars, n);")
|
|
1902
|
-
lines.append(" strat->fill_report(out);")
|
|
1911
|
+
lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
|
|
1912
|
+
lines.append(' } catch (const std::exception& _pf_error) {')
|
|
1913
|
+
lines.append(' if (out) *out = ReportC{};')
|
|
1914
|
+
lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", _pf_error.what());')
|
|
1915
|
+
lines.append(' } catch (...) {')
|
|
1916
|
+
lines.append(' if (out) *out = ReportC{};')
|
|
1917
|
+
lines.append(' if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest", "unknown C++ exception");')
|
|
1918
|
+
lines.append(' }')
|
|
1903
1919
|
lines.append(" }")
|
|
1904
|
-
lines.append(" void
|
|
1920
|
+
lines.append(" static void _pf_run_backtest_full_impl(void* s, Bar* bars, int n,")
|
|
1905
1921
|
lines.append(' const char* input_tf, const char* script_tf,')
|
|
1906
1922
|
lines.append(" int bar_magnifier, int magnifier_samples,")
|
|
1907
1923
|
lines.append(" int magnifier_dist,")
|
|
1908
1924
|
lines.append(" ReportC* out) {")
|
|
1909
1925
|
lines.append(' auto* strat = static_cast<GeneratedStrategy*>(s);')
|
|
1926
|
+
lines.append("#ifndef PF_SETTINGS_API_VERSION")
|
|
1927
|
+
lines.append(" if (strat->_pf_refuse_failed_setting(out)) return;")
|
|
1928
|
+
lines.append("#endif")
|
|
1910
1929
|
lines.append(' std::string itf = input_tf ? input_tf : "";')
|
|
1911
1930
|
lines.append(' std::string stf = script_tf ? script_tf : "";')
|
|
1912
1931
|
if self._security_calls:
|
|
@@ -1943,26 +1962,42 @@ class TopLevelEmitter:
|
|
|
1943
1962
|
lines.append(" strat->run(bars, n, itf, stf, bar_magnifier != 0, magnifier_samples,")
|
|
1944
1963
|
lines.append(" static_cast<MagnifierDistribution>(magnifier_dist));")
|
|
1945
1964
|
lines.append(" }")
|
|
1946
|
-
lines.append(" strat->fill_report(out);")
|
|
1965
|
+
lines.append(" if (!strat->_pf_refuse_failed_setting(out)) strat->fill_report(out);")
|
|
1947
1966
|
lines.append(" }")
|
|
1967
|
+
lines.extend([
|
|
1968
|
+
" void run_backtest_full(void* s, Bar* bars, int n, const char* input_tf, const char* script_tf,",
|
|
1969
|
+
" int bar_magnifier, int magnifier_samples, int magnifier_dist, ReportC* out) {",
|
|
1970
|
+
" try { _pf_run_backtest_full_impl(s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, out); }",
|
|
1971
|
+
' catch (const std::exception& _pf_error) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", _pf_error.what()); }',
|
|
1972
|
+
' catch (...) { if (out) *out = ReportC{}; if (s) static_cast<GeneratedStrategy*>(s)->_pf_record_failure("run_backtest_full", "unknown C++ exception"); }',
|
|
1973
|
+
" }",
|
|
1974
|
+
])
|
|
1948
1975
|
lines.append(" void strategy_free(void* s) {")
|
|
1949
|
-
lines.append(" delete static_cast<GeneratedStrategy*>(s);")
|
|
1976
|
+
lines.append(" try { delete static_cast<GeneratedStrategy*>(s); } catch (...) {}")
|
|
1950
1977
|
lines.append(" }")
|
|
1951
1978
|
lines.append(" void report_free(ReportC* report) {")
|
|
1952
|
-
lines.append(" BacktestEngine::free_report(report);")
|
|
1979
|
+
lines.append(" try { BacktestEngine::free_report(report); } catch (...) {}")
|
|
1953
1980
|
lines.append(" }")
|
|
1954
1981
|
lines.append(" void strategy_set_input(void* s, const char* key, const char* value) {")
|
|
1955
1982
|
lines.append(" if (!s || !key || !value) return;")
|
|
1956
|
-
lines.append(" static_cast<GeneratedStrategy*>(s)->set_input(key, value);")
|
|
1983
|
+
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_input(key, value); }")
|
|
1984
|
+
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_input", _pf_error.what()); }')
|
|
1985
|
+
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_input", "unknown C++ exception"); }')
|
|
1957
1986
|
lines.append(" }")
|
|
1958
1987
|
lines.append(" void strategy_set_override(void* s, const char* key, const char* value) {")
|
|
1959
1988
|
lines.append(" if (!s || !key || !value) return;")
|
|
1960
|
-
lines.append(" static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value);")
|
|
1989
|
+
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_strategy_override(key, value); }")
|
|
1990
|
+
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_override", _pf_error.what()); }')
|
|
1991
|
+
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_override", "unknown C++ exception"); }')
|
|
1961
1992
|
lines.append(" }")
|
|
1962
1993
|
lines.append(" void strategy_set_magnifier_volume_weighted(void* s, int on) {")
|
|
1963
1994
|
lines.append(" if (!s) return;")
|
|
1964
|
-
lines.append(" static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0);")
|
|
1995
|
+
lines.append(" try { static_cast<GeneratedStrategy*>(s)->set_magnifier_volume_weighted(on != 0); }")
|
|
1996
|
+
lines.append(' catch (const std::exception& _pf_error) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_magnifier_volume_weighted", _pf_error.what()); }')
|
|
1997
|
+
lines.append(' catch (...) { static_cast<GeneratedStrategy*>(s)->_pf_record_setting_failure("strategy_set_magnifier_volume_weighted", "unknown C++ exception"); }')
|
|
1965
1998
|
lines.append(" }")
|
|
1999
|
+
from .checked_settings import emit_settings_exports
|
|
2000
|
+
emit_settings_exports(lines)
|
|
1966
2001
|
if self._declares_bar_magnifier():
|
|
1967
2002
|
# TradingView runs a script that declares use_bar_magnifier = true
|
|
1968
2003
|
# on its bar magnifier; the host reads this export to run it on
|
|
@@ -314,7 +314,13 @@ CPP_EMITTER_NAMES = frozenset("""
|
|
|
314
314
|
_pf_drawing_na _pf_collect_drawings _pf_collect_lines_ _pf_collect_boxes_
|
|
315
315
|
_pf_collect_labels_ _pf_new _pf_held
|
|
316
316
|
get_input_int get_input_float get_input_bool get_input_string
|
|
317
|
-
|
|
317
|
+
_pf_record_failure _pf_refuse_failed_setting _pf_settings_declared_config
|
|
318
|
+
_pf_record_setting_failure _pf_require_settings_ok
|
|
319
|
+
_pf_setting_failed_ _pf_setting_failure_
|
|
320
|
+
_pf_settings_inputs _pf_settings_overrides _pf_set_input_checked
|
|
321
|
+
_pf_set_override_checked _pf_settings_receipt
|
|
322
|
+
_pf_close_entries_rule_word _pf_default_qty_type_word _pf_commission_type_word
|
|
323
|
+
checked_settings trace is_na na nz fixnan
|
|
318
324
|
""".split())
|
|
319
325
|
|
|
320
326
|
# The generated history members, ``_<kind>_<n>`` (codegen/base.py
|
|
@@ -8,6 +8,7 @@ strategy class, so ``_safe_name`` renames it.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
HOST_MEMBER_NAMES = frozenset({
|
|
11
|
+
"StreamPhase",
|
|
11
12
|
"_src_close_",
|
|
12
13
|
"_src_high_",
|
|
13
14
|
"_src_hl2_",
|
|
@@ -50,6 +51,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
50
51
|
"closed_trade_profit_percent",
|
|
51
52
|
"closed_trade_size",
|
|
52
53
|
"commit_script_state",
|
|
54
|
+
"config_",
|
|
53
55
|
"configure_pine_strategy",
|
|
54
56
|
"configure_security_evaluators",
|
|
55
57
|
"count_losstrades",
|
|
@@ -74,9 +76,12 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
74
76
|
"history_advances_new_bar",
|
|
75
77
|
"initial_capital_",
|
|
76
78
|
"input_tf_",
|
|
79
|
+
"inputs_",
|
|
77
80
|
"is_first_tick",
|
|
78
81
|
"is_last_tick_",
|
|
79
82
|
"last_bar_time_",
|
|
83
|
+
"last_error",
|
|
84
|
+
"last_error_",
|
|
80
85
|
"main_period",
|
|
81
86
|
"margin_liquidation_price",
|
|
82
87
|
"max_contracts_held_all",
|
|
@@ -104,6 +109,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
104
109
|
"open_trade_profit_percent",
|
|
105
110
|
"open_trade_size",
|
|
106
111
|
"open_trades_capital_held",
|
|
112
|
+
"override_",
|
|
107
113
|
"pine_bar_index",
|
|
108
114
|
"pine_last_bar_index",
|
|
109
115
|
"pine_session_ismarket",
|
|
@@ -123,6 +129,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
123
129
|
"restore_script_state",
|
|
124
130
|
"round_to_mintick",
|
|
125
131
|
"run",
|
|
132
|
+
"script_bars_processed",
|
|
126
133
|
"script_tf_",
|
|
127
134
|
"security_column_value",
|
|
128
135
|
"security_eval_states_",
|
|
@@ -151,6 +158,7 @@ HOST_MEMBER_NAMES = frozenset({
|
|
|
151
158
|
"strategy_exit",
|
|
152
159
|
"strategy_exit_cancel_bracket",
|
|
153
160
|
"strategy_order",
|
|
161
|
+
"stream_phase_",
|
|
154
162
|
"symbol_facts_",
|
|
155
163
|
"symbol_feeds_",
|
|
156
164
|
"syminfo_",
|
|
@@ -85,14 +85,43 @@ class InputHelper:
|
|
|
85
85
|
while merged and merged[-1] is None:
|
|
86
86
|
merged.pop()
|
|
87
87
|
if merged and merged[0] is not None:
|
|
88
|
-
return merged[0]
|
|
88
|
+
return self._fold_enum_input_default(node, merged[0])
|
|
89
89
|
return None
|
|
90
90
|
if node.args:
|
|
91
|
-
return node.args[0]
|
|
91
|
+
return self._fold_enum_input_default(node, node.args[0])
|
|
92
92
|
if "defval" in node.kwargs:
|
|
93
|
-
return node.kwargs["defval"]
|
|
93
|
+
return self._fold_enum_input_default(node, node.kwargs["defval"])
|
|
94
94
|
return None
|
|
95
95
|
|
|
96
|
+
def _fold_enum_input_default(self, node: FuncCall, default):
|
|
97
|
+
"""Resolve an enum defval's immutable, source-ordered initializer."""
|
|
98
|
+
func_name, namespace = self._resolve_callee(node.callee)
|
|
99
|
+
if namespace != "input" or func_name != "enum" or not isinstance(default, Identifier):
|
|
100
|
+
return default
|
|
101
|
+
declarations = {}
|
|
102
|
+
reassigned = self._find_reassigned_vars()
|
|
103
|
+
for statement in self.ctx.ast.body:
|
|
104
|
+
if any(candidate is node for candidate in self._walk_ast(statement)):
|
|
105
|
+
break
|
|
106
|
+
if isinstance(statement, VarDecl):
|
|
107
|
+
declarations.setdefault(statement.name, []).append(statement)
|
|
108
|
+
candidate = default
|
|
109
|
+
visited = set()
|
|
110
|
+
while isinstance(candidate, Identifier) and candidate.name not in visited:
|
|
111
|
+
visited.add(candidate.name)
|
|
112
|
+
bindings = declarations.get(candidate.name, [])
|
|
113
|
+
if len(bindings) != 1 or candidate.name in reassigned:
|
|
114
|
+
return default
|
|
115
|
+
declaration = bindings[0]
|
|
116
|
+
if declaration.is_var or declaration.is_varip:
|
|
117
|
+
return default
|
|
118
|
+
candidate = declaration.value
|
|
119
|
+
if isinstance(candidate, MemberAccess) and isinstance(candidate.object, Identifier):
|
|
120
|
+
members = self._enum_defs.get(candidate.object.name, [])
|
|
121
|
+
if candidate.member in members:
|
|
122
|
+
return candidate
|
|
123
|
+
return default
|
|
124
|
+
|
|
96
125
|
def _input_default_value(self, node: FuncCall) -> tuple[bool, object]:
|
|
97
126
|
"""``(True, value)`` when the defval is the constant a ``v = input...``
|
|
98
127
|
binding records as ``v``'s known value -- a number, bool or string
|
|
@@ -527,6 +556,10 @@ class InputHelper:
|
|
|
527
556
|
"type": form_type,
|
|
528
557
|
"default": default_val,
|
|
529
558
|
}
|
|
559
|
+
if namespace == "input" and func_name == "symbol":
|
|
560
|
+
# A symbol string, which the type does not tell from a string's:
|
|
561
|
+
# the requests of another symbol key their feeds on its value.
|
|
562
|
+
entry["kind"] = "symbol"
|
|
530
563
|
# Pull min/max/step/options by signature param name; emit only
|
|
531
564
|
# const literals so the override form never references a runtime
|
|
532
565
|
# value it can't reproduce.
|
|
@@ -858,6 +858,9 @@ def unpin_requests(program: Program, reasons: dict[int, str]) -> None:
|
|
|
858
858
|
lowered.annotations = {**(lowered.annotations or {}),
|
|
859
859
|
UNPINNED_ANNOTATION: marker["message"]}
|
|
860
860
|
swaps[request_id] = lowered
|
|
861
|
+
# The request leaves the program; its lowering is the deferred
|
|
862
|
+
# refusal's now (``request_discovery`` lists it so).
|
|
863
|
+
request.annotations = {**(request.annotations or {}), LOWERING_ANNOTATION: "unpinned"}
|
|
861
864
|
warnings.append(pass_warning(
|
|
862
865
|
request,
|
|
863
866
|
f"{spell_call(request)}: no data is pinned for this request; the run stops with "
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""The other symbols' data a script requests, listed before it runs.
|
|
2
|
+
|
|
3
|
+
``transpile_full(...)["requests"]`` names every request site that reads
|
|
4
|
+
another symbol's feed, so a host can fetch the bars a run needs before it
|
|
5
|
+
starts it. A run supplies another symbol's bars as one feed per (symbol
|
|
6
|
+
string, timeframe) and the engine looks a site's feed up byte for byte
|
|
7
|
+
(``codegen/emit_top.py``, ``_emit_foreign_security_registration``), so each
|
|
8
|
+
entry states both the way registration computes them:
|
|
9
|
+
|
|
10
|
+
{"line": 7, "fn": "request.security",
|
|
11
|
+
"symbol": {"kind": "input", "title": "Other symbol", "default": "BINANCE:ETHUSDT"},
|
|
12
|
+
"timeframe": {"kind": "chart"},
|
|
13
|
+
"lookahead": false, "gaps": false, "ignore_invalid_symbol": false}
|
|
14
|
+
|
|
15
|
+
The sites are the registrations the C++ makes: ``request.security`` of
|
|
16
|
+
another symbol that the support checker lowered onto its feed
|
|
17
|
+
(``external_requests.FEED_LOWERING``), one entry per context a helper's call
|
|
18
|
+
paths give it (``security_contexts``), with the feed column a footprint reads
|
|
19
|
+
(``column``). A site whose value reaches display and alert sinks only is
|
|
20
|
+
lowered to ``na`` and reads nothing, and a helper nothing reaches never runs:
|
|
21
|
+
neither is listed. A deferred refusal -- another symbol's request whose value
|
|
22
|
+
can reach a trade but which registration cannot key before the first bar --
|
|
23
|
+
is listed with an ``unresolvable`` symbol: its first read stops the run.
|
|
24
|
+
Every other request reads no feed: the chart's symbol, ``request.*`` of
|
|
25
|
+
fundamentals (recorded series, not bars) and ``request.security_lower_tf`` of
|
|
26
|
+
another symbol (it stops the run where it is evaluated).
|
|
27
|
+
|
|
28
|
+
Names are expanded as registration expands them
|
|
29
|
+
(``_substitute_tf_input_reads``: the expression the C++ renders and the run
|
|
30
|
+
evaluates before the first bar), and a ``value`` is computed only over
|
|
31
|
+
``ScriptIndex.registration_value``'s grammar -- literals, inputs at their
|
|
32
|
+
defaults, ``+ == != and or not``, ternaries and ``ticker.inherit`` /
|
|
33
|
+
``standard`` -- so discovery states a value where the run computes the same
|
|
34
|
+
one from the same expression.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from .ast_nodes import (
|
|
40
|
+
BinOp, BoolLiteral, ExprStmt, FuncCall, Identifier, MemberAccess, NaLiteral,
|
|
41
|
+
NumberLiteral, StringLiteral, Subscript, SwitchStmt, Ternary, UnaryOp,
|
|
42
|
+
)
|
|
43
|
+
from .errors import CompileError, Phase
|
|
44
|
+
from .external_requests import FEED_LOWERING, LOWERING_ANNOTATION, _nodes
|
|
45
|
+
from .pine_spelling import is_input_call, pine_string_literal
|
|
46
|
+
from .security_contexts import _REGISTRATION_INPUTS, ScriptIndex, _reached, ticker_symbol_arg
|
|
47
|
+
|
|
48
|
+
# A deferred refusal's lowering (``support_checker._lower_no_data_request``,
|
|
49
|
+
# ``external_requests.unpin_requests``).
|
|
50
|
+
_UNPINNED_LOWERING = "unpinned"
|
|
51
|
+
# The chart's own timeframe where a fold meets ``timeframe.period``.
|
|
52
|
+
_CHART = object()
|
|
53
|
+
_UNKNOWN = object()
|
|
54
|
+
# A folded value longer than any symbol or timeframe is no key.
|
|
55
|
+
_MAX_VALUE_CHARS = 1024
|
|
56
|
+
# Names a fold follows, one through another, and nodes it visits, before it
|
|
57
|
+
# gives up (the value is then unknown).
|
|
58
|
+
_MAX_FOLD_DEPTH = 256
|
|
59
|
+
_MAX_FOLD_STEPS = 100_000
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _Fold:
|
|
63
|
+
"""One value's fold: each name expanded and folded once."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, gen) -> None:
|
|
66
|
+
self.gen = gen
|
|
67
|
+
self.names: dict[str, object] = {}
|
|
68
|
+
self.steps = 0
|
|
69
|
+
_PRECEDENCE = {"or": 1, "and": 2, "==": 3, "!=": 3, "<": 4, "<=": 4, ">": 4, ">=": 4,
|
|
70
|
+
"+": 5, "-": 5, "*": 6, "/": 6, "%": 6}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def request_sites(program) -> list[FuncCall]:
|
|
74
|
+
"""The ``request.security`` calls the support checker lowered onto
|
|
75
|
+
another symbol's feed or to a deferred refusal, outside the helpers
|
|
76
|
+
nothing reaches: taken before the passes that replace a refusal by its
|
|
77
|
+
``na`` (``lower_no_data_requests``) or turn a feed back into one
|
|
78
|
+
(``unpin_requests``), which mark it so."""
|
|
79
|
+
sites = [node for node in _nodes(program)
|
|
80
|
+
if isinstance(node, FuncCall) and _call_name(node) == ("request", "security")
|
|
81
|
+
and (node.annotations or {}).get(LOWERING_ANNOTATION)
|
|
82
|
+
in (FEED_LOWERING, _UNPINNED_LOWERING)]
|
|
83
|
+
if not sites:
|
|
84
|
+
return sites
|
|
85
|
+
index = ScriptIndex(program)
|
|
86
|
+
reached = _reached(index)
|
|
87
|
+
return [site for site in sites
|
|
88
|
+
if (owner := index.owner.get(id(site))) not in index.funcs or owner in reached]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def discover_requests(gen, ctx, sites: list[FuncCall]) -> list[dict]:
|
|
92
|
+
"""The requests of another symbol's data (module docstring), by line,
|
|
93
|
+
each distinct entry once."""
|
|
94
|
+
entries: list[tuple[tuple, dict]] = []
|
|
95
|
+
calls = {call.sec_id: call for call in ctx.security_calls}
|
|
96
|
+
mutable_reads = set(gen._security_tf_mutable_reads)
|
|
97
|
+
|
|
98
|
+
def add(loc, entry: dict) -> None:
|
|
99
|
+
entry = {"line": loc.line if loc is not None else None, "fn": "request.security",
|
|
100
|
+
**entry}
|
|
101
|
+
where = (loc.line or 0, loc.col or 0) if loc is not None else (0, 0)
|
|
102
|
+
entries.append(((*where, len(entries)), entry))
|
|
103
|
+
|
|
104
|
+
budget = getattr(gen, "_budget", None)
|
|
105
|
+
foreign = [info for info in gen._security_eval_info if info.get("foreign")]
|
|
106
|
+
helpers: set[str] = set()
|
|
107
|
+
unreached: set[str] = set()
|
|
108
|
+
if any(getattr(calls.get(info["sec_id"]), "containing_func", "") for info in foreign):
|
|
109
|
+
# A helper nothing reaches never runs: its registration is never read.
|
|
110
|
+
index = ScriptIndex(ctx.ast)
|
|
111
|
+
helpers = set(index.funcs)
|
|
112
|
+
unreached = helpers - _reached(index)
|
|
113
|
+
try:
|
|
114
|
+
for info in foreign:
|
|
115
|
+
helper = getattr(calls.get(info["sec_id"]), "containing_func", "")
|
|
116
|
+
if helper in helpers and helper in unreached:
|
|
117
|
+
continue
|
|
118
|
+
# Past the time budget the rest are listed as written, unexpanded:
|
|
119
|
+
# discovery never fails a script that transpiled.
|
|
120
|
+
expand = _within(budget)
|
|
121
|
+
entry = {
|
|
122
|
+
"symbol": _guarded(_symbol, gen, info["symbol_node"], expand=expand),
|
|
123
|
+
"timeframe": _guarded(_registered_timeframe, gen, info, expand=expand),
|
|
124
|
+
"lookahead": bool(info.get("lookahead_on")),
|
|
125
|
+
"gaps": bool(info.get("gaps_on")),
|
|
126
|
+
"ignore_invalid_symbol": _guarded(
|
|
127
|
+
_flag, gen, info.get("ignore_invalid_node"), expand=expand),
|
|
128
|
+
}
|
|
129
|
+
column = gen._security_footprint_column(info["sec_id"])
|
|
130
|
+
if column:
|
|
131
|
+
entry["column"] = column
|
|
132
|
+
add(getattr(calls.get(info["sec_id"]), "loc", None), entry)
|
|
133
|
+
for site in sites:
|
|
134
|
+
if (site.annotations or {}).get(LOWERING_ANNOTATION) != _UNPINNED_LOWERING:
|
|
135
|
+
continue
|
|
136
|
+
# The C++ never registered it, so nothing expands its names here:
|
|
137
|
+
# its arguments are listed as written.
|
|
138
|
+
symbol, timeframe = _request_args(site)
|
|
139
|
+
gaps, lookahead = _gaps_lookahead(site)
|
|
140
|
+
ignore = site.kwargs.get("ignore_invalid_symbol")
|
|
141
|
+
add(site.loc, {
|
|
142
|
+
"symbol": {"kind": "unresolvable", "expr": _spell(symbol)},
|
|
143
|
+
"timeframe": _guarded(_timeframe, gen, timeframe, expand=False),
|
|
144
|
+
"lookahead": _barmerge(lookahead, "lookahead_on"),
|
|
145
|
+
"gaps": _barmerge(gaps, "gaps_on"),
|
|
146
|
+
"ignore_invalid_symbol": (
|
|
147
|
+
False if ignore is None else
|
|
148
|
+
ignore.value if isinstance(ignore, BoolLiteral) else None),
|
|
149
|
+
})
|
|
150
|
+
finally:
|
|
151
|
+
# Expanding a name registers a reassigned global's read with the
|
|
152
|
+
# first-bar replay; generation is over, so leave its record as it was.
|
|
153
|
+
gen._security_tf_mutable_reads = mutable_reads
|
|
154
|
+
out: list[dict] = []
|
|
155
|
+
for _, entry in sorted(entries, key=lambda item: item[0]):
|
|
156
|
+
if entry not in out:
|
|
157
|
+
out.append(entry)
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Symbol and timeframe
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def _within(budget) -> bool:
|
|
166
|
+
"""Whether the transpile's time budget still holds."""
|
|
167
|
+
if budget is None:
|
|
168
|
+
return True
|
|
169
|
+
try:
|
|
170
|
+
budget.check(phase=Phase.CODEGEN)
|
|
171
|
+
except CompileError:
|
|
172
|
+
return False
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _guarded(classify, gen, node, **options):
|
|
177
|
+
"""``classify(gen, node)``; a shape it does not know is listed as
|
|
178
|
+
computed, with no value: discovery never fails a script that
|
|
179
|
+
transpiled."""
|
|
180
|
+
try:
|
|
181
|
+
return classify(gen, node, **options)
|
|
182
|
+
except Exception: # noqa: BLE001
|
|
183
|
+
if isinstance(node, dict):
|
|
184
|
+
node = node.get("tf_node")
|
|
185
|
+
return None if classify is _flag else {"kind": "computed", "expr": _spell(node)}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _symbol(gen, node, expand: bool = True) -> dict:
|
|
189
|
+
"""``literal`` / ``input`` / ``computed`` as registration computes the
|
|
190
|
+
symbol string the run keys its feed on."""
|
|
191
|
+
resolved = _passthrough(_expand(gen, node) if expand else node)
|
|
192
|
+
if isinstance(resolved, StringLiteral):
|
|
193
|
+
return {"kind": "literal", "value": resolved.value}
|
|
194
|
+
if _is_registered_input(resolved):
|
|
195
|
+
return _input(gen, resolved)
|
|
196
|
+
if not expand:
|
|
197
|
+
return {"kind": "computed", "expr": _spell(node)}
|
|
198
|
+
return _computed(gen, node, resolved, timeframe=False)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _registered_timeframe(gen, info: dict, expand: bool = True) -> dict:
|
|
202
|
+
"""The timeframe a feed site registers with (``_resolve_security_tf``'s
|
|
203
|
+
``tf`` / ``tf_expr``, ``configure_security_evaluators``). ``input_tf_``
|
|
204
|
+
is the chart's: a run reading another symbol's feed takes the chart's
|
|
205
|
+
bars unaggregated (the engine refuses it otherwise)."""
|
|
206
|
+
tf, tf_expr = info.get("tf"), info.get("tf_expr")
|
|
207
|
+
if tf:
|
|
208
|
+
return {"kind": "literal", "value": canonical_timeframe(tf)}
|
|
209
|
+
if tf == "" or tf_expr in ("input_tf_", "script_tf_"):
|
|
210
|
+
return {"kind": "chart"}
|
|
211
|
+
return _timeframe(gen, info.get("tf_node"), expand=expand)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _timeframe(gen, node, expand: bool = True) -> dict:
|
|
215
|
+
"""A timeframe registration expands (``expand``), or as written."""
|
|
216
|
+
if node is None:
|
|
217
|
+
return {"kind": "chart"}
|
|
218
|
+
resolved = _expand(gen, node) if expand else node
|
|
219
|
+
if isinstance(resolved, StringLiteral):
|
|
220
|
+
if resolved.value == "":
|
|
221
|
+
return {"kind": "chart"}
|
|
222
|
+
return {"kind": "literal", "value": canonical_timeframe(resolved.value)}
|
|
223
|
+
if _is_timeframe_period(resolved):
|
|
224
|
+
return {"kind": "chart"}
|
|
225
|
+
if _is_registered_input(resolved, timeframe=True):
|
|
226
|
+
return _input(gen, resolved)
|
|
227
|
+
if not expand:
|
|
228
|
+
return {"kind": "computed", "expr": _spell(node)}
|
|
229
|
+
return _computed(gen, node, resolved, timeframe=True)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def canonical_timeframe(tf: str) -> str:
|
|
233
|
+
"""The engine's spelling of a feed's timeframe
|
|
234
|
+
(``canonical_symbol_timeframe``): Pine's bare ``D`` / ``W`` / ``M`` /
|
|
235
|
+
``S`` folded to ``1D`` / ``1W`` / ``1M`` / ``1S``, any other text kept
|
|
236
|
+
as it is (whole minutes ``"240"``, ``<n>D|W|M|S``; a feed matches no
|
|
237
|
+
other spelling)."""
|
|
238
|
+
return "1" + tf if tf in ("D", "W", "M", "S") else tf
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _input(gen, call: FuncCall) -> dict:
|
|
242
|
+
default = _input_default(gen, call)
|
|
243
|
+
return {"kind": "input", "title": gen._get_input_title(call),
|
|
244
|
+
"default": None if default is _UNKNOWN else default}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _computed(gen, node, resolved, *, timeframe: bool) -> dict:
|
|
248
|
+
out = {"kind": "computed", "expr": _spell(node)}
|
|
249
|
+
value = _fold(_Fold(gen), resolved)
|
|
250
|
+
if value is _CHART and timeframe:
|
|
251
|
+
out["value"] = ""
|
|
252
|
+
elif isinstance(value, str):
|
|
253
|
+
out["value"] = canonical_timeframe(value) if timeframe and value else value
|
|
254
|
+
titles: list[str] = []
|
|
255
|
+
for call in _input_calls(gen, resolved):
|
|
256
|
+
title = gen._get_input_title(call)
|
|
257
|
+
if title not in titles:
|
|
258
|
+
titles.append(title)
|
|
259
|
+
if titles:
|
|
260
|
+
out["inputs"] = titles
|
|
261
|
+
return out
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _flag(gen, node, expand: bool = True) -> bool | None:
|
|
265
|
+
"""``ignore_invalid_symbol`` as registration reads it: false when left
|
|
266
|
+
out, else its value at the inputs' defaults (None when unknown)."""
|
|
267
|
+
if node is None:
|
|
268
|
+
return False
|
|
269
|
+
if not expand:
|
|
270
|
+
return node.value if isinstance(node, BoolLiteral) else None
|
|
271
|
+
value = _fold(_Fold(gen), _expand(gen, node))
|
|
272
|
+
return value if isinstance(value, bool) else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _expand(gen, node):
|
|
276
|
+
"""``node`` with its names expanded as registration expands them."""
|
|
277
|
+
return gen._substitute_tf_input_reads(node, set())
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _passthrough(node):
|
|
281
|
+
"""``ticker.inherit`` / ``ticker.standard`` render their symbol argument
|
|
282
|
+
unchanged (``visit_call``)."""
|
|
283
|
+
while (isinstance(node, FuncCall) and _call_name(node) in (
|
|
284
|
+
("ticker", "inherit"), ("ticker", "standard"))):
|
|
285
|
+
symbol = ticker_symbol_arg(node)
|
|
286
|
+
if symbol is None:
|
|
287
|
+
break
|
|
288
|
+
node = symbol
|
|
289
|
+
return node
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _is_registered_input(node, timeframe: bool = False) -> bool:
|
|
293
|
+
"""An input whose value registration reads: ``input.symbol`` /
|
|
294
|
+
``string`` (``timeframe`` for a timeframe), or ``input()`` of a string."""
|
|
295
|
+
if not is_input_call(node):
|
|
296
|
+
return False
|
|
297
|
+
ns, name = _call_name(node)
|
|
298
|
+
if ns is None:
|
|
299
|
+
default = node.args[0] if node.args else node.kwargs.get("defval")
|
|
300
|
+
return isinstance(default, StringLiteral)
|
|
301
|
+
return name in (_REGISTRATION_INPUTS - {"bool"}) or (timeframe and name == "timeframe")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _input_default(gen, call: FuncCall):
|
|
305
|
+
default = gen._get_input_default(call)
|
|
306
|
+
if isinstance(default, (StringLiteral, BoolLiteral, NumberLiteral)):
|
|
307
|
+
return default.value
|
|
308
|
+
return _UNKNOWN
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _input_calls(gen, node) -> list[FuncCall]:
|
|
312
|
+
"""The input calls ``node`` reads, through the names registration
|
|
313
|
+
expands (a ``switch``'s too), in source order."""
|
|
314
|
+
found: dict[int, FuncCall] = {}
|
|
315
|
+
seen: set[str] = set()
|
|
316
|
+
pending = [node]
|
|
317
|
+
while pending:
|
|
318
|
+
for item in _nodes(pending.pop()):
|
|
319
|
+
if is_input_call(item):
|
|
320
|
+
found.setdefault(id(item), item)
|
|
321
|
+
elif isinstance(item, Identifier) and item.name not in seen:
|
|
322
|
+
seen.add(item.name)
|
|
323
|
+
expanded = _expand(gen, item)
|
|
324
|
+
if expanded is not item:
|
|
325
|
+
pending.append(expanded)
|
|
326
|
+
return sorted(found.values(), key=lambda call: (call.loc.line, call.loc.col)
|
|
327
|
+
if call.loc is not None else (0, 0))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _fold(state: _Fold, node, depth: int = 0):
|
|
331
|
+
"""The value at the inputs' defaults of an expression in
|
|
332
|
+
``ScriptIndex.registration_value``'s grammar (and, for a timeframe, a
|
|
333
|
+
``switch`` of single-expression arms, as registration renders it), else
|
|
334
|
+
``_UNKNOWN``. The chart's own symbol strings are the chart's (unknown
|
|
335
|
+
here); ``timeframe.period`` is ``_CHART``."""
|
|
336
|
+
state.steps += 1
|
|
337
|
+
if depth > _MAX_FOLD_DEPTH or state.steps > _MAX_FOLD_STEPS:
|
|
338
|
+
return _UNKNOWN
|
|
339
|
+
depth += 1
|
|
340
|
+
if isinstance(node, (StringLiteral, BoolLiteral)):
|
|
341
|
+
return node.value
|
|
342
|
+
if isinstance(node, NumberLiteral):
|
|
343
|
+
return node.value
|
|
344
|
+
if _is_timeframe_period(node):
|
|
345
|
+
return _CHART
|
|
346
|
+
if isinstance(node, Identifier):
|
|
347
|
+
# A name registration expands (a switch's subject: the expansion
|
|
348
|
+
# leaves a switch as written); one it keeps has no value here.
|
|
349
|
+
if node.name not in state.names:
|
|
350
|
+
state.names[node.name] = _UNKNOWN # a name read inside its own value
|
|
351
|
+
expanded = _expand(state.gen, node)
|
|
352
|
+
state.names[node.name] = (_UNKNOWN if expanded is node
|
|
353
|
+
else _fold(state, expanded, depth))
|
|
354
|
+
return state.names[node.name]
|
|
355
|
+
if isinstance(node, SwitchStmt):
|
|
356
|
+
return _fold_switch(state, node, depth)
|
|
357
|
+
if isinstance(node, FuncCall):
|
|
358
|
+
if is_input_call(node):
|
|
359
|
+
ns, name = _call_name(node)
|
|
360
|
+
if ns is None or name in _REGISTRATION_INPUTS or name == "timeframe":
|
|
361
|
+
return _input_default(state.gen, node)
|
|
362
|
+
return _UNKNOWN
|
|
363
|
+
passed = _passthrough(node)
|
|
364
|
+
return _UNKNOWN if passed is node else _fold(state, passed, depth)
|
|
365
|
+
if isinstance(node, Ternary):
|
|
366
|
+
condition = _fold(state, node.condition, depth)
|
|
367
|
+
if not isinstance(condition, bool):
|
|
368
|
+
return _UNKNOWN
|
|
369
|
+
return _fold(state, node.true_val if condition else node.false_val, depth)
|
|
370
|
+
if isinstance(node, UnaryOp):
|
|
371
|
+
operand = _fold(state, node.operand, depth)
|
|
372
|
+
return (not operand) if node.op == "not" and isinstance(operand, bool) else _UNKNOWN
|
|
373
|
+
if isinstance(node, BinOp) and node.op in ("+", "==", "!=", "and", "or"):
|
|
374
|
+
left = _fold(state, node.left, depth)
|
|
375
|
+
if left is _UNKNOWN or left is _CHART:
|
|
376
|
+
return _UNKNOWN
|
|
377
|
+
right = _fold(state, node.right, depth)
|
|
378
|
+
if right is _UNKNOWN or right is _CHART:
|
|
379
|
+
return _UNKNOWN
|
|
380
|
+
if node.op == "+":
|
|
381
|
+
if not (isinstance(left, str) and isinstance(right, str)
|
|
382
|
+
and len(left) + len(right) <= _MAX_VALUE_CHARS):
|
|
383
|
+
return _UNKNOWN
|
|
384
|
+
return left + right
|
|
385
|
+
if node.op in ("and", "or"):
|
|
386
|
+
if not (isinstance(left, bool) and isinstance(right, bool)):
|
|
387
|
+
return _UNKNOWN
|
|
388
|
+
return (left and right) if node.op == "and" else (left or right)
|
|
389
|
+
if type(left) is not type(right):
|
|
390
|
+
return _UNKNOWN
|
|
391
|
+
return (left == right) if node.op == "==" else (left != right)
|
|
392
|
+
return _UNKNOWN
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _fold_switch(state: _Fold, node: SwitchStmt, depth: int):
|
|
396
|
+
"""A timeframe ``switch`` as registration renders it
|
|
397
|
+
(``_security_tf_switch_runtime_expr``): the first arm whose value equals
|
|
398
|
+
the subject (or whose condition holds), else the default arm, else the
|
|
399
|
+
chart's timeframe; every arm one expression."""
|
|
400
|
+
subject = None
|
|
401
|
+
if node.expr is not None:
|
|
402
|
+
subject = _fold(state, node.expr, depth)
|
|
403
|
+
if subject is _UNKNOWN or subject is _CHART:
|
|
404
|
+
return _UNKNOWN
|
|
405
|
+
|
|
406
|
+
def arm(body):
|
|
407
|
+
if len(body) != 1 or not isinstance(body[0], ExprStmt) or isinstance(
|
|
408
|
+
body[0].expr, NaLiteral):
|
|
409
|
+
return _UNKNOWN
|
|
410
|
+
return _fold(state, body[0].expr, depth)
|
|
411
|
+
|
|
412
|
+
default = node.default_body or None
|
|
413
|
+
for condition, body in node.cases:
|
|
414
|
+
if condition is None:
|
|
415
|
+
default = default or body
|
|
416
|
+
continue
|
|
417
|
+
value = _fold(state, condition, depth)
|
|
418
|
+
if value is _UNKNOWN or value is _CHART:
|
|
419
|
+
return _UNKNOWN
|
|
420
|
+
if (value is True if node.expr is None
|
|
421
|
+
else type(value) is type(subject) and value == subject):
|
|
422
|
+
return arm(body)
|
|
423
|
+
return arm(default) if default else _CHART
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
# Call shapes and spelling
|
|
428
|
+
# ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
def _call_name(node: FuncCall) -> tuple[str | None, str | None]:
|
|
431
|
+
callee = node.callee
|
|
432
|
+
if isinstance(callee, Identifier):
|
|
433
|
+
return None, callee.name
|
|
434
|
+
if isinstance(callee, MemberAccess) and isinstance(callee.object, Identifier):
|
|
435
|
+
return callee.object.name, callee.member
|
|
436
|
+
return None, None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _is_timeframe_period(node) -> bool:
|
|
440
|
+
"""``timeframe.period`` or ``timeframe.main_period``: the chart's."""
|
|
441
|
+
return (isinstance(node, MemberAccess) and node.member in ("period", "main_period")
|
|
442
|
+
and isinstance(node.object, Identifier) and node.object.name == "timeframe")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _request_args(node: FuncCall):
|
|
446
|
+
symbol = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
447
|
+
timeframe = node.args[1] if len(node.args) > 1 else node.kwargs.get("timeframe")
|
|
448
|
+
return symbol, timeframe
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _gaps_lookahead(node: FuncCall):
|
|
452
|
+
gaps = node.kwargs.get("gaps")
|
|
453
|
+
if gaps is None and len(node.args) > 3:
|
|
454
|
+
gaps = node.args[3]
|
|
455
|
+
lookahead = node.kwargs.get("lookahead")
|
|
456
|
+
if lookahead is None and len(node.args) > 4:
|
|
457
|
+
lookahead = node.args[4]
|
|
458
|
+
return gaps, lookahead
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _barmerge(node, on: str) -> bool | None:
|
|
462
|
+
"""A ``barmerge.*`` constant's flag; left out is off; None when computed."""
|
|
463
|
+
if node is None:
|
|
464
|
+
return False
|
|
465
|
+
if (isinstance(node, MemberAccess) and isinstance(node.object, Identifier)
|
|
466
|
+
and node.object.name == "barmerge"):
|
|
467
|
+
return node.member == on
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _spell(node, parent: int = 0, right: bool = False) -> str:
|
|
472
|
+
"""``node`` spelled as Pine source (display only)."""
|
|
473
|
+
if node is None:
|
|
474
|
+
return ""
|
|
475
|
+
if isinstance(node, StringLiteral):
|
|
476
|
+
return pine_string_literal(node.value)
|
|
477
|
+
if isinstance(node, BoolLiteral):
|
|
478
|
+
return "true" if node.value else "false"
|
|
479
|
+
if isinstance(node, NumberLiteral):
|
|
480
|
+
return str(node.value)
|
|
481
|
+
if isinstance(node, NaLiteral):
|
|
482
|
+
return "na"
|
|
483
|
+
if isinstance(node, Identifier):
|
|
484
|
+
return node.name
|
|
485
|
+
if isinstance(node, MemberAccess):
|
|
486
|
+
return f"{_spell(node.object, 9)}.{node.member}"
|
|
487
|
+
if isinstance(node, FuncCall):
|
|
488
|
+
args = [_spell(a) for a in node.args]
|
|
489
|
+
args += [f"{key} = {_spell(value)}" for key, value in node.kwargs.items()]
|
|
490
|
+
return f"{_spell(node.callee, 9)}({', '.join(args)})"
|
|
491
|
+
if isinstance(node, Subscript):
|
|
492
|
+
return f"{_spell(node.object, 9)}[{_spell(node.index)}]"
|
|
493
|
+
if isinstance(node, UnaryOp):
|
|
494
|
+
text = f"not {_spell(node.operand, 7)}" if node.op == "not" else (
|
|
495
|
+
f"{node.op}{_spell(node.operand, 7)}")
|
|
496
|
+
return f"({text})" if parent > 7 else text
|
|
497
|
+
if isinstance(node, BinOp):
|
|
498
|
+
level = _PRECEDENCE.get(node.op, 5)
|
|
499
|
+
text = (f"{_spell(node.left, level)} {node.op} "
|
|
500
|
+
f"{_spell(node.right, level, right=True)}")
|
|
501
|
+
return f"({text})" if level < parent or (right and level == parent) else text
|
|
502
|
+
if isinstance(node, Ternary):
|
|
503
|
+
text = (f"{_spell(node.condition, 1)} ? {_spell(node.true_val)} : "
|
|
504
|
+
f"{_spell(node.false_val)}")
|
|
505
|
+
return f"({text})" if parent else text
|
|
506
|
+
return "..."
|
|
Binary file
|
package/release.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"codegen": "1.0
|
|
2
|
+
"codegen": "1.1.0",
|
|
3
3
|
"pyodide": "314.0.0",
|
|
4
4
|
"python": "3.14.0",
|
|
5
5
|
"emscripten": "emscripten_5_0_3",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "37b1ca85a2006f6f07d52074a7180187536047bfbb857681978a372b1d7f9bef"
|
|
7
7
|
}
|
package/tables.json
CHANGED
package/transpile.worker.mjs
CHANGED
|
@@ -49,7 +49,8 @@ def transpile_json(source: str) -> str:
|
|
|
49
49
|
# A script that transpiled carries its warnings in the same entry format.
|
|
50
50
|
return json.dumps({"ok": True, "cpp": full["cpp"], "inputs": full["inputs"],
|
|
51
51
|
"strategyParams": full["strategyParams"],
|
|
52
|
-
"diagnostics": _diagnostic_entries(full["diagnostics"])
|
|
52
|
+
"diagnostics": _diagnostic_entries(full["diagnostics"]),
|
|
53
|
+
"requests": full["requests"]})
|
|
53
54
|
`;
|
|
54
55
|
|
|
55
56
|
const post = (m) => self.postMessage(m);
|
|
Binary file
|