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

378 lines
11 KiB
Go

package services
import (
"encoding/json"
"fmt"
)
// AccountService 계좌 조회 서비스 (잔고/미체결/체결내역/주문가능)
type AccountService struct {
client *KiwoomClient
}
var accountService *AccountService
// GetAccountService 계좌 서비스 싱글턴 반환
func GetAccountService() *AccountService {
if accountService == nil {
accountService = &AccountService{client: GetKiwoomClient()}
}
return accountService
}
// --- 미체결 ---
// PendingOrder 미체결 주문 항목
type PendingOrder struct {
OrdNo string `json:"ordNo"` // 주문번호
StkCd string `json:"stkCd"` // 종목코드
StkNm string `json:"stkNm"` // 종목명
OrdQty string `json:"ordQty"` // 주문수량
OrdPric string `json:"ordPric"` // 주문가격
OsoQty string `json:"osoQty"` // 미체결수량
IoTpNm string `json:"ioTpNm"` // 주문구분
TrdeTp string `json:"trdeTp"` // 매매구분 (1:매도, 2:매수)
Tm string `json:"tm"` // 시간
CntrPric string `json:"cntrPric"` // 체결가
CntrQty string `json:"cntrQty"` // 체결량
}
// GetPendingOrders 미체결 주문 조회 (ka10075)
func (s *AccountService) GetPendingOrders() ([]PendingOrder, error) {
body := map[string]string{
"all_stk_tp": "0", // 전체종목
"trde_tp": "0", // 전체
"stk_cd": "",
"stex_tp": "0", // 통합
}
respBody, err := s.client.post("ka10075", "/api/dostk/acnt", body)
if err != nil {
return nil, fmt.Errorf("미체결 조회 실패: %w", err)
}
var result struct {
Oso []struct {
OrdNo string `json:"ord_no"`
StkCd string `json:"stk_cd"`
StkNm string `json:"stk_nm"`
OrdQty string `json:"ord_qty"`
OrdPric string `json:"ord_pric"`
OsoQty string `json:"oso_qty"`
IoTpNm string `json:"io_tp_nm"`
TrdeTp string `json:"trde_tp"`
Tm string `json:"tm"`
CntrPric string `json:"cntr_pric"`
CntrQty string `json:"cntr_qty"`
} `json:"oso"`
ReturnCode int `json:"return_code"`
ReturnMsg string `json:"return_msg"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("미체결 응답 파싱 실패: %w", err)
}
if result.ReturnCode != 0 {
return nil, fmt.Errorf("미체결 조회 오류: %s", result.ReturnMsg)
}
orders := make([]PendingOrder, 0, len(result.Oso))
for _, o := range result.Oso {
orders = append(orders, PendingOrder{
OrdNo: o.OrdNo,
StkCd: o.StkCd,
StkNm: o.StkNm,
OrdQty: o.OrdQty,
OrdPric: o.OrdPric,
OsoQty: o.OsoQty,
IoTpNm: o.IoTpNm,
TrdeTp: o.TrdeTp,
Tm: o.Tm,
CntrPric: o.CntrPric,
CntrQty: o.CntrQty,
})
}
return orders, nil
}
// --- 체결내역 ---
// OrderHistory 체결내역 항목
type OrderHistory struct {
OrdNo string `json:"ordNo"`
StkNm string `json:"stkNm"`
IoTpNm string `json:"ioTpNm"`
OrdPric string `json:"ordPric"`
OrdQty string `json:"ordQty"`
CntrPric string `json:"cntrPric"`
CntrQty string `json:"cntrQty"`
OsoQty string `json:"osoQty"`
TrdeCmsn string `json:"trdeCmsn"`
TrdeTax string `json:"trdeTax"`
OrdStt string `json:"ordStt"`
TrdeTp string `json:"trdeTp"`
OrdTm string `json:"ordTm"`
StkCd string `json:"stkCd"`
}
// GetOrderHistory 체결내역 조회 (ka10076)
func (s *AccountService) GetOrderHistory() ([]OrderHistory, error) {
body := map[string]string{
"stk_cd": "",
"qry_tp": "0", // 전체
"sell_tp": "0", // 전체
"ord_no": "",
"stex_tp": "0", // 통합
}
respBody, err := s.client.post("ka10076", "/api/dostk/acnt", body)
if err != nil {
return nil, fmt.Errorf("체결내역 조회 실패: %w", err)
}
var result struct {
Cntr []struct {
OrdNo string `json:"ord_no"`
StkNm string `json:"stk_nm"`
IoTpNm string `json:"io_tp_nm"`
OrdPric string `json:"ord_pric"`
OrdQty string `json:"ord_qty"`
CntrPric string `json:"cntr_pric"`
CntrQty string `json:"cntr_qty"`
OsoQty string `json:"oso_qty"`
TdyTrdeCmsn string `json:"tdy_trde_cmsn"`
TdyTrdeTax string `json:"tdy_trde_tax"`
OrdStt string `json:"ord_stt"`
TrdeTp string `json:"trde_tp"`
OrdTm string `json:"ord_tm"`
StkCd string `json:"stk_cd"`
} `json:"cntr"`
ReturnCode int `json:"return_code"`
ReturnMsg string `json:"return_msg"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("체결내역 응답 파싱 실패: %w", err)
}
if result.ReturnCode != 0 {
return nil, fmt.Errorf("체결내역 조회 오류: %s", result.ReturnMsg)
}
history := make([]OrderHistory, 0, len(result.Cntr))
for _, c := range result.Cntr {
history = append(history, OrderHistory{
OrdNo: c.OrdNo,
StkNm: c.StkNm,
IoTpNm: c.IoTpNm,
OrdPric: c.OrdPric,
OrdQty: c.OrdQty,
CntrPric: c.CntrPric,
CntrQty: c.CntrQty,
OsoQty: c.OsoQty,
TrdeCmsn: c.TdyTrdeCmsn,
TrdeTax: c.TdyTrdeTax,
OrdStt: c.OrdStt,
TrdeTp: c.TrdeTp,
OrdTm: c.OrdTm,
StkCd: c.StkCd,
})
}
return history, nil
}
// --- 잔고 ---
// BalanceStock 잔고 개별 종목
type BalanceStock struct {
StkCd string `json:"stkCd"`
StkNm string `json:"stkNm"`
EvltvPrft string `json:"evltvPrft"` // 평가손익
PrftRt string `json:"prftRt"` // 수익률
PurPric string `json:"purPric"` // 매입가
RmndQty string `json:"rmndQty"` // 보유수량
TrdeAbleQty string `json:"trdeAbleQty"` // 매매가능수량
CurPrc string `json:"curPrc"` // 현재가
PurAmt string `json:"purAmt"` // 매입금액
EvltAmt string `json:"evltAmt"` // 평가금액
}
// BalanceResult 잔고 조회 결과
type BalanceResult struct {
TotPurAmt string `json:"totPurAmt"` // 총매입금액
TotEvltAmt string `json:"totEvltAmt"` // 총평가금액
TotEvltPl string `json:"totEvltPl"` // 총평가손익
TotPrftRt string `json:"totPrftRt"` // 총수익률
PrsmDpstAsetAmt string `json:"prsmDpstAsetAmt"` // 추정예탁자산
Stocks []BalanceStock `json:"stocks"`
}
// GetBalance 계좌 잔고 조회 (kt00018)
func (s *AccountService) GetBalance() (*BalanceResult, error) {
body := map[string]string{
"qry_tp": "2", // 개별
"dmst_stex_tp": "KRX",
}
respBody, err := s.client.post("kt00018", "/api/dostk/acnt", body)
if err != nil {
return nil, fmt.Errorf("잔고 조회 실패: %w", err)
}
var result struct {
TotPurAmt string `json:"tot_pur_amt"`
TotEvltAmt string `json:"tot_evlt_amt"`
TotEvltPl string `json:"tot_evlt_pl"`
TotPrftRt string `json:"tot_prft_rt"`
PrsmDpstAsetAmt string `json:"prsm_dpst_aset_amt"`
AcntEvltRemnIndvTot []struct {
StkCd string `json:"stk_cd"`
StkNm string `json:"stk_nm"`
EvltvPrft string `json:"evltv_prft"`
PrftRt string `json:"prft_rt"`
PurPric string `json:"pur_pric"`
RmndQty string `json:"rmnd_qty"`
TrdeAbleQty string `json:"trde_able_qty"`
CurPrc string `json:"cur_prc"`
PurAmt string `json:"pur_amt"`
EvltAmt string `json:"evlt_amt"`
} `json:"acnt_evlt_remn_indv_tot"`
ReturnCode int `json:"return_code"`
ReturnMsg string `json:"return_msg"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("잔고 응답 파싱 실패: %w", err)
}
if result.ReturnCode != 0 {
return nil, fmt.Errorf("잔고 조회 오류: %s", result.ReturnMsg)
}
stocks := make([]BalanceStock, 0, len(result.AcntEvltRemnIndvTot))
for _, s := range result.AcntEvltRemnIndvTot {
stocks = append(stocks, BalanceStock{
StkCd: s.StkCd,
StkNm: s.StkNm,
EvltvPrft: s.EvltvPrft,
PrftRt: s.PrftRt,
PurPric: s.PurPric,
RmndQty: s.RmndQty,
TrdeAbleQty: s.TrdeAbleQty,
CurPrc: s.CurPrc,
PurAmt: s.PurAmt,
EvltAmt: s.EvltAmt,
})
}
return &BalanceResult{
TotPurAmt: result.TotPurAmt,
TotEvltAmt: result.TotEvltAmt,
TotEvltPl: result.TotEvltPl,
TotPrftRt: result.TotPrftRt,
PrsmDpstAsetAmt: result.PrsmDpstAsetAmt,
Stocks: stocks,
}, nil
}
// --- 예수금상세 ---
// DepositResult 예수금 상세 결과
type DepositResult struct {
Entr string `json:"entr"` // 예수금
D2Entra string `json:"d2Entra"` // D+2 추정예수금
OrdAlowAmt string `json:"ordAlowAmt"` // 주문가능금액
}
// GetDepositDetail 예수금 상세 조회 (kt00001)
func (s *AccountService) GetDepositDetail() (*DepositResult, error) {
body := map[string]string{
"qry_tp": "3", // 추정조회
}
respBody, err := s.client.post("kt00001", "/api/dostk/acnt", body)
if err != nil {
return nil, fmt.Errorf("예수금 조회 실패: %w", err)
}
var result struct {
Entr string `json:"entr"`
D2Entra string `json:"d2_entra"`
OrdAlowAmt string `json:"ord_alow_amt"`
ReturnCode int `json:"return_code"`
ReturnMsg string `json:"return_msg"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("예수금 응답 파싱 실패: %w", err)
}
if result.ReturnCode != 0 {
return nil, fmt.Errorf("예수금 조회 오류: %s", result.ReturnMsg)
}
return &DepositResult{
Entr: result.Entr,
D2Entra: result.D2Entra,
OrdAlowAmt: result.OrdAlowAmt,
}, nil
}
// --- 주문가능금액 ---
// OrderableResult 주문가능금액/수량
type OrderableResult struct {
OrdAlowa string `json:"ordAlowa"` // 주문가능현금
Entr string `json:"entr"` // 예수금
OrdAlowq string `json:"ordAlowq"` // 주문가능수량 (증거금100%)
}
// GetOrderable 주문가능금액/수량 조회 (kt00010)
// 모의투자 환경에서 RC7006 오류 시 kt00001(예수금상세) 폴백
// side: "1"=매도, "2"=매수
func (s *AccountService) GetOrderable(code, price, side string) (*OrderableResult, error) {
body := map[string]string{
"stk_cd": code,
"trde_tp": side,
"uv": price,
}
respBody, err := s.client.post("kt00010", "/api/dostk/acnt", body)
if err != nil {
// 네트워크 오류 시 예수금상세(kt00001)로 폴백
return s.orderableFromDeposit()
}
var result struct {
OrdAlowa string `json:"ord_alowa"`
Entr string `json:"entr"`
Profa100OrdAlowq string `json:"profa_100ord_alowq"`
ReturnCode int `json:"return_code"`
ReturnMsg string `json:"return_msg"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("주문가능금액 응답 파싱 실패: %w", err)
}
if result.ReturnCode != 0 {
// 모의투자 환경(RC7006) 등 kt00010 미지원 시 kt00001 폴백
return s.orderableFromDeposit()
}
return &OrderableResult{
OrdAlowa: result.OrdAlowa,
Entr: result.Entr,
OrdAlowq: result.Profa100OrdAlowq,
}, nil
}
// orderableFromDeposit kt00001(예수금상세)로 주문가능금액 조회 (kt00010 폴백)
func (s *AccountService) orderableFromDeposit() (*OrderableResult, error) {
dep, err := s.GetDepositDetail()
if err != nil {
return nil, fmt.Errorf("주문가능금액 조회 실패(폴백): %w", err)
}
return &OrderableResult{
OrdAlowa: dep.OrdAlowAmt,
Entr: dep.Entr,
OrdAlowq: "0", // kt00001은 수량 미제공
}, nil
}