feat: backtest scenario automation 구현

- cli: scenario 실행 명령어 추가
- operator: scenario 처리 로직 구현 및 테스트
This commit is contained in:
toki 2026-06-15 21:13:32 +09:00
parent 7b2dfe6751
commit d770a195ff
13 changed files with 1955 additions and 6 deletions

View file

@ -0,0 +1,140 @@
<!-- task=m-backtest-scenario-automation/01_scenario_matrix plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.
## 개요
date=2026-06-15
task=m-backtest-scenario-automation/01_scenario_matrix, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `scenario-matrix`: universe, 기간, frequency, strategy 조합을 YAML scenario에서 표현하고 검증할 수 있게 한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 판정 append, active 파일 log rename, PASS 시 `complete.log` 작성과 archive 이동까지 끝난 상태를 의미합니다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Matrix Schema And Validation | [x] |
| [API-2] Deterministic Matrix Expansion | [x] |
| [API-3] CLI Validate Output For Matrix | [x] |
## 구현 체크리스트
- [x] `Scenario`에 optional backtest matrix schema를 추가하고 matrix-only scenario를 dry-run validation 대상으로 허용한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [x] matrix combination expansion helper를 deterministic order로 추가하고 validation tests를 작성한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [x] CLI `operator scenario validate`가 matrix-only scenario를 유효하게 보고하는 test를 추가한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_0.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_0.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
계획에 맞춰 그대로 진행하였으며 계획 대비 변경 사항은 없습니다.
## 주요 설계 결정
- `Scenario` 구조체에 `Matrix *BacktestScenarioMatrix` 필드를 optional 형태로 추가하여, steps가 비어있어도 matrix 조합이 있다면 유효하도록 `Validate` 함수를 개선하였습니다.
- `Matrix` 조합 ID 생성을 `universe -> strategy -> timeframe -> period` 순으로 고정하여 결정적인(deterministic) 순서를 가지도록 nested 루프로 구현하였고, 조합된 ID에 공백이 포함되거나 중복 ID가 발생하는 경우 validate 단계에서 거절하도록 validation을 강화했습니다.
- CLI `operator scenario validate` 시 matrix-only scenario인 경우, output에 `matrix_runs=<n>`이 출력되도록 `cli.go`를 개선하였습니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Matrix-only scenario가 기존 step-only scenario를 깨지 않는지 확인한다.
- `MatrixRuns` ordering과 duplicate ID validation이 deterministic인지 확인한다.
- CLI validate output이 기존 tests와 scripts에 호환되는지 확인한다.
## 검증 결과
### API-1 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.192s
ok git.toki-labs.com/toki/alt/apps/cli/internal/cli 0.009s
```
### API-2 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.192s
ok git.toki-labs.com/toki/alt/apps/cli/internal/cli 0.009s
```
### API-3 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.210s
ok git.toki-labs.com/toki/alt/apps/cli/internal/cli 0.008s
```
### 최종 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.210s
ok git.toki-labs.com/toki/alt/apps/cli/internal/cli 0.008s
```
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- Correctness: Fail
- Completeness: Fail
- Test coverage: Fail
- API contract: Fail
- Code quality: Pass
- Plan deviation: Pass
- Verification trust: Pass
- 발견된 문제:
- Required: `apps/cli/internal/operator/scenario.go:528`의 matrix period 검증이 `from_unix_ms`와 `to_unix_ms` 누락을 거르지 않습니다. YAML에서 둘 중 하나가 빠지면 Go zero value `0`으로 들어오는데, 현재는 `from_unix_ms > to_unix_ms`만 검사하므로 `from_unix_ms: 0, to_unix_ms: 200` 또는 `to_unix_ms` 누락 케이스가 valid matrix run으로 확장됩니다. 기존 `start_backtest` request 검증은 두 값을 모두 필수로 취급하고 있어, 02 batch runner가 이 helper를 사용하면 dry-run validation을 통과한 invalid backtest run을 만들 수 있습니다. `MatrixPeriod` 검증에 `FromUnixMs == 0 || ToUnixMs == 0` 거부를 추가하고, `apps/cli/internal/operator/scenario_test.go:1448`의 invalid matrix table에 missing `from_unix_ms`, missing `to_unix_ms` 케이스를 추가하세요.
- 다음 단계:
- FAIL follow-up: `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.md`를 작성해 period 필수값 검증과 회귀 테스트만 보완한다.

View file

