fix(domain): capability matrix API 구현 누락 항목을 보완한다
ProviderCapability에 Rules 필드 추가 CheckBars, ProviderCapabilityRequest, ProviderCapabilityDecision, ProviderCapabilityRule 타입 추가 types_test.go에 TestProviderCapabilityMatrix 테스트 추가 CODE_REVIEW-local-G05.md 구현 체크리스트 및 검증 결과 작성
This commit is contained in:
parent
97373ca14e
commit
012935e906
3 changed files with 244 additions and 11 deletions
|
|
@ -35,15 +35,15 @@ task=m-backtest-multi-timeframe-coverage/01_domain_capability_matrix, plan=0, ta
|
|||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Domain capability matrix API | [ ] |
|
||||
| [API-2] Compatibility compile guard | [ ] |
|
||||
| [API-1] Domain capability matrix API | [x] |
|
||||
| [API-2] Compatibility compile guard | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `ProviderCapability`를 provider/market/venue/asset type/timeframe matrix request와 accepted/rejected decision을 표현하도록 확장하되, 후속 rollout 전 기존 daily call site가 컴파일될 수 있는 호환 wrapper를 유지한다.
|
||||
- [ ] domain tests에 KR/US, equity/ETF, monthly/daily/minute_1/minute_5 accepted/rejected와 rejection reason case를 추가한다.
|
||||
- [ ] `go test ./packages/domain/...`와 `go test ./services/worker/...`를 실행해 domain behavior와 기존 worker call-site compile을 확인한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
- [x] `ProviderCapability`를 provider/market/venue/asset type/timeframe matrix request와 accepted/rejected decision을 표현하도록 확장하되, 후속 rollout 전 기존 daily call site가 컴파일될 수 있는 호환 wrapper를 유지한다.
|
||||
- [x] domain tests에 KR/US, equity/ETF, monthly/daily/minute_1/minute_5 accepted/rejected와 rejection reason case를 추가한다.
|
||||
- [x] `go test ./packages/domain/...`와 `go test ./services/worker/...`를 실행해 domain behavior와 기존 worker call-site compile을 확인한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
|
|
@ -65,11 +65,14 @@ task=m-backtest-multi-timeframe-coverage/01_domain_capability_matrix, plan=0, ta
|
|||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
- PLAN에서는 `ProviderCapability` struct을 `Rules []ProviderCapabilityRule`로 완전히 교체하도록 명시했지만, 기존 worker call site(`RequireDailyBars`)의 컴파일을 유지하기 위해 기존 `Venues`/`Timeframes` 필드와 `SupportsDailyBars` 메서드를 deprecated 호환 필드로 유지했다.
|
||||
- PLAN의 After 예시에서는 `ProviderCapability`에 `Venues`/`Timeframes` 필드가 없었지만, 실제 구현에서는 후속 rollout 전 호환성을 위해 함께 두었다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
- `CheckBars`는 `ProviderCapabilityRule` slice를 순회하며 첫 번째 일치하는 rule의 timeframe 지원 여부를 판정 기준으로 사용한다. rule이 하나도 없거나 timeframe이 false이면 rejected.
|
||||
- `RequireDailyBars`는 기존 call site가 깨지지 않도록 deprecated 메서드로 유지하고, 새 matrix API(`CheckBars`)를 호출하지 않는다.
|
||||
- `ProviderCapabilityRule.Timeframes`는 `map[Timeframe]bool`로, 허용/불허를 명시적으로 설정한다. (false 기본값을 사용하여 미지원도 명시 가능)
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
|
|
@ -103,19 +106,34 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
|
|||
### API-1 중간 검증
|
||||
```bash
|
||||
$ go test ./packages/domain/...
|
||||
(output)
|
||||
ok git.toki-labs.com/toki/alt/packages/domain/backtest (cached)
|
||||
ok git.toki-labs.com/toki/alt/packages/domain/market 0.003s
|
||||
ok git.toki-labs.com/toki/alt/packages/domain/trading (cached)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ go test ./services/worker/...
|
||||
(output)
|
||||
ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker 0.012s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-migrate 0.007s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.008s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/config (cached)
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/contracts (cached)
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.006s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/livetrading 0.015s
|
||||
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.009s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/papertrading 0.009s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.025s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys (cached)
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.067s
|
||||
ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.155s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ go test ./packages/domain/... ./services/worker/...
|
||||
(output)
|
||||
(위 API-1, API-2 검증 결과와 동일 — 모든 패키지 통과)
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -104,10 +104,12 @@ const (
|
|||
// can serve. It keeps provider distinct from market: one provider (KIS) can
|
||||
// declare several venues across multiple markets, while a narrower future
|
||||
// provider can declare KR-only coverage without changing the import boundary.
|
||||
// Rules holds the provider/market/venue/asset type/timeframe capability matrix.
|
||||
type ProviderCapability struct {
|
||||
Provider Provider
|
||||
Venues map[Venue]bool
|
||||
Timeframes map[Timeframe]bool
|
||||
Rules []ProviderCapabilityRule
|
||||
}
|
||||
|
||||
// SupportsDailyBars reports whether the capability covers daily bars for venue.
|
||||
|
|
@ -115,11 +117,104 @@ func (c ProviderCapability) SupportsDailyBars(venue Venue) bool {
|
|||
return c.Venues[venue] && c.Timeframes[TimeframeDaily]
|
||||
}
|
||||
|
||||
// ProviderCapabilityRule defines a single row in the capability matrix.
|
||||
// Each rule matches a market/venue combination, a set of allowed asset types
|
||||
// and a set of allowed timeframes.
|
||||
type ProviderCapabilityRule struct {
|
||||
Market Market
|
||||
Venue Venue
|
||||
AssetTypes map[AssetType]bool
|
||||
Timeframes map[Timeframe]bool
|
||||
}
|
||||
|
||||
// ProviderCapabilityRequest is a call-site request that describes what
|
||||
// provider/market/venue/asset type/timeframe combination the caller needs.
|
||||
type ProviderCapabilityRequest struct {
|
||||
Provider Provider
|
||||
Market Market
|
||||
Venue Venue
|
||||
AssetType AssetType
|
||||
Timeframe Timeframe
|
||||
}
|
||||
|
||||
// ProviderCapabilityDecision holds the result of a capability check.
|
||||
type ProviderCapabilityDecision struct {
|
||||
Accepted bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
// CheckBars evaluates the full provider/market/venue/asset type/timeframe
|
||||
// capability matrix and returns whether the request is accepted or rejected
|
||||
// with a stable reason string.
|
||||
func (c ProviderCapability) CheckBars(req ProviderCapabilityRequest) ProviderCapabilityDecision {
|
||||
if req.Provider != c.Provider {
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: false,
|
||||
Reason: fmt.Sprintf("provider %q does not match capability provider %q", req.Provider, c.Provider),
|
||||
}
|
||||
}
|
||||
if !c.Venues[req.Venue] {
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: false,
|
||||
Reason: fmt.Sprintf("provider %q does not support venue %q", req.Provider, req.Venue),
|
||||
}
|
||||
}
|
||||
meta, ok := GetVenueMetadata(req.Venue)
|
||||
if !ok {
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: false,
|
||||
Reason: fmt.Sprintf("provider %q does not support venue %q", req.Provider, req.Venue),
|
||||
}
|
||||
}
|
||||
if req.Market != "" && req.Market != meta.Market {
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: false,
|
||||
Reason: fmt.Sprintf("market %q does not match venue %q market %q", req.Market, req.Venue, meta.Market),
|
||||
}
|
||||
}
|
||||
if len(c.Rules) == 0 {
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: false,
|
||||
Reason: fmt.Sprintf("no capability rules defined for provider %q", c.Provider),
|
||||
}
|
||||
}
|
||||
var matched bool
|
||||
for _, rule := range c.Rules {
|
||||
if rule.Market != req.Market || rule.Venue != req.Venue {
|
||||
continue
|
||||
}
|
||||
if !rule.AssetTypes[req.AssetType] {
|
||||
continue
|
||||
}
|
||||
if !rule.Timeframes[req.Timeframe] {
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: false,
|
||||
Reason: fmt.Sprintf("provider %q does not support asset type %q timeframe %q on venue %q",
|
||||
req.Provider, req.AssetType, req.Timeframe, req.Venue),
|
||||
}
|
||||
}
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
if !matched {
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: false,
|
||||
Reason: fmt.Sprintf("no capability rule matches market %q venue %q asset type %q timeframe %q", req.Market, req.Venue, req.AssetType, req.Timeframe),
|
||||
}
|
||||
}
|
||||
return ProviderCapabilityDecision{
|
||||
Accepted: true,
|
||||
Reason: "",
|
||||
}
|
||||
}
|
||||
|
||||
// RequireDailyBars is the pre-import gate that rejects unsupported
|
||||
// provider/market/venue combinations before any fetch. It validates that the
|
||||
// request provider matches this capability, that the selector pins a venue this
|
||||
// capability supports, that the selector market (when set) agrees with the
|
||||
// venue's market, and that the capability covers daily bars for that venue.
|
||||
// Deprecated: use CheckBars instead. This wrapper is kept for backward
|
||||
// compatibility with existing call sites until full rollout.
|
||||
func (c ProviderCapability) RequireDailyBars(provider Provider, selector UniverseSelector) error {
|
||||
if provider != c.Provider {
|
||||
return fmt.Errorf("provider %q does not match capability provider %q", provider, c.Provider)
|
||||
|
|
|
|||
|
|
@ -164,3 +164,123 @@ func TestAssetTypeVocabulary(t *testing.T) {
|
|||
t.Errorf("AssetTypeETF: got %q, want %q", string(AssetTypeETF), "etf")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderCapabilityMatrix(t *testing.T) {
|
||||
// KIS capability with explicit rules for KR/US equity/ETF daily/monthly/minute.
|
||||
kis := ProviderCapability{
|
||||
Provider: ProviderKIS,
|
||||
Venues: map[Venue]bool{
|
||||
VenueKRX: true,
|
||||
VenueNASDAQ: true,
|
||||
VenueNYSE: true,
|
||||
},
|
||||
Timeframes: map[Timeframe]bool{
|
||||
TimeframeDaily: true,
|
||||
TimeframeMonthly: true,
|
||||
TimeframeMin1: true,
|
||||
TimeframeMin5: true,
|
||||
},
|
||||
Rules: []ProviderCapabilityRule{
|
||||
{
|
||||
Market: MarketKR,
|
||||
Venue: VenueKRX,
|
||||
AssetTypes: map[AssetType]bool{
|
||||
AssetTypeEquity: true,
|
||||
AssetTypeETF: true,
|
||||
},
|
||||
Timeframes: map[Timeframe]bool{
|
||||
TimeframeDaily: true,
|
||||
TimeframeMonthly: false,
|
||||
TimeframeMin1: false,
|
||||
TimeframeMin5: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Market: MarketUS,
|
||||
Venue: VenueNASDAQ,
|
||||
AssetTypes: map[AssetType]bool{
|
||||
AssetTypeEquity: true,
|
||||
AssetTypeETF: true,
|
||||
},
|
||||
Timeframes: map[Timeframe]bool{
|
||||
TimeframeDaily: true,
|
||||
TimeframeMonthly: false,
|
||||
TimeframeMin1: false,
|
||||
TimeframeMin5: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req ProviderCapabilityRequest
|
||||
wantAccepted bool
|
||||
wantReason string
|
||||
}{
|
||||
// Accepted cases
|
||||
{"KIS KR KRX equity daily", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketKR, Venue: VenueKRX,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeDaily,
|
||||
}, true, ""},
|
||||
{"KIS KR KRX etf daily", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketKR, Venue: VenueKRX,
|
||||
AssetType: AssetTypeETF, Timeframe: TimeframeDaily,
|
||||
}, true, ""},
|
||||
{"KIS US NASDAQ equity daily", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketUS, Venue: VenueNASDAQ,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeDaily,
|
||||
}, true, ""},
|
||||
{"KIS US NASDAQ etf daily", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketUS, Venue: VenueNASDAQ,
|
||||
AssetType: AssetTypeETF, Timeframe: TimeframeDaily,
|
||||
}, true, ""},
|
||||
// Rejected: monthly
|
||||
{"KIS KR KRX equity monthly rejected", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketKR, Venue: VenueKRX,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeMonthly,
|
||||
}, false, "provider \"kis\" does not support asset type \"equity\" timeframe \"1mo\" on venue \"KRX\""},
|
||||
// Rejected: minute_1
|
||||
{"KIS KR KRX equity minute_1 rejected", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketKR, Venue: VenueKRX,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeMin1,
|
||||
}, false, "provider \"kis\" does not support asset type \"equity\" timeframe \"1m\" on venue \"KRX\""},
|
||||
// Rejected: minute_5
|
||||
{"KIS KR KRX equity minute_5 rejected", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketKR, Venue: VenueKRX,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeMin5,
|
||||
}, false, "provider \"kis\" does not support asset type \"equity\" timeframe \"5m\" on venue \"KRX\""},
|
||||
// Rejected: provider mismatch
|
||||
{"provider mismatch rejected", ProviderCapabilityRequest{
|
||||
Provider: ProviderYahoo, Market: MarketUS, Venue: VenueNASDAQ,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeDaily,
|
||||
}, false, "provider \"yahoo\" does not match capability provider \"kis\""},
|
||||
// Rejected: unknown venue
|
||||
{"unknown venue rejected", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketUS, Venue: Venue("LSE"),
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeDaily,
|
||||
}, false, "provider \"kis\" does not support venue \"LSE\""},
|
||||
// Rejected: market/venue mismatch
|
||||
{"market/venue mismatch rejected", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketKR, Venue: VenueNASDAQ,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeDaily,
|
||||
}, false, "market \"KR\" does not match venue \"NASDAQ\" market \"US\""},
|
||||
// Rejected: no matching rule (asset type not in rule)
|
||||
{"no matching rule", ProviderCapabilityRequest{
|
||||
Provider: ProviderKIS, Market: MarketUS, Venue: VenueNYSE,
|
||||
AssetType: AssetTypeEquity, Timeframe: TimeframeDaily,
|
||||
}, false, "no capability rule matches market \"US\" venue \"NYSE\" asset type \"equity\" timeframe \"1d\""},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
dec := kis.CheckBars(c.req)
|
||||
if dec.Accepted != c.wantAccepted {
|
||||
t.Errorf("CheckBars(%q) accepted: got %v, want %v", c.name, dec.Accepted, c.wantAccepted)
|
||||
}
|
||||
if c.wantReason != "" && dec.Reason != c.wantReason {
|
||||
t.Errorf("CheckBars(%q) reason: got %q, want %q", c.name, dec.Reason, c.wantReason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue