// Package kis decodes and normalizes Korea Investment & Securities (KIS) daily // chart provider payloads into ALT domain market types. It works against mock // fixtures only; live HTTP and credential wiring belong to a later milestone. package kis import ( "encoding/json" "fmt" "time" "git.toki-labs.com/toki/alt/packages/domain/market" ) const ( DailyItemChartPricePath = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice" DailyItemChartPriceTRID = "FHKST03010100" ) // seoulOffset is the fixed KST offset. KRX has no DST, so a fixed +09:00 zone is // deterministic and avoids depending on tzdata being present at runtime. const seoulOffsetSeconds = 9 * 60 * 60 // kisDateLayout matches the KIS stck_bsop_date field, e.g. "20240527". const kisDateLayout = "20060102" // DailyItemChartPriceRequest mirrors the KIS inquire-daily-itemchartprice // request shape captured in the worker test fixtures. type DailyItemChartPriceRequest struct { Provider string `json:"provider"` Endpoint string `json:"endpoint"` TrID string `json:"tr_id"` Params map[string]string `json:"params"` } // DailyItemChartPriceResponse mirrors the KIS daily chart response payload. type DailyItemChartPriceResponse struct { ReturnCode string `json:"rt_cd"` MessageCd string `json:"msg_cd"` Message string `json:"msg1"` Output1 DailyItemChartPriceSummary `json:"output1"` Output2 []DailyItemChartPriceBarRow `json:"output2"` } // DailyItemChartPriceSummary is the KIS output1 instrument-level metadata block. type DailyItemChartPriceSummary struct { Name string `json:"hts_kor_isnm"` ShortCode string `json:"stck_shrn_iscd"` CurrentPrice string `json:"stck_prpr"` AccumVolume string `json:"acml_vol"` AccumTradeAmt string `json:"acml_tr_pbmn"` } // DailyItemChartPriceBarRow is a single KIS output2 OHLCV row. type DailyItemChartPriceBarRow struct { BusinessDate string `json:"stck_bsop_date"` Open string `json:"stck_oprc"` High string `json:"stck_hgpr"` Low string `json:"stck_lwpr"` Close string `json:"stck_clpr"` Volume string `json:"acml_vol"` TradeAmount string `json:"acml_tr_pbmn"` } // DecodeDailyItemChartPriceRequest decodes a KIS daily chart request fixture. func DecodeDailyItemChartPriceRequest(data []byte) (DailyItemChartPriceRequest, error) { var req DailyItemChartPriceRequest if err := json.Unmarshal(data, &req); err != nil { return DailyItemChartPriceRequest{}, fmt.Errorf("decode kis daily request: %w", err) } return req, nil } // DecodeDailyItemChartPriceResponse decodes a KIS daily chart response fixture // and validates that it represents a successful payload with bar rows. func DecodeDailyItemChartPriceResponse(data []byte) (DailyItemChartPriceResponse, error) { var resp DailyItemChartPriceResponse if err := json.Unmarshal(data, &resp); err != nil { return DailyItemChartPriceResponse{}, fmt.Errorf("decode kis daily response: %w", err) } if resp.ReturnCode != "0" { return DailyItemChartPriceResponse{}, fmt.Errorf("kis daily response not successful: rt_cd=%q msg=%q", resp.ReturnCode, resp.Message) } if len(resp.Output2) == 0 { return DailyItemChartPriceResponse{}, fmt.Errorf("kis daily response has no output2 rows") } return resp, nil } // NormalizeDailyBars converts KIS output2 rows into ALT domain bars for the // given instrument. KRX daily defaults are applied: Asia/Seoul midnight // timestamps, the daily timeframe, and the instrument currency (KRW for KRX). func NormalizeDailyBars(resp DailyItemChartPriceResponse, inst market.Instrument) ([]market.Bar, error) { currency := inst.Currency if currency == "" { currency = market.CurrencyKRW } seoul := time.FixedZone("Asia/Seoul", seoulOffsetSeconds) bars := make([]market.Bar, 0, len(resp.Output2)) for i, row := range resp.Output2 { ts, err := time.ParseInLocation(kisDateLayout, row.BusinessDate, seoul) if err != nil { return nil, fmt.Errorf("parse business date for row %d: %w", i, err) } bars = append(bars, market.Bar{ InstrumentID: inst.ID, Timeframe: market.TimeframeDaily, Timestamp: ts, Open: market.Price{Currency: currency, Amount: market.Decimal{Value: row.Open}}, High: market.Price{Currency: currency, Amount: market.Decimal{Value: row.High}}, Low: market.Price{Currency: currency, Amount: market.Decimal{Value: row.Low}}, Close: market.Price{Currency: currency, Amount: market.Decimal{Value: row.Close}}, Volume: market.Quantity{Amount: market.Decimal{Value: row.Volume}}, }) } return bars, nil }