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
44 changes: 40 additions & 4 deletions src/session_time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,40 @@ bool pine_session_ispostmarket(const std::string& session,
// Public functions
// =========================================================================

// A 2-argument time()/time_close() call binds its second string to the
// `session` parameter, but scripts commonly pass a TIMEZONE there — e.g.
// `time("D", "America/New_York")` to detect a day-change in that zone.
// TradingView honors this idiom. When no explicit timezone was supplied and
// the session slot instead holds an unambiguous timezone (IANA "Area/Location"
// or a GMT/UTC specifier), reinterpret it as the timezone with no session
// filter. A real session window ("0930-1600") never contains '/' nor starts
// with GMT/UTC, and a 3-arg call supplies tz_in, so neither is affected.
static bool session_arg_is_timezone(const std::string& s) {
if (s.empty())
return false;
if (s.find('/') != std::string::npos) // IANA e.g. America/New_York
return true;
if (s.rfind("GMT", 0) == 0 || s.rfind("UTC", 0) == 0) // GMT / UTC / ±offset
return true;
return false;
}

// Resolve the (session, tz) pair for a time()/time_close() call, applying the
// 2-arg timezone-in-session-slot reinterpretation above.
static void resolve_session_tz(const std::string& session,
const std::string& tz_in,
std::string& sess_out,
std::string& tz_out) {
sess_out = session;
tz_out = tz_in;
if (tz_out.empty() && session_arg_is_timezone(sess_out)) {
tz_out = sess_out;
sess_out.clear();
}
if (tz_out.empty())
tz_out = "UTC";
}

int64_t pine_time(int64_t bar_ms,
const std::string& tf_in,
const std::string& session,
Expand All @@ -427,9 +461,10 @@ int64_t pine_time(int64_t bar_ms,
if (tf.empty())
tf = "1";

std::string tz = tz_in.empty() ? "UTC" : tz_in;
std::string sess, tz;
resolve_session_tz(session, tz_in, sess, tz);

if (!session.empty() && !passes_session_filter(session, tz, bar_ms))
if (!sess.empty() && !passes_session_filter(sess, tz, bar_ms))
return na<int64_t>();

return compute_tf_open_ms(bar_ms, tf, tz);
Expand All @@ -444,9 +479,10 @@ int64_t pine_time_close(int64_t bar_ms,
if (tf.empty())
tf = "1";

std::string tz = tz_in.empty() ? "UTC" : tz_in;
std::string sess, tz;
resolve_session_tz(session, tz_in, sess, tz);

if (!session.empty() && !passes_session_filter(session, tz, bar_ms))
if (!sess.empty() && !passes_session_filter(sess, tz, bar_ms))
return na<int64_t>();

int64_t t_open = compute_tf_open_ms(bar_ms, tf, tz);
Expand Down
50 changes: 50 additions & 0 deletions tests/test_session_time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,62 @@ static void test_time_close_hourly() {
CHECK(tc == to + 3600000 - 1);
}

// --- 2-arg time(tf, tz): a timezone passed in the session slot ---

static void test_time_tz_in_session_slot_equiv() {
std::printf("test_time_tz_in_session_slot_equiv\n");
// time("D", "America/New_York") binds the tz into the session slot. The
// engine must reinterpret it as the timezone (not a session) — previously
// it treated it as a session, matched no HHMM window, and returned na.
int64_t bar = 1775572200000LL; // 2026-04-07 14:30 UTC = 10:30 NY (EDT)
int64_t two_arg = pine_time(bar, "D", "America/New_York", "", "15");
int64_t three_arg = pine_time(bar, "D", "", "America/New_York", "15");
CHECK(!is_na(two_arg)); // was na for every bar (the bug)
CHECK(two_arg == three_arg); // reinterpreted identically to the 3-arg form
}

static void test_time_tz_in_session_daily_change() {
std::printf("test_time_tz_in_session_daily_change\n");
// Bars in the same NY-day share the daily time; the next NY-day differs —
// this is what makes ta.change(time("D", tz)) fire once per day.
int64_t bar_a = 1775572200000LL; // 2026-04-07 10:30 NY
int64_t bar_b = bar_a + 3600000LL; // +1h, same NY-day
int64_t bar_next = bar_a + 24LL * 3600000LL; // +24h, next NY-day
int64_t ta = pine_time(bar_a, "D", "America/New_York", "", "15");
int64_t tb = pine_time(bar_b, "D", "America/New_York", "", "15");
int64_t tn = pine_time(bar_next, "D", "America/New_York", "", "15");
CHECK(ta == tb); // same day: no change
CHECK(ta != tn); // new day: change fires
}

static void test_time_real_session_2arg_still_filters() {
std::printf("test_time_real_session_2arg_still_filters\n");
// A genuine 2-arg session (no tz) must still filter — the reinterpretation
// only triggers for tz-looking strings, never "0800-1600".
int64_t bar_in = 1775572200000LL; // 14:30 UTC — inside 0800-1600 UTC
int64_t bar_out = 1775601000000LL; // 22:30 UTC — outside
CHECK(!is_na(pine_time(bar_in, "60", "0800-1600", "", "60")));
CHECK( is_na(pine_time(bar_out, "60", "0800-1600", "", "60")));
}

static void test_time_gmt_in_session_slot() {
std::printf("test_time_gmt_in_session_slot\n");
// GMT/UTC specifiers in the session slot are also recognized as timezones.
int64_t bar = 1775572200000LL;
CHECK(!is_na(pine_time(bar, "D", "GMT+0", "", "15")));
CHECK(!is_na(pine_time(bar, "D", "UTC", "", "15")));
}

int main() {
test_time_hourly_bucket_utc();
test_time_session_ny_inside();
test_time_session_ny_outside();
test_time_weekday_filter_mon_fri_only();
test_time_close_hourly();
test_time_tz_in_session_slot_equiv();
test_time_tz_in_session_daily_change();
test_time_real_session_2arg_still_filters();
test_time_gmt_in_session_slot();

std::printf("session_time: %d passed, %d failed\n", tests_passed, tests_failed);
return tests_failed > 0 ? 1 : 0;
Expand Down
Loading