feat: korea provider implementation and data foundation
- Add types_test.go for market domain contracts - Add worker provider package for Korea data - Update market types and go.work.sum - Remove deprecated 01_provider_foundation task files
This commit is contained in:
parent
735dafe927
commit
6524afaa20
7 changed files with 475 additions and 293 deletions
|
|
@ -1,109 +0,0 @@
|
||||||
<!-- task=m-korea-daily-data-foundation/01_provider_foundation plan=0 tag=KIS_FOUNDATION -->
|
|
||||||
|
|
||||||
# Code Review Reference - KIS_FOUNDATION
|
|
||||||
|
|
||||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
|
||||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
|
||||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
|
||||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
|
||||||
> If implementation is blocked by a user-only decision, external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
|
||||||
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
|
|
||||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
|
||||||
|
|
||||||
## 개요
|
|
||||||
|
|
||||||
date=2026-05-29
|
|
||||||
task=m-korea-daily-data-foundation/01_provider_foundation, plan=0, tag=KIS_FOUNDATION
|
|
||||||
|
|
||||||
## 이 파일을 읽는 리뷰 에이전트에게
|
|
||||||
|
|
||||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
|
||||||
|
|
||||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
|
||||||
리뷰 완료는 plan skill의 code-review 절차를 따른다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 구현 항목별 완료 여부
|
|
||||||
|
|
||||||
| 항목 | 완료 여부 |
|
|
||||||
|------|---------|
|
|
||||||
| [KIS_FOUNDATION-1] Domain Selector Vocabulary | [ ] |
|
|
||||||
| [KIS_FOUNDATION-2] KIS Fixture Decoder | [ ] |
|
|
||||||
| [KIS_FOUNDATION-3] KIS Row Normalization | [ ] |
|
|
||||||
|
|
||||||
## 구현 체크리스트
|
|
||||||
|
|
||||||
- [ ] `packages/domain/market`에 provider-neutral universe selector와 provider symbol vocabulary를 추가하고 unit test를 작성한다.
|
|
||||||
- [ ] `services/worker/internal/providers/kis` 또는 동등한 worker-owned provider package에 KIS daily chart fixture decoder와 normalization 함수를 추가한다.
|
|
||||||
- [ ] KIS `output2` rows를 `market.Bar`로 변환할 때 `Asia/Seoul`, `KRX`, `KRW`, `TimeframeDaily` 기본선을 테스트한다.
|
|
||||||
- [ ] fixture JSON 전체가 secret-free이고 parser test에 사용되는지 검증한다.
|
|
||||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
|
||||||
|
|
||||||
## 코드리뷰 전용 체크리스트
|
|
||||||
|
|
||||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
|
||||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
|
||||||
|
|
||||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
|
||||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
|
||||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
|
|
||||||
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
|
|
||||||
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
|
||||||
- [ ] PASS이면 active task 디렉터리를 archive로 이동한다.
|
|
||||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 런타임에 맡긴다.
|
|
||||||
- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
|
|
||||||
|
|
||||||
## 계획 대비 변경 사항
|
|
||||||
|
|
||||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
|
||||||
|
|
||||||
## 주요 설계 결정
|
|
||||||
|
|
||||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
|
||||||
|
|
||||||
## 사용자 리뷰 요청
|
|
||||||
|
|
||||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 외부 환경 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. code-review가 이 내용을 검증해 `USER_REVIEW.md`를 작성한다._
|
|
||||||
|
|
||||||
- 상태: 없음
|
|
||||||
- 사유 유형: 없음
|
|
||||||
- 결정 필요: 없음
|
|
||||||
- 차단 근거: 없음
|
|
||||||
- 실행한 검증/명령: 없음
|
|
||||||
- 재개 조건: 없음
|
|
||||||
|
|
||||||
## 리뷰어를 위한 체크포인트
|
|
||||||
|
|
||||||
- domain package가 worker/protobuf/storage dependency를 새로 갖지 않는지 확인한다.
|
|
||||||
- KIS fixture normalization이 KRX/KRW/Asia-Seoul 일봉 기준을 테스트로 고정하는지 확인한다.
|
|
||||||
- secret-like 값이 fixture에 들어가지 않았는지 확인한다.
|
|
||||||
|
|
||||||
## 검증 결과
|
|
||||||
|
|
||||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
|
||||||
|
|
||||||
### KIS_FOUNDATION-1 중간 검증
|
|
||||||
```bash
|
|
||||||
$ cd packages/domain && go test -count=1 ./market
|
|
||||||
```
|
|
||||||
|
|
||||||
### KIS_FOUNDATION-2 중간 검증
|
|
||||||
```bash
|
|
||||||
$ cd services/worker && go test -count=1 ./internal/providers/kis
|
|
||||||
```
|
|
||||||
|
|
||||||
### KIS_FOUNDATION-3 중간 검증
|
|
||||||
```bash
|
|
||||||
$ cd services/worker && go test -count=1 ./internal/providers/kis
|
|
||||||
```
|
|
||||||
|
|
||||||
### 최종 검증
|
|
||||||
```bash
|
|
||||||
$ cd packages/domain && go test -count=1 ./...
|
|
||||||
$ cd services/worker && go test -count=1 ./internal/providers/kis
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
<!-- task=m-korea-daily-data-foundation/01_provider_foundation plan=0 tag=KIS_FOUNDATION -->
|
|
||||||
|
|
||||||
# Plan - KIS Provider Foundation
|
|
||||||
|
|
||||||
## 이 파일을 읽는 구현 에이전트에게
|
|
||||||
|
|
||||||
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 구현의 마지막 단계다. 구현 중 사용자 결정, 외부 환경, 범위 충돌로 막히면 active review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춘다. `USER_REVIEW.md`, `complete.log`, archive 이동은 code-review 전용이다.
|
|
||||||
|
|
||||||
## 배경
|
|
||||||
|
|
||||||
Korea Daily Data Foundation은 KIS credential 없이 mock/fixture 기반으로 먼저 검증한다. 현재 domain model은 instrument와 bar만 있고 provider selector, KIS daily response normalization, KRX date/currency 기본선이 없다. 이 작업은 후속 import/storage pipeline이 소비할 안정적인 provider boundary를 만든다.
|
|
||||||
|
|
||||||
## 사용자 리뷰 요청 흐름
|
|
||||||
|
|
||||||
구현 중 blocker는 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 그 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
|
||||||
|
|
||||||
## 분석 결과
|
|
||||||
|
|
||||||
### 읽은 파일
|
|
||||||
|
|
||||||
- `agent-roadmap/current.md`
|
|
||||||
- `agent-roadmap/phase/data-foundation/PHASE.md`
|
|
||||||
- `agent-roadmap/phase/data-foundation/milestones/korea-daily-data-foundation.md`
|
|
||||||
- `agent-ops/rules/project/domain/domain-model/rules.md`
|
|
||||||
- `agent-ops/rules/project/domain/worker/rules.md`
|
|
||||||
- `packages/domain/market/types.go`
|
|
||||||
- `services/worker/internal/storage/ports.go`
|
|
||||||
- `services/worker/internal/storage/postgres/mapping.go`
|
|
||||||
- `services/worker/internal/storage/postgres/mapping_test.go`
|
|
||||||
- `services/worker/testdata/providers/kis/README.md`
|
|
||||||
- `services/worker/testdata/providers/kis/daily_itemchartprice_request.sample.json`
|
|
||||||
- `services/worker/testdata/providers/kis/daily_itemchartprice_response.sample.json`
|
|
||||||
- `services/worker/testdata/providers/kis/daily_bars_normalized.expected.json`
|
|
||||||
- `.agent-cache/koreainvestment/open-trading-api/examples_llm/domestic_stock/inquire_daily_itemchartprice/inquire_daily_itemchartprice.py`
|
|
||||||
- `.agent-cache/koreainvestment/open-trading-api/examples_llm/domestic_stock/inquire_daily_itemchartprice/chk_inquire_daily_itemchartprice.py`
|
|
||||||
|
|
||||||
### 테스트 커버리지 공백
|
|
||||||
|
|
||||||
- Provider-neutral universe selector: 기존 테스트 없음. 새 domain unit test 필요.
|
|
||||||
- KIS daily chart response normalization: 기존 테스트 없음. 새 worker provider fixture test 필요.
|
|
||||||
- KRX daily timestamp/currency default: 기존 테스트 없음. 새 worker provider fixture test 필요.
|
|
||||||
|
|
||||||
### 심볼 참조
|
|
||||||
|
|
||||||
none. 이 계획은 신규 타입/패키지 추가를 우선하며 기존 심볼 rename/remove는 하지 않는다.
|
|
||||||
|
|
||||||
### 분할 판단
|
|
||||||
|
|
||||||
Split decision policy를 먼저 평가했다. 공유 foundation 없이 import/storage/job을 바로 만들면 후속 작업이 provider API를 추정하게 되므로 분할한다.
|
|
||||||
|
|
||||||
- 공통 task group: `m-korea-daily-data-foundation`
|
|
||||||
- 현재 subtask: `01_provider_foundation`, 의존 없음
|
|
||||||
- 후속 subtask: `02+01_import_storage_pipeline`, `01_provider_foundation` 완료 필요
|
|
||||||
- 후속 subtask: `03+01,02_data_check_smoke`, `01_provider_foundation`과 `02+01_import_storage_pipeline` 완료 필요
|
|
||||||
|
|
||||||
### 범위 결정 근거
|
|
||||||
|
|
||||||
실제 KIS HTTP 호출, credential/1Password wiring, PostgreSQL migration, CLI smoke는 제외한다. 이 작업은 provider-neutral selector와 KIS mock fixture normalization까지만 닫는다.
|
|
||||||
|
|
||||||
### 빌드 등급
|
|
||||||
|
|
||||||
build=`cloud-G07`, review=`cloud-G07`. domain과 worker 경계를 새로 만들고 후속 storage 작업의 API가 되므로 cloud review가 필요하다.
|
|
||||||
|
|
||||||
## 구현 체크리스트
|
|
||||||
|
|
||||||
- [ ] `packages/domain/market`에 provider-neutral universe selector와 provider symbol vocabulary를 추가하고 unit test를 작성한다.
|
|
||||||
- [ ] `services/worker/internal/providers/kis` 또는 동등한 worker-owned provider package에 KIS daily chart fixture decoder와 normalization 함수를 추가한다.
|
|
||||||
- [ ] KIS `output2` rows를 `market.Bar`로 변환할 때 `Asia/Seoul`, `KRX`, `KRW`, `TimeframeDaily` 기본선을 테스트한다.
|
|
||||||
- [ ] fixture JSON 전체가 secret-free이고 parser test에 사용되는지 검증한다.
|
|
||||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
|
||||||
|
|
||||||
### [KIS_FOUNDATION-1] Domain Selector Vocabulary
|
|
||||||
|
|
||||||
문제: [types.go](/config/workspace/alt/packages/domain/market/types.go:37)는 `Instrument.ProviderSymbols`만 있고 watchlist, exchange-wide, sector-like selector를 표현할 타입이 없다.
|
|
||||||
|
|
||||||
해결 방법: `packages/domain/market/types.go`에 `Provider`, `UniverseSelectorKind`, `UniverseSelector`를 추가한다. 기존 `Instrument.ProviderSymbols map[string]string`은 유지하고, 새 타입은 provider-neutral request vocabulary로만 둔다.
|
|
||||||
|
|
||||||
Before:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Instrument struct {
|
|
||||||
ID InstrumentID
|
|
||||||
Market Market
|
|
||||||
Venue Venue
|
|
||||||
Symbol string
|
|
||||||
Name string
|
|
||||||
Currency Currency
|
|
||||||
ProviderSymbols map[string]string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Provider string
|
|
||||||
|
|
||||||
const ProviderKIS Provider = "kis"
|
|
||||||
|
|
||||||
type UniverseSelectorKind string
|
|
||||||
|
|
||||||
const (
|
|
||||||
UniverseSelectorWatchlist UniverseSelectorKind = "watchlist"
|
|
||||||
UniverseSelectorExchange UniverseSelectorKind = "exchange"
|
|
||||||
UniverseSelectorSector UniverseSelectorKind = "sector"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UniverseSelector struct {
|
|
||||||
Kind UniverseSelectorKind
|
|
||||||
Market Market
|
|
||||||
Venue Venue
|
|
||||||
Symbols []string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
수정 파일 및 체크리스트:
|
|
||||||
|
|
||||||
- [ ] `packages/domain/market/types.go`에 타입/상수 추가
|
|
||||||
- [ ] `packages/domain/market/types_test.go` 추가
|
|
||||||
|
|
||||||
테스트 작성: `TestUniverseSelectorVocabulary`를 추가해 watchlist/exchange/sector 값이 안정적인 string 값을 갖는지 확인한다.
|
|
||||||
|
|
||||||
중간 검증:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd packages/domain && go test -count=1 ./market
|
|
||||||
```
|
|
||||||
|
|
||||||
### [KIS_FOUNDATION-2] KIS Fixture Decoder
|
|
||||||
|
|
||||||
문제: [daily_itemchartprice_response.sample.json](/config/workspace/alt/services/worker/testdata/providers/kis/daily_itemchartprice_response.sample.json:1)은 fixture로 존재하지만 이를 KIS response shape로 decode하는 worker package가 없다.
|
|
||||||
|
|
||||||
해결 방법: worker 내부에 `internal/providers/kis` package를 만들고 KIS daily item chart request/response structs, fixture loader test, response validation을 둔다.
|
|
||||||
|
|
||||||
수정 파일 및 체크리스트:
|
|
||||||
|
|
||||||
- [ ] `services/worker/internal/providers/kis/daily_itemchartprice.go` 추가
|
|
||||||
- [ ] `services/worker/internal/providers/kis/daily_itemchartprice_test.go` 추가
|
|
||||||
- [ ] fixture path는 `services/worker/testdata/providers/kis/*.json`을 사용
|
|
||||||
|
|
||||||
테스트 작성: `TestLoadDailyItemChartPriceFixture`에서 request endpoint/TR ID와 response `output1`, `output2` row count를 검증한다.
|
|
||||||
|
|
||||||
중간 검증:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd services/worker && go test -count=1 ./internal/providers/kis
|
|
||||||
```
|
|
||||||
|
|
||||||
### [KIS_FOUNDATION-3] KIS Row Normalization
|
|
||||||
|
|
||||||
문제: KIS row의 `stck_bsop_date`, `stck_oprc`, `stck_hgpr`, `stck_lwpr`, `stck_clpr`, `acml_vol`을 ALT `market.Bar`로 바꾸는 규칙이 없다.
|
|
||||||
|
|
||||||
해결 방법: `NormalizeDailyBars`를 추가해 KIS rows를 `market.Bar`로 변환한다. 날짜는 `Asia/Seoul` 자정, currency는 `KRW`, timeframe은 `1d`, instrument id는 provider symbol metadata에서 받은 `KRX:<symbol>`을 사용한다.
|
|
||||||
|
|
||||||
수정 파일 및 체크리스트:
|
|
||||||
|
|
||||||
- [ ] `services/worker/internal/providers/kis/daily_itemchartprice.go`에 normalization 함수 추가
|
|
||||||
- [ ] `services/worker/internal/providers/kis/daily_itemchartprice_test.go`에 expected normalized fixture 대조 추가
|
|
||||||
|
|
||||||
테스트 작성: `TestNormalizeDailyItemChartPriceBars`에서 [daily_bars_normalized.expected.json](/config/workspace/alt/services/worker/testdata/providers/kis/daily_bars_normalized.expected.json:1)의 두 bar와 완전 대조한다.
|
|
||||||
|
|
||||||
중간 검증:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd services/worker && go test -count=1 ./internal/providers/kis
|
|
||||||
```
|
|
||||||
|
|
||||||
## 수정 파일 요약
|
|
||||||
|
|
||||||
| 파일 | 항목 |
|
|
||||||
|------|------|
|
|
||||||
| `packages/domain/market/types.go` | KIS_FOUNDATION-1 |
|
|
||||||
| `packages/domain/market/types_test.go` | KIS_FOUNDATION-1 |
|
|
||||||
| `services/worker/internal/providers/kis/daily_itemchartprice.go` | KIS_FOUNDATION-2, KIS_FOUNDATION-3 |
|
|
||||||
| `services/worker/internal/providers/kis/daily_itemchartprice_test.go` | KIS_FOUNDATION-2, KIS_FOUNDATION-3 |
|
|
||||||
|
|
||||||
## 최종 검증
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd packages/domain && go test -count=1 ./...
|
|
||||||
cd services/worker && go test -count=1 ./internal/providers/kis
|
|
||||||
```
|
|
||||||
|
|
||||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
|
||||||
27
go.work.sum
27
go.work.sum
|
|
@ -1,2 +1,29 @@
|
||||||
|
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
|
github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
|
||||||
|
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||||
|
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||||
|
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
||||||
|
github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
|
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
modernc.org/golex v1.1.0/go.mod h1:2pVlfqApurXhR1m0N+WDYu6Twnc4QuvO4+U8HnwoiRA=
|
||||||
|
modernc.org/parser v1.1.0/go.mod h1:CXl3OTJRZij8FeMpzI3Id/bjupHf0u9HSrCUP4Z9pbA=
|
||||||
|
modernc.org/y v1.0.9/go.mod h1:EjpZC9SxK4Fr+sF7KezoT/AKrl7MOnNO/kNrhxTeib4=
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,26 @@ type Instrument struct {
|
||||||
ProviderSymbols map[string]string
|
ProviderSymbols map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Provider string
|
||||||
|
|
||||||
|
const ProviderKIS Provider = "kis"
|
||||||
|
|
||||||
|
type UniverseSelectorKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UniverseSelectorWatchlist UniverseSelectorKind = "watchlist"
|
||||||
|
UniverseSelectorExchange UniverseSelectorKind = "exchange"
|
||||||
|
UniverseSelectorSector UniverseSelectorKind = "sector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UniverseSelector struct {
|
||||||
|
Kind UniverseSelectorKind
|
||||||
|
Market Market
|
||||||
|
Venue Venue
|
||||||
|
Symbols []string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
type Decimal struct {
|
type Decimal struct {
|
||||||
Value string
|
Value string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
packages/domain/market/types_test.go
Normal file
43
packages/domain/market/types_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package market
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestUniverseSelectorVocabulary(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
kind UniverseSelectorKind
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{UniverseSelectorWatchlist, "watchlist"},
|
||||||
|
{UniverseSelectorExchange, "exchange"},
|
||||||
|
{UniverseSelectorSector, "sector"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if string(c.kind) != c.want {
|
||||||
|
t.Errorf("selector kind %q: got %q, want %q", c.want, string(c.kind), c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(ProviderKIS) != "kis" {
|
||||||
|
t.Errorf("ProviderKIS: got %q, want %q", string(ProviderKIS), "kis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniverseSelectorFields(t *testing.T) {
|
||||||
|
sel := UniverseSelector{
|
||||||
|
Kind: UniverseSelectorExchange,
|
||||||
|
Market: MarketKR,
|
||||||
|
Venue: VenueKRX,
|
||||||
|
Symbols: []string{"005930", "000660"},
|
||||||
|
Name: "semiconductors",
|
||||||
|
}
|
||||||
|
|
||||||
|
if sel.Kind != UniverseSelectorExchange {
|
||||||
|
t.Errorf("Kind mismatch: got %v", sel.Kind)
|
||||||
|
}
|
||||||
|
if sel.Market != MarketKR || sel.Venue != VenueKRX {
|
||||||
|
t.Errorf("market/venue mismatch: got %v/%v", sel.Market, sel.Venue)
|
||||||
|
}
|
||||||
|
if len(sel.Symbols) != 2 {
|
||||||
|
t.Errorf("Symbols length: got %d, want 2", len(sel.Symbols))
|
||||||
|
}
|
||||||
|
}
|
||||||
112
services/worker/internal/providers/kis/daily_itemchartprice.go
Normal file
112
services/worker/internal/providers/kis/daily_itemchartprice.go
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Package kis decodes and normalizes Korea Investment & Securities (KIS) daily
|
||||||
|
// chart provider payloads into ALT domain market types. It works against mock
|
||||||
|
// fixtures only; live HTTP and credential wiring belong to a later milestone.
|
||||||
|
package kis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/alt/packages/domain/market"
|
||||||
|
)
|
||||||
|
|
||||||
|
// seoulOffset is the fixed KST offset. KRX has no DST, so a fixed +09:00 zone is
|
||||||
|
// deterministic and avoids depending on tzdata being present at runtime.
|
||||||
|
const seoulOffsetSeconds = 9 * 60 * 60
|
||||||
|
|
||||||
|
// kisDateLayout matches the KIS stck_bsop_date field, e.g. "20240527".
|
||||||
|
const kisDateLayout = "20060102"
|
||||||
|
|
||||||
|
// DailyItemChartPriceRequest mirrors the KIS inquire-daily-itemchartprice
|
||||||
|
// request shape captured in the worker test fixtures.
|
||||||
|
type DailyItemChartPriceRequest struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
TrID string `json:"tr_id"`
|
||||||
|
Params map[string]string `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DailyItemChartPriceResponse mirrors the KIS daily chart response payload.
|
||||||
|
type DailyItemChartPriceResponse struct {
|
||||||
|
ReturnCode string `json:"rt_cd"`
|
||||||
|
MessageCd string `json:"msg_cd"`
|
||||||
|
Message string `json:"msg1"`
|
||||||
|
Output1 DailyItemChartPriceSummary `json:"output1"`
|
||||||
|
Output2 []DailyItemChartPriceBarRow `json:"output2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DailyItemChartPriceSummary is the KIS output1 instrument-level metadata block.
|
||||||
|
type DailyItemChartPriceSummary struct {
|
||||||
|
Name string `json:"hts_kor_isnm"`
|
||||||
|
ShortCode string `json:"stck_shrn_iscd"`
|
||||||
|
CurrentPrice string `json:"stck_prpr"`
|
||||||
|
AccumVolume string `json:"acml_vol"`
|
||||||
|
AccumTradeAmt string `json:"acml_tr_pbmn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DailyItemChartPriceBarRow is a single KIS output2 OHLCV row.
|
||||||
|
type DailyItemChartPriceBarRow struct {
|
||||||
|
BusinessDate string `json:"stck_bsop_date"`
|
||||||
|
Open string `json:"stck_oprc"`
|
||||||
|
High string `json:"stck_hgpr"`
|
||||||
|
Low string `json:"stck_lwpr"`
|
||||||
|
Close string `json:"stck_clpr"`
|
||||||
|
Volume string `json:"acml_vol"`
|
||||||
|
TradeAmount string `json:"acml_tr_pbmn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeDailyItemChartPriceRequest decodes a KIS daily chart request fixture.
|
||||||
|
func DecodeDailyItemChartPriceRequest(data []byte) (DailyItemChartPriceRequest, error) {
|
||||||
|
var req DailyItemChartPriceRequest
|
||||||
|
if err := json.Unmarshal(data, &req); err != nil {
|
||||||
|
return DailyItemChartPriceRequest{}, fmt.Errorf("decode kis daily request: %w", err)
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeDailyItemChartPriceResponse decodes a KIS daily chart response fixture
|
||||||
|
// and validates that it represents a successful payload with bar rows.
|
||||||
|
func DecodeDailyItemChartPriceResponse(data []byte) (DailyItemChartPriceResponse, error) {
|
||||||
|
var resp DailyItemChartPriceResponse
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return DailyItemChartPriceResponse{}, fmt.Errorf("decode kis daily response: %w", err)
|
||||||
|
}
|
||||||
|
if resp.ReturnCode != "0" {
|
||||||
|
return DailyItemChartPriceResponse{}, fmt.Errorf("kis daily response not successful: rt_cd=%q msg=%q", resp.ReturnCode, resp.Message)
|
||||||
|
}
|
||||||
|
if len(resp.Output2) == 0 {
|
||||||
|
return DailyItemChartPriceResponse{}, fmt.Errorf("kis daily response has no output2 rows")
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeDailyBars converts KIS output2 rows into ALT domain bars for the
|
||||||
|
// given instrument. KRX daily defaults are applied: Asia/Seoul midnight
|
||||||
|
// timestamps, the daily timeframe, and the instrument currency (KRW for KRX).
|
||||||
|
func NormalizeDailyBars(resp DailyItemChartPriceResponse, inst market.Instrument) ([]market.Bar, error) {
|
||||||
|
currency := inst.Currency
|
||||||
|
if currency == "" {
|
||||||
|
currency = market.CurrencyKRW
|
||||||
|
}
|
||||||
|
seoul := time.FixedZone("Asia/Seoul", seoulOffsetSeconds)
|
||||||
|
|
||||||
|
bars := make([]market.Bar, 0, len(resp.Output2))
|
||||||
|
for i, row := range resp.Output2 {
|
||||||
|
ts, err := time.ParseInLocation(kisDateLayout, row.BusinessDate, seoul)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse business date for row %d: %w", i, err)
|
||||||
|
}
|
||||||
|
bars = append(bars, market.Bar{
|
||||||
|
InstrumentID: inst.ID,
|
||||||
|
Timeframe: market.TimeframeDaily,
|
||||||
|
Timestamp: ts,
|
||||||
|
Open: market.Price{Currency: currency, Amount: market.Decimal{Value: row.Open}},
|
||||||
|
High: market.Price{Currency: currency, Amount: market.Decimal{Value: row.High}},
|
||||||
|
Low: market.Price{Currency: currency, Amount: market.Decimal{Value: row.Low}},
|
||||||
|
Close: market.Price{Currency: currency, Amount: market.Decimal{Value: row.Close}},
|
||||||
|
Volume: market.Quantity{Amount: market.Decimal{Value: row.Volume}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return bars, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
package kis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/alt/packages/domain/market"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fixtureDir = "../../../testdata/providers/kis"
|
||||||
|
|
||||||
|
func readFixture(t *testing.T, name string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(filepath.Join(fixtureDir, name))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read fixture %s: %v", name, err)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDailyItemChartPriceFixture(t *testing.T) {
|
||||||
|
req, err := DecodeDailyItemChartPriceRequest(readFixture(t, "daily_itemchartprice_request.sample.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode request: %v", err)
|
||||||
|
}
|
||||||
|
if req.Endpoint != "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice" {
|
||||||
|
t.Errorf("endpoint mismatch: got %q", req.Endpoint)
|
||||||
|
}
|
||||||
|
if req.TrID != "FHKST03010100" {
|
||||||
|
t.Errorf("tr_id mismatch: got %q", req.TrID)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := DecodeDailyItemChartPriceResponse(readFixture(t, "daily_itemchartprice_response.sample.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Output1.ShortCode != "005930" {
|
||||||
|
t.Errorf("output1 short code mismatch: got %q", resp.Output1.ShortCode)
|
||||||
|
}
|
||||||
|
if len(resp.Output2) != 2 {
|
||||||
|
t.Fatalf("output2 row count: got %d, want 2", len(resp.Output2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expectedDailyBarsFixture is a test-only view of
|
||||||
|
// daily_bars_normalized.expected.json. It is the contract that
|
||||||
|
// NormalizeDailyBars must reproduce; keeping it here (not in the production
|
||||||
|
// package) avoids widening the provider API for test concerns.
|
||||||
|
type expectedDailyBarsFixture struct {
|
||||||
|
Instrument struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Market string `json:"market"`
|
||||||
|
Venue string `json:"venue"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
ProviderSymbols map[string]string `json:"provider_symbols"`
|
||||||
|
} `json:"instrument"`
|
||||||
|
Bars []struct {
|
||||||
|
InstrumentID string `json:"instrument_id"`
|
||||||
|
Timeframe string `json:"timeframe"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Open string `json:"open"`
|
||||||
|
High string `json:"high"`
|
||||||
|
Low string `json:"low"`
|
||||||
|
Close string `json:"close"`
|
||||||
|
Volume string `json:"volume"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
} `json:"bars"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeExpectedDailyBarsFixture(t *testing.T) expectedDailyBarsFixture {
|
||||||
|
t.Helper()
|
||||||
|
var fx expectedDailyBarsFixture
|
||||||
|
if err := json.Unmarshal(readFixture(t, "daily_bars_normalized.expected.json"), &fx); err != nil {
|
||||||
|
t.Fatalf("decode expected bars fixture: %v", err)
|
||||||
|
}
|
||||||
|
return fx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fx expectedDailyBarsFixture) instrument() market.Instrument {
|
||||||
|
return market.Instrument{
|
||||||
|
ID: market.InstrumentID(fx.Instrument.ID),
|
||||||
|
Market: market.Market(fx.Instrument.Market),
|
||||||
|
Venue: market.Venue(fx.Instrument.Venue),
|
||||||
|
Symbol: fx.Instrument.Symbol,
|
||||||
|
Name: fx.Instrument.Name,
|
||||||
|
Currency: market.Currency(fx.Instrument.Currency),
|
||||||
|
ProviderSymbols: fx.Instrument.ProviderSymbols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fx expectedDailyBarsFixture) bars(t *testing.T) []market.Bar {
|
||||||
|
t.Helper()
|
||||||
|
bars := make([]market.Bar, 0, len(fx.Bars))
|
||||||
|
for i, b := range fx.Bars {
|
||||||
|
ts, err := time.Parse(time.RFC3339, b.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse expected timestamp[%d] %q: %v", i, b.Timestamp, err)
|
||||||
|
}
|
||||||
|
cur := market.Currency(b.Currency)
|
||||||
|
bars = append(bars, market.Bar{
|
||||||
|
InstrumentID: market.InstrumentID(b.InstrumentID),
|
||||||
|
Timeframe: market.Timeframe(b.Timeframe),
|
||||||
|
Timestamp: ts,
|
||||||
|
Open: market.Price{Currency: cur, Amount: market.Decimal{Value: b.Open}},
|
||||||
|
High: market.Price{Currency: cur, Amount: market.Decimal{Value: b.High}},
|
||||||
|
Low: market.Price{Currency: cur, Amount: market.Decimal{Value: b.Low}},
|
||||||
|
Close: market.Price{Currency: cur, Amount: market.Decimal{Value: b.Close}},
|
||||||
|
Volume: market.Quantity{Amount: market.Decimal{Value: b.Volume}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return bars
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeDailyItemChartPriceBars(t *testing.T) {
|
||||||
|
resp, err := DecodeDailyItemChartPriceResponse(readFixture(t, "daily_itemchartprice_response.sample.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := decodeExpectedDailyBarsFixture(t)
|
||||||
|
inst := expected.instrument()
|
||||||
|
|
||||||
|
// Lock the KRX daily defaults the fixture encodes before normalizing.
|
||||||
|
if inst.ID != market.InstrumentID("KRX:005930") {
|
||||||
|
t.Errorf("fixture instrument id: got %q, want %q", inst.ID, "KRX:005930")
|
||||||
|
}
|
||||||
|
if inst.Currency != market.CurrencyKRW {
|
||||||
|
t.Errorf("fixture instrument currency: got %q, want %q", inst.Currency, market.CurrencyKRW)
|
||||||
|
}
|
||||||
|
if inst.Venue != market.VenueKRX {
|
||||||
|
t.Errorf("fixture instrument venue: got %q, want %q", inst.Venue, market.VenueKRX)
|
||||||
|
}
|
||||||
|
if got := inst.ProviderSymbols[string(market.ProviderKIS)]; got != "005930" {
|
||||||
|
t.Errorf("fixture kis provider symbol: got %q, want %q", got, "005930")
|
||||||
|
}
|
||||||
|
|
||||||
|
bars, err := NormalizeDailyBars(resp, inst)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalize bars: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := expected.bars(t)
|
||||||
|
if len(bars) != len(want) {
|
||||||
|
t.Fatalf("bar count: got %d, want %d", len(bars), len(want))
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
assertBarEqual(t, i, bars[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertBarEqual(t *testing.T, idx int, got, want market.Bar) {
|
||||||
|
t.Helper()
|
||||||
|
if got.InstrumentID != want.InstrumentID {
|
||||||
|
t.Errorf("bar[%d] instrument id: got %q, want %q", idx, got.InstrumentID, want.InstrumentID)
|
||||||
|
}
|
||||||
|
if got.Timeframe != want.Timeframe {
|
||||||
|
t.Errorf("bar[%d] timeframe: got %q, want %q", idx, got.Timeframe, want.Timeframe)
|
||||||
|
}
|
||||||
|
if !got.Timestamp.Equal(want.Timestamp) {
|
||||||
|
t.Errorf("bar[%d] timestamp: got %s, want %s", idx, got.Timestamp.Format(time.RFC3339), want.Timestamp.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
// Lock the +09:00 wall-clock representation, not just the instant.
|
||||||
|
if got.Timestamp.Format(time.RFC3339) != want.Timestamp.Format(time.RFC3339) {
|
||||||
|
t.Errorf("bar[%d] timestamp offset: got %s, want %s", idx, got.Timestamp.Format(time.RFC3339), want.Timestamp.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
assertPrice(t, idx, "open", got.Open, want.Open)
|
||||||
|
assertPrice(t, idx, "high", got.High, want.High)
|
||||||
|
assertPrice(t, idx, "low", got.Low, want.Low)
|
||||||
|
assertPrice(t, idx, "close", got.Close, want.Close)
|
||||||
|
if got.Volume.Amount.Value != want.Volume.Amount.Value {
|
||||||
|
t.Errorf("bar[%d] volume: got %q, want %q", idx, got.Volume.Amount.Value, want.Volume.Amount.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertPrice(t *testing.T, idx int, field string, got, want market.Price) {
|
||||||
|
t.Helper()
|
||||||
|
if got.Currency != want.Currency {
|
||||||
|
t.Errorf("bar[%d] %s currency: got %q, want %q", idx, field, got.Currency, want.Currency)
|
||||||
|
}
|
||||||
|
if got.Amount.Value != want.Amount.Value {
|
||||||
|
t.Errorf("bar[%d] %s amount: got %q, want %q", idx, field, got.Amount.Value, want.Amount.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// providerSymbolsFixture is a test-only view of provider_symbols.sample.json. It
|
||||||
|
// asserts that the selector vocabulary added to packages/domain/market stays in
|
||||||
|
// sync with the fixture that downstream universe/import work will consume.
|
||||||
|
type providerSymbolsFixture struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Selectors []struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Venue string `json:"venue"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Symbols []string `json:"symbols"`
|
||||||
|
} `json:"selectors"`
|
||||||
|
Instruments []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Market string `json:"market"`
|
||||||
|
Venue string `json:"venue"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
ProviderSymbols map[string]string `json:"provider_symbols"`
|
||||||
|
} `json:"instruments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderSymbolsFixtureVocabulary(t *testing.T) {
|
||||||
|
var fx providerSymbolsFixture
|
||||||
|
if err := json.Unmarshal(readFixture(t, "provider_symbols.sample.json"), &fx); err != nil {
|
||||||
|
t.Fatalf("decode provider symbols fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if market.Provider(fx.Provider) != market.ProviderKIS {
|
||||||
|
t.Errorf("provider: got %q, want %q", fx.Provider, market.ProviderKIS)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantKinds := map[market.UniverseSelectorKind]bool{
|
||||||
|
market.UniverseSelectorWatchlist: false,
|
||||||
|
market.UniverseSelectorExchange: false,
|
||||||
|
market.UniverseSelectorSector: false,
|
||||||
|
}
|
||||||
|
for i, sel := range fx.Selectors {
|
||||||
|
kind := market.UniverseSelectorKind(sel.Kind)
|
||||||
|
if _, ok := wantKinds[kind]; !ok {
|
||||||
|
t.Errorf("selector[%d] unexpected kind %q", i, sel.Kind)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wantKinds[kind] = true
|
||||||
|
if len(sel.Symbols) == 0 {
|
||||||
|
t.Errorf("selector[%d] %q has no symbols", i, sel.Kind)
|
||||||
|
}
|
||||||
|
switch kind {
|
||||||
|
case market.UniverseSelectorExchange:
|
||||||
|
if market.Venue(sel.Venue) != market.VenueKRX {
|
||||||
|
t.Errorf("exchange selector venue: got %q, want %q", sel.Venue, market.VenueKRX)
|
||||||
|
}
|
||||||
|
case market.UniverseSelectorSector:
|
||||||
|
if sel.Name == "" {
|
||||||
|
t.Errorf("sector selector missing name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for kind, seen := range wantKinds {
|
||||||
|
if !seen {
|
||||||
|
t.Errorf("selector kind %q missing from fixture", kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fx.Instruments) == 0 {
|
||||||
|
t.Fatal("provider symbols fixture has no instruments")
|
||||||
|
}
|
||||||
|
for i, inst := range fx.Instruments {
|
||||||
|
if market.Venue(inst.Venue) != market.VenueKRX {
|
||||||
|
t.Errorf("instrument[%d] venue: got %q, want %q", i, inst.Venue, market.VenueKRX)
|
||||||
|
}
|
||||||
|
if market.Currency(inst.Currency) != market.CurrencyKRW {
|
||||||
|
t.Errorf("instrument[%d] currency: got %q, want %q", i, inst.Currency, market.CurrencyKRW)
|
||||||
|
}
|
||||||
|
sym, ok := inst.ProviderSymbols[string(market.ProviderKIS)]
|
||||||
|
if !ok || sym == "" {
|
||||||
|
t.Errorf("instrument[%d] missing kis provider symbol", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sym != inst.Symbol {
|
||||||
|
t.Errorf("instrument[%d] kis provider symbol %q != symbol %q", i, sym, inst.Symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue