ㅇ Changes: - Introduced the DART strategy to the trading system, including its configuration and integration into the existing framework. - Updated the database schema to include DART-specific tables for disclosures and watchlists. - Enhanced the backtesting and parameter search functionalities to support the DART strategy. - Implemented new rules for browser verification and API interactions to ensure compliance with the updated DART strategy. Impact: - These additions expand the trading capabilities of the system, allowing for more comprehensive analysis and execution of DART-related strategies, while maintaining system integrity and performance.
169 lines
4.9 KiB
Python
169 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
kis_trader/scan/dart_watchlist.py — DART 임시 워치 (영구구독 아님)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from kis_trader.utils.env import get_env_bool, get_env_int
|
|
from kis_trader.utils.logger import get_logger
|
|
|
|
logger = get_logger("kis_trader.dart_watch")
|
|
|
|
|
|
def _now() -> dt.datetime:
|
|
return dt.datetime.now()
|
|
|
|
|
|
def _fmt(ts: dt.datetime) -> str:
|
|
return ts.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
def ensure_watchlist_table(db: Any) -> None:
|
|
db.conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS dart_watchlist (
|
|
stock_code VARCHAR(20) NOT NULL PRIMARY KEY,
|
|
corp_name VARCHAR(100) NOT NULL DEFAULT '',
|
|
rcept_no VARCHAR(32) NOT NULL DEFAULT '',
|
|
report_nm VARCHAR(255) NOT NULL DEFAULT '',
|
|
added_at VARCHAR(30) NOT NULL,
|
|
expires_at VARCHAR(30) NOT NULL,
|
|
enabled TINYINT NOT NULL DEFAULT 1,
|
|
KEY idx_dart_watch_exp (expires_at)
|
|
) CHARACTER SET utf8mb4
|
|
"""
|
|
)
|
|
|
|
|
|
def purge_expired(db: Any) -> int:
|
|
ensure_watchlist_table(db)
|
|
now = _fmt(_now())
|
|
cur = db.conn.execute(
|
|
"DELETE FROM dart_watchlist WHERE expires_at < %s OR enabled=0",
|
|
(now,),
|
|
)
|
|
try:
|
|
return int(cur.rowcount or 0)
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def list_active_watch(db: Any) -> List[Dict[str, Any]]:
|
|
ensure_watchlist_table(db)
|
|
purge_expired(db)
|
|
now = _fmt(_now())
|
|
rows = db.conn.execute(
|
|
"""
|
|
SELECT stock_code, corp_name, rcept_no, report_nm, added_at, expires_at
|
|
FROM dart_watchlist
|
|
WHERE enabled=1 AND expires_at >= %s
|
|
ORDER BY added_at DESC
|
|
""",
|
|
(now,),
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def watch_count(db: Any) -> int:
|
|
return len(list_active_watch(db))
|
|
|
|
|
|
def upsert_watch(
|
|
db: Any,
|
|
*,
|
|
stock_code: str,
|
|
corp_name: str = "",
|
|
rcept_no: str = "",
|
|
report_nm: str = "",
|
|
ttl_hours: Optional[int] = None,
|
|
watch_max: Optional[int] = None,
|
|
) -> bool:
|
|
"""
|
|
구독 스위치 ON일 때만 호출. 상한 초과 시 가장 오래된 것 제거 후 삽입.
|
|
"""
|
|
if not get_env_bool("DART_SUBSCRIBE_ENABLED", False):
|
|
return False
|
|
code = (stock_code or "").strip()
|
|
if not code:
|
|
return False
|
|
ensure_watchlist_table(db)
|
|
purge_expired(db)
|
|
ttl = int(ttl_hours if ttl_hours is not None else get_env_int("DART_WATCH_TTL_HOURS", 24))
|
|
cap = int(watch_max if watch_max is not None else get_env_int("DART_WATCH_MAX", 15))
|
|
now = _now()
|
|
exp = now + dt.timedelta(hours=max(1, ttl))
|
|
|
|
active = list_active_watch(db)
|
|
if code not in {a["stock_code"] for a in active} and len(active) >= cap:
|
|
# 가장 오래된 1건 제거
|
|
oldest = sorted(active, key=lambda x: x.get("added_at") or "")[:1]
|
|
for o in oldest:
|
|
db.conn.execute(
|
|
"DELETE FROM dart_watchlist WHERE stock_code=%s",
|
|
(o["stock_code"],),
|
|
)
|
|
logger.info("DART 워치 상한 — 제거 %s", o["stock_code"])
|
|
|
|
db.conn.execute(
|
|
"""
|
|
INSERT INTO dart_watchlist
|
|
(stock_code, corp_name, rcept_no, report_nm, added_at, expires_at, enabled)
|
|
VALUES (%s, %s, %s, %s, %s, %s, 1)
|
|
ON DUPLICATE KEY UPDATE
|
|
corp_name=VALUES(corp_name),
|
|
rcept_no=VALUES(rcept_no),
|
|
report_nm=VALUES(report_nm),
|
|
added_at=VALUES(added_at),
|
|
expires_at=VALUES(expires_at),
|
|
enabled=1
|
|
""",
|
|
(
|
|
code[:20],
|
|
(corp_name or "")[:100],
|
|
(rcept_no or "")[:32],
|
|
(report_nm or "")[:255],
|
|
_fmt(now),
|
|
_fmt(exp),
|
|
),
|
|
)
|
|
return True
|
|
|
|
|
|
def event_time_for_code(db: Any, code: str) -> Optional[str]:
|
|
"""워치/최신 공시 시각 → 봉키 YYYYMMDDHHMM 근사."""
|
|
ensure_watchlist_table(db)
|
|
code = (code or "").strip()
|
|
row = db.conn.execute(
|
|
"""
|
|
SELECT added_at, rcept_no FROM dart_watchlist
|
|
WHERE stock_code=%s AND enabled=1
|
|
ORDER BY added_at DESC LIMIT 1
|
|
""",
|
|
(code,),
|
|
).fetchone()
|
|
if row and row.get("added_at"):
|
|
s = str(row["added_at"]).replace("-", "").replace(":", "").replace(" ", "")
|
|
if len(s) >= 12:
|
|
return s[:12]
|
|
# disclosures fallback
|
|
d = db.conn.execute(
|
|
"""
|
|
SELECT first_seen_at, rcept_dt FROM dart_disclosures
|
|
WHERE stock_code=%s ORDER BY first_seen_at DESC LIMIT 1
|
|
""",
|
|
(code,),
|
|
).fetchone()
|
|
if not d:
|
|
return None
|
|
if d.get("first_seen_at"):
|
|
s = str(d["first_seen_at"]).replace("-", "").replace(":", "").replace(" ", "")
|
|
if len(s) >= 12:
|
|
return s[:12]
|
|
rd = str(d.get("rcept_dt") or "")
|
|
if len(rd) == 8:
|
|
return rd + "0900"
|
|
return None
|