@pineforge/codegen-pyodide 0.10.1 → 0.10.3
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/codegen/base.py +207 -19
- package/pineforge_codegen/codegen/emit_top.py +73 -20
- package/pineforge_codegen/codegen/security.py +160 -0
- package/pineforge_codegen/codegen/ta.py +569 -287
- package/pineforge_codegen/codegen/tables.py +140 -34
- package/pineforge_codegen/codegen/types.py +6 -1
- package/pineforge_codegen/codegen/visit_call.py +144 -72
- package/pineforge_codegen/codegen/visit_expr.py +12 -2
- package/pineforge_codegen/support_checker.py +68 -4
- package/pineforge_codegen-0.10.3.tar.gz +0 -0
- package/release.json +2 -2
- package/tables.json +1 -1
- package/pineforge_codegen-0.10.1.tar.gz +0 -0
|
@@ -248,12 +248,17 @@ TA_IMPLICIT_APPEND = {
|
|
|
248
248
|
"kc": "current_bar_.high, current_bar_.low, current_bar_.close",
|
|
249
249
|
"mfi": "current_bar_.volume",
|
|
250
250
|
"kcw": "current_bar_.high, current_bar_.low, current_bar_.close",
|
|
251
|
-
# ta.vwap needs the bar timestamp
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
|
|
251
|
+
# ta.vwap needs the bar timestamp plus the symbol clock (syminfo.timezone
|
|
252
|
+
# + syminfo.session) so the runtime can detect Daily anchor boundaries:
|
|
253
|
+
# Pine v6's default `anchor = timeframe.change("1D")` is the SYMBOL's
|
|
254
|
+
# daily bar (17:00 ET on OANDA forex, 09:30 ET RTH on NASDAQ equities,
|
|
255
|
+
# 00:00 UTC on a 24x7 UTC symbol). The engine's tz/session overload
|
|
256
|
+
# reduces to the UTC-day integer math for the default UTC/24x7 syminfo.
|
|
257
|
+
# The tail is wrapped in PF_VWAP_SESSION_ANCHOR_ARGS (prelude, emit_top.py)
|
|
258
|
+
# so it compiles out against engines without the overload.
|
|
259
|
+
"vwap": "current_bar_.volume, current_bar_.timestamp PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)",
|
|
255
260
|
# 3-arg bands form uses the same implicit append as the scalar form.
|
|
256
|
-
"vwap_bands": "current_bar_.volume, current_bar_.timestamp",
|
|
261
|
+
"vwap_bands": "current_bar_.volume, current_bar_.timestamp PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)",
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
# Tuple field names for TA functions returning tuples.
|
|
@@ -459,45 +464,143 @@ ORDER_DIRECTION_MAP = {
|
|
|
459
464
|
# ---------------------------------------------------------------------------
|
|
460
465
|
|
|
461
466
|
|
|
462
|
-
def _checked_array_index_prelude(
|
|
467
|
+
def _checked_array_index_prelude(
|
|
468
|
+
*,
|
|
469
|
+
normalize_negative: bool = True,
|
|
470
|
+
allow_size: bool = False,
|
|
471
|
+
name: str = "index",
|
|
472
|
+
) -> str:
|
|
463
473
|
"""C++ body shared by checked single-index operations.
|
|
464
474
|
|
|
465
|
-
Pine v6 explicitly gives end-relative negative indices to
|
|
466
|
-
``
|
|
467
|
-
``percentrank``
|
|
468
|
-
|
|
469
|
-
|
|
475
|
+
Pine v6 explicitly gives end-relative negative indices to ``array.get``,
|
|
476
|
+
``array.set``, ``array.insert``, and ``array.remove``. Other indexed
|
|
477
|
+
functions such as ``percentrank`` and the ``fill``/``slice`` range
|
|
478
|
+
endpoints still need the same NA/non-finite/range checks, but must reject
|
|
479
|
+
negative indices instead of normalizing them (``normalize_negative``).
|
|
480
|
+
|
|
481
|
+
``allow_size`` widens the upper bound from ``index < size`` to
|
|
482
|
+
``index <= size``. An *element access* is invalid at ``size``, but an
|
|
483
|
+
*insertion point* is not: ``array.insert(id, size, v)`` appends, and the
|
|
484
|
+
half-open ``[index_from, index_to)`` ranges of ``array.fill`` /
|
|
485
|
+
``array.slice`` legally end at ``size``.
|
|
486
|
+
|
|
487
|
+
``name`` renames every emitted local so two endpoints can be checked in
|
|
488
|
+
one C++ scope without colliding. ``name="index"`` reproduces the original
|
|
489
|
+
identifiers byte-for-byte.
|
|
470
490
|
"""
|
|
491
|
+
raw_value = f"__pf_raw_{name}_value"
|
|
492
|
+
raw_type = f"__pf_raw_{name}_type"
|
|
493
|
+
raw_text = f"__pf_raw_{name}_text"
|
|
494
|
+
raw_wide = f"__pf_raw_{name}_wide"
|
|
495
|
+
raw = f"__pf_raw_{name}"
|
|
496
|
+
checked = f"__pf_array_{name}"
|
|
497
|
+
size = "__pf_array_size" if name == "index" else f"__pf_array_size_{name}"
|
|
471
498
|
checked_index = (
|
|
472
|
-
"
|
|
473
|
-
if normalize_negative
|
|
474
|
-
else "__pf_raw_index"
|
|
499
|
+
f"{raw}<0?{raw}+{size}:{raw}" if normalize_negative else raw
|
|
475
500
|
)
|
|
501
|
+
upper = ">" if allow_size else ">="
|
|
476
502
|
return (
|
|
477
|
-
"using
|
|
478
|
-
"if constexpr(!std::is_same_v<
|
|
479
|
-
"if(is_na(
|
|
503
|
+
f"using {raw_type}=std::decay_t<decltype({raw_value})>; "
|
|
504
|
+
f"if constexpr(!std::is_same_v<{raw_type},bool>) {{ "
|
|
505
|
+
f"if(is_na({raw_value})) "
|
|
480
506
|
"pine_runtime_error(std::string(\"Index na is out of bounds. Array size is \")+"
|
|
481
507
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
482
|
-
"if constexpr(std::is_floating_point_v<
|
|
483
|
-
"if(!std::isfinite(
|
|
484
|
-
"std::string
|
|
485
|
-
"pine_runtime_error(std::string(\"Index \")+
|
|
508
|
+
f"if constexpr(std::is_floating_point_v<{raw_type}>) {{ "
|
|
509
|
+
f"if(!std::isfinite({raw_value})) {{ "
|
|
510
|
+
f"std::string {raw_text}={raw_value}>0?\"inf\":\"-inf\"; "
|
|
511
|
+
f"pine_runtime_error(std::string(\"Index \")+{raw_text}+"
|
|
486
512
|
"\" is out of bounds. Array size is \"+"
|
|
487
513
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
488
|
-
"long double
|
|
489
|
-
"if(
|
|
490
|
-
"
|
|
514
|
+
f"long double {raw_wide}=(long double){raw_value}; "
|
|
515
|
+
f"if({raw_wide}<(long double)std::numeric_limits<int64_t>::min()||"
|
|
516
|
+
f"{raw_wide}>(long double)std::numeric_limits<int64_t>::max()) "
|
|
491
517
|
"pine_runtime_error(std::string(\"Index \")+"
|
|
492
|
-
"std::to_string((double)
|
|
518
|
+
f"std::to_string((double){raw_value})+"
|
|
493
519
|
"\" is out of bounds. Array size is \"+"
|
|
494
520
|
"std::to_string((int64_t)__pf_array.size())); } "
|
|
495
|
-
"int64_t
|
|
496
|
-
"int64_t
|
|
497
|
-
f"int64_t
|
|
498
|
-
"if(
|
|
499
|
-
"pine_runtime_error(std::string(\"Index \")+std::to_string(
|
|
500
|
-
"\" is out of bounds. Array size is \"+std::to_string(
|
|
521
|
+
f"int64_t {raw}=(int64_t){raw_value}; "
|
|
522
|
+
f"int64_t {size}=(int64_t)__pf_array.size(); "
|
|
523
|
+
f"int64_t {checked}={checked_index}; "
|
|
524
|
+
f"if({checked}<0||{checked}{upper}{size}) "
|
|
525
|
+
f"pine_runtime_error(std::string(\"Index \")+std::to_string({raw})+"
|
|
526
|
+
f"\" is out of bounds. Array size is \"+std::to_string({size})); "
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _checked_array_range_prelude() -> str:
|
|
531
|
+
"""Validate the half-open ``[index_from, index_to)`` range of fill/slice.
|
|
532
|
+
|
|
533
|
+
Both endpoints are checked with ``allow_size`` (``index_to`` is exclusive,
|
|
534
|
+
so ``size`` is a legal endpoint) and without negative normalization
|
|
535
|
+
(neither function appears in the Pine v6 reference's negative-indexing
|
|
536
|
+
set). An inverted range is rejected rather than silently treated as empty:
|
|
537
|
+
``begin()+from > begin()+to`` is undefined behaviour in the STL forms this
|
|
538
|
+
replaces, and no evidence pins TradingView's behaviour there.
|
|
539
|
+
"""
|
|
540
|
+
return (
|
|
541
|
+
_checked_array_index_prelude(
|
|
542
|
+
normalize_negative=False, allow_size=True, name="index_from"
|
|
543
|
+
)
|
|
544
|
+
+ _checked_array_index_prelude(
|
|
545
|
+
normalize_negative=False, allow_size=True, name="index_to"
|
|
546
|
+
)
|
|
547
|
+
+ "if(__pf_array_index_from>__pf_array_index_to) "
|
|
548
|
+
"pine_runtime_error(std::string(\"Index range \")+"
|
|
549
|
+
"std::to_string(__pf_array_index_from)+\"..\"+"
|
|
550
|
+
"std::to_string(__pf_array_index_to)+"
|
|
551
|
+
"\" is invalid. Array size is \"+"
|
|
552
|
+
"std::to_string(__pf_array_size_index_from)); "
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _checked_array_insert(a: str, args: list[str]) -> str:
|
|
557
|
+
"""``array.insert(id, index, value)`` — index is an INSERTION point.
|
|
558
|
+
|
|
559
|
+
``index == size`` appends, so the bound is ``index <= size``; a negative
|
|
560
|
+
index is end-relative, exactly as for ``get``/``set``/``remove``.
|
|
561
|
+
"""
|
|
562
|
+
check = _checked_array_index_prelude(allow_size=True)
|
|
563
|
+
return (
|
|
564
|
+
"[&](auto&& __pf_array){ "
|
|
565
|
+
"return [&](auto&& __pf_raw_index_value){ "
|
|
566
|
+
"return [&](auto&& __pf_array_value){ "
|
|
567
|
+
f"{check}"
|
|
568
|
+
"__pf_array.insert(__pf_array.begin()+(size_t)__pf_array_index, "
|
|
569
|
+
"__pf_array_value); "
|
|
570
|
+
f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _checked_array_fill_range(a: str, args: list[str]) -> str:
|
|
575
|
+
"""``array.fill(id, value, index_from, index_to)`` — bounded range fill."""
|
|
576
|
+
check = _checked_array_range_prelude()
|
|
577
|
+
return (
|
|
578
|
+
"[&](auto&& __pf_array){ "
|
|
579
|
+
"return [&](auto&& __pf_array_value){ "
|
|
580
|
+
"return [&](auto&& __pf_raw_index_from_value){ "
|
|
581
|
+
"return [&](auto&& __pf_raw_index_to_value){ "
|
|
582
|
+
f"{check}"
|
|
583
|
+
"std::fill(__pf_array.begin()+(size_t)__pf_array_index_from, "
|
|
584
|
+
"__pf_array.begin()+(size_t)__pf_array_index_to, __pf_array_value); "
|
|
585
|
+
f"}}(({args[2]})); }}(({args[1]})); }}(({args[0]})); }}(({a}))"
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def checked_array_slice(a: str, args: list[str], *, result_type: str) -> str:
|
|
590
|
+
"""``array.slice(id, index_from, index_to)`` — bounded range extraction.
|
|
591
|
+
|
|
592
|
+
``result_type`` stays caller-supplied so the typed method lane keeps
|
|
593
|
+
emitting the receiver's own element type; only the bounds checks are new.
|
|
594
|
+
"""
|
|
595
|
+
check = _checked_array_range_prelude()
|
|
596
|
+
return (
|
|
597
|
+
"[&](auto&& __pf_array){ "
|
|
598
|
+
"return [&](auto&& __pf_raw_index_from_value){ "
|
|
599
|
+
"return [&](auto&& __pf_raw_index_to_value){ "
|
|
600
|
+
f"{check}"
|
|
601
|
+
f"return {result_type}(__pf_array.begin()+(size_t)__pf_array_index_from, "
|
|
602
|
+
"__pf_array.begin()+(size_t)__pf_array_index_to); "
|
|
603
|
+
f"}}(({args[1]})); }}(({args[0]})); }}(({a}))"
|
|
501
604
|
)
|
|
502
605
|
|
|
503
606
|
|
|
@@ -600,7 +703,7 @@ ARRAY_METHODS = {
|
|
|
600
703
|
"set": _checked_array_set,
|
|
601
704
|
"push": lambda a, args: f"{a}.push_back({args[0]})",
|
|
602
705
|
"unshift": lambda a, args: f"{a}.insert({a}.begin(), {args[0]})",
|
|
603
|
-
"insert":
|
|
706
|
+
"insert": _checked_array_insert,
|
|
604
707
|
"pop": lambda a, args: _checked_array_end_remove(a, "pop"),
|
|
605
708
|
"shift": lambda a, args: _checked_array_end_remove(a, "shift"),
|
|
606
709
|
"remove": _checked_array_remove,
|
|
@@ -609,7 +712,7 @@ ARRAY_METHODS = {
|
|
|
609
712
|
"size": lambda a, args: f"(double){a}.size()",
|
|
610
713
|
"clear": lambda a, args: f"{a}.clear()",
|
|
611
714
|
"fill": lambda a, args: f"std::fill({a}.begin(), {a}.end(), {args[0]})" if len(args) == 1
|
|
612
|
-
else
|
|
715
|
+
else _checked_array_fill_range(a, args),
|
|
613
716
|
"includes": lambda a, args: f"(std::find({a}.begin(), {a}.end(), {args[0]}) != {a}.end())",
|
|
614
717
|
"indexof": lambda a, args: f"[&](){{ auto it=std::find({a}.begin(),{a}.end(),{args[0]}); return it!={a}.end()?(double)(it-{a}.begin()):-1.0; }}()",
|
|
615
718
|
"lastindexof": lambda a, args: f"[&](){{ for(int i=(int){a}.size()-1;i>=0;i--)if({a}[i]=={args[0]})return(double)i; return -1.0; }}()",
|
|
@@ -619,7 +722,7 @@ ARRAY_METHODS = {
|
|
|
619
722
|
),
|
|
620
723
|
"reverse": lambda a, args: f"std::reverse({a}.begin(),{a}.end())",
|
|
621
724
|
"copy": lambda a, args: f"std::vector<double>({a})",
|
|
622
|
-
"slice": lambda a, args:
|
|
725
|
+
"slice": lambda a, args: checked_array_slice(a, args, result_type="std::vector<double>"),
|
|
623
726
|
"concat": lambda a, args: f"{a}.insert({a}.end(),{args[0]}.begin(),{args[0]}.end())",
|
|
624
727
|
"sum": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0))",
|
|
625
728
|
"avg": lambda a, args: f"({a}.empty()?na<double>():std::accumulate({a}.begin(),{a}.end(),0.0)/{a}.size())",
|
|
@@ -667,6 +770,9 @@ CHECKED_ARRAY_METHOD_KWARGS: dict[str, list[str]] = {
|
|
|
667
770
|
"set": ["index", "value"],
|
|
668
771
|
"remove": ["index"],
|
|
669
772
|
"percentrank": ["index"],
|
|
773
|
+
"insert": ["index", "value"],
|
|
774
|
+
"fill": ["value", "index_from", "index_to"],
|
|
775
|
+
"slice": ["index_from", "index_to"],
|
|
670
776
|
"first": [],
|
|
671
777
|
"last": [],
|
|
672
778
|
"pop": [],
|
|
@@ -50,6 +50,7 @@ from .tables import (
|
|
|
50
50
|
MATRIX_RETURNING_METHODS,
|
|
51
51
|
PINE_TYPE_TO_CPP,
|
|
52
52
|
TA_RETURNS_BOOL,
|
|
53
|
+
checked_array_slice,
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
# Collection (array / map / matrix) methods that MUTATE the receiver in place.
|
|
@@ -960,7 +961,11 @@ class TypeInferer:
|
|
|
960
961
|
if method == "copy":
|
|
961
962
|
lower_receiver = lambda recv: f"{arr_cpp_type}({recv})"
|
|
962
963
|
elif method == "slice":
|
|
963
|
-
|
|
964
|
+
# Bounds-checked in the shared helper; the element type stays
|
|
965
|
+
# caller-supplied so the typed lane keeps its own vector type.
|
|
966
|
+
lower_receiver = lambda recv: checked_array_slice(
|
|
967
|
+
recv, args, result_type=arr_cpp_type
|
|
968
|
+
)
|
|
964
969
|
elif method == "join" and elem_cpp == "std::string":
|
|
965
970
|
sep = args[0] if args else 'std::string(",")'
|
|
966
971
|
lower_receiver = lambda recv: f"[&](){{ std::string r; for(size_t i=0;i<{recv}.size();i++){{ if(i>0)r+={sep}; r+={recv}[i]; }} return r; }}()"
|
|
@@ -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_] : "
|
|
@@ -1722,22 +1779,51 @@ class CallVisitor:
|
|
|
1722
1779
|
return f"pine_{func_name}((int64_t)({ts_arg}), {tz_arg})"
|
|
1723
1780
|
|
|
1724
1781
|
# time(timeframe) or time(timeframe, session[, tz])
|
|
1782
|
+
#
|
|
1783
|
+
# Timezone of the SESSION argument (Pine v6 reference): "Timezone of
|
|
1784
|
+
# the session argument. Can only be used when a session is
|
|
1785
|
+
# specified. Optional. The default is syminfo.timezone." So a tz-less
|
|
1786
|
+
# ``time(tf, "0930-1600")`` reads the window in the EXCHANGE timezone
|
|
1787
|
+
# (America/New_York for NASDAQ/OANDA symbols), not UTC. We keep the
|
|
1788
|
+
# tz slot empty (the engine's tf-open/close path is unchanged and an
|
|
1789
|
+
# explicit tz still wins) and hand the engine ``syminfo_.timezone`` as
|
|
1790
|
+
# the trailing session-default — the same field the tz-less
|
|
1791
|
+
# hour()/minute()/dayofweek() forms above resolve against, which the
|
|
1792
|
+
# harness populates from runtime_overrides.timezone. For the corpus'
|
|
1793
|
+
# crypto data it is "UTC", which keeps that lane byte-identical.
|
|
1794
|
+
#
|
|
1795
|
+
# Composition (cand/symbol-r5): the engine's 7-arg pine_time /
|
|
1796
|
+
# pine_time_close take (..., script_tf_, syminfo_.timezone,
|
|
1797
|
+
# syminfo_.session). syminfo_.timezone is the tz-less session default
|
|
1798
|
+
# above; the pair also lets a D/W/M `tf` WITHOUT a valid session
|
|
1799
|
+
# resolve to the SYMBOL's session-day bar open (17:00 ET on OANDA
|
|
1800
|
+
# forex, 09:30 ET RTH on equities, 00:00 UTC on 24x7) instead of a
|
|
1801
|
+
# UTC calendar floor. A valid session keeps the explicit-tz-else-UTC
|
|
1802
|
+
# floor. The pair is wrapped in PF_PINE_TIME_SESSION_DAY_ARGS (prelude,
|
|
1803
|
+
# emit_top.py): it expands to ``, syminfo_.timezone, syminfo_.session``
|
|
1804
|
+
# only when the engine header defines PF_PINE_TIME_HAS_SESSION_DAY and
|
|
1805
|
+
# to nothing against the base engine (legacy 5-arg call), so the same
|
|
1806
|
+
# generated.cpp builds in every lab-experiment cell.
|
|
1807
|
+
#
|
|
1725
1808
|
if func_name == "time" and namespace is None and (node.args or node.kwargs):
|
|
1726
1809
|
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time"), self._visit_expr)
|
|
1727
1810
|
tf_e = args[0] if len(args) > 0 else 'script_tf_'
|
|
1728
1811
|
sess = args[1] if len(args) > 1 else 'std::string("")'
|
|
1729
1812
|
tz_e = args[2] if len(args) > 2 else 'std::string("")'
|
|
1730
1813
|
return (
|
|
1731
|
-
f"pine_time(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_
|
|
1814
|
+
f"pine_time(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_"
|
|
1815
|
+
" PF_PINE_TIME_SESSION_DAY_ARGS(syminfo_.timezone, syminfo_.session))"
|
|
1732
1816
|
)
|
|
1733
|
-
# time_close(timeframe) or time_close(tf, session, tz)
|
|
1817
|
+
# time_close(timeframe) or time_close(tf, session, tz) — same
|
|
1818
|
+
# session-tz default as time() above.
|
|
1734
1819
|
if func_name == "time_close" and namespace is None and (node.args or node.kwargs):
|
|
1735
1820
|
args = _merge_kwargs(node.args, node.kwargs, sigs.get_param_names(None, "time_close"), self._visit_expr)
|
|
1736
1821
|
tf_e = args[0] if len(args) > 0 else 'script_tf_'
|
|
1737
1822
|
sess = args[1] if len(args) > 1 else 'std::string("")'
|
|
1738
1823
|
tz_e = args[2] if len(args) > 2 else 'std::string("")'
|
|
1739
1824
|
return (
|
|
1740
|
-
f"pine_time_close(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_
|
|
1825
|
+
f"pine_time_close(current_bar_.timestamp, {tf_e}, {sess}, {tz_e}, script_tf_"
|
|
1826
|
+
" PF_PINE_TIME_SESSION_DAY_ARGS(syminfo_.timezone, syminfo_.session))"
|
|
1741
1827
|
)
|
|
1742
1828
|
|
|
1743
1829
|
# timestamp(year, month, day, hour, minute) → Unix ms
|
|
@@ -1798,39 +1884,7 @@ class CallVisitor:
|
|
|
1798
1884
|
hr = args[4] if len(args) > 4 else "0"
|
|
1799
1885
|
mn = args[5] if len(args) > 5 else "0"
|
|
1800
1886
|
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
|
-
)
|
|
1887
|
+
return _timestamp_calendar_lambda(tz, yr, mo, dy, hr, mn, sc)
|
|
1834
1888
|
else:
|
|
1835
1889
|
# Numeric form requires year, month, and day (hour/minute/
|
|
1836
1890
|
# second default to 0). Anything shorter used to emit "0".
|
|
@@ -1855,24 +1909,16 @@ class CallVisitor:
|
|
|
1855
1909
|
hr = args[3] if len(args) > 3 else "0"
|
|
1856
1910
|
mn = args[4] if len(args) > 4 else "0"
|
|
1857
1911
|
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
|
-
)
|
|
1912
|
+
# No timezone argument: Pine resolves the calendar fields in
|
|
1913
|
+
# syminfo.timezone (the EXCHANGE tz), not UTC. This used to be
|
|
1914
|
+
# a bare timegm(), which put e.g. timestamp(y, m, d, 8, 0, 0)
|
|
1915
|
+
# on a NASDAQ/OANDA symbol 4-5 h early. Same field the tz-less
|
|
1916
|
+
# hour()/minute() forms and the tz-first overload above use;
|
|
1917
|
+
# "UTC" keeps the timegm fast path so crypto stays identical.
|
|
1918
|
+
# (timestamp(dateString) is different: Pine reads a tz-less
|
|
1919
|
+
# dateString as GMT+0, which _parse_pine_datestring_ms does.)
|
|
1920
|
+
return _timestamp_calendar_lambda(
|
|
1921
|
+
"syminfo_.timezone", yr, mo, dy, hr, mn, sc)
|
|
1876
1922
|
|
|
1877
1923
|
# barssince() — unsupported. Defensive: support_checker rejects bare
|
|
1878
1924
|
# barssince(...) with a hint to use ta.barssince(...). Reaching here
|
|
@@ -2017,7 +2063,15 @@ class CallVisitor:
|
|
|
2017
2063
|
if namespace == "timeframe":
|
|
2018
2064
|
if func_name == "change":
|
|
2019
2065
|
tf_arg = self._visit_expr(node.args[0]) if node.args else 'script_tf_'
|
|
2020
|
-
|
|
2066
|
+
# Symbol-clock overload: D/W/M boundaries fall on the SYMBOL's
|
|
2067
|
+
# session day (17:00 ET on OANDA forex, 09:30 ET RTH on
|
|
2068
|
+
# equities), intraday buckets on the session-open grid — the
|
|
2069
|
+
# same clock request.security aggregation uses. UTC/24x7
|
|
2070
|
+
# syminfo reduces to the tz-less epoch math bit-for-bit.
|
|
2071
|
+
return (
|
|
2072
|
+
f'tf_change(prev_bar_timestamp_, current_bar_.timestamp, {tf_arg}, '
|
|
2073
|
+
'syminfo_.timezone, syminfo_.session)'
|
|
2074
|
+
)
|
|
2021
2075
|
if func_name == "in_seconds":
|
|
2022
2076
|
tf_arg = self._visit_expr(node.args[0]) if node.args else 'script_tf_'
|
|
2023
2077
|
return f'tf_to_seconds({tf_arg})'
|
|
@@ -2145,25 +2199,25 @@ class CallVisitor:
|
|
|
2145
2199
|
arg_idx,
|
|
2146
2200
|
self._callable_target_callsite_idx(fi_lookup, node),
|
|
2147
2201
|
)
|
|
2148
|
-
|
|
2202
|
+
chart_execution_context = (
|
|
2203
|
+
id(node) not in self._requested_context_inline_node_ids
|
|
2204
|
+
and (
|
|
2205
|
+
getattr(self, "_active_func_name", None),
|
|
2206
|
+
self._current_instance_name,
|
|
2207
|
+
) not in self._requested_context_only_inline_contexts
|
|
2208
|
+
)
|
|
2209
|
+
if not chart_execution_context and isinstance(arg_node, Identifier):
|
|
2210
|
+
# request.security/request.security_lower_tf own a separate
|
|
2211
|
+
# evaluator clock and inline their UDF expressions in
|
|
2212
|
+
# security.py. Preserve the pre-factor direct-Series
|
|
2213
|
+
# lowering in the otherwise-emitted helper bodies.
|
|
2149
2214
|
aname = arg_node.name
|
|
2150
|
-
# Bar field: pass _s_close instead of current_bar_.close
|
|
2151
2215
|
if (
|
|
2152
2216
|
(aname in BAR_FIELDS or aname in BAR_SERIES_PUSH)
|
|
2153
2217
|
and expected_cpp_type == "double"
|
|
2154
2218
|
):
|
|
2155
2219
|
return f"_s_{aname}"
|
|
2156
|
-
# Exact Series binding: pass the Series object directly.
|
|
2157
|
-
# A raw name can also denote a scalar sibling/callable
|
|
2158
|
-
# shadow, in which case lexical state must override the
|
|
2159
|
-
# legacy ``ctx.series_vars`` union and fall through to the
|
|
2160
|
-
# synthetic history bridge below.
|
|
2161
2220
|
safe = self._safe_name(aname)
|
|
2162
|
-
# Function parameters are lexical C++ arguments in every
|
|
2163
|
-
# emitted variant. The legacy function-series clone table
|
|
2164
|
-
# can contain the same raw name, but applying it here makes
|
|
2165
|
-
# cs1+ ignore the actual parameter and read an unrelated
|
|
2166
|
-
# class member instead.
|
|
2167
2221
|
is_current_series_param = (
|
|
2168
2222
|
aname in self._current_func_series_params
|
|
2169
2223
|
)
|
|
@@ -2188,13 +2242,31 @@ class CallVisitor:
|
|
|
2188
2242
|
expr_cpp, cpp_t
|
|
2189
2243
|
)
|
|
2190
2244
|
member = self._inline_history_member(
|
|
2191
|
-
|
|
2245
|
+
(
|
|
2246
|
+
"udf_series_arg"
|
|
2247
|
+
if chart_execution_context
|
|
2248
|
+
else "series_arg"
|
|
2249
|
+
),
|
|
2250
|
+
node,
|
|
2251
|
+
arg_idx=arg_idx,
|
|
2192
2252
|
)
|
|
2253
|
+
if not chart_execution_context:
|
|
2254
|
+
return (
|
|
2255
|
+
f"([&]() -> const Series<{cpp_t}>& {{ "
|
|
2256
|
+
f"{cpp_t} _sv = {bridge_cpp}; "
|
|
2257
|
+
f"if (history_advances_new_bar()) {member}.push(_sv); "
|
|
2258
|
+
f"else {member}.update(_sv); "
|
|
2259
|
+
f"return {member}; }}())"
|
|
2260
|
+
)
|
|
2261
|
+
# A chart-executed plain UDF's history-reading parameter
|
|
2262
|
+
# belongs to this lexical call site, not to the caller Series
|
|
2263
|
+
# object. The on_bar preamble has already advanced its
|
|
2264
|
+
# synthetic buffer by one chart slot, so execution replaces
|
|
2265
|
+
# that slot rather than pushing a second one.
|
|
2193
2266
|
return (
|
|
2194
2267
|
f"([&]() -> const Series<{cpp_t}>& {{ "
|
|
2195
2268
|
f"{cpp_t} _sv = {bridge_cpp}; "
|
|
2196
|
-
f"
|
|
2197
|
-
f"else {member}.update(_sv); "
|
|
2269
|
+
f"{member}.update(_sv); "
|
|
2198
2270
|
f"return {member}; }}())"
|
|
2199
2271
|
)
|
|
2200
2272
|
# A concrete nullable collection specialization learned through an
|
|
@@ -679,11 +679,15 @@ class ExprVisitor:
|
|
|
679
679
|
ta_short = site.class_name.split("::")[-1].lower()
|
|
680
680
|
if site.member_name.startswith(f"_ta_{node.member}_"):
|
|
681
681
|
if node.member == "vwap":
|
|
682
|
+
# Same implicit tail as TA_IMPLICIT_APPEND["vwap"]:
|
|
683
|
+
# the symbol clock keys the Daily anchor reset.
|
|
682
684
|
return (
|
|
683
685
|
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
684
|
-
"current_bar_.close, current_bar_.volume, current_bar_.timestamp
|
|
686
|
+
"current_bar_.close, current_bar_.volume, current_bar_.timestamp"
|
|
687
|
+
" PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)) "
|
|
685
688
|
f": {site.member_name}.recompute(current_bar_.close, "
|
|
686
|
-
"current_bar_.volume, current_bar_.timestamp
|
|
689
|
+
"current_bar_.volume, current_bar_.timestamp"
|
|
690
|
+
" PF_VWAP_SESSION_ANCHOR_ARGS(syminfo_.timezone, syminfo_.session)))"
|
|
687
691
|
)
|
|
688
692
|
return (
|
|
689
693
|
f"(history_advances_new_bar() ? {site.member_name}.compute("
|
|
@@ -1114,6 +1118,12 @@ class ExprVisitor:
|
|
|
1114
1118
|
# sites never share history, and on_bar clears every synthetic member at
|
|
1115
1119
|
# run-start even when this particular expression is conditional.
|
|
1116
1120
|
if isinstance(node.object, FuncCall):
|
|
1121
|
+
# ``[k]`` on a hoisted top-level lazy-edge TA call: its Series was
|
|
1122
|
+
# pushed unconditionally before the statement, so ``[1]`` is the
|
|
1123
|
+
# previous chart bar in every run mode (``_lazy_edge_ta_hoist_plan``).
|
|
1124
|
+
hoisted_member = self._hoisted_hist_reads.get(id(node))
|
|
1125
|
+
if hoisted_member is not None:
|
|
1126
|
+
return f"{hoisted_member}[(int)({idx})]"
|
|
1117
1127
|
inner = self._visit_expr(node.object)
|
|
1118
1128
|
cpp_t = self._infer_type(node.object)
|
|
1119
1129
|
if cpp_t not in ("double", "int", "int64_t", "bool"):
|