refactor(phase1): Create trading module core files
This commit is contained in:
194
backend/app/trading/order_manager.py
Normal file
194
backend/app/trading/order_manager.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user