153 lines
4.4 KiB
Go
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
|
|
}
|