refactor: enhance Optuna backtesting framework, optimize orderbook filtering, and update database management utilities.

This commit is contained in:
Your Name
2026-08-12 10:19:19 +09:00
parent cb7e5037a0
commit c6bd62a25f
218 changed files with 31613 additions and 759 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
-- config_breakout — 수익구간 호가매도 / 손절호가 컬럼 설명 (MariaDB)
-- 실행: mysql ... < scripts/sql/config_breakout_ob_column_comments.sql
-- 타입은 기존과 동일 TEXT. COMMENT만 부여.
-- 주의: pymysql로 돌릴 때 % 는 %% 로 이스케이프 필요 → 본 파일은 %% 사용.
-- 호가 소스: 돌파=LS(ls_ws_orderbook) · 모멘텀=키움 — 교차 금지.
ALTER TABLE `config_breakout`
MODIFY COLUMN `BREAKOUT_EXIT_OB_ENABLED`
TEXT NULL
COMMENT '수익구간 호가매도 ON/OFF. true=이미 이익일 때 OR붕괴면 호가컷. 기본 false. LS호가. 진입필터와 무관'
, MODIFY COLUMN `BREAKOUT_EXIT_OB_RATIO_MIN`
TEXT NULL
COMMENT '수익구간: 총사겠음÷총팔겠음(OR) 이동평균이 이 값 미만이면 붕괴. 시드 0.4'
, MODIFY COLUMN `BREAKOUT_EXIT_OB_MA_WINDOW`
TEXT NULL
COMMENT '수익구간: OR 이동평균에 쓰는 최근 호가 스냅 개수. 시드 5'
, MODIFY COLUMN `BREAKOUT_EXIT_OB_MIN_PROFIT_PCT`
TEXT NULL
COMMENT '수익구간: 최소 이익(분율). 0.005=+0.5%%. 미달이면 호가컷 안 함'
, MODIFY COLUMN `BREAKOUT_EXIT_OB_MIN_HOLD_BARS`
TEXT NULL
COMMENT '수익구간: 산 뒤 최소 보유 분(1분봉≈1). 시드 3. 미달이면 안 함'
, MODIFY COLUMN `BREAKOUT_STOP_OB_ENABLED`
TEXT NULL
COMMENT '손절호가 ON/OFF. true=이미 손해+OR붕괴면 하드손절 전에 손절호가. 기본 false. LS호가'
, MODIFY COLUMN `BREAKOUT_STOP_OB_RATIO_MIN`
TEXT NULL
COMMENT '손절호가: OR 이동평균이 이 값 미만이면 붕괴. 시드 0.4'
, MODIFY COLUMN `BREAKOUT_STOP_OB_MA_WINDOW`
TEXT NULL
COMMENT '손절호가: OR 이동평균 스냅 개수. 시드 5'
, MODIFY COLUMN `BREAKOUT_STOP_OB_MIN_LOSS_PCT`
TEXT NULL
COMMENT '손절호가: 최소 손실 밴드(분율). 0.003=0.3%%. 이만큼 이상 손해일 때만'
, MODIFY COLUMN `BREAKOUT_STOP_OB_MIN_HOLD_BARS`
TEXT NULL
COMMENT '손절호가: 산 뒤 최소 보유 분. 시드 2. 미달이면 안 함'
;

View File

