feat: add ledger backfill command

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-08-24 10:24:05 +00:00
committed by GitHub
co-authored by genichin
parent f124e9509c
commit e2c4bbba34
5 changed files with 707 additions and 0 deletions
+114
View File
@@ -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,