# 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 ` - run(실 API 연결): `alt operator scenario run --file --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 종목 `20240527–20240528` 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://:/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_date−90d, 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 "cd && git rev-parse HEAD && git status --short" ``` 2. **인프라(PostgreSQL / Redis) 헬스체크** - docker compose를 통해 실행 중인 DB 및 Redis 서비스 컨테이너의 준비 상태를 점검한다: ```bash ssh "docker exec code-server-postgres pg_isready -U -d " ssh "docker exec code-server-redis redis-cli ping" ``` 3. **마이그레이션 적용 및 버전 확인** - 현재 checkout 기준으로 데이터베이스 마이그레이션을 실행한다: ```bash ssh "docker exec code-server bash -lc 'cd /services/worker && DATABASE_URL= REDIS_URL= go run ./cmd/alt-worker-migrate'" ``` - schema_migrations 테이블에서 적용된 최신 마이그레이션 버전을 확인하여 일치하는지 점검한다: ```bash ssh "docker exec code-server-postgres psql -U -d -t -A -c 'select max(version) from schema_migrations;'" ```