@@ -0,0 +1,46 @@
-- config_momentum — 수익구간 호가매도 / 손절호가 컬럼 설명 (MariaDB)
-- 실행: mysql ... < scripts/sql/config_momentum_ob_column_comments.sql
-- 타입은 기존과 동일 TEXT. COMMENT만 부여.
-- 주의: pymysql로 돌릴 때 % 는 %% 로 이스케이프 필요 → 본 파일은 %% 사용.
ALTER TABLE `config_momentum`
MODIFY COLUMN `MOMENTUM_EXIT_OB_ENABLED`
TEXT NULL
COMMENT '수익구간 호가매도 ON/OFF. true=이미 이익일 때 OR붕괴면 호가컷. 기본 false. 진입필터와 무관'
, MODIFY COLUMN `MOMENTUM_EXIT_OB_RATIO_MIN`
TEXT NULL
COMMENT '수익구간: 총사겠음÷총팔겠음(OR) 이동평균이 이 값 미만이면 붕괴. 시드 0.4'
, MODIFY COLUMN `MOMENTUM_EXIT_OB_MA_WINDOW`
TEXT NULL
COMMENT '수익구간: OR 이동평균에 쓰는 최근 호가 스냅 개수. 시드 5'
, MODIFY COLUMN `MOMENTUM_EXIT_OB_MIN_PROFIT_PCT`
TEXT NULL
COMMENT '수익구간: 최소 이익(분율). 0.005=+0.5%%. 미달이면 호가컷 안 함'
, MODIFY COLUMN `MOMENTUM_EXIT_OB_MIN_HOLD_BARS`
TEXT NULL
COMMENT '수익구간: 산 뒤 최소 보유 분(1분봉≈1). 시드 3. 미달이면 안 함'
, MODIFY COLUMN `MOMENTUM_STOP_OB_ENABLED`
TEXT NULL
COMMENT '손절호가 ON/OFF. true=이미 손해+OR붕괴면 하드손절 전에 손절호가. 기본 false'
, MODIFY COLUMN `MOMENTUM_STOP_OB_RATIO_MIN`
TEXT NULL
COMMENT '손절호가: OR 이동평균이 이 값 미만이면 붕괴. 시드 0.4'
, MODIFY COLUMN `MOMENTUM_STOP_OB_MA_WINDOW`
TEXT NULL
COMMENT '손절호가: OR 이동평균 스냅 개수. 시드 5. 수익구간과 히스토리 공유 가능'
, MODIFY COLUMN `MOMENTUM_STOP_OB_MIN_LOSS_PCT`
TEXT NULL
COMMENT '손절호가: 최소 손실 밴드(분율). 0.003=0.3%%. 이만큼 이상 손해일 때만'
, MODIFY COLUMN `MOMENTUM_STOP_OB_MIN_HOLD_BARS`
TEXT NULL
COMMENT '손절호가: 산 뒤 최소 보유 분. 시드 2. 미달이면 안 함'
;

View File

@@ -0,0 +1,378 @@
#!/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())

View File

@@ -0,0 +1,434 @@
#!/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())

View File

