feat(runtime): dcy-014 trading_date KST 타임존 분리 + 일일 슬롯 preallocation (BAT-018)
데몬이 시스템 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>
This commit is contained in:
@@ -10,3 +10,11 @@ instance_key = "default"
|
||||
# portfolio-hub auth token (kept out of .env). Injected into PortfolioHubConfig.token
|
||||
# when TREX_PORTFOLIO_HUB_BASE_URL is set in the environment.
|
||||
portfolio_hub_token = ""
|
||||
|
||||
# dcy-014 / REQ-005: trading_date is computed in this timezone (IANA name),
|
||||
# while all timestamps stay UTC. Default Asia/Seoul (KST).
|
||||
trading_timezone = "Asia/Seoul"
|
||||
|
||||
# Daily slot preallocation trigger (HH:MM, in trading_timezone). The next day's
|
||||
# slots are created at this local time without a daemon restart. Default midnight.
|
||||
daily_preallocation_time = "00:00"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# BAT-018 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-018
|
||||
|
||||
## 설계 목표
|
||||
- `trading_date` 를 설정 타임존(기본 KST) 기준으로 분리해 KST 캘린더 일자와 DB 일자를 일치시키고, 재시작 없이 일자 경계마다 다음날 슬롯을 자동 생성한다(issue #14). timestamp 는 UTC 유지(NFR-1).
|
||||
|
||||
## 1. DailyPreallocationScheduler (REQ-005, REQ-030 패턴)
|
||||
- `application/preallocation_scheduler.py`(신규). 의존: `slot_service`(preallocate_daily_slots), `total_slots`, `tz`(tzinfo), `schedule_time`(time, 기본 00:00), `clock`(주입식, 기본 utcnow).
|
||||
- `run_once()`: `trading_date = clock().astimezone(tz).date()` → `preallocate_daily_slots(trading_date, total_slots)`(멱등) → `_last_fired_date = trading_date`. 반환 trading_date.
|
||||
- `_maybe_tick()`: `now_tz = clock().astimezone(tz)`; `target = combine(now_tz.date(), schedule_time, tz)`; `now_tz < target` 면 skip; `_last_fired_date == now_tz.date()` 면 skip(오늘 이미 생성); 아니면 run_once.
|
||||
- `run_forever(stop, tick_sec=60)`: boot run_once(당일 즉시 확보) 후 `stop.wait(tick_sec)` 루프 + try/except(루프 생존).
|
||||
- 경계 의미: boot 는 항상 현재 TZ 일자 슬롯 확보(시각 무관). schedule_time 은 **다음날** 경계 fire 시점 게이트. 기본 00:00 → 자정 직후 다음날 생성.
|
||||
|
||||
## 2. 도메인 무침범 (A-3)
|
||||
- `SlotService.preallocate_daily_slots(trading_date: date, count)` 시그니처/멱등 그대로 재사용. TZ 계산은 스케줄러(호출부)에서만 수행 → REQ-002 도메인 본문 변경 없음.
|
||||
|
||||
## 3. 설정 (REQ-048, TOML 일원화)
|
||||
- `DaemonInstanceConfig`(TOML, frozen): `trading_timezone: str = "Asia/Seoul"`, `daily_preallocation_time: str = "00:00"`. `from_toml` 이 optional 키 파싱(부재 시 기본).
|
||||
- `DaemonConfig`: 동일 두 필드(기본 KST/00:00). `from_env` 는 기본값 사용(TOML 일원화 — env var 미도입). `from_config` 가 TOML 값으로 `replace`.
|
||||
- 운영자는 `--config` TOML 로 명시 지정(FR-4). 신규 설정 표면 아님.
|
||||
|
||||
## 4. daemon 배선 (REQ-005)
|
||||
- `__init__`: `DailyPreallocationScheduler(slot_service, total_slots, tz=ZoneInfo(config.trading_timezone), schedule_time=_parse_kst_time(config.daily_preallocation_time))`.
|
||||
- `setup()`: UTC 날짜 `preallocate_daily_slots` 직접 호출 제거 → `preallocation_scheduler.run_once()`(boot, TZ 당일 확보).
|
||||
- `_preallocation_loop`: `run_forever(stop, tick_sec=60)`. workers 에 `trex-preallocation` 추가(8 스레드).
|
||||
|
||||
## 5. timestamp UTC 유지 (NFR-1)
|
||||
- 슬롯 `created_at`/`updated_at`/전이 ts 는 기존대로 `datetime.now(tz=timezone.utc)`. 본 변경은 trading_date(date) 계산만 TZ 화.
|
||||
|
||||
## 6. baseline 문서 (REQ-009)
|
||||
- runtime-flows: 데몬 lifecycle(TOML 키 추가·8 스레드·setup preallocation_scheduler) + `일일 슬롯 preallocation` Flow 신규.
|
||||
- data-model: trading_date=설정 TZ 기준 / timestamp=UTC 구분.
|
||||
- config/instance.example.toml: 두 키 주석 문서화.
|
||||
|
||||
## 멱등성/안정성 (NFR-2/3)
|
||||
- 일자당 1회 fire + preallocate date 멱등 → 중복 tick·다중 인스턴스 중복 생성 0. 재시작 시 boot run_once 가 당일 확보. 무재시작 일자 경계는 _maybe_tick 가 다음날 생성.
|
||||
|
||||
## 테스트 설계
|
||||
- `tests/application/test_preallocation_scheduler.py`(고정 clock 주입): KST/UTC 자정 경계 trading_date(S-1), timestamp UTC 유지(S-2), 일자 경계 자동 생성+멱등 재tick(S-3), 설정 TZ 변경(S-4), 트리거 시각 게이팅, idle 슬롯.
|
||||
- `tests/runtime/test_instance_config.py` 확장: TOML 기본값(KST/00:00) + TOML 값 from_config 전파.
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# BAT-018 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-018
|
||||
|
||||
## 구현 요약
|
||||
- dcy-014 trading_date 설정 타임존(KST) 분리 + 일일 슬롯 preallocation(issue #14)을 전부 change-req 로 구현(신규 REQ 없음; 스케줄러는 REQ-030 패턴 준용).
|
||||
|
||||
## 변경 내역
|
||||
|
||||
### REQ-005 — 스케줄러 + 부트스트랩
|
||||
- `trex_core/application/preallocation_scheduler.py`(신규): `DailyPreallocationScheduler` — `run_once()`(TZ trading_date 멱등 preallocate), `_maybe_tick()`(TZ 일자당 1회 + 트리거 시각 게이트), `run_forever(stop, tick_sec)`. clock 주입식(테스트 결정론).
|
||||
- `trex_core/runtime/daemon.py`: `DailyPreallocationScheduler` 인스턴스화(`tz=ZoneInfo(config.trading_timezone)`, `schedule_time=_parse_kst_time(config.daily_preallocation_time)`); `setup()` 의 UTC 날짜 직접 preallocation → `preallocation_scheduler.run_once()`; `_preallocation_loop` + `trex-preallocation` 워커(8 스레드); `zoneinfo.ZoneInfo` import. 미사용 `datetime, timezone` import 제거.
|
||||
|
||||
### REQ-048 — TOML 설정
|
||||
- `trex_core/runtime/config.py`: `DaemonInstanceConfig` 에 `trading_timezone="Asia/Seoul"`/`daily_preallocation_time="00:00"` + `from_toml` optional 파싱; `DaemonConfig` 동일 필드(기본값); `from_config` 가 TOML 값 전파(`replace`). `from_env` 는 기본값.
|
||||
- `config/instance.example.toml`: 두 키 주석 문서화.
|
||||
|
||||
### REQ-009 — baseline 문서
|
||||
- `docs/runtime-flows.md`: 데몬 lifecycle TOML 키·8 스레드·setup preallocation_scheduler + `일일 슬롯 preallocation` Flow 신규. Last Updated → BAT-018.
|
||||
- `docs/data-model.md`: trading_date=설정 TZ 기준 / timestamp=UTC 구분. Last Updated → BAT-018.
|
||||
|
||||
## 테스트
|
||||
- 신규 `tests/application/test_preallocation_scheduler.py`(고정 clock): 6건 — KST/UTC 자정 경계 trading_date(S-1), timestamp UTC 유지(S-2), 일자 경계 자동 생성+멱등 재tick(S-3), 설정 TZ 변경(S-4), 트리거 시각 게이팅, idle 슬롯.
|
||||
- `tests/runtime/test_instance_config.py` 확장: TOML 기본값(KST/00:00) + TOML 값 from_config 전파 2건.
|
||||
|
||||
## 검증 실행
|
||||
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `302 passed / 32 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
|
||||
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `334 passed`.
|
||||
- `ruff check trex_core tests` → All checks passed.
|
||||
@@ -0,0 +1,34 @@
|
||||
# BAT-018: trading_date 설정 타임존(KST) 분리 + 일일 슬롯 preallocation (issue #14)
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-014
|
||||
- Included REQ: REQ-005 (CHG-20260608-01), REQ-048 (CHG-20260608-01), REQ-009 (CHG-20260608-01)
|
||||
|
||||
## Summary
|
||||
- dcy-014(당일 슬롯 생성 기준 시간을 KST(설정 가능)로 분리 — trading_date 타임존 분리 + 일자 경계 preallocation, issue #14)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-014 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**(스케줄러는 REQ-030 SnapshotScheduler 패턴 준용).
|
||||
- 핵심: `trading_date` 를 시스템 UTC 가 아니라 설정 타임존(기본 `Asia/Seoul`) 기준으로 계산하고, timestamp 는 UTC 유지. 데몬 시작 1회 UTC preallocation 을 `DailyPreallocationScheduler`(tick 루프 + TZ 일자당 1회 멱등 fire)로 대체해 재시작 없이 다음날 슬롯이 자동 생성된다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `trading_date` 의 설정 가능한 타임존 분리(기본 KST) + timestamp UTC 유지 (REQ-005)
|
||||
- TZ 일자 경계 기준 일일 멱등 preallocation 스케줄러 + daemon 배선 (REQ-005)
|
||||
- 타임존·트리거 시각 TOML 설정(`trading_timezone`/`daily_preallocation_time`) (REQ-048)
|
||||
- runtime-flows/data-model 문서 갱신 (REQ-009)
|
||||
- Out of Scope:
|
||||
- timestamp 저장 형식 변경(UTC 유지)
|
||||
- 슬롯 용량/total_slots 정책 변경
|
||||
- 거래일 캘린더(주말·공휴일 비거래일 제외) — 매 캘린더일 생성으로 확정(OQ-2), 캘린더 도입은 후속 분리
|
||||
- REQ-002(slot 도메인) 본문 변경 — `trading_date: date` 시그니처 유지, TZ 계산은 호출부(스케줄러)에서 수행(A-3)
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- Source discovery 단일(dcy-014). OQ-1(기본 00:00 TZ 자정·TOML 설정)·OQ-2(매 캘린더일 생성)·OQ-3(전부 change-req) Answered.
|
||||
- 스케줄러는 `SnapshotScheduler`(REQ-030)의 KST 일자당 1회 fire + tick 루프 패턴을 준용(A-1).
|
||||
- 멱등성(A-2): `preallocate_daily_slots` 가 date 멱등 → 일일 재실행·다중 인스턴스·중복 tick 안전(NFR-2).
|
||||
- 운영 설정은 기존 `--config` TOML(REQ-048)에 키 추가로 편입(신규 설정 표면 아님). from_env 는 KST/00:00 기본.
|
||||
@@ -0,0 +1,47 @@
|
||||
# BAT-018 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-018
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-005 (Installation, change-req): setup() UTC→TZ trading_date + `DailyPreallocationScheduler` 신규 + daemon 스레드 배선.
|
||||
- REQ-048 (Configuration, change-req): TOML `trading_timezone`/`daily_preallocation_time` 키 + from_config 전파.
|
||||
- REQ-009 (Documentation, change-req): runtime-flows/data-model 정합.
|
||||
|
||||
## Delivery Plan
|
||||
1. REQ-005: `application/preallocation_scheduler.py`(SnapshotScheduler 패턴, TZ date + 일자당 1회 멱등 fire + boot run_once).
|
||||
2. REQ-048: `config.py` DaemonInstanceConfig TOML 키 2종 + DaemonConfig 필드 + from_config 전파(+ from_env KST/00:00 기본).
|
||||
3. REQ-005 배선: `daemon.py` setup() → `preallocation_scheduler.run_once()`, `_preallocation_loop` 스레드 + workers 등록, ZoneInfo(tz).
|
||||
4. REQ-009: runtime-flows(데몬 lifecycle + 일일 preallocation Flow)/data-model(trading_date TZ vs UTC) + instance.example.toml.
|
||||
5. 신규 테스트(test_preallocation_scheduler, test_instance_config 확장) + 전체 회귀 + ruff + 인프라 게이트.
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- dcy-002/BAT-002 daemon 부트스트랩(REQ-005) — preallocation 지점.
|
||||
- dcy-005/BAT-008 SnapshotScheduler(REQ-030) — 스케줄러 패턴 준용.
|
||||
- dcy-009/BAT-013 DaemonInstanceConfig TOML(REQ-048) — 설정 키 추가.
|
||||
- REQ-002 preallocate_daily_slots — date 멱등(재사용, 본문 변경 없음).
|
||||
- External:
|
||||
- stdlib `zoneinfo`(py3.12) — IANA 타임존.
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: (a) trading_date TZ 분리는 daemon 부트스트랩·스케줄러·설정·도메인 호출부 경계에 걸쳐, "TZ 는 호출부에서, 도메인 시그니처는 date 유지"(A-3) 경계를 설계에서 못박지 않으면 도메인 침범/회귀 위험. (b) 일자 경계 fire/멱등/트리거 시각 게이팅 규칙(SnapshotScheduler 준용)을 설계에서 고정해야 중복 생성·누락(NFR-2/3)을 막는다. (c) 설정 표면(TOML 일원화 + from_env 기본)·timestamp UTC 유지(NFR-1) 경계 명시. → design.md 동일 턴 scaffold.
|
||||
|
||||
## Milestones
|
||||
1. M1 — REQ-005 DailyPreallocationScheduler, test_preallocation_scheduler green
|
||||
2. M2 — REQ-048 TOML 키 + from_config 전파, test_instance_config 확장 green
|
||||
3. M3 — REQ-005 daemon 배선(setup + 스레드)
|
||||
4. M4 — REQ-009 문서 정합 + instance.example.toml
|
||||
5. M5 — 전체 회귀 + ruff + 인프라 게이트
|
||||
|
||||
## Risks
|
||||
- R-1 (Low, OQ-1): 트리거 시점 운영 기대 불일치. 대응: 기본 00:00 + TOML 설정, SnapshotScheduler 패턴 재사용.
|
||||
- R-2 (Medium): TZ 전환 경계에서 기존 UTC 날짜 슬롯과 새 TZ trading_date 불일치. 대응: 멱등 생성으로 중복 방지 + 경계 케이스 테스트(KST/UTC 경계).
|
||||
- R-3 (Low, OQ-2 수용): 비거래일 idle 슬롯 누적. 대응: 매 캘린더일 생성 수용, idle 은 buy admit 시에만 활성화로 무해. 거래일 캘린더 후속.
|
||||
|
||||
## Notes
|
||||
- 1차 게이트: 전체 회귀 green(부트스트랩 변경 무회귀).
|
||||
- 경계 검증은 고정 clock 주입 단위로 결정론 확보(스레드 불필요).
|
||||
- 멱등 preallocation 은 PG 게이트 전이/마이그레이션 테스트 회귀로도 간접 검증.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# BAT-018 Verification
|
||||
|
||||
- Status: verified
|
||||
- Batch: BAT-018
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-005 (스케줄러 + 부트스트랩, change-req)
|
||||
| AC | Evidence |
|
||||
|----|----------|
|
||||
| trading_date = 설정 TZ 기준(UTC 아님) | tests/application/test_preallocation_scheduler.py::test_trading_date_uses_kst_not_utc_at_midnight_boundary |
|
||||
| 일자 경계 자동 생성(무재시작) + 멱등 | ::test_cross_day_boundary_auto_creates_next_day_idempotently |
|
||||
| 트리거 시각 게이팅 | ::test_trigger_time_gates_next_day_creation |
|
||||
| idle 슬롯 생성 | ::test_idle_slots_created_for_trading_date |
|
||||
| 부트스트랩/스레드 배선 회귀 | 전체 회귀(daemon import/setup) |
|
||||
|
||||
### REQ-048 (TOML 설정, change-req)
|
||||
| AC | Evidence |
|
||||
|----|----------|
|
||||
| TOML 기본값(KST/00:00) | tests/runtime/test_instance_config.py::test_trading_timezone_defaults_to_kst |
|
||||
| TOML 값 from_config 전파 | ::test_trading_timezone_from_toml_propagates |
|
||||
| 기존 TOML 키 회귀 | 기존 test_instance_config 전체 |
|
||||
|
||||
### REQ-009 (baseline 문서, change-req)
|
||||
| AC | Evidence |
|
||||
|----|----------|
|
||||
| runtime-flows/data-model 정합 | 문서 diff(BAT-018) + instance.example.toml |
|
||||
|
||||
## Evidence
|
||||
- Test:
|
||||
- 단위 전체 회귀: `302 passed / 32 skipped`(신규 스케줄러 6 + config 2 포함; `::test_sandbox_token_and_submit` deselect).
|
||||
- 인프라 게이트(PG trex_core_test + RabbitMQ): `334 passed`.
|
||||
- `ruff check trex_core tests` → All checks passed.
|
||||
- Logs: `302 passed, 32 skipped, 1 deselected` / `334 passed, 1 deselected ~16.1s`.
|
||||
|
||||
## 성공 기준(dcy-014) 대응
|
||||
- S-1(설정 TZ "오늘" = DB trading_date, UTC 자정 경계 포함): test_trading_date_uses_kst_not_utc_at_midnight_boundary(15:30Z → KST 6/9).
|
||||
- S-2(timestamp UTC 유지): test_timestamps_stay_utc(updated_at utcoffset 0).
|
||||
- S-3(무재시작 일자 경계 자동 생성 + 중복 0): test_cross_day_boundary_auto_creates_next_day_idempotently.
|
||||
- S-4(TOML 타임존 설정 반영): test_configurable_timezone_changes_trading_date(UTC TZ → 6/8) + test_trading_timezone_from_toml_propagates.
|
||||
|
||||
## Carried-Over / 비차단
|
||||
- 기본 타임존 `Asia/Seoul` 확정·TOML 키 명명(DATA): 구현에서 `trading_timezone`/`daily_preallocation_time` 로 확정. 운영 배포 안내는 instance.example.toml 에 문서화.
|
||||
- 실 다중 인스턴스 동시 일일 preallocation 무중복(NFR-2): 멱등 preallocate 로 보장, 실 환경 동시 기동 e2e 는 운영 검증 비차단.
|
||||
- 거래일 캘린더(주말/공휴일): OQ-2 결정으로 매 캘린더일 생성, 후속 분리.
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues:
|
||||
- 없음. 부트스트랩 TZ 전환·일일 preallocation·TOML 설정 모두 단위 green(334 passed 회귀). LIVE sandbox 토큰 deselect 는 `.env` 환경 의존(본 batch 무관).
|
||||
@@ -25,3 +25,4 @@
|
||||
| BAT-015 | standard | release-candidate | REQ-007 (CHG-20260608-01), REQ-018 (CHG-20260608-01), REQ-021 (CHG-20260608-01), REQ-055 | dcy-011 | [bat-015](./bat-015_20260608_buy-intent-portfolio-id-contract/) |
|
||||
| BAT-016 | standard | release-candidate | REQ-002 (CHG-20260608-01), REQ-003 (CHG-20260608-01), REQ-006 (CHG-20260608-01), REQ-007 (CHG-20260608-02), REQ-013 (CHG-20260608-01), REQ-015 (CHG-20260608-01), REQ-036 (CHG-20260608-01) | dcy-012 | [bat-016](./bat-016_20260608_sell-pricing-limit-price-rename/) |
|
||||
| BAT-017 | standard | release-candidate | REQ-012 (CHG-20260608-01), REQ-013 (CHG-20260608-02), REQ-018 (CHG-20260608-02), REQ-031 (CHG-20260608-01), REQ-014 (CHG-20260608-01), REQ-015 (CHG-20260608-02) | dcy-013 | [bat-017](./bat-017_20260608_external-cancel-detection-stale-watchdog/) |
|
||||
| BAT-018 | standard | release-candidate | REQ-005 (CHG-20260608-01), REQ-048 (CHG-20260608-01), REQ-009 (CHG-20260608-01) | dcy-014 | [bat-018](./bat-018_20260608_trading-date-timezone-daily-preallocation/) |
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-08 (BAT-016)
|
||||
- Last Updated (KST): 2026-06-08 (BAT-018)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery)
|
||||
|
||||
## Project Summary
|
||||
@@ -41,7 +41,7 @@
|
||||
### Key Fields
|
||||
|
||||
- slot_id :: UUID :: 슬롯 식별자(PK)
|
||||
- trading_date :: date :: 거래일
|
||||
- trading_date :: date :: 거래일 — 설정 타임존(`trading_timezone`, 기본 `Asia/Seoul`) 기준 날짜로 계산(dcy-014/REQ-005). 시스템 UTC 날짜가 아니라 거래 캘린더 일자와 일치. 일일 preallocation 이 TZ 일자 경계마다 멱등 생성.
|
||||
- status :: SlotState :: 6 상태(idle/pending_buy/holding/pending_sell/closed/error) 중 현재 상태
|
||||
- symbol :: str? :: 점유 종목
|
||||
- qty / avg_price :: numeric? :: 수량 / 평단
|
||||
@@ -51,7 +51,7 @@
|
||||
- strategy_id / sell_strategy_id :: str? :: 진입/청산 전략 식별
|
||||
- portfolio_id :: str? :: 포트폴리오 식별
|
||||
- forced_close_at :: datetime? :: 강제 청산 마감선
|
||||
- created_at / updated_at :: datetime? :: 생성/갱신 시각
|
||||
- created_at / updated_at :: datetime? :: 생성/갱신 시각 — **UTC 저장 유지**(dcy-014/REQ-005 NFR-1: `trading_date` 만 TZ 기준, timestamp 는 UTC)
|
||||
|
||||
### Relationships
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-014_20260608_trading-date-configurable-timezone-kst
|
||||
- 이슈명: 당일 슬롯 생성 기준 시간을 KST(설정 가능)로 분리 — trading_date 타임존 분리 + 일자 경계 preallocation
|
||||
- 작성 시각(KST): 2026-06-08 13:58 KST
|
||||
- 마지막 갱신 시각(KST): 2026-06-08 17:20 KST
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: REQ-005/048/009 (전부 change-req, 신규 REQ 없음 — OQ-3; 스케줄러는 REQ-030 SnapshotScheduler 패턴 준용). 각 CHG-20260608-01. delivery: BAT-018
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 점수 파일: 미생성
|
||||
- 관련 TODO/메모: GitHub issue #14 (fourblocks2020/trex-core)
|
||||
- 해석:
|
||||
- daemon 이 시작 시 `datetime.now(tz=timezone.utc).date()` 기준으로 슬롯을 1회 preallocate 해, `trading_date` 가 UTC 날짜로 생성되어 KST 기준 "오늘 슬롯"과 어긋나고, 재시작 없이는 다음날 슬롯이 자동 생성되지 않는 문제를 정식 SDLC 주기로 착수한다.
|
||||
- 이번 Discovery에서는 `trading_date` 계산 기준을 설정 가능한 타임존(기본 `Asia/Seoul`)으로 분리하고, timestamp 저장은 UTC 유지하며, 일자 경계에 맞춰 다음날 슬롯이 자동 생성되도록 일일 preallocation 을 보강하는 범위를 판정한다. 이는 daemon 런타임/스케줄러/설정 baseline 변경이므로, 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-005/048/009, 스케줄러는 REQ-030 패턴 준용) `change-req` 로 처리하고 신규 REQ 는 원칙적으로 생성하지 않는다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 기존 Discovery(dcy-001~dcy-013) 중 `trading_date` 타임존 기준이나 일자 경계 일일 preallocation 을 다룬 문서는 없다. dcy-002(슬롯 코어 데몬화)는 부트스트랩 시 preallocation 을 도입했으나 타임존·일자 경계 자동화를 정의하지 않았다.
|
||||
- 변경이 이미 Implemented·released 된 daemon 부트스트랩(`setup`)·슬롯 도메인(`trading_date`)·설정 계약의 의미를 바꾸고, 일일 스케줄링이라는 런타임 동작을 추가하므로, 기존 Discovery 미세 조정이 아니라 독립 주기로 추적하는 것이 명확하다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- `trex_core/runtime/daemon.py` 의 `setup()`(daemon.py:291~)은 `self.slot_service.preallocate_daily_slots(datetime.now(tz=timezone.utc).date(), self.config.total_slots)`(daemon.py:293~294)로, **UTC 날짜** 기준 슬롯을 데몬 시작 시 1회만 생성한다.
|
||||
- `preallocate_daily_slots`(`trex_core/domain/slot/service.py:42~53`)는 동일 `trading_date` 에 대해 멱등(2회차 이후 0 반환)이다. 즉 같은 날짜에 대한 재실행은 안전하나, **날짜가 바뀌어도 자동으로 다음날 슬롯을 만들지 않는다**(재시작 의존).
|
||||
- timestamp 저장은 UTC 다 — 슬롯 `created_at`/`updated_at`·전이 로그가 UTC. 그러나 `trading_date` 도 UTC 날짜라 KST 자정~오전 구간에 KST 캘린더 일자와 어긋난다.
|
||||
- 타임존을 설정으로 분리하는 경로는 아직 없다 — `trex_core/runtime/config.py` 에 `trading_date` 용 타임존 설정이 없다. 다만 설정 인프라는 이미 두 표면을 갖춘다: env(`.env`, `TREX_*`, 대부분 설정)와 TOML(`DaemonInstanceConfig.from_toml`, `--config <path>.toml`, REQ-048 per-instance). 즉 새 운영 설정은 기존 TOML 에 키를 추가하면 된다.
|
||||
- 유사 패턴은 이미 존재한다 — `trex_core/application/snapshot_scheduler.py` 의 `SnapshotScheduler` 는 `_KST` 로 `now_kst.date()` 를 구해 **KST 일자당 1회** fire 하고(`_last_fired_date.date() == now_kst.date()`), tick 루프(`tick_sec`)로 동작한다. 일일 KST 경계 작업의 직접적인 선례다.
|
||||
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신이다. daemon 부트스트랩/스케줄러는 runtime flow 에 속하므로 이번 변경은 `docs/runtime-flows.md` 갱신을 동반한다.
|
||||
- 기대 상태
|
||||
- `trading_date` 가 설정 가능한 타임존(기본 `Asia/Seoul`) 기준으로 계산되어, KST "오늘 슬롯" 과 DB `trading_date` 가 일치한다.
|
||||
- timestamp(`created_at`/`updated_at`/전이 ts)는 UTC 저장을 유지한다.
|
||||
- daemon 재시작 없이도 TZ 일자 경계에서 다음날 슬롯이 누락 없이 자동 생성된다(멱등 일일 preallocation).
|
||||
- 운영자가 타임존을 TOML 설정으로 명시 지정할 수 있다.
|
||||
- 이해관계자
|
||||
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
|
||||
- 리뷰어/승인자 — 타임존·스케줄링·설정 계약 변경 승인
|
||||
- 운영 담당자 — 일자 경계 슬롯 생성·타임존 설정 운영
|
||||
- 전략 모듈/테스트 — KST 기준 "오늘 슬롯" 을 일관되게 관측
|
||||
- 제약사항
|
||||
- 이미 released 된 daemon 부트스트랩·슬롯 도메인·설정 변경이므로 ad-hoc 수정이 아니라 정식 SDLC(change-req 또는 신규 REQ) 경유가 필요하다.
|
||||
- timestamp 는 UTC 를 유지하고 `trading_date` 만 TZ 기준으로 분리해, 저장 일관성과 일자 해석을 명확히 구분해야 한다.
|
||||
- 다중 인스턴스(dcy-009) 운영 시 일일 preallocation 이 중복 생성되지 않도록 멱등성에 의존한다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
|
||||
|
||||
- 운영자가 KST 오전에 "오늘 슬롯" 을 조회한다 → DB `trading_date` 가 KST 당일과 일치해 직관적으로 해석된다(UTC 자정 경계 혼동 제거).
|
||||
- daemon 이 며칠간 재시작 없이 떠 있다 → TZ 일자 경계마다 다음날 슬롯이 자동 생성되어 매일 거래 준비가 누락되지 않는다.
|
||||
- 운영자가 다른 거래소/타임존을 쓰는 환경에서 TOML 로 타임존을 명시 설정한다 → 시스템 시간과 무관하게 일관된 `trading_date` 가 계산된다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- `trading_date` 계산 기준을 시스템 UTC 가 아니라 설정 가능한 타임존(기본 `Asia/Seoul`)으로 분리한다. timestamp 저장은 UTC 유지.
|
||||
- daemon 시작 1회 preallocation 을 TZ 일자 경계 기준 일일 preallocation 으로 보강한다 — `SnapshotScheduler` 패턴(tick 루프 + TZ 일자당 1회 fire)을 따르고 기존 멱등성을 활용한다.
|
||||
- 본 기능의 운영 설정(타임존 + 일일 트리거 시각)을 기존 TOML 설정(REQ-048 `DaemonInstanceConfig`, `--config <path>.toml`)에 추가하고, `docs/runtime-flows.md`·`docs/data-model.md` 에 trading_date 타임존·일일 생성 흐름을 반영한다. (이 프로젝트는 이미 env[.env, 대부분]와 TOML[per-instance, REQ-048]을 병용하므로, 본 설정은 TOML 측으로 자연스럽게 편입된다.)
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- `trex_core/runtime/config.py` — trading_date 타임존 + 일일 트리거 시각을 기존 TOML 설정(`DaemonInstanceConfig.from_toml`, REQ-048)에 추가(기본 `Asia/Seoul`/00:00). 신규 설정 표면이 아니라 기존 `--config <path>.toml` 에 키 추가
|
||||
- `trex_core/runtime/daemon.py` — `setup()` 의 UTC 날짜 preallocation 을 TZ 기준으로 변경 + 일일 preallocation 루프/스케줄러 배선
|
||||
- `trex_core/application/` — 일일 preallocation 스케줄러(또는 `SnapshotScheduler` 유사 컴포넌트). 기존 멱등 `preallocate_daily_slots` 재사용
|
||||
- `trex_core/domain/slot/service.py` — `trading_date` 를 TZ 기준 date 로 받는 호출부 정합(도메인 시그니처는 `date` 유지)
|
||||
- 문서 영향:
|
||||
- `docs/runtime-flows.md` — daemon 부트스트랩/일일 preallocation·trading_date 타임존 흐름 반영
|
||||
- `docs/data-model.md` — `trading_date`(TZ 기준 거래일) vs timestamp(UTC) 구분 명시
|
||||
- 운영 영향:
|
||||
- 타임존·트리거 시각 TOML 설정 추가 → 배포/운영 문서 안내 필요
|
||||
- 일자 경계 자동 생성으로 재시작 의존 제거
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: `trading_date` 는 설정 가능한 타임존(기본 `Asia/Seoul`) 기준 날짜로 계산한다. 시스템 UTC 날짜에 의존하지 않는다.
|
||||
- FR-2: timestamp(`created_at`/`updated_at`/전이 ts)는 UTC 저장을 유지한다(거래일과 시각 저장 분리).
|
||||
- FR-3: daemon 재시작 없이 TZ 일자 경계에서 다음날 슬롯을 자동 생성한다 — tick 루프로 TZ 일자당 1회 멱등 preallocation(`SnapshotScheduler` 패턴 준용). 생성 트리거 시각은 기본 **00:00(TZ 자정)** 이며, TOML 설정으로 변경 가능하다(OQ-1).
|
||||
- FR-4: 운영자가 타임존을 TOML 설정으로 명시 지정할 수 있다(기본 `Asia/Seoul`). 본 기능의 운영 설정(타임존 + 트리거 시각)은 TOML 로 일원화한다.
|
||||
- FR-5: `docs/runtime-flows.md` 와 `docs/data-model.md` 에 trading_date 타임존·일일 preallocation 흐름을 반영한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: 저장 일관성 — 모든 timestamp 는 UTC, `trading_date` 만 TZ 기준으로 분리되어 해석 혼동이 없어야 한다.
|
||||
- NFR-2: 멱등성 — 일일 preallocation 이 같은 `trading_date` 에 중복 슬롯을 만들지 않아야 한다(다중 인스턴스·중복 tick 안전).
|
||||
- NFR-3: 자동화 안정성 — 날짜 경계에서 다음날 슬롯 생성이 누락되지 않고, daemon 재시작 없이 동작해야 한다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- `trading_date` 의 설정 가능한 타임존 분리(기본 KST) + timestamp UTC 유지
|
||||
- TZ 일자 경계 기준 일일 멱등 preallocation
|
||||
- 타임존·트리거 시각 TOML 설정 + runtime-flows/data-model 문서 갱신
|
||||
- Out of Scope
|
||||
- timestamp 저장 형식 변경(UTC 유지)
|
||||
- 슬롯 용량/total_slots 정책 변경
|
||||
- (OQ-2 결정) 거래일 캘린더(주말·공휴일 비거래일 제외) — 매 캘린더일 생성으로 확정, 캘린더 도입은 후속 분리
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (Low, OQ-1 결정으로 완화): 일일 preallocation 트리거 시점이 운영 기대와 어긋날 수 있다.
|
||||
- 완화 방안: 기본 00:00(TZ 자정) + TOML 설정(OQ-1)으로 운영자가 조정 가능하게 하고, `SnapshotScheduler` 의 KST 일자당 1회 fire 패턴을 재사용해 검증 가능하게 한다.
|
||||
- R-2 (Medium): 타임존 분리 전환 시점에 기존 UTC 날짜로 만들어진 슬롯과 새 TZ `trading_date` 가 경계 구간에서 불일치할 수 있다.
|
||||
- 완화 방안: 전환 시 당일 경계 동작을 문서화하고, 멱등성으로 중복을 막되 경계 케이스를 테스트로 확인한다.
|
||||
- R-3 (Low, OQ-2 결정으로 수용): 비거래일(주말/공휴일)에도 슬롯이 생성되어 idle 슬롯이 쌓일 수 있다.
|
||||
- 완화 방안: OQ-2 결정에 따라 매 캘린더일 생성을 수용한다 — idle 슬롯은 멱등 생성이고 buy intent admit 시에만 활성화되어 무해하다. 거래일 캘린더(외부 데이터)는 후속 분리.
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: `SnapshotScheduler`(snapshot_scheduler.py) 가 이미 `_KST` 기준 일자당 1회 fire + tick 루프를 구현하므로, 일일 preallocation 도 동일 패턴/유사 컴포넌트로 안전하게 구현할 수 있다.
|
||||
- A-2: `preallocate_daily_slots` 가 `trading_date` 멱등이므로, 일일 재실행·다중 인스턴스에서도 중복 생성이 발생하지 않는다.
|
||||
- A-3: 도메인의 `trading_date: date` 시그니처는 유지하고, 타임존 해석은 호출부(daemon/스케줄러)에서 수행하면 도메인 변경을 최소화할 수 있다.
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: 일일 preallocation 트리거 시점 — TZ 자정(00:00) 경계에 생성하는가, 장 시작 전 특정 시각(설정)에 생성하는가?
|
||||
- 상태: Answered (2026-06-08, 사용자 결정 — 기본 00:00(TZ 일자 경계), TOML 로 설정 가능)
|
||||
- 결정: 일일 preallocation 은 설정 타임존의 **자정(00:00)** 일자 경계에 다음날 슬롯을 생성하는 것을 기본값으로 하고, 트리거 시각은 **TOML 설정**으로 운영자가 변경할 수 있다(`SnapshotScheduler` 의 KST 일자당 1회 fire 패턴 준용). 본 기능의 운영 설정(타임존 + 트리거 시각)은 TOML 로 일원화한다(FR-4 함께 결정). 이 프로젝트는 이미 env 와 TOML(REQ-048 per-instance)을 병용하므로, 본 설정은 기존 TOML(`--config`)에 키를 추가하는 형태로 편입된다(신규 설정 표면 아님).
|
||||
- 반영 위치: FR-3, FR-4, R-1, 4. 변경 항목/영향 범위
|
||||
- OQ-2: 비거래일(주말/공휴일) 처리 — 매 캘린더일 슬롯을 생성하는가, 거래일만 생성하는가(거래일 캘린더 도입)?
|
||||
- 상태: Answered (2026-06-08, 사용자 결정 — A안: 매 캘린더일 생성)
|
||||
- 결정: 거래일 캘린더를 도입하지 않고 매 캘린더일에 슬롯을 생성한다. 휴장일 idle 슬롯은 무해하다 — 멱등 생성이고, 슬롯은 buy intent 가 admit 될 때만 활성화되므로 휴장일엔 비어 있다가 사라지며 safety-sell·워치독 등은 holding/pending 슬롯만 대상으로 한다. KRX 공휴일 캘린더(외부 데이터·매년 유지보수)는 idle 슬롯 회피 이득 대비 비용이 커 후속으로 분리한다(필요 시: 주말 스킵 또는 거래일 캘린더).
|
||||
- 반영 위치: 범위 경계(Out of Scope), R-3
|
||||
- OQ-3: 거버넌스 경로 — 기존 REQ(데몬 부트스트랩·슬롯 도메인·설정)의 change-req 로 처리할지, 일일 스케줄러를 신규 REQ 로 추적할지?
|
||||
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
|
||||
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-005(슬롯 코어 데몬 런타임 부트스트랩: `setup()` 의 UTC→TZ trading_date 계산 + 일일 preallocation 루프 배선), REQ-048(per-instance TOML 설정 로더: trading_date 타임존 + 일일 트리거 시각 키 추가), REQ-009(데몬/API/영속화 baseline 문서 정합: runtime-flows/data-model 갱신). 일일 preallocation 스케줄러는 REQ-030(일일 스냅샷 스케줄러)의 `SnapshotScheduler` 패턴을 준용한다. `preallocate_daily_slots` 가 이미 date 멱등이고 호출부(daemon)에서 TZ 계산을 수행하므로 REQ-002(slot 도메인) 본문 변경은 원칙적으로 불필요(정제에서 확인). 신규 REQ 는 원칙적으로 생성하지 않으며, 일일 preallocation 을 별도 컴포넌트로 분리해야 한다고 판단되면 REQ 정제에서 소형 신규 REQ 를 둘 수 있다(조건부).
|
||||
- 반영 위치: 1. 거버넌스 해석, R-1 완화 방안, 8. 확인 체크
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: 설정 타임존(기본 KST) 기준 "오늘 날짜" 와 생성된 슬롯의 DB `trading_date` 가 일치한다(UTC 자정 경계 포함).
|
||||
- S-2: 모든 timestamp(`created_at`/`updated_at`/전이 ts)가 UTC 로 저장됨이 유지된다.
|
||||
- S-3: daemon 재시작 없이 TZ 일자 경계를 넘기면 다음날 `trading_date` 슬롯이 자동 생성되고, 중복 생성은 0건이다.
|
||||
- S-4: 운영자가 TOML 로 타임존을 설정하면 그 타임존 기준으로 `trading_date` 가 계산된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: KST 자정 전후 시점을 고정 clock 으로 주입한 단위 테스트에서 `trading_date` 일치 확인 / 데이터 출처: 슬롯 row `trading_date`, 주입 clock
|
||||
- S-2 측정 방식: 슬롯 생성/전이 후 timestamp 컬럼 UTC 확인 / 데이터 출처: `created_at`/`updated_at`/transition_log
|
||||
- S-3 측정 방식: 일자 경계를 넘기는 clock 주입 테스트에서 다음날 슬롯 1회 생성·재호출 0건(멱등) 확인 / 데이터 출처: preallocation 스케줄러 테스트, 슬롯 count
|
||||
- S-4 측정 방식: 타임존 TOML 설정 케이스(KST/타 TZ) 테스트에서 `trading_date` 계산 확인 / 데이터 출처: config 로드, 슬롯 `trading_date`
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: 트리거 시점(OQ-1)·거래일 캘린더(OQ-2)·거버넌스(OQ-3) 모두 확정됨
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1~S-4 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: 전환 경계(R-2)의 change-req 대상(REQ-005/048/009, 스케줄러 REQ-030 패턴)이 OQ-3 으로 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ-1~OQ-3 모두 `Answered`, `Open` 0개
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: Confirmed By = Justin Ko (사람 승인자 지정 완료)
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-014_20260608_trading-date-configurable-timezone-kst.md
|
||||
- 참조 문서:
|
||||
- GitHub issue #14 (fourblocks2020/trex-core) — 당일 슬롯 생성 기준 시간 KST 분리
|
||||
- docs/discovery/dcy-002_20260603_slot-core-daemon-http-api.md — 데몬 부트스트랩·preallocation 도입 근거
|
||||
- trex_core/runtime/daemon.py:291-294 — UTC 날짜 1회 preallocation(현행 갭)
|
||||
- trex_core/application/snapshot_scheduler.py — KST 일자당 1회 fire 스케줄러 선례(A-1)
|
||||
- docs/runtime-flows.md, docs/data-model.md — 런타임/데이터 baseline 갱신 대상
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- (없음 — OQ-1/OQ-2 확정으로 해소)
|
||||
|
||||
> 결정 완료(2026-06-08): OQ-1 일일 preallocation 트리거 = 기본 00:00(TZ 자정 일자 경계), TOML 설정 가능(타임존도 TOML 일원화) / OQ-2 비거래일 = 매 캘린더일 생성(거래일 캘린더 후속 분리). 결정 항목에서 제외됨.
|
||||
|
||||
## CONFIRM
|
||||
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-005/048/009 change-req, 스케줄러는 REQ-030 패턴 준용, 신규 REQ 원칙적 없음).
|
||||
|
||||
## DATA
|
||||
- (비차단, REQ/delivery 단계) 기본 타임존 `Asia/Seoul` 확정과 TOML 키/스키마 명명 규약. 운영 정책·키 명명은 REQ 정제/설계에서 확정하며, 기본값 KST 가 본 Discovery 의 FR/성공 기준을 충분히 정의하므로 confirm 을 차단하지 않는다.
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: confirm-discovery 완료. 오픈 질문 OQ-1~OQ-3 모두 Answered (트리거=기본 00:00 TZ 자정·TOML 설정[타임존 포함] / 비거래일=매 캘린더일 생성 / 거버넌스=REQ-005/048/009 change-req, 스케줄러 REQ-030 패턴, 신규 REQ 원칙적 없음). C 유형 플레이스홀더 0개, 저장소 사실관계 일치(TOML 설정 인프라 정정 반영). DATA(기본 TZ·TOML 키 명명)는 REQ/delivery 단계 비차단. REQ 초안 작성 인계 가능.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin Ko
|
||||
- Confirmed At (KST): 2026-06-08 14:50 KST
|
||||
@@ -18,4 +18,5 @@
|
||||
| dcy-010_20260607_safety-sell-persistent-rejection-backoff | 20260607 | safety-sell 영속 거부 재시도 백오프 — 장 휴장·잔고 부족·규제 등 지속 거부 상황에서 KIS 주문 spam 방지를 위한 슬롯별 재시도 backoff/cooldown 정책 | [Discovery](./dcy-010_20260607_safety-sell-persistent-rejection-backoff.md) | confirmed |
|
||||
| dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve | 20260608 | buy intent — portfolio_id를 필수 키로, symbol은 universe(SnapshotStore)에서 resolve (+ issue #11 portfolio_id 누락 체결 ledger silent skip 가시화 흡수) | [Discovery](./dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve.md) | confirmed |
|
||||
| dcy-012_20260608_sell-order-pricing-entry-price-bug | 20260608 | 매도 주문이 현재가가 아닌 entry_price(매수가) 지정가로 나가는 버그 — sell dispatch 가격 정책 부재 (issue #10) | [Discovery](./dcy-012_20260608_sell-order-pricing-entry-price-bug.md) | confirmed |
|
||||
| dcy-013_20260608_kis-external-cancel-pending-sell-stuck | 20260608 | KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구히 갇힘 — cncl_yn 미파싱 (issue #9) | [Discovery](./dcy-013_20260608_kis-external-cancel-pending-sell-stuck.md) | confirmed |
|
||||
| dcy-013_20260608_kis-external-cancel-pending-sell-stuck | 20260608 | KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구히 갇힘 — cncl_yn 미파싱 (issue #9) | [Discovery](./dcy-013_20260608_kis-external-cancel-pending-sell-stuck.md) | confirmed |
|
||||
| dcy-014_20260608_trading-date-configurable-timezone-kst | 20260608 | 당일 슬롯 생성 기준 시간을 KST(설정 가능)로 분리 — trading_date 타임존 분리 + 일자 경계 preallocation (issue #14) | [Discovery](./dcy-014_20260608_trading-date-configurable-timezone-kst.md) | confirmed |
|
||||
+14
-4
@@ -4,7 +4,7 @@
|
||||
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
|
||||
- Status: draft
|
||||
- Owner: project maintainers
|
||||
- Last Updated (KST): 2026-06-08 (BAT-017)
|
||||
- Last Updated (KST): 2026-06-08 (BAT-018)
|
||||
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
|
||||
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
|
||||
|
||||
@@ -31,15 +31,16 @@
|
||||
|
||||
## Flow: `데몬 lifecycle` (BAT-002, BAT-007)
|
||||
|
||||
1. `trex-core-daemon --config <path>.toml` 기동 → `DaemonConfig.from_config(toml)` = `from_env()`(TREX_POSTGRESQL_DSN/`TREX_RABBITMQ_URL` 등) + TOML(`instance_key`, `portfolio_hub_token`) 병합. `--config` 필수(없으면 오류 종료, BAT-013/REQ-048). `instance_key` 로 큐명·라우팅 분리.
|
||||
2. setup: `apply_migrations(dsn)`(멱등) → 거래일 슬롯 `preallocate_daily_slots`(멱등) → `execution.reconcile()`(REQ-014) → 이벤트 publisher 의 `declare_topology()`.
|
||||
3. start: 7개 백그라운드 스레드 동시 기동
|
||||
1. `trex-core-daemon --config <path>.toml` 기동 → `DaemonConfig.from_config(toml)` = `from_env()`(TREX_POSTGRESQL_DSN/`TREX_RABBITMQ_URL` 등) + TOML(`instance_key`, `portfolio_hub_token`, `trading_timezone`, `daily_preallocation_time`) 병합. `--config` 필수(없으면 오류 종료, BAT-013/REQ-048). `instance_key` 로 큐명·라우팅 분리.
|
||||
2. setup: `apply_migrations(dsn)`(멱등) → `preallocation_scheduler.run_once()`(거래일 TZ 기준 당일 슬롯 `preallocate_daily_slots`, 멱등 — dcy-014/REQ-005) → `execution.reconcile()`(REQ-014) → 이벤트 publisher 의 `declare_topology()`.
|
||||
3. start: 8개 백그라운드 스레드 동시 기동
|
||||
- `trex-execution` :: 브로커 dispatch/poll 루프 (REQ-013/014)
|
||||
- `trex-ledger` :: ledger outbox 드레인 (REQ-019)
|
||||
- `trex-intent-rpc` :: `trex.intent.req` 소비자 (prefetch=1, FIFO, dedup via intent_log; `update_thresholds` 액션 포함 REQ-040)
|
||||
- `trex-snapshot-rpc` :: `trex.snapshot.req` 소비자 (응답 holding 슬롯마다 워치독 `last_seen_at` 갱신 REQ-034)
|
||||
- `trex-control-rpc` :: `trex.control.req` 소비자 — pause/resume/list + kill_switch/resume_kill_switch (REQ-032/043)
|
||||
- `trex-snapshot-scheduler` :: 일일 portfolio-hub 스냅샷 refresh (REQ-030)
|
||||
- `trex-preallocation` :: 거래일 TZ 일자 경계 일일 슬롯 preallocation (dcy-014/REQ-005, 아래 흐름)
|
||||
- `trex-safety-sell` :: 자율 안전 매도 트리거 루프 (forced_close / stop-loss·take-profit / 장 마감, REQ-037/038/039)
|
||||
4. shutdown: `SIGINT`/`SIGTERM` → `stop.set()` → 모든 스레드 join + 모든 transport `close()`. HTTP 표면 0, 별도 포트 바인딩 없음.
|
||||
|
||||
@@ -79,6 +80,15 @@
|
||||
3. fire 단계: `PortfolioHubReadPort.whoami()` 로 daemon_id 도출(1회 캐시) → `get_portfolios(daemon_id)` 로 universe 페치 → `SnapshotStore.swap(items)` atomic 교체 → `reconcile_positions(store, broker)` 정합 검증.
|
||||
4. 실패 분기: `PortfolioHubUnavailableError` → `degraded = portfolio_hub_unavailable`; 응답 파싱 실패 → `snapshot_fetch_failed`. 둘 다 다음 RPC 응답 envelope 에 즉시 노출(REQ-028 AC-2).
|
||||
|
||||
## Flow: `일일 슬롯 preallocation` (BAT-018 / REQ-005, dcy-014)
|
||||
|
||||
`trex-preallocation` 스레드가 설정 타임존(`trading_timezone`, 기본 `Asia/Seoul`) 기준 `trading_date` 로 매 TZ 일자 1회 멱등 슬롯 생성을 수행한다. `trading_date` 만 TZ 기준이고 모든 timestamp(`created_at`/`updated_at`/전이 ts)는 UTC 저장을 유지한다(NFR-1). `SnapshotScheduler` 패턴 준용.
|
||||
|
||||
1. boot: `DailyPreallocationScheduler.run_once()` 1회 → 현재 TZ 일자의 `preallocate_daily_slots(tz_today, total_slots)`(멱등) → 기동 즉시 당일 슬롯 확보. setup 단계에서 호출(데몬 lifecycle 2).
|
||||
2. tick(60s): 현재 TZ 시각이 `daily_preallocation_time`(기본 00:00, TOML 설정) 이상이고 그날 아직 안 돌았으면(`_last_fired_date != tz_today`) fire → 다음날 슬롯 자동 생성(재시작 불필요, NFR-3).
|
||||
3. 멱등성: `preallocate_daily_slots` 가 동일 `trading_date` 2회차 이후 0 반환 → 중복 tick·다중 인스턴스에서도 중복 생성 0(NFR-2).
|
||||
4. 비거래일: 거래일 캘린더 없이 매 캘린더일 생성(dcy-014 OQ-2). idle 슬롯은 buy admit 시에만 활성화되어 휴장일엔 무해.
|
||||
|
||||
## Flow: `포지션 정합 검증` (BAT-008 / REQ-031)
|
||||
|
||||
`reconcile_positions` 는 portfolio-hub 보유 수량을 symbol 별로 합산해 broker `get_balance()` 결과와 비교한다.
|
||||
|
||||
@@ -146,3 +146,43 @@
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current (Implemented)
|
||||
- Follow-up Action: draft-release 로 BAT-013 배포 편성. token .env 제거는 배포 rollout.
|
||||
|
||||
### CHG-20260608-01
|
||||
|
||||
- Date: 2026-06-08
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- (change-req, dcy-014) per-instance TOML 설정에 `trading_timezone`(기본 `Asia/Seoul`)·`daily_preallocation_time`(기본 `00:00`) 키 추가. `DaemonInstanceConfig.from_toml` 파싱 + `from_config` → `DaemonConfig` 전파. 본 기능 운영 설정을 TOML 로 일원화(dcy-014 FR-4). BAT-018 재구현·재검증 완료 → Implemented 유지.
|
||||
|
||||
#### Intent
|
||||
|
||||
- 거래일 타임존·일일 트리거 시각을 신규 설정 표면 없이 기존 `--config` TOML 에 편입(운영자 명시 지정 가능).
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- Added:
|
||||
- TOML optional 키 `trading_timezone`/`daily_preallocation_time`(부재 시 KST/00:00 기본). from_config 가 DaemonConfig 로 전파.
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- Modules: `trex_core/runtime/config.py`(DaemonInstanceConfig/DaemonConfig/from_config)
|
||||
- Docs: `config/instance.example.toml`
|
||||
- Tests: tests/runtime/test_instance_config.py(기본값/전파)
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: BAT-018
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: yes (BAT-018 에서 완료)
|
||||
- Existing Implementation Invalidated: no (optional 키 추가, 기존 TOML 호환)
|
||||
- Status Recommendation: Implemented (change-req → BAT-018 재검증)
|
||||
- Follow-up Action: confirm-req-implemented(BAT-018).
|
||||
|
||||
#### Validation Plan
|
||||
|
||||
- BAT-018: from_toml 기본값(KST/00:00) + TOML 값 from_config 전파 단위.
|
||||
|
||||
@@ -149,3 +149,41 @@
|
||||
|
||||
#### Validation Plan
|
||||
- BAT-002 verification evidence 재참조로 충분.
|
||||
|
||||
### CHG-20260608-01
|
||||
|
||||
- Date: 2026-06-08
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- (change-req, dcy-014) baseline 문서를 trading_date 타임존 분리·일일 preallocation 흐름에 맞춰 정합: `docs/runtime-flows.md`(데몬 lifecycle TOML 키·8 스레드·`일일 슬롯 preallocation` Flow 신규), `docs/data-model.md`(trading_date=설정 TZ 기준 / timestamp=UTC 구분). BAT-018 정합 완료 → Implemented 유지.
|
||||
|
||||
#### Intent
|
||||
|
||||
- 코드(preallocation_scheduler/daemon/config) 변경과 baseline 문서를 일치(S-1~S-4)시켜 운영자 참조 정합 유지.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- Updated:
|
||||
- runtime-flows 데몬 lifecycle/일일 preallocation, data-model trading_date TZ vs UTC timestamp 기술 정합.
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- Docs: docs/runtime-flows.md, docs/data-model.md
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: BAT-018
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: no (문서 정합)
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: Implemented (change-req → BAT-018 정합)
|
||||
- Follow-up Action: confirm-req-implemented(BAT-018).
|
||||
|
||||
#### Validation Plan
|
||||
|
||||
- BAT-018: 코드-문서 diff 대조.
|
||||
|
||||
@@ -151,3 +151,42 @@
|
||||
|
||||
#### Validation Plan
|
||||
- BAT-002 verification evidence 재참조로 충분.
|
||||
|
||||
### CHG-20260608-01
|
||||
|
||||
- Date: 2026-06-08
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
|
||||
- (change-req, dcy-014/issue #14) 데몬 부트스트랩의 슬롯 preallocation 을 시스템 UTC 날짜 1회 생성에서 **설정 타임존(기본 `Asia/Seoul`) 기준 `trading_date` 일일 멱등 생성**으로 전환. `DailyPreallocationScheduler`(REQ-030 SnapshotScheduler 패턴 준용) 신규 + `setup()` 배선 + `trex-preallocation` 스레드. timestamp 는 UTC 유지. BAT-018 재구현·재검증 완료 → Implemented 유지.
|
||||
|
||||
#### Intent
|
||||
|
||||
- `trading_date` 가 KST 캘린더 일자와 일치하고, 재시작 없이 TZ 일자 경계마다 다음날 슬롯이 자동 생성되도록(issue #14).
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
|
||||
- Updated:
|
||||
- setup preallocation 이 UTC 날짜 → 설정 TZ `trading_date` 기준. 일일 멱등 preallocation 스레드 추가(TZ 일자당 1회).
|
||||
|
||||
#### Impacted Area Delta
|
||||
|
||||
- Modules: `trex_core/application/preallocation_scheduler.py`(신규), `trex_core/runtime/daemon.py`(setup TZ preallocation + 스레드 배선)
|
||||
- Tests: tests/application/test_preallocation_scheduler.py
|
||||
|
||||
#### Delivery Trace Delta
|
||||
|
||||
- Batches: BAT-018
|
||||
- Releases: none
|
||||
|
||||
#### Revalidation Impact
|
||||
|
||||
- Reverification Needed: yes (BAT-018 에서 완료)
|
||||
- Existing Implementation Invalidated: yes (UTC 날짜 1회 preallocation 경로)
|
||||
- Status Recommendation: Implemented (change-req → BAT-018 재검증)
|
||||
- Follow-up Action: confirm-req-implemented(BAT-018).
|
||||
|
||||
#### Validation Plan
|
||||
|
||||
- BAT-018: KST/UTC 경계 trading_date, 일자 경계 자동 생성+멱등, 설정 TZ, 트리거 시각 게이팅 단위.
|
||||
|
||||
+1
-1
@@ -139,8 +139,8 @@
|
||||
|
||||
## Recent Change Log Summary
|
||||
|
||||
- 2026-06-08 :: REQ-005/048/009 change-req (dcy-014, BAT-018): 당일 슬롯 생성 기준을 시스템 UTC 날짜에서 설정 타임존(기본 KST) 기준 `trading_date` 로 분리(timestamp 는 UTC 유지) + 데몬 1회 preallocation 을 `DailyPreallocationScheduler`(TZ 일자당 1회 멱등 fire, 재시작 없이 다음날 자동 생성)로 대체(REQ-005), TOML `trading_timezone`/`daily_preallocation_time` 키(REQ-048), runtime-flows/data-model 정합(REQ-009). 전부 change-req(신규 REQ 없음, OQ-3; 스케줄러는 REQ-030 패턴 준용). 단위 302 passed, 인프라 게이트 334 passed, ruff clean. (change-req/confirm-req-implemented)
|
||||
- 2026-06-08 :: REQ-012/013/018/031/014/015 change-req (dcy-013, BAT-017): KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구 갇히던 버그(issue #9) 정정. KIS `poll_fill` 이 `cncl_yn` 파싱해 외부 취소 감지 — 체결 0 취소→`cancelled`(롤백 자동 복구), 부분 체결 후 취소→`is_partial_cancel`(체결분 원장 1회+qty 차감+holding 복귀, OQ-1 A안)(REQ-012/013/018), 잔여 포지션 정합 연계(REQ-031), pending stale 워치독 Alert-only(`TREX_PENDING_STALE_THRESHOLD`, WARN+카운터, 상태 미변경)(REQ-014), baseline 2문서 정합(REQ-015). 전부 change-req(신규 REQ 없음, OQ-3). 단위 294 passed, 인프라 게이트 326 passed, ruff clean. (change-req/confirm-req-implemented)
|
||||
- 2026-06-08 :: REQ-013/007/036/002/006/003/015 change-req (dcy-012, BAT-016): 매도 주문이 매수가(entry_price) 지정가로 나가던 버그(issue #10) 정정. 슬롯 `entry_price`→`limit_price`(side 무관 pending 지정가) rename + pending 이탈 clear(REQ-002), 컬럼 rename 마이그레이션 006(REQ-006), `_dispatch` 가 slot.limit_price 단일 필드로 가격/주문유형 결정(REQ-013), SellIntent/sell payload optional limit_price·_admit_sell 운반(REQ-007), SlotService.sell optional 가격→자율 매도 시장가(REQ-036), 컬럼 계약 가드(REQ-003), baseline 3문서 정합(REQ-015). 전부 change-req(신규 REQ 없음, OQ-3). 단위 286 passed, 인프라 게이트 318 passed, ruff clean. (change-req/confirm-req-implemented)
|
||||
- 2026-06-08 :: REQ-007/018/021 change-req + REQ-055 생성·Implemented (dcy-011, BAT-015): buy intent 계약 hard cutover — portfolio_id 필수화 + symbol 서버 resolve(SnapshotStore) + unknown_portfolio/portfolio_id_required 거부(REQ-007), buy payload symbol 제거(REQ-021), ledger skip 가시화(WARN+ledger_skipped 카운터, REQ-018), closed↔outbox anti-join 운영 점검 `find_ledger_gap_slots` 신규(REQ-055). 전체 인프라 게이트 314 passed, ruff clean. (change-req/draft-req/confirm-req-implemented)
|
||||
- 2026-06-08 :: REQ-052~054 Approved → Implemented (confirm-req-implemented): BAT-014(batch-lite) verify 완료(신규 8 단위, 전체 278 passed, ruff clean). SafetySellService per-slot 쿨다운(skip/만료재시도/경고/prune, market_close 제외), SafetySellConfig env 2종(min_retry_interval_sec 600/max_retry_warn_count 3), runtime-flows 정합. (confirm-req-implemented)
|
||||
- 2026-06-08 :: REQ-052~054 Proposed → Approved (confirm-req): dcy-010 safety-sell 쿨다운 3개 전원 승인 게이트 통과. dcy-010(confirmed) FR/OQ 와 모순 없음, 필수 필드·구체 AC 완비. 피드백 채널·DB 영속은 Out of Scope 비핵심. (confirm-req)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""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())
|
||||
@@ -51,6 +51,27 @@ def test_default_instance_key_when_from_env(monkeypatch):
|
||||
assert DaemonConfig.from_env().instance_key == "default"
|
||||
|
||||
|
||||
def test_trading_timezone_defaults_to_kst(tmp_path, monkeypatch):
|
||||
"""dcy-014 / REQ-048: trading TZ + daily trigger default to KST / midnight."""
|
||||
monkeypatch.setenv("TREX_POSTGRESQL_DSN", "postgresql://x/y")
|
||||
inst = DaemonInstanceConfig.from_toml(_write_toml(tmp_path, 'instance_key = "i1"\n'))
|
||||
assert inst.trading_timezone == "Asia/Seoul"
|
||||
assert inst.daily_preallocation_time == "00:00"
|
||||
assert DaemonConfig.from_env().trading_timezone == "Asia/Seoul"
|
||||
|
||||
|
||||
def test_trading_timezone_from_toml_propagates(tmp_path, monkeypatch):
|
||||
"""dcy-014 / REQ-048: TOML trading_timezone + trigger flow into DaemonConfig."""
|
||||
monkeypatch.setenv("TREX_POSTGRESQL_DSN", "postgresql://x/y")
|
||||
path = _write_toml(
|
||||
tmp_path,
|
||||
'instance_key = "i2"\ntrading_timezone = "UTC"\ndaily_preallocation_time = "07:30"\n',
|
||||
)
|
||||
cfg = DaemonConfig.from_config(path)
|
||||
assert cfg.trading_timezone == "UTC"
|
||||
assert cfg.daily_preallocation_time == "07:30"
|
||||
|
||||
|
||||
def test_config_required_cli_exits_nonzero():
|
||||
"""AC-2: daemon entry without --config exits non-zero (argparse required)."""
|
||||
proc = subprocess.run(
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Daily slot preallocation scheduler (dcy-014 / REQ-005, issue #14).
|
||||
|
||||
trading_date is computed in a configurable timezone (default Asia/Seoul) so the
|
||||
DB trading_date matches the local trading calendar, while all timestamps stay
|
||||
UTC (NFR-1). Modeled on SnapshotScheduler (REQ-030): a tick loop fires once per
|
||||
TZ-day at/after the configured trigger time, calling the idempotent
|
||||
`preallocate_daily_slots` so the next day's slots are created without a daemon
|
||||
restart (NFR-2/NFR-3).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from datetime import date, datetime, time, timezone
|
||||
from typing import Callable, Optional, Protocol
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _Preallocator(Protocol):
|
||||
def preallocate_daily_slots(self, trading_date: date, count: int) -> int:
|
||||
...
|
||||
|
||||
|
||||
class DailyPreallocationScheduler:
|
||||
"""Background-thread-friendly daily preallocation. Use `run_once()` for tests."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
slot_service: _Preallocator,
|
||||
total_slots: int,
|
||||
tz: timezone,
|
||||
schedule_time: time = time(hour=0, minute=0),
|
||||
clock: Optional[Callable[[], datetime]] = None,
|
||||
) -> None:
|
||||
self._svc = slot_service
|
||||
self._total = total_slots
|
||||
self._tz = tz # tzinfo used to derive trading_date (default Asia/Seoul)
|
||||
self._schedule = schedule_time
|
||||
self._clock = clock or (lambda: datetime.now(tz=timezone.utc))
|
||||
self._last_fired_date: Optional[date] = None
|
||||
|
||||
# ---- callable from a daemon thread loop --------------------------------
|
||||
def run_forever(self, stop: threading.Event, *, tick_sec: float = 60.0) -> None:
|
||||
# Boot: ensure the current TZ-day's slots exist immediately (idempotent),
|
||||
# then enter the daily cadence for subsequent day boundaries.
|
||||
self.run_once()
|
||||
while not stop.wait(tick_sec):
|
||||
try:
|
||||
self._maybe_tick()
|
||||
except Exception: # noqa: BLE001 - loop must survive transient errors
|
||||
_log.exception("preallocation scheduler tick failed")
|
||||
|
||||
def _maybe_tick(self) -> None:
|
||||
now_tz = self._clock().astimezone(self._tz)
|
||||
target = datetime.combine(now_tz.date(), self._schedule, tzinfo=self._tz)
|
||||
if now_tz < target:
|
||||
return # before today's trigger time
|
||||
if self._last_fired_date is not None and self._last_fired_date == now_tz.date():
|
||||
return # already created today's slots
|
||||
self.run_once()
|
||||
|
||||
# ---- the actual preallocation -----------------------------------------
|
||||
def run_once(self) -> date:
|
||||
"""Preallocate slots for the current TZ trading_date (idempotent). Returns it."""
|
||||
trading_date = self._clock().astimezone(self._tz).date()
|
||||
created = self._svc.preallocate_daily_slots(trading_date, self._total)
|
||||
if created:
|
||||
_log.info("preallocated %d slots for trading_date=%s", created, trading_date)
|
||||
self._last_fired_date = trading_date
|
||||
return trading_date
|
||||
@@ -99,6 +99,10 @@ class DaemonInstanceConfig:
|
||||
|
||||
instance_key: str
|
||||
portfolio_hub_token: str = ""
|
||||
# dcy-014 / REQ-048: trading_date timezone + daily preallocation trigger.
|
||||
# Optional — absent keys fall back to KST / midnight defaults.
|
||||
trading_timezone: str = "Asia/Seoul"
|
||||
daily_preallocation_time: str = "00:00"
|
||||
|
||||
@staticmethod
|
||||
def from_toml(path: str) -> "DaemonInstanceConfig":
|
||||
@@ -114,6 +118,8 @@ class DaemonInstanceConfig:
|
||||
return DaemonInstanceConfig(
|
||||
instance_key=str(instance_key),
|
||||
portfolio_hub_token=str(data.get("portfolio_hub_token") or ""),
|
||||
trading_timezone=str(data.get("trading_timezone") or "Asia/Seoul"),
|
||||
daily_preallocation_time=str(data.get("daily_preallocation_time") or "00:00"),
|
||||
)
|
||||
|
||||
|
||||
@@ -144,6 +150,11 @@ class DaemonConfig:
|
||||
# threshold in seconds. 0 disables (default). When >0, dispatched-pending
|
||||
# slots older than this are surfaced (WARN + counter) without state change.
|
||||
pending_stale_threshold_sec: int = 0
|
||||
# dcy-014 / REQ-005 (BAT-018): trading_date timezone (default KST) + daily
|
||||
# preallocation trigger time (HH:MM, TZ-local). Operative surface is the
|
||||
# per-instance TOML (REQ-048); these are the env-only defaults.
|
||||
trading_timezone: str = "Asia/Seoul"
|
||||
daily_preallocation_time: str = "00:00"
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> "DaemonConfig":
|
||||
@@ -241,4 +252,11 @@ class DaemonConfig:
|
||||
pf = base.portfolio_hub
|
||||
if inst.portfolio_hub_token and pf is not None:
|
||||
pf = replace(pf, token=inst.portfolio_hub_token)
|
||||
return replace(base, instance_key=inst.instance_key, portfolio_hub=pf)
|
||||
# dcy-014 / REQ-048: trading_date TZ + daily trigger come from TOML.
|
||||
return replace(
|
||||
base,
|
||||
instance_key=inst.instance_key,
|
||||
portfolio_hub=pf,
|
||||
trading_timezone=inst.trading_timezone,
|
||||
daily_preallocation_time=inst.daily_preallocation_time,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ Threads (started in `run()`):
|
||||
- execution loop — broker dispatch + poll + slot transitions + event emit
|
||||
- ledger writer — ledger outbox drain (REQ-019)
|
||||
- snapshot scheduler — daily portfolio-hub snapshot refresh (REQ-030)
|
||||
- preallocation scheduler — daily slot preallocation in trading TZ (REQ-005, dcy-014)
|
||||
- safety-sell worker — forced_close / stop-loss·take-profit / market-close (REQ-037/038/039)
|
||||
|
||||
Each consumer/publisher uses its OWN RabbitMQTransport instance because
|
||||
@@ -21,9 +22,8 @@ from __future__ import annotations
|
||||
import logging
|
||||
import signal
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from datetime import time as time_cls
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from trex_core.adapters.broker.paper import PaperBroker
|
||||
from trex_core.adapters.ledger import InMemoryLedger, PortfolioHubLedger
|
||||
@@ -62,6 +62,7 @@ from trex_core.application.portfolio import (
|
||||
SnapshotStore,
|
||||
)
|
||||
from trex_core.application.safety_sell import SafetySellService
|
||||
from trex_core.application.preallocation_scheduler import DailyPreallocationScheduler
|
||||
from trex_core.application.snapshot_scheduler import SnapshotScheduler
|
||||
from trex_core.application.watchdog import WatchdogStore
|
||||
from trex_core.domain.slot import SlotService
|
||||
@@ -167,6 +168,15 @@ class Daemon:
|
||||
broker=self.broker,
|
||||
schedule_kst=_parse_kst_time(config.snapshot_schedule.kst_time),
|
||||
)
|
||||
# dcy-014 / REQ-005: daily slot preallocation in the configured
|
||||
# trading timezone (default KST), so trading_date matches the local
|
||||
# calendar and the next day's slots appear without a restart.
|
||||
self.preallocation_scheduler = DailyPreallocationScheduler(
|
||||
slot_service=self.slot_service,
|
||||
total_slots=config.total_slots,
|
||||
tz=ZoneInfo(config.trading_timezone),
|
||||
schedule_time=_parse_kst_time(config.daily_preallocation_time),
|
||||
)
|
||||
|
||||
# Event publisher transport — one dedicated instance for the
|
||||
# execution-loop thread (pika is not thread-safe).
|
||||
@@ -287,12 +297,19 @@ class Daemon:
|
||||
except Exception: # noqa: BLE001
|
||||
_log.exception("snapshot scheduler loop crashed")
|
||||
|
||||
def _preallocation_loop(self) -> None:
|
||||
try:
|
||||
self.preallocation_scheduler.run_forever(self.stop, tick_sec=60.0)
|
||||
except Exception: # noqa: BLE001
|
||||
_log.exception("preallocation scheduler loop crashed")
|
||||
|
||||
# ------------------------------------------------------------ lifecycle
|
||||
def setup(self) -> None:
|
||||
apply_migrations(self.config.dsn)
|
||||
self.slot_service.preallocate_daily_slots(
|
||||
datetime.now(tz=timezone.utc).date(), self.config.total_slots
|
||||
)
|
||||
# dcy-014 / REQ-005: preallocate against the configured trading timezone
|
||||
# (default KST), not the system UTC date. Boot-time run_once ensures the
|
||||
# current TZ-day's slots exist immediately (idempotent).
|
||||
self.preallocation_scheduler.run_once()
|
||||
self.execution.reconcile() # REQ-014 restart recovery
|
||||
try:
|
||||
self.event_publisher.declare_topology(self.config.instance_key)
|
||||
@@ -307,6 +324,7 @@ class Daemon:
|
||||
("trex-snapshot-rpc", self._snapshot_rpc_loop),
|
||||
("trex-control-rpc", self._control_rpc_loop),
|
||||
("trex-snapshot-scheduler", self._snapshot_scheduler_loop),
|
||||
("trex-preallocation", self._preallocation_loop),
|
||||
("trex-safety-sell", self._safety_sell_loop),
|
||||
]
|
||||
for name, target in workers:
|
||||
|
||||
Reference in New Issue
Block a user