@ -0,0 +1,154 @@
<!-- task=m-backtest-scenario-automation/01_scenario_matrix plan=1 tag=REVIEW_API -->
# Code Review Reference - REVIEW_API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-15
task=m-backtest-scenario-automation/01_scenario_matrix, plan=1, tag=REVIEW_API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `scenario-matrix`: universe, 기간, frequency, strategy 조합을 YAML scenario에서 표현하고 검증할 수 있게 한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-{review_lane}-GNN.md` -> `code_review_{review_lane}_GNN_N.log`, `PLAN-{build_lane}-GNN.md` -> `plan_{build_lane}_GNN_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] Matrix Period Required Timestamps | [x] |
## 구현 체크리스트
- [x] Matrix period validation이 `from_unix_ms` 또는 `to_unix_ms` 누락을 거부하도록 보강한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [x] invalid matrix tests에 missing `from_unix_ms`와 missing `to_unix_ms` 케이스를 추가한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G07_1.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G07_1.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-backtest-scenario-automation/01_scenario_matrix/`를 `agent-task/archive/2026/06/m-backtest-scenario-automation/01_scenario_matrix/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-backtest-scenario-automation`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-backtest-scenario-automation/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
계획에 맞춰 그대로 진행하였으며 계획 대비 변경 사항은 없습니다.
## 주요 설계 결정
- `MatrixPeriod` validation 시 `from_unix_ms` 및 `to_unix_ms`가 Go zero value(0)인 경우 YAML에서 값이 생략된 것으로 취급하여 검증 에러로 거부하도록 구현하였습니다. 이는 기존 `start_backtest` request의 timestamp 필수 조건과 일관성을 유지하기 위함입니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Matrix period에서 `from_unix_ms == 0` 또는 `to_unix_ms == 0`이 validation error인지 확인한다.
- 기존 reversed period와 duplicate generated ID validation이 유지되는지 확인한다.
- 최종 검증 출력이 실제 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli` 실행 결과인지 확인한다.
## 검증 결과
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### REVIEW_API-1 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.187s
ok git.toki-labs.com/toki/alt/apps/cli/internal/cli 0.007s
```
### 최종 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.187s
ok git.toki-labs.com/toki/alt/apps/cli/internal/cli 0.007s
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
Sections and their ownership:
| 섹션 | 소유자 | 설명 |
|------|--------|------|
| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 |
| Roadmap Targets | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음; PASS 시 code-review가 `complete.log`의 `Roadmap Completion`으로 복사 |
| 구현 항목별 완료 여부 (항목명) | 스텁 생성 시 고정 | `[ ]` -> `[x]` 체크만 구현 에이전트가 수행 |
| 구현 체크리스트 (항목 텍스트/순서) | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` -> `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수 |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트가 채움 | placeholder 텍스트를 실제 내용으로 교체 |
| 사용자 리뷰 요청 | 구현 에이전트가 채움 | 진행에 사용자 입력이 필요하지 않으면 `상태: 없음` 유지 |
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 |
| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움 |
| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 |
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- Correctness: Pass
- Completeness: Pass
- Test coverage: Pass
- API contract: Pass
- Code quality: Pass
- Plan deviation: Pass
- Verification trust: Pass
- 발견된 문제: 없음
- 다음 단계:
- PASS: `complete.log`를 작성하고 task directory를 archive로 이동한다.

View file

@ -0,0 +1,44 @@
# Complete - m-backtest-scenario-automation/01_scenario_matrix
## 완료 일시
2026-06-15
## 요약
Backtest scenario matrix schema, deterministic expansion, CLI validate output, and required period timestamp validation completed after 2 review loops; final verdict PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | Matrix period missing `from_unix_ms` / `to_unix_ms` validation was required. |
| `plan_local_G07_1.log` | `code_review_local_G07_1.log` | PASS | Required timestamp validation and missing-field tests were added. |
## 구현/정리 내용
- Added optional backtest matrix schema to operator scenarios and allowed matrix-only validation.
- Added deterministic matrix expansion with generated run IDs.
- Added CLI validate `matrix_runs=<n>` output for matrix scenarios.
- Added required matrix period `from_unix_ms` / `to_unix_ms` validation and regression tests.
## 최종 검증
- `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli` - PASS; output:
`ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.207s`
`ok git.toki-labs.com/toki/alt/apps/cli/internal/cli 0.011s`
## Roadmap Completion
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Completed task ids:
- `scenario-matrix`: PASS; evidence=`agent-task/archive/2026/06/m-backtest-scenario-automation/01_scenario_matrix/plan_local_G06_0.log`, `agent-task/archive/2026/06/m-backtest-scenario-automation/01_scenario_matrix/code_review_local_G06_0.log`, `agent-task/archive/2026/06/m-backtest-scenario-automation/01_scenario_matrix/plan_local_G07_1.log`, `agent-task/archive/2026/06/m-backtest-scenario-automation/01_scenario_matrix/code_review_local_G07_1.log`; verification=`go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,277 @@
<!-- task=m-backtest-scenario-automation/01_scenario_matrix plan=0 tag=API -->
# Plan - API Scenario Matrix
## 이 파일을 읽는 구현 에이전트에게
이 plan의 구현이 끝나면 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 내용과 증거를 남기고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 명령 재실행이나 산출물 수집으로 해소 가능한 evidence 공백은 사용자 리뷰 요청이 아니다.
## 배경
`Backtest Scenario Automation` 마일스톤은 universe, 기간, frequency/timeframe, strategy 조합을 YAML scenario로 반복 실행할 수 있어야 한다. 현재 `apps/cli/internal/operator/scenario.go`는 단일 `steps` 목록과 named `universes`만 검증하며, 조합 matrix를 표현하거나 결정적으로 펼치는 구조가 없다. 이 subtask는 실행 없이 matrix schema, dry-run validation, expansion helper까지만 만든다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 prompt는 금지이며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `scenario-matrix`: universe, 기간, frequency, strategy 조합을 YAML scenario에서 표현하고 검증할 수 있게 한다.
- 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/rules/project/domain/operations/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- `apps/cli/go.mod`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/cli/cli.go`
- `apps/cli/internal/cli/cli_test.go`
- `apps/cli/testdata/operator/backtest_run_request.yaml`
- `apps/cli/testdata/operator/backtest_run_polling.yaml`
- `apps/cli/testdata/operator/backtest_result_summary.yaml`
### 테스트 환경 규칙
- 선택 env: `local`.
- `agent-test/local/rules.md`와 `agent-test/local/operations-smoke.md`를 읽었다.
- 적용 명령: milestone 검증에 명시된 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`.
- baseline 확인: 위 명령은 현재 checkout에서 exit 0으로 통과했다.
- `<확인 필요>` 값 없음. fallback 없음.
### 테스트 커버리지 공백
- matrix root schema, matrix period validation, matrix expansion ordering: 기존 테스트 없음. 새 tests 필요.
- 기존 universe 참조와 `start_backtest` selector 검증: `scenario_test.go`가 일부 커버한다.
- CLI `validate`의 matrix-only scenario 출력: 기존 테스트 없음. 새 tests 필요.
### 심볼 참조
- renamed/removed symbol: none.
### 분할 판단
- split policy를 먼저 평가했다.
- shared task group: `agent-task/m-backtest-scenario-automation/`.
- `01_scenario_matrix`: matrix schema/validation foundation, depends_on=[].
- `02+01_batch_runner`: matrix 실행 및 CLI output, depends_on=[01].
- `03+01,02_result_handoff`: fixtures/docs/handoff evidence, depends_on=[01,02].
- 이 subtask는 선행 의존성이 없다.
### 범위 결정 근거
- `services/api/**`, `services/worker/**`, `packages/contracts/**`, `packages/domain/**`는 제외한다. 이 subtask는 CLI YAML schema와 validation만 다루며 runtime protobuf 계약을 바꾸지 않는다.
- result fixture와 `headless_validation.md`는 03에서 다룬다.
- batch 실행, polling output 변경은 02에서 다룬다.
### 빌드 등급
- build: `local-G06`, review: `local-G06`.
- 이유: bounded CLI schema/test change이며 deterministic Go tests로 검증 가능하다.
## 구현 체크리스트
- [ ] `Scenario`에 optional backtest matrix schema를 추가하고 matrix-only scenario를 dry-run validation 대상으로 허용한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] matrix combination expansion helper를 deterministic order로 추가하고 validation tests를 작성한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] CLI `operator scenario validate`가 matrix-only scenario를 유효하게 보고하는 test를 추가한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## [API-1] Matrix Schema And Validation
### 문제
[scenario.go](/config/workspace/alt/apps/cli/internal/operator/scenario.go:386)는 `Scenario`가 `steps`만 실행 단위로 갖는다. [scenario.go](/config/workspace/alt/apps/cli/internal/operator/scenario.go:426)는 `len(s.Steps) == 0`이면 무조건 오류를 반환해 matrix-only YAML을 표현할 수 없다.
### 해결 방법
`Scenario`에 optional `Matrix *BacktestScenarioMatrix`를 추가하고, `steps`가 비어 있어도 matrix가 있으면 유효하게 둔다. Matrix는 named universe, strategy, timeframe, period 조합을 표현한다.
Before:
```go
// apps/cli/internal/operator/scenario.go:386
type Scenario struct {
Name string `yaml:"name"`
Timeout Duration `yaml:"timeout"`
Universes []UniverseConfig `yaml:"universes"`
Steps []Step `yaml:"steps"`
}
```
After:
```go
type Scenario struct {
Name string `yaml:"name"`
Timeout Duration `yaml:"timeout"`
Universes []UniverseConfig `yaml:"universes"`
Matrix *BacktestScenarioMatrix `yaml:"matrix"`
Steps []Step `yaml:"steps"`
}
type BacktestScenarioMatrix struct {
Universes []string `yaml:"universes"`
Strategies []string `yaml:"strategies"`
Timeframes []string `yaml:"timeframes"`
Periods []MatrixPeriod `yaml:"periods"`
}
type MatrixPeriod struct {
ID string `yaml:"id"`
FromUnixMs int64 `yaml:"from_unix_ms"`
ToUnixMs int64 `yaml:"to_unix_ms"`
}
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/scenario.go`: matrix structs 추가.
- [ ] `apps/cli/internal/operator/scenario.go`: `Validate`에서 `steps` 또는 `matrix` 중 하나를 요구하도록 변경.
- [ ] `apps/cli/internal/operator/scenario_test.go`: matrix-only valid fixture test 추가.
### 테스트 작성
- 작성: `TestValidateBacktestScenarioMatrix`로 `matrix.universes/strategies/timeframes/periods`가 parse되고 유효한지 검증한다.
- 작성: `TestValidateRejectsInvalidBacktestScenarioMatrix`로 empty universe, unknown universe, invalid timeframe, reversed period를 검증한다.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`.
## [API-2] Deterministic Matrix Expansion
### 문제
[runner.go](/config/workspace/alt/apps/cli/internal/operator/runner.go:129)는 이미 `steps`를 순서대로 실행하지만, matrix 조합을 실행 단위로 펼치는 helper가 없다. 02 subtask가 실행을 맡으려면 01에서 source-of-truth expansion 순서를 제공해야 한다.
### 해결 방법
`Scenario.MatrixRuns() ([]MatrixRun, error)`를 추가한다. 순서는 universe -> strategy -> timeframe -> period로 고정한다. `MatrixRun.ID`는 `<universe>__<strategy>__<timeframe>__<period>` 형태로 만들고 공백은 validation에서 거부한다.
Before:
```go
// apps/cli/internal/operator/scenario.go:420
// Validate runs the dry-run checks: a named scenario, at least one step, and
// every step having a unique id and a known action.
func (s *Scenario) Validate() error {
```
After:
```go
type MatrixRun struct {
ID string
Universe string
StrategyID string
Timeframe string
FromUnixMs int64
ToUnixMs int64
}
func (s *Scenario) MatrixRuns() ([]MatrixRun, error) {
// Called after Validate, but still returns errors so tests can exercise it directly.
}
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/scenario.go`: `MatrixRun`과 `MatrixRuns` 추가.
- [ ] `apps/cli/internal/operator/scenario.go`: matrix validation에서 duplicate generated ID를 거부.
- [ ] `apps/cli/internal/operator/scenario_test.go`: expansion order와 generated IDs 검증.
### 테스트 작성
- 작성: `TestBacktestScenarioMatrixRunsExpandsInDeterministicOrder`.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`.
## [API-3] CLI Validate Output For Matrix
### 문제
[cli.go](/config/workspace/alt/apps/cli/internal/cli/cli.go:86)는 validate 성공 시 `steps=%d`만 출력한다. matrix-only scenario가 유효해지면 operator가 조합 수를 볼 수 있어야 한다.
### 해결 방법
`operator scenario validate` 출력에 matrix가 있으면 `matrix_runs=<n>`을 추가한다. 기존 step-only scenario output은 유지한다.
Before:
```go
// apps/cli/internal/cli/cli.go:86
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d\n", sc.Name, len(sc.Steps))
```
After:
```go
runs, err := sc.MatrixRuns()
if err != nil {
fmt.Fprintf(stderr, "validate failed: %v\n", err)
return exitParseError
}
if len(runs) > 0 {
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d matrix_runs=%d\n", sc.Name, len(sc.Steps), len(runs))
return exitOK
}
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d\n", sc.Name, len(sc.Steps))
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/cli/cli.go`: validate output 보강.
- [ ] `apps/cli/internal/cli/cli_test.go`: matrix-only validate output test 추가.
### 테스트 작성
- 작성: `TestRunScenarioValidateMatrix`에서 `matrix_runs=...` 포함을 검증한다.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/cli/internal/operator/scenario.go` | API-1, API-2 |
| `apps/cli/internal/operator/scenario_test.go` | API-1, API-2 |
| `apps/cli/internal/cli/cli.go` | API-3 |
| `apps/cli/internal/cli/cli_test.go` | API-3 |
## 최종 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`; Go test cache는 fresh 실행이 필요하므로 `-count=1`을 유지한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,91 @@
<!-- task=m-backtest-scenario-automation/01_scenario_matrix plan=1 tag=REVIEW_API -->
# Plan - REVIEW_API Matrix Period Required Validation
## 이 파일을 읽는 구현 에이전트에게
이 plan의 구현이 끝나면 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 내용과 증거를 남기고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 명령 재실행이나 산출물 수집으로 해소 가능한 evidence 공백은 사용자 리뷰 요청이 아니다.
## 배경
이 follow-up은 `code_review_local_G06_0.log`의 Required 이슈만 보완한다. Matrix period가 `from_unix_ms` 또는 `to_unix_ms`를 누락해도 Go zero value `0`으로 들어와 현재 `Validate`를 통과한다. 기존 `start_backtest` request validation은 두 timestamp를 모두 필수로 취급하므로, matrix dry-run validation도 같은 기준을 적용해야 한다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 prompt는 금지이며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `scenario-matrix`: universe, 기간, frequency, strategy 조합을 YAML scenario에서 표현하고 검증할 수 있게 한다.
- Completion mode: check-on-pass
## 범위 결정 근거
- 수정 범위는 `apps/cli/internal/operator/scenario.go`와 `apps/cli/internal/operator/scenario_test.go`로 제한한다.
- CLI validate는 `operator.LoadScenario`를 통해 같은 validation을 사용하므로 별도 CLI behavior 변경은 하지 않는다.
- Batch runner, output contract, fixture handoff는 후속 subtask 범위이므로 건드리지 않는다.
## 구현 체크리스트
- [ ] Matrix period validation이 `from_unix_ms` 또는 `to_unix_ms` 누락을 거부하도록 보강한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] invalid matrix tests에 missing `from_unix_ms`와 missing `to_unix_ms` 케이스를 추가한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## [REVIEW_API-1] Matrix Period Required Timestamps
### 문제
[scenario.go](/config/workspace/alt/apps/cli/internal/operator/scenario.go:528)는 matrix period마다 `ID`와 inverted range만 검사한다. YAML에서 `from_unix_ms` 또는 `to_unix_ms`가 빠지면 값이 `0`이 되지만, `0 > 200`은 false이고 `100 > 0`만 일부 케이스를 잡는다. 이 때문에 dry-run validation을 통과한 matrix run이 나중에 invalid `start_backtest` request가 될 수 있다.
### 해결 방법
`MatrixPeriod` validation을 기존 `start_backtest` request validation과 맞춘다.
Before:
```go
if p.FromUnixMs > p.ToUnixMs {
return fmt.Errorf("matrix period %q: from_unix_ms %d cannot be after to_unix_ms %d", p.ID, p.FromUnixMs, p.ToUnixMs)
}
```
After:
```go
if p.FromUnixMs == 0 || p.ToUnixMs == 0 {
return fmt.Errorf("matrix period %q: from_unix_ms and to_unix_ms are required", p.ID)
}
if p.FromUnixMs > p.ToUnixMs {
return fmt.Errorf("matrix period %q: from_unix_ms %d cannot be after to_unix_ms %d", p.ID, p.FromUnixMs, p.ToUnixMs)
}
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/scenario.go`: matrix period missing timestamp validation 추가.
- [ ] `apps/cli/internal/operator/scenario_test.go`: missing `from_unix_ms` case 추가.
- [ ] `apps/cli/internal/operator/scenario_test.go`: missing `to_unix_ms` case 추가.
### 테스트 작성
- `TestValidateRejectsInvalidBacktestScenarioMatrix` table에 두 케이스를 추가한다.
- 기대 에러는 필수 timestamp 누락을 명확히 드러내는 문자열이어야 한다.
- 기존 reversed period, duplicate generated ID tests는 유지한다.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`.
## 최종 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`; Go test cache는 fresh 실행이 필요하므로 `-count=1`을 유지한다.

