<!-- task=m-backtest-data-collection-infrastructure/02+01_storage_bar_selector plan=0 tag=REFACTOR -->
# PLAN-cloud-G05: Storage Bar Selector

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

## 배경
현재 backtest engine은 market 전체 instrument의 bar를 가져온다. `input-selector`를 완성하려면 저장된 정제 데이터에서 선택된 instrument/symbol만 읽어야 한다. 이 subtask는 worker 내부 storage-backed bar source와 engine 경계에 selector 동작을 적용한다.

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

## 분석 결과
### 읽은 파일
- `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/worker/rules.md`
- `agent-ops/rules/project/domain/domain-model/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`
- `packages/domain/market/types.go`
- `services/worker/internal/backtest/bar_source.go`
- `services/worker/internal/backtest/bar_source_test.go`
- `services/worker/internal/backtest/engine.go`
- `services/worker/internal/backtest/engine_test.go`
- `services/worker/internal/backtest/flow_test.go`
- `services/worker/internal/backtest/fixture_test.go`
- `services/worker/internal/jobs/backtest_jobs.go`
- `services/worker/internal/jobs/backtest_jobs_test.go`
- `services/worker/internal/jobs/backtest_starter.go`
- `services/worker/internal/jobs/backtest_starter_test.go`
- `services/worker/internal/storage/ports.go`
- `services/worker/internal/storage/postgres/mapping.go`
- `services/worker/cmd/alt-worker/main.go`

### 테스트 환경 규칙
- `test_env=local`로 판단했다.
- `agent-test/local/rules.md`가 존재해 읽었다.
- 매칭 profile로 `worker-smoke.md`, `domain-model-smoke.md`, `operations-smoke.md`를 읽었다.
- worker/domain 변경이므로 `go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs`를 필수로 사용한다.
- `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임을 확인했다.

### 테스트 커버리지 공백
- 기존 `TestStorageBarSourceReadsMarketBars`는 market filter와 정렬만 검증한다.
- selector가 instrument id, symbol/provider symbol로 resolve되고 누락 selector를 error로 돌려주는 테스트가 없다.
- `Engine.Execute`가 `RunSpec.Selector`를 bar source까지 넘기는 테스트가 없다.
- 저장소 기반 command flow가 선택되지 않은 같은 market instrument를 배제하는 테스트가 없다.

### 심볼 참조
- renamed/removed symbol: none.
- `StorageBarSource.GetBars` call sites: `services/worker/internal/backtest/bar_source_test.go:82`, `services/worker/internal/backtest/bar_source_test.go:136`, `services/worker/internal/backtest/engine.go:47`, `services/worker/internal/papertrading/engine.go:70`.
- `BarSource` interface call sites: `services/worker/internal/backtest/engine.go:15`, `services/worker/internal/backtest/engine_test.go:16`, `services/worker/internal/backtest/fixture_test.go:47`.

### 분할 판단
- split decision policy를 평가했고 multi-plan을 선택했다.
- shared task group: `agent-task/m-backtest-data-collection-infrastructure/`
- 이 subtask: `02+01_storage_bar_selector`.
- predecessor `01_input_selector_contract`: PASS 완료. 증거: `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log`.
- sibling `03+01,02_backtest_selector_flow`는 이 subtask 완료 후 진행한다.

### 범위 결정 근거
- proto/domain selector shape는 선행 `01_input_selector_contract` 범위이므로 여기서 재설계하지 않는다.
- CLI scenario universe handoff는 후속 `03+01,02_backtest_selector_flow` 범위다.
- paper trading engine의 public behavior는 변경하지 않는다. `StorageBarSource.GetBars`를 유지해 paper trading compatibility를 보존한다.

### 빌드 등급
- build lane: `cloud-G05`. worker 내부 execution/storage 경계 변경이고 public proto 변경은 선행 plan에 있으므로 G05로 잡는다.

## 의존 관계 및 구현 순서
- `01_input_selector_contract` 완료가 선행이며, 현재 PASS 완료되어 이 plan 구현을 시작할 수 있다.
- 선행 완료 증거: `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log`.

## 구현 체크리스트
- [ ] backtest engine 전용 RunSpec 기반 bar source 경계를 추가하고 paper trading용 `GetBars` compatibility를 유지한다.
- [ ] `StorageBarSource`가 selector instrument id/symbol을 resolve해 선택된 저장 bar만 반환하도록 구현한다.
- [ ] worker unit/flow tests가 selector 적용, missing selector error, 기존 full-market fallback을 검증하도록 갱신한다.
- [ ] 지정 `go test -count=1` 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Backtest Engine RunSpec Bar Boundary
#### 문제
backtest engine은 bar source에 market/timeframe/date만 넘긴다. selector가 domain spec에 추가되어도 engine이 넘기지 않으면 실행에 반영되지 않는다.

```go
// services/worker/internal/backtest/engine.go:14
type BarSource interface {
	GetBars(ctx context.Context, mkt market.Market, timeframe market.Timeframe, from, to time.Time) ([]market.Bar, error)
}
```

```go
// services/worker/internal/backtest/engine.go:47
bars, err := e.barSource.GetBars(ctx, run.Spec.Market, run.Spec.Timeframe, run.Spec.From, run.Spec.To)
```

#### 해결 방법
backtest package의 engine-facing interface를 `RunSpec` 기반으로 바꾼다. `StorageBarSource.GetBars`는 paper trading compatibility용으로 유지하고, backtest engine용 method를 추가한다.

```go
type BarSource interface {
	GetBarsForRun(ctx context.Context, spec backtest.RunSpec) ([]market.Bar, error)
}
```

```go
bars, err := e.barSource.GetBarsForRun(ctx, run.Spec)
```

#### 수정 파일 및 체크리스트
- [ ] `services/worker/internal/backtest/engine.go`의 `BarSource` interface와 호출부를 `GetBarsForRun(ctx, run.Spec)`로 변경한다.
- [ ] `services/worker/internal/backtest/engine_test.go` mock이 받은 `RunSpec.Selector`를 assertion하도록 갱신한다.
- [ ] `services/worker/internal/backtest/fixture_test.go`의 test source interface 구현을 새 method에 맞춘다.
- [ ] `services/worker/internal/papertrading/engine.go`는 변경하지 않는다.

#### 테스트 작성
- `engine_test.go`에 `TestEnginePassesRunSpecToBarSource` 또는 기존 engine test assertion을 추가한다.
- 목적: `RunSpec.Selector`가 engine에서 손실되지 않고 bar source에 전달되는지 검증한다.

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

### [REFACTOR-2] Storage Selector Resolution
#### 문제
storage-backed source는 market 전체 instrument를 순회한다.

```go
// services/worker/internal/backtest/bar_source.go:29
instruments, err := s.instruments.ListInstruments(ctx)
if err != nil {
	return nil, fmt.Errorf("backtest storage bar source: list instruments: %w", err)
}

