refactor: market status parsing과 backtest runtime 흐름 통합
- parser_map.go 업데이트하여 market status 파싱 로직 통일 - protobuf market.proto 변경사항 적용 (market.pb.go, market.pb.dart) - socket handlers, market, backtest 관련 테스트 및 런타임 코드 개선 - workerclient와 alt-worker main.go 변경사항 반영 - agent-task archive 이동 (01_import_contract_worker_api → archive/2026/06/)
This commit is contained in:
parent
7134a2a0e2
commit
620ca92e2e
30 changed files with 1877 additions and 200 deletions
|
|
@ -0,0 +1,222 @@
|
|||
<!-- task=m-command-first-operator-workflow/01_import_contract_worker_api plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-03
|
||||
task=m-command-first-operator-workflow/01_import_contract_worker_api, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-command-first-operator-workflow/01_import_contract_worker_api/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Contract And Parser Surface | [x] |
|
||||
| [API-2] API Forwarding | [x] |
|
||||
| [API-3] Worker Import Handler | [x] |
|
||||
| [API-4] Worker Entrypoint Wiring | [x] |
|
||||
| [API-5] Focused Verification | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [API-1] Add `ImportDailyBarsRequest/Response` protobuf messages, regenerate contracts, and register parsers in API/worker/CLI maps.
|
||||
- [x] [API-2] Add API workerclient and API socket forwarding for import daily bars with typed unavailable/timeout/internal errors.
|
||||
- [x] [API-3] Add worker socket import handler that validates request shape, calls the importer dependency, and returns instrument/bar counts.
|
||||
- [x] [API-4] Wire `alt-worker` store-backed deps with the KIS importer command surface without exposing secrets.
|
||||
- [x] [API-5] Run focused contract/API/worker verification and `git diff --check`.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-command-first-operator-workflow/01_import_contract_worker_api/`를 `agent-task/archive/YYYY/MM/m-command-first-operator-workflow/01_import_contract_worker_api/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-command-first-operator-workflow/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.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로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 검증 명령은 계획대로 실행했다. 단, `bin/contracts-gen`/`bin/contracts-check`는 committed 생성물이 protoc v5.29.3(=protoc 29.3)으로 생성되어 있었고 PATH 기본 `protoc`는 v3.21.12였다. 둘로 재생성하면 생성 헤더의 protoc 버전 주석만 달라져 무관한 drift가 발생하므로, 환경에 존재하던 protoc 29.3(`/tmp/protoc29/bin`)을 PATH 앞에 두고 재생성했다. 그 결과 생성물 diff는 계획대로 `market.pb.go`/`market.pb.dart`/`market.pbjson.dart`에만 한정됐고 `bin/contracts-check`는 exit 0이다. 명령 자체는 대체하지 않았다.
|
||||
- `services/api/internal/contracts/parser_map_test.go`는 항목 추가 후 `gofmt -w`가 import 블록 순서를 표준 정렬로 정규화했다(기능 변화 없음). `git diff --check`와 gofmt 모두 clean.
|
||||
- 계획 [API-2]/[API-4]에서 capability 추가는 "advertise 시"로 선택 사항이었다. operator 명령 표면을 노출하므로 API에는 정적 `market-import` capability를, worker에는 `DailyBarImporter` 의존이 있을 때만 켜지는 `market-import` capability를 추가하고 관련 sync 테스트를 갱신했다.
|
||||
- worker `cmd/alt-worker/main.go` ready 로그에 `market_import_surface` 불리언 지표를 추가했다(기존 `market_read_surface` 패턴과 동일, secret 미노출).
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- proto는 additive로만 변경했다. `ImportDailyBarsRequest/Response`를 `ListBarsResponse` 뒤에 새 field 번호로 추가하고 기존 field 번호는 건드리지 않았다(contracts domain rule: additive 우선, 기존 field 번호 불변).
|
||||
- API는 얇은 control plane을 유지했다. `handleImportDailyBars`는 provider/selector_kind/symbols 필수 검증, market/venue enum 범위 검증, YYYYMMDD 날짜 형식 검증을 worker 연결 전에 수행하고, 실행은 worker로 forward한다. worker 내부 타입을 import하지 않는다.
|
||||
- worker가 import 실행을 소유한다. socket handler는 좁은 `DailyBarImporter` 인터페이스에만 의존해 provider/storage wiring과 분리되고, 결과 counts를 typed 응답으로 돌려준다. 오류는 handler error가 아니라 `ErrorInfo`로 매핑한다(worker rule: typed contract error 반환).
|
||||
- venue 변환은 `venueFromProto`를 추가해 처리했다. import selector는 venue를 지정하지 않을 수 있으므로 `VENUE_UNSPECIFIED`는 빈 도메인 venue로 허용하고 범위 밖 enum만 거부한다. market은 기존 `marketFilterFromProto`(unspecified 허용)를 재사용했다.
|
||||
- `alt-worker` entrypoint는 importer 인스턴스 하나를 만들어 job handler와 socket dep에 함께 주입한다. app key/token/account/raw header/secret item 이름은 로깅하거나 노출하지 않는다.
|
||||
- import 명령 handler는 항상 등록하되 capability는 의존 존재 여부로 게이트한다(worker는 `DailyBarImporter`가 있을 때만 `market-import` advertise). backtest 핸들러/capability와 동일한 패턴.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Protobuf change is additive and generated files were regenerated rather than hand-edited.
|
||||
- API remains a thin validator/forwarder and does not import worker internals.
|
||||
- Worker owns import execution and returns typed `ErrorInfo` instead of handler errors.
|
||||
- Capability strings match registered handlers and available deps.
|
||||
- No secret/account/token/raw KIS payload is logged or written.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
> 참고: `bin/contracts-check`/`bin/contracts-gen`는 PATH에 protoc 29.3(committed 생성물과 동일 버전)을 두고 실행했다. 근거는 `계획 대비 변경 사항` 참고.
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ bin/contracts-check
|
||||
[exit 0]
|
||||
$ go test ./packages/contracts/gen/go/... -count=1
|
||||
? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files]
|
||||
$ go test ./services/api/internal/contracts ./services/worker/internal/contracts ./apps/cli/internal/operator -run ParserMap -count=1
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/contracts 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.009s
|
||||
ok git.toki-labs.com/toki/alt/apps/cli/internal/operator 0.006s
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ go test ./services/api/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/api/cmd/alt-api [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/config 0.004s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/contracts 0.007s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.022s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/workerclient 0.414s
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```bash
|
||||
$ go test ./services/worker/internal/socket -count=1
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.063s
|
||||
```
|
||||
|
||||
### API-4 중간 검증
|
||||
```bash
|
||||
$ go build ./services/worker/...
|
||||
BUILD OK
|
||||
$ go test ./services/worker/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker [no test files]
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-data-check [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-migrate 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/config 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.007s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/datacheck 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.027s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys 0.005s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.068s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.305s
|
||||
```
|
||||
|
||||
### API-5 중간 검증
|
||||
```bash
|
||||
$ git diff --check
|
||||
[diff-check clean, exit 0]
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ bin/contracts-check
|
||||
[exit 0]
|
||||
$ go test ./packages/contracts/gen/go/... -count=1
|
||||
? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files]
|
||||
$ go test ./services/api/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/api/cmd/alt-api [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/config 0.002s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/contracts 0.010s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.011s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/workerclient 0.416s
|
||||
$ go test ./services/worker/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker [no test files]
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-data-check [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-migrate 0.005s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.004s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/config 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/datacheck 0.005s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.026s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys 0.003s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.069s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.218s
|
||||
$ git diff --check
|
||||
[diff-check clean, exit 0]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- correctness: Fail
|
||||
- completeness: Fail
|
||||
- test coverage: Fail
|
||||
- API contract: Fail
|
||||
- code quality: Pass
|
||||
- plan deviation: Fail
|
||||
- verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Required: [services/api/internal/socket/market.go](/config/workspace/alt/services/api/internal/socket/market.go:77) and [services/worker/internal/socket/market.go](/config/workspace/alt/services/worker/internal/socket/market.go:88) validate `from_yyyymmdd` and `to_yyyymmdd` independently, but never require the live KIS date range or reject `from > to` before forwarding. The live provider requires both dates and only checks ordering after worker/provider handling ([services/worker/internal/providers/kis/live_client.go](/config/workspace/alt/services/worker/internal/providers/kis/live_client.go:298), [services/worker/internal/providers/kis/live_client.go](/config/workspace/alt/services/worker/internal/providers/kis/live_client.go:223)), so missing or reversed ranges become backend/internal failures instead of typed `invalid_request` responses and can cross the API/worker boundary unnecessarily. Fix by centralizing import date range validation in both API and worker handlers, requiring both fields for the current KIS import surface, rejecting `from > to` as `invalid_request`, and adding API/worker regression tests that prove no worker/importer call occurs for missing or reversed ranges.
|
||||
- 다음 단계: WARN/FAIL이고 user-review gate가 트리거되지 않았으므로 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성한다.
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
<!-- task=m-command-first-operator-workflow/01_import_contract_worker_api plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Code Review Reference - REVIEW_API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-03
|
||||
task=m-command-first-operator-workflow/01_import_contract_worker_api, plan=1, tag=REVIEW_API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-command-first-operator-workflow/01_import_contract_worker_api/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_API-1] Date Range Validation | [x] |
|
||||
| [REVIEW_API-2] Verification | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [REVIEW_API-1] Tighten `ImportDailyBars` date range validation in API and worker handlers, with regression tests for missing and reversed ranges.
|
||||
- [x] [REVIEW_API-2] Run focused and full verification, including generated-contract and client lint checks.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 active task 디렉터리 `agent-task/m-command-first-operator-workflow/01_import_contract_worker_api/`를 `agent-task/archive/YYYY/MM/m-command-first-operator-workflow/01_import_contract_worker_api/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-command-first-operator-workflow/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.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로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 계획대로 구현했고 명령 대체는 없다. `bin/contracts-check`는 committed 생성물과 동일한 protoc 29.3(=v5.29.3)을 PATH 앞에 두고 실행했다(기본 PATH의 protoc v3.21.12로 재생성하면 생성 헤더의 protoc 버전 주석만 달라지는 무관한 drift 발생). 본 follow-up은 `.proto`를 건드리지 않으므로 생성물 변경은 없고 `git diff --check`/`bin/contracts-check` 모두 clean이다.
|
||||
- `bin/lint`의 `flutter analyze`는 `lib/src/integrations/mattermost/mattermost_auth_service.dart`의 `avoid_print` info 13건을 보고하지만 본 변경(생성 Dart contract 및 Go socket validation)과 무관한 기존 항목이며, `--no-fatal-infos`로 exit 0이다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- 첫 리뷰 FAIL 원인: import 명령의 날짜를 optional로 두고 `from <= to`를 검증하지 않아, live KIS importer가 요구하는 bounded/정렬된 date range 위반이 socket boundary의 typed `invalid_request`가 아니라 worker-internal backend 실패로 새어 나갔다.
|
||||
- API와 worker handler 양쪽에서 worker/importer 호출 **이전에** date range를 검증하도록 좁혔다. 현재 KIS import command 표면에서는 `from_yyyymmdd`/`to_yyyymmdd`를 둘 다 필수로 요구하고, YYYYMMDD 파싱 실패와 `from > to`를 모두 `invalid_request`로 닫는다.
|
||||
- provider의 기본 날짜 동작은 바꾸지 않았다(범위 결정 근거대로). live KIS provider가 날짜를 요구하므로 socket 명령은 빈 날짜에 default range를 발명하지 않고 거부한다.
|
||||
- API는 `validateImportDate`(빈 값 허용)를 `validateImportDateRange(from, to)`로 교체했고, worker는 endpoint별 `parseImportDate`를 범위 검증과 정렬 검사를 포함해 파싱된 from/to를 돌려주는 `parseImportDateRange(from, to)`로 교체했다. worker `market.go`에서 더는 쓰이지 않는 `fmt` import를 제거했다.
|
||||
- 회귀 테스트는 API/worker 모두 valid 요청을 복제해 한 필드만 변형하는 table 패턴으로 재작성하여 missing `from`/missing `to`/malformed/reversed 범위를 각각 격리 검증한다. API 케이스는 검증 실패 시 worker가 연결조차 되지 않음을 `connectCount == 0`과 `importReq == nil`로, worker 케이스는 `fakeDailyBarImporter.called == false`로 단언한다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 또는 범위 충돌 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Missing `from_yyyymmdd`, missing `to_yyyymmdd`, and `from > to` are rejected as typed `invalid_request`.
|
||||
- API validation rejects bad date ranges before `worker.Connect` or `ImportDailyBars`.
|
||||
- Worker validation rejects bad date ranges before `DailyBarImporter.ImportDailyBars`.
|
||||
- Focused tests and full original contract/API/worker/client lint verification were rerun with actual stdout/stderr.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
> 참고: `bin/contracts-check`는 PATH에 protoc 29.3(committed 생성물과 동일 버전)을 두고 실행했다. 근거는 `계획 대비 변경 사항` 참고.
|
||||
|
||||
### REVIEW_API-1 중간 검증
|
||||
```bash
|
||||
$ go test ./services/api/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.007s
|
||||
# 신규/회귀 케이스(-v): missing_from_date, missing_to_date, malformed_from_date,
|
||||
# malformed_to_date, reversed_range 모두 PASS (invalid_request 반환, connectCount==0)
|
||||
$ go test ./services/worker/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.005s
|
||||
# 신규/회귀 케이스(-v): missing_from_date, missing_to_date, malformed_from_date,
|
||||
# malformed_to_date, reversed_range 모두 PASS (invalid_request 반환, importer 미호출)
|
||||
```
|
||||
|
||||
### REVIEW_API-2 중간 검증
|
||||
```bash
|
||||
$ bin/contracts-check
|
||||
[exit 0]
|
||||
$ go test ./packages/contracts/gen/go/... -count=1
|
||||
? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files]
|
||||
$ go test ./services/api/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/api/cmd/alt-api [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/config 0.009s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/contracts 0.008s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.011s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/workerclient 0.414s
|
||||
$ go test ./services/worker/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker [no test files]
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-data-check [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-migrate 0.008s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.005s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/config 0.004s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.008s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/datacheck 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer 0.004s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.036s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys 0.005s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.065s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.206s
|
||||
$ bin/lint
|
||||
Analyzing client...
|
||||
info • Don't invoke 'print' in production code • lib/src/integrations/mattermost/mattermost_auth_service.dart:27:7 • avoid_print
|
||||
... (mattermost avoid_print info 13건, 본 변경과 무관)
|
||||
13 issues found. (ran in 12.7s)
|
||||
[go vet 5개 모듈 무출력; flutter analyze --no-fatal-infos exit 0; bin/lint exit 0]
|
||||
$ git diff --check
|
||||
[diff-check clean, exit 0]
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ go test ./services/api/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.007s
|
||||
$ go test ./services/worker/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.005s
|
||||
$ bin/contracts-check
|
||||
[exit 0]
|
||||
$ go test ./packages/contracts/gen/go/... -count=1
|
||||
? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files]
|
||||
$ go test ./services/api/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/api/cmd/alt-api [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/config 0.009s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/contracts 0.008s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/socket 0.011s
|
||||
ok git.toki-labs.com/toki/alt/services/api/internal/workerclient 0.414s
|
||||
$ go test ./services/worker/... -count=1
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker [no test files]
|
||||
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-data-check [no test files]
|
||||
ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-migrate 0.008s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.005s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/config 0.004s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.008s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/datacheck 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer 0.004s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.036s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys 0.005s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.065s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.206s
|
||||
$ bin/lint
|
||||
[go vet 무출력; flutter analyze mattermost avoid_print info 13건만 보고; exit 0]
|
||||
$ git diff --check
|
||||
[diff-check clean, exit 0]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS이므로 `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다.
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Complete - m-command-first-operator-workflow/01_import_contract_worker_api
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-03
|
||||
|
||||
## 요약
|
||||
|
||||
Import daily bars contract/API/worker rail completed after 2 review loops; final verdict PASS.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | FAIL | Import command date range validation was incomplete; follow-up required API/worker invalid_request handling before worker/importer calls. |
|
||||
| `plan_cloud_G08_1.log` | `code_review_cloud_G08_1.log` | PASS | Missing, malformed, and reversed import date ranges are rejected at API/worker boundaries with regression coverage. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- Added additive `ImportDailyBarsRequest` and `ImportDailyBarsResponse` contracts with regenerated Go/Dart outputs.
|
||||
- Registered import request/response parsers in API, worker, and CLI parser maps.
|
||||
- Added API workerclient forwarding and API socket validation/forwarding for import daily bars.
|
||||
- Added worker socket import handler, request conversion, typed error mapping, and `alt-worker` importer dependency wiring.
|
||||
- Added `market-import` capability coverage for API and worker surfaces.
|
||||
- Closed the review follow-up by requiring `from_yyyymmdd`/`to_yyyymmdd`, rejecting malformed and reversed ranges before worker/importer calls, and adding regression tests.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `go test ./services/api/internal/socket -run TestHandleImportDailyBars -count=1` - PASS; `ok git.toki-labs.com/toki/alt/services/api/internal/socket`.
|
||||
- `go test ./services/worker/internal/socket -run TestHandleImportDailyBars -count=1` - PASS; `ok git.toki-labs.com/toki/alt/services/worker/internal/socket`.
|
||||
- `bin/contracts-check` - PASS; exit 0 with no generated drift output.
|
||||
- `go test ./packages/contracts/gen/go/... -count=1` - PASS; package has no test files.
|
||||
- `go test ./services/api/... -count=1` - PASS; all API packages passed.
|
||||
- `go test ./services/worker/... -count=1` - PASS; all worker packages passed.
|
||||
- `bin/lint` - PASS; exit 0. Flutter analyze still reports 13 pre-existing `avoid_print` info entries in `lib/src/integrations/mattermost/mattermost_auth_service.dart`, unrelated to this change.
|
||||
- `git diff --check` - PASS; no whitespace warnings.
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
<!-- task=m-command-first-operator-workflow/01_import_contract_worker_api plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Plan - Review Follow-Up Import Date Range Validation
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 plan은 코드리뷰 FAIL 후속 구현 지시서다. 구현 완료 전 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 실제 내용과 검증 출력으로 채운다. 검증은 아래 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인다. 최종 판정, archive 이동, `complete.log` 작성은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경/secret, 또는 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청`을 채우고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
첫 구현은 import command contract/API/worker rail을 추가했지만, date range를 API/worker handler에서 의미적으로 막지 못했다. 현재 live KIS importer는 `from/to` date range를 요구하고 `from <= to`를 기대하므로, operator command의 잘못된 날짜 입력은 API/worker boundary에서 typed `invalid_request`로 닫혀야 한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 사용자-only blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 기준이며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/private/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/code-review/SKILL.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/contracts-smoke.md`
|
||||
- `agent-test/local/api-smoke.md`
|
||||
- `agent-test/local/worker-smoke.md`
|
||||
- `agent-test/local/client-smoke.md`
|
||||
- `agent-test/local/operations-smoke.md`
|
||||
- `agent-ops/rules/project/domain/contracts/rules.md`
|
||||
- `agent-ops/rules/project/domain/api/rules.md`
|
||||
- `agent-ops/rules/project/domain/worker/rules.md`
|
||||
- `agent-ops/rules/project/domain/client/rules.md`
|
||||
- `agent-ops/rules/project/domain/operations/rules.md`
|
||||
- `agent-task/m-command-first-operator-workflow/01_import_contract_worker_api/plan_cloud_G08_0.log`
|
||||
- `agent-task/m-command-first-operator-workflow/01_import_contract_worker_api/code_review_cloud_G08_0.log`
|
||||
- `packages/contracts/proto/alt/v1/market.proto`
|
||||
- `packages/contracts/gen/go/alt/v1/market.pb.go`
|
||||
- `apps/client/lib/src/generated/alt/v1/market.pb.dart`
|
||||
- `apps/client/lib/src/generated/alt/v1/market.pbjson.dart`
|
||||
- `services/api/internal/socket/market.go`
|
||||
- `services/api/internal/socket/market_test.go`
|
||||
- `services/api/internal/socket/backtest_test.go`
|
||||
- `services/api/internal/socket/server_test.go`
|
||||
- `services/api/internal/workerclient/client.go`
|
||||
- `services/api/internal/workerclient/client_test.go`
|
||||
- `services/worker/internal/socket/backtest.go`
|
||||
- `services/worker/internal/socket/handlers.go`
|
||||
- `services/worker/internal/socket/market.go`
|
||||
- `services/worker/internal/socket/market_mapping.go`
|
||||
- `services/worker/internal/socket/market_test.go`
|
||||
- `services/worker/internal/socket/server_test.go`
|
||||
- `services/worker/internal/jobs/marketdata_jobs.go`
|
||||
- `services/worker/internal/jobs/marketdata_jobs_test.go`
|
||||
- `services/worker/internal/marketdata/importer/importer.go`
|
||||
- `services/worker/internal/providers/kis/live_client.go`
|
||||
- `services/worker/internal/providers/kis/daily_itemchartprice_test.go`
|
||||
- `services/worker/cmd/alt-worker/main.go`
|
||||
- `packages/domain/market/types.go`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- `agent-test/local/rules.md` present/read.
|
||||
- Matched profiles read: `contracts-smoke`, `api-smoke`, `worker-smoke`, `client-smoke`, `operations-smoke`.
|
||||
- Applied commands: focused `go test` for API/worker socket regressions, full `bin/contracts-check`, `go test ./packages/contracts/gen/go/... -count=1`, `go test ./services/api/... -count=1`, `go test ./services/worker/... -count=1`, `bin/lint`, `git diff --check`.
|
||||
- Fallback: none. No `<확인 필요>` command values in matched profiles.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- API import validation tests cover malformed date strings, but not missing `from_yyyymmdd`, missing `to_yyyymmdd`, or `from > to`.
|
||||
- Worker import validation tests cover malformed date strings, but not missing date endpoints or reversed date ranges.
|
||||
- Existing KIS provider tests prove the live provider requires date range and validates ordering, but socket handlers currently let those user-input errors reach backend handling.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- Renamed/removed symbols: none.
|
||||
- New or touched symbols to audit after the fix: `handleImportDailyBars`, `validateImportDate`, `parseImportDate`, `validImportRequest`, `fakeWorkerClient.connectCount`, `fakeDailyBarImporter.called`.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- Split policy evaluated. This is a follow-up inside existing split subtask `01_import_contract_worker_api`.
|
||||
- No new subtask is created because the fix is one narrow validation/test unit across API and worker handlers, and splitting would separate mirrored boundary tests from the same bug.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- Include API/worker import date range validation and regression tests only.
|
||||
- Exclude protobuf schema changes, CLI YAML action implementation, generated contract regeneration, Flutter UI, KIS live smoke, and roadmap updates.
|
||||
- Keep provider default-date behavior unchanged. Current live KIS provider requires `from/to`, so the socket command should reject missing dates instead of inventing a default range.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build=`cloud-G08`, review=`cloud-G08`: follow-up is narrow but still closes a cross-domain protocol/API/worker validation failure from a cloud-routed contract task.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [REVIEW_API-1] Tighten `ImportDailyBars` date range validation in API and worker handlers, with regression tests for missing and reversed ranges.
|
||||
- [ ] [REVIEW_API-2] Run focused and full verification, including generated-contract and client lint checks.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## [REVIEW_API-1] Date Range Validation
|
||||
|
||||
### 문제
|
||||
|
||||
[services/api/internal/socket/market.go](/config/workspace/alt/services/api/internal/socket/market.go:77) validates `from_yyyymmdd` and `to_yyyymmdd` independently, then opens the worker connection. [services/worker/internal/socket/market.go](/config/workspace/alt/services/worker/internal/socket/market.go:88) does the same before calling the importer. The live KIS provider requires both dates and validates `from <= to` only after worker/provider handling ([services/worker/internal/providers/kis/live_client.go](/config/workspace/alt/services/worker/internal/providers/kis/live_client.go:298), [services/worker/internal/providers/kis/live_client.go](/config/workspace/alt/services/worker/internal/providers/kis/live_client.go:223)), so bad user input becomes `internal` backend failure instead of typed `invalid_request`.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
Add date range validation in both socket boundaries before worker/importer calls:
|
||||
|
||||
- Require `from_yyyymmdd` and `to_yyyymmdd` to be present for the current KIS import command surface.
|
||||
- Parse both as YYYYMMDD.
|
||||
- Reject `from > to` with `invalid_request`.
|
||||
- In API tests, assert the fake worker is not connected or called for missing/reversed ranges.
|
||||
- In worker tests, assert the fake importer is not called for missing/reversed ranges.
|
||||
|
||||
Before ([services/api/internal/socket/market.go](/config/workspace/alt/services/api/internal/socket/market.go:77)):
|
||||
|
||||
```go
|
||||
if err := validateImportDate("from_yyyymmdd", req.GetFromYyyymmdd()); err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
||||
}
|
||||
if err := validateImportDate("to_yyyymmdd", req.GetToYyyymmdd()); err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
if err := validateImportDateRange(req.GetFromYyyymmdd(), req.GetToYyyymmdd()); err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
||||
}
|
||||
```
|
||||
|
||||
Use equivalent worker-side parsing that returns parsed `from` and `to` for the importer request.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `services/api/internal/socket/market.go`: replace independent optional date validation with required range validation.
|
||||
- [ ] `services/api/internal/socket/market_test.go`: add missing `from_yyyymmdd`, missing `to_yyyymmdd`, and reversed range validation cases; assert `connectCount == 0` and `importReq == nil`.
|
||||
- [ ] `services/worker/internal/socket/market.go`: replace independent optional date parsing with required range parsing and `from <= to` validation.
|
||||
- [ ] `services/worker/internal/socket/market_test.go`: add missing date and reversed range cases; assert fake importer is not called.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- Add API regression coverage to `TestHandleImportDailyBarsValidation`.
|
||||
- Add worker regression coverage to `TestHandleImportDailyBarsValidation`.
|
||||
- No provider test changes are needed; KIS provider already covers malformed date/order behavior.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test ./services/api/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
go test ./services/worker/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
```
|
||||
|
||||
Expected: both commands exit 0 and the new invalid range cases return `invalid_request`.
|
||||
|
||||
## [REVIEW_API-2] Verification
|
||||
|
||||
### 문제
|
||||
|
||||
The original task spans generated contracts, API, worker, and generated Dart client output. The follow-up must prove the validation fix does not leave contract drift or client generated analyzer fallout.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
Run focused tests first, then rerun the full final command set. Use fresh Go tests with `-count=1`; cached output is not acceptable.
|
||||
|
||||
### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] No additional source files.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
No new test files beyond `REVIEW_API-1`.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
bin/contracts-check
|
||||
go test ./packages/contracts/gen/go/... -count=1
|
||||
go test ./services/api/... -count=1
|
||||
go test ./services/worker/... -count=1
|
||||
bin/lint
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all commands exit 0. If `bin/lint` reports pre-existing analyzer info but exits 0, paste the actual output and note whether it is unrelated to this generated contract change.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `services/api/internal/socket/market.go` | REVIEW_API-1 |
|
||||
| `services/api/internal/socket/market_test.go` | REVIEW_API-1 |
|
||||
| `services/worker/internal/socket/market.go` | REVIEW_API-1 |
|
||||
| `services/worker/internal/socket/market_test.go` | REVIEW_API-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test ./services/api/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
go test ./services/worker/internal/socket -run TestHandleImportDailyBars -count=1
|
||||
bin/contracts-check
|
||||
go test ./packages/contracts/gen/go/... -count=1
|
||||
go test ./services/api/... -count=1
|
||||
go test ./services/worker/... -count=1
|
||||
bin/lint
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all commands exit 0. Fresh `go test -count=1` is required; cached output is not acceptable.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
<!-- task=m-command-first-operator-workflow/01_import_contract_worker_api plan=0 tag=API -->
|
||||
|
||||
# Code Review Reference - API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-06-03
|
||||
task=m-command-first-operator-workflow/01_import_contract_worker_api, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-command-first-operator-workflow/01_import_contract_worker_api/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Contract And Parser Surface | [ ] |
|
||||
| [API-2] API Forwarding | [ ] |
|
||||
| [API-3] Worker Import Handler | [ ] |
|
||||
| [API-4] Worker Entrypoint Wiring | [ ] |
|
||||
| [API-5] Focused Verification | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [API-1] Add `ImportDailyBarsRequest/Response` protobuf messages, regenerate contracts, and register parsers in API/worker/CLI maps.
|
||||
- [ ] [API-2] Add API workerclient and API socket forwarding for import daily bars with typed unavailable/timeout/internal errors.
|
||||
- [ ] [API-3] Add worker socket import handler that validates request shape, calls the importer dependency, and returns instrument/bar counts.
|
||||
- [ ] [API-4] Wire `alt-worker` store-backed deps with the KIS importer command surface without exposing secrets.
|
||||
- [ ] [API-5] Run focused contract/API/worker verification and `git diff --check`.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-command-first-operator-workflow/01_import_contract_worker_api/`를 `agent-task/archive/YYYY/MM/m-command-first-operator-workflow/01_import_contract_worker_api/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-command-first-operator-workflow/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Protobuf change is additive and generated files were regenerated rather than hand-edited.
|
||||
- API remains a thin validator/forwarder and does not import worker internals.
|
||||
- Worker owns import execution and returns typed `ErrorInfo` instead of handler errors.
|
||||
- Capability strings match registered handlers and available deps.
|
||||
- No secret/account/token/raw KIS payload is logged or written.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ bin/contracts-check
|
||||
$ go test ./packages/contracts/gen/go/... -count=1
|
||||
$ go test ./services/api/internal/contracts ./services/worker/internal/contracts ./apps/cli/internal/operator -run ParserMap -count=1
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ go test ./services/api/... -count=1
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-3 중간 검증
|
||||
```bash
|
||||
$ go test ./services/worker/internal/socket -count=1
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-4 중간 검증
|
||||
```bash
|
||||
$ go test ./services/worker/... -count=1
|
||||
(output)
|
||||
```
|
||||
|
||||
### API-5 중간 검증
|
||||
```bash
|
||||
$ git diff --check
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ bin/contracts-check
|
||||
$ go test ./packages/contracts/gen/go/... -count=1
|
||||
$ go test ./services/api/... -count=1
|
||||
$ go test ./services/worker/... -count=1
|
||||
$ 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.
|
||||
|
|
@ -22,6 +22,9 @@ func messageFactories() []func() proto.Message {
|
|||
func() proto.Message { return &altv1.ListInstrumentsResponse{} },
|
||||
func() proto.Message { return &altv1.ListBarsRequest{} },
|
||||
func() proto.Message { return &altv1.ListBarsResponse{} },
|
||||
// market import command: daily bars
|
||||
func() proto.Message { return &altv1.ImportDailyBarsRequest{} },
|
||||
func() proto.Message { return &altv1.ImportDailyBarsResponse{} },
|
||||
// backtest surface: start / list / detail / result / compare
|
||||
func() proto.Message { return &altv1.StartBacktestRequest{} },
|
||||
func() proto.Message { return &altv1.StartBacktestResponse{} },
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ func TestParserMapIncludesRunnerMessages(t *testing.T) {
|
|||
&altv1.ListInstrumentsResponse{},
|
||||
&altv1.ListBarsRequest{},
|
||||
&altv1.ListBarsResponse{},
|
||||
// market import command surface a later CLI subtask sends/receives
|
||||
&altv1.ImportDailyBarsRequest{},
|
||||
&altv1.ImportDailyBarsResponse{},
|
||||
// backtest surface is parsed too so the CLI rail matches the API/worker
|
||||
// parser maps and a later subtask can reuse it without drift.
|
||||
&altv1.StartBacktestRequest{},
|
||||
|
|
|
|||
|
|
@ -707,6 +707,235 @@ class ListBarsResponse extends $pb.GeneratedMessage {
|
|||
$0.ErrorInfo ensureError() => $_ensure(1);
|
||||
}
|
||||
|
||||
class ImportDailyBarsRequest extends $pb.GeneratedMessage {
|
||||
factory ImportDailyBarsRequest({
|
||||
$core.String? provider,
|
||||
$core.String? selectorKind,
|
||||
$0.Market? market,
|
||||
$0.Venue? venue,
|
||||
$core.String? name,
|
||||
$core.Iterable<$core.String>? symbols,
|
||||
$core.String? fromYyyymmdd,
|
||||
$core.String? toYyyymmdd,
|
||||
}) {
|
||||
final result = create();
|
||||
if (provider != null) result.provider = provider;
|
||||
if (selectorKind != null) result.selectorKind = selectorKind;
|
||||
if (market != null) result.market = market;
|
||||
if (venue != null) result.venue = venue;
|
||||
if (name != null) result.name = name;
|
||||
if (symbols != null) result.symbols.addAll(symbols);
|
||||
if (fromYyyymmdd != null) result.fromYyyymmdd = fromYyyymmdd;
|
||||
if (toYyyymmdd != null) result.toYyyymmdd = toYyyymmdd;
|
||||
return result;
|
||||
}
|
||||
|
||||
ImportDailyBarsRequest._();
|
||||
|
||||
factory ImportDailyBarsRequest.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ImportDailyBarsRequest.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ImportDailyBarsRequest',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'provider')
|
||||
..aOS(2, _omitFieldNames ? '' : 'selectorKind')
|
||||
..aE<$0.Market>(3, _omitFieldNames ? '' : 'market',
|
||||
enumValues: $0.Market.values)
|
||||
..aE<$0.Venue>(4, _omitFieldNames ? '' : 'venue',
|
||||
enumValues: $0.Venue.values)
|
||||
..aOS(5, _omitFieldNames ? '' : 'name')
|
||||
..pPS(6, _omitFieldNames ? '' : 'symbols')
|
||||
..aOS(7, _omitFieldNames ? '' : 'fromYyyymmdd')
|
||||
..aOS(8, _omitFieldNames ? '' : 'toYyyymmdd')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ImportDailyBarsRequest clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ImportDailyBarsRequest copyWith(
|
||||
void Function(ImportDailyBarsRequest) updates) =>
|
||||
super.copyWith((message) => updates(message as ImportDailyBarsRequest))
|
||||
as ImportDailyBarsRequest;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ImportDailyBarsRequest create() => ImportDailyBarsRequest._();
|
||||
@$core.override
|
||||
ImportDailyBarsRequest createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ImportDailyBarsRequest getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<ImportDailyBarsRequest>(create);
|
||||
static ImportDailyBarsRequest? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get provider => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set provider($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasProvider() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearProvider() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.String get selectorKind => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set selectorKind($core.String value) => $_setString(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasSelectorKind() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearSelectorKind() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$0.Market get market => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set market($0.Market value) => $_setField(3, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasMarket() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearMarket() => $_clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$0.Venue get venue => $_getN(3);
|
||||
@$pb.TagNumber(4)
|
||||
set venue($0.Venue value) => $_setField(4, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasVenue() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearVenue() => $_clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.String get name => $_getSZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set name($core.String value) => $_setString(4, value);
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasName() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearName() => $_clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$pb.PbList<$core.String> get symbols => $_getList(5);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
$core.String get fromYyyymmdd => $_getSZ(6);
|
||||
@$pb.TagNumber(7)
|
||||
set fromYyyymmdd($core.String value) => $_setString(6, value);
|
||||
@$pb.TagNumber(7)
|
||||
$core.bool hasFromYyyymmdd() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearFromYyyymmdd() => $_clearField(7);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.String get toYyyymmdd => $_getSZ(7);
|
||||
@$pb.TagNumber(8)
|
||||
set toYyyymmdd($core.String value) => $_setString(7, value);
|
||||
@$pb.TagNumber(8)
|
||||
$core.bool hasToYyyymmdd() => $_has(7);
|
||||
@$pb.TagNumber(8)
|
||||
void clearToYyyymmdd() => $_clearField(8);
|
||||
}
|
||||
|
||||
class ImportDailyBarsResponse extends $pb.GeneratedMessage {
|
||||
factory ImportDailyBarsResponse({
|
||||
$core.String? provider,
|
||||
$core.int? instrumentCount,
|
||||
$core.int? barCount,
|
||||
$0.ErrorInfo? error,
|
||||
}) {
|
||||
final result = create();
|
||||
if (provider != null) result.provider = provider;
|
||||
if (instrumentCount != null) result.instrumentCount = instrumentCount;
|
||||
if (barCount != null) result.barCount = barCount;
|
||||
if (error != null) result.error = error;
|
||||
return result;
|
||||
}
|
||||
|
||||
ImportDailyBarsResponse._();
|
||||
|
||||
factory ImportDailyBarsResponse.fromBuffer($core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory ImportDailyBarsResponse.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'ImportDailyBarsResponse',
|
||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'alt.v1'),
|
||||
createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'provider')
|
||||
..aI(2, _omitFieldNames ? '' : 'instrumentCount')
|
||||
..aI(3, _omitFieldNames ? '' : 'barCount')
|
||||
..aOM<$0.ErrorInfo>(4, _omitFieldNames ? '' : 'error',
|
||||
subBuilder: $0.ErrorInfo.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ImportDailyBarsResponse clone() => deepCopy();
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
ImportDailyBarsResponse copyWith(
|
||||
void Function(ImportDailyBarsResponse) updates) =>
|
||||
super.copyWith((message) => updates(message as ImportDailyBarsResponse))
|
||||
as ImportDailyBarsResponse;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ImportDailyBarsResponse create() => ImportDailyBarsResponse._();
|
||||
@$core.override
|
||||
ImportDailyBarsResponse createEmptyInstance() => create();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ImportDailyBarsResponse getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<ImportDailyBarsResponse>(create);
|
||||
static ImportDailyBarsResponse? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get provider => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set provider($core.String value) => $_setString(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasProvider() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearProvider() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get instrumentCount => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set instrumentCount($core.int value) => $_setSignedInt32(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasInstrumentCount() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearInstrumentCount() => $_clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.int get barCount => $_getIZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set barCount($core.int value) => $_setSignedInt32(2, value);
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasBarCount() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearBarCount() => $_clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$0.ErrorInfo get error => $_getN(3);
|
||||
@$pb.TagNumber(4)
|
||||
set error($0.ErrorInfo value) => $_setField(4, value);
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasError() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearError() => $_clearField(4);
|
||||
@$pb.TagNumber(4)
|
||||
$0.ErrorInfo ensureError() => $_ensure(3);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||
const $core.bool _omitMessageNames =
|
||||
|
|
|
|||
|
|
@ -266,3 +266,66 @@ const ListBarsResponse$json = {
|
|||
final $typed_data.Uint8List listBarsResponseDescriptor = $convert.base64Decode(
|
||||
'ChBMaXN0QmFyc1Jlc3BvbnNlEh8KBGJhcnMYASADKAsyCy5hbHQudjEuQmFyUgRiYXJzEicKBW'
|
||||
'Vycm9yGAIgASgLMhEuYWx0LnYxLkVycm9ySW5mb1IFZXJyb3I=');
|
||||
|
||||
@$core.Deprecated('Use importDailyBarsRequestDescriptor instead')
|
||||
const ImportDailyBarsRequest$json = {
|
||||
'1': 'ImportDailyBarsRequest',
|
||||
'2': [
|
||||
{'1': 'provider', '3': 1, '4': 1, '5': 9, '10': 'provider'},
|
||||
{'1': 'selector_kind', '3': 2, '4': 1, '5': 9, '10': 'selectorKind'},
|
||||
{
|
||||
'1': 'market',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.alt.v1.Market',
|
||||
'10': 'market'
|
||||
},
|
||||
{
|
||||
'1': 'venue',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 14,
|
||||
'6': '.alt.v1.Venue',
|
||||
'10': 'venue'
|
||||
},
|
||||
{'1': 'name', '3': 5, '4': 1, '5': 9, '10': 'name'},
|
||||
{'1': 'symbols', '3': 6, '4': 3, '5': 9, '10': 'symbols'},
|
||||
{'1': 'from_yyyymmdd', '3': 7, '4': 1, '5': 9, '10': 'fromYyyymmdd'},
|
||||
{'1': 'to_yyyymmdd', '3': 8, '4': 1, '5': 9, '10': 'toYyyymmdd'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ImportDailyBarsRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List importDailyBarsRequestDescriptor = $convert.base64Decode(
|
||||
'ChZJbXBvcnREYWlseUJhcnNSZXF1ZXN0EhoKCHByb3ZpZGVyGAEgASgJUghwcm92aWRlchIjCg'
|
||||
'1zZWxlY3Rvcl9raW5kGAIgASgJUgxzZWxlY3RvcktpbmQSJgoGbWFya2V0GAMgASgOMg4uYWx0'
|
||||
'LnYxLk1hcmtldFIGbWFya2V0EiMKBXZlbnVlGAQgASgOMg0uYWx0LnYxLlZlbnVlUgV2ZW51ZR'
|
||||
'ISCgRuYW1lGAUgASgJUgRuYW1lEhgKB3N5bWJvbHMYBiADKAlSB3N5bWJvbHMSIwoNZnJvbV95'
|
||||
'eXl5bW1kZBgHIAEoCVIMZnJvbVl5eXltbWRkEh8KC3RvX3l5eXltbWRkGAggASgJUgp0b1l5eX'
|
||||
'ltbWRk');
|
||||
|
||||
@$core.Deprecated('Use importDailyBarsResponseDescriptor instead')
|
||||
const ImportDailyBarsResponse$json = {
|
||||
'1': 'ImportDailyBarsResponse',
|
||||
'2': [
|
||||
{'1': 'provider', '3': 1, '4': 1, '5': 9, '10': 'provider'},
|
||||
{'1': 'instrument_count', '3': 2, '4': 1, '5': 5, '10': 'instrumentCount'},
|
||||
{'1': 'bar_count', '3': 3, '4': 1, '5': 5, '10': 'barCount'},
|
||||
{
|
||||
'1': 'error',
|
||||
'3': 4,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.alt.v1.ErrorInfo',
|
||||
'10': 'error'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ImportDailyBarsResponse`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List importDailyBarsResponseDescriptor = $convert.base64Decode(
|
||||
'ChdJbXBvcnREYWlseUJhcnNSZXNwb25zZRIaCghwcm92aWRlchgBIAEoCVIIcHJvdmlkZXISKQ'
|
||||
'oQaW5zdHJ1bWVudF9jb3VudBgCIAEoBVIPaW5zdHJ1bWVudENvdW50EhsKCWJhcl9jb3VudBgD'
|
||||
'IAEoBVIIYmFyQ291bnQSJwoFZXJyb3IYBCABKAsyES5hbHQudjEuRXJyb3JJbmZvUgVlcnJvcg'
|
||||
'==');
|
||||
|
|
|
|||
|
|
@ -533,6 +533,174 @@ func (x *ListBarsResponse) GetError() *ErrorInfo {
|
|||
return nil
|
||||
}
|
||||
|
||||
type ImportDailyBarsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"`
|
||||
SelectorKind string `protobuf:"bytes,2,opt,name=selector_kind,json=selectorKind,proto3" json:"selector_kind,omitempty"`
|
||||
Market Market `protobuf:"varint,3,opt,name=market,proto3,enum=alt.v1.Market" json:"market,omitempty"`
|
||||
Venue Venue `protobuf:"varint,4,opt,name=venue,proto3,enum=alt.v1.Venue" json:"venue,omitempty"`
|
||||
Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Symbols []string `protobuf:"bytes,6,rep,name=symbols,proto3" json:"symbols,omitempty"`
|
||||
FromYyyymmdd string `protobuf:"bytes,7,opt,name=from_yyyymmdd,json=fromYyyymmdd,proto3" json:"from_yyyymmdd,omitempty"`
|
||||
ToYyyymmdd string `protobuf:"bytes,8,opt,name=to_yyyymmdd,json=toYyyymmdd,proto3" json:"to_yyyymmdd,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) Reset() {
|
||||
*x = ImportDailyBarsRequest{}
|
||||
mi := &file_alt_v1_market_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ImportDailyBarsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ImportDailyBarsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_market_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 ImportDailyBarsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ImportDailyBarsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_market_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetProvider() string {
|
||||
if x != nil {
|
||||
return x.Provider
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetSelectorKind() string {
|
||||
if x != nil {
|
||||
return x.SelectorKind
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetMarket() Market {
|
||||
if x != nil {
|
||||
return x.Market
|
||||
}
|
||||
return Market_MARKET_UNSPECIFIED
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetVenue() Venue {
|
||||
if x != nil {
|
||||
return x.Venue
|
||||
}
|
||||
return Venue_VENUE_UNSPECIFIED
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetSymbols() []string {
|
||||
if x != nil {
|
||||
return x.Symbols
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetFromYyyymmdd() string {
|
||||
if x != nil {
|
||||
return x.FromYyyymmdd
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsRequest) GetToYyyymmdd() string {
|
||||
if x != nil {
|
||||
return x.ToYyyymmdd
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ImportDailyBarsResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"`
|
||||
InstrumentCount int32 `protobuf:"varint,2,opt,name=instrument_count,json=instrumentCount,proto3" json:"instrument_count,omitempty"`
|
||||
BarCount int32 `protobuf:"varint,3,opt,name=bar_count,json=barCount,proto3" json:"bar_count,omitempty"`
|
||||
Error *ErrorInfo `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsResponse) Reset() {
|
||||
*x = ImportDailyBarsResponse{}
|
||||
mi := &file_alt_v1_market_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ImportDailyBarsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ImportDailyBarsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_alt_v1_market_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 ImportDailyBarsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ImportDailyBarsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_alt_v1_market_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsResponse) GetProvider() string {
|
||||
if x != nil {
|
||||
return x.Provider
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsResponse) GetInstrumentCount() int32 {
|
||||
if x != nil {
|
||||
return x.InstrumentCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsResponse) GetBarCount() int32 {
|
||||
if x != nil {
|
||||
return x.BarCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ImportDailyBarsResponse) GetError() *ErrorInfo {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_alt_v1_market_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_alt_v1_market_proto_rawDesc = "" +
|
||||
|
|
@ -579,7 +747,22 @@ const file_alt_v1_market_proto_rawDesc = "" +
|
|||
"to_unix_ms\x18\x04 \x01(\x03R\btoUnixMs\"\\\n" +
|
||||
"\x10ListBarsResponse\x12\x1f\n" +
|
||||
"\x04bars\x18\x01 \x03(\v2\v.alt.v1.BarR\x04bars\x12'\n" +
|
||||
"\x05error\x18\x02 \x01(\v2\x11.alt.v1.ErrorInfoR\x05errorBCZAgit.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1;altv1b\x06proto3"
|
||||
"\x05error\x18\x02 \x01(\v2\x11.alt.v1.ErrorInfoR\x05error\"\x9a\x02\n" +
|
||||
"\x16ImportDailyBarsRequest\x12\x1a\n" +
|
||||
"\bprovider\x18\x01 \x01(\tR\bprovider\x12#\n" +
|
||||
"\rselector_kind\x18\x02 \x01(\tR\fselectorKind\x12&\n" +
|
||||
"\x06market\x18\x03 \x01(\x0e2\x0e.alt.v1.MarketR\x06market\x12#\n" +
|
||||
"\x05venue\x18\x04 \x01(\x0e2\r.alt.v1.VenueR\x05venue\x12\x12\n" +
|
||||
"\x04name\x18\x05 \x01(\tR\x04name\x12\x18\n" +
|
||||
"\asymbols\x18\x06 \x03(\tR\asymbols\x12#\n" +
|
||||
"\rfrom_yyyymmdd\x18\a \x01(\tR\ffromYyyymmdd\x12\x1f\n" +
|
||||
"\vto_yyyymmdd\x18\b \x01(\tR\n" +
|
||||
"toYyyymmdd\"\xa6\x01\n" +
|
||||
"\x17ImportDailyBarsResponse\x12\x1a\n" +
|
||||
"\bprovider\x18\x01 \x01(\tR\bprovider\x12)\n" +
|
||||
"\x10instrument_count\x18\x02 \x01(\x05R\x0finstrumentCount\x12\x1b\n" +
|
||||
"\tbar_count\x18\x03 \x01(\x05R\bbarCount\x12'\n" +
|
||||
"\x05error\x18\x04 \x01(\v2\x11.alt.v1.ErrorInfoR\x05errorBCZAgit.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1;altv1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_alt_v1_market_proto_rawDescOnce sync.Once
|
||||
|
|
@ -593,7 +776,7 @@ func file_alt_v1_market_proto_rawDescGZIP() []byte {
|
|||
return file_alt_v1_market_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_alt_v1_market_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_alt_v1_market_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_alt_v1_market_proto_goTypes = []any{
|
||||
(*Instrument)(nil), // 0: alt.v1.Instrument
|
||||
(*Price)(nil), // 1: alt.v1.Price
|
||||
|
|
@ -603,39 +786,44 @@ var file_alt_v1_market_proto_goTypes = []any{
|
|||
(*ListInstrumentsResponse)(nil), // 5: alt.v1.ListInstrumentsResponse
|
||||
(*ListBarsRequest)(nil), // 6: alt.v1.ListBarsRequest
|
||||
(*ListBarsResponse)(nil), // 7: alt.v1.ListBarsResponse
|
||||
nil, // 8: alt.v1.Instrument.ProviderSymbolsEntry
|
||||
(Market)(0), // 9: alt.v1.Market
|
||||
(Venue)(0), // 10: alt.v1.Venue
|
||||
(Currency)(0), // 11: alt.v1.Currency
|
||||
(*Decimal)(nil), // 12: alt.v1.Decimal
|
||||
(Timeframe)(0), // 13: alt.v1.Timeframe
|
||||
(*ErrorInfo)(nil), // 14: alt.v1.ErrorInfo
|
||||
(*ImportDailyBarsRequest)(nil), // 8: alt.v1.ImportDailyBarsRequest
|
||||
(*ImportDailyBarsResponse)(nil), // 9: alt.v1.ImportDailyBarsResponse
|
||||
nil, // 10: alt.v1.Instrument.ProviderSymbolsEntry
|
||||
(Market)(0), // 11: alt.v1.Market
|
||||
(Venue)(0), // 12: alt.v1.Venue
|
||||
(Currency)(0), // 13: alt.v1.Currency
|
||||
(*Decimal)(nil), // 14: alt.v1.Decimal
|
||||
(Timeframe)(0), // 15: alt.v1.Timeframe
|
||||
(*ErrorInfo)(nil), // 16: alt.v1.ErrorInfo
|
||||
}
|
||||
var file_alt_v1_market_proto_depIdxs = []int32{
|
||||
9, // 0: alt.v1.Instrument.market:type_name -> alt.v1.Market
|
||||
10, // 1: alt.v1.Instrument.venue:type_name -> alt.v1.Venue
|
||||
11, // 2: alt.v1.Instrument.currency:type_name -> alt.v1.Currency
|
||||
8, // 3: alt.v1.Instrument.provider_symbols:type_name -> alt.v1.Instrument.ProviderSymbolsEntry
|
||||
11, // 4: alt.v1.Price.currency:type_name -> alt.v1.Currency
|
||||
12, // 5: alt.v1.Price.amount:type_name -> alt.v1.Decimal
|
||||
12, // 6: alt.v1.Quantity.amount:type_name -> alt.v1.Decimal
|
||||
13, // 7: alt.v1.Bar.timeframe:type_name -> alt.v1.Timeframe
|
||||
11, // 0: alt.v1.Instrument.market:type_name -> alt.v1.Market
|
||||
12, // 1: alt.v1.Instrument.venue:type_name -> alt.v1.Venue
|
||||
13, // 2: alt.v1.Instrument.currency:type_name -> alt.v1.Currency
|
||||
10, // 3: alt.v1.Instrument.provider_symbols:type_name -> alt.v1.Instrument.ProviderSymbolsEntry
|
||||
13, // 4: alt.v1.Price.currency:type_name -> alt.v1.Currency
|
||||
14, // 5: alt.v1.Price.amount:type_name -> alt.v1.Decimal
|
||||
14, // 6: alt.v1.Quantity.amount:type_name -> alt.v1.Decimal
|
||||
15, // 7: alt.v1.Bar.timeframe:type_name -> alt.v1.Timeframe
|
||||
1, // 8: alt.v1.Bar.open:type_name -> alt.v1.Price
|
||||
1, // 9: alt.v1.Bar.high:type_name -> alt.v1.Price
|
||||
1, // 10: alt.v1.Bar.low:type_name -> alt.v1.Price
|
||||
1, // 11: alt.v1.Bar.close:type_name -> alt.v1.Price
|
||||
2, // 12: alt.v1.Bar.volume:type_name -> alt.v1.Quantity
|
||||
9, // 13: alt.v1.ListInstrumentsRequest.market:type_name -> alt.v1.Market
|
||||
11, // 13: alt.v1.ListInstrumentsRequest.market:type_name -> alt.v1.Market
|
||||
0, // 14: alt.v1.ListInstrumentsResponse.instruments:type_name -> alt.v1.Instrument
|
||||
14, // 15: alt.v1.ListInstrumentsResponse.error:type_name -> alt.v1.ErrorInfo
|
||||
13, // 16: alt.v1.ListBarsRequest.timeframe:type_name -> alt.v1.Timeframe
|
||||
16, // 15: alt.v1.ListInstrumentsResponse.error:type_name -> alt.v1.ErrorInfo
|
||||
15, // 16: alt.v1.ListBarsRequest.timeframe:type_name -> alt.v1.Timeframe
|
||||
3, // 17: alt.v1.ListBarsResponse.bars:type_name -> alt.v1.Bar
|
||||
14, // 18: alt.v1.ListBarsResponse.error:type_name -> alt.v1.ErrorInfo
|
||||
19, // [19:19] is the sub-list for method output_type
|
||||
19, // [19:19] is the sub-list for method input_type
|
||||
19, // [19:19] is the sub-list for extension type_name
|
||||
19, // [19:19] is the sub-list for extension extendee
|
||||
0, // [0:19] is the sub-list for field type_name
|
||||
16, // 18: alt.v1.ListBarsResponse.error:type_name -> alt.v1.ErrorInfo
|
||||
11, // 19: alt.v1.ImportDailyBarsRequest.market:type_name -> alt.v1.Market
|
||||
12, // 20: alt.v1.ImportDailyBarsRequest.venue:type_name -> alt.v1.Venue
|
||||
16, // 21: alt.v1.ImportDailyBarsResponse.error:type_name -> alt.v1.ErrorInfo
|
||||
22, // [22:22] is the sub-list for method output_type
|
||||
22, // [22:22] is the sub-list for method input_type
|
||||
22, // [22:22] is the sub-list for extension type_name
|
||||
22, // [22:22] is the sub-list for extension extendee
|
||||
0, // [0:22] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_alt_v1_market_proto_init() }
|
||||
|
|
@ -650,7 +838,7 @@ func file_alt_v1_market_proto_init() {
|
|||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_alt_v1_market_proto_rawDesc), len(file_alt_v1_market_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumMessages: 11,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,3 +57,21 @@ message ListBarsResponse {
|
|||
repeated Bar bars = 1;
|
||||
ErrorInfo error = 2;
|
||||
}
|
||||
|
||||
message ImportDailyBarsRequest {
|
||||
string provider = 1;
|
||||
string selector_kind = 2;
|
||||
Market market = 3;
|
||||
Venue venue = 4;
|
||||
string name = 5;
|
||||
repeated string symbols = 6;
|
||||
string from_yyyymmdd = 7;
|
||||
string to_yyyymmdd = 8;
|
||||
}
|
||||
|
||||
message ImportDailyBarsResponse {
|
||||
string provider = 1;
|
||||
int32 instrument_count = 2;
|
||||
int32 bar_count = 3;
|
||||
ErrorInfo error = 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ func messageFactories() []func() proto.Message {
|
|||
func() proto.Message { return &altv1.ListInstrumentsResponse{} },
|
||||
func() proto.Message { return &altv1.ListBarsRequest{} },
|
||||
func() proto.Message { return &altv1.ListBarsResponse{} },
|
||||
// market import command: daily bars
|
||||
func() proto.Message { return &altv1.ImportDailyBarsRequest{} },
|
||||
func() proto.Message { return &altv1.ImportDailyBarsResponse{} },
|
||||
// backtest surface: start / list / detail / result / compare
|
||||
func() proto.Message { return &altv1.StartBacktestRequest{} },
|
||||
func() proto.Message { return &altv1.StartBacktestResponse{} },
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ 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"
|
||||
)
|
||||
|
||||
// requiredAPIMessages is the milestone contract surface the API parser map must
|
||||
|
|
@ -22,6 +22,9 @@ func requiredAPIMessages() []proto.Message {
|
|||
&altv1.ListInstrumentsResponse{},
|
||||
&altv1.ListBarsRequest{},
|
||||
&altv1.ListBarsResponse{},
|
||||
// market import command: daily bars
|
||||
&altv1.ImportDailyBarsRequest{},
|
||||
&altv1.ImportDailyBarsResponse{},
|
||||
// backtest start / list / detail / result / compare
|
||||
&altv1.StartBacktestRequest{},
|
||||
&altv1.StartBacktestResponse{},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type fakeWorkerClient struct {
|
|||
compareReq *altv1.CompareBacktestRunsRequest
|
||||
instReq *altv1.ListInstrumentsRequest
|
||||
barsReq *altv1.ListBarsRequest
|
||||
importReq *altv1.ImportDailyBarsRequest
|
||||
|
||||
startRes *altv1.StartBacktestResponse
|
||||
listRes *altv1.ListBacktestRunsResponse
|
||||
|
|
@ -30,6 +31,7 @@ type fakeWorkerClient struct {
|
|||
compareRes *altv1.CompareBacktestRunsResponse
|
||||
instRes *altv1.ListInstrumentsResponse
|
||||
barsRes *altv1.ListBarsResponse
|
||||
importRes *altv1.ImportDailyBarsResponse
|
||||
|
||||
err error
|
||||
connectErr error
|
||||
|
|
@ -81,6 +83,10 @@ func (f *fakeWorkerClient) ListBars(ctx context.Context, req *altv1.ListBarsRequ
|
|||
f.barsReq = req
|
||||
return f.barsRes, f.err
|
||||
}
|
||||
func (f *fakeWorkerClient) ImportDailyBars(ctx context.Context, req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
|
||||
f.importReq = req
|
||||
return f.importRes, f.err
|
||||
}
|
||||
|
||||
func validStart() *altv1.StartBacktestRequest {
|
||||
return &altv1.StartBacktestRequest{Spec: &altv1.BacktestRunSpec{StrategyId: "strat-abc"}}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func handleHello(worker workerclient.WorkerClient, req *altv1.HelloRequest) (*al
|
|||
}
|
||||
|
||||
func capabilitiesForSession(worker workerclient.WorkerClient) []string {
|
||||
caps := []string{"hello", "request-response", "market-read", "backtest-read", "backtest-start", "worker-execution"}
|
||||
caps := []string{"hello", "request-response", "market-read", "market-import", "backtest-read", "backtest-start", "worker-execution"}
|
||||
if worker != nil && worker.IsConnected() {
|
||||
caps = append(caps, "worker-available")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package socket
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
||||
|
||||
|
|
@ -10,6 +11,11 @@ import (
|
|||
"git.toki-labs.com/toki/alt/services/api/internal/workerclient"
|
||||
)
|
||||
|
||||
// importDateLayout matches the YYYYMMDD date fields the import command carries.
|
||||
// The API only validates the shape so a malformed date is rejected before a
|
||||
// worker connection is opened.
|
||||
const importDateLayout = "20060102"
|
||||
|
||||
const (
|
||||
marketErrorInvalidRequest = "invalid_request"
|
||||
marketErrorUnavailable = "unavailable"
|
||||
|
|
@ -37,9 +43,61 @@ func marketHandlers(worker workerclient.WorkerClient) []sessionHandler {
|
|||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
requestType: protoSocket.TypeNameOf(&altv1.ImportDailyBarsRequest{}),
|
||||
register: func(client *protoSocket.WsClient) {
|
||||
protoSocket.AddRequestListenerTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](&client.Communicator, func(req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
|
||||
return handleImportDailyBars(worker, req)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// handleImportDailyBars validates the import command shape, then forwards it to
|
||||
// the worker which owns provider fetch and persistence. The API stays a thin
|
||||
// control plane: it rejects malformed commands before opening a worker
|
||||
// connection and never runs the import itself.
|
||||
func handleImportDailyBars(worker workerclient.WorkerClient, req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
|
||||
if req.GetProvider() == "" {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("provider is required")}, nil
|
||||
}
|
||||
if req.GetSelectorKind() == "" {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("selector_kind is required")}, nil
|
||||
}
|
||||
if len(req.GetSymbols()) == 0 {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("symbols is required")}, nil
|
||||
}
|
||||
if !isValidMarketFilter(req.GetMarket()) {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("market is unsupported")}, nil
|
||||
}
|
||||
if !isValidVenueFilter(req.GetVenue()) {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("venue is unsupported")}, nil
|
||||
}
|
||||
if err := validateImportDateRange(req.GetFromYyyymmdd(), req.GetToYyyymmdd()); err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
||||
}
|
||||
if worker == nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketWorkerUnavailable()}, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := worker.Connect(ctx); err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketWorkerErrorInfo(err)}, nil
|
||||
}
|
||||
|
||||
res, err := worker.ImportDailyBars(ctx, req)
|
||||
if err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketWorkerErrorInfo(err)}, nil
|
||||
}
|
||||
if res == nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInternalError("worker returned no import daily bars response")}, nil
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func handleListInstruments(worker workerclient.WorkerClient, req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error) {
|
||||
if !isValidMarketFilter(req.GetMarket()) {
|
||||
return &altv1.ListInstrumentsResponse{Error: marketInvalidRequest("market filter is unsupported")}, nil
|
||||
|
|
@ -111,6 +169,42 @@ func isValidMarketFilter(m altv1.Market) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// isValidVenueFilter accepts the unspecified venue (the import selector may not
|
||||
// pin a venue) and the known venues, rejecting only out-of-range enum values.
|
||||
func isValidVenueFilter(v altv1.Venue) bool {
|
||||
switch v {
|
||||
case altv1.Venue_VENUE_UNSPECIFIED, altv1.Venue_VENUE_KRX, altv1.Venue_VENUE_NASDAQ, altv1.Venue_VENUE_NYSE:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// validateImportDateRange requires both endpoints of the import date range. The
|
||||
// live KIS importer requires a bounded from/to range and rejects an inverted
|
||||
// range, so the API closes those operator-input errors here as typed
|
||||
// invalid_request instead of letting them surface as worker-internal failures.
|
||||
func validateImportDateRange(fromValue, toValue string) error {
|
||||
if fromValue == "" {
|
||||
return errors.New("from_yyyymmdd is required")
|
||||
}
|
||||
if toValue == "" {
|
||||
return errors.New("to_yyyymmdd is required")
|
||||
}
|
||||
from, err := time.Parse(importDateLayout, fromValue)
|
||||
if err != nil {
|
||||
return errors.New("from_yyyymmdd must be YYYYMMDD")
|
||||
}
|
||||
to, err := time.Parse(importDateLayout, toValue)
|
||||
if err != nil {
|
||||
return errors.New("to_yyyymmdd must be YYYYMMDD")
|
||||
}
|
||||
if from.After(to) {
|
||||
return errors.New("from_yyyymmdd cannot be after to_yyyymmdd")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidTimeframe(t altv1.Timeframe) bool {
|
||||
switch t {
|
||||
case altv1.Timeframe_TIMEFRAME_DAILY, altv1.Timeframe_TIMEFRAME_MINUTE_1, altv1.Timeframe_TIMEFRAME_MINUTE_5:
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ func TestMarketHandlersRegisteredInSession(t *testing.T) {
|
|||
required := []string{
|
||||
protoSocket.TypeNameOf(&altv1.ListInstrumentsRequest{}),
|
||||
protoSocket.TypeNameOf(&altv1.ListBarsRequest{}),
|
||||
protoSocket.TypeNameOf(&altv1.ImportDailyBarsRequest{}),
|
||||
}
|
||||
for _, req := range required {
|
||||
if !registered[req] {
|
||||
|
|
@ -211,6 +212,162 @@ func TestMarketHandlersRegisteredInSession(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func validImportRequest() *altv1.ImportDailyBarsRequest {
|
||||
return &altv1.ImportDailyBarsRequest{
|
||||
Provider: "kis",
|
||||
SelectorKind: "watchlist",
|
||||
Market: altv1.Market_MARKET_KR,
|
||||
Venue: altv1.Venue_VENUE_KRX,
|
||||
Symbols: []string{"005930"},
|
||||
FromYyyymmdd: "20260501",
|
||||
ToYyyymmdd: "20260515",
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsForwards(t *testing.T) {
|
||||
fake := &fakeWorkerClient{importRes: &altv1.ImportDailyBarsResponse{
|
||||
Provider: "kis", InstrumentCount: 1, BarCount: 10,
|
||||
}}
|
||||
|
||||
resp, err := handleImportDailyBars(fake, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if fake.importReq == nil {
|
||||
t.Fatal("expected request to be forwarded to worker")
|
||||
}
|
||||
if fake.importReq.GetProvider() != "kis" || len(fake.importReq.GetSymbols()) != 1 {
|
||||
t.Errorf("request was not forwarded unchanged: %+v", fake.importReq)
|
||||
}
|
||||
if resp.GetInstrumentCount() != 1 || resp.GetBarCount() != 10 {
|
||||
t.Errorf("unexpected response counts: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsValidation(t *testing.T) {
|
||||
// withImport starts from a fully valid request and mutates one field so each
|
||||
// case isolates a single validation failure.
|
||||
withImport := func(mut func(*altv1.ImportDailyBarsRequest)) *altv1.ImportDailyBarsRequest {
|
||||
req := validImportRequest()
|
||||
mut(req)
|
||||
return req
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
req *altv1.ImportDailyBarsRequest
|
||||
}{
|
||||
{"missing provider", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Provider = "" })},
|
||||
{"missing selector kind", withImport(func(r *altv1.ImportDailyBarsRequest) { r.SelectorKind = "" })},
|
||||
{"missing symbols", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Symbols = nil })},
|
||||
{"unsupported market", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Market = altv1.Market(99) })},
|
||||
{"unsupported venue", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Venue = altv1.Venue(99) })},
|
||||
{"missing from date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.FromYyyymmdd = "" })},
|
||||
{"missing to date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.ToYyyymmdd = "" })},
|
||||
{"malformed from date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.FromYyyymmdd = "2026-05-01" })},
|
||||
{"malformed to date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.ToYyyymmdd = "not-a-date" })},
|
||||
{"reversed range", withImport(func(r *altv1.ImportDailyBarsRequest) {
|
||||
r.FromYyyymmdd, r.ToYyyymmdd = r.GetToYyyymmdd(), r.GetFromYyyymmdd()
|
||||
})},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fake := &fakeWorkerClient{}
|
||||
|
||||
resp, err := handleImportDailyBars(fake, tt.req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed validation response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorInvalidRequest)
|
||||
if fake.importReq != nil {
|
||||
t.Error("worker must not be called when validation fails")
|
||||
}
|
||||
if fake.connectCount != 0 {
|
||||
t.Errorf("worker must not be connected when validation fails, got connectCount=%d", fake.connectCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsNilWorker(t *testing.T) {
|
||||
resp, err := handleImportDailyBars(nil, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed unavailable response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorUnavailable)
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsNilWorkerResponse(t *testing.T) {
|
||||
fake := &fakeWorkerClient{}
|
||||
|
||||
resp, err := handleImportDailyBars(fake, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed internal response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorInternal)
|
||||
if fake.importReq == nil {
|
||||
t.Error("expected request to be forwarded before nil response was detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsMapsWorkerErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
code string
|
||||
}{
|
||||
{"unavailable", workerclient.ErrUnavailable, marketErrorUnavailable},
|
||||
{"timeout", workerclient.ErrTimeout, marketErrorTimeout},
|
||||
{"unexpected", context.Canceled, marketErrorInternal},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fake := &fakeWorkerClient{err: tt.err}
|
||||
|
||||
resp, err := handleImportDailyBars(fake, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed worker error response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), tt.code)
|
||||
if fake.importReq == nil {
|
||||
t.Error("valid request should be forwarded before worker error is mapped")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBars_ConnectFailure(t *testing.T) {
|
||||
fake := &fakeWorkerClient{connectErr: workerclient.ErrUnavailable}
|
||||
|
||||
resp, err := handleImportDailyBars(fake, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed unavailable response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorUnavailable)
|
||||
if fake.importReq != nil {
|
||||
t.Error("expected request not to be forwarded to worker on connect failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportSocketValidationReturnsTypedErrors(t *testing.T) {
|
||||
fake := &fakeWorkerClient{}
|
||||
client := startBacktestAPITestClient(t, fake)
|
||||
|
||||
resp, err := protoSocket.SendRequestTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](
|
||||
&client.Communicator,
|
||||
&altv1.ImportDailyBarsRequest{},
|
||||
500*time.Millisecond,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("request should return typed error response without timeout: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorInvalidRequest)
|
||||
if fake.importReq != nil {
|
||||
t.Fatal("worker must not be called for socket-level validation failures")
|
||||
}
|
||||
}
|
||||
|
||||
func requireMarketError(t *testing.T, errInfo *altv1.ErrorInfo, code string) {
|
||||
t.Helper()
|
||||
if errInfo == nil {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ func TestServerRespondsToHelloRequest(t *testing.T) {
|
|||
"hello": true,
|
||||
"request-response": true,
|
||||
"market-read": true,
|
||||
"market-import": true,
|
||||
"backtest-read": true,
|
||||
"backtest-start": true,
|
||||
"worker-execution": true,
|
||||
|
|
@ -123,6 +124,7 @@ func TestServerRespondsToHelloRequest_WorkerAvailable(t *testing.T) {
|
|||
"hello": true,
|
||||
"request-response": true,
|
||||
"market-read": true,
|
||||
"market-import": true,
|
||||
"backtest-read": true,
|
||||
"backtest-start": true,
|
||||
"worker-execution": true,
|
||||
|
|
@ -315,6 +317,11 @@ func TestCapabilitiesAndHandlersSync(t *testing.T) {
|
|||
t.Error("market-read capability missing although ListInstruments/ListBars handlers are registered")
|
||||
}
|
||||
|
||||
// 1b. Check ImportDailyBars -> market-import
|
||||
if registered[protoSocket.TypeNameOf(&altv1.ImportDailyBarsRequest{})] && !capSet["market-import"] {
|
||||
t.Error("market-import capability missing although ImportDailyBars handler is registered")
|
||||
}
|
||||
|
||||
// 2. Check backtest queries -> backtest-read
|
||||
hasBacktestReadRequest := registered[protoSocket.TypeNameOf(&altv1.ListBacktestRunsRequest{})] &&
|
||||
registered[protoSocket.TypeNameOf(&altv1.GetBacktestRunDetailRequest{})] &&
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ type WorkerClient interface {
|
|||
// reads to the worker-owned storage boundary.
|
||||
ListInstruments(ctx context.Context, req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error)
|
||||
ListBars(ctx context.Context, req *altv1.ListBarsRequest) (*altv1.ListBarsResponse, error)
|
||||
|
||||
// Market import command surface. Import execution runs inside the worker; the
|
||||
// API only validates shape and forwards the command.
|
||||
ImportDailyBars(ctx context.Context, req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error)
|
||||
}
|
||||
|
||||
type socketClient struct {
|
||||
|
|
@ -152,6 +156,10 @@ func (c *socketClient) ListBars(ctx context.Context, req *altv1.ListBarsRequest)
|
|||
return sendTyped[*altv1.ListBarsRequest, *altv1.ListBarsResponse](c, ctx, req)
|
||||
}
|
||||
|
||||
func (c *socketClient) ImportDailyBars(ctx context.Context, req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
|
||||
return sendTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](c, ctx, req)
|
||||
}
|
||||
|
||||
// sendTyped is the shared request path for every worker call. It centralises
|
||||
// context-cancellation, deadline-derived timeouts, and the unavailable/timeout
|
||||
// error mapping so each new request method stays a one-line forwarder and the
|
||||
|
|
|
|||
|
|
@ -194,6 +194,56 @@ func TestWorkerClient_ListBars_Success(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWorkerClient_ImportDailyBars_Success(t *testing.T) {
|
||||
port, cleanup := startFakeWorker(t, func(client *protoSocket.WsClient) {
|
||||
protoSocket.AddRequestListenerTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](&client.Communicator, func(req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
|
||||
return &altv1.ImportDailyBarsResponse{
|
||||
Provider: req.GetProvider(),
|
||||
InstrumentCount: int32(len(req.GetSymbols())),
|
||||
BarCount: 2,
|
||||
}, nil
|
||||
})
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
client := New(fmt.Sprintf("ws://127.0.0.1:%d/socket", port))
|
||||
ctx := context.Background()
|
||||
if err := client.Connect(ctx); err != nil {
|
||||
t.Fatalf("failed to connect: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
res, err := client.ImportDailyBars(ctx, &altv1.ImportDailyBarsRequest{
|
||||
Provider: "kis",
|
||||
SelectorKind: "watchlist",
|
||||
Market: altv1.Market_MARKET_KR,
|
||||
Venue: altv1.Venue_VENUE_KRX,
|
||||
Symbols: []string{"005930"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ImportDailyBars failed: %v", err)
|
||||
}
|
||||
if res.GetProvider() != "kis" {
|
||||
t.Errorf("provider did not round-trip, got %q", res.GetProvider())
|
||||
}
|
||||
if res.GetInstrumentCount() != 1 || res.GetBarCount() != 2 {
|
||||
t.Errorf("counts did not round-trip, got instruments=%d bars=%d", res.GetInstrumentCount(), res.GetBarCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerClient_ImportDailyBars_Unavailable(t *testing.T) {
|
||||
// A never-connected client must report ErrUnavailable instead of blocking.
|
||||
client := New("ws://127.0.0.1:54321/socket")
|
||||
_, err := client.ImportDailyBars(context.Background(), &altv1.ImportDailyBarsRequest{
|
||||
Provider: "kis",
|
||||
SelectorKind: "watchlist",
|
||||
Symbols: []string{"005930"},
|
||||
})
|
||||
if !errors.Is(err, ErrUnavailable) {
|
||||
t.Errorf("expected ErrUnavailable, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerClient_StartBacktest_Unavailable(t *testing.T) {
|
||||
// A client that was never connected must report ErrUnavailable rather than
|
||||
// panicking or blocking, exercising the shared sendTyped guard.
|
||||
|
|
|
|||
|
|
@ -38,12 +38,17 @@ func main() {
|
|||
defer pool.Close()
|
||||
store := postgres.NewStore(pool)
|
||||
kisProvider := kis.NewLiveProvider(kis.NewClient(kis.ConfigFromEnv()))
|
||||
jobs.RegisterDailyBarImportHandler(runner, market.ProviderKIS, importer.New(kisProvider, store))
|
||||
// One importer instance backs both the async job handler and the socket
|
||||
// import command so the operator-facing command and scheduled jobs share
|
||||
// the same provider/store wiring.
|
||||
kisImporter := importer.New(kisProvider, store)
|
||||
jobs.RegisterDailyBarImportHandler(runner, market.ProviderKIS, kisImporter)
|
||||
deps = socket.Deps{
|
||||
Analysis: store,
|
||||
Results: store,
|
||||
Instruments: store,
|
||||
Bars: store,
|
||||
Analysis: store,
|
||||
Results: store,
|
||||
Instruments: store,
|
||||
Bars: store,
|
||||
DailyBarImporter: kisImporter,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +65,7 @@ func main() {
|
|||
"backtest_read_surface", deps.Analysis != nil,
|
||||
"backtest_start_surface", deps.Starter != nil,
|
||||
"market_read_surface", deps.Instruments != nil && deps.Bars != nil,
|
||||
"market_import_surface", deps.DailyBarImporter != nil,
|
||||
"addr", fmt.Sprintf("%s:%d%s", cfg.Host, cfg.Port, cfg.SocketPath),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ func messageFactories() []func() proto.Message {
|
|||
func() proto.Message { return &altv1.ListInstrumentsResponse{} },
|
||||
func() proto.Message { return &altv1.ListBarsRequest{} },
|
||||
func() proto.Message { return &altv1.ListBarsResponse{} },
|
||||
// market import command: daily bars
|
||||
func() proto.Message { return &altv1.ImportDailyBarsRequest{} },
|
||||
func() proto.Message { return &altv1.ImportDailyBarsResponse{} },
|
||||
// backtest surface: start / list / detail / result / compare
|
||||
func() proto.Message { return &altv1.StartBacktestRequest{} },
|
||||
func() proto.Message { return &altv1.StartBacktestResponse{} },
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ func requiredWorkerMessages() []proto.Message {
|
|||
&altv1.ListInstrumentsResponse{},
|
||||
&altv1.ListBarsRequest{},
|
||||
&altv1.ListBarsResponse{},
|
||||
// market import command: daily bars
|
||||
&altv1.ImportDailyBarsRequest{},
|
||||
&altv1.ImportDailyBarsResponse{},
|
||||
// backtest start / list / detail / result / compare
|
||||
&altv1.StartBacktestRequest{},
|
||||
&altv1.StartBacktestResponse{},
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
||||
"git.toki-labs.com/toki/alt/packages/domain/backtest"
|
||||
"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
|
||||
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
|
||||
)
|
||||
|
||||
|
|
@ -32,15 +33,23 @@ type BacktestStarter interface {
|
|||
StartBacktest(ctx context.Context, spec backtest.RunSpec) (backtest.Run, error)
|
||||
}
|
||||
|
||||
// DailyBarImporter runs a daily bar import command. The marketdata importer
|
||||
// provides the concrete implementation; the narrow interface keeps the socket
|
||||
// handler testable with a fake and decoupled from provider/storage wiring.
|
||||
type DailyBarImporter interface {
|
||||
ImportDailyBars(ctx context.Context, request importer.DailyBarRequest) (importer.Result, error)
|
||||
}
|
||||
|
||||
// Deps bundles the worker-owned capabilities socket handlers need. A nil field
|
||||
// disables only its related handler surface so the server can still run a
|
||||
// reduced surface (e.g. hello-only) without panicking.
|
||||
type Deps struct {
|
||||
Starter BacktestStarter
|
||||
Analysis storage.BacktestAnalysisStore
|
||||
Results storage.BacktestResultStore
|
||||
Instruments storage.InstrumentStore
|
||||
Bars storage.BarStore
|
||||
Starter BacktestStarter
|
||||
Analysis storage.BacktestAnalysisStore
|
||||
Results storage.BacktestResultStore
|
||||
Instruments storage.InstrumentStore
|
||||
Bars storage.BarStore
|
||||
DailyBarImporter DailyBarImporter
|
||||
}
|
||||
|
||||
// BacktestDeps is retained for existing call sites while the socket dependency
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ func capabilitiesForDeps(deps Deps) []string {
|
|||
if deps.Instruments != nil && deps.Bars != nil {
|
||||
caps = append(caps, "market-read")
|
||||
}
|
||||
if deps.DailyBarImporter != nil {
|
||||
caps = append(caps, "market-import")
|
||||
}
|
||||
if deps.Analysis != nil && deps.Results != nil {
|
||||
caps = append(caps, "backtest-read")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,14 @@ import (
|
|||
|
||||
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
||||
"git.toki-labs.com/toki/alt/packages/domain/market"
|
||||
"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
|
||||
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
|
||||
)
|
||||
|
||||
// importDateLayout matches the YYYYMMDD date fields the import command carries,
|
||||
// mirroring the job payload date contract so the same dates reach the provider.
|
||||
const importDateLayout = "20060102"
|
||||
|
||||
const (
|
||||
marketErrorInvalidRequest = "invalid_request"
|
||||
marketErrorUnavailable = "unavailable"
|
||||
|
|
@ -44,9 +49,100 @@ func marketHandlers(deps Deps) []sessionHandler {
|
|||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
requestType: protoSocket.TypeNameOf(&altv1.ImportDailyBarsRequest{}),
|
||||
register: func(client *protoSocket.WsClient) {
|
||||
protoSocket.AddRequestListenerTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](
|
||||
&client.Communicator,
|
||||
func(req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
|
||||
return handleImportDailyBars(deps, req)
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// handleImportDailyBars validates the import command, converts it into the
|
||||
// importer request, and runs the import. The worker owns provider fetch and
|
||||
// persistence; counts come back so the operator surface can confirm what landed.
|
||||
func handleImportDailyBars(deps Deps, req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
|
||||
if req.GetProvider() == "" {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("provider is required")}, nil
|
||||
}
|
||||
if req.GetSelectorKind() == "" {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("selector_kind is required")}, nil
|
||||
}
|
||||
if len(req.GetSymbols()) == 0 {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("symbols is required")}, nil
|
||||
}
|
||||
mkt, _, err := marketFilterFromProto(req.GetMarket())
|
||||
if err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
||||
}
|
||||
venue, err := venueFromProto(req.GetVenue())
|
||||
if err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
||||
}
|
||||
from, to, err := parseImportDateRange(req.GetFromYyyymmdd(), req.GetToYyyymmdd())
|
||||
if err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
|
||||
}
|
||||
if deps.DailyBarImporter == nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketUnavailableError("daily bar import is not available")}, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
||||
defer cancel()
|
||||
|
||||
result, err := deps.DailyBarImporter.ImportDailyBars(ctx, importer.DailyBarRequest{
|
||||
Provider: market.Provider(req.GetProvider()),
|
||||
Selector: market.UniverseSelector{
|
||||
Kind: market.UniverseSelectorKind(req.GetSelectorKind()),
|
||||
Market: mkt,
|
||||
Venue: venue,
|
||||
Symbols: req.GetSymbols(),
|
||||
Name: req.GetName(),
|
||||
},
|
||||
From: from,
|
||||
To: to,
|
||||
})
|
||||
if err != nil {
|
||||
return &altv1.ImportDailyBarsResponse{Error: marketBackendErrorInfo(err)}, nil
|
||||
}
|
||||
return &altv1.ImportDailyBarsResponse{
|
||||
Provider: req.GetProvider(),
|
||||
InstrumentCount: int32(result.Instruments),
|
||||
BarCount: int32(result.Bars),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseImportDateRange requires both endpoints of the import date range and
|
||||
// returns them parsed for the importer request. The live KIS importer requires
|
||||
// a bounded from/to range and rejects an inverted range, so the worker closes
|
||||
// those operator-input errors here as invalid_request before reaching the
|
||||
// importer instead of letting them surface as an internal backend failure.
|
||||
func parseImportDateRange(fromValue, toValue string) (time.Time, time.Time, error) {
|
||||
if fromValue == "" {
|
||||
return time.Time{}, time.Time{}, errors.New("from_yyyymmdd is required")
|
||||
}
|
||||
if toValue == "" {
|
||||
return time.Time{}, time.Time{}, errors.New("to_yyyymmdd is required")
|
||||
}
|
||||
from, err := time.Parse(importDateLayout, fromValue)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, errors.New("from_yyyymmdd must be YYYYMMDD")
|
||||
}
|
||||
to, err := time.Parse(importDateLayout, toValue)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, errors.New("to_yyyymmdd must be YYYYMMDD")
|
||||
}
|
||||
if from.After(to) {
|
||||
return time.Time{}, time.Time{}, errors.New("from_yyyymmdd cannot be after to_yyyymmdd")
|
||||
}
|
||||
return from.UTC(), to.UTC(), nil
|
||||
}
|
||||
|
||||
func handleListInstruments(deps Deps, req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error) {
|
||||
marketFilter, hasMarketFilter, err := marketFilterFromProto(req.GetMarket())
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package socket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
||||
"git.toki-labs.com/toki/alt/packages/domain/market"
|
||||
)
|
||||
|
|
@ -46,6 +48,24 @@ func barsToProto(bars []market.Bar) []*altv1.Bar {
|
|||
return out
|
||||
}
|
||||
|
||||
// venueFromProto converts a proto venue to the domain venue. The unspecified
|
||||
// venue maps to an empty domain venue because an import selector may not pin a
|
||||
// venue (e.g. a watchlist selector); only out-of-range enum values are an error.
|
||||
func venueFromProto(v altv1.Venue) (market.Venue, error) {
|
||||
switch v {
|
||||
case altv1.Venue_VENUE_UNSPECIFIED:
|
||||
return "", nil
|
||||
case altv1.Venue_VENUE_KRX:
|
||||
return market.VenueKRX, nil
|
||||
case altv1.Venue_VENUE_NASDAQ:
|
||||
return market.VenueNASDAQ, nil
|
||||
case altv1.Venue_VENUE_NYSE:
|
||||
return market.VenueNYSE, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported venue %q", v.String())
|
||||
}
|
||||
}
|
||||
|
||||
func venueToProto(v market.Venue) altv1.Venue {
|
||||
switch v {
|
||||
case market.VenueKRX:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
||||
"git.toki-labs.com/toki/alt/packages/domain/market"
|
||||
"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
|
||||
"git.toki-labs.com/toki/alt/services/worker/internal/storage"
|
||||
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
||||
)
|
||||
|
|
@ -50,6 +51,32 @@ func (f *fakeBarStore) GetBars(ctx context.Context, id market.InstrumentID, time
|
|||
return f.bars, f.err
|
||||
}
|
||||
|
||||
type fakeDailyBarImporter struct {
|
||||
gotReq importer.DailyBarRequest
|
||||
called bool
|
||||
result importer.Result
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeDailyBarImporter) ImportDailyBars(ctx context.Context, request importer.DailyBarRequest) (importer.Result, error) {
|
||||
f.called = true
|
||||
f.gotReq = request
|
||||
return f.result, f.err
|
||||
}
|
||||
|
||||
func validImportRequest() *altv1.ImportDailyBarsRequest {
|
||||
return &altv1.ImportDailyBarsRequest{
|
||||
Provider: "kis",
|
||||
SelectorKind: "watchlist",
|
||||
Market: altv1.Market_MARKET_KR,
|
||||
Venue: altv1.Venue_VENUE_KRX,
|
||||
Name: "my-watchlist",
|
||||
Symbols: []string{"005930", "000660"},
|
||||
FromYyyymmdd: "20260501",
|
||||
ToYyyymmdd: "20260515",
|
||||
}
|
||||
}
|
||||
|
||||
func validListBarsRequest() *altv1.ListBarsRequest {
|
||||
return &altv1.ListBarsRequest{
|
||||
InstrumentId: "KRX:005930",
|
||||
|
|
@ -192,6 +219,133 @@ func TestHandleListBarsNotFound(t *testing.T) {
|
|||
requireMarketError(t, resp.GetError(), marketErrorNotFound)
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsSuccess(t *testing.T) {
|
||||
imp := &fakeDailyBarImporter{result: importer.Result{Instruments: 2, Bars: 20}}
|
||||
|
||||
resp, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp.GetError() != nil {
|
||||
t.Fatalf("unexpected import error: %+v", resp.GetError())
|
||||
}
|
||||
if !imp.called {
|
||||
t.Fatal("expected importer to be called")
|
||||
}
|
||||
if resp.GetProvider() != "kis" || resp.GetInstrumentCount() != 2 || resp.GetBarCount() != 20 {
|
||||
t.Errorf("unexpected response: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsConvertsRequest(t *testing.T) {
|
||||
imp := &fakeDailyBarImporter{}
|
||||
|
||||
if _, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, validImportRequest()); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := imp.gotReq
|
||||
if got.Provider != market.ProviderKIS {
|
||||
t.Errorf("provider mismatch: %q", got.Provider)
|
||||
}
|
||||
if got.Selector.Kind != market.UniverseSelectorWatchlist {
|
||||
t.Errorf("selector kind mismatch: %q", got.Selector.Kind)
|
||||
}
|
||||
if got.Selector.Market != market.MarketKR || got.Selector.Venue != market.VenueKRX {
|
||||
t.Errorf("selector market/venue mismatch: %q/%q", got.Selector.Market, got.Selector.Venue)
|
||||
}
|
||||
if got.Selector.Name != "my-watchlist" || len(got.Selector.Symbols) != 2 {
|
||||
t.Errorf("selector name/symbols mismatch: %+v", got.Selector)
|
||||
}
|
||||
wantFrom := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
wantTo := time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC)
|
||||
if !got.From.Equal(wantFrom) || !got.To.Equal(wantTo) {
|
||||
t.Errorf("date range mismatch: got %v..%v", got.From, got.To)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsValidation(t *testing.T) {
|
||||
// withImport starts from a fully valid request and mutates one field so each
|
||||
// case isolates a single validation failure.
|
||||
withImport := func(mut func(*altv1.ImportDailyBarsRequest)) *altv1.ImportDailyBarsRequest {
|
||||
req := validImportRequest()
|
||||
mut(req)
|
||||
return req
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
req *altv1.ImportDailyBarsRequest
|
||||
}{
|
||||
{"missing provider", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Provider = "" })},
|
||||
{"missing selector kind", withImport(func(r *altv1.ImportDailyBarsRequest) { r.SelectorKind = "" })},
|
||||
{"missing symbols", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Symbols = nil })},
|
||||
{"unsupported market", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Market = altv1.Market(99) })},
|
||||
{"unsupported venue", withImport(func(r *altv1.ImportDailyBarsRequest) { r.Venue = altv1.Venue(99) })},
|
||||
{"missing from date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.FromYyyymmdd = "" })},
|
||||
{"missing to date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.ToYyyymmdd = "" })},
|
||||
{"malformed from date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.FromYyyymmdd = "2026-05-01" })},
|
||||
{"malformed to date", withImport(func(r *altv1.ImportDailyBarsRequest) { r.ToYyyymmdd = "nope" })},
|
||||
{"reversed range", withImport(func(r *altv1.ImportDailyBarsRequest) {
|
||||
r.FromYyyymmdd, r.ToYyyymmdd = r.GetToYyyymmdd(), r.GetFromYyyymmdd()
|
||||
})},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
imp := &fakeDailyBarImporter{}
|
||||
resp, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, tt.req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed validation response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorInvalidRequest)
|
||||
if imp.called {
|
||||
t.Error("importer must not be called when validation fails")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsUnavailable(t *testing.T) {
|
||||
resp, err := handleImportDailyBars(Deps{}, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed unavailable response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorUnavailable)
|
||||
}
|
||||
|
||||
func TestHandleImportDailyBarsImporterError(t *testing.T) {
|
||||
imp := &fakeDailyBarImporter{err: errors.New("provider boom")}
|
||||
|
||||
resp, err := handleImportDailyBars(Deps{DailyBarImporter: imp}, validImportRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("expected typed internal response, got error: %v", err)
|
||||
}
|
||||
requireMarketError(t, resp.GetError(), marketErrorInternal)
|
||||
}
|
||||
|
||||
func TestWorkerImportSocketRoundTrip(t *testing.T) {
|
||||
imp := &fakeDailyBarImporter{result: importer.Result{Instruments: 1, Bars: 5}}
|
||||
client := startBacktestWorkerTestClient(t, Deps{DailyBarImporter: imp})
|
||||
|
||||
resp, err := protoSocket.SendRequestTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](
|
||||
&client.Communicator,
|
||||
validImportRequest(),
|
||||
500*time.Millisecond,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("request should return import response without timeout: %v", err)
|
||||
}
|
||||
if resp.GetError() != nil {
|
||||
t.Fatalf("unexpected import error: %+v", resp.GetError())
|
||||
}
|
||||
if resp.GetInstrumentCount() != 1 || resp.GetBarCount() != 5 {
|
||||
t.Fatalf("unexpected counts: %+v", resp)
|
||||
}
|
||||
if !imp.called {
|
||||
t.Fatal("expected importer to be called through socket")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarketHandlersCoverAllRequests(t *testing.T) {
|
||||
registered := make(map[string]bool)
|
||||
for _, h := range marketHandlers(Deps{}) {
|
||||
|
|
@ -207,6 +361,7 @@ func TestMarketHandlersCoverAllRequests(t *testing.T) {
|
|||
required := []string{
|
||||
"alt.v1.ListInstrumentsRequest",
|
||||
"alt.v1.ListBarsRequest",
|
||||
"alt.v1.ImportDailyBarsRequest",
|
||||
}
|
||||
for _, req := range required {
|
||||
if !registered[req] {
|
||||
|
|
|
|||
|
|
@ -131,6 +131,13 @@ func TestWorkerHelloCapabilitiesReflectDeps(t *testing.T) {
|
|||
},
|
||||
expected: []string{"hello", "market-read"},
|
||||
},
|
||||
{
|
||||
name: "import deps",
|
||||
deps: Deps{
|
||||
DailyBarImporter: &fakeDailyBarImporter{},
|
||||
},
|
||||
expected: []string{"hello", "market-import"},
|
||||
},
|
||||
{
|
||||
name: "backtest read deps",
|
||||
deps: Deps{
|
||||
|
|
@ -149,13 +156,14 @@ func TestWorkerHelloCapabilitiesReflectDeps(t *testing.T) {
|
|||
{
|
||||
name: "all deps",
|
||||
deps: Deps{
|
||||
Instruments: &fakeInstrumentStore{},
|
||||
Bars: &fakeBarStore{},
|
||||
Analysis: &fakeAnalysisStore{},
|
||||
Results: &fakeResultStore{},
|
||||
Starter: &fakeStarter{},
|
||||
Instruments: &fakeInstrumentStore{},
|
||||
Bars: &fakeBarStore{},
|
||||
Analysis: &fakeAnalysisStore{},
|
||||
Results: &fakeResultStore{},
|
||||
Starter: &fakeStarter{},
|
||||
DailyBarImporter: &fakeDailyBarImporter{},
|
||||
},
|
||||
expected: []string{"hello", "market-read", "backtest-read", "backtest-start", "worker-execution"},
|
||||
expected: []string{"hello", "market-read", "market-import", "backtest-read", "backtest-start", "worker-execution"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue