alt/services/worker/internal/marketdata/datacheck/datacheck.go
toki 8c3c4ba5a2 feat: operator surface phase updates, worker backtest/kis live client, binary utilities
- Add bin/kis-paper-daily-smoke and bin/kis-sops-env utilities
- Update agent-roadmap for operator surface phase
- Add worker backtest bar source and Kis live client
- Update KIS live secret configuration and documentation
- Refine worker datacheck and daily chart price providers
2026-06-03 12:10:48 +09:00

108 lines
4.1 KiB
Go

// Package datacheck provides a credential-free smoke surface for the Korea
// daily data foundation: it feeds an embedded KIS daily-chart fixture through
// the real decode/normalize and import pipeline into an in-memory store, then
// queries the stored bars back and prints a stable one-line summary. It proves
// normalized daily bars are queryable in the shape the backtest milestone will
// consume, without any live KIS credential, secret store, or PostgreSQL.
package datacheck
import (
"context"
_ "embed"
"fmt"
"io"
"sort"
"time"
"git.toki-labs.com/toki/alt/packages/domain/market"
"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
"git.toki-labs.com/toki/alt/services/worker/internal/providers/kis"
)
// dailyChartResponseFixture is a self-contained copy of the KIS daily chart
// response fixture. It is embedded here (rather than read from the module's
// testdata tree) so the smoke command builds and runs as a normal binary: Go
// build tooling ignores testdata, and go:embed cannot reach across module
// directories with "..".
//
//go:embed fixtures/daily_itemchartprice_response.sample.json
var dailyChartResponseFixture []byte
// fixtureProvider implements importer.DailyBarProvider by normalizing the
// embedded KIS fixture. It exercises the real kis decode/normalize path instead
// of fabricating domain bars, so the smoke covers provider normalization too.
type fixtureProvider struct{}
func (fixtureProvider) FetchDailyBars(_ context.Context, _ importer.DailyBarRequest) ([]importer.InstrumentBars, error) {
resp, err := kis.DecodeDailyItemChartPriceResponse(dailyChartResponseFixture)
if err != nil {
return nil, fmt.Errorf("datacheck: decode fixture: %w", err)
}
inst := market.Instrument{
ID: market.InstrumentID("KRX:" + resp.Output1.ShortCode),
Market: market.MarketKR,
Venue: market.VenueKRX,
Symbol: resp.Output1.ShortCode,
Name: resp.Output1.Name,
Currency: market.CurrencyKRW,
ProviderSymbols: map[string]string{string(market.ProviderKIS): resp.Output1.ShortCode},
}
bars, err := kis.NormalizeDailyBars(resp, inst)
if err != nil {
return nil, fmt.Errorf("datacheck: normalize bars: %w", err)
}
return []importer.InstrumentBars{{Instrument: inst, Bars: bars}}, nil
}
// request mirrors the watchlist daily bar request a real import job would carry.
func request() importer.DailyBarRequest {
return importer.DailyBarRequest{
Provider: market.ProviderKIS,
Selector: market.UniverseSelector{
Kind: market.UniverseSelectorWatchlist,
Venue: market.VenueKRX,
Symbols: []string{"005930"},
},
}
}
// Run executes the mock data check and writes a stable summary line to w. The
// summary reports the instrument count, total daily bar count, and the first
// and last bar dates (YYYY-MM-DD) queried back from the store.
func Run(ctx context.Context, w io.Writer) error {
store := newMemStore()
imp := importer.New(fixtureProvider{}, store)
if _, err := imp.ImportDailyBars(ctx, request()); err != nil {
return fmt.Errorf("datacheck: import: %w", err)
}
instruments, err := store.ListInstruments(ctx)
if err != nil {
return fmt.Errorf("datacheck: list instruments: %w", err)
}
// Query daily bars back over an unbounded range so the summary reflects what
// a backtest consumer would read, not just what the importer reported.
var bars []market.Bar
for _, inst := range instruments {
got, err := store.GetBars(ctx, inst.ID, market.TimeframeDaily, time.Time{}, time.Unix(1<<62, 0))
if err != nil {
return fmt.Errorf("datacheck: get bars for %q: %w", inst.ID, err)
}
bars = append(bars, got...)
}
sort.Slice(bars, func(i, j int) bool { return bars[i].Timestamp.Before(bars[j].Timestamp) })
first, last := "none", "none"
if len(bars) > 0 {
first = bars[0].Timestamp.Format("2006-01-02")
last = bars[len(bars)-1].Timestamp.Format("2006-01-02")
}
if _, err := fmt.Fprintf(w, "instrument_count=%d bar_count=%d first=%s last=%s\n",
len(instruments), len(bars), first, last); err != nil {
return fmt.Errorf("datacheck: write summary: %w", err)
}
return nil
}