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
« 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
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
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
22class KoreaInvestApiClient:
23 """
24 한국투자증권 Open API와 상호작용하는 메인 클라이언트입니다.
25 각 도메인별 API 클래스를 통해 접근합니다.
26 """
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()로 주입받음
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)
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)
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 )
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
173 # async def search_stocks_by_keyword(self, keyword: str) -> ResCommonResponse:
174 # """
175 # 키워드로 종목을 검색합니다.
176 # """
177 # return await self._quotations.search_stocks_by_keyword(keyword)
179 async def get_top_rise_fall_stocks(self, rise: bool = True) -> ResCommonResponse:
180 """
181 상승률 또는 하락률 상위 종목을 조회합니다.
183 Args:
184 rise (bool): True이면 상승률, False이면 하락률 상위를 조회합니다.
185 """
186 return await self._quotations.get_top_rise_fall_stocks(rise)
188 async def get_top_volume_stocks(self) -> ResCommonResponse:
189 """
190 거래량 상위 종목을 조회합니다.
191 """
192 return await self._quotations.get_top_volume_stocks()
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)
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)
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)
206 # async def get_stock_news(self, stock_code: str) -> ResCommonResponse:
207 # """
208 # 특정 종목의 뉴스를 조회합니다.
209 # """
210 # return await self._quotations.get_stock_news(stock_code)
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)
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)
222 async def get_financial_ratio(self, stock_code: str) -> ResCommonResponse:
223 """기업 재무비율을 조회합니다 (영업이익 증가율 등)."""
224 return await self._quotations.get_financial_ratio(stock_code)
226 async def check_holiday(self, date: str) -> ResCommonResponse:
227 """국내 휴장일 조회"""
228 return await self._quotations.check_holiday(date)
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)
237 async def disconnect_websocket(self) -> Any:
238 """웹소켓 연결을 종료합니다."""
239 return await self._websocketAPI.disconnect()
241 async def subscribe_realtime_price(self, stock_code) -> Any:
242 """실시간 주식체결 데이터(현재가)를 구독합니다."""
243 return await self._websocketAPI.subscribe_realtime_price(stock_code)
245 async def unsubscribe_realtime_price(self, stock_code) -> Any:
246 """실시간 주식체결 데이터(현재가) 구독을 해지합니다."""
247 return await self._websocketAPI.unsubscribe_realtime_price(stock_code)
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)
253 async def unsubscribe_unified_price(self, stock_code: str) -> bool:
254 """실시간 통합 체결가(H0UNCNT0) 구독을 해지합니다."""
255 return await self._websocketAPI.unsubscribe_unified_price(stock_code)
257 async def subscribe_realtime_quote(self, stock_code) -> Any:
258 """실시간 주식호가 데이터를 구독합니다."""
259 return await self._websocketAPI.subscribe_realtime_quote(stock_code)
261 async def unsubscribe_realtime_quote(self, stock_code) -> Any:
262 """실시간 주식호가 데이터 구독을 해지합니다."""
263 return await self._websocketAPI.unsubscribe_realtime_quote(stock_code)
266 def is_websocket_receive_alive(self) -> bool:
267 """웹소켓 수신 태스크가 살아있는지 확인."""
268 return self._websocketAPI.is_receive_alive()
270 async def subscribe_program_trading(self, stock_code: str):
271 return await self._websocketAPI.subscribe_program_trading(stock_code)
273 async def unsubscribe_program_trading(self, stock_code: str):
274 return await self._websocketAPI.unsubscribe_program_trading(stock_code)