@pineforge/codegen-pyodide 1.1.0 → 1.3.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 +261 -0
- package/README.md +5 -0
- package/glue.py +4 -0
- package/package.json +6 -4
- package/pineforge_codegen/__init__.py +1 -0
- package/pineforge_codegen/codegen/base.py +48 -2
- package/pineforge_codegen/codegen/capabilities.py +319 -0
- package/pineforge_codegen/codegen/checked_settings.py +64 -5
- package/pineforge_codegen/codegen/emit_top.py +5 -3
- package/pineforge_codegen/codegen/helpers.py +7 -0
- package/pineforge_codegen/codegen/order_shapes.py +356 -0
- package/pineforge_codegen/codegen/security.py +54 -5
- package/pineforge_codegen/codegen/tables.py +12 -0
- package/pineforge_codegen/codegen/visit_call.py +61 -0
- package/pineforge_codegen/codegen/visit_expr.py +8 -2
- package/pineforge_codegen/codegen/visit_stmt.py +2 -4
- 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 +16 -0
- package/pineforge_codegen/support_checker.py +19 -10
- package/pineforge_codegen-1.3.0.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/transpile.worker.mjs +4 -0
- package/pineforge_codegen-1.1.0.tar.gz +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
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, Subscript, 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
|
+
MODELED_ORDER_ARGUMENTS = {
|
|
38
|
+
"strategy.entry": frozenset(("id", "direction", "limit", "stop")),
|
|
39
|
+
"strategy.exit": frozenset(("id", "from_entry", "limit", "stop")),
|
|
40
|
+
"strategy.close": frozenset(("id", "immediately")),
|
|
41
|
+
"strategy.close_all": frozenset(("immediately",)),
|
|
42
|
+
}
|
|
43
|
+
READ_ONLY_STRATEGY_CALLS = frozenset(
|
|
44
|
+
f"strategy.{namespace}.{accessor}"
|
|
45
|
+
for namespace in ("opentrades", "closedtrades")
|
|
46
|
+
for accessor in ("entry_id", "entry_price", "entry_time", "entry_bar_index", "size",
|
|
47
|
+
"profit", "profit_percent", "commission", "max_runup", "max_runup_percent",
|
|
48
|
+
"max_drawdown", "max_drawdown_percent", "exit_id", "exit_price",
|
|
49
|
+
"exit_time", "exit_bar_index", "entry_comment", "exit_comment")
|
|
50
|
+
) | frozenset(("strategy.convert_to_account", "strategy.convert_to_symbol"))
|
|
51
|
+
ORDER_SETTINGS = frozenset(("pyramiding", "default_qty_type", "default_qty_value", "initial_capital",
|
|
52
|
+
"slippage", "commission_type", "commission_value", "close_entries_rule",
|
|
53
|
+
"margin_long", "margin_short", "risk_free_rate"))
|
|
54
|
+
PROVEN_BRACKET_SETTINGS = {"default_qty_type": "strategy.fixed", "default_qty_value": 1,
|
|
55
|
+
"commission_type": "strategy.commission.percent", "commission_value": 0}
|
|
56
|
+
PROVEN_SLIPPED_SETTINGS = {"default_qty_type": "strategy.percent_of_equity",
|
|
57
|
+
"default_qty_value": 100, "slippage": 15}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _expression(node):
|
|
61
|
+
if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
|
|
62
|
+
return node.value
|
|
63
|
+
if isinstance(node, Identifier):
|
|
64
|
+
return node.name
|
|
65
|
+
if isinstance(node, MemberAccess):
|
|
66
|
+
return f"{_expression(node.object)}.{node.member}"
|
|
67
|
+
if isinstance(node, FuncCall):
|
|
68
|
+
return f"{_expression(node.callee)}({','.join(str(_expression(arg)) for arg in node.args)})"
|
|
69
|
+
if isinstance(node, Subscript):
|
|
70
|
+
return f"{_expression(node.object)}[{_expression(node.index)}]"
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _literal(node):
|
|
75
|
+
if isinstance(node, (StringLiteral, NumberLiteral, BoolLiteral)):
|
|
76
|
+
return node.value, True
|
|
77
|
+
if isinstance(node, UnaryOp) and node.op in ("+", "-"):
|
|
78
|
+
value, valid = _literal(node.operand)
|
|
79
|
+
if valid and type(value) in (int, float):
|
|
80
|
+
return value if node.op == "+" else -value, True
|
|
81
|
+
if isinstance(node, MemberAccess):
|
|
82
|
+
value = _expression(node)
|
|
83
|
+
if value and value.split(".")[0] in ("currency", "format", "scale", "strategy"):
|
|
84
|
+
return value, True
|
|
85
|
+
return _expression(node), False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _order_arguments(node):
|
|
89
|
+
name = _expression(node.callee)
|
|
90
|
+
positional = {
|
|
91
|
+
"strategy.entry": ("id", "direction", "qty", "limit", "stop", "oca_name", "oca_type", "comment", "alert_message", "disable_alert"),
|
|
92
|
+
"strategy.exit": ("id", "from_entry", "qty", "qty_percent", "profit", "limit", "loss", "stop", "trail_price", "trail_points", "trail_offset", "oca_name", "comment", "alert_message", "alert_profit", "alert_loss", "alert_trailing", "disable_alert"),
|
|
93
|
+
"strategy.close": ("id", "comment", "qty", "qty_percent", "alert_message", "immediately", "disable_alert"),
|
|
94
|
+
"strategy.close_all": ("comment", "alert_message", "immediately", "disable_alert"),
|
|
95
|
+
}.get(name, ())
|
|
96
|
+
arguments = {positional[index] if index < len(positional) else f"positional_{index}": argument
|
|
97
|
+
for index, argument in enumerate(node.args)}
|
|
98
|
+
if arguments.keys() & node.kwargs.keys():
|
|
99
|
+
arguments["duplicate_argument"] = None
|
|
100
|
+
arguments.update(node.kwargs)
|
|
101
|
+
return arguments
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _order_shape(node, short_ids):
|
|
105
|
+
name = _expression(node.callee)
|
|
106
|
+
arguments = _order_arguments(node)
|
|
107
|
+
if name not in MODELED_ORDER_ARGUMENTS or not set(arguments) <= MODELED_ORDER_ARGUMENTS[name]:
|
|
108
|
+
return name + (" (unproven exit terms)" if name == "strategy.exit" else " (unproven order shape)")
|
|
109
|
+
if name != "strategy.close_all" and not isinstance(arguments.get("id"), StringLiteral):
|
|
110
|
+
return name + " (unproven order id)"
|
|
111
|
+
if name == "strategy.entry" and _expression(arguments.get("direction")) in ("strategy.long", "strategy.short"):
|
|
112
|
+
priced = {key for key in ("limit", "stop") if key in arguments}
|
|
113
|
+
if len(priced) < 2 and not (set(arguments) & {"oca_name", "oca_type"}):
|
|
114
|
+
return "entry:" + (next(iter(priced)) if priced else "market")
|
|
115
|
+
if name == "strategy.exit":
|
|
116
|
+
if (set(arguments) & {"profit", "loss", "trail_price", "trail_points", "trail_offset", "qty", "qty_percent"}):
|
|
117
|
+
return name + " (unproven exit terms)"
|
|
118
|
+
if ("limit" in arguments and "stop" in arguments
|
|
119
|
+
and isinstance(arguments.get("from_entry"), StringLiteral)
|
|
120
|
+
and _expression(arguments["from_entry"]) in short_ids):
|
|
121
|
+
return "exit:short_bracket"
|
|
122
|
+
if name in ("strategy.close", "strategy.close_all"):
|
|
123
|
+
immediate = arguments.get("immediately")
|
|
124
|
+
if immediate is None or isinstance(immediate, BoolLiteral) and not immediate.value:
|
|
125
|
+
if not (set(arguments) & {"qty", "qty_percent"}):
|
|
126
|
+
return "close:market"
|
|
127
|
+
return name + " (unproven order shape)"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def capabilities_documents(emitter) -> tuple[str, str]:
|
|
131
|
+
declarations = dict(DECLARATION_DEFAULTS)
|
|
132
|
+
unresolved = []
|
|
133
|
+
requests = []
|
|
134
|
+
lower_symbols = {}
|
|
135
|
+
intrabar = False
|
|
136
|
+
recorded = False
|
|
137
|
+
order_nodes = []
|
|
138
|
+
order_settings = {}
|
|
139
|
+
for node, _depth in iter_ast_nodes(emitter.ctx.ast):
|
|
140
|
+
if isinstance(node, StrategyDecl):
|
|
141
|
+
arguments = {STRATEGY_PARAMETERS[index] if index < len(STRATEGY_PARAMETERS)
|
|
142
|
+
else f"strategy() positional argument {index + 1}": argument
|
|
143
|
+
for index, argument in enumerate(node.args)}
|
|
144
|
+
unresolved.extend(arguments.keys() & node.kwargs.keys())
|
|
145
|
+
arguments.update(node.kwargs)
|
|
146
|
+
order_settings.update({name: _literal(argument)[0] for name, argument in arguments.items()
|
|
147
|
+
if name in ORDER_SETTINGS})
|
|
148
|
+
for name, argument in arguments.items():
|
|
149
|
+
value, valid = _literal(argument)
|
|
150
|
+
default = DECLARATION_DEFAULTS.get(name)
|
|
151
|
+
if name in DECLARATION_DEFAULTS:
|
|
152
|
+
valid &= type(value) is type(default)
|
|
153
|
+
if name == "backtest_fill_limits_assumption":
|
|
154
|
+
valid = (type(value) is int and value >= 0) or (
|
|
155
|
+
type(value) is float and math.isfinite(value) and value >= 0 and value == int(value))
|
|
156
|
+
if valid:
|
|
157
|
+
value = int(value)
|
|
158
|
+
declarations[name] = value
|
|
159
|
+
if not valid or name not in STRATEGY_PARAMETERS and name not in DECLARATION_DEFAULTS:
|
|
160
|
+
unresolved.append(name)
|
|
161
|
+
elif isinstance(node, VarDecl):
|
|
162
|
+
intrabar |= node.is_varip
|
|
163
|
+
elif isinstance(node, FuncCall):
|
|
164
|
+
name, namespace = emitter._resolve_callee(node.callee)
|
|
165
|
+
qualified = _expression(node.callee) or ""
|
|
166
|
+
if qualified.startswith("strategy.") and qualified not in READ_ONLY_STRATEGY_CALLS:
|
|
167
|
+
order_nodes.append(node)
|
|
168
|
+
if namespace == "request" and name == "security_lower_tf":
|
|
169
|
+
expression = node.args[2] if len(node.args) > 2 else node.kwargs.get("expression")
|
|
170
|
+
lower_symbols[id(expression)] = node.args[0] if node.args else node.kwargs.get("symbol")
|
|
171
|
+
parts = (node.annotations or {}).get("pf_recorded_key")
|
|
172
|
+
if parts is not None:
|
|
173
|
+
recorded = True
|
|
174
|
+
requests.append({
|
|
175
|
+
"function": f"{namespace}.{name}",
|
|
176
|
+
"symbol": _expression(node.args[0]) if node.args else None,
|
|
177
|
+
"timeframe": "",
|
|
178
|
+
"lookahead": f"barmerge.lookahead_{parts.get('lookahead', 'off')}",
|
|
179
|
+
"gaps": f"barmerge.gaps_{parts.get('gaps', 'off')}",
|
|
180
|
+
"heikinashi": False,
|
|
181
|
+
"feed": "recorded",
|
|
182
|
+
})
|
|
183
|
+
if isinstance(node, Identifier) and node.name == "timenow":
|
|
184
|
+
unresolved.append("timenow (bar timestamp approximation)")
|
|
185
|
+
if isinstance(node, Identifier) and node.name in ("last_bar_index", "last_bar_time"):
|
|
186
|
+
unresolved.append(f"{node.name} (stream endpoint is not batch-equivalent)")
|
|
187
|
+
if isinstance(node, MemberAccess) and _expression(node) == "barstate.isrealtime":
|
|
188
|
+
unresolved.append("barstate.isrealtime (compiled historical-only approximation)")
|
|
189
|
+
if isinstance(node, MemberAccess) and _expression(node) in (
|
|
190
|
+
"barstate.islast", "barstate.islastconfirmedhistory"):
|
|
191
|
+
unresolved.append(f"{_expression(node)} (stream endpoint is not batch-equivalent)")
|
|
192
|
+
|
|
193
|
+
auxiliary = False
|
|
194
|
+
confirmed_requests = []
|
|
195
|
+
lowering_sites = {site["sec_id"]: site for site in emitter._security_eval_info}
|
|
196
|
+
for site in emitter._security_calls:
|
|
197
|
+
lowering = lowering_sites[site["sec_id"]]
|
|
198
|
+
symbol_node = site.get("symbol_node")
|
|
199
|
+
if site.get("is_lower_tf_array"):
|
|
200
|
+
symbol_node = lower_symbols.get(id(site.get("expr_node")), symbol_node)
|
|
201
|
+
if isinstance(symbol_node, FuncCall) and site.get("heikinashi") and symbol_node.args:
|
|
202
|
+
symbol_node = symbol_node.args[0]
|
|
203
|
+
symbol = _expression(symbol_node) if symbol_node is not None else "syminfo.tickerid"
|
|
204
|
+
timeframe = lowering.get("tf")
|
|
205
|
+
if timeframe is None:
|
|
206
|
+
timeframe = "timeframe.period" if lowering.get("tf_expr") == "script_tf_" else _expression(site.get("tf_node"))
|
|
207
|
+
lookahead = _expression(site.get("lookahead_node")) or "barmerge.lookahead_off"
|
|
208
|
+
gaps = _expression(site.get("gaps_node")) or "barmerge.gaps_off"
|
|
209
|
+
if isinstance(symbol_node, Identifier) and symbol_node.name not in emitter._global_mutable_infos:
|
|
210
|
+
symbol = _expression((getattr(emitter.ctx, "global_expr_map", {}) or {}).get(
|
|
211
|
+
symbol_node.name, symbol_node))
|
|
212
|
+
feed = "auxiliary" if lowering.get("foreign") else "chart"
|
|
213
|
+
auxiliary |= feed == "auxiliary"
|
|
214
|
+
if lowering.get("tf") is None and timeframe != "timeframe.period":
|
|
215
|
+
unresolved.append(f"request.security[{site['sec_id']}].timeframe")
|
|
216
|
+
if lookahead not in ("barmerge.lookahead_off", "barmerge.lookahead_on"):
|
|
217
|
+
unresolved.append(f"request.security[{site['sec_id']}].lookahead")
|
|
218
|
+
requests.append({
|
|
219
|
+
"function": "request.security_lower_tf" if site.get("is_lower_tf_array") else "request.security",
|
|
220
|
+
"symbol": symbol,
|
|
221
|
+
"timeframe": timeframe,
|
|
222
|
+
"lookahead": lookahead,
|
|
223
|
+
"gaps": gaps,
|
|
224
|
+
"heikinashi": bool(lowering.get("heikinashi")),
|
|
225
|
+
"feed": feed,
|
|
226
|
+
})
|
|
227
|
+
expression = _expression(site.get("expr_node"))
|
|
228
|
+
if expression in ("close", "close[1]", "ta.sma(close,4)", "ta.ema(close,3)") and "close" in (
|
|
229
|
+
getattr(emitter.ctx, "global_expr_map", {}) or {}):
|
|
230
|
+
unresolved.append("request.security.expression (user-bound close)")
|
|
231
|
+
confirmed_requests.append({**requests[-1], "expression": expression})
|
|
232
|
+
for index, site in enumerate((emitter.ctx.ast.annotations or {}).get(CAPABILITY_UNPINNED_ANNOTATION, ())):
|
|
233
|
+
requests.append({
|
|
234
|
+
"function": site["function"],
|
|
235
|
+
"symbol": _expression(site["symbol_node"]),
|
|
236
|
+
"timeframe": _expression(site["tf_node"]) if site["function"] in (
|
|
237
|
+
"request.security", "request.security_lower_tf") else "",
|
|
238
|
+
"lookahead": _expression(site["lookahead_node"]) or "barmerge.lookahead_off",
|
|
239
|
+
"gaps": _expression(site["gaps_node"]) or "barmerge.gaps_off",
|
|
240
|
+
"heikinashi": False,
|
|
241
|
+
"feed": "unpinned",
|
|
242
|
+
})
|
|
243
|
+
unresolved.append(f"{site['function']}[unpinned:{index}]")
|
|
244
|
+
requirements = {
|
|
245
|
+
"auxiliary_security_feeds": auxiliary,
|
|
246
|
+
"native_security_feeds": False,
|
|
247
|
+
"fx_curve": declarations["currency"] != "currency.NONE",
|
|
248
|
+
"recorded_series": recorded,
|
|
249
|
+
"historical_probe_overrides": False,
|
|
250
|
+
"intrabar_persistence": intrabar,
|
|
251
|
+
}
|
|
252
|
+
short_ids = set()
|
|
253
|
+
conflicting_ids = set()
|
|
254
|
+
dynamic_entry_id = False
|
|
255
|
+
for node in order_nodes:
|
|
256
|
+
if _expression(node.callee) != "strategy.entry":
|
|
257
|
+
continue
|
|
258
|
+
arguments = _order_arguments(node)
|
|
259
|
+
identifier = arguments.get("id")
|
|
260
|
+
if not isinstance(identifier, StringLiteral):
|
|
261
|
+
dynamic_entry_id = True
|
|
262
|
+
elif _expression(arguments.get("direction")) == "strategy.short":
|
|
263
|
+
short_ids.add(identifier.value)
|
|
264
|
+
else:
|
|
265
|
+
conflicting_ids.add(identifier.value)
|
|
266
|
+
if dynamic_entry_id or conflicting_ids:
|
|
267
|
+
short_ids.clear()
|
|
268
|
+
orders = set(_order_shape(node, short_ids - conflicting_ids) for node in order_nodes)
|
|
269
|
+
if declarations["process_orders_on_close"]:
|
|
270
|
+
bracket = orders == {"entry:market", "exit:short_bracket"}
|
|
271
|
+
proven = (not order_settings and not bracket) or (
|
|
272
|
+
bracket and order_settings == PROVEN_BRACKET_SETTINGS) or (
|
|
273
|
+
orders == {"entry:market"} and order_settings == PROVEN_SLIPPED_SETTINGS
|
|
274
|
+
and not confirmed_requests and not intrabar)
|
|
275
|
+
if not proven:
|
|
276
|
+
orders.add("strategy() (unproven POOC sizing, slippage or account settings)")
|
|
277
|
+
confirmed = {"version": 1, "requests": confirmed_requests,
|
|
278
|
+
"orders": sorted(orders),
|
|
279
|
+
"intrabar_persistence": intrabar}
|
|
280
|
+
encode = lambda value: json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
281
|
+
return (encode({"version": 1, "declarations": declarations, "requests": requests,
|
|
282
|
+
"requirements": requirements, "unresolved": sorted(set(unresolved))}), encode(confirmed))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def capabilities_document(emitter) -> str:
|
|
286
|
+
return capabilities_documents(emitter)[0]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def emit_capabilities_exports(emitter, lines: list[str]) -> None:
|
|
290
|
+
legacy, confirmed = capabilities_documents(emitter)
|
|
291
|
+
literal = json.dumps(legacy)
|
|
292
|
+
lines.extend([
|
|
293
|
+
"#ifdef PF_SETTINGS_API_VERSION",
|
|
294
|
+
"#ifdef PF_CAPABILITIES_API_VERSION",
|
|
295
|
+
" uint32_t strategy_capabilities_api_version(void) { return PF_CAPABILITIES_API_VERSION; }",
|
|
296
|
+
" int strategy_capabilities_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
|
|
297
|
+
" if (required) *required = 0;",
|
|
298
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
299
|
+
' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
|
|
300
|
+
f" ::pineforge::checked_settings::receipt({literal}, json, capacity, required);",
|
|
301
|
+
" });", " }",
|
|
302
|
+
" uint32_t strategy_confirmed_bar_api_version(void) { return 1u; }",
|
|
303
|
+
" int strategy_confirmed_bar_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
|
|
304
|
+
" if (required) *required = 0;",
|
|
305
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
306
|
+
' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
|
|
307
|
+
f" ::pineforge::checked_settings::receipt({json.dumps(confirmed)}, json, capacity, required);",
|
|
308
|
+
" });", " }", "#endif", "#endif",
|
|
309
|
+
])
|
|
310
|
+
from .order_shapes import order_shapes_document
|
|
311
|
+
lines[-2:-2] = [
|
|
312
|
+
" uint32_t strategy_order_shapes_api_version(void) { return 1u; }",
|
|
313
|
+
" int strategy_order_shapes_receipt(void* s, char* json, size_t capacity, size_t* required, char* error, size_t error_capacity) {",
|
|
314
|
+
" if (required) *required = 0;",
|
|
315
|
+
" return ::pineforge::checked_settings::boundary(error, error_capacity, [&] {",
|
|
316
|
+
' ::pineforge::checked_settings::require(s != nullptr, "null strategy");',
|
|
317
|
+
f" ::pineforge::checked_settings::receipt({json.dumps(order_shapes_document(emitter))}, json, capacity, required);",
|
|
318
|
+
" });", " }",
|
|
319
|
+
]
|
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
"""Opt-in settings metadata, validation and exception-contained C exports."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
|
|
3
5
|
from ..ast_nodes import Identifier, MemberAccess
|
|
6
|
+
from ..errors import CompileError
|
|
7
|
+
from .tables import ALERT_FREQ_VALUES, NAME_ECHO_STRING_MEMBERS, ORDER_DIRECTION_MAP
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_QUOTED_STRING = r'"(?:[^"\\]|\\.)*"'
|
|
11
|
+
_STRING_LITERAL = re.compile(rf'(?:std::string\({_QUOTED_STRING}\)|{_QUOTED_STRING})')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _string_setting_arg(expr, lowered: str) -> str | None:
|
|
15
|
+
"""Use the body's known string constants, never its inert visual codes.
|
|
16
|
+
|
|
17
|
+
Name-echo strings are accepted only for inventoried currency/format members.
|
|
18
|
+
Unknown constants leave the checked input unsupported; legacy getters
|
|
19
|
+
and setters retain their existing execution semantics.
|
|
20
|
+
"""
|
|
21
|
+
if isinstance(expr, MemberAccess):
|
|
22
|
+
if not isinstance(expr.object, Identifier):
|
|
23
|
+
return None
|
|
24
|
+
namespace = expr.object.name
|
|
25
|
+
known = (
|
|
26
|
+
namespace == "alert" and expr.member in ALERT_FREQ_VALUES
|
|
27
|
+
or namespace == "order" and expr.member in ORDER_DIRECTION_MAP
|
|
28
|
+
or namespace == "session" and expr.member in ("regular", "extended")
|
|
29
|
+
or expr.member in NAME_ECHO_STRING_MEMBERS.get(namespace, ())
|
|
30
|
+
)
|
|
31
|
+
if not known:
|
|
32
|
+
return None
|
|
33
|
+
return lowered if _STRING_LITERAL.fullmatch(lowered) else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _visit_setting_arg(emitter, expr) -> str:
|
|
37
|
+
"""An input argument's C++ for the settings metadata. The metadata is
|
|
38
|
+
emitted in the constructor, ahead of the script body: an error here is
|
|
39
|
+
held until the body is generated (``CodeGen.generate``), so the script's
|
|
40
|
+
first error in source order is the one raised."""
|
|
41
|
+
try:
|
|
42
|
+
return emitter._visit_expr(expr)
|
|
43
|
+
except CompileError as error:
|
|
44
|
+
emitter._defer_settings_error(error)
|
|
45
|
+
return "0"
|
|
4
46
|
|
|
5
47
|
|
|
6
48
|
def emit_settings_members(emitter, lines: list[str], constructor: list[str]) -> None:
|
|
@@ -38,7 +80,8 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
38
80
|
key = emitter._input_key_literal(name)
|
|
39
81
|
default = emitter._get_input_default(node)
|
|
40
82
|
getter = emitter._input_getter_for_call(node, func_name, namespace)
|
|
41
|
-
|
|
83
|
+
default_lowered = _visit_setting_arg(emitter, default) if default is not None else "0"
|
|
84
|
+
default_cpp = default_lowered
|
|
42
85
|
default_cpp = emitter._coerce_string_input_default(getter, default_cpp)
|
|
43
86
|
value_type = {
|
|
44
87
|
"get_input_int": "int", "get_input_int64": "int",
|
|
@@ -50,6 +93,11 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
50
93
|
option_values = []
|
|
51
94
|
supported = "true"
|
|
52
95
|
default_serialized = None
|
|
96
|
+
if getter == "get_input_string":
|
|
97
|
+
default_serialized = _string_setting_arg(default, default_lowered)
|
|
98
|
+
if default_serialized is None:
|
|
99
|
+
default_serialized = 'std::string("")'
|
|
100
|
+
supported = "false"
|
|
53
101
|
names, merged = emitter._merged_args(node, func_name, namespace)
|
|
54
102
|
arguments = dict(zip(names or [], merged))
|
|
55
103
|
arguments.update(node.kwargs)
|
|
@@ -89,16 +137,27 @@ def emit_settings_members(emitter, lines: list[str], constructor: list[str]) ->
|
|
|
89
137
|
declared = getattr(arguments.get("options"), "elements", None)
|
|
90
138
|
if declared:
|
|
91
139
|
for option in declared:
|
|
92
|
-
option_cpp = emitter
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
option_cpp = _visit_setting_arg(emitter, option)
|
|
141
|
+
if getter == "get_input_string":
|
|
142
|
+
option_cpp = _string_setting_arg(option, option_cpp)
|
|
143
|
+
if option_cpp is None:
|
|
144
|
+
supported = "false"
|
|
145
|
+
else:
|
|
146
|
+
options.append(option_cpp)
|
|
147
|
+
else:
|
|
148
|
+
options.append(f'::pineforge::checked_settings::number({option_cpp})')
|
|
149
|
+
if getter == "get_input_string":
|
|
150
|
+
if arguments.get("options") is not None and declared is None:
|
|
151
|
+
supported = "false"
|
|
152
|
+
if supported == "false":
|
|
153
|
+
options = []
|
|
95
154
|
expression = f'{getter}({key}, {default_cpp})'
|
|
96
155
|
effective = (expression if getter == "get_input_string" else
|
|
97
156
|
f'::pineforge::checked_settings::number({expression})')
|
|
98
157
|
if default_serialized is None:
|
|
99
158
|
default_serialized = (default_cpp if value_type in ("string", "source") else
|
|
100
159
|
f'::pineforge::checked_settings::number({default_cpp})')
|
|
101
|
-
constraints = [emitter
|
|
160
|
+
constraints = [_visit_setting_arg(emitter, arguments[name]) if arguments.get(name) is not None
|
|
102
161
|
else "std::numeric_limits<double>::quiet_NaN()"
|
|
103
162
|
for name in ("minval", "maxval", "step")]
|
|
104
163
|
metadata = (f'{{{key}, "{value_type}", {default_serialized}, '
|
|
@@ -1281,6 +1281,7 @@ class TopLevelEmitter:
|
|
|
1281
1281
|
lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
|
|
1282
1282
|
lines.append(" }")
|
|
1283
1283
|
from .checked_settings import emit_settings_members
|
|
1284
|
+
self._order_shape_constructor = tuple(ctor_body)
|
|
1284
1285
|
emit_settings_members(self, lines, ctor_body)
|
|
1285
1286
|
|
|
1286
1287
|
if self._uses_recorded_requests():
|
|
@@ -1314,7 +1315,7 @@ class TopLevelEmitter:
|
|
|
1314
1315
|
tf = info.get("tf")
|
|
1315
1316
|
tf_expr = info.get("tf_expr")
|
|
1316
1317
|
if tf:
|
|
1317
|
-
tf_expr =
|
|
1318
|
+
tf_expr = self._security_tf_literal(tf)
|
|
1318
1319
|
elif tf == "" and not tf_expr:
|
|
1319
1320
|
# An empty timeframe string is the chart's.
|
|
1320
1321
|
tf_expr = "input_tf_"
|
|
@@ -1421,7 +1422,7 @@ class TopLevelEmitter:
|
|
|
1421
1422
|
sec_id = info["sec_id"]
|
|
1422
1423
|
symbol = self._security_tf_runtime_expr(info["symbol_node"])
|
|
1423
1424
|
column = self._security_footprint_column(sec_id)
|
|
1424
|
-
column = f', "{column}"' if column else ""
|
|
1425
|
+
column = f', "{self._cpp_string_escape(column)}"' if column else ""
|
|
1425
1426
|
ignore_node = info.get("ignore_invalid_node")
|
|
1426
1427
|
if ignore_node is None:
|
|
1427
1428
|
ignore = "false"
|
|
@@ -1886,7 +1887,7 @@ class TopLevelEmitter:
|
|
|
1886
1887
|
for pragma in pragmas:
|
|
1887
1888
|
cpp_expr = self._visit_expr(pragma.expr_node)
|
|
1888
1889
|
lines.append(
|
|
1889
|
-
f'{inner}trace(std::string("{pragma.name}"), '
|
|
1890
|
+
f'{inner}trace(std::string("{self._cpp_string_escape(pragma.name)}"), '
|
|
1890
1891
|
f"(double)({cpp_expr}));"
|
|
1891
1892
|
)
|
|
1892
1893
|
lines.append(f"{pad}}}")
|
|
@@ -1998,6 +1999,7 @@ class TopLevelEmitter:
|
|
|
1998
1999
|
lines.append(" }")
|
|
1999
2000
|
from .checked_settings import emit_settings_exports
|
|
2000
2001
|
emit_settings_exports(lines)
|
|
2002
|
+
self._capabilities_export_at = len(lines)
|
|
2001
2003
|
if self._declares_bar_magnifier():
|
|
2002
2004
|
# TradingView runs a script that declares use_bar_magnifier = true
|
|
2003
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
|
|
@@ -433,6 +434,12 @@ class NamingHelper:
|
|
|
433
434
|
.replace("\t", "\\t")
|
|
434
435
|
)
|
|
435
436
|
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _cpp_comment_escape(s: str) -> str:
|
|
439
|
+
"""Keep text (a script's string rendered into it) inside the C++
|
|
440
|
+
``/* ... */`` comment it is written in."""
|
|
441
|
+
return s.replace("/*", "/ *").replace("*/", "* /")
|
|
442
|
+
|
|
436
443
|
def _initialise_safe_names(self, ast) -> None:
|
|
437
444
|
"""Reserve all authored spellings before assigning escaped ones.
|
|
438
445
|
|