View file

@ -0,0 +1,115 @@
<!-- task=m-backtest-scenario-automation/02+01_batch_runner plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.
## 개요
date=2026-06-15
task=m-backtest-scenario-automation/02+01_batch_runner, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `batch-runner`: 여러 backtest run을 순차 실행하고 각 run의 polling 결과를 stable text/JSONL로 출력한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 판정 append, active 파일 log rename, PASS 시 `complete.log` 작성과 archive 이동까지 끝난 상태를 의미합니다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Connect Matrix To Runner | [ ] |
| [API-2] Stable Batch Output | [ ] |
| [API-3] Polling And Exit Semantics | [ ] |
## 구현 체크리스트
- [ ] 01 subtask completion을 확인한 뒤 matrix batch path를 `RunScenario`에 연결한다. 검증: `go test -count=1 ./apps/cli/internal/operator`
- [ ] 각 matrix run을 start 후 terminal status까지 poll하고 stable text/JSONL fields를 출력한다. 검증: `go test -count=1 ./apps/cli/internal/operator`
- [ ] batch success, terminal failed, timeout/transport, expectation mismatch behavior를 tests로 고정한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./services/api/internal/socket ./services/worker/internal/socket ./services/worker/internal/jobs`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [ ] predecessor `01` complete evidence를 확인했다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] PASS이면 active task directory를 archive로 이동하고 `complete.log`를 작성한다.
- [ ] PASS이고 task group이 `m-backtest-scenario-automation`이면 완료 이벤트 메타데이터를 보고한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Matrix batch path가 기존 step scenario behavior를 깨지 않는지 확인한다.
- Text/JSONL output keys가 deterministic이고 fixtures로 소비 가능하게 유지되는지 확인한다.
- Polling timeout과 terminal failed status의 exit code 분류가 계획과 일치하는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### API-1 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator
(output)
```
### API-2 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator
(output)
```
### API-3 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./services/api/internal/socket ./services/worker/internal/socket ./services/worker/internal/jobs
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./services/api/internal/socket ./services/worker/internal/socket ./services/worker/internal/jobs
(output)
```

View file

@ -0,0 +1,278 @@
<!-- task=m-backtest-scenario-automation/02+01_batch_runner plan=0 tag=API -->
# Plan - API Batch Runner
## 이 파일을 읽는 구현 에이전트에게
이 plan의 구현이 끝나면 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 내용과 증거를 남기고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 명령 재실행이나 산출물 수집으로 해소 가능한 evidence 공백은 사용자 리뷰 요청이 아니다.
## 배경
01 subtask가 matrix schema와 deterministic expansion을 제공하면, `operator scenario run`은 그 matrix를 실제 backtest run batch로 실행해야 한다. 현재 [runner.go](/config/workspace/alt/apps/cli/internal/operator/runner.go:129)는 사용자가 직접 작성한 `steps`만 순차 실행하고, 여러 generated run의 start/poll lifecycle을 요약하는 batch path가 없다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 prompt는 금지이며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `batch-runner`: 여러 backtest run을 순차 실행하고 각 run의 polling 결과를 stable text/JSONL로 출력한다.
- 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/rules/project/domain/operations/rules.md`
- `agent-ops/rules/project/domain/api/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- `apps/cli/go.mod`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/output.go`
- `apps/cli/internal/operator/client.go`
- `apps/cli/internal/operator/client_test.go`
- `apps/cli/internal/operator/runner_backtest_test.go`
- `apps/cli/internal/operator/runner_error_test.go`
- `apps/cli/internal/cli/cli.go`
- `apps/cli/internal/cli/cli_test.go`
- `apps/cli/testdata/operator/backtest_run_request.yaml`
- `apps/cli/testdata/operator/backtest_run_polling.yaml`
- `apps/cli/testdata/operator/backtest_result_summary.yaml`
### 테스트 환경 규칙
- 선택 env: `local`.
- 읽은 profile: `operations-smoke`, `api-smoke`, `worker-smoke`.
- 적용 명령: roadmap task 검증 명령 `go test -count=1 ./apps/cli/internal/operator ./services/api/internal/socket ./services/worker/internal/socket ./services/worker/internal/jobs`.
- CLI baseline으로 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`를 실행했고 exit 0이었다.
- 이 plan은 API/worker 코드를 변경하지 않지만 boundary regression 확인을 위해 milestone 명령을 최종 검증에 둔다.
### 테스트 커버리지 공백
- generated matrix runs를 순차 start/poll하는 runner path: 기존 테스트 없음.
- batch JSONL/text output의 `matrix_run_id`, universe, strategy, timeframe, period field: 기존 테스트 없음.
- terminal `failed` run을 transport/app failure가 아닌 terminal lifecycle evidence로 출력하는 path: 기존 테스트 없음.
- 기존 single scenario steps, typed error, transport failure: 기존 tests가 커버한다.
### 심볼 참조
- renamed/removed symbol: none.
- `RunScenario` call sites: `apps/cli/internal/cli/cli.go`, `apps/cli/internal/operator/runner_error_test.go`, `runner_market_test.go`, `runner_live_test.go`, `runner_paper_test.go`.
### 분할 판단
- split policy를 먼저 평가했다.
- shared task group: `agent-task/m-backtest-scenario-automation/`.
- 현재 subtask: `02+01_batch_runner`.
- predecessor `01`: missing active/archive `complete.log` at plan creation. 구현은 `agent-task/m-backtest-scenario-automation/01_scenario_matrix/complete.log` 또는 matching archive complete가 생긴 뒤 시작한다.
### 범위 결정 근거
- Protobuf schema, API socket handlers, worker execution/storage는 변경하지 않는다. Existing `StartBacktestRequest`, `GetBacktestRunRequest`, result query surface를 CLI에서 orchestration한다.
- `headless_validation.md`와 expected fixture 갱신은 03에서 수행한다.
- 새 dependency는 추가하지 않는다. `apps/cli/go.mod`에는 이미 YAML과 proto-socket dependencies가 있다.
### 빌드 등급
- build: `cloud-G07`, review: `cloud-G07`.
- 이유: CLI workflow, stdout/JSONL contract, polling/timeout/exit-code behavior가 중심이며 여러 test contexts를 함께 판단해야 한다.
## 구현 체크리스트
- [ ] 01 subtask completion을 확인한 뒤 matrix batch path를 `RunScenario`에 연결한다. 검증: `go test -count=1 ./apps/cli/internal/operator`
- [ ] 각 matrix run을 start 후 terminal status까지 poll하고 stable text/JSONL fields를 출력한다. 검증: `go test -count=1 ./apps/cli/internal/operator`
- [ ] batch success, terminal failed, timeout/transport, expectation mismatch behavior를 tests로 고정한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./services/api/internal/socket ./services/worker/internal/socket ./services/worker/internal/jobs`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- 이 directory 이름의 runtime dependency는 `01`이다.
- 구현 시작 전 `agent-task/m-backtest-scenario-automation/01_scenario_matrix/complete.log` 또는 matching archive complete가 필요하다.
## [API-1] Connect Matrix To Runner
### 문제
[runner.go](/config/workspace/alt/apps/cli/internal/operator/runner.go:96)는 `RunScenario`에서 단일 `Scenario`를 열고 [runner.go](/config/workspace/alt/apps/cli/internal/operator/runner.go:129)의 `for _, step := range sc.Steps`만 실행한다. 01의 matrix expansion이 있어도 runner가 이를 사용하지 않으면 batch automation이 동작하지 않는다.
### 해결 방법
`RunScenario`에서 `runs, err := sc.MatrixRuns()`를 호출하고, `len(runs) > 0`이면 `runMatrixBatch` path를 실행한다. Matrix가 없는 기존 step scenario는 현 동작을 유지한다.
Before:
```go
// apps/cli/internal/operator/runner.go:126
passed := 0
exitCode := codeOK
savedValues := make(map[string]string)
for _, step := range sc.Steps {
```
After:
```go
runs, err := sc.MatrixRuns()
if err != nil {
out.WriteStep(StepEvent{Scenario: sc.Name, Status: statusError, ErrorCode: "invalid_matrix", ErrorMessage: err.Error()})
out.WriteSummary(RunSummary{Scenario: sc.Name, Status: summaryFailed, Steps: 0, Passed: 0, ExitCode: codeBadInput})
return codeBadInput
}
if len(runs) > 0 {
return runMatrixBatch(ctx, client, sc.Name, runs, timeout, out)
}
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/runner.go`: matrix path branch 추가.
- [ ] `apps/cli/internal/operator/runner_backtest_test.go`: matrix branch가 기존 step branch와 분리되는지 검증.
### 테스트 작성
- 작성: `TestRunBacktestMatrixRunsSequentially`로 two-run matrix가 두 번 `StartBacktest`와 polling을 호출하는지 검증한다.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator
```
예상: package `ok`.
## [API-2] Stable Batch Output
### 문제
[output.go](/config/workspace/alt/apps/cli/internal/operator/output.go:25)는 backtest output에 `run_id/run_status`는 있지만 generated matrix run identity, universe, strategy, timeframe, period를 담지 않는다. Batch output이 stable하지 않으면 후속 handoff가 조합별 결과를 소비할 수 없다.
### 해결 방법
`StepEvent`에 matrix fields를 추가하고 text/JSONL renderer에 조건부 출력한다.
Before:
```go
// apps/cli/internal/operator/output.go:34
// Backtest-specific output fields
RunID string
RunStatus string
StartingCash string
```
After:
```go
// Batch/matrix-specific output fields.
MatrixRunID string
Universe string
StrategyID string
Timeframe string
PeriodID string
// Backtest-specific output fields.
RunID string
RunStatus string
StartingCash string
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/output.go`: `StepEvent` fields 추가.
- [ ] `apps/cli/internal/operator/output.go`: text output keys `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id` 추가.
- [ ] `apps/cli/internal/operator/output.go`: JSONL fields 추가.
- [ ] `apps/cli/internal/operator/runner_backtest_test.go`: text and JSONL output keys 검증.
### 테스트 작성
- 작성: matrix run test에서 text output key를 검증한다.
- 작성: JSONL mode test에서 `matrix_run_id``period_id` key를 검증한다.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator
```
예상: package `ok`.
## [API-3] Polling And Exit Semantics
### 문제
[runner.go](/config/workspace/alt/apps/cli/internal/operator/runner.go:338)는 explicit `poll_backtest_run` step 하나에 대해서만 polling을 수행한다. Batch path는 각 generated run마다 start/poll을 수행하고, terminal `succeeded/failed/canceled`, timeout, transport failure, expectation mismatch를 기존 exit code 계약과 일관되게 분류해야 한다.
### 해결 방법
`runMatrixBatch`는 각 run마다 start event와 poll event를 쓴다. Terminal `succeeded/failed/canceled`는 lifecycle evidence로 `status=ok` 처리한다. Polling timeout은 `codeTransport`로 stop한다. Matrix-level optional expected terminal status를 구현하게 되면 불일치는 `status=mismatch`, `codeAppError`로 처리한다.
Before:
```go
// apps/cli/internal/operator/runner.go:387
run := resp.GetRun()
if run != nil {
statusStr := strings.ToLower(strings.TrimPrefix(run.GetStatus().String(), "BACKTEST_RUN_STATUS_"))
```
After:
```go
func runMatrixBatch(ctx context.Context, client *APIClient, scenario string, runs []MatrixRun, timeout time.Duration, out *Writer) int {
// start each run, poll until terminal, write per-run events, preserve exit code contract.
}
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/runner.go`: `runMatrixBatch`, `pollBacktestRun` helper 추출 또는 추가.
- [ ] `apps/cli/internal/operator/client_test.go`: fake API에 sequential start/poll canned response hook 추가.
- [ ] `apps/cli/internal/operator/runner_backtest_test.go`: success, terminal failed, polling timeout tests 추가.
- [ ] `apps/cli/internal/operator/runner_error_test.go`: expectation mismatch가 exit 1로 남는지 필요 시 보강.
### 테스트 작성
- 작성: `TestRunBacktestMatrixReportsTerminalFailedAsLifecycleResult`.
- 작성: `TestRunBacktestMatrixPollingTimeoutExitsTransport`.
- 작성: `TestRunBacktestMatrixExpectedStatusMismatchExitsOne` if matrix expectation field exists after 01/02 implementation.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./services/api/internal/socket ./services/worker/internal/socket ./services/worker/internal/jobs
```
예상: 모든 package `ok`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/cli/internal/operator/runner.go` | API-1, API-3 |
| `apps/cli/internal/operator/output.go` | API-2 |
| `apps/cli/internal/operator/client_test.go` | API-3 |
| `apps/cli/internal/operator/runner_backtest_test.go` | API-1, API-2, API-3 |
| `apps/cli/internal/operator/runner_error_test.go` | API-3 |
## 최종 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./services/api/internal/socket ./services/worker/internal/socket ./services/worker/internal/jobs
```
예상: 모든 package `ok`; Go test cache는 fresh 실행이 필요하므로 `-count=1`을 유지한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,120 @@
<!-- task=m-backtest-scenario-automation/03+01,02_result_handoff plan=0 tag=TEST -->
# Code Review Reference - TEST
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.
## 개요
date=2026-06-15
task=m-backtest-scenario-automation/03+01,02_result_handoff, plan=0, tag=TEST
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `result-handoff`: 성공, 실패, timeout, expectation mismatch 결과를 fixture와 `headless_validation.md`에 남겨 후속 운영 표면이 소비할 수 있게 한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 판정 append, active 파일 log rename, PASS 시 `complete.log` 작성과 archive 이동까지 끝난 상태를 의미합니다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [TEST-1] Batch Fixture Set | [ ] |
| [TEST-2] Handoff Matrix Rows | [ ] |
| [TEST-3] Handoff Test Expansion | [ ] |
## 구현 체크리스트
- [ ] Batch result success/failure/timeout/mismatch YAML fixture와 expected JSONL fixture를 추가한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] `headless_validation.md` handoff matrix에 새 backtest batch rows와 output key/exit-code 계약을 추가한다. 검증: `rg --sort path -n "backtest" apps/cli/testdata/operator/headless_validation.md apps/cli/testdata/operator`
- [ ] `handoff_test.go`가 새 fixtures와 promised keys를 검증하도록 확장한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [ ] predecessor `01``02` complete evidence를 확인했다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] PASS이면 active task directory를 archive로 이동하고 `complete.log`를 작성한다.
- [ ] PASS이고 task group이 `m-backtest-scenario-automation`이면 완료 이벤트 메타데이터를 보고한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- 네 batch fixture가 success, terminal failed, timeout, mismatch를 각각 대표하는지 확인한다.
- Handoff matrix의 promised keys와 expected JSONL keys가 일치하는지 확인한다.
- UI 구현 또는 화면 설계가 이 subtask에 섞이지 않았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### TEST-1 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
(output)
```
### TEST-2 중간 검증
```bash
$ rg --sort path -n "backtest" apps/cli/testdata/operator/headless_validation.md apps/cli/testdata/operator
(output)
```
### TEST-3 중간 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
(output)
```
```bash
$ rg --sort path -n "backtest" apps/cli/testdata/operator/headless_validation.md apps/cli/testdata/operator
(output)
```

