Coverage for brokers / korea_investment / korea_invest_client.py: 95%

100 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-04 15:08 +0000

1# brokers/korea_investment/korea_invest_client.py 

2from typing import Optional 

3 

4from brokers.korea_investment.korea_invest_env import KoreaInvestApiEnv 

5from brokers.korea_investment.korea_invest_quotations_api import KoreaInvestApiQuotations 

6from brokers.korea_investment.korea_invest_account_api import KoreaInvestApiAccount 

7from brokers.korea_investment.korea_invest_trading_api import KoreaInvestApiTrading 

8from brokers.korea_investment.korea_invest_websocket_api import KoreaInvestWebSocketAPI 

9from brokers.korea_investment.korea_invest_header_provider import build_header_provider_from_env 

10from brokers.korea_investment.korea_invest_url_provider import KoreaInvestUrlProvider 

11from brokers.korea_investment.korea_invest_trid_provider import KoreaInvestTrIdProvider 

12 

13import certifi 

14import logging 

15import httpx # 비동기 처리를 위해 requests 대신 httpx 사용 

16import ssl 

17from typing import Any 

18from common.types import ResCommonResponse, Exchange 

19from services.market_calendar_service import MarketCalendarService 

20 

21 

22class KoreaInvestApiClient: 

23 """ 

24 한국투자증권 Open API와 상호작용하는 메인 클라이언트입니다. 

25 각 도메인별 API 클래스를 통해 접근합니다. 

26 """ 

27 

28 def __init__(self, env: KoreaInvestApiEnv, logger=None, market_clock=None, 

29 market_calendar_service: Optional[MarketCalendarService] = None): 

30 self._env = env 

31 self._logger = logger if logger else logging.getLogger(__name__) 

32 self.market_clock = market_clock 

33 self._mcs = market_calendar_service # MarketCalendar는 나중에 set_market_calendar_service()로 주입받음 

34 

35 ssl_context = ssl.create_default_context(cafile=certifi.where()) 

36 limits = httpx.Limits(max_keepalive_connections=50, max_connections=100, keepalive_expiry=30.0) 

37 shared_client = httpx.AsyncClient(verify=ssl_context, limits=limits) 

38 

39 header_provider = build_header_provider_from_env(env) # UA만 갖고 생성 

40 url_provider = KoreaInvestUrlProvider.from_env_and_kis_config(env=env) 

41 trid_provider = KoreaInvestTrIdProvider.from_config_loader(env=env) 

42 

43 # 조회 API 전용: 항상 실전 URL 사용 

44 quotation_url_provider = KoreaInvestUrlProvider.from_env_and_kis_config( 

45 env=env, get_base_url_override=env.get_real_base_url 

46 ) 

47 

48 self._quotations = KoreaInvestApiQuotations( 

49 self._env, 

50 self._logger, 

51 self.market_clock, 

52 async_client=shared_client, 

53 header_provider=header_provider.fork(), 

54 url_provider=quotation_url_provider, 

55 trid_provider=trid_provider, 

56 ) 

57 self._quotations._use_real_auth = True # 항상 실전 인증 

58 self._account = KoreaInvestApiAccount( 

59 self._env, 

60 self._logger, 

61 self.market_clock, 

62 async_client=shared_client, 

63 header_provider=header_provider.fork(), 

64 url_provider=url_provider, 

65 trid_provider=trid_provider, 

66 ) 

67 self._trading = KoreaInvestApiTrading( 

68 self._env, 

69 self._logger, 

70 self.market_clock, 

71 async_client=shared_client, 

72 header_provider=header_provider.fork(), 

73 url_provider=url_provider, 

74 trid_provider=trid_provider, 

75 ) 

76 self._websocketAPI = KoreaInvestWebSocketAPI(self._env, self._logger, market_clock=self.market_clock, market_calendar_service=self._mcs) 

77 

78 # --- Account API delegation --- 

