<!-- task=m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow plan=0 tag=API -->
# PLAN-cloud-G06: Backtest Selector Flow

## 이 파일을 읽는 구현 에이전트에게
이 계획은 `01_input_selector_contract`와 `02+01_storage_bar_selector`가 PASS되어 각각 `complete.log`를 만든 뒤에만 구현한다. 구현 후 반드시 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 최종 PASS, log rename, `complete.log`, archive 이동은 code-review skill 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 또는 범위 충돌 때문에 멈춰야 하면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청`에 증거를 채운 뒤 중단한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청 사유가 아니다.

## 배경
선행 subtasks가 selector 계약과 storage filtering을 만들더라도 async job payload와 operator scenario가 selector를 잃으면 실제 backtest run input까지 이어지지 않는다. 이 subtask는 start command, async run job, operator named universe, headless handoff fixture를 연결한다. PASS되면 현재 마일스톤의 `backtest-ready/input-selector` 기능 Task를 완료할 수 있다.

## 사용자 리뷰 요청 흐름
구현 중 사용자 전용 결정이나 범위 충돌이 생기면 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식 그대로 기록한다. 구현 에이전트는 채팅으로 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. code-review가 사용자 리뷰 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- Task ids:
  - `input-selector`: 저장된 정제 데이터에서 종목군, 기간, frequency를 선택해 backtest run input으로 넘기는 경계를 만든다.
- Completion mode: check-on-pass

## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/domain-model-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-ops/rules/project/domain/domain-model/rules.md`
- `agent-ops/rules/project/domain/api/rules.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- `packages/domain/backtest/types.go`
- `services/worker/internal/jobs/backtest_starter.go`
- `services/worker/internal/jobs/backtest_starter_test.go`
- `services/worker/internal/jobs/backtest_jobs.go`
- `services/worker/internal/jobs/backtest_jobs_test.go`
- `services/worker/internal/backtest/flow_test.go`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/runner_backtest_test.go`
- `apps/cli/internal/operator/client.go`
- `apps/cli/internal/operator/client_test.go`
- `apps/cli/internal/operator/handoff_test.go`
- `apps/cli/testdata/operator/backtest_run_request.yaml`
- `apps/cli/testdata/operator/backtest_result_summary.yaml`
- `apps/cli/testdata/operator/backtest_run_polling.yaml`
- `apps/cli/testdata/operator/invalid_request_matrix.yaml`
- `apps/cli/testdata/operator/headless_validation.md`

### 테스트 환경 규칙
- `test_env=local`로 판단했다.
- `agent-test/local/rules.md`가 존재해 읽었다.
- 매칭 profile로 `operations-smoke.md`, `worker-smoke.md`, `api-smoke.md`, `contracts-smoke.md`, `domain-model-smoke.md`를 읽었다.
- operations/headless 변경이므로 CLI operator tests와 fixture handoff tests를 포함한다.
- worker async job payload 변경이므로 worker jobs/backtest tests를 포함한다.
- `go test -count=1` fresh 실행만 허용하고 test cache 출력은 허용하지 않는다.
- 현재 기준으로 최종 검증 후보 `go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator ./apps/cli/internal/cli`가 exit 0임을 확인했다.

### 테스트 커버리지 공백
- `RunBacktestPayload`는 strategy/market/timeframe/date만 담고 selector를 보존하지 않는다.
- `RegisterRunBacktestHandler`는 payload에서 spec을 재구성하며 기존 run spec을 덮어쓰므로 selector가 사라질 수 있다.
- operator `start_backtest`는 named universe를 지원하지 않고 selector를 `BacktestRunSpec`에 넣지 않는다.
- headless fixtures는 import 후 start가 같은 universe를 backtest input selector로 넘기는지 문서화하지 않는다.

### 심볼 참조
- renamed/removed symbol: none.
- `RunBacktestPayload` references: `services/worker/internal/jobs/backtest_starter.go:80`, `services/worker/internal/jobs/backtest_jobs.go:20`, `services/worker/internal/jobs/backtest_jobs.go:41`, `services/worker/internal/jobs/backtest_jobs.go:90`, `services/worker/internal/jobs/backtest_starter_test.go:66`, `services/worker/internal/jobs/backtest_jobs_test.go:350`.
- `ActionStartBacktest` references: `apps/cli/internal/operator/scenario.go:35`, `apps/cli/internal/operator/scenario.go:638`, `apps/cli/internal/operator/runner.go:276`, `apps/cli/internal/operator/runner_backtest_test.go:27`, `apps/cli/internal/operator/runner_backtest_test.go:92`.

### 분할 판단
- split decision policy를 평가했고 multi-plan을 선택했다.
- shared task group: `agent-task/m-backtest-data-collection-infrastructure/`
- 이 subtask: `03+01,02_backtest_selector_flow`.
- predecessor `01_input_selector_contract`: PASS 완료. 증거: `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log`.
- predecessor `02+01_storage_bar_selector`: 아직 active plan/review 상태이며 `complete.log` 미생성. 구현 시작 전 active 또는 archived complete.log가 필요하다.

