<!-- task=m-backtest-data-collection-infrastructure/03+02_gap_handling plan=1 tag=REVIEW_DATASET_GAP -->

# PLAN-cloud-G06 - Review Follow-up: Dataset Gap Counting

## 이 파일을 읽는 구현 에이전트에게

구현 완료의 마지막 단계는 active `CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증 명령을 실행하고 실제 stdout/stderr를 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log` 작성, archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 정책, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌 때문에 안전하게 진행할 수 없으면 `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 사항, 근거, 실행한 명령과 출력, 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 채팅 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`, `complete.log`, archive log를 직접 만들지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

첫 리뷰는 `apps/cli/internal/operator/runner.go:1281`의 gap 계산 double count 문제로 `FAIL` 판정됐다. 현재 구현은 bar가 0개인 matched instrument를 `missingSymbols`에 넣은 뒤, expected date 루프에서 matched instrument의 `cnt == 0`으로 한 번 세고 `missingSymbols` 전체를 다시 더해 한 번 더 세운다. 결과적으로 일부 symbol은 instrument로 매칭됐지만 bar만 비어 있는 정상적인 freshness-missing 케이스에서 `gap_count`와 `gap_status` 계약이 실제보다 과장될 수 있다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- Task ids:
  - `gap-handling`: 결측, 휴장일, 중복, provider 지연을 백테스트 입력에서 식별할 수 있게 한다.
- Completion mode: check-on-pass

## 범위 결정 근거

이번 후속은 기존 `collection_freshness` gap 계산의 카운팅 정확도와 그 regression test만 다룬다. scenario schema, output key 이름, fixture 계약, roadmap 문서는 새로 넓히지 않는다. 범위 밖 변경이 작업 트리에 있어도 이 후속의 수정 대상은 `apps/cli/internal/operator/runner.go`와 `apps/cli/internal/operator/runner_market_test.go`로 제한한다.

## 구현 체크리스트

- [ ] matched-zero-bars symbol과 unmatched symbol이 `expected_yyyymmdd` gap 계산에서 각 symbol/date 조합당 정확히 한 번만 세어지도록 `runCollectionFreshness`를 수정한다.
- [ ] `runner_market_test.go`에 matched instrument가 bar 0개를 반환하는 케이스와 unmatched symbol이 함께 있는 regression을 추가해 `gap_count`, `gap_yyyymmdd`, `missing_count`, `missing_symbols`, `gap_status`를 검증한다.
- [ ] `go test -count=1 ./apps/cli/internal/operator -run 'TestRunCollectionFreshness|TestRunScenarioCollectionFreshnessMissingSymbol'`를 실행하고 통과 출력을 기록한다.
- [ ] `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`를 실행하고 통과 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_DATASET_GAP-1] Zero-Bar Gap Count Regression

#### 문제

`runCollectionFreshness`는 matched instrument가 bar 0개를 반환하면 해당 symbol을 `missingSymbols`에 추가한다. 이후 gap 계산에서 matched instrument loop가 `cnt == 0`을 보고 gap을 1회 추가하고, 같은 expected date에 대해 `missingSymbols` loop가 다시 gap을 추가한다.

#### 해결 방법

gap 계산 입력을 symbol 단위로 정리한다. 한 가지 안전한 방식은 unmatched symbol만 별도 목록으로 유지하고, matched instrument는 `symbolDateCounts`에서만 계산하는 것이다. 또는 request symbol별로 단일 루프를 만들고 각 symbol/date 조합을 정확히 한 번만 평가한다. 어느 방식이든 matched-zero-bars symbol, unmatched symbol, partial date gap, duplicate date가 서로 중복 카운트되지 않아야 한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/cli/internal/operator/runner.go`: `missingSymbols` 재사용으로 인한 double count를 제거한다.
- [ ] `apps/cli/internal/operator/runner_market_test.go`: matched-zero-bars + unmatched symbol regression을 추가한다.

#### 테스트 작성

`runner_market_test.go`의 freshness tests에 다음 성격의 케이스를 추가한다.

- universe symbols: `005930`, `000660`, `000020`
- instrument list: `005930`, `000660`만 반환
- bars: `005930`은 expected date에 bar 1개, `000660`은 bar 0개
- expected dates: 단일 date 또는 두 date 중 delay가 섞이지 않는 범위
- expected assertions: `missing_count=2`, `missing_symbols=000020,000660`, `gap_status=gap`, `gap_count=2` for one expected date, `gap_yyyymmdd=<date>`; 같은 symbol/date가 중복되어 `gap_count=3` 이상이 되면 실패해야 한다.

#### 중간 검증

```bash
go test -count=1 ./apps/cli/internal/operator -run 'TestRunCollectionFreshness|TestRunScenarioCollectionFreshnessMissingSymbol'
```

예상: operator package PASS.

## 최종 검증

```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```

예상: 두 package 모두 PASS. Go test cache output은 허용하지 않는다. 반드시 `-count=1`로 fresh run을 기록한다.
