- Move flutter operator console and push notification milestones to operator-ui phase - Archive kis-live-data-collection-pipeline milestone - Add command-first operator workflow milestone - Add operator-ui phase definition - Add paper-trading-command-workflow milestone - Update phase documentation and dependencies
117 lines
4.6 KiB
Go
117 lines
4.6 KiB
Go
// Package kis decodes and normalizes Korea Investment & Securities (KIS) daily
|
|
// chart provider payloads into ALT domain market types, with live REST client
|
|
// wiring kept behind explicit runtime configuration.
|
|
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
|
|
}
|