feat: add ledger backfill command
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
f124e9509c
commit
e2c4bbba34
@@ -43,6 +43,10 @@ _SUBMISSION_LOOKUP_GRACE_SECONDS = 90.0
|
||||
_SUPPORTED_ORDER_VENUES = frozenset({"KRX", "NXT", "SOR"})
|
||||
|
||||
|
||||
class KISEvidenceLookupError(RuntimeError):
|
||||
"""Read-only daily-fill evidence lookup failed."""
|
||||
|
||||
|
||||
def _balance_http_error_code(status: Optional[int]) -> str:
|
||||
if status == 401:
|
||||
return "authentication_failed"
|
||||
@@ -594,6 +598,116 @@ class KISBroker:
|
||||
# SOR is routing provenance, not an execution exchange.
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _order_venue_from_row(row: dict) -> str | None:
|
||||
"""Parse original routing venue (KRX/NXT/SOR) without time inference."""
|
||||
for key in ("excg_id_dvsn_cd", "excg_id_dvsn_Cd", "EXCG_ID_DVSN_CD", "order_venue"):
|
||||
value = str(row.get(key) or "").strip().upper()
|
||||
if value in _SUPPORTED_ORDER_VENUES:
|
||||
return value
|
||||
if value in {"SOR-KRX", "SOR-NXT"}:
|
||||
return "SOR"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _side_from_row(row: dict) -> str | None:
|
||||
code = str(row.get("sll_buy_dvsn_cd") or row.get("SLL_BUY_DVSN_CD") or "").strip()
|
||||
if code == "01":
|
||||
return "sell"
|
||||
if code == "02":
|
||||
return "buy"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_daily_ccld_execution_id(execution_id: str) -> dict[str, str]:
|
||||
parts = str(execution_id or "").split(":")
|
||||
if len(parts) != 5 or parts[0] != "kis-daily-ccld":
|
||||
raise KISEvidenceLookupError("unsupported execution_id format")
|
||||
order_date, orgno, odno, qty = parts[1], parts[2], parts[3], parts[4]
|
||||
if len(order_date) != 8 or not order_date.isdigit():
|
||||
raise KISEvidenceLookupError("execution_id missing order date")
|
||||
if not odno or not qty.isdigit() or int(qty) <= 0:
|
||||
raise KISEvidenceLookupError("execution_id missing order number or qty")
|
||||
return {
|
||||
"order_date": order_date,
|
||||
"orgno": orgno,
|
||||
"odno": odno,
|
||||
"qty": qty,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _executed_at_from_row(row: dict, fallback_date: str) -> datetime:
|
||||
order_date = str(row.get("ord_dt") or fallback_date).strip()
|
||||
order_time = str(row.get("ord_tmd") or "").strip()[:6]
|
||||
if len(order_time) == 6 and order_time.isdigit():
|
||||
ordered_at = datetime.strptime(order_date + order_time, "%Y%m%d%H%M%S")
|
||||
return ordered_at.replace(tzinfo=_KST).astimezone(timezone.utc)
|
||||
ordered_at = datetime.strptime(order_date, "%Y%m%d")
|
||||
return ordered_at.replace(tzinfo=_KST).astimezone(timezone.utc)
|
||||
|
||||
def lookup_fill_evidence(self, *, trading_date: str, execution_id: str) -> dict:
|
||||
"""Read one exact filled/partial-cancel evidence row for a historical execution."""
|
||||
parsed = self._parse_daily_ccld_execution_id(execution_id)
|
||||
if trading_date != parsed["order_date"]:
|
||||
raise KISEvidenceLookupError("execution_id date does not match --date")
|
||||
tr_id, params = self._daily_fill_request(parsed["order_date"], order_number=parsed["odno"])
|
||||
try:
|
||||
resp = httpx.get(
|
||||
f"{self._base_url}/uapi/domestic-stock/v1/trading/inquire-daily-ccld",
|
||||
headers=self._headers(tr_id),
|
||||
params=params,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
payload = resp.json()
|
||||
except (httpx.HTTPError, ValueError) as exc:
|
||||
raise KISEvidenceLookupError(
|
||||
f"daily fill evidence lookup failed: {exc.__class__.__name__}"
|
||||
) from exc
|
||||
if getattr(resp, "status_code", 200) >= 400 or str(payload.get("rt_cd")) != "0":
|
||||
raise KISEvidenceLookupError("daily fill evidence lookup rejected")
|
||||
matches: list[dict] = []
|
||||
normalized_odno = parsed["odno"].lstrip("0") or "0"
|
||||
for row in payload.get("output1") or []:
|
||||
row_odno = str(row.get("odno") or "").strip().lstrip("0") or "0"
|
||||
if row_odno != normalized_odno:
|
||||
continue
|
||||
side = self._side_from_row(row)
|
||||
if side is None:
|
||||
continue
|
||||
status = self._fill_status_from_row(
|
||||
row, side=side, today=parsed["order_date"], fallback_odno=parsed["odno"]
|
||||
)
|
||||
if status.execution_id != execution_id:
|
||||
continue
|
||||
order_venue = self._order_venue_from_row(row)
|
||||
if order_venue is None:
|
||||
continue
|
||||
orgno = str(row.get("ord_gno_brno") or "").strip()
|
||||
odno = str(row.get("odno") or parsed["odno"]).strip()
|
||||
symbol = str(row.get("pdno") or "").strip()
|
||||
if not symbol:
|
||||
continue
|
||||
matches.append(
|
||||
{
|
||||
"symbol": symbol.zfill(6),
|
||||
"side": side,
|
||||
"qty": float(status.filled_qty),
|
||||
"price": float(status.avg_price or 0.0),
|
||||
"fee": float(status.fee),
|
||||
"tax": float(status.tax),
|
||||
"order_date": parsed["order_date"],
|
||||
"execution_id": execution_id,
|
||||
"order_id": f"{orgno}:{odno}" if orgno else odno,
|
||||
"order_venue": order_venue,
|
||||
"execution_venue": status.execution_venue,
|
||||
"executed_at": self._executed_at_from_row(row, parsed["order_date"]),
|
||||
"raw": dict(row),
|
||||
}
|
||||
)
|
||||
if len(matches) != 1:
|
||||
raise KISEvidenceLookupError("daily fill evidence not found or ambiguous")
|
||||
return matches[0]
|
||||
|
||||
@staticmethod
|
||||
def _fill_status_from_row(
|
||||
row: dict,
|
||||
|
||||
Reference in New Issue
Block a user