@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.
- package/LICENSE +245 -0
- package/README.md +4 -0
- package/glue.py +6 -1
- package/package.json +6 -4
- package/pineforge_codegen/__init__.py +25 -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/base.py +38 -1
- package/pineforge_codegen/codegen/capabilities.py +186 -0
- package/pineforge_codegen/codegen/checked_settings.py +285 -0
- package/pineforge_codegen/codegen/emit_top.py +46 -9
- package/pineforge_codegen/codegen/helpers.py +8 -1
- package/pineforge_codegen/codegen/host_members.py +8 -0
- package/pineforge_codegen/codegen/input.py +36 -3
- package/pineforge_codegen/diagnostic_codes.py +341 -0
- package/pineforge_codegen/diagnostics_catalog.json +631 -0
- package/pineforge_codegen/errors.py +23 -0
- package/pineforge_codegen/external_requests.py +19 -0
- package/pineforge_codegen/request_discovery.py +506 -0
- package/pineforge_codegen-1.2.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +6 -1
- package/pineforge_codegen-1.0.1.tar.gz +0 -0
|
@@ -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
|
|
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
|
-
|
|
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
|