### 범위 결정 근거
- 새로운 universe persistence나 DB schema는 만들지 않는다. CLI scenario root `universes`를 start request selector로 materialize하는 범위까지만 다룬다.
- paper trading start flow에는 selector requirement를 강제하지 않는다. `BacktestRunSpec`은 paper trading도 재사용하므로 이 subtask의 CLI validation은 `ActionStartBacktest`에 한정한다.
- 실제 provider import 동작은 기존 `import_daily_bars`를 사용하고, provider client나 KIS adapter는 변경하지 않는다.

### 빌드 등급
- build lane: `cloud-G06`. async job payload, worker execution handoff, CLI scenario schema, headless fixtures를 함께 건드리는 cross-surface 변경이므로 G06로 잡는다.

## 의존 관계 및 구현 순서
- `01_input_selector_contract`와 `02+01_storage_bar_selector` 완료가 선행이다.
- `01_input_selector_contract`는 PASS 완료되었고, `02+01_storage_bar_selector`는 아직 active plan/review 상태다.
- 구현 시작 전에 `02+01_storage_bar_selector`의 active 또는 archived `complete.log`가 없으면 이 plan을 구현하지 말고 선행 subtask를 먼저 완료한다.
- 구현 순서: worker job payload 보존 -> CLI scenario/runner selector 생성 -> fixtures/handoff 갱신 -> 전체 검증.

## 구현 체크리스트
- [ ] `RunBacktestPayload`가 selector를 encode/decode하고 handler가 실행 spec에 복원하도록 구현한다.
- [ ] operator `start_backtest`가 universe/symbol/instrument_ids selector를 검증하고 `BacktestRunSpec.selector`로 전송하도록 구현한다.
- [ ] headless backtest fixtures와 handoff 문서가 import universe -> start selector -> poll/result 흐름을 증명하도록 갱신한다.
- [ ] 지정 `go test -count=1` 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Async Run Payload Selector Preservation
#### 문제
starter payload와 job handler spec 재구성이 selector를 포함하지 않는다.

```go
// services/worker/internal/jobs/backtest_starter.go:80
payload := RunBacktestPayload{
	RunID:      string(run.ID),
	StrategyID: string(spec.StrategyID),
	Market:     string(spec.Market),
	Timeframe:  string(spec.Timeframe),
	From:       spec.From.UTC().Format(time.RFC3339),
	To:         spec.To.UTC().Format(time.RFC3339),
}
```

```go
// services/worker/internal/jobs/backtest_jobs.go:99
spec := backtest.RunSpec{
	StrategyID: backtest.StrategyID(p.StrategyID),
	Market:     market.Market(p.Market),
	Timeframe:  market.Timeframe(p.Timeframe),
	From:       fromTime,
	To:         toTime,
}
```

#### 해결 방법
payload에 selector DTO를 추가하고 blank entry validation을 넣는다. handler가 payload selector를 `backtest.RunSpec.Selector`에 복원한 뒤 executor에 전달한다.

```go
type RunBacktestSelectorPayload struct {
	InstrumentIDs []string `json:"instrument_ids,omitempty"`
	Symbols        []string `json:"symbols,omitempty"`
}

type RunBacktestPayload struct {
	RunID      string                     `json:"run_id"`
	StrategyID string                     `json:"strategy_id"`
	Market     string                     `json:"market"`
	Timeframe  string                     `json:"timeframe"`
	From       string                     `json:"from"`
	To         string                     `json:"to"`
	Selector   RunBacktestSelectorPayload `json:"selector,omitempty"`
}
```

#### 수정 파일 및 체크리스트
- [ ] `services/worker/internal/jobs/backtest_jobs.go`에 selector payload type, decode validation, domain conversion helper를 추가한다.
- [ ] `services/worker/internal/jobs/backtest_starter.go`가 `spec.Selector`를 payload에 encode하도록 한다.
- [ ] `services/worker/internal/jobs/backtest_starter_test.go`가 decoded payload selector를 assertion한다.
- [ ] `services/worker/internal/jobs/backtest_jobs_test.go`가 executor가 받은 run spec selector와 blank selector reject를 assertion한다.

#### 테스트 작성
- `TestBacktestStarterRecordsPendingAndLaunches`에 selector payload assertion 추가.
- `TestRegisterRunBacktestHandlerTransitionsSucceeded`에 `executor.run.Spec.Selector` assertion 추가.
- `TestDecodeRunBacktestPayloadRejectsMissingFields` 또는 새 test에 blank selector entry reject 추가.

#### 중간 검증
```bash
go test -count=1 ./services/worker/internal/jobs ./services/worker/internal/backtest
```
예상 결과: jobs/backtest tests exit 0.

### [API-2] Operator Start Backtest Selector
#### 문제
operator scenario root에는 universe가 있지만 `start_backtest`는 universe를 적용하지 않고 raw market/timeframe/date만 보낸다.

```go
// apps/cli/internal/operator/scenario.go:495
func applyUniverseDefaults(step *Step, universeByName map[string]UniverseConfig) error {
	if step.Action != ActionImportDailyBars && step.Action != ActionCollectionFreshness {
		return nil
	}
```

