- 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
108 lines
4.1 KiB
Go
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
|
|
}
|