79 async def get_account_balance(self, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

80 return await self._account.get_account_balance(exchange=exchange) 

81 

82 # --- Trading API delegation --- 

83 async def place_stock_order(self, stock_code, order_price, order_qty, is_buy: bool, 

84 exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

85 return await self._trading.place_stock_order(stock_code, order_price, order_qty, is_buy, exchange=exchange) 

86 

87 # --- Quotations API delegation (Updated) --- 

88 # KoreaInvestApiQuotations의 모든 메서드가 ResCommonResponse를 반환하도록 이미 수정되었으므로, 해당 반환 타입을 반영 

89 async def get_stock_info_by_code(self, stock_code: str, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

90 """종목코드로 종목의 전체 정보를 가져옵니다. ResCommonResponse를 반환합니다.""" 

91 return await self._quotations.get_stock_info_by_code(stock_code, exchange=exchange) 

92 

93 async def get_current_price(self, code: str, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

94 """현재가를 조회합니다. ResCommonResponse를 반환합니다.""" 

95 return await self._quotations.get_current_price(code, exchange=exchange) 

96 

97 async def get_stock_conclusion(self, code: str, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

98 """주식 체결(체결강도) 정보를 조회합니다.""" 

99 return await self._quotations.get_stock_conclusion(code, exchange=exchange) 

100 

101 async def get_price_summary(self, code: str, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

102 """주어진 종목코드에 대해 시가/현재가/등락률(%) 요약 정보를 반환합니다. ResCommonResponse를 반환합니다.""" 

103 return await self._quotations.get_price_summary(code, exchange=exchange) 

104 

105 async def get_market_cap(self, code: str, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

106 """종목코드로 시가총액을 반환합니다. ResCommonResponse를 반환합니다.""" 

107 return await self._quotations.get_market_cap(code, exchange=exchange) 

108 

109 async def get_top_market_cap_stocks_code(self, market_code: str, count: int = 30) -> ResCommonResponse: 

110 """시가총액 상위 종목 목록을 반환합니다. ResCommonResponse를 반환합니다.""" 

111 return await self._quotations.get_top_market_cap_stocks_code(market_code, count) 

112 

113 async def inquire_daily_itemchartprice(self, stock_code: str, start_date: str, end_date: str, 

114 fid_period_div_code: str = 'D', 

115 exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

116 """일별/분별 주식 시세 차트 데이터를 조회합니다. ResCommonResponse를 반환합니다.""" 

117 return await self._quotations.inquire_daily_itemchartprice(stock_code, start_date=start_date, end_date=end_date, 

118 fid_period_div_code=fid_period_div_code, 

119 exchange=exchange) 

120 

121 async def inquire_time_itemchartprice( 

122 self, 

123 *, 

124 stock_code: str, 

125 input_hour_1: str, 

126 pw_data_incu_yn: str = "Y", 

127 etc_cls_code: str = "0", 

128 ) -> ResCommonResponse: 

129 """ 

130 당일 분봉 조회 

131 URL : /uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice 

132 TRID: FHKST03010200 (모의/실전 공통) 

133 """ 

134 return await self._quotations.inquire_time_itemchartprice( 

135 stock_code=stock_code, 

136 input_hour=input_hour_1, 

137 include_past=pw_data_incu_yn, 

138 etc_cls_code=etc_cls_code) 

139 

140 async def inquire_time_dailychartprice( 

141 self, 

142 *, 

143 stock_code: str, 

144 input_date_1: str, # "YYYYMMDD" 

145 input_hour_1: str = "", # 옵션(길이 10 권장) 

146 pw_data_incu_yn: str = "Y", 

147 fake_tick_incu_yn: str = "", # 허봉 포함 여부: 공백 필수 

148 ) -> ResCommonResponse: 

149 """ 

150 일별(특정 일자) 분봉 조회 

151 URL : /uapi/domestic-stock/v1/quotations/inquire-time-dailychartprice 

152 TRID: FHKST03010230 (모의투자 미지원) 

153 """ 

154 return await self._quotations.inquire_time_dailychartprice( 

155 stock_code=stock_code, 

156 input_hour=input_hour_1, 

157 input_date=input_date_1, 

158 include_past=pw_data_incu_yn, 

159 fid_pw_data_incu_yn=fake_tick_incu_yn) 

160 

161 async def get_asking_price(self, stock_code: str, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

162 """ 

163 종목의 실시간 호가(매도/매수 잔량 포함) 정보를 조회합니다. 

164 """ 

165 return await self._quotations.get_asking_price(stock_code, exchange=exchange) 

166 

167 async def get_time_concluded_prices(self, stock_code: str, exchange: Exchange = Exchange.KRX) -> ResCommonResponse: 

168 """ 

169 종목의 시간대별 체결가/체결량 정보를 조회합니다. 

170 """ 

171 return await self._quotations.get_time_concluded_prices(stock_code, exchange=exchange) 

172 

173 # async def search_stocks_by_keyword(self, keyword: str) -> ResCommonResponse: 

174 # """ 

175 # 키워드로 종목을 검색합니다. 

176 # """ 

177 # return await self._quotations.search_stocks_by_keyword(keyword) 

178 

179 async def get_top_rise_fall_stocks(self, rise: bool = True) -> ResCommonResponse: 

180 """ 

181 상승률 또는 하락률 상위 종목을 조회합니다. 

182 

183 Args: 

184 rise (bool): True이면 상승률, False이면 하락률 상위를 조회합니다. 

185 """ 

186 return await self._quotations.get_top_rise_fall_stocks(rise) 

187 

188 async def get_top_volume_stocks(self) -> ResCommonResponse: 

189 """ 

190 거래량 상위 종목을 조회합니다. 

191 """ 

192 return await self._quotations.get_top_volume_stocks() 

193 

194 async def get_investor_trade_by_stock_daily(self, stock_code: str, date: str = None) -> ResCommonResponse: 

195 """종목별 투자자 매매동향(일별) 조회 (실전 전용)""" 

196 return await self._quotations.get_investor_trade_by_stock_daily(stock_code, date) 

197 

198 async def get_investor_trade_by_stock_daily_multi(self, stock_code: str, date: str = None, days: int = 3) -> ResCommonResponse: 

199 """종목별 투자자 매매동향(일별) 다중일 조회 (실전 전용) — output2[:days] 리스트 반환""" 

200 return await self._quotations.get_investor_trade_by_stock_daily_multi(stock_code, date, days) 

201 

202 async def get_program_trade_by_stock_daily(self, stock_code: str, date: str = None) -> ResCommonResponse: 

203 """종목별 프로그램 매매동향(일별) 조회 (실전 전용)""" 

204 return await self._quotations.get_program_trade_by_stock_daily(stock_code, date) 

205 

206 # async def get_stock_news(self, stock_code: str) -> ResCommonResponse: 

207 # """ 

208 # 특정 종목의 뉴스를 조회합니다. 

209 # """ 

210 # return await self._quotations.get_stock_news(stock_code) 

211 

212 async def get_multi_price(self, stock_codes: list[str]) -> ResCommonResponse: 

213 """복수종목 현재가를 조회합니다 (최대 30종목). ResCommonResponse를 반환합니다.""" 

214 return await self._quotations.get_multi_price(stock_codes) 

215 

216 async def get_etf_info(self, etf_code: str) -> ResCommonResponse: 

217 """ 

218 특정 ETF의 상세 정보를 조회합니다. 

219 """ 

220 return await self._quotations.get_etf_info(etf_code) 

221 

222 async def get_financial_ratio(self, stock_code: str) -> ResCommonResponse: 

223 """기업 재무비율을 조회합니다 (영업이익 증가율 등).""" 

224 return await self._quotations.get_financial_ratio(stock_code) 

225 

226 async def check_holiday(self, date: str) -> ResCommonResponse: 

227 """국내 휴장일 조회""" 

228 return await self._quotations.check_holiday(date) 

229 

230 # --- WebSocket API delegation --- 

231 # 웹소켓 API는 연결/구독 성공 여부만 반환할 수 있으므로, ResCommonResponse로 래핑 여부는 구현에 따라 달라집니다. 

232 # 여기서는 임시로 Any로 두지만, ResCommonResponse(rt_cd, msg1, data=True/False) 형태로 변경하는 것을 고려할 수 있습니다. 

233 async def connect_websocket(self, on_message_callback=None) -> Any: 

234 """웹소켓 연결을 시작하고 실시간 데이터 수신을 준비합니다.""" 

235 return await self._websocketAPI.connect(on_message_callback) 

236 

237 async def disconnect_websocket(self) -> Any: 

238 """웹소켓 연결을 종료합니다.""" 

239 return await self._websocketAPI.disconnect() 

240 

241 async def subscribe_realtime_price(self, stock_code) -> Any: 

242 """실시간 주식체결 데이터(현재가)를 구독합니다.""" 

243 return await self._websocketAPI.subscribe_realtime_price(stock_code) 

244 

245 async def unsubscribe_realtime_price(self, stock_code) -> Any: 

246 """실시간 주식체결 데이터(현재가) 구독을 해지합니다.""" 

247 return await self._websocketAPI.unsubscribe_realtime_price(stock_code) 

248 

249 async def subscribe_unified_price(self, stock_code: str) -> bool: 

250 """실시간 통합 체결가(H0UNCNT0) 구독합니다 (KRX+NXT 통합).""" 

251 return await self._websocketAPI.subscribe_unified_price(stock_code) 

252 

253 async def unsubscribe_unified_price(self, stock_code: str) -> bool: 

254 """실시간 통합 체결가(H0UNCNT0) 구독을 해지합니다.""" 

255 return await self._websocketAPI.unsubscribe_unified_price(stock_code) 

256 

257 async def subscribe_realtime_quote(self, stock_code) -> Any: 

258 """실시간 주식호가 데이터를 구독합니다.""" 

259 return await self._websocketAPI.subscribe_realtime_quote(stock_code) 

260 

261 async def unsubscribe_realtime_quote(self, stock_code) -> Any: 

262 """실시간 주식호가 데이터 구독을 해지합니다.""" 

263 return await self._websocketAPI.unsubscribe_realtime_quote(stock_code) 

264 

265 

266 def is_websocket_receive_alive(self) -> bool: 

267 """웹소켓 수신 태스크가 살아있는지 확인.""" 

268 return self._websocketAPI.is_receive_alive() 

269 

270 async def subscribe_program_trading(self, stock_code: str): 

271 return await self._websocketAPI.subscribe_program_trading(stock_code) 

272 

273 async def unsubscribe_program_trading(self, stock_code: str): 

274 return await self._websocketAPI.unsubscribe_program_trading(stock_code)