from __future__ import annotations import httpx from app.core.auth import token_manager from app.core.config import get_base_url, settings from app.core.logger import setup_logger from app.core.rate_limiter import RateLimiter logger = setup_logger("account") _BALANCE_URL = "/uapi/domestic-stock/v1/trading/inquire-balance" _ORDERABLE_URL = "/uapi/domestic-stock/v1/trading/inquire-psamount" _BALANCE_TR_IDS = {"real": "TTTC0311R", "vps": "VTTC0311R"} class AccountService: def __init__(self) -> None: self._client = httpx.AsyncClient(timeout=10.0) self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second) async def get_balance(self) -> dict: await self._rate_limiter.acquire() token = await token_manager.get_access_token() tr_id = _BALANCE_TR_IDS.get(settings.kis.server_mode, "VTTC0311R") headers = token_manager.get_auth_headers(tr_id) params = { "CANO": settings.kis.account_no, "ACNT_PRDT_CD": settings.kis.account_code, "AFHR_FLPR_YN": "N", "OFLN_YN": "N", "INQR_DVSN": "02", "UNPR_DVSN": "01", "FUND_STTL_ICLD_YN": "N", "CNCD_UNCLD_YN": "N", } url = f"{get_base_url()}{_BALANCE_URL}" resp = await self._client.get(url, headers=headers, params=params) resp.raise_for_status() data = resp.json() if data.get("rt_cd") != "0": logger.warning("잔고 조회 실패: %s", data.get("msg1")) return {"deposits": [], "stocks": []} stocks = [] for item in data.get("output1", []): stocks.append({ "stock_code": item.get("pdno", ""), "stock_name": item.get("hsts_km_name", ""), "qty": int(item.get("hldg_qty", 0)), "avg_price": float(item.get("pchs_avg_pric", 0)), "current_price": int(item.get("prpr", 0)), "profit": float(item.get("evlu_pfls_amt", 0)), "profit_rate": float(item.get("pfls_rt", 0)), "buy_amount": float(item.get("pchs_amt", 0)), "eval_amount": float(item.get("evlu_amt", 0)), }) deposits = [] for item in data.get("output2", []): deposits.append({ "currency": item.get("crcy_cd", "KRW"), "amount": float(item.get("nmbdy_now_amt", 0)), "orderable": float(item.get("ord_psbl_amt", 0)), }) return {"deposits": deposits, "stocks": stocks} async def get_orderable_amount(self, stock_code: str, price: int) -> dict: await self._rate_limiter.acquire() token = await token_manager.get_access_token() headers = token_manager.get_auth_headers("VTTC0830R" if settings.kis.server_mode == "vps" else "TTTC0830R") params = { "CANO": settings.kis.account_no, "ACNT_PRDT_CD": settings.kis.account_code, "PDNO": stock_code, "ORD_UNPR": str(price), "ORD_DVSN": "00", } url = f"{get_base_url()}{_ORDERABLE_URL}" resp = await self._client.get(url, headers=headers, params=params) resp.raise_for_status() data = resp.json() if data.get("rt_cd") != "0": return {"orderable_amount": 0, "orderable_qty": 0} output = data.get("output", {}) return { "orderable_amount": float(output.get("psmps_numb", 0)), "orderable_qty": int(output.get("ord_psbl_qty", 0)), "max_buy_amount": float(output.get("max_buy_psbl_amt", 0)), } async def close(self) -> None: await self._client.aclose() account_service = AccountService()