Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions pineforge_codegen/codegen/visit_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,21 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
"MMM DD YYYY ..." forms. A dateString without a time zone is GMT+0 per
the Pine reference. Returns None when the string cannot be parsed.
"""
from datetime import datetime, timezone
import re
from datetime import datetime, timezone, timedelta

txt = text.strip()
# Pine dateStrings may carry a trailing timezone WORD ("2024-01-01 00:00 UTC",
# "1 Jan 2020 09:30 GMT+2") that neither fromisoformat nor the strptime forms
# below can read. Peel it off and fold it into an explicit offset (UTC/GMT
# with an optional ±H[:MM] suffix; the bare word is +00:00).
tzoff = None
m = re.search(r"\s+(?:UTC|GMT)([+-]\d{1,2})?(?::?(\d{2}))?$", txt, re.I)
if m:
txt = txt[: m.start()].strip()
h = int(m.group(1) or 0)
mm = int(m.group(2) or 0)
tzoff = timezone(timedelta(hours=h, minutes=(mm if h >= 0 else -mm)))
dt = None
try:
dt = datetime.fromisoformat(txt)
Expand All @@ -188,6 +200,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
"%d %b %Y %H:%M:%S", "%d %b %Y %H:%M", "%d %b %Y",
"%b %d %Y %H:%M:%S %z", "%b %d %Y %H:%M %z",
"%b %d %Y %H:%M:%S", "%b %d %Y %H:%M", "%b %d %Y",
"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d",
):
try:
dt = datetime.strptime(txt, fmt)
Expand All @@ -197,7 +210,7 @@ def _parse_pine_datestring_ms(text: str) -> int | None:
if dt is None:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt = dt.replace(tzinfo=(tzoff or timezone.utc))
return int(dt.timestamp() * 1000)


Expand Down
8 changes: 6 additions & 2 deletions pineforge_codegen/support_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,14 @@
# request.security parameter rules.
# Codegen supports symbol/timeframe/expression plus gaps/lookahead (read in
# _eval_security_* emission and forwarded to register_security_eval).
# ignore_invalid_symbol/currency are accepted by the parser but silently
# dropped by codegen — reject loudly to surface unsupported behavior.
# `currency` is still rejected loudly (it changes the returned values via FX
# conversion, which codegen drops — silently wrong). `ignore_invalid_symbol` is
# a guaranteed no-op here: codegen forces request.security onto the current
# chart symbol (see the "symbol must reference current chart symbol" check),
# which is always valid, so the flag can never change the result — accept+ignore.
SECURITY_ALLOWED_PARAMS: frozenset[str] = frozenset(
{"symbol", "timeframe", "expression", "gaps", "lookahead",
"ignore_invalid_symbol",
# Data-adjustment constants — silently accepted and ignored by codegen;
# the underlying engine uses a fixed unadjusted data source.
"backadjustment", "settlement_as_close", "adjustment"}
Expand Down
14 changes: 14 additions & 0 deletions tests/test_codegen_audit_fixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ def test_timestamp_datestring_dd_mmm_yyyy_parsed():
assert "1626728400000LL" in cpp


def test_timestamp_datestring_ymd_utc_word_parsed():
# "YYYY-MM-DD HH:MM UTC" — space-separated with a trailing tz WORD (not a
# numeric offset). 2024-01-01 00:00 UTC = 1704067200000 ms.
cpp = _gen('t = timestamp("2024-01-01 00:00 UTC")\nplot(close)\n')
assert "1704067200000LL" in cpp


def test_timestamp_datestring_gmt_offset_word_parsed():
# "GMT+2" trailing word = +2h offset. 1 Jan 2020 09:30 GMT+2 = 07:30 UTC
# = 1577863800000 ms.
cpp = _gen('t = timestamp("1 Jan 2020 09:30 GMT+2")\nplot(close)\n')
assert "1577863800000LL" in cpp


def test_timestamp_unparseable_datestring_rejected():
with pytest.raises(CompileError, match="could not parse"):
_gen('t = timestamp("not a date")\nplot(close)\n')
Expand Down
13 changes: 8 additions & 5 deletions tests/test_compile_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,11 +517,14 @@ def test_regression_year_function_call_form_compiles():
cpp = transpile(src)
# Spot-check the lowering shape so anyone deleting the special case
# in visit_call.py learns about it from the assertion, not from a
# mysterious clang error.
assert "gmtime_r" in cpp, (
"year(time) etc. should lower to a gmtime_r-based extraction lambda; "
"if you intentionally moved the implementation to a runtime helper, "
"update this assertion to match the new emission shape."
# mysterious clang error. The bar-time builtins now lower to the engine's
# cached pine_<field>() helpers (session_time.cpp) instead of an inline
# gmtime_r/localtime_r lambda — the per-call tzset churn caused a macOS
# notifyd storm (KI-35). The UTC/tz decode lives inside the helper now.
assert "pine_year(" in cpp and "pine_hour(" in cpp, (
"year(time)/hour(time) etc. should lower to the engine pine_<field>() "
"helpers; if you intentionally changed the emission, update this "
"assertion to match the new shape."
)
compile_cpp(cpp, label="regression_year_function_form")

Expand Down
9 changes: 7 additions & 2 deletions tests/test_support_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,14 @@ def test_request_security_currency_kwarg_rejected():
_expect_error(src, "currency")


def test_request_security_ignore_invalid_symbol_kwarg_rejected():
def test_request_security_ignore_invalid_symbol_kwarg_accepted():
# ignore_invalid_symbol is a guaranteed no-op: codegen forces the current
# chart symbol (always valid), so the flag can never change the result.
# Accept it (codegen drops it) rather than reject a valid, harmless script.
src = (PRELUDE +
'a = request.security(syminfo.tickerid, "60", close, '
'ignore_invalid_symbol=true)\n')
_expect_error(src, "ignore_invalid_symbol")
assert _errors(src) == []


def test_request_security_positional_gaps_lookahead_passes():
Expand Down Expand Up @@ -626,9 +629,11 @@ def test_security_allowed_params_locked():
G2 sprint: backadjustment / settlement_as_close / adjustment added so scripts
using those request.security kwargs compile instead of being hard-rejected.
Codegen silently drops them (engine uses fixed unadjusted data).
ignore_invalid_symbol added: a guaranteed no-op on the forced chart symbol.
"""
assert SECURITY_ALLOWED_PARAMS == frozenset(
{"symbol", "timeframe", "expression", "gaps", "lookahead",
"ignore_invalid_symbol",
"backadjustment", "settlement_as_close", "adjustment"}
)

Expand Down
Loading