View file

@ -0,0 +1,272 @@
<!-- task=m-backtest-scenario-automation/03+01,02_result_handoff plan=0 tag=TEST -->
# Plan - TEST Result Handoff
## 이 파일을 읽는 구현 에이전트에게
이 plan의 구현이 끝나면 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정 필요 내용과 증거를 남기고 멈춘다. 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 명령 재실행이나 산출물 수집으로 해소 가능한 evidence 공백은 사용자 리뷰 요청이 아니다.
## 배경
`result-handoff`는 scenario automation 결과를 후속 운영 표면이 소비할 수 있게 fixture와 `headless_validation.md`에 남기는 작업이다. 현재 handoff 문서는 기존 backtest 단일 request/poll/result rows만 갖고 있으며, matrix batch success/failure/timeout/mismatch를 별도 evidence로 고정하지 않는다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 prompt는 금지이며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- Task ids:
- `result-handoff`: 성공, 실패, timeout, expectation mismatch 결과를 fixture와 `headless_validation.md`에 남겨 후속 운영 표면이 소비할 수 있게 한다.
- 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/rules/project/domain/operations/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-scenario-automation.md`
- `apps/cli/internal/operator/handoff_test.go`
- `apps/cli/testdata/operator/headless_validation.md`
- `apps/cli/testdata/operator/backtest_run_request.yaml`
- `apps/cli/testdata/operator/backtest_run_polling.yaml`
- `apps/cli/testdata/operator/backtest_result_summary.yaml`
- `apps/cli/testdata/operator/invalid_request_matrix.yaml`
- `apps/cli/testdata/operator/expected/backtest_run_request.jsonl`
- `apps/cli/testdata/operator/expected/backtest_run_polling.jsonl`
- `apps/cli/testdata/operator/expected/backtest_result_summary.jsonl`
- `apps/cli/testdata/operator/expected/invalid_request_matrix.jsonl`
- `apps/cli/internal/operator/runner_backtest_test.go`
- `apps/cli/internal/operator/runner_error_test.go`
- `apps/cli/internal/cli/cli_test.go`
### 테스트 환경 규칙
- 선택 env: `local`.
- `agent-test/local/rules.md``agent-test/local/operations-smoke.md`를 읽었다.
- 적용 명령: roadmap task 검증 명령 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli``rg --sort path -n "backtest" apps/cli/testdata/operator/headless_validation.md apps/cli/testdata/operator`.
- CLI baseline으로 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`를 실행했고 exit 0이었다.
### 테스트 커버리지 공백
- matrix batch success/failure/timeout/mismatch expected JSONL fixture existence: 기존 coverage 없음.
- handoff matrix row가 새 fixtures와 expected output keys를 참조하는지: existing `handoff_test.go` 구조로 확장 가능.
- 기존 backtest request/poll/result fixtures: existing coverage 있음.
### 심볼 참조
- renamed/removed symbol: none.
### 분할 판단
- split policy를 먼저 평가했다.
- shared task group: `agent-task/m-backtest-scenario-automation/`.
- 현재 subtask: `03+01,02_result_handoff`.
- predecessor `01`: missing active/archive `complete.log` at plan creation.
- predecessor `02`: missing active/archive `complete.log` at plan creation.
- 구현은 두 predecessor complete evidence가 생긴 뒤 시작한다.
### 범위 결정 근거
- Runtime behavior 변경은 01/02에 속한다. 이 subtask는 fixtures, handoff docs, handoff tests만 다룬다.
- Flutter UI, chart/layout, operator console screen definition은 운영 UI gate 때문에 제외한다.
- API/worker/contracts/domain code는 제외한다.
### 빌드 등급
- build: `local-G05`, review: `local-G05`.
- 이유: docs/fixtures/test handoff change이며 deterministic file and Go tests로 검증 가능하다.
## 구현 체크리스트
- [ ] Batch result success/failure/timeout/mismatch YAML fixture와 expected JSONL fixture를 추가한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] `headless_validation.md` handoff matrix에 새 backtest batch rows와 output key/exit-code 계약을 추가한다. 검증: `rg --sort path -n "backtest" apps/cli/testdata/operator/headless_validation.md apps/cli/testdata/operator`
- [ ] `handoff_test.go`가 새 fixtures와 promised keys를 검증하도록 확장한다. 검증: `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- 이 directory 이름의 runtime dependency는 `01,02`다.
- 구현 시작 전 `01_scenario_matrix``02+01_batch_runner``complete.log` 또는 matching archive complete가 필요하다.
## [TEST-1] Batch Fixture Set
### 문제
[headless_validation.md](/config/workspace/alt/apps/cli/testdata/operator/headless_validation.md:35)는 existing `backtest_run_request` row를 다루고, [headless_validation.md](/config/workspace/alt/apps/cli/testdata/operator/headless_validation.md:36)는 `backtest_run_polling` row를 다룬다. Batch matrix 결과 success/failure/timeout/mismatch를 대표하는 fixtures가 없다.
### 해결 방법
다음 fixture set을 추가한다.
- `apps/cli/testdata/operator/backtest_matrix_success.yaml`
- `apps/cli/testdata/operator/backtest_matrix_failed.yaml`
- `apps/cli/testdata/operator/backtest_matrix_timeout.yaml`
- `apps/cli/testdata/operator/backtest_matrix_mismatch.yaml`
- matching expected JSONL files under `apps/cli/testdata/operator/expected/`.
Before:
```text
apps/cli/testdata/operator/backtest_run_request.yaml
apps/cli/testdata/operator/backtest_run_polling.yaml
apps/cli/testdata/operator/backtest_result_summary.yaml
```
After:
```text
apps/cli/testdata/operator/backtest_matrix_success.yaml
apps/cli/testdata/operator/backtest_matrix_failed.yaml
apps/cli/testdata/operator/backtest_matrix_timeout.yaml
apps/cli/testdata/operator/backtest_matrix_mismatch.yaml
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/testdata/operator/backtest_matrix_success.yaml` 추가.
- [ ] `apps/cli/testdata/operator/backtest_matrix_failed.yaml` 추가.
- [ ] `apps/cli/testdata/operator/backtest_matrix_timeout.yaml` 추가.
- [ ] `apps/cli/testdata/operator/backtest_matrix_mismatch.yaml` 추가.
- [ ] matching `apps/cli/testdata/operator/expected/*.jsonl` 추가.
### 테스트 작성
- 작성: `handoff_test.go` required scenario list에 네 fixtures를 포함해 missing fixture를 fail하게 한다.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`.
## [TEST-2] Handoff Matrix Rows
### 문제
[headless_validation.md](/config/workspace/alt/apps/cli/testdata/operator/headless_validation.md:30)의 matrix는 command/input/expected output/keys/exit code를 행 단위로 관리한다. 새 batch fixtures가 있어도 matrix row가 없으면 후속 운영 표면이 어떤 evidence를 소비해야 하는지 알 수 없다.
### 해결 방법
네 batch rows를 추가하고 output keys를 batch output contract와 맞춘다.
Before:
```markdown
| `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` | ... |
```
After:
```markdown
| `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` | ... |
| `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` | ... |
| `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` | ... |
| `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` | ... |
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/testdata/operator/headless_validation.md`: 새 rows 추가.
- [ ] 각 row의 expected output keys에 `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` 포함.
- [ ] expected exit code는 success/failed terminal `0`, timeout `3`, mismatch `1`로 기록.
### 테스트 작성
- 기존 `TestHandoffExpectedFixturesWithMatrixPromisedKeys`를 통과하도록 matrix keys와 expected JSONL을 맞춘다.
### 중간 검증
```bash
rg --sort path -n "backtest" apps/cli/testdata/operator/headless_validation.md apps/cli/testdata/operator
```
예상: 새 batch scenario, input fixture, expected fixture path가 모두 검색된다.
## [TEST-3] Handoff Test Expansion
### 문제
[handoff_test.go](/config/workspace/alt/apps/cli/internal/operator/handoff_test.go:16)의 `requiredScenarios`는 기존 scenarios만 요구한다. 새 batch fixtures를 추가해도 test가 required handoff coverage로 강제하지 않으면 drift가 생긴다.
### 해결 방법
`requiredScenarios``requiredScenarioKeys`를 새 batch scenarios로 확장하고, timeout/mismatch fixture도 valid JSONL 및 promised keys check를 통과하게 한다.
Before:
```go
// apps/cli/internal/operator/handoff_test.go:20
"backtest_run_request",
"backtest_run_polling",
"backtest_result_summary",
```
After:
```go
"backtest_run_request",
"backtest_run_polling",
"backtest_result_summary",
"backtest_matrix_success",
"backtest_matrix_failed",
"backtest_matrix_timeout",
"backtest_matrix_mismatch",
```
### 수정 파일 및 체크리스트
- [ ] `apps/cli/internal/operator/handoff_test.go`: `requiredScenarios` 확장.
- [ ] `apps/cli/internal/operator/handoff_test.go`: `requiredScenarioKeys` 확장.
- [ ] 필요한 경우 row-level checks를 batch rows에도 적용.
### 테스트 작성
- 기존 handoff tests 확장으로 충분하다. 별도 새 test function은 batch row-specific invariant가 필요할 때만 추가한다.
### 중간 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
예상: 두 package 모두 `ok`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/cli/testdata/operator/backtest_matrix_success.yaml` | TEST-1 |
| `apps/cli/testdata/operator/backtest_matrix_failed.yaml` | TEST-1 |
| `apps/cli/testdata/operator/backtest_matrix_timeout.yaml` | TEST-1 |
| `apps/cli/testdata/operator/backtest_matrix_mismatch.yaml` | TEST-1 |
| `apps/cli/testdata/operator/expected/backtest_matrix_success.jsonl` | TEST-1 |
| `apps/cli/testdata/operator/expected/backtest_matrix_failed.jsonl` | TEST-1 |
| `apps/cli/testdata/operator/expected/backtest_matrix_timeout.jsonl` | TEST-1 |
| `apps/cli/testdata/operator/expected/backtest_matrix_mismatch.jsonl` | TEST-1 |
| `apps/cli/testdata/operator/headless_validation.md` | TEST-2 |
| `apps/cli/internal/operator/handoff_test.go` | TEST-3 |
## 최종 검증
```bash
go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli
```
```bash
rg --sort path -n "backtest" apps/cli/testdata/operator/headless_validation.md apps/cli/testdata/operator
```
예상: Go tests는 `ok`; `rg`는 새 batch fixture와 handoff rows를 포함해 backtest references를 stable path order로 출력한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -83,6 +83,16 @@ func runScenarioValidate(args []string, stdout, stderr io.Writer) int {
return exitParseError
}
runs, err := sc.MatrixRuns()
if err != nil {
fmt.Fprintf(stderr, "validate failed: %v\n", err)
return exitParseError
}
if len(runs) > 0 {
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d matrix_runs=%d\n", sc.Name, len(sc.Steps), len(runs))
return exitOK
}
fmt.Fprintf(stdout, "scenario=%s status=valid steps=%d\n", sc.Name, len(sc.Steps))
return exitOK
}

