diff --git a/agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md b/agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md index 433e03a..c768bfa 100644 --- a/agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md +++ b/agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md @@ -48,20 +48,20 @@ - [x] [provider-boundary] KIS/KRX/DART/US provider 등 확장 가능한 data source adapter 경계를 정리한다. - [x] [normalized-store] 백테스트에 필요한 정제 필드만 DB에 저장하고 원천 payload 전체 저장은 기본값에서 제외한다. -- [ ] [gap-handling] 결측, 휴장일, 중복, provider 지연을 백테스트 입력에서 식별할 수 있게 한다. +- [x] [gap-handling] 결측, 휴장일, 중복, provider 지연을 백테스트 입력에서 식별할 수 있게 한다. ### Epic: [backtest-ready] Repeatable execution input 정제 데이터가 실제 백테스트 실행 입력으로 반복 사용될 수 있게 연결한다. - [ ] [input-selector] 저장된 정제 데이터에서 종목군, 기간, frequency를 선택해 backtest run input으로 넘기는 경계를 만든다. -- [ ] [headless-smoke] 샘플 종목군 수집부터 기본 backtest 실행까지 headless smoke로 검증한다. +- [x] [headless-smoke] 샘플 종목군 수집부터 기본 backtest 실행까지 headless smoke로 검증한다. ## 완료 리뷰 - 상태: 없음 - 요청일: 없음 -- 완료 근거: `watchlist-config`, `daily-collect`, `freshness-status`, `provider-boundary`, `normalized-store` 기능 Task가 완료되었으나 전체 마일스톤 완료 조건은 아직 충족되지 않았다. +- 완료 근거: `watchlist-config`, `daily-collect`, `freshness-status`, `provider-boundary`, `normalized-store`, `gap-handling`, `headless-smoke` 기능 Task가 완료되었으나 `input-selector`가 남아 있어 전체 마일스톤 완료 조건은 아직 충족되지 않았다. - 리뷰 필요: - [ ] 사용자가 완료 결과를 확인했다 - [ ] archive 이동을 승인했다 @@ -86,6 +86,8 @@ - 완료 근거(freshness-status): `collection_freshness` operator scenario가 freshness status, missing count, latest date를 text/JSONL 출력과 handoff matrix에 포함하며, 2026-06-14에 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`가 통과했다. 증거: `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/02+01_freshness_status/complete.log`. - 완료 근거(provider-boundary): provider vocabulary에 KIS/KRX/DART/Yahoo data source id를 고정하고 `ProviderCapability`가 provider, venue, market, daily-bar timeframe 경계를 검증하도록 보강했다. 2026-06-14에 `go test -count=1 ./packages/domain/...`와 `go test -count=1 ./services/worker/internal/jobs`가 통과했다. - 완료 근거(normalized-store): PostgreSQL bars schema와 SQL query contract가 normalized OHLCV 필드만 사용하고 raw provider payload column을 포함하지 않도록 guard test를 추가했다. 2026-06-14에 `go test -count=1 ./services/worker/internal/storage/postgres`가 통과했다. +- 완료 근거(gap-handling): `collection_freshness` scenario가 `expected_yyyymmdd` 기반 gap, duplicate, provider delay를 text/JSONL 출력과 expectation mismatch 검증에 포함하며, matched-zero-bars symbol의 double count regression을 보강했다. 2026-06-14에 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`가 통과했다. 증거: `agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/03+02_gap_handling/complete.log`. +- 완료 근거(headless-smoke): `backtest_run_polling` operator scenario가 named universe 기반 `import_daily_bars` 후 `start_backtest`와 `poll_backtest_run`으로 terminal 상태까지 확인하는 headless smoke를 문서화했다. 2026-06-14에 `go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli`가 통과했다. - 선행 작업: Backtest Engine Baseline, Backtest Analysis Surface - 후속 작업: Backtest Scenario Automation, Data Quality Monitoring, Agent Tooling Bridge 후보 - 확인 필요: 없음 diff --git a/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/code_review_cloud_G06_0.log b/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/code_review_cloud_G06_0.log new file mode 100644 index 0000000..c013bb0 --- /dev/null +++ b/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/code_review_cloud_G06_0.log @@ -0,0 +1,169 @@ + + +# 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`. 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-14 +task=m-backtest-data-collection-infrastructure/01_input_selector_contract, plan=0, tag=API + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-backtest-data-collection-infrastructure/01_input_selector_contract/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Proto Selector Shape | [x] | +| [API-2] Domain And Worker Mapping | [x] | +| [API-3] Forwarding And Client Request Preservation | [x] | + +## 구현 체크리스트 + +- [x] `BacktestRunSpec`에 additive selector message/field를 추가하고 generated Go를 갱신한다. +- [x] domain `RunSpec`과 worker socket mapping이 selector를 정상/경계 케이스로 왕복하도록 구현한다. +- [x] API/workerclient/CLI request 생성 테스트가 selector 필드 보존을 확인하도록 갱신한다. +- [x] `bin/contracts-gen`, `bin/contracts-check`, 지정 `go test -count=1` 검증을 실행한다. +- [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_cloud_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G06_M.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-data-collection-infrastructure/01_input_selector_contract/`를 `agent-task/archive/YYYY/MM/m-backtest-data-collection-infrastructure/01_input_selector_contract/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-backtest-data-collection-infrastructure/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.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로 이동한다. + +## 계획 대비 변경 사항 + +- 계획대로 모두 이상 없이 구현 완료되었습니다. + +## 주요 설계 결정 + +- `BacktestInputSelector`는 `repeated string instrument_ids`와 `repeated string symbols`를 필드로 하여 `BacktestRunSpec`의 6번 태그에 추가되었습니다. +- domain layer의 `RunSpec`에 `Selector InputSelector` 필드를 추가하고, worker socket mapping(`backtest_mapping.go`)에서 `strings.TrimSpace` 검증을 통해 blank symbol/instrument ID가 요청되었을 때 reject하도록 견고화하였습니다. +- API layer 및 CLI operator client layer에서 `StartBacktest` 요청 생성 시 selector의 보존을 검증하는 round-trip 테스트들을 추가 및 보완하였습니다. + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `BacktestRunSpec.selector`가 additive field이고 기존 field number를 바꾸지 않았는지 확인한다. +- worker socket mapping이 blank selector entries를 reject하고 정상 selector를 from/to proto 모두 보존하는지 확인한다. +- API/workerclient/CLI request path가 selector를 임의로 검증하거나 삭제하지 않는지 확인한다. + +## 검증 결과 + +### API-1 중간 검증 +```bash +$ bin/contracts-gen +$ bin/contracts-check +(exit 0, no output) +``` + +### API-2 중간 검증 +```bash +$ go test -count=1 ./packages/domain/... ./services/worker/internal/socket +ok git.toki-labs.com/toki/alt/packages/domain/backtest 0.004s +ok git.toki-labs.com/toki/alt/packages/domain/market 0.002s +ok git.toki-labs.com/toki/alt/packages/domain/trading 0.002s +ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.062s +``` + +### API-3 중간 검증 +```bash +$ go test -count=1 ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator +ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.012s +ok git.toki-labs.com/toki/alt/services/api/internal/workerclient 0.412s +ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.206s +``` + +### 최종 검증 +```bash +$ bin/contracts-gen +$ bin/contracts-check +$ go test -count=1 ./packages/domain/... ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator +ok git.toki-labs.com/toki/alt/packages/domain/backtest 0.002s +ok git.toki-labs.com/toki/alt/packages/domain/market 0.002s +ok git.toki-labs.com/toki/alt/packages/domain/trading 0.002s +ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.062s +ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.010s +ok git.toki-labs.com/toki/alt/services/api/internal/workerclient 0.412s +ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.187s +``` + +--- + +## Ownership + +| 섹션 | 소유자 | +|------|--------| +| 구현 항목별 완료 여부 | Implementing agent | +| 구현 체크리스트 | Implementing agent | +| 계획 대비 변경 사항 | Implementing agent | +| 주요 설계 결정 | Implementing agent | +| 사용자 리뷰 요청 | Implementing agent | +| 검증 결과 | Implementing agent | +| 코드리뷰 전용 체크리스트 | Review agent | +| 코드리뷰 결과 | Review agent | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 리뷰어 검증: + - `bin/contracts-gen`: PASS; exit 0, no output + - `bin/contracts-check`: PASS; exit 0, no output + - `go test -count=1 ./packages/contracts/gen/go/...`: PASS; no test files + - `go test -count=1 ./packages/domain/... ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator`: PASS + - `bin/lint`: PASS; exit 0, Flutter analyze info-only `avoid_print` findings in existing Mattermost auth service + - `flutter test` in `apps/client`: PASS; `All tests passed!` + - `git diff --check`: PASS; no output +- 다음 단계: PASS이므로 `complete.log`를 작성하고 active task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log b/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log new file mode 100644 index 0000000..d00546f --- /dev/null +++ b/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log @@ -0,0 +1,39 @@ +# Complete - m-backtest-data-collection-infrastructure/01_input_selector_contract + +## 완료 일시 + +2026-06-14 + +## 요약 + +Backtest input selector contract, domain mapping, and forwarding preservation were reviewed in loop 0 with final verdict PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G06_0.log` | `code_review_cloud_G06_0.log` | PASS | Selector contract and round-trip tests are complete; no Required, Suggested, or Nit issues remain. | + +## 구현/정리 내용 + +- Added additive `BacktestInputSelector` and `BacktestRunSpec.selector = 6` to the ALT protobuf contract, with regenerated Go and Dart outputs. +- Added domain `backtest.InputSelector` and worker socket mapping for proto-to-domain validation and domain-to-proto preservation. +- Added API, workerclient, worker socket, and operator client tests that verify selector forwarding, round-trip behavior, and blank selector entry rejection. + +## 최종 검증 + +- `bin/contracts-gen` - PASS; exit 0, no output. +- `bin/contracts-check` - PASS; exit 0, no output. +- `go test -count=1 ./packages/contracts/gen/go/...` - PASS; `? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files]`. +- `go test -count=1 ./packages/domain/... ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator` - PASS; all listed packages returned `ok`. +- `bin/lint` - PASS; exit 0, Flutter analyze reported existing info-only `avoid_print` findings in `mattermost_auth_service.dart`. +- `flutter test` in `apps/client` - PASS; `All tests passed!`. +- `git diff --check` - PASS; no output. + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/plan_cloud_G06_0.log b/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/plan_cloud_G06_0.log new file mode 100644 index 0000000..82ca752 --- /dev/null +++ b/agent-task/archive/2026/06/m-backtest-data-collection-infrastructure/01_input_selector_contract/plan_cloud_G06_0.log @@ -0,0 +1,262 @@ + +# PLAN-cloud-G06: Backtest Input Selector Contract + +## 이 파일을 읽는 구현 에이전트에게 +이 계획은 backtest run input에 종목군 selector를 싣기 위한 계약, domain, mapping, round-trip 테스트만 만든다. 구현 후 반드시 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 최종 PASS, log rename, `complete.log`, archive 이동은 code-review skill 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 또는 범위 충돌 때문에 멈춰야 하면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청`에 증거를 채운 뒤 중단한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청 사유가 아니다. + +## 배경 +현재 `BacktestRunSpec`은 strategy, market, timeframe, 기간만 담고 종목군을 지정할 수 없다. `backtest-ready`의 `input-selector`는 저장된 정제 데이터에서 종목군/기간/frequency를 backtest run input으로 넘기는 경계가 필요하다. 이 subtask는 wire/domain 경계를 먼저 만들고, 저장소 filtering과 operator flow는 후속 subtask에서 연결한다. + +## 사용자 리뷰 요청 흐름 +구현 중 사용자 전용 결정이나 범위 충돌이 생기면 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/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/contracts/proto/alt/v1/backtest.proto` +- `packages/contracts/proto/alt/v1/market.proto` +- `packages/domain/backtest/types.go` +- `packages/domain/backtest/types_test.go` +- `packages/domain/market/types.go` +- `services/worker/internal/socket/backtest_mapping.go` +- `services/worker/internal/socket/backtest_mapping_test.go` +- `services/worker/internal/socket/backtest.go` +- `services/worker/internal/socket/backtest_test.go` +- `services/api/internal/socket/backtest.go` +- `services/api/internal/socket/backtest_test.go` +- `services/api/internal/workerclient/client.go` +- `services/api/internal/workerclient/client_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` + +### 테스트 환경 규칙 +- `test_env=local`로 판단했다. +- `agent-test/local/rules.md`가 존재해 읽었다. +- 매칭 profile로 `operations-smoke.md`, `worker-smoke.md`, `contracts-smoke.md`, `domain-model-smoke.md`, `api-smoke.md`를 읽었다. +- 계약 변경이 있으므로 `bin/contracts-gen` 후 `bin/contracts-check`를 사용한다. +- Go 검증은 fresh 실행이 필요하므로 `go test -count=1`만 허용하고 test cache 출력은 허용하지 않는다. +- 현재 기준으로 `bin/contracts-check`와 통합 `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` 실행 가능함을 확인했다. +- `agent-contract/index.md`는 없어서 계약 포인터 문서는 적용하지 않았다. + +### 테스트 커버리지 공백 +- 새 public proto field는 generated Go, worker socket mapping, API/workerclient round-trip 정상/경계 테스트가 필요하다. +- selector가 실제 저장소 bar를 줄이는 동작은 이 subtask에서 구현하지 않는다. 그 공백은 `02+01_storage_bar_selector`가 닫는다. +- operator scenario가 named universe를 start request selector로 바꾸는 흐름은 이 subtask에서 구현하지 않는다. 그 공백은 `03+01,02_backtest_selector_flow`가 닫는다. + +### 심볼 참조 +- renamed/removed symbol: none. +- 기존 `BacktestRunSpec` 참조: `packages/contracts/proto/alt/v1/backtest.proto:19`, `packages/contracts/proto/alt/v1/paper_trading.proto:18`, `packages/domain/backtest/types.go:25`, `services/worker/internal/socket/backtest_mapping.go:20`, `services/worker/internal/socket/backtest_mapping.go:144`, `services/worker/internal/jobs/backtest_starter.go:66`, `services/worker/internal/jobs/backtest_jobs.go:99`, `apps/cli/internal/operator/runner.go:248`, `services/api/internal/workerclient/client.go:168`. + +### 분할 판단 +- split decision policy를 평가했고 multi-plan을 선택했다. +- shared task group: `agent-task/m-backtest-data-collection-infrastructure/` +- 이 subtask: `01_input_selector_contract`, predecessor 없음. +- sibling: `02+01_storage_bar_selector`, predecessor `01_input_selector_contract` complete.log 필요, 현재 missing. +- sibling: `03+01,02_backtest_selector_flow`, predecessor `01_input_selector_contract`, `02+01_storage_bar_selector` complete.log 필요, 현재 missing. + +### 범위 결정 근거 +- 실제 bar filtering, storage source selector resolution, engine execution filtering은 제외한다. 계약 shape가 먼저 안정되어야 한다. +- CLI scenario `start_backtest`에서 universe를 selector로 전환하는 작업은 제외한다. operator flow는 selector가 실행까지 연결된 뒤 검증해야 한다. +- postgres schema 변경은 하지 않는다. run spec의 기존 DB columns는 strategy/market/timeframe/from/to만 저장하고, selector persistence 여부는 후속 작업에서 필요한 경우 별도 판단한다. + +### 빌드 등급 +- build lane: `cloud-G06`. public proto, generated code, worker/API/CLI test surface를 건드리는 cross-boundary 변경이므로 G06로 잡는다. + +## 구현 체크리스트 +- [ ] `BacktestRunSpec`에 additive selector message/field를 추가하고 generated Go를 갱신한다. +- [ ] domain `RunSpec`과 worker socket mapping이 selector를 정상/경계 케이스로 왕복하도록 구현한다. +- [ ] API/workerclient/CLI request 생성 테스트가 selector 필드 보존을 확인하도록 갱신한다. +- [ ] `bin/contracts-gen`, `bin/contracts-check`, 지정 `go test -count=1` 검증을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Proto Selector Shape +#### 문제 +`BacktestRunSpec`이 기간과 frequency만 표현하고 종목군 selector를 담을 필드가 없다. + +```proto +// packages/contracts/proto/alt/v1/backtest.proto:19 +message BacktestRunSpec { + string strategy_id = 1; + Market market = 2; + Timeframe timeframe = 3; + int64 from_unix_ms = 4; + int64 to_unix_ms = 5; +} +``` + +#### 해결 방법 +기존 field number를 유지하고 additive field만 추가한다. selector가 비어 있으면 기존 full-market 동작을 보존한다. + +```proto +message BacktestInputSelector { + repeated string instrument_ids = 1; + repeated string symbols = 2; +} + +message BacktestRunSpec { + string strategy_id = 1; + Market market = 2; + Timeframe timeframe = 3; + int64 from_unix_ms = 4; + int64 to_unix_ms = 5; + BacktestInputSelector selector = 6; +} +``` + +#### 수정 파일 및 체크리스트 +- [ ] `packages/contracts/proto/alt/v1/backtest.proto`에 `BacktestInputSelector`와 `BacktestRunSpec.selector = 6`을 추가한다. +- [ ] `bin/contracts-gen`으로 `packages/contracts/gen/go/alt/v1/backtest.pb.go`를 갱신한다. +- [ ] parser map에는 새 message type을 직접 주고받지 않으므로 등록을 추가하지 않는다. + +#### 테스트 작성 +- generated contract shape는 `bin/contracts-check`로 확인한다. +- 별도 proto-only unit test는 작성하지 않는다. field behavior는 mapping/round-trip 테스트에서 검증한다. + +#### 중간 검증 +```bash +bin/contracts-gen +bin/contracts-check +``` +예상 결과: 두 명령 모두 exit 0. + +### [API-2] Domain And Worker Mapping +#### 문제 +worker socket mapping은 proto spec을 domain `RunSpec`으로 변환하지만 selector를 담을 domain field가 없다. + +```go +// packages/domain/backtest/types.go:25 +type RunSpec struct { + StrategyID StrategyID + Market market.Market + Timeframe market.Timeframe + From time.Time + To time.Time +} +``` + +```go +// services/worker/internal/socket/backtest_mapping.go:51 +return backtest.RunSpec{ + StrategyID: backtest.StrategyID(spec.GetStrategyId()), + Market: mkt, + Timeframe: timeframe, + From: from, + To: to, +}, nil +``` + +#### 해결 방법 +domain에 `InputSelector`를 추가하고 mapping에서 trim/blank 검증 후 복사한다. `runSpecToProto`도 selector를 되돌려 run 조회 응답에서 손실이 없게 한다. + +```go +type InputSelector struct { + InstrumentIDs []market.InstrumentID + Symbols []string +} + +type RunSpec struct { + StrategyID StrategyID + Market market.Market + Timeframe market.Timeframe + From time.Time + To time.Time + Selector InputSelector +} +``` + +#### 수정 파일 및 체크리스트 +- [ ] `packages/domain/backtest/types.go`에 `InputSelector`와 `RunSpec.Selector`를 추가한다. +- [ ] `services/worker/internal/socket/backtest_mapping.go`에 selector from/to proto helper를 추가한다. +- [ ] `services/worker/internal/socket/backtest_mapping_test.go`에 정상 selector round-trip과 blank `instrument_ids`/`symbols` reject 케이스를 추가한다. +- [ ] `packages/domain/backtest/types_test.go`는 domain helper를 추가한 경우에만 갱신한다. + +#### 테스트 작성 +- `TestRunSpecFromProtoValid`에 `Selector` assertion을 추가한다. +- `TestRunSpecFromProtoRejectsInvalid`에 blank selector entries를 추가한다. +- `TestRunToProto`에 selector가 proto로 보존되는 assertion을 추가한다. + +#### 중간 검증 +```bash +go test -count=1 ./packages/domain/... ./services/worker/internal/socket +``` +예상 결과: selector mapping 테스트 포함 exit 0. + +### [API-3] Forwarding And Client Request Preservation +#### 문제 +API는 start request를 worker로 그대로 전달하고 CLI runner는 `BacktestRunSpec`을 직접 구성한다. selector field가 추가되면 request 생성과 forwarding이 손실 없이 유지되는지 확인해야 한다. + +```go +// apps/cli/internal/operator/runner.go:248 +Spec: &altv1.BacktestRunSpec{ + StrategyId: step.Request.StrategyID, + Market: marketByName[step.Request.Market], + Timeframe: timeframeByName[step.Request.Timeframe], + FromUnixMs: step.Request.FromUnixMs, + ToUnixMs: step.Request.ToUnixMs, +}, +``` + +#### 해결 방법 +이 subtask에서는 CLI schema를 확장하지 않는다. 대신 API/workerclient tests가 generated selector를 보존하는지 확인하고, CLI request 생성부는 후속 plan에서 selector를 채울 수 있도록 기존 구조를 유지한다. + +#### 수정 파일 및 체크리스트 +- [ ] `services/api/internal/socket/backtest_test.go`의 `validStart` 또는 forwarding test에 selector field를 포함해 worker fake가 받은 request를 assertion한다. +- [ ] `services/api/internal/workerclient/client_test.go`의 `TestWorkerClient_StartBacktest_Success`에 selector round-trip assertion을 추가한다. +- [ ] `apps/cli/internal/operator/client_test.go`의 start backtest request capture가 새 field를 깨뜨리지 않는지 필요한 assertion을 추가한다. + +#### 테스트 작성 +- public wire field 보존 테스트이므로 정상 round-trip 테스트를 작성한다. +- boundary validation은 worker socket mapping test에서 담당하므로 API handler에는 중복 validation을 추가하지 않는다. + +#### 중간 검증 +```bash +go test -count=1 ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator +``` +예상 결과: selector field가 start request에서 손실되지 않고 exit 0. + +## 수정 파일 요약 +| 파일 | 항목 | +|------|------| +| `packages/contracts/proto/alt/v1/backtest.proto` | API-1 | +| `packages/contracts/gen/go/alt/v1/backtest.pb.go` | API-1 | +| `packages/domain/backtest/types.go` | API-2 | +| `packages/domain/backtest/types_test.go` | API-2 | +| `services/worker/internal/socket/backtest_mapping.go` | API-2 | +| `services/worker/internal/socket/backtest_mapping_test.go` | API-2 | +| `services/api/internal/socket/backtest_test.go` | API-3 | +| `services/api/internal/workerclient/client_test.go` | API-3 | +| `apps/cli/internal/operator/client_test.go` | API-3 | + +## 최종 검증 +```bash +bin/contracts-gen +bin/contracts-check +go test -count=1 ./packages/domain/... ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator +``` +예상 결과: 모든 명령 exit 0. Go test cache 출력은 허용하지 않는다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/CODE_REVIEW-cloud-G05.md b/agent-task/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/CODE_REVIEW-cloud-G05.md new file mode 100644 index 0000000..32a5faf --- /dev/null +++ b/agent-task/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/CODE_REVIEW-cloud-G05.md @@ -0,0 +1,142 @@ + + +# Code Review Reference - REFACTOR + +> **[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-14 +task=m-backtest-data-collection-infrastructure/02+01_storage_bar_selector, plan=0, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G05.md` -> `code_review_cloud_G05_N.log`, `PLAN-cloud-G05.md` -> `plan_cloud_G05_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] Backtest Engine RunSpec Bar Boundary | [ ] | +| [REFACTOR-2] Storage Selector Resolution | [ ] | +| [REFACTOR-3] Worker Command Flow Evidence | [ ] | + +## 구현 체크리스트 + +- [ ] 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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G05_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G05_M.log`로 아카이브한다. +- [ ] `.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/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/`를 `agent-task/archive/YYYY/MM/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-backtest-data-collection-infrastructure/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G05.md`와 `CODE_REVIEW-cloud-G05.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로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `StorageBarSource.GetBars`가 paper trading compatibility를 위해 남아 있는지 확인한다. +- backtest engine이 `RunSpec.Selector`를 source에 전달하고 selector를 무시하지 않는지 확인한다. +- missing selector가 조용한 empty run이 아니라 typed worker error로 이어질 수 있는 error를 반환하는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### REFACTOR-1 중간 검증 +```bash +$ go test -count=1 ./services/worker/internal/backtest +(output) +``` + +### REFACTOR-2 중간 검증 +```bash +$ go test -count=1 ./services/worker/internal/backtest +(output) +``` + +### REFACTOR-3 중간 검증 +```bash +$ go test -count=1 ./services/worker/internal/backtest ./services/worker/internal/jobs +(output) +``` + +### 최종 검증 +```bash +$ go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs +(output) +``` + +--- + +## Ownership + +| 섹션 | 소유자 | +|------|--------| +| 구현 항목별 완료 여부 | Implementing agent | +| 구현 체크리스트 | Implementing agent | +| 계획 대비 변경 사항 | Implementing agent | +| 주요 설계 결정 | Implementing agent | +| 사용자 리뷰 요청 | Implementing agent | +| 검증 결과 | Implementing agent | +| 코드리뷰 전용 체크리스트 | Review agent | +| 코드리뷰 결과 | Review agent | diff --git a/agent-task/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/PLAN-cloud-G05.md b/agent-task/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/PLAN-cloud-G05.md new file mode 100644 index 0000000..0b8ba5e --- /dev/null +++ b/agent-task/m-backtest-data-collection-infrastructure/02+01_storage_bar_selector/PLAN-cloud-G05.md @@ -0,0 +1,232 @@ + +# 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`: `complete.log` 현재 missing. 구현 시작 전 `agent-task/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log` 또는 matching archive 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` 완료가 선행이다. +- 구현 시작 전에 `agent-task/m-backtest-data-collection-infrastructure/01_input_selector_contract/complete.log`가 없으면 이 plan을 구현하지 말고 선행 subtask를 먼저 완료한다. + +## 구현 체크리스트 +- [ ] 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/CODE_REVIEW-cloud-G06.md b/agent-task/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/CODE_REVIEW-cloud-G06.md new file mode 100644 index 0000000..bbe1cf4 --- /dev/null +++ b/agent-task/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/CODE_REVIEW-cloud-G06.md @@ -0,0 +1,150 @@ + + +# 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`. 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-14 +task=m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow, plan=0, tag=API + +## 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 + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Async Run Payload Selector Preservation | [ ] | +| [API-2] Operator Start Backtest Selector | [ ] | +| [API-3] Headless Fixtures And Handoff Evidence | [ ] | + +## 구현 체크리스트 + +- [ ] `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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G06_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G06_M.log`로 아카이브한다. +- [ ] `.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/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/`를 `agent-task/archive/YYYY/MM/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-backtest-data-collection-infrastructure/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.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로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- selector가 starter payload와 job handler spec 재구성 과정에서 손실되지 않는지 확인한다. +- `start_backtest` selector requirement가 paper trading이나 non-backtest actions에 번지지 않았는지 확인한다. +- headless fixtures가 import와 start 모두 같은 universe를 참조하고 expected JSONL 변경이 불필요한지 확인한다. +- PASS 시 Roadmap Targets의 `input-selector`만 완료 이벤트 대상인지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### API-1 중간 검증 +```bash +$ go test -count=1 ./services/worker/internal/jobs ./services/worker/internal/backtest +(output) +``` + +### API-2 중간 검증 +```bash +$ go test -count=1 ./apps/cli/internal/operator +(output) +``` + +### API-3 중간 검증 +```bash +$ go test -count=1 ./apps/cli/internal/operator ./apps/cli/internal/cli +(output) +``` + +### 최종 검증 +```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 +(output) +``` + +--- + +## Ownership + +| 섹션 | 소유자 | +|------|--------| +| 구현 항목별 완료 여부 | Implementing agent | +| 구현 체크리스트 | Implementing agent | +| 계획 대비 변경 사항 | Implementing agent | +| 주요 설계 결정 | Implementing agent | +| 사용자 리뷰 요청 | Implementing agent | +| 검증 결과 | Implementing agent | +| 코드리뷰 전용 체크리스트 | Review agent | +| 코드리뷰 결과 | Review agent | diff --git a/agent-task/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/PLAN-cloud-G06.md b/agent-task/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/PLAN-cloud-G06.md new file mode 100644 index 0000000..90efc00 --- /dev/null +++ b/agent-task/m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow/PLAN-cloud-G06.md @@ -0,0 +1,274 @@ + +# 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`: `complete.log` 현재 missing. 구현 시작 전 active 또는 archived complete.log가 필요하다. +- predecessor `02+01_storage_bar_selector`: `complete.log` 현재 missing. 구현 시작 전 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` 완료가 선행이다. +- 구현 시작 전에 두 경로의 `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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/cli/internal/operator/client_test.go b/apps/cli/internal/operator/client_test.go index 82c19f6..42add6c 100644 --- a/apps/cli/internal/operator/client_test.go +++ b/apps/cli/internal/operator/client_test.go @@ -715,3 +715,49 @@ func TestAPIClientImportDailyBars(t *testing.T) { t.Errorf("server received to_yyyymmdd = %q, want 20240528", got.GetToYyyymmdd()) } } + +func TestAPIClientStartBacktest(t *testing.T) { + api := &fakeAPI{startBacktestResp: &altv1.StartBacktestResponse{ + Run: &altv1.BacktestRun{Id: "run-1"}, + }} + url := startFakeAPIServer(t, api) + client := dialTestClient(t, url) + + req := &altv1.StartBacktestRequest{ + Spec: &altv1.BacktestRunSpec{ + StrategyId: "strat-abc", + Selector: &altv1.BacktestInputSelector{ + InstrumentIds: []string{"005930"}, + Symbols: []string{"AAPL"}, + }, + }, + } + resp, err := client.StartBacktest(context.Background(), req) + if err != nil { + t.Fatalf("StartBacktest: %v", err) + } + if resp.GetError() != nil { + t.Fatalf("unexpected typed error: %+v", resp.GetError()) + } + if resp.GetRun().GetId() != "run-1" { + t.Errorf("resp run id = %q, want run-1", resp.GetRun().GetId()) + } + + got := api.lastStartBacktestReq() + if got == nil { + t.Fatal("server did not receive a start backtest request") + } + if got.GetSpec().GetStrategyId() != "strat-abc" { + t.Errorf("server received strategy_id = %q, want strat-abc", got.GetSpec().GetStrategyId()) + } + pSel := got.GetSpec().GetSelector() + if pSel == nil { + t.Fatal("server received nil selector") + } + if len(pSel.GetInstrumentIds()) != 1 || pSel.GetInstrumentIds()[0] != "005930" { + t.Errorf("server received instrument_ids = %v, want [005930]", pSel.GetInstrumentIds()) + } + if len(pSel.GetSymbols()) != 1 || pSel.GetSymbols()[0] != "AAPL" { + t.Errorf("server received symbols = %v, want [AAPL]", pSel.GetSymbols()) + } +} diff --git a/apps/cli/internal/operator/handoff_test.go b/apps/cli/internal/operator/handoff_test.go index ee36bf2..ab53641 100644 --- a/apps/cli/internal/operator/handoff_test.go +++ b/apps/cli/internal/operator/handoff_test.go @@ -233,7 +233,7 @@ func TestUSStatusScenarioFixtureImportsBeforeReads(t *testing.T) { var requiredScenarioKeys = map[string][]string{ "api_connection_smoke": {"scenario", "status", "type", "action"}, "backtest_result_summary": {"run_id", "run_status", "starting_cash", "ending_equity", "total_return", "trade_count"}, - "backtest_run_polling": {"run_id", "run_status", "bar_count", "instrument_count"}, + "backtest_run_polling": {"run_id", "run_status", "provider", "bar_count", "instrument_count"}, "backtest_run_request": {"run_id", "run_status", "bar_count", "instrument_count", "provider"}, "invalid_request_matrix": {"scenario", "status", "type", "action", "error_code", "error_message"}, "kis_daily_import_smoke": {"provider", "instrument_count", "bar_count"}, diff --git a/apps/cli/testdata/operator/backtest_run_polling.yaml b/apps/cli/testdata/operator/backtest_run_polling.yaml index 8a594ea..21a3889 100644 --- a/apps/cli/testdata/operator/backtest_run_polling.yaml +++ b/apps/cli/testdata/operator/backtest_run_polling.yaml @@ -1,14 +1,17 @@ name: backtest_run_polling timeout: 5s +universes: + - name: kr-backtest-smoke + provider: kis + selector_kind: watchlist + market: kr + venue: krx + symbols: ["005930"] steps: - id: import_kr_daily_bars action: import_daily_bars request: - provider: kis - selector_kind: watchlist - market: kr - venue: krx - symbols: ["005930"] + universe: kr-backtest-smoke from_yyyymmdd: "20240527" to_yyyymmdd: "20240528" expect: diff --git a/apps/cli/testdata/operator/headless_validation.md b/apps/cli/testdata/operator/headless_validation.md index 8790eab..02f8b19 100644 --- a/apps/cli/testdata/operator/headless_validation.md +++ b/apps/cli/testdata/operator/headless_validation.md @@ -33,7 +33,7 @@ ALT 운영 UI 게이트(`agent-ops/rules/project/rules.md`의 "운영 UI 구현 | `market_data_status_query` | `alt operator scenario run --file testdata/operator/market_data_status_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/market_data_status_query.yaml` | `testdata/operator/expected/market_data_status_query.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `count`, `universe`, `freshness_status`, `missing_count`, `latest_yyyymmdd`, `gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days` | `0` (성공/empty), `1` (mismatch) | yes | market data status card (종목수·바개수·최신도) | `import_daily_bars` → `ImportDailyBarsResponse.instrument_count`/`bar_count`; `ListInstrumentsResponse.instruments`, `ListBarsResponse.bars`; `collection_freshness` → `freshness_status`, `missing_count`, `latest_yyyymmdd`; `ErrorInfo.code` | 차트/바 시각화 레이아웃 미정 | | `us_market_data_status_query` | `alt operator scenario run --file testdata/operator/us_market_data_status_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/us_market_data_status_query.yaml` | `testdata/operator/expected/us_market_data_status_query.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `count`, `universe`, `freshness_status`, `missing_count`, `latest_yyyymmdd`, `gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days` | `0` (성공/empty), `1` (mismatch) | yes | market data status card (종목수·바개수·최신도) | `import_daily_bars` → `ImportDailyBarsResponse.instrument_count`/`bar_count`; `ListInstrumentsResponse.instruments`, `ListBarsResponse.bars`; `collection_freshness` → `freshness_status`, `missing_count`, `latest_yyyymmdd`; `ErrorInfo.code` | 차트/바 시각화 레이아웃 미정 | | `backtest_run_request` | `alt operator scenario run --file testdata/operator/backtest_run_request.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_run_request.yaml` | `testdata/operator/expected/backtest_run_request.jsonl` | `scenario`, `status`, `type`, `action`, `run_id`, `run_status` | `0` (성공), `1` (typed error/mismatch) | yes | backtest 생성 form (parameter 입력/검증) | `StartBacktestResponse.run.id`, `BacktestRun.status` | form validation layout 미정 | -| `backtest_run_polling` | `alt operator scenario run --file testdata/operator/backtest_run_polling.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_run_polling.yaml` | `testdata/operator/expected/backtest_run_polling.jsonl` | `scenario`, `status`, `type`, `action`, `run_id`, `run_status` | `0` (terminal 도달), `3` (polling timeout) | yes | backtest 진행 상태 표시 (상태 칩/진행바) | `GetBacktestRunResponse.run.status` 전이 (pending→running→succeeded/failed/canceled) | 진행바/상태 전이 애니메이션 미정 | +| `backtest_run_polling` | `alt operator scenario run --file testdata/operator/backtest_run_polling.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_run_polling.yaml` | `testdata/operator/expected/backtest_run_polling.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `run_id`, `run_status` | `0` (terminal 도달), `3` (polling timeout) | yes | backtest 진행 상태 표시 (상태 칩/진행바) | named universe `import_daily_bars` → `ImportDailyBarsResponse.provider`/`instrument_count`/`bar_count`; `StartBacktestResponse.run.id`; `GetBacktestRunResponse.run.status` 전이 (pending→running→succeeded/failed/canceled) | 진행바/상태 전이 애니메이션 미정 | | `backtest_result_summary` | `alt operator scenario run --file testdata/operator/backtest_result_summary.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_result_summary.yaml` | `testdata/operator/expected/backtest_result_summary.jsonl` | `scenario`, `status`, `type`, `action`, `run_id`, `starting_cash`, `ending_equity`, `total_return`, `trade_count` | `0` (성공), `1` (missing run/typed error) | yes | backtest 결과 요약 (metrics chips) | `GetBacktestResultResponse.result.summary` | 차트/메트릭 레이아웃 미정 | | `invalid_request_matrix` | `alt operator scenario run --file testdata/operator/invalid_request_matrix.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/invalid_request_matrix.yaml` | `testdata/operator/expected/invalid_request_matrix.jsonl` | `scenario`, `status`, `type`, `action`, `error_code`, `error_message` | `0` (기대한 typed error 도달), `2` (malformed scenario) | no (개발/검증 전용) | error/message 노출 (검증용) | `ErrorInfo.code` (`invalid_request`, `not_found`) | error 메시지 UI 미정 | | `kis_daily_import_smoke` | `alt operator scenario run --file testdata/operator/kis_daily_import_smoke.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/kis_daily_import_smoke.yaml` | `testdata/operator/expected/kis_daily_import_smoke.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count` | `0` (성공), `1` (mismatch/typed error) | yes | KIS import status (진행률/결과) | `ImportDailyBarsResponse.provider`, `instrument_count`, `bar_count` | import 진행 UI 미정 | diff --git a/apps/client/lib/src/generated/alt/v1/backtest.pb.dart b/apps/client/lib/src/generated/alt/v1/backtest.pb.dart index 6830132..a16ab0c 100644 --- a/apps/client/lib/src/generated/alt/v1/backtest.pb.dart +++ b/apps/client/lib/src/generated/alt/v1/backtest.pb.dart @@ -23,6 +23,61 @@ export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; export 'backtest.pbenum.dart'; +class BacktestInputSelector extends $pb.GeneratedMessage { + factory BacktestInputSelector({ + $core.Iterable<$core.String>? instrumentIds, + $core.Iterable<$core.String>? symbols, + }) { + final result = create(); + if (instrumentIds != null) result.instrumentIds.addAll(instrumentIds); + if (symbols != null) result.symbols.addAll(symbols); + return result; + } + + BacktestInputSelector._(); + + factory BacktestInputSelector.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory BacktestInputSelector.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'BacktestInputSelector', + package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'), + createEmptyInstance: create) + ..pPS(1, _omitFieldNames ? '' : 'instrumentIds') + ..pPS(2, _omitFieldNames ? '' : 'symbols') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + BacktestInputSelector clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + BacktestInputSelector copyWith( + void Function(BacktestInputSelector) updates) => + super.copyWith((message) => updates(message as BacktestInputSelector)) + as BacktestInputSelector; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BacktestInputSelector create() => BacktestInputSelector._(); + @$core.override + BacktestInputSelector createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static BacktestInputSelector getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static BacktestInputSelector? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList<$core.String> get instrumentIds => $_getList(0); + + @$pb.TagNumber(2) + $pb.PbList<$core.String> get symbols => $_getList(1); +} + class BacktestRunSpec extends $pb.GeneratedMessage { factory BacktestRunSpec({ $core.String? strategyId, @@ -30,6 +85,7 @@ class BacktestRunSpec extends $pb.GeneratedMessage { $0.Timeframe? timeframe, $fixnum.Int64? fromUnixMs, $fixnum.Int64? toUnixMs, + BacktestInputSelector? selector, }) { final result = create(); if (strategyId != null) result.strategyId = strategyId; @@ -37,6 +93,7 @@ class BacktestRunSpec extends $pb.GeneratedMessage { if (timeframe != null) result.timeframe = timeframe; if (fromUnixMs != null) result.fromUnixMs = fromUnixMs; if (toUnixMs != null) result.toUnixMs = toUnixMs; + if (selector != null) result.selector = selector; return result; } @@ -60,6 +117,8 @@ class BacktestRunSpec extends $pb.GeneratedMessage { enumValues: $0.Timeframe.values) ..aInt64(4, _omitFieldNames ? '' : 'fromUnixMs') ..aInt64(5, _omitFieldNames ? '' : 'toUnixMs') + ..aOM(6, _omitFieldNames ? '' : 'selector', + subBuilder: BacktestInputSelector.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -125,6 +184,17 @@ class BacktestRunSpec extends $pb.GeneratedMessage { $core.bool hasToUnixMs() => $_has(4); @$pb.TagNumber(5) void clearToUnixMs() => $_clearField(5); + + @$pb.TagNumber(6) + BacktestInputSelector get selector => $_getN(5); + @$pb.TagNumber(6) + set selector(BacktestInputSelector value) => $_setField(6, value); + @$pb.TagNumber(6) + $core.bool hasSelector() => $_has(5); + @$pb.TagNumber(6) + void clearSelector() => $_clearField(6); + @$pb.TagNumber(6) + BacktestInputSelector ensureSelector() => $_ensure(5); } class BacktestRun extends $pb.GeneratedMessage { diff --git a/apps/client/lib/src/generated/alt/v1/backtest.pbjson.dart b/apps/client/lib/src/generated/alt/v1/backtest.pbjson.dart index a7e638e..d6270ae 100644 --- a/apps/client/lib/src/generated/alt/v1/backtest.pbjson.dart +++ b/apps/client/lib/src/generated/alt/v1/backtest.pbjson.dart @@ -36,6 +36,20 @@ final $typed_data.Uint8List backtestRunStatusDescriptor = $convert.base64Decode( 'VTVF9SVU5fU1RBVFVTX0ZBSUxFRBAEEiAKHEJBQ0tURVNUX1JVTl9TVEFUVVNfQ0FOQ0VMRUQQ' 'BQ=='); +@$core.Deprecated('Use backtestInputSelectorDescriptor instead') +const BacktestInputSelector$json = { + '1': 'BacktestInputSelector', + '2': [ + {'1': 'instrument_ids', '3': 1, '4': 3, '5': 9, '10': 'instrumentIds'}, + {'1': 'symbols', '3': 2, '4': 3, '5': 9, '10': 'symbols'}, + ], +}; + +/// Descriptor for `BacktestInputSelector`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List backtestInputSelectorDescriptor = $convert.base64Decode( + 'ChVCYWNrdGVzdElucHV0U2VsZWN0b3ISJQoOaW5zdHJ1bWVudF9pZHMYASADKAlSDWluc3RydW' + '1lbnRJZHMSGAoHc3ltYm9scxgCIAMoCVIHc3ltYm9scw=='); + @$core.Deprecated('Use backtestRunSpecDescriptor instead') const BacktestRunSpec$json = { '1': 'BacktestRunSpec', @@ -59,6 +73,14 @@ const BacktestRunSpec$json = { }, {'1': 'from_unix_ms', '3': 4, '4': 1, '5': 3, '10': 'fromUnixMs'}, {'1': 'to_unix_ms', '3': 5, '4': 1, '5': 3, '10': 'toUnixMs'}, + { + '1': 'selector', + '3': 6, + '4': 1, + '5': 11, + '6': '.alt.v1.BacktestInputSelector', + '10': 'selector' + }, ], }; @@ -67,7 +89,8 @@ final $typed_data.Uint8List backtestRunSpecDescriptor = $convert.base64Decode( 'Cg9CYWNrdGVzdFJ1blNwZWMSHwoLc3RyYXRlZ3lfaWQYASABKAlSCnN0cmF0ZWd5SWQSJgoGbW' 'Fya2V0GAIgASgOMg4uYWx0LnYxLk1hcmtldFIGbWFya2V0Ei8KCXRpbWVmcmFtZRgDIAEoDjIR' 'LmFsdC52MS5UaW1lZnJhbWVSCXRpbWVmcmFtZRIgCgxmcm9tX3VuaXhfbXMYBCABKANSCmZyb2' - '1Vbml4TXMSHAoKdG9fdW5peF9tcxgFIAEoA1IIdG9Vbml4TXM='); + '1Vbml4TXMSHAoKdG9fdW5peF9tcxgFIAEoA1IIdG9Vbml4TXMSOQoIc2VsZWN0b3IYBiABKAsy' + 'HS5hbHQudjEuQmFja3Rlc3RJbnB1dFNlbGVjdG9yUghzZWxlY3Rvcg=='); @$core.Deprecated('Use backtestRunDescriptor instead') const BacktestRun$json = { diff --git a/packages/contracts/gen/go/alt/v1/backtest.pb.go b/packages/contracts/gen/go/alt/v1/backtest.pb.go index c669b5d..52bd242 100644 --- a/packages/contracts/gen/go/alt/v1/backtest.pb.go +++ b/packages/contracts/gen/go/alt/v1/backtest.pb.go @@ -79,6 +79,58 @@ func (BacktestRunStatus) EnumDescriptor() ([]byte, []int) { return file_alt_v1_backtest_proto_rawDescGZIP(), []int{0} } +type BacktestInputSelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstrumentIds []string `protobuf:"bytes,1,rep,name=instrument_ids,json=instrumentIds,proto3" json:"instrument_ids,omitempty"` + Symbols []string `protobuf:"bytes,2,rep,name=symbols,proto3" json:"symbols,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BacktestInputSelector) Reset() { + *x = BacktestInputSelector{} + mi := &file_alt_v1_backtest_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BacktestInputSelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BacktestInputSelector) ProtoMessage() {} + +func (x *BacktestInputSelector) ProtoReflect() protoreflect.Message { + mi := &file_alt_v1_backtest_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BacktestInputSelector.ProtoReflect.Descriptor instead. +func (*BacktestInputSelector) Descriptor() ([]byte, []int) { + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{0} +} + +func (x *BacktestInputSelector) GetInstrumentIds() []string { + if x != nil { + return x.InstrumentIds + } + return nil +} + +func (x *BacktestInputSelector) GetSymbols() []string { + if x != nil { + return x.Symbols + } + return nil +} + type BacktestRunSpec struct { state protoimpl.MessageState `protogen:"open.v1"` StrategyId string `protobuf:"bytes,1,opt,name=strategy_id,json=strategyId,proto3" json:"strategy_id,omitempty"` @@ -86,13 +138,14 @@ type BacktestRunSpec struct { Timeframe Timeframe `protobuf:"varint,3,opt,name=timeframe,proto3,enum=alt.v1.Timeframe" json:"timeframe,omitempty"` FromUnixMs int64 `protobuf:"varint,4,opt,name=from_unix_ms,json=fromUnixMs,proto3" json:"from_unix_ms,omitempty"` ToUnixMs int64 `protobuf:"varint,5,opt,name=to_unix_ms,json=toUnixMs,proto3" json:"to_unix_ms,omitempty"` + Selector *BacktestInputSelector `protobuf:"bytes,6,opt,name=selector,proto3" json:"selector,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *BacktestRunSpec) Reset() { *x = BacktestRunSpec{} - mi := &file_alt_v1_backtest_proto_msgTypes[0] + mi := &file_alt_v1_backtest_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -104,7 +157,7 @@ func (x *BacktestRunSpec) String() string { func (*BacktestRunSpec) ProtoMessage() {} func (x *BacktestRunSpec) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[0] + mi := &file_alt_v1_backtest_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -117,7 +170,7 @@ func (x *BacktestRunSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use BacktestRunSpec.ProtoReflect.Descriptor instead. func (*BacktestRunSpec) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{0} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{1} } func (x *BacktestRunSpec) GetStrategyId() string { @@ -155,6 +208,13 @@ func (x *BacktestRunSpec) GetToUnixMs() int64 { return 0 } +func (x *BacktestRunSpec) GetSelector() *BacktestInputSelector { + if x != nil { + return x.Selector + } + return nil +} + type BacktestRun struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -168,7 +228,7 @@ type BacktestRun struct { func (x *BacktestRun) Reset() { *x = BacktestRun{} - mi := &file_alt_v1_backtest_proto_msgTypes[1] + mi := &file_alt_v1_backtest_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -180,7 +240,7 @@ func (x *BacktestRun) String() string { func (*BacktestRun) ProtoMessage() {} func (x *BacktestRun) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[1] + mi := &file_alt_v1_backtest_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -193,7 +253,7 @@ func (x *BacktestRun) ProtoReflect() protoreflect.Message { // Deprecated: Use BacktestRun.ProtoReflect.Descriptor instead. func (*BacktestRun) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{1} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{2} } func (x *BacktestRun) GetId() string { @@ -240,7 +300,7 @@ type StartBacktestRequest struct { func (x *StartBacktestRequest) Reset() { *x = StartBacktestRequest{} - mi := &file_alt_v1_backtest_proto_msgTypes[2] + mi := &file_alt_v1_backtest_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -252,7 +312,7 @@ func (x *StartBacktestRequest) String() string { func (*StartBacktestRequest) ProtoMessage() {} func (x *StartBacktestRequest) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[2] + mi := &file_alt_v1_backtest_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -265,7 +325,7 @@ func (x *StartBacktestRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBacktestRequest.ProtoReflect.Descriptor instead. func (*StartBacktestRequest) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{2} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{3} } func (x *StartBacktestRequest) GetSpec() *BacktestRunSpec { @@ -285,7 +345,7 @@ type StartBacktestResponse struct { func (x *StartBacktestResponse) Reset() { *x = StartBacktestResponse{} - mi := &file_alt_v1_backtest_proto_msgTypes[3] + mi := &file_alt_v1_backtest_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -297,7 +357,7 @@ func (x *StartBacktestResponse) String() string { func (*StartBacktestResponse) ProtoMessage() {} func (x *StartBacktestResponse) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[3] + mi := &file_alt_v1_backtest_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -310,7 +370,7 @@ func (x *StartBacktestResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StartBacktestResponse.ProtoReflect.Descriptor instead. func (*StartBacktestResponse) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{3} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{4} } func (x *StartBacktestResponse) GetRun() *BacktestRun { @@ -336,7 +396,7 @@ type GetBacktestRunRequest struct { func (x *GetBacktestRunRequest) Reset() { *x = GetBacktestRunRequest{} - mi := &file_alt_v1_backtest_proto_msgTypes[4] + mi := &file_alt_v1_backtest_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -348,7 +408,7 @@ func (x *GetBacktestRunRequest) String() string { func (*GetBacktestRunRequest) ProtoMessage() {} func (x *GetBacktestRunRequest) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[4] + mi := &file_alt_v1_backtest_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -361,7 +421,7 @@ func (x *GetBacktestRunRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBacktestRunRequest.ProtoReflect.Descriptor instead. func (*GetBacktestRunRequest) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{4} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{5} } func (x *GetBacktestRunRequest) GetRunId() string { @@ -381,7 +441,7 @@ type GetBacktestRunResponse struct { func (x *GetBacktestRunResponse) Reset() { *x = GetBacktestRunResponse{} - mi := &file_alt_v1_backtest_proto_msgTypes[5] + mi := &file_alt_v1_backtest_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -393,7 +453,7 @@ func (x *GetBacktestRunResponse) String() string { func (*GetBacktestRunResponse) ProtoMessage() {} func (x *GetBacktestRunResponse) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[5] + mi := &file_alt_v1_backtest_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -406,7 +466,7 @@ func (x *GetBacktestRunResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBacktestRunResponse.ProtoReflect.Descriptor instead. func (*GetBacktestRunResponse) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{5} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{6} } func (x *GetBacktestRunResponse) GetRun() *BacktestRun { @@ -436,7 +496,7 @@ type BacktestTrade struct { func (x *BacktestTrade) Reset() { *x = BacktestTrade{} - mi := &file_alt_v1_backtest_proto_msgTypes[6] + mi := &file_alt_v1_backtest_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -448,7 +508,7 @@ func (x *BacktestTrade) String() string { func (*BacktestTrade) ProtoMessage() {} func (x *BacktestTrade) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[6] + mi := &file_alt_v1_backtest_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -461,7 +521,7 @@ func (x *BacktestTrade) ProtoReflect() protoreflect.Message { // Deprecated: Use BacktestTrade.ProtoReflect.Descriptor instead. func (*BacktestTrade) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{6} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{7} } func (x *BacktestTrade) GetInstrumentId() string { @@ -510,7 +570,7 @@ type BacktestPosition struct { func (x *BacktestPosition) Reset() { *x = BacktestPosition{} - mi := &file_alt_v1_backtest_proto_msgTypes[7] + mi := &file_alt_v1_backtest_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -522,7 +582,7 @@ func (x *BacktestPosition) String() string { func (*BacktestPosition) ProtoMessage() {} func (x *BacktestPosition) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[7] + mi := &file_alt_v1_backtest_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -535,7 +595,7 @@ func (x *BacktestPosition) ProtoReflect() protoreflect.Message { // Deprecated: Use BacktestPosition.ProtoReflect.Descriptor instead. func (*BacktestPosition) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{7} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{8} } func (x *BacktestPosition) GetInstrumentId() string { @@ -571,7 +631,7 @@ type BacktestSummaryMetrics struct { func (x *BacktestSummaryMetrics) Reset() { *x = BacktestSummaryMetrics{} - mi := &file_alt_v1_backtest_proto_msgTypes[8] + mi := &file_alt_v1_backtest_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -583,7 +643,7 @@ func (x *BacktestSummaryMetrics) String() string { func (*BacktestSummaryMetrics) ProtoMessage() {} func (x *BacktestSummaryMetrics) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[8] + mi := &file_alt_v1_backtest_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -596,7 +656,7 @@ func (x *BacktestSummaryMetrics) ProtoReflect() protoreflect.Message { // Deprecated: Use BacktestSummaryMetrics.ProtoReflect.Descriptor instead. func (*BacktestSummaryMetrics) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{8} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{9} } func (x *BacktestSummaryMetrics) GetStartingCash() *Price { @@ -637,7 +697,7 @@ type BacktestEquityPoint struct { func (x *BacktestEquityPoint) Reset() { *x = BacktestEquityPoint{} - mi := &file_alt_v1_backtest_proto_msgTypes[9] + mi := &file_alt_v1_backtest_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -649,7 +709,7 @@ func (x *BacktestEquityPoint) String() string { func (*BacktestEquityPoint) ProtoMessage() {} func (x *BacktestEquityPoint) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[9] + mi := &file_alt_v1_backtest_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -662,7 +722,7 @@ func (x *BacktestEquityPoint) ProtoReflect() protoreflect.Message { // Deprecated: Use BacktestEquityPoint.ProtoReflect.Descriptor instead. func (*BacktestEquityPoint) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{9} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{10} } func (x *BacktestEquityPoint) GetTimestampUnixMs() int64 { @@ -694,7 +754,7 @@ type BacktestResult struct { func (x *BacktestResult) Reset() { *x = BacktestResult{} - mi := &file_alt_v1_backtest_proto_msgTypes[10] + mi := &file_alt_v1_backtest_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -706,7 +766,7 @@ func (x *BacktestResult) String() string { func (*BacktestResult) ProtoMessage() {} func (x *BacktestResult) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[10] + mi := &file_alt_v1_backtest_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -719,7 +779,7 @@ func (x *BacktestResult) ProtoReflect() protoreflect.Message { // Deprecated: Use BacktestResult.ProtoReflect.Descriptor instead. func (*BacktestResult) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{10} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{11} } func (x *BacktestResult) GetRunId() string { @@ -780,7 +840,7 @@ type GetBacktestResultRequest struct { func (x *GetBacktestResultRequest) Reset() { *x = GetBacktestResultRequest{} - mi := &file_alt_v1_backtest_proto_msgTypes[11] + mi := &file_alt_v1_backtest_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -792,7 +852,7 @@ func (x *GetBacktestResultRequest) String() string { func (*GetBacktestResultRequest) ProtoMessage() {} func (x *GetBacktestResultRequest) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[11] + mi := &file_alt_v1_backtest_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -805,7 +865,7 @@ func (x *GetBacktestResultRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBacktestResultRequest.ProtoReflect.Descriptor instead. func (*GetBacktestResultRequest) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{11} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{12} } func (x *GetBacktestResultRequest) GetRunId() string { @@ -825,7 +885,7 @@ type GetBacktestResultResponse struct { func (x *GetBacktestResultResponse) Reset() { *x = GetBacktestResultResponse{} - mi := &file_alt_v1_backtest_proto_msgTypes[12] + mi := &file_alt_v1_backtest_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -837,7 +897,7 @@ func (x *GetBacktestResultResponse) String() string { func (*GetBacktestResultResponse) ProtoMessage() {} func (x *GetBacktestResultResponse) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[12] + mi := &file_alt_v1_backtest_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -850,7 +910,7 @@ func (x *GetBacktestResultResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBacktestResultResponse.ProtoReflect.Descriptor instead. func (*GetBacktestResultResponse) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{12} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{13} } func (x *GetBacktestResultResponse) GetResult() *BacktestResult { @@ -876,7 +936,7 @@ type ListBacktestRunsRequest struct { func (x *ListBacktestRunsRequest) Reset() { *x = ListBacktestRunsRequest{} - mi := &file_alt_v1_backtest_proto_msgTypes[13] + mi := &file_alt_v1_backtest_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -888,7 +948,7 @@ func (x *ListBacktestRunsRequest) String() string { func (*ListBacktestRunsRequest) ProtoMessage() {} func (x *ListBacktestRunsRequest) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[13] + mi := &file_alt_v1_backtest_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -901,7 +961,7 @@ func (x *ListBacktestRunsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBacktestRunsRequest.ProtoReflect.Descriptor instead. func (*ListBacktestRunsRequest) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{13} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{14} } func (x *ListBacktestRunsRequest) GetStatus() BacktestRunStatus { @@ -921,7 +981,7 @@ type ListBacktestRunsResponse struct { func (x *ListBacktestRunsResponse) Reset() { *x = ListBacktestRunsResponse{} - mi := &file_alt_v1_backtest_proto_msgTypes[14] + mi := &file_alt_v1_backtest_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -933,7 +993,7 @@ func (x *ListBacktestRunsResponse) String() string { func (*ListBacktestRunsResponse) ProtoMessage() {} func (x *ListBacktestRunsResponse) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[14] + mi := &file_alt_v1_backtest_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -946,7 +1006,7 @@ func (x *ListBacktestRunsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBacktestRunsResponse.ProtoReflect.Descriptor instead. func (*ListBacktestRunsResponse) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{14} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{15} } func (x *ListBacktestRunsResponse) GetRuns() []*BacktestRun { @@ -972,7 +1032,7 @@ type GetBacktestRunDetailRequest struct { func (x *GetBacktestRunDetailRequest) Reset() { *x = GetBacktestRunDetailRequest{} - mi := &file_alt_v1_backtest_proto_msgTypes[15] + mi := &file_alt_v1_backtest_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -984,7 +1044,7 @@ func (x *GetBacktestRunDetailRequest) String() string { func (*GetBacktestRunDetailRequest) ProtoMessage() {} func (x *GetBacktestRunDetailRequest) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[15] + mi := &file_alt_v1_backtest_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -997,7 +1057,7 @@ func (x *GetBacktestRunDetailRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBacktestRunDetailRequest.ProtoReflect.Descriptor instead. func (*GetBacktestRunDetailRequest) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{15} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{16} } func (x *GetBacktestRunDetailRequest) GetRunId() string { @@ -1018,7 +1078,7 @@ type GetBacktestRunDetailResponse struct { func (x *GetBacktestRunDetailResponse) Reset() { *x = GetBacktestRunDetailResponse{} - mi := &file_alt_v1_backtest_proto_msgTypes[16] + mi := &file_alt_v1_backtest_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1030,7 +1090,7 @@ func (x *GetBacktestRunDetailResponse) String() string { func (*GetBacktestRunDetailResponse) ProtoMessage() {} func (x *GetBacktestRunDetailResponse) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[16] + mi := &file_alt_v1_backtest_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1043,7 +1103,7 @@ func (x *GetBacktestRunDetailResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBacktestRunDetailResponse.ProtoReflect.Descriptor instead. func (*GetBacktestRunDetailResponse) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{16} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{17} } func (x *GetBacktestRunDetailResponse) GetRun() *BacktestRun { @@ -1076,7 +1136,7 @@ type CompareBacktestRunsRequest struct { func (x *CompareBacktestRunsRequest) Reset() { *x = CompareBacktestRunsRequest{} - mi := &file_alt_v1_backtest_proto_msgTypes[17] + mi := &file_alt_v1_backtest_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1088,7 +1148,7 @@ func (x *CompareBacktestRunsRequest) String() string { func (*CompareBacktestRunsRequest) ProtoMessage() {} func (x *CompareBacktestRunsRequest) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[17] + mi := &file_alt_v1_backtest_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1101,7 +1161,7 @@ func (x *CompareBacktestRunsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CompareBacktestRunsRequest.ProtoReflect.Descriptor instead. func (*CompareBacktestRunsRequest) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{17} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{18} } func (x *CompareBacktestRunsRequest) GetRunIds() []string { @@ -1121,7 +1181,7 @@ type CompareBacktestRunsResponse struct { func (x *CompareBacktestRunsResponse) Reset() { *x = CompareBacktestRunsResponse{} - mi := &file_alt_v1_backtest_proto_msgTypes[18] + mi := &file_alt_v1_backtest_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1133,7 +1193,7 @@ func (x *CompareBacktestRunsResponse) String() string { func (*CompareBacktestRunsResponse) ProtoMessage() {} func (x *CompareBacktestRunsResponse) ProtoReflect() protoreflect.Message { - mi := &file_alt_v1_backtest_proto_msgTypes[18] + mi := &file_alt_v1_backtest_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1146,7 +1206,7 @@ func (x *CompareBacktestRunsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CompareBacktestRunsResponse.ProtoReflect.Descriptor instead. func (*CompareBacktestRunsResponse) Descriptor() ([]byte, []int) { - return file_alt_v1_backtest_proto_rawDescGZIP(), []int{18} + return file_alt_v1_backtest_proto_rawDescGZIP(), []int{19} } func (x *CompareBacktestRunsResponse) GetResults() []*BacktestResult { @@ -1167,7 +1227,10 @@ var File_alt_v1_backtest_proto protoreflect.FileDescriptor const file_alt_v1_backtest_proto_rawDesc = "" + "\n" + - "\x15alt/v1/backtest.proto\x12\x06alt.v1\x1a\x13alt/v1/common.proto\x1a\x13alt/v1/market.proto\"\xcb\x01\n" + + "\x15alt/v1/backtest.proto\x12\x06alt.v1\x1a\x13alt/v1/common.proto\x1a\x13alt/v1/market.proto\"X\n" + + "\x15BacktestInputSelector\x12%\n" + + "\x0einstrument_ids\x18\x01 \x03(\tR\rinstrumentIds\x12\x18\n" + + "\asymbols\x18\x02 \x03(\tR\asymbols\"\x86\x02\n" + "\x0fBacktestRunSpec\x12\x1f\n" + "\vstrategy_id\x18\x01 \x01(\tR\n" + "strategyId\x12&\n" + @@ -1176,7 +1239,8 @@ const file_alt_v1_backtest_proto_rawDesc = "" + "\ffrom_unix_ms\x18\x04 \x01(\x03R\n" + "fromUnixMs\x12\x1c\n" + "\n" + - "to_unix_ms\x18\x05 \x01(\x03R\btoUnixMs\"\xd7\x01\n" + + "to_unix_ms\x18\x05 \x01(\x03R\btoUnixMs\x129\n" + + "\bselector\x18\x06 \x01(\v2\x1d.alt.v1.BacktestInputSelectorR\bselector\"\xd7\x01\n" + "\vBacktestRun\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12+\n" + "\x04spec\x18\x02 \x01(\v2\x17.alt.v1.BacktestRunSpecR\x04spec\x121\n" + @@ -1263,74 +1327,76 @@ func file_alt_v1_backtest_proto_rawDescGZIP() []byte { } var file_alt_v1_backtest_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_alt_v1_backtest_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_alt_v1_backtest_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_alt_v1_backtest_proto_goTypes = []any{ (BacktestRunStatus)(0), // 0: alt.v1.BacktestRunStatus - (*BacktestRunSpec)(nil), // 1: alt.v1.BacktestRunSpec - (*BacktestRun)(nil), // 2: alt.v1.BacktestRun - (*StartBacktestRequest)(nil), // 3: alt.v1.StartBacktestRequest - (*StartBacktestResponse)(nil), // 4: alt.v1.StartBacktestResponse - (*GetBacktestRunRequest)(nil), // 5: alt.v1.GetBacktestRunRequest - (*GetBacktestRunResponse)(nil), // 6: alt.v1.GetBacktestRunResponse - (*BacktestTrade)(nil), // 7: alt.v1.BacktestTrade - (*BacktestPosition)(nil), // 8: alt.v1.BacktestPosition - (*BacktestSummaryMetrics)(nil), // 9: alt.v1.BacktestSummaryMetrics - (*BacktestEquityPoint)(nil), // 10: alt.v1.BacktestEquityPoint - (*BacktestResult)(nil), // 11: alt.v1.BacktestResult - (*GetBacktestResultRequest)(nil), // 12: alt.v1.GetBacktestResultRequest - (*GetBacktestResultResponse)(nil), // 13: alt.v1.GetBacktestResultResponse - (*ListBacktestRunsRequest)(nil), // 14: alt.v1.ListBacktestRunsRequest - (*ListBacktestRunsResponse)(nil), // 15: alt.v1.ListBacktestRunsResponse - (*GetBacktestRunDetailRequest)(nil), // 16: alt.v1.GetBacktestRunDetailRequest - (*GetBacktestRunDetailResponse)(nil), // 17: alt.v1.GetBacktestRunDetailResponse - (*CompareBacktestRunsRequest)(nil), // 18: alt.v1.CompareBacktestRunsRequest - (*CompareBacktestRunsResponse)(nil), // 19: alt.v1.CompareBacktestRunsResponse - (Market)(0), // 20: alt.v1.Market - (Timeframe)(0), // 21: alt.v1.Timeframe - (*ErrorInfo)(nil), // 22: alt.v1.ErrorInfo - (*Quantity)(nil), // 23: alt.v1.Quantity - (*Price)(nil), // 24: alt.v1.Price - (*Decimal)(nil), // 25: alt.v1.Decimal + (*BacktestInputSelector)(nil), // 1: alt.v1.BacktestInputSelector + (*BacktestRunSpec)(nil), // 2: alt.v1.BacktestRunSpec + (*BacktestRun)(nil), // 3: alt.v1.BacktestRun + (*StartBacktestRequest)(nil), // 4: alt.v1.StartBacktestRequest + (*StartBacktestResponse)(nil), // 5: alt.v1.StartBacktestResponse + (*GetBacktestRunRequest)(nil), // 6: alt.v1.GetBacktestRunRequest + (*GetBacktestRunResponse)(nil), // 7: alt.v1.GetBacktestRunResponse + (*BacktestTrade)(nil), // 8: alt.v1.BacktestTrade + (*BacktestPosition)(nil), // 9: alt.v1.BacktestPosition + (*BacktestSummaryMetrics)(nil), // 10: alt.v1.BacktestSummaryMetrics + (*BacktestEquityPoint)(nil), // 11: alt.v1.BacktestEquityPoint + (*BacktestResult)(nil), // 12: alt.v1.BacktestResult + (*GetBacktestResultRequest)(nil), // 13: alt.v1.GetBacktestResultRequest + (*GetBacktestResultResponse)(nil), // 14: alt.v1.GetBacktestResultResponse + (*ListBacktestRunsRequest)(nil), // 15: alt.v1.ListBacktestRunsRequest + (*ListBacktestRunsResponse)(nil), // 16: alt.v1.ListBacktestRunsResponse + (*GetBacktestRunDetailRequest)(nil), // 17: alt.v1.GetBacktestRunDetailRequest + (*GetBacktestRunDetailResponse)(nil), // 18: alt.v1.GetBacktestRunDetailResponse + (*CompareBacktestRunsRequest)(nil), // 19: alt.v1.CompareBacktestRunsRequest + (*CompareBacktestRunsResponse)(nil), // 20: alt.v1.CompareBacktestRunsResponse + (Market)(0), // 21: alt.v1.Market + (Timeframe)(0), // 22: alt.v1.Timeframe + (*ErrorInfo)(nil), // 23: alt.v1.ErrorInfo + (*Quantity)(nil), // 24: alt.v1.Quantity + (*Price)(nil), // 25: alt.v1.Price + (*Decimal)(nil), // 26: alt.v1.Decimal } var file_alt_v1_backtest_proto_depIdxs = []int32{ - 20, // 0: alt.v1.BacktestRunSpec.market:type_name -> alt.v1.Market - 21, // 1: alt.v1.BacktestRunSpec.timeframe:type_name -> alt.v1.Timeframe - 1, // 2: alt.v1.BacktestRun.spec:type_name -> alt.v1.BacktestRunSpec - 0, // 3: alt.v1.BacktestRun.status:type_name -> alt.v1.BacktestRunStatus - 1, // 4: alt.v1.StartBacktestRequest.spec:type_name -> alt.v1.BacktestRunSpec - 2, // 5: alt.v1.StartBacktestResponse.run:type_name -> alt.v1.BacktestRun - 22, // 6: alt.v1.StartBacktestResponse.error:type_name -> alt.v1.ErrorInfo - 2, // 7: alt.v1.GetBacktestRunResponse.run:type_name -> alt.v1.BacktestRun - 22, // 8: alt.v1.GetBacktestRunResponse.error:type_name -> alt.v1.ErrorInfo - 23, // 9: alt.v1.BacktestTrade.quantity:type_name -> alt.v1.Quantity - 24, // 10: alt.v1.BacktestTrade.price:type_name -> alt.v1.Price - 23, // 11: alt.v1.BacktestPosition.quantity:type_name -> alt.v1.Quantity - 24, // 12: alt.v1.BacktestPosition.last_price:type_name -> alt.v1.Price - 24, // 13: alt.v1.BacktestSummaryMetrics.starting_cash:type_name -> alt.v1.Price - 24, // 14: alt.v1.BacktestSummaryMetrics.ending_equity:type_name -> alt.v1.Price - 25, // 15: alt.v1.BacktestSummaryMetrics.total_return:type_name -> alt.v1.Decimal - 24, // 16: alt.v1.BacktestEquityPoint.equity:type_name -> alt.v1.Price - 24, // 17: alt.v1.BacktestResult.starting_cash:type_name -> alt.v1.Price - 24, // 18: alt.v1.BacktestResult.ending_equity:type_name -> alt.v1.Price - 7, // 19: alt.v1.BacktestResult.trades:type_name -> alt.v1.BacktestTrade - 8, // 20: alt.v1.BacktestResult.positions:type_name -> alt.v1.BacktestPosition - 9, // 21: alt.v1.BacktestResult.summary:type_name -> alt.v1.BacktestSummaryMetrics - 10, // 22: alt.v1.BacktestResult.equity_curve:type_name -> alt.v1.BacktestEquityPoint - 11, // 23: alt.v1.GetBacktestResultResponse.result:type_name -> alt.v1.BacktestResult - 22, // 24: alt.v1.GetBacktestResultResponse.error:type_name -> alt.v1.ErrorInfo - 0, // 25: alt.v1.ListBacktestRunsRequest.status:type_name -> alt.v1.BacktestRunStatus - 2, // 26: alt.v1.ListBacktestRunsResponse.runs:type_name -> alt.v1.BacktestRun - 22, // 27: alt.v1.ListBacktestRunsResponse.error:type_name -> alt.v1.ErrorInfo - 2, // 28: alt.v1.GetBacktestRunDetailResponse.run:type_name -> alt.v1.BacktestRun - 11, // 29: alt.v1.GetBacktestRunDetailResponse.result:type_name -> alt.v1.BacktestResult - 22, // 30: alt.v1.GetBacktestRunDetailResponse.error:type_name -> alt.v1.ErrorInfo - 11, // 31: alt.v1.CompareBacktestRunsResponse.results:type_name -> alt.v1.BacktestResult - 22, // 32: alt.v1.CompareBacktestRunsResponse.error:type_name -> alt.v1.ErrorInfo - 33, // [33:33] is the sub-list for method output_type - 33, // [33:33] is the sub-list for method input_type - 33, // [33:33] is the sub-list for extension type_name - 33, // [33:33] is the sub-list for extension extendee - 0, // [0:33] is the sub-list for field type_name + 21, // 0: alt.v1.BacktestRunSpec.market:type_name -> alt.v1.Market + 22, // 1: alt.v1.BacktestRunSpec.timeframe:type_name -> alt.v1.Timeframe + 1, // 2: alt.v1.BacktestRunSpec.selector:type_name -> alt.v1.BacktestInputSelector + 2, // 3: alt.v1.BacktestRun.spec:type_name -> alt.v1.BacktestRunSpec + 0, // 4: alt.v1.BacktestRun.status:type_name -> alt.v1.BacktestRunStatus + 2, // 5: alt.v1.StartBacktestRequest.spec:type_name -> alt.v1.BacktestRunSpec + 3, // 6: alt.v1.StartBacktestResponse.run:type_name -> alt.v1.BacktestRun + 23, // 7: alt.v1.StartBacktestResponse.error:type_name -> alt.v1.ErrorInfo + 3, // 8: alt.v1.GetBacktestRunResponse.run:type_name -> alt.v1.BacktestRun + 23, // 9: alt.v1.GetBacktestRunResponse.error:type_name -> alt.v1.ErrorInfo + 24, // 10: alt.v1.BacktestTrade.quantity:type_name -> alt.v1.Quantity + 25, // 11: alt.v1.BacktestTrade.price:type_name -> alt.v1.Price + 24, // 12: alt.v1.BacktestPosition.quantity:type_name -> alt.v1.Quantity + 25, // 13: alt.v1.BacktestPosition.last_price:type_name -> alt.v1.Price + 25, // 14: alt.v1.BacktestSummaryMetrics.starting_cash:type_name -> alt.v1.Price + 25, // 15: alt.v1.BacktestSummaryMetrics.ending_equity:type_name -> alt.v1.Price + 26, // 16: alt.v1.BacktestSummaryMetrics.total_return:type_name -> alt.v1.Decimal + 25, // 17: alt.v1.BacktestEquityPoint.equity:type_name -> alt.v1.Price + 25, // 18: alt.v1.BacktestResult.starting_cash:type_name -> alt.v1.Price + 25, // 19: alt.v1.BacktestResult.ending_equity:type_name -> alt.v1.Price + 8, // 20: alt.v1.BacktestResult.trades:type_name -> alt.v1.BacktestTrade + 9, // 21: alt.v1.BacktestResult.positions:type_name -> alt.v1.BacktestPosition + 10, // 22: alt.v1.BacktestResult.summary:type_name -> alt.v1.BacktestSummaryMetrics + 11, // 23: alt.v1.BacktestResult.equity_curve:type_name -> alt.v1.BacktestEquityPoint + 12, // 24: alt.v1.GetBacktestResultResponse.result:type_name -> alt.v1.BacktestResult + 23, // 25: alt.v1.GetBacktestResultResponse.error:type_name -> alt.v1.ErrorInfo + 0, // 26: alt.v1.ListBacktestRunsRequest.status:type_name -> alt.v1.BacktestRunStatus + 3, // 27: alt.v1.ListBacktestRunsResponse.runs:type_name -> alt.v1.BacktestRun + 23, // 28: alt.v1.ListBacktestRunsResponse.error:type_name -> alt.v1.ErrorInfo + 3, // 29: alt.v1.GetBacktestRunDetailResponse.run:type_name -> alt.v1.BacktestRun + 12, // 30: alt.v1.GetBacktestRunDetailResponse.result:type_name -> alt.v1.BacktestResult + 23, // 31: alt.v1.GetBacktestRunDetailResponse.error:type_name -> alt.v1.ErrorInfo + 12, // 32: alt.v1.CompareBacktestRunsResponse.results:type_name -> alt.v1.BacktestResult + 23, // 33: alt.v1.CompareBacktestRunsResponse.error:type_name -> alt.v1.ErrorInfo + 34, // [34:34] is the sub-list for method output_type + 34, // [34:34] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name } func init() { file_alt_v1_backtest_proto_init() } @@ -1346,7 +1412,7 @@ func file_alt_v1_backtest_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_alt_v1_backtest_proto_rawDesc), len(file_alt_v1_backtest_proto_rawDesc)), NumEnums: 1, - NumMessages: 19, + NumMessages: 20, NumExtensions: 0, NumServices: 0, }, diff --git a/packages/contracts/proto/alt/v1/backtest.proto b/packages/contracts/proto/alt/v1/backtest.proto index cb0bbda..609938a 100644 --- a/packages/contracts/proto/alt/v1/backtest.proto +++ b/packages/contracts/proto/alt/v1/backtest.proto @@ -16,12 +16,18 @@ enum BacktestRunStatus { BACKTEST_RUN_STATUS_CANCELED = 5; } +message BacktestInputSelector { + repeated string instrument_ids = 1; + repeated string symbols = 2; +} + message BacktestRunSpec { string strategy_id = 1; Market market = 2; Timeframe timeframe = 3; int64 from_unix_ms = 4; int64 to_unix_ms = 5; + BacktestInputSelector selector = 6; } message BacktestRun { diff --git a/packages/domain/backtest/types.go b/packages/domain/backtest/types.go index 6b52214..5264ecb 100644 --- a/packages/domain/backtest/types.go +++ b/packages/domain/backtest/types.go @@ -22,12 +22,18 @@ const ( RunStatusCanceled RunStatus = "canceled" ) +type InputSelector struct { + InstrumentIDs []market.InstrumentID + Symbols []string +} + type RunSpec struct { StrategyID StrategyID Market market.Market Timeframe market.Timeframe From time.Time To time.Time + Selector InputSelector } type Run struct { diff --git a/services/api/internal/socket/backtest_test.go b/services/api/internal/socket/backtest_test.go index 28633d9..12781f0 100644 --- a/services/api/internal/socket/backtest_test.go +++ b/services/api/internal/socket/backtest_test.go @@ -191,7 +191,15 @@ func (f *fakeWorkerClient) ListLiveAuditEvents(ctx context.Context, req *altv1.L } func validStart() *altv1.StartBacktestRequest { - return &altv1.StartBacktestRequest{Spec: &altv1.BacktestRunSpec{StrategyId: "strat-abc"}} + return &altv1.StartBacktestRequest{ + Spec: &altv1.BacktestRunSpec{ + StrategyId: "strat-abc", + Selector: &altv1.BacktestInputSelector{ + InstrumentIds: []string{"005930"}, + Symbols: []string{"AAPL"}, + }, + }, + } } func TestHandleStartBacktestForwards(t *testing.T) { @@ -207,6 +215,17 @@ func TestHandleStartBacktestForwards(t *testing.T) { if resp.GetRun().GetId() != "run-1" { t.Errorf("unexpected response run id: %q", resp.GetRun().GetId()) } + + pSel := fake.startReq.GetSpec().GetSelector() + if pSel == nil { + t.Fatal("expected selector to be forwarded") + } + if len(pSel.GetInstrumentIds()) != 1 || pSel.GetInstrumentIds()[0] != "005930" { + t.Errorf("unexpected forwarded instrument ids: %v", pSel.GetInstrumentIds()) + } + if len(pSel.GetSymbols()) != 1 || pSel.GetSymbols()[0] != "AAPL" { + t.Errorf("unexpected forwarded symbols: %v", pSel.GetSymbols()) + } } func TestHandleStartBacktestRejectsMissingSpec(t *testing.T) { diff --git a/services/api/internal/workerclient/client_test.go b/services/api/internal/workerclient/client_test.go index c77b84f..2091c51 100644 --- a/services/api/internal/workerclient/client_test.go +++ b/services/api/internal/workerclient/client_test.go @@ -109,7 +109,13 @@ func TestWorkerClient_StartBacktest_Success(t *testing.T) { defer client.Close() res, err := client.StartBacktest(ctx, &altv1.StartBacktestRequest{ - Spec: &altv1.BacktestRunSpec{StrategyId: "strat-abc"}, + Spec: &altv1.BacktestRunSpec{ + StrategyId: "strat-abc", + Selector: &altv1.BacktestInputSelector{ + InstrumentIds: []string{"005930"}, + Symbols: []string{"AAPL"}, + }, + }, }) if err != nil { t.Fatalf("StartBacktest failed: %v", err) @@ -120,6 +126,16 @@ func TestWorkerClient_StartBacktest_Success(t *testing.T) { if res.GetRun().GetSpec().GetStrategyId() != "strat-abc" { t.Errorf("spec did not round-trip, got %q", res.GetRun().GetSpec().GetStrategyId()) } + pSel := res.GetRun().GetSpec().GetSelector() + if pSel == nil { + t.Fatal("expected selector to round-trip") + } + if len(pSel.GetInstrumentIds()) != 1 || pSel.GetInstrumentIds()[0] != "005930" { + t.Errorf("unexpected selector instrument ids: %v", pSel.GetInstrumentIds()) + } + if len(pSel.GetSymbols()) != 1 || pSel.GetSymbols()[0] != "AAPL" { + t.Errorf("unexpected selector symbols: %v", pSel.GetSymbols()) + } } func TestWorkerClient_StartPaperTrading_Success(t *testing.T) { diff --git a/services/worker/internal/socket/backtest_mapping.go b/services/worker/internal/socket/backtest_mapping.go index 83b3225..e33463a 100644 --- a/services/worker/internal/socket/backtest_mapping.go +++ b/services/worker/internal/socket/backtest_mapping.go @@ -2,6 +2,7 @@ package socket import ( "fmt" + "strings" "time" altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1" @@ -48,12 +49,32 @@ func runSpecFromProto(spec *altv1.BacktestRunSpec) (backtest.RunSpec, error) { return backtest.RunSpec{}, fmt.Errorf("backtest spec: from cannot be after to") } + var selector backtest.InputSelector + if spec.GetSelector() != nil { + pSel := spec.GetSelector() + for _, id := range pSel.GetInstrumentIds() { + trimmed := strings.TrimSpace(id) + if trimmed == "" { + return backtest.RunSpec{}, fmt.Errorf("backtest spec: selector: instrument_id cannot be blank") + } + selector.InstrumentIDs = append(selector.InstrumentIDs, market.InstrumentID(trimmed)) + } + for _, sym := range pSel.GetSymbols() { + trimmed := strings.TrimSpace(sym) + if trimmed == "" { + return backtest.RunSpec{}, fmt.Errorf("backtest spec: selector: symbol cannot be blank") + } + selector.Symbols = append(selector.Symbols, trimmed) + } + } + return backtest.RunSpec{ StrategyID: backtest.StrategyID(spec.GetStrategyId()), Market: mkt, Timeframe: timeframe, From: from, To: to, + Selector: selector, }, nil } @@ -142,12 +163,24 @@ func runStatusToProto(s backtest.RunStatus) altv1.BacktestRunStatus { } func runSpecToProto(spec backtest.RunSpec) *altv1.BacktestRunSpec { + var pSel *altv1.BacktestInputSelector + if len(spec.Selector.InstrumentIDs) > 0 || len(spec.Selector.Symbols) > 0 { + insts := make([]string, len(spec.Selector.InstrumentIDs)) + for i, id := range spec.Selector.InstrumentIDs { + insts[i] = string(id) + } + pSel = &altv1.BacktestInputSelector{ + InstrumentIds: insts, + Symbols: spec.Selector.Symbols, + } + } return &altv1.BacktestRunSpec{ StrategyId: string(spec.StrategyID), Market: marketToProto(spec.Market), Timeframe: timeframeToProto(spec.Timeframe), FromUnixMs: timeToUnixMs(spec.From), ToUnixMs: timeToUnixMs(spec.To), + Selector: pSel, } } diff --git a/services/worker/internal/socket/backtest_mapping_test.go b/services/worker/internal/socket/backtest_mapping_test.go index 59997e4..39a2f3d 100644 --- a/services/worker/internal/socket/backtest_mapping_test.go +++ b/services/worker/internal/socket/backtest_mapping_test.go @@ -19,6 +19,10 @@ func TestRunSpecFromProtoValid(t *testing.T) { Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: from.UnixMilli(), ToUnixMs: to.UnixMilli(), + Selector: &altv1.BacktestInputSelector{ + InstrumentIds: []string{"005930"}, + Symbols: []string{"AAPL"}, + }, }) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -39,6 +43,12 @@ func TestRunSpecFromProtoValid(t *testing.T) { if !spec.To.Equal(to) { t.Errorf("to mismatch: got %v want %v", spec.To, to) } + if len(spec.Selector.InstrumentIDs) != 1 || spec.Selector.InstrumentIDs[0] != "005930" { + t.Errorf("selector instrument ids mismatch: got %v", spec.Selector.InstrumentIDs) + } + if len(spec.Selector.Symbols) != 1 || spec.Selector.Symbols[0] != "AAPL" { + t.Errorf("selector symbols mismatch: got %v", spec.Selector.Symbols) + } } func TestRunSpecFromProtoRejectsInvalid(t *testing.T) { @@ -56,6 +66,8 @@ func TestRunSpecFromProtoRejectsInvalid(t *testing.T) { {"missing from", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, ToUnixMs: validTo}}, {"missing to", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validFrom}}, {"inverted range", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validTo, ToUnixMs: validFrom}}, + {"blank instrument id", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validFrom, ToUnixMs: validTo, Selector: &altv1.BacktestInputSelector{InstrumentIds: []string{" "}}}}, + {"blank symbol", &altv1.BacktestRunSpec{StrategyId: "s", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: validFrom, ToUnixMs: validTo, Selector: &altv1.BacktestInputSelector{Symbols: []string{""}}}}, } for _, tt := range tests { @@ -122,6 +134,10 @@ func TestRunToProto(t *testing.T) { Timeframe: market.TimeframeMin5, From: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC), To: time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC), + Selector: backtest.InputSelector{ + InstrumentIDs: []market.InstrumentID{"AAPL"}, + Symbols: []string{"MSFT"}, + }, }, Status: backtest.RunStatusRunning, CreatedAt: created, @@ -144,6 +160,17 @@ func TestRunToProto(t *testing.T) { if got.GetUpdatedAtUnixMs() != updated.UnixMilli() { t.Errorf("updated mismatch: %d", got.GetUpdatedAtUnixMs()) } + + pSel := got.GetSpec().GetSelector() + if pSel == nil { + t.Fatal("selector should not be nil") + } + if len(pSel.GetInstrumentIds()) != 1 || pSel.GetInstrumentIds()[0] != "AAPL" { + t.Errorf("instrument ids mismatch: got %v", pSel.GetInstrumentIds()) + } + if len(pSel.GetSymbols()) != 1 || pSel.GetSymbols()[0] != "MSFT" { + t.Errorf("symbols mismatch: got %v", pSel.GetSymbols()) + } } func TestRunToProtoZeroTimeMapsToZero(t *testing.T) {