feat: backtest analysis surface implementation
- Add backtest analysis results migration (000002) - Update backtest proto definitions and generated code - Extend backtest domain types and engine - Add parser map for backtest analysis - Update storage layer with backtest analysis queries and keys - Add client contract tests and generated code - Add m-backtest-analysis-surface task documentation
This commit is contained in:
parent
a5710b20ff
commit
9efc0a68dc
26 changed files with 2366 additions and 152 deletions
|
|
@ -1,65 +0,0 @@
|
|||
# Milestone: Backtest Engine Baseline
|
||||
|
||||
## 위치
|
||||
|
||||
- Roadmap: `agent-roadmap/ROADMAP.md`
|
||||
- Phase: `agent-roadmap/phase/backtest-loop/PHASE.md`
|
||||
|
||||
## 목표
|
||||
|
||||
정규화된 일봉 데이터를 사용해 전략 실행, portfolio state, result summary가 재현 가능한 backtest loop를 만든다. 이 milestone은 백테스트 기반을 ALT 확장의 중심축으로 고정한다.
|
||||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- 결정 필요: 없음
|
||||
|
||||
## 범위
|
||||
|
||||
- daily bar 기반 strategy execution skeleton
|
||||
- deterministic backtest run lifecycle
|
||||
- starting cash, ending equity, trades/positions summary
|
||||
- result persistence and query
|
||||
- fixture data 기반 검증
|
||||
|
||||
## 기능
|
||||
|
||||
### Epic: [backtest-engine] Deterministic backtest engine
|
||||
|
||||
일봉 기반 전략 실행, portfolio state, result 저장과 fixture 검증 흐름을 묶는다.
|
||||
|
||||
- [ ] [run-lifecycle] backtest run의 pending/running/succeeded/failed/canceled lifecycle이 있다. 검증: worker가 backtest 실행을 소유하고 API는 실행 요청/조회 경계만 담당한다.
|
||||
- [x] [strategy-port] strategy를 engine에 주입할 port 또는 interface가 있다.
|
||||
- [x] [portfolio-state] cash, position, equity를 계산하는 최소 portfolio state가 있다.
|
||||
- [ ] [result-store] backtest result가 저장되고 조회된다. 검증: 결과가 contracts/client에서 조회 가능한 형태로 연결될 수 있다.
|
||||
- [ ] [fixture-test] fixture daily bars로 deterministic test가 통과한다. 검증: 동일한 fixture와 strategy 입력으로 동일한 result가 나오고 `bin/test`와 `bin/lint`가 통과한다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 아직 기능 Task와 각 Task의 검증 조건이 충족되지 않았다.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
|
||||
## 범위 제외
|
||||
|
||||
- 실시간 전략 실행
|
||||
- paper/live order routing
|
||||
- 고급 리스크 모델
|
||||
- 고급 성과 분석 지표 전체
|
||||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 관련 경로: `packages/domain/backtest/`, `services/worker/`, `packages/contracts/proto/alt/v1/backtest.proto`
|
||||
- 표준선(선택): Go는 Hexagonal Architecture, light CQRS, domain event 표현을 기본 설계 프레임으로 삼는다.
|
||||
- 선행 작업: Korea Daily Data Foundation
|
||||
- 후속 작업: Backtest Analysis Surface
|
||||
- 진행 근거(2026-05-30): `[strategy-port]`, `[portfolio-state]`는 `packages/domain/backtest/types.go`, `packages/domain/backtest/types_test.go`, `go test ./packages/domain/...`로 확인했다.
|
||||
- 확인 필요: 없음
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<!-- task=m-backtest-analysis-surface/03+01,02_api_client_boundary plan=0 tag=BAS-BOUNDARY -->
|
||||
|
||||
# Code Review Reference - BAS-BOUNDARY
|
||||
|
||||
> **[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, 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`.
|
||||
> 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-05-30
|
||||
task=m-backtest-analysis-surface/03+01,02_api_client_boundary, plan=0, tag=BAS-BOUNDARY
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. active plan/review files를 `.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-backtest-analysis-surface/03+01,02_api_client_boundary/`로 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정은 런타임 책임이다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [BAS-BOUNDARY-1] Align API parser boundary | [ ] |
|
||||
| [BAS-BOUNDARY-2] Add typed client request helpers | [ ] |
|
||||
| [BAS-BOUNDARY-3] Test backtest socket helpers | [ ] |
|
||||
| [BAS-BOUNDARY-4] Boundary verification | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [BAS-BOUNDARY-1] API parser map and tests include all analysis request/response messages if not already done by contracts task.
|
||||
- [ ] [BAS-BOUNDARY-2] `AltSocketClient` exposes typed backtest list/detail/result/compare request helpers.
|
||||
- [ ] [BAS-BOUNDARY-3] Flutter socket client tests verify request type names, payloads, and response parsing. 검증: worker/API/client 경계가 backtest result를 일관되게 다룬다.
|
||||
- [ ] [BAS-BOUNDARY-4] `go test ./services/api/...`, `cd apps/client && flutter test test/integrations/socket/alt_socket_client_test.dart`, `bin/test`, `bin/lint`를 실행한다. 검증: `bin/test`와 `bin/lint`가 통과한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 외부 환경 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. code-review가 이 내용을 검증해 `USER_REVIEW.md`를 작성한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Client helper request/response generic types match generated protobuf messages.
|
||||
- Fake websocket tests assert `PacketBase.typeName`, request payload, and parsed response.
|
||||
- API parser map remains aligned with client parser map for analysis messages.
|
||||
- No worker internals are imported into API or client.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
|
||||
### BAS-BOUNDARY-1 중간 검증
|
||||
```text
|
||||
$ go test ./services/api/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### BAS-BOUNDARY-2 중간 검증
|
||||
```text
|
||||
$ cd apps/client && flutter analyze --no-fatal-infos
|
||||
(output)
|
||||
```
|
||||
|
||||
### BAS-BOUNDARY-3 중간 검증
|
||||
```text
|
||||
$ cd apps/client && flutter test test/integrations/socket/alt_socket_client_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ go test ./services/api/...
|
||||
$ cd apps/client && flutter test test/integrations/socket/alt_socket_client_test.dart
|
||||
$ bin/test
|
||||
$ bin/lint
|
||||
$ git diff --check
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
<!-- task=m-backtest-analysis-surface/03+01,02_api_client_boundary plan=0 tag=BAS-BOUNDARY -->
|
||||
|
||||
# Plan - BAS-BOUNDARY
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수다. 구현 후 검증을 실행하고, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 사용자 결정, 외부 환경 준비, 범위 충돌이 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 멈춘다. archive, `complete.log`, 최종 판정은 code-review 전용이다.
|
||||
|
||||
## 배경
|
||||
|
||||
Contract와 worker storage가 준비되더라도 operator client가 typed backtest query 요청을 보낼 수 없으면 analysis surface가 실제 사용 흐름으로 연결되지 않는다. 이 계획은 API parser boundary와 Flutter socket client request helpers를 좁게 연결해 run list/detail/compare 표면의 첫 호출 경계를 만든다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단 조건은 active `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 이를 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/backtest-loop/milestones/backtest-analysis-surface.md`
|
||||
- `services/api/internal/contracts/parser_map.go`
|
||||
- `services/api/internal/contracts/parser_map_test.go`
|
||||
- `apps/client/lib/src/contracts/alt_contracts.dart`
|
||||
- `apps/client/test/contracts/alt_contracts_test.dart`
|
||||
- `apps/client/lib/src/integrations/socket/alt_socket_client.dart`
|
||||
- `apps/client/test/integrations/socket/alt_socket_client_test.dart`
|
||||
- `packages/contracts/proto/alt/v1/backtest.proto`
|
||||
- `bin/test`
|
||||
- `bin/lint`
|
||||
- `services/api/go.mod`
|
||||
- `apps/client/pubspec.yaml`
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- `AltSocketClient` currently tests only `hello()` request/response at `apps/client/test/integrations/socket/alt_socket_client_test.dart:113`.
|
||||
- No client test sends backtest run list/detail/result/compare requests through proto-socket.
|
||||
- API parser tests cover current message types only and must remain aligned with generated messages from `01_contract_surface`.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- Renamed/removed symbols: none planned.
|
||||
- New client methods will call existing inherited `sendRequest` in `AltSocketClient`, currently used by `hello()` at `apps/client/lib/src/integrations/socket/alt_socket_client.dart:32`.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- Split policy evaluated before writing plan files.
|
||||
- This task depends on `01_contract_surface` and `02+01_worker_analysis_store`.
|
||||
- It is separate because API/client boundary tests can be reviewed independently after schema and storage behavior exist.
|
||||
- It does not create full Flutter operator UI; that belongs to later `Flutter Operator Console`.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- Include API parser alignment if new messages were not already covered by `01_contract_surface`, typed client request helpers, and socket client tests.
|
||||
- Exclude server-side business handlers because current API service only owns parser/socket boundary; worker owns execution and storage.
|
||||
- Exclude dashboard UI and navigation.
|
||||
- Exclude migrations/storage behavior already covered by `02+01_worker_analysis_store`.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build lane: `cloud-G06`; review lane: `cloud-G06`.
|
||||
- Rationale: cross-language boundary helpers and tests, but no storage or schema design should remain by this stage.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [BAS-BOUNDARY-1] API parser map and tests include all analysis request/response messages if not already done by contracts task.
|
||||
- [ ] [BAS-BOUNDARY-2] `AltSocketClient` exposes typed backtest list/detail/result/compare request helpers.
|
||||
- [ ] [BAS-BOUNDARY-3] Flutter socket client tests verify request type names, payloads, and response parsing. 검증: worker/API/client 경계가 backtest result를 일관되게 다룬다.
|
||||
- [ ] [BAS-BOUNDARY-4] `go test ./services/api/...`, `cd apps/client && flutter test test/integrations/socket/alt_socket_client_test.dart`, `bin/test`, `bin/lint`를 실행한다. 검증: `bin/test`와 `bin/lint`가 통과한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [BAS-BOUNDARY-1] Align API parser boundary
|
||||
|
||||
문제: `services/api/internal/contracts/parser_map.go:19` currently registers existing backtest messages. If `01_contract_surface` added list/detail/compare messages and did not register them, API parser tests will not cover the analysis request surface.
|
||||
|
||||
해결 방법:
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
19 protoSocket.TypeNameOf(&altv1.StartBacktestRequest{}): parserFor(func() proto.Message { return &altv1.StartBacktestRequest{} }),
|
||||
21 protoSocket.TypeNameOf(&altv1.GetBacktestRunRequest{}): parserFor(func() proto.Message { return &altv1.GetBacktestRunRequest{} }),
|
||||
23 protoSocket.TypeNameOf(&altv1.GetBacktestResultRequest{}): parserFor(func() proto.Message { return &altv1.GetBacktestResultRequest{} }),
|
||||
```
|
||||
|
||||
After: ensure `ListBacktestRuns*`, `GetBacktestRunDetail*`, and `CompareBacktestRuns*` generated messages are present in `ParserMap()` and `TestParserMapIncludesAltMessages`.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `services/api/internal/contracts/parser_map.go` includes all analysis request/response messages.
|
||||
- [ ] `services/api/internal/contracts/parser_map_test.go` includes all analysis messages and round-trip parsing.
|
||||
|
||||
테스트 작성: update existing parser map test only.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./services/api/...
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
### [BAS-BOUNDARY-2] Add typed client request helpers
|
||||
|
||||
문제: `AltSocketClient.hello()` at `apps/client/lib/src/integrations/socket/alt_socket_client.dart:32` is the only typed helper. Backtest analysis requests would require callers to use raw generated messages directly.
|
||||
|
||||
해결 방법:
|
||||
|
||||
Before:
|
||||
|
||||
```dart
|
||||
32 Future<HelloResponse> hello({Duration timeout = const Duration(seconds: 2)}) {
|
||||
33 return sendRequest<HelloRequest, HelloResponse>(
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```dart
|
||||
Future<ListBacktestRunsResponse> listBacktestRuns(
|
||||
ListBacktestRunsRequest request, {
|
||||
Duration timeout = const Duration(seconds: 2),
|
||||
}) {
|
||||
return sendRequest<ListBacktestRunsRequest, ListBacktestRunsResponse>(
|
||||
request,
|
||||
timeout: timeout,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Add equivalent helpers for `GetBacktestRunDetailRequest`, existing `GetBacktestResultRequest`, and `CompareBacktestRunsRequest`.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/client/lib/src/integrations/socket/alt_socket_client.dart` imports generated `backtest.pb.dart`.
|
||||
- [ ] Add `listBacktestRuns`, `getBacktestRunDetail`, `getBacktestResult`, and `compareBacktestRuns` helpers.
|
||||
- [ ] Keep default timeout consistent with `hello()`.
|
||||
|
||||
테스트 작성: tests in BAS-BOUNDARY-3.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter analyze --no-fatal-infos
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
### [BAS-BOUNDARY-3] Test backtest socket helpers
|
||||
|
||||
문제: `apps/client/test/integrations/socket/alt_socket_client_test.dart:113` verifies hello only; no test asserts backtest request type names or response parsing over `PacketBase`.
|
||||
|
||||
해결 방법: Extend fake websocket tests with one helper to assert sent packet type/payload and feed a matching response packet. Cover at least list runs, run detail, get result, and compare runs.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/client/test/integrations/socket/alt_socket_client_test.dart` imports generated `backtest.pb.dart`.
|
||||
- [ ] Add tests for each new client helper.
|
||||
- [ ] Tests assert sent `PacketBase.typeName`, serialized request fields, and parsed response fields.
|
||||
|
||||
테스트 작성: add focused tests in existing `AltSocketClient tests` group.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter test test/integrations/socket/alt_socket_client_test.dart
|
||||
```
|
||||
|
||||
Expected: exits 0.
|
||||
|
||||
### [BAS-BOUNDARY-4] Boundary verification
|
||||
|
||||
문제: API parser, generated contract, and client helper changes must remain consistent across the workspace.
|
||||
|
||||
해결 방법: run focused API/client checks and full workspace smoke.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] Run API parser tests.
|
||||
- [ ] Run client socket helper tests.
|
||||
- [ ] Run full `bin/test`.
|
||||
- [ ] Run full `bin/lint`.
|
||||
- [ ] Run `git diff --check`.
|
||||
|
||||
테스트 작성: no extra tests beyond BAS-BOUNDARY-1 and BAS-BOUNDARY-3.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./services/api/...
|
||||
cd apps/client && flutter test test/integrations/socket/alt_socket_client_test.dart
|
||||
bin/test
|
||||
bin/lint
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all commands exit 0. Go test cache output is acceptable after focused API tests pass; Flutter test output must be actual command output.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
Directory dependency `03+01,02_api_client_boundary` means both predecessor task directories must produce `complete.log` before this task starts:
|
||||
|
||||
- `agent-task/m-backtest-analysis-surface/01_contract_surface/complete.log`
|
||||
- `agent-task/m-backtest-analysis-surface/02+01_worker_analysis_store/complete.log`
|
||||
|
||||
Do not begin if either file is absent.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `services/api/internal/contracts/parser_map.go` | BAS-BOUNDARY-1 |
|
||||
| `services/api/internal/contracts/parser_map_test.go` | BAS-BOUNDARY-1 |
|
||||
| `apps/client/lib/src/integrations/socket/alt_socket_client.dart` | BAS-BOUNDARY-2 |
|
||||
| `apps/client/test/integrations/socket/alt_socket_client_test.dart` | BAS-BOUNDARY-3 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test ./services/api/...
|
||||
cd apps/client && flutter test test/integrations/socket/alt_socket_client_test.dart
|
||||
bin/test
|
||||
bin/lint
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all commands exit 0. Go test cache output is acceptable after focused API tests pass; Flutter test output must be actual command output.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -19,5 +19,11 @@ Map<String, GeneratedMessage Function(List<int>)> altParserMap() {
|
|||
GetBacktestResultRequest.getDefault().info_.qualifiedMessageName: GetBacktestResultRequest.fromBuffer,
|
||||
GetBacktestResultResponse.getDefault().info_.qualifiedMessageName: GetBacktestResultResponse.fromBuffer,
|
||||
BacktestResult.getDefault().info_.qualifiedMessageName: BacktestResult.fromBuffer,
|
||||
ListBacktestRunsRequest.getDefault().info_.qualifiedMessageName: ListBacktestRunsRequest.fromBuffer,
|
||||
ListBacktestRunsResponse.getDefault().info_.qualifiedMessageName: ListBacktestRunsResponse.fromBuffer,
|
||||
GetBacktestRunDetailRequest.getDefault().info_.qualifiedMessageName: GetBacktestRunDetailRequest.fromBuffer,
|
||||
GetBacktestRunDetailResponse.getDefault().info_.qualifiedMessageName: GetBacktestRunDetailResponse.fromBuffer,
|
||||
CompareBacktestRunsRequest.getDefault().info_.qualifiedMessageName: CompareBacktestRunsRequest.fromBuffer,
|
||||
CompareBacktestRunsResponse.getDefault().info_.qualifiedMessageName: CompareBacktestRunsResponse.fromBuffer,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import 'package:fixnum/fixnum.dart' as $fixnum;
|
|||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
import 'backtest.pbenum.dart';
|
||||
import 'common.pbenum.dart' as $1;
|
||||
import 'common.pb.dart' as $1;
|
||||
import 'market.pb.dart' as $0;
|
||||
|
||||
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
||||
|
|
@ -653,6 +653,175 @@ class BacktestPosition extends $pb.GeneratedMessage {
|
|||
$0.Price ensureLastPrice() => $_ensure(2);
|
||||
}
|
||||
|
||||
class BacktestSummaryMetrics extends $pb.GeneratedMessage {
|
||||
factory BacktestSummaryMetrics({
|
||||
$0.Price? startingCash,
|
||||
$0.Price? endingEquity,
|
||||
$1.Decimal? totalReturn,
|
||||
$core.int? tradeCount,
|
||||
}) {
|
||||
final result = create();
|
||||
if (startingCash != null) result.startingCash = startingCash;
|
||||
if (endingEquity != null) result.endingEquity = endingEquity;
|
||||
if (totalReturn != null) result.totalReturn = totalReturn;
|
||||
if (tradeCount != null) result.tradeCount = tradeCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
BacktestSummaryMetrics._();
|
||||
|
||||
factory BacktestSummaryMetrics.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory BacktestSummaryMetrics.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'BacktestSummaryMetrics',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..aOM<$0.Price>(1, _omitFieldNames ? '' : 'startingCash',
|
||||
subBuilder: $0.Price.create)
|
||||
..aOM<$0.Price>(2, _omitFieldNames ? '' : 'endingEquity',
|
||||
subBuilder: $0.Price.create)
|
||||
..aOM<$1.Decimal>(3, _omitFieldNames ? '' : 'totalReturn',
|
||||
subBuilder: $1.Decimal.create)
|
||||
..aI(4, _omitFieldNames ? '' : 'tradeCount')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
BacktestSummaryMetrics clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
BacktestSummaryMetrics copyWith(
|
||||
void Function(BacktestSummaryMetrics) updates) =>
|
||||
super.copyWith((message) => updates(message as BacktestSummaryMetrics))
|
||||
as BacktestSummaryMetrics;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static BacktestSummaryMetrics create() => BacktestSummaryMetrics._();
|
||||
@$core.override
|
||||
BacktestSummaryMetrics createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static BacktestSummaryMetrics getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<BacktestSummaryMetrics>(create);
|
||||
static BacktestSummaryMetrics? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$0.Price get startingCash => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set startingCash($0.Price value) => $_setField(1, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasStartingCash() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearStartingCash() => $_clearField(1);
|
||||
@$pb.TagNumber(1)
|
||||
$0.Price ensureStartingCash() => $_ensure(0);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$0.Price get endingEquity => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set endingEquity($0.Price value) => $_setField(2, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasEndingEquity() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearEndingEquity() => $_clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
$0.Price ensureEndingEquity() => $_ensure(1);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$1.Decimal get totalReturn => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set totalReturn($1.Decimal value) => $_setField(3, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasTotalReturn() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearTotalReturn() => $_clearField(3);
|
||||
@$pb.TagNumber(3)
|
||||
$1.Decimal ensureTotalReturn() => $_ensure(2);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.int get tradeCount => $_getIZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set tradeCount($core.int value) => $_setSignedInt32(3, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasTradeCount() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearTradeCount() => $_clearField(4);
|
||||
}
|
||||
|
||||
class BacktestEquityPoint extends $pb.GeneratedMessage {
|
||||
factory BacktestEquityPoint({
|
||||
$fixnum.Int64? timestampUnixMs,
|
||||
$0.Price? equity,
|
||||
}) {
|
||||
final result = create();
|
||||
if (timestampUnixMs != null) result.timestampUnixMs = timestampUnixMs;
|
||||
if (equity != null) result.equity = equity;
|
||||
return result;
|
||||
}
|
||||
|
||||
BacktestEquityPoint._();
|
||||
|
||||
factory BacktestEquityPoint.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory BacktestEquityPoint.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'BacktestEquityPoint',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..aInt64(1, _omitFieldNames ? '' : 'timestampUnixMs')
|
||||
..aOM<$0.Price>(2, _omitFieldNames ? '' : 'equity',
|
||||
subBuilder: $0.Price.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
BacktestEquityPoint clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
BacktestEquityPoint copyWith(void Function(BacktestEquityPoint) updates) =>
|
||||
super.copyWith((message) => updates(message as BacktestEquityPoint))
|
||||
as BacktestEquityPoint;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static BacktestEquityPoint create() => BacktestEquityPoint._();
|
||||
@$core.override
|
||||
BacktestEquityPoint createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static BacktestEquityPoint getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<BacktestEquityPoint>(create);
|
||||
static BacktestEquityPoint? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$fixnum.Int64 get timestampUnixMs => $_getI64(0);
|
||||
@$pb.TagNumber(1)
|
||||
set timestampUnixMs($fixnum.Int64 value) => $_setInt64(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasTimestampUnixMs() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearTimestampUnixMs() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$0.Price get equity => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set equity($0.Price value) => $_setField(2, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasEquity() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearEquity() => $_clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
$0.Price ensureEquity() => $_ensure(1);
|
||||
}
|
||||
|
||||
class BacktestResult extends $pb.GeneratedMessage {
|
||||
factory BacktestResult({
|
||||
$core.String? runId,
|
||||
|
|
@ -660,6 +829,8 @@ class BacktestResult extends $pb.GeneratedMessage {
|
|||
$0.Price? endingEquity,
|
||||
$core.Iterable<BacktestTrade>? trades,
|
||||
$core.Iterable<BacktestPosition>? positions,
|
||||
BacktestSummaryMetrics? summary,
|
||||
$core.Iterable<BacktestEquityPoint>? equityCurve,
|
||||
}) {
|
||||
final result = create();
|
||||
if (runId != null) result.runId = runId;
|
||||
|
|
@ -667,6 +838,8 @@ class BacktestResult extends $pb.GeneratedMessage {
|
|||
if (endingEquity != null) result.endingEquity = endingEquity;
|
||||
if (trades != null) result.trades.addAll(trades);
|
||||
if (positions != null) result.positions.addAll(positions);
|
||||
if (summary != null) result.summary = summary;
|
||||
if (equityCurve != null) result.equityCurve.addAll(equityCurve);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -692,6 +865,10 @@ class BacktestResult extends $pb.GeneratedMessage {
|
|||
subBuilder: BacktestTrade.create)
|
||||
..pPM<BacktestPosition>(5, _omitFieldNames ? '' : 'positions',
|
||||
subBuilder: BacktestPosition.create)
|
||||
..aOM<BacktestSummaryMetrics>(6, _omitFieldNames ? '' : 'summary',
|
||||
subBuilder: BacktestSummaryMetrics.create)
|
||||
..pPM<BacktestEquityPoint>(7, _omitFieldNames ? '' : 'equityCurve',
|
||||
subBuilder: BacktestEquityPoint.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -749,6 +926,20 @@ class BacktestResult extends $pb.GeneratedMessage {
|
|||
|
||||
@$pb.TagNumber(5)
|
||||
$pb.PbList<BacktestPosition> get positions => $_getList(4);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
BacktestSummaryMetrics get summary => $_getN(5);
|
||||
@$pb.TagNumber(6)
|
||||
set summary(BacktestSummaryMetrics value) => $_setField(6, value);
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasSummary() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearSummary() => $_clearField(6);
|
||||
@$pb.TagNumber(6)
|
||||
BacktestSummaryMetrics ensureSummary() => $_ensure(5);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
$pb.PbList<BacktestEquityPoint> get equityCurve => $_getList(6);
|
||||
}
|
||||
|
||||
class GetBacktestResultRequest extends $pb.GeneratedMessage {
|
||||
|
|
@ -864,6 +1055,346 @@ class GetBacktestResultResponse extends $pb.GeneratedMessage {
|
|||
BacktestResult ensureResult() => $_ensure(0);
|
||||
}
|
||||
|
||||
class ListBacktestRunsRequest extends $pb.GeneratedMessage {
|
||||
factory ListBacktestRunsRequest({
|
||||
BacktestRunStatus? status,
|
||||
}) {
|
||||
final result = create();
|
||||
if (status != null) result.status = status;
|
||||
return result;
|
||||
}
|
||||
|
||||
ListBacktestRunsRequest._();
|
||||
|
||||
factory ListBacktestRunsRequest.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ListBacktestRunsRequest.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ListBacktestRunsRequest',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..aE<BacktestRunStatus>(1, _omitFieldNames ? '' : 'status',
|
||||
enumValues: BacktestRunStatus.values)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ListBacktestRunsRequest clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ListBacktestRunsRequest copyWith(
|
||||
void Function(ListBacktestRunsRequest) updates) =>
|
||||
super.copyWith((message) => updates(message as ListBacktestRunsRequest))
|
||||
as ListBacktestRunsRequest;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ListBacktestRunsRequest create() => ListBacktestRunsRequest._();
|
||||
@$core.override
|
||||
ListBacktestRunsRequest createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ListBacktestRunsRequest getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<ListBacktestRunsRequest>(create);
|
||||
static ListBacktestRunsRequest? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
BacktestRunStatus get status => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set status(BacktestRunStatus value) => $_setField(1, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasStatus() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearStatus() => $_clearField(1);
|
||||
}
|
||||
|
||||
class ListBacktestRunsResponse extends $pb.GeneratedMessage {
|
||||
factory ListBacktestRunsResponse({
|
||||
$core.Iterable<BacktestRun>? runs,
|
||||
}) {
|
||||
final result = create();
|
||||
if (runs != null) result.runs.addAll(runs);
|
||||
return result;
|
||||
}
|
||||
|
||||
ListBacktestRunsResponse._();
|
||||
|
||||
factory ListBacktestRunsResponse.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ListBacktestRunsResponse.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ListBacktestRunsResponse',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..pPM<BacktestRun>(1, _omitFieldNames ? '' : 'runs',
|
||||
subBuilder: BacktestRun.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ListBacktestRunsResponse clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ListBacktestRunsResponse copyWith(
|
||||
void Function(ListBacktestRunsResponse) updates) =>
|
||||
super.copyWith((message) => updates(message as ListBacktestRunsResponse))
|
||||
as ListBacktestRunsResponse;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ListBacktestRunsResponse create() => ListBacktestRunsResponse._();
|
||||
@$core.override
|
||||
ListBacktestRunsResponse createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ListBacktestRunsResponse getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<ListBacktestRunsResponse>(create);
|
||||
static ListBacktestRunsResponse? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$pb.PbList<BacktestRun> get runs => $_getList(0);
|
||||
}
|
||||
|
||||
class GetBacktestRunDetailRequest extends $pb.GeneratedMessage {
|
||||
factory GetBacktestRunDetailRequest({
|
||||
$core.String? runId,
|
||||
}) {
|
||||
final result = create();
|
||||
if (runId != null) result.runId = runId;
|
||||
return result;
|
||||
}
|
||||
|
||||
GetBacktestRunDetailRequest._();
|
||||
|
||||
factory GetBacktestRunDetailRequest.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory GetBacktestRunDetailRequest.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'GetBacktestRunDetailRequest',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'runId')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
GetBacktestRunDetailRequest clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
GetBacktestRunDetailRequest copyWith(
|
||||
void Function(GetBacktestRunDetailRequest) updates) =>
|
||||
super.copyWith(
|
||||
(message) => updates(message as GetBacktestRunDetailRequest))
|
||||
as GetBacktestRunDetailRequest;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static GetBacktestRunDetailRequest create() =>
|
||||
GetBacktestRunDetailRequest._();
|
||||
@$core.override
|
||||
GetBacktestRunDetailRequest createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static GetBacktestRunDetailRequest getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<GetBacktestRunDetailRequest>(create);
|
||||
static GetBacktestRunDetailRequest? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get runId => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set runId($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasRunId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearRunId() => $_clearField(1);
|
||||
}
|
||||
|
||||
class GetBacktestRunDetailResponse extends $pb.GeneratedMessage {
|
||||
factory GetBacktestRunDetailResponse({
|
||||
BacktestRun? run,
|
||||
BacktestResult? result,
|
||||
}) {
|
||||
final result$ = create();
|
||||
if (run != null) result$.run = run;
|
||||
if (result != null) result$.result = result;
|
||||
return result$;
|
||||
}
|
||||
|
||||
GetBacktestRunDetailResponse._();
|
||||
|
||||
factory GetBacktestRunDetailResponse.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory GetBacktestRunDetailResponse.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'GetBacktestRunDetailResponse',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..aOM<BacktestRun>(1, _omitFieldNames ? '' : 'run',
|
||||
subBuilder: BacktestRun.create)
|
||||
..aOM<BacktestResult>(2, _omitFieldNames ? '' : 'result',
|
||||
subBuilder: BacktestResult.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
GetBacktestRunDetailResponse clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
GetBacktestRunDetailResponse copyWith(
|
||||
void Function(GetBacktestRunDetailResponse) updates) =>
|
||||
super.copyWith(
|
||||
(message) => updates(message as GetBacktestRunDetailResponse))
|
||||
as GetBacktestRunDetailResponse;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static GetBacktestRunDetailResponse create() =>
|
||||
GetBacktestRunDetailResponse._();
|
||||
@$core.override
|
||||
GetBacktestRunDetailResponse createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static GetBacktestRunDetailResponse getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<GetBacktestRunDetailResponse>(create);
|
||||
static GetBacktestRunDetailResponse? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
BacktestRun get run => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set run(BacktestRun value) => $_setField(1, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasRun() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearRun() => $_clearField(1);
|
||||
@$pb.TagNumber(1)
|
||||
BacktestRun ensureRun() => $_ensure(0);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
BacktestResult get result => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set result(BacktestResult value) => $_setField(2, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasResult() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearResult() => $_clearField(2);
|
||||
@$pb.TagNumber(2)
|
||||
BacktestResult ensureResult() => $_ensure(1);
|
||||
}
|
||||
|
||||
class CompareBacktestRunsRequest extends $pb.GeneratedMessage {
|
||||
factory CompareBacktestRunsRequest({
|
||||
$core.Iterable<$core.String>? runIds,
|
||||
}) {
|
||||
final result = create();
|
||||
if (runIds != null) result.runIds.addAll(runIds);
|
||||
return result;
|
||||
}
|
||||
|
||||
CompareBacktestRunsRequest._();
|
||||
|
||||
factory CompareBacktestRunsRequest.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory CompareBacktestRunsRequest.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'CompareBacktestRunsRequest',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..pPS(1, _omitFieldNames ? '' : 'runIds')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
CompareBacktestRunsRequest clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
CompareBacktestRunsRequest copyWith(
|
||||
void Function(CompareBacktestRunsRequest) updates) =>
|
||||
super.copyWith(
|
||||
(message) => updates(message as CompareBacktestRunsRequest))
|
||||
as CompareBacktestRunsRequest;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static CompareBacktestRunsRequest create() => CompareBacktestRunsRequest._();
|
||||
@$core.override
|
||||
CompareBacktestRunsRequest createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static CompareBacktestRunsRequest getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<CompareBacktestRunsRequest>(create);
|
||||
static CompareBacktestRunsRequest? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$pb.PbList<$core.String> get runIds => $_getList(0);
|
||||
}
|
||||
|
||||
class CompareBacktestRunsResponse extends $pb.GeneratedMessage {
|
||||
factory CompareBacktestRunsResponse({
|
||||
$core.Iterable<BacktestResult>? results,
|
||||
}) {
|
||||
final result = create();
|
||||
if (results != null) result.results.addAll(results);
|
||||
return result;
|
||||
}
|
||||
|
||||
CompareBacktestRunsResponse._();
|
||||
|
||||
factory CompareBacktestRunsResponse.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory CompareBacktestRunsResponse.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'CompareBacktestRunsResponse',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..pPM<BacktestResult>(1, _omitFieldNames ? '' : 'results',
|
||||
subBuilder: BacktestResult.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
CompareBacktestRunsResponse clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
CompareBacktestRunsResponse copyWith(
|
||||
void Function(CompareBacktestRunsResponse) updates) =>
|
||||
super.copyWith(
|
||||
(message) => updates(message as CompareBacktestRunsResponse))
|
||||
as CompareBacktestRunsResponse;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static CompareBacktestRunsResponse create() =>
|
||||
CompareBacktestRunsResponse._();
|
||||
@$core.override
|
||||
CompareBacktestRunsResponse createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static CompareBacktestRunsResponse getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<CompareBacktestRunsResponse>(create);
|
||||
static CompareBacktestRunsResponse? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$pb.PbList<BacktestResult> get results => $_getList(0);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||
const $core.bool _omitMessageNames =
|
||||
|
|
|
|||
|
|
@ -251,6 +251,66 @@ final $typed_data.Uint8List backtestPositionDescriptor = $convert.base64Decode(
|
|||
'IsCghxdWFudGl0eRgCIAEoCzIQLmFsdC52MS5RdWFudGl0eVIIcXVhbnRpdHkSLAoKbGFzdF9w'
|
||||
'cmljZRgDIAEoCzINLmFsdC52MS5QcmljZVIJbGFzdFByaWNl');
|
||||
|
||||
@$core.Deprecated('Use backtestSummaryMetricsDescriptor instead')
|
||||
const BacktestSummaryMetrics$json = {
|
||||
'1': 'BacktestSummaryMetrics',
|
||||
'2': [
|
||||
{
|
||||
'1': 'starting_cash',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.Price',
|
||||
'10': 'startingCash'
|
||||
},
|
||||
{
|
||||
'1': 'ending_equity',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.Price',
|
||||
'10': 'endingEquity'
|
||||
},
|
||||
{
|
||||
'1': 'total_return',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.Decimal',
|
||||
'10': 'totalReturn'
|
||||
},
|
||||
{'1': 'trade_count', '3': 4, '4': 1, '5': 5, '10': 'tradeCount'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `BacktestSummaryMetrics`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List backtestSummaryMetricsDescriptor = $convert.base64Decode(
|
||||
'ChZCYWNrdGVzdFN1bW1hcnlNZXRyaWNzEjIKDXN0YXJ0aW5nX2Nhc2gYASABKAsyDS5hbHQudj'
|
||||
'EuUHJpY2VSDHN0YXJ0aW5nQ2FzaBIyCg1lbmRpbmdfZXF1aXR5GAIgASgLMg0uYWx0LnYxLlBy'
|
||||
'aWNlUgxlbmRpbmdFcXVpdHkSMgoMdG90YWxfcmV0dXJuGAMgASgLMg8uYWx0LnYxLkRlY2ltYW'
|
||||
'xSC3RvdGFsUmV0dXJuEh8KC3RyYWRlX2NvdW50GAQgASgFUgp0cmFkZUNvdW50');
|
||||
|
||||
@$core.Deprecated('Use backtestEquityPointDescriptor instead')
|
||||
const BacktestEquityPoint$json = {
|
||||
'1': 'BacktestEquityPoint',
|
||||
'2': [
|
||||
{'1': 'timestamp_unix_ms', '3': 1, '4': 1, '5': 3, '10': 'timestampUnixMs'},
|
||||
{
|
||||
'1': 'equity',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.Price',
|
||||
'10': 'equity'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `BacktestEquityPoint`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List backtestEquityPointDescriptor = $convert.base64Decode(
|
||||
'ChNCYWNrdGVzdEVxdWl0eVBvaW50EioKEXRpbWVzdGFtcF91bml4X21zGAEgASgDUg90aW1lc3'
|
||||
'RhbXBVbml4TXMSJQoGZXF1aXR5GAIgASgLMg0uYWx0LnYxLlByaWNlUgZlcXVpdHk=');
|
||||
|
||||
@$core.Deprecated('Use backtestResultDescriptor instead')
|
||||
const BacktestResult$json = {
|
||||
'1': 'BacktestResult',
|
||||
|
|
@ -288,6 +348,22 @@ const BacktestResult$json = {
|
|||
'6': '.alt.v1.BacktestPosition',
|
||||
'10': 'positions'
|
||||
},
|
||||
{
|
||||
'1': 'summary',
|
||||
'3': 6,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.BacktestSummaryMetrics',
|
||||
'10': 'summary'
|
||||
},
|
||||
{
|
||||
'1': 'equity_curve',
|
||||
'3': 7,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.BacktestEquityPoint',
|
||||
'10': 'equityCurve'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -297,7 +373,9 @@ final $typed_data.Uint8List backtestResultDescriptor = $convert.base64Decode(
|
|||
'gYAiABKAsyDS5hbHQudjEuUHJpY2VSDHN0YXJ0aW5nQ2FzaBIyCg1lbmRpbmdfZXF1aXR5GAMg'
|
||||
'ASgLMg0uYWx0LnYxLlByaWNlUgxlbmRpbmdFcXVpdHkSLQoGdHJhZGVzGAQgAygLMhUuYWx0Ln'
|
||||
'YxLkJhY2t0ZXN0VHJhZGVSBnRyYWRlcxI2Cglwb3NpdGlvbnMYBSADKAsyGC5hbHQudjEuQmFj'
|
||||
'a3Rlc3RQb3NpdGlvblIJcG9zaXRpb25z');
|
||||
'a3Rlc3RQb3NpdGlvblIJcG9zaXRpb25zEjgKB3N1bW1hcnkYBiABKAsyHi5hbHQudjEuQmFja3'
|
||||
'Rlc3RTdW1tYXJ5TWV0cmljc1IHc3VtbWFyeRI+CgxlcXVpdHlfY3VydmUYByADKAsyGy5hbHQu'
|
||||
'djEuQmFja3Rlc3RFcXVpdHlQb2ludFILZXF1aXR5Q3VydmU=');
|
||||
|
||||
@$core.Deprecated('Use getBacktestResultRequestDescriptor instead')
|
||||
const GetBacktestResultRequest$json = {
|
||||
|
|
@ -332,3 +410,122 @@ final $typed_data.Uint8List getBacktestResultResponseDescriptor =
|
|||
$convert.base64Decode(
|
||||
'ChlHZXRCYWNrdGVzdFJlc3VsdFJlc3BvbnNlEi4KBnJlc3VsdBgBIAEoCzIWLmFsdC52MS5CYW'
|
||||
'NrdGVzdFJlc3VsdFIGcmVzdWx0');
|
||||
|
||||
@$core.Deprecated('Use listBacktestRunsRequestDescriptor instead')
|
||||
const ListBacktestRunsRequest$json = {
|
||||
'1': 'ListBacktestRunsRequest',
|
||||
'2': [
|
||||
{
|
||||
'1': 'status',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.alt.v1.BacktestRunStatus',
|
||||
'10': 'status'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ListBacktestRunsRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List listBacktestRunsRequestDescriptor =
|
||||
$convert.base64Decode(
|
||||
'ChdMaXN0QmFja3Rlc3RSdW5zUmVxdWVzdBIxCgZzdGF0dXMYASABKA4yGS5hbHQudjEuQmFja3'
|
||||
'Rlc3RSdW5TdGF0dXNSBnN0YXR1cw==');
|
||||
|
||||
@$core.Deprecated('Use listBacktestRunsResponseDescriptor instead')
|
||||
const ListBacktestRunsResponse$json = {
|
||||
'1': 'ListBacktestRunsResponse',
|
||||
'2': [
|
||||
{
|
||||
'1': 'runs',
|
||||
'3': 1,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.BacktestRun',
|
||||
'10': 'runs'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ListBacktestRunsResponse`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List listBacktestRunsResponseDescriptor =
|
||||
$convert.base64Decode(
|
||||
'ChhMaXN0QmFja3Rlc3RSdW5zUmVzcG9uc2USJwoEcnVucxgBIAMoCzITLmFsdC52MS5CYWNrdG'
|
||||
'VzdFJ1blIEcnVucw==');
|
||||
|
||||
@$core.Deprecated('Use getBacktestRunDetailRequestDescriptor instead')
|
||||
const GetBacktestRunDetailRequest$json = {
|
||||
'1': 'GetBacktestRunDetailRequest',
|
||||
'2': [
|
||||
{'1': 'run_id', '3': 1, '4': 1, '5': 9, '10': 'runId'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `GetBacktestRunDetailRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List getBacktestRunDetailRequestDescriptor =
|
||||
$convert.base64Decode(
|
||||
'ChtHZXRCYWNrdGVzdFJ1bkRldGFpbFJlcXVlc3QSFQoGcnVuX2lkGAEgASgJUgVydW5JZA==');
|
||||
|
||||
@$core.Deprecated('Use getBacktestRunDetailResponseDescriptor instead')
|
||||
const GetBacktestRunDetailResponse$json = {
|
||||
'1': 'GetBacktestRunDetailResponse',
|
||||
'2': [
|
||||
{
|
||||
'1': 'run',
|
||||
'3': 1,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.BacktestRun',
|
||||
'10': 'run'
|
||||
},
|
||||
{
|
||||
'1': 'result',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.BacktestResult',
|
||||
'10': 'result'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `GetBacktestRunDetailResponse`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List getBacktestRunDetailResponseDescriptor =
|
||||
$convert.base64Decode(
|
||||
'ChxHZXRCYWNrdGVzdFJ1bkRldGFpbFJlc3BvbnNlEiUKA3J1bhgBIAEoCzITLmFsdC52MS5CYW'
|
||||
'NrdGVzdFJ1blIDcnVuEi4KBnJlc3VsdBgCIAEoCzIWLmFsdC52MS5CYWNrdGVzdFJlc3VsdFIG'
|
||||
'cmVzdWx0');
|
||||
|
||||
@$core.Deprecated('Use compareBacktestRunsRequestDescriptor instead')
|
||||
const CompareBacktestRunsRequest$json = {
|
||||
'1': 'CompareBacktestRunsRequest',
|
||||
'2': [
|
||||
{'1': 'run_ids', '3': 1, '4': 3, '5': 9, '10': 'runIds'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `CompareBacktestRunsRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List compareBacktestRunsRequestDescriptor =
|
||||
$convert.base64Decode(
|
||||
'ChpDb21wYXJlQmFja3Rlc3RSdW5zUmVxdWVzdBIXCgdydW5faWRzGAEgAygJUgZydW5JZHM=');
|
||||
|
||||
@$core.Deprecated('Use compareBacktestRunsResponseDescriptor instead')
|
||||
const CompareBacktestRunsResponse$json = {
|
||||
'1': 'CompareBacktestRunsResponse',
|
||||
'2': [
|
||||
{
|
||||
'1': 'results',
|
||||
'3': 1,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.BacktestResult',
|
||||
'10': 'results'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `CompareBacktestRunsResponse`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List compareBacktestRunsResponseDescriptor =
|
||||
$convert.base64Decode(
|
||||
'ChtDb21wYXJlQmFja3Rlc3RSdW5zUmVzcG9uc2USMAoHcmVzdWx0cxgBIAMoCzIWLmFsdC52MS'
|
||||
'5CYWNrdGVzdFJlc3VsdFIHcmVzdWx0cw==');
|
||||
|
|
|
|||
|
|
@ -23,9 +23,15 @@ void main() {
|
|||
GetBacktestResultRequest(),
|
||||
GetBacktestResultResponse(),
|
||||
BacktestResult(),
|
||||
ListBacktestRunsRequest(),
|
||||
ListBacktestRunsResponse(),
|
||||
GetBacktestRunDetailRequest(),
|
||||
GetBacktestRunDetailResponse(),
|
||||
CompareBacktestRunsRequest(),
|
||||
CompareBacktestRunsResponse(),
|
||||
];
|
||||
|
||||
expect(parsers.length, equals(13));
|
||||
expect(parsers.length, equals(19));
|
||||
|
||||
for (final msg in expectedMessages) {
|
||||
final qualifiedName = msg.info_.qualifiedMessageName;
|
||||
|
|
|
|||
|
|
@ -543,20 +543,142 @@ func (x *BacktestPosition) GetLastPrice() *Price {
|
|||
return nil
|
||||
}
|
||||
|
||||
type BacktestResult struct {
|
||||
type BacktestSummaryMetrics struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"`
|
||||
StartingCash *Price `protobuf:"bytes,2,opt,name=starting_cash,json=startingCash,proto3" json:"starting_cash,omitempty"`
|
||||
EndingEquity *Price `protobuf:"bytes,3,opt,name=ending_equity,json=endingEquity,proto3" json:"ending_equity,omitempty"`
|
||||
Trades []*BacktestTrade `protobuf:"bytes,4,rep,name=trades,proto3" json:"trades,omitempty"`
|
||||
Positions []*BacktestPosition `protobuf:"bytes,5,rep,name=positions,proto3" json:"positions,omitempty"`
|
||||
StartingCash *Price `protobuf:"bytes,1,opt,name=starting_cash,json=startingCash,proto3" json:"starting_cash,omitempty"`
|
||||
EndingEquity *Price `protobuf:"bytes,2,opt,name=ending_equity,json=endingEquity,proto3" json:"ending_equity,omitempty"`
|
||||
TotalReturn *Decimal `protobuf:"bytes,3,opt,name=total_return,json=totalReturn,proto3" json:"total_return,omitempty"`
|
||||
TradeCount int32 `protobuf:"varint,4,opt,name=trade_count,json=tradeCount,proto3" json:"trade_count,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BacktestSummaryMetrics) Reset() {
|
||||
*x = BacktestSummaryMetrics{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BacktestSummaryMetrics) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BacktestSummaryMetrics) ProtoMessage() {}
|
||||
|
||||
func (x *BacktestSummaryMetrics) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[8]
|
||||
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 BacktestSummaryMetrics.ProtoReflect.Descriptor instead.
|
||||
func (*BacktestSummaryMetrics) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *BacktestSummaryMetrics) GetStartingCash() *Price {
|
||||
if x != nil {
|
||||
return x.StartingCash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BacktestSummaryMetrics) GetEndingEquity() *Price {
|
||||
if x != nil {
|
||||
return x.EndingEquity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BacktestSummaryMetrics) GetTotalReturn() *Decimal {
|
||||
if x != nil {
|
||||
return x.TotalReturn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BacktestSummaryMetrics) GetTradeCount() int32 {
|
||||
if x != nil {
|
||||
return x.TradeCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type BacktestEquityPoint struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
TimestampUnixMs int64 `protobuf:"varint,1,opt,name=timestamp_unix_ms,json=timestampUnixMs,proto3" json:"timestamp_unix_ms,omitempty"`
|
||||
Equity *Price `protobuf:"bytes,2,opt,name=equity,proto3" json:"equity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BacktestEquityPoint) Reset() {
|
||||
*x = BacktestEquityPoint{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BacktestEquityPoint) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BacktestEquityPoint) ProtoMessage() {}
|
||||
|
||||
func (x *BacktestEquityPoint) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[9]
|
||||
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 BacktestEquityPoint.ProtoReflect.Descriptor instead.
|
||||
func (*BacktestEquityPoint) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *BacktestEquityPoint) GetTimestampUnixMs() int64 {
|
||||
if x != nil {
|
||||
return x.TimestampUnixMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *BacktestEquityPoint) GetEquity() *Price {
|
||||
if x != nil {
|
||||
return x.Equity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BacktestResult struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"`
|
||||
StartingCash *Price `protobuf:"bytes,2,opt,name=starting_cash,json=startingCash,proto3" json:"starting_cash,omitempty"`
|
||||
EndingEquity *Price `protobuf:"bytes,3,opt,name=ending_equity,json=endingEquity,proto3" json:"ending_equity,omitempty"`
|
||||
Trades []*BacktestTrade `protobuf:"bytes,4,rep,name=trades,proto3" json:"trades,omitempty"`
|
||||
Positions []*BacktestPosition `protobuf:"bytes,5,rep,name=positions,proto3" json:"positions,omitempty"`
|
||||
Summary *BacktestSummaryMetrics `protobuf:"bytes,6,opt,name=summary,proto3" json:"summary,omitempty"`
|
||||
EquityCurve []*BacktestEquityPoint `protobuf:"bytes,7,rep,name=equity_curve,json=equityCurve,proto3" json:"equity_curve,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BacktestResult) Reset() {
|
||||
*x = BacktestResult{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[8]
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -568,7 +690,7 @@ func (x *BacktestResult) String() string {
|
|||
func (*BacktestResult) ProtoMessage() {}
|
||||
|
||||
func (x *BacktestResult) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[8]
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -581,7 +703,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{8}
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *BacktestResult) GetRunId() string {
|
||||
|
|
@ -619,6 +741,20 @@ func (x *BacktestResult) GetPositions() []*BacktestPosition {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *BacktestResult) GetSummary() *BacktestSummaryMetrics {
|
||||
if x != nil {
|
||||
return x.Summary
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BacktestResult) GetEquityCurve() []*BacktestEquityPoint {
|
||||
if x != nil {
|
||||
return x.EquityCurve
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetBacktestResultRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"`
|
||||
|
|
@ -628,7 +764,7 @@ type GetBacktestResultRequest struct {
|
|||
|
||||
func (x *GetBacktestResultRequest) Reset() {
|
||||
*x = GetBacktestResultRequest{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[9]
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -640,7 +776,7 @@ func (x *GetBacktestResultRequest) String() string {
|
|||
func (*GetBacktestResultRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetBacktestResultRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[9]
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -653,7 +789,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{9}
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *GetBacktestResultRequest) GetRunId() string {
|
||||
|
|
@ -672,7 +808,7 @@ type GetBacktestResultResponse struct {
|
|||
|
||||
func (x *GetBacktestResultResponse) Reset() {
|
||||
*x = GetBacktestResultResponse{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[10]
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
|
@ -684,7 +820,7 @@ func (x *GetBacktestResultResponse) String() string {
|
|||
func (*GetBacktestResultResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetBacktestResultResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[10]
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
|
|
@ -697,7 +833,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{10}
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *GetBacktestResultResponse) GetResult() *BacktestResult {
|
||||
|
|
@ -707,6 +843,278 @@ func (x *GetBacktestResultResponse) GetResult() *BacktestResult {
|
|||
return nil
|
||||
}
|
||||
|
||||
type ListBacktestRunsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Status BacktestRunStatus `protobuf:"varint,1,opt,name=status,proto3,enum=alt.v1.BacktestRunStatus" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListBacktestRunsRequest) Reset() {
|
||||
*x = ListBacktestRunsRequest{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListBacktestRunsRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListBacktestRunsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListBacktestRunsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[13]
|
||||
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 ListBacktestRunsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListBacktestRunsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *ListBacktestRunsRequest) GetStatus() BacktestRunStatus {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return BacktestRunStatus_BACKTEST_RUN_STATUS_UNSPECIFIED
|
||||
}
|
||||
|
||||
type ListBacktestRunsResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Runs []*BacktestRun `protobuf:"bytes,1,rep,name=runs,proto3" json:"runs,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListBacktestRunsResponse) Reset() {
|
||||
*x = ListBacktestRunsResponse{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListBacktestRunsResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListBacktestRunsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListBacktestRunsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[14]
|
||||
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 ListBacktestRunsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListBacktestRunsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *ListBacktestRunsResponse) GetRuns() []*BacktestRun {
|
||||
if x != nil {
|
||||
return x.Runs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetBacktestRunDetailRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetBacktestRunDetailRequest) Reset() {
|
||||
*x = GetBacktestRunDetailRequest{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetBacktestRunDetailRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetBacktestRunDetailRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetBacktestRunDetailRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[15]
|
||||
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 GetBacktestRunDetailRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetBacktestRunDetailRequest) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{15}
|
||||
}
|
||||
|
||||
func (x *GetBacktestRunDetailRequest) GetRunId() string {
|
||||
if x != nil {
|
||||
return x.RunId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetBacktestRunDetailResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Run *BacktestRun `protobuf:"bytes,1,opt,name=run,proto3" json:"run,omitempty"`
|
||||
Result *BacktestResult `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetBacktestRunDetailResponse) Reset() {
|
||||
*x = GetBacktestRunDetailResponse{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetBacktestRunDetailResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetBacktestRunDetailResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetBacktestRunDetailResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[16]
|
||||
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 GetBacktestRunDetailResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetBacktestRunDetailResponse) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{16}
|
||||
}
|
||||
|
||||
func (x *GetBacktestRunDetailResponse) GetRun() *BacktestRun {
|
||||
if x != nil {
|
||||
return x.Run
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetBacktestRunDetailResponse) GetResult() *BacktestResult {
|
||||
if x != nil {
|
||||
return x.Result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CompareBacktestRunsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RunIds []string `protobuf:"bytes,1,rep,name=run_ids,json=runIds,proto3" json:"run_ids,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CompareBacktestRunsRequest) Reset() {
|
||||
*x = CompareBacktestRunsRequest{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CompareBacktestRunsRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CompareBacktestRunsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CompareBacktestRunsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[17]
|
||||
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 CompareBacktestRunsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CompareBacktestRunsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *CompareBacktestRunsRequest) GetRunIds() []string {
|
||||
if x != nil {
|
||||
return x.RunIds
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CompareBacktestRunsResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Results []*BacktestResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CompareBacktestRunsResponse) Reset() {
|
||||
*x = CompareBacktestRunsResponse{}
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CompareBacktestRunsResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CompareBacktestRunsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CompareBacktestRunsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_backtest_proto_msgTypes[18]
|
||||
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 CompareBacktestRunsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CompareBacktestRunsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_backtest_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *CompareBacktestRunsResponse) GetResults() []*BacktestResult {
|
||||
if x != nil {
|
||||
return x.Results
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_alt_v1_backtest_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_alt_v1_backtest_proto_rawDesc = "" +
|
||||
|
|
@ -745,17 +1153,41 @@ const file_alt_v1_backtest_proto_rawDesc = "" +
|
|||
"\rinstrument_id\x18\x01 \x01(\tR\finstrumentId\x12,\n" +
|
||||
"\bquantity\x18\x02 \x01(\v2\x10.alt.v1.QuantityR\bquantity\x12,\n" +
|
||||
"\n" +
|
||||
"last_price\x18\x03 \x01(\v2\r.alt.v1.PriceR\tlastPrice\"\xf6\x01\n" +
|
||||
"last_price\x18\x03 \x01(\v2\r.alt.v1.PriceR\tlastPrice\"\xd5\x01\n" +
|
||||
"\x16BacktestSummaryMetrics\x122\n" +
|
||||
"\rstarting_cash\x18\x01 \x01(\v2\r.alt.v1.PriceR\fstartingCash\x122\n" +
|
||||
"\rending_equity\x18\x02 \x01(\v2\r.alt.v1.PriceR\fendingEquity\x122\n" +
|
||||
"\ftotal_return\x18\x03 \x01(\v2\x0f.alt.v1.DecimalR\vtotalReturn\x12\x1f\n" +
|
||||
"\vtrade_count\x18\x04 \x01(\x05R\n" +
|
||||
"tradeCount\"h\n" +
|
||||
"\x13BacktestEquityPoint\x12*\n" +
|
||||
"\x11timestamp_unix_ms\x18\x01 \x01(\x03R\x0ftimestampUnixMs\x12%\n" +
|
||||
"\x06equity\x18\x02 \x01(\v2\r.alt.v1.PriceR\x06equity\"\xf0\x02\n" +
|
||||
"\x0eBacktestResult\x12\x15\n" +
|
||||
"\x06run_id\x18\x01 \x01(\tR\x05runId\x122\n" +
|
||||
"\rstarting_cash\x18\x02 \x01(\v2\r.alt.v1.PriceR\fstartingCash\x122\n" +
|
||||
"\rending_equity\x18\x03 \x01(\v2\r.alt.v1.PriceR\fendingEquity\x12-\n" +
|
||||
"\x06trades\x18\x04 \x03(\v2\x15.alt.v1.BacktestTradeR\x06trades\x126\n" +
|
||||
"\tpositions\x18\x05 \x03(\v2\x18.alt.v1.BacktestPositionR\tpositions\"1\n" +
|
||||
"\tpositions\x18\x05 \x03(\v2\x18.alt.v1.BacktestPositionR\tpositions\x128\n" +
|
||||
"\asummary\x18\x06 \x01(\v2\x1e.alt.v1.BacktestSummaryMetricsR\asummary\x12>\n" +
|
||||
"\fequity_curve\x18\a \x03(\v2\x1b.alt.v1.BacktestEquityPointR\vequityCurve\"1\n" +
|
||||
"\x18GetBacktestResultRequest\x12\x15\n" +
|
||||
"\x06run_id\x18\x01 \x01(\tR\x05runId\"K\n" +
|
||||
"\x19GetBacktestResultResponse\x12.\n" +
|
||||
"\x06result\x18\x01 \x01(\v2\x16.alt.v1.BacktestResultR\x06result*\xdf\x01\n" +
|
||||
"\x06result\x18\x01 \x01(\v2\x16.alt.v1.BacktestResultR\x06result\"L\n" +
|
||||
"\x17ListBacktestRunsRequest\x121\n" +
|
||||
"\x06status\x18\x01 \x01(\x0e2\x19.alt.v1.BacktestRunStatusR\x06status\"C\n" +
|
||||
"\x18ListBacktestRunsResponse\x12'\n" +
|
||||
"\x04runs\x18\x01 \x03(\v2\x13.alt.v1.BacktestRunR\x04runs\"4\n" +
|
||||
"\x1bGetBacktestRunDetailRequest\x12\x15\n" +
|
||||
"\x06run_id\x18\x01 \x01(\tR\x05runId\"u\n" +
|
||||
"\x1cGetBacktestRunDetailResponse\x12%\n" +
|
||||
"\x03run\x18\x01 \x01(\v2\x13.alt.v1.BacktestRunR\x03run\x12.\n" +
|
||||
"\x06result\x18\x02 \x01(\v2\x16.alt.v1.BacktestResultR\x06result\"5\n" +
|
||||
"\x1aCompareBacktestRunsRequest\x12\x17\n" +
|
||||
"\arun_ids\x18\x01 \x03(\tR\x06runIds\"O\n" +
|
||||
"\x1bCompareBacktestRunsResponse\x120\n" +
|
||||
"\aresults\x18\x01 \x03(\v2\x16.alt.v1.BacktestResultR\aresults*\xdf\x01\n" +
|
||||
"\x11BacktestRunStatus\x12#\n" +
|
||||
"\x1fBACKTEST_RUN_STATUS_UNSPECIFIED\x10\x00\x12\x1f\n" +
|
||||
"\x1bBACKTEST_RUN_STATUS_PENDING\x10\x01\x12\x1f\n" +
|
||||
|
|
@ -777,47 +1209,67 @@ 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, 11)
|
||||
var file_alt_v1_backtest_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
|
||||
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
|
||||
(*BacktestResult)(nil), // 9: alt.v1.BacktestResult
|
||||
(*GetBacktestResultRequest)(nil), // 10: alt.v1.GetBacktestResultRequest
|
||||
(*GetBacktestResultResponse)(nil), // 11: alt.v1.GetBacktestResultResponse
|
||||
(Market)(0), // 12: alt.v1.Market
|
||||
(Timeframe)(0), // 13: alt.v1.Timeframe
|
||||
(*Quantity)(nil), // 14: alt.v1.Quantity
|
||||
(*Price)(nil), // 15: alt.v1.Price
|
||||
(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
|
||||
(*Quantity)(nil), // 22: alt.v1.Quantity
|
||||
(*Price)(nil), // 23: alt.v1.Price
|
||||
(*Decimal)(nil), // 24: alt.v1.Decimal
|
||||
}
|
||||
var file_alt_v1_backtest_proto_depIdxs = []int32{
|
||||
12, // 0: alt.v1.BacktestRunSpec.market:type_name -> alt.v1.Market
|
||||
13, // 1: alt.v1.BacktestRunSpec.timeframe:type_name -> alt.v1.Timeframe
|
||||
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
|
||||
2, // 6: alt.v1.GetBacktestRunResponse.run:type_name -> alt.v1.BacktestRun
|
||||
14, // 7: alt.v1.BacktestTrade.quantity:type_name -> alt.v1.Quantity
|
||||
15, // 8: alt.v1.BacktestTrade.price:type_name -> alt.v1.Price
|
||||
14, // 9: alt.v1.BacktestPosition.quantity:type_name -> alt.v1.Quantity
|
||||
15, // 10: alt.v1.BacktestPosition.last_price:type_name -> alt.v1.Price
|
||||
15, // 11: alt.v1.BacktestResult.starting_cash:type_name -> alt.v1.Price
|
||||
15, // 12: alt.v1.BacktestResult.ending_equity:type_name -> alt.v1.Price
|
||||
7, // 13: alt.v1.BacktestResult.trades:type_name -> alt.v1.BacktestTrade
|
||||
8, // 14: alt.v1.BacktestResult.positions:type_name -> alt.v1.BacktestPosition
|
||||
9, // 15: alt.v1.GetBacktestResultResponse.result:type_name -> alt.v1.BacktestResult
|
||||
16, // [16:16] is the sub-list for method output_type
|
||||
16, // [16:16] is the sub-list for method input_type
|
||||
16, // [16:16] is the sub-list for extension type_name
|
||||
16, // [16:16] is the sub-list for extension extendee
|
||||
0, // [0:16] is the sub-list for field type_name
|
||||
22, // 7: alt.v1.BacktestTrade.quantity:type_name -> alt.v1.Quantity
|
||||
23, // 8: alt.v1.BacktestTrade.price:type_name -> alt.v1.Price
|
||||
22, // 9: alt.v1.BacktestPosition.quantity:type_name -> alt.v1.Quantity
|
||||
23, // 10: alt.v1.BacktestPosition.last_price:type_name -> alt.v1.Price
|
||||
23, // 11: alt.v1.BacktestSummaryMetrics.starting_cash:type_name -> alt.v1.Price
|
||||
23, // 12: alt.v1.BacktestSummaryMetrics.ending_equity:type_name -> alt.v1.Price
|
||||
24, // 13: alt.v1.BacktestSummaryMetrics.total_return:type_name -> alt.v1.Decimal
|
||||
23, // 14: alt.v1.BacktestEquityPoint.equity:type_name -> alt.v1.Price
|
||||
23, // 15: alt.v1.BacktestResult.starting_cash:type_name -> alt.v1.Price
|
||||
23, // 16: alt.v1.BacktestResult.ending_equity:type_name -> alt.v1.Price
|
||||
7, // 17: alt.v1.BacktestResult.trades:type_name -> alt.v1.BacktestTrade
|
||||
8, // 18: alt.v1.BacktestResult.positions:type_name -> alt.v1.BacktestPosition
|
||||
9, // 19: alt.v1.BacktestResult.summary:type_name -> alt.v1.BacktestSummaryMetrics
|
||||
10, // 20: alt.v1.BacktestResult.equity_curve:type_name -> alt.v1.BacktestEquityPoint
|
||||
11, // 21: alt.v1.GetBacktestResultResponse.result:type_name -> alt.v1.BacktestResult
|
||||
0, // 22: alt.v1.ListBacktestRunsRequest.status:type_name -> alt.v1.BacktestRunStatus
|
||||
2, // 23: alt.v1.ListBacktestRunsResponse.runs:type_name -> alt.v1.BacktestRun
|
||||
2, // 24: alt.v1.GetBacktestRunDetailResponse.run:type_name -> alt.v1.BacktestRun
|
||||
11, // 25: alt.v1.GetBacktestRunDetailResponse.result:type_name -> alt.v1.BacktestResult
|
||||
11, // 26: alt.v1.CompareBacktestRunsResponse.results:type_name -> alt.v1.BacktestResult
|
||||
27, // [27:27] is the sub-list for method output_type
|
||||
27, // [27:27] is the sub-list for method input_type
|
||||
27, // [27:27] is the sub-list for extension type_name
|
||||
27, // [27:27] is the sub-list for extension extendee
|
||||
0, // [0:27] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_alt_v1_backtest_proto_init() }
|
||||
|
|
@ -833,7 +1285,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: 11,
|
||||
NumMessages: 19,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,12 +62,26 @@ message BacktestPosition {
|
|||
Price last_price = 3;
|
||||
}
|
||||
|
||||
message BacktestSummaryMetrics {
|
||||
Price starting_cash = 1;
|
||||
Price ending_equity = 2;
|
||||
Decimal total_return = 3;
|
||||
int32 trade_count = 4;
|
||||
}
|
||||
|
||||
message BacktestEquityPoint {
|
||||
int64 timestamp_unix_ms = 1;
|
||||
Price equity = 2;
|
||||
}
|
||||
|
||||
message BacktestResult {
|
||||
string run_id = 1;
|
||||
Price starting_cash = 2;
|
||||
Price ending_equity = 3;
|
||||
repeated BacktestTrade trades = 4;
|
||||
repeated BacktestPosition positions = 5;
|
||||
BacktestSummaryMetrics summary = 6;
|
||||
repeated BacktestEquityPoint equity_curve = 7;
|
||||
}
|
||||
|
||||
message GetBacktestResultRequest {
|
||||
|
|
@ -78,3 +92,28 @@ message GetBacktestResultResponse {
|
|||
BacktestResult result = 1;
|
||||
}
|
||||
|
||||
message ListBacktestRunsRequest {
|
||||
BacktestRunStatus status = 1;
|
||||
}
|
||||
|
||||
message ListBacktestRunsResponse {
|
||||
repeated BacktestRun runs = 1;
|
||||
}
|
||||
|
||||
message GetBacktestRunDetailRequest {
|
||||
string run_id = 1;
|
||||
}
|
||||
|
||||
message GetBacktestRunDetailResponse {
|
||||
BacktestRun run = 1;
|
||||
BacktestResult result = 2;
|
||||
}
|
||||
|
||||
message CompareBacktestRunsRequest {
|
||||
repeated string run_ids = 1;
|
||||
}
|
||||
|
||||
message CompareBacktestRunsResponse {
|
||||
repeated BacktestResult results = 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,12 +52,42 @@ type PositionSummary struct {
|
|||
LastPrice market.Price
|
||||
}
|
||||
|
||||
type SummaryMetrics struct {
|
||||
StartingCash market.Price
|
||||
EndingEquity market.Price
|
||||
TotalReturn market.Decimal
|
||||
TradeCount int
|
||||
}
|
||||
|
||||
type EquityPoint struct {
|
||||
Timestamp time.Time
|
||||
Equity market.Price
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
RunID RunID
|
||||
StartingCash market.Price
|
||||
EndingEquity market.Price
|
||||
Trades []TradeSummary
|
||||
Positions []PositionSummary
|
||||
Summary SummaryMetrics
|
||||
EquityCurve []EquityPoint
|
||||
}
|
||||
|
||||
// TotalReturn computes (endingEquity - startingCash) / startingCash as a decimal
|
||||
// string. Starting cash that is zero or invalid yields "0" so callers never divide
|
||||
// by zero or surface float artifacts.
|
||||
func TotalReturn(startingCash, endingEquity market.Price) market.Decimal {
|
||||
start, err := decimalRat(startingCash.Amount)
|
||||
if err != nil || start.Sign() == 0 {
|
||||
return market.Decimal{Value: "0"}
|
||||
}
|
||||
end, err := decimalRat(endingEquity.Amount)
|
||||
if err != nil {
|
||||
return market.Decimal{Value: "0"}
|
||||
}
|
||||
ratio := new(big.Rat).Quo(new(big.Rat).Sub(end, start), start)
|
||||
return decimalValue(ratio)
|
||||
}
|
||||
|
||||
type Strategy interface {
|
||||
|
|
|
|||
|
|
@ -221,3 +221,55 @@ func TestResultCarriesTradeAndPositionSummary(t *testing.T) {
|
|||
t.Errorf("Position LastPrice mismatch: got %v", pos.LastPrice.Amount.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultCarriesAnalysisSummaryAndEquityCurve(t *testing.T) {
|
||||
t1 := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
t2 := time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC)
|
||||
res := Result{
|
||||
RunID: RunID("run-summary"),
|
||||
StartingCash: price("1000"),
|
||||
EndingEquity: price("1500"),
|
||||
Summary: SummaryMetrics{
|
||||
StartingCash: price("1000"),
|
||||
EndingEquity: price("1500"),
|
||||
TotalReturn: TotalReturn(price("1000"), price("1500")),
|
||||
TradeCount: 3,
|
||||
},
|
||||
EquityCurve: []EquityPoint{
|
||||
{Timestamp: t1, Equity: price("1200")},
|
||||
{Timestamp: t2, Equity: price("1500")},
|
||||
},
|
||||
}
|
||||
|
||||
if res.Summary.TotalReturn.Value != "0.5" {
|
||||
t.Errorf("Summary TotalReturn mismatch: got %q, want 0.5", res.Summary.TotalReturn.Value)
|
||||
}
|
||||
if res.Summary.TradeCount != 3 {
|
||||
t.Errorf("Summary TradeCount mismatch: got %d, want 3", res.Summary.TradeCount)
|
||||
}
|
||||
if res.Summary.StartingCash.Amount.Value != "1000" || res.Summary.EndingEquity.Amount.Value != "1500" {
|
||||
t.Errorf("Summary cash/equity mismatch: %+v", res.Summary)
|
||||
}
|
||||
|
||||
if len(res.EquityCurve) != 2 {
|
||||
t.Fatalf("EquityCurve length mismatch: got %d, want 2", len(res.EquityCurve))
|
||||
}
|
||||
if !res.EquityCurve[0].Timestamp.Equal(t1) || res.EquityCurve[0].Equity.Amount.Value != "1200" {
|
||||
t.Errorf("EquityCurve point 0 mismatch: %+v", res.EquityCurve[0])
|
||||
}
|
||||
if !res.EquityCurve[1].Timestamp.Equal(t2) || res.EquityCurve[1].Equity.Amount.Value != "1500" {
|
||||
t.Errorf("EquityCurve point 1 mismatch: %+v", res.EquityCurve[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalReturnHandlesZeroAndInvalidStartingCash(t *testing.T) {
|
||||
if got := TotalReturn(price("0"), price("1500")); got.Value != "0" {
|
||||
t.Errorf("zero starting cash: got %q, want 0", got.Value)
|
||||
}
|
||||
if got := TotalReturn(market.Price{Amount: market.Decimal{Value: "bad"}}, price("1500")); got.Value != "0" {
|
||||
t.Errorf("invalid starting cash: got %q, want 0", got.Value)
|
||||
}
|
||||
if got := TotalReturn(price("10000000"), price("10000300")); got.Value != "0.00003" {
|
||||
t.Errorf("fixture total return: got %q, want 0.00003", got.Value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,26 +3,32 @@ package contracts
|
|||
import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
||||
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
||||
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
||||
)
|
||||
|
||||
// ParserMap returns a fresh ParserMap populated with all ALT API messages.
|
||||
func ParserMap() protoSocket.ParserMap {
|
||||
return protoSocket.ParserMap{
|
||||
protoSocket.TypeNameOf(&altv1.HelloRequest{}): parserFor(func() proto.Message { return &altv1.HelloRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.HelloResponse{}): parserFor(func() proto.Message { return &altv1.HelloResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListInstrumentsRequest{}): parserFor(func() proto.Message { return &altv1.ListInstrumentsRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListInstrumentsResponse{}): parserFor(func() proto.Message { return &altv1.ListInstrumentsResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListBarsRequest{}): parserFor(func() proto.Message { return &altv1.ListBarsRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListBarsResponse{}): parserFor(func() proto.Message { return &altv1.ListBarsResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.StartBacktestRequest{}): parserFor(func() proto.Message { return &altv1.StartBacktestRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.StartBacktestResponse{}): parserFor(func() proto.Message { return &altv1.StartBacktestResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestRunRequest{}): parserFor(func() proto.Message { return &altv1.GetBacktestRunRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestRunResponse{}): parserFor(func() proto.Message { return &altv1.GetBacktestRunResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestResultRequest{}): parserFor(func() proto.Message { return &altv1.GetBacktestResultRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestResultResponse{}): parserFor(func() proto.Message { return &altv1.GetBacktestResultResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.BacktestResult{}): parserFor(func() proto.Message { return &altv1.BacktestResult{} }),
|
||||
protoSocket.TypeNameOf(&altv1.HelloRequest{}): parserFor(func() proto.Message { return &altv1.HelloRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.HelloResponse{}): parserFor(func() proto.Message { return &altv1.HelloResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListInstrumentsRequest{}): parserFor(func() proto.Message { return &altv1.ListInstrumentsRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListInstrumentsResponse{}): parserFor(func() proto.Message { return &altv1.ListInstrumentsResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListBarsRequest{}): parserFor(func() proto.Message { return &altv1.ListBarsRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListBarsResponse{}): parserFor(func() proto.Message { return &altv1.ListBarsResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.StartBacktestRequest{}): parserFor(func() proto.Message { return &altv1.StartBacktestRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.StartBacktestResponse{}): parserFor(func() proto.Message { return &altv1.StartBacktestResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestRunRequest{}): parserFor(func() proto.Message { return &altv1.GetBacktestRunRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestRunResponse{}): parserFor(func() proto.Message { return &altv1.GetBacktestRunResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestResultRequest{}): parserFor(func() proto.Message { return &altv1.GetBacktestResultRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestResultResponse{}): parserFor(func() proto.Message { return &altv1.GetBacktestResultResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.BacktestResult{}): parserFor(func() proto.Message { return &altv1.BacktestResult{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListBacktestRunsRequest{}): parserFor(func() proto.Message { return &altv1.ListBacktestRunsRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.ListBacktestRunsResponse{}): parserFor(func() proto.Message { return &altv1.ListBacktestRunsResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestRunDetailRequest{}): parserFor(func() proto.Message { return &altv1.GetBacktestRunDetailRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.GetBacktestRunDetailResponse{}): parserFor(func() proto.Message { return &altv1.GetBacktestRunDetailResponse{} }),
|
||||
protoSocket.TypeNameOf(&altv1.CompareBacktestRunsRequest{}): parserFor(func() proto.Message { return &altv1.CompareBacktestRunsRequest{} }),
|
||||
protoSocket.TypeNameOf(&altv1.CompareBacktestRunsResponse{}): parserFor(func() proto.Message { return &altv1.CompareBacktestRunsResponse{} }),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ func TestParserMapIncludesAltMessages(t *testing.T) {
|
|||
&altv1.GetBacktestResultRequest{},
|
||||
&altv1.GetBacktestResultResponse{},
|
||||
&altv1.BacktestResult{},
|
||||
&altv1.ListBacktestRunsRequest{},
|
||||
&altv1.ListBacktestRunsResponse{},
|
||||
&altv1.GetBacktestRunDetailRequest{},
|
||||
&altv1.GetBacktestRunDetailResponse{},
|
||||
&altv1.CompareBacktestRunsRequest{},
|
||||
&altv1.CompareBacktestRunsResponse{},
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ func (e *Engine) Execute(ctx context.Context, run backtest.Run) error {
|
|||
portfolio := backtest.NewPortfolioState(startingCash)
|
||||
var history []market.Bar
|
||||
var trades []backtest.TradeSummary
|
||||
var equityCurve []backtest.EquityPoint
|
||||
|
||||
for _, bar := range bars {
|
||||
input := backtest.StrategyInput{
|
||||
|
|
@ -115,6 +116,14 @@ func (e *Engine) Execute(ctx context.Context, run backtest.Run) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Record one equity point per processed bar after marking the current close
|
||||
// so the curve is deterministic and ordered by bar timestamp.
|
||||
equity, err := portfolio.Equity()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate equity at %s: %w", bar.Timestamp, err)
|
||||
}
|
||||
equityCurve = append(equityCurve, backtest.EquityPoint{Timestamp: bar.Timestamp, Equity: equity})
|
||||
|
||||
history = append(history, bar)
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +153,13 @@ func (e *Engine) Execute(ctx context.Context, run backtest.Run) error {
|
|||
EndingEquity: endingEquity,
|
||||
Trades: trades,
|
||||
Positions: positions,
|
||||
Summary: backtest.SummaryMetrics{
|
||||
StartingCash: startingCash,
|
||||
EndingEquity: endingEquity,
|
||||
TotalReturn: backtest.TotalReturn(startingCash, endingEquity),
|
||||
TradeCount: len(trades),
|
||||
},
|
||||
EquityCurve: equityCurve,
|
||||
}
|
||||
|
||||
if e.resultStore != nil {
|
||||
|
|
|
|||
|
|
@ -257,4 +257,49 @@ func TestEngineStoresAndQueriesFixtureResult(t *testing.T) {
|
|||
if pos.InstrumentID != instID || pos.Quantity.Amount.Value != "1" || pos.LastPrice.Amount.Value != "1200" {
|
||||
t.Errorf("unexpected position: %+v", pos)
|
||||
}
|
||||
|
||||
// Verify derived summary metrics.
|
||||
if res.Summary.StartingCash.Amount.Value != "10000000" {
|
||||
t.Errorf("summary starting cash: got %s, want 10000000", res.Summary.StartingCash.Amount.Value)
|
||||
}
|
||||
if res.Summary.EndingEquity.Amount.Value != "10000300" {
|
||||
t.Errorf("summary ending equity: got %s, want 10000300", res.Summary.EndingEquity.Amount.Value)
|
||||
}
|
||||
if res.Summary.TradeCount != 2 {
|
||||
t.Errorf("summary trade count: got %d, want 2", res.Summary.TradeCount)
|
||||
}
|
||||
// (10000300 - 10000000) / 10000000 = 0.00003
|
||||
if res.Summary.TotalReturn.Value != "0.00003" {
|
||||
t.Errorf("summary total return: got %s, want 0.00003", res.Summary.TotalReturn.Value)
|
||||
}
|
||||
|
||||
// Verify deterministic equity curve: one point per bar, ordered by timestamp.
|
||||
// Day 1: cash 9,998,000 + 2*1000 = 10,000,000
|
||||
// Day 2: cash 9,999,100 + 1*1100 = 10,000,200
|
||||
// Day 3: cash 9,999,100 + 1*1200 = 10,000,300
|
||||
if len(res.EquityCurve) != 3 {
|
||||
t.Fatalf("equity curve length: got %d, want 3", len(res.EquityCurve))
|
||||
}
|
||||
wantCurve := []struct {
|
||||
ts time.Time
|
||||
equity string
|
||||
}{
|
||||
{bar1.Timestamp, "10000000"},
|
||||
{bar2.Timestamp, "10000200"},
|
||||
{bar3.Timestamp, "10000300"},
|
||||
}
|
||||
for i, want := range wantCurve {
|
||||
point := res.EquityCurve[i]
|
||||
if !point.Timestamp.Equal(want.ts) {
|
||||
t.Errorf("equity curve point %d timestamp: got %v, want %v", i, point.Timestamp, want.ts)
|
||||
}
|
||||
if point.Equity.Amount.Value != want.equity {
|
||||
t.Errorf("equity curve point %d equity: got %s, want %s", i, point.Equity.Amount.Value, want.equity)
|
||||
}
|
||||
}
|
||||
// Final equity curve point must match the reported ending equity.
|
||||
finalPoint := res.EquityCurve[len(res.EquityCurve)-1]
|
||||
if finalPoint.Equity.Amount.Value != res.EndingEquity.Amount.Value {
|
||||
t.Errorf("final equity point %s does not match ending equity %s", finalPoint.Equity.Amount.Value, res.EndingEquity.Amount.Value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,3 +32,20 @@ type BacktestResultStore interface {
|
|||
UpsertResult(ctx context.Context, result backtest.Result) error
|
||||
GetResult(ctx context.Context, id backtest.RunID) (backtest.Result, error)
|
||||
}
|
||||
|
||||
// RunDetail bundles a run with its analysis result for detail views. HasResult is
|
||||
// false when the run exists but has not produced a result yet.
|
||||
type RunDetail struct {
|
||||
Run backtest.Run
|
||||
Result backtest.Result
|
||||
HasResult bool
|
||||
}
|
||||
|
||||
// BacktestAnalysisStore exposes list, detail, and compare read access for the
|
||||
// analysis surface. It is separate from the execution-side run/result stores so the
|
||||
// engine and job handlers keep a minimal write dependency.
|
||||
type BacktestAnalysisStore interface {
|
||||
ListRuns(ctx context.Context, status backtest.RunStatus) ([]backtest.Run, error)
|
||||
GetRunDetail(ctx context.Context, id backtest.RunID) (RunDetail, error)
|
||||
CompareResults(ctx context.Context, ids []backtest.RunID) ([]backtest.Result, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,14 +233,38 @@ func mapResultToParams(res backtest.Result) (sqlc.UpsertResultParams, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Summary defaults to "0" total return when callers persist a result without a
|
||||
// computed summary so the NOT NULL numeric column never receives an empty value.
|
||||
totalReturnValue := res.Summary.TotalReturn.Value
|
||||
if totalReturnValue == "" {
|
||||
totalReturnValue = "0"
|
||||
}
|
||||
totalReturn, err := stringToNumeric(totalReturnValue)
|
||||
if err != nil {
|
||||
return sqlc.UpsertResultParams{}, fmt.Errorf("invalid total return: %w", err)
|
||||
}
|
||||
|
||||
var equityCurveBytes []byte
|
||||
if res.EquityCurve == nil {
|
||||
equityCurveBytes = []byte("[]")
|
||||
} else {
|
||||
equityCurveBytes, err = json.Marshal(res.EquityCurve)
|
||||
if err != nil {
|
||||
return sqlc.UpsertResultParams{}, fmt.Errorf("failed to marshal equity curve: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return sqlc.UpsertResultParams{
|
||||
RunID: string(res.RunID),
|
||||
StartingCashCurrency: string(res.StartingCash.Currency),
|
||||
StartingCashAmount: startingCashAmount,
|
||||
EndingEquityCurrency: string(res.EndingEquity.Currency),
|
||||
EndingEquityAmount: endingEquityAmount,
|
||||
TotalReturn: totalReturn,
|
||||
TradeCount: int32(res.Summary.TradeCount),
|
||||
Trades: tradesBytes,
|
||||
Positions: positionsBytes,
|
||||
EquityCurve: equityCurveBytes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -268,17 +292,39 @@ func mapRowToResult(row sqlc.BacktestResult) (backtest.Result, error) {
|
|||
}
|
||||
}
|
||||
|
||||
totalReturnStr, err := numericToString(row.TotalReturn)
|
||||
if err != nil {
|
||||
return backtest.Result{}, fmt.Errorf("failed to parse total return: %w", err)
|
||||
}
|
||||
|
||||
var equityCurve []backtest.EquityPoint
|
||||
if len(row.EquityCurve) > 0 {
|
||||
if err := json.Unmarshal(row.EquityCurve, &equityCurve); err != nil {
|
||||
return backtest.Result{}, fmt.Errorf("failed to unmarshal equity curve: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
startingCash := market.Price{
|
||||
Currency: market.Currency(row.StartingCashCurrency),
|
||||
Amount: market.Decimal{Value: startingCashAmountStr},
|
||||
}
|
||||
endingEquity := market.Price{
|
||||
Currency: market.Currency(row.EndingEquityCurrency),
|
||||
Amount: market.Decimal{Value: endingEquityAmountStr},
|
||||
}
|
||||
|
||||
return backtest.Result{
|
||||
RunID: backtest.RunID(row.RunID),
|
||||
StartingCash: market.Price{
|
||||
Currency: market.Currency(row.StartingCashCurrency),
|
||||
Amount: market.Decimal{Value: startingCashAmountStr},
|
||||
RunID: backtest.RunID(row.RunID),
|
||||
StartingCash: startingCash,
|
||||
EndingEquity: endingEquity,
|
||||
Trades: trades,
|
||||
Positions: positions,
|
||||
Summary: backtest.SummaryMetrics{
|
||||
StartingCash: startingCash,
|
||||
EndingEquity: endingEquity,
|
||||
TotalReturn: market.Decimal{Value: totalReturnStr},
|
||||
TradeCount: int(row.TradeCount),
|
||||
},
|
||||
EndingEquity: market.Price{
|
||||
Currency: market.Currency(row.EndingEquityCurrency),
|
||||
Amount: market.Decimal{Value: endingEquityAmountStr},
|
||||
},
|
||||
Trades: trades,
|
||||
Positions: positions,
|
||||
EquityCurve: equityCurve,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,16 @@ func TestBacktestResultMappingRoundTrip(t *testing.T) {
|
|||
LastPrice: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "52000"}},
|
||||
},
|
||||
},
|
||||
Summary: backtest.SummaryMetrics{
|
||||
StartingCash: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100000000"}},
|
||||
EndingEquity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "123456789.12"}},
|
||||
TotalReturn: market.Decimal{Value: "0.2345678912"},
|
||||
TradeCount: 1,
|
||||
},
|
||||
EquityCurve: []backtest.EquityPoint{
|
||||
{Timestamp: now.Add(-2 * time.Hour), Equity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100000000"}}},
|
||||
{Timestamp: now.Add(-1 * time.Hour), Equity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "123456789.12"}}},
|
||||
},
|
||||
}
|
||||
|
||||
params, err := mapResultToParams(res)
|
||||
|
|
@ -158,8 +168,11 @@ func TestBacktestResultMappingRoundTrip(t *testing.T) {
|
|||
StartingCashAmount: params.StartingCashAmount,
|
||||
EndingEquityCurrency: params.EndingEquityCurrency,
|
||||
EndingEquityAmount: params.EndingEquityAmount,
|
||||
TotalReturn: params.TotalReturn,
|
||||
TradeCount: params.TradeCount,
|
||||
Trades: params.Trades,
|
||||
Positions: params.Positions,
|
||||
EquityCurve: params.EquityCurve,
|
||||
}
|
||||
|
||||
mappedBack, err := mapRowToResult(row)
|
||||
|
|
@ -192,6 +205,112 @@ func TestBacktestResultMappingRoundTrip(t *testing.T) {
|
|||
if mappedBack.Positions[0].InstrumentID != res.Positions[0].InstrumentID || mappedBack.Positions[0].Quantity.Amount.Value != res.Positions[0].Quantity.Amount.Value {
|
||||
t.Errorf("Position mismatch: got %v, want %v", mappedBack.Positions[0], res.Positions[0])
|
||||
}
|
||||
|
||||
// Summary round-trips, reconstructed from persisted columns.
|
||||
if mappedBack.Summary.TotalReturn.Value != res.Summary.TotalReturn.Value {
|
||||
t.Errorf("Summary TotalReturn mismatch: got %q, want %q", mappedBack.Summary.TotalReturn.Value, res.Summary.TotalReturn.Value)
|
||||
}
|
||||
if mappedBack.Summary.TradeCount != res.Summary.TradeCount {
|
||||
t.Errorf("Summary TradeCount mismatch: got %d, want %d", mappedBack.Summary.TradeCount, res.Summary.TradeCount)
|
||||
}
|
||||
if mappedBack.Summary.StartingCash.Amount.Value != res.Summary.StartingCash.Amount.Value || mappedBack.Summary.EndingEquity.Amount.Value != res.Summary.EndingEquity.Amount.Value {
|
||||
t.Errorf("Summary cash/equity mismatch: got %+v, want %+v", mappedBack.Summary, res.Summary)
|
||||
}
|
||||
|
||||
// Equity curve round-trips with order and timestamps preserved.
|
||||
if len(mappedBack.EquityCurve) != len(res.EquityCurve) {
|
||||
t.Fatalf("EquityCurve length mismatch: got %d, want %d", len(mappedBack.EquityCurve), len(res.EquityCurve))
|
||||
}
|
||||
for i := range res.EquityCurve {
|
||||
if !mappedBack.EquityCurve[i].Timestamp.Equal(res.EquityCurve[i].Timestamp) {
|
||||
t.Errorf("EquityCurve[%d] timestamp mismatch: got %v, want %v", i, mappedBack.EquityCurve[i].Timestamp, res.EquityCurve[i].Timestamp)
|
||||
}
|
||||
if mappedBack.EquityCurve[i].Equity.Amount.Value != res.EquityCurve[i].Equity.Amount.Value {
|
||||
t.Errorf("EquityCurve[%d] equity mismatch: got %q, want %q", i, mappedBack.EquityCurve[i].Equity.Amount.Value, res.EquityCurve[i].Equity.Amount.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBacktestResultMappingDefaultsEmptySummary(t *testing.T) {
|
||||
// A result persisted without a computed summary or equity curve must still map
|
||||
// to valid NOT NULL columns ("0" total return, empty JSON arrays).
|
||||
res := backtest.Result{
|
||||
RunID: backtest.RunID("run-empty"),
|
||||
StartingCash: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100"}},
|
||||
EndingEquity: market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: "100"}},
|
||||
}
|
||||
|
||||
params, err := mapResultToParams(res)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to map result to params: %v", err)
|
||||
}
|
||||
if string(params.EquityCurve) != "[]" {
|
||||
t.Errorf("expected empty equity curve JSON array, got %q", string(params.EquityCurve))
|
||||
}
|
||||
|
||||
mappedBack, err := mapRowToResult(sqlc.BacktestResult{
|
||||
RunID: params.RunID,
|
||||
StartingCashCurrency: params.StartingCashCurrency,
|
||||
StartingCashAmount: params.StartingCashAmount,
|
||||
EndingEquityCurrency: params.EndingEquityCurrency,
|
||||
EndingEquityAmount: params.EndingEquityAmount,
|
||||
TotalReturn: params.TotalReturn,
|
||||
TradeCount: params.TradeCount,
|
||||
Trades: params.Trades,
|
||||
Positions: params.Positions,
|
||||
EquityCurve: params.EquityCurve,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to map row back to result: %v", err)
|
||||
}
|
||||
if mappedBack.Summary.TotalReturn.Value != "0" {
|
||||
t.Errorf("expected default total return 0, got %q", mappedBack.Summary.TotalReturn.Value)
|
||||
}
|
||||
if len(mappedBack.EquityCurve) != 0 {
|
||||
t.Errorf("expected empty equity curve, got %d points", len(mappedBack.EquityCurve))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBacktestRunListRowMapping(t *testing.T) {
|
||||
// ListRuns returns sqlc.BacktestRun rows; verify they map to domain runs the same
|
||||
// way single-run detail rows do, so list and detail surfaces stay consistent.
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
run := backtest.Run{
|
||||
ID: backtest.RunID("run-list"),
|
||||
Spec: backtest.RunSpec{
|
||||
StrategyID: backtest.StrategyID("strat-list"),
|
||||
Market: market.MarketUS,
|
||||
Timeframe: market.TimeframeDaily,
|
||||
From: now.Add(-48 * time.Hour),
|
||||
To: now,
|
||||
},
|
||||
Status: backtest.RunStatusRunning,
|
||||
CreatedAt: now.Add(-49 * time.Hour),
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
params, err := mapRunToParams(run)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to map run to params: %v", err)
|
||||
}
|
||||
|
||||
mappedBack, err := mapRowToRun(sqlc.BacktestRun{
|
||||
ID: params.ID,
|
||||
StrategyID: params.StrategyID,
|
||||
Market: params.Market,
|
||||
Timeframe: params.Timeframe,
|
||||
FromTime: params.FromTime,
|
||||
ToTime: params.ToTime,
|
||||
Status: params.Status,
|
||||
CreatedAt: params.CreatedAt,
|
||||
UpdatedAt: params.UpdatedAt,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to map list row to run: %v", err)
|
||||
}
|
||||
if mappedBack.ID != run.ID || mappedBack.Status != run.Status || mappedBack.Spec.Market != run.Spec.Market {
|
||||
t.Errorf("list run mapping mismatch: got %+v, want %+v", mappedBack, run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBacktestResultMappingRejectsInvalidDecimal(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -79,3 +79,29 @@ func TestMigrationStatementsAreNamed(t *testing.T) {
|
|||
t.Error("migrations FS has no entries")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalysisResultColumnsAddedInVersionedMigration(t *testing.T) {
|
||||
// The analysis result columns must arrive through the v2 migration so databases
|
||||
// already at version 1 receive them, rather than being edited into 000001.
|
||||
content, err := migrationsFS.ReadFile("migrations/000002_backtest_analysis_results.up.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read 000002 up migration: %v", err)
|
||||
}
|
||||
up := string(content)
|
||||
for _, column := range []string{"total_return", "trade_count", "equity_curve"} {
|
||||
if !strings.Contains(up, column) {
|
||||
t.Errorf("000002 up migration missing column %q", column)
|
||||
}
|
||||
}
|
||||
|
||||
// The originally applied 000001 migration must not carry the analysis columns.
|
||||
backbone, err := migrationsFS.ReadFile("migrations/000001_worker_backbone.up.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read 000001 up migration: %v", err)
|
||||
}
|
||||
for _, column := range []string{"total_return", "trade_count", "equity_curve"} {
|
||||
if strings.Contains(string(backbone), column) {
|
||||
t.Errorf("000001 up migration should not contain analysis column %q", column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE backtest_results
|
||||
DROP COLUMN IF EXISTS equity_curve,
|
||||
DROP COLUMN IF EXISTS trade_count,
|
||||
DROP COLUMN IF EXISTS total_return;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
-- Add analysis result columns to backtest_results for summary metrics and equity
|
||||
-- curve. Existing databases already at version 1 receive these columns here.
|
||||
-- Backfill defaults keep the NOT NULL constraint valid for existing rows, then the
|
||||
-- defaults are dropped so application code stays the single source of values.
|
||||
ALTER TABLE backtest_results
|
||||
ADD COLUMN IF NOT EXISTS total_return NUMERIC NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS trade_count INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS equity_curve JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
|
||||
ALTER TABLE backtest_results
|
||||
ALTER COLUMN total_return DROP DEFAULT,
|
||||
ALTER COLUMN trade_count DROP DEFAULT,
|
||||
ALTER COLUMN equity_curve DROP DEFAULT;
|
||||
|
|
@ -52,18 +52,27 @@ SELECT id, strategy_id, market, timeframe, from_time, to_time, status, created_a
|
|||
FROM backtest_runs
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListRuns :many
|
||||
SELECT id, strategy_id, market, timeframe, from_time, to_time, status, created_at, updated_at
|
||||
FROM backtest_runs
|
||||
WHERE (@status::text = '' OR status = @status::text)
|
||||
ORDER BY created_at DESC, id ASC;
|
||||
|
||||
-- name: UpsertResult :exec
|
||||
INSERT INTO backtest_results (run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO backtest_results (run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions, total_return, trade_count, equity_curve)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (run_id) DO UPDATE SET
|
||||
starting_cash_currency = EXCLUDED.starting_cash_currency,
|
||||
starting_cash_amount = EXCLUDED.starting_cash_amount,
|
||||
ending_equity_currency = EXCLUDED.ending_equity_currency,
|
||||
ending_equity_amount = EXCLUDED.ending_equity_amount,
|
||||
trades = EXCLUDED.trades,
|
||||
positions = EXCLUDED.positions;
|
||||
positions = EXCLUDED.positions,
|
||||
total_return = EXCLUDED.total_return,
|
||||
trade_count = EXCLUDED.trade_count,
|
||||
equity_curve = EXCLUDED.equity_curve;
|
||||
|
||||
-- name: GetResult :one
|
||||
SELECT run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions
|
||||
SELECT run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions, total_return, trade_count, equity_curve
|
||||
FROM backtest_results
|
||||
WHERE run_id = $1;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ type BacktestResult struct {
|
|||
EndingEquityAmount pgtype.Numeric
|
||||
Trades []byte
|
||||
Positions []byte
|
||||
TotalReturn pgtype.Numeric
|
||||
TradeCount int32
|
||||
EquityCurve []byte
|
||||
}
|
||||
|
||||
type BacktestRun struct {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (q *Queries) GetInstrument(ctx context.Context, id string) (Instrument, err
|
|||
}
|
||||
|
||||
const getResult = `-- name: GetResult :one
|
||||
SELECT run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions
|
||||
SELECT run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions, total_return, trade_count, equity_curve
|
||||
FROM backtest_results
|
||||
WHERE run_id = $1
|
||||
`
|
||||
|
|
@ -97,6 +97,9 @@ func (q *Queries) GetResult(ctx context.Context, runID string) (BacktestResult,
|
|||
&i.EndingEquityAmount,
|
||||
&i.Trades,
|
||||
&i.Positions,
|
||||
&i.TotalReturn,
|
||||
&i.TradeCount,
|
||||
&i.EquityCurve,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -157,6 +160,43 @@ func (q *Queries) ListInstruments(ctx context.Context) ([]Instrument, error) {
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const listRuns = `-- name: ListRuns :many
|
||||
SELECT id, strategy_id, market, timeframe, from_time, to_time, status, created_at, updated_at
|
||||
FROM backtest_runs
|
||||
WHERE ($1::text = '' OR status = $1::text)
|
||||
ORDER BY created_at DESC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListRuns(ctx context.Context, status string) ([]BacktestRun, error) {
|
||||
rows, err := q.db.Query(ctx, listRuns, status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []BacktestRun
|
||||
for rows.Next() {
|
||||
var i BacktestRun
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.StrategyID,
|
||||
&i.Market,
|
||||
&i.Timeframe,
|
||||
&i.FromTime,
|
||||
&i.ToTime,
|
||||
&i.Status,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertBar = `-- name: UpsertBar :exec
|
||||
INSERT INTO bars (instrument_id, timeframe, timestamp, open, high, low, close, volume)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
|
|
@ -229,15 +269,18 @@ func (q *Queries) UpsertInstrument(ctx context.Context, arg UpsertInstrumentPara
|
|||
}
|
||||
|
||||
const upsertResult = `-- name: UpsertResult :exec
|
||||
INSERT INTO backtest_results (run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO backtest_results (run_id, starting_cash_currency, starting_cash_amount, ending_equity_currency, ending_equity_amount, trades, positions, total_return, trade_count, equity_curve)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (run_id) DO UPDATE SET
|
||||
starting_cash_currency = EXCLUDED.starting_cash_currency,
|
||||
starting_cash_amount = EXCLUDED.starting_cash_amount,
|
||||
ending_equity_currency = EXCLUDED.ending_equity_currency,
|
||||
ending_equity_amount = EXCLUDED.ending_equity_amount,
|
||||
trades = EXCLUDED.trades,
|
||||
positions = EXCLUDED.positions
|
||||
positions = EXCLUDED.positions,
|
||||
total_return = EXCLUDED.total_return,
|
||||
trade_count = EXCLUDED.trade_count,
|
||||
equity_curve = EXCLUDED.equity_curve
|
||||
`
|
||||
|
||||
type UpsertResultParams struct {
|
||||
|
|
@ -248,6 +291,9 @@ type UpsertResultParams struct {
|
|||
EndingEquityAmount pgtype.Numeric
|
||||
Trades []byte
|
||||
Positions []byte
|
||||
TotalReturn pgtype.Numeric
|
||||
TradeCount int32
|
||||
EquityCurve []byte
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertResult(ctx context.Context, arg UpsertResultParams) error {
|
||||
|
|
@ -259,6 +305,9 @@ func (q *Queries) UpsertResult(ctx context.Context, arg UpsertResultParams) erro
|
|||
arg.EndingEquityAmount,
|
||||
arg.Trades,
|
||||
arg.Positions,
|
||||
arg.TotalReturn,
|
||||
arg.TradeCount,
|
||||
arg.EquityCurve,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ var _ storage.InstrumentStore = (*Store)(nil)
|
|||
var _ storage.BarStore = (*Store)(nil)
|
||||
var _ storage.BacktestRunStore = (*Store)(nil)
|
||||
var _ storage.BacktestResultStore = (*Store)(nil)
|
||||
var _ storage.BacktestAnalysisStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
|
|
@ -138,6 +139,50 @@ func (s *Store) GetRun(ctx context.Context, id backtest.RunID) (backtest.Run, er
|
|||
return run, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListRuns(ctx context.Context, status backtest.RunStatus) ([]backtest.Run, error) {
|
||||
rows, err := s.queries.ListRuns(ctx, string(status))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list runs: %w", err)
|
||||
}
|
||||
runs := make([]backtest.Run, len(rows))
|
||||
for i, row := range rows {
|
||||
run, err := mapRowToRun(row)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to map run row at index %d: %w", i, err)
|
||||
}
|
||||
runs[i] = run
|
||||
}
|
||||
return runs, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetRunDetail(ctx context.Context, id backtest.RunID) (storage.RunDetail, error) {
|
||||
run, err := s.GetRun(ctx, id)
|
||||
if err != nil {
|
||||
return storage.RunDetail{}, err
|
||||
}
|
||||
|
||||
result, err := s.GetResult(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrResultNotFound) {
|
||||
return storage.RunDetail{Run: run, HasResult: false}, nil
|
||||
}
|
||||
return storage.RunDetail{}, err
|
||||
}
|
||||
return storage.RunDetail{Run: run, Result: result, HasResult: true}, nil
|
||||
}
|
||||
|
||||
func (s *Store) CompareResults(ctx context.Context, ids []backtest.RunID) ([]backtest.Result, error) {
|
||||
results := make([]backtest.Result, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
result, err := s.GetResult(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get result for run %s: %w", id, err)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpsertResult(ctx context.Context, result backtest.Result) error {
|
||||
params, err := mapResultToParams(result)
|
||||
if err != nil {
|
||||
|
|
|
|||
200
services/worker/internal/storage/postgres/store_test.go
Normal file
200
services/worker/internal/storage/postgres/store_test.go
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.toki-labs.com/toki/alt/packages/domain/backtest"
|
||||
"git.toki-labs.com/toki/alt/packages/domain/market"
|
||||
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
|
||||
)
|
||||
|
||||
// newTestStore connects to a local PostgreSQL instance using DATABASE_URL and runs
|
||||
// the migrations inside a throwaway schema so the test is isolated from any existing
|
||||
// data or prior migration state in the target database. It skips when no database is
|
||||
// reachable so unit-only environments stay green; integration coverage runs when the
|
||||
// local docker-compose Postgres (deployments/local) is up and DATABASE_URL is set.
|
||||
func newTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("DATABASE_URL not set; skipping store integration test (start deployments/local Postgres and export DATABASE_URL)")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cfg, err := pgxpool.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
t.Skipf("DATABASE_URL set but parse failed (%v); skipping store integration test", err)
|
||||
}
|
||||
|
||||
// Identifier built only from digits/letters, safe to interpolate.
|
||||
schema := fmt.Sprintf("test_analysis_%d", time.Now().UnixNano())
|
||||
// Every pooled connection isolates into the throwaway schema.
|
||||
cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
||||
_, err := conn.Exec(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s; SET search_path TO %s", schema, schema))
|
||||
return err
|
||||
}
|
||||
|
||||
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
t.Skipf("DATABASE_URL set but pool create failed (%v); skipping store integration test", err)
|
||||
}
|
||||
|
||||
conn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
t.Skipf("DATABASE_URL set but connection unavailable (%v); skipping store integration test", err)
|
||||
}
|
||||
if err := RunMigrations(ctx, conn.Conn()); err != nil {
|
||||
conn.Release()
|
||||
pool.Close()
|
||||
t.Fatalf("failed to run migrations: %v", err)
|
||||
}
|
||||
conn.Release()
|
||||
|
||||
t.Cleanup(func() {
|
||||
// Drop the throwaway schema (and its tables) regardless of search_path.
|
||||
if c, err := pool.Acquire(ctx); err == nil {
|
||||
_, _ = c.Exec(ctx, fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schema))
|
||||
c.Release()
|
||||
}
|
||||
pool.Close()
|
||||
})
|
||||
|
||||
return NewStore(pool)
|
||||
}
|
||||
|
||||
func krw(amount string) market.Price {
|
||||
return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: amount}}
|
||||
}
|
||||
|
||||
func TestStoreBacktestAnalysisReads(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
// Unique IDs keep the test isolated from any other rows in a shared database.
|
||||
suffix := now.Format("20060102T150405.000000000")
|
||||
runOlderID := backtest.RunID("test-analysis-older-" + suffix)
|
||||
runNewerID := backtest.RunID("test-analysis-newer-" + suffix)
|
||||
runNoResultID := backtest.RunID("test-analysis-noresult-" + suffix)
|
||||
|
||||
spec := backtest.RunSpec{
|
||||
StrategyID: backtest.StrategyID("strat-analysis"),
|
||||
Market: market.MarketKR,
|
||||
Timeframe: market.TimeframeDaily,
|
||||
From: now.Add(-72 * time.Hour),
|
||||
To: now,
|
||||
}
|
||||
|
||||
runOlder := backtest.Run{ID: runOlderID, Spec: spec, Status: backtest.RunStatusSucceeded, CreatedAt: now.Add(-2 * time.Hour), UpdatedAt: now.Add(-2 * time.Hour)}
|
||||
runNewer := backtest.Run{ID: runNewerID, Spec: spec, Status: backtest.RunStatusSucceeded, CreatedAt: now.Add(-1 * time.Hour), UpdatedAt: now.Add(-1 * time.Hour)}
|
||||
runNoResult := backtest.Run{ID: runNoResultID, Spec: spec, Status: backtest.RunStatusRunning, CreatedAt: now, UpdatedAt: now}
|
||||
|
||||
for _, run := range []backtest.Run{runOlder, runNewer, runNoResult} {
|
||||
if err := store.UpsertRun(ctx, run); err != nil {
|
||||
t.Fatalf("failed to seed run %s: %v", run.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
resultOlder := backtest.Result{
|
||||
RunID: runOlderID,
|
||||
StartingCash: krw("10000000"),
|
||||
EndingEquity: krw("10000300"),
|
||||
Summary: backtest.SummaryMetrics{StartingCash: krw("10000000"), EndingEquity: krw("10000300"), TotalReturn: market.Decimal{Value: "0.00003"}, TradeCount: 2},
|
||||
EquityCurve: []backtest.EquityPoint{{Timestamp: now.Add(-2 * time.Hour), Equity: krw("10000300")}},
|
||||
}
|
||||
resultNewer := backtest.Result{
|
||||
RunID: runNewerID,
|
||||
StartingCash: krw("10000000"),
|
||||
EndingEquity: krw("9999000"),
|
||||
Summary: backtest.SummaryMetrics{StartingCash: krw("10000000"), EndingEquity: krw("9999000"), TotalReturn: market.Decimal{Value: "-0.0001"}, TradeCount: 1},
|
||||
EquityCurve: []backtest.EquityPoint{{Timestamp: now.Add(-1 * time.Hour), Equity: krw("9999000")}},
|
||||
}
|
||||
for _, res := range []backtest.Result{resultOlder, resultNewer} {
|
||||
if err := store.UpsertResult(ctx, res); err != nil {
|
||||
t.Fatalf("failed to seed result %s: %v", res.RunID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ListRuns returns newest CreatedAt first: runNewer must appear before runOlder.
|
||||
runs, err := store.ListRuns(ctx, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListRuns returned error: %v", err)
|
||||
}
|
||||
idxNewer, idxOlder := indexOfRun(runs, runNewerID), indexOfRun(runs, runOlderID)
|
||||
if idxNewer < 0 || idxOlder < 0 {
|
||||
t.Fatalf("ListRuns missing seeded runs: newer=%d older=%d", idxNewer, idxOlder)
|
||||
}
|
||||
if idxNewer > idxOlder {
|
||||
t.Errorf("ListRuns ordering: newer run at %d should precede older run at %d", idxNewer, idxOlder)
|
||||
}
|
||||
|
||||
// ListRuns filters by status: the running run appears, succeeded runs do not.
|
||||
running, err := store.ListRuns(ctx, backtest.RunStatusRunning)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRuns(running) returned error: %v", err)
|
||||
}
|
||||
if indexOfRun(running, runNoResultID) < 0 {
|
||||
t.Errorf("ListRuns(running) should include the running run")
|
||||
}
|
||||
if indexOfRun(running, runNewerID) >= 0 {
|
||||
t.Errorf("ListRuns(running) should not include a succeeded run")
|
||||
}
|
||||
|
||||
// GetRunDetail for a run without a result reports HasResult=false and no error.
|
||||
detail, err := store.GetRunDetail(ctx, runNoResultID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRunDetail(no-result) returned error: %v", err)
|
||||
}
|
||||
if detail.HasResult {
|
||||
t.Errorf("GetRunDetail(no-result) HasResult should be false")
|
||||
}
|
||||
if detail.Run.ID != runNoResultID {
|
||||
t.Errorf("GetRunDetail(no-result) run id mismatch: got %s", detail.Run.ID)
|
||||
}
|
||||
|
||||
// GetRunDetail for a run with a result returns it.
|
||||
detailWith, err := store.GetRunDetail(ctx, runNewerID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRunDetail(with-result) returned error: %v", err)
|
||||
}
|
||||
if !detailWith.HasResult || detailWith.Result.Summary.TradeCount != 1 {
|
||||
t.Errorf("GetRunDetail(with-result) unexpected: %+v", detailWith)
|
||||
}
|
||||
|
||||
// CompareResults preserves the caller's input order.
|
||||
compared, err := store.CompareResults(ctx, []backtest.RunID{runNewerID, runOlderID})
|
||||
if err != nil {
|
||||
t.Fatalf("CompareResults returned error: %v", err)
|
||||
}
|
||||
if len(compared) != 2 {
|
||||
t.Fatalf("CompareResults length: got %d, want 2", len(compared))
|
||||
}
|
||||
if compared[0].RunID != runNewerID || compared[1].RunID != runOlderID {
|
||||
t.Errorf("CompareResults order not preserved: got %s, %s", compared[0].RunID, compared[1].RunID)
|
||||
}
|
||||
|
||||
// CompareResults propagates a missing result as ErrResultNotFound.
|
||||
_, err = store.CompareResults(ctx, []backtest.RunID{runNewerID, runNoResultID})
|
||||
if !errors.Is(err, storage.ErrResultNotFound) {
|
||||
t.Errorf("CompareResults missing result: expected ErrResultNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func indexOfRun(runs []backtest.Run, id backtest.RunID) int {
|
||||
for i, run := range runs {
|
||||
if run.ID == id {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Loading…
Reference in a new issue