@pineforge/codegen-pyodide 0.10.2 → 0.10.4
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/package.json +1 -1
- package/pineforge_codegen/analyzer/base.py +13 -1
- package/pineforge_codegen/analyzer/call_handlers.py +8 -2
- package/pineforge_codegen/codegen/base.py +77 -21
- package/pineforge_codegen/codegen/emit_top.py +73 -20
- package/pineforge_codegen/codegen/ta.py +579 -288
- package/pineforge_codegen/codegen/tables.py +22 -5
- package/pineforge_codegen/codegen/types.py +195 -6
- package/pineforge_codegen/codegen/visit_call.py +120 -57
- package/pineforge_codegen/codegen/visit_expr.py +39 -8
- package/pineforge_codegen/codegen/visit_stmt.py +15 -3
- package/pineforge_codegen/symbols.py +4 -2
- package/pineforge_codegen-0.10.4.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-0.10.2.tar.gz +0 -0
|
@@ -222,6 +222,18 @@ TA_COMPUTE_ARGS = {
|
|
|
222
222
|
"max": [0], "min": [0], "rci": [0],
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
# issue #178: ta.atr / ta.tr read their true range against the previous
|
|
226
|
+
# CHART bar's close (close[1]) even when the call site executes sparsely —
|
|
227
|
+
# the RMA advances on the executions only (TradingView pin 2026-09-06,
|
|
228
|
+
# lab tv i178-sparse-atr-sense, BINANCE:BTCUSDT 60: 398/398 executions equal
|
|
229
|
+
# the chart-close model, 0/398 the previous-execution model). The chart
|
|
230
|
+
# context therefore passes ``BacktestEngine::prev_chart_close()`` as the
|
|
231
|
+
# 4th compute() argument (the engine's always-on tracker); the per-object
|
|
232
|
+
# prev_close (3-arg form) remains for request.security contexts, whose
|
|
233
|
+
# ``bar.`` rewrite has no chart-close tracker.
|
|
234
|
+
TA_CHART_PREV_CLOSE = {"atr", "tr"}
|
|
235
|
+
TA_CHART_PREV_CLOSE_ARG = "prev_chart_close()"
|
|
236
|
+
|
|
225
237
|
# TA functions whose ``.compute()`` always receives bar OHLC implicitly.
|
|
226
238
|
TA_IMPLICIT_COMPUTE_FULL = {
|
|
227
239
|
"atr": "current_bar_.high, current_bar_.low, current_bar_.close",
|
|
@@ -248,12 +260,17 @@ TA_IMPLICIT_APPEND = {
|
|
|
248
260
|
"kc": "current_bar_.high, current_bar_.low, current_bar_.close",
|
|
249
261
|
"mfi": "current_bar_.volume",
|
|
250
262
|
"kcw": "current_bar_.high, current_bar_.low, current_bar_.close",
|
|
251
|
-
# ta.vwap needs the bar timestamp
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
|
|
263
|
+
# ta.vwap needs the bar timestamp plus the symbol clock (syminfo.timezone
|
|
264
|
+
# + syminfo.session) so the runtime can detect Daily anchor boundaries:
|
|
265
|
+
# Pine v6's default `anchor = timeframe.change("1D")` is the SYMBOL's
|
|
266
|
+
# daily bar (17:00 ET on OANDA forex, 09:30 ET RTH on NASDAQ equities,
|
|
267
|
+
# 00:00 UTC on a 24x7 UTC symbol). The engine's tz/session overload
|
|
268
|
+
# reduces to the UTC-day integer math for the default UTC/24x7 syminfo.
|
|
269
|
+
# The tail is wrapped in PF_VWAP_SESSION_ANCHOR_ARGS (prelude, emit_top.py)
|
|
270
|
+
# so it compiles out against engines without the overload.
|
|
271
|
+
"vwap": "current_bar_.volume, current_bar_.timestamp PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)",
|
|
255
272
|
# 3-arg bands form uses the same implicit append as the scalar form.
|
|
256
|
-
"vwap_bands": "current_bar_.volume, current_bar_.timestamp",
|
|
273
|
+
"vwap_bands": "current_bar_.volume, current_bar_.timestamp PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)",
|
|
257
274
|
}
|
|
258
275
|
|
|
259
276
|
# Tuple field names for TA functions returning tuples.
|
|
@@ -135,8 +135,9 @@ class TypeInferer:
|
|
|
135
135
|
if spec is None:
|
|
136
136
|
return "double"
|
|
137
137
|
if spec.kind == "primitive":
|
|
138
|
-
return {"float": "double", "int": "int", "
|
|
139
|
-
"
|
|
138
|
+
return {"float": "double", "int": "int", "int64": "int64_t",
|
|
139
|
+
"bool": "bool", "string": "std::string",
|
|
140
|
+
"color": "int"}.get(spec.name or "float", "double")
|
|
140
141
|
if spec.kind == "udt" and spec.name:
|
|
141
142
|
# Drawing handle structs (P1): map BEFORE the _udt_defs check so
|
|
142
143
|
# array<line> -> std::vector<Line> and scalar line -> Line instead
|
|
@@ -269,6 +270,175 @@ class TypeInferer:
|
|
|
269
270
|
return self._default_for_type(cpp_type)
|
|
270
271
|
|
|
271
272
|
def _collection_spec_for_name(self, name: str) -> TypeSpec | None:
|
|
273
|
+
"""``_collection_spec_for_name_raw`` with the wide-int element applied
|
|
274
|
+
(``_widen_array_spec_for_name``): an ``array<int>`` that stores epoch
|
|
275
|
+
milliseconds resolves as ``std::vector<int64_t>`` everywhere it is
|
|
276
|
+
read, declared or passed."""
|
|
277
|
+
return self._widen_array_spec_for_name(
|
|
278
|
+
name, self._collection_spec_for_name_raw(name)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Element-returning array reads whose result carries the receiver's wide
|
|
282
|
+
# provenance (``_expr_returns_wide_int``).
|
|
283
|
+
_WIDE_ARRAY_ELEMENT_READS = frozenset({
|
|
284
|
+
"get", "first", "last", "pop", "shift", "remove", "max", "min",
|
|
285
|
+
})
|
|
286
|
+
# Writers whose VALUE argument decides whether an int array is wide:
|
|
287
|
+
# name -> (functional-form value index, method-form value index).
|
|
288
|
+
_ARRAY_ELEMENT_WRITERS = {
|
|
289
|
+
"push": (1, 0), "unshift": (1, 0), "set": (2, 1), "insert": (2, 1),
|
|
290
|
+
"fill": (1, 0),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _int_fits_int32(value: int) -> bool:
|
|
295
|
+
return -(1 << 31) <= value < (1 << 31)
|
|
296
|
+
|
|
297
|
+
def _pure_int_literal_value(self, node) -> int | None:
|
|
298
|
+
"""Exact value of an expression built only from int literals and
|
|
299
|
+
``+ - *`` / unary minus, else ``None``.
|
|
300
|
+
|
|
301
|
+
Pine ``int`` is 64-bit: ``90 * 24 * 60 * 60 * 1000`` (three months in
|
|
302
|
+
milliseconds) is 7 776 000 000 on TradingView. Emitted as C++ ``int``
|
|
303
|
+
literal arithmetic the same product overflows (wraps to -813 934 592),
|
|
304
|
+
so a ``(time - t0) > threeMonths`` expiry fires on every bar (round 8
|
|
305
|
+
family U: latibonit15 execution-signals-confluence, six lanes; lab tv
|
|
306
|
+
u-lati-levels-nq15 vs the engine, 2026-09-05). Folding the literal
|
|
307
|
+
subtree in Python keeps the exact value; the caller emits it as a
|
|
308
|
+
64-bit literal only when it does not fit ``int32`` so every in-range
|
|
309
|
+
expression is byte-identical to before.
|
|
310
|
+
"""
|
|
311
|
+
if isinstance(node, NumberLiteral):
|
|
312
|
+
return node.value if isinstance(node.value, int) and not isinstance(node.value, bool) else None
|
|
313
|
+
if isinstance(node, UnaryOp) and node.op == "-":
|
|
314
|
+
inner = self._pure_int_literal_value(node.operand)
|
|
315
|
+
return -inner if inner is not None else None
|
|
316
|
+
if isinstance(node, BinOp) and node.op in ("+", "-", "*"):
|
|
317
|
+
left = self._pure_int_literal_value(node.left)
|
|
318
|
+
if left is None:
|
|
319
|
+
return None
|
|
320
|
+
right = self._pure_int_literal_value(node.right)
|
|
321
|
+
if right is None:
|
|
322
|
+
return None
|
|
323
|
+
if node.op == "+":
|
|
324
|
+
return left + right
|
|
325
|
+
if node.op == "-":
|
|
326
|
+
return left - right
|
|
327
|
+
return left * right
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def _literal_overflows_int32(self, node) -> bool:
|
|
331
|
+
value = self._pure_int_literal_value(node)
|
|
332
|
+
return value is not None and not self._int_fits_int32(value)
|
|
333
|
+
|
|
334
|
+
def _array_receiver_and_value(self, call):
|
|
335
|
+
"""``(receiver_name, value_node)`` of an element-writing array call in
|
|
336
|
+
either form (``array.push(a, v)`` / ``a.push(v)``), else ``None``."""
|
|
337
|
+
if not isinstance(call, FuncCall):
|
|
338
|
+
return None
|
|
339
|
+
func_name, namespace = self._resolve_callee(call.callee)
|
|
340
|
+
if func_name not in self._ARRAY_ELEMENT_WRITERS:
|
|
341
|
+
return None
|
|
342
|
+
functional_idx, method_idx = self._ARRAY_ELEMENT_WRITERS[func_name]
|
|
343
|
+
if namespace == "array":
|
|
344
|
+
receiver = call.args[0] if call.args else call.kwargs.get("id")
|
|
345
|
+
value = (call.args[functional_idx]
|
|
346
|
+
if len(call.args) > functional_idx
|
|
347
|
+
else call.kwargs.get("value"))
|
|
348
|
+
elif (isinstance(call.callee, MemberAccess)
|
|
349
|
+
and isinstance(call.callee.object, Identifier)):
|
|
350
|
+
receiver = call.callee.object
|
|
351
|
+
value = (call.args[method_idx]
|
|
352
|
+
if len(call.args) > method_idx
|
|
353
|
+
else call.kwargs.get("value"))
|
|
354
|
+
else:
|
|
355
|
+
return None
|
|
356
|
+
if not isinstance(receiver, Identifier) or value is None:
|
|
357
|
+
return None
|
|
358
|
+
return receiver.name, value
|
|
359
|
+
|
|
360
|
+
def _wide_int_array_names(self) -> set[str]:
|
|
361
|
+
"""Names of int arrays that hold a wide integer: one that receives an
|
|
362
|
+
epoch-millisecond value (``time``, ``time_close``, ``timestamp(...)``,
|
|
363
|
+
a wide callable result, ...) through ``array.push`` / ``unshift`` /
|
|
364
|
+
``set`` / ``insert`` / ``fill``, or is built by ``array.new_int`` /
|
|
365
|
+
``array.new<int>`` / ``array.from`` from one. Their element type is
|
|
366
|
+
``int64_t`` (TypeSpec primitive ``int64``): Pine's ``int`` holds the
|
|
367
|
+
epoch, ``std::vector<int>`` truncates it. Cached on the instance."""
|
|
368
|
+
cached = getattr(self, "_wide_int_array_cache", None)
|
|
369
|
+
if cached is not None:
|
|
370
|
+
return cached
|
|
371
|
+
# The scan below asks ``_expr_returns_wide_int``, whose receiver typing
|
|
372
|
+
# resolves collection specs through ``_collection_spec_for_name`` and so
|
|
373
|
+
# back here: publish the (growing) set first so the recursion reads the
|
|
374
|
+
# partial answer instead of re-entering, then iterate to the fixpoint so
|
|
375
|
+
# an array filled from another wide array's elements widens too.
|
|
376
|
+
names: set[str] = set()
|
|
377
|
+
self._wide_int_array_cache = names
|
|
378
|
+
|
|
379
|
+
def scan(nodes, owner_info):
|
|
380
|
+
for child in nodes:
|
|
381
|
+
if isinstance(child, FuncCall):
|
|
382
|
+
pair = self._array_receiver_and_value(child)
|
|
383
|
+
if pair is not None and self._expr_returns_wide_int(
|
|
384
|
+
pair[1], owner_info, set(), None
|
|
385
|
+
):
|
|
386
|
+
names.add(pair[0])
|
|
387
|
+
continue
|
|
388
|
+
target = None
|
|
389
|
+
value = None
|
|
390
|
+
if isinstance(child, VarDecl):
|
|
391
|
+
target, value = child.name, child.value
|
|
392
|
+
elif (isinstance(child, Assignment)
|
|
393
|
+
and isinstance(child.target, Identifier)):
|
|
394
|
+
target, value = child.target.name, child.value
|
|
395
|
+
if target is None or not isinstance(value, FuncCall):
|
|
396
|
+
continue
|
|
397
|
+
fn, ns = self._resolve_callee(value.callee)
|
|
398
|
+
if ns != "array":
|
|
399
|
+
continue
|
|
400
|
+
candidates = []
|
|
401
|
+
if fn in ("new_int", "new"):
|
|
402
|
+
if fn == "new":
|
|
403
|
+
targs = (self._template_args_from_call(value)
|
|
404
|
+
if hasattr(value, "annotations") else [])
|
|
405
|
+
if not targs or targs[0] != "int":
|
|
406
|
+
continue
|
|
407
|
+
if len(value.args) > 1:
|
|
408
|
+
candidates.append(value.args[1])
|
|
409
|
+
if "initial_value" in value.kwargs:
|
|
410
|
+
candidates.append(value.kwargs["initial_value"])
|
|
411
|
+
elif fn == "from":
|
|
412
|
+
candidates = list(value.args)
|
|
413
|
+
if any(self._expr_returns_wide_int(c, owner_info, set(), None)
|
|
414
|
+
for c in candidates):
|
|
415
|
+
names.add(target)
|
|
416
|
+
|
|
417
|
+
ast = getattr(self.ctx, "ast", None)
|
|
418
|
+
for _round in range(8):
|
|
419
|
+
before = len(names)
|
|
420
|
+
if ast is not None:
|
|
421
|
+
scan(self._walk_ast(ast), None)
|
|
422
|
+
for info in getattr(self.ctx, "func_infos", ()):
|
|
423
|
+
node = getattr(info, "node", None)
|
|
424
|
+
if node is not None:
|
|
425
|
+
scan(self._walk_ast_list(node.body), info)
|
|
426
|
+
if len(names) == before:
|
|
427
|
+
break
|
|
428
|
+
return names
|
|
429
|
+
|
|
430
|
+
def _widen_array_spec_for_name(self, name, spec):
|
|
431
|
+
"""``array<int>`` -> ``array<int64>`` when ``name`` is a wide int
|
|
432
|
+
array; every other spec (and ``None``) passes through unchanged."""
|
|
433
|
+
if (spec is None or spec.kind != "array" or spec.element is None
|
|
434
|
+
or spec.element.kind != "primitive"
|
|
435
|
+
or spec.element.name != "int"):
|
|
436
|
+
return spec
|
|
437
|
+
if name in self._wide_int_array_names():
|
|
438
|
+
return TypeSpec.array(TypeSpec.primitive("int64"))
|
|
439
|
+
return spec
|
|
440
|
+
|
|
441
|
+
def _collection_spec_for_name_raw(self, name: str) -> TypeSpec | None:
|
|
272
442
|
"""Resolve collection metadata with lexical precedence.
|
|
273
443
|
|
|
274
444
|
Source-ordered callable locals shadow loop bindings and parameters once
|
|
@@ -417,7 +587,9 @@ class TypeInferer:
|
|
|
417
587
|
return TypeSpec.primitive("string")
|
|
418
588
|
if node.op == "/" or left.name == "float" or right.name == "float":
|
|
419
589
|
return TypeSpec.primitive("float")
|
|
420
|
-
if left.name
|
|
590
|
+
if left.name in ("int", "int64") and right.name in ("int", "int64"):
|
|
591
|
+
if "int64" in (left.name, right.name):
|
|
592
|
+
return TypeSpec.primitive("int64")
|
|
421
593
|
return TypeSpec.primitive("int")
|
|
422
594
|
return None
|
|
423
595
|
spec = self._type_spec_from_expr(node)
|
|
@@ -976,7 +1148,7 @@ class TypeInferer:
|
|
|
976
1148
|
"percentrank", "abs", "standardize", "covariance", "binary_search",
|
|
977
1149
|
"binary_search_leftmost", "binary_search_rightmost", "sort_indices",
|
|
978
1150
|
}
|
|
979
|
-
if method in numeric_only and elem_cpp not in ("double", "int"):
|
|
1151
|
+
if method in numeric_only and elem_cpp not in ("double", "int", "int64_t"):
|
|
980
1152
|
self._codegen_error(
|
|
981
1153
|
None,
|
|
982
1154
|
f"array.{method} requires a numeric array",
|
|
@@ -1187,7 +1359,7 @@ class TypeInferer:
|
|
|
1187
1359
|
# truly untyped slot may consult the per-callsite specialization map.
|
|
1188
1360
|
spec = declared_spec
|
|
1189
1361
|
if spec is not None and spec.kind == "primitive":
|
|
1190
|
-
if spec.name
|
|
1362
|
+
if spec.name in ("int", "int64"):
|
|
1191
1363
|
return "int64_t"
|
|
1192
1364
|
if spec.name == "bool":
|
|
1193
1365
|
return "bool"
|
|
@@ -1217,7 +1389,7 @@ class TypeInferer:
|
|
|
1217
1389
|
specs = list(getattr(func_info, "param_type_specs", ()) or ())
|
|
1218
1390
|
spec = specs[index] if index < len(specs) else None
|
|
1219
1391
|
if spec is not None and spec.kind == "primitive":
|
|
1220
|
-
if spec.name
|
|
1392
|
+
if spec.name in ("int", "int64"):
|
|
1221
1393
|
return "int64_t"
|
|
1222
1394
|
if spec.name == "bool":
|
|
1223
1395
|
return "bool"
|
|
@@ -1352,8 +1524,25 @@ class TypeInferer:
|
|
|
1352
1524
|
return self._expr_is_int64_builtin(expr)
|
|
1353
1525
|
if self._expr_is_int64_builtin(expr):
|
|
1354
1526
|
return True
|
|
1527
|
+
if isinstance(expr, (BinOp, UnaryOp)) and self._literal_overflows_int32(expr):
|
|
1528
|
+
# ``90 * 24 * 60 * 60 * 1000``: an int-literal product beyond
|
|
1529
|
+
# int32 is a 64-bit value in Pine (``_pure_int_literal_value``).
|
|
1530
|
+
return True
|
|
1355
1531
|
if isinstance(expr, FuncCall):
|
|
1356
1532
|
func_name, namespace = self._resolve_callee(expr.callee)
|
|
1533
|
+
if func_name in self._WIDE_ARRAY_ELEMENT_READS:
|
|
1534
|
+
# An element read off a wide int array (``array.get(times, i)``
|
|
1535
|
+
# / ``times.get(i)``) carries the epoch: the destination must
|
|
1536
|
+
# not narrow it (``_wide_int_array_names``).
|
|
1537
|
+
receiver = None
|
|
1538
|
+
if namespace == "array":
|
|
1539
|
+
receiver = expr.args[0] if expr.args else expr.kwargs.get("id")
|
|
1540
|
+
elif (isinstance(expr.callee, MemberAccess)
|
|
1541
|
+
and isinstance(expr.callee.object, Identifier)):
|
|
1542
|
+
receiver = expr.callee.object
|
|
1543
|
+
if (isinstance(receiver, Identifier)
|
|
1544
|
+
and receiver.name in self._wide_int_array_names()):
|
|
1545
|
+
return True
|
|
1357
1546
|
if namespace is None and func_name in {
|
|
1358
1547
|
"int",
|
|
1359
1548
|
"float",
|
|
@@ -222,6 +222,55 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
|
|
|
222
222
|
return int(dt.timestamp() * 1000)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def _timestamp_calendar_lambda(tz: str, yr: str, mo: str, dy: str,
|
|
226
|
+
hr: str, mn: str, sc: str) -> str:
|
|
227
|
+
"""Inline C++ lambda turning calendar fields in timezone ``tz`` into Unix ms.
|
|
228
|
+
|
|
229
|
+
Shared by both ``timestamp(...)`` overloads: the tz-first form passes the
|
|
230
|
+
explicit tz expression, the numeric form passes ``syminfo_.timezone``
|
|
231
|
+
(Pine v6: "If the timezone argument is not specified, the function uses
|
|
232
|
+
the exchange time zone (syminfo.timezone)"). UTC / "" / "Etc/UTC" stays on
|
|
233
|
+
the ``timegm`` fast path — byte-identical to the historical numeric
|
|
234
|
+
emission for UTC symbols; anything else takes the mutex-guarded
|
|
235
|
+
setenv+``mktime`` block with ``tm_isdst = -1`` so DST is resolved by the
|
|
236
|
+
tz database. A per-call-site thread_local memo keeps the setenv churn to
|
|
237
|
+
one flip per distinct (tz, fields) tuple.
|
|
238
|
+
"""
|
|
239
|
+
return (
|
|
240
|
+
f"[&]() -> int64_t {{ "
|
|
241
|
+
f"std::string _tz = pineforge::normalize_timezone_for_posix(({tz})); "
|
|
242
|
+
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
243
|
+
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
244
|
+
f"static thread_local std::string _last_tz; "
|
|
245
|
+
f"static thread_local int _last_yr = -1, _last_mo = -1, _last_dy = -1, _last_hr = -1, _last_min = -1, _last_sc = -1; "
|
|
246
|
+
f"static thread_local int64_t _last_res = -1; "
|
|
247
|
+
f"if (_last_res != -1 && _last_tz == _tz && _last_yr == _yr && _last_mo == _mo && _last_dy == _dy && _last_hr == _hr && _last_min == _min && _last_sc == _sc) {{ "
|
|
248
|
+
f"return _last_res; "
|
|
249
|
+
f"}} "
|
|
250
|
+
f"struct tm t = {{}}; "
|
|
251
|
+
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
252
|
+
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
253
|
+
f"t.tm_isdst = -1; "
|
|
254
|
+
f"int64_t _res; "
|
|
255
|
+
f"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") {{ "
|
|
256
|
+
f"_res = (int64_t)timegm(&t) * 1000; "
|
|
257
|
+
f"}} else {{ "
|
|
258
|
+
f"static std::mutex _pf_ts_mu; "
|
|
259
|
+
f"std::lock_guard<std::mutex> _pf_ts_mu_lock(_pf_ts_mu); "
|
|
260
|
+
f"const char* _old = std::getenv(\"TZ\"); "
|
|
261
|
+
f"std::string _old_tz = _old ? _old : \"\"; bool _had_old = (_old != nullptr); "
|
|
262
|
+
f"::setenv(\"TZ\", _tz.c_str(), 1); ::tzset(); "
|
|
263
|
+
f"_res = (int64_t)mktime(&t) * 1000; "
|
|
264
|
+
f"if (_had_old) {{ ::setenv(\"TZ\", _old_tz.c_str(), 1); }} "
|
|
265
|
+
f"else {{ ::unsetenv(\"TZ\"); }} ::tzset(); "
|
|
266
|
+
f"}} "
|
|
267
|
+
f"_last_tz = _tz; _last_yr = _yr; _last_mo = _mo; _last_dy = _dy; _last_hr = _hr; _last_min = _min; _last_sc = _sc; "
|
|
268
|
+
f"_last_res = _res; "
|
|
269
|
+
f"return _res; "
|
|
270
|
+
f"}}()"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
225
274
|
class CallVisitor:
|
|
226
275
|
"""Function-call dispatch visitor methods shared across the codegen.
|
|
227
276
|
|
|
@@ -1371,13 +1420,21 @@ class CallVisitor:
|
|
|
1371
1420
|
# ta.* calls -> member.compute(...)
|
|
1372
1421
|
site = self._get_ta_site(node)
|
|
1373
1422
|
if site is not None:
|
|
1423
|
+
# A top-level lazy-edge site already evaluated for this bar in a
|
|
1424
|
+
# ``_pf_every_bar_ta_N`` local (see ``_lazy_edge_ta_hoist_plan``):
|
|
1425
|
+
# read it instead of stepping the indicator again.
|
|
1426
|
+
hoisted = self._hoisted_ta_values.get(id(node))
|
|
1427
|
+
if hoisted is not None:
|
|
1428
|
+
return hoisted
|
|
1374
1429
|
compute_args = self._ta_compute_args_for_site(site)
|
|
1375
1430
|
ta_mem = self._ta_member_name(site)
|
|
1376
1431
|
uses_precalc = self._ta_site_uses_precalc(site)
|
|
1377
1432
|
if getattr(self, "_precalc_loop_active", False) and uses_precalc:
|
|
1378
1433
|
return f"_precalc_{ta_mem}[i]"
|
|
1379
|
-
if
|
|
1380
|
-
|
|
1434
|
+
if id(node) in getattr(self, "_lazy_source_clock_by_node", {}):
|
|
1435
|
+
# change/mom/roc below a top-level lazy edge: TradingView reads
|
|
1436
|
+
# the call's own held ``source[length]`` (see ``ta.py``).
|
|
1437
|
+
return self._lazy_source_clock_expr(site, node)
|
|
1381
1438
|
if uses_precalc:
|
|
1382
1439
|
return (
|
|
1383
1440
|
f"(_use_precalc ? _precalc_{ta_mem}[bar_index_] : "
|
|
@@ -1498,6 +1555,12 @@ class CallVisitor:
|
|
|
1498
1555
|
if namespace == "array":
|
|
1499
1556
|
if func_name in ("new", "new_float", "new_int", "new_bool", "new_string") or func_name in ARRAY_DRAWING_NEW_CTORS:
|
|
1500
1557
|
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
1558
|
+
# The constructor of a wide int array (``_wide_int_array_names``)
|
|
1559
|
+
# must spell the declared ``std::vector<int64_t>``: the
|
|
1560
|
+
# declaration site names its target here.
|
|
1561
|
+
target = getattr(self, "_array_ctor_target_name", None)
|
|
1562
|
+
if target is not None:
|
|
1563
|
+
spec = self._widen_array_spec_for_name(target, spec)
|
|
1501
1564
|
cpp_type = self._type_spec_to_cpp(spec)
|
|
1502
1565
|
elem_spec = spec.element if spec.element is not None else TypeSpec.primitive("float")
|
|
1503
1566
|
init_default = self._default_for_spec(elem_spec)
|
|
@@ -1511,6 +1574,9 @@ class CallVisitor:
|
|
|
1511
1574
|
return f"{cpp_type}()"
|
|
1512
1575
|
if func_name == "from":
|
|
1513
1576
|
spec = self._type_spec_from_expr(node) or TypeSpec.array(TypeSpec.primitive("float"))
|
|
1577
|
+
target = getattr(self, "_array_ctor_target_name", None)
|
|
1578
|
+
if target is not None:
|
|
1579
|
+
spec = self._widen_array_spec_for_name(target, spec)
|
|
1514
1580
|
elems = ", ".join(self._visit_expr(a) for a in node.args)
|
|
1515
1581
|
return f"{self._type_spec_to_cpp(spec)}{{{elems}}}"
|
|
1516
1582
|
# Method calls: array.method(arr, args...)
|
|
@@ -1722,22 +1788,51 @@ class CallVisitor:
|
|
|
1722
1788
|
return f"pine_{func_name}((int64_t)({ts_arg}), {tz_arg})"
|
|
1723
1789
|
|
|
1724
1790
|
# time(timeframe) or time(timeframe, session[, tz])
|
|
1791
|
+
#
|
|
1792
|
+
# Timezone of the SESSION argument (Pine v6 reference): "Timezone of
|
|
1793
|
+
# the session argument. Can only be used when a session is
|
|
1794
|
+
# specified. Optional. The default is syminfo.timezone." So a tz-less
|
|
1795
|
+
# ``time(tf, "0930-1600")`` reads the window in the EXCHANGE timezone
|
|
1796
|
+
# (America/New_York for NASDAQ/OANDA symbols), not UTC. We keep the
|
|
1797
|
+
# tz slot empty (the engine's tf-open/close path is unchanged and an
|
|
1798
|
+
# explicit tz still wins) and hand the engine ``syminfo_.timezone`` as
|
|
1799
|
+
# the trailing session-default — the same field the tz-less
|
|
1800
|
+
# hour()/minute()/dayofweek() forms above resolve against, which the
|
|
1801
|
+
# harness populates from runtime_overrides.timezone. For the corpus'
|
|
1802
|
+
# crypto data it is "UTC", which keeps that lane byte-identical.
|
|
1803
|
+
#
|
|
1804
|
+
# Composition (cand/symbol-r5): the engine's 7-arg pine_time /
|
|
1805
|
+
# pine_time_close take (..., script_tf_, syminfo_.timezone,
|
|
1806
|
+
# syminfo_.session). syminfo_.timezone is the tz-less session default
|
|
1807
|
+
# above; the pair also lets a D/W/M `tf` WITHOUT a valid session
|
|
1808
|
+
# resolve to the SYMBOL's session-day bar open (17:00 ET on OANDA
|
|
1809
|
+
# forex, 09:30 ET RTH on equities, 00:00 UTC on 24x7) instead of a
|
|
1810
|
+
# UTC calendar floor. A valid session keeps the explicit-tz-else-UTC
|
|
1811
|
+
# floor. The pair is wrapped in PF_PINE_TIME_SESSION_DAY_ARGS (prelude,
|
|
1812
|
+
# emit_top.py): it expands to ``, syminfo_.timezone, syminfo_.session``
|
|
1813
|
+
# only when the engine header defines PF_PINE_TIME_HAS_SESSION_DAY and
|
|
1814
|
+
# to nothing against the base engine (legacy 5-arg call), so the same
|
|
1815
|
+
# generated.cpp builds in every lab-experiment cell.
|
|
1816
|
+
#
|
|
1725
1817
|
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
1726
1818
|
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time"), self._visit_expr)
|
|
1727
1819
|
tf_e = args[0] if len(args) > 0 else 'script_tf_'
|
|
1728
1820
|
sess = args[1] if len(args) > 1 else 'std::string("")'
|
|
1729
1821
|
tz_e = args[2] if len(args) > 2 else 'std::string("")'
|
|
1730
1822
|
return (
|
|
1731
|
-
f"pine_time(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_
|
|
1823
|
+
f"pine_time(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_"
|
|
1824
|
+
" PF_PINE_TIME_SESSION_DAY_ARGS(syminfo_.timezone, syminfo_.session))"
|
|
1732
1825
|
)
|
|
1733
|
-
# time_close(timeframe) or time_close(tf, session, tz)
|
|
1826
|
+
# time_close(timeframe) or time_close(tf, session, tz) — same
|
|
1827
|
+
# session-tz default as time() above.
|
|
1734
1828
|
if func_name == "time_close" and namespace is None and (node.args or node.kwargs):
|
|
1735
1829
|
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time_close"), self._visit_expr)
|
|
1736
1830
|
tf_e = args[0] if len(args) > 0 else 'script_tf_'
|
|
1737
1831
|
sess = args[1] if len(args) > 1 else 'std::string("")'
|
|
1738
1832
|
tz_e = args[2] if len(args) > 2 else 'std::string("")'
|
|
1739
1833
|
return (
|
|
1740
|
-
f"pine_time_close(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_
|
|
1834
|
+
f"pine_time_close(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_"
|
|
1835
|
+
" PF_PINE_TIME_SESSION_DAY_ARGS(syminfo_.timezone, syminfo_.session))"
|
|
1741
1836
|
)
|
|
1742
1837
|
|
|
1743
1838
|
# timestamp(year, month, day, hour, minute) → Unix ms
|
|
@@ -1798,39 +1893,7 @@ class CallVisitor:
|
|
|
1798
1893
|
hr = args[4] if len(args) > 4 else "0"
|
|
1799
1894
|
mn = args[5] if len(args) > 5 else "0"
|
|
1800
1895
|
sc = args[6] if len(args) > 6 else "0"
|
|
1801
|
-
return (
|
|
1802
|
-
f"[&]() -> int64_t {{ "
|
|
1803
|
-
f"std::string _tz = pineforge::normalize_timezone_for_posix(({tz})); "
|
|
1804
|
-
f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
|
|
1805
|
-
f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
|
|
1806
|
-
f"static thread_local std::string _last_tz; "
|
|
1807
|
-
f"static thread_local int _last_yr = -1, _last_mo = -1, _last_dy = -1, _last_hr = -1, _last_min = -1, _last_sc = -1; "
|
|
1808
|
-
f"static thread_local int64_t _last_res = -1; "
|
|
1809
|
-
f"if (_last_res != -1 && _last_tz == _tz && _last_yr == _yr && _last_mo == _mo && _last_dy == _dy && _last_hr == _hr && _last_min == _min && _last_sc == _sc) {{ "
|
|
1810
|
-
f"return _last_res; "
|
|
1811
|
-
f"}} "
|
|
1812
|
-
f"struct tm t = {{}}; "
|
|
1813
|
-
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
1814
|
-
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
1815
|
-
f"t.tm_isdst = -1; "
|
|
1816
|
-
f"int64_t _res; "
|
|
1817
|
-
f"if (_tz.empty() || _tz == \"UTC\" || _tz == \"Etc/UTC\") {{ "
|
|
1818
|
-
f"_res = (int64_t)timegm(&t) * 1000; "
|
|
1819
|
-
f"}} else {{ "
|
|
1820
|
-
f"static std::mutex _pf_ts_mu; "
|
|
1821
|
-
f"std::lock_guard<std::mutex> _pf_ts_mu_lock(_pf_ts_mu); "
|
|
1822
|
-
f"const char* _old = std::getenv(\"TZ\"); "
|
|
1823
|
-
f"std::string _old_tz = _old ? _old : \"\"; bool _had_old = (_old != nullptr); "
|
|
1824
|
-
f"::setenv(\"TZ\", _tz.c_str(), 1); ::tzset(); "
|
|
1825
|
-
f"_res = (int64_t)mktime(&t) * 1000; "
|
|
1826
|
-
f"if (_had_old) {{ ::setenv(\"TZ\", _old_tz.c_str(), 1); }} "
|
|
1827
|
-
f"else {{ ::unsetenv(\"TZ\"); }} ::tzset(); "
|
|
1828
|
-
f"}} "
|
|
1829
|
-
f"_last_tz = _tz; _last_yr = _yr; _last_mo = _mo; _last_dy = _dy; _last_hr = _hr; _last_min = _min; _last_sc = _sc; "
|
|
1830
|
-
f"_last_res = _res; "
|
|
1831
|
-
f"return _res; "
|
|
1832
|
-
f"}}()"
|
|
1833
|
-
)
|
|
1896
|
+
return _timestamp_calendar_lambda(tz, yr, mo, dy, hr, mn, sc)
|
|
1834
1897
|
else:
|
|
1835
1898
|
# Numeric form requires year, month, and day (hour/minute/
|
|
1836
1899
|
# second default to 0). Anything shorter used to emit "0".
|
|
@@ -1855,24 +1918,16 @@ class CallVisitor:
|
|
|
1855
1918
|
hr = args[3] if len(args) > 3 else "0"
|
|
1856
1919
|
mn = args[4] if len(args) > 4 else "0"
|
|
1857
1920
|
sc = args[5] if len(args) > 5 else "0"
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
f"t.tm_year = _yr - 1900; t.tm_mon = _mo - 1; "
|
|
1869
|
-
f"t.tm_mday = _dy; t.tm_hour = _hr; t.tm_min = _min; t.tm_sec = _sc; "
|
|
1870
|
-
f"int64_t _res = (int64_t)timegm(&t) * 1000; "
|
|
1871
|
-
f"_last_yr = _yr; _last_mo = _mo; _last_dy = _dy; _last_hr = _hr; _last_min = _min; _last_sc = _sc; "
|
|
1872
|
-
f"_last_res = _res; "
|
|
1873
|
-
f"return _res; "
|
|
1874
|
-
f"}}()"
|
|
1875
|
-
)
|
|
1921
|
+
# No timezone argument: Pine resolves the calendar fields in
|
|
1922
|
+
# syminfo.timezone (the EXCHANGE tz), not UTC. This used to be
|
|
1923
|
+
# a bare timegm(), which put e.g. timestamp(y, m, d, 8, 0, 0)
|
|
1924
|
+
# on a NASDAQ/OANDA symbol 4-5 h early. Same field the tz-less
|
|
1925
|
+
# hour()/minute() forms and the tz-first overload above use;
|
|
1926
|
+
# "UTC" keeps the timegm fast path so crypto stays identical.
|
|
1927
|
+
# (timestamp(dateString) is different: Pine reads a tz-less
|
|
1928
|
+
# dateString as GMT+0, which _parse_pine_datestring_ms does.)
|
|
1929
|
+
return _timestamp_calendar_lambda(
|
|
1930
|
+
"syminfo_.timezone", yr, mo, dy, hr, mn, sc)
|
|
1876
1931
|
|
|
1877
1932
|
# barssince() — unsupported. Defensive: support_checker rejects bare
|
|
1878
1933
|
# barssince(...) with a hint to use ta.barssince(...). Reaching here
|
|
@@ -2017,7 +2072,15 @@ class CallVisitor:
|
|
|
2017
2072
|
if namespace == "timeframe":
|
|
2018
2073
|
if func_name == "change":
|
|
2019
2074
|
tf_arg = self._visit_expr(node.args[0]) if node.args else 'script_tf_'
|
|
2020
|
-
|
|
2075
|
+
# Symbol-clock overload: D/W/M boundaries fall on the SYMBOL's
|
|
2076
|
+
# session day (17:00 ET on OANDA forex, 09:30 ET RTH on
|
|
2077
|
+
# equities), intraday buckets on the session-open grid — the
|
|
2078
|
+
# same clock request.security aggregation uses. UTC/24x7
|
|
2079
|
+
# syminfo reduces to the tz-less epoch math bit-for-bit.
|
|
2080
|
+
return (
|
|
2081
|
+
f'tf_change(prev_bar_timestamp_, current_bar_.timestamp, {tf_arg}, '
|
|
2082
|
+
'syminfo_.timezone, syminfo_.session)'
|
|
2083
|
+
)
|
|
2021
2084
|
if func_name == "in_seconds":
|
|
2022
2085
|
tf_arg = self._visit_expr(node.args[0]) if node.args else 'script_tf_'
|
|
2023
2086
|
return f'tf_to_seconds({tf_arg})'
|
|
@@ -673,22 +673,40 @@ class ExprVisitor:
|
|
|
673
673
|
# to dead ``f5``'s first-in-order ``_ta_vwap_10`` and emitted
|
|
674
674
|
# ``use of undeclared identifier '_ta_vwap_10'``. A live read
|
|
675
675
|
# must bind to a live site.
|
|
676
|
-
|
|
677
|
-
|
|
676
|
+
# The read's OWN site first (the analyzer keys the
|
|
677
|
+
# synthetic call-site on this MemberAccess node), so two
|
|
678
|
+
# bare reads are two sites, each advanced once per bar;
|
|
679
|
+
# the first-live-site scan below stays as the fallback.
|
|
680
|
+
_own = self._get_ta_site(node)
|
|
681
|
+
_candidates = (
|
|
682
|
+
[(self._ta_index_by_site_id.get(id(_own)), _own)]
|
|
683
|
+
if _own is not None else []
|
|
684
|
+
) + list(enumerate(self.ctx.ta_call_sites))
|
|
685
|
+
for _i, site in _candidates:
|
|
686
|
+
if _i is None or _i in self._dead_ta_indices:
|
|
678
687
|
continue
|
|
679
688
|
ta_short = site.class_name.split("::")[-1].lower()
|
|
680
689
|
if site.member_name.startswith(f"_ta_{node.member}_"):
|
|
681
690
|
if node.member == "vwap":
|
|
691
|
+
# Same implicit tail as TA_IMPLICIT_APPEND["vwap"]:
|
|
692
|
+
# the symbol clock keys the Daily anchor reset.
|
|
693
|
+
# The bare property is the VWAP of hlc3 (Pine
|
|
694
|
+
# v6 reference; lab tv notrade-session-vwap-f1d,
|
|
695
|
+
# 2026-09-05: TradingView's read equals hlc3 on
|
|
696
|
+
# a daily bar), never of the close.
|
|
697
|
+
_hlc3 = "((current_bar_.high + current_bar_.low + current_bar_.close) / 3.0)"
|
|
682
698
|
return (
|
|
683
|
-
f"(history_advances_new_bar() ? {site
|
|
684
|
-
"
|
|
685
|
-
|
|
686
|
-
"
|
|
699
|
+
f"(history_advances_new_bar() ? {self._ta_member_name(site)}.compute("
|
|
700
|
+
f"{_hlc3}, current_bar_.volume, current_bar_.timestamp"
|
|
701
|
+
" PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)) "
|
|
702
|
+
f": {self._ta_member_name(site)}.recompute({_hlc3}, "
|
|
703
|
+
"current_bar_.volume, current_bar_.timestamp"
|
|
704
|
+
" PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)))"
|
|
687
705
|
)
|
|
688
706
|
return (
|
|
689
|
-
f"(history_advances_new_bar() ? {site
|
|
707
|
+
f"(history_advances_new_bar() ? {self._ta_member_name(site)}.compute("
|
|
690
708
|
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}) : "
|
|
691
|
-
f"{site
|
|
709
|
+
f"{self._ta_member_name(site)}.recompute("
|
|
692
710
|
f"{TA_IMPLICIT_COMPUTE_FULL[node.member]}))"
|
|
693
711
|
)
|
|
694
712
|
# No registered call site for this TA property read —
|
|
@@ -1033,6 +1051,13 @@ class ExprVisitor:
|
|
|
1033
1051
|
return f"({left_cpp} {op} {right_cpp})"
|
|
1034
1052
|
|
|
1035
1053
|
def _visit_binop(self, node: BinOp) -> str:
|
|
1054
|
+
# An int-literal-only ``+ - *`` tree whose exact value leaves int32 is
|
|
1055
|
+
# a 64-bit Pine int (``90 * 24 * 60 * 60 * 1000`` = 7 776 000 000);
|
|
1056
|
+
# C++ ``int`` literal arithmetic would wrap it. Fold it here and spell
|
|
1057
|
+
# the value as a 64-bit literal. In-range trees are emitted as before.
|
|
1058
|
+
folded = self._pure_int_literal_value(node)
|
|
1059
|
+
if folded is not None and not self._int_fits_int32(folded):
|
|
1060
|
+
return f"static_cast<int64_t>({folded}LL)"
|
|
1036
1061
|
left = self._visit_expr(node.left)
|
|
1037
1062
|
right = self._visit_expr(node.right)
|
|
1038
1063
|
cpp_ops = {"and": "&&", "or": "||"}
|
|
@@ -1114,6 +1139,12 @@ class ExprVisitor:
|
|
|
1114
1139
|
# sites never share history, and on_bar clears every synthetic member at
|
|
1115
1140
|
# run-start even when this particular expression is conditional.
|
|
1116
1141
|
if isinstance(node.object, FuncCall):
|
|
1142
|
+
# ``[k]`` on a hoisted top-level lazy-edge TA call: its Series was
|
|
1143
|
+
# pushed unconditionally before the statement, so ``[1]`` is the
|
|
1144
|
+
# previous chart bar in every run mode (``_lazy_edge_ta_hoist_plan``).
|
|
1145
|
+
hoisted_member = self._hoisted_hist_reads.get(id(node))
|
|
1146
|
+
if hoisted_member is not None:
|
|
1147
|
+
return f"{hoisted_member}[(int)({idx})]"
|
|
1117
1148
|
inner = self._visit_expr(node.object)
|
|
1118
1149
|
cpp_t = self._infer_type(node.object)
|
|
1119
1150
|
if cpp_t not in ("double", "int", "int64_t", "bool"):
|