```go
// apps/cli/internal/operator/runner.go:276
case ActionStartBacktest:
	resp, err := client.StartBacktest(stepCtx, &altv1.StartBacktestRequest{
		Spec: &altv1.BacktestRunSpec{
			StrategyId: step.Request.StrategyID,
			Market:     marketByName[step.Request.Market],
```

#### 해결 방법
`Request`에 repeated `instrument_ids`를 추가하고 `start_backtest`가 `universe`, `symbols`, 또는 `instrument_ids` 중 하나를 요구하게 한다. `universe`는 scenario root에서 market/symbols를 materialize한다. runner는 `BacktestInputSelector`를 만들어 `BacktestRunSpec.selector`에 넣는다.

#### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/scenario.go`의 `Request`에 `InstrumentIDs []string yaml:"instrument_ids"`를 추가한다.
- [ ] `applyUniverseDefaults`가 `ActionStartBacktest`에도 universe defaults를 적용하도록 확장한다.
- [ ] `ActionStartBacktest` validation에 selector requirement와 blank `symbols`/`instrument_ids` reject를 추가한다.
- [ ] `apps/cli/internal/operator/runner.go`에 start selector helper를 추가하고 `BacktestRunSpec.selector`를 설정한다.
- [ ] `apps/cli/internal/operator/scenario_test.go`에 valid universe start와 missing selector reject를 추가한다.
- [ ] `apps/cli/internal/operator/runner_backtest_test.go`가 fake API에 전달된 selector를 assertion한다.

#### 테스트 작성
- `TestValidateStartBacktestUniverseReference`: universe가 market/symbols로 materialize되는지 검증.
- `TestValidateStartBacktestRejectsMissingSelector`: selector 없는 start가 dry-run에서 실패하는지 검증.
- `TestRunBacktestRequestOutputsRunID` 또는 별도 test: fake API가 `spec.selector.symbols=["005930"]`를 받는지 검증.

#### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator
```
예상 결과: operator scenario/runner tests exit 0.

### [API-3] Headless Fixtures And Handoff Evidence
#### 문제
현재 fixtures는 import와 start가 같은 종목군을 공유한다는 것을 request로 증명하지 않는다.

```yaml
# apps/cli/testdata/operator/backtest_run_polling.yaml:19
- id: start
  action: start_backtest
  request:
    strategy_id: strategy-v1
    market: kr
    timeframe: daily
```

#### 해결 방법
backtest 관련 fixtures에 root `universes`를 두고 `import_daily_bars`와 `start_backtest`가 같은 universe를 참조하게 한다. `invalid_request_matrix`는 missing-strategy typed error를 유지해야 하므로 selector를 추가하되 strategy id는 nonexistent로 둔다. `headless_validation.md`의 checked field는 import universe와 start selector handoff를 명시한다.

#### 수정 파일 및 체크리스트
- [ ] `apps/cli/testdata/operator/backtest_run_request.yaml`에 universe를 추가하고 import/start가 이를 참조하게 한다.
- [ ] `apps/cli/testdata/operator/backtest_result_summary.yaml`에 universe를 추가하고 import/start가 이를 참조하게 한다.
- [ ] `apps/cli/testdata/operator/backtest_run_polling.yaml`의 start request가 `universe: kr-backtest-smoke`를 사용하게 한다.
- [ ] `apps/cli/testdata/operator/invalid_request_matrix.yaml`의 `start_backtest_missing_strategy` request에 selector를 추가해 API typed error 경로를 유지한다.
- [ ] `apps/cli/testdata/operator/headless_validation.md` checked field에 `StartBacktestRequest.spec.selector` 증거를 추가한다.
- [ ] expected JSONL fixture는 output key가 바뀌지 않으면 수정하지 않는다.

#### 테스트 작성
- 기존 handoff tests가 fixture 존재와 promised keys를 검증한다.
- selector가 출력되지 않는 내부 request field라 expected JSONL key 추가는 하지 않는다.

#### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상 결과: fixture/handoff tests 포함 exit 0.

## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/worker/internal/jobs/backtest_jobs.go` | API-1 |
| `services/worker/internal/jobs/backtest_jobs_test.go` | API-1 |
| `services/worker/internal/jobs/backtest_starter.go` | API-1 |
| `services/worker/internal/jobs/backtest_starter_test.go` | API-1 |
| `apps/cli/internal/operator/scenario.go` | API-2 |
| `apps/cli/internal/operator/scenario_test.go` | API-2 |
| `apps/cli/internal/operator/runner.go` | API-2 |
| `apps/cli/internal/operator/runner_backtest_test.go` | API-2 |
| `apps/cli/testdata/operator/backtest_run_request.yaml` | API-3 |
| `apps/cli/testdata/operator/backtest_result_summary.yaml` | API-3 |
| `apps/cli/testdata/operator/backtest_run_polling.yaml` | API-3 |
| `apps/cli/testdata/operator/invalid_request_matrix.yaml` | API-3 |
| `apps/cli/testdata/operator/headless_validation.md` | API-3 |

## 최종 검증
```bash
go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상 결과: 모든 package exit 0. Go test cache 출력은 허용하지 않는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
