alt/apps/cli/testdata/operator/headless_validation.md

243 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Headless Validation Handoff
이 문서는 `operator-headless-workflow-validation` 마일스톤의 handoff table 기준
(`agent-roadmap/phase/operator-surface/milestones/operator-headless-workflow-validation.md`)
을 충족하는 검증 evidence를 한곳에 모은다. 후속 `Flutter Operator Console MVP`
실화면을 만들기 전에 이 matrix로 각 scenario의 command/input/expected output/exit
code/protobuf field를 확인한다.
ALT 운영 UI 게이트(`agent-ops/rules/project/rules.md`의 "운영 UI 구현 게이트")에 따라
모든 검증은 화면 없이 CLI와 machine-readable 출력으로 먼저 수행한다.
## 공통 실행 형식
- validate(offline, 소켓 없이 dry-run): `alt operator scenario validate --file <yaml>`
- run(실 API 연결): `alt operator scenario run --file <yaml> --api-url ws://127.0.0.1:18030/socket --output jsonl`
- 로컬 개발에서는 `cd apps/cli && go run ./cmd/alt <위 인자>`로 실행한다(`bin/dev` 참고).
- exit code 계약: `0` 전 step 기대 일치, `1` typed error/expectation mismatch, `2` parse/validation/flag 오류, `3` transport 실패.
- expected output fixture는 `--output jsonl` 기준의 step/summary line이다. `run_id`,
`count`, `starting_cash`, `ending_equity`, `total_return`, `trade_count`,
`error_message` 값은 seed 데이터에 따라 달라지는 illustrative 값이며, 후속 MVP가
계약으로 의존하는 부분은 line별 `scenario`/`step`/`action`/`status` key와 step별로
검증하는 protobuf field의 존재 여부다.
### command-first workflow handoff matrix
이 matrix는 import/status/backtest workflow별로 command, expected output keys,
반복 운영 여부, UI candidate/defer reason을 명시한다. 화면 레이아웃·위젯·차트는
범위 밖이며 Flutter Operator Console MVP phase에서 별도로 정의한다.
| Scenario | Command | Input fixture | Expected output fixture | Expected output keys | Expected exit code | Repeatable operation | UI candidate | Checked protobuf/view-model field | UI defer reason |
|-----|-|---|-|-|--|-|-|-|-|
| `api_connection_smoke` | `alt operator scenario run --file testdata/operator/api_connection_smoke.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/api_connection_smoke.yaml` | `testdata/operator/expected/api_connection_smoke.jsonl` | `scenario`, `status`, `type`, `action` | `0` (성공), `3` (transport 실패 시) | yes | connection status display | `HelloResponse.capabilities`; summary `status`/`exit_code` | Flutter capability chip layout 미정 |
| `market_data_status_query` | `alt operator scenario run --file testdata/operator/market_data_status_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/market_data_status_query.yaml` | `testdata/operator/expected/market_data_status_query.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `count`, `universe`, `freshness_status`, `missing_count`, `latest_yyyymmdd`, `gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days` | `0` (성공/empty), `1` (mismatch) | yes | market data status card (종목수·바개수·최신도) | `import_daily_bars``ImportDailyBarsResponse.instrument_count`/`bar_count`; `ListInstrumentsResponse.instruments`, `ListBarsResponse.bars`; `collection_freshness``freshness_status`, `missing_count`, `latest_yyyymmdd`; `ErrorInfo.code` | 차트/바 시각화 레이아웃 미정 |
| `us_market_data_status_query` | `alt operator scenario run --file testdata/operator/us_market_data_status_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/us_market_data_status_query.yaml` | `testdata/operator/expected/us_market_data_status_query.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `count`, `universe`, `freshness_status`, `missing_count`, `latest_yyyymmdd`, `gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days` | `0` (성공/empty), `1` (mismatch) | yes | market data status card (종목수·바개수·최신도) | `import_daily_bars``ImportDailyBarsResponse.instrument_count`/`bar_count`; `ListInstrumentsResponse.instruments`, `ListBarsResponse.bars`; `collection_freshness``freshness_status`, `missing_count`, `latest_yyyymmdd`; `ErrorInfo.code` | 차트/바 시각화 레이아웃 미정 |
| `backtest_run_request` | `alt operator scenario run --file testdata/operator/backtest_run_request.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_run_request.yaml` | `testdata/operator/expected/backtest_run_request.jsonl` | `scenario`, `status`, `type`, `action`, `run_id`, `run_status` | `0` (성공), `1` (typed error/mismatch) | yes | backtest 생성 form (parameter 입력/검증) | `StartBacktestRequest.spec.selector`; `StartBacktestResponse.run.id`, `BacktestRun.status` | form validation layout 미정 |
| `backtest_run_polling` | `alt operator scenario run --file testdata/operator/backtest_run_polling.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_run_polling.yaml` | `testdata/operator/expected/backtest_run_polling.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `run_id`, `run_status` | `0` (terminal 도달), `3` (polling timeout) | yes | backtest 진행 상태 표시 (상태 칩/진행바) | named universe `import_daily_bars``ImportDailyBarsResponse.provider`/`instrument_count`/`bar_count`; `StartBacktestRequest.spec.selector`; `StartBacktestResponse.run.id`; `GetBacktestRunResponse.run.status` 전이 (pending→running→succeeded/failed/canceled) | 진행바/상태 전이 애니메이션 미정 |
| `backtest_result_summary` | `alt operator scenario run --file testdata/operator/backtest_result_summary.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_result_summary.yaml` | `testdata/operator/expected/backtest_result_summary.jsonl` | `scenario`, `status`, `type`, `action`, `run_id`, `run_status`, `starting_cash`, `ending_equity`, `total_return`, `trade_count` | `0` (성공), `1` (missing run/typed error) | yes | backtest 결과 요약 (metrics chips) | `GetBacktestResultResponse.result.summary`; `GetBacktestRunResponse.run.status` | 차트/메트릭 레이아웃 미정 |
| `backtest_matrix_success` | `alt operator scenario run --file testdata/operator/backtest_matrix_success.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_success.yaml` | `testdata/operator/expected/backtest_matrix_success.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `0` (성공) | yes | backtest matrix batch 성공 상태 표시 | `StartBacktestResponse.run.id`, `BacktestRun.status`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
| `backtest_matrix_failed` | `alt operator scenario run --file testdata/operator/backtest_matrix_failed.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_failed.yaml` | `testdata/operator/expected/backtest_matrix_failed.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `0` (terminal failed) | yes | backtest matrix batch 실패 상태 표시 | `StartBacktestResponse.run.id`, `BacktestRun.status`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
| `backtest_matrix_timeout` | `alt operator scenario run --file testdata/operator/backtest_matrix_timeout.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_timeout.yaml` | `testdata/operator/expected/backtest_matrix_timeout.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `3` (timeout) | yes | backtest matrix batch 타임아웃 표시 | `StartBacktestResponse.run.id`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
| `backtest_matrix_mismatch` | `alt operator scenario run --file testdata/operator/backtest_matrix_mismatch.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_mismatch.yaml` | `testdata/operator/expected/backtest_matrix_mismatch.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `1` (mismatch) | yes | backtest matrix batch 기대 불일치 표시 | `StartBacktestResponse.run.id`, `BacktestRun.status`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
| `invalid_request_matrix` | `alt operator scenario run --file testdata/operator/invalid_request_matrix.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/invalid_request_matrix.yaml` | `testdata/operator/expected/invalid_request_matrix.jsonl` | `scenario`, `status`, `type`, `action`, `error_code`, `error_message` | `0` (기대한 typed error 도달), `2` (malformed scenario) | no (개발/검증 전용) | error/message 노출 (검증용) | `ErrorInfo.code` (`invalid_request`, `not_found`) | error 메시지 UI 미정 |
| `kis_daily_import_smoke` | `alt operator scenario run --file testdata/operator/kis_daily_import_smoke.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/kis_daily_import_smoke.yaml` | `testdata/operator/expected/kis_daily_import_smoke.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count` | `0` (성공), `1` (mismatch/typed error) | yes | KIS import status (진행률/결과) | `ImportDailyBarsResponse.provider`, `instrument_count`, `bar_count` | import 진행 UI 미정 |
| `minute_import_rejected` | `alt operator scenario run --file testdata/operator/minute_import_rejected.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/minute_import_rejected.yaml` | `testdata/operator/expected/minute_import_rejected.jsonl` | `scenario`, `status`, `type`, `action`, `error_code`, `error_message` | `0` (기대한 typed error 도달), `2` (malformed scenario) | no (개발/검증 전용) | error/message 노출 (검증용) | `ErrorInfo.code` (`invalid_request`); `ImportDailyBarsResponse.error` | error 메시지 UI 미정 |
| `monthly_bars_aggregation` | `alt operator scenario run --file testdata/operator/monthly_bars_aggregation.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/monthly_bars_aggregation.yaml` | `testdata/operator/expected/monthly_bars_aggregation.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `source_daily_bar_count`, `monthly_bar_count`, `provenance_count`, `aggregation_rule_id` | `0` (성공), `1` (mismatch/typed error) | yes | monthly aggregation status card (일봉 source·월봉 count·provenance 규칙) | `aggregate_monthly_bars``AggregateMonthlyBarsResponse.provider`/`instrument_count`/`source_daily_bar_count`/`monthly_bar_count`; `MonthlyProvenance.aggregation_rule_id`; `ErrorInfo.code` | provenance/aggregation 시각화 레이아웃 미정 |
| `paper_trading_state` | `alt operator scenario run --file testdata/operator/paper_trading_state.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/paper_trading_state.yaml` | `testdata/operator/expected/paper_trading_state.jsonl` | `scenario`, `status`, `type`, `action`, `account_id`, `run_id`, `run_status`, `cash`, `equity_point_count`, `fill_count`, `latest_equity`, `position_count`, `risk` | `0` (성공/terminal 도착), `1` (typed error/mismatch) | yes | paper trading dashboard (account 상태/포지션/평가금액/리스크) | `StartPaperTradingResponse.run.id`, `run_status`; `GetPaperTradingStateResponse.account_id`, `cash`, `equity_point_count`, `fill_count`, `latest_equity`, `position_count`, `risk_rejections`; summary `status`/`exit_code` | Flutter dashboard 레이아웃 미정 |
| `paper_order_lifecycle` | `alt operator scenario run --file testdata/operator/paper_order_lifecycle.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/paper_order_lifecycle.yaml` | `testdata/operator/expected/paper_order_lifecycle.jsonl` | `scenario`, `status`, `type`, `action`, `account_id`, `run_id`, `run_status`, `cash`, `equity_point_count`, `fill_count`, `latest_equity`, `position_count`, `risk`, `order_id`, `order_status`, `fill_price` | `0` (성공/terminal 도착), `1` (typed error/mismatch) | yes | paper order management list (주문 상태/체결가/취소) | `StartPaperTradingResponse.run.id`, `run_status`; `SubmitPaperOrderResponse.order.id`, `order_status`; `FillPaperOrderResponse.fill_price`, `order_status`; `CancelPaperOrderResponse.order_status`; `GetPaperTradingStateResponse.account_id`, `cash`, `fill_count`; summary `status`/`exit_code` | Flutter 주문 목록/상세 레이아웃 미정 |
| `live_order_lifecycle` | `alt operator scenario run --file testdata/operator/live_order_lifecycle.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_order_lifecycle.yaml` | `testdata/operator/expected/live_order_lifecycle.jsonl` | `scenario`, `status`, `type`, `action`, `live_order_id`, `live_order_status`, `broker_order_id`, `broker_status`, `operator_confirmed` | `0` (성공/operator gate 통과), `1` (typed error/confirmation missing/mismatch) | yes | live order management (실주문 상태/취소/브로커 상태) | `SubmitLiveOrderResponse.order.id`, `order.status`, `order.broker_id`, `order.broker_status`; `OperatorConfirmation.confirmed`; `CancelLiveOrderResponse.order.status`; `GetLiveOrderResponse.order.status`; summary `status`/`exit_code` | Flutter 실주문 목록/상세 레이아웃 미정 |
| `live_risk_kill_switch` | `alt operator scenario run --file testdata/operator/live_risk_kill_switch.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_risk_kill_switch.yaml` | `testdata/operator/expected/live_risk_kill_switch.jsonl` | `scenario`, `status`, `type`, `action`, `max_daily_orders`, `max_open_orders`, `allow_short_selling`, `kill_switch_halted`, `kill_switch_reason` | `0` (성공/정책 조회·킬스위치 해제), `1` (typed error/mismatch) | yes | live risk 제어 패널 (정책 표시·킬스위치 토글) | `GetLiveRiskPolicyResponse.policy.max_daily_orders`, `max_open_orders`, `allow_short_selling`; `GetLiveKillSwitchResponse.state.halted`, `state.reason`; `SetLiveKillSwitchResponse.state.halted`, `state.reason`; summary `status`/`exit_code` | Flutter 킬스위치 토글·정책 표시 레이아웃 미정 |
| `live_account_sync` | `alt operator scenario run --file testdata/operator/live_account_sync.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_account_sync.yaml` | `testdata/operator/expected/live_account_sync.jsonl` | `scenario`, `status`, `type`, `action`, `live_account_id`, `live_broker`, `cash_count`, `live_position_count`, `synced_at_unix_ms`, `live_stale` | `0` (sync 성공·snapshot 조회 성공·not_found 기대 충족), `1` (typed error/mismatch) | yes | live 계좌 잔고·포지션 스냅샷 (계좌 현황 패널) | `SyncLiveAccountResponse.snapshot.account_id`, `broker`, `cash[]`, `positions[]`, `synced_at_unix_ms`, `stale`; `GetLiveAccountSnapshotResponse.snapshot`; summary `status`/`exit_code` | Flutter 계좌 잔고·포지션 목록 레이아웃 미정 |
| `live_audit_query` | `alt operator scenario run --file testdata/operator/live_audit_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_audit_query.yaml` | `testdata/operator/expected/live_audit_query.jsonl` | `scenario`, `status`, `type`, `action`, `audit_event_count` | `0` (조회 성공·필터 조회 성공), `1` (typed error/mismatch) | yes | live 감사 이력 조회 (운영 감사 패널) | `ListLiveAuditEventsResponse.events[]`; `LiveAuditEvent.event_id`, `type`, `account_id`, `order_id`, `timestamp_unix_ms`; summary `status`/`exit_code` | Flutter 감사 이력 목록 레이아웃 미정 |
| `scheduler_refresh_status` | `alt operator scenario run --file testdata/operator/scheduler_refresh_status.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/scheduler_refresh_status.yaml` | `testdata/operator/expected/scheduler_refresh_status.jsonl` | `scenario`, `status`, `type`, `action`, `schedule`, `scheduler_status`, `last_success_unix_ms`, `last_error`, `next_run_unix_ms`, `imported_bar_count`, `missing_count`, `gap_count`, `duplicate_count`, `provider_delay_days` | `0` (성공·status 일치), `1` (typed error/mismatch) | yes | 스케줄러 새로고침 상태 조회 (S04 evidence 패널) | `SchedulerRefreshStatusResponse.entries[].schedule`, `status`, `last_success_unix_ms`, `last_error`, `next_run_unix_ms`, `imported_bar_count`, `missing_count`, `gap_count`, `duplicate_count`, `provider_delay_days`; summary `status`/`exit_code` | Flutter 스케줄러 상태 대시보드 레이아웃 미정 |
| `scheduled_backtest_readiness` | `alt operator scenario run --file testdata/operator/scheduled_backtest_readiness.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/scheduled_backtest_readiness.yaml` | `testdata/operator/expected/scheduled_backtest_readiness.jsonl` | `scenario`, `status`, `type`, `action`, `scheduler_status`, `imported_bar_count`, `last_success_unix_ms`, `next_run_unix_ms`, `run_id`, `run_status`, `starting_cash`, `ending_equity`, `total_return`, `trade_count` | `0` (성공·scheduler tick 적재 데이터로 backtest succeeded) | yes | 스케줄러 적재 데이터 backtest readiness 패널 (S05 evidence) | `SchedulerRefreshStatusResponse.entries[].status`/`imported_bar_count`; `StartBacktestResponse.run.id`; `GetBacktestRunResponse.run.status`; `GetBacktestResultResponse.result.summary`; summary `status`/`exit_code` | Flutter readiness 대시보드 레이아웃 미정 |
## Matrix pre-seed 절차
`backtest_matrix_success.yaml`은 matrix run마다 `import_daily_bars` step 없이 바로 start/poll을
수행한다. 신규 원격 DB처럼 해당 종목·기간 bar 데이터가 없는 상태에서 실행하면 backtest가
`failed` 상태로 끝나거나 data-missing 오류가 발생해 deterministic 성공을 보장할 수 없다.
따라서 **신규 원격 DB에서 matrix를 실행하기 전에 반드시 `backtest_matrix_seed_import.yaml`
먼저 실행해 seed 데이터를 DB에 적재해야 한다.** `backtest_matrix_seed_import.yaml`
import-only scenario로 kr-backtest-smoke universe의 005930 종목 `2024052720240528` bar를
KIS에서 가져와 DB에 저장하고 exit 0으로 종료한다. backtest 실행이나 result polling을 포함하지
않으므로 `07+05_result_poll_remote` 작업 완료 여부와 독립적으로 사용할 수 있다.
### 실행 순서 (신규 원격 DB 기준)
```bash
# Step 1: seed/import — import_daily_bars(20240527-20240528, 005930)만 실행, exit 0 확인
go run ./cmd/alt operator scenario run \
--file testdata/operator/backtest_matrix_seed_import.yaml \
--api-url ws://127.0.0.1:18030/socket \
--output jsonl
# Step 2: matrix batch — seeded data로 deterministic 성공 검증
go run ./cmd/alt operator scenario run \
--file testdata/operator/backtest_matrix_success.yaml \
--api-url ws://127.0.0.1:18030/socket \
--output jsonl
```
- Step 1의 `import_kr_daily_bars` step은 KIS import를 포함하므로 worker에 `KIS_ACTIVE_ENV`
대응 credential 환경변수가 주입된 상태여야 한다.
- `backtest_matrix_seed_import.yaml``timeout: 30s`(per-step)를 사용해 KIS 응답 시간 변동에
여유를 둔다. 이 fixture는 backtest 실행·결과 조회·polling을 포함하지 않는다.
- `backtest_matrix_success.yaml`은 matrix 고유의 `timeout: 5s` 설정을 사용한다. Step 1
완료 후 data가 DB에 있으면 각 matrix run은 5초 이내에 succeeded로 종료된다.
- runner 구조 변경(matrix 내 seed step 자동 실행) 없이 handoff 절차로만 seed를 보장한다.
## Scheduler-backed backtest readiness 절차 (SDD S05)
`scheduled_backtest_readiness` scenario는 **seed import 없이** scheduler tick이 적재한
데이터로 backtest가 실행 가능한지를 원격 runner에서 증명한다. 따라서 matrix pre-seed
절차와 달리 `import_daily_bars` step을 두지 않는다. 첫 step은 `scheduler_refresh_status`
action으로 scheduler tick 성공과 적재 bar 수를 확인하고, 이어서 start/poll/result로
backtest가 exit code 0으로 succeeded에 도달함을 확인한다. 첫 step expect는
`scheduler_status: success`에 더해 `min_imported_bar_count: 1`을 강제하므로, scheduler
tick이 0 bar만 적재하면 runner가 mismatch(non-zero exit)로 실패시킨다. 즉 scheduler tick
성공과 적재 bar 수는 시각적 확인이 아니라 이 단일 scenario가 강제하는 PASS 조건이다.
**S05 gate의 universe와 date range는 fixture·handoff·field scheduler가 모두 동일하게
보장해야 한다(재현성 계약).** `scheduled_backtest_readiness.yaml`의 backtest step은 고정
universe `kr-backtest-smoke`(종목 `005930`, KRX)와 **최근 rolling backtest window**를 사용한다:
- `from_unix_ms: 1779753600000``2026-05-26 00:00:00Z`
- `to_unix_ms: 1781222400000``2026-06-12 00:00:00Z`
scheduler tick(`services/worker/internal/scheduler/runner.go`의 `RunTick`)은 import window를
`[windowTime-backfill_window, windowTime]`로 계산하고, KIS daily importer는
`services/worker/internal/providers/kis/live_client.go``fetchDomesticDailyBars`에서
**단발(non-paginated, `tr_cont=""`) 호출 1회**로 그 구간을 조회한다. KIS
`inquire-daily-itemchartprice`는 1회 호출에서 **최근 ~100거래일만** 반환하므로, tick이 적재할
수 있는 구간은 사실상 `[now backfill_window, now]`의 최근 데이터다. **임의의 먼 과거 구간(예:
1년 전)은 backfill_window를 키워도 적재되지 않는다**(100행에서 잘림). 따라서 S05 backtest
window는 schedule의 rolling backfill_window 안에 드는 **최근 구간**이어야 하며, 고정 먼 과거
구간으로 두면 backtest 단계가 데이터 없음으로 실패해 exit 0이 불가능하다. gate를 한참 뒤에
다시 돌릴 때는 위 window를 그 시점 기준 최근 구간으로 갱신한다.
**S05 live exit-0 gate는 `scheduled_backtest_readiness.yaml` 단일 scenario뿐이다.**
`scheduler_refresh_status.yaml`은 S05 live gate가 아니다. 그 fixture는 SDD S04용
deterministic 3-state(success/stale/error) expected-output 검증 fixture이며, single-schedule
live runner에는 stale/error schedule row가 없어 live 실행 시 mismatch로 exit `1`이 정상적으로
발생할 수 있다. 따라서 S04 fixture를 S05 final verification의 standalone exit-0 단계로
요구하지 않는다. S04 검증은 unit test와 expected-output 비교로 deterministic하게 수행한다
(아래 "scheduler_refresh_status S04 검증 노트" 참고).
### 실행 순서 (원격 runner, scheduler enabled)
```bash
# Precondition: scheduler enabled worker/API runtime를 띄운다. worker scheduler config가
# kr-daily 등 named universe로 enabled이고 최소 1회 tick을 완료한 상태여야 한다. scheduler
# tick을 강제로 트리거하는 별도 수단이 없으면 worker scheduler cadence에 맞춰 tick 완료를
# 기다린다. tick이 0 bar만 적재하면 아래 단일 명령의 refresh_status step이
# min_imported_bar_count expect로 mismatch(non-zero exit)된다.
# 단일 S05 명령: scheduler 적재 데이터로 readiness scenario를 실행한다 (seed import 없음).
go run ./cmd/alt operator scenario run \
--file testdata/operator/scheduled_backtest_readiness.yaml \
--api-url "$ALT_OPERATOR_API_URL" \
--output jsonl
# PASS 기준: refresh_status step이 scheduler_status=success 이고
# imported_bar_count >= 1 (min_imported_bar_count expect로 강제), backtest result
# step ok, summary status=ok 이며 process exit code 0.
```
- `$ALT_OPERATOR_API_URL`은 원격 API control plane endpoint(예: `ws://<host>:<port>/socket`)로,
실제 값과 credential은 이 문서에 기록하지 않는다.
- readiness scenario는 `kr-backtest-smoke` universe(005930)를 사용하므로 scheduler
schedule(kr-daily)이 해당 universe bar를 적재해 둔 상태여야 backtest가 deterministic
succeeded로 끝난다.
### S05 schedule config 계약 (재현성)
field scheduler config(repo에 committed된 고정 schedule YAML은 없고, 운영자가 field
runtime에서 주입한다)는 S05 gate를 재현하려면 아래를 모두 만족해야 한다. 환경별 endpoint·
credential·host 원문은 이 문서에 기록하지 않고 private testing-env 경계에서만 다룬다.
- `selector`: `kind: watchlist`, `market: kr`, `venue: krx`, `symbols: ["005930"]`,
`name`은 readiness scenario와 동일한 `kr-backtest-smoke` universe를 가리킨다.
- `timeframe: daily`, `provider: kis`, `cadence: daily`, `timezone: Asia/Seoul`.
- `backfill_window`: tick 기준 시각에서 backtest window(`2026-05-26..2026-06-12`)를 포함하도록
충분히 길되, KIS 단발 호출 100행 한도 안(대략 ≤140 calendar days)에 들어야 한다. 권장값
`90d`이면 mid-2026 tick의 import 범위 `[tick_date90d, tick_date]`가 위 backtest window를
포함하고, 100행 안에서 전부 반환된다.
- 위 조건이 만족되면 scheduler tick이 005930의 최근 일봉(위 window 포함)을 적재하고, readiness
scenario의 `refresh_status` step은 `scheduler_status=success``imported_bar_count>=1`을,
backtest step은 그 적재 데이터로 succeeded를 만족한다.
- fixture(`scheduled_backtest_readiness.yaml`)·handoff(이 문서)·field scheduler가 같은
universe와 같은 (rolling) date range를 가리켜야 한다는 계약은 동일하다. gate를 한참 뒤에 다시
돌릴 때는 backtest window와 backfill_window를 그 시점 기준 최근 구간으로 함께 갱신한다.
### scheduler_refresh_status S04 검증 노트
- `scheduler_refresh_status.yaml`은 success/stale/error 세 schedule을 모두 조회하는 SDD S04
deterministic fixture다. 이 검증은 live runner가 아니라 unit test와 expected-output
byte 비교로 수행한다: `cd apps/cli && go test ./internal/operator -run 'Test(RunScenarioRefreshStatus|ExpectedOutputFixtures)'`.
- single-schedule live runner에는 stale/error row가 없으므로 이 fixture를 그대로 live로
실행하면 mismatch로 exit `1`이 날 수 있다. 이는 S04 fixture의 정상 동작이며 S05 readiness
실패로 해석하지 않는다.
## Exit code 계약 보강 노트
- `invalid_request_matrix.yaml`은 typed error가 "도착하는지"를 검증하므로, 의도한
error code가 오면 step은 통과하고 run은 exit `0`으로 끝난다. 이는 API가 올바른
typed `ErrorInfo`를 돌려준다는 evidence다.
- 잘못된 fixture 자체(필수 step id 누락 등)의 non-zero exit는
`testdata/operator/malformed_scenario.yaml``alt operator scenario validate`
돌리면 exit code `2`로 재현된다.
## 월봉 aggregation 검증 노트 (SDD S04)
- `monthly_bars_aggregation` scenario는 `backtest-multi-timeframe-coverage` 마일스톤의
`monthly-bars` task와 SDD `backtest-loop/backtest-multi-timeframe-coverage` S04
(deterministic monthly OHLCV + provenance) evidence를 담당한다.
- worker/API의 월봉 aggregation core(`01_monthly_worker_core`)가 선행 완료되어야
하며, operator는 daily→monthly aggregation을 직접 재구현하지 않고
`aggregate_monthly_bars` action으로 API operation을 호출한다.
- 검증 명령: `cd apps/cli && go test ./internal/operator -run 'Test(RunScenarioAggregateMonthlyBars|ValidateAggregateMonthlyBars|MonthlyBarsFixture)'`.
`TestMonthlyBarsFixture``expected/monthly_bars_aggregation.jsonl`을 byte 단위로
비교해 evidence가 반복 실행에서 안정적임을 보장한다.
- 출력 evidence key: `provider`, `instrument_count`, `source_daily_bar_count`,
`monthly_bar_count`, `provenance_count`, `aggregation_rule_id`. 전체 provenance
payload는 API 응답에 남기고 operator 출력은 S04 증빙에 필요한 요약 필드만 노출한다.
## 남은 wireframe 의존성 요약
마일스톤 비범위(out-of-scope) 항목과 직접 연결된다:
- Flutter 실화면 구현 및 dashboard layout/navigation/card/chart/form 시각 디자인 확정.
- 위 matrix의 scenario별 "UI defer reason" 열에 적힌 화면 정의.
- push notification, production 운영 자동화는 이번 마일스톤 handoff 범위 밖이다.
이 화면 정의는 `Flutter Operator Console UX Plan` / `Flutter Operator Console MVP`에서
wireframe 또는 동등한 화면 정의 산출물과 사용자 승인 기준이 준비되어야 구현 잠금이
풀린다(운영 UI 구현 게이트).
## 원격 환경 동기화 및 마이그레이션 확인 절차
원격 runner(macOS host) 및 `code-server` 컨테이너 네트워크 내의 DB/Redis 인프라 정합성과 마이그레이션 확인은 아래 절차를 통해 검증한다. 환경 정보(IP, 비밀번호 등)는 노출하지 않는다.
1. **원격 호스트 및 코드 정합성 확인**
- macOS host와 로컬 code-server 컨테이너는 동일한 workspace 디렉토리를 마운트하여 사용하므로 코드는 동기화된다.
- git commit hash 및 dirty 파일 상태가 일치하는지 확인한다:
```bash
ssh <ALT_REMOTE_SSH> "cd <ALT_REMOTE_ALT_ROOT> && git rev-parse HEAD && git status --short"
```
2. **인프라(PostgreSQL / Redis) 헬스체크**
- docker compose를 통해 실행 중인 DB 및 Redis 서비스 컨테이너의 준비 상태를 점검한다:
```bash
ssh <ALT_REMOTE_SSH> "docker exec code-server-postgres pg_isready -U <DB_USER> -d <DB_NAME>"
ssh <ALT_REMOTE_SSH> "docker exec code-server-redis redis-cli ping"
```
3. **마이그레이션 적용 및 버전 확인**
- 현재 checkout 기준으로 데이터베이스 마이그레이션을 실행한다:
```bash
ssh <ALT_REMOTE_SSH> "docker exec code-server bash -lc 'cd <CONTAINER_ALT_ROOT>/services/worker && DATABASE_URL=<DATABASE_URL> REDIS_URL=<REDIS_URL> go run ./cmd/alt-worker-migrate'"
```
- schema_migrations 테이블에서 적용된 최신 마이그레이션 버전을 확인하여 일치하는지 점검한다:
```bash
ssh <ALT_REMOTE_SSH> "docker exec code-server-postgres psql -U <DB_USER> -d <DB_NAME> -t -A -c 'select max(version) from schema_migrations;'"
```