var out []market.Bar
for _, inst := range instruments {
	if inst.Market != mkt {
		continue
	}
	bars, err := s.bars.GetBars(ctx, inst.ID, timeframe, from, to)
```

#### 해결 방법
`GetBarsForRun`에서 `RunSpec.Selector`를 해석한다.
- selector가 비어 있으면 기존처럼 market 전체를 사용한다.
- `InstrumentIDs`는 exact `market.Instrument.ID`와 비교한다.
- `Symbols`는 `Instrument.Symbol`과 `Instrument.ProviderSymbols` value 모두와 비교한다.
- selector가 일부라도 resolve되지 않으면 error를 반환해 잘못된 input으로 조용히 실행하지 않는다.
- 반환 bar 정렬은 timestamp, instrument id tie-breaker를 유지한다.

#### 수정 파일 및 체크리스트
- [ ] `services/worker/internal/backtest/bar_source.go`에 `GetBarsForRun`과 selector resolution helper를 추가한다.
- [ ] 기존 `GetBars(ctx, mkt, timeframe, from, to)`는 유지하고 내부 helper를 공유한다.
- [ ] error message는 `backtest storage bar source:` prefix를 유지한다.
- [ ] selector 비교 전후로 whitespace-only 값은 선행 mapping에서 reject되지만, 방어적으로 trim해서 missing 처리한다.

#### 테스트 작성
- `services/worker/internal/backtest/bar_source_test.go`
  - `TestStorageBarSourceSelectsInstrumentIDs`: 같은 market instrument 2개 중 선택 id만 반환.
  - `TestStorageBarSourceSelectsSymbols`: `Instrument.Symbol` 또는 provider symbol로 선택.
  - `TestStorageBarSourceRejectsMissingSelector`: 요청 selector가 resolve되지 않으면 error.
  - 기존 full-market tests는 유지해 empty selector fallback을 검증한다.

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

### [REFACTOR-3] Worker Command Flow Evidence
#### 문제
현재 storage-backed command flow는 단일 instrument만 심어 두어서 selector가 실제 실행 입력을 줄였는지 증명하지 못한다.

```go
// services/worker/internal/backtest/flow_test.go:140
instID := market.InstrumentID("KRX:005930")
if err := store.UpsertInstrument(ctx, market.Instrument{ID: instID, Market: market.MarketKR}); err != nil {
	t.Fatalf("upsert instrument: %v", err)
}
```

#### 해결 방법
flow test에 같은 market의 미선택 instrument와 bars를 추가하고 run spec에는 선택 selector만 넣는다. 결과의 trades/equity/trade count가 선택 instrument bars만 처리한 값임을 확인한다.

#### 수정 파일 및 체크리스트
- [ ] `services/worker/internal/backtest/flow_test.go`에 second KR instrument와 bars를 seed한다.
- [ ] `spec.Selector`에 선택 instrument id 또는 symbol을 넣는다.
- [ ] result assertions가 미선택 instrument bar가 처리되지 않았음을 보여주도록 기대값을 유지하거나 명시 assertion을 추가한다.
- [ ] `services/worker/internal/jobs/backtest_jobs_test.go`는 payload decode가 selector를 포함하지 않는 상태라면 `03`에서 다룬다. 이 plan에서는 engine/storage behavior에 집중한다.

#### 테스트 작성
- 기존 `TestBacktestCommandFlowFromImportedBars`를 확장한다.
- 새 별도 flow test를 만들 경우 이름은 `TestBacktestCommandFlowUsesInputSelector`로 한다.

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

## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/worker/internal/backtest/engine.go` | REFACTOR-1 |
| `services/worker/internal/backtest/engine_test.go` | REFACTOR-1 |
| `services/worker/internal/backtest/fixture_test.go` | REFACTOR-1 |
| `services/worker/internal/backtest/bar_source.go` | REFACTOR-2 |
| `services/worker/internal/backtest/bar_source_test.go` | REFACTOR-2 |
| `services/worker/internal/backtest/flow_test.go` | REFACTOR-3 |

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

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