diff --git a/agent-roadmap/phase/trading-expansion/milestones/paper-trading-command-workflow.md b/agent-roadmap/archive/phase/trading-expansion/milestones/paper-trading-command-workflow.md similarity index 72% rename from agent-roadmap/phase/trading-expansion/milestones/paper-trading-command-workflow.md rename to agent-roadmap/archive/phase/trading-expansion/milestones/paper-trading-command-workflow.md index 4bb6367..5e1717f 100644 --- a/agent-roadmap/phase/trading-expansion/milestones/paper-trading-command-workflow.md +++ b/agent-roadmap/archive/phase/trading-expansion/milestones/paper-trading-command-workflow.md @@ -11,7 +11,7 @@ ## 상태 -[진행중] +[완료] ## 구현 잠금 @@ -37,20 +37,23 @@ paper trading을 화면 없이 반복 검증하고, 나중에 UI로 올릴 운영 항목을 command evidence로 먼저 정리한다. - [x] [paper-status-command] paper account, portfolio, position 상태를 command로 조회할 수 있다. 검증: status command 또는 scenario가 account_id, cash, position_count, risk key를 출력한다. -- [ ] [paper-order-command] virtual order submit/cancel/fill simulation을 command로 실행할 수 있다. 검증: order lifecycle scenario가 order id, status transition, fill summary를 출력한다. +- [x] [paper-order-command] virtual order submit/cancel/fill simulation을 command로 실행할 수 있다. 검증: order lifecycle scenario가 order id, status transition, fill summary를 출력한다. - [x] [paper-risk-command] 최소 risk guard 결과를 command workflow에서 확인할 수 있다. 검증: 허용/차단 case가 typed status/error key로 구분된다. - [x] [paper-loop-smoke] paper execution loop를 fixture 또는 fake market data로 smoke 실행할 수 있다. 검증: loop command가 run id, terminal status, position/equity summary를 출력한다. -- [ ] [paper-ui-handoff] paper trading에서 화면에 올릴 후보와 아직 command로만 둘 항목을 분리한다. 검증: handoff 문서가 command, expected output key, 반복 운영 여부, UI defer 사유를 기록한다. +- [x] [paper-ui-handoff] paper trading에서 화면에 올릴 후보와 아직 command로만 둘 항목을 분리한다. 검증: handoff 문서가 command, expected output key, 반복 운영 여부, UI defer 사유를 기록한다. ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 아직 기능 Task와 검증이 충족되지 않았다. +- 상태: 승인됨 +- 요청일: 2026-06-06 +- 완료 근거: + - 모든 기능 Task가 완료 로그의 `Roadmap Completion` Task id와 매칭되어 `[x]`로 동기화되었다. + - `paper-order-command`는 `agent-task/archive/2026/06/m-paper-trading-command-workflow/03+02_order_lifecycle/complete.log` 기준 PASS다. + - `paper-ui-handoff`는 `agent-task/archive/2026/06/m-paper-trading-command-workflow/04+01,02,03_ui_handoff/complete.log` 기준 PASS다. - 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 사용자 요청으로 현 마일스톤을 완료 처리하고 archive로 이동한다. ## 범위 제외 @@ -65,16 +68,18 @@ paper trading을 화면 없이 반복 검증하고, 나중에 UI로 올릴 운 - 관련 경로: `apps/cli/`, `apps/cli/testdata/operator/`, `services/worker/`, `services/api/`, `packages/domain/` - 표준선(선택): paper trading은 실거래 전 검증 경계이며, 먼저 command workflow로 반복 운영성과 상태 key를 확인한다. - 표준선(선택): UI는 command workflow에서 반복되는 status/action/evidence key가 정리된 뒤에만 후보로 올린다. -- 현재 작업: order lifecycle과 UI handoff subtask가 active review 상태다. +- 현재 작업: 사용자 완료 승인에 따라 archive 이동까지 완료됐다. - 직접 처리 완료: - [x] `paper-status-command`: status output에 `account_id`, `cash`, `position_count`, `risk` key를 고정했고 2026-06-05 local `bin/test`가 통과했다. - [x] `paper-loop-smoke`: `agent-task/archive/2026/06/m-paper-trading-command-workflow/01_loop_smoke/complete.log` 기준 PASS. 검증: `go test -count=1 ./apps/cli/internal/operator`, `PATH="/tmp/protoc293/bin:$PATH" bin/test`. - [x] `paper-risk-command`: `agent-task/archive/2026/06/m-paper-trading-command-workflow/02+01_risk_command/complete.log` 기준 PASS. 검증: `PATH="/tmp/protoc293/bin:$PATH" bin/contracts-check`, `go test ./services/worker/...`, `go test ./services/api/...`, `go test ./apps/cli/internal/operator`, `PATH="/tmp/protoc293/bin:$PATH" bin/test`. + - [x] `paper-order-command`: `agent-task/archive/2026/06/m-paper-trading-command-workflow/03+02_order_lifecycle/complete.log` 기준 PASS. 검증: `go test -count=1 ./services/worker/internal/papertrading ./services/worker/internal/socket`, `go test -count=1 ./apps/cli/internal/operator`, `PATH="/tmp/protoc293/bin:$PATH" bin/test`. + - [x] `paper-ui-handoff`: `agent-task/archive/2026/06/m-paper-trading-command-workflow/04+01,02,03_ui_handoff/complete.log` 기준 PASS. 검증: `go test -count=1 ./apps/cli/internal/operator`, `bin/test`, `git diff --check`. - 큰 작업 plan: - 완료: `agent-task/archive/2026/06/m-paper-trading-command-workflow/01_loop_smoke/complete.log` - 완료: `agent-task/archive/2026/06/m-paper-trading-command-workflow/02+01_risk_command/complete.log` - - 활성: `agent-task/m-paper-trading-command-workflow/03+02_order_lifecycle/PLAN-cloud-G09.md` - - 활성: `agent-task/m-paper-trading-command-workflow/04+01,02,03_ui_handoff/PLAN-local-G04.md` + - 완료: `agent-task/archive/2026/06/m-paper-trading-command-workflow/03+02_order_lifecycle/complete.log` + - 완료: `agent-task/archive/2026/06/m-paper-trading-command-workflow/04+01,02,03_ui_handoff/complete.log` - 진행 순서: 1. status 조회 scenario로 account_id, cash, position_count, risk key를 고정한다. 2. paper execution loop smoke로 run id, terminal status, position/equity summary를 확인한다. diff --git a/agent-roadmap/phase/trading-expansion/PHASE.md b/agent-roadmap/phase/trading-expansion/PHASE.md index 3828bcd..9b947ee 100644 --- a/agent-roadmap/phase/trading-expansion/PHASE.md +++ b/agent-roadmap/phase/trading-expansion/PHASE.md @@ -16,10 +16,10 @@ - [완료] Paper Trading Readiness - 경로: `agent-roadmap/archive/phase/trading-expansion/milestones/paper-trading-readiness.md` - 요약: 실거래 전에 전략 실행, 가상 주문, 포지션, 리스크 흐름을 paper trading 경계로 검증한다. -- [진행중] Paper Trading Command Workflow - - 경로: `agent-roadmap/phase/trading-expansion/milestones/paper-trading-command-workflow.md` +- [완료] Paper Trading Command Workflow + - 경로: `agent-roadmap/archive/phase/trading-expansion/milestones/paper-trading-command-workflow.md` - 요약: paper trading account/order/fill/position/risk 흐름을 Flutter 화면 없이 CLI/YAML/log 중심의 반복 가능한 command workflow로 검증한다. -- [계획] US Market Expansion +- [진행중] US Market Expansion - 경로: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` - 요약: 한국장 MVP 구조 위에 미국장 데이터를 추가한다. - [보류] Live Trading Boundary diff --git a/agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md b/agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md index 3f639d1..78db98f 100644 --- a/agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md +++ b/agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md @@ -11,40 +11,36 @@ ## 상태 -[계획] +[진행중] ## 구현 잠금 -- 상태: 잠금 -- 결정 필요: 아래 체크리스트 - - [ ] 미국장 MVP universe를 ETF 중심, 개별주 중심, 또는 watchlist 중심 중 무엇으로 둘지 결정한다. - - [ ] 미국장 데이터 provider를 KIS 우선으로 고정할지 보조 provider를 함께 고려할지 결정한다. +- 상태: 해제 +- 결정 필요: 없음 +- 결정 내용: + - [x] 미국장 MVP universe는 watchlist 중심으로 시작한다. ETF/개별주는 universe entry의 `asset_type` metadata로 표현하고, 구현은 특정 종목 종류에 고정하지 않는다. + - [x] 초기 미국장 데이터 provider는 KIS로 고정한다. 다만 provider adapter/capability 형식은 유지해 Kiwoom 같은 후속 provider가 시장별 지원 범위를 선언할 수 있게 한다. ## 범위 - US market instrument mapping +- watchlist 기반 US market universe selector - USD/NASDAQ/NYSE calendar handling - daily bar import - 한국장과 동일한 normalized data/query path - backtest compatibility -## 필수 기능 +## 기능 ### Epic: [us-market] US market expansion 미국장 instrument, calendar, daily data, shared query, backtest compatibility 확장을 묶는다. -- [ ] [us-instruments] 미국장 instrument identity와 provider symbol mapping이 있다. -- [ ] [us-calendar] 미국장 timezone/currency/venue 기준이 있다. -- [ ] [us-daily-bars] 미국장 일봉 데이터를 normalized bar model로 저장한다. -- [ ] [shared-query] 한국장과 미국장이 같은 query/contract pattern을 공유한다. -- [ ] [us-backtest] 미국장 daily data로 backtest fixture 또는 smoke가 동작한다. - -## 완료 기준 - -- [ ] 한국장 pipeline을 깨지 않고 미국장 데이터를 추가한다. -- [ ] market/currency/venue 차이가 domain model과 contracts에 일관되게 반영된다. -- [ ] `bin/test`와 관련 worker 검증이 통과한다. +- [ ] [us-instruments] 미국장 instrument identity와 provider symbol mapping이 있다. 검증: watchlist fixture가 `market=us`, `venue`, `currency=USD`, `asset_type` metadata와 provider symbol을 분리해 가진다. +- [ ] [us-calendar] 미국장 timezone/currency/venue 기준이 있다. 검증: market/currency/venue 차이가 domain model과 contracts에 일관되게 반영된다. +- [ ] [us-daily-bars] KIS를 초기 provider로 사용해 미국장 일봉 데이터를 normalized bar model로 저장한다. 검증: 한국장 pipeline을 깨지 않고 provider capability가 미국장 daily bar import를 허용한다. +- [ ] [shared-query] 한국장과 미국장이 같은 query/contract pattern을 공유한다. 검증: market selector만 바꿔도 공통 query path를 사용한다. +- [ ] [us-backtest] 미국장 daily data로 backtest fixture 또는 smoke가 동작한다. 검증: `bin/test`와 관련 worker 검증이 통과한다. ## 완료 리뷰 @@ -66,6 +62,9 @@ - 관련 경로: `services/worker/`, `packages/domain/market/`, `packages/contracts/proto/alt/v1/market.proto` - 표준선(선택): 시장 추가는 provider adapter와 market metadata 확장으로 처리하고 백테스트 코어를 복제하지 않는다. +- 표준선(선택): universe는 watchlist 중심 selector로 두고, ETF/개별주는 instrument metadata의 `asset_type`으로 열어둔다. +- 표준선(선택): 초기 provider는 KIS로 고정하지만 provider abstraction은 유지한다. Provider는 `supported_markets`, `supported_venues`, `supported_asset_types`, `capabilities`를 선언하고, Kiwoom 같은 후속 provider는 한국장만 지원하더라도 같은 provider boundary에 붙일 수 있어야 한다. +- 표준선(선택): provider와 market은 별도 축이다. KIS가 한국장/미국장을 모두 지원하더라도 `provider=KIS`가 곧 `market=us`를 뜻하지 않으며, Kiwoom 추가 시 `provider=Kiwoom`, `market=kr` 조합처럼 capability matching으로 선택한다. - 선행 작업: Korea Daily Data Foundation, Backtest Engine Baseline, Command-First Operator Workflow, Paper Trading Command Workflow - 후속 작업: Live Trading Boundary, Flutter Operator Console MVP -- 확인 필요: 미국장 universe와 provider 우선순위 +- 확인 필요: 없음 diff --git a/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_cloud_G07_0.log b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_cloud_G07_0.log new file mode 100644 index 0000000..d261a24 --- /dev/null +++ b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_cloud_G07_0.log @@ -0,0 +1,165 @@ + + +# Code Review Reference - US_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, 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`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation. +> Finalization is review-agent-only. + +## 개요 + +date=2026-06-06 +task=m-us-market-expansion/01_market_foundation, plan=0, tag=US_FOUNDATION + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-instruments`: 미국장 instrument identity와 provider symbol mapping + - `us-calendar`: 미국장 timezone/currency/venue 기준 +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. 리뷰 완료는 판정 append, active plan/review log archive, PASS 시 `complete.log` 작성과 archive 이동까지 끝난 상태를 의미합니다. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터만 보고하고 roadmap 수정은 런타임 책임입니다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [US_FOUNDATION-1] Domain Asset Type And Market Metadata | [x] | +| [US_FOUNDATION-2] Contract And Storage Asset Type Round Trip | [x] | + +## 구현 체크리스트 + +- [x] [US_FOUNDATION-1] domain market model에 `AssetType`과 market/venue metadata를 추가하고 KR/US fixture vocabulary를 갱신한다. 검증: watchlist fixture가 `market=us`, `venue`, `currency=USD`, `asset_type`, provider symbol을 분리해 가진다. +- [x] [US_FOUNDATION-2] contract와 PostgreSQL persistence가 `asset_type`을 round-trip하도록 schema/query/mapping/generated output을 갱신한다. 검증: market/currency/venue/asset_type 차이가 domain model과 contracts에 일관되게 반영된다. +- [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_G07_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block을 확인한다. +- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-us-market-expansion/01_market_foundation/`로 이동한다. +- [ ] PASS이고 task group이 `m-us-market-expansion`이면 완료 이벤트 메타데이터를 보고한다. +- [ ] PASS split 작업이면 이동 후 빈 active parent 처리 여부를 확인한다. +- [x] WARN/FAIL이면 user-review gate 확인 뒤 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +- 없음. 계획된 설계 및 구현 방법론을 온전히 따랐음. + +## 주요 설계 결정 + +- `packages/domain/market/types.go` 내에 `AssetType` (equity, etf)과 `VenueMetadata` 를 추가하고, Venue를 Key로 메타데이터를 리턴하는 `GetVenueMetadata` 헬퍼 함수를 추가함. +- `common.proto`와 `market.proto`에 `AssetType` enum 및 `Instrument.asset_type` 필드를 반영하고, `bin/contracts-gen`을 통해 generated code를 갱신함. +- Postgres DB migration에 `000003_instrument_asset_type` 마이그레이션(up/down)을 추가하여 `instruments` 테이블에 `asset_type` 컬럼을 생성하고, sqlc query 및 Go mapper/socket mapper 코드를 수정하여 round-trip 매핑이 정상 작동하도록 구현함. + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `asset_type`이 provider symbol과 섞이지 않고 별도 field로 round-trip되는지 확인한다. +- `.proto`와 generated output이 drift 없이 일치하는지 확인한다. +- storage migration이 기존 rows를 깨지 않는 default/null 정책을 갖는지 확인한다. +- US fixture가 equity와 ETF를 모두 표현하고 `currency=USD`를 보존하는지 확인한다. + +## 검증 결과 + +### US_FOUNDATION-1 중간 검증 +```text +$ go test -count=1 ./packages/domain/... +ok git.toki-labs.com/toki/alt/packages/domain/backtest 0.002s +ok git.toki-labs.com/toki/alt/packages/domain/market 0.003s + +$ go test -count=1 ./services/worker/internal/providers/kis +ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.007s +``` + +### US_FOUNDATION-2 중간 검증 +```text +$ bin/contracts-gen +(exit 0) + +$ bin/contracts-check +(exit 0) + +$ go test -count=1 ./packages/contracts/gen/go/... +? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files] + +$ go test -count=1 ./services/worker/internal/storage/postgres ./services/worker/internal/socket +ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.073s +ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.061s +``` + +### 최종 검증 +```text +$ bin/contracts-gen +(exit 0) + +$ bin/contracts-check +(exit 0) + +$ go test -count=1 ./packages/domain/... +ok git.toki-labs.com/toki/alt/packages/domain/backtest 0.002s +ok git.toki-labs.com/toki/alt/packages/domain/market 0.003s + +$ go test -count=1 ./packages/contracts/gen/go/... +? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files] + +$ go test -count=1 ./services/worker/internal/storage/postgres ./services/worker/internal/socket ./services/worker/internal/providers/kis +ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.067s +ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.073s +ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.016s + +$ bin/worker-storage-check +Generating worker storage code via sqlc... +Generation complete. + +$ bin/test +All tests passed! + +$ git diff --check +(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?** + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - Correctness: Pass + - Completeness: Fail + - Test coverage: Fail + - API contract: Pass + - Code quality: Pass + - Plan deviation: Fail + - Verification trust: Pass +- 발견된 문제: + - Required: [services/worker/internal/providers/kis/daily_itemchartprice_test.go](/config/workspace/alt/services/worker/internal/providers/kis/daily_itemchartprice_test.go:258)의 `TestProviderSymbolsFixtureVocabulary`는 각 instrument 값이 허용 enum 집합 안에 있는지만 확인합니다. 그래서 [services/worker/testdata/providers/kis/provider_symbols.sample.json](/config/workspace/alt/services/worker/testdata/providers/kis/provider_symbols.sample.json:5)에서 `AAPL`/`SPY` US watchlist 항목이나 `NASDAQ`/`USD`/`etf` fixture row가 제거되어도 KR equity fixture만 남아 있으면 테스트가 계속 통과합니다. `US_FOUNDATION-1`의 통합 검증 조건인 US watchlist, USD, venue, equity/ETF, KIS provider symbol 분리를 실제로 잠그도록 필수 instrument id와 field 값을 명시적으로 assert하세요. +- 다음 단계: user-review gate는 트리거하지 않는다. active plan/review를 archive하고 `REVIEW_US_FOUNDATION` follow-up PLAN/CODE_REVIEW를 작성한다. diff --git a/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_local_G06_1.log b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_local_G06_1.log new file mode 100644 index 0000000..15d017e --- /dev/null +++ b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_local_G06_1.log @@ -0,0 +1,159 @@ + + +# Code Review Reference - REVIEW_US_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, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-06 +task=m-us-market-expansion/01_market_foundation, plan=1, tag=REVIEW_US_FOUNDATION + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-instruments`: 미국장 instrument identity와 provider symbol mapping + - `us-calendar`: 미국장 timezone/currency/venue 기준 +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G06.md` -> `code_review_local_G06_N.log`, `PLAN-local-G06.md` -> `plan_local_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-us-market-expansion/01_market_foundation/`로 이동한다. 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-us-market-expansion`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_US_FOUNDATION-1] Strengthen US Fixture Vocabulary Assertions | [x] | + +## 구현 체크리스트 + +- [x] [REVIEW_US_FOUNDATION-1] KIS provider symbols fixture test가 US watchlist와 `NASDAQ`/`USD`/`equity`/`etf`/KIS provider symbol mapping을 필수 row 단위로 검증하도록 강화한다. 검증: `AAPL`/`SPY` 또는 ETF row가 제거되면 테스트가 실패해야 한다. +- [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_local_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-us-market-expansion/01_market_foundation/`를 `agent-task/archive/YYYY/MM/m-us-market-expansion/01_market_foundation/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-us-market-expansion`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-us-market-expansion/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +계획에 정확히 따라 구현했다. 변경 범위는 단일 worker fixture test와 관련 JSON fixture 검증에 한정된다. + +## 주요 설계 결정 + +- US instrument 검증에 `requiredUS` map을 사용해 `NASDAQ:AAPL`과 `NASDAQ:SPY`의 exact field 값을 검증하도록 했다. +- watchlist selector의 US symbol 포함 여부(`AAPL`, `SPY`)도 assert하도록 했다. +- enum 허용 범위는 기존과 동일하게 유지하되, US rows에 대해 추가 row-level 검증만 도입했다. +- JSON fixture(`provider_symbols.sample.json`)는 assertion과 이미 일치하므로 수정하지 않았다. + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `TestProviderSymbolsFixtureVocabulary`가 `NASDAQ:AAPL`과 `NASDAQ:SPY`의 exact field 값을 assert하는지 확인한다. +- watchlist selector가 US symbols `AAPL`, `SPY`를 포함해야 테스트가 통과하는지 확인한다. +- production mapper/schema/generated output이 불필요하게 바뀌지 않았는지 확인한다. + +## 검증 결과 + +### REVIEW_US_FOUNDATION-1 중간 검증 +```text +$ go test -count=1 -run TestProviderSymbolsFixtureVocabulary ./services/worker/internal/providers/kis +ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.004s +``` + +### 최종 검증 +```text +$ go test -count=1 ./services/worker/internal/providers/kis +ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.012s + +$ go test -count=1 ./services/worker/... +ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker 0.005s +? 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.002s +ok git.toki-labs.com/toki/alt/services/worker/internal/config 0.002s +ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.013s +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.009s +ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer 0.005s +ok git.toki-labs.com/toki/alt/services/worker/internal/papertrading 0.006s +ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.011s +ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys 0.006s +ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.059s +? git.toki-labs.com/toki/alt/services/worker/internal/storage [no test files] +ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.058s +? git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres/sqlc [no test files] + +$ bin/test +? git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1 [no test files] +ok git.toki-labs.com/toki/alt/packages/domain/backtest (cached) +ok git.toki-labs.com/toki/alt/packages/domain/market (cached) +? git.toki-labs.com/toki/alt/services/api/cmd/alt-api [no test files] +[... Go package tests omitted for brevity ...] +00:02 +60: All tests passed! + +$ git diff --check +(no output - no whitespace errors) +``` + +--- + +> **[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`를 작성하고 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/complete.log b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/complete.log new file mode 100644 index 0000000..fd7fd44 --- /dev/null +++ b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/complete.log @@ -0,0 +1,46 @@ +# Complete - m-us-market-expansion/01_market_foundation + +## 완료 일시 + +2026-06-06 + +## 요약 + +US market foundation fixture vocabulary follow-up까지 2회 리뷰 루프로 완료했으며 최종 판정은 PASS다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | FAIL | US watchlist/USD/ETF fixture 존재를 테스트가 정확히 잠그지 못해 follow-up 필요 | +| `plan_local_G06_1.log` | `code_review_local_G06_1.log` | PASS | KIS provider symbols fixture test가 required US instruments와 watchlist symbols를 exact assertion으로 검증 | + +## 구현/정리 내용 + +- `TestProviderSymbolsFixtureVocabulary`가 `NASDAQ:AAPL`과 `NASDAQ:SPY`의 market, venue, symbol, currency, asset_type, KIS provider symbol을 필수 row 단위로 검증하도록 보강했다. +- watchlist selector가 `AAPL`과 `SPY`를 포함해야 통과하도록 검증을 추가했다. +- 1차 구현의 domain/contract/storage/socket foundation과 follow-up fixture 검증이 모두 PASS 상태로 정리되었다. + +## 최종 검증 + +- `go test -count=1 -run TestProviderSymbolsFixtureVocabulary ./services/worker/internal/providers/kis` - PASS; `ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.003s` +- `go test -count=1 ./services/worker/internal/providers/kis` - PASS; `ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.015s` +- `go test -count=1 ./services/worker/...` - PASS; all worker packages passed, no-test packages reported as `[no test files]` +- `bin/test` - PASS; workspace Go and Flutter tests completed with `All tests passed!` +- `git diff --check` - PASS; no whitespace errors + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Completed task ids: + - `us-instruments`: PASS; evidence=`agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_local_G06_1.log`, `agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_local_G06_1.log`; verification=`go test -count=1 ./services/worker/internal/providers/kis`, `go test -count=1 ./services/worker/...`, `bin/test` + - `us-calendar`: PASS; evidence=`agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_cloud_G07_0.log`, `agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/code_review_cloud_G07_0.log`; verification=`go test -count=1 ./packages/domain/...`, `bin/test` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_cloud_G07_0.log b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_cloud_G07_0.log new file mode 100644 index 0000000..dbad985 --- /dev/null +++ b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_cloud_G07_0.log @@ -0,0 +1,248 @@ + + +# Implementation Plan - US_FOUNDATION + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 결정, 검증 출력으로 채우는 것이다. 검증을 실행하고 실제 stdout/stderr를 붙이며, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret 준비, 또는 계획 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 쓰고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 재실행으로 모을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. finalization, log rename, `complete.log`, archive 이동은 code-review 전용이다. + +## 배경 + +US Market Expansion은 한국장 market data 구조를 복제하지 않고 market metadata와 provider-neutral instrument model을 확장해야 한다. 현재 `MarketUS`, `VenueNASDAQ`, `VenueNYSE`, `CurrencyUSD` enum은 있지만 instrument에는 ETF/개별주를 구분할 `asset_type`이 없고, persistence/contract도 같은 정보를 보존하지 않는다. 이 선행 작업은 뒤의 KIS US import, shared query, US backtest가 같은 model을 쓰게 하는 foundation이다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 템플릿은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`와 동일하며, 구현 에이전트는 채팅으로 직접 질문하지 않는다. code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-instruments`: 미국장 instrument identity와 provider symbol mapping + - `us-calendar`: 미국장 timezone/currency/venue 기준 +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/private/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/trading-expansion/PHASE.md` +- `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- `agent-test/local/rules.md` +- `agent-test/local/domain-model-smoke.md` +- `agent-test/local/contracts-smoke.md` +- `agent-test/local/worker-smoke.md` +- `packages/domain/market/types.go` +- `packages/domain/market/types_test.go` +- `packages/contracts/proto/alt/v1/common.proto` +- `packages/contracts/proto/alt/v1/market.proto` +- `services/worker/internal/storage/ports.go` +- `services/worker/internal/storage/postgres/migrations/000001_worker_backbone.up.sql` +- `services/worker/internal/storage/postgres/queries/queries.sql` +- `services/worker/internal/storage/postgres/mapping.go` +- `services/worker/internal/storage/postgres/store.go` +- `services/worker/internal/storage/postgres/mapping_test.go` +- `services/worker/internal/storage/postgres/store_test.go` +- `services/worker/testdata/providers/kis/provider_symbols.sample.json` +- `services/worker/testdata/providers/kis/daily_bars_normalized.expected.json` + +### 테스트 환경 규칙 + +`test_env=local`. `agent-test/local/rules.md`는 존재하고 읽었다. 적용 profile은 domain-model-smoke, contracts-smoke, worker-smoke다. 적용 명령은 `go test ./packages/domain/...`, `.proto` 변경 시 `bin/contracts-gen`, `bin/contracts-check`, generated Go 변경 시 `go test ./packages/contracts/gen/go/...`, worker storage/schema 변경 시 `go test ./services/worker/...`와 `bin/worker-storage-check`, milestone confidence용 `bin/test`다. storage integration은 `DATABASE_URL` 또는 Docker-backed infra가 없으면 실행 차단/skip 사유를 review에 기록한다. + +### 테스트 커버리지 공백 + +- `Instrument`의 provider symbol round-trip은 storage mapping 일부로만 간접 검증된다. `asset_type` round-trip test가 필요하다. +- US venue/currency enum은 contracts에 있지만 `asset_type` contract field는 없다. proto/generated drift 검증이 필요하다. +- provider symbol fixture는 KR instruments만 있고 US watchlist/ETF/equity metadata가 없다. fixture vocabulary test 확장이 필요하다. +- market timezone 기준은 KIS normalizer에 KST 상수로만 있다. domain-level market/venue metadata test가 필요하다. + +### 심볼 참조 + +Renamed/removed symbol: none. New symbols only: `AssetType`, `AssetTypeEquity`, `AssetTypeETF`, market/venue metadata helpers, provider capability vocabulary. + +### 분할 판단 + +Split decision policy를 먼저 평가했다. 공유 domain/contract/storage foundation, provider import, query surface, backtest smoke는 서로 다른 도메인과 risk profile을 가진다. 따라서 shared task group은 `m-us-market-expansion`이고 sibling은 `01_market_foundation`(독립), `02+01_kis_us_daily_bars`(01 의존), `03+01_shared_query_surface`(01 의존), `04+02,03_us_backtest_smoke`(02,03 의존)이다. 이 plan은 독립 선행 작업이라 predecessor 없음. + +### 범위 결정 근거 + +이 작업은 domain/contract/storage/fixture foundation만 다룬다. KIS overseas endpoint, live REST client, operator scenario, backtest execution fixture는 후속 split에서 다룬다. 실거래 주문, live trading, Flutter UI는 milestone 범위 제외와 UI gate에 따라 제외한다. + +### 빌드 등급 + +`cloud-G07`: protobuf schema, generated code, storage migration/sqlc, shared domain model이 함께 바뀌는 cross-domain foundation이라 local보다 넓은 contract/storage 리뷰가 필요하다. + +## 구현 체크리스트 + +- [ ] [US_FOUNDATION-1] domain market model에 `AssetType`과 market/venue metadata를 추가하고 KR/US fixture vocabulary를 갱신한다. 검증: watchlist fixture가 `market=us`, `venue`, `currency=USD`, `asset_type`, provider symbol을 분리해 가진다. +- [ ] [US_FOUNDATION-2] contract와 PostgreSQL persistence가 `asset_type`을 round-trip하도록 schema/query/mapping/generated output을 갱신한다. 검증: market/currency/venue/asset_type 차이가 domain model과 contracts에 일관되게 반영된다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [US_FOUNDATION-1] Domain Asset Type And Market Metadata + +문제: [packages/domain/market/types.go](/config/workspace/alt/packages/domain/market/types.go:37)의 `Instrument`는 `Market`, `Venue`, `Currency`, `ProviderSymbols`만 보존해 ETF와 개별주를 열어둘 metadata가 없다. KIS fixture [services/worker/testdata/providers/kis/provider_symbols.sample.json](/config/workspace/alt/services/worker/testdata/providers/kis/provider_symbols.sample.json:1)도 KR instruments만 가진다. + +해결 방법: + +```go +// Before packages/domain/market/types.go:37 +type Instrument struct { + ID InstrumentID + Market Market + Venue Venue + Symbol string + Name string + Currency Currency + ProviderSymbols map[string]string +} +``` + +```go +// After +type AssetType string + +const ( + AssetTypeEquity AssetType = "equity" + AssetTypeETF AssetType = "etf" +) + +type VenueMetadata struct { + Venue Venue + Market Market + Currency Currency + Timezone string +} + +type Instrument struct { + ID InstrumentID + Market Market + Venue Venue + Symbol string + Name string + Currency Currency + AssetType AssetType + ProviderSymbols map[string]string +} +``` + +수정 파일 및 체크리스트: + +- [ ] `packages/domain/market/types.go`: `AssetType`, metadata helper, `Instrument.AssetType` 추가. +- [ ] `packages/domain/market/types_test.go`: KR/US venue metadata, asset type vocabulary, KIS provider vocabulary test 추가. +- [ ] `services/worker/testdata/providers/kis/provider_symbols.sample.json`: KR instruments에 `asset_type: "equity"` 추가, US watchlist selector와 `NASDAQ:AAPL` equity, `NASDAQ:SPY` ETF fixture 추가. +- [ ] `services/worker/internal/providers/kis/daily_itemchartprice_test.go`: fixture view struct와 assertions에 `asset_type` 추가. + +테스트 작성: 작성한다. `TestMarketVenueMetadata`와 `TestProviderSymbolsFixtureVocabulary` 확장으로 US watchlist, USD, NASDAQ/NYSE, equity/etf를 검증한다. + +중간 검증: + +```bash +go test -count=1 ./packages/domain/... +go test -count=1 ./services/worker/internal/providers/kis +``` + +### [US_FOUNDATION-2] Contract And Storage Asset Type Round Trip + +문제: [packages/contracts/proto/alt/v1/market.proto](/config/workspace/alt/packages/contracts/proto/alt/v1/market.proto:9)의 `Instrument`는 `provider_symbols = 7`까지만 있고 `asset_type`이 없다. storage schema [services/worker/internal/storage/postgres/migrations/000001_worker_backbone.up.sql](/config/workspace/alt/services/worker/internal/storage/postgres/migrations/000001_worker_backbone.up.sql:1), query [services/worker/internal/storage/postgres/queries/queries.sql](/config/workspace/alt/services/worker/internal/storage/postgres/queries/queries.sql:1), mapping [services/worker/internal/storage/postgres/mapping.go](/config/workspace/alt/services/worker/internal/storage/postgres/mapping.go:54)도 같은 field를 보존하지 않는다. + +해결 방법: + +```proto +// Before packages/contracts/proto/alt/v1/market.proto:9 +message Instrument { + string id = 1; + Market market = 2; + Venue venue = 3; + string symbol = 4; + string name = 5; + Currency currency = 6; + map provider_symbols = 7; +} +``` + +```proto +// After +enum AssetType { + ASSET_TYPE_UNSPECIFIED = 0; + ASSET_TYPE_EQUITY = 1; + ASSET_TYPE_ETF = 2; +} + +message Instrument { + string id = 1; + Market market = 2; + Venue venue = 3; + string symbol = 4; + string name = 5; + Currency currency = 6; + map provider_symbols = 7; + AssetType asset_type = 8; +} +``` + +수정 파일 및 체크리스트: + +- [ ] `packages/contracts/proto/alt/v1/common.proto` 또는 `market.proto`: `AssetType` enum 추가 위치를 기존 enum ownership에 맞춰 결정한다. +- [ ] `packages/contracts/proto/alt/v1/market.proto`: `Instrument.asset_type = 8` 추가. +- [ ] `bin/contracts-gen` 실행으로 generated Go/Dart contract를 갱신한다. +- [ ] `services/worker/internal/storage/postgres/migrations/000003_instrument_asset_type.up.sql`/`.down.sql`: 기존 rows를 깨지 않도록 `asset_type TEXT NOT NULL DEFAULT ''` 또는 명시 default 정책 추가. +- [ ] `services/worker/internal/storage/postgres/queries/queries.sql`: insert/select/update에 `asset_type` 포함. +- [ ] `services/worker/internal/storage/postgres/sqlc/*.go`: sqlc generated output 갱신. +- [ ] `services/worker/internal/storage/postgres/mapping.go`: domain <-> sqlc row mapping에 `AssetType` 추가. +- [ ] `services/worker/internal/socket/market_mapping.go`: domain <-> proto mapping에 `AssetType` 추가. +- [ ] `services/worker/internal/storage/postgres/mapping_test.go`: asset_type round-trip test 추가. +- [ ] `services/worker/internal/socket/market_mapping_test.go`: proto mapping round-trip test 추가. + +테스트 작성: 작성한다. storage mapping round-trip과 socket proto mapping에서 equity/etf/unspecified를 모두 확인한다. + +중간 검증: + +```bash +bin/contracts-gen +bin/contracts-check +go test -count=1 ./packages/contracts/gen/go/... +go test -count=1 ./services/worker/internal/storage/postgres ./services/worker/internal/socket +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `packages/domain/market/types.go` | US_FOUNDATION-1 | +| `packages/domain/market/types_test.go` | US_FOUNDATION-1 | +| `services/worker/testdata/providers/kis/provider_symbols.sample.json` | US_FOUNDATION-1 | +| `services/worker/internal/providers/kis/daily_itemchartprice_test.go` | US_FOUNDATION-1 | +| `packages/contracts/proto/alt/v1/common.proto` | US_FOUNDATION-2 | +| `packages/contracts/proto/alt/v1/market.proto` | US_FOUNDATION-2 | +| `packages/contracts/gen/**` | US_FOUNDATION-2 | +| `services/worker/internal/storage/postgres/migrations/*.sql` | US_FOUNDATION-2 | +| `services/worker/internal/storage/postgres/queries/queries.sql` | US_FOUNDATION-2 | +| `services/worker/internal/storage/postgres/sqlc/*.go` | US_FOUNDATION-2 | +| `services/worker/internal/storage/postgres/mapping.go` | US_FOUNDATION-2 | +| `services/worker/internal/storage/postgres/mapping_test.go` | US_FOUNDATION-2 | +| `services/worker/internal/socket/market_mapping.go` | US_FOUNDATION-2 | +| `services/worker/internal/socket/market_mapping_test.go` | US_FOUNDATION-2 | + +## 최종 검증 + +```bash +bin/contracts-gen +bin/contracts-check +go test -count=1 ./packages/domain/... +go test -count=1 ./packages/contracts/gen/go/... +go test -count=1 ./services/worker/internal/storage/postgres ./services/worker/internal/socket ./services/worker/internal/providers/kis +bin/worker-storage-check +bin/test +git diff --check +``` + +`go test` cache는 허용하지 않는다. `bin/worker-storage-check`가 infra 준비 부족으로 실행 불가하면 `command -v`/환경 확인 출력과 함께 차단 또는 skip 근거를 review에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_local_G06_1.log b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_local_G06_1.log new file mode 100644 index 0000000..1bf7cc8 --- /dev/null +++ b/agent-task/archive/2026/06/m-us-market-expansion/01_market_foundation/plan_local_G06_1.log @@ -0,0 +1,143 @@ + + +# Implementation Plan - REVIEW_US_FOUNDATION + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 결정, 검증 출력으로 채우는 것이다. 검증을 실행하고 실제 stdout/stderr를 붙이며, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret 준비, 또는 계획 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 쓰고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 재실행으로 모을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. finalization, log rename, `complete.log`, archive 이동은 code-review 전용이다. + +## 배경 + +이 리뷰는 `US_FOUNDATION` 1차 구현에서 production contract/storage round-trip과 검증 명령은 통과했지만, provider symbols fixture 테스트가 계획의 핵심 조건을 충분히 잠그지 못한 문제를 다룬다. 현재 fixture에는 `AAPL`/`SPY`, `USD`, `equity`/`etf`가 들어 있지만 테스트는 허용 enum 집합만 확인하므로 US/ETF row가 사라져도 통과할 수 있다. 이 follow-up은 fixture vocabulary 검증만 강화한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 템플릿은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`와 동일하며, 구현 에이전트는 채팅으로 직접 질문하지 않는다. code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-instruments`: 미국장 instrument identity와 provider symbol mapping + - `us-calendar`: 미국장 timezone/currency/venue 기준 +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/private/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/code-review/SKILL.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-test/local/rules.md` +- `agent-test/local/worker-smoke.md` +- `agent-ops/rules/project/domain/worker/rules.md` +- `agent-task/m-us-market-expansion/01_market_foundation/plan_cloud_G07_0.log` +- `agent-task/m-us-market-expansion/01_market_foundation/code_review_cloud_G07_0.log` +- `services/worker/internal/providers/kis/daily_itemchartprice_test.go` +- `services/worker/testdata/providers/kis/provider_symbols.sample.json` +- `packages/domain/market/types_test.go` + +### 테스트 환경 규칙 + +`test_env=local`. `agent-test/local/rules.md`와 `agent-test/local/worker-smoke.md`를 읽었다. 적용 명령은 worker test 변경 기준 `go test -count=1 ./services/worker/internal/providers/kis`, 필요 시 `go test -count=1 ./services/worker/...`, milestone confidence용 `bin/test`, whitespace 확인용 `git diff --check`다. 이 follow-up은 `.proto`, generated output, sqlc output을 바꾸지 않는다. + +### 테스트 커버리지 공백 + +- `TestProviderSymbolsFixtureVocabulary`는 venue/currency/asset_type이 허용 목록에 속하는지만 확인한다. `AAPL`/`SPY` US watchlist coverage, `NASDAQ`/`USD`, `equity`와 `etf`의 동시 존재, KIS provider symbol mapping을 필수 row 단위로 검증하지 않는다. + +### 심볼 참조 + +Renamed/removed symbol: none. + +### 분할 판단 + +기존 split task `m-us-market-expansion/01_market_foundation` 안의 리뷰 follow-up이다. 변경 범위가 단일 worker fixture test와 관련 JSON fixture 검증에 한정되어 새 subtask split은 만들지 않는다. sibling `02+01_kis_us_daily_bars`, `03+01_shared_query_surface`, `04+02,03_us_backtest_smoke`의 active state는 건드리지 않는다. + +### 범위 결정 근거 + +Production `AssetType` domain/contract/storage/socket mapping, migration, generated contract output은 1차 구현과 재검증에서 통과했으므로 이 follow-up 범위에서 제외한다. fixture 내용 자체는 이미 목표 데이터를 담고 있으므로, 테스트를 강화하다가 fixture 불일치가 드러난 경우에만 `provider_symbols.sample.json`을 최소 수정한다. KIS live import, CLI query surface, backtest smoke, roadmap 문서는 제외한다. + +### 빌드 등급 + +`local-G06`: repo 안에서 결정적으로 재현 가능한 test coverage follow-up이고 변경 파일이 bounded worker test/fixture에 한정된다. + +## 구현 체크리스트 + +- [ ] [REVIEW_US_FOUNDATION-1] KIS provider symbols fixture test가 US watchlist와 `NASDAQ`/`USD`/`equity`/`etf`/KIS provider symbol mapping을 필수 row 단위로 검증하도록 강화한다. 검증: `AAPL`/`SPY` 또는 ETF row가 제거되면 테스트가 실패해야 한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REVIEW_US_FOUNDATION-1] Strengthen US Fixture Vocabulary Assertions + +문제: [services/worker/internal/providers/kis/daily_itemchartprice_test.go](/config/workspace/alt/services/worker/internal/providers/kis/daily_itemchartprice_test.go:258)의 `TestProviderSymbolsFixtureVocabulary`는 fixture instrument가 허용 venue/currency/asset_type 중 하나인지 확인하지만, [services/worker/testdata/providers/kis/provider_symbols.sample.json](/config/workspace/alt/services/worker/testdata/providers/kis/provider_symbols.sample.json:45)의 US rows가 실제로 존재해야 한다는 조건은 확인하지 않는다. + +해결 방법: + +```go +// Before services/worker/internal/providers/kis/daily_itemchartprice_test.go:258 +for i, inst := range fx.Instruments { + v := market.Venue(inst.Venue) + if v != market.VenueKRX && v != market.VenueNASDAQ && v != market.VenueNYSE { + t.Errorf("instrument[%d] unexpected venue: %q", i, inst.Venue) + } + c := market.Currency(inst.Currency) + if c != market.CurrencyKRW && c != market.CurrencyUSD { + t.Errorf("instrument[%d] unexpected currency: %q", i, inst.Currency) + } + at := market.AssetType(inst.AssetType) + if at != market.AssetTypeEquity && at != market.AssetTypeETF { + t.Errorf("instrument[%d] unexpected asset type: %q", i, inst.AssetType) + } +} +``` + +```go +// After +required := map[string]struct { + market market.Market + venue market.Venue + symbol string + currency market.Currency + assetType market.AssetType +}{ + "NASDAQ:AAPL": {market.MarketUS, market.VenueNASDAQ, "AAPL", market.CurrencyUSD, market.AssetTypeEquity}, + "NASDAQ:SPY": {market.MarketUS, market.VenueNASDAQ, "SPY", market.CurrencyUSD, market.AssetTypeETF}, +} +``` + +수정 파일 및 체크리스트: + +- [ ] `services/worker/internal/providers/kis/daily_itemchartprice_test.go`: fixture instruments를 id map으로 만들고 `NASDAQ:AAPL`, `NASDAQ:SPY`의 market/venue/symbol/currency/asset_type/provider symbol 값을 정확히 assert한다. +- [ ] `services/worker/internal/providers/kis/daily_itemchartprice_test.go`: watchlist selector가 `AAPL`과 `SPY`를 포함하는지 assert한다. +- [ ] `services/worker/testdata/providers/kis/provider_symbols.sample.json`: 강화된 assertion과 fixture가 불일치할 때만 최소 수정한다. + +테스트 작성: 작성한다. `TestProviderSymbolsFixtureVocabulary`에 exact US fixture expectations와 watchlist symbol coverage assertion을 추가한다. + +중간 검증: + +```bash +go test -count=1 ./services/worker/internal/providers/kis +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `services/worker/internal/providers/kis/daily_itemchartprice_test.go` | REVIEW_US_FOUNDATION-1 | +| `services/worker/testdata/providers/kis/provider_symbols.sample.json` | REVIEW_US_FOUNDATION-1 | + +## 최종 검증 + +```bash +go test -count=1 ./services/worker/internal/providers/kis +go test -count=1 ./services/worker/... +bin/test +git diff --check +``` + +`go test` cache는 허용하지 않는다. `bin/test`는 workspace confidence 확인이며 일부 Go package가 cache를 출력해도, 앞선 `-count=1` worker 검증이 이 follow-up의 fresh 검증이다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-us-market-expansion/02+01_kis_us_daily_bars/CODE_REVIEW-cloud-G07.md b/agent-task/m-us-market-expansion/02+01_kis_us_daily_bars/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..4c8ff6e --- /dev/null +++ b/agent-task/m-us-market-expansion/02+01_kis_us_daily_bars/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,119 @@ + + +# Code Review Reference - KIS_US_DAILY + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Complete implementation-owned sections, then stop with active files in place and report ready for review. Do not finalize, archive, write `complete.log`, ask the user directly, or call `request_user_input`. + +## 개요 + +date=2026-06-06 +task=m-us-market-expansion/02+01_kis_us_daily_bars, plan=0, tag=KIS_US_DAILY + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-daily-bars`: KIS를 초기 provider로 사용해 미국장 일봉 데이터를 normalized bar model로 저장 +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +각 항목의 구현을 실제 소스 파일과 대조하고 검증 출력이 코드와 일치하는지 확인한다. PASS이면 완료 이벤트 메타데이터만 보고하고 roadmap 수정은 런타임 책임이다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [KIS_US_DAILY-1] Provider Capability Gate | [ ] | +| [KIS_US_DAILY-2] KIS Overseas Daily Normalization | [ ] | +| [KIS_US_DAILY-3] Worker Import Path For US Watchlists | [ ] | + +## 구현 체크리스트 + +- [ ] [KIS_US_DAILY-1] provider capability를 도입해 KIS가 KR/US daily bars 지원 범위를 선언하고 unsupported market/venue 조합을 import 전에 거절한다. 검증: 한국장 pipeline을 깨지 않고 provider capability가 미국장 daily bar import를 허용한다. +- [ ] [KIS_US_DAILY-2] KIS US daily bar fixture decoder/normalizer를 추가해 USD/NASDAQ/NYSE bar를 normalized model로 만든다. +- [ ] [KIS_US_DAILY-3] worker socket/job/importer 경로가 US watchlist import request를 KIS provider로 전달하고 instrument/bar upsert count를 보존한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. + +- [ ] 판정을 append한다. +- [ ] active review/plan 파일을 `*.log`로 아카이브한다. +- [ ] `.gitignore` Agent-Ops block을 확인한다. +- [ ] PASS이면 `complete.log`를 작성하고 task directory를 archive로 이동한다. +- [ ] PASS이면 완료 이벤트 메타데이터를 보고한다. +- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- provider capability가 `provider != market` 원칙을 지키는지 확인한다. +- KIS US fixture가 endpoint/TR_ID/field mapping을 test로 잠그는지 확인한다. +- KR existing daily import tests가 계속 통과하는지 확인한다. +- live secret 없이 deterministic fixture/unit 검증만으로 PASS를 주장하는지 확인한다. + +## 검증 결과 + +### KIS_US_DAILY-1 중간 검증 +```text +$ go test -count=1 ./packages/domain/... ./services/worker/internal/jobs +(output) +``` + +### KIS_US_DAILY-2 중간 검증 +```text +$ go test -count=1 ./services/worker/internal/providers/kis +(output) +``` + +### KIS_US_DAILY-3 중간 검증 +```text +$ go test -count=1 ./services/worker/internal/socket ./services/worker/internal/marketdata/importer ./services/worker/internal/jobs +(output) +``` + +### 최종 검증 +```text +$ go test -count=1 ./packages/domain/... +(output) + +$ go test -count=1 ./services/worker/internal/providers/kis ./services/worker/internal/marketdata/importer ./services/worker/internal/jobs ./services/worker/internal/socket +(output) + +$ go test -count=1 ./services/worker/... +(output) + +$ bin/test +(output) + +$ git diff --check +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/agent-task/m-us-market-expansion/02+01_kis_us_daily_bars/PLAN-cloud-G07.md b/agent-task/m-us-market-expansion/02+01_kis_us_daily_bars/PLAN-cloud-G07.md new file mode 100644 index 0000000..8933b8f --- /dev/null +++ b/agent-task/m-us-market-expansion/02+01_kis_us_daily_bars/PLAN-cloud-G07.md @@ -0,0 +1,180 @@ + + +# Implementation Plan - KIS_US_DAILY + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 결정, 검증 출력으로 채우는 것이다. `01_market_foundation`의 `complete.log`가 없으면 구현을 시작하지 말고 active review stub에 blocker를 기록한다. 사용자-only 결정이 필요하면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 쓰고 멈춘다. 직접 사용자에게 질문하지 않는다. finalization은 code-review 전용이다. + +## 배경 + +초기 US data provider는 KIS로 고정됐지만, provider가 곧 market을 뜻하지 않도록 capability boundary를 유지해야 한다. 현재 KIS daily chart code는 domestic endpoint와 KST timestamp 전제만 갖는다. 이 작업은 KIS의 US daily bar path를 fixture-first로 추가하고, provider capability가 KR/US 지원 범위를 명시하게 한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 후속 에이전트가 docs 확인, fixture 수집, 명령 재실행으로 해결할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다. code-review가 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-daily-bars`: KIS를 초기 provider로 사용해 미국장 일봉 데이터를 normalized bar model로 저장 +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/domain-model-smoke.md` +- `agent-test/local/worker-smoke.md` +- `packages/domain/market/types.go` +- `services/worker/internal/providers/kis/daily_itemchartprice.go` +- `services/worker/internal/providers/kis/daily_itemchartprice_test.go` +- `services/worker/internal/marketdata/importer/importer.go` +- `services/worker/internal/marketdata/importer/importer_test.go` +- `services/worker/internal/jobs/marketdata_jobs.go` +- `services/worker/internal/jobs/marketdata_jobs_test.go` +- `services/worker/internal/socket/market.go` +- `services/worker/internal/socket/market_test.go` +- `services/worker/testdata/providers/kis/daily_itemchartprice_request.sample.json` +- `services/worker/testdata/providers/kis/daily_itemchartprice_response.sample.json` + +### 테스트 환경 규칙 + +`test_env=local`. worker-smoke와 domain-model-smoke를 적용한다. worker 변경의 기본 검증은 `go test ./services/worker/...`, provider/importer/job/socket focused 검증은 `go test -count=1`로 실행한다. live KIS credential이 필요한 검증은 범위 밖이며 tracked file에 secret을 쓰지 않는다. + +### 테스트 커버리지 공백 + +- KIS normalizer는 domestic response fixture만 검증한다. +- importer/job/socket은 provider/selector/date pass-through는 검증하지만 provider capability로 market/venue 지원 여부를 검증하지 않는다. +- US daily bars가 USD/NASDAQ timestamp/currency로 저장되는 integration path가 없다. + +### 심볼 참조 + +Renamed/removed symbol: none. New symbols expected: KIS overseas daily request/response structs, KIS daily capability declaration, capability validation helper. + +### 분할 판단 + +Shared task group: `m-us-market-expansion`. This subtask is `02+01_kis_us_daily_bars` and depends on `01_market_foundation`. Predecessor status at plan creation: missing active/archived `complete.log`; implementation must wait for `agent-task/m-us-market-expansion/01_market_foundation/complete.log` or matching archived complete log. + +### 범위 결정 근거 + +이 작업은 KIS US daily bars와 provider capability만 다룬다. shared query fixtures와 backtest smoke는 후속 split에서 다룬다. live credential smoke와 real REST 호출은 private/env 준비가 필요하므로 fixture/unit 중심으로 제한한다. + +### 빌드 등급 + +`cloud-G07`: external provider shape, capability boundary, worker import pipeline이 연결되어 있고 KIS API fixture 해석 리스크가 있다. + +## 구현 체크리스트 + +- [ ] [KIS_US_DAILY-1] provider capability를 도입해 KIS가 KR/US daily bars 지원 범위를 선언하고 unsupported market/venue 조합을 import 전에 거절한다. 검증: 한국장 pipeline을 깨지 않고 provider capability가 미국장 daily bar import를 허용한다. +- [ ] [KIS_US_DAILY-2] KIS US daily bar fixture decoder/normalizer를 추가해 USD/NASDAQ/NYSE bar를 normalized model로 만든다. +- [ ] [KIS_US_DAILY-3] worker socket/job/importer 경로가 US watchlist import request를 KIS provider로 전달하고 instrument/bar upsert count를 보존한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +`02+01_kis_us_daily_bars`는 디렉터리명 기준으로 `01_market_foundation`의 `complete.log`가 필요하다. 추가 hidden dependency를 만들지 않는다. + +### [KIS_US_DAILY-1] Provider Capability Gate + +문제: [services/worker/internal/jobs/marketdata_jobs.go](/config/workspace/alt/services/worker/internal/jobs/marketdata_jobs.go:106)은 `expectedProvider` 문자열만 비교하고, provider가 특정 market/venue/daily-bars capability를 지원하는지 확인하지 않는다. Kiwoom 같은 후속 provider가 KR만 지원해도 같은 boundary에 붙일 방법이 약하다. + +해결 방법: + +```go +// Before services/worker/internal/jobs/marketdata_jobs.go:112 +if market.Provider(p.Provider) != expectedProvider { + return fmt.Errorf("daily bar import payload: provider %q does not match expected %q", p.Provider, expectedProvider) +} +``` + +```go +// After +if err := capability.RequireDailyBars(market.Provider(p.Provider), p.selector()); err != nil { + return err +} +``` + +수정 파일 및 체크리스트: + +- [ ] `packages/domain/market/types.go` 또는 worker provider package: provider capability vocabulary와 matcher 추가. +- [ ] `services/worker/internal/providers/kis/*`: KIS capability가 KR/KRX와 US/NASDAQ/NYSE daily bars를 지원한다고 선언. +- [ ] `services/worker/internal/jobs/marketdata_jobs.go`: provider mismatch와 unsupported market/venue/capability를 분리해 validation. +- [ ] `services/worker/internal/jobs/marketdata_jobs_test.go`: KIS US 허용, unsupported provider/venue 거절, KR 기존 payload 유지 test 추가. + +테스트 작성: 작성한다. Kiwoom 구현은 하지 않지만 test fixture에서 KR-only fake capability가 US를 거절하는 사례를 둔다. + +중간 검증: + +```bash +go test -count=1 ./packages/domain/... ./services/worker/internal/jobs +``` + +### [KIS_US_DAILY-2] KIS Overseas Daily Normalization + +문제: [services/worker/internal/providers/kis/daily_itemchartprice.go](/config/workspace/alt/services/worker/internal/providers/kis/daily_itemchartprice.go:14)는 domestic endpoint/TR_ID만 갖고, [lines 89-97](/config/workspace/alt/services/worker/internal/providers/kis/daily_itemchartprice.go:89)는 KST/KRW 전제를 적용한다. + +해결 방법: fixture-first로 overseas daily request/response structs를 추가한다. endpoint/TR_ID는 구현 시 official KIS docs 또는 captured fixture와 대조해 확정한다. 검색 후보는 `/uapi/overseas-price/v1/quotations/dailyprice`, `HHDFS76240000`이지만, 코드에는 fixture assertion으로 잠근다. timestamp는 domain venue metadata의 timezone을 사용하고 price currency는 instrument currency를 사용한다. + +수정 파일 및 체크리스트: + +- [ ] `services/worker/internal/providers/kis/daily_itemchartprice.go`: domestic/overseas decoder와 normalizer를 분리하거나 공통 `NormalizeDailyBars`가 venue metadata를 사용하도록 확장. +- [ ] `services/worker/testdata/providers/kis/*`: US daily request/response/expected normalized fixture 추가. +- [ ] `services/worker/internal/providers/kis/daily_itemchartprice_test.go`: USD/NASDAQ/NYSE fixture decode와 normalized bar assertion 추가. + +테스트 작성: 작성한다. AAPL 또는 SPY fixture로 `Instrument.ID=NASDAQ:AAPL`, `Currency=USD`, `AssetType` 보존, timestamp timezone을 검증한다. + +중간 검증: + +```bash +go test -count=1 ./services/worker/internal/providers/kis +``` + +### [KIS_US_DAILY-3] Worker Import Path For US Watchlists + +문제: [services/worker/internal/socket/market.go](/config/workspace/alt/services/worker/internal/socket/market.go:69)은 US enum을 변환할 수 있지만 tests의 `validImportRequest()`는 KR/KRX만 다룬다. importer tests도 KR sample data 중심이다. + +해결 방법: US watchlist request를 socket/job/importer tests에 추가한다. provider가 US instrument와 USD bars를 반환하면 importer가 instrument first, bars after 순서로 storage upsert count를 보존해야 한다. + +수정 파일 및 체크리스트: + +- [ ] `services/worker/internal/socket/market_test.go`: `Market_US` + `Venue_NASDAQ` import conversion test 추가. +- [ ] `services/worker/internal/marketdata/importer/importer_test.go`: US sample bars와 asset_type persistence path 추가. +- [ ] `services/worker/internal/jobs/marketdata_jobs_test.go`: payload `market=US`, `venue=NASDAQ`, `symbols=["AAPL"]` dispatch test 추가. + +테스트 작성: 작성한다. live provider 호출 없이 fake provider/store로 deterministic count와 request conversion을 검증한다. + +중간 검증: + +```bash +go test -count=1 ./services/worker/internal/socket ./services/worker/internal/marketdata/importer ./services/worker/internal/jobs +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `packages/domain/market/types.go` | KIS_US_DAILY-1 | +| `services/worker/internal/providers/kis/daily_itemchartprice.go` | KIS_US_DAILY-1, KIS_US_DAILY-2 | +| `services/worker/internal/providers/kis/daily_itemchartprice_test.go` | KIS_US_DAILY-2 | +| `services/worker/testdata/providers/kis/*` | KIS_US_DAILY-2 | +| `services/worker/internal/jobs/marketdata_jobs.go` | KIS_US_DAILY-1 | +| `services/worker/internal/jobs/marketdata_jobs_test.go` | KIS_US_DAILY-1, KIS_US_DAILY-3 | +| `services/worker/internal/socket/market_test.go` | KIS_US_DAILY-3 | +| `services/worker/internal/marketdata/importer/importer_test.go` | KIS_US_DAILY-3 | + +## 최종 검증 + +```bash +go test -count=1 ./packages/domain/... +go test -count=1 ./services/worker/internal/providers/kis ./services/worker/internal/marketdata/importer ./services/worker/internal/jobs ./services/worker/internal/socket +go test -count=1 ./services/worker/... +bin/test +git diff --check +``` + +`go test` cache는 허용하지 않는다. live KIS credential 검증은 이 plan의 pass condition이 아니다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-us-market-expansion/03+01_shared_query_surface/CODE_REVIEW-local-G06.md b/agent-task/m-us-market-expansion/03+01_shared_query_surface/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..d922ac0 --- /dev/null +++ b/agent-task/m-us-market-expansion/03+01_shared_query_surface/CODE_REVIEW-local-G06.md @@ -0,0 +1,104 @@ + + +# Code Review Reference - US_QUERY + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is mandatory.** +> Fill implementation-owned sections, keep active files in place, and report ready for review. Do not ask the user directly or finalize the loop. + +## 개요 + +date=2026-06-06 +task=m-us-market-expansion/03+01_shared_query_surface, plan=0, tag=US_QUERY + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `shared-query`: 한국장과 미국장이 같은 query/contract pattern을 공유 +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +각 항목의 구현과 검증 출력을 대조한다. PASS이면 completion metadata만 보고하고 roadmap update는 런타임에 맡긴다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [US_QUERY-1] API And Worker US Query Contract Tests | [ ] | +| [US_QUERY-2] Operator US Headless Query Fixture | [ ] | + +## 구현 체크리스트 + +- [ ] [US_QUERY-1] API/worker socket tests가 `Market_US`와 provider filter로 US instruments를 같은 ListInstruments contract에서 조회하는지 검증한다. 검증: market selector만 바꿔도 공통 query path를 사용한다. +- [ ] [US_QUERY-2] CLI headless fixture에 US market data status query를 추가하고 expected JSONL을 잠근다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] 판정을 append한다. +- [ ] active review/plan 파일을 `*.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] PASS이면 완료 이벤트 메타데이터를 보고한다. +- [ ] WARN/FAIL이면 후속 plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- 새 query API를 만들지 않고 기존 contract path를 쓰는지 확인한다. +- `market: us`만 바꿔 동일한 operator workflow가 실행되는지 확인한다. +- expected JSONL이 실제 runner output과 일치하는지 확인한다. + +## 검증 결과 + +### US_QUERY-1 중간 검증 +```text +$ go test -count=1 ./services/api/internal/socket ./services/worker/internal/socket +(output) +``` + +### US_QUERY-2 중간 검증 +```text +$ go test -count=1 ./apps/cli/internal/operator +(output) +``` + +### 최종 검증 +```text +$ go test -count=1 ./services/api/internal/socket ./services/worker/internal/socket ./apps/cli/internal/operator +(output) + +$ go test -count=1 ./services/api/... ./services/worker/... +(output) + +$ bin/test +(output) + +$ git diff --check +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/agent-task/m-us-market-expansion/03+01_shared_query_surface/PLAN-local-G06.md b/agent-task/m-us-market-expansion/03+01_shared_query_surface/PLAN-local-G06.md new file mode 100644 index 0000000..227184d --- /dev/null +++ b/agent-task/m-us-market-expansion/03+01_shared_query_surface/PLAN-local-G06.md @@ -0,0 +1,144 @@ + + +# Implementation Plan - US_QUERY + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. `01_market_foundation` 완료 전에는 구현하지 않는다. 사용자-only 결정이 필요하면 review stub의 `사용자 리뷰 요청` 섹션에 기록하고 멈춘다. finalization은 code-review 전용이다. + +## 배경 + +US instruments와 bars가 들어와도 operator/API/worker read surface가 KR과 다른 경로를 쓰면 milestone의 shared-query 목표를 만족하지 못한다. 현재 API와 worker는 US enum을 대부분 받아들이지만, headless scenario와 focused tests는 KR 중심이다. 이 작업은 market selector만 바꿔 같은 query pattern을 쓰는 것을 fixtures와 tests로 잠근다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 사용자에게 직접 질문하지 않는다. code-review가 실제 user-review stop 여부를 판단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `shared-query`: 한국장과 미국장이 같은 query/contract pattern을 공유 +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/api-smoke.md` +- `agent-test/local/worker-smoke.md` +- `agent-test/local/operations-smoke.md` +- `services/api/internal/socket/market.go` +- `services/api/internal/socket/market_test.go` +- `services/worker/internal/socket/market.go` +- `services/worker/internal/socket/market_mapping.go` +- `services/worker/internal/socket/market_test.go` +- `apps/cli/internal/operator/scenario.go` +- `apps/cli/internal/operator/runner.go` +- `apps/cli/internal/operator/scenario_test.go` +- `apps/cli/internal/operator/runner_market_test.go` +- `apps/cli/testdata/operator/market_data_status_query.yaml` +- `apps/cli/testdata/operator/expected/market_data_status_query.jsonl` + +### 테스트 환경 규칙 + +`test_env=local`. api-smoke, worker-smoke, operations-smoke를 적용한다. API socket boundary는 `go test ./services/api/...`, worker socket은 `go test ./services/worker/...`, CLI operator는 package focused `go test -count=1 ./apps/cli/internal/operator`, milestone smoke는 `bin/test`를 적용한다. + +### 테스트 커버리지 공백 + +- API `isValidMarketFilter`와 `isValidVenueFilter`는 US enum을 허용하지만 US import/list forwarding test가 없다. +- Worker `filterInstruments`는 provider + market filter를 다루지만 US-only list/query fixture가 약하다. +- CLI operator는 이 작업 시작 전 작은 패치로 `nasdaq`/`nyse` venue validation/mapping을 추가했지만, US end-to-end headless scenario fixture는 아직 없다. + +### 심볼 참조 + +Renamed/removed symbol: none. New fixture/action symbols only. + +### 분할 판단 + +Shared task group: `m-us-market-expansion`. This subtask is `03+01_shared_query_surface` and depends on `01_market_foundation`. Predecessor status at plan creation: missing complete.log. It can run in parallel with `02+01_kis_us_daily_bars` only after 01 completes. + +### 범위 결정 근거 + +이 작업은 read/query surface와 headless scenario만 다룬다. KIS provider fetch implementation은 02, US backtest result smoke는 04에서 다룬다. Flutter UI는 operational UI gate에 따라 제외한다. + +### 빌드 등급 + +`local-G06`: API/worker/CLI 경계가 여러 개지만 대부분 existing enum pass-through와 test/fixture 강화이며 deterministic local tests로 리뷰 가능하다. + +## 구현 체크리스트 + +- [ ] [US_QUERY-1] API/worker socket tests가 `Market_US`와 provider filter로 US instruments를 같은 ListInstruments contract에서 조회하는지 검증한다. 검증: market selector만 바꿔도 공통 query path를 사용한다. +- [ ] [US_QUERY-2] CLI headless fixture에 US market data status query를 추가하고 expected JSONL을 잠근다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +`03+01_shared_query_surface`는 디렉터리명 기준으로 `01_market_foundation`의 `complete.log`가 필요하다. 추가 dependency를 만들지 않는다. + +### [US_QUERY-1] API And Worker US Query Contract Tests + +문제: [services/api/internal/socket/market.go](/config/workspace/alt/services/api/internal/socket/market.go:159)는 US market/venue enum을 허용하지만 test는 KR import fixture 중심이다. [services/worker/internal/socket/market.go](/config/workspace/alt/services/worker/internal/socket/market.go:148)의 `filterInstruments`는 provider+market filter를 지원하지만 US read contract가 충분히 잠겨 있지 않다. + +해결 방법: API forwarding tests와 worker socket tests에 `Market_US`, `Venue_NASDAQ`, `ProviderSymbols["kis"]`, `AssetType`이 포함된 instruments를 추가한다. implementation code는 existing contract path를 우선 재사용하고, 새 request type을 만들지 않는다. + +수정 파일 및 체크리스트: + +- [ ] `services/api/internal/socket/market_test.go`: US `ImportDailyBarsRequest` validation/forwarding test 추가. +- [ ] `services/worker/internal/socket/market_test.go`: US list instruments provider filter와 US list bars response mapping test 추가. +- [ ] `services/worker/internal/socket/market_mapping_test.go`: asset_type 포함 proto mapping 검증이 01에서 누락됐다면 보강. + +테스트 작성: 작성한다. 새 public contract path가 아니라 기존 path의 US coverage를 추가한다. + +중간 검증: + +```bash +go test -count=1 ./services/api/internal/socket ./services/worker/internal/socket +``` + +### [US_QUERY-2] Operator US Headless Query Fixture + +문제: [apps/cli/internal/operator/scenario.go](/config/workspace/alt/apps/cli/internal/operator/scenario.go:119)와 [runner.go](/config/workspace/alt/apps/cli/internal/operator/runner.go:62)는 US venue string을 받을 수 있어야 한다. 현재 작은 선행 패치로 `nasdaq`/`nyse` mapping은 추가됐지만, [apps/cli/testdata/operator/market_data_status_query.yaml](/config/workspace/alt/apps/cli/testdata/operator/market_data_status_query.yaml:1)은 KR-only status workflow다. + +해결 방법: `us_market_data_status_query.yaml`과 expected JSONL을 추가한다. Steps는 `import_daily_bars` with `market: us`, `venue: nasdaq`, `symbols: ["AAPL"]`, `list_instruments market: us`, `list_bars instrument_id: NASDAQ:AAPL` 순서로 구성한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/cli/testdata/operator/us_market_data_status_query.yaml`: US query fixture 추가. +- [ ] `apps/cli/testdata/operator/expected/us_market_data_status_query.jsonl`: expected output 추가. +- [ ] `apps/cli/testdata/operator/headless_validation.md`: fixture matrix에 US query row 추가. +- [ ] `apps/cli/internal/operator/runner_market_test.go`: fixture run test 추가 또는 기존 fake API test 확장. + +테스트 작성: 작성한다. Fake API로 transport 없이 runner output과 call order를 검증한다. + +중간 검증: + +```bash +go test -count=1 ./apps/cli/internal/operator +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `services/api/internal/socket/market_test.go` | US_QUERY-1 | +| `services/worker/internal/socket/market_test.go` | US_QUERY-1 | +| `services/worker/internal/socket/market_mapping_test.go` | US_QUERY-1 | +| `apps/cli/testdata/operator/us_market_data_status_query.yaml` | US_QUERY-2 | +| `apps/cli/testdata/operator/expected/us_market_data_status_query.jsonl` | US_QUERY-2 | +| `apps/cli/testdata/operator/headless_validation.md` | US_QUERY-2 | +| `apps/cli/internal/operator/runner_market_test.go` | US_QUERY-2 | + +## 최종 검증 + +```bash +go test -count=1 ./services/api/internal/socket ./services/worker/internal/socket ./apps/cli/internal/operator +go test -count=1 ./services/api/... ./services/worker/... +bin/test +git diff --check +``` + +`go test` cache는 허용하지 않는다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/CODE_REVIEW-local-G06.md b/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..8342c1e --- /dev/null +++ b/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/CODE_REVIEW-local-G06.md @@ -0,0 +1,112 @@ + + +# Code Review Reference - US_BACKTEST + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is mandatory.** +> Fill implementation-owned sections, keep active files in place, and report ready for review. Do not ask the user directly or finalize the loop. + +## 개요 + +date=2026-06-06 +task=m-us-market-expansion/04+02,03_us_backtest_smoke, plan=0, tag=US_BACKTEST + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-backtest`: 미국장 daily data로 backtest fixture 또는 smoke 동작 +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +각 항목의 구현과 검증 출력을 대조한다. PASS이면 completion metadata만 보고하고 roadmap update는 런타임에 맡긴다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [US_BACKTEST-1] Storage Bar Source US Selector | [ ] | +| [US_BACKTEST-2] Engine USD Fixture | [ ] | +| [US_BACKTEST-3] Operator Or Worker Smoke Closure | [ ] | + +## 구현 체크리스트 + +- [ ] [US_BACKTEST-1] `StorageBarSource`가 US market selector로 USD/NASDAQ bars만 반환하는 test를 추가한다. 검증: `bin/test`와 관련 worker 검증이 통과한다. +- [ ] [US_BACKTEST-2] Engine fixture가 `MarketUS` daily bars로 USD starting/ending equity를 계산하는 smoke를 추가한다. +- [ ] [US_BACKTEST-3] headless/operator backtest smoke가 US market fixture를 실행하거나, worker-only fixture로 충분한 경우 범위 근거를 review에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] 판정을 append한다. +- [ ] active review/plan 파일을 `*.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] PASS이면 완료 이벤트 메타데이터를 보고한다. +- [ ] WARN/FAIL이면 후속 plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- US source test가 KR bars를 섞지 않는지 확인한다. +- Engine result가 USD starting cash와 USD price를 보존하는지 확인한다. +- Operator fixture 생략 시 live provider 의존성 때문에 worker deterministic fixture가 충분하다는 근거가 review에 기록됐는지 확인한다. + +## 검증 결과 + +### US_BACKTEST-1 중간 검증 +```text +$ go test -count=1 ./services/worker/internal/backtest +(output) +``` + +### US_BACKTEST-2 중간 검증 +```text +$ go test -count=1 ./services/worker/internal/backtest +(output) +``` + +### US_BACKTEST-3 중간 검증 +```text +$ go test -count=1 ./apps/cli/internal/operator +(output) +``` + +### 최종 검증 +```text +$ go test -count=1 ./services/worker/internal/backtest ./apps/cli/internal/operator +(output) + +$ go test -count=1 ./services/worker/... +(output) + +$ bin/test +(output) + +$ git diff --check +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/PLAN-local-G06.md b/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/PLAN-local-G06.md new file mode 100644 index 0000000..6bf47ca --- /dev/null +++ b/agent-task/m-us-market-expansion/04+02,03_us_backtest_smoke/PLAN-local-G06.md @@ -0,0 +1,157 @@ + + +# Implementation Plan - US_BACKTEST + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. `02+01_kis_us_daily_bars`와 `03+01_shared_query_surface` 완료 전에는 구현하지 않는다. 사용자-only 차단은 review stub의 `사용자 리뷰 요청` 섹션에만 기록한다. finalization은 code-review 전용이다. + +## 배경 + +US daily data가 저장되고 query surface가 열려도 backtest가 실제 USD/NASDAQ data로 smoke되지 않으면 milestone의 마지막 기능을 닫을 수 없다. Engine은 `MarketUS`면 USD starting cash를 쓰지만, tests는 KR fixture 중심이다. 이 작업은 US bars를 backtest source와 engine fixture에 넣어 compatibility를 잠근다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자에게 질문하지 않는다. code-review가 user-review stop 여부를 판단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/trading-expansion/milestones/us-market-expansion.md` +- Task ids: + - `us-backtest`: 미국장 daily data로 backtest fixture 또는 smoke 동작 +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/worker-smoke.md` +- `agent-test/local/operations-smoke.md` +- `services/worker/internal/backtest/bar_source.go` +- `services/worker/internal/backtest/bar_source_test.go` +- `services/worker/internal/backtest/engine.go` +- `services/worker/internal/backtest/fixture_test.go` +- `services/worker/internal/jobs/backtest_jobs.go` +- `services/worker/internal/jobs/backtest_jobs_test.go` +- `apps/cli/internal/operator/runner.go` +- `apps/cli/internal/operator/runner_backtest_test.go` +- `apps/cli/testdata/operator/backtest_result_summary.yaml` +- `apps/cli/testdata/operator/expected/backtest_result_summary.jsonl` + +### 테스트 환경 규칙 + +`test_env=local`. worker-smoke와 operations-smoke를 적용한다. worker backtest 변경은 `go test ./services/worker/...`, CLI/headless fixture 변경은 `go test -count=1 ./apps/cli/internal/operator`, milestone verification은 `bin/test`다. + +### 테스트 커버리지 공백 + +- `StorageBarSource` test는 US instrument를 seed하지만 KR query 결과만 assert한다. +- Engine tests는 `MarketKR` starting cash/ending equity만 assert한다. `MarketUS` USD cash path는 code에 있으나 direct fixture test가 없다. +- Operator backtest scenario fixtures are KR-centered. + +### 심볼 참조 + +Renamed/removed symbol: none. New tests/fixtures only unless source behavior gap is found. + +### 분할 판단 + +Shared task group: `m-us-market-expansion`. This subtask is `04+02,03_us_backtest_smoke` and depends on `02+01_kis_us_daily_bars` plus `03+01_shared_query_surface`. Predecessor status at plan creation: both missing complete.log. The transitive foundation dependency is carried by 02/03; no hidden dependency is added. + +### 범위 결정 근거 + +이 작업은 deterministic fixture/smoke only. Strategy architecture, live provider smoke, paper trading, Flutter UI는 제외한다. If implementation finds a real engine bug, fix only the US compatibility bug needed for this smoke. + +### 빌드 등급 + +`local-G06`: deterministic worker/CLI tests with bounded source impact; no schema or external runtime required if predecessors are complete. + +## 구현 체크리스트 + +- [ ] [US_BACKTEST-1] `StorageBarSource`가 US market selector로 USD/NASDAQ bars만 반환하는 test를 추가한다. 검증: `bin/test`와 관련 worker 검증이 통과한다. +- [ ] [US_BACKTEST-2] Engine fixture가 `MarketUS` daily bars로 USD starting/ending equity를 계산하는 smoke를 추가한다. +- [ ] [US_BACKTEST-3] headless/operator backtest smoke가 US market fixture를 실행하거나, worker-only fixture로 충분한 경우 범위 근거를 review에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +`04+02,03_us_backtest_smoke`는 디렉터리명 기준으로 `02+01_kis_us_daily_bars`와 `03+01_shared_query_surface`의 `complete.log`가 필요하다. 추가 dependency를 만들지 않는다. + +### [US_BACKTEST-1] Storage Bar Source US Selector + +문제: [services/worker/internal/backtest/bar_source.go](/config/workspace/alt/services/worker/internal/backtest/bar_source.go:24)는 market filter로 instruments를 골라 bars를 읽는다. [bar_source_test.go](/config/workspace/alt/services/worker/internal/backtest/bar_source_test.go:60)는 US instrument를 seed하지만 KR query만 assert한다. + +해결 방법: 같은 test 또는 새 test에서 `source.GetBars(..., market.MarketUS, ...)`를 호출하고 `NASDAQ:AAPL`만 반환되는지, USD close currency가 유지되는지 검증한다. + +수정 파일 및 체크리스트: + +- [ ] `services/worker/internal/backtest/bar_source_test.go`: US selector assertion 추가. + +테스트 작성: 작성한다. 기존 in-memory store를 재사용한다. + +중간 검증: + +```bash +go test -count=1 ./services/worker/internal/backtest +``` + +### [US_BACKTEST-2] Engine USD Fixture + +문제: [services/worker/internal/backtest/engine.go](/config/workspace/alt/services/worker/internal/backtest/engine.go:57)는 `MarketUS`면 USD starting cash를 사용하지만 [fixture_test.go](/config/workspace/alt/services/worker/internal/backtest/fixture_test.go:92)는 KR-only bars와 KRW expected result를 검증한다. + +해결 방법: `NASDAQ:AAPL` USD bars와 deterministic strategy를 사용해 `MarketUS` run을 실행하고 starting cash `10000` USD, trades/positions USD price, final equity/summary를 assert한다. + +수정 파일 및 체크리스트: + +- [ ] `services/worker/internal/backtest/fixture_test.go`: US fixture test 추가. +- [ ] 필요 시 helper 함수로 KR/US fixture 중복만 최소화한다. + +테스트 작성: 작성한다. Numeric expectation은 KR test처럼 명시한다. + +중간 검증: + +```bash +go test -count=1 ./services/worker/internal/backtest +``` + +### [US_BACKTEST-3] Operator Or Worker Smoke Closure + +문제: [apps/cli/testdata/operator/backtest_result_summary.yaml](/config/workspace/alt/apps/cli/testdata/operator/backtest_result_summary.yaml:1)는 KR scenario다. milestone은 US daily data로 backtest fixture 또는 smoke가 동작해야 한다. + +해결 방법: predecessors가 US import/query fixture를 제공하면 `us_backtest_result_summary.yaml`과 expected JSONL을 추가한다. If the runtime cannot seed US bars without live KIS credentials, keep worker deterministic fixture as completion evidence and record why operator fixture is deferred. + +수정 파일 및 체크리스트: + +- [ ] `apps/cli/testdata/operator/us_backtest_result_summary.yaml`: 가능한 경우 추가. +- [ ] `apps/cli/testdata/operator/expected/us_backtest_result_summary.jsonl`: 가능한 경우 추가. +- [ ] `apps/cli/internal/operator/runner_backtest_test.go`: US backtest scenario test 추가 또는 worker-only closure 근거 기록. + +테스트 작성: 작성한다. Operator fixture가 live provider를 요구하면 작성하지 않고 worker deterministic smoke를 completion evidence로 남긴다. + +중간 검증: + +```bash +go test -count=1 ./apps/cli/internal/operator +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `services/worker/internal/backtest/bar_source_test.go` | US_BACKTEST-1 | +| `services/worker/internal/backtest/fixture_test.go` | US_BACKTEST-2 | +| `apps/cli/testdata/operator/us_backtest_result_summary.yaml` | US_BACKTEST-3 | +| `apps/cli/testdata/operator/expected/us_backtest_result_summary.jsonl` | US_BACKTEST-3 | +| `apps/cli/internal/operator/runner_backtest_test.go` | US_BACKTEST-3 | + +## 최종 검증 + +```bash +go test -count=1 ./services/worker/internal/backtest ./apps/cli/internal/operator +go test -count=1 ./services/worker/... +bin/test +git diff --check +``` + +`go test` cache는 허용하지 않는다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/cli/internal/operator/runner.go b/apps/cli/internal/operator/runner.go index e786ec1..7890ae1 100644 --- a/apps/cli/internal/operator/runner.go +++ b/apps/cli/internal/operator/runner.go @@ -64,6 +64,8 @@ var venueByName = map[string]altv1.Venue{ "": altv1.Venue_VENUE_UNSPECIFIED, "unspecified": altv1.Venue_VENUE_UNSPECIFIED, "krx": altv1.Venue_VENUE_KRX, + "nasdaq": altv1.Venue_VENUE_NASDAQ, + "nyse": altv1.Venue_VENUE_NYSE, } // currencyByMarket derives the paper account currency from the request market so diff --git a/apps/cli/internal/operator/runner_market_test.go b/apps/cli/internal/operator/runner_market_test.go index c5891f4..e9fb430 100644 --- a/apps/cli/internal/operator/runner_market_test.go +++ b/apps/cli/internal/operator/runner_market_test.go @@ -320,6 +320,60 @@ func TestRunScenarioImportDailyBarsSuccess(t *testing.T) { } } +func TestRunScenarioImportDailyBarsMapsUSVenues(t *testing.T) { + tests := []struct { + name string + venue string + want altv1.Venue + }{ + {name: "nasdaq", venue: "nasdaq", want: altv1.Venue_VENUE_NASDAQ}, + {name: "nyse", venue: "nyse", want: altv1.Venue_VENUE_NYSE}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + api := &fakeAPI{ + importDailyBarsResp: &altv1.ImportDailyBarsResponse{ + Provider: "kis", + InstrumentCount: 1, + BarCount: 1, + }, + } + url := startFakeAPIServer(t, api) + sc := &Scenario{ + Name: "import_us_daily_bars", + Steps: []Step{{ + ID: "import1", + Action: ActionImportDailyBars, + Request: Request{ + Provider: "kis", + SelectorKind: "watchlist", + Market: "us", + Venue: tt.venue, + Symbols: []string{"AAPL"}, + }, + Expect: Expect{Status: "ok"}, + }}, + } + + out, code := runScenario(t, sc, url) + if code != codeOK { + t.Fatalf("exit code = %d, want 0 (out=%q)", code, out) + } + req := api.lastImportDailyBarsReq() + if req == nil { + t.Fatal("server did not receive an import_daily_bars request") + } + if req.GetMarket() != altv1.Market_MARKET_US { + t.Errorf("market = %v, want MARKET_US", req.GetMarket()) + } + if req.GetVenue() != tt.want { + t.Errorf("venue = %v, want %v", req.GetVenue(), tt.want) + } + }) + } +} + func TestRunScenarioImportDailyBarsExpectedTypedError(t *testing.T) { url := startFakeAPIServer(t, &fakeAPI{ importDailyBarsResp: &altv1.ImportDailyBarsResponse{ diff --git a/apps/cli/internal/operator/scenario.go b/apps/cli/internal/operator/scenario.go index e4f53fc..2607ddf 100644 --- a/apps/cli/internal/operator/scenario.go +++ b/apps/cli/internal/operator/scenario.go @@ -121,6 +121,8 @@ var validVenues = map[string]bool{ "": true, "unspecified": true, "krx": true, + "nasdaq": true, + "nyse": true, } // validTimeframes is the set of bar timeframe strings a list_bars request may diff --git a/apps/cli/internal/operator/scenario_test.go b/apps/cli/internal/operator/scenario_test.go index 435fdc1..2a10ee1 100644 --- a/apps/cli/internal/operator/scenario_test.go +++ b/apps/cli/internal/operator/scenario_test.go @@ -1,6 +1,7 @@ package operator import ( + "fmt" "path/filepath" "strings" "testing" @@ -464,6 +465,38 @@ steps: } } +func TestValidateImportDailyBarsUSVenues(t *testing.T) { + for _, venue := range []string{"nasdaq", "nyse"} { + t.Run(venue, func(t *testing.T) { + yamlText := fmt.Sprintf(` +name: valid_us_import +timeout: 5s +steps: + - id: step1 + action: import_daily_bars + request: + provider: kis + selector_kind: watchlist + market: us + venue: %s + symbols: ["AAPL"] + from_yyyymmdd: "20240527" + to_yyyymmdd: "20240528" + expect: + status: ok +`, venue) + sc, err := ParseScenario([]byte(yamlText)) + if err != nil { + t.Fatalf("unexpected error parsing valid US import scenario: %v", err) + } + got := sc.Steps[0].Request.Venue + if got != venue { + t.Errorf("venue = %q, want %q", got, venue) + } + }) + } +} + func TestValidatePaperTradingScenario(t *testing.T) { yamlText := ` name: valid_paper diff --git a/apps/client/lib/src/generated/alt/v1/common.pbenum.dart b/apps/client/lib/src/generated/alt/v1/common.pbenum.dart index f127d13..2f17528 100644 --- a/apps/client/lib/src/generated/alt/v1/common.pbenum.dart +++ b/apps/client/lib/src/generated/alt/v1/common.pbenum.dart @@ -82,6 +82,28 @@ class Currency extends $pb.ProtobufEnum { const Currency._(super.value, super.name); } +class AssetType extends $pb.ProtobufEnum { + static const AssetType ASSET_TYPE_UNSPECIFIED = + AssetType._(0, _omitEnumNames ? '' : 'ASSET_TYPE_UNSPECIFIED'); + static const AssetType ASSET_TYPE_EQUITY = + AssetType._(1, _omitEnumNames ? '' : 'ASSET_TYPE_EQUITY'); + static const AssetType ASSET_TYPE_ETF = + AssetType._(2, _omitEnumNames ? '' : 'ASSET_TYPE_ETF'); + + static const $core.List values = [ + ASSET_TYPE_UNSPECIFIED, + ASSET_TYPE_EQUITY, + ASSET_TYPE_ETF, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 2); + static AssetType? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const AssetType._(super.value, super.name); +} + class Timeframe extends $pb.ProtobufEnum { static const Timeframe TIMEFRAME_UNSPECIFIED = Timeframe._(0, _omitEnumNames ? '' : 'TIMEFRAME_UNSPECIFIED'); diff --git a/apps/client/lib/src/generated/alt/v1/common.pbjson.dart b/apps/client/lib/src/generated/alt/v1/common.pbjson.dart index 598dcf3..369a8c6 100644 --- a/apps/client/lib/src/generated/alt/v1/common.pbjson.dart +++ b/apps/client/lib/src/generated/alt/v1/common.pbjson.dart @@ -61,6 +61,21 @@ final $typed_data.Uint8List currencyDescriptor = $convert.base64Decode( 'CghDdXJyZW5jeRIYChRDVVJSRU5DWV9VTlNQRUNJRklFRBAAEhAKDENVUlJFTkNZX0tSVxABEh' 'AKDENVUlJFTkNZX1VTRBAC'); +@$core.Deprecated('Use assetTypeDescriptor instead') +const AssetType$json = { + '1': 'AssetType', + '2': [ + {'1': 'ASSET_TYPE_UNSPECIFIED', '2': 0}, + {'1': 'ASSET_TYPE_EQUITY', '2': 1}, + {'1': 'ASSET_TYPE_ETF', '2': 2}, + ], +}; + +/// Descriptor for `AssetType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List assetTypeDescriptor = $convert.base64Decode( + 'CglBc3NldFR5cGUSGgoWQVNTRVRfVFlQRV9VTlNQRUNJRklFRBAAEhUKEUFTU0VUX1RZUEVfRV' + 'FVSVRZEAESEgoOQVNTRVRfVFlQRV9FVEYQAg=='); + @$core.Deprecated('Use timeframeDescriptor instead') const Timeframe$json = { '1': 'Timeframe', diff --git a/apps/client/lib/src/generated/alt/v1/market.pb.dart b/apps/client/lib/src/generated/alt/v1/market.pb.dart index c32ed3a..b4eaec0 100644 --- a/apps/client/lib/src/generated/alt/v1/market.pb.dart +++ b/apps/client/lib/src/generated/alt/v1/market.pb.dart @@ -28,6 +28,7 @@ class Instrument extends $pb.GeneratedMessage { $core.String? name, $0.Currency? currency, $core.Iterable<$core.MapEntry<$core.String, $core.String>>? providerSymbols, + $0.AssetType? assetType, }) { final result = create(); if (id != null) result.id = id; @@ -38,6 +39,7 @@ class Instrument extends $pb.GeneratedMessage { if (currency != null) result.currency = currency; if (providerSymbols != null) result.providerSymbols.addEntries(providerSymbols); + if (assetType != null) result.assetType = assetType; return result; } @@ -68,6 +70,8 @@ class Instrument extends $pb.GeneratedMessage { keyFieldType: $pb.PbFieldType.OS, valueFieldType: $pb.PbFieldType.OS, packageName: const $pb.PackageName('alt.v1')) + ..aE<$0.AssetType>(8, _omitFieldNames ? '' : 'assetType', + enumValues: $0.AssetType.values) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -144,6 +148,15 @@ class Instrument extends $pb.GeneratedMessage { @$pb.TagNumber(7) $pb.PbMap<$core.String, $core.String> get providerSymbols => $_getMap(6); + + @$pb.TagNumber(8) + $0.AssetType get assetType => $_getN(7); + @$pb.TagNumber(8) + set assetType($0.AssetType value) => $_setField(8, value); + @$pb.TagNumber(8) + $core.bool hasAssetType() => $_has(7); + @$pb.TagNumber(8) + void clearAssetType() => $_clearField(8); } class Price extends $pb.GeneratedMessage { diff --git a/apps/client/lib/src/generated/alt/v1/market.pbjson.dart b/apps/client/lib/src/generated/alt/v1/market.pbjson.dart index 5e023ea..55ae39e 100644 --- a/apps/client/lib/src/generated/alt/v1/market.pbjson.dart +++ b/apps/client/lib/src/generated/alt/v1/market.pbjson.dart @@ -54,6 +54,14 @@ const Instrument$json = { '6': '.alt.v1.Instrument.ProviderSymbolsEntry', '10': 'providerSymbols' }, + { + '1': 'asset_type', + '3': 8, + '4': 1, + '5': 14, + '6': '.alt.v1.AssetType', + '10': 'assetType' + }, ], '3': [Instrument_ProviderSymbolsEntry$json], }; @@ -75,8 +83,9 @@ final $typed_data.Uint8List instrumentDescriptor = $convert.base64Decode( 'bWJvbBgEIAEoCVIGc3ltYm9sEhIKBG5hbWUYBSABKAlSBG5hbWUSLAoIY3VycmVuY3kYBiABKA' '4yEC5hbHQudjEuQ3VycmVuY3lSCGN1cnJlbmN5ElIKEHByb3ZpZGVyX3N5bWJvbHMYByADKAsy' 'Jy5hbHQudjEuSW5zdHJ1bWVudC5Qcm92aWRlclN5bWJvbHNFbnRyeVIPcHJvdmlkZXJTeW1ib2' - 'xzGkIKFFByb3ZpZGVyU3ltYm9sc0VudHJ5EhAKA2tleRgBIAEoCVIDa2V5EhQKBXZhbHVlGAIg' - 'ASgJUgV2YWx1ZToCOAE='); + 'xzEjAKCmFzc2V0X3R5cGUYCCABKA4yES5hbHQudjEuQXNzZXRUeXBlUglhc3NldFR5cGUaQgoU' + 'UHJvdmlkZXJTeW1ib2xzRW50cnkSEAoDa2V5GAEgASgJUgNrZXkSFAoFdmFsdWUYAiABKAlSBX' + 'ZhbHVlOgI4AQ=='); @$core.Deprecated('Use priceDescriptor instead') const Price$json = { diff --git a/packages/contracts/gen/go/alt/v1/common.pb.go b/packages/contracts/gen/go/alt/v1/common.pb.go index b1afd90..f17def8 100644 --- a/packages/contracts/gen/go/alt/v1/common.pb.go +++ b/packages/contracts/gen/go/alt/v1/common.pb.go @@ -171,6 +171,55 @@ func (Currency) EnumDescriptor() ([]byte, []int) { return file_alt_v1_common_proto_rawDescGZIP(), []int{2} } +type AssetType int32 + +const ( + AssetType_ASSET_TYPE_UNSPECIFIED AssetType = 0 + AssetType_ASSET_TYPE_EQUITY AssetType = 1 + AssetType_ASSET_TYPE_ETF AssetType = 2 +) + +// Enum value maps for AssetType. +var ( + AssetType_name = map[int32]string{ + 0: "ASSET_TYPE_UNSPECIFIED", + 1: "ASSET_TYPE_EQUITY", + 2: "ASSET_TYPE_ETF", + } + AssetType_value = map[string]int32{ + "ASSET_TYPE_UNSPECIFIED": 0, + "ASSET_TYPE_EQUITY": 1, + "ASSET_TYPE_ETF": 2, + } +) + +func (x AssetType) Enum() *AssetType { + p := new(AssetType) + *p = x + return p +} + +func (x AssetType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AssetType) Descriptor() protoreflect.EnumDescriptor { + return file_alt_v1_common_proto_enumTypes[3].Descriptor() +} + +func (AssetType) Type() protoreflect.EnumType { + return &file_alt_v1_common_proto_enumTypes[3] +} + +func (x AssetType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AssetType.Descriptor instead. +func (AssetType) EnumDescriptor() ([]byte, []int) { + return file_alt_v1_common_proto_rawDescGZIP(), []int{3} +} + type Timeframe int32 const ( @@ -207,11 +256,11 @@ func (x Timeframe) String() string { } func (Timeframe) Descriptor() protoreflect.EnumDescriptor { - return file_alt_v1_common_proto_enumTypes[3].Descriptor() + return file_alt_v1_common_proto_enumTypes[4].Descriptor() } func (Timeframe) Type() protoreflect.EnumType { - return &file_alt_v1_common_proto_enumTypes[3] + return &file_alt_v1_common_proto_enumTypes[4] } func (x Timeframe) Number() protoreflect.EnumNumber { @@ -220,7 +269,7 @@ func (x Timeframe) Number() protoreflect.EnumNumber { // Deprecated: Use Timeframe.Descriptor instead. func (Timeframe) EnumDescriptor() ([]byte, []int) { - return file_alt_v1_common_proto_rawDescGZIP(), []int{3} + return file_alt_v1_common_proto_rawDescGZIP(), []int{4} } type Decimal struct { @@ -481,7 +530,11 @@ const file_alt_v1_common_proto_rawDesc = "" + "\bCurrency\x12\x18\n" + "\x14CURRENCY_UNSPECIFIED\x10\x00\x12\x10\n" + "\fCURRENCY_KRW\x10\x01\x12\x10\n" + - "\fCURRENCY_USD\x10\x02*k\n" + + "\fCURRENCY_USD\x10\x02*R\n" + + "\tAssetType\x12\x1a\n" + + "\x16ASSET_TYPE_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11ASSET_TYPE_EQUITY\x10\x01\x12\x12\n" + + "\x0eASSET_TYPE_ETF\x10\x02*k\n" + "\tTimeframe\x12\x19\n" + "\x15TIMEFRAME_UNSPECIFIED\x10\x00\x12\x13\n" + "\x0fTIMEFRAME_DAILY\x10\x01\x12\x16\n" + @@ -500,17 +553,18 @@ func file_alt_v1_common_proto_rawDescGZIP() []byte { return file_alt_v1_common_proto_rawDescData } -var file_alt_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_alt_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_alt_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_alt_v1_common_proto_goTypes = []any{ (Market)(0), // 0: alt.v1.Market (Venue)(0), // 1: alt.v1.Venue (Currency)(0), // 2: alt.v1.Currency - (Timeframe)(0), // 3: alt.v1.Timeframe - (*Decimal)(nil), // 4: alt.v1.Decimal - (*ErrorInfo)(nil), // 5: alt.v1.ErrorInfo - (*HelloRequest)(nil), // 6: alt.v1.HelloRequest - (*HelloResponse)(nil), // 7: alt.v1.HelloResponse + (AssetType)(0), // 3: alt.v1.AssetType + (Timeframe)(0), // 4: alt.v1.Timeframe + (*Decimal)(nil), // 5: alt.v1.Decimal + (*ErrorInfo)(nil), // 6: alt.v1.ErrorInfo + (*HelloRequest)(nil), // 7: alt.v1.HelloRequest + (*HelloResponse)(nil), // 8: alt.v1.HelloResponse } var file_alt_v1_common_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -530,7 +584,7 @@ func file_alt_v1_common_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_alt_v1_common_proto_rawDesc), len(file_alt_v1_common_proto_rawDesc)), - NumEnums: 4, + NumEnums: 5, NumMessages: 4, NumExtensions: 0, NumServices: 0, diff --git a/packages/contracts/gen/go/alt/v1/market.pb.go b/packages/contracts/gen/go/alt/v1/market.pb.go index 727a8de..de69ae7 100644 --- a/packages/contracts/gen/go/alt/v1/market.pb.go +++ b/packages/contracts/gen/go/alt/v1/market.pb.go @@ -30,6 +30,7 @@ type Instrument struct { Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` Currency Currency `protobuf:"varint,6,opt,name=currency,proto3,enum=alt.v1.Currency" json:"currency,omitempty"` ProviderSymbols map[string]string `protobuf:"bytes,7,rep,name=provider_symbols,json=providerSymbols,proto3" json:"provider_symbols,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + AssetType AssetType `protobuf:"varint,8,opt,name=asset_type,json=assetType,proto3,enum=alt.v1.AssetType" json:"asset_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -113,6 +114,13 @@ func (x *Instrument) GetProviderSymbols() map[string]string { return nil } +func (x *Instrument) GetAssetType() AssetType { + if x != nil { + return x.AssetType + } + return AssetType_ASSET_TYPE_UNSPECIFIED +} + type Price struct { state protoimpl.MessageState `protogen:"open.v1"` Currency Currency `protobuf:"varint,1,opt,name=currency,proto3,enum=alt.v1.Currency" json:"currency,omitempty"` @@ -705,7 +713,7 @@ var File_alt_v1_market_proto protoreflect.FileDescriptor const file_alt_v1_market_proto_rawDesc = "" + "\n" + - "\x13alt/v1/market.proto\x12\x06alt.v1\x1a\x13alt/v1/common.proto\"\xdb\x02\n" + + "\x13alt/v1/market.proto\x12\x06alt.v1\x1a\x13alt/v1/common.proto\"\x8d\x03\n" + "\n" + "Instrument\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12&\n" + @@ -714,7 +722,9 @@ const file_alt_v1_market_proto_rawDesc = "" + "\x06symbol\x18\x04 \x01(\tR\x06symbol\x12\x12\n" + "\x04name\x18\x05 \x01(\tR\x04name\x12,\n" + "\bcurrency\x18\x06 \x01(\x0e2\x10.alt.v1.CurrencyR\bcurrency\x12R\n" + - "\x10provider_symbols\x18\a \x03(\v2'.alt.v1.Instrument.ProviderSymbolsEntryR\x0fproviderSymbols\x1aB\n" + + "\x10provider_symbols\x18\a \x03(\v2'.alt.v1.Instrument.ProviderSymbolsEntryR\x0fproviderSymbols\x120\n" + + "\n" + + "asset_type\x18\b \x01(\x0e2\x11.alt.v1.AssetTypeR\tassetType\x1aB\n" + "\x14ProviderSymbolsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"^\n" + @@ -792,38 +802,40 @@ var file_alt_v1_market_proto_goTypes = []any{ (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 + (AssetType)(0), // 14: alt.v1.AssetType + (*Decimal)(nil), // 15: alt.v1.Decimal + (Timeframe)(0), // 16: alt.v1.Timeframe + (*ErrorInfo)(nil), // 17: alt.v1.ErrorInfo } var file_alt_v1_market_proto_depIdxs = []int32{ 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 - 11, // 13: alt.v1.ListInstrumentsRequest.market:type_name -> alt.v1.Market - 0, // 14: alt.v1.ListInstrumentsResponse.instruments:type_name -> alt.v1.Instrument - 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 - 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 + 14, // 4: alt.v1.Instrument.asset_type:type_name -> alt.v1.AssetType + 13, // 5: alt.v1.Price.currency:type_name -> alt.v1.Currency + 15, // 6: alt.v1.Price.amount:type_name -> alt.v1.Decimal + 15, // 7: alt.v1.Quantity.amount:type_name -> alt.v1.Decimal + 16, // 8: alt.v1.Bar.timeframe:type_name -> alt.v1.Timeframe + 1, // 9: alt.v1.Bar.open:type_name -> alt.v1.Price + 1, // 10: alt.v1.Bar.high:type_name -> alt.v1.Price + 1, // 11: alt.v1.Bar.low:type_name -> alt.v1.Price + 1, // 12: alt.v1.Bar.close:type_name -> alt.v1.Price + 2, // 13: alt.v1.Bar.volume:type_name -> alt.v1.Quantity + 11, // 14: alt.v1.ListInstrumentsRequest.market:type_name -> alt.v1.Market + 0, // 15: alt.v1.ListInstrumentsResponse.instruments:type_name -> alt.v1.Instrument + 17, // 16: alt.v1.ListInstrumentsResponse.error:type_name -> alt.v1.ErrorInfo + 16, // 17: alt.v1.ListBarsRequest.timeframe:type_name -> alt.v1.Timeframe + 3, // 18: alt.v1.ListBarsResponse.bars:type_name -> alt.v1.Bar + 17, // 19: alt.v1.ListBarsResponse.error:type_name -> alt.v1.ErrorInfo + 11, // 20: alt.v1.ImportDailyBarsRequest.market:type_name -> alt.v1.Market + 12, // 21: alt.v1.ImportDailyBarsRequest.venue:type_name -> alt.v1.Venue + 17, // 22: alt.v1.ImportDailyBarsResponse.error:type_name -> alt.v1.ErrorInfo + 23, // [23:23] is the sub-list for method output_type + 23, // [23:23] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_alt_v1_market_proto_init() } diff --git a/packages/contracts/proto/alt/v1/common.proto b/packages/contracts/proto/alt/v1/common.proto index 140e56a..db31415 100644 --- a/packages/contracts/proto/alt/v1/common.proto +++ b/packages/contracts/proto/alt/v1/common.proto @@ -27,6 +27,12 @@ enum Currency { CURRENCY_USD = 2; } +enum AssetType { + ASSET_TYPE_UNSPECIFIED = 0; + ASSET_TYPE_EQUITY = 1; + ASSET_TYPE_ETF = 2; +} + enum Timeframe { TIMEFRAME_UNSPECIFIED = 0; TIMEFRAME_DAILY = 1; diff --git a/packages/contracts/proto/alt/v1/market.proto b/packages/contracts/proto/alt/v1/market.proto index 62ec569..ee96243 100644 --- a/packages/contracts/proto/alt/v1/market.proto +++ b/packages/contracts/proto/alt/v1/market.proto @@ -14,6 +14,7 @@ message Instrument { string name = 5; Currency currency = 6; map provider_symbols = 7; + AssetType asset_type = 8; } message Price { diff --git a/packages/domain/market/types.go b/packages/domain/market/types.go index c3fec8d..c9a6370 100644 --- a/packages/domain/market/types.go +++ b/packages/domain/market/types.go @@ -34,6 +34,48 @@ const ( type InstrumentID string +type AssetType string + +const ( + AssetTypeEquity AssetType = "equity" + AssetTypeETF AssetType = "etf" +) + +type VenueMetadata struct { + Venue Venue + Market Market + Currency Currency + Timezone string +} + +func GetVenueMetadata(v Venue) (VenueMetadata, bool) { + switch v { + case VenueKRX: + return VenueMetadata{ + Venue: VenueKRX, + Market: MarketKR, + Currency: CurrencyKRW, + Timezone: "Asia/Seoul", + }, true + case VenueNASDAQ: + return VenueMetadata{ + Venue: VenueNASDAQ, + Market: MarketUS, + Currency: CurrencyUSD, + Timezone: "America/New_York", + }, true + case VenueNYSE: + return VenueMetadata{ + Venue: VenueNYSE, + Market: MarketUS, + Currency: CurrencyUSD, + Timezone: "America/New_York", + }, true + default: + return VenueMetadata{}, false + } +} + type Instrument struct { ID InstrumentID Market Market @@ -41,6 +83,7 @@ type Instrument struct { Symbol string Name string Currency Currency + AssetType AssetType ProviderSymbols map[string]string } diff --git a/packages/domain/market/types_test.go b/packages/domain/market/types_test.go index 54365fa..8e549d5 100644 --- a/packages/domain/market/types_test.go +++ b/packages/domain/market/types_test.go @@ -41,3 +41,38 @@ func TestUniverseSelectorFields(t *testing.T) { t.Errorf("Symbols length: got %d, want 2", len(sel.Symbols)) } } + +func TestMarketVenueMetadata(t *testing.T) { + cases := []struct { + venue Venue + wantOk bool + wantMarket Market + wantCurrency Currency + wantTimezone string + }{ + {VenueKRX, true, MarketKR, CurrencyKRW, "Asia/Seoul"}, + {VenueNASDAQ, true, MarketUS, CurrencyUSD, "America/New_York"}, + {VenueNYSE, true, MarketUS, CurrencyUSD, "America/New_York"}, + {Venue("UNKNOWN"), false, Market(""), Currency(""), ""}, + } + for _, c := range cases { + meta, ok := GetVenueMetadata(c.venue) + if ok != c.wantOk { + t.Errorf("GetVenueMetadata(%q) ok: got %v, want %v", c.venue, ok, c.wantOk) + } + if ok { + if meta.Market != c.wantMarket || meta.Currency != c.wantCurrency || meta.Timezone != c.wantTimezone { + t.Errorf("GetVenueMetadata(%q) meta mismatch: got %+v, want Market: %v, Currency: %v, Timezone: %v", c.venue, meta, c.wantMarket, c.wantCurrency, c.wantTimezone) + } + } + } +} + +func TestAssetTypeVocabulary(t *testing.T) { + if string(AssetTypeEquity) != "equity" { + t.Errorf("AssetTypeEquity: got %q, want %q", string(AssetTypeEquity), "equity") + } + if string(AssetTypeETF) != "etf" { + t.Errorf("AssetTypeETF: got %q, want %q", string(AssetTypeETF), "etf") + } +} diff --git a/services/worker/internal/providers/kis/daily_itemchartprice_test.go b/services/worker/internal/providers/kis/daily_itemchartprice_test.go index a4fe139..cd1b84f 100644 --- a/services/worker/internal/providers/kis/daily_itemchartprice_test.go +++ b/services/worker/internal/providers/kis/daily_itemchartprice_test.go @@ -205,6 +205,7 @@ type providerSymbolsFixture struct { Symbol string `json:"symbol"` Name string `json:"name"` Currency string `json:"currency"` + AssetType string `json:"asset_type"` ProviderSymbols map[string]string `json:"provider_symbols"` } `json:"instruments"` } @@ -224,6 +225,7 @@ func TestProviderSymbolsFixtureVocabulary(t *testing.T) { market.UniverseSelectorExchange: false, market.UniverseSelectorSector: false, } + usWatchlistSymbols := map[string]bool{} for i, sel := range fx.Selectors { kind := market.UniverseSelectorKind(sel.Kind) if _, ok := wantKinds[kind]; !ok { @@ -235,6 +237,10 @@ func TestProviderSymbolsFixtureVocabulary(t *testing.T) { t.Errorf("selector[%d] %q has no symbols", i, sel.Kind) } switch kind { + case market.UniverseSelectorWatchlist: + for _, s := range sel.Symbols { + usWatchlistSymbols[s] = true + } case market.UniverseSelectorExchange: if market.Venue(sel.Venue) != market.VenueKRX { t.Errorf("exchange selector venue: got %q, want %q", sel.Venue, market.VenueKRX) @@ -251,23 +257,89 @@ func TestProviderSymbolsFixtureVocabulary(t *testing.T) { } } + // Verify watchlist contains required US symbols. + for _, wanted := range []string{"AAPL", "SPY"} { + if !usWatchlistSymbols[wanted] { + t.Errorf("watchlist selector missing US symbol %q", wanted) + } + } + 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) + + // Required US instruments: key is "venue:symbol", values are the exact fields. + requiredUS := map[string]struct { + market market.Market + venue market.Venue + symbol string + currency market.Currency + assetType market.AssetType + }{ + "NASDAQ:AAPL": {market.MarketUS, market.VenueNASDAQ, "AAPL", market.CurrencyUSD, market.AssetTypeEquity}, + "NASDAQ:SPY": {market.MarketUS, market.VenueNASDAQ, "SPY", market.CurrencyUSD, market.AssetTypeETF}, + } + + // Build instrument map for quick lookup by id. + instMap := make(map[string]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"` + AssetType string `json:"asset_type"` + ProviderSymbols map[string]string `json:"provider_symbols"` + }, len(fx.Instruments)) + for _, inst := range fx.Instruments { + instMap[inst.ID] = inst + } + + // Validate US required instruments. + for key, want := range requiredUS { + inst, ok := instMap[key] + if !ok { + t.Errorf("required US instrument %q missing from fixture", key) + continue } - if market.Currency(inst.Currency) != market.CurrencyKRW { - t.Errorf("instrument[%d] currency: got %q, want %q", i, inst.Currency, market.CurrencyKRW) + if market.Market(inst.Market) != want.market { + t.Errorf("instrument[%s] market: got %q, want %q", key, inst.Market, want.market) + } + if market.Venue(inst.Venue) != want.venue { + t.Errorf("instrument[%s] venue: got %q, want %q", key, inst.Venue, want.venue) + } + if inst.Symbol != want.symbol { + t.Errorf("instrument[%s] symbol: got %q, want %q", key, inst.Symbol, want.symbol) + } + if market.Currency(inst.Currency) != want.currency { + t.Errorf("instrument[%s] currency: got %q, want %q", key, inst.Currency, want.currency) + } + if market.AssetType(inst.AssetType) != want.assetType { + t.Errorf("instrument[%s] asset_type: got %q, want %q", key, inst.AssetType, want.assetType) } sym, ok := inst.ProviderSymbols[string(market.ProviderKIS)] if !ok || sym == "" { - t.Errorf("instrument[%d] missing kis provider symbol", i) + t.Errorf("instrument[%s] missing kis provider symbol", key) continue } if sym != inst.Symbol { - t.Errorf("instrument[%d] kis provider symbol %q != symbol %q", i, sym, inst.Symbol) + t.Errorf("instrument[%s] kis provider symbol %q != symbol %q", key, sym, inst.Symbol) + } + } + + // Validate non-US instruments still allow expected enum values. + for i, inst := range fx.Instruments { + v := market.Venue(inst.Venue) + if v != market.VenueKRX && v != market.VenueNASDAQ && v != market.VenueNYSE { + t.Errorf("instrument[%d] unexpected venue: %q", i, inst.Venue) + } + c := market.Currency(inst.Currency) + if c != market.CurrencyKRW && c != market.CurrencyUSD { + t.Errorf("instrument[%d] unexpected currency: %q", i, inst.Currency) + } + at := market.AssetType(inst.AssetType) + if at != market.AssetTypeEquity && at != market.AssetTypeETF { + t.Errorf("instrument[%d] unexpected asset type: %q", i, inst.AssetType) } } } diff --git a/services/worker/internal/socket/market_mapping.go b/services/worker/internal/socket/market_mapping.go index 1bd88b8..c6d9174 100644 --- a/services/worker/internal/socket/market_mapping.go +++ b/services/worker/internal/socket/market_mapping.go @@ -16,6 +16,31 @@ func instrumentToProto(inst market.Instrument) *altv1.Instrument { Name: inst.Name, Currency: currencyToProto(inst.Currency), ProviderSymbols: cloneProviderSymbols(inst.ProviderSymbols), + AssetType: assetTypeToProto(inst.AssetType), + } +} + +func assetTypeToProto(at market.AssetType) altv1.AssetType { + switch at { + case market.AssetTypeEquity: + return altv1.AssetType_ASSET_TYPE_EQUITY + case market.AssetTypeETF: + return altv1.AssetType_ASSET_TYPE_ETF + default: + return altv1.AssetType_ASSET_TYPE_UNSPECIFIED + } +} + +func assetTypeFromProto(at altv1.AssetType) (market.AssetType, error) { + switch at { + case altv1.AssetType_ASSET_TYPE_UNSPECIFIED: + return "", nil + case altv1.AssetType_ASSET_TYPE_EQUITY: + return market.AssetTypeEquity, nil + case altv1.AssetType_ASSET_TYPE_ETF: + return market.AssetTypeETF, nil + default: + return "", fmt.Errorf("unsupported asset type %q", at.String()) } } diff --git a/services/worker/internal/socket/market_mapping_test.go b/services/worker/internal/socket/market_mapping_test.go index ec73d00..6ec42fe 100644 --- a/services/worker/internal/socket/market_mapping_test.go +++ b/services/worker/internal/socket/market_mapping_test.go @@ -10,12 +10,13 @@ import ( func TestInstrumentToProto(t *testing.T) { got := instrumentToProto(market.Instrument{ - ID: "KRX:005930", - Market: market.MarketKR, - Venue: market.VenueKRX, - Symbol: "005930", - Name: "Samsung Electronics", - Currency: market.CurrencyKRW, + ID: "KRX:005930", + Market: market.MarketKR, + Venue: market.VenueKRX, + Symbol: "005930", + Name: "Samsung Electronics", + Currency: market.CurrencyKRW, + AssetType: market.AssetTypeEquity, ProviderSymbols: map[string]string{ "kis": "005930", }, @@ -33,11 +34,38 @@ func TestInstrumentToProto(t *testing.T) { if got.GetCurrency() != altv1.Currency_CURRENCY_KRW { t.Errorf("currency mismatch: %v", got.GetCurrency()) } + if got.GetAssetType() != altv1.AssetType_ASSET_TYPE_EQUITY { + t.Errorf("asset type mismatch: %v", got.GetAssetType()) + } if got.GetProviderSymbols()["kis"] != "005930" { t.Errorf("provider symbols mismatch: %+v", got.GetProviderSymbols()) } } +func TestAssetTypeMapping(t *testing.T) { + cases := []struct { + domain market.AssetType + proto altv1.AssetType + }{ + {market.AssetTypeEquity, altv1.AssetType_ASSET_TYPE_EQUITY}, + {market.AssetTypeETF, altv1.AssetType_ASSET_TYPE_ETF}, + {market.AssetType(""), altv1.AssetType_ASSET_TYPE_UNSPECIFIED}, + } + for _, c := range cases { + gotProto := assetTypeToProto(c.domain) + if gotProto != c.proto { + t.Errorf("assetTypeToProto(%q): got %v, want %v", c.domain, gotProto, c.proto) + } + gotDomain, err := assetTypeFromProto(c.proto) + if err != nil { + t.Errorf("assetTypeFromProto(%v) error: %v", c.proto, err) + } + if gotDomain != c.domain { + t.Errorf("assetTypeFromProto(%v): got %q, want %q", c.proto, gotDomain, c.domain) + } + } +} + func TestInstrumentToProtoClonesProviderSymbols(t *testing.T) { symbols := map[string]string{"kis": "005930"} got := instrumentToProto(market.Instrument{ProviderSymbols: symbols}) diff --git a/services/worker/internal/storage/postgres/mapping.go b/services/worker/internal/storage/postgres/mapping.go index aa47c7a..7aecb37 100644 --- a/services/worker/internal/storage/postgres/mapping.go +++ b/services/worker/internal/storage/postgres/mapping.go @@ -67,6 +67,7 @@ func mapInstrumentToParams(inst market.Instrument) (sqlc.UpsertInstrumentParams, Name: inst.Name, Currency: string(inst.Currency), ProviderSymbols: provBytes, + AssetType: string(inst.AssetType), }, nil } @@ -87,6 +88,7 @@ func mapRowToInstrument(row sqlc.Instrument) (market.Instrument, error) { Symbol: row.Symbol, Name: row.Name, Currency: market.Currency(row.Currency), + AssetType: market.AssetType(row.AssetType), ProviderSymbols: provSymbols, }, nil } diff --git a/services/worker/internal/storage/postgres/mapping_test.go b/services/worker/internal/storage/postgres/mapping_test.go index d3badbe..430b698 100644 --- a/services/worker/internal/storage/postgres/mapping_test.go +++ b/services/worker/internal/storage/postgres/mapping_test.go @@ -338,3 +338,64 @@ func TestBacktestResultMappingRejectsInvalidDecimal(t *testing.T) { t.Fatal("expected error when mapping result with invalid ending equity decimal, got nil") } } + +func TestInstrumentMappingRoundTrip(t *testing.T) { + inst := market.Instrument{ + ID: "NASDAQ:AAPL", + Market: market.MarketUS, + Venue: market.VenueNASDAQ, + Symbol: "AAPL", + Name: "Apple Inc.", + Currency: market.CurrencyUSD, + AssetType: market.AssetTypeEquity, + ProviderSymbols: map[string]string{ + "kis": "AAPL", + }, + } + + params, err := mapInstrumentToParams(inst) + if err != nil { + t.Fatalf("failed to map instrument to params: %v", err) + } + + row := sqlc.Instrument{ + ID: params.ID, + Market: params.Market, + Venue: params.Venue, + Symbol: params.Symbol, + Name: params.Name, + Currency: params.Currency, + ProviderSymbols: params.ProviderSymbols, + AssetType: params.AssetType, + } + + mappedBack, err := mapRowToInstrument(row) + if err != nil { + t.Fatalf("failed to map row back to instrument: %v", err) + } + + if mappedBack.ID != inst.ID { + t.Errorf("ID mismatch: got %v, want %v", mappedBack.ID, inst.ID) + } + if mappedBack.Market != inst.Market { + t.Errorf("Market mismatch: got %v, want %v", mappedBack.Market, inst.Market) + } + if mappedBack.Venue != inst.Venue { + t.Errorf("Venue mismatch: got %v, want %v", mappedBack.Venue, inst.Venue) + } + if mappedBack.Symbol != inst.Symbol { + t.Errorf("Symbol mismatch: got %v, want %v", mappedBack.Symbol, inst.Symbol) + } + if mappedBack.Name != inst.Name { + t.Errorf("Name mismatch: got %v, want %v", mappedBack.Name, inst.Name) + } + if mappedBack.Currency != inst.Currency { + t.Errorf("Currency mismatch: got %v, want %v", mappedBack.Currency, inst.Currency) + } + if mappedBack.AssetType != inst.AssetType { + t.Errorf("AssetType mismatch: got %v, want %v", mappedBack.AssetType, inst.AssetType) + } + if mappedBack.ProviderSymbols["kis"] != inst.ProviderSymbols["kis"] { + t.Errorf("ProviderSymbols mismatch: got %+v, want %+v", mappedBack.ProviderSymbols, inst.ProviderSymbols) + } +} diff --git a/services/worker/internal/storage/postgres/migrations/000003_instrument_asset_type.down.sql b/services/worker/internal/storage/postgres/migrations/000003_instrument_asset_type.down.sql new file mode 100644 index 0000000..c7f8030 --- /dev/null +++ b/services/worker/internal/storage/postgres/migrations/000003_instrument_asset_type.down.sql @@ -0,0 +1 @@ +ALTER TABLE instruments DROP COLUMN asset_type; diff --git a/services/worker/internal/storage/postgres/migrations/000003_instrument_asset_type.up.sql b/services/worker/internal/storage/postgres/migrations/000003_instrument_asset_type.up.sql new file mode 100644 index 0000000..cd7be74 --- /dev/null +++ b/services/worker/internal/storage/postgres/migrations/000003_instrument_asset_type.up.sql @@ -0,0 +1 @@ +ALTER TABLE instruments ADD COLUMN asset_type TEXT NOT NULL DEFAULT ''; diff --git a/services/worker/internal/storage/postgres/queries/queries.sql b/services/worker/internal/storage/postgres/queries/queries.sql index da9e2e0..5b5ab1c 100644 --- a/services/worker/internal/storage/postgres/queries/queries.sql +++ b/services/worker/internal/storage/postgres/queries/queries.sql @@ -1,21 +1,22 @@ -- name: UpsertInstrument :exec -INSERT INTO instruments (id, market, venue, symbol, name, currency, provider_symbols) -VALUES ($1, $2, $3, $4, $5, $6, $7) +INSERT INTO instruments (id, market, venue, symbol, name, currency, provider_symbols, asset_type) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO UPDATE SET market = EXCLUDED.market, venue = EXCLUDED.venue, symbol = EXCLUDED.symbol, name = EXCLUDED.name, currency = EXCLUDED.currency, - provider_symbols = EXCLUDED.provider_symbols; + provider_symbols = EXCLUDED.provider_symbols, + asset_type = EXCLUDED.asset_type; -- name: GetInstrument :one -SELECT id, market, venue, symbol, name, currency, provider_symbols +SELECT id, market, venue, symbol, name, currency, provider_symbols, asset_type FROM instruments WHERE id = $1; -- name: ListInstruments :many -SELECT id, market, venue, symbol, name, currency, provider_symbols +SELECT id, market, venue, symbol, name, currency, provider_symbols, asset_type FROM instruments; -- name: UpsertBar :exec diff --git a/services/worker/internal/storage/postgres/sqlc/models.go b/services/worker/internal/storage/postgres/sqlc/models.go index 059aa91..81e0a06 100644 --- a/services/worker/internal/storage/postgres/sqlc/models.go +++ b/services/worker/internal/storage/postgres/sqlc/models.go @@ -52,4 +52,5 @@ type Instrument struct { Name string Currency string ProviderSymbols []byte + AssetType string } diff --git a/services/worker/internal/storage/postgres/sqlc/queries.sql.go b/services/worker/internal/storage/postgres/sqlc/queries.sql.go index 9b476fb..1121e40 100644 --- a/services/worker/internal/storage/postgres/sqlc/queries.sql.go +++ b/services/worker/internal/storage/postgres/sqlc/queries.sql.go @@ -60,7 +60,7 @@ func (q *Queries) GetBars(ctx context.Context, arg GetBarsParams) ([]Bar, error) } const getInstrument = `-- name: GetInstrument :one -SELECT id, market, venue, symbol, name, currency, provider_symbols +SELECT id, market, venue, symbol, name, currency, provider_symbols, asset_type FROM instruments WHERE id = $1 ` @@ -76,6 +76,7 @@ func (q *Queries) GetInstrument(ctx context.Context, id string) (Instrument, err &i.Name, &i.Currency, &i.ProviderSymbols, + &i.AssetType, ) return i, err } @@ -128,7 +129,7 @@ func (q *Queries) GetRun(ctx context.Context, id string) (BacktestRun, error) { } const listInstruments = `-- name: ListInstruments :many -SELECT id, market, venue, symbol, name, currency, provider_symbols +SELECT id, market, venue, symbol, name, currency, provider_symbols, asset_type FROM instruments ` @@ -149,6 +150,7 @@ func (q *Queries) ListInstruments(ctx context.Context) ([]Instrument, error) { &i.Name, &i.Currency, &i.ProviderSymbols, + &i.AssetType, ); err != nil { return nil, err } @@ -234,15 +236,16 @@ func (q *Queries) UpsertBar(ctx context.Context, arg UpsertBarParams) error { } const upsertInstrument = `-- name: UpsertInstrument :exec -INSERT INTO instruments (id, market, venue, symbol, name, currency, provider_symbols) -VALUES ($1, $2, $3, $4, $5, $6, $7) +INSERT INTO instruments (id, market, venue, symbol, name, currency, provider_symbols, asset_type) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO UPDATE SET market = EXCLUDED.market, venue = EXCLUDED.venue, symbol = EXCLUDED.symbol, name = EXCLUDED.name, currency = EXCLUDED.currency, - provider_symbols = EXCLUDED.provider_symbols + provider_symbols = EXCLUDED.provider_symbols, + asset_type = EXCLUDED.asset_type ` type UpsertInstrumentParams struct { @@ -253,6 +256,7 @@ type UpsertInstrumentParams struct { Name string Currency string ProviderSymbols []byte + AssetType string } func (q *Queries) UpsertInstrument(ctx context.Context, arg UpsertInstrumentParams) error { @@ -264,6 +268,7 @@ func (q *Queries) UpsertInstrument(ctx context.Context, arg UpsertInstrumentPara arg.Name, arg.Currency, arg.ProviderSymbols, + arg.AssetType, ) return err } diff --git a/services/worker/testdata/providers/kis/provider_symbols.sample.json b/services/worker/testdata/providers/kis/provider_symbols.sample.json index 13d4345..e880725 100644 --- a/services/worker/testdata/providers/kis/provider_symbols.sample.json +++ b/services/worker/testdata/providers/kis/provider_symbols.sample.json @@ -3,7 +3,7 @@ "selectors": [ { "kind": "watchlist", - "symbols": ["005930", "000660"] + "symbols": ["005930", "000660", "AAPL", "SPY"] }, { "kind": "exchange", @@ -24,6 +24,7 @@ "symbol": "005930", "name": "Samsung Electronics", "currency": "KRW", + "asset_type": "equity", "provider_symbols": { "kis": "005930" } @@ -35,9 +36,34 @@ "symbol": "000660", "name": "SK Hynix", "currency": "KRW", + "asset_type": "equity", "provider_symbols": { "kis": "000660" } + }, + { + "id": "NASDAQ:AAPL", + "market": "US", + "venue": "NASDAQ", + "symbol": "AAPL", + "name": "Apple Inc.", + "currency": "USD", + "asset_type": "equity", + "provider_symbols": { + "kis": "AAPL" + } + }, + { + "id": "NASDAQ:SPY", + "market": "US", + "venue": "NASDAQ", + "symbol": "SPY", + "name": "SPDR S&P 500 ETF Trust", + "currency": "USD", + "asset_type": "etf", + "provider_symbols": { + "kis": "SPY" + } } ] }