@pineforge/codegen-pyodide 1.0.1 → 1.2.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.
@@ -0,0 +1,186 @@
1
+ """Immutable execution metadata; it does not alter generated computation."""
2
+
3
+ import json
4
+ import math
5
+
6
+ from ..ast_nodes import BoolLiteral, FuncCall, Identifier, MemberAccess, NumberLiteral, StrategyDecl, StringLiteral, UnaryOp, VarDecl
7
+ from ..external_requests import CAPABILITY_UNPINNED_ANNOTATION
8
+ from ..limits import iter_ast_nodes
9
+
10
+
11
+ STRATEGY_PARAMETERS = (
12
+ "title", "shorttitle", "overlay", "format", "precision", "scale", "pyramiding",
13
+ "calc_on_order_fills", "calc_on_every_tick", "max_bars_back",
14
+ "backtest_fill_limits_assumption", "default_qty_type", "default_qty_value",
15
+ "initial_capital", "currency", "slippage", "commission_type", "commission_value",
16
+ "process_orders_on_close", "close_entries_rule", "margin_long", "margin_short",
17
+ "explicit_plot_zorder", "max_lines_count", "max_labels_count", "max_boxes_count",
18
+ "calc_bars_count", "risk_free_rate", "use_bar_magnifier", "fill_orders_on_standard_ohlc",
19
+ "max_polylines_count", "dynamic_requests", "behind_chart", "calc_on_every_history_tick",
20
+ )
21
+
22
+
23
+ DECLARATION_DEFAULTS = {
24
+ "calc_on_every_tick": False,
25
+ "calc_on_order_fills": False,
26
+ "process_orders_on_close": False,
27
+ "use_bar_magnifier": False,
28
+ "fill_orders_on_standard_ohlc": False,
29
+ "backtest_fill_limits_assumption": 0,
30
+ "currency": "currency.NONE",
31
+ "timeframe": "",
32
+ "timeframe_gaps": True,
33
+ "dynamic_requests": True,
34
+ "calc_on_every_history_tick": False,
35
+ }
36
+
37
+
38
+ def _expression(node):
39
+ if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
40
+ return node.value
41
+ if isinstance(node, Identifier):
42
+ return node.name
43
+ if isinstance(node, MemberAccess):
44
+ return f"{_expression(node.object)}.{node.member}"
45
+ if isinstance(node, FuncCall):
46
+ return f"{_expression(node.callee)}({','.join(str(_expression(arg)) for arg in node.args)})"
47
+ return None
48
+
49
+
50
+ def _literal(node):
51
+ if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
52
+ return node.value, True
53
+ if isinstance(node, UnaryOp) and node.op in ("+", "-"):
54
+ value, valid = _literal(node.operand)
55
+ if valid and type(value) in (int, float):
56
+ return value if node.op == "+" else -value, True
57
+ if isinstance(node, MemberAccess):
58
+ value = _expression(node)
59
+ if value and value.split(".")[0] in ("currency", "format", "scale", "strategy"):
60
+ return value, True
61
+ return _expression(node), False
62
+
63
+
64
+ def capabilities_document(emitter) -> str:
65
+ declarations = dict(DECLARATION_DEFAULTS)
66
+ unresolved = []
67
+ requests = []
68
+ lower_symbols = {}
69
+ intrabar = False
70
+ recorded = False
71
+ for node, _depth in iter_ast_nodes(emitter.ctx.ast):
72
+ if isinstance(node, StrategyDecl):
73
+ arguments = {STRATEGY_PARAMETERS[index] if index < len(STRATEGY_PARAMETERS)
74
+ else f"strategy() positional argument {index + 1}": argument
75
+ for index, argument in enumerate(node.args)}
76
+ unresolved.extend(arguments.keys() & node.kwargs.keys())
77
+ arguments.update(node.kwargs)
78
+ for name, argument in arguments.items():
79
+ value, valid = _literal(argument)
80
+ default = DECLARATION_DEFAULTS.get(name)
81
+ if name in DECLARATION_DEFAULTS:
82
+ valid &= type(value) is type(default)
83
+ if name == "backtest_fill_limits_assumption":
84
+ valid = (type(value) is int and value >= 0) or (
85
+ type(value) is float and math.isfinite(value) and value >= 0 and value == int(value))
86
+ if valid:
87
+ value = int(value)
88
+ declarations[name] = value
89
+ if not valid or name not in STRATEGY_PARAMETERS and name not in DECLARATION_DEFAULTS:
90
+ unresolved.append(name)
91
+ elif isinstance(node, VarDecl):
92
+ intrabar |= node.is_varip
93
+ elif isinstance(node, FuncCall):
94
+ name, namespace = emitter._resolve_callee(node.callee)
95
+ if namespace == "request" and name == "security_lower_tf":
96
+ expression = node.args[2] if len(node.args) > 2 else node.kwargs.get("expression")
97
+ lower_symbols[id(expression)] = node.args[0] if node.args else node.kwargs.get("symbol")
98
+ parts = (node.annotations or {}).get("pf_recorded_key")
99
+ if parts is not None:
100
+ recorded = True
101
+ requests.append({
102
+ "function": f"{namespace}.{name}",
103
+ "symbol": _expression(node.args[0]) if node.args else None,
104
+ "timeframe": "",
105
+ "lookahead": f"barmerge.lookahead_{parts.get('lookahead', 'off')}",
106
+ "gaps": f"barmerge.gaps_{parts.get('gaps', 'off')}",
107
+ "heikinashi": False,
108
+ "feed": "recorded",
109
+ })
110
+ if isinstance(node, Identifier) and node.name == "timenow":
111
+ unresolved.append("timenow (bar timestamp approximation)")
112
+ if isinstance(node, Identifier) and node.name in ("last_bar_index", "last_bar_time"):
113
+ unresolved.append(f"{node.name} (stream endpoint is not batch-equivalent)")
114
+ if isinstance(node, MemberAccess) and _expression(node) == "barstate.isrealtime":
115
+ unresolved.append("barstate.isrealtime (compiled historical-only approximation)")
116
+ if isinstance(node, MemberAccess) and _expression(node) in (
117
+ "barstate.islast", "barstate.islastconfirmedhistory"):
118
+ unresolved.append(f"{_expression(node)} (stream endpoint is not batch-equivalent)")
119
+
120
+ auxiliary = False
121
+ for site in emitter._security_calls:
122
+ symbol_node = site.get("symbol_node")
123
+ if site.get("is_lower_tf_array"):
124
+ symbol_node = lower_symbols.get(id(site.get("expr_node")), symbol_node)
125
+ if isinstance(symbol_node, FuncCall) and site.get("heikinashi") and symbol_node.args:
126
+ symbol_node = symbol_node.args[0]
127
+ symbol = _expression(symbol_node) if symbol_node is not None else "syminfo.tickerid"
128
+ timeframe = _expression(site.get("tf_node"))
129
+ lookahead = _expression(site.get("lookahead_node")) or "barmerge.lookahead_off"
130
+ gaps = _expression(site.get("gaps_node")) or "barmerge.gaps_off"
131
+ if isinstance(symbol_node, Identifier) and symbol_node.name not in emitter._global_mutable_infos:
132
+ symbol = _expression((getattr(emitter.ctx, "global_expr_map", {}) or {}).get(
133
+ symbol_node.name, symbol_node))
134
+ feed = "chart" if symbol in ("syminfo.tickerid", "syminfo.ticker", "") else "auxiliary"
135
+ auxiliary |= feed == "auxiliary"
136
+ if not isinstance(site.get("tf_node"), StringLiteral) and timeframe != "timeframe.period":
137
+ unresolved.append(f"request.security[{site['sec_id']}].timeframe")
138
+ if lookahead not in ("barmerge.lookahead_off", "barmerge.lookahead_on"):
139
+ unresolved.append(f"request.security[{site['sec_id']}].lookahead")
140
+ requests.append({
141
+ "function": "request.security_lower_tf" if site.get("is_lower_tf_array") else "request.security",
142
+ "symbol": symbol,
143
+ "timeframe": timeframe,
144
+ "lookahead": lookahead,
145
+ "gaps": gaps,
146
+ "heikinashi": bool(site.get("heikinashi")),
147
+ "feed": feed,
148
+ })
149
+ for index, site in enumerate((emitter.ctx.ast.annotations or {}).get(CAPABILITY_UNPINNED_ANNOTATION, ())):
150
+ requests.append({
151
+ "function": site["function"],
152
+ "symbol": _expression(site["symbol_node"]),
153
+ "timeframe": _expression(site["tf_node"]) if site["function"] in (
154
+ "request.security", "request.security_lower_tf") else "",
155
+ "lookahead": _expression(site["lookahead_node"]) or "barmerge.lookahead_off",
156
+ "gaps": _expression(site["gaps_node"]) or "barmerge.gaps_off",
157
+ "heikinashi": False,
158
+ "feed": "unpinned",
159
+ })
160
+ unresolved.append(f"{site['function']}[unpinned:{index}]")
161
+ requirements = {
162
+ "auxiliary_security_feeds": auxiliary,
163
+ "native_security_feeds": False,
164
+ "fx_curve": declarations["currency"] != "currency.NONE",
165
+ "recorded_series": recorded,
166
+ "historical_probe_overrides": False,
167
+ "intrabar_persistence": intrabar,
168
+ }
169
+ return json.dumps({"version": 1, "declarations": declarations, "requests": requests,
170
+ "requirements": requirements, "unresolved": sorted(set(unresolved))},
171
+ sort_keys=True, separators=(",", ":"), allow_nan=False)
172
+
173
+
174
+ def emit_capabilities_exports(emitter, lines: list[str]) -> None:
175
+ literal = json.dumps(capabilities_document(emitter))
176
+ lines.extend([
177
+ "#ifdef PF_SETTINGS_API_VERSION",
178
+ "#ifdef PF_CAPABILITIES_API_VERSION",
179
+ " uint32_t strategy_capabilities_api_version(void) { return PF_CAPABILITIES_API_VERSION; }",
180
+ " int strategy_capabilities_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
181
+ " if (required) *required = 0;",
182
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
183
+ ' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
184
+ f" ::pineforge::checked_settings::receipt({literal}, json, capacity, required);",
185
+ " });", " }", "#endif", "#endif",
186
+ ])
@@ -0,0 +1,285 @@
1
+ """Opt-in settings metadata, validation and exception-contained C exports."""
2
+
3
+ from ..ast_nodes import Identifier, MemberAccess
4
+ from ..errors import CompileError
5
+
6
+
7
+ def _visit_setting_arg(emitter, expr) -> str:
8
+ """An input argument's C++ for the settings metadata. The metadata is
9
+ emitted in the constructor, ahead of the script body: an error here is
10
+ held until the body is generated (``CodeGen.generate``), so the script's
11
+ first error in source order is the one raised."""
12
+ try:
13
+ return emitter._visit_expr(expr)
14
+ except CompileError as error:
15
+ emitter._defer_settings_error(error)
16
+ return "0"
17
+
18
+
19
+ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
20
+ lines.extend([
21
+ " bool _pf_setting_failed_ = false;",
22
+ " std::string _pf_setting_failure_;",
23
+ " void _pf_record_failure(const char* entrypoint, const char* message) noexcept {",
24
+ " try { last_error_ = entrypoint; last_error_ += \": \"; last_error_ += message; } catch (...) {}",
25
+ " }",
26
+ " void _pf_record_setting_failure(const char* entrypoint, const char* message) noexcept {",
27
+ " if (!_pf_setting_failed_) {",
28
+ " _pf_setting_failed_ = true;",
29
+ ' try { _pf_setting_failure_ = entrypoint; _pf_setting_failure_ += ": "; _pf_setting_failure_ += message; } catch (...) {}',
30
+ " }",
31
+ " _pf_refuse_failed_setting(nullptr);",
32
+ " }",
33
+ " void _pf_require_settings_ok() const {",
34
+ "#ifdef PF_SETTINGS_API_VERSION",
35
+ ' if (_pf_setting_failed_) throw ::pineforge::checked_settings::LatchedSettingsFailure(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
36
+ "#else",
37
+ ' if (_pf_setting_failed_) throw std::runtime_error(_pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_);',
38
+ "#endif",
39
+ " }",
40
+ " bool _pf_refuse_failed_setting(ReportC* out) noexcept {",
41
+ " if (!_pf_setting_failed_) return false;",
42
+ ' try { last_error_ = _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_; } catch (...) {}',
43
+ " if (out) *out = ReportC{};",
44
+ " return true;",
45
+ " }",
46
+ ])
47
+ inputs = []
48
+ for node, binding in emitter._global_input_calls_with_names():
49
+ func_name, namespace = emitter._resolve_callee(node.callee)
50
+ name = emitter._get_input_title(node, var_name=binding)
51
+ key = emitter._input_key_literal(name)
52
+ default = emitter._get_input_default(node)
53
+ getter = emitter._input_getter_for_call(node, func_name, namespace)
54
+ default_cpp = _visit_setting_arg(emitter, default) if default is not None else "0"
55
+ default_cpp = emitter._coerce_string_input_default(getter, default_cpp)
56
+ value_type = {
57
+ "get_input_int": "int", "get_input_int64": "int",
58
+ "get_input_double": "float", "get_input_bool": "bool",
59
+ "get_input_string": "string",
60
+ }[getter]
61
+ kind = emitter._FORM_TYPE.get(func_name, value_type) if namespace == "input" else value_type
62
+ options = []
63
+ option_values = []
64
+ supported = "true"
65
+ default_serialized = None
66
+ names, merged = emitter._merged_args(node, func_name, namespace)
67
+ arguments = dict(zip(names or [], merged))
68
+ arguments.update(node.kwargs)
69
+ if emitter._is_source_input(node):
70
+ value_type = "source"
71
+ source = emitter._source_defval_to_base_series(default)[5:-1]
72
+ default_cpp = emitter._input_key_literal(source)
73
+ effective = f'(inputs_.count({key}) ? inputs_.at({key}) : {default_cpp})'
74
+ options = [emitter._input_key_literal(value)
75
+ for value in sorted(emitter._NATIVE_SOURCE_SERIES)]
76
+ elif namespace == "input" and func_name == "enum":
77
+ value_type = "enum"
78
+ declared = getattr(arguments.get("options"), "elements", None)
79
+ enum_members = ([default] if isinstance(default, MemberAccess) else []) + list(declared or [])
80
+ enum_names = {member.object.name for member in enum_members
81
+ if isinstance(member, MemberAccess) and isinstance(member.object, Identifier)}
82
+ if (len(enum_names) == 1 and enum_members
83
+ and all(isinstance(member, MemberAccess) and isinstance(member.object, Identifier)
84
+ for member in enum_members)):
85
+ enum_name = next(iter(enum_names))
86
+ members = emitter._enum_defs.get(enum_name, [])
87
+ selected = [member.member for member in declared] if declared is not None else members
88
+ options = [emitter._input_key_literal(f"{enum_name}.{member}")
89
+ for member in selected]
90
+ option_values = [emitter._input_key_literal(str(members.index(member)))
91
+ for member in selected if member in members]
92
+ supported = "true" if options and len(options) == len(option_values) else "false"
93
+ else:
94
+ supported = "false"
95
+ if not isinstance(default, MemberAccess):
96
+ supported = "false"
97
+ default_serialized = 'std::string("na")'
98
+ effective = f'(inputs_.count({key}) ? inputs_.at({key}) : std::string("na"))'
99
+ else:
100
+ effective = f'::pineforge::checked_settings::number({getter}({key}, {default_cpp}))'
101
+ else:
102
+ declared = getattr(arguments.get("options"), "elements", None)
103
+ if declared:
104
+ for option in declared:
105
+ option_cpp = _visit_setting_arg(emitter, option)
106
+ options.append(option_cpp if getter == "get_input_string" else
107
+ f'::pineforge::checked_settings::number({option_cpp})')
108
+ expression = f'{getter}({key}, {default_cpp})'
109
+ effective = (expression if getter == "get_input_string" else
110
+ f'::pineforge::checked_settings::number({expression})')
111
+ if default_serialized is None:
112
+ default_serialized = (default_cpp if value_type in ("string", "source") else
113
+ f'::pineforge::checked_settings::number({default_cpp})')
114
+ constraints = [_visit_setting_arg(emitter, arguments[name]) if arguments.get(name) is not None
115
+ else "std::numeric_limits<double>::quiet_NaN()"
116
+ for name in ("minval", "maxval", "step")]
117
+ metadata = (f'{{{key}, "{value_type}", {default_serialized}, '
118
+ f'{{{", ".join(options)}}}, {", ".join(constraints)}, '
119
+ f'{64 if getter == "get_input_int64" else 32}, {supported}, '
120
+ f'{{{", ".join(option_values)}}}, "{kind}"}}')
121
+ inputs.append((metadata, effective))
122
+
123
+ overrides = [
124
+ ("initial_capital", "float", "initial_capital", "initial_capital", "0.0"),
125
+ ("commission_value", "float", "commission_value", "commission_value", "0.0"),
126
+ ("default_qty_value", "float", "default_qty_value", "default_qty_value", "0.0"),
127
+ ("pyramiding", "int", "pyramiding", "pyramiding", "0.0"),
128
+ ("slippage", "int", "slippage", "slippage", "0.0"),
129
+ ("process_orders_on_close", "bool", "process_orders_on_close", "process_orders_on_close", "nan"),
130
+ ("calc_on_order_fills", "bool", "calc_on_order_fills", "calc_on_order_fills", "nan"),
131
+ ("close_entries_rule", "string", "close_entries_rule_any", "close_entries_rule", "nan"),
132
+ ("default_qty_type", "string", "default_qty_type", "default_qty_type", "nan"),
133
+ ("commission_type", "string", "commission_type", "commission_type", "nan"),
134
+ ]
135
+ enum_options = {
136
+ "close_entries_rule": ["FIFO", "ANY"],
137
+ "default_qty_type": ["fixed", "percent_of_equity", "cash"],
138
+ "commission_type": ["percent", "cash_per_order", "cash_per_contract"],
139
+ }
140
+ lines.extend([
141
+ "#ifdef PF_SETTINGS_API_VERSION",
142
+ " static pineforge::source::PineStrategyConfig _pf_settings_declared_config() {",
143
+ " pineforge::source::PineStrategyConfig cfg{};",
144
+ ])
145
+ lines.extend(statement for statement in constructor if statement.startswith(" cfg."))
146
+ lines.extend([
147
+ " return cfg;", " }",
148
+ " std::vector<::pineforge::checked_settings::Setting> _pf_settings_inputs() const {",
149
+ " return {",
150
+ ])
151
+ lines.extend(f" {metadata}," for metadata, _effective in inputs)
152
+ lines.extend([
153
+ " };", " }",
154
+ " std::vector<::pineforge::checked_settings::Setting> _pf_settings_overrides() const {",
155
+ " const double _pf_nan = std::numeric_limits<double>::quiet_NaN();",
156
+ " const auto _pf_defaults = _pf_settings_declared_config();",
157
+ " return {",
158
+ ])
159
+ override_effective = []
160
+ for name, value_type, config_field, override_field, minimum in overrides:
161
+ raw = f'config_.{config_field}'
162
+ declared_default = f'_pf_defaults.{config_field}'
163
+ overridden = f'override_.{override_field}'
164
+ selected = (f'(std::isnan({overridden}) ? {raw} : {overridden})'
165
+ if value_type == "float" else
166
+ f'({overridden} < 0 ? {raw} : {overridden})')
167
+ if name in enum_options:
168
+ options = enum_options[name]
169
+ default_value = f'_pf_{name}_word({declared_default})'
170
+ effective = f'_pf_{name}_word({selected})'
171
+ else:
172
+ options = []
173
+ default_value = f'::pineforge::checked_settings::number({declared_default})'
174
+ effective = f'::pineforge::checked_settings::number({selected})'
175
+ if value_type == "bool":
176
+ effective = f'::pineforge::checked_settings::number(static_cast<bool>({selected}))'
177
+ option_cpp = ", ".join(emitter._input_key_literal(option) for option in options)
178
+ floor = "_pf_nan" if minimum == "nan" else minimum
179
+ lines.append(f' {{"{name}", "{value_type}", {default_value}, '
180
+ f'{{{option_cpp}}}, {floor}}},')
181
+ override_effective.append(effective)
182
+ lines.extend([" };", " }"])
183
+ for name, options in enum_options.items():
184
+ lines.append(f' static std::string _pf_{name}_word(int _pf_value) {{')
185
+ for index, option in enumerate(options):
186
+ lines.append(f' if (_pf_value == {index}) return "{option}";')
187
+ lines.extend([' return "invalid";', ' }'])
188
+ lines.extend([
189
+ " void _pf_set_input_checked(const std::string& _pf_key, const std::string& _pf_value) {",
190
+ " if (_pf_refuse_failed_setting(nullptr)) throw ::pineforge::checked_settings::Error{PF_SETTINGS_RUN_FAILED, last_error_.c_str()};",
191
+ ' ::pineforge::checked_settings::require(script_bars_processed() == 0 && stream_phase_ == StreamPhase::IDLE, "settings are frozen after execution begins", PF_SETTINGS_UNSUPPORTED);',
192
+ " const auto _pf_inputs = _pf_settings_inputs();",
193
+ " const ::pineforge::checked_settings::Setting* _pf_match = nullptr;",
194
+ " for (const auto& _pf_input : _pf_inputs) {",
195
+ " if (_pf_input.name != _pf_key) continue;",
196
+ ' ::pineforge::checked_settings::require(_pf_match == nullptr, "ambiguous input key", PF_SETTINGS_UNSUPPORTED);',
197
+ " _pf_match = &_pf_input;",
198
+ " }",
199
+ ' ::pineforge::checked_settings::require(_pf_match != nullptr, "unknown input key");',
200
+ " const auto _pf_canonical = ::pineforge::checked_settings::validate(*_pf_match, _pf_value);",
201
+ " set_input(_pf_key, _pf_canonical);",
202
+ ' ::pineforge::checked_settings::require(inputs_.count(_pf_key) && inputs_.at(_pf_key) == _pf_canonical, "input was not installed", PF_SETTINGS_UNSUPPORTED);',
203
+ " }",
204
+ " void _pf_set_override_checked(const std::string& _pf_key, const std::string& _pf_value) {",
205
+ " if (_pf_refuse_failed_setting(nullptr)) throw ::pineforge::checked_settings::Error{PF_SETTINGS_RUN_FAILED, last_error_.c_str()};",
206
+ ' ::pineforge::checked_settings::require(script_bars_processed() == 0 && stream_phase_ == StreamPhase::IDLE, "settings are frozen after execution begins", PF_SETTINGS_UNSUPPORTED);',
207
+ " for (const auto& _pf_override : _pf_settings_overrides()) {",
208
+ " if (_pf_override.name != _pf_key) continue;",
209
+ " auto _pf_alias = _pf_value;",
210
+ ])
211
+ for name, options in enum_options.items():
212
+ lines.append(f' if (_pf_key == "{name}") {{')
213
+ for index, option in enumerate(options):
214
+ aliases = [str(index)]
215
+ if name == "close_entries_rule":
216
+ aliases.append(option.lower())
217
+ else:
218
+ prefix = "strategy.commission." if name == "commission_type" else "strategy."
219
+ aliases.append(prefix + option)
220
+ condition = " || ".join(f'_pf_value == "{alias}"' for alias in aliases)
221
+ lines.append(f' if ({condition}) _pf_alias = "{option}";')
222
+ lines.append(" }")
223
+ lines.extend([
224
+ " const auto _pf_canonical = ::pineforge::checked_settings::validate(_pf_override, _pf_alias);",
225
+ " set_strategy_override(_pf_key, _pf_canonical);",
226
+ " return;",
227
+ " }",
228
+ ' throw ::pineforge::checked_settings::Error{PF_SETTINGS_INVALID_ARGUMENT, "unknown override key"};',
229
+ " }",
230
+ " std::string _pf_settings_receipt() const {",
231
+ ' ::pineforge::checked_settings::require(!_pf_setting_failed_, _pf_setting_failure_.empty() ? "legacy strategy setter failed" : _pf_setting_failure_.c_str(), PF_SETTINGS_RUN_FAILED);',
232
+ ' std::string _pf_document = "{\\\"version\\\":1,\\\"inputs\\\":[";',
233
+ " const auto _pf_inputs = _pf_settings_inputs();",
234
+ ])
235
+ for index, (_metadata, effective) in enumerate(inputs):
236
+ if index:
237
+ lines.append(" _pf_document += ',';")
238
+ lines.append(f" _pf_document += ::pineforge::checked_settings::describe(_pf_inputs[{index}], {effective});")
239
+ lines.extend([
240
+ ' _pf_document += "],\\\"overrides\\\":[";',
241
+ " const auto _pf_overrides = _pf_settings_overrides();",
242
+ ])
243
+ for index, effective in enumerate(override_effective):
244
+ if index:
245
+ lines.append(" _pf_document += ',';")
246
+ lines.append(f" _pf_document += ::pineforge::checked_settings::describe(_pf_overrides[{index}], {effective});")
247
+ lines.extend([' return _pf_document + "]}";', " }", "#endif"])
248
+
249
+
250
+ def emit_settings_exports(lines: list[str]) -> None:
251
+ lines.extend([
252
+ "#ifdef PF_SETTINGS_API_VERSION",
253
+ " uint32_t strategy_settings_api_version(void) { return PF_SETTINGS_API_VERSION; }",
254
+ " int strategy_create_checked(const char* params_json, void** out, char* error, size_t error_capacity) {",
255
+ " if (out) *out = nullptr;",
256
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
257
+ ' ::pineforge::checked_settings::require(out != nullptr, "strategy output pointer is null");',
258
+ ' ::pineforge::checked_settings::require(!params_json || !*params_json, "params_json is reserved; use checked setters", PF_SETTINGS_UNSUPPORTED);',
259
+ " *out = new GeneratedStrategy();",
260
+ " });",
261
+ " }",
262
+ ])
263
+ for kind in ("input", "override"):
264
+ lines.extend([
265
+ f" int strategy_set_{kind}_checked(void* s, const char* key, const char* value, char* error, size_t error_capacity) {{",
266
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
267
+ ' ::pineforge::checked_settings::require(s && key && value, "null strategy, key or value");',
268
+ f" static_cast<GeneratedStrategy*>(s)->_pf_set_{kind}_checked(key, value);",
269
+ " });", " }",
270
+ ])
271
+ lines.extend([
272
+ " int strategy_get_effective_settings(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
273
+ " if (required) *required = 0;",
274
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
275
+ ' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
276
+ " ::pineforge::checked_settings::receipt(static_cast<GeneratedStrategy*>(s)->_pf_settings_receipt(), json, capacity, required);",
277
+ " });", " }",
278
+ " 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) {",
279
+ " return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
280
+ ' ::pineforge::checked_settings::require(s && out && n >= 0 && (n == 0 || bars), "invalid batch arguments");',
281
+ " _pf_run_backtest_full_impl(s, bars, n, input_tf, script_tf, bar_magnifier, magnifier_samples, magnifier_dist, out);",
282
+ " const auto& _pf_error = static_cast<GeneratedStrategy*>(s)->last_error();",
283
+ ' ::pineforge::checked_settings::require(_pf_error.empty(), _pf_error.c_str(), PF_SETTINGS_RUN_FAILED);',
284
+ " });", " }", "#endif",
285
+ ])
@@ -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 run_backtest_full(void* s, Bar* bars, int n,")
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,44 @@ 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)
2001
+ from .capabilities import emit_capabilities_exports
2002
+ emit_capabilities_exports(self, lines)
1966
2003
  if self._declares_bar_magnifier():
1967
2004
  # TradingView runs a script that declares use_bar_magnifier = true
1968
2005
  # on its bar magnifier; the host reads this export to run it on
@@ -282,6 +282,7 @@ CPP_EMITTER_NAMES = frozenset("""
282
282
  configure_security_evaluators snapshot_script_state restore_script_state
283
283
  commit_script_state set_strategy_override set_input
284
284
  set_magnifier_volume_weighted strategy_declares_bar_magnifier fill_report run
285
+ strategy_capabilities_api_version strategy_capabilities_receipt
285
286
  precalculate
286
287
  strategy_entry strategy_close strategy_close_all strategy_exit
287
288
  strategy_exit_cancel_bracket strategy_cancel strategy_cancel_all strategy_order
@@ -314,7 +315,13 @@ CPP_EMITTER_NAMES = frozenset("""
314
315
  _pf_drawing_na _pf_collect_drawings _pf_collect_lines_ _pf_collect_boxes_
315
316
  _pf_collect_labels_ _pf_new _pf_held
316
317
  get_input_int get_input_float get_input_bool get_input_string
317
- trace is_na na nz fixnan
318
+ _pf_record_failure _pf_refuse_failed_setting _pf_settings_declared_config
319
+ _pf_record_setting_failure _pf_require_settings_ok
320
+ _pf_setting_failed_ _pf_setting_failure_
321
+ _pf_settings_inputs _pf_settings_overrides _pf_set_input_checked
322
+ _pf_set_override_checked _pf_settings_receipt
323
+ _pf_close_entries_rule_word _pf_default_qty_type_word _pf_commission_type_word
324
+ checked_settings trace is_na na nz fixnan
318
325
  """.split())
319
326
 
320
327
  # The generated history members, ``_<kind>_<n>`` (codegen/base.py