데몬이 시스템 UTC 날짜로 슬롯을 1회만 preallocate 해 trading_date 가 KST 캘린더와 어긋나고 재시작 없이는 다음날 슬롯이 안 생기던 문제(issue #14) 정정. 거버넌스: 전부 change-req (신규 REQ 없음, dcy-014 OQ-3; 스케줄러는 REQ-030 패턴 준용). - REQ-005: trading_date 를 설정 타임존(기본 Asia/Seoul) 기준 계산(timestamp 는 UTC 유지). DailyPreallocationScheduler 신규 — TZ 일자당 1회 멱등 fire, 재시작 없이 다음날 슬롯 자동 생성. setup() 배선 + trex-preallocation 스레드 - REQ-048: TOML trading_timezone/daily_preallocation_time 키 + from_config 전파 - REQ-009: runtime-flows/data-model 정합 + instance.example.toml OQ-1 기본 00:00(TZ 자정, TOML 설정 가능) / OQ-2 매 캘린더일 생성(거래일 캘린더 후속). 멱등 preallocate 로 중복 tick·다중 인스턴스 중복 생성 0. 검증: 단위 302 passed/32 skipped, 인프라 게이트(PG+RabbitMQ) 334 passed, ruff clean. 신규 test_preallocation_scheduler(고정 clock) + test_instance_config 확장. BAT-018 release-candidate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
4.4 KiB
Python
110 lines
4.4 KiB
Python
"""Daily preallocation scheduler tests (dcy-014 / REQ-005, issue #14).
|
|
|
|
trading_date is derived in a configurable timezone (default KST) with an
|
|
injected clock; timestamps stay UTC. Covers KST/UTC boundary, cross-day
|
|
auto-create + idempotency, configurable TZ, and trigger-time gating.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, time, timedelta, timezone
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from trex_core.application.preallocation_scheduler import DailyPreallocationScheduler
|
|
from trex_core.domain.slot import SlotService, SlotState
|
|
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
|
|
|
|
KST = ZoneInfo("Asia/Seoul")
|
|
UTC = timezone.utc
|
|
|
|
|
|
def _wire():
|
|
repo = InMemorySlotRepository()
|
|
svc = SlotService(repo)
|
|
return repo, svc
|
|
|
|
|
|
def _dates(repo):
|
|
return sorted({s.trading_date for s in repo._slots.values()})
|
|
|
|
|
|
def _count(repo, d):
|
|
return sum(1 for s in repo._slots.values() if s.trading_date == d)
|
|
|
|
|
|
def test_trading_date_uses_kst_not_utc_at_midnight_boundary():
|
|
"""S-1: 2026-06-08 15:30Z is already 2026-06-09 in KST."""
|
|
repo, svc = _wire()
|
|
clock = lambda: datetime(2026, 6, 8, 15, 30, tzinfo=UTC) # noqa: E731
|
|
sched = DailyPreallocationScheduler(
|
|
slot_service=svc, total_slots=10, tz=KST, clock=clock,
|
|
)
|
|
td = sched.run_once()
|
|
assert td == date(2026, 6, 9) # KST date, not UTC 6/8
|
|
assert _dates(repo) == [date(2026, 6, 9)]
|
|
assert _count(repo, date(2026, 6, 9)) == 10
|
|
|
|
|
|
def test_timestamps_stay_utc():
|
|
"""S-2: slot created_at/updated_at remain UTC even with KST trading_date."""
|
|
repo, svc = _wire()
|
|
clock = lambda: datetime(2026, 6, 8, 15, 30, tzinfo=UTC) # noqa: E731
|
|
DailyPreallocationScheduler(
|
|
slot_service=svc, total_slots=1, tz=KST, clock=clock,
|
|
).run_once()
|
|
slot = next(iter(repo._slots.values()))
|
|
assert slot.updated_at.utcoffset() == timedelta(0) # UTC
|
|
|
|
|
|
def test_cross_day_boundary_auto_creates_next_day_idempotently():
|
|
"""S-3: a day boundary creates next-day slots once; re-ticks add nothing."""
|
|
repo, svc = _wire()
|
|
now = {"t": datetime(2026, 6, 7, 16, 0, tzinfo=UTC)} # 2026-06-08 01:00 KST
|
|
sched = DailyPreallocationScheduler(
|
|
slot_service=svc, total_slots=10, tz=KST, clock=lambda: now["t"],
|
|
)
|
|
sched.run_once() # boot -> 6/8
|
|
assert _dates(repo) == [date(2026, 6, 8)]
|
|
now["t"] = datetime(2026, 6, 8, 15, 30, tzinfo=UTC) # 2026-06-09 00:30 KST
|
|
sched._maybe_tick() # boundary -> 6/9
|
|
assert _dates(repo) == [date(2026, 6, 8), date(2026, 6, 9)]
|
|
now["t"] = datetime(2026, 6, 8, 16, 30, tzinfo=UTC) # still 6/9 KST
|
|
sched._maybe_tick() # already fired today
|
|
assert _count(repo, date(2026, 6, 9)) == 10 # no duplicates
|
|
|
|
|
|
def test_configurable_timezone_changes_trading_date():
|
|
"""S-4: same instant yields a different trading_date under a different TZ."""
|
|
repo, svc = _wire()
|
|
clock = lambda: datetime(2026, 6, 8, 15, 30, tzinfo=UTC) # noqa: E731
|
|
DailyPreallocationScheduler(
|
|
slot_service=svc, total_slots=1, tz=UTC, clock=clock,
|
|
).run_once()
|
|
assert _dates(repo) == [date(2026, 6, 8)] # UTC date, not KST 6/9
|
|
|
|
|
|
def test_trigger_time_gates_next_day_creation():
|
|
"""A configured trigger time defers next-day creation until reached."""
|
|
repo, svc = _wire()
|
|
now = {"t": datetime(2026, 6, 7, 16, 0, tzinfo=UTC)} # 6/8 01:00 KST
|
|
sched = DailyPreallocationScheduler(
|
|
slot_service=svc, total_slots=5, tz=KST,
|
|
schedule_time=time(hour=7), clock=lambda: now["t"],
|
|
)
|
|
sched.run_once() # boot -> 6/8
|
|
now["t"] = datetime(2026, 6, 8, 18, 0, tzinfo=UTC) # 6/9 03:00 KST (< 07:00)
|
|
sched._maybe_tick()
|
|
assert date(2026, 6, 9) not in _dates(repo) # not yet
|
|
now["t"] = datetime(2026, 6, 8, 22, 30, tzinfo=UTC) # 6/9 07:30 KST (>= 07:00)
|
|
sched._maybe_tick()
|
|
assert date(2026, 6, 9) in _dates(repo) # created after trigger
|
|
|
|
|
|
def test_idle_slots_created_for_trading_date():
|
|
"""Created slots are idle (active only on buy admission)."""
|
|
repo, svc = _wire()
|
|
clock = lambda: datetime(2026, 6, 8, 1, 0, tzinfo=UTC) # noqa: E731
|
|
DailyPreallocationScheduler(
|
|
slot_service=svc, total_slots=3, tz=KST, clock=clock,
|
|
).run_once()
|
|
assert all(s.status is SlotState.IDLE for s in repo._slots.values())
|