// 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 }