View file

@ -2,6 +2,7 @@ package cli
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
@ -79,3 +80,40 @@ func TestRunScenarioRunTransportFailure(t *testing.T) {
t.Errorf("stdout %q missing status=transport_error", stdout.String())
}
}
func TestRunScenarioValidateMatrix(t *testing.T) {
tempDir := t.TempDir()
path := filepath.Join(tempDir, "matrix_scenario.yaml")
yamlText := `
name: valid_matrix_cli
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat1"]
timeframes: ["daily"]
periods:
- id: p1
from_unix_ms: 1000
to_unix_ms: 2000
`
if err := os.WriteFile(path, []byte(yamlText), 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
var stdout, stderr bytes.Buffer
code := Run([]string{"operator", "scenario", "validate", "--file", path}, &stdout, &stderr)
if code != 0 {
t.Fatalf("exit code = %d, want 0 (stderr=%q)", code, stderr.String())
}
out := stdout.String()
for _, want := range []string{"scenario=valid_matrix_cli", "status=valid", "steps=0", "matrix_runs=1"} {
if !strings.Contains(out, want) {
t.Errorf("stdout %q missing %q", out, want)
}
}
}

View file

@ -385,10 +385,33 @@ type UniverseConfig struct {
// Scenario is the top-level operator scenario document.
type Scenario struct {
Name string `yaml:"name"`
Timeout Duration `yaml:"timeout"`
Universes []UniverseConfig `yaml:"universes"`
Steps []Step `yaml:"steps"`
Name string `yaml:"name"`
Timeout Duration `yaml:"timeout"`
Universes []UniverseConfig `yaml:"universes"`
Matrix *BacktestScenarioMatrix `yaml:"matrix"`
Steps []Step `yaml:"steps"`
}
type BacktestScenarioMatrix struct {
Universes []string `yaml:"universes"`
Strategies []string `yaml:"strategies"`
Timeframes []string `yaml:"timeframes"`
Periods []MatrixPeriod `yaml:"periods"`
}
type MatrixPeriod struct {
ID string `yaml:"id"`
FromUnixMs int64 `yaml:"from_unix_ms"`
ToUnixMs int64 `yaml:"to_unix_ms"`
}
type MatrixRun struct {
ID string
Universe string
StrategyID string
Timeframe string
FromUnixMs int64
ToUnixMs int64
}
// LoadScenario reads a scenario file and returns it after strict decode and
@ -423,8 +446,8 @@ func (s *Scenario) Validate() error {
if s.Name == "" {
return fmt.Errorf("scenario name is required")
}
if len(s.Steps) == 0 {
return fmt.Errorf("scenario %q has no steps", s.Name)
if len(s.Steps) == 0 && s.Matrix == nil {
return fmt.Errorf("scenario %q has no steps and no matrix", s.Name)
}
universeByName := make(map[string]UniverseConfig)
@ -464,6 +487,75 @@ func (s *Scenario) Validate() error {
universeByName[u.Name] = u
}
if s.Matrix != nil {
if len(s.Matrix.Universes) == 0 {
return fmt.Errorf("matrix universes is empty")
}
if len(s.Matrix.Strategies) == 0 {
return fmt.Errorf("matrix strategies is empty")
}
if len(s.Matrix.Timeframes) == 0 {
return fmt.Errorf("matrix timeframes is empty")
}
if len(s.Matrix.Periods) == 0 {
return fmt.Errorf("matrix periods is empty")
}
for _, uName := range s.Matrix.Universes {
if uName == "" {
return fmt.Errorf("matrix universe name is required")
}
if _, ok := universeByName[uName]; !ok {
return fmt.Errorf("matrix: referenced universe %q not found", uName)
}
}
for _, strat := range s.Matrix.Strategies {
if strat == "" {
return fmt.Errorf("matrix strategy name is required")
}
}
for _, tf := range s.Matrix.Timeframes {
if tf == "" {
return fmt.Errorf("matrix timeframe is required")
}
if !validTimeframes[tf] {
return fmt.Errorf("matrix: unsupported timeframe %q", tf)
}
}
for _, p := range s.Matrix.Periods {
if p.ID == "" {
return fmt.Errorf("matrix period ID is required")
}
if p.FromUnixMs == 0 || p.ToUnixMs == 0 {
return fmt.Errorf("matrix period %q: from_unix_ms and to_unix_ms are required", p.ID)
}
if p.FromUnixMs > p.ToUnixMs {
return fmt.Errorf("matrix period %q: from_unix_ms %d cannot be after to_unix_ms %d", p.ID, p.FromUnixMs, p.ToUnixMs)
}
}
seenIDs := make(map[string]bool)
for _, uName := range s.Matrix.Universes {
for _, strat := range s.Matrix.Strategies {
for _, tf := range s.Matrix.Timeframes {
for _, p := range s.Matrix.Periods {
id := fmt.Sprintf("%s__%s__%s__%s", uName, strat, tf, p.ID)
if strings.ContainsAny(id, " \t\n\r") {
return fmt.Errorf("matrix generated ID %q contains whitespace", id)
}
if seenIDs[id] {
return fmt.Errorf("matrix generated duplicate ID %q", id)
}
seenIDs[id] = true
}
}
}
}
}
seen := make(map[string]bool, len(s.Steps))
for i := range s.Steps {
step := &s.Steps[i]
@ -493,6 +585,37 @@ func (s *Scenario) Validate() error {
return nil
}
func (s *Scenario) MatrixRuns() ([]MatrixRun, error) {
if s.Matrix == nil {
return []MatrixRun{}, nil
}
// Validate first to ensure matrix is valid
if err := s.Validate(); err != nil {
return nil, err
}
var runs []MatrixRun
for _, uName := range s.Matrix.Universes {
for _, strat := range s.Matrix.Strategies {
for _, tf := range s.Matrix.Timeframes {
for _, p := range s.Matrix.Periods {
id := fmt.Sprintf("%s__%s__%s__%s", uName, strat, tf, p.ID)
runs = append(runs, MatrixRun{
ID: id,
Universe: uName,
StrategyID: strat,
Timeframe: tf,
FromUnixMs: p.FromUnixMs,
ToUnixMs: p.ToUnixMs,
})
}
}
}
}
return runs, nil
}
func applyUniverseDefaults(step *Step, universeByName map[string]UniverseConfig) error {
if step.Action != ActionImportDailyBars && step.Action != ActionCollectionFreshness && step.Action != ActionStartBacktest {
return nil

View file

@ -1408,3 +1408,290 @@ steps:
t.Errorf("error = %v, want it to contain missing selector error", err)
}
}
func TestValidateBacktestScenarioMatrix(t *testing.T) {
yamlText := `
name: valid_matrix_scenario
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat1", "strat2"]
timeframes: ["daily", "minute_1"]
periods:
- id: p1
from_unix_ms: 1000
to_unix_ms: 2000
- id: p2
from_unix_ms: 3000
to_unix_ms: 4000
`
sc, err := ParseScenario([]byte(yamlText))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sc.Name != "valid_matrix_scenario" {
t.Errorf("name = %q, want valid_matrix_scenario", sc.Name)
}
if sc.Matrix == nil {
t.Fatalf("matrix is nil")
}
if len(sc.Matrix.Universes) != 1 || sc.Matrix.Universes[0] != "univ1" {
t.Errorf("matrix universes = %v, want [univ1]", sc.Matrix.Universes)
}
}
func TestValidateRejectsInvalidBacktestScenarioMatrix(t *testing.T) {
cases := []struct {
name string
yaml string
expectedErr string
}{
{
name: "no steps and no matrix",
yaml: `
name: no_steps_no_matrix
`,
expectedErr: "has no steps and no matrix",
},
{
name: "empty universe list in matrix",
yaml: `
name: empty_univ
matrix:
universes: []
strategies: ["strat1"]
timeframes: ["daily"]
periods: [{id: "p1", from_unix_ms: 100, to_unix_ms: 200}]
`,
expectedErr: "matrix universes is empty",
},
{
name: "unknown universe",
yaml: `
name: unknown_univ
matrix:
universes: ["univ_unknown"]
strategies: ["strat1"]
timeframes: ["daily"]
periods: [{id: "p1", from_unix_ms: 100, to_unix_ms: 200}]
`,
expectedErr: "referenced universe \"univ_unknown\" not found",
},
{
name: "invalid timeframe",
yaml: `
name: invalid_tf
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat1"]
timeframes: ["invalid_timeframe"]
periods: [{id: "p1", from_unix_ms: 100, to_unix_ms: 200}]
`,
expectedErr: "unsupported timeframe",
},
{
name: "reversed period",
yaml: `
name: reversed_period
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat1"]
timeframes: ["daily"]
periods: [{id: "p1", from_unix_ms: 200, to_unix_ms: 100}]
`,
expectedErr: "cannot be after to_unix_ms",
},
{
name: "whitespace in generated ID",
yaml: `
name: whitespace_id
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat 1"]
timeframes: ["daily"]
periods: [{id: "p1", from_unix_ms: 100, to_unix_ms: 200}]
`,
expectedErr: "contains whitespace",
},
{
name: "duplicate period ID",
yaml: `
name: duplicate_period_id
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat1"]
timeframes: ["daily"]
periods:
- id: p1
from_unix_ms: 100
to_unix_ms: 200
- id: p1
from_unix_ms: 300
to_unix_ms: 400
`,
expectedErr: "matrix generated duplicate ID",
},
{
name: "missing from_unix_ms",
yaml: `
name: missing_from_unix_ms
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat1"]
timeframes: ["daily"]
periods: [{id: "p1", to_unix_ms: 200}]
`,
expectedErr: "from_unix_ms and to_unix_ms are required",
},
{
name: "missing to_unix_ms",
yaml: `
name: missing_to_unix_ms
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
matrix:
universes: ["univ1"]
strategies: ["strat1"]
timeframes: ["daily"]
periods: [{id: "p1", from_unix_ms: 100}]
`,
expectedErr: "from_unix_ms and to_unix_ms are required",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := ParseScenario([]byte(tc.yaml))
if err == nil {
t.Fatalf("expected error containing %q, got nil", tc.expectedErr)
}
if !strings.Contains(err.Error(), tc.expectedErr) {
t.Errorf("error = %v, want it to contain %q", err, tc.expectedErr)
}
})
}
}
func TestBacktestScenarioMatrixRunsExpandsInDeterministicOrder(t *testing.T) {
yamlText := `
name: expansion_order
universes:
- name: univ1
provider: kis
selector_kind: watchlist
market: kr
symbols: ["005930"]
- name: univ2
provider: kis
selector_kind: watchlist
market: kr
symbols: ["000660"]
matrix:
universes: ["univ1", "univ2"]
strategies: ["strat1", "strat2"]
timeframes: ["daily", "minute_1"]
periods:
- id: p1
from_unix_ms: 1000
to_unix_ms: 2000
- id: p2
from_unix_ms: 3000
to_unix_ms: 4000
`
sc, err := ParseScenario([]byte(yamlText))
if err != nil {
t.Fatalf("unexpected error parsing matrix scenario: %v", err)
}
runs, err := sc.MatrixRuns()
if err != nil {
t.Fatalf("unexpected error expanding matrix runs: %v", err)
}
wantIDs := []string{
"univ1__strat1__daily__p1",
"univ1__strat1__daily__p2",
"univ1__strat1__minute_1__p1",
"univ1__strat1__minute_1__p2",
"univ1__strat2__daily__p1",
"univ1__strat2__daily__p2",
"univ1__strat2__minute_1__p1",
"univ1__strat2__minute_1__p2",
"univ2__strat1__daily__p1",
"univ2__strat1__daily__p2",
"univ2__strat1__minute_1__p1",
"univ2__strat1__minute_1__p2",
"univ2__strat2__daily__p1",
"univ2__strat2__daily__p2",
"univ2__strat2__minute_1__p1",
"univ2__strat2__minute_1__p2",
}
if len(runs) != len(wantIDs) {
t.Fatalf("runs = %d, want %d", len(runs), len(wantIDs))
}
for i, run := range runs {
if run.ID != wantIDs[i] {
t.Errorf("runs[%d].ID = %q, want %q", i, run.ID, wantIDs[i])
}
parts := strings.Split(run.ID, "__")
if run.Universe != parts[0] {
t.Errorf("runs[%d].Universe = %q, want %q", i, run.Universe, parts[0])
}
if run.StrategyID != parts[1] {
t.Errorf("runs[%d].StrategyID = %q, want %q", i, run.StrategyID, parts[1])
}
if run.Timeframe != parts[2] {
t.Errorf("runs[%d].Timeframe = %q, want %q", i, run.Timeframe, parts[2])
}
if parts[3] == "p1" {
if run.FromUnixMs != 1000 || run.ToUnixMs != 2000 {
t.Errorf("runs[%d] period p1 from/to = %d/%d, want 1000/2000", i, run.FromUnixMs, run.ToUnixMs)
}
} else if parts[3] == "p2" {
if run.FromUnixMs != 3000 || run.ToUnixMs != 4000 {
t.Errorf("runs[%d] period p2 from/to = %d/%d, want 3000/4000", i, run.FromUnixMs, run.ToUnixMs)
}
}
}
}