# SDD: Scheduled Market Data Refresh

## 위치

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/scheduled-market-data-refresh.md`
- Phase: `agent-roadmap/phase/backtest-loop/PHASE.md`

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목: 없음

## 문제 / 비목표

- 문제: market data import가 수동 scenario 실행에만 의존하면 원격 서버의 backtest input freshness를 반복 확인하기 어렵다. scheduler는 provider 호출, DB upsert, retry/backfill, freshness/readiness, remote smoke까지 같은 운영 경계로 묶어야 한다.
- 비목표:
  - Flutter 운영 화면 구현
  - 새 strategy 판단 로직 또는 투자 의사결정 알고리즘 설계
  - paper/live order routing과 실거래 adapter 변경
  - 월봉/분봉 provider import와 backtest engine 지원 확장 전체
  - 외부 provider credential 값이나 개인 secret 문서화

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/backtest-loop/milestones/scheduled-market-data-refresh.md` | Milestone 목표, 기능 Task, 범위 제외, 완료 후보 반영 기준 |
| Code | `services/worker/cmd/alt-worker/main.go` | worker runtime wiring과 job runner 등록 기준 |
| Code | `services/worker/internal/config/config.go` | worker runtime env/config source of truth. scheduler config는 secret 값을 직접 보관하지 않는다. |
| Code | `services/worker/internal/jobs/marketdata_jobs.go` | `import_daily_bars` job payload, provider capability gate, importer dispatch 기준 |
| Code | `services/worker/internal/storage/postgres/queries/queries.sql` | `bars` upsert idempotency 기준 |
| Code | `apps/cli/internal/operator/scenario.go` | scenario validation, `collection_freshness`, matrix/readiness smoke 기준 |
| Code | `apps/cli/testdata/operator/headless_validation.md` | operator-facing headless evidence key와 scenario handoff 기준 |
| External Provider | KIS | scheduled tick이 호출할 provider이며 delay, gap, missing, typed error를 상태로 남겨야 한다. |
| User Decision | `user_review_0.log` | D01: scheduler는 `services/worker` 내장 loop가 1차 실행 주체다. OS/systemd는 process supervision만 맡고 external cron은 사용하지 않는다. worker scheduler는 설정된 한도 안에서 병렬 수집을 지원해야 한다. |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| `config-candidate` | schedule config가 named universe, provider, selector, timeframe, cadence, timezone, backfill window, parallelism limit를 선언한다. | `config-valid` 또는 `config-rejected` | validate/dry-run output |
| `config-rejected` | 필수 필드 누락 또는 provider/timeframe 조합 미지원이다. | terminal | typed validation error, stable text/JSONL |
| `config-valid` | schedule config가 validation을 통과한다. | `scheduled` | next window calculation |
| `scheduled` | worker 내장 scheduler loop에서 tick 시간이 도래하거나 manual tick smoke가 실행된다. | `dispatching` 또는 `skipped` | scheduler log/status |
| `dispatching` | worker scheduler가 tick lock을 획득하고 universe를 수집 작업 단위로 펼친다. | `running` | bounded parallel dispatch log/status |
| `skipped` | 동일 tick이 이미 running이거나 lock/idempotency guard가 막는다. | terminal | duplicate tick evidence |
| `running` | 하나 이상의 import job이 설정된 병렬 한도 안에서 provider 호출과 DB upsert를 실행한다. | `succeeded`, `stale`, 또는 `failed` | job result, importer result, DB state |
| `succeeded` | import와 freshness/readiness가 expected window를 만족한다. | `ready-for-backtest` | status JSONL, freshness output |
| `stale` | provider delay, missing, gap이 남아 backfill/retry가 필요하다. | `scheduled` 또는 terminal | retry/backfill calculation, stale status |
| `failed` | provider, config, DB, runtime 오류가 발생한다. | `scheduled` 또는 terminal | last_error, error status |
| `ready-for-backtest` | scheduler가 적재한 데이터로 selector가 bars를 찾는다. | terminal | backtest matrix/result smoke exit code `0` |

## Interface Contract

- 계약 원문: 없음
- 입력:
  - `schedule_config`: named universe별 provider, selector, timeframe, cadence, timezone, backfill window, parallelism limit.
  - `tick`: worker 내장 scheduler loop 또는 manual smoke로 실행되는 refresh trigger. 1차 구현에서 external cron은 사용하지 않는다.
  - `provider_capability`: provider, market, venue, timeframe별 accepted/rejected 판정.
  - `runtime_env`: provider credential과 DB URL은 config 파일에 쓰지 않고 원격 runtime env/SOPS 주입 경계에서 제공한다.
  - `freshness_window`: latest, expected dates, missing/gap/duplicate/provider delay를 계산할 기간.
