feat: add portfolio snapshot read layer
This commit is contained in:
@@ -16,6 +16,7 @@ from typing import Optional
|
||||
import httpx
|
||||
|
||||
from trex_core.application.broker_port import (
|
||||
BalanceItem,
|
||||
FillStatus,
|
||||
OrderAck,
|
||||
OrderRequest,
|
||||
@@ -196,6 +197,43 @@ class KISBroker:
|
||||
return FillStatus(state="partially_filled", filled_qty=ccld_qty,
|
||||
avg_price=float(avg) if avg else None, raw=row)
|
||||
|
||||
def get_balance(self) -> list[BalanceItem]:
|
||||
"""KIS 주식잔고조회 (`TTTC8434R`/`VTTC8434R`).
|
||||
|
||||
Aggregates `output1[*].pdno` + `hldg_qty`, drops zero-qty rows
|
||||
(D-2 leftover, Design Decision 7). Network/credential errors map to
|
||||
an empty list (caller treats as no broker positions).
|
||||
"""
|
||||
cano, product = self._account()
|
||||
tr_id = "VTTC8434R" if self._mock else "TTTC8434R"
|
||||
params = {
|
||||
"CANO": cano, "ACNT_PRDT_CD": product,
|
||||
"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": "",
|
||||
}
|
||||
try:
|
||||
resp = httpx.get(
|
||||
f"{self._base_url}/uapi/domestic-stock/v1/trading/inquire-balance",
|
||||
headers=self._headers(tr_id), params=params, timeout=self._timeout,
|
||||
)
|
||||
payload = resp.json()
|
||||
except (httpx.HTTPError, ValueError):
|
||||
return []
|
||||
if str(payload.get("rt_cd")) != "0":
|
||||
return []
|
||||
rows = payload.get("output1") or []
|
||||
out: list[BalanceItem] = []
|
||||
for row in rows:
|
||||
symbol = str(row.get("pdno") or "").strip()
|
||||
qty = _safe_int(row.get("hldg_qty")) or 0
|
||||
if not symbol or qty <= 0:
|
||||
continue
|
||||
out.append(BalanceItem(symbol=symbol, hldg_qty=qty))
|
||||
return out
|
||||
|
||||
def cancel_order(self, broker_order_id: str, *, side: str, symbol: str, qty: int) -> bool:
|
||||
cano, product = self._account()
|
||||
parts = broker_order_id.split(":")
|
||||
|
||||
@@ -11,6 +11,7 @@ import threading
|
||||
from typing import Optional
|
||||
|
||||
from trex_core.application.broker_port import (
|
||||
BalanceItem,
|
||||
FillStatus,
|
||||
OrderAck,
|
||||
OrderRequest,
|
||||
@@ -94,3 +95,19 @@ class PaperBroker:
|
||||
return False
|
||||
o.cancelled = True
|
||||
return True
|
||||
|
||||
def get_balance(self) -> list[BalanceItem]:
|
||||
"""Deterministic per-symbol fill aggregate (REQ-031 AC-3).
|
||||
|
||||
Aggregates buys − sells across all filled (or partially filled)
|
||||
paper orders, drops zero-qty rows (Design Decision 7).
|
||||
"""
|
||||
with self._lock:
|
||||
balances: dict[str, int] = {}
|
||||
for o in self._orders.values():
|
||||
if o.filled <= 0:
|
||||
continue
|
||||
sign = 1 if o.side == "buy" else -1
|
||||
balances[o.symbol] = balances.get(o.symbol, 0) + sign * int(o.filled)
|
||||
return [BalanceItem(symbol=sym, hldg_qty=qty)
|
||||
for sym, qty in balances.items() if qty != 0]
|
||||
|
||||
Reference in New Issue
Block a user