Fix scheduler: wrong path + no timezone = never fires
Browse filesBug 1 β Wrong schedule file path:
SCHEDULE_FILE defaulted to /app/shared_data/market_schedule.txt but
the volume/Dockerfile uses /app/data/. File was never found so
load_market_schedule() silently returned {}, scheduler never triggered.
Fixed default path + added ENV in Dockerfile as belt-and-suspenders.
Bug 2 β Server in UTC, market in Athens (GMT+2):
datetime.now() returned UTC time; at 11:46 Athens = 09:46 UTC the
scheduler saw 09:46 < 10:00 and did nothing.
Added Timezone field to market_schedule.txt and _local_now(tz) helper
that uses zoneinfo (Python 3.9+) with pytz fallback.
market_schedule.txt now:
Timezone Europe/Athens
Start 10:00
End 17:00
Scheduler logs local time on every tick for observability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dockerfile +2 -0
- dashboard/dashboard.py +24 -6
- shared_data/market_schedule.txt +1 -0
|
@@ -41,6 +41,8 @@ WORKDIR /app
|
|
| 41 |
COPY shared/ /app/shared/
|
| 42 |
COPY shared_data/securities.txt /app/data/securities.txt
|
| 43 |
COPY shared_data/market_schedule.txt /app/data/market_schedule.txt
|
|
|
|
|
|
|
| 44 |
|
| 45 |
# Matcher service
|
| 46 |
COPY matcher/matcher.py /app/matcher.py
|
|
|
|
| 41 |
COPY shared/ /app/shared/
|
| 42 |
COPY shared_data/securities.txt /app/data/securities.txt
|
| 43 |
COPY shared_data/market_schedule.txt /app/data/market_schedule.txt
|
| 44 |
+
# Also expose schedule path via env so dashboard finds it
|
| 45 |
+
ENV SCHEDULE_FILE=/app/data/market_schedule.txt
|
| 46 |
|
| 47 |
# Matcher service
|
| 48 |
COPY matcher/matcher.py /app/matcher.py
|
|
@@ -22,7 +22,7 @@ sse_clients_lock = threading.Lock()
|
|
| 22 |
# Session state
|
| 23 |
session_state = {"active": False, "start_time": None, "suspended": False, "mode": "automatic"}
|
| 24 |
|
| 25 |
-
SCHEDULE_FILE = os.getenv("SCHEDULE_FILE", "/app/
|
| 26 |
FRONTEND_URL = os.getenv("FRONTEND_URL", "")
|
| 27 |
|
| 28 |
# ββ OHLCV History ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -301,11 +301,27 @@ def load_market_schedule():
|
|
| 301 |
if not line or line.startswith("#"):
|
| 302 |
continue
|
| 303 |
parts = line.split()
|
| 304 |
-
if len(parts) =
|
| 305 |
schedule[parts[0].lower()] = parts[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
except Exception:
|
| 307 |
pass
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
|
| 310 |
|
| 311 |
def _do_session_start():
|
|
@@ -354,13 +370,15 @@ def schedule_runner():
|
|
| 354 |
if session_state.get("mode") == "automatic":
|
| 355 |
sched = load_market_schedule()
|
| 356 |
start_str = sched.get("start")
|
| 357 |
-
end_str
|
|
|
|
| 358 |
if start_str and end_str:
|
| 359 |
-
now =
|
| 360 |
sh, sm = int(start_str.split(":")[0]), int(start_str.split(":")[1])
|
| 361 |
-
eh, em = int(end_str.split(":")[0]),
|
| 362 |
start_t = now.replace(hour=sh, minute=sm, second=0, microsecond=0)
|
| 363 |
end_t = now.replace(hour=eh, minute=em, second=0, microsecond=0)
|
|
|
|
| 364 |
if now >= end_t and session_state["active"]:
|
| 365 |
print("[Scheduler] Auto end of day")
|
| 366 |
_do_session_end()
|
|
|
|
| 22 |
# Session state
|
| 23 |
session_state = {"active": False, "start_time": None, "suspended": False, "mode": "automatic"}
|
| 24 |
|
| 25 |
+
SCHEDULE_FILE = os.getenv("SCHEDULE_FILE", "/app/data/market_schedule.txt")
|
| 26 |
FRONTEND_URL = os.getenv("FRONTEND_URL", "")
|
| 27 |
|
| 28 |
# ββ OHLCV History ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 301 |
if not line or line.startswith("#"):
|
| 302 |
continue
|
| 303 |
parts = line.split()
|
| 304 |
+
if len(parts) >= 2:
|
| 305 |
schedule[parts[0].lower()] = parts[1]
|
| 306 |
+
except Exception as e:
|
| 307 |
+
print(f"[Scheduler] Cannot read schedule file {SCHEDULE_FILE}: {e}")
|
| 308 |
+
return schedule
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def _local_now(tz_name):
|
| 312 |
+
"""Return current datetime in the given IANA timezone (e.g. 'Europe/Athens')."""
|
| 313 |
+
try:
|
| 314 |
+
from zoneinfo import ZoneInfo
|
| 315 |
+
return datetime.datetime.now(ZoneInfo(tz_name)).replace(tzinfo=None)
|
| 316 |
except Exception:
|
| 317 |
pass
|
| 318 |
+
try:
|
| 319 |
+
import pytz
|
| 320 |
+
tz = pytz.timezone(tz_name)
|
| 321 |
+
return datetime.datetime.now(tz).replace(tzinfo=None)
|
| 322 |
+
except Exception:
|
| 323 |
+
pass
|
| 324 |
+
return datetime.datetime.utcnow()
|
| 325 |
|
| 326 |
|
| 327 |
def _do_session_start():
|
|
|
|
| 370 |
if session_state.get("mode") == "automatic":
|
| 371 |
sched = load_market_schedule()
|
| 372 |
start_str = sched.get("start")
|
| 373 |
+
end_str = sched.get("end")
|
| 374 |
+
tz_name = sched.get("timezone", "UTC")
|
| 375 |
if start_str and end_str:
|
| 376 |
+
now = _local_now(tz_name)
|
| 377 |
sh, sm = int(start_str.split(":")[0]), int(start_str.split(":")[1])
|
| 378 |
+
eh, em = int(end_str.split(":")[0]), int(end_str.split(":")[1])
|
| 379 |
start_t = now.replace(hour=sh, minute=sm, second=0, microsecond=0)
|
| 380 |
end_t = now.replace(hour=eh, minute=em, second=0, microsecond=0)
|
| 381 |
+
print(f"[Scheduler] Local time ({tz_name}): {now.strftime('%H:%M')} window {start_str}-{end_str}")
|
| 382 |
if now >= end_t and session_state["active"]:
|
| 383 |
print("[Scheduler] Auto end of day")
|
| 384 |
_do_session_end()
|
|
@@ -1,2 +1,3 @@
|
|
|
|
|
| 1 |
Start 10:00
|
| 2 |
End 17:00
|
|
|
|
| 1 |
+
Timezone Europe/Athens
|
| 2 |
Start 10:00
|
| 3 |
End 17:00
|