Files
kis_trader/scripts/sql/fill_all_db_column_comments.py

435 lines
14 KiB
Python

#!/usr/bin/env python3
"""kis_quant_db 전 테이블·전 컬럼 MariaDB COMMENT 채우기 (타입·값 변경 없음).
우선순위:
1) 기존 COMMENT 유지
2) live_config_schema label+hint (env/config 키)
3) 공통 컬럼명 사전 + 테이블 문맥
4) 키/컬럼명 휴리스틱
실행:
cd /home/hoon/kis_bot && python3 scripts/sql/fill_all_db_column_comments.py --apply
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from typing import Dict, List, Tuple
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
# 공통 컬럼명 → 설명 (테이블 무관)
_COL_EXACT: Dict[str, str] = {
"id": "PK · AUTO_INCREMENT",
"code": "종목코드 (KR 6자리 / US 티커)",
"name": "종목명",
"strategy": "전략 ID (SHORT/MOMENTUM/BREAKOUT/US_MOMENTUM 등)",
"strategy_id": "전략 ID",
"avg_buy_price": "평균 매수가",
"current_price": "최근 평가/현재가 스냅샷",
"stop_price": "손절가",
"target_price": "목표가",
"max_price": "보유 중 고점(트레일·어깨컷용)",
"atr_entry": "진입 시점 ATR",
"target_qty": "목표 수량",
"current_qty": "현재 보유 수량",
"total_invested": "총 투자금액",
"status": "상태",
"buy_date": "매수 시각/일자",
"sell_date": "매도 시각/일자",
"updated_at": "마지막 갱신 시각",
"created_at": "생성 시각",
"size_class": "사이즈 등급(소/중/대 등)",
"rsi": "진입/기록 시점 RSI",
"volume_ratio": "거래량 배수(기록 스냅샷)",
"tail_length_pct": "꼬리 길이%(기록 스냅샷)",
"ma5_gap_pct": "MA5 괴리%(기록 스냅샷)",
"ma20_gap_pct": "MA20 괴리%(기록 스냅샷)",
"foreign_net_buy": "외국인 순매수(기록 스냅샷)",
"institution_net_buy": "기관 순매수(기록 스냅샷)",
"market_hour": "장중 시각 메타",
"buy_price": "매수가",
"sell_price": "매도가",
"qty": "수량(주)",
"profit_rate": "수익률(비율)",
"realized_pnl": "실현손익",
"hold_minutes": "보유 분",
"sell_reason": "매도 사유",
"env_snapshot": "체결 시점 env/파라미터 JSON 스냅샷",
"ord_no": "증권사 주문번호",
"side": "매수/매도 구분",
"price": "주문/체결 가격",
"filled_qty": "체결 수량",
"filled_avg_price": "체결 평균가",
"msg_cd": "응답 코드",
"msg1": "응답 메시지",
"ord_date": "주문일",
"submitted_at": "주문 제출 시각",
"filled_at": "체결 시각",
"raw_json": "원본 응답 JSON",
"market": "시장 구분 (KR|US 등)",
"exchange": "거래소 (NASD/NYSE/AMEX/KRX 등)",
"symbol": "심볼/티커",
"tick_time": "틱 시각 YYYYMMDDHHMMSS",
"candle_time": "봉 시각 YYYYMMDDHHMM",
"snap_time": "스냅샷 시각 YYYYMMDDHHMMSS",
"volume": "거래량(체결량/봉거래량)",
"tick_seq": "틱 일련번호",
"session": "세션 태그",
"currency": "통화 (KRW|USD)",
"source": "데이터 소스 (kis/kiwoom/ls 등)",
"recv_ts": "수신 시각",
"timeframe": "분봉 주기(분)",
"tf_min": "분봉 주기(분)",
"open": "시가",
"high": "고가",
"low": "저가",
"close": "종가",
"is_confirmed": "봉 확정 여부 (1=확정)",
"holding_peak": "보유 중 트레일 고점(WS 틱 max_price 스냅샷)",
"best_bid": "최우선 매수호가",
"best_ask": "최우선 매도호가",
"total_bid_qty": "총 매수잔량",
"total_ask_qty": "총 매도잔량",
"bid_qty_l3": "매수 1~3호가 잔량합",
"ask_qty_l3": "매도 1~3호가 잔량합",
"levels_json": "호가 단계 JSON",
"enabled": "활성 여부 (1=ON)",
"note": "메모",
"market_type": "시장유형 (KR|US)",
"k": "",
"v": "",
"env_key": "설정 키",
"env_value": "설정 값",
"score": "점수/랭크",
"scan_time": "스캔 시각",
"slot_key": "슬롯/히스토리 키",
"sector": "섹터",
"theme": "테마",
}
_TABLE_SCOPE: Dict[str, str] = {
"active_trades": "실보유",
"active_trades_paper": "페이퍼보유",
"orders": "실주문",
"orders_paper": "페이퍼주문",
"trade_history": "실청산이력",
"trade_history_paper": "페이퍼청산이력",
"ws_ticks": "국장틱",
"ws_ticks_us": "해외틱",
"ws_candles": "분봉",
"ws_orderbook": "호가스냅",
"ws_program": "프로그램매매",
"ls_ws_ticks": "LS틱",
"ls_ws_candles": "LS분봉",
"ls_ws_orderbook": "LS호가",
"ls_ws_vi": "LS VI",
"permanent_subscriptions": "영구구독",
"target_candidates": "현재후보",
"target_candidates_history": "후보이력",
"ls_candidates_history": "LS후보이력",
"ls_universe_history": "LS유니버스이력",
}
_TOKEN_KO: List[Tuple[str, str]] = [
("STRATEGY", "전략"),
("STOP_LOSS", "손절"),
("TAKE_PROFIT", "익절"),
("TRAIL", "트레일"),
("SHOULDER", "어깨"),
("RATCHET", "래칫"),
("ORDERBOOK", "호가"),
("PROGRAM", "프로그램"),
("WHIPSAW", "휩쏘"),
("ENABLED", "ON/OFF"),
("HOLDING_PEAK", "보유고점"),
("IS_CONFIRMED", "봉확정"),
("CANDLE_TIME", "봉시각"),
("TICK_TIME", "틱시각"),
("SNAP_TIME", "스냅시각"),
("BEST_BID", "최우선매수"),
("BEST_ASK", "최우선매도"),
("TOTAL_BID", "총매수잔량"),
("TOTAL_ASK", "총매도잔량"),
("LEVELS_JSON", "호가단계JSON"),
("RAW_JSON", "원본JSON"),
("FILLED_AVG", "체결평균"),
("FILLED_QTY", "체결수량"),
("FILLED_AT", "체결시각"),
("SUBMITTED", "제출"),
("REALIZED_PNL", "실현손익"),
("PROFIT_RATE", "수익률"),
("HOLD_MINUTES", "보유분"),
("SELL_REASON", "매도사유"),
("ENV_SNAPSHOT", "env스냅샷"),
("AVG_BUY", "평균매수"),
("CURRENT_QTY", "현재수량"),
("TARGET_QTY", "목표수량"),
("CURRENT_PRICE", "현재가"),
("STOP_PRICE", "손절가"),
("TARGET_PRICE", "목표가"),
("MAX_PRICE", "고점"),
("MARKET_TYPE", "시장유형"),
("TIMEFRAME", "분봉주기"),
("TF_MIN", "분봉분"),
("RECV_TS", "수신시각"),
("TICK_SEQ", "틱일련"),
("CURRENCY", "통화"),
("EXCHANGE", "거래소"),
("SESSION", "세션"),
("VOLUME", "거래량"),
("SOURCE", "소스"),
("STATUS", "상태"),
("SIDE", "매매방향"),
("PRICE", "가격"),
("QTY", "수량"),
("CODE", "종목코드"),
("NAME", "종목명"),
("SYMBOL", "심볼"),
("MARKET", "시장"),
("RSI", "RSI"),
("EMA", "EMA"),
("ATR", "ATR"),
("PCT", "%"),
("SEC", ""),
("MS", "ms"),
("JSON", "JSON"),
]
def _esc_comment(s: str) -> str:
s = re.sub(r"\s+", " ", (s or "").strip())
s = s.replace("\\", "\\\\").replace("'", "''")
s = s.replace("%", "%%")
if len(s) > 240:
s = s[:237] + "..."
return s
def _heuristic(col: str, table: str) -> str:
if col in _COL_EXACT:
base = _COL_EXACT[col]
scope = _TABLE_SCOPE.get(table)
if scope and scope not in base:
return f"{scope}: {base}"
return base
# rsi_14 / ema_9 style
m = re.match(r"^(rsi|ema)_(\d+)$", col, re.I)
if m:
return f"{m.group(1).upper()}({m.group(2)}) materialized"
k = col.upper()
parts: List[str] = []
rest = k
while rest:
matched = False
for tok, ko in _TOKEN_KO:
if rest.startswith(tok):
parts.append(ko)
rest = rest[len(tok):]
if rest.startswith("_"):
rest = rest[1:]
matched = True
break
if not matched:
if "_" in rest:
chunk, rest = rest.split("_", 1)
else:
chunk, rest = rest, ""
parts.append(chunk)
body = " ".join(parts) if parts else col
scope = _TABLE_SCOPE.get(table, table)
return f"{scope}: {body} ({col})"
def _load_live_labels() -> Dict[str, str]:
out: Dict[str, str] = {}
try:
from kis_trader.web.live_config_schema import build_live_config_groups
except Exception:
return out
for g in build_live_config_groups():
for f in g.get("fields") or []:
k = f.get("key")
if not k:
continue
lab = str(f.get("label") or "").strip()
hint = str(f.get("hint") or "").strip()
bits = [b for b in (lab, hint) if b]
if bits:
out[str(k)] = " · ".join(bits)
return out
def _describe(col: str, table: str, labels: Dict[str, str], existing: str) -> str:
if (existing or "").strip():
return existing.strip()
if col in labels:
return labels[col]
return _heuristic(col, table)
def _col_type_clause(row: dict) -> str:
typ = row["Type"]
null = "NULL" if row["Null"] == "YES" else "NOT NULL"
default = row["Default"]
extra = row.get("Extra") or ""
parts = [typ, null]
if default is None:
if row["Null"] == "YES":
parts.append("DEFAULT NULL")
else:
draw = str(default).strip()
dnorm = draw.upper().replace(" ", "")
# MariaDB SHOW 가 current_timestamp() 문자열로 주는 경우
if dnorm in (
"CURRENT_TIMESTAMP",
"CURRENT_TIMESTAMP()",
"NOW()",
"CURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP",
) or dnorm.startswith("CURRENT_TIMESTAMP"):
# ON UPDATE 는 Extra 에 있을 수 있음
parts.append("DEFAULT CURRENT_TIMESTAMP")
if "on update" in extra.lower():
parts.append("ON UPDATE CURRENT_TIMESTAMP")
elif re.match(
r"^(tinyint|smallint|mediumint|int|bigint|decimal|float|double|bit)",
typ,
re.I,
):
parts.append(f"DEFAULT {draw}")
else:
d = draw.replace("\\", "\\\\").replace("'", "''")
parts.append(f"DEFAULT '{d}'")
if "auto_increment" in extra.lower():
parts.append("AUTO_INCREMENT")
return " ".join(parts)
def _list_tables(db) -> List[str]:
rows = db.conn.execute(
"SELECT TABLE_NAME AS t FROM information_schema.TABLES "
"WHERE TABLE_SCHEMA=DATABASE() AND TABLE_TYPE='BASE TABLE' "
"ORDER BY TABLE_NAME"
).fetchall()
return [r["t"] for r in rows]
def _sync_erd(db, tables: List[str]) -> int:
path = ROOT / "docs/like_mcp.md/db_erd.md"
if not path.exists():
return 0
comments = {}
for t in tables:
for r in db.conn.execute(f"SHOW FULL COLUMNS FROM `{t}`").fetchall():
comments[(t, r["Field"])] = (r.get("Comment") or "").strip()
text = path.read_text(encoding="utf-8")
current = None
out = []
row_re = re.compile(
r"^\| `([^`]+)`( 🔑)? \| `([^`]+)` \| ([^|]*) \| ([^|]*) \| (.*) \|$"
)
n_upd = 0
for line in text.splitlines():
m = re.match(r"^#### `([^`]+)`\s*$", line)
if m:
current = m.group(1)
out.append(line)
continue
m = row_re.match(line)
if m and current:
col = m.group(1)
key = (current, col)
if key in comments and comments[key]:
cmt = comments[key].replace("|", "/")
keymark = m.group(2) or ""
typ = m.group(3)
nul = m.group(4).strip()
dflt = m.group(5).strip()
newline = f"| `{col}`{keymark} | `{typ}` | {nul} | {dflt} | {cmt} |"
if newline != line:
n_upd += 1
out.append(newline)
continue
out.append(line)
path.write_text("\n".join(out) + "\n", encoding="utf-8")
return n_upd
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true", default=True)
ap.add_argument("--sql-only", action="store_true")
ap.add_argument(
"--out",
default=str(ROOT / "scripts/sql/all_db_column_comments.generated.sql"),
)
ap.add_argument("--no-erd", action="store_true")
args = ap.parse_args()
if args.sql_only:
args.apply = False
from database import TradeDB
db = TradeDB()
labels = _load_live_labels()
tables = _list_tables(db)
print(f"tables={len(tables)} live_labels={len(labels)}")
sql_chunks: List[str] = [
"-- AUTO-GENERATED: 전 DB 테이블 컬럼 COMMENT (타입·값 불변)",
"-- python3 scripts/sql/fill_all_db_column_comments.py --apply",
"",
]
filled = kept = 0
for table in tables:
rows = db.conn.execute(f"SHOW FULL COLUMNS FROM `{table}`").fetchall()
mods = []
for r in rows:
field = r["Field"]
existing = (r.get("Comment") or "").strip()
desc = _describe(field, table, labels, existing)
if existing:
kept += 1
else:
filled += 1
type_clause = _col_type_clause(r)
cmt = _esc_comment(desc)
mods.append(
f" MODIFY COLUMN `{field}`\n {type_clause}\n COMMENT '{cmt}'"
)
if args.apply and existing != desc:
stmt = (
f"ALTER TABLE `{table}` MODIFY COLUMN `{field}` "
f"{type_clause} COMMENT '{cmt}'"
)
db.conn.execute(stmt)
sql_chunks.append(f"-- ── {table} ({len(rows)} cols) ──")
sql_chunks.append(f"ALTER TABLE `{table}`")
sql_chunks.append(",\n".join(mods) + ";\n")
print(f"{'applied' if args.apply else 'planned'} {table} ({len(rows)})")
out = Path(args.out)
out.write_text("\n".join(sql_chunks) + "\n", encoding="utf-8")
print(f"wrote {out} ({out.stat().st_size} bytes)")
print(f"kept_existing={kept} newly_filled={filled}")
if args.apply:
empty = 0
for table in tables:
for r in db.conn.execute(f"SHOW FULL COLUMNS FROM `{table}`").fetchall():
if not (r.get("Comment") or "").strip():
empty += 1
print("EMPTY", table, r["Field"])
print("remaining empty:", empty)
if not args.no_erd:
n = _sync_erd(db, tables)
print(f"db_erd.md rows updated: {n}")
return 0
if __name__ == "__main__":
raise SystemExit(main())