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 }