@@ -0,0 +1,116 @@
-- 토큰·세션·전략 ON/OFF 관련 컬럼 COMMENT (선별·정밀 문구)
-- 전 컬럼(config_*/env_config ~1200+)은 아래로 일괄:
-- python3 scripts/sql/fill_all_config_env_column_comments.py --apply
-- → scripts/sql/all_config_env_column_comments.generated.sql
-- 참고: docs/증권사_토큰_시세.md · kis_ws_session_windows · ls_ws_session_windows
-- 실행: python 또는 mysql 클라이언트. pymysql 사용 시 COMMENT 안 % → %% .
-- ── config_* : STRATEGY_*_ENABLED ─────────────────────────────────────
ALTER TABLE `config_us_momentum`
MODIFY COLUMN `STRATEGY_US_MOMENTUM_ENABLED`
TEXT NULL
COMMENT '해외모멘텀 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함. 기동시OFF→켜려면재시작';
ALTER TABLE `config_momentum`
MODIFY COLUMN `STRATEGY_MOMENTUM_ENABLED`
TEXT NULL
COMMENT '국내모멘텀 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함. 기동시OFF→켜려면재시작';
ALTER TABLE `config_short`
MODIFY COLUMN `STRATEGY_SHORT_ENABLED`
TEXT NULL
COMMENT '꼬리잡기 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함. 기동시OFF→켜려면재시작';
ALTER TABLE `config_breakout`
MODIFY COLUMN `STRATEGY_BREAKOUT_ENABLED`
TEXT NULL
COMMENT '돌파 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함. 기동시OFF→켜려면재시작';
ALTER TABLE `config_updow`
MODIFY COLUMN `STRATEGY_UPDOW_ENABLED`
TEXT NULL
COMMENT 'UPDOWN박스 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함. 기동시OFF→켜려면재시작';
ALTER TABLE `config_scalp`
MODIFY COLUMN `STRATEGY_SCALP_ENABLED`
TEXT NULL
COMMENT '스캘핑 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함. 기동시OFF→켜려면재시작';
ALTER TABLE `config_range_break`
MODIFY COLUMN `STRATEGY_RANGE_BREAK_ENABLED`
TEXT NULL
COMMENT '박스권돌파 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함';
ALTER TABLE `config_dbband`
MODIFY COLUMN `STRATEGY_DBBAND_ENABLED`
TEXT NULL
COMMENT '더블BB ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함';
ALTER TABLE `config_dart`
MODIFY COLUMN `STRATEGY_DART_ENABLED`
TEXT NULL
COMMENT 'DART수주 ON/OFF. 다음루프 신규매수중단(보유청산유지). WS구독해제 안함';
-- ── env_config : KIS approval·hold · 키움 구독한도 ─────────────────────
ALTER TABLE `env_config`
MODIFY COLUMN `KIS_WS_KR_HOLD_START_HM`
TEXT NULL
COMMENT '국내WS approval점유 시작 HHMM. 기본700. 갭·해외창에선 소켓양보(재발급없음)'
, MODIFY COLUMN `KIS_WS_KR_HOLD_END_HM`
TEXT NULL
COMMENT '국내WS approval점유 종료 HHMM. 기본2000. kis_ws_session_windows'
, MODIFY COLUMN `KIS_WS_US_HOLD_START_HM`
TEXT NULL
COMMENT '해외WS(HDFSCNT0) 점유 시작 HHMM. 기본2100. 국내와 동시점유금지(1키=1세션)'
, MODIFY COLUMN `KIS_WS_US_HOLD_END_HM`
TEXT NULL
COMMENT '해외WS 점유 종료 HHMM. 기본600(자정넘김). 세션전환시 approval재발급금지'
, MODIFY COLUMN `KIS_WS_APPROVAL_MIN_REISSUE_SEC`
TEXT NULL
COMMENT 'approval_key 최소재발급간격(초). 기본21600=6h. REST재발급은기존키무효화'
, MODIFY COLUMN `KIS_WS_APPROVAL_MAX_VALID_SEC`
TEXT NULL
COMMENT 'approval_key 최대유효(초). 기본82800≈23h. 파일캐시 kis_approval_manager'
, MODIFY COLUMN `KIS_WS_INVALID_APPROVAL_REISSUE_AFTER`
TEXT NULL
COMMENT 'invalid approval 연속N회 후 응급재발급 허용. 6h가드우회조건'
, MODIFY COLUMN `KIS_WS_INVALID_APPROVAL_BYPASS_6H`
TEXT NULL
COMMENT 'true면 invalid루프시 6h가드우회응급재발급. 남용금지'
, MODIFY COLUMN `KIS_WS_EMERGENCY_REISSUE_MIN_SEC`
TEXT NULL
COMMENT '응급approval재발급 최소간격(초). 기본=정상6h와동일. 하드캡'
, MODIFY COLUMN `KIWOOM_WS_MAX_SUBSCRIPTIONS`
TEXT NULL
COMMENT '키움WS 그룹당최대구독. 기본100. 0B체결·0D호가·0w프로그램. 한투41과별개'
;
-- ── env_config_ext : KV 테이블 자체 설명 (키별 행은 컬럼 아님) ───────────
ALTER TABLE `env_config_ext`
COMMENT='env_config 확장 KV. LS_WS_*·LS_TOKEN_*·OVERSEAS_WS_ENABLED·워치독/hold 등 다수키. 컬럼추가없이 env_key행'
, MODIFY COLUMN `env_key`
VARCHAR(80) NOT NULL
COMMENT '설정키. 예: LS_WS_WATCHDOG_SESSION_GATE, LS_WS_KR_HOLD_*, OVERSEAS_WS_ENABLED, LS_TOKEN_*'
, MODIFY COLUMN `env_value`
TEXT NOT NULL
COMMENT '설정값 문자열. bool/int도 텍스트저장'
, MODIFY COLUMN `updated_at`
VARCHAR(30) NOT NULL
COMMENT '마지막갱신시각'
;
ALTER TABLE `config_us_momentum`
COMMENT='해외모멘텀 US_MOMENTUM_* · STRATEGY_US_MOMENTUM_ENABLED. 시세=한투HDFSCNT0 · 유니버스=permanent US'
;