feat: backtest data collection infrastructure implementation

- Add backtest proto definitions with selector and state types
- Update domain types for backtest session and configuration
- Implement backtest selector flow for data range input
- Add CLI operator client and handoff test coverage
- Update generated code for proto changes
- Add worker socket backtest mapping implementations
- Include agent-task for milestone tracking
This commit is contained in:
toki 2026-06-14 20:39:44 +09:00
parent 5f7b972c44
commit a533efce9e
21 changed files with 1722 additions and 137 deletions

View file

@ -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 후보
- 확인 필요: 없음

View file

@ -0,0 +1,169 @@
<!-- task=m-backtest-data-collection-infrastructure/01_input_selector_contract plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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로 이동한다.

View file

@ -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
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,262 @@
<!-- task=m-backtest-data-collection-infrastructure/01_input_selector_contract plan=0 tag=API -->
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,142 @@
<!-- task=m-backtest-data-collection-infrastructure/02+01_storage_bar_selector plan=0 tag=REFACTOR -->
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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 |

View file

@ -0,0 +1,232 @@
<!-- task=m-backtest-data-collection-infrastructure/02+01_storage_bar_selector plan=0 tag=REFACTOR -->
# PLAN-cloud-G05: Storage Bar Selector
## 이 파일을 읽는 구현 에이전트에게
이 계획은 `01_input_selector_contract`가 PASS되어 `complete.log`를 만든 뒤에만 구현한다. 구현 후 반드시 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 최종 PASS, log rename, `complete.log`, archive 이동은 code-review skill 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 또는 범위 충돌 때문에 멈춰야 하면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청`에 증거를 채운 뒤 중단한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청 사유가 아니다.
## 배경
현재 backtest engine은 market 전체 instrument의 bar를 가져온다. `input-selector`를 완성하려면 저장된 정제 데이터에서 선택된 instrument/symbol만 읽어야 한다. 이 subtask는 worker 내부 storage-backed bar source와 engine 경계에 selector 동작을 적용한다.
## 사용자 리뷰 요청 흐름
구현 중 사용자 전용 결정이나 범위 충돌이 생기면 active `CODE_REVIEW-*-G??.md``사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식 그대로 기록한다. 구현 에이전트는 채팅으로 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. code-review가 사용자 리뷰 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/domain-model-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/domain-model/rules.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- `packages/domain/backtest/types.go`
- `packages/domain/market/types.go`
- `services/worker/internal/backtest/bar_source.go`
- `services/worker/internal/backtest/bar_source_test.go`
- `services/worker/internal/backtest/engine.go`
- `services/worker/internal/backtest/engine_test.go`
- `services/worker/internal/backtest/flow_test.go`
- `services/worker/internal/backtest/fixture_test.go`
- `services/worker/internal/jobs/backtest_jobs.go`
- `services/worker/internal/jobs/backtest_jobs_test.go`
- `services/worker/internal/jobs/backtest_starter.go`
- `services/worker/internal/jobs/backtest_starter_test.go`
- `services/worker/internal/storage/ports.go`
- `services/worker/internal/storage/postgres/mapping.go`
- `services/worker/cmd/alt-worker/main.go`
### 테스트 환경 규칙
- `test_env=local`로 판단했다.
- `agent-test/local/rules.md`가 존재해 읽었다.
- 매칭 profile로 `worker-smoke.md`, `domain-model-smoke.md`, `operations-smoke.md`를 읽었다.
- worker/domain 변경이므로 `go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs`를 필수로 사용한다.
- `go test -count=1` fresh 실행만 허용하고 test cache 출력은 허용하지 않는다.
- 현재 기준으로 더 넓은 묶음 `go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator ./apps/cli/internal/cli`가 exit 0임을 확인했다.
### 테스트 커버리지 공백
- 기존 `TestStorageBarSourceReadsMarketBars`는 market filter와 정렬만 검증한다.
- selector가 instrument id, symbol/provider symbol로 resolve되고 누락 selector를 error로 돌려주는 테스트가 없다.
- `Engine.Execute``RunSpec.Selector`를 bar source까지 넘기는 테스트가 없다.
- 저장소 기반 command flow가 선택되지 않은 같은 market instrument를 배제하는 테스트가 없다.
### 심볼 참조
- renamed/removed symbol: none.
- `StorageBarSource.GetBars` call sites: `services/worker/internal/backtest/bar_source_test.go:82`, `services/worker/internal/backtest/bar_source_test.go:136`, `services/worker/internal/backtest/engine.go:47`, `services/worker/internal/papertrading/engine.go:70`.
- `BarSource` interface call sites: `services/worker/internal/backtest/engine.go:15`, `services/worker/internal/backtest/engine_test.go:16`, `services/worker/internal/backtest/fixture_test.go:47`.
### 분할 판단
- split decision policy를 평가했고 multi-plan을 선택했다.
- shared task group: `agent-task/m-backtest-data-collection-infrastructure/`
- 이 subtask: `02+01_storage_bar_selector`.
- predecessor `01_input_selector_contract`: `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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,150 @@
<!-- task=m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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 |

View file

@ -0,0 +1,274 @@
<!-- task=m-backtest-data-collection-infrastructure/03+01,02_backtest_selector_flow plan=0 tag=API -->
# PLAN-cloud-G06: Backtest Selector Flow
## 이 파일을 읽는 구현 에이전트에게
이 계획은 `01_input_selector_contract``02+01_storage_bar_selector`가 PASS되어 각각 `complete.log`를 만든 뒤에만 구현한다. 구현 후 반드시 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 변경 내용과 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 최종 PASS, log rename, `complete.log`, archive 이동은 code-review skill 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 또는 범위 충돌 때문에 멈춰야 하면 사용자에게 직접 묻지 말고 review stub의 `사용자 리뷰 요청`에 증거를 채운 뒤 중단한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청 사유가 아니다.
## 배경
선행 subtasks가 selector 계약과 storage filtering을 만들더라도 async job payload와 operator scenario가 selector를 잃으면 실제 backtest run input까지 이어지지 않는다. 이 subtask는 start command, async run job, operator named universe, headless handoff fixture를 연결한다. PASS되면 현재 마일스톤의 `backtest-ready/input-selector` 기능 Task를 완료할 수 있다.
## 사용자 리뷰 요청 흐름
구현 중 사용자 전용 결정이나 범위 충돌이 생기면 active `CODE_REVIEW-*-G??.md``사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식 그대로 기록한다. 구현 에이전트는 채팅으로 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. code-review가 사용자 리뷰 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- Task ids:
- `input-selector`: 저장된 정제 데이터에서 종목군, 기간, frequency를 선택해 backtest run input으로 넘기는 경계를 만든다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-test/local/domain-model-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-ops/rules/project/domain/domain-model/rules.md`
- `agent-ops/rules/project/domain/api/rules.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/backtest-loop/PHASE.md`
- `agent-roadmap/phase/backtest-loop/milestones/backtest-data-collection-infrastructure.md`
- `packages/domain/backtest/types.go`
- `services/worker/internal/jobs/backtest_starter.go`
- `services/worker/internal/jobs/backtest_starter_test.go`
- `services/worker/internal/jobs/backtest_jobs.go`
- `services/worker/internal/jobs/backtest_jobs_test.go`
- `services/worker/internal/backtest/flow_test.go`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/runner_backtest_test.go`
- `apps/cli/internal/operator/client.go`
- `apps/cli/internal/operator/client_test.go`
- `apps/cli/internal/operator/handoff_test.go`
- `apps/cli/testdata/operator/backtest_run_request.yaml`
- `apps/cli/testdata/operator/backtest_result_summary.yaml`
- `apps/cli/testdata/operator/backtest_run_polling.yaml`
- `apps/cli/testdata/operator/invalid_request_matrix.yaml`
- `apps/cli/testdata/operator/headless_validation.md`
### 테스트 환경 규칙
- `test_env=local`로 판단했다.
- `agent-test/local/rules.md`가 존재해 읽었다.
- 매칭 profile로 `operations-smoke.md`, `worker-smoke.md`, `api-smoke.md`, `contracts-smoke.md`, `domain-model-smoke.md`를 읽었다.
- operations/headless 변경이므로 CLI operator tests와 fixture handoff tests를 포함한다.
- worker async job payload 변경이므로 worker jobs/backtest tests를 포함한다.
- `go test -count=1` fresh 실행만 허용하고 test cache 출력은 허용하지 않는다.
- 현재 기준으로 최종 검증 후보 `go test -count=1 ./packages/domain/... ./services/worker/internal/backtest ./services/worker/internal/jobs ./services/worker/internal/socket ./services/api/internal/socket ./services/api/internal/workerclient ./apps/cli/internal/operator ./apps/cli/internal/cli`가 exit 0임을 확인했다.
### 테스트 커버리지 공백
- `RunBacktestPayload`는 strategy/market/timeframe/date만 담고 selector를 보존하지 않는다.
- `RegisterRunBacktestHandler`는 payload에서 spec을 재구성하며 기존 run spec을 덮어쓰므로 selector가 사라질 수 있다.
- operator `start_backtest`는 named universe를 지원하지 않고 selector를 `BacktestRunSpec`에 넣지 않는다.
- headless fixtures는 import 후 start가 같은 universe를 backtest input selector로 넘기는지 문서화하지 않는다.
### 심볼 참조
- renamed/removed symbol: none.
- `RunBacktestPayload` references: `services/worker/internal/jobs/backtest_starter.go:80`, `services/worker/internal/jobs/backtest_jobs.go:20`, `services/worker/internal/jobs/backtest_jobs.go:41`, `services/worker/internal/jobs/backtest_jobs.go:90`, `services/worker/internal/jobs/backtest_starter_test.go:66`, `services/worker/internal/jobs/backtest_jobs_test.go:350`.
- `ActionStartBacktest` references: `apps/cli/internal/operator/scenario.go:35`, `apps/cli/internal/operator/scenario.go:638`, `apps/cli/internal/operator/runner.go:276`, `apps/cli/internal/operator/runner_backtest_test.go:27`, `apps/cli/internal/operator/runner_backtest_test.go:92`.
### 분할 판단
- split decision policy를 평가했고 multi-plan을 선택했다.
- shared task group: `agent-task/m-backtest-data-collection-infrastructure/`
- 이 subtask: `03+01,02_backtest_selector_flow`.
- predecessor `01_input_selector_contract`: `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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -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())
}
}

View file

@ -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"},

View file

@ -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:

View file

@ -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 미정 |

View file

@ -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<BacktestInputSelector>(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<BacktestInputSelector>(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 {

View file

@ -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 = {

View file

@ -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,
},

View file

@ -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 {

View file

@ -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 {

View file

@ -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) {

View file

@ -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) {

View file

@ -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,
}
}

View file

@ -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) {