@@ -25,7 +25,54 @@ int hhmm_to_minutes(const std::string& hhmm) {
2525 return h * 60 + m;
2626}
2727
28+ // Decompose a Unix-ms timestamp into local calendar fields for `tz` WITHOUT the
29+ // per-call setenv/tzset churn that made hour()/minute()/... pathologically slow
30+ // on macOS (each changed-TZ tzset() does a notifyd Mach-IPC round-trip ~173us;
31+ // ~888k/run over the full feed -> minutes of apparent "hang", KI-35). UTC needs
32+ // no tz -> tzset-free gmtime_r; other zones use the CACHED pine_tz::ScopedTimezone
33+ // (skips tzset when the active zone is unchanged, and never restores), so a run
34+ // that stays on one zone pays a single tzset. DST stays exact: localtime_r
35+ // consults the tz database exactly as the old inline lambda did.
36+ static void decompose_ms_local (int64_t bar_ms, const std::string& tz, struct tm & out) {
37+ time_t secs = static_cast <time_t >(bar_ms / 1000 );
38+ const std::string t = normalize_timezone_for_posix (tz);
39+ if (t.empty () || t == " UTC" || t == " Etc/UTC" ) {
40+ gmtime_r (&secs, &out);
41+ } else {
42+ pine_tz::ScopedTimezone guard (t);
43+ localtime_r (&secs, &out);
44+ }
45+ }
46+
47+ // Pine time/date extraction — value-identical to the codegen's former inline
48+ // setenv+tzset lambda (pineforge-codegen tables.py TIME_FIELD_EXPRS) but
49+ // churn-free. hour()/minute()/... route through these instead of open-coding it.
50+ int pine_hour (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return t.tm_hour ; }
51+ int pine_minute (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return t.tm_min ; }
52+ int pine_second (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return t.tm_sec ; }
53+ int pine_dayofmonth (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return t.tm_mday ; }
54+ int pine_dayofweek (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return t.tm_wday + 1 ; }
55+ int pine_month (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return t.tm_mon + 1 ; }
56+ int pine_year (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return t.tm_year + 1900 ; }
57+ int pine_weekofyear (int64_t bar_ms, const std::string& tz) { struct tm t; decompose_ms_local (bar_ms, tz, t); return (t.tm_yday + 7 - ((t.tm_wday + 6 ) % 7 )) / 7 ; }
58+
59+ static int64_t calendar_day_open_local_ms_tz (int64_t bar_ms, const std::string& tz);
60+
2861int64_t calendar_day_open_local_ms (int64_t bar_ms, const std::string& tz) {
62+ // UTC needs no tzset: local midnight is exact integer floor. Avoiding
63+ // ScopedTimezone(UTC) here keeps the process TZ from flipping to UTC every
64+ // bar (which would re-slow the strategy's hour()/minute() zone) — see KI-35.
65+ {
66+ const std::string t = normalize_timezone_for_posix (tz);
67+ if (t.empty () || t == " UTC" || t == " Etc/UTC" ) {
68+ time_t secs = static_cast <time_t >(bar_ms / 1000 );
69+ return static_cast <int64_t >((secs / 86400 ) * 86400 ) * 1000 ;
70+ }
71+ }
72+ return calendar_day_open_local_ms_tz (bar_ms, tz);
73+ }
74+
75+ static int64_t calendar_day_open_local_ms_tz (int64_t bar_ms, const std::string& tz) {
2976 pine_tz::ScopedTimezone guard (tz);
3077 time_t secs = static_cast <time_t >(bar_ms / 1000 );
3178 struct tm local_tm {};
@@ -534,10 +581,8 @@ int64_t pine_time_tradingday(int64_t bar_ms,
534581 std::string eff_tz = tz.empty () ? " UTC" : tz;
535582 int bar_local_min;
536583 {
537- pine_tz::ScopedTimezone guard (eff_tz);
538- time_t secs = static_cast <time_t >(bar_ms / 1000 );
539584 struct tm local_tm {};
540- localtime_r (&secs, & local_tm);
585+ decompose_ms_local (bar_ms, eff_tz, local_tm); // gmtime_r for UTC (no TZ flip)
541586 bar_local_min = local_tm.tm_hour * 60 + local_tm.tm_min ;
542587 }
543588
0 commit comments