195 lines
6.8 KiB
Python
195 lines
6.8 KiB
Python
"""
|
|
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()
|