435 lines
14 KiB
Python
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())
|