Files
kis_trader/scripts/sql/fill_all_config_env_column_comments.py

379 lines
12 KiB
Python

#!/usr/bin/env python3
"""config_* / env_config 전 컬럼 MariaDB COMMENT 채우기 (타입·값 변경 없음).
소스 우선순위:
1) 이미 DB에 있는 COMMENT (비어있지 않으면 유지)
2) live_config_schema label+hint
3) 키 이름 휴리스틱 한글 설명
4) 메타 컬럼(id/created_at) 고정문구
실행:
cd /home/hoon/kis_bot && python3 scripts/sql/fill_all_config_env_column_comments.py
# SQL만: ... --sql-only > scripts/sql/all_config_env_column_comments.generated.sql
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
TABLES = [
"config_us_momentum",
"config_momentum",
"config_short",
"config_breakout",
"config_updow",
"config_scalp",
"config_range_break",
"config_dbband",
"config_dart",
"env_config",
]
# 긴 토큰 우선
_TOKEN_KO: List[Tuple[str, str]] = [
("STRATEGY", "전략"),
("DAILY_PROFIT", "일일익절"),
("STOP_LOSS", "손절"),
("TAKE_PROFIT", "익절"),
("TRAIL_ARM", "트레일무장"),
("TRAIL", "트레일"),
("SHOULDER_MIN_HIGH", "어깨무장고점"),
("SHOULDER_CUT", "어깨컷"),
("SHOULDER", "어깨"),
("RATCHET_TIERS", "다단래칫"),
("RATCHET", "래칫"),
("ORDERBOOK", "호가"),
("PROGRAM", "프로그램매매"),
("WHIPSAW", "휩쏘"),
("UNIVERSE_SOURCE", "유니버스소스"),
("UNIVERSE", "유니버스"),
("CAND_LIMIT", "후보상한"),
("MAX_STOCKS", "동시보유상한"),
("SLOT_MONEY", "슬롯금액"),
("TOTAL_BUDGET", "총운용한도"),
("MAX_BUY_AMOUNT", "1회최대매수금액"),
("MAX_ORDER_QTY", "주문수량상한"),
("FORCE_BUY", "강제매수테스트"),
("LIVE_MIN_CANDLES", "실매최소봉수"),
("MIN_CANDLES", "최소봉수"),
("MAX_HOLD_BARS", "최대보유봉"),
("MIN_HOLD", "최소보유"),
("HOLD_BARS", "보유봉"),
("MAX_DAILY_CHG", "당일등락상한"),
("MAX_DAILY", "일일매수횟수상한"),
("HIGH_CHASE", "고점추격"),
("FROM_OPEN", "시가대비"),
("PULLBACK", "되돌림"),
("BREAKOUT", "돌파"),
("PATTERN", "패턴"),
("DEFENSE", "방어필터"),
("EMA_FAST", "EMA단기"),
("EMA_SLOW", "EMA장기"),
("EMA", "EMA"),
("RSI_OVERBOUGHT", "RSI과매수"),
("RSI_OVERSOLD", "RSI과매도"),
("RSI_MAX", "RSI상한"),
("RSI_MIN", "RSI하한"),
("RSI", "RSI"),
("VOL_MULT", "거래량배수"),
("VOL_WIN", "거래량윈도우"),
("VOL", "거래량"),
("ATR_SL", "ATR손절"),
("ATR_TP", "ATR익절"),
("ATR", "ATR"),
("EXIT_OB", "수익구간호가매도"),
("STOP_OB", "손절호가"),
("TICK_SELL", "틱매도"),
("TICK_FALLBACK_OHLC", "틱없을때OHLC폴백(위험)"),
("FALLBACK_OHLC", "OHLC폴백"),
("SKIP_HTS_SCAN_DUPES", "HTS스캔중복스킵"),
("SESSION_START", "세션시작"),
("SESSION_END", "세션종료"),
("SELL_TIME_END", "매도세션종료"),
("TIME_START", "매수시작"),
("TIME_END", "매수종료"),
("EOD_ENABLED", "장마감청산ON"),
("EOD_HM", "장마감시각HHMM"),
("EOD", "장마감"),
("DRY_RUN", "모의/페이퍼전용"),
("FEE_RATE", "수수료율"),
("SELL_TAX", "매도세금"),
("FX_FEE", "환전수수료"),
("MIN_PRICE", "최소가격"),
("MAX_PRICE", "최대가격"),
("COOLDOWN", "쿨다운"),
("RELOAD_INTERVAL", "설정리로드주기"),
("LOOP_SLEEP", "루프대기"),
("SCAN_REJECT", "스캔탈락대기"),
("PSAMOUNT", "매수가능수량"),
("BUY_WAIT_LOG", "매수대기로그"),
("APPROVAL", "WS_approval키"),
("REISSUE", "재발급"),
("HOLD_START", "hold시작"),
("HOLD_END", "hold종료"),
("WATCHDOG", "워치독"),
("SILENCE", "틱공백"),
("PING", "프로토콜ping"),
("ORDERBOOK", "호가"),
("ENABLED", "ON/OFF"),
("TARGET", "목표"),
("BUDGET", "한도"),
("HALT", "중단"),
("NOTIFY", "알림"),
("MODE", "모드"),
("TIERS", "다단"),
("DROP", "되돌림/하락"),
("ARM", "무장"),
("PCT", "%"),
("KRW", ""),
("USD", "USD"),
("SEC", ""),
("MS", "밀리초"),
("HM", "HHMM"),
("BARS", "봉수"),
("PERIOD", "기간"),
("WINDOW", "윈도우"),
("MULT", "배수"),
("MIN", "최소"),
("MAX", "최대"),
("AVG", "평균"),
("FILTER", "필터"),
("USE_", "사용_"),
]
_META = {
"id": "PK · AUTO_INCREMENT",
"created_at": "설정행 생성시각",
"updated_at": "마지막 갱신시각",
}
_TABLE_PREFIX = {
"config_us_momentum": "해외모멘텀",
"config_momentum": "모멘텀",
"config_short": "꼬리잡기",
"config_breakout": "돌파",
"config_updow": "UPDOWN",
"config_scalp": "스캘핑",
"config_range_break": "박스권돌파",
"config_dbband": "더블BB",
"config_dart": "DART",
"env_config": "공통env",
}
def _esc_comment(s: str) -> str:
"""MariaDB COMMENT 문자열 — 작은따옴표 이스케이프, 길이 제한.
TradeDB/pymysql 은 SQL 의 ``%`` 를 포맷으로 해석 → COMMENT 안 ``%`` 는 ``%%``.
"""
s = re.sub(r"\s+", " ", (s or "").strip())
s = s.replace("\\", "\\\\").replace("'", "''")
s = s.replace("%", "%%")
# MySQL comment 최대 1024; 안전하게 240자 (%% 두 배 전에 자름)
if len(s) > 240:
s = s[:237] + "..."
return s
def _heuristic(key: str, table: str) -> str:
if key in _META:
return _META[key]
k = key
# strip known strategy prefixes for readability
for pref in (
"US_MOMENTUM_", "MOMENTUM_", "BREAKOUT_", "SCALP_", "TAIL_", "SHORT_",
"UPDOW_", "UPDOWN_", "RANGE_BREAK_", "DBBAND_", "DART_",
"KIS_WS_", "KIWOOM_WS_", "LS_WS_", "LS_TOKEN_", "LS_AFR_",
):
if k.startswith(pref):
k = k[len(pref):]
break
if k.startswith("STRATEGY_") and k.endswith("_ENABLED"):
sid = k[len("STRATEGY_"):-len("_ENABLED")]
return f"{sid} 전략 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함"
parts: List[str] = []
rest = k
# greedy token replace
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:
# take next underscore chunk
if "_" in rest:
chunk, rest = rest.split("_", 1)
else:
chunk, rest = rest, ""
parts.append(chunk)
body = " ".join(parts) if parts else k
scope = _TABLE_PREFIX.get(table, table)
# avoid duplicate scope if already in body
if scope and scope not in body:
return f"{scope}: {body} ({key})"
return f"{body} ({key})"
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 as e:
print("live_config_schema import 실패:", e)
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(key: str, table: str, labels: Dict[str, str], existing: str) -> str:
if (existing or "").strip():
return existing.strip()
if key in labels:
return labels[key]
return _heuristic(key, table)
def _col_type_clause(row: dict) -> str:
"""SHOW FULL COLUMNS → MODIFY 타입 절 (Null/Default 유지)."""
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:
# numeric vs string
if re.match(r"^(tinyint|smallint|int|bigint|decimal|float|double|bit)", typ, re.I):
parts.append(f"DEFAULT {default}")
else:
d = str(default).replace("\\", "\\\\").replace("'", "''")
parts.append(f"DEFAULT '{d}'")
if "auto_increment" in extra.lower():
parts.append("AUTO_INCREMENT")
return " ".join(parts)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--sql-only", action="store_true")
ap.add_argument("--apply", action="store_true", help="DB에 COMMENT 적용")
ap.add_argument(
"--out",
default=str(ROOT / "scripts/sql/all_config_env_column_comments.generated.sql"),
)
args = ap.parse_args()
if not args.sql_only and not args.apply:
args.apply = True # 기본 적용
labels = _load_live_labels()
print(f"live_config labels: {len(labels)}")
from database import TradeDB
db = TradeDB()
sql_chunks: List[str] = [
"-- AUTO-GENERATED: config_*/env_config 전 컬럼 COMMENT (타입·값 불변)",
"-- 생성: scripts/sql/fill_all_config_env_column_comments.py",
"",
]
stats = {"tables": 0, "cols": 0, "kept": 0, "new": 0, "updated": 0}
for table in TABLES:
rows = db.conn.execute(f"SHOW FULL COLUMNS FROM `{table}`").fetchall()
stats["tables"] += 1
mods: List[str] = []
for r in rows:
field = r["Field"]
stats["cols"] += 1
existing = (r.get("Comment") or "").strip()
desc = _describe(field, table, labels, existing)
if existing:
stats["kept"] += 1
# 기존 유지 — 그래도 SQL에 명시해 문서화
else:
stats["new"] += 1
if existing == desc and existing:
# still emit for generated SQL completeness
pass
else:
if existing and existing != desc:
stats["updated"] += 1 # shouldn't happen (we keep existing)
type_clause = _col_type_clause(r)
cmt = _esc_comment(desc)
mods.append(
f" MODIFY COLUMN `{field}`\n"
f" {type_clause}\n"
f" COMMENT '{cmt}'"
)
sql_chunks.append(f"-- ── {table} ({len(rows)} cols) ──")
sql_chunks.append(f"ALTER TABLE `{table}`")
sql_chunks.append(",\n".join(mods))
sql_chunks.append(";\n")
if args.apply:
# apply one MODIFY at a time (safer than huge multi)
for r in rows:
field = r["Field"]
existing = (r.get("Comment") or "").strip()
desc = _describe(field, table, labels, existing)
if existing == desc:
continue
type_clause = _col_type_clause(r)
cmt = _esc_comment(desc)
stmt = (
f"ALTER TABLE `{table}` MODIFY COLUMN `{field}` "
f"{type_clause} COMMENT '{cmt}'"
)
try:
db.conn.execute(stmt)
except Exception as e:
print(f"FAIL {table}.{field}: {e}")
raise
print(f"applied {table}")
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"tables={stats['tables']} cols={stats['cols']} "
f"kept_existing={stats['kept']} filled_empty={stats['new']}"
)
# empty remaining?
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("still empty", table, r["Field"])
print("remaining empty comments:", empty)
return 0
if __name__ == "__main__":
raise SystemExit(main())