""" Order Manager 주문 실행·취소·조회 전담 모듈 """ from typing import Dict, Any, Optional import logging from app.trading.kis_client import kis_client logger = logging.getLogger(__name__) class OrderManager: """주문 실행 관리자""" def __init__(self, client=None): self.client = client or kis_client async def place_order( self, stock_code: str, order_type: str, # "BUY" or "SELL" quantity: int, price: Optional[float] = None # None = 시장가 ) -> Dict[str, Any]: """주문 실행""" try: account_number_prefix = self.client.account_number[:8] account_number_suffix = self.client.account_number[8:] is_virtual = "vts" in self.client.base_url.lower() if order_type == "BUY": tr_id = "VTTC0802U" if is_virtual else "TTTC0802U" else: tr_id = "VTTC0801U" if is_virtual else "TTTC0801U" order_dvsn = "01" if price is None else "00" order_price = "0" if price is None else str(int(price)) logger.info(f"📤 주문: {order_type} {stock_code} {quantity}주 @{order_price}") result, _ = await self.client.request( "POST", "/uapi/domestic-stock/v1/trading/order-cash", tr_id, json={ "CANO": account_number_prefix, "ACNT_PRDT_CD": account_number_suffix, "PDNO": stock_code, "ORD_DVSN": order_dvsn, "ORD_QTY": str(quantity), "ORD_UNPR": order_price } ) logger.info(f"📥 주문 응답: rt_cd={result.get('rt_cd')}, odno={result.get('odno')}") return result except Exception as e: logger.error(f"❌ 주문 실패: {e}") return {"rt_cd": "-1", "msg1": str(e), "odno": None} async def cancel_order(self, order_id: str, quantity: int) -> Dict[str, Any]: """주문 취소""" try: account_number_prefix = self.client.account_number[:8] account_number_suffix = self.client.account_number[8:] is_virtual = "vts" in self.client.base_url.lower() tr_id = "VTTC0803U" if is_virtual else "TTTC0803U" result, _ = await self.client.request( "POST", "/uapi/domestic-stock/v1/trading/order-rvsecncl", tr_id, json={ "CANO": account_number_prefix, "ACNT_PRDT_CD": account_number_suffix, "KRX_FWDG_ORD_ORGNO": "", "ORGN_ODNO": order_id, "ORD_DVSN": "00", "RVSE_CNCL_DVSN_CD": "02", "ORD_QTY": "0", "ORD_UNPR": "0", "QTY_ALL_ORD_YN": "Y" } ) return result except Exception as e: logger.error(f"❌ 취소 실패: {e}") raise async def get_balance(self) -> Dict[str, Any]: """계좌 잔고 조회""" try: account_number_prefix = self.client.account_number[:8] account_number_suffix = self.client.account_number[8:] is_virtual = "vts" in self.client.base_url.lower() tr_id = "VTTC8434R" if is_virtual else "TTTC8434R" result, _ = await self.client.request( "GET", "/uapi/domestic-stock/v1/trading/inquire-balance", tr_id, params={ "CANO": account_number_prefix, "ACNT_PRDT_CD": account_number_suffix, "AFHR_FLPR_YN": "N", "OFL_YN": "", "INQR_DVSN": "02", "UNPR_DVSN": "01", "FUND_STTL_ICLD_YN": "N", "FNCG_AMT_AUTO_RDPT_YN": "N", "PRCS_DVSN": "01", "CTX_AREA_FK100": "", "CTX_AREA_NK100": "" } ) return result except Exception as e: logger.error(f"❌ 잔고 조회 실패: {e}") raise async def place_overseas_order( self, symbol: str, order_type: str, quantity: int, price: float, exchange_code: str = "NASD" ) -> Dict[str, Any]: """해외주식 주문""" try: account_number_prefix = self.client.account_number[:8] account_number_suffix = self.client.account_number[8:] is_virtual = "vts" in self.client.base_url.lower() if order_type == "BUY": tr_id = "VTTT1002U" if is_virtual else "TTTT1002U" else: tr_id = "VTTT1006U" if is_virtual else "TTTT1006U" result, _ = await self.client.request( "POST", "/uapi/overseas-stock/v1/trading/order", tr_id, json={ "CANO": account_number_prefix, "ACNT_PRDT_CD": account_number_suffix, "OVRS_EXCG_CD": exchange_code.upper(), "PDNO": symbol.upper(), "ORD_QTY": str(quantity), "OVRS_ORD_UNPR": str(price), "ORD_SVR_DVSN_CD": "0", "ORD_DVSN": "00" } ) return result except Exception as e: logger.error(f"❌ 해외주식 주문 실패: {e}") raise async def get_overseas_balance(self) -> Dict[str, Any]: """해외주식 잔고 조회""" try: account_number_prefix = self.client.account_number[:8] account_number_suffix = self.client.account_number[8:] is_virtual = "vts" in self.client.base_url.lower() tr_id = "VTTS3012R" if is_virtual else "TTTS3012R" result, _ = await self.client.request( "GET", "/uapi/overseas-stock/v1/trading/inquire-balance", tr_id, params={ "CANO": account_number_prefix, "ACNT_PRDT_CD": account_number_suffix, "OVRS_EXCG_CD": "NASD", "TR_CRCY_CD": "USD", "CTX_AREA_FK200": "", "CTX_AREA_NK200": "", } ) return result except Exception as e: logger.error(f"❌ 해외주식 잔고 조회 실패: {e}") return {} # 싱글톤 order_manager = OrderManager()