Files
stocksearch/services/theme_service.go
2026-03-31 19:32:59 +09:00

153 lines
4.4 KiB
Go

package services
import (
"encoding/json"
"fmt"
"stocksearch/models"
"strings"
"time"
)
// ThemeService 테마 분석 서비스
type ThemeService struct {
kiwoom *KiwoomClient
cache *CacheService
}
var themeSvc *ThemeService
// GetThemeService 테마 서비스 싱글턴 반환
func GetThemeService() *ThemeService {
if themeSvc == nil {
themeSvc = &ThemeService{
kiwoom: GetKiwoomClient(),
cache: GetCacheService(),
}
}
return themeSvc
}
// GetThemes ka90001: 테마그룹 목록 조회 (캐시 1분)
// sortTp: "3"=상위등락률, "1"=상위기간수익률
func (s *ThemeService) GetThemes(dateTp, sortTp string) ([]models.ThemeGroup, error) {
cacheKey := fmt.Sprintf("themes:%s:%s", dateTp, sortTp)
if cached, ok := s.cache.Get(cacheKey); ok {
if groups, ok := cached.([]models.ThemeGroup); ok {
return groups, nil
}
}
body, err := s.kiwoom.post("ka90001", "/api/dostk/thme", map[string]string{
"qry_tp": "0", // 전체검색
"stk_cd": "",
"date_tp": dateTp,
"thema_nm": "",
"flu_pl_amt_tp": sortTp,
"stex_tp": "1", // KRX
})
if err != nil {
return nil, fmt.Errorf("테마 목록 조회 실패: %w", err)
}
var result struct {
ThemaGrp []struct {
ThemaGrpCd string `json:"thema_grp_cd"`
ThemaNm string `json:"thema_nm"`
StkNum string `json:"stk_num"`
FluSig string `json:"flu_sig"`
FluRt string `json:"flu_rt"`
RisingStkNum string `json:"rising_stk_num"`
FallStkNum string `json:"fall_stk_num"`
DtPrftRt string `json:"dt_prft_rt"`
MainStk string `json:"main_stk"`
} `json:"thema_grp"`
ReturnCode int `json:"return_code"`
ReturnMsg string `json:"return_msg"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("테마 목록 파싱 실패: %w", err)
}
if result.ReturnCode != 0 {
return nil, fmt.Errorf("테마 목록 조회 실패: %s", result.ReturnMsg)
}
groups := make([]models.ThemeGroup, 0, len(result.ThemaGrp))
for _, g := range result.ThemaGrp {
groups = append(groups, models.ThemeGroup{
Code: g.ThemaGrpCd,
Name: g.ThemaNm,
StockCount: int(parseIntSafe(g.StkNum)),
FluSig: g.FluSig,
FluRt: parseFloatSafe(strings.TrimPrefix(g.FluRt, "+")),
RisingCount: int(parseIntSafe(g.RisingStkNum)),
FallCount: int(parseIntSafe(g.FallStkNum)),
PeriodRt: parseFloatSafe(strings.TrimPrefix(g.DtPrftRt, "+")),
MainStock: g.MainStk,
})
}
s.cache.Set(cacheKey, groups, time.Minute)
return groups, nil
}
// GetThemeStocks ka90002: 테마구성종목 조회 (캐시 2분)
func (s *ThemeService) GetThemeStocks(themeCode, dateTp string) (*models.ThemeDetail, error) {
cacheKey := fmt.Sprintf("theme_stocks:%s:%s", themeCode, dateTp)
if cached, ok := s.cache.Get(cacheKey); ok {
if detail, ok := cached.(*models.ThemeDetail); ok {
return detail, nil
}
}
body, err := s.kiwoom.post("ka90002", "/api/dostk/thme", map[string]string{
"thema_grp_cd": themeCode,
"date_tp": dateTp,
"stex_tp": "1", // KRX
})
if err != nil {
return nil, fmt.Errorf("테마 구성종목 조회 실패: %w", err)
}
var result struct {
FluRt string `json:"flu_rt"`
DtPrftRt string `json:"dt_prft_rt"`
ThemaCompStk []struct {
StkCd string `json:"stk_cd"`
StkNm string `json:"stk_nm"`
CurPrc string `json:"cur_prc"`
FluSig string `json:"flu_sig"`
PredPre string `json:"pred_pre"`
FluRt string `json:"flu_rt"`
} `json:"thema_comp_stk"`
ReturnCode int `json:"return_code"`
ReturnMsg string `json:"return_msg"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("테마 구성종목 파싱 실패: %w", err)
}
if result.ReturnCode != 0 {
return nil, fmt.Errorf("테마 구성종목 조회 실패: %s", result.ReturnMsg)
}
stocks := make([]models.ThemeStock, 0, len(result.ThemaCompStk))
for _, s := range result.ThemaCompStk {
stocks = append(stocks, models.ThemeStock{
Code: s.StkCd,
Name: s.StkNm,
CurPrc: absParseIntSafe(s.CurPrc),
FluSig: s.FluSig,
PredPre: parseIntSafe(s.PredPre),
FluRt: parseFloatSafe(strings.TrimPrefix(s.FluRt, "+")),
})
}
detail := &models.ThemeDetail{
FluRt: parseFloatSafe(strings.TrimPrefix(result.FluRt, "+")),
PeriodRt: parseFloatSafe(strings.TrimPrefix(result.DtPrftRt, "+")),
Stocks: stocks,
}
s.cache.Set(cacheKey, detail, 2*time.Minute)
return detail, nil
}