@pineforge/codegen-pyodide 0.10.2 → 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.
@@ -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 so the runtime can detect Daily
252
- # anchor boundaries (Pine v6 default for `ta.vwap(source)` resets
253
- # the cumulator at every UTC-day change).
254
- "vwap": "current_bar_.volume, current_bar_.timestamp",
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.
@@ -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 self._is_lazy_saturated_roc3_site(site):
1380
- return self._lazy_saturated_roc3_expr(site)
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
- return (
1859
- f"[&]() -> int64_t {{ "
1860
- f"int _yr = ({yr}); int _mo = ({mo}); int _dy = ({dy}); "
1861
- f"int _hr = ({hr}); int _min = ({mn}); int _sc = ({sc}); "
1862
- f"static thread_local int _last_yr = -1, _last_mo = -1, _last_dy = -1, _last_hr = -1, _last_min = -1, _last_sc = -1; "
1863
- f"static thread_local int64_t _last_res = -1; "
1864
- f"if (_last_res != -1 && _last_yr == _yr && _last_mo == _mo && _last_dy == _dy && _last_hr == _hr && _last_min == _min && _last_sc == _sc) {{ "
1865
- f"return _last_res; "
1866
- f"}} "
1867
- f"struct tm t = {{}}; "
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
- return f'tf_change(prev_bar_timestamp_, current_bar_.timestamp, {tf_arg})'
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})'
@@ -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"):
Binary file
package/release.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "codegen": "0.10.2",
2
+ "codegen": "0.10.3",
3
3
  "pyodide": "314.0.0",
4
4
  "python": "3.14.0",
5
5
  "emscripten": "emscripten_5_0_3",
6
- "sha256": "97ebb8d0b14c32d3c88b9521c8f13150ee9cb9e3859227af970ec4dcbfa6bf71"
6
+ "sha256": "50f545e8a008c23ec01a752a5e8bd00e885dda9dcd2bb417e2af18d680c22a6e"
7
7
  }
package/tables.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "CODEGEN_VERSION": "0.10.2",
2
+ "CODEGEN_VERSION": "0.10.3",
3
3
  "TA_CLASS_MAP_KEYS": [
4
4
  "sma",
5
5
  "ema",
Binary file