alt/bin/kis-paper-daily-smoke
toki ee01990117 refactor: organize operator-ui phase and update roadmap structures
- Move flutter operator console and push notification milestones to operator-ui phase
- Archive kis-live-data-collection-pipeline milestone
- Add command-first operator workflow milestone
- Add operator-ui phase definition
- Add paper-trading-command-workflow milestone
- Update phase documentation and dependencies
2026-06-03 18:10:52 +09:00

218 lines
6.8 KiB
Python
Executable file

#!/usr/bin/env python3
"""Run a minimal KIS daily-price smoke without printing secrets."""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
ENV_CONFIG = {
"paper": {
"base_url": "https://openapivts.koreainvestment.com:29443",
"app_key": "KIS_PAPER_APP_KEY",
"app_secret": "KIS_PAPER_APP_SECRET",
"cano": "KIS_PAPER_CANO",
"product": "KIS_PAPER_ACNT_PRDT_CD",
},
"real": {
"base_url": "https://openapi.koreainvestment.com:9443",
"app_key": "KIS_REAL_APP_KEY",
"app_secret": "KIS_REAL_APP_SECRET",
"cano": "KIS_REAL_CANO",
"product": "KIS_REAL_ACNT_PRDT_CD",
},
}
TOKEN_PATH = "/oauth2/tokenP"
DAILY_PATH = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
DAILY_TR_ID = "FHKST03010100"
USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
)
class SmokeError(Exception):
pass
def required_env(name: str) -> str:
value = os.environ.get(name, "")
if value == "":
raise SmokeError(f"missing required env: {name}")
return value
def redact(text: str, secrets: list[str]) -> str:
redacted = text
for secret in secrets:
if secret:
redacted = redacted.replace(secret, "[redacted]")
return redacted
def request_json(
method: str,
url: str,
*,
headers: dict[str, str],
body: dict[str, str] | None = None,
params: dict[str, str] | None = None,
timeout: float,
secrets: list[str],
) -> dict:
if params:
url = f"{url}?{urllib.parse.urlencode(params)}"
data = None
if body is not None:
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as err:
raw = err.read().decode("utf-8", errors="replace")
raise SmokeError(f"HTTP {err.code}: {redact(raw, secrets)}") from err
except urllib.error.URLError as err:
raise SmokeError(f"network error: {err.reason}") from err
try:
return json.loads(raw)
except json.JSONDecodeError as err:
raise SmokeError(f"non-JSON response: {redact(raw[:1000], secrets)}") from err
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run a KIS daily item chart price smoke.",
)
parser.add_argument("--symbol", default=os.environ.get("KIS_SMOKE_SYMBOL", "005930"))
parser.add_argument("--start", default=os.environ.get("KIS_SMOKE_START", "20240527"))
parser.add_argument("--end", default=os.environ.get("KIS_SMOKE_END", "20240528"))
parser.add_argument("--timeout", type=float, default=20.0)
return parser.parse_args(argv)
def validate_yyyymmdd(name: str, value: str) -> None:
if len(value) != 8 or not value.isdigit():
raise SmokeError(f"{name} must be YYYYMMDD")
def main(argv: list[str]) -> int:
args = parse_args(argv)
validate_yyyymmdd("--start", args.start)
validate_yyyymmdd("--end", args.end)
if args.start > args.end:
raise SmokeError("--start must be before or equal to --end")
if args.symbol == "" or not args.symbol.isdigit():
raise SmokeError("--symbol must be a numeric KRX symbol")
active_env = os.environ.get("KIS_ACTIVE_ENV", "")
env_config = ENV_CONFIG.get(active_env)
if env_config is None:
raise SmokeError('KIS_ACTIVE_ENV must be "paper" or "real" for this smoke')
app_key = required_env(env_config["app_key"])
app_secret = required_env(env_config["app_secret"])
account_no = required_env(env_config["cano"])
product = required_env(env_config["product"])
if product != "01":
raise SmokeError(f"{env_config['product']} must be 01 for domestic stock smoke")
base_url = env_config["base_url"]
static_secrets = [app_key, app_secret, account_no]
base_headers = {
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json",
"User-Agent": USER_AGENT,
}
token_payload = request_json(
"POST",
base_url + TOKEN_PATH,
headers=base_headers,
body={
"grant_type": "client_credentials",
"appkey": app_key,
"appsecret": app_secret,
},
timeout=args.timeout,
secrets=static_secrets,
)
access_token = token_payload.get("access_token", "")
if access_token == "":
raise SmokeError(
"token response missing access_token: "
+ redact(json.dumps(token_payload, ensure_ascii=False), static_secrets)
)
query_payload = request_json(
"GET",
base_url + DAILY_PATH,
headers={
**base_headers,
"authorization": f"Bearer {access_token}",
"appkey": app_key,
"appsecret": app_secret,
"tr_id": DAILY_TR_ID,
"custtype": "P",
"tr_cont": "",
},
params={
"FID_COND_MRKT_DIV_CODE": "J",
"FID_INPUT_ISCD": args.symbol,
"FID_INPUT_DATE_1": args.start,
"FID_INPUT_DATE_2": args.end,
"FID_PERIOD_DIV_CODE": "D",
"FID_ORG_ADJ_PRC": "0",
},
timeout=args.timeout,
secrets=static_secrets + [access_token],
)
if query_payload.get("rt_cd") != "0":
raise SmokeError(
"KIS daily query failed: "
+ redact(json.dumps(query_payload, ensure_ascii=False), static_secrets + [access_token])
)
output1 = query_payload.get("output1") or {}
output2 = query_payload.get("output2") or []
if not isinstance(output2, list) or len(output2) == 0:
raise SmokeError("KIS daily query returned no output2 rows")
dates = sorted(
row.get("stck_bsop_date", "")
for row in output2
if isinstance(row, dict) and row.get("stck_bsop_date")
)
if not dates:
raise SmokeError("KIS daily rows did not include stck_bsop_date")
print(f"KIS {active_env} daily smoke ok")
print(f"env={active_env}")
print("provider=kis")
print("endpoint=inquire-daily-itemchartprice")
print(f"symbol={args.symbol}")
print(f"name={output1.get('hts_kor_isnm', '')}")
print(f"bar_count={len(output2)}")
print(f"first_date={dates[0]}")
print(f"last_date={dates[-1]}")
print(f"msg_cd={query_payload.get('msg_cd', '')}")
print(f"msg={query_payload.get('msg1', '')}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main(sys.argv[1:]))
except SmokeError as err:
print(f"KIS daily smoke failed: {err}", file=sys.stderr)
raise SystemExit(1)