ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -220,6 +220,33 @@ class KISClient(SafeRequest):
|
||||
j = r.json()
|
||||
return j.get("output") if j.get("rt_cd") == "0" else None
|
||||
|
||||
def inquire_stock_name(self, code: str) -> Optional[str]:
|
||||
"""국내 종목명 [주식기본조회 CTPF1002R]. 약명(prdt_abrv_name) 우선."""
|
||||
code = str(code or "").strip()
|
||||
if not code:
|
||||
return None
|
||||
try:
|
||||
r = self._get(
|
||||
"/uapi/domestic-stock/v1/quotations/search-stock-info",
|
||||
"CTPF1002R",
|
||||
{"PRDT_TYPE_CD": "300", "PDNO": code},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
j = r.json()
|
||||
if j.get("rt_cd") != "0":
|
||||
return None
|
||||
out = j.get("output") or {}
|
||||
if isinstance(out, list):
|
||||
out = out[0] if out else {}
|
||||
name = (
|
||||
str(out.get("prdt_abrv_name") or out.get("prdt_name") or "").strip()
|
||||
)
|
||||
return name or None
|
||||
except Exception as e:
|
||||
logger.debug("종목명 조회 실패(%s): %s", code, e)
|
||||
return None
|
||||
|
||||
def inquire_multi_price(self, codes: List[str]) -> Dict[str, dict]:
|
||||
"""
|
||||
관심종목(멀티종목) 시세조회 [국내주식-205] — FHKST11300006.
|
||||
@@ -765,10 +792,32 @@ class KISClient(SafeRequest):
|
||||
"ORD_QTY": str(qty), "ORD_UNPR": str(price),
|
||||
}
|
||||
try:
|
||||
from kis_trader.utils.api_reject_log import record_api_reject
|
||||
|
||||
r = self._post(path, tr_id, body)
|
||||
if r.status_code != 200:
|
||||
logger.error("주문 HTTP 에러 side=%s code=%s status=%s",
|
||||
side, code, r.status_code)
|
||||
body_snip = (r.text or "")[:200]
|
||||
http_cd = f"HTTP_{int(r.status_code)}"
|
||||
if side == "BUY":
|
||||
self._last_order_msg_cd = http_cd
|
||||
self._last_order_msg1 = body_snip
|
||||
else:
|
||||
self._last_sell_msg_cd = http_cd
|
||||
self._last_sell_msg1 = body_snip
|
||||
logger.error(
|
||||
"주문 HTTP 에러 side=%s code=%s status=%s",
|
||||
side, code, r.status_code,
|
||||
)
|
||||
record_api_reject(
|
||||
kind="domestic_order_http",
|
||||
side=side,
|
||||
code=str(code or ""),
|
||||
msg_cd=http_cd,
|
||||
msg1=body_snip,
|
||||
http=r.status_code,
|
||||
path=str(path),
|
||||
extra={"mock": bool(self.mock)},
|
||||
)
|
||||
return None
|
||||
j = r.json()
|
||||
if j.get("rt_cd") == "0":
|
||||
@@ -797,9 +846,45 @@ class KISClient(SafeRequest):
|
||||
code, j.get("rt_cd"),
|
||||
self._last_sell_msg_cd, self._last_sell_msg1,
|
||||
)
|
||||
record_api_reject(
|
||||
kind="domestic_order_reject",
|
||||
side=side,
|
||||
code=str(code or ""),
|
||||
msg_cd=str(
|
||||
(self._last_order_msg_cd if side == "BUY" else self._last_sell_msg_cd)
|
||||
or ""
|
||||
),
|
||||
msg1=str(
|
||||
(self._last_order_msg1 if side == "BUY" else self._last_sell_msg1)
|
||||
or ""
|
||||
),
|
||||
rt_cd=str(j.get("rt_cd") or ""),
|
||||
http=200,
|
||||
path=str(path),
|
||||
extra={"mock": bool(self.mock)},
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
if side == "BUY":
|
||||
self._last_order_msg_cd = "EXC"
|
||||
self._last_order_msg1 = str(e)[:200]
|
||||
else:
|
||||
self._last_sell_msg_cd = "EXC"
|
||||
self._last_sell_msg1 = str(e)[:200]
|
||||
logger.error("주문 예외 side=%s code=%s: %s", side, code, e)
|
||||
try:
|
||||
from kis_trader.utils.api_reject_log import record_api_reject
|
||||
record_api_reject(
|
||||
kind="domestic_order_exception",
|
||||
side=side,
|
||||
code=str(code or ""),
|
||||
msg_cd="EXC",
|
||||
msg1=str(e)[:300],
|
||||
path=str(path),
|
||||
extra={"mock": bool(self.mock)},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def buy_market_order(self, code: str, qty: int) -> Optional[str]:
|
||||
@@ -872,6 +957,323 @@ class KISClient(SafeRequest):
|
||||
code=code, qty=qty, price=0, order_type="01", side="SELL"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 해외주식 시세·주문 (미국 NASD/NYSE/AMEX)
|
||||
# ※ 매수/매도는 body 가 아니라 헤더 tr_id 로만 구분 (국내 SLL_BUY 와 다름)
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def _ovrs_excg_cd(exchange: Optional[str] = None) -> str:
|
||||
"""주문용 거래소 코드 (NASD/NYSE/AMEX). 시세 EXCD(NAS) 와 다름."""
|
||||
raw = (
|
||||
exchange
|
||||
or get_env_from_db("KIS_OVRS_DEFAULT_EXCG", "NASD")
|
||||
or "NASD"
|
||||
).strip().upper()
|
||||
aliases = {
|
||||
"NAS": "NASD", "NASDAQ": "NASD", "NASD": "NASD",
|
||||
"NYS": "NYSE", "NYSE": "NYSE",
|
||||
"AMS": "AMEX", "AMEX": "AMEX",
|
||||
}
|
||||
return aliases.get(raw, raw or "NASD")
|
||||
|
||||
@staticmethod
|
||||
def _ovrs_price_excd(exchange: Optional[str] = None) -> str:
|
||||
"""시세용 EXCD (NAS/NYS/AMS)."""
|
||||
order_cd = KISClient._ovrs_excg_cd(exchange)
|
||||
return {"NASD": "NAS", "NYSE": "NYS", "AMEX": "AMS"}.get(order_cd, "NAS")
|
||||
|
||||
def inquire_overseas_price(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
exchange: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
"""해외주식 현재가 [해외주식-029] HHDFS00000300."""
|
||||
symb = str(symbol or "").strip().upper()
|
||||
if not symb:
|
||||
return None
|
||||
excd = self._ovrs_price_excd(exchange)
|
||||
try:
|
||||
r = self._get(
|
||||
"/uapi/overseas-price/v1/quotations/price",
|
||||
"HHDFS00000300",
|
||||
{"AUTH": "", "EXCD": excd, "SYMB": symb},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
logger.warning("해외시세 HTTP %s %s", r.status_code, symb)
|
||||
return None
|
||||
j = r.json()
|
||||
if j.get("rt_cd") != "0":
|
||||
logger.warning(
|
||||
"해외시세 실패 %s rt_cd=%s msg=%s",
|
||||
symb, j.get("rt_cd"), j.get("msg1"),
|
||||
)
|
||||
return None
|
||||
return j.get("output") or {}
|
||||
except Exception as e:
|
||||
logger.warning("해외시세 예외 %s: %s", symb, e)
|
||||
return None
|
||||
|
||||
def _overseas_order_tr_id(self, side: str, exchange: str) -> str:
|
||||
"""미국 매수 TTTT1002U / 매도 TTTT1006U. 모의는 앞 T→V."""
|
||||
side_u = (side or "").upper()
|
||||
excg = self._ovrs_excg_cd(exchange)
|
||||
if excg not in ("NASD", "NYSE", "AMEX"):
|
||||
raise ValueError(f"unsupported overseas exchange for US order: {excg}")
|
||||
if side_u == "BUY":
|
||||
real_tr = get_env_from_db("KIS_OVRS_US_BUY_TR_ID", "TTTT1002U") or "TTTT1002U"
|
||||
elif side_u == "SELL":
|
||||
real_tr = get_env_from_db("KIS_OVRS_US_SELL_TR_ID", "TTTT1006U") or "TTTT1006U"
|
||||
else:
|
||||
raise ValueError(f"side must be BUY/SELL, got {side}")
|
||||
real_tr = str(real_tr).strip().upper()
|
||||
if self.mock:
|
||||
# 공식 샘플: demo 시 'V' + tr_id[1:] (TTTT… → VTTT…)
|
||||
return "V" + real_tr[1:]
|
||||
return real_tr
|
||||
|
||||
def overseas_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
qty: int,
|
||||
price: float,
|
||||
side: str,
|
||||
exchange: Optional[str] = None,
|
||||
ord_dvsn: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
해외주식 주문 [v1_해외주식-001]. 성공 시 ODNO, 실패 시 None.
|
||||
|
||||
모의(VTTT*): ORD_DVSN=00 지정가만 가능. price>0 필수.
|
||||
"""
|
||||
symb = str(symbol or "").strip().upper()
|
||||
qty_i = int(qty or 0)
|
||||
if not symb or qty_i <= 0:
|
||||
return None
|
||||
excg = self._ovrs_excg_cd(exchange)
|
||||
side_u = (side or "").upper()
|
||||
if ord_dvsn is None or str(ord_dvsn).strip() == "":
|
||||
ord_dvsn = get_env_from_db("KIS_OVRS_ORD_DVSN", "00") or "00"
|
||||
ord_dvsn = str(ord_dvsn).strip()
|
||||
px = float(price or 0)
|
||||
if self.mock and (ord_dvsn != "00" or px <= 0):
|
||||
logger.error(
|
||||
"해외모의주문은 지정가(00)+가격>0 필수 side=%s %s",
|
||||
side_u, symb,
|
||||
)
|
||||
return None
|
||||
px_decimals = int(get_env_int("KIS_OVRS_PRICE_DECIMALS", 2))
|
||||
px_str = f"{px:.{px_decimals}f}"
|
||||
try:
|
||||
tr_id = self._overseas_order_tr_id(side_u, excg)
|
||||
except ValueError as e:
|
||||
logger.error("해외주문 tr_id: %s", e)
|
||||
return None
|
||||
path = get_env_from_db(
|
||||
"KIS_OVRS_ORDER_PATH",
|
||||
"/uapi/overseas-stock/v1/trading/order",
|
||||
) or "/uapi/overseas-stock/v1/trading/order"
|
||||
# 매도만 SLL_TYPE=00 (공식 샘플). 매수는 공란.
|
||||
sll_type = "00" if side_u == "SELL" else ""
|
||||
body = {
|
||||
"CANO": self.account_no,
|
||||
"ACNT_PRDT_CD": self.account_code,
|
||||
"OVRS_EXCG_CD": excg,
|
||||
"PDNO": symb,
|
||||
"ORD_QTY": str(qty_i),
|
||||
"OVRS_ORD_UNPR": px_str,
|
||||
"CTAC_TLNO": "",
|
||||
"MGCO_APTM_ODNO": "",
|
||||
"SLL_TYPE": sll_type,
|
||||
"ORD_SVR_DVSN_CD": get_env_from_db("KIS_OVRS_ORD_SVR_DVSN_CD", "0") or "0",
|
||||
"ORD_DVSN": ord_dvsn,
|
||||
}
|
||||
try:
|
||||
from kis_trader.utils.api_reject_log import record_api_reject
|
||||
|
||||
r = self._post(path, tr_id, body)
|
||||
if r.status_code != 200:
|
||||
body_snip = (r.text or "")[:200]
|
||||
http_cd = f"HTTP_{int(r.status_code)}"
|
||||
if side_u == "BUY":
|
||||
self._last_order_msg_cd = http_cd
|
||||
self._last_order_msg1 = body_snip
|
||||
else:
|
||||
self._last_sell_msg_cd = http_cd
|
||||
self._last_sell_msg1 = body_snip
|
||||
logger.error(
|
||||
"해외주문 HTTP side=%s %s status=%s body=%s",
|
||||
side_u, symb, r.status_code, body_snip,
|
||||
)
|
||||
record_api_reject(
|
||||
kind="overseas_order_http",
|
||||
side=side_u,
|
||||
code=symb,
|
||||
msg_cd=http_cd,
|
||||
msg1=body_snip,
|
||||
http=r.status_code,
|
||||
path=str(path),
|
||||
extra={"tr_id": tr_id, "mock": bool(self.mock)},
|
||||
)
|
||||
return None
|
||||
j = r.json()
|
||||
if j.get("rt_cd") == "0":
|
||||
if side_u == "BUY":
|
||||
self._last_order_msg_cd = None
|
||||
self._last_order_msg1 = None
|
||||
else:
|
||||
self._last_sell_msg_cd = None
|
||||
self._last_sell_msg1 = None
|
||||
out = j.get("output") or {}
|
||||
ord_no = str(out.get("ODNO") or out.get("odno") or "").strip()
|
||||
logger.info(
|
||||
"✅ 해외주문 접수 side=%s %s qty=%s @%s excg=%s tr=%s odno=%s",
|
||||
side_u, symb, qty_i, px_str, excg, tr_id, ord_no,
|
||||
)
|
||||
return ord_no or None
|
||||
msg_cd = j.get("msg_cd", "")
|
||||
msg1 = str(j.get("msg1", "") or "")
|
||||
if side_u == "BUY":
|
||||
self._last_order_msg_cd = msg_cd
|
||||
self._last_order_msg1 = msg1
|
||||
else:
|
||||
self._last_sell_msg_cd = msg_cd
|
||||
self._last_sell_msg1 = msg1
|
||||
logger.error(
|
||||
"[해외주문실패] side=%s %s rt_cd=%s msg_cd=%s msg1=%s",
|
||||
side_u, symb, j.get("rt_cd"), msg_cd, msg1,
|
||||
)
|
||||
record_api_reject(
|
||||
kind="overseas_order_reject",
|
||||
side=side_u,
|
||||
code=symb,
|
||||
msg_cd=str(msg_cd or ""),
|
||||
msg1=msg1,
|
||||
rt_cd=str(j.get("rt_cd") or ""),
|
||||
http=200,
|
||||
path=str(path),
|
||||
extra={"tr_id": tr_id, "mock": bool(self.mock)},
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
if side_u == "BUY":
|
||||
self._last_order_msg_cd = "EXC"
|
||||
self._last_order_msg1 = str(e)[:200]
|
||||
else:
|
||||
self._last_sell_msg_cd = "EXC"
|
||||
self._last_sell_msg1 = str(e)[:200]
|
||||
logger.error("해외주문 예외 side=%s %s: %s", side_u, symb, e)
|
||||
try:
|
||||
from kis_trader.utils.api_reject_log import record_api_reject
|
||||
record_api_reject(
|
||||
kind="overseas_order_exception",
|
||||
side=side_u,
|
||||
code=symb,
|
||||
msg_cd="EXC",
|
||||
msg1=str(e)[:300],
|
||||
path=str(path),
|
||||
extra={"mock": bool(self.mock)},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def inquire_overseas_psamount(
|
||||
self,
|
||||
symbol: str,
|
||||
price: float,
|
||||
*,
|
||||
exchange: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""
|
||||
해외주식 매수가능금액조회 [v1_해외주식-014] (TTTS3007R / VTTS3007R).
|
||||
|
||||
Returns:
|
||||
dict: ord_psbl_qty, max_ord_psbl_qty, ord_psbl_frcr_amt, exrt, ...
|
||||
실패 시 빈 dict.
|
||||
"""
|
||||
symb = str(symbol or "").strip().upper()
|
||||
px = float(price or 0)
|
||||
if not symb or px <= 0:
|
||||
return {}
|
||||
excg = self._ovrs_excg_cd(exchange)
|
||||
px_decimals = int(get_env_int("KIS_OVRS_PRICE_DECIMALS", 2))
|
||||
px_str = f"{px:.{px_decimals}f}"
|
||||
path = get_env_from_db(
|
||||
"KIS_OVRS_PSAMOUNT_PATH",
|
||||
"/uapi/overseas-stock/v1/trading/inquire-psamount",
|
||||
) or "/uapi/overseas-stock/v1/trading/inquire-psamount"
|
||||
tr_id = "VTTS3007R" if self.mock else "TTTS3007R"
|
||||
params = {
|
||||
"CANO": self.account_no,
|
||||
"ACNT_PRDT_CD": self.account_code,
|
||||
"OVRS_EXCG_CD": excg,
|
||||
"OVRS_ORD_UNPR": px_str,
|
||||
"ITEM_CD": symb,
|
||||
}
|
||||
try:
|
||||
r = self._get(path, tr_id, params)
|
||||
if r.status_code != 200:
|
||||
logger.warning(
|
||||
"해외매수가능 HTTP %s %s status=%s",
|
||||
symb, excg, r.status_code,
|
||||
)
|
||||
return {}
|
||||
j = r.json() if r is not None else {}
|
||||
if j.get("rt_cd") != "0":
|
||||
logger.warning(
|
||||
"해외매수가능 실패 %s rt_cd=%s msg_cd=%s msg1=%s",
|
||||
symb, j.get("rt_cd"), j.get("msg_cd"), j.get("msg1"),
|
||||
)
|
||||
return {}
|
||||
out = j.get("output") or j.get("output1") or {}
|
||||
if isinstance(out, list):
|
||||
out = out[0] if out else {}
|
||||
if not isinstance(out, dict):
|
||||
return {}
|
||||
# 수량 필드 정규화 (문자열→int)
|
||||
def _qi(key: str) -> int:
|
||||
try:
|
||||
return int(float(str(out.get(key) or "0").replace(",", "") or 0))
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
out = dict(out)
|
||||
out["ord_psbl_qty"] = _qi("ord_psbl_qty")
|
||||
out["max_ord_psbl_qty"] = _qi("max_ord_psbl_qty")
|
||||
out["ovrs_max_ord_psbl_qty"] = _qi("ovrs_max_ord_psbl_qty")
|
||||
return out
|
||||
except Exception as e:
|
||||
logger.warning("해외매수가능 예외 %s: %s", symb, e)
|
||||
return {}
|
||||
|
||||
def buy_overseas_limit(
|
||||
self,
|
||||
symbol: str,
|
||||
qty: int,
|
||||
price: float,
|
||||
*,
|
||||
exchange: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""해외 지정가 매수."""
|
||||
return self.overseas_order(
|
||||
symbol=symbol, qty=qty, price=price, side="BUY", exchange=exchange,
|
||||
)
|
||||
|
||||
def sell_overseas_limit(
|
||||
self,
|
||||
symbol: str,
|
||||
qty: int,
|
||||
price: float,
|
||||
*,
|
||||
exchange: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""해외 지정가 매도."""
|
||||
return self.overseas_order(
|
||||
symbol=symbol, qty=qty, price=price, side="SELL", exchange=exchange,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 조건검색 (REST — 웹소켓은 공식 지원 안 함. 30초 폴링이 정석)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -941,33 +1343,139 @@ class KISClient(SafeRequest):
|
||||
def get_order_history_today(self, odno: str = "") -> Optional[dict]:
|
||||
"""당일 주문 내역 조회 [국내주식-005 inquire-daily-ccld].
|
||||
|
||||
``odno`` 지정 시 해당 주문번호만 조회 → 응답 경량화로 장중 혼잡 시
|
||||
모의서버 HTTP 500 회피율을 높인다. (모의가 ODNO 필터를 무시하면
|
||||
기존처럼 전체를 받아 호출부에서 필터하므로 동작은 동일)
|
||||
TR_ID: 공식 샘플 기준 3개월이내 = 실전 ``TTTC0081R`` / 모의 ``VTTC0081R``.
|
||||
(레거시 ``*8001R`` 은 모의에서 '내역 없음'/빈맵이 나와 체결 재확인이 깨짐)
|
||||
|
||||
``odno`` 지정 시 해당 주문번호만 조회. 미지정 시 연속조회(모의 15건/페이지)로
|
||||
output1 을 병합해 반환한다.
|
||||
"""
|
||||
tr_id = "VTTC8001R" if self.mock else "TTTC8001R"
|
||||
# 공식 OpenAPI 샘플(inquire_daily_ccld) — env 로만 레거시 TR 오버라이드
|
||||
default_tr = "VTTC0081R" if self.mock else "TTTC0081R"
|
||||
tr_id = str(
|
||||
get_env_from_db("DAILY_CCLD_TR_ID", default_tr) or default_tr
|
||||
).strip() or default_tr
|
||||
today = dt.now().strftime("%Y%m%d")
|
||||
# 모의 1페이지≈15건 — 장중 체결 누락 방지. 과도 연속조회 방지 상한.
|
||||
max_pages = max(1, int(get_env_int("DAILY_CCLD_MAX_PAGES", 20)))
|
||||
try:
|
||||
r = self._get(
|
||||
"/uapi/domestic-stock/v1/trading/inquire-daily-ccld",
|
||||
tr_id,
|
||||
{
|
||||
"CANO": self.account_no, "ACNT_PRDT_CD": self.account_code,
|
||||
"INQR_STRT_DT": today, "INQR_END_DT": today,
|
||||
"SLL_BUY_DVSN_CD": "00", "INQR_DVSN": "00", "PDNO": "",
|
||||
"CCLD_DVSN": "00", "ORD_GNO_BRNO": "", "ODNO": str(odno or ""),
|
||||
"INQR_DVSN_3": "00", "INQR_DVSN_1": "",
|
||||
"CTX_AREA_FK100": "", "CTX_AREA_NK100": "",
|
||||
},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
merged_out1: list = []
|
||||
last_j: Optional[dict] = None
|
||||
fk100 = ""
|
||||
nk100 = ""
|
||||
tr_cont = ""
|
||||
for _page in range(max_pages):
|
||||
r = self._get(
|
||||
"/uapi/domestic-stock/v1/trading/inquire-daily-ccld",
|
||||
tr_id,
|
||||
{
|
||||
"CANO": self.account_no,
|
||||
"ACNT_PRDT_CD": self.account_code,
|
||||
"INQR_STRT_DT": today,
|
||||
"INQR_END_DT": today,
|
||||
"SLL_BUY_DVSN_CD": "00",
|
||||
"INQR_DVSN": "00",
|
||||
"PDNO": "",
|
||||
"CCLD_DVSN": "00",
|
||||
"ORD_GNO_BRNO": "",
|
||||
"ODNO": str(odno or ""),
|
||||
"INQR_DVSN_3": "00",
|
||||
"INQR_DVSN_1": "",
|
||||
"CTX_AREA_FK100": fk100,
|
||||
"CTX_AREA_NK100": nk100,
|
||||
"EXCG_ID_DVSN_CD": str(
|
||||
get_env_from_db("DAILY_CCLD_EXCG_ID", "KRX") or "KRX"
|
||||
),
|
||||
},
|
||||
tr_cont=tr_cont,
|
||||
)
|
||||
if r is None or r.status_code != 200:
|
||||
if merged_out1 and last_j is not None:
|
||||
break
|
||||
return None
|
||||
j = r.json()
|
||||
if j.get("rt_cd") != "0":
|
||||
if merged_out1 and last_j is not None:
|
||||
break
|
||||
return None
|
||||
last_j = j
|
||||
chunk = j.get("output1") or []
|
||||
if isinstance(chunk, dict):
|
||||
chunk = [chunk]
|
||||
if isinstance(chunk, list):
|
||||
merged_out1.extend(chunk)
|
||||
# ODNO 단건이면 1페이지로 충분
|
||||
if odno:
|
||||
break
|
||||
hdr_cont = ""
|
||||
try:
|
||||
hdr_cont = str(r.headers.get("tr_cont") or "").strip().upper()
|
||||
except Exception:
|
||||
hdr_cont = ""
|
||||
body_fk = str(j.get("ctx_area_fk100") or "").strip()
|
||||
body_nk = str(j.get("ctx_area_nk100") or "").strip()
|
||||
if hdr_cont not in ("M", "F") or not body_nk:
|
||||
break
|
||||
fk100 = body_fk
|
||||
nk100 = body_nk
|
||||
tr_cont = "N"
|
||||
# 연속조회 간 짧은 간격 (유량 존중)
|
||||
import time as _t
|
||||
_t.sleep(max(0.05, float(get_env_float("DAILY_CCLD_PAGE_GAP_SEC", 0.08))))
|
||||
if last_j is None:
|
||||
return None
|
||||
j = r.json()
|
||||
return j if j.get("rt_cd") == "0" else None
|
||||
out = dict(last_j)
|
||||
out["output1"] = merged_out1
|
||||
return out
|
||||
except Exception as e:
|
||||
logger.debug("주문 내역 조회 실패: %s", e)
|
||||
return None
|
||||
|
||||
def inquire_psbl_order(
|
||||
self,
|
||||
code: str = "005930",
|
||||
*,
|
||||
price: int = 0,
|
||||
ord_dvsn: str = "01",
|
||||
) -> Optional[Dict]:
|
||||
"""매수가능조회 [국내주식-007] inquire-psbl-order (TTTC8908R / VTTC8908R).
|
||||
|
||||
주식잔고(inquire-balance) output2 에는 주문가능현금 필드가 없다.
|
||||
주문가능현금·미수없는매수금액은 이 API 의
|
||||
``ord_psbl_cash`` / ``nrcvb_buy_amt`` 를 쓴다 (공식 샘플 주석과 동일).
|
||||
"""
|
||||
pdno = str(code or "005930").strip()
|
||||
tr_id = "VTTC8908R" if self.mock else "TTTC8908R"
|
||||
try:
|
||||
r = self._get(
|
||||
"/uapi/domestic-stock/v1/trading/inquire-psbl-order",
|
||||
tr_id,
|
||||
{
|
||||
"CANO": self.account_no,
|
||||
"ACNT_PRDT_CD": self.account_code,
|
||||
"PDNO": pdno,
|
||||
"ORD_UNPR": str(int(price or 0)),
|
||||
"ORD_DVSN": str(ord_dvsn or "01"),
|
||||
"CMA_EVLU_AMT_ICLD_YN": str(
|
||||
get_env_from_db("PSBL_ORDER_CMA_ICLD", "N") or "N"
|
||||
),
|
||||
"OVRS_ICLD_YN": str(
|
||||
get_env_from_db("PSBL_ORDER_OVRS_ICLD", "N") or "N"
|
||||
),
|
||||
},
|
||||
)
|
||||
if r is None or r.status_code != 200:
|
||||
return None
|
||||
j = r.json()
|
||||
if j.get("rt_cd") != "0":
|
||||
return None
|
||||
out = j.get("output") or j.get("output1") or {}
|
||||
if isinstance(out, list) and out:
|
||||
out = out[0]
|
||||
return out if isinstance(out, dict) else None
|
||||
except Exception as e:
|
||||
logger.debug("매수가능조회 실패: %s", e)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _execution_map_from_history(j: Optional[dict]) -> Dict[str, Dict]:
|
||||
"""inquire-daily-ccld 응답 → {odno: {filled_qty, avg_price, code}}."""
|
||||
|
||||
Reference in New Issue
Block a user