first commit
This commit is contained in:
377
services/account_service.go
Normal file
377
services/account_service.go
Normal file
@@ -0,0 +1,377 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user