- 출력:
  - `refresh_status`: last_success, last_error, next_run, imported bar count, missing/gap/duplicate/provider_delay.
  - `job_result`: import job success/stale/error와 retry/backfill decision, parallel dispatch item별 result.
  - `readiness`: backtest selector가 scheduled import 결과를 사용할 수 있는지에 대한 headless result.
  - `remote_smoke`: migration, runtime startup, tick, freshness query, backtest selector/result scenario exit code.
- 금지:
  - external cron을 1차 scheduler 실행 주체로 사용하지 않는다.
  - OS/systemd timer가 schedule state나 tick 판단을 소유하지 않는다. OS/systemd는 worker process supervision까지만 맡는다.
  - schedule config에 secret 값을 저장하지 않는다.
  - 동일 tick 재실행이 duplicate `bars` row를 만들지 않는다.
  - provider delay나 gap을 성공으로 조용히 숨기지 않는다.
  - scheduler가 Flutter 화면 구현을 전제하지 않는다.
  - 월봉/분봉 import 확장은 이 Milestone에서 새로 확정하지 않고 지원되는 timeframe만 대상으로 삼는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `schedule-config` | named universe별 provider, selector, timeframe, cadence, timezone, backfill window가 있다. | validate/dry-run을 실행한다. | next window와 rejected combination이 text/JSONL로 출력된다. |
| S02 | `scheduled-runner` | 원격 worker runtime, idempotent `bars` upsert, 병렬 수집 대상이 있다. | worker 내장 scheduler tick을 2회 실행하고 병렬 수집을 수행한다. | external cron 없이 duplicate row 없이 item별 job result와 freshness/readiness output이 남는다. |
| S03 | `retry-backfill` | provider delay, missing, gap fixture가 있다. | retry/backfill calculation을 실행한다. | stale/error 상태와 backfill window가 stable text/JSONL로 구분된다. |
| S04 | `refresh-status` | success, stale, error 상태가 있다. | status scenario를 실행한다. | last_success, last_error, next_run, bar count, missing/gap/duplicate/provider_delay가 구분된다. |
| S05 | `backtest-readiness` | scheduler tick이 bars를 적재했다. | remote runner에서 backtest matrix 또는 result summary scenario를 실행한다. | exit code `0`과 result/readiness evidence가 남는다. |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | `Spec Completion` 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | schedule config validate test와 dry-run text/JSONL | `agent-task/m-scheduled-market-data-refresh/...` | `schedule-config` task와 next window/rejected combination evidence |
| S02 | worker 내장 scheduler tick smoke, bounded parallel dispatch output, `bars` key 중복 없음, freshness/readiness output | `agent-task/m-scheduled-market-data-refresh/...` | `scheduled-runner` task와 2회 tick idempotency 및 병렬 수집 evidence |
| S03 | missing/gap/provider delay fixture 또는 local smoke | `agent-task/m-scheduled-market-data-refresh/...` | `retry-backfill` task와 stale/error/backfill evidence |
| S04 | success/stale/error status scenario expected JSONL | `agent-task/m-scheduled-market-data-refresh/...` | `refresh-status` task와 status field evidence |
| S05 | remote runner smoke after scheduler tick | `agent-task/m-scheduled-market-data-refresh/...` | `backtest-readiness` task와 exit code `0` evidence |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 plan/code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰 항목은 `user_review_0.log`로 해결 기록을 남겼다.

## 사용자 리뷰 이력

- `user_review_0.log`: D01은 `services/worker` 내장 scheduler loop를 1차 실행 주체로 결정했고, external cron은 사용하지 않으며, worker scheduler는 설정된 한도 안에서 병렬 수집을 지원해야 한다.

## 작업 컨텍스트

- 표준선: 운영 기능은 먼저 화면 없이 CLI, YAML scenario, JSONL/text output, fixture, remote smoke로 검증한다.
- 표준선: scheduled import는 provider 호출과 DB upsert를 idempotent하게 유지하고 동일 tick 재실행이 중복 row를 만들지 않아야 한다.
- 표준선: `services/worker`가 데이터 수집, 정규화, backtest, scheduled job처럼 오래 걸리거나 비동기적인 일을 담당한다.
- 표준선: scheduler의 일정 판단과 상태 관리는 worker 내부에 둔다. OS/systemd는 worker process supervision만 맡고 external cron은 쓰지 않는다.
- 표준선: worker scheduler는 provider/API 부하를 제어할 수 있는 bounded parallelism으로 여러 수집 작업을 병렬 실행할 수 있어야 한다.
- 표준선: scheduler config에는 secret 값을 넣지 않고 원격 runtime env/SOPS 주입 경계를 따른다.
- 표준선: `services/api`는 client-facing 요청을 worker 실행/조회 경계로 중계하는 control plane으로 남긴다.
- 